zusammenfügen 02.2.

This commit is contained in:
2026-02-02 20:01:37 +01:00
parent 774acc3854
commit 4e7c55500b
11 changed files with 62593 additions and 2121 deletions

View File

@@ -1,67 +1,243 @@
from typing import Any
import sympy as sp
from decimal import Decimal
import sympy as sp
from typing import Any
import math
import numpy as np
from numpy import ndarray, dtype
import Datenbank
class Berechnungen:
"""Geodätische Hilfsberechnungen auf einem Rotationsellipsoid.
Die Klasse stellt Methoden zur Verfügung für:
- erste numerische Exzentrizität e² und zweite numerische Exzentrizität e'²,
- Umrechnung ECEF (X, Y, Z) → geodätische Breite B, geodätische Länge L und ellipsoidische Höhe H,
- lokale ENU-Komponenten (E, N, U),
- Horizontalstrecke, Zenitwinkel, Azimut und Richtung,
- Berechnung von Azimut, Richtung und Zenitwinkel aus Tachymeterbeobachtungen.
:ivar a_wert: Große Halbachse a in Meter.
:vartype a_wert: float
:ivar b_wert: Kleine Halbachse b in Meter.
:vartype b_wert: float
:ivar e_quadrat_wert: Quadrat der ersten numerischen Exzentrizität e² (einheitenlos).
:vartype e_quadrat_wert: float
:ivar e_strich_quadrat_wert: Quadrat der zweiten numerischen Exzentrizität e'² (einheitenlos).
:vartype e_strich_quadrat_wert: float
"""
def __init__(self, a: float, b: float) -> None:
"""Initialisiert die Ellipsoidparameter.
Berechnet die erste und zweite numerische Exzentrizität.
:param a: Große Halbachse a des Rotationsellipsoids in Meter.
:type a: float
:param b: Kleine Halbachse b des Rotationsellipsoids in Meter.
:type b: float
"""
self.a_wert = a
self.b_wert = b
self.e_quadrat_wert = self.e_quadrat()
self.e_strich_quadrat_wert = self.e_strich_quadrat()
def e_quadrat(self) -> float:
"""Berechnet das Quadrat der ersten numerischen Exzentrizität e².
Es gilt: e² = (a² b²) / a², wobei a die große und b die kleine Halbachse ist.
:return: Quadrat der ersten numerischen Exzentrizität e² (einheitenlos).
:rtype: float
"""
return (self.a_wert**2 - self.b_wert**2) / self.a_wert **2
def e_strich_quadrat(self) -> float:
"""Berechnet das Quadrat der zweiten numerischen Exzentrizität e'².
Es gilt: e'² = (a² b²) / b², wobei a die große und b die kleine Halbachse ist
:return: Quadrat der zweiten numerischen Exzentrizität e'² (einheitenlos).
:rtype: float
"""
return (self.a_wert**2 - self.b_wert**2) / self.b_wert **2
def P(self, x: float, y: float) -> float:
"""Berechnet den Hilfswert P aus geozentrischen Koordinaten.
Es gilt: P = sqrt(x² + y²).
:param x: Geozentrische kartesische X-Koordinate (ECEF) in Meter.
:type x: float
:param y: Geozentrische kartesische Y-Koordinate (ECEF) in Meter.
:type y: float
:return: Hilfswert P in Meter.
:rtype: float
"""
return np.sqrt(x**2 + y**2)
def hilfswinkel(self, x: float, y: float, z: float) -> float:
"""Berechnet den Hilfswinkel für die Bestimmung der geodätischen Breite.
Es gilt: hw = atan2(z * a, P(x, y) * b).
:param x: Geozentrische kartesische X-Koordinate (ECEF) in Meter.
:type x: float
:param y: Geozentrische kartesische Y-Koordinate (ECEF) in Meter.
:type y: float
:param z: Geozentrische kartesische Z-Koordinate (ECEF) in Meter.
:type z: float
:return: Hilfswinkel in Radiant.
:rtype: float
"""
hw = np.atan2(z * self.a_wert, self.P(x, y) * self.b_wert)
return hw
def B(self, x: float, y: float, z: float) -> float:
"""Berechnet die geodätische Breite B aus geozentrischen Koordinaten.
Verwendet den Hilfswinkel, e² und e'² zur Berechnung.
:param x: Geozentrische kartesische X-Koordinate (ECEF) in Meter.
:type x: float
:param y: Geozentrische kartesische Y-Koordinate (ECEF) in Meter.
:type y: float
:param z: Geozentrische kartesische Z-Koordinate (ECEF) in Meter.
:type z: float
:return: Geodätische Breite B in Radiant.
:rtype: float
"""
hilfswinkel = self.hilfswinkel(x, y, z)
B = np.atan2((z + self.e_strich_quadrat_wert * self.b_wert * np.sin(hilfswinkel) ** 3), (self.P(x, y) - self.e_quadrat_wert * self.a_wert * np.cos(hilfswinkel) ** 3))
return B
def L(self, x: float, y: float) -> float:
"""Berechnet die geodätische Länge L aus geozentrischen Koordinaten.
Es gilt: L = atan2(y, x).
:param x: Geozentrische kartesische X-Koordinate (ECEF) in Meter.
:type x: float
:param y: Geozentrische kartesische Y-Koordinate (ECEF) in Meter.
:type y: float
:return: Geodätische Länge L in Radiant.
:rtype: float
"""
return np.atan2(y, x)
def H(self, x: float, y: float, z: float) -> float:
"""Berechnet die ellipsoidische Höhe H aus geozentrisch kartesischen Koordinaten.
Die ellipsoidische Höhe wird mithilfe der geodätischen Breite B und der Ellipsoidparameter berechnet.
:param x: Geozentrische kartesische X-Koordinate (ECEF) in Meter.
:type x: float
:param y: Geozentrische kartesische Y-Koordinate (ECEF) in Meter.
:type y: float
:param z: Geozentrische kartesische Z-Koordinate (ECEF) in Meter.
:type z: float
:return: Ellipsoidische Höhe H in Meter.
:rtype: float
"""
B = self.B(x, y, z)
H = (self.P(x, y) / np.cos(B)) - self.a_wert / (np.sqrt(1 - self.e_quadrat_wert * np.sin(B) ** 2))
return H
def E(self, L: float, dX: float, dY: float) -> float:
"""Berechnet die Ostkomponente E im lokalen ENU-System.
:param L: Geodätische Länge in Radiant.
:type L: float
:param dX: Differenz dX = X2 X1 in Meter.
:type dX: float
:param dY: Differenz dY = Y2 Y1 in Meter.
:type dY: float
:return: Ostkomponente E in Meter.
:rtype: float
"""
E = -np.sin(L) * dX + np.cos(L) * dY
return E
def N(self, B: float, L: float, dX: float, dY: float, dZ: float) -> float:
"""Berechnet die Nordkomponente N im lokalen ENU-System.
:param B: Geodätische Breite in Radiant.
:type B: float
:param L: Geodätische Länge in Radiant.
:type L: float
:param dX: Differenz dX = X2 X1 in Meter.
:type dX: float
:param dY: Differenz dY = Y2 Y1 in Meter.
:type dY: float
:param dZ: Differenz dZ = Z2 Z1 in Meter.
:type dZ: float
:return: Nordkomponente N in Meter.
:rtype: float
"""
N = -np.sin(B) * np.cos(L) * dX - np.sin(B) * np.sin(L) * dY + np.cos(B) * dZ
return N
def U(self, B: float, L: float, dX: float, dY: float, dZ: float) -> float:
"""Berechnet die Up-Komponente U im lokalen ENU-System.
:param B: Geodätischee Breite in Radiant.
:type B: float
:param L: Geodätische Länge in Radiant.
:type L: float
:param dX: Differenz dX = X2 X1 in Meter.
:type dX: float
:param dY: Differenz dY = Y2 Y1 in Meter.
:type dY: float
:param dZ: Differenz dZ = Z2 Z1 in Meter.
:type dZ: float
:return: Up-Komponente U in Meter.
:rtype: float
"""
U = np.cos(B) * np.cos(L) * dX + np.cos(B) * np.sin(L) *dY + np.sin(B) * dZ
return U
def horizontalstrecke_ENU(self, E: float, N: float) -> float:
"""Berechnet die Horizontalstrecke aus den ENU-Komponenten E und N.
Es gilt: s = sqrt(E² + N²).
:param E: Ostkomponente E in Meter.
:type E: float
:param N: Nordkomponente N in Meter.
:type N: float
:return: Horizontalstrecke in Meter.
:rtype: float
"""
s = np.sqrt(E**2 + N**2)
return s
def Zenitwinkel(self, horizontalstrecke_ENU: float, U: float) -> float:
"""Berechnet den Zenitwinkel aus Horizontalstrecke und Up-Komponente.
Es gilt: zw = atan2(horizontalstrecke_ENU, U).
:param horizontalstrecke_ENU: Horizontalstrecke in Meter.
:type horizontalstrecke_ENU: float
:param U: Up-Komponente U in Meter.
:type U: float
:return: Zenitwinkel in Radiant.
:rtype: float
"""
zw = np.atan2(horizontalstrecke_ENU, U)
return zw
def Azimut(self, E: float, N: float) -> float:
"""Berechnet den Azimut aus den ENU-Komponenten E und N.
Der Azimut wird auf [0, 2π) normiert.
:param E: Ostkomponente E in Meter.
:type E: float
:param N: Nordkomponente N in Meter.
:type N: float
:return: Azimut in Radiant im Bereich [0, 2π).
:rtype: float
"""
Azimut = np.atan2(E, N)
if Azimut < 0:
Azimut += 2 * np.pi
@@ -70,6 +246,17 @@ class Berechnungen:
return Azimut
def Richtung(self, Azimut: float, Orientierung: float) -> float:
"""Berechnet die Richtung aus Azimut und Orientierung.
Die Richtung wird auf [0, 2π) normiert.
:param Azimut: Azimut in Radiant.
:type Azimut: float
:param Orientierung: Orientierung in Radiant.
:type Orientierung: float
:return: Richtung in Radiant im Bereich [0, 2π).
:rtype: float
"""
Richtung = Azimut - Orientierung
if Richtung < 0:
Richtung += 2 * np.pi
@@ -77,13 +264,37 @@ class Berechnungen:
Richtung -= 2 * np.pi
return Richtung
def geometrische_breite_laenge(self, dict_koordinaten: dict) -> dict:
def geodätische_breite_laenge(self, dict_koordinaten: dict) -> dict:
"""Berechnet geodätische Breite und Länge für alle Punkte eines Koordinatendictionaries.
Die Einträge des Dictionaries werden in einer Schleife erweitert zu [Koordinatenmatrix, B, L], wobei Koordinatenmatrix = (X, Y, Z) ist.
:param dict_koordinaten: Dictionary mit geozentrischen Koordinaten je Punkt.
:type dict_koordinaten: dict
:return: Das übergebene Dictionary mit erweiterten Einträgen.
:rtype: dict
"""
for punktnummer, matrix in dict_koordinaten.items():
dict_koordinaten[punktnummer] = [matrix, self.B(matrix[0], matrix[1], matrix[2]), self.L(matrix[0], matrix[1])]
return dict_koordinaten
def berechnung_richtung_azimut_zenitwinkel(self, pfad_datenbank: str, dict_koordinaten: dict) -> tuple[list[Any], dict[Any, Any]]:
"""Berechnet Azimut, Richtung und Zenitwinkel aus Tachymeterbeobachtungen.
Die Tachymeterbeobachtungen werden aus der Datenbank gelesen. Für jede Beobachtung
(Standpunkt → Zielpunkt) werden aus den Koordinatendifferenzen die lokalen ENU-Komponenten
am Standpunkt berechnet und daraus Azimut, Richtung und Zenitwinkel berechnet.
Die Orientierung wird pro Beobachtungsgruppe durch den ersten Azimut der Gruppe gesetzt.
:param pfad_datenbank: Pfad zur Datenbank.
:type pfad_datenbank: str
:param dict_koordinaten: Dictionary mit geozentrisch kartesischen Koordinaten je Punkt (ECEF).
:type dict_koordinaten: dict
:return: Tupel aus Ergebnisliste mit (beobachtungsgruppeID, standpunkt, zielpunkt, Azimut, richtung, Zenitwinkel, schraegstrecke, orientierung) und Dictionary der Orientierungen je Beobachtungsgruppe. Winkel in Radiant, Strecken in Meter.
:rtype: tuple[list[Any], dict[Any, Any]]
"""
dict_koordinaten_erweitert = {}
dict_orientierungen = {}
liste_azimut_richtungen = []
@@ -149,6 +360,21 @@ class Berechnungen:
def berechne_zenitwinkel_distanz_bodenbezogen(self, zenitwinkel_messung: float, schraegdistanz_messung: float,
instrumentenhoehe: float, prismenhoehe: float):
"""Berechnet bodenbezogene Schrägdistanz und bodenbezogenen Zenitwinkel.
Aus gemessener Schrägdistanz und gemessenem Zenitwinkel werden die Horizontalstrecke, der bodenbezogene Höhenunterschied sowie die bodenbezogenen Größen abgeleitet.
:param zenitwinkel_messung: Gemessener Zenitwinkel in Radiant.
:type zenitwinkel_messung: float
:param schraegdistanz_messung: Gemessene Schrägdistanz in Meter.
:type schraegdistanz_messung: float
:param instrumentenhoehe: Instrumentenhöhe in Meter.
:type instrumentenhoehe: float
:param prismenhoehe: Prismen-/Zielhöhe in Meter.
:type prismenhoehe: float
:return: Bodenbezogene Schrägdistanz in Meter und bodenbezogener Zenitwinkel in Radiant.
:rtype: tuple[float, float]
"""
HD = np.sin(np.pi - zenitwinkel_messung) * schraegdistanz_messung
delta_h_ihzh = schraegdistanz_messung * np.cos(zenitwinkel_messung)
delta_h_boden = delta_h_ihzh + instrumentenhoehe - prismenhoehe
@@ -158,30 +384,166 @@ class Berechnungen:
class Einheitenumrechnung:
def __init__(self) -> None:
pass
"""Einheitenumrechnungen für Winkel- und Längeneinheiten.
Die Klasse stellt Methoden zur Verfügung für:
- Umrechnung von Millibogensekunden (mas) in Radiant,
- Umrechnung von Millimetern (mm) in Meter,
- Umrechnung von Gon und Milligon (mgon) in Radiant (Decimal-basiert).
"""
def mas_to_rad(mas: float) -> float:
"""Rechnet Millibogensekunden (mas) in Radiant um.
Es gilt: rad = mas * (pi / (180 * 3600 * 1000)).
:param mas: Winkel in Millibogensekunden (mas).
:type mas: float
:return: Winkel in Radiant.
:rtype: float
"""
umrechnungsfaktor = 1 / 1000 * 1 / 3600 * sp.pi / 180
grad = mas * umrechnungsfaktor
return grad
def mm_to_m(mm: float) -> float:
"""Rechnet Millimeter in Meter um.
Es gilt: m = mm / 1000.
:param mm: Länge in Millimeter.
:type mm: float
:return: Länge in Meter.
:rtype: float
"""
m = mm / 1000
return m
def ppb(ppb: float) -> float:
ppb *= 10 ** (-9)
return ppb
def gon_to_rad_Decimal(gon: float) -> Decimal:
"""Rechnet Gon in Radiant um (Decimal-basiert).
def gon_to_rad_Decimal(gon: float) -> float:
Es gilt: 400 gon = 2*pi und damit rad = (gon / 200) * pi.
:param gon: Winkel in Gon.
:type gon: float
:return: Winkel in Radiant als Decimal.
:rtype: Decimal
"""
gon = Decimal(gon)
pi = Decimal(str(math.pi))
rad = (gon / Decimal(200)) * pi
return rad
def mgon_to_rad_Decimal(gon: float) -> float:
def mgon_to_rad_Decimal(gon: float) -> Decimal:
"""Rechnet Milligon (mgon) in Radiant um (Decimal-basiert).
Es gilt: 1 mgon = 0.001 gon und damit rad = (mgon / 200000) * pi.
:param gon: Winkel in Milligon (mgon).
:type gon: float
:return: Winkel in Radiant als Decimal.
:rtype: Decimal
"""
gon = Decimal(gon)
pi = Decimal(str(math.pi))
rad = (gon / Decimal(200000)) * pi
return rad
def rad_to_gon_Decimal(rad: float) -> Decimal:
"""Rechnet Radiant in Gon um (Decimal-basiert).
Es gilt: 400 gon = 2*pi und damit rad = (gon / 200) * pi.
:param rad: Winkel in Rad.
:type rad: float
:return: Winkel in Gon als Decimal.
:rtype: Decimal
"""
rad = Decimal(rad)
pi = Decimal(str(math.pi))
gon = (rad / pi) * Decimal(200)
return gon
class ENU:
@staticmethod
def berechne_schwerpunkt_fuer_enu(berechnungen, dict_xyz):
XYZ = np.array(list(dict_xyz.values()), dtype=float)
X0, Y0, Z0 = XYZ.mean(axis=0)
B0 = float(berechnungen.B(X0, Y0, Z0))
L0 = float(berechnungen.L(X0, Y0))
return B0, L0
@staticmethod
def berechne_R0_ENU(berechnungen, B, L):
# East
r11 = berechnungen.E(L, 1, 0)
r12 = berechnungen.E(L, 0, 1)
r13 = berechnungen.E(L, 0, 0)
# North
r21 = berechnungen.N(B, L, 1, 0, 0)
r22 = berechnungen.N(B, L, 0, 1, 0)
r23 = berechnungen.N(B, L, 0, 0, 1)
# Up
r31 = berechnungen.U(B, L, 1, 0, 0)
r32 = berechnungen.U(B, L, 0, 1, 0)
r33 = berechnungen.U(B, L, 0, 0, 1)
R0 = np.array([
[r11, r12, r13],
[r21, r22, r23],
[r31, r32, r33]
], dtype=float)
return R0
@staticmethod
def berechne_R_ENU(unbekannten_liste, R0):
names = [str(s) for s in unbekannten_liste]
n = len(names)
R = np.eye(n, dtype=float)
punkt_ids = [nm[1:] for nm in names if nm and nm[0].upper() == "X"]
for pid in punkt_ids:
try:
ix = next(i for i, nm in enumerate(names) if nm.upper() == f"X{pid}".upper())
iy = next(i for i, nm in enumerate(names) if nm.upper() == f"Y{pid}".upper())
iz = next(i for i, nm in enumerate(names) if nm.upper() == f"Z{pid}".upper())
except StopIteration:
continue
I = [ix, iy, iz]
R[np.ix_(I, I)] = R0
return R
@staticmethod
def transform_Qxx_zu_QxxENU(Qxx, unbekannten_liste, berechnungen, dict_xyz):
B0, L0 = ENU.berechne_schwerpunkt_fuer_enu(berechnungen, dict_xyz)
R0 = ENU.berechne_R0_ENU(berechnungen, B0, L0)
R_ENU = ENU.berechne_R_ENU(unbekannten_liste, R0)
Qenu = R_ENU @ Qxx @ R_ENU.T
Qenu = 0.5 * (Qenu + Qenu.T)
return Qenu, (B0, L0), R0
@staticmethod
def transform_Koord_zu_KoordENU(dict_xyz, R0):
XYZ = np.asarray(list(dict_xyz.values()), dtype=float).reshape(-1, 3)
XYZ0 = XYZ.mean(axis=0).reshape(3, )
Koord_ENU = {}
for pid, xyz in dict_xyz.items():
xyz = np.asarray(xyz, dtype=float).reshape(3, )
enu = (R0 @ (xyz - XYZ0)).reshape(3, )
Koord_ENU[str(pid)] = (float(enu[0]), float(enu[1]), float(enu[2]))
return Koord_ENU

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,53 @@
from decimal import Decimal
import os
import sqlite3
from typing import Any
import sympy as sp
from sympy import MutableDenseMatrix
from typing import Any
from Berechnungen import Einheitenumrechnung
from decimal import Decimal
class Datenbank_anlegen:
"""Legt die SQLite-Datenbank für die Ausgleichungsrechnung an.
Die Klasse erstellt die Datenbankdatei nur dann, wenn am angegebenen Pfad noch
keine Datenbank existiert. In diesem Fall werden die Tabellen und Constraints
per SQL-Skript angelegt.
Angelegte Tabellen:
- Netzpunkte,
- Beobachtungen,
- Instrumente,
- Genauigkeiten,
- Varianzkomponentenschaetzung.
:ivar pfad_datenbank: Pfad zur SQLite-Datenbankdatei.
:vartype pfad_datenbank: str
"""
def __init__(self, pfad_datenbank: str) -> None:
"""Initialisiert den Datenbankpfad und legt die Datenbank bei Bedarf an.
Beim Erstellen der Instanz wird db_anlegen() aufgerufen. Existiert die
Datenbankdatei unter dem übergebenen Pfad bereits, erfolgt keine Änderung.
:param pfad_datenbank: Pfad zur SQLite-Datenbankdatei.
:type pfad_datenbank: str
:return: None
:rtype: None
"""
self.pfad_datenbank = pfad_datenbank
self.db_anlegen()
def db_anlegen(self) -> None:
# pfad = r"C:\Users\fabia\OneDrive\Jade HS\Master\MGW2\Masterprojekt_allgemein\Masterprojekt\Programmierung\Campusnetz\Campusnetz.db"
"""Legt die SQLite-Datenbank an, sofern die Datei nicht existiert.
Prüft, ob die Datenbankdatei am Pfad vorhanden ist. Falls nicht, wird eine neue
SQLite-Datenbank erstellt.
Anschließend werden die Änderungen committet und die Verbindung geschlossen.
:return: None
:rtype: None
"""
if not os.path.exists(self.pfad_datenbank):
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -105,16 +136,43 @@ class Datenbank_anlegen:
CONSTRAINT fk_Varianzkomponentenschaetzung_Instrumente FOREIGN KEY (instrumenteID) REFERENCES Instrumente(instrumenteID)
);
""")
con.commit()
cursor.close()
con.close()
class Datenbankzugriff:
"""Zugriff auf die SQLite-Datenbank des Ausgleichungsprogramms.
Die Klasse stellt Methoden zur Verfügung für:
- Schreiben in die Datenbank durch alle set_* Methoden
- Lesen aus der Datenbank durch alle get_* Methoden
:ivar pfad_datenbank: Pfad zur SQLite-Datenbankdatei.
:vartype pfad_datenbank: str
"""
def __init__(self, pfad_datenbank: str) -> None:
"""Initialisiert den Datenbankzugriff.
:param pfad_datenbank: Pfad zur SQLite-Datenbankdatei.
:type pfad_datenbank: str
"""
self.pfad_datenbank = pfad_datenbank
def set_koordinaten(self, dict_koordinaten: dict, koordinatenart: str) -> None:
"""Schreibt Koordinaten in die Tabelle Netzpunkte.
Die Koordinaten werden aus `dict_koordinaten` gelesen und abhängig von der koordinatenart
in die entsprechenden Felder geschrieben. Es werden nur Einträge aktualisiert, deren
Zielspalten aktuell ``NULL`` sind.
:param dict_koordinaten: Dictionary mit Punktnummer → (X, Y, Z).
:type dict_koordinaten: dict
:param koordinatenart: Art der Koordinaten ("naeherung_lh" für die Koordinaten im lokalen Horizontsystem aus der Tachymetermessung oder "naeherung_us" für die Näherungskoordinaten der Netzpunkte im geozentrisch-kartesischen-system).
:type koordinatenart: str
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -127,18 +185,33 @@ class Datenbankzugriff:
float(wert[2]),
str(punktnummer)
))
# Der Import für die Koordinatenart "naeherung_lh" ist im Zuge möglicher Erweiterungen zu implementieren.
if koordinatenart == "naeherung_lh":
pass
elif koordinatenart == "naeherung_us":
cursor.executemany(f"""UPDATE Netzpunkte SET naeherungx_us = ?, naeherungy_us = ?, naeherungz_us = ? WHERE punktnummer = ? AND naeherungx_us IS NULL AND naeherungy_us IS NULL AND naeherungz_us IS NULL""", daten)
con.commit()
cursor.close()
con.close()
def set_instrument(self, typ: str, name: str, liste_beobachtungsarten: list) -> None:
"""Fügt ein Instrument hinzu und initialisiert Varianzkomponenten.
Ist das Instrument (typ, name) noch nicht in der Datenbank vorhanden, wird es in die Tabelle Instrumente eingefügt.
Zusätzlich werden Einträge in Varianzkomponentenschaetzung für die angegebenen
Beobachtungsarten erzeugt. Falls die Varianzkomponentenschätzung noch leer ist, wird
ein zusätzlicher Eintrag für die Beobachtungsgruppe "Anschlusspunkte" angelegt.
:param typ: Instrumenttyp.
:type typ: str
:param name: Instrumentname.
:type name: str
:param liste_beobachtungsarten: Liste der Beobachtungsgruppen zur Initialisierung.
:type liste_beobachtungsarten: list
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_instrumente = cursor.execute("SELECT * FROM Instrumente WHERE typ = ? AND name =?", (typ, name)).fetchall()
@@ -175,9 +248,27 @@ class Datenbankzugriff:
def set_genauigkeiten(self, instrumenteID: int, beobachtungsart: str, stabw_apriori_konstant: float = None,
stabw_apriori_streckenprop: float = None) -> None:
"""Speichert a-priori Genauigkeiten für die Beobachtungsgruppen pro Instrument.
Prüft, ob instrumenteID existiert und ob mindestens eine Genauigkeitsangabe übergeben wurde.
Je nach Beobachtungsart werden Einheitenumrechnungen durchgeführt (z. B. mgon → rad bzw. mm → m).
Der Eintrag wird nur ergänzt, wenn in Genauigkeiten kein identischer Datensatz vorhanden ist.
:param instrumenteID: ID des Instruments in der Tabelle Instrumente.
:type instrumenteID: int
:param beobachtungsart: Bezeichnung der Beobachtungsart.
:type beobachtungsart: str
:param stabw_apriori_konstant: Konstanter Anteil der Standardabweichung (optional).
:type stabw_apriori_konstant: float | None
:param stabw_apriori_streckenprop: Streckenproportionaler Anteil der Standardabweichung (optional).
:type stabw_apriori_streckenprop: float | None
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
# Überprüfung auf vorhandensein der Instrumentes in der Tabelle Instrumente
instrumentenname = cursor.execute("SELECT name FROM Instrumente WHERE instrumenteID = ?",
(instrumenteID, )).fetchone()
if instrumentenname is None:
@@ -188,6 +279,7 @@ class Datenbankzugriff:
return
instrumentenname = instrumentenname[0]
# Überprüfung, ob Genauigkeitsinformationen vom Benutzer übergeben wurden.
if stabw_apriori_konstant is None and stabw_apriori_streckenprop is None:
print(
"Es wurden keine Genauigkeiten importiert. Bitte stabw_apriori_konstant und / oder stabw_apriori_streckenprop angeben.")
@@ -195,6 +287,7 @@ class Datenbankzugriff:
con.close()
return
# Umrechnen der Einheiten in Radiant und Meter
if beobachtungsart == "Tachymeter_Richtung" or beobachtungsart == "Tachymeter_Zenitwinkel" :
stabw_apriori_konstant = Einheitenumrechnung.mgon_to_rad_Decimal(stabw_apriori_konstant)
@@ -207,6 +300,7 @@ class Datenbankzugriff:
if isinstance(stabw_apriori_streckenprop, Decimal):
stabw_apriori_streckenprop = float(stabw_apriori_streckenprop)
# Überprüfen, ob die Genauigkeitsinformation für die jeweilige Beobachtungsart des Instruments bereits vorhanden ist. Wenn nein, wird die Benutzereingabe in die Datenbank gespeichert.
sql = "SELECT 1 FROM Genauigkeiten WHERE instrumenteID = ? AND beobachtungsart = ?"
params = [instrumenteID, beobachtungsart]
@@ -248,6 +342,7 @@ class Datenbankzugriff:
)
print(
f"Die Genauigkeitsangabe für die Beobachtungsart {beobachtungsart} des Instrumentes {instrumentenname} wurde erfolgreich hinzugefügt.")
else:
print("Die Genauigkeitsangabe ist bereits in der Datenbank vorhanden.")
@@ -258,9 +353,28 @@ class Datenbankzugriff:
def set_datumskoordinaten(self, liste_datumskoordinaten_x: list, liste_datumskoordinaten_y: list,
liste_datumskoordinaten_z: list,
liste_datumskoordinaten_x_y_z: list) -> None:
"""Setzt Datumskoordinaten für Netzpunkte unter Berücksichtigung von Vorinformationen.
Für die übergebenen Punktlisten werden die Felder datumskoordinate_x, datumskoordinate_y
und / oder datumskoordinate_z auf 1 gesetzt. Ein Setzen erfolgt nur, wenn zu der jeweiligen Koordinate
eine Vorinformation zur Standardabweichung (stabw_vorinfo_*) vorhanden ist. Eine 1 bedeutet,
dass dieser Punkt für die Datumdefinition verwendet wird.
:param liste_datumskoordinaten_x: Liste der Punktnummern mit Datumsdefinition in X.
:type liste_datumskoordinaten_x: list
:param liste_datumskoordinaten_y: Liste der Punktnummern mit Datumsdefinition in Y.
:type liste_datumskoordinaten_y: list
:param liste_datumskoordinaten_z: Liste der Punktnummern mit Datumsdefinition in Z.
:type liste_datumskoordinaten_z: list
:param liste_datumskoordinaten_x_y_z: Liste der Punktnummern mit Datumsdefinition in X, Y und Z.
:type liste_datumskoordinaten_x_y_z: list
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
# Überprüfen, ob Genauigkeitsinformationen zu den Punkten in der Datenbank gespeichert sind. Ist dies der Fall, wird eine 1 in das entsprechende Tupel geschrieben.
liste_stabw_vorinfo_x = [str(row[0]).strip() for row in cursor.execute(
"SELECT punktnummer FROM Netzpunkte WHERE stabw_vorinfo_x IS NOT NULL").fetchall()]
liste_stabw_vorinfo_y = [str(row[0]).strip() for row in cursor.execute(
@@ -277,6 +391,7 @@ class Datenbankzugriff:
cursor.execute(f"UPDATE Netzpunkte SET datumskoordinate_x = 1 WHERE punktnummer = ? AND stabw_vorinfo_x IS NOT NULL", (str(punktnummer),))
else:
print(f"Die X-Koordinate des Punktes {punktnummer} wurde nicht in eine Datumskoordinate geändert, weil keine Vorinformationen zur Standardabweichung der X-Koordinate des Punktes vorliegen. Diese bitte zuerst erfassen und Datumsdefinition wiederholen.")
if liste_datumskoordinaten_y != []:
for punktnummer in liste_datumskoordinaten_y:
punktnummer = str(punktnummer).strip()
@@ -308,26 +423,48 @@ class Datenbankzugriff:
def set_datumskoordinaten_to_neupunkte(self, liste_datumskoordinaten_x: list, liste_datumskoordinaten_y: list,
liste_datumskoordinaten_z: list,
liste_datumskoordinaten_x_y_z: list) -> None:
"""Setzt Datumskoordinaten für angegebene Punkte wieder zurück.
Für die übergebenen Punktlisten werden die Felder datumskoordinate_x, datumskoordinate_y
und / oder datumskoordinate_z auf 0 gesetzt. Dadurch werden diese in der Ausgleichung wieder
als Neupunkte behandelt und werden nicht für die Datumsdefinition verwendet.
:param liste_datumskoordinaten_x: Liste der Punktnummern, deren X-Datumsfestlegung zurückgesetzt wird.
:type liste_datumskoordinaten_x: list
:param liste_datumskoordinaten_y: Liste der Punktnummern, deren Y-Datumsfestlegung zurückgesetzt wird.
:type liste_datumskoordinaten_y: list
:param liste_datumskoordinaten_z: Liste der Punktnummern, deren Z-Datumsfestlegung zurückgesetzt wird.
:type liste_datumskoordinaten_z: list
:param liste_datumskoordinaten_x_y_z: Liste der Punktnummern, deren X-, Y- und Z-Datumsfestlegung zurückgesetzt werden.
:type liste_datumskoordinaten_x_y_z: list
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
# Überprüfung, ob eine Benutzereingabe in der entsprechenden Liste vorgenommen wurde. Wenn ja, wird ein UPDATE in der Datenbank durchgeführt.
if liste_datumskoordinaten_x != []:
for punktnummer in liste_datumskoordinaten_x:
punktnummer = str(punktnummer).strip()
cursor.execute(
f"UPDATE Netzpunkte SET datumskoordinate_x = 0 WHERE punktnummer = ?",
(str(punktnummer),))
if liste_datumskoordinaten_y != []:
for punktnummer in liste_datumskoordinaten_y:
punktnummer = str(punktnummer).strip()
cursor.execute(
f"UPDATE Netzpunkte SET datumskoordinate_y = 0 WHERE punktnummer = ?",
(str(punktnummer),))
if liste_datumskoordinaten_z != []:
for punktnummer in liste_datumskoordinaten_z:
punktnummer = str(punktnummer).strip()
cursor.execute(
f"UPDATE Netzpunkte SET datumskoordinate_z = 0 WHERE punktnummer = ?",
(str(punktnummer),))
if liste_datumskoordinaten_x_y_z != []:
for punktnummer in liste_datumskoordinaten_x_y_z:
punktnummer = str(punktnummer).strip()
@@ -340,6 +477,16 @@ class Datenbankzugriff:
con.close()
def set_normalhoehe_hfp(self, liste_normalhoehe_hfp: list) -> LiteralString | str:
"""Speichert Normalhöhen von amtlichen Höhenfestpunkten (HFP) in Netzpunkte.
Existiert die Punktnummer bereits in der Tabelle Netzpunkte, wird das Attribut normalhoehe_hfp aktualisiert.
Andernfalls wird ein neuer Datensatz mit Punktnummer und Normalhöhe eingefügt.
:param liste_normalhoehe_hfp: Liste mit HFP-Informationen (u. a. Punktnummer und Normalhöhe).
:type liste_normalhoehe_hfp: list
:return: Zusammenfassung der durchgeführten Aktualisierungen bzw. Hinweis, falls keine Daten übergeben wurden.
:rtype: str
"""
liste_hfp_in_db = self.get_normalhoehe_hfp()
if liste_normalhoehe_hfp != []:
con = sqlite3.connect(self.pfad_datenbank)
@@ -369,22 +516,36 @@ class Datenbankzugriff:
return f"Es wurden keine neuen Normalhöhen übergeben. Folgende Normalhöhen sind in der Datenbank enthalten: {liste_hfp_in_db}"
def set_beobachtung_ausschalten(self, dict_beobachtung_ausschalten: dict) -> None:
"""Schaltet ausgewählte Beobachtungen für die Ausgleichung aus.
Die zu deaktivierenden Beobachtungen werden über dict_beobachtung_ausschalten übergeben.
Der Schlüssel wird ausgewertet und das zugehörige *_ausschalten-Tupel in der Datenbank
auf 1 gesetzt (z. B. Richtungen, Zenitwinkel, Strecken, GNSS-Komponenten, Geometrisches Nivellement).
Dadurch wird die jeweilige Beobachtung in allen folgenden Iterationen nicht mehr berücksichtigt.
:param dict_beobachtung_ausschalten: Dictionary zur Steuerung, welche Beobachtungen deaktiviert werden, in der Form: {Beobachtung: "beobachtung_ausschalten"} zum ausschalten und {Beobachtung: ""} fürs beibehalten.
:type dict_beobachtung_ausschalten: dict
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
for beobachtung, ausschalten in dict_beobachtung_ausschalten.items():
# Nur in die Schleife gehen, wenn im Value des Dicts "beobachtung_ausschalten" enthalten ist.
if ausschalten == "beobachtung_ausschalten":
beobachtung_gesplittet = beobachtung.split("_")
# Distanz
if beobachtung_gesplittet[1] == "SD":
#print(f"SD: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET tachymeter_distanz_auschalten = 1
WHERE beobachtungenID = ? AND beobachtungsgruppeID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
(beobachtung_gesplittet[0], beobachtung_gesplittet[2], beobachtung_gesplittet[3], beobachtung_gesplittet[4]))
except:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
# Richtungen
if beobachtung_gesplittet[1] == "R":
#print(f"R: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET tachymeter_richtung_ausschalten = 1
WHERE beobachtungenID = ? AND beobachtungsgruppeID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -392,8 +553,8 @@ class Datenbankzugriff:
except:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
# Zenitwinkel
if beobachtung_gesplittet[1] == "ZW":
#print(f"ZW: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET tachymeter_zenitwinkel_ausschalten = 1
WHERE beobachtungenID = ? AND beobachtungsgruppeID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -402,7 +563,6 @@ class Datenbankzugriff:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
if beobachtung_gesplittet[1] == "gnssbx":
#print(f"gnssbx: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET gnss_bx_ausschalten = 1
WHERE beobachtungenID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -410,9 +570,8 @@ class Datenbankzugriff:
except:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
#GNSS-Basislinien
if beobachtung_gesplittet[1] == "gnssby":
#print(f"gnssby: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET gnss_by_ausschalten = 1
WHERE beobachtungenID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -421,7 +580,6 @@ class Datenbankzugriff:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
if beobachtung_gesplittet[1] == "gnssbz":
#print(f"gnssbz: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET gnss_bz_ausschalten = 1
WHERE beobachtungenID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -429,8 +587,8 @@ class Datenbankzugriff:
except:
print(f"Die Beobachtung {beobachtung} konnte aufgrund eines Fehlers nicht ausgeschaltet werden.")
# Geometrisches Nivellement
if beobachtung_gesplittet[1] == "niv":
#print(f"niv: {beobachtung_gesplittet}")
try:
cursor.execute(f"""UPDATE Beobachtungen SET niv_ausschalten = 1
WHERE beobachtungenID = ? AND punktnummer_sp = ? AND punktnummer_zp = ?""",
@@ -443,12 +601,25 @@ class Datenbankzugriff:
con.close()
def set_varianzkomponente(self, liste_varianzkomponten_anpassen: list) -> None:
"""Passt Varianzkomponenten für die nächste Iteration an.
Für jede angegebene Varianzkomponente wird der aktuelle Datenbankwert mit einem vom Benutzer übergebenen
Faktor multipliziert und zurückgeschrieben.
:param liste_varianzkomponten_anpassen: Liste mit Einträgen (instrumenteID, beobachtungsgruppe, faktor).
:type liste_varianzkomponten_anpassen: list
:return: None
:rtype: None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
if liste_varianzkomponten_anpassen != []:
for varianzkomponente in liste_varianzkomponten_anpassen:
# Faktor für die Varianzkomponentenschätzung der aktuellen Iteration aus der Datenbank abfragen
varianz_db = cursor.execute(f"SELECT varianz_varianzkomponentenschaetzung FROM Varianzkomponentenschaetzung WHERE instrumenteID = ? AND beobachtungsgruppe = ?",
(varianzkomponente[0], varianzkomponente[1])).fetchone()[0]
# Faktor für die Varianzkompontenschätzung aus der Datenbank mit der Benutzereingabe multiplizieren und das Produkt in der Datenbank für die nächste Iteration speichern.
cursor.execute(
f"UPDATE Varianzkomponentenschaetzung SET varianz_varianzkomponentenschaetzung = ? WHERE instrumenteID = ? AND beobachtungsgruppe = ?",
(varianz_db * varianzkomponente[2], varianzkomponente[0], varianzkomponente[1]))
@@ -457,38 +628,43 @@ class Datenbankzugriff:
cursor.close()
con.close()
def set_s0_apriori(self, liste_s0_apriori_anpassen: list) -> None:
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
if liste_s0_apriori_anpassen != []:
for s0_apriori in liste_s0_apriori_anpassen:
cursor.execute(
f"UPDATE Varianzkomponentenschaetzung SET s0_apriori_vorherige_iteration = ? WHERE instrumenteID = ? AND beobachtungsgruppe = ?",
(s0_apriori[2], s0_apriori[0], s0_apriori[1]))
print(f"Folgende S0_apriori wurde für die nächste Iteration gespeichert: {liste_s0_apriori_anpassen}.")
con.commit()
cursor.close()
con.close()
def get_koordinaten(self, koordinatenart: str, ausgabeart: str = "Dict") -> dict[Any, MutableDenseMatrix] | None:
"""Liest Koordinaten aus der Tabelle Netzpunkte.
Abhängig von der koordinatenart werden die entsprechenden Koordinatenspalten gelesen.
Es werden nur Datensätze berücksichtigt, bei denen X, Y und Z nicht NULL sind.
Bei ausgabeart="Dict" wird ein Dictionary in der Form {Punktnummer : sympy.Matrix([X, Y, Z])} zurückgegeben.
:param koordinatenart: Art der Koordinaten ("naeherung_lh" für Koordinaten im lokalen Horizontsystem aus dem Tachymeterimport oder "naeherung_us" für Näherungskoordinaten im geozentrisch-kartesischen System).
:type koordinatenart: str
:param ausgabeart: Ausgabeformat (Standard: "Dict").
:type ausgabeart: str
:return: Dictionary mit Punktnummer → Koordinatenmatrix oder None (abhängig von ausgabeart).
:rtype: dict | None
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
# Festlegen der Attribute, welche je nach Koordinatenart aus der Datenbank abgefragt werden sollen
if koordinatenart == "naeherung_lh":
values = "punktnummer, naeherungx_lh, naeherungy_lh, naeherungz_lh"
elif koordinatenart == "naeherung_us":
values = "punktnummer, naeherungx_us, naeherungy_us, naeherungz_us"
# SQL-Abfrage
liste_koordinaten = cursor.execute(f"""
SELECT {values} FROM Netzpunkte;
""").fetchall()
cursor.close()
con.close()
# Nur ausgeben, wenn für einen Punkt X, Y und Z Koordinaten vorhanden sind
liste_koordinaten = [
koordinate for koordinate in liste_koordinaten
if koordinate[1] is not None and koordinate[2] is not None and koordinate[3] is not None
]
# Aktuell ist nur die Ausgabeart "Dict" implementiert. Weitere Ausgabearten sind im Zuge möglicher erweiterungen zu implementieren.
if ausgabeart == "Dict":
return {
koordinate[0]: sp.Matrix([
@@ -500,6 +676,11 @@ class Datenbankzugriff:
}
def get_normalhoehe_hfp(self) -> list[Any]:
"""Liest alle Normalhöhen von amtlichen Höhenfestpunkten (HFP) aus der Tabelle Netzpunkte.
:return: Liste mit Tupeln (punktnummer, normalhoehe_hfp) für alle Punkte mit gesetzter Normalhöhe.
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_hfp = cursor.execute("SELECT punktnummer, normalhoehe_hfp FROM Netzpunkte WHERE normalhoehe_hfp IS NOT NULL").fetchall()
@@ -509,6 +690,16 @@ class Datenbankzugriff:
def get_instrument_liste(self, typ: str) -> list:
"""Liest Instrumente eines gegebenen Typs aus der Tabelle Instrumente.
Gibt eine Liste der gefundenen Instrumente zurück. Falls keine Instrumente vorhanden sind,
wird eine Textausgabe mit verfügbaren Typen zurückgegeben.
:param typ: Instrumenttyp.
:type typ: str
:return: Liste der Instrumente oder Hinweistext, falls keine Instrumente gefunden wurden.
:rtype: list | str
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_instrumente = cursor.execute("SELECT * FROM Instrumente WHERE typ = ?", (typ,)).fetchall()
@@ -520,6 +711,11 @@ class Datenbankzugriff:
return liste_instrumente
def get_genauigkeiten_dict(self) -> dict[Any, Any]:
"""Liest alle Genauigkeiten aus der Tabelle Genauigkeiten und gibt diese als Dictionary zurück.
:return: Dictionary {genauigkeitenID : restliche Tabellenwerte}.
:rtype: dict
"""
dict = {}
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -531,6 +727,11 @@ class Datenbankzugriff:
return dict
def get_instrumenteID_beobachtungenID_dict(self) -> dict[Any, Any]:
"""Liest die Zuordnung BeobachtungenID → InstrumenteID aus der Tabelle Beobachtungen.
:return: Dictionary {beobachtungenID : instrumenteID}.
:rtype: dict
"""
dict = {}
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -541,7 +742,18 @@ class Datenbankzugriff:
con.close()
return dict
def get_beobachtungen_id_beobachtungsgruppe_standpunkt_zielpunkt(self, beobachtungsart: str) -> list[Any]:
def get_puntknummern_beobachtungen_tachymeter(self, beobachtungsart: str) -> list[Any]:
"""Liest Beobachtungen (ID, Gruppe, Standpunkt, Zielpunkt) für Tachymeterbeobachtungen.
Es werden nur Beobachtungen berücksichtigt, bei denen die angegebene Beobachtungsart
nicht NULL ist. Zudem werden zusätzlich werden nur Beobachtungen übergeben, die nicht
ausgeschaltet werden.
:param beobachtungsart: Name der Beobachtungsspalte.
:type beobachtungsart: str
:return: Liste von Tupeln (beobachtungenID, beobachtungsgruppeID, punktnummer_sp, punktnummer_zp).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
sql_ausdruck = f"SELECT beobachtungenID, beobachtungsgruppeID, punktnummer_sp, punktnummer_zp FROM Beobachtungen WHERE {beobachtungsart} IS NOT NULL"
@@ -557,6 +769,13 @@ class Datenbankzugriff:
return liste_beobachtungen
def get_beobachtungen_from_beobachtungenid(self) -> list[Any]:
"""Liest Tachymeterbeobachtungen aus der Tabelle ``Beobachtungen``.
Es werden nur Datensätze mit gesetzter Tachymeter-Richtung berücksichtigt.
:return: Liste von Tupeln (punktnummer_sp, punktnummer_zp, beobachtungenID, beobachtungsgruppeID, tachymeter_richtung, tachymeter_zenitwinkel, tachymeter_distanz).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_beobachtungen = cursor.execute(f"SELECT punktnummer_sp, punktnummer_zp, beobachtungenID, beobachtungsgruppeID, tachymeter_richtung, tachymeter_zenitwinkel, tachymeter_distanz FROM Beobachtungen WHERE tachymeter_richtung IS NOT NULL").fetchall()
@@ -565,6 +784,14 @@ class Datenbankzugriff:
return liste_beobachtungen
def get_beobachtungen_gnssbasislinien(self) -> list[Any]:
"""Liest GNSS-Basislinienbeobachtungen inklusive Kovarianzangaben.
Es werden nur Datensätze zurückgegeben, bei denen alle benötigten GNSS-Felder (bx, by, bz, s0
und Kovarianzelemente) nicht NULL sind.
:return: Liste von Tupeln mit GNSS-Basisliniendaten und Kovarianzelementen.
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_beobachtungen = cursor.execute(f"SELECT beobachtungenID, punktnummer_sp, punktnummer_zp, gnss_bx, gnss_by, gnss_bz, gnss_s0, gnss_cxx, gnss_cxy, gnss_cxz, gnss_cyy, gnss_cyz, gnss_czz FROM Beobachtungen WHERE gnss_bx IS NOT NULL AND gnss_by IS NOT NULL AND gnss_bz IS NOT NULL AND gnss_s0 IS NOT NULL AND gnss_cxx IS NOT NULL AND gnss_cxy IS NOT NULL AND gnss_cxz IS NOT NULL AND gnss_cyy IS NOT NULL AND gnss_cyz IS NOT NULL AND gnss_czz IS NOT NULL").fetchall()
@@ -573,6 +800,15 @@ class Datenbankzugriff:
return liste_beobachtungen
def get_beobachtungen_nivellement(self) -> list[Any]:
"""Liest Nivellementbeobachtungen aus der Tabelle Beobachtungen.
Es werden nur Datensätze zurückgegeben, bei denen alle Nivellementfelder Daten enthalten
und niv_ausschalten = 0 ist, also diese Beobachtung in der Ausgleichung verwendet wird.
:return: Liste von Tupeln (beobachtungenID, punktnummer_sp, punktnummer_zp, niv_dh, niv_strecke, niv_anz_standpkte).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_beobachtungen = cursor.execute(f"SELECT beobachtungenID, punktnummer_sp, punktnummer_zp, niv_dh, niv_strecke, niv_anz_standpkte FROM Beobachtungen WHERE niv_dh IS NOT NULL AND niv_strecke IS NOT NULL AND niv_anz_standpkte IS NOT NULL AND niv_ausschalten = 0").fetchall()
@@ -581,6 +817,15 @@ class Datenbankzugriff:
return liste_beobachtungen
def get_datumskoordinate(self) -> list[Any]:
"""Liest die aktuell gesetzten Datumskoordinaten aus der Tabelle Netzpunkte.
Die Rückgabe erfolgt als Liste von Symbolnamen der Form "X<punktnummer>", "Y<punktnummer>"
und "Z<punktnummer>".
:return: Liste der Datumskoordinatenbezeichner.
:rtype: list
"""
# Abragen der Koordinaten mit einer 1 im Attribut datumskoordinate_*, da diese für die Lagerung verwendet werden sollen.
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_datumskoordinate_x = cursor.execute(
@@ -593,6 +838,7 @@ class Datenbankzugriff:
cursor.close()
con.close()
# Hinzufügen der jeweiligen Punktnummer als Symbol mit X, Y oder Z-Präfix zur Rückgabeliste
liste_datumskoordinaten = []
if liste_datumskoordinate_x != []:
for datumskoordinate in liste_datumskoordinate_x:
@@ -610,6 +856,15 @@ class Datenbankzugriff:
return liste_datumskoordinaten
def get_stabw_AA_Netzpunkte(self) -> dict[Any, Any]:
"""Liest Vorinformationen zur Standardabweichung der Netzpunkte aus der Tabelle Netzpunkte.
Die Rückgabe erfolgt als Dictionary mit Schlüsseln der Form "StabwAA_X<punktnummer>",
"StabwAA_Y<punktnummer>" und "StabwAA_Z<punktnummer>".
:return: Dictionary der Vorinformationen zur Standardabweichung je Koordinate.
:rtype: dict
"""
# Abfragen aller Punktnummern und s0 apriori Vorinformationen aus der Datenbank, wenn die Vorinformationen vorliegen.
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_stabwAA_x = cursor.execute(f"SELECT punktnummer, stabw_vorinfo_x FROM Netzpunkte WHERE stabw_vorinfo_x IS NOT NULL").fetchall()
@@ -621,6 +876,7 @@ class Datenbankzugriff:
cursor.close()
con.close()
# Erstellen des Rückgabedictionaries.
dict_stabwAA = {}
for stabwAA_x in liste_stabwAA_x:
punktnummer = str(stabwAA_x[0]).strip()
@@ -640,6 +896,17 @@ class Datenbankzugriff:
return dict_stabwAA
def get_gnss_beobachtungen_punktnummern(self, gnss_komponente: str) -> list[Any]:
"""Liest GNSS-Beobachtungen (ID, Standpunkt, Zielpunkt) für eine GNSS-Komponente.
Es werden nur Datensätze berücksichtigt, bei denen die geforderte Komponente sowie alle
zugehörigen GNSS-Genauigkeitsangaben (s0 und Kovarianzelemente) vorhanden sind.
Zusätzlich wird das jeweilige Ausschalt-Attribut berücksichtigt.
:param gnss_komponente: Name der GNSS-Basislinienkomponente (z. B. "gnss_bx", "gnss_by", "gnss_bz").
:type gnss_komponente: str
:return: Liste von Tupeln (beobachtungenID, punktnummer_sp, punktnummer_zp).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
sql = f"""SELECT beobachtungenID, punktnummer_sp, punktnummer_zp
@@ -662,6 +929,15 @@ class Datenbankzugriff:
return liste_gnss_beobachtungen
def get_nivellement_beobachtungen_punktnummern(self) -> list[Any]:
"""Liest Beobachtungen vom geometrischen Nivellement (ID, Standpunkt, Zielpunkt) aus der Tabelle Beobachtungen.
Es werden nur Datensätze berücksichtigt, bei denen alle Attribute gesetzt sind
und niv_ausschalten = 0 ist.
:return: Liste von Tupeln (beobachtungenID, punktnummer_sp, punktnummer_zp).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_nivellement_beobachtungen = cursor.execute(f"SELECT beobachtungenID, punktnummer_sp, punktnummer_zp FROM Beobachtungen WHERE niv_dh IS NOT NULL AND niv_strecke IS NOT NULL AND niv_anz_standpkte IS NOT NULL AND niv_ausschalten = 0").fetchall()
@@ -670,6 +946,11 @@ class Datenbankzugriff:
return liste_nivellement_beobachtungen
def get_varianzkomponentenschaetzung(self) -> list[Any]:
"""Liest alle Einträge der Varianzkomponentenschätzung aus der Datenbank.
:return: Liste von Tupeln (varianzkomponenteID, instrumenteID, beobachtungsgruppe, varianz_varianzkomponentenschaetzung).
:rtype: list
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_varianzkomponenten = cursor.execute(f"SELECT varianzkomponenteID, instrumenteID, beobachtungsgruppe, varianz_varianzkomponentenschaetzung FROM Varianzkomponentenschaetzung").fetchall()
@@ -677,12 +958,4 @@ class Datenbankzugriff:
con.close()
return liste_varianzkomponenten
def get_s0_apriori(self) -> list[Any]:
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_s0_apriori = cursor.execute(f"SELECT varianzkomponenteID, instrumenteID, beobachtungsgruppe, varianz_varianzkomponentenschaetzung, s0_apriori_vorherige_iteration FROM Varianzkomponentenschaetzung").fetchall()
cursor.close()
con.close()
return liste_s0_apriori

View File

@@ -1,19 +1,41 @@
import csv
from datetime import datetime
import numpy as np
import os
import webbrowser
from datetime import datetime
class Export:
def __init__(self) -> None:
pass
"""Hilfsfunktionen zum Exportieren von Ergebnissen und Protokollen.
Die Klasse stellt Methoden zur Verfügung für:
- Export von Matrizen in CSV-Dateien,
- Export von Ausgleichungsergebnissen (Skalare und Matrizen) in CSV-Dateien,
- Erzeugung und Speicherung eines HTML-Protokolls inklusive automatischem Öffnen im Browser.
"""
@staticmethod
def matrix_to_csv(dateiname: str, liste_spaltenbeschriftung: list, liste_zeilenbeschriftung: list, Matrix: np.matrix | sp.Matrix,
beschriftung_kopfzeile: object = "") -> None:
"""Schreibt eine Matrix mit Zeilen- und Spaltenbeschriftungen in eine CSV-Datei.
Die Ausgabe erfolgt mit Semikolon als Trennzeichen. Die Kopfzeile enthält optional eine
zusätzliche Beschriftung sowie Spaltenbeschriftungen.
Zudem werden Zeilenbeschriftungen durchgeführt.
:param dateiname: Pfad zu schreibenden CSV-Datei.
:type dateiname: str
:param liste_spaltenbeschriftung: Liste der Spaltenbeschriftungen.
:type liste_spaltenbeschriftung: list
:param liste_zeilenbeschriftung: Liste der Zeilenbeschriftungen.
:type liste_zeilenbeschriftung: list
:param Matrix: Zu exportierende Matrix.
:type Matrix: np.matrix | sp.Matrix
:param beschriftung_kopfzeile: Optionaler Eintrag in der linken oberen Zelle der Kopfzeile.
:type beschriftung_kopfzeile: object
:return: None
:rtype: None
"""
with open(dateiname, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile, delimiter=";")
@@ -26,6 +48,7 @@ class Export:
zeile_als_text = [zeilenbeschriftung]
for eintrag in zeile:
try:
# Dezimaltrenner von Punkt in Komma ändern, weil Python und Excel andere Konventionen vertreten.
eintrag_text = str(eintrag).replace(".", ",")
try:
eintrag_text = float(eintrag_text)
@@ -36,46 +59,24 @@ class Export:
zeile_als_text.append(eintrag_text)
writer.writerow(zeile_als_text)
@staticmethod
def ausgleichung_to_datei(dateiname: str, dict_ausgleichung: dict) -> None:
with open(dateiname, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile, delimiter=";")
def speichere_html_protokoll(metadaten: dict, ergebnisse: dict) -> None:
"""Erzeugt ein HTML-Protokoll der Ausgleichungsergebnisse und speichert es als Datei.
writer.writerow(["Parameter", "Wert"])
Es wird der Unterordner Protokolle angelegt, falls dieser noch nicht existiert.
Das Protokoll wird als HTML-Datei gespeichert und anschließend im Browser geöffnet.
for key, value in dict_ausgleichung.items():
Erwartete Schlüssel in metadaten sind u. a. "projekt", "bearbeiter" und "datum".
Erwartete Schlüssel in ergebnisse sind u. a. "df_globaltest", "df_redundanz",
"df_ellipsen", "df_konfidenzellipsen", "df_koordinaten_geozentrisch_kartesisch"
und "df_koordinaten_utm". Die zugehörigen Werte müssen die Methode to_html bereitstellen.
if hasattr(value, "tolist"):
rows = value.rows
cols = value.cols
writer.writerow([key, f"Matrix {rows}x{cols}"])
for i, zeile in enumerate(value.tolist()):
zeile_als_text = [f"{key}_zeile_{i+1}"]
for eintrag in zeile:
try:
eintrag_float = float(eintrag)
eintrag_text = f"{eintrag_float}".replace(".", ",")
try:
eintrag_text = float(eintrag_text)
except:
eintrag_text = eintrag_text
except Exception:
eintrag_text = str(eintrag)
zeile_als_text.append(eintrag_text)
writer.writerow(zeile_als_text)
else:
try:
value_float = float(value)
value_text = f"{value_float}".replace(".", ",")
except Exception:
value_text = str(value)
writer.writerow([key, value_text])
def speichere_html_protokoll(metadaten, ergebnisse):
:param metadaten: Dictionary mit Metadaten zum Protokoll.
:type metadaten: dict
:param ergebnisse: Dictionary mit Ergebnisobjekten (z. B. DataFrames) zur HTML-Ausgabe.
:type ergebnisse: dict
:return: None
:rtype: None
"""
# Pfad für den Ordner erstellen
ordner = "Protokolle"
if not os.path.exists(ordner):

View File

@@ -1,30 +1,55 @@
from typing import Any
import numpy as np
from numpy import ndarray, dtype
import sympy as sp
from sympy import MutableDenseMatrix
from sympy.matrices.expressions.matexpr import MatrixElement
from typing import Any
from Datenbank import Datenbankzugriff
import sympy as sp
from Export import Export
from Berechnungen import Berechnungen
import numpy as np
import importlib
from Datenbank import Datenbankzugriff
from Export import Export
from Koordinatentransformationen import Transformationen
from pathlib import Path
import pandas as pd
import numpy as np
import sympy as sp
class FunktionalesModell:
"""Aufstellung von Beobachtungsgleichungen und der Jacobi-Matrix.
Die Klasse stellt Methoden zur Verfügung für:
- symbolische Aufstellung der Jacobi-Matrix für Tachymeter-, GNSS- und Nivellementbeobachtungen,
- numerische Auswertung der symbolischen Jacobi-Matrix über Substitutionen und Lambdify,
- Aufbau des Beobachtungsvektors (numerisch) und des Näherungs-Beobachtungsvektors (symbolisch/numerisch),
- Aufbau des Unbekanntenvektors (symbolisch/numerisch) inklusive Iterationsfortschreibung,
- Berechnung des Verbesserungsvektors dl = l f(x0) mit Winkel-Normierung für Richtungen,
- Erstellung und Aktualisierung eines Substitutions-Dictionaries für alle im Modell verwendeten Symbole.
Die grundlegende Funktionsweise der Matrixdefinition lautet:
1) Einmaligen Aufbauen der Symbolischen Matrix einmalig
2) In jeder Iteration Substituieren der Symbolischen Matrizen in Numerische np.asarrays
"""
def __init__(self, pfad_datenbank: str, a: float, b: float, pfad_tif_quasigeoidundolation: str = None) -> None:
"""Initialisiert das funktionale Modell.
Legt die Ellipsoidparameter a und b fest, initialisiert Hilfsklassen (Berechnungen, Datenbankzugriff, Transformationen)
und erzeugt das Substitutions-Dictionary für das geozentrisch-kartesische System.
:param pfad_datenbank: Pfad zur SQLite-Datenbank.
:type pfad_datenbank: str
:param a: Große Halbachse a des Referenzellipsoids in Meter.
:type a: float
:param b: Kleine Halbachse b des Referenzellipsoids in Meter.
:type b: float
:param pfad_tif_quasigeoidundolation: Pfad zu Quasigeoidundulationsdaten als GeoTIFF vom BKG für Transformationen (optional).
:type pfad_tif_quasigeoidundolation: str | None
:return: None
:rtype: None
"""
self.pfad_datenbank = pfad_datenbank
self.a = a
self.b = b
self.berechnungen = Berechnungen(self.a, self.b)
self.db_zugriff = Datenbankzugriff(self.pfad_datenbank)
self.trafos = Transformationen(pfad_datenbank)
self.pfad_tif_quasigeoidundolation = pfad_tif_quasigeoidundolation
self.substitutionen_dict = self.dict_substitutionen_uebergeordnetes_system()
@@ -35,25 +60,43 @@ class FunktionalesModell:
self.func_u0 = None
self.liste_beobachtungsvektor_symbolisch = None
def jacobi_matrix_symbolisch(self, datumsfestlegung: str = None, liste_unbekannte_datumsfestlegung: list = None) -> tuple[MutableDenseMatrix | MatrixElement | list[Any] | Any, list[Any], list[Any]] | None:
#liste_beobachtungsarten = ["tachymeter_distanz", "tachymeter_richtung", "tachymeter_zenitwinkel"]
"""Erstellt die symbolische Jacobi-Matrix (A-Matrix) für die Ausgleichung.
Es werden die in der Datenbank vorhandenen Beobachtungen (Tachymeter: Distanz/Richtung/Zenitwinkel,
GNSS: Basislinienkomponenten bx/by/bz, Geometrisches Nivellement: dh) eingelesen, daraus die benötigten Unbekannten
(X, Y, Z je Punkt sowie Orientierungen O je Richtungs-Beobachtungsgruppe) aufgebaut und anschließend
die symbolischen Gleichungen zur Jacobi-Matrix zusammengesetzt.
Bei datumsfestlegung == "weiche Lagerung" werden Unbekannte so umsortiert, dass die zur Lagerung
zu verwendenden Unbekannten rechts stehen. Zusätzlich werden die Gleichungen der Anschlusspunkte (lA_*) als
Zusatzzeilen an A angehängt.
Die symbolische Jacobi-Matrix wird als CSV in Zwischenergebnisse\\Jacobi_Matrix_Symbolisch.csv exportiert.
:param datumsfestlegung: Art der Datumsfestlegung (aktuell nur "weiche Lagerung").
:type datumsfestlegung: str | None
:param liste_unbekannte_datumsfestlegung: Liste der Symbolnamen der zur Lagerung zu verwendenden Unbekannten (z. B. ["X100", "Y100", "Z100"]).
:type liste_unbekannte_datumsfestlegung: list | None
:return: Tupel aus symbolischer Jacobi-Matrix, Liste der Unbekannten (Symbole) und Liste der Zeilenbeschriftungen (Beobachtungskennungen). Falls keine Beobachtungen vorliegen: None.
:rtype: tuple[MutableDenseMatrix | MatrixElement | list[Any] | Any, list[Any], list[Any]] | None
"""
# Über die liste_beobachtungsarten wird festgelegt, welche Beobachtungsarten in der Ausgleichung verwendet werden.
liste_beobachtungsarten = ["tachymeter_distanz", "tachymeter_richtung", "tachymeter_zenitwinkel", "gnss_basislinien", "geometrisches_nivellement"]
#liste_beobachtungsarten = ["tachymeter_distanz", "tachymeter_richtung", "tachymeter_zenitwinkel", "gnss_basislinien"]
db_zugriff = Datenbankzugriff(self.pfad_datenbank)
# Initialisieren der zu befüllenden Listen
liste_beobachtungen_rohdaten_gnssbasislinien = []
liste_beobachtungen_rohdaten_tachymeter = []
liste_beobachtungen_rohdaten_nivellement = []
liste_punktnummern =[]
liste_orientierungsunbekannte = []
# Für jede Beobachtungsart wird aus der Datenbank abgefragt, welche Beobachtungen vorliegen. Von diesen werden der Stand- und Zielpunkt gespeichert, um diese im weiteren Verlauf dieser Methode zu verarbeiten.
for beobachtungsart in liste_beobachtungsarten:
#Tachymeter Block
if beobachtungsart.startswith("tachymeter"):
liste_id_standpunkt_zielpunkt = db_zugriff.get_beobachtungen_id_beobachtungsgruppe_standpunkt_zielpunkt(beobachtungsart)
liste_id_standpunkt_zielpunkt = self.db_zugriff.get_puntknummern_beobachtungen_tachymeter(beobachtungsart)
for beobachtungenID, beobachtungsgruppeID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
liste_beobachtungen_rohdaten_tachymeter.append(
@@ -68,23 +111,10 @@ class FunktionalesModell:
if beobachtungsart == "tachymeter_richtung":
if beobachtungsgruppeID not in liste_orientierungsunbekannte:
liste_orientierungsunbekannte.append(beobachtungsgruppeID)
#GNSS Block
#if beobachtungsart == "gnss_basislinien":
# liste_id_standpunkt_zielpunkt = db_zugriff.get_gnss_beobachtungen_punktnummern()
# for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
# standpunkt = str(standpunkt).strip()
# zielpunkt = str(zielpunkt).strip()
# liste_beobachtungen_rohdaten_gnssbasislinien.append((beobachtungsart, beobachtungenID, standpunkt, zielpunkt))
# if standpunkt not in liste_punktnummern:
# liste_punktnummern.append(standpunkt)
# if zielpunkt not in liste_punktnummern:
# liste_punktnummern.append(zielpunkt)
#GNSS Block
if beobachtungsart == "gnss_basislinien":
liste_id_standpunkt_zielpunkt = db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_bx")
liste_id_standpunkt_zielpunkt = self.db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_bx")
for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
standpunkt = str(standpunkt).strip()
zielpunkt = str(zielpunkt).strip()
@@ -94,7 +124,7 @@ class FunktionalesModell:
if zielpunkt not in liste_punktnummern:
liste_punktnummern.append(zielpunkt)
liste_id_standpunkt_zielpunkt = db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_by")
liste_id_standpunkt_zielpunkt = self.db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_by")
for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
standpunkt = str(standpunkt).strip()
zielpunkt = str(zielpunkt).strip()
@@ -104,7 +134,7 @@ class FunktionalesModell:
if zielpunkt not in liste_punktnummern:
liste_punktnummern.append(zielpunkt)
liste_id_standpunkt_zielpunkt = db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_bz")
liste_id_standpunkt_zielpunkt = self.db_zugriff.get_gnss_beobachtungen_punktnummern("gnss_bz")
for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
standpunkt = str(standpunkt).strip()
zielpunkt = str(zielpunkt).strip()
@@ -116,7 +146,7 @@ class FunktionalesModell:
if beobachtungsart == "geometrisches_nivellement":
liste_id_standpunkt_zielpunkt = db_zugriff.get_nivellement_beobachtungen_punktnummern()
liste_id_standpunkt_zielpunkt = self.db_zugriff.get_nivellement_beobachtungen_punktnummern()
for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
standpunkt = str(standpunkt).strip()
@@ -129,13 +159,7 @@ class FunktionalesModell:
if zielpunkt not in liste_punktnummern:
liste_punktnummern.append(zielpunkt)
#if liste_beobachtungen_rohdaten_tachymeter == []:
# return None
#dict_punkt_symbole = {}
# Erstellen der Symbole für die Unbekannten (X, Y, Z und Orientierung)
liste_unbekannte = []
for punkt in liste_punktnummern:
@@ -151,6 +175,7 @@ class FunktionalesModell:
dict_orientierung_symbole[orientierungsunbekannte] = O
liste_unbekannte.append(O)
# Erstellen der Symbolischen Gleichungen für die Jacobimatrix
liste_beobachtungsgleichungen_distanz =[]
liste_zeilenbeschriftungen_distanz = []
@@ -161,39 +186,30 @@ class FunktionalesModell:
liste_zeilenbeschriftungen_zenitwinkel = []
liste_beobachtungsgleichungen_gnssbasislinien = []
liste_A_gnssbasislinien_zeilen = []
liste_zeilenbeschriftungen_gnssbasislinien = []
liste_beobachtungsgleichungen_nivellement = []
liste_A_nivellement_zeilen = []
liste_zeilenbeschriftungen_nivellement = []
if liste_beobachtungen_rohdaten_tachymeter != []:
for beobachtungsart, beobachtungenID, beobachtungsgruppeID, standpunkt, zielpunkt in liste_beobachtungen_rohdaten_tachymeter:
# Symbole erstellen
X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]
B_sp, L_sp = sp.symbols(f"B{standpunkt} L{standpunkt}")
alpha = sp.symbols(f"{beobachtungenID}_R_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
zw = sp.symbols(f"{beobachtungenID}_ZW_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
s = sp.symbols(f"{beobachtungenID}_SD_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
azimut_berechnet = sp.symbols(f"azimut_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
zw_berechnet = sp.symbols(f"zw_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
s_berechnet = sp.symbols(f"strecke_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
# Symbolische Gleichungen aufstellen
if beobachtungsart == "tachymeter_distanz":
beobachtungsgleichung = sp.sqrt((X_zp - X_sp) ** 2 + (Y_zp - Y_sp) ** 2 + (Z_zp - Z_sp) ** 2)
liste_beobachtungsgleichungen_distanz.append(beobachtungsgleichung)
liste_zeilenbeschriftungen_distanz.append(
f"{beobachtungenID}_SD_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
elif beobachtungsart == "tachymeter_richtung":
# for beobachtungenID, beobachtungsgruppeID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
d_r_dX_zp = (
(sp.sin(B_sp) * sp.cos(L_sp) * sp.sin(azimut_berechnet) - sp.sin(L_sp) * sp.cos(azimut_berechnet)) / (
s_berechnet * sp.sin(zw_berechnet)))
@@ -207,6 +223,8 @@ class FunktionalesModell:
d_r_dO_sp = -1
zeile_A_Matrix = []
# Symbolische Gleichungen zur Jacobimatrix hinzufügen
for punkt in liste_punktnummern:
if punkt == standpunkt:
zeile_A_Matrix.extend([d_r_dX_sp, d_r_dY_sp, d_r_dZ_sp])
@@ -228,7 +246,7 @@ class FunktionalesModell:
elif beobachtungsart == "tachymeter_zenitwinkel":
# Symbolische Gleichungen aufstellen
d_r_dX_zp = ((X_zp - X_sp) * sp.cos(zw_berechnet) - s_berechnet * sp.cos(B_sp) * sp.cos(L_sp)) / (s_berechnet ** 2 * sp.sin(zw_berechnet))
d_r_dX_sp = - d_r_dX_zp
d_r_dY_zp = ((Y_zp - Y_sp) * sp.cos(zw_berechnet) - s_berechnet * sp.cos(B_sp) * sp.sin(L_sp)) / (s_berechnet ** 2 * sp.sin(zw_berechnet))
@@ -237,6 +255,8 @@ class FunktionalesModell:
d_r_dZ_sp = - d_r_dZ_zp
zeile_A_Matrix = []
# Symbolische Gleichungen zur Jacobimatrix hinzufügen
for punkt in liste_punktnummern:
if punkt == standpunkt:
zeile_A_Matrix.extend([d_r_dX_sp, d_r_dY_sp, d_r_dZ_sp])
@@ -253,27 +273,10 @@ class FunktionalesModell:
f"{beobachtungenID}_ZW_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}"
)
#if liste_beobachtungen_rohdaten_gnssbasislinien != []:
# for beobachtungsart, beobachtungenID, standpunkt, zielpunkt in liste_beobachtungen_rohdaten_gnssbasislinien:
# X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
# X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]
# if beobachtungsart == "gnss_basislinien":
# beobachtungsgleichung_bx = X_zp - X_sp
# beobachtungsgleichung_by = Y_zp - Y_sp
# beobachtungsgleichung_bz = Z_zp - Z_sp
# liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_bx)
# liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_by)
# liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_bz)
# liste_zeilenbeschriftungen_gnssbasislinien.append(
# f"{beobachtungenID}_gnssbx_{standpunkt}_{zielpunkt}")
# liste_zeilenbeschriftungen_gnssbasislinien.append(
# f"{beobachtungenID}_gnssby_{standpunkt}_{zielpunkt}")
# liste_zeilenbeschriftungen_gnssbasislinien.append(
# f"{beobachtungenID}_gnssbz_{standpunkt}_{zielpunkt}")
# GNSS-Basislinien
if liste_beobachtungen_rohdaten_gnssbasislinien != []:
for beobachtungsart, beobachtungenID, standpunkt, zielpunkt in liste_beobachtungen_rohdaten_gnssbasislinien:
# Symbolische Gleichungen aufstellen
X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]
@@ -296,10 +299,10 @@ class FunktionalesModell:
f"{beobachtungenID}_gnssbz_{standpunkt}_{zielpunkt}")
# Geometrisches Nivellement
if liste_beobachtungen_rohdaten_nivellement != []:
for beobachtungsart, beobachtungenID, standpunkt, zielpunkt in liste_beobachtungen_rohdaten_nivellement:
X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]
# Symbolische Gleichungen aufstellen
B_sp, L_sp = sp.symbols(f"B{standpunkt} L{standpunkt}")
B_zp, L_zp = sp.symbols(f"B{zielpunkt} L{zielpunkt}")
@@ -312,6 +315,7 @@ class FunktionalesModell:
d_r_dZ_zp = sp.sin(B_zp)
d_r_dZ_sp = -sp.sin(B_sp)
# Symbolische Gleichungen zur Jacobimatrix hinzufügen
zeile_A_Matrix = []
for punkt in liste_punktnummern:
if punkt == standpunkt:
@@ -329,6 +333,7 @@ class FunktionalesModell:
f"{beobachtungenID}_niv_{standpunkt}_{zielpunkt}"
)
# Jacobimatrix aus den einzelnen Listen zusammensetzen, Sy,bolischen Unbekanntenvektor erstellen
if liste_beobachtungsgleichungen_distanz:
f_matrix_dist = sp.Matrix(liste_beobachtungsgleichungen_distanz)
unbekanntenvektor = sp.Matrix(liste_unbekannte)
@@ -354,15 +359,13 @@ class FunktionalesModell:
A_gnssbasislinien = None
if liste_A_nivellement_zeilen:
#f_matrix_nivellement = sp.Matrix(liste_beobachtungsgleichungen_nivellement)
#unbekanntenvektor = sp.Matrix(liste_unbekannte)
#A_nivellement = f_matrix_nivellement.jacobian(unbekanntenvektor)
A_nivellement = sp.Matrix(liste_A_nivellement_zeilen)
else:
A_nivellement = None
A_gesamt = None
# liste_zeilenbeschriftungen_gesamt enthält die Symbolischen Beobachtungen
liste_zeilenbeschriftungen_gesamt = []
if A_dist is not None:
@@ -400,9 +403,9 @@ class FunktionalesModell:
if A_gesamt is None:
return None
# Wenn eine weiche Lagerung erfolgt, werden die Unbekannten umsortiert, sodass die für die Lagerung zu verwendenden Unbekannten rechts in der Matrix aufgeführt werden.
# Zudem werden für die Anschlusspunkte weiter Gleichungen als "Beobachtungen" unten an die Jacobimatrix angefügt.
if datumsfestlegung == "weiche Lagerung":
vertauschung = list(range(len(liste_unbekannte)))
if liste_unbekannte_datumsfestlegung is not None and liste_unbekannte_datumsfestlegung != []:
liste_unbekannte_alt = list(liste_unbekannte)
liste_unbekannte_datumsfestlegung = [str(u).strip() for u in liste_unbekannte_datumsfestlegung]
@@ -420,7 +423,7 @@ class FunktionalesModell:
A_gesamt = A_gesamt[:, vertauschung]
liste_unbekannte = [liste_unbekannte_alt[i] for i in vertauschung]
# Zusatzgeleichungen der weichen Lagerung
# Zusatzgeleichungen der weichen Lagerung für die Anschlusspunkte
anzhl_einheitsmatrix = len(liste_unbekannte_datumsfestlegung)
if anzhl_einheitsmatrix > 0:
nullenmatrix = sp.zeros(anzhl_einheitsmatrix, A_gesamt.shape[1] - anzhl_einheitsmatrix)
@@ -430,24 +433,49 @@ class FunktionalesModell:
for unbekannte_datumsfestlegung in liste_unbekannte_datumsfestlegung:
liste_zeilenbeschriftungen_gesamt.append(f"lA_{unbekannte_datumsfestlegung}")
# Symbolische Liste der Unbekannten speichern
self.liste_unbekanntenvektor_symbolisch = liste_unbekannte
# Symbolische Jacobimatrix speichern
Export.matrix_to_csv(r"Zwischenergebnisse\Jacobi_Matrix_Symbolisch.csv", liste_unbekannte,
liste_zeilenbeschriftungen_gesamt, A_gesamt, "Beobachtung")
return A_gesamt, liste_unbekannte, liste_zeilenbeschriftungen_gesamt
def jacobi_matrix_zahlen_iteration_0(self, A_symbolisch: sp.Matrix, koordinatenart: str,
def jacobi_matrix_numerisch(self, A_symbolisch: sp.Matrix, koordinatenart: str,
liste_unbekannte: list = None,
liste_zeilenbeschriftungen_gesamt: list = None,
iterationsnummer: int = 0) -> ndarray[tuple[Any, ...], dtype[Any]] | None:
"""Erstellt eine numerische Matrix aus einer symbolischen Jacobi-Matrix.
Es wird sympy.lambdify verwendet, um die symbolische Matrix
mit den aktuellen Substitutionen effizient als Numpy-Array auszuwerten. Die Lambdify-Funktion wird
gecached (self.func_A0), um Rechenzeit bei Ausführen mehrerer Iterationen zu sparen.
Die numerische Jacobi-Matrix wird als CSV in den Ordner Zwischenergebnisse exportiert.
:param A_symbolisch: Symbolische Jacobi-Matrix.
:type A_symbolisch: sp.Matrix
:param koordinatenart: Bezeichnung der Koordinatenart (aktuell implementiert: "naeherung_us").
:type koordinatenart: str
:param liste_unbekannte: Liste der Unbekannten (Symbole) in der Spaltenreihenfolge.
:type liste_unbekannte: list | None
:param liste_zeilenbeschriftungen_gesamt: Liste der Zeilenbeschriftungen (Beobachtungskennungen).
:type liste_zeilenbeschriftungen_gesamt: list | None
:param iterationsnummer: Iterationsnummer für Dateinamen der Zwischenergebnisse.
:type iterationsnummer: int
:return: Numerische Jacobi-Matrix als Numpy-Array.
:rtype: ndarray[tuple[Any, ...], dtype[Any]] | None
:raises ValueError: Falls Symbole in A_symbolisch enthalten sind, für die keine Substitutionen vorhanden sind.
"""
# Symbolischen Beobachtungsvektor als Instanzvariable speichern
self.liste_beobachtungsvektor_symbolisch = [str(x) for x in liste_zeilenbeschriftungen_gesamt]
# Wenn es sich um geozentrisch-kartesischen Koordinaten handelt, wird die Sympy-Methode Lambdify verwendet, um die Symbole aus der vorherigen Methode jacobi_matrix_symbolisch in Numerische Zahlen umzuwandeln.
# Zur Ersparnis von Rechenzeit werden die numerischen Zahlen als Numpy-Werte behandelt.
if koordinatenart == "naeherung_us":
#A_numerisch = A_symbolisch.xreplace(self.substitutionen_dict)
if self.func_A0 is None:
#self.liste_symbole_lambdify = sorted(self.substitutionen_dict.keys(), key=lambda s: str(s))
self.func_A0 = sp.lambdify(
self.liste_symbole_lambdify,
A_symbolisch,
@@ -455,45 +483,43 @@ class FunktionalesModell:
cse=True
)
# Überprüfung, ob alle in der Symbolischen Matrix enthaltenen Symbole substituiert werden können. Ist ein Symbol nicht durch einen numerischen Wert ersetzbar, wird eine Fehlermeldung ausgegeben.
fehlend = [s for s in self.liste_symbole_lambdify if s not in self.substitutionen_dict]
if fehlend:
Export.matrix_to_csv(
r"Zwischenergebnisse\fehlende_substitutionen_A.csv",
[""],
[str(s) for s in fehlend],
sp.Matrix([[str(s)] for s in fehlend]),
"fehlend"
)
raise ValueError(f"Fehlende Substitutionen in A: {[str(s) for s in fehlend[:30]]}")
liste_werte = [self.substitutionen_dict[s] for s in self.liste_symbole_lambdify]
#A_numerisch = sp.Matrix(self.func_A0(*liste_werte))
A_numerisch = np.asarray(self.func_A0(*liste_werte), dtype=float)
Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}Jacobi_Matrix_Numerisch_Iteration0.csv", liste_unbekannte,
Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}Jacobi_Matrix_Numerisch.csv", liste_unbekannte,
liste_zeilenbeschriftungen_gesamt, A_numerisch, "Beobachtung")
condA = float(np.linalg.cond(A_numerisch))
rankA = int(np.linalg.matrix_rank(A_numerisch))
Export.matrix_to_csv(
fr"Zwischenergebnisse\{iterationsnummer}_Jacobi_Matrix_Stats.csv",
[""],
["condA", "rankA"],
np.array([[condA], [rankA]], dtype=float),
"Wert"
)
return A_numerisch
else:
print("Koordinaten noch nicht implementiert!")
print(f"Die Koordinatenart {koordinatenart} ist noch nicht im Programm implementiert!")
def beobachtungsvektor_numerisch(self, liste_beobachtungsvektor_symbolisch: list) -> MutableDenseMatrix:
"""Erstellt den numerischen Beobachtungsvektor aus symbolischen Beobachtungskennungen.
Die Einträge des symbolischen Beobachtungsvektors werden über self.substitutionen_dict substituiert.
Anschlusspunkte der weichen Lagerung (lA_*) werden gesondert behandelt, indem das Präfix entfernt wird.
Der numerische Beobachtungsvektor wird als CSV-Datei in Zwischenergebnisse\\Beobachtungsvektor_Numerisch.csv exportiert.
:param liste_beobachtungsvektor_symbolisch: Liste symbolischer Beobachtungsbezeichnungen (Strings).
:type liste_beobachtungsvektor_symbolisch: list
:return: Numerischer Beobachtungsvektor als SymPy-Matrix (Spaltenvektor).
:rtype: MutableDenseMatrix
"""
liste_beobachtungsvektor_numerisch = []
for beobachtung_symbolisch in liste_beobachtungsvektor_symbolisch:
beobachtung_symbolisch = str(beobachtung_symbolisch).strip()
# Die Anschlusspunkte für die weiche Lagerung (lA) werden gesondert bearbeitet, weil die Symbole anders aufgebaut sind.
if beobachtung_symbolisch.startswith("lA_"):
beobachtung_symbolisch = str(beobachtung_symbolisch.split("_", 1)[1]).strip()
# Substituieren des symbolischen Beobachtungsvektors
liste_beobachtungsvektor_numerisch.append(self.substitutionen_dict[sp.Symbol(beobachtung_symbolisch)])
beobachtungsvektor_numerisch = sp.Matrix(liste_beobachtungsvektor_numerisch)
@@ -501,10 +527,24 @@ class FunktionalesModell:
return beobachtungsvektor_numerisch
def beobachtungsvektor_naeherung_symbolisch(self, liste_beobachtungsvektor_symbolisch: list) -> sp.Matrix:
"""Erstellt den symbolischen Näherungs-Beobachtungsvektor f(x0).
Aus den Beobachtungskennungen werden Stand-/Zielpunkte und Beobachtungsarten abgeleitet und die
entsprechenden symbolischen Beobachtungsgleichungen aufgebaut (z. B. geometrische Distanz, GNSS-Differenzen,
Richtungs-/Zenitwinkel-Symbole, Geometrisches Nivellement über Normalhöhen).
Der symbolische Näherungs-Beobachtungsvektor wird als CSV <-Datei in Zwischenergebnisse\\Beobachtungsvektor_Näherung_Symbolisch.csv exportiert.
:param liste_beobachtungsvektor_symbolisch: Liste symbolischer Beobachtungskennungen (Strings).
:type liste_beobachtungsvektor_symbolisch: list
:return: Symbolischer Näherungs-Beobachtungsvektor als SymPy-Matrix.
:rtype: sp.Matrix
"""
liste_beobachtungsgleichungen = []
self.dict_punkt_symbole = {}
liste_punktnummern = []
# Speichern der Punktnummern der Stand- und Zeilpunkte der Beobachtungen, um daraus später die Symbole für die Unbekannten zu erstellen.
for beobachtung_symbolisch in liste_beobachtungsvektor_symbolisch:
aufgeteilt = str(beobachtung_symbolisch).strip().split("_")
if aufgeteilt[0] == "lA":
@@ -528,6 +568,7 @@ class FunktionalesModell:
if zielpunkt not in liste_punktnummern:
liste_punktnummern.append(zielpunkt)
# Erstellen der Symbole für die Unbekannten
for punkt in liste_punktnummern:
X, Y, Z = sp.symbols(f"X{punkt} Y{punkt} Z{punkt}")
self.dict_punkt_symbole[str(punkt)] = (X, Y, Z)
@@ -541,60 +582,30 @@ class FunktionalesModell:
continue
if aufgeteilt[1] == "SD" or aufgeteilt[1] == "R" or aufgeteilt[1] == "ZW":
#beobachtungen_ID = aufgeteilt[0]
beobachtungsart = aufgeteilt[1] # "SD", "R", "ZW"
beobachtungsgruppeID = aufgeteilt[2]
standpunkt = str(aufgeteilt[3]).strip()
zielpunkt = str(aufgeteilt[4]).strip()
# Hinzufügen der Symbole zum dict_punkt_symbole als Vorbereitung auf die spätere Substitution der Symbole durch numerische Werte
X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]
# Aufstellen der Symbolischen Gleichungen
dX = X_zp - X_sp
dY = Y_zp - Y_sp
dZ = Z_zp - Z_sp
s = sp.sqrt(dX ** 2 + dY ** 2 + dZ ** 2) # Schrägstrecke
B_sp = sp.Symbol(f"B{standpunkt}")
L_sp = sp.Symbol(f"L{standpunkt}")
if beobachtungsart == "SD":
s_geom = sp.sqrt(dX ** 2 + dY ** 2 + dZ ** 2)
liste_beobachtungsgleichungen.append(s_geom)
elif beobachtungsart == "R":
#O_sp = sp.Symbol(f"O_{beobachtungsgruppeID}")
r_sp_zp = sp.Symbol(f"richtung_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
# Lokales System: x_loc = Nord, y_loc = Ost
#x_loc = (-sp.sin(B_sp) * sp.cos(L_sp)) * dX + (-sp.sin(B_sp) * sp.sin(L_sp)) * dY + (sp.cos(B_sp)) * dZ
#y_loc = (-sp.sin(L_sp)) * dX + (sp.cos(L_sp)) * dY
#a12 = sp.atan2(y_loc, x_loc)
# Richtung nach Otepka: r = a12 - O
liste_beobachtungsgleichungen.append(r_sp_zp)
elif beobachtungsart == "ZW":
#dX = X_zp - X_sp
#dY = Y_zp - Y_sp
#dZ = Z_zp - Z_sp
#s_geom = sp.sqrt(dX ** 2 + dY ** 2 + dZ ** 2)
#z_loc = (sp.cos(B_sp) * sp.cos(L_sp)) * dX + (sp.cos(B_sp) * sp.sin(L_sp)) * dY + (sp.sin(B_sp)) * dZ
#zw = sp.acos(z_loc / s_geom)
zw_sp_zp = sp.Symbol(f"zw_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
liste_beobachtungsgleichungen.append(zw_sp_zp)
if aufgeteilt[1] == "gnssbx" or aufgeteilt[1] == "gnssby" or aufgeteilt[1] == "gnssbz":
@@ -617,7 +628,6 @@ class FunktionalesModell:
liste_beobachtungsgleichungen.append(dZ)
if aufgeteilt[1] == "niv":
beobachtungsart = aufgeteilt[1]
standpunkt = str(aufgeteilt[2]).strip()
zielpunkt = str(aufgeteilt[3]).strip()
@@ -628,18 +638,36 @@ class FunktionalesModell:
liste_beobachtungsgleichungen.append(niv_sp_zp)
# Finalisieren des Symbolischen Beobachtungsvektors und exportieren als csv-Datei.
beobachtungsvektor_naeherung_symbolisch = sp.Matrix(liste_beobachtungsgleichungen)
Export.matrix_to_csv(r"Zwischenergebnisse\Beobachtungsvektor_Näherung_Symbolisch.csv", [""],
liste_beobachtungsvektor_symbolisch, beobachtungsvektor_naeherung_symbolisch, "Beobachtungsvektor")
return beobachtungsvektor_naeherung_symbolisch
def beobachtungsvektor_naeherung_numerisch_iteration0(self, liste_beobachtungsvektor_symbolisch: list,
def beobachtungsvektor_naeherung_numerisch(self, liste_beobachtungsvektor_symbolisch: list,
beobachtungsvektor_naeherung_symbolisch: sp.Matrix,
iterationsnummer: int = 0) -> ndarray[tuple[int, int], Any]:
#beobachtungsvektor_naeherung_numerisch_iteration0 = beobachtungsvektor_naeherung_symbolisch.xreplace(self.substitutionen_dict)
"""Erstellt einen numerischen Vektor aus dem smbolischen Näherungs-Beobachtungsvektor.
Es wird sympy.lambdify verwendet, um den in beobachtungsvektor_naeherung_symbolisch erzeugten
symbolischen Vektor effizient als Numpy-Array auszuwerten. Die Lambdify-Funktion wird gecached
(self.func_beob0), um Rechenzeit in Iterationen zu sparen.
Der numerische Näherungs-Beobachtungsvektor wird als CSV-Datei im Ordner Zwischenergebnisse exportiert.
:param liste_beobachtungsvektor_symbolisch: Liste symbolischer Beobachtungskennungen (Strings).
:type liste_beobachtungsvektor_symbolisch: list
:param beobachtungsvektor_naeherung_symbolisch: Symbolischer Näherungs-Beobachtungsvektor f(x0).
:type beobachtungsvektor_naeherung_symbolisch: sp.Matrix
:param iterationsnummer: Iterationsnummer für Dateinamen der Zwischenergebnisse.
:type iterationsnummer: int
:return: Numerischer Näherungs-Beobachtungsvektor als Numpy-Array (Spaltenvektor).
:rtype: ndarray[tuple[int, int], Any]
"""
# Es wird die Sympy-Methode Lambdify verwendet, um die Symbole aus der vorherigen Methode beobachtungsvektor_naeherung_symbolisch in Numerische Zahlen umzuwandeln.
# Zur Ersparnis von Rechenzeit werden die numerischen Zahlen als Numpy-Werte behandelt.
if self.func_beob0 is None:
#self.liste_symbole_lambdify = sorted(self.substitutionen_dict.keys(), key=lambda s: str(s))
self.func_beob0 = sp.lambdify(
self.liste_symbole_lambdify,
beobachtungsvektor_naeherung_symbolisch,
@@ -648,17 +676,26 @@ class FunktionalesModell:
)
liste_werte = [self.substitutionen_dict[s] for s in self.liste_symbole_lambdify]
#beobachtungsvektor_naeherung_numerisch_iteration0 = sp.Matrix(self.func_beob0(*liste_werte))
beobachtungsvektor_naeherung_numerisch_iteration0 = np.asarray(self.func_beob0(*liste_werte),
beobachtungsvektor_naeherung_numerisch = np.asarray(self.func_beob0(*liste_werte),
dtype=float).reshape(-1, 1)
Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}_Beobachtungsvektor_Näherung_Numerisch_Iteration0.csv", [""],
liste_beobachtungsvektor_symbolisch, beobachtungsvektor_naeherung_numerisch_iteration0,
liste_beobachtungsvektor_symbolisch, beobachtungsvektor_naeherung_numerisch,
"Beobachtungsvektor")
return beobachtungsvektor_naeherung_numerisch_iteration0
return beobachtungsvektor_naeherung_numerisch
def unbekanntenvektor_symbolisch(self, liste_unbekannte: list) -> sp.Matrix:
"""Erstellt den symbolischen Unbekanntenvektor.
Der Unbekanntenvektor wird als SymPy-Matrix aufgebaut und als CSV-Datei in
Zwischenergebnisse\\Unbekanntenvektor_Symbolisch.csv exportiert.
:param liste_unbekannte: Liste der Unbekannten (Symbole).
:type liste_unbekannte: list
:return: Symbolischer Unbekanntenvektor als SymPy-Matrix.
:rtype: sp.Matrix
"""
unbekanntenvektor_symbolisch = sp.Matrix(liste_unbekannte)
Export.matrix_to_csv(r"Zwischenergebnisse\Unbekanntenvektor_Symbolisch.csv", [""], liste_unbekannte, unbekanntenvektor_symbolisch,
"Unbekanntenvektor")
@@ -666,64 +703,82 @@ class FunktionalesModell:
def unbekanntenvektor_numerisch(self, liste_unbekanntenvektor_symbolisch: list,
unbekanntenvektor_symbolisch: sp.Matrix,
dX_Vektor: np.Matrix = None,
unbekanntenvektor_neumerisch_vorherige_Iteration: np.Matrix = None,
dX_Vektor: np.asarray = None,
unbekanntenvektor_numerisch_vorherige_Iteration: np.asarray = None,
iterationsnummer: int = 0) -> ndarray[tuple[int, int], Any] | ndarray[tuple[Any, ...], dtype[Any]]:
"""Erstellt den numerischen Unbekanntenvektor jeder Iteration.
Wenn keine Iterationsfortschreibung übergeben wird (dX_Vektor und unbekanntenvektor_numerisch_vorherige_Iteration sind None),
werden die aktuellen Substitutionen genutzt und daraus der numerische Unbekanntenvektor aufgebaut.
Wenn dX_Vektor und ein Unbekanntenvektor der vorherigen Iteration übergeben werden, wird der neue
Unbekanntenvektor aus der Summe gebildet: x_neu = x_alt + dX.
Anschließend wird self.substitutionen_dict auf Basis des neuen Unbekanntenvektors aktualisiert.
Der numerische Unbekanntenvektor wird als CSV-Datei in den Ordner Zwischenergebnisse exportiert.
:param liste_unbekanntenvektor_symbolisch: Liste der Unbekannten (Symbole) in der Reihenfolge des numerischen Vektors.
:type liste_unbekanntenvektor_symbolisch: list
:param unbekanntenvektor_symbolisch: Symbolischer Unbekanntenvektor.
:type unbekanntenvektor_symbolisch: sp.Matrix
:param dX_Vektor: Verbesserungsvektor der aktuellen Iteration (optional).
:type dX_Vektor: np.asarray | None
:param unbekanntenvektor_numerisch_vorherige_Iteration: Numerischer Unbekanntenvektor der vorherigen Iteration (optional).
:type unbekanntenvektor_numerisch_vorherige_Iteration: np.asarray | None
:param iterationsnummer: Iterationsnummer für Dateinamen der Zwischenergebnisse.
:type iterationsnummer: int
:return: Numerischer Unbekanntenvektor als Numpy-Array
:rtype: ndarray[tuple[int, int], Any] | ndarray[tuple[Any, ...], dtype[Any]]
"""
self.liste_unbekanntenvektor_symbolisch = liste_unbekanntenvektor_symbolisch
#if not hasattr(self, "liste_unbekanntenvektor_symbolisch"):
# self.liste_unbekanntenvektor_symbolisch = liste_unbekanntenvektor_symbolisch
if dX_Vektor is None and unbekanntenvektor_neumerisch_vorherige_Iteration is None:
#unbekanntenvektor_numerisch = unbekanntenvektor_symbolisch.xreplace(self.substitutionen_dict)
#if self.func_u0 is None:
# self.func_u0 = sp.lambdify(
# self.liste_symbole_lambdify,
# unbekanntenvektor_symbolisch,
# modules="numpy",
# cse=True
# )
# Überprüfung, ob dX und der unbekanntenvektor aus der vorhigen Iteration übergeben wurden. Wenn ja, wird daraus der neue unbekanntenvektor nach der Iteration berechnet.
if dX_Vektor is None and unbekanntenvektor_numerisch_vorherige_Iteration is None:
unbekanntenvektor_numerisch = np.asarray(
[[float(self.substitutionen_dict[sym])] for sym in self.liste_unbekanntenvektor_symbolisch],
dtype=float
).reshape(-1, 1)
#liste_werte = [self.substitutionen_dict[s] for s in self.liste_symbole_lambdify]
#unbekanntenvektor_numerisch = sp.Matrix(self.func_u0(*liste_werte))
#unbekanntenvektor_numerisch = np.asarray(self.func_u0(*liste_werte), dtype=float).reshape(-1, 1)
else:
#unbekanntenvektor_numerisch = unbekanntenvektor_neumerisch_vorherige_Iteration + dX_Vektor
unbekanntenvektor_neumerisch_vorherige_Iteration = np.asarray(
unbekanntenvektor_neumerisch_vorherige_Iteration, dtype=float).reshape(-1, 1)
unbekanntenvektor_numerisch_vorherige_Iteration = np.asarray(
unbekanntenvektor_numerisch_vorherige_Iteration, dtype=float).reshape(-1, 1)
dX_Vektor = np.asarray(dX_Vektor, dtype=float).reshape(-1, 1)
unbekanntenvektor_numerisch = unbekanntenvektor_numerisch_vorherige_Iteration + dX_Vektor
unbekanntenvektor_numerisch = unbekanntenvektor_neumerisch_vorherige_Iteration + dX_Vektor
# Aktualisieren des Dictionaries für die Subsitutionen in den anderen Methoden.
self.substitutionen_dict = self.dict_substitutionen_uebergeordnetes_system(unbekanntenvektor_numerisch)
Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}_Unbekanntenvektor_Numerisch_Iteration0.csv", [""],
Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}_Unbekanntenvektor_Numerisch.csv", [""],
liste_unbekanntenvektor_symbolisch, unbekanntenvektor_numerisch,
"Unbekanntenvektor")
return unbekanntenvektor_numerisch
def unbekanntenvektor_numerisch_to_dict_unbekanntenvektor(self, liste_unbekanntenvektor_symbolisch: list, unbekanntenvektor_numerisch: np.Matrix) -> dict:
dict_unbekanntenvektor_numerisch = {}
#index = 0
"""Wandelt einen numerischen Unbekanntenvektor in ein Koordinatendictionary um.
Aus dem numerischen Unbekanntenvektor werden für alle Punkte die Koordinaten (X, Y, Z) extrahiert
und als sp.Matrix([X, Y, Z]) in einem Dictionary gespeichert.
:param liste_unbekanntenvektor_symbolisch: Liste der Unbekannten (Symbole) in der Reihenfolge des numerischen Vektors.
:type liste_unbekanntenvektor_symbolisch: list
:param unbekanntenvektor_numerisch: Numerischer Unbekanntenvektor.
:type unbekanntenvektor_numerisch: np.Matrix
:return: Dictionary {punktnummer: sp.Matrix([X, Y, Z])}.
:rtype: dict
"""
unbekanntenvektor_numerisch = np.asarray(unbekanntenvektor_numerisch, dtype=float).reshape(-1, 1)
idx = {str(sym): i for i, sym in enumerate(liste_unbekanntenvektor_symbolisch)}
punktnummern = []
for sym in liste_unbekanntenvektor_symbolisch:
name = str(sym)
for symbol in liste_unbekanntenvektor_symbolisch:
name = str(symbol)
if name.startswith("X"):
pn = name[1:]
if pn not in punktnummern:
punktnummern.append(pn)
# Annahme: Für jeden Punkt, für den eine X-Koordinate vorliegt, gibt es auch immer zwingend eine Y- und Z-Koordinate
dict_koordinaten = {}
for pn in punktnummern:
iX = idx.get(f"X{pn}", None)
@@ -739,19 +794,33 @@ class FunktionalesModell:
float(unbekanntenvektor_numerisch[iZ, 0]),
])
#dict_unbekanntenvektor_numerisch[punktnummer] = sp.Matrix([
# float(unbekanntenvektor_numerisch[index, 0]),
# float(unbekanntenvektor_numerisch[index + 1, 0]),
# float(unbekanntenvektor_numerisch[index + 2, 0])
#])
#index += 3
return dict_koordinaten
def berechnung_dl(self, beobachtungsvektor_numerisch: np.Matrix, beobachtungsvektor_naeherung_numerisch: sp.Matrix,
liste_beobachtungsvektor_symbolisch: list = None, iterationsnummer: int = 0) -> np.Matrix:
liste_beobachtungsvektor_symbolisch: list = None, iterationsnummer: int = 0) -> np.asarray:
"""Berechnet den Verbesserungsvektor dl = l f(x0).
Der Vektor wird als Differenz aus numerischem Beobachtungsvektor und numerischem Näherungs-Beobachtungsvektor gebildet.
Für Richtungsbeobachtungen wird dl normiert.
Der Vektor dl wird als CSV-Datei in den Ordner Zwischenergebnisse exportiert.
:param beobachtungsvektor_numerisch: Numerischer Beobachtungsvektor l.
:type beobachtungsvektor_numerisch: np.Matrix
:param beobachtungsvektor_naeherung_numerisch: Numerischer Näherungs-Beobachtungsvektor f(x0).
:type beobachtungsvektor_naeherung_numerisch: sp.Matrix
:param liste_beobachtungsvektor_symbolisch: Optional: Liste der Beobachtungskennungen.
:type liste_beobachtungsvektor_symbolisch: list | None
:param iterationsnummer: Iterationsnummer für Dateinamen der Zwischenergebnisse.
:type iterationsnummer: int
:return: Verbesserungsvektor dl.
:rtype: np.asarray
"""
dl = beobachtungsvektor_numerisch - beobachtungsvektor_naeherung_numerisch
# Umwandeln in einen Numpy-Array, um Rechenzeit im Vergleich zu einer sympy.MAtrix zu sparen
dl = np.asarray(dl, dtype=float)
# Wird keine liste_beobachtungsvektor_symbolisch übergeben, wird diese aus der Instanzvariable verwendet.
if liste_beobachtungsvektor_symbolisch is None:
liste_beobachtungsvektor_symbolisch = self.liste_beobachtungsvektor_symbolisch
@@ -770,11 +839,25 @@ class FunktionalesModell:
return dl
def dict_substitutionen_uebergeordnetes_system(self,
unbekanntenvektor_aus_iteration: np.Matrix = None) -> dict[Any, Any]:
db_zugriff = Datenbankzugriff(self.pfad_datenbank)
berechnungen = Berechnungen(self.a, self.b)
unbekanntenvektor_aus_iteration: np.asarray = None) -> dict[Any, Any]:
"""Erstellt das Substitutions-Dictionary für das geozentrisch-kartesische System.
Es werden (abhängig davon, ob ein Unbekanntenvektor aus einer Iteration übergeben wurde) die aktuellen
Koordinaten und daraus abgeleitete Größen erzeugt und als Substitutionen abgelegt, u. a.:
- Punktkoordinaten X*, Y*, Z* sowie geodätische Breite B* und Länge L*,
- berechnete Größen aus Tachymeterbeziehungen (Azimut, Richtung, Zenitwinkel, Schrägstrecke),
- Normalhöhen NH (über Transformationen),
- Beobachtungssymbole der Messungen (Tachymeter, GNSS, Nivellement),
- Orientierungsunbekannte O.
:param unbekanntenvektor_aus_iteration: Optionaler numerischer Unbekanntenvektor einer Iteration zur Aktualisierung der Substitutionen.
:type unbekanntenvektor_aus_iteration: np.asarray | None
:return: Dictionary mit SymPy-Symbolen als Key und numerischen Werten als Value.
:rtype: dict[Any, Any]
"""
if unbekanntenvektor_aus_iteration is None:
dict_koordinaten = db_zugriff.get_koordinaten("naeherung_us")
dict_koordinaten = self.db_zugriff.get_koordinaten("naeherung_us")
else:
dict_koordinaten = self.unbekanntenvektor_numerisch_to_dict_unbekanntenvektor(
self.liste_unbekanntenvektor_symbolisch,
@@ -784,13 +867,15 @@ class FunktionalesModell:
for punktnummer, matrix in dict_koordinaten.items():
dict_koordinaten[punktnummer] = [float(matrix[0]), float(matrix[1]), float(matrix[2])]
# Abfragen der Beobachtungen der einzelnen Beobachtungsarten aus der Tabelle Beobachtungen
liste_beobachtungen_tachymeter = self.db_zugriff.get_beobachtungen_from_beobachtungenid()
liste_beobachtungen_gnssbasislinien = self.db_zugriff.get_beobachtungen_gnssbasislinien()
liste_beobachtungen_nivellemente = self.db_zugriff.get_beobachtungen_nivellement()
liste_azimut_richtungen, dict_orientierungen = self.berechnungen.berechnung_richtung_azimut_zenitwinkel(self.pfad_datenbank, dict_koordinaten)
liste_beobachtungen_tachymeter = db_zugriff.get_beobachtungen_from_beobachtungenid()
liste_beobachtungen_gnssbasislinien = db_zugriff.get_beobachtungen_gnssbasislinien()
liste_beobachtungen_nivellemente = db_zugriff.get_beobachtungen_nivellement()
liste_azimut_richtungen, dict_orientierungen = berechnungen.berechnung_richtung_azimut_zenitwinkel(self.pfad_datenbank, dict_koordinaten)
# Erstellen von Dictionaries für die weitere Verarbeitung
dict_koordinaten_xyz_kopie = {pn: [v[0], v[1], v[2]] for pn, v in dict_koordinaten.items()}
dict_koordinaten_B_L = berechnungen.geometrische_breite_laenge(dict_koordinaten_xyz_kopie)
dict_koordinaten_B_L = self.berechnungen.geodätische_breite_laenge(dict_koordinaten_xyz_kopie)
punktnummern_niv = set()
for beobachtungenID, pn_sp, pn_zp, niv_dh, niv_strecke, niv_anz_standpkte in liste_beobachtungen_nivellemente:
@@ -806,8 +891,8 @@ class FunktionalesModell:
dict_koordinaten_niv,
self.pfad_tif_quasigeoidundolation)
# Zuweisen der Symbole zu dem jeweiligen numerischen Wert. Gespeichert wird dies in einem Dictionary.
substitutionen = {}
for punktnummer, vektor in dict_koordinaten_B_L.items():
X_sym, Y_sym, Z_sym, B_sym, L_Sym = sp.symbols(
f"X{punktnummer} Y{punktnummer} Z{punktnummer} B{punktnummer} L{punktnummer}")
@@ -818,10 +903,6 @@ class FunktionalesModell:
substitutionen[B_sym] = float(vektor[1])
substitutionen[L_Sym] = float(vektor[2])
#for beobachtungsgruppeID, orientierung in dict_orientierungen.items():
# O_sym = sp.symbols(f"O_{beobachtungsgruppeID}")
# substitutionen[O_sym] = float(orientierung)
for beobachtungsgruppeID, standpunkt, zielpunkt, azimut, richtung, zenitwinkel, schraegstrecke, orientierung in liste_azimut_richtungen:
richtung_sym = sp.symbols(f"richtung_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
substitutionen[richtung_sym] = float(richtung)
@@ -839,8 +920,6 @@ class FunktionalesModell:
normalhoehe_sym = sp.symbols(f"NH{punktnummer}")
substitutionen[normalhoehe_sym] = float(koordinaten_utm[2])
for standpunkt, zielpunkt, beobachtungenID, beobachtungsgruppeID, tachymeter_richtung, tachymeter_zenitwinkel, tachymeter_distanz in liste_beobachtungen_tachymeter:
alpha = sp.symbols(f"{beobachtungenID}_R_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
zw = sp.symbols(f"{beobachtungenID}_ZW_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
@@ -852,7 +931,6 @@ class FunktionalesModell:
substitutionen[alpha] = float(tachymeter_richtung)
substitutionen[zw] = float(tachymeter_zenitwinkel)
substitutionen[s] = float(tachymeter_distanz)
#substitutionen[sp.Symbol(f"O{beobachtungsgruppeID}")] = 0.0
for beobachtungenID, punktnummer_sp, punktnummer_zp, gnss_bx, gnss_by, gnss_bz, gnss_s0, gnss_cxx, gnss_cxy, gnss_cxz, gnss_cyy, gnss_cyz, gnss_czz in liste_beobachtungen_gnssbasislinien:
beobachtungenID = str(beobachtungenID).strip()
@@ -890,10 +968,6 @@ class FunktionalesModell:
for orientierungs_id, wert in dict_O.items():
substitutionen[sp.Symbol(f"O{orientierungs_id}")] = float(wert)
else:
#for standpunkt, zielpunkt, beobachtungenID, beobachtungsgruppeID, *_ in liste_beobachtungen_tachymeter:
# O_sym = sp.Symbol(f"O{beobachtungsgruppeID}")
# if O_sym not in substitutionen:
# substitutionen[O_sym] = 0
for beobachtungsgruppeID, standpunkt, zielpunkt, azimut, richtung, zenitwinkel, schraegstrecke, orientierung in liste_azimut_richtungen:
O_sym = sp.Symbol(f"O{beobachtungsgruppeID}")
if O_sym not in substitutionen:
@@ -902,7 +976,18 @@ class FunktionalesModell:
return substitutionen
def unbekanntenvektor_numerisch_to_dict_orientierungen(self, liste_unbekanntenvektor_symbolisch: list,
unbekanntenvektor_numerisch: np.Matrix) -> dict[Any, Any]:
unbekanntenvektor_numerisch: np.asarray) -> dict[Any, Any]:
"""Extrahiert Orientierungsparameter aus einem numerischen Unbekanntenvektor in ein Dictionary.
Alle Unbekannten, deren Symbolname mit "O" beginnt, werden als als Dictionary in der Form {orientierungs_id: wert} zurückgegeben.
:param liste_unbekanntenvektor_symbolisch: Liste der Unbekannten (Symbole) in der Reihenfolge des numerischen Vektors.
:type liste_unbekanntenvektor_symbolisch: list
:param unbekanntenvektor_numerisch: Numerischer Unbekanntenvektor.
:type unbekanntenvektor_numerisch: np.asarray
:return: Dictionary der Orientierungen je Beobachtungsgruppe.
:rtype: dict[Any, Any]
"""
dict_O = {}
unbekanntenvektor_numerisch = np.asarray(unbekanntenvektor_numerisch, dtype=float).reshape(-1, 1)
for i, symbol in enumerate(liste_unbekanntenvektor_symbolisch):
@@ -912,4 +997,3 @@ class FunktionalesModell:
dict_O[orientierungs_id] = float(unbekanntenvektor_numerisch[i, 0])
return dict_O

461
Import.py
View File

@@ -1,37 +1,95 @@
import csv
import sqlite3
from decimal import Decimal
from typing import Any
import re
import xml.etree.ElementTree as ET
from decimal import Decimal, getcontext, ROUND_HALF_UP
import sqlite3
from typing import Any
import xml.etree.ElementTree as ET
from Berechnungen import Berechnungen
import Berechnungen
class Import:
def __init__(self, pfad_datenbank: str) -> None:
"""Importfunktionen für Messdaten und Näherungswerte in die SQLite-Datenbank.
Die Klasse stellt Methoden zur Verfügung für:
- Import von Näherungskoordinaten (LH-Tachymeter, GNSS/ECEF) in die Tabelle Netzpunkte,
- Import und Vorverarbeitung von Tachymeterbeobachtungen (CSV-Datei) inkl. optionaler Korrektur durch nicht gerundete Leica-JXL,
- Import von GNSS-Basislinien inkl. Kovarianzanteilen in die Tabelle Beobachtungen,
- Import von Nivellementdaten (Normalhöhen als Näherungen sowie RVVR-Züge als dh-Beobachtungen),
- einfache Plausibilitätsprüfungen (Duplikate, InstrumentenID, Dateiname bereits importiert).
"""
def __init__(self, pfad_datenbank: str, a : float, b : float) -> None:
"""Initialisiert die Importklasse.
Speichert den Pfad zur SQLite-Datenbank und initialisiert die Hilfsklasse Berechnungen.
:param a: Große Halbachse a des Referenzellipsoids in Meter.
:type a: float
:param b: Kleine Halbachse b des Referenzellipsoids in Meter.
:type b: float
:param pfad_datenbank: Pfad zur SQLite-Datenbank.
:type pfad_datenbank: str
:return: None
:rtype: None
"""
self.a = a
self.b = b
self.pfad_datenbank = pfad_datenbank
pass
self.berechnungen = Berechnungen.Berechnungen(self.a, self.b)
def string_to_float(self, zahl: str) -> float:
"""Konvertiert einen Zahlenstring in float.
Ersetzt das Dezimaltrennzeichen "," durch "." und führt anschließend float() aus.
:param zahl: Zahlenstring (z. B. "12,345" oder "12.345").
:type zahl: str
:return: Zahlenwert als float.
:rtype: float
"""
zahl = zahl.replace(',', '.')
return float(zahl)
def string_to_decimal(self, zahl: str) -> Decimal:
"""Konvertiert einen Zahlenstring in Decimal.
Ersetzt das Dezimaltrennzeichen "," durch "." und erzeugt anschließend ein Decimal.
:param zahl: Zahlenstring (z. B. "12,345" oder "12.345").
:type zahl: str
:return: Konvertierter Decimal-Wert.
:rtype: Decimal
"""
zahl = zahl.replace(',', '.')
return Decimal(zahl)
def import_koordinaten_lh_tachymeter(self, pfad_datei: str) -> None:
"""Importiert Näherungskoordinaten im lokalen Horizontsystem des Tachymeters in die Tabelle Netzpunkte.
Die Datei wird semikolon-separiert erwartet. Pro Zeile werden Punktnummer sowie X/Y/Z-Näherung
ausgelesen und in Netzpunkte (naeherungx_lh, naeherungy_lh, naeherungz_lh) geschrieben.
Vor dem Import werden zwei zentrale Prüfungen durchgeführt:
- Punktnummern dürfen in der Importdatei nicht doppelt vorkommen,
- Punktnummern aus der Importdatei dürfen noch nicht in der Tabelle Netzpunkte vorhanden sein.
:param pfad_datei: Pfad zur CSV-Datei mit Punktnummer und Koordinaten.
:type pfad_datei: str
:return: None
:rtype: None
"""
liste_punktnummern = []
liste_punktnummern_vorher = []
liste_punktnummern_vorher_db = []
# Import_abbrechen wird True, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_abbrechen = False
with open (pfad_datei, newline='', encoding='utf-8') as csvfile:
# Abfragen aller in der Tabelle Netzpunkte enthaltenen Punktnummern
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_punktnummern_db = [r[0] for r in cursor.execute("SELECT DISTINCT punktnummer FROM Netzpunkte").fetchall()]
@@ -41,6 +99,7 @@ class Import:
r = csv.reader(csvfile, delimiter=';')
for row in r:
liste_punktnummern.append(row[0])
# Abbruch des Imports und Ausgabe einer Fehlermeldung, wenn ein Punkt doppelt in der Importdatei vorhanden ist.
if row[0] in liste_punktnummern_vorher:
Import_abbrechen = True
print(f"Der Import wurde abgebrochen, weil in der Datei {pfad_datei} Punktnummern doppelt vorhanden sind. Bitte in der Datei ändern und Import wiederholen.")
@@ -48,12 +107,14 @@ class Import:
liste_punktnummern_vorher.append(row[0])
# Abbruch des Imports und Ausgabe einer Fehlermeldung, wenn mindestens eine Punktnummer aus der Importdatei bereits in der Tabelle Netzpunkte vorhanden ist.
if row[0] in liste_punktnummern_db:
Import_abbrechen = True
print(f"Der Import wurde abgebrochen, weil mindestens ein Teil der Punktnummern aus der Datei {pfad_datei} bereits in der Datenbank vorhanden ist. Bitte in der Datei ändern und Import wiederholen.")
break
liste_punktnummern_vorher_db.append(row[0])
# Import durchführen, wenn keine Fehler festgestellt wurden
if Import_abbrechen == False:
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -70,15 +131,30 @@ class Import:
con.close()
print("Der Import der Näherungskoordinaten wurde erfolgreich abgeschlossen")
def ist_rundung_von_jxl(self, wert_csv: str, wert_jxl_voll: str) -> bool:
def ist_rundung_von_jxl(self, wert_csv: str, wert_jxl: str) -> bool:
"""Prüft, ob ein CSV-Wert eine Rundung eines JXL-Wertes (mit mehr Nachkommastellen) ist.
Es wird geprüft, ob:
- beide Werte echte Zahlen sind,
- der CSV-Wert weniger Nachkommastellen als der JXL-Wert besitzt,
- der JXL-Wert auf die Nachkommastellenanzahl des CSV-Wertes gerundet exakt dem CSV-Wert entspricht.
:param wert_csv: Wert aus der CSV-Datei.
:type wert_csv: str
:param wert_jxl: Wert aus der JXL-Datei.
:type wert_jxl: str
:return: True, wenn wert_csv eine Rundung von wert_jxl auf weniger Nachkommastellen darstellt, sonst False.
:rtype: bool
"""
wert_csv = str(wert_csv).strip()
wert_jxl_voll = str(wert_jxl_voll).strip()
wert_jxl = str(wert_jxl).strip()
if ":ZH:" in wert_csv:
wert_csv = wert_csv.split(":ZH:", 1)[0].strip()
if ":ZH:" in wert_jxl_voll:
wert_jxl_voll = wert_jxl_voll.split(":ZH:", 1)[0].strip()
if ":ZH:" in wert_jxl:
wert_jxl = wert_jxl.split(":ZH:", 1)[0].strip()
def ist_zahl(text: str) -> bool:
text = str(text).strip()
@@ -115,9 +191,10 @@ class Import:
if ist_zahl(wert_csv) == False:
return False
if ist_zahl(wert_jxl_voll) == False:
if ist_zahl(wert_jxl) == False:
return False
# Ermittlung der Anzahl Nachkommastellen in CSV-Datei und JXL-Datei
anzahl_nachkommastellen_csv = 0
if "," in wert_csv:
anzahl_nachkommastellen_csv = len(wert_csv.split(",", 1)[1])
@@ -125,16 +202,16 @@ class Import:
anzahl_nachkommastellen_csv = len(wert_csv.split(".", 1)[1])
anzahl_nachkommastellen_jxl = 0
if "," in wert_jxl_voll:
anzahl_nachkommastellen_jxl = len(wert_jxl_voll.split(",", 1)[1])
elif "." in wert_jxl_voll:
anzahl_nachkommastellen_jxl = len(wert_jxl_voll.split(".", 1)[1])
if "," in wert_jxl:
anzahl_nachkommastellen_jxl = len(wert_jxl.split(",", 1)[1])
elif "." in wert_jxl:
anzahl_nachkommastellen_jxl = len(wert_jxl.split(".", 1)[1])
if anzahl_nachkommastellen_csv >= anzahl_nachkommastellen_jxl:
return False
wert_csv_decimal = self.string_to_decimal(wert_csv.replace(".", ","))
wert_jxl_decimal = self.string_to_decimal(wert_jxl_voll.replace(".", ","))
wert_jxl_decimal = self.string_to_decimal(wert_jxl.replace(".", ","))
q = Decimal("1") if anzahl_nachkommastellen_csv == 0 else Decimal("1." + ("0" * anzahl_nachkommastellen_csv))
wert_jxl_gerundet = wert_jxl_decimal.quantize(q, rounding=ROUND_HALF_UP)
@@ -143,6 +220,13 @@ class Import:
return wert_jxl_gerundet == wert_csv_gerundet
def ist_zahl_csv(self, text: str) -> bool:
"""Prüft, ob ein String in der CSV als Zahl interpretiert werden kann.
:param text: Zu prüfender Text.
:type text: str
:return: True, wenn der Text eine Zahl ist, sonst False.
:rtype: bool
"""
text = str(text).strip()
if text == "":
return False
@@ -172,58 +256,92 @@ class Import:
pfad_datei_csv: str,
pfad_datei_jxl: str,
pfad_datei_csv_out: str) -> dict:
Import_fortsetzen = True
getcontext().prec = 70
"""Korrigiert Tachymeterbeobachtungen in einer CSV-Datei über eine JXL-Datei (Leica XML).
Die JXL-Datei wird eingelesen und je StationID eine Sequenz an Beobachtungen (Zielpunktname,
Horizontal-/Vertikalkreisablesung, Schrägdistanz sowie Prismenhöhe) aufgebaut. Die Winkel werden
von Grad nach gon umgerechnet und in der Nachkommastellenauflösung der JXL gerundet.
Anschließend wird die CSV zeilenweise verarbeitet:
- Standpunktzeilen werden der zugehörigen StationID/IH aus der JXL zugeordnet,
- Beobachtungszeilen werden falls der CSV-Wert eine Rundung des JXL-Werts ist durch den JXL-Wert mit voller Nachkommastellenzahl ersetzt,
- Prismenhöhen (ZH) werden aus der JXL übernommen und in der letzten Spalte als ":ZH:" angehängt.
Zusätzlich werden Fehler-Listen erstellt (fehlende IH, fehlende ZH, Standpunkte in CSV aber nicht in JXL). Diese werden dem Benutzer übergeben.
:param pfad_datei_csv: Pfad zur Eingabe-CSV (Tachymeterbeobachtungen).
:type pfad_datei_csv: str
:param pfad_datei_jxl: Pfad zur JXL-Datei.
:type pfad_datei_jxl: str
:param pfad_datei_csv_out: Pfad zur Ausgabe-CSV (korrigierte Datei).
:type pfad_datei_csv_out: str
:return: Ergebnisdictionary mit Status und Diagnosen, u. a.:
- "Import_fortsetzen" (bool),
- "dict_ersetzungen" (dict mit Zählungen für "Hz", "Z", "SD"),
- "liste_zeilen_ohne_IH" (list),
- "liste_zeilen_ohne_ZH" (list),
- "liste_zeilen_standpunkt_nicht_in_jxl" (list),
- "pfad_datei_csv_out" (str).
:rtype: dict
"""
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
getcontext().prec = 70
dict_ersetzungen = {"Hz": 0, "Z": 0, "SD": 0}
# IH = Instrumentenhöhe | ZH = Zielhöhe, bzw. Prismenhöhe
liste_zeilen_ohne_IH = []
liste_zeilen_ohne_ZH = []
liste_zeilen_standpunkt_nicht_in_jxl = []
liste_stationrecords = []
liste_tachymeterstandpunkte = []
dict_stationname_stationrecords = {}
dict_stationname_zaehler = {}
dict_stationname_tachymeterstandpunkte = {}
dict_standpunkte_anzahl = {}
dict_targetID_zu_ZH = {}
dict_stationID_zu_seq = {}
dict_stationnamen = {}
# Vorbereitung jxl-Datei lesen
tree = ET.parse(pfad_datei_jxl)
root = tree.getroot()
# JXL-Datei auslesen
if Import_fortsetzen:
# StationRecords einlesen (Standpunkt, StationID, Instrumentenhöhe)
# Standpunkt, StationID, Instrumentenhöhe aus der JXL-Datei auslesen
for sr in root.iter("StationRecord"):
stationname = (sr.findtext("StationName") or "").strip()
station_id = (sr.attrib.get("ID") or "").strip()
ih = (sr.findtext("TheodoliteHeight") or "").strip()
if stationname != "" and station_id != "":
liste_stationrecords.append((stationname, station_id, ih))
liste_tachymeterstandpunkte.append((stationname, station_id, ih))
dict_stationnamen[stationname] = 1
if stationname not in dict_stationname_stationrecords:
dict_stationname_stationrecords[stationname] = []
dict_stationname_stationrecords[stationname].append((station_id, ih))
if stationname not in dict_stationname_tachymeterstandpunkte:
dict_stationname_tachymeterstandpunkte[stationname] = []
dict_stationname_tachymeterstandpunkte[stationname].append((station_id, ih))
for stationname in dict_stationname_stationrecords.keys():
dict_stationname_zaehler[stationname] = 0
for stationname in dict_stationname_tachymeterstandpunkte.keys():
dict_standpunkte_anzahl[stationname] = 0
# Prismenhöhe auslesen und in Dict speichern
for tr in root.iter("TargetRecord"):
target_id = (tr.attrib.get("ID") or "").strip()
zh = (tr.findtext("TargetHeight") or "").strip()
if target_id != "":
dict_targetID_zu_ZH[target_id] = zh
for tupel in liste_stationrecords:
for tupel in liste_tachymeterstandpunkte:
station_id = tupel[1]
if station_id not in dict_stationID_zu_seq:
dict_stationID_zu_seq[station_id] = []
# Horizontal- und Vertikalkreisablesungen, sowie Schrägdistanzablesungen aus JXL-Datei auslesen
for pr in root.iter("PointRecord"):
station_id = (pr.findtext("StationID") or "").strip()
if station_id == "" or station_id not in dict_stationID_zu_seq:
@@ -267,13 +385,14 @@ class Import:
z_gon_decimal = z_gon_decimal.quantize(q_z, rounding=ROUND_HALF_UP)
sd_decimal = Decimal(sd_m).quantize(q_sd, rounding=ROUND_HALF_UP)
# Ausgabe mit Komma
# Ausgabe mit Kommatrennung
hz_gon_text = format(hz_gon_decimal, "f").replace(".", ",")
z_gon_text = format(z_gon_decimal, "f").replace(".", ",")
sd_text = format(sd_decimal, "f").replace(".", ",")
zh = dict_targetID_zu_ZH.get(target_id, "")
# Dictionary mit den Abfragen für die Weiterverarbeitung füllen.
dict_stationID_zu_seq[station_id].append({
"target": zielpunkt_name,
"hz_gon": hz_gon_text,
@@ -283,7 +402,7 @@ class Import:
})
station_id_aktuell = None
index_seq_aktuell = 0
index_csv_jxl_aktuell = 0
standpunkt_aktuell = None
# CSV-Datei zeilenweise durchgehen und ggf. Werte ersetzen
@@ -304,9 +423,10 @@ class Import:
standpunkt = row[0].strip()
if standpunkt in dict_stationnamen:
zaehler = dict_stationname_zaehler.get(standpunkt, 0)
liste_records = dict_stationname_stationrecords[standpunkt]
zaehler = dict_standpunkte_anzahl.get(standpunkt, 0)
liste_records = dict_stationname_tachymeterstandpunkte[standpunkt]
# Überprüfung, ob in beiden Dateien die selben Tachymeterstandpunkte vorhanden sind
if zaehler >= len(liste_records):
Import_fortsetzen = False
print(
@@ -314,18 +434,20 @@ class Import:
break
station_id, ih = liste_records[zaehler]
dict_stationname_zaehler[standpunkt] = zaehler + 1
dict_standpunkte_anzahl[standpunkt] = zaehler + 1
station_id_aktuell = station_id
index_seq_aktuell = 0
index_csv_jxl_aktuell = 0
standpunkt_aktuell = standpunkt
# Erstellen einer Liste mit allen Zeilennummern aus der JXL, für die keine Instrumentenhöhe vorliegt, damit dies als Fehlermeldung mit Bearbeitungshinweisen ausgegeben werden kann.
if ih is None or str(ih).strip() == "":
liste_zeilen_ohne_IH.append((nummer_zeile, standpunkt))
writer.writerow([standpunkt, f"IH:{ih}", "", "", ""])
continue
# Erstellen einer Liste mit allen Zeilennummern aus der JXL, für die keine Prismenhöhe vorliegt, damit dies als Fehlermeldung mit Bearbeitungshinweisen ausgegeben werden kann.
if standpunkt.isdigit():
liste_zeilen_standpunkt_nicht_in_jxl.append((nummer_zeile, standpunkt))
@@ -338,8 +460,6 @@ class Import:
wert_z = row[2].split(":ZH:", 1)[0].strip()
wert_sd = row[3].split(":ZH:", 1)[0].strip()
if self.ist_zahl_csv(wert_hz) and self.ist_zahl_csv(wert_z) and self.ist_zahl_csv(wert_sd):
ist_beobachtung = True
@@ -355,21 +475,23 @@ class Import:
writer.writerow(row)
continue
if index_seq_aktuell >= len(liste_seq):
if index_csv_jxl_aktuell >= len(liste_seq):
writer.writerow(row)
continue
jxl_eintrag = liste_seq[index_seq_aktuell]
index_neu = index_seq_aktuell + 1
# Abfragen der selben Daten aus der JXL-Datei über den index
jxl_eintrag = liste_seq[index_csv_jxl_aktuell]
index_neu = index_csv_jxl_aktuell + 1
if jxl_eintrag["target"] != zielpunkt:
index_ende = min(len(liste_seq), index_seq_aktuell + 200)
index_ende = min(len(liste_seq), index_csv_jxl_aktuell + 200)
liste_kandidaten = []
for index_kandidat in range(index_seq_aktuell, index_ende):
for index_kandidat in range(index_csv_jxl_aktuell, index_ende):
if liste_seq[index_kandidat]["target"] == zielpunkt:
liste_kandidaten.append((index_kandidat, liste_seq[index_kandidat]))
# Überprüfung, ob die Ablesungen in der CSV-Datei wirklich Rundungen der selben Daten mit mehr Nachkommastellen aus der JXL-Datei sind und abspeichern der Prüfergebnisse.
if liste_kandidaten != []:
if len(liste_kandidaten) == 1:
index_kandidat, kandidat = liste_kandidaten[0]
@@ -392,9 +514,9 @@ class Import:
jxl_eintrag = kandidat_best
index_neu = index_best + 1
index_seq_aktuell = index_neu
index_csv_jxl_aktuell = index_neu
# Nur ersetzen, wenn die CSV-Werte tatsächlich eine Rundung der JXL-Werte sind
# Nur in der CSV-Datei ersetzen, wenn die CSV-Werte tatsächlich eine Rundung der JXL-Werte sind
hz_out = hz_csv
z_out = z_csv
sd_out = sd_csv
@@ -423,24 +545,27 @@ class Import:
if Import_fortsetzen:
print(f"Korrektur erfolgreich abgeschlossen. Ausgabe: {pfad_datei_csv_out}")
print(f"Ersetzungen (Rundung -> JXL volle Nachkommastellen): {dict_ersetzungen}")
print(f"Ersetzungen in der CSV-Datei (Rundung -> JXL volle Nachkommastellen): {dict_ersetzungen}")
print("\n--- Fehlende IH ---")
print(f"Anzahl: {len(liste_zeilen_ohne_IH)}")
# Ausgabe der Zeilennummern in der JXL-Datei ohne Instrumentenhöhe
if len(liste_zeilen_ohne_IH) > 0:
print("\n--- Fehlende IH in JXL-Datei ---")
print(f"Anzahl: {len(liste_zeilen_ohne_IH)}")
print(liste_zeilen_ohne_IH)
# Ausgabe der Zeilennummern in der JXL-Datei ohne Prismenhöhe
if len(liste_zeilen_ohne_ZH) > 0:
print("\n--- Fehlende ZH ---")
print(f"Anzahl: {len(liste_zeilen_ohne_ZH)}")
if len(liste_zeilen_ohne_ZH) > 0:
print(liste_zeilen_ohne_ZH)
print("\n--- Standpunkt in CSV, aber kein StationRecord in JXL ---")
print(f"Anzahl: {len(liste_zeilen_standpunkt_nicht_in_jxl)}")
# Ausgabe der Zeilennummern in der JXL-Datei mit unterschiedlichen Tachymeterstandpunkten im Vergleich zur CSV-Datei
if len(liste_zeilen_standpunkt_nicht_in_jxl) > 0:
print("\n--- Standpunkt in CSV-Datei, aber nicht in JXL-Datei---")
print(f"Anzahl: {len(liste_zeilen_standpunkt_nicht_in_jxl)}")
print(liste_zeilen_standpunkt_nicht_in_jxl)
else:
print("Die Korrektur wurde abgebrochen.")
print("Die Korrektur der CSV-Datei wurde abgebrochen.")
return {
"Import_fortsetzen": Import_fortsetzen,
@@ -451,8 +576,32 @@ class Import:
"pfad_datei_csv_out": pfad_datei_csv_out
}
def import_beobachtungen_tachymeter(self, pfad_datei: str, instrumentenID: int, a: float, b: float) -> None:
berechnungen = Berechnungen.Berechnungen(a, b)
def import_beobachtungen_tachymeter(self, pfad_datei: str, instrumentenID: int) -> None:
"""Importiert Tachymeterbeobachtungen aus einer CSV-Datei in die Tabelle Beobachtungen.
Die Datei wird blockweise verarbeitet (Standpunktzeile + Beobachtungszeilen). Aus je zwei Halbsätzen
wird ein Vollsatz abgeleitet:
- Richtung: Mittel aus Hz(HS1) und Hz(HS2-200 gon), normiert in [0, 400),
- Zenitwinkel: (ZW1 - ZW2 + 400) / 2,
- Schrägdistanz: Mittel der beiden SD.
Zusätzlich werden aus Instrumenten- und Prismenhöhe bodenbezogene Werte berechnet
(schraegdistanz_bodenbezogen, zenitwinkel_bodenbezogen) über berechne_zenitwinkel_distanz_bodenbezogen().
Vorabprüfungen:
- Abbruch, wenn der Dateiname bereits in Beobachtungen vorkommt,
- Abbruch, wenn instrumentenID nicht in Tabelle Instrumente existiert,
- Plausibilitätsprüfung auf Vollsatz-Struktur (6 Zeilen pro Zielpunkt: 3 Vollsätze * 2 Halbsätze).
:param pfad_datei: Pfad zur CSV-Datei.
:type pfad_datei: str
:param instrumentenID: ID des verwendeten Instruments aus Tabelle Instrumente.
:type instrumentenID: int
:return: None
:rtype: None
"""
# Prüfen, ob Bereits Daten aus der Datei in der Datenbank vorhanden sind
con = sqlite3.connect(self.pfad_datenbank)
@@ -464,16 +613,19 @@ class Import:
FROM Beobachtungen""").fetchall()]
liste_instrumentenid = [r[0] for r in cursor.execute("SELECT instrumenteID FROM Instrumente").fetchall()]
cursor.close()
con.close()
cursor.close
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
# Abbrechen des Imports, wenn bereits Daten aus der selben Datei importiert wurden.
if pfad_datei in liste_dateinamen_in_db:
Import_fortsetzen = False
if Import_fortsetzen:
nummer_zielpunkt = 0
# Abfragen der aktuell höschten Nummer im Attribut beobachtungsgruppeID der Tabelle Beobachtungen
try:
nummer_beobachtungsgruppeID = max(liste_beobachtungsgruppeID)
except:
@@ -487,20 +639,22 @@ class Import:
liste_beobachtungen_vorbereitung = []
for i, zeile in enumerate(f):
# Die ersten drei Zeilen der Importdatei müssen überprungen werden
if i < 3:
continue
zeile = zeile.strip().split(";")
if len(zeile) < 5:
zeile = zeile + [""] * (5 - len(zeile))
# Tachymeterstandpunkt speichern und beobachtungsgruppeID als ID für das einmalige Aufbauen eines Tachymeters auf einem Punkt festlegen
if zeile[2] == "" and zeile[3] == "" and zeile[4] == "":
nummer_beobachtungsgruppeID += 1
# print("Standpunkt: ",nummer_beobachtungsgruppeID ,zeile[0])
standpunkt = zeile[0]
instrumentenhoehe = zeile[1]
if instrumentenhoehe.startswith("IH:"):
instrumentenhoehe = instrumentenhoehe.split("IH:", 1)[1].strip()
# Überprüfung, ob für jeden Beobachteten Zielpunkt 6 Zeilen vorhanden sind (3 Vollszätze mit je 2 Halbsätzen)
if nummer_zielpunkt % 6 != 0:
liste_fehlerhafte_zeile.append(i)
@@ -509,6 +663,8 @@ class Import:
liste_zielpunkte_vs2 = []
liste_zielpunkte_vs3 = []
else:
# ZH = Prismenhöhe | VS = Vollsatz | HS = Halbsatz
# Erstellen einer Liste mit Standpunkt, Vollsatz und Halbsatzzuordnung für die Beobachtungen für die Weiterverarbeitung
nummer_zielpunkt += 1
if ":ZH:" in zeile[3]:
@@ -519,17 +675,14 @@ class Import:
if zeile[0] not in liste_zielpunkte_hs:
liste_zielpunkte_hs.append(zeile[0])
if zeile[0] in liste_zielpunkte_vs3:
# print(f"{nummer_zielpunkt} VS3 HS1 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS3", "HS1", standpunkt, zeile[0], zeile[1],
zeile[2], zeile[3], zeile[4], instrumentenhoehe])
elif zeile[0] in liste_zielpunkte_vs2:
# print(f"{nummer_zielpunkt} VS2 HS1 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS2", "HS1", standpunkt, zeile[0], zeile[1],
zeile[2], zeile[3], zeile[4], instrumentenhoehe])
else:
# print(f"{nummer_zielpunkt} VS1 HS1 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS1", "HS1", standpunkt, zeile[0], zeile[1],
zeile[2],
@@ -538,7 +691,6 @@ class Import:
else:
liste_zielpunkte_hs.remove(zeile[0])
if zeile[0] in liste_zielpunkte_vs3:
# print(f"{nummer_zielpunkt} VS3 HS2 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS3", "HS2", standpunkt, zeile[0], zeile[1],
zeile[2],
@@ -547,7 +699,6 @@ class Import:
elif zeile[0] in liste_zielpunkte_vs2:
if zeile[0] not in liste_zielpunkte_vs3:
liste_zielpunkte_vs3.append(zeile[0])
# print(f"{nummer_zielpunkt} VS2 HS2 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS2", "HS2", standpunkt, zeile[0], zeile[1],
zeile[2],
@@ -555,18 +706,19 @@ class Import:
else:
if zeile[0] not in liste_zielpunkte_vs2:
liste_zielpunkte_vs2.append(zeile[0])
# print(f"{nummer_zielpunkt} VS1 HS2 {zeile}")
liste_beobachtungen_vorbereitung.append(
[nummer_beobachtungsgruppeID, "VS1", "HS2", standpunkt, zeile[0], zeile[1],
zeile[2],
zeile[3], zeile[4], instrumentenhoehe])
if liste_fehlerhafte_zeile == []:
# print(f"Einlesen der Datei {pfad_datei} erfolgreich beendet.")
pass
else:
# Ausgabe, welche Zeilen in der Importdatei bearbeitet werden müssen.
if liste_fehlerhafte_zeile != []:
fehler_zeilen = ", ".join(map(str, liste_fehlerhafte_zeile))
print(
f"Das Einlesen der Datei {pfad_datei} wurde abgebrochen.\nBitte bearbeiten Sie die Zeilen rund um: {", ".join(map(str, liste_fehlerhafte_zeile))} in der csv-Datei und wiederholen Sie den Import.")
f"Das Einlesen der Datei {pfad_datei} wurde abgebrochen.\n"
f"Bitte bearbeiten Sie die Zeilen rund um: {fehler_zeilen} in der csv-Datei "
f"und wiederholen Sie den Import."
)
Import_fortsetzen = False
else:
@@ -579,13 +731,12 @@ class Import:
while len(liste_beobachtungen_vorbereitung) > 0:
liste_aktueller_zielpunkt = liste_beobachtungen_vorbereitung[0]
aktueller_zielpunkt = liste_aktueller_zielpunkt[4]
# print(liste_beobachtungen_vorbereitung[0])
for index in range(1, len(liste_beobachtungen_vorbereitung)):
liste = liste_beobachtungen_vorbereitung[index]
# Berechnen der zu importierenden Beobachtungen. (Jeweils reduziert auf den Vollsatz)
if liste[4] == aktueller_zielpunkt:
# print(liste)
richtung1 = self.string_to_decimal(liste_aktueller_zielpunkt[5])
richtung2 = self.string_to_decimal(liste[5]) - Decimal(200)
zenitwinkel_vollsatz_gon = (self.string_to_decimal(liste_aktueller_zielpunkt[6]) - self.string_to_decimal(
@@ -612,12 +763,8 @@ class Import:
Import_fortsetzen = False
print(f"Der Import wurde abgebrochen, weil für zwei Halbsätze vom Standpunkt {liste_aktueller_zielpunkt[3]} zum Zielpunkt {aktueller_zielpunkt} unterschiedliche Instrumentenhöhen vorliegen. Bitte in der Datei {pfad_datei} korrigieren und Import neustarten.")
# print(richtung_vollsatz)
# print(zenitwinkel_vollsatz)
# print(distanz_vollsatz)
schraegdistanz_bodenbezogen, zenitwinkel_bodenbezogen = berechnungen.berechne_zenitwinkel_distanz_bodenbezogen(
# Umrechnen der Zenitwinkel und Schrägdistanzen auf den Boden unter Verwendung der Instrumenten- und Prismenhöhen
schraegdistanz_bodenbezogen, zenitwinkel_bodenbezogen = self.berechnungen.berechne_zenitwinkel_distanz_bodenbezogen(
float(zenitwinkel_vollsatz_rad), float(distanz_vollsatz), float(instrumentenhoehe_import), float(prismenhoehe))
liste_beobachtungen_import.append(
@@ -627,12 +774,13 @@ class Import:
del liste_beobachtungen_vorbereitung[0]
break
# Überprüfung, ob das Instrument bereits vom Benutzer angelegt wurde.
if instrumentenID not in liste_instrumentenid:
Import_fortsetzen = False
print(
"Der Import wurde abgebrochen. Bitte eine gültige InstrumentenID eingeben. Bei Bedarf ist das Instrument neu anzulegen.")
# Berechnete bodenbezogene Beobachtungen, welche jeweils auf den Vollsatz reduziert sind, in die Tabelle Beobachtungen importieren.
if Import_fortsetzen:
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -649,28 +797,47 @@ class Import:
def vorbereitung_import_beobachtungen_nivellement_naeherung_punkthoehen(self, pfad_datei: str,
instrumentenID: int) -> None | tuple[None, None] | tuple[dict[Any, Any], list[Any]]:
# Prüfen, ob Bereits Daten aus der Datei in der Datenbank vorhanden sind
"""Bereitet den Import von Nivellementdaten zur Ableitung von Näherungs-Normalhöhen vor.
Aus einer semikolon-separierten Datei werden Zeilen erkannt, die berechnete Zielweiten/Punkthöhen enthalten,
und daraus pro Punktnummer alle gefundenen Z-Werte gesammelt. Anschließend wird je Punktnummer
der Mittelwert berechnet (Rundung auf 6 Nachkommastellen).
Danach wird geprüft, welche dieser Punktnummern bereits in der Tabelle Netzpunkte existieren.
Abbruchbedingungen:
- Dateiname bereits in Beobachtungen vorhanden,
- instrumentenID nicht in Instrumente vorhanden.
:param pfad_datei: Pfad zur Importdatei.
:type pfad_datei: str
:param instrumentenID: ID des verwendeten Niv-Instruments aus der Tabelle Instrumente.
:type instrumentenID: int
:return: (dict_punkt_mittelwert_punkthoehen, liste_punktnummern_in_db) oder (None, None) bei Abbruch.
:rtype: None | tuple[None, None] | tuple[dict[Any, Any], list[Any]]
"""
# Prüfen, ob bereits Daten aus der Datei in der Datenbank vorhanden sind
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
liste_dateinamen_in_db = [r[0] for r in cursor.execute(
"SELECT DISTINCT dateiname FROM Beobachtungen"
).fetchall()]
liste_beobachtungsgruppeID = [r[0] for r in cursor.execute("""SELECT DISTINCT beobachtungsgruppeID
FROM Beobachtungen""").fetchall()]
liste_instrumentenid = [r[0] for r in cursor.execute("SELECT instrumenteID FROM Instrumente").fetchall()]
liste_netzpunkte = [r[0] for r in cursor.execute("SELECT punktnummer FROM Netzpunkte").fetchall()]
cursor.close()
con.close()
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
# Abbruch des Imports, wenn bereits Daten aus der Datei importiert wurden.
if pfad_datei in liste_dateinamen_in_db:
Import_fortsetzen = False
print(f"Der Import wurde abgebrochen, weil die Beobachtungen aus der Datei {pfad_datei} bereits in der Datenbank vorhanden sind.")
return None, None
# Abbruch, wenn das Instrument noch nicht vom Benutzer angelegt wurde.
if instrumentenID not in liste_instrumentenid:
Import_fortsetzen = False
print(
@@ -678,7 +845,7 @@ class Import:
return None, None
if Import_fortsetzen:
# Berechnete Punkthöhe Importieren
# Messwerte aus der Importdatei abfragen
muster_berechnete_zielweiten = "| | |Z "
dict_punkt_alle_punkthoehen = {}
dict_punkt_mittelwert_punkthoehen = {}
@@ -702,7 +869,6 @@ class Import:
wert_z = self.string_to_float(teil.split()[0])
if punktnummer is not None and wert_z is not None:
#print(f"{punktnummer}, {float(wert_z)}")
if punktnummer not in dict_punkt_alle_punkthoehen:
dict_punkt_alle_punkthoehen[punktnummer] = []
@@ -711,6 +877,7 @@ class Import:
# Hier wird auf 6 Nachkommastellen gerundet!
dict_punkt_mittelwert_punkthoehen[punktnummer] = round(sum(liste_z) / len(liste_z),6)
# Erstellen einer Liste mit allen Punktnummern, die in der Datenbank vorliegen und für die Nivellementbeobachtungen vorliegen.
if Import_fortsetzen:
# Ausgabe, welche Niv-Punkte bereits in der Tabelle Netzpunkte enthalten sind
liste_punktnummern_nivellement = dict_punkt_mittelwert_punkthoehen.keys()
@@ -722,6 +889,7 @@ class Import:
else:
liste_punktnummern_nicht_in_db.append(punktnummer)
# Es werden nur Höhendifferenzen für Punkte berechnet, für die Näherungskoordinaten in der Datenbank vorliegen.
if Import_fortsetzen:
print(f"Für folgende Nivellementpunkte werden die Höhen in der Ausgleichung berechnet: {liste_punktnummern_in_db}\nFür folgende Punkte wird aktuell keine Höhe in der Ausgleichung berechnet: {liste_punktnummern_nicht_in_db}. Bei Bedarf im folgenden Schritt ändern!")
return dict_punkt_mittelwert_punkthoehen, liste_punktnummern_in_db
@@ -729,6 +897,21 @@ class Import:
def import_beobachtungen_nivellement_naeherung_punkthoehen(self, dict_punkt_mittelwert_punkthoehen: dict,
liste_punktnummern_in_db: list,
liste_punktnummern_hinzufuegen: list) -> str | None:
"""Importiert Näherungsnormalhöhen in die Tabelle Netzpunkte.
Es werden Listen geführt für neu hinzugefügte, bereits vorhandene und geänderte Punkte und
abschließend als Konsolen-Ausgabe ausgegeben.
:param dict_punkt_mittelwert_punkthoehen: Dictionary {punktnummer: normalhoehe} als Mittelwerte.
:type dict_punkt_mittelwert_punkthoehen: dict
:param liste_punktnummern_in_db: Punktnummern, die bereits in Netzpunkte existieren.
:type liste_punktnummern_in_db: list
:param liste_punktnummern_hinzufuegen: Zusätzliche Punktnummern, die ebenfalls übernommen werden sollen.
:type liste_punktnummern_hinzufuegen: list
:return: Status-String mit der Liste der Punkte, für die Normalhöhen in der Ausgleichung verfügbar sind, oder None bei Abbruch.
:rtype: str | None
"""
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
if dict_punkt_mittelwert_punkthoehen == None or liste_punktnummern_in_db == None or liste_punktnummern_hinzufuegen == None:
@@ -744,6 +927,7 @@ class Import:
liste_punkte_geaendert = []
for punktnummer in liste_punktnummern_hinzufuegen:
# Neu anlegen oder aktualisieren der Normalhöhen in der Tabelle Netzpunkte
try:
cursor.execute(f"INSERT INTO Netzpunkte (punktnummer, normalhoehe_hfp) VALUES (?, ?)", (punktnummer, dict_punkt_mittelwert_punkthoehen[punktnummer]))
liste_punkte_neu_hinzugefuegt.append(punktnummer)
@@ -765,10 +949,31 @@ class Import:
print(f"Bereits vorhanden ({len(liste_punkte_bereits_vorhanden)}): {liste_punkte_bereits_vorhanden}")
print(f"Geändert ({len(liste_punkte_geaendert)}): {liste_punkte_geaendert}\n")
return f"Für folgende Punkte werden die Höhen Ausgeglichen: {liste_punktnummern_hinzufuegen + liste_punktnummern_in_db}"
return f"Für folgende Punkte werden die Höhen Ausgeglichen: {liste_punktnummern_in_db}"
def import_beobachtungen_nivellement_RVVR(self, pfad_datei: str, instrumentenID: int) -> str | None:
"""Importiert geometrische Nivellementbeobachtungen nach dem RVVR Prinzip die Tabelle Beobachtungen.
Es werden Zeilen mit Rück-/Vormessungen erkannt (rvvr: Rück, Vor, Vor, Rück). Pro Block werden
dh und Entfernung berechnet (Rundung jeweils auf 8 Nachkommastellen). Anschließend werden Züge reduziert, indem
Wechselpunkte, die nicht in Netzpunkte vorhanden sind, durch Summation herausgekürzt werden.
Damit werden nur Beobachtungen zwischen Punkten in Netzpunkte (bzw. reduzierten Start-/Zielpunkten)
in Beobachtungen gespeichert.
Abbruchbedingungen:
- Dateiname bereits in Beobachtungen vorhanden,
- instrumentenID nicht in Instrumente vorhanden,
- Anzahl RVVR-Zeilen nicht durch 4 teilbar.
:param pfad_datei: Pfad zur Textdatei.
:type pfad_datei: str
:param instrumentenID: ID des verwendeten Niv-Instruments aus der Tabelle Instrumente.
:type instrumentenID: int
:return: Status-String bei Erfolg oder None bei Abbruch.
:rtype: str | None
"""
# Prüfen, ob Bereits Daten aus der Datei in der Datenbank vorhanden sind
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -781,36 +986,36 @@ class Import:
cursor.close()
con.close()
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
# Import abbrechen, wenn bereits Daten aus der selben Datei importiert wurden
if pfad_datei in liste_dateinamen_in_db:
Import_fortsetzen = False
print(f"Der Import wurde abgebrochen, weil die Beobachtungen aus der Datei {pfad_datei} bereits in der Datenbank vorhanden sind.")
# Import abbrechen, wenn das Instrument noch nicht vom Benutzer angelegt wurde.
if instrumentenID not in liste_instrumentenid:
Import_fortsetzen = False
print(
"Der Import wurde abgebrochen. Bitte eine gültige InstrumentenID eingeben. Bei Bedarf ist das Instrument neu anzulegen.")
if Import_fortsetzen:
# rvvr = Rück, Vor, Vor, Rück
# Berechnen der Höhenunterschiede delta H und Schrägstrecken zwischen Zwei Punkten gemäß der Importdatei aus rvvr.
anzahl_zeilen_rvvr = 0
liste_zeilen_rvvr = []
liste_punktpaare = []
with open(pfad_datei, "r", encoding="utf-8") as f:
for i, zeile in enumerate(f):
if ("Lr" in zeile) or ("Lv" in zeile):
#print(zeile.rstrip())
liste_zeilen_rvvr.append(zeile)
anzahl_zeilen_rvvr += 1
if anzahl_zeilen_rvvr % 4 == 0:
index = 0
while index < len(liste_zeilen_rvvr):
block_4 = liste_zeilen_rvvr[index:index + 4]
liste_punktnummern_block = []
for zeile_block in block_4:
@@ -885,7 +1090,9 @@ class Import:
index += 4
# Berechnen der Höhendifferenzen und schrägstrecken zwischen Punkten in der Tabelle Netzpunkte.
# Somit werden alle Wechselpunkte nicht in der Ausgleichung berücksichtigt.
# Die Berechnung erfolgt durch addition.
liste_beobachtungen_reduziert = []
liste_beobachtungen_bearbeitung = []
zugnummer_vorher = liste_punktpaare[0][0]
@@ -893,15 +1100,12 @@ class Import:
zugnummer = einzelbeobachtung[0]
if zugnummer == zugnummer_vorher:
if einzelbeobachtung[1] in liste_netzpunkte and einzelbeobachtung[2] in liste_netzpunkte:
#print(einzelbeobachtung)
liste_beobachtungen_reduziert.append(einzelbeobachtung + (1,))
elif einzelbeobachtung[1] in liste_netzpunkte and einzelbeobachtung[2] not in liste_netzpunkte:
#print(f"Zielpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
elif einzelbeobachtung[1] not in liste_netzpunkte and einzelbeobachtung[2] in liste_netzpunkte:
#print(f"Startpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
startpunkt = None
zielpunkt = None
@@ -919,24 +1123,20 @@ class Import:
summe_dh += beobachtung_bearbeiten[3]
summe_entfernung += beobachtung_bearbeiten[4]
anzahl_standpunkte += 1
# Achtung:Hier Rundung auf 8 Nachkommastellen!
liste_beobachtungen_reduziert.append(
(zugnummer, startpunkt, zielpunkt, round(summe_dh, 8),
round(summe_entfernung, 8), anzahl_standpunkte))
liste_beobachtungen_bearbeitung = []
else:
#print(f"Startpunkt und Zielpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
else:
#print(f"-----------------------------")
if einzelbeobachtung[1] in liste_netzpunkte and einzelbeobachtung[2] in liste_netzpunkte:
#print(einzelbeobachtung)
liste_beobachtungen_reduziert.append(einzelbeobachtung + (1,))
elif einzelbeobachtung[1] in liste_netzpunkte and einzelbeobachtung[2] not in liste_netzpunkte:
#print(f"Zielpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
elif einzelbeobachtung[1] not in liste_netzpunkte and einzelbeobachtung[2] in liste_netzpunkte:
#print(f"Startpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
startpunkt = None
zielpunkt = None
@@ -954,12 +1154,12 @@ class Import:
summe_dh += beobachtung_bearbeiten[3]
summe_entfernung += beobachtung_bearbeiten[4]
anzahl_standpunkte += 1
#Achtung:Hier Rundung auf 8 Nachkommastellen!
liste_beobachtungen_reduziert.append(
(zugnummer, startpunkt, zielpunkt, round(summe_dh,8), round(summe_entfernung,8), anzahl_standpunkte))
liste_beobachtungen_bearbeitung = []
else:
# print(f"Startpunkt und Zielpunkt nicht enthalten {einzelbeobachtung}")
liste_beobachtungen_bearbeitung.append(einzelbeobachtung)
zugnummer_vorher = zugnummer
@@ -970,22 +1170,28 @@ class Import:
con.commit()
cursor.close()
con.close()
return f"Die Beobachtungen aus der Datei {pfad_datei} wurden erfolgreich importiert."
else:
print(f"Anzahl nicht RVVR durch 4 teilbar. Bitte die Datei {pfad_datei} überprüfen! Der Import wurde abgebrochen.")
Import_fortsetzen = False
def import_koordinaten_gnss(self, pfad_datei: str, liste_sapos_stationen_genauigkeiten: list) -> str:
liste_zeilen = []
dict_koordinaten = {}
"""Importiert GNSS-Koordinaten (ECEF) in die Tabelle Netzpunkte.
Die Datei wird semikolon-separiert gelesen und zusätzlich aufgesplittet (Leerzeichen und Kommas).
Für Referenzstationen (Kennzeichnung "Referenz" und Standardabweichungen 0.0000/0.0000/0.0000)
werden die übergebenen Genauigkeiten (X/Y/Z) eingesetzt.
Die Koordinaten und Standardabweichungen werden in Netzpunkte geschrieben.
:param pfad_datei: Pfad zur Koordinatendatei.
:type pfad_datei: str
:param liste_sapos_stationen_genauigkeiten: Liste mit drei Werten [σX, σY, σZ] für Referenzstationen.
:type liste_sapos_stationen_genauigkeiten: list
:return: Statusmeldung zum erfolgreichen Import.
:rtype: str
"""
con = sqlite3.connect(self.pfad_datenbank)
cursor = con.cursor()
@@ -995,7 +1201,6 @@ class Import:
row_neu = []
for eintrag in row:
eintrag = str(eintrag).strip()
eintrag = eintrag.replace("'", "")
aufgeteilt = eintrag.split()
for teil in aufgeteilt:
@@ -1012,14 +1217,33 @@ class Import:
stabw_vorinfo_y = excluded.stabw_vorinfo_y,
stabw_vorinfo_z = excluded.stabw_vorinfo_z""", (row_neu[0], row_neu[4], row_neu[5], row_neu[6], row_neu[7], row_neu[8], row_neu[9])
)
#liste_zeilen.append(row_neu)
con.commit()
con.close()
return "Import der Koordinaten aus stationärem GNSS abgeschlossen."
def import_basislinien_gnss(self, pfad_datei: str, instrumentenID: int) -> None:
"""Importiert GNSS-Basislinien inkl. Kovarianzen in die Tabelle Beobachtungen.
Die Datei wird zeilenweise gelesen und Basislinien werden über Präfixe erkannt:
- @+ : Ziel-/Punktkennung (ID),
- @- : Gegenpunktkennung und Komponenten (bx/by/bz),
- @= : s0 und Kovarianzterme (cxx, cxy, cxz, cyy, cyz, czz).
Vorabprüfungen:
- Abbruch, wenn der Dateiname bereits in Beobachtungen vorkommt,
- Abbruch, wenn instrumentenID nicht in Instrumente existiert.
:param pfad_datei: Pfad zur Basisliniendatei (Text).
:type pfad_datei: str
:param instrumentenID: ID des verwendeten GNSS-Instruments (FK auf Instrumente).
:type instrumentenID: int
:return: None
:rtype: None
"""
# Import_fortsetzen wird False, sobald eine Fehler festgestellt wird. Als Folge wird der Import abgebrochen und eine Fehlermeldung mit Handlungshinweisen für den Benutzer ausgegeben.
Import_fortsetzen = True
# Prüfen, ob Bereits Daten aus der Datei in der Datenbank vorhanden sind
@@ -1033,18 +1257,16 @@ class Import:
if pfad_datei in liste_dateinamen_in_db:
Import_fortsetzen = False
# Import abbrechen, wenn das Instrument noch nicht vom Benutzer angelegt wurde.
liste_instrumentenid = [r[0] for r in cursor.execute("SELECT instrumenteID FROM Instrumente").fetchall()]
cursor.close()
con.close()
cursor.close
if instrumentenID not in liste_instrumentenid:
Import_fortsetzen = False
print(
"Der Import wurde abgebrochen. Bitte eine gültige InstrumentenID eingeben. Bei Bedarf ist das Instrument neu anzulegen.")
if Import_fortsetzen:
liste_basilinien = []
tupel_basislinie = ()
with (open(pfad_datei, "r", encoding="utf-8") as txt):
@@ -1052,17 +1274,13 @@ class Import:
zeile = str(zeile).rstrip("\n").rstrip("\r")
aufgeteilt = zeile.split()
if aufgeteilt[0][:2] == "@+":
#print(aufgeteilt[0][2:])
tupel_basislinie += (aufgeteilt[0][2:],)
if aufgeteilt[0][:2] == "@-":
#print(aufgeteilt[0][2:], aufgeteilt[1], aufgeteilt[2], aufgeteilt[3])
tupel_basislinie += (aufgeteilt[0][2:], aufgeteilt[1], aufgeteilt[2], aufgeteilt[3],)
if aufgeteilt[0][:2] == "@=":
#print(aufgeteilt[1], aufgeteilt[2], aufgeteilt[3], aufgeteilt[4], aufgeteilt[5], aufgeteilt[6], aufgeteilt[7])
tupel_basislinie += (aufgeteilt[1], aufgeteilt[2], aufgeteilt[3], aufgeteilt[4], aufgeteilt[5], aufgeteilt[6], aufgeteilt[7], )
liste_basilinien.append(tupel_basislinie)
tupel_basislinie = ()
#print(liste_basilinien)
else:
print(
@@ -1079,6 +1297,3 @@ class Import:
cursor.close()
con.close()
print(f"Der Import der Datei {pfad_datei} wurde erfolgreich abgeschlossen.")

View File

@@ -13,11 +13,56 @@ from pyproj import CRS, Transformer
class Transformationen:
"""Koordinatentransformationen und Helmert-Transformation (Euler-Winkel) zwischen Referenzsystemen.
Die Klasse stellt Methoden zur Verfügung für:
- Aufbau einer Rotationsmatrix aus Eulerwinkeln,
- Schätzung von 7-Parameter-Transformationsparametern (dX, dY, dZ, Maßstab m, Eulerwinkel e1/e2/e3)
aus identischen Punkten zwischen lokalem Horizontsystem (LH) und geozentrisch-kartesischem System (ECEF),
- Anwendung der geschätzten Helmerttransformation auf Punkte, die nur im Ausgangssystem vorliegen,
- Transformation zwischen ETRS89 / UTM (+ DHHN2016 Normalhöhe) und ETRS89 geozentrisch-kartesisch (ECEF),
inkl. Nutzung einer BKG-Quasigeoidundulations-Datei (GeoTIFF) für PROJ.
Die grundlegende Funktionsweise der Transformationsschätzung lautet:
1) Identische Punkte aus Ausgangs- und Zielsystem ermitteln.
2) Näherung für Maßstab m0 aus mittleren Streckenverhältnissen bilden.
3) Näherungs-Rotation R0 aus lokalen Basen (u/v/w und U/V/W) bestimmen und daraus Euler-Näherungen ableiten.
4) Iterative Parameterschätzung (Gauss-Newton) auf Basis der Beobachtungsgleichung:
P = T + m * R(e1,e2,e3) * p
"""
def __init__(self, pfad_datenbank: str) -> None:
"""Initialisiert die Transformationsklasse.
Speichert den Pfad zur SQLite-Datenbank und initialisiert den Datenbankzugriff.
:param pfad_datenbank: Pfad zur SQLite-Datenbank.
:type pfad_datenbank: str
:return: None
:rtype: None
"""
self.pfad_datenbank = pfad_datenbank
self.db_zugriff = Datenbank.Datenbankzugriff(self.pfad_datenbank)
@staticmethod
def R_matrix_aus_euler(e1: float, e2: float, e3: float) -> sp.Matrix:
def R_matrix_aus_eulerwinkeln(e1: float, e2: float, e3: float) -> sp.Matrix:
"""Erstellt eine 3x3-Rotationsmatrix aus Eulerwinkeln.
Die Rotationsmatrix wird symbolisch (SymPy) aufgebaut. Die Eulerwinkel e1, e2, e3 werden
direkt in trigonometrische Ausdrücke eingesetzt und eine orthogonale Rotationsmatrix R(e1,e2,e3)
zur Verwendung in Helmert-Transformationen zurückgegeben.
:param e1: Eulerwinkel 1 (Radiant).
:type e1: float
:param e2: Eulerwinkel 2 (Radiant).
:type e2: float
:param e3: Eulerwinkel 3 (Radiant).
:type e3: float
:return: Rotationsmatrix R als SymPy-Matrix (3x3).
:rtype: sp.Matrix
"""
return sp.Matrix([
[
sp.cos(e2) * sp.cos(e3),
@@ -36,32 +81,48 @@ class Transformationen:
]
])
def Helmerttransformation_Euler_Transformationsparameter_berechne(self) -> dict[Any, float]:
db = Datenbank.Datenbankzugriff(self.pfad_datenbank)
dict_ausgangssystem = db.get_koordinaten("naeherung_lh", "Dict")
dict_zielsystem = db.get_koordinaten("naeherung_us", "Dict")
def Helmerttransformation_Euler_Transformationsparameter_berechnen(self) -> dict[Any, float]:
"""Schätzt die Helmert-Transformationsparameter aus identischen Punkten.
Aus der Datenbank werden Näherungskoordinaten des lokalen Horizontsystems (naeherung_lh)
und des geozentrisch-kartesischen Systems (naeherung_us) geladen. Für die Schnittmenge der
Punkte werden die 7 Helmertparameter geschätzt:
- Translation (dX, dY, dZ),
- Maßstab m,
- Eulerwinkel (e1, e2, e3).
Näherungen:
- m0: Mittelwert der Streckenverhältnisse aus allen Punktpaaren,
- R0: Anfangsrotationsmatrix aus lokalen Basisvektoren (u/v/w und U/V/W),
- Translation0: aus Schwerpunkten und m0/R0.
Die Parameterschätzung erfolgt iterativ mit P = I.
Abbruchkriterium: |dx_i| < schwellenwert in zwei aufeinanderfolgenden Iterationen oder max. 100 Iterationen.
:param: None
:return: Dictionary der finalen Parameter mit SymPy-Symbolen als Keys und float-Werten als Values
(Keys: dX, dY, dZ, m, e1, e2, e3).
:rtype: dict[Any, float]
"""
# Koordinaten des lokalen Horizontsystems des Tachymeters und der geozentrisch Kartesischen Näherungskoordinaten aus den statischen GNSS-Messungen aus der Tabelle Netzpunkte abfragen
dict_ausgangssystem = self.db_zugriff.get_koordinaten("naeherung_lh", "Dict")
dict_zielsystem = self.db_zugriff.get_koordinaten("naeherung_us", "Dict")
# Identische Punkte ermitteln
gemeinsame_punktnummern = sorted(set(dict_ausgangssystem.keys()) & set(dict_zielsystem.keys()))
anzahl_gemeinsame_punkte = len(gemeinsame_punktnummern)
liste_punkte_ausgangssystem = [dict_ausgangssystem[i] for i in gemeinsame_punktnummern]
liste_punkte_zielsystem = [dict_zielsystem[i] for i in gemeinsame_punktnummern]
print("Anzahl gemeinsame Punkte:", anzahl_gemeinsame_punkte)
print("Anzahl verwendete Punkte für die Helmerttransformation:", anzahl_gemeinsame_punkte)
print("\nErste Zielpunkte:")
for pn, P in list(zip(gemeinsame_punktnummern, liste_punkte_zielsystem))[:5]:
print(pn, [float(P[0]), float(P[1]), float(P[2])])
print("\nErste Ausgangspunkte:")
for pn, p in list(zip(gemeinsame_punktnummern, liste_punkte_ausgangssystem))[:5]:
print(pn, [float(p[0]), float(p[1]), float(p[2])])
# --- Näherungswerte (minimal erweitert) ---
p1, p2, p3 = liste_punkte_ausgangssystem[0], liste_punkte_ausgangssystem[1], liste_punkte_ausgangssystem[2]
P1, P2, P3 = liste_punkte_zielsystem[0], liste_punkte_zielsystem[1], liste_punkte_zielsystem[2]
# 1) Näherungswert Maßstab: Mittelwert aus allen Punktpaaren
# Näherungswert für dem Maßstab berechnen aus dem Mittelwert aller Punktpaare
ratios = []
for i, j in combinations(range(anzahl_gemeinsame_punkte), 2):
dp = (liste_punkte_ausgangssystem[j] - liste_punkte_ausgangssystem[i]).norm()
@@ -72,12 +133,7 @@ class Transformationen:
m0 = sum(ratios) / len(ratios)
if ratios:
print("min/mean/max:",
min(ratios),
sum(ratios) / len(ratios),
max(ratios))
# Näherungswert für die Translation berechnen
U = (P2 - P1) / (P2 - P1).norm()
W = (U.cross(P3 - P1)) / (U.cross(P3 - P1)).norm()
V = W.cross(U)
@@ -93,28 +149,18 @@ class Transformationen:
Translation0 = XS - m0 * R0 * xS
# 2) Test auf orthonormale Drehmatrix bei 3 Nachkommastellen!
if R0.T.applyfunc(lambda x: round(float(x), 3)) == R0.inv().applyfunc(lambda x: round(float(x), 3)) \
and (R0.T * R0).applyfunc(lambda x: round(float(x), 3)) == sp.eye(3).applyfunc(
lambda x: round(float(x), 3)) \
and ((round(R0.det(), 3) == 1.000 or round(R0.det(), 3) == -1.000)):
print("R ist Orthonormal!")
else:
print("R ist nicht Orthonormal!")
# 3) Euler-Näherungswerte aus R0
# Euler-Näherungswerte aus der Anfangsrotationsmatrix
e2_0 = sp.asin(R0[2, 0])
# Schutz gegen Division durch 0 wenn cos(e2) ~ 0:
cos_e2_0 = sp.cos(e2_0)
e1_0 = sp.acos(R0[2, 2] / cos_e2_0)
e3_0 = sp.acos(R0[0, 0] / cos_e2_0)
# --- Symbolische Unbekannte (klassische 7 Parameter) ---
# Symbolische Unbekannte
dX, dY, dZ, m, e1, e2, e3 = sp.symbols('dX dY dZ m e1 e2 e3')
R_symbolisch = self.R_matrix_aus_euler(e1, e2, e3)
R_symbolisch = self.R_matrix_aus_eulerwinkeln(e1, e2, e3)
# 4) Funktionales Modell
# Funktionales Modell
f_zeilen = []
for punkt in liste_punkte_ausgangssystem:
punkt_vektor = sp.Matrix([punkt[0], punkt[1], punkt[2]])
@@ -134,7 +180,7 @@ class Transformationen:
l_vektor = sp.Matrix([koord for P in liste_punkte_zielsystem for koord in P])
l = l_vektor
P_mat = sp.eye(3 * anzahl_gemeinsame_punkte)
P_matrix = sp.eye(3 * anzahl_gemeinsame_punkte)
l_berechnet_0 = None
while True:
@@ -156,8 +202,8 @@ class Transformationen:
dl_0 = l_vektor - l_berechnet_0
A_0 = A_ohne_zahlen.subs(zahlen_0).evalf(n=30)
N = A_0.T * P_mat * A_0
n_0 = A_0.T * P_mat * dl_0
N = A_0.T * P_matrix * A_0
n_0 = A_0.T * P_matrix * dl_0
Qxx_0 = N.inv()
dx = Qxx_0 * n_0
x = x0 + dx
@@ -165,7 +211,6 @@ class Transformationen:
anzahl_iterationen += 1
print(f"Iteration Nr.{anzahl_iterationen} abgeschlossen")
print(dx.evalf(n=3))
else:
zahlen_i = {
@@ -182,16 +227,17 @@ class Transformationen:
dl_i = l_vektor - l_berechnet_i
A_i = A_ohne_zahlen.subs(zahlen_i).evalf(n=30)
N_i = A_i.T * P_mat * A_i
N_i = A_i.T * P_matrix * A_i
Qxx_i = N_i.inv()
n_i = A_i.T * P_mat * dl_i
n_i = A_i.T * P_matrix * dl_i
dx = Qxx_i * n_i
x = sp.Matrix(x + dx)
anzahl_iterationen += 1
print(f"Iteration Nr.{anzahl_iterationen} abgeschlossen")
print(dx.evalf(n=3))
alle_kleiner = True
for i in range(dx.rows):
@@ -204,11 +250,7 @@ class Transformationen:
alle_kleiner_vorherige_iteration = alle_kleiner
print(l.evalf(n=3))
print(l_berechnet_0.evalf(n=3))
print(f"x = {x.evalf(n=3)}")
# --- Neuberechnung Zielsystem ---
# Neuberechnung Zielsystem
zahlen_final = {
dX: float(x[0]),
dY: float(x[1]),
@@ -228,12 +270,11 @@ class Transformationen:
Zi = l_berechnet_final[3 * i + 2]
liste_l_berechnet_final.append(sp.Matrix([Xi, Yi, Zi]))
print("")
print("l_berechnet_final:")
for punktnummer, l_fin in zip(gemeinsame_punktnummern, liste_l_berechnet_final):
print(f"{punktnummer}: {float(l_fin[0]):.3f}, {float(l_fin[1]):.3f}, {float(l_fin[2]):.3f}")
print("Streckendifferenzen:")
print("Streckendifferenzen zwischen Näherungskoordinate aus statischer GNSS-Messung und ergebnis der Helmerttransformation:")
streckendifferenzen = [
(punkt_zielsys - l_final).norm()
for punkt_zielsys, l_final in zip(liste_punkte_zielsystem, liste_l_berechnet_final)
@@ -245,30 +286,51 @@ class Transformationen:
Schwerpunktsdifferenz = Schwerpunkt_Zielsystem - Schwerpunkt_berechnet
print("\nDifferenz Schwerpunkt (Vektor):")
print("\nDifferenz Schwerpunkt zwischen Näherungskoordinate aus statischer GNSS-Messung und ergebnis der Helmerttransformation::")
print(Schwerpunktsdifferenz.evalf(3))
print("Betrag der Schwerpunkt-Differenz:")
print("Betrag der Schwerpunkt-Differenz zwischen Näherungskoordinate aus statischer GNSS-Messung und ergebnis der Helmerttransformation::")
print(f"{float(Schwerpunktsdifferenz.norm()):.3f}m")
return zahlen_final
def Helmerttransformation(self, transformationsparameter: dict) -> dict[Any, Any]:
db = Datenbank.Datenbankzugriff(self.pfad_datenbank)
dict_ausgangssystem = db.get_koordinaten("naeherung_lh", "Dict")
dict_zielsystem = db.get_koordinaten("naeherung_us", "Dict")
"""Wendet eine Helmerttransformation auf Punkte des Ausgangssystems an.
Aus der Datenbank werden Koordinaten des Ausgangssystems (naeherung_lh) und Zielsystems (naeherung_us) geladen.
Transformiert werden genau die Punkte, die:
- im Ausgangssystem vorhanden sind,
- im Zielsystem fehlen (symmetrische Differenz der Punktmengen, anschließend Filter auf Ausgangssystem).
Die Transformation erfolgt gemäß:
P = [dX, dY, dZ]^T + m * R(e1,e2,e3) * p
:param transformationsparameter: Transformationsparameter als Dictionary mit SymPy-Symbolen als Keys
(dX, dY, dZ, m, e1, e2, e3) und numerischen Werten als Values.
:type transformationsparameter: dict
:return: Dictionary {punktnummer: sp.Matrix([X, Y, Z])} der transformierten geozentrisch-kartesischen Koordinaten.
:rtype: dict[Any, Any]
"""
# Koordinaten des lokalen Horizontsystems des Tachymeters und der geozentrisch Kartesischen Näherungskoordinaten aus den statischen GNSS-Messungen aus der Tabelle Netzpunkte abfragen
dict_ausgangssystem = self.db_zugriff.get_koordinaten("naeherung_lh", "Dict")
dict_zielsystem = self.db_zugriff.get_koordinaten("naeherung_us", "Dict")
# Symbole definieren
dX, dY, dZ, m, e1, e2, e3 = sp.symbols('dX dY dZ m e1 e2 e3')
# Unterschiedliche Punkte zwischen Ausgangs- und Zielsystem ermitteln
unterschiedliche_punktnummern = sorted(set(dict_ausgangssystem.keys()) ^ set(dict_zielsystem.keys()))
punktnummern_transformieren = [
punktnummer for punktnummer in unterschiedliche_punktnummern if punktnummer in dict_ausgangssystem
]
liste_punkte_ausgangssystem = [dict_ausgangssystem[punktnummer] for punktnummer in punktnummern_transformieren]
R = self.R_matrix_aus_euler(transformationsparameter[e1], transformationsparameter[e2], transformationsparameter[e3])
# Rotationsmatrix aufstellen
R = self.R_matrix_aus_eulerwinkeln(transformationsparameter[e1], transformationsparameter[e2], transformationsparameter[e3])
f_zeilen = []
# Helmertransformation durchführen und Koordinaten speichern
for punkt in liste_punkte_ausgangssystem:
punkt_vektor = sp.Matrix([punkt[0], punkt[1], punkt[2]])
f_zeile_i = sp.Matrix([transformationsparameter[dX], transformationsparameter[dY], transformationsparameter[dZ]]) + transformationsparameter[m] * R * punkt_vektor
@@ -289,9 +351,28 @@ class Transformationen:
return dict_transformiert
def utm_to_XYZ(self, pfad_tif_quasigeoidundolation: str, liste_utm: list) -> dict[Any, Any]:
"""Rechnet UTM-Koordinaten (ETRS89 / UTM + DHHN2016) in ECEF-Koordinaten (ETRS89 geozentrisch-kartesisch) um.
Es wird ein PROJ-Transformer von:
- Quelle: EPSG:25832 + EPSG:7837 (ETRS89 / UTM Zone 32N + DHHN2016 Normalhöhe),
- Ziel: EPSG:4936 (ETRS89 geozentrisch-kartesisch)
initialisiert. Zusätzlich wird ein BKG-GeoTIFF (Quasigeoidunndulation) in den PROJ-Datenpfad eingebunden,
indem eine Kopie mit dem erwarteten Dateinamen "de_bkg_gcg2016.tif" im selben Ordner erzeugt wird.
:param pfad_tif_quasigeoidundolation: Pfad zur BKG-GeoTIFF-Datei (Quasigeoidundulation).
:type pfad_tif_quasigeoidundolation: str
:param liste_utm: Liste von UTM-Koordinaten in der Form [(punktnummer, E, N, Normalhoehe), ...].
:type liste_utm: list
:return: Dictionary {punktnummer: sp.Matrix([X, Y, Z])} mit ECEF-Koordinaten (Meter).
:rtype: dict[Any, Any]
"""
# tif vom BKG zur Quasigeoidundolation übergeben
pfad_gcg_tif = Path(pfad_tif_quasigeoidundolation)
pfad_gcg_tif_proj = pfad_gcg_tif.with_name("de_bkg_gcg2016.tif")
# Kopie des TIF anlegen (Dies ist voraussetzung für die Transformer-Bibliothek
if (not pfad_gcg_tif_proj.exists()) or (pfad_gcg_tif_proj.stat().st_size != pfad_gcg_tif.stat().st_size):
shutil.copy2(pfad_gcg_tif, pfad_gcg_tif_proj)
@@ -299,8 +380,9 @@ class Transformationen:
utm_epsg = 25832
crs_src = CRS.from_user_input(f"EPSG:{utm_epsg}+EPSG:7837") # ETRS89/DREF91 + DHHN2016
crs_dst = CRS.from_epsg(4936) # ETRS89 geozentrisch (ECEF)
crs_dst = CRS.from_epsg(4936) # ETRS89 geozentrisch kartesisch
# Umrechnungsvorgaben übergeben
tr_best = Transformer.from_crs(
crs_src,
crs_dst,
@@ -308,52 +390,54 @@ class Transformationen:
allow_ballpark=False,
)
# Koordinaten rechnen und in Dictionary speichern
dict_geozentrisch_kartesisch = {}
for Punktnummer, E, N, Normalhoehe in liste_utm:
X, Y, Z = tr_best.transform(E, N, Normalhoehe)
dict_geozentrisch_kartesisch[Punktnummer] = sp.Matrix([X, Y, Z])
# geographisch 3D + zeta
#crs_geog3d = CRS.from_epsg(4937) # ETRS89 (lon, lat, h)
#tr_h = Transformer.from_crs(
# crs_src,
# crs_geog3d,
# always_xy=True,
# allow_ballpark=False,
#)
#lon, lat, h = tr_h.transform(E, N, H)
#print("lon/lat/h:", lon, lat, h)
#print("zeta (h-H):", h - H)
return dict_geozentrisch_kartesisch
def ecef_to_utm(
self,
dict_koordinaten: dict,
pfad_gcg_tif: str | Path | None = None,
zone: int = 32,
):
pfad_gcg_tif: str | Path | None = None):
"""Rechnet ECEF-Koordinaten (ETRS89 geozentrisch-kartesisch) nach nach UTM (+ DHHN2016 Normalhöhe).
if pfad_gcg_tif is not None:
Es wird ein PROJ-Transformer von:
- Quelle: EPSG:4936 (ETRS89 geozentrisch-kartesisch),
- Ziel: EPSG:25832 + EPSG:7837 (ETRS89 / UTM Zone 32N + DHHN2016 Normalhöhe)
initialisiert. Zusätzlich wird die BKG-GeoTIFF-Datei (Quasigeoidundulation) als PROJ-Grid eingebunden,
indem eine Kopie mit dem erwarteten Namen "de_bkg_gcg2016.tif" im selben Ordner erzeugt wird.
Die Methode akzeptiert Koordinatenwerte in verschiedenen Formen (SymPy-Matrix, numpy.ndarray,
Liste/Tuple, Skalar) und extrahiert daraus drei Werte (X, Y, Z). Die Ergebnisse (E, N, H) werden auf 8 Nachkommastellen gerundet.
:param dict_koordinaten: Dictionary {punktnummer: koordinate}, wobei koordinate X/Y/Z enthält.
:type dict_koordinaten: dict
:param pfad_gcg_tif: Pfad zur BKG-GeoTIFF-Datei (Quasigeoidundulation) als str.
:type pfad_gcg_tif: str | Path | None
:return: Dictionary {punktnummer: (E, N, H)} mit UTM-Koordinaten (Meter) und Normalhöhe.
:rtype: dict
"""
# Kopie des TIF vom BKG mit der Quasigeoidundolation erstellen
pfad_gcg_tif = Path(pfad_gcg_tif).resolve()
if not pfad_gcg_tif.exists():
raise FileNotFoundError(f"Quasigeoid-Datei nicht gefunden: {pfad_gcg_tif}")
pfad_proj_grid = pfad_gcg_tif.with_name("de_bkg_gcg2016.tif")
if (
not pfad_proj_grid.exists()
or pfad_proj_grid.stat().st_size != pfad_gcg_tif.stat().st_size
):
shutil.copy2(pfad_gcg_tif, pfad_proj_grid)
datadir.append_data_dir(str(pfad_proj_grid.parent))
crs_src = CRS.from_epsg(4936) # ETRS89 geocentric (ECEF)
# EPSG-Codes feslegen
crs_src = CRS.from_epsg(4936) # ETRS89 geozentrisch-kartesisch
# Ziel-CRS: ETRS89 / UTM Zone 32/33 + DHHN2016 Normalhöhe
# EPSG:25832/25833 = ETRS89 / UTM; EPSG:7837 = DHHN2016 height
utm_epsg = 25800 + zone # 25832 oder 25833
utm_epsg = 25832
crs_dst = CRS.from_user_input(f"EPSG:{utm_epsg}+EPSG:7837")
tr = Transformer.from_crs(
@@ -365,6 +449,7 @@ class Transformationen:
tr_geo = Transformer.from_crs(CRS.from_epsg(4936), CRS.from_epsg(4979), always_xy=True)
# Koordinaten an Dictionary übergeben
dict_koordinaten_utm = {}
for punktnummer, koordinate in dict_koordinaten.items():
werte = []
@@ -399,20 +484,8 @@ class Transformationen:
# Skalar
werte.append(float(v))
if len(werte) < 3:
raise ValueError(f"Zu wenig skalare Werte gefunden: {werte}")
X, Y, Z = werte[0], werte[1], werte[2]
try:
E, N, H = tr.transform(X, Y, Z, errcheck=True)
except ProjError as e:
lon, lat, h_ell = tr_geo.transform(X, Y, Z, errcheck=True)
raise ProjError(
f"transform error (outside grid) | pn={punktnummer} | "
f"X,Y,Z={X},{Y},{Z} | lon/lat={lon},{lat} | h_ell={h_ell} | {e}"
)
E, N, H = tr.transform(X, Y, Z, errcheck=True)
# Runden, weil ansonsten aufgrund begrenzter Rechenkapazität falsche Werte Resultieren
dict_koordinaten_utm[punktnummer] = (round(E, 8), round(N, 8), round(H, 8))

View File

@@ -116,11 +116,9 @@ class Genauigkeitsmaße:
float(s_max), float(s_min),
float(t_gon)
])
except:
continue
standardellipse = pd.DataFrame(daten, columns=["Punkt", "σx", "σy", "σxy", "s_max", "s_min", "θ [gon]"])
standardellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]", "Kleine Halbachse [m]", "θ [gon]"])
return standardellipse
@@ -191,442 +189,133 @@ class Genauigkeitsmaße:
except:
continue
konfidenzellipse = pd.DataFrame(daten, columns= ["Punkt", "σx", "σy", "σxy", "a_K", "b_K","θ [gon]"])
konfidenzellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]",
"Kleine Halbachse [m]", "θ [gon]"])
return konfidenzellipse
class Plot:
@staticmethod
def konfidenzellipsoid(Qxx, s0_apost, unbekannten_liste, R, alpha, skala="f", return_2d_schnitte=True):
def netzplot_ellipsen(
Koord_ENU,
unbekannten_labels,
beobachtungs_labels,
df_konf_ellipsen_enu,
v_faktor=1000,
n_ellipse_pts=60,
title="Netzplot im ENU-System mit Konfidenzellipsen"
):
names = [str(s).strip() for s in unbekannten_labels]
Qxx = np.asarray(Qxx, float)
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = sorted({n[1:] for n in namen_str if n and n[0].upper() in ("X", "Y", "Z")})
# Skalierungsfaktor für Konfidenzbereich
if skala.lower() == "f":
k2_3d = f.ppf(1.0 - alpha, df=3)
elif skala.lower() == "f":
k2_3d = 3.0 * f.ppf(1.0 - alpha, dfn=3, dfd=R)
if "θ_EN [gon]" in df_konf_ellipsen_enu.columns:
theta_col = "θ_EN [gon]"
elif "θ [gon]" in df_konf_ellipsen_enu.columns:
theta_col = "θ [gon]"
else:
raise ValueError("skala muss 'chi2' oder 'f' sein.")
raise ValueError("Spalte 'θ_EN [gon]' oder 'θ [gon]' fehlt im DataFrame.")
daten = []
for pid in punkt_ids:
try:
idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())
idx_z = next(i for i, n in enumerate(namen_str) if n.upper() == f"Z{pid}".upper())
except StopIteration:
continue
# 3x3-Block aus Qxx ziehen
I = [idx_x, idx_y, idx_z]
Qp = Qxx[np.ix_(I, I)]
# Kovarianzmatrix (Sigma) des Punkts
Sigma = (s0_apost ** 2) * Qp
# Standardabweichungen
sx = float(np.sqrt(Sigma[0, 0]))
sy = float(np.sqrt(Sigma[1, 1]))
sz = float(np.sqrt(Sigma[2, 2]))
# Kovarianzen
sxy = float(Sigma[0, 1])
sxz = float(Sigma[0, 2])
syz = float(Sigma[1, 2])
# Eigenzerlegung (symmetrisch -> eigh)
evals, evecs = np.linalg.eigh(Sigma)
order = np.argsort(evals)[::-1]
evals = evals[order]
evecs = evecs[:, order]
# Numerische Sicherheit: negative Mini-Eigenwerte durch Rundung abklemmen
evals = np.clip(evals, 0.0, None)
# Halbachsen des Konfidenzellipsoids:
A, B, C = (np.sqrt(evals * k2_3d)).tolist()
row = {
"Punkt": pid,
"σx": sx, "σy": sy, "σz": sz,
"σxy": sxy, "σxz": sxz, "σyz": syz,
"A_K": float(A), "B_K": float(B), "C_K": float(C),
# Orientierung als Spaltenvektoren (Eigenvektoren)
"evec_1": evecs[:, 0].tolist(),
"evec_2": evecs[:, 1].tolist(),
"evec_3": evecs[:, 2].tolist(),
"skala_k2": float(k2_3d),
"skala_typ": skala.lower()
}
# Optional: 2D-Schnitte (XY, XZ, YZ) als Ellipsenparameter
if return_2d_schnitte:
row.update(Genauigkeitsmaße.ellipsen_schnitt_2d(Sigma, alpha, R, skala))
daten.append(row)
return pd.DataFrame(daten)
@staticmethod
def ellipsen_schnitt_2d(Sigma3, alpha, R, skala):
def ellipse_from_2x2(S2):
# Skalierung für 2D
if skala.lower() == "f":
k2 = f.ppf(1.0 - alpha, df=2)
else:
k2 = 2.0 * f.ppf(1.0 - alpha, dfn=2, dfd=R)
evals, evecs = np.linalg.eigh(S2)
order = np.argsort(evals)[::-1]
evals = np.clip(evals[order], 0.0, None)
evecs = evecs[:, order]
a, b = np.sqrt(evals * k2)
# Winkel der Hauptachse (zu a) in der Ebene: atan2(vy, vx)
vx, vy = evecs[0, 0], evecs[1, 0]
theta_rad = np.arctan2(vy, vx)
theta_gon = float(theta_rad * (200.0 / np.pi)) % 200.0
return float(a), float(b), theta_gon
# Submatrizen
S_xy = Sigma3[np.ix_([0, 1], [0, 1])]
S_xz = Sigma3[np.ix_([0, 2], [0, 2])]
S_yz = Sigma3[np.ix_([1, 2], [1, 2])]
axy, bxy, txy = ellipse_from_2x2(S_xy)
axz, bxz, txz = ellipse_from_2x2(S_xz)
ayz, byz, tyz = ellipse_from_2x2(S_yz)
return {
"aXY": axy, "bXY": bxy, "θXY [gon]": txy,
"aXZ": axz, "bXZ": bxz, "θXZ [gon]": txz,
"aYZ": ayz, "bYZ": byz, "θYZ [gon]": tyz,
}
@staticmethod
def transform_q_with_your_functions(q_xyz, B, L):
# East
r11 = Berechnungen.E(L, 1, 0)
r12 = Berechnungen.E(L, 0, 1)
r13 = 0
# North
r21 = Berechnungen.N(B, L, 1, 0, 0)
r22 = Berechnungen.N(B, L, 0, 1, 0)
r23 = Berechnungen.N(B, L, 0, 0, 1)
# Up
r31 = Berechnungen.U(B, L, 1, 0, 0)
r32 = Berechnungen.U(B, L, 0, 1, 0)
r33 = Berechnungen.U(B, L, 0, 0, 1)
R = np.array([
[r11, r12, r13],
[r21, r22, r23],
[r31, r32, r33]
])
q_enu = R @ q_xyz @ R.T
return q_enu
def plot_netz_komplett_final(x_vektor, unbekannten_labels, beobachtungs_labels, Qxx, sigma0_apost,
k_faktor=2.447, v_faktor=1000):
"""
Optimierter Plot für Jupyter Notebook:
- k_faktor: Statistischer Sicherheitsfaktor (2.447 entspricht 95% für 2D)
- v_faktor: Optische Überhöhung der Ellipsen (z.B. 1000 = mm werden als m dargestellt)
"""
x_vektor = np.asarray(x_vektor, float).reshape(-1)
Qxx = np.asarray(Qxx, float)
# 1. Datenaufbereitung
coords = {}
punkt_ids = sorted(set(str(l)[1:] for l in unbekannten_labels if str(l).startswith(('X', 'Y', 'Z'))))
pts_data = []
for pid in punkt_ids:
try:
ix = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"X{pid}")
iy = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"Y{pid}")
x, y = float(x_vektor[ix]), float(x_vektor[iy])
coords[pid] = (x, y)
# Kovarianzmatrix extrahieren und mit s0^2 skalieren
q_idx = [ix, iy]
Q_sub = Qxx[np.ix_(q_idx, q_idx)] * (sigma0_apost ** 2)
pts_data.append({'id': pid, 'x': x, 'y': y, 'Q': Q_sub})
except:
continue
if len(pts_data) == 0:
raise ValueError(
"Keine Netzpunkte extrahiert. Prüfe: x_vektor Form (u,) und Qxx Form (u,u) sowie Labels 'X<id>'/'Y<id>'.")
punkt_ids = sorted({nm[1:] for nm in names if nm and nm[0].upper() in ("X", "Y", "Z")})
fig = go.Figure()
# 2. Beobachtungen (Gruppiert)
# 1) Darstellungen der Beobachtungen
beob_typen = {
'GNSS-Basislinien': {'pattern': 'gnss', 'color': 'rgba(255, 100, 0, 0.4)'},
'Nivellement': {'pattern': 'niv', 'color': 'rgba(0, 200, 100, 0.4)'},
'Tachymeter': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
'Tachymeter-Beob': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
}
for typ, info in beob_typen.items():
x_l, y_l = [], []
for bl in beobachtungs_labels:
bl_str = str(bl).lower()
if (info['pattern'] in bl_str and info['pattern'] != '') or (
info['pattern'] == '' and 'gnss' not in bl_str and 'niv' not in bl_str):
pts = [pid for pid in coords if f"_{pid}" in str(bl) or str(bl).startswith(f"{pid}_")]
is_typ = ((info['pattern'] in bl_str and info['pattern'] != '') or
(info['pattern'] == '' and 'gnss' not in bl_str and 'niv' not in bl_str))
if not is_typ:
continue
bl_raw = str(bl)
pts = []
for pid in punkt_ids:
if (f"_{pid}" in bl_raw) or bl_raw.startswith(f"{pid}_"):
if pid in Koord_ENU:
pts.append(pid)
if len(pts) >= 2:
x_l.extend([coords[pts[0]][0], coords[pts[1]][0], None])
y_l.extend([coords[pts[0]][1], coords[pts[1]][1], None])
p1, p2 = pts[0], pts[1]
x_l.extend([Koord_ENU[p1][0], Koord_ENU[p2][0], None]) # E
y_l.extend([Koord_ENU[p1][1], Koord_ENU[p2][1], None]) # N
if x_l:
fig.add_trace(go.Scatter(x=x_l, y=y_l, mode='lines', name=typ, line=dict(color=info['color'], width=1)))
fig.add_trace(go.Scatter(x=x_l, y=y_l, mode='lines', name=typ,
line=dict(color=info['color'], width=1)))
# 3. Konfidenzellipsen mit v_faktor
for pt in pts_data:
vals, vecs = np.linalg.eigh(pt['Q'])
order = vals.argsort()[::-1]
vals, vecs = vals[order], vecs[:, order]
# 2) Darstellung der Konfidenzellipsen
t = np.linspace(0, 2 * np.pi, n_ellipse_pts)
first = True
for _, row in df_konf_ellipsen_enu.iterrows():
pid = str(row["Punkt"])
if pid not in Koord_ENU:
continue
theta = np.degrees(np.arctan2(vecs[1, 0], vecs[0, 0]))
# Skalierung: k_faktor (Statistik) * v_faktor (Optik)
a = k_faktor * np.sqrt(vals[0]) * v_faktor
b = k_faktor * np.sqrt(vals[1]) * v_faktor
a = float(row["a_K"]) * v_faktor
b = float(row["b_K"]) * v_faktor
theta = float(row[theta_col]) * np.pi / 200.0 # gon->rad
t = np.linspace(0, 2 * np.pi, 40)
e_x = a * np.cos(t)
e_y = b * np.sin(t)
R = np.array([[np.cos(np.radians(theta)), -np.sin(np.radians(theta))],
[np.sin(np.radians(theta)), np.cos(np.radians(theta))]])
rot = np.dot(R, np.array([e_x, e_y]))
ex = a * np.cos(t)
ey = b * np.sin(t)
c, s = np.cos(theta), np.sin(theta)
xr = c * ex - s * ey
yr = s * ex + c * ey
E0, N0, _ = Koord_ENU[pid]
fig.add_trace(go.Scatter(
x=rot[0, :] + pt['x'], y=rot[1, :] + pt['y'],
mode='lines', line=dict(color='red', width=1.5),
name=f"Ellipsen (Vergrößert {v_faktor}x)",
x=E0 + xr, y=N0 + yr,
mode="lines",
line=dict(color="red", width=1.5),
name=f"Ellipsen (×{v_faktor})",
legendgroup="Ellipsen",
showlegend=(pt == pts_data[0]), # Nur einmal in der Legende zeigen
hoverinfo='skip'
showlegend=first,
hoverinfo="skip"
))
first = False
# 4. Punkte
df_pts = pd.DataFrame(pts_data)
fig.add_trace(go.Scatter(
x=df_pts['x'], y=df_pts['y'], mode='markers+text',
text=df_pts['id'], textposition="top center",
marker=dict(size=8, color='black'), name="Netzpunkte"
))
# 5. Layout & Notebook-Größe
fig.update_layout(
title=f"Netzausgleichung: Ellipsen {v_faktor}-fach vergrößert (k={k_faktor})",
xaxis=dict(title="X [m]", tickformat="f", separatethousands=True, scaleanchor="y", scaleratio=1, showgrid=True,
gridcolor='lightgrey'),
yaxis=dict(title="Y [m]", tickformat="f", separatethousands=True, showgrid=True, gridcolor='lightgrey'),
width=1100, # Breite angepasst
height=900, # Höhe deutlich vergrößert für Jupiter Notebook
plot_bgcolor='white',
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01, bgcolor="rgba(255,255,255,0.8)")
)
# Info-Annotation als Ersatz für einen physischen Maßstabstab
fig.add_annotation(
text=f"<b>Maßstab Ellipsen:</b><br>Dargestellte Größe = Wahre Ellipse × {v_faktor}",
align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.05,
bgcolor="white", bordercolor="black", borderwidth=1)
fig.show(config={'scrollZoom': True})
def plot_netz_final_mit_df_ellipsen(x_vektor, unbekannten_labels, beobachtungs_labels, df_ellipsen, v_faktor=1000):
# 1. Punkte extrahieren
coords = {}
# Wir nehmen an, dass die Reihenfolge im x_vektor X, Y, Z pro Punkt ist
punkt_ids = sorted(set(str(l)[1:] for l in unbekannten_labels if str(l).startswith(('X', 'Y', 'Z'))))
# 3) Darstellung der Punkte
xs, ys, texts, hovers = [], [], [], []
for pid in punkt_ids:
try:
ix = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"X{pid}")
iy = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"Y{pid}")
coords[pid] = (float(x_vektor[ix]), float(x_vektor[iy]))
except:
if pid not in Koord_ENU:
continue
fig = go.Figure()
# 2. Beobachtungslinien (Gruppiert)
beob_typen = {
'GNSS-Basislinien': {'pattern': 'gnss', 'color': 'rgba(255, 100, 0, 0.4)'},
'Nivellement': {'pattern': 'niv', 'color': 'rgba(0, 200, 100, 0.4)'},
'Tachymeter': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
}
for typ, info in beob_typen.items():
x_l, y_l = [], []
for bl in beobachtungs_labels:
bl_str = str(bl).lower()
# Einfache Logik zur Typtrennung
if (info['pattern'] in bl_str and info['pattern'] != '') or \
(info['pattern'] == '' and 'gnss' not in bl_str and 'niv' not in bl_str):
pts = [pid for pid in coords if f"_{pid}" in str(bl) or str(bl).startswith(f"{pid}_")]
if len(pts) >= 2:
x_l.extend([coords[pts[0]][0], coords[pts[1]][0], None])
y_l.extend([coords[pts[0]][1], coords[pts[1]][1], None])
if x_l:
fig.add_trace(go.Scatter(x=x_l, y=y_l, mode='lines', name=typ, line=dict(color=info['color'], width=1)))
# 3. Ellipsen aus dem DataFrame zeichnen
for _, row in df_ellipsen.iterrows():
pid = str(row['Punkt'])
if pid in coords:
x0, y0 = coords[pid]
# Werte aus DF (mit v_faktor skalieren)
a = row['a_K'] * v_faktor
b = row['b_K'] * v_faktor
theta_gon = row['θ [gon]']
# Umrechnung: gon -> rad für die Rotation
# Da im Plot X horizontal und Y vertikal ist, entspricht theta_gon dem Winkel zur X-Achse
theta_rad = theta_gon * (np.pi / 200.0)
# Ellipsen berechnen
t = np.linspace(0, 2 * np.pi, 50)
e_x = a * np.cos(t)
e_y = b * np.sin(t)
# Ausrichtung der Ellipsen
R = np.array([[np.cos(theta_rad), -np.sin(theta_rad)],
[np.sin(theta_rad), np.cos(theta_rad)]])
rot = np.dot(R, np.array([e_x, e_y]))
E, N, U = Koord_ENU[pid]
xs.append(E);
ys.append(N);
texts.append(pid)
hovers.append(f"Punkt {pid}<br>E={E:.4f} m<br>N={N:.4f} m<br>U={U:.4f} m")
fig.add_trace(go.Scatter(
x=rot[0, :] + x0, y=rot[1, :] + y0,
mode='lines', line=dict(color='red', width=1.5),
name='Konfidenzellipsen',
legendgroup='Ellipsen',
showlegend=(pid == df_ellipsen.iloc[0]['Punkt']),
hoverinfo='text',
text=f"Punkt {pid}<br>a_K: {row['a_K']:.4f}m<br>b_K: {row['b_K']:.4f}m"
x=xs, y=ys, mode="markers+text",
text=texts, textposition="top center",
marker=dict(size=8, color="black"),
name="Netzpunkte",
hovertext=hovers, hoverinfo="text"
))
# Punkte plotten
df_pts = pd.DataFrame([(pid, c[0], c[1]) for pid, c in coords.items()], columns=['ID', 'X', 'Y'])
fig.add_trace(go.Scatter(
x=df_pts['X'], y=df_pts['Y'], mode='markers+text',
text=df_pts['ID'], textposition="top center",
marker=dict(size=8, color='black'), name="Netzpunkte"))
# Layout
fig.update_layout(
title=f"Netzplot (Ellipsen {v_faktor}x überhöht)",
xaxis=dict(title="X [m]", tickformat="f", separatethousands=True, scaleanchor="y", scaleratio=1,
showgrid=True, gridcolor='lightgrey'),
yaxis=dict(title="Y [m]", tickformat="f", separatethousands=True, showgrid=True, gridcolor='lightgrey'),
title=f"{title} (Ellipsen ×{v_faktor})",
xaxis=dict(title="E [m]", scaleanchor="y", scaleratio=1, showgrid=True, gridcolor="lightgrey"),
yaxis=dict(title="N [m]", showgrid=True, gridcolor="lightgrey"),
width=1100, height=900,
plot_bgcolor='white')
# Maßstabsangabe
fig.add_annotation(
text=f"<b>Skalierung:</b><br>Ellipsengröße im Plot = {v_faktor} × Realität",
align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.02,
bgcolor="rgba(255,255,255,0.8)", bordercolor="black", borderwidth=1)
fig.show(config={'scrollZoom': True})
import plotly.graph_objects as go
import numpy as np
def plot_netz_3D(x_vektor, unbekannten_labels, beobachtungs_labels, df_ellipsen, v_faktor=1000):
"""
Erzeugt einen interaktiven 3D-Plot des Netzes.
- v_faktor: Vergrößerung der Genauigkeits-Achsen (z.B. 1000 für mm -> m)
"""
# 1. Punkte extrahieren
pts = {}
punkt_ids = sorted(set(str(l)[1:] for l in unbekannten_labels if str(l).startswith(('X', 'Y', 'Z'))))
for pid in punkt_ids:
try:
ix = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"X{pid}")
iy = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"Y{pid}")
iz = next(i for i, s in enumerate(unbekannten_labels) if str(s) == f"Z{pid}")
pts[pid] = (float(x_vektor[ix]), float(x_vektor[iy]), float(x_vektor[iz]))
except:
continue
fig = go.Figure()
# 2. Beobachtungen (Linien im Raum)
# Wir zeichnen hier einfach alle Verbindungen
x_line, y_line, z_line = [], [], []
for bl in beobachtungs_labels:
p_in_l = [pid for pid in pts if f"_{pid}" in str(bl) or str(bl).startswith(f"{pid}_")]
if len(p_in_l) >= 2:
p1, p2 = pts[p_in_l[0]], pts[p_in_l[1]]
x_line.extend([p1[0], p2[0], None])
y_line.extend([p1[1], p2[1], None])
z_line.extend([p1[2], p2[2], None])
fig.add_trace(go.Scatter3d(
x=x_line, y=y_line, z=z_line,
mode='lines', line=dict(color='gray', width=2),
name='Beobachtungen'
))
# 3. Punkte & "Fehler-Kreuze" (als Ersatz für Ellipsoide)
# Ein echtes 3D-Ellipsoid ist grafisch schwer, daher zeichnen wir 3 Achsen
for pid, coord in pts.items():
# Hier könnten wir die echten Halbachsen aus der 3D-Eigenwertanalyse nutzen
# Für den Anfang plotten wir die Standardabweichungen sX, sY, sZ als Kreuz
fig.add_trace(go.Scatter3d(
x=[coord[0]], y=[coord[1]], z=[coord[2]],
mode='markers+text', text=[pid],
marker=dict(size=4, color='black'), name=f'Punkt {pid}'
))
# 4. Layout
fig.update_layout(
scene=dict(
xaxis_title='X [m]',
yaxis_title='Y [m]',
zaxis_title='Z [m]',
aspectmode='data' # WICHTIG: Verhältnisse 1:1:1 bewahren
),
width=1000, height=800,
title="Geozentrisches Netz in 3D"
template="plotly_white",
plot_bgcolor="white"
)
fig.show()
fig.add_annotation(
text=f"<b>Maßstab Ellipsen:</b><br>Dargestellte Größe = Konfidenzellipse × {v_faktor}",
align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.05,
bgcolor="white", bordercolor="black", borderwidth=1
)
# Aufruf
fig.show(config={'scrollZoom': True})

View File

@@ -117,199 +117,193 @@ class Zuverlaessigkeit:
def aeussere_zuverlaessigkeit_EF_EP_stabil(Lokaltest, labels, Qxx, A, P, s0_apost, unbekannten_liste, x):
def aeussere_zuverlaessigkeit(
Lokaltest, labels, Qxx, A, P, s0_apost, unbekannten_liste, x,
angle_units="rad",
ep_use_abs=True,
exclude_prefixes=("lA_",),
):
df = Lokaltest.copy()
labels = list(labels)
labels = [str(l) for l in list(labels)]
Qxx = np.asarray(Qxx, float)
A = np.asarray(A, float)
P = np.asarray(P, float)
x = np.asarray(x, float).reshape(-1)
namen_str = [str(sym) for sym in unbekannten_liste]
n = A.shape[0]
if len(labels) != n:
raise ValueError(f"len(labels)={len(labels)} passt nicht zu A.shape[0]={n}.")
if len(df) != n:
raise ValueError(f"Lokaltest hat {len(df)} Zeilen, A hat {n} Beobachtungen.")
# Pseudobeobachtungen rausfiltern
keep = np.ones(n, dtype=bool)
if exclude_prefixes:
for i, lbl in enumerate(labels):
if any(lbl.startswith(pref) for pref in exclude_prefixes):
keep[i] = False
# alles konsistent kürzen (wichtig: auch A & P!)
df = df.loc[keep].reset_index(drop=True)
labels = [lbl for (lbl, k) in zip(labels, keep) if k]
A = A[keep, :]
P = P[np.ix_(keep, keep)]
# neue n
n = A.shape[0]
# Daten aus dem Lokaltest
ri = df["r_i"].astype(float).to_numpy()
GF = df["GF_i"].astype(float).to_numpy()
GRZW = df["GRZW_i"].astype(float).to_numpy()
n = A.shape[0]
# Namen als Strings für die Suche
namen_str = [str(sym) for sym in unbekannten_liste]
s0 = float(s0_apost)
# 1) Einflussfaktor EF berechnen
EF = np.zeros(n, dtype=float)
for i in range(n):
nabla_l = np.zeros((n, 1))
nabla_l[i, 0] = GRZW[i]
nabla_x = Qxx @ (A.T @ (P @ nabla_l))
Qxx_inv_nabla_x = np.linalg.solve(Qxx, nabla_x)
EF2 = ((nabla_x.T @ Qxx_inv_nabla_x) / (float(s0_apost) ** 2)).item()
EF[i] = np.sqrt(max(0, EF2))
def to_rad(val):
if angle_units == "rad":
return val
if angle_units == "gon":
return val * (np.pi / 200.0)
if angle_units == "deg":
return val * (np.pi / 180.0)
raise ValueError("angle_units muss 'rad', 'gon' oder 'deg' sein.")
# 2) Koordinaten-Dict
# Punktkoordinaten aus x (für Streckenäquivalent bei Winkel-EP)
coords = {}
punkt_ids = [n[1:] for n in namen_str if n.upper().startswith("X")]
punkt_ids = sorted({name[1:] for name in namen_str
if name[:1].upper() in ("X", "Y", "Z") and len(name) > 1})
for pid in punkt_ids:
try:
ix = namen_str.index(f"X{pid}")
iy = namen_str.index(f"Y{pid}")
iz = namen_str.index(f"Z{pid}")
coords[pid] = (x[ix], x[iy], x[iz] if iz is not None else 0.0)
except:
coords[pid] = (x[ix], x[iy], x[iz])
except ValueError:
continue
# 3) EP + Standpunkte
EP_m = np.full(len(labels), np.nan, dtype=float)
standpunkte = [""] * len(labels)
# Standpunkt/Zielpunkt
standpunkte = [""] * n
zielpunkte = [""] * n
for i, lbl in enumerate(labels):
parts = lbl.split("_")
sp, zp = None, None
if any(k in lbl for k in ["_SD_", "_R_", "_ZW_"]):
if len(parts) >= 5: sp, zp = parts[3].strip(), parts[4].strip()
if len(parts) >= 5:
sp, zp = parts[3].strip(), parts[4].strip()
elif "gnss" in lbl.lower():
if len(parts) >= 2:
sp, zp = parts[-2].strip(), parts[-1].strip()
elif "niv" in lbl.lower():
if len(parts) >= 4:
sp = parts[3].strip()
if len(parts) >= 5:
zp = parts[4].strip()
else:
sp = parts[-1].strip()
standpunkte[i] = sp if sp is not None else ""
standpunkte[i] = sp or ""
zielpunkte[i] = zp or ""
# SD, GNSS, Niv: direkt Wegfehler
if "_SD_" in lbl or "gnss" in lbl.lower() or "niv" in lbl.lower():
EP_m[i] = (1.0 - ri[i]) * GF[i]
# Winkel: Streckenäquivalent
elif "_R_" in lbl or "_ZW_" in lbl:
if sp in coords and zp in coords:
X1, Y1, _ = coords[sp]
X2, Y2, _ = coords[zp]
s = np.sqrt((X2 - X1) ** 2 + (Y2 - Y1) ** 2)
EP_m[i] = (1.0 - ri[i]) * (GF[i] * s)
# Berechnung des EPs
EP_GF = (1.0 - ri) * GF
EP_grzw = (1.0 - ri) * GRZW
if ep_use_abs:
EP_GF = np.abs(EP_GF)
EP_grzw = np.abs(EP_grzw)
# 4) SP am Standpunkt (2D oder 1D)
diagQ = np.diag(Qxx)
SP_cache_mm = {}
for sp in set([s for s in standpunkte if s]):
try:
ix = namen_str.index(f"X{sp}")
iy = namen_str.index(f"Y{sp}")
SP_cache_mm[sp] = float(s0_apost) * np.sqrt(diagQ[ix] + diagQ[iy]) * 1000.0
except ValueError:
# Falls keine Lage, prüfe Höhe (Nivellement)
try:
iz = namen_str.index(f"Z{sp}")
SP_cache_mm[sp] = float(s0_apost) * np.sqrt(diagQ[iz]) * 1000.0
except ValueError:
SP_cache_mm[sp] = 0.0
SP_mm = np.array([SP_cache_mm.get(sp, np.nan) for sp in standpunkte], dtype=float)
return pd.DataFrame({
"Beobachtung": labels, "Stand-Pkt": standpunkte, "EF": EF,
"EP [mm]": EP_m * 1000.0, "SP [mm]": SP_mm, "EF*SP [mm]": EF * SP_mm
})
def aeussere_zuverlaessigkeit_EF_EP(Lokaltest, labels, Qxx, A, P, s0_apost, unbekannten_liste, x):
df = Lokaltest.copy()
labels = list(labels)
Qxx = np.asarray(Qxx, float)
A = np.asarray(A, float)
P = np.asarray(P, float)
x = np.asarray(x, float).reshape(-1)
ri = df["r_i"].astype(float).to_numpy()
GF = df["GF_i"].astype(float).to_numpy()
s_vi = df["s_vi"].astype(float).to_numpy()
GRZW = df["GRZW_i"].astype(float).to_numpy()
nzp = df["δ0"].astype(float).to_numpy()
n = A.shape[0] # Anzahl Beobachtungen
u = A.shape[1] # Anzahl Unbekannte
# Einflussfaktor EF berechnen
EF = np.zeros(n, dtype=float)
for i in range(n):
# 1) ∇l_i aufstellen
nabla_l = np.zeros((n, 1))
nabla_l[i, 0] = GRZW[i]
# 2) ∇x_i = Qxx * A^T * P * ∇l_i
nabla_x = Qxx @ (A.T @ (P @ nabla_l))
# 3) EF_i^2 = (∇x_i^T * Qxx^{-1} * ∇x_i) / s0^2
Qxx_inv_nabla_x = np.linalg.solve(Qxx, nabla_x) # = Qxx^{-1} ∇x_i
#EF2 = float((nabla_x.T @ Qxx_inv_nabla_x) / (float(s0_apost) ** 2)).item()
EF2 = ((nabla_x.T @ Qxx_inv_nabla_x) / (float(s0_apost) ** 2)).item()
EF[i] = np.sqrt(EF2)
# Koordinaten-Dict aus x
coords = {}
j = 0
while j < len(unbekannten_liste):
name = str(unbekannten_liste[j])
if name.startswith("X"):
pn = name[1:]
coords[pn] = (x[j], x[j + 1], x[j + 2])
j += 3
else:
j += 1
# EP + Standpunkte
EP_m = np.full(len(labels), np.nan, dtype=float)
standpunkte = [""] * len(labels)
EP_hat_m = np.full(n, np.nan, float)
EP_grzw_m = np.full(n, np.nan, float)
for i, lbl in enumerate(labels):
parts = lbl.split("_")
sp = None
zp = None
sp = standpunkte[i]
zp = zielpunkte[i]
# Tachymeter: ID_SD_GRP_SP_ZP / ID_R_GRP_SP_ZP / ID_ZW_GRP_SP_ZP
if ("_SD_" in lbl) or ("_R_" in lbl) or ("_ZW_" in lbl):
if len(parts) >= 5:
sp = parts[3].strip()
zp = parts[4].strip()
is_angle = ("_R_" in lbl) or ("_ZW_" in lbl)
if not is_angle:
EP_hat_m[i] = EP_GF[i]
EP_grzw_m[i] = EP_grzw[i]
continue
# GNSS: *_gnssbx_SP_ZP etc.
if ("gnss" in lbl) and (len(parts) >= 4):
sp = parts[-2].strip()
zp = parts[-1].strip()
standpunkte[i] = sp if sp is not None else ""
one_minus_r = (1.0 - ri[i])
# SD + GNSS: direkt in m
if ("_SD_" in lbl) or ("gnss" in lbl):
EP_m[i] = one_minus_r * GF[i]
# R / ZW: Winkel -> Streckenäquivalent über s
elif ("_R_" in lbl) or ("_ZW_" in lbl):
if sp and zp and (sp in coords) and (zp in coords):
# Winkel -> Querabweichung = Winkel(rad) * Strecke (3D)
if sp in coords and zp in coords:
X1, Y1, Z1 = coords[sp]
X2, Y2, Z2 = coords[zp]
s = float(np.sqrt((X2 - X1) ** 2 + (Y2 - Y1) ** 2 + (Z2 - Z1) ** 2))
EP_m[i] = one_minus_r * ((GF[i]) * s)
s = np.sqrt((X2 - X1) ** 2 + (Y2 - Y1) ** 2 + (Z2 - Z1) ** 2)
# SP am Standpunkt (2D)
diagQ = np.diag(Qxx)
SP_cache_mm = {}
EP_hat_m[i] = to_rad(EP_GF[i]) * s
EP_grzw_m[i] = to_rad(EP_grzw[i]) * s
for sp in set([s for s in standpunkte if s]):
idx_x = [k for k, sym in enumerate(unbekannten_liste) if str(sym) == f"X{sp}"][0]
qx = diagQ[idx_x]
qy = diagQ[idx_x + 1]
SP_cache_mm[sp] = float(s0_apost) * np.sqrt(qx + qy) * 1000.0
# 3x3 Blöcke
def idx_xyz(pid):
return [
namen_str.index(f"X{pid}"),
namen_str.index(f"Y{pid}"),
namen_str.index(f"Z{pid}")
]
SP_mm = np.array([SP_cache_mm.get(sp, np.nan) for sp in standpunkte], dtype=float)
# EF lokal + SP lokal (3D)
EF = np.full(n, np.nan, float)
SP_loc_m = np.full(n, np.nan, float)
EFSP_loc_m = np.full(n, np.nan, float)
out = pd.DataFrame({
for i in range(n):
sp = standpunkte[i]
zp = zielpunkte[i]
blocks = []
idx = []
try:
if sp:
b = idx_xyz(sp)
blocks.append(b)
idx += b
if zp:
b = idx_xyz(zp)
blocks.append(b)
idx += b
except ValueError:
continue
if not blocks:
continue
idx = list(dict.fromkeys(idx)) # unique
# Δx_i aus Grenzstörung
dl = np.zeros((n, 1))
dl[i, 0] = GRZW[i]
dx = Qxx @ (A.T @ (P @ dl))
dx_loc = dx[idx, :]
Q_loc = Qxx[np.ix_(idx, idx)]
# EF lokal
EF2 = (dx_loc.T @ np.linalg.solve(Q_loc, dx_loc)).item() / (s0 ** 2)
EF[i] = np.sqrt(max(0.0, EF2))
# SP lokal 3D: max trace der 3x3 Punktblöcke
tr_list = [np.trace(Qxx[np.ix_(b, b)]) for b in blocks]
if not tr_list:
continue
sigmaPmax_loc = s0 * np.sqrt(max(tr_list))
SP_loc_m[i] = sigmaPmax_loc
EFSP_loc_m[i] = EF[i] * sigmaPmax_loc
ausgabe_zuv = pd.DataFrame({
"Beobachtung": labels,
"Stand-Pkt": standpunkte,
"Ziel-Pkt": zielpunkte,
"r_i": ri,
"EP_GF [mm]": EP_hat_m * 1000.0,
"EP_grzw [mm]": EP_grzw_m * 1000.0,
"EF": EF,
"EP [mm]": EP_m * 1000.0,
"SP [mm]": SP_mm,
"EF*SP [mm]": EF * SP_mm,
"SP_loc_3D [mm]": SP_loc_m * 1000.0,
"EF*SP_loc_3D [mm]": EFSP_loc_m * 1000.0,
})
return out
return ausgabe_zuv

View File

@@ -1,91 +1,85 @@
import sympy as sp
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, Tuple, Iterable, Any
from Export import Export
import numpy as np
import sympy as sp
from typing import Dict, Iterable
from Datenbank import Datenbankzugriff
from Export import Export
@dataclass
class StochastischesModell:
"""Stochastisches Modell zur Aufstellung und Auswertung von Varianz-Kovarianz-Matrizen.
Die Klasse stellt Methoden zur Verfügung für:
- symbolischen Aufbau der Varianz-Kovarianz-Matrix der Beobachtungen Qll,
- numerische Substitution von Qll aus der Datenbank,
- symbolischen und numerischen Aufbau der Zusatz-Varianz-Kovarianz-Matrix QAA für Anschlusspunkte (weiche Lagerung, lA_*),
- Ableitung zentraler Matrizen der Ausgleichung (P, Qxx, Qll_dach, Qvv) als Hilfsfunktionen.
Die Grundidee ist, die stochastischen Zusammenhänge zunächst symbolisch abzubilden (SymPy) und anschließend
mit Datenbankwerten zu substituieren.
"""
n_beob: int
sigma_beob: Iterable[float] =None #σ a priori der einzelnen Beobachtung
gruppe_beob: Iterable[int] =None #Gruppenzugehörigkeit jeder Beobachtung (Distanz, Richtung, GNSS, Nivellement,...,)
sigma0_gruppe: Dict[int, float] = field(default_factory=dict) #σ0² für jede Gruppe
def __post_init__(self):
# Defaults setzen
if self.sigma_beob is None:
self.sigma_beob = [1.0] * int(self.n_beob)
def __init__(self, pfad_datenbank: str) -> None:
"""Initialisiert das stochastische Modell.
if self.gruppe_beob is None:
self.gruppe_beob = [1] * int(self.n_beob)
# In SymPy-Spaltenvektoren umwandeln
#self.sigma_beob = sp.Matrix(list(self.sigma_beob))
#self.gruppe_beob = sp.Matrix(list(self.gruppe_beob))
# Dimension prüfen
#if self.sigma_beob.rows != self.gruppe_beob.rows:
# raise ValueError("sigma_beob und gruppe_beob müssen gleich viele Einträge haben.")
#if self.sigma_beob.rows != int(self.n_beob):
# raise ValueError("n_beob passt nicht zur Länge von sigma_beob / gruppe_beob.")
# Fehlende Gruppen mit sigma0_sq = 1.0 ergänzen
#unique_groups = sorted({int(g) for g in self.gruppe_beob})
#for g in unique_groups:
# if g not in self.sigma0_gruppe:
# self.sigma0_gruppe[g] = 1.0
# In NumPy-Spaltenvektoren umwandeln
self.sigma_beob = np.asarray(list(self.sigma_beob), dtype=float).reshape(-1, 1)
self.gruppe_beob = np.asarray(list(self.gruppe_beob), dtype=int).reshape(-1, 1)
# Dimension prüfen
if self.sigma_beob.shape[0] != self.gruppe_beob.shape[0]:
raise ValueError("sigma_beob und gruppe_beob müssen gleich viele Einträge haben.")
if self.sigma_beob.shape[0] != int(self.n_beob):
raise ValueError("n_beob passt nicht zur Länge von sigma_beob / gruppe_beob.")
# Fehlende Gruppen mit sigma0_sq = 1.0 ergänzen
unique_groups = sorted({int(g) for g in self.gruppe_beob.flatten()})
for g in unique_groups:
if g not in self.sigma0_gruppe:
self.sigma0_gruppe[g] = 1.0
Speichert den Pfad zur SQLite-Datenbank, initialisiert den Datenbankzugriff und legt Cache-Variablen an,
die in den numerischen Auswertungen zur Rechenzeitersparnis wiederverwendet werden.
:param pfad_datenbank: Pfad zur SQLite-Datenbank.
:type pfad_datenbank: str
:return: None
:rtype: None
"""
self.pfad_datenbank = pfad_datenbank
self.func_Qll_numerisch = None
self.liste_symbole_lambdify = None
self.db_zugriff = Datenbankzugriff(self.pfad_datenbank)
def Qll_symbolisch(self, liste_beobachtungen_symbolisch: list) -> sp.Matrix:
"""Erstellt die symbolische Varianz-Kovarianz-Matrix Qll der Beobachtungen.
#def berechne_Qll(self) -> Tuple[sp.Matrix, sp.Matrix]:
# n = self.n_beob
# Q_ll = sp.zeros(n, n)
# P = sp.zeros(n, n)
# for i in range(self.n_beob):
# sigma_i = self.sigma_beob[i, 0] #σ-Wert der i-ten Beobachtung holen
# g = int(self.gruppe_beob[i, 0]) #Gruppenzugehörigkeit der Beobachtung bestimmen
# sigma0_sq = self.sigma0_gruppe[g] #Den Varianzfaktor der Gruppe holen
# q_ii = sigma_i**2 #σ² berechnen
# Q_ll[i, i] = q_ii #Diagonale
# return Q_ll
Aus den symbolischen Beobachtungskennungen wird die Beobachtungsart abgeleitet (Tachymeter: SD/R/ZW,
GNSS: gnssbx/gnssby/gnssbz, Geometrisches Nivellement: niv). Für jede Beobachtung wird eine symbolische Varianzgleichung
aufgestellt und in Qll eingetragen.
def Qll_symbolisch(self, pfad_datenbank: str, liste_beobachtungen_symbolisch: list) -> sp.Matrix:
Berücksichtigte Gleichungen:
- Tachymeter SD: σ = sqrt(σ_konstant² + (σ_streckenprop * SD / 1 000 000)²), Varianz = varkomp * σ²
- Tachymeter R/ZW: σ = sqrt(σ_konstant² + (σ_konstant_SD / SD)²), Varianz = varkomp * σ²
- GNSS: Diagonale und Korrelationen je Basislinie aus cxx/cyy/czz und cxy/cxz/cyz, jeweils skaliert mit s0²
- Nivellement: σ = sqrt(n_wechselpunkte * σ_konstant² + σ_streckenprop² * distanz / 1000), Varianz = varkomp * σ²
Die symbolische Matrix wird als CSV-Datei in Zwischenergebnisse\\Qll_Symbolisch.csv exportiert.
:param liste_beobachtungen_symbolisch: Liste der symbolischen Beobachtungskennungen.
:type liste_beobachtungen_symbolisch: list
:return: Symbolische Varianz-Kovarianz-Matrix Qll.
:rtype: sp.Matrix
"""
liste_standardabweichungen_symbole = []
# Vorbereitung und Abfrage der notwendigen Listen und Dictionaries
liste_beobachtungen_symbolisch = [str(b) for b in liste_beobachtungen_symbolisch]
liste_beobachtungen_symbolisch = [b for b in liste_beobachtungen_symbolisch if not b.startswith("lA_")]
Qll = sp.zeros(len(liste_beobachtungen_symbolisch), len(liste_beobachtungen_symbolisch))
dict_beobachtungenID_instrumenteID = self.db_zugriff.get_instrumenteID_beobachtungenID_dict()
db_zugriff = Datenbankzugriff(pfad_datenbank)
dict_beobachtungenID_instrumenteID = db_zugriff.get_instrumenteID_beobachtungenID_dict()
# Aufstellen der Symbolischen Gleichungen für die Einträge in der Qll-Matrix jeder Beobachtungsgruppe
for i, beobachtung_symbolisch_i in enumerate(liste_beobachtungen_symbolisch):
aufgeteilt_i = beobachtung_symbolisch_i.split("_")
beobachtungenID_i = int(aufgeteilt_i[0])
instrumenteID_i = dict_beobachtungenID_instrumenteID[beobachtungenID_i]
# SD = Schrägdistanzen | R = Richtung | ZW = Zenitwinkel
# varkomp = Varianzkomponentenschätzung (Ist noch keine Erfolgt, ist dieser Faktor = 1
if aufgeteilt_i[1] == "SD" or aufgeteilt_i[1] == "R" or aufgeteilt_i[1] == "ZW":
beobachtungsart_i = str(aufgeteilt_i[1])
@@ -120,6 +114,7 @@ class StochastischesModell:
Qll[i, i] = (varianzkompontenschaetzung) * sigma ** 2
# Setzen der 0-Einträge in der Qll-Matrix
for j in range(i + 1, len(liste_beobachtungen_symbolisch)):
beobachtung_symbolisch_j = liste_beobachtungen_symbolisch[j]
aufgeteilt_j = beobachtung_symbolisch_j.split("_")
@@ -128,8 +123,10 @@ class StochastischesModell:
if beobachtungsart_i == "SD" and beobachtungsart_j == "SD":
Qll[i, j] = 0
Qll[j, i] = 0
# GNSS
# s0, sowie die kovarianzen cxx, ... entstammen direkt LeicaGeooffice
if aufgeteilt_i [1] == "gnssbx" or aufgeteilt_i[1] == "gnssby" or aufgeteilt_i[1] == "gnssbz":
#beobachtungenID_i = int(aufgeteilt_i[0])
beobachtungsart_i = str(aufgeteilt_i[1])
varianzkompontenschaetzung = sp.Symbol(
f"varkomp_{instrumenteID_i}_GNSS-Rover_Basislinienbeobachtungen")
@@ -185,9 +182,8 @@ class StochastischesModell:
liste_standardabweichungen_symbole.append(czz)
Qll[i, i] = (varianzkompontenschaetzung) * (czz * (s0 ** 2))
# Geometrisches Nivellement
if aufgeteilt_i[1] == "niv":
#beobachtungenID_i = int(aufgeteilt_i[0])
#instrumenteID_i = dict_beobachtungenID_instrumenteID[beobachtungenID_i]
beobachtungsart_i = str(aufgeteilt_i[1])
varianzkompontenschaetzung = sp.Symbol(
@@ -198,6 +194,7 @@ class StochastischesModell:
nivellement_distanz = sp.Symbol(f"niv_distanz_{beobachtungenID_i}")
nivellement_anz_wechselpunkte = sp.Symbol(f"niv_anz_wechselpunkte_{beobachtungenID_i}")
# Berechnen der Standardabweichung unter Einbeziehung der Anzahl Wechselpunkte
sigma = sp.sqrt(nivellement_anz_wechselpunkte * stabw_apriori_konstant ** 2 + stabw_apriori_streckenprop ** 2 * nivellement_distanz / 1000)
liste_standardabweichungen_symbole.append(sigma)
@@ -206,24 +203,49 @@ class StochastischesModell:
Export.matrix_to_csv(r"Zwischenergebnisse\Qll_Symbolisch.csv", liste_beobachtungen_symbolisch, liste_beobachtungen_symbolisch, Qll, "Qll")
return Qll
def Qll_numerisch(self, pfad_datenbank: str, Qll_Matrix_Symbolisch: sp.Matrix, liste_beobachtungen_symbolisch: list) -> np.Matrix:
def Qll_numerisch(self, Qll_Matrix_Symbolisch: sp.Matrix, liste_beobachtungen_symbolisch: list) -> np.Matrix:
"""Erstellt eine numerische Varianz-Kovarianz-Matrix aus einer symbolischen Qll-Matrix.
Es werden die zur Substitution benötigten Werte aus der Datenbank abgefragt und den in Qll vorkommenden Symbolen zugeordnet,
u. a.:
- Genauigkeiten (stabw_apriori_konstant / stabw_apriori_streckenprop) je Instrument und Beobachtungsart,
- Varianzkomponenten (varkomp_*) je Instrument und Beobachtungsgruppe,
- gemessene Tachymeter-Distanzen SD_* zur Bildung der Winkel-/Zenitwinkel-Ansätze,
- GNSS-Kovarianzen cxx, cxy, cxz, cyy, cyz, czz sowie s0 je Basislinie,
- Geomtrisches Nivellement-Strecken und Anzahl Wechsel-/Standpunkte.
Die Methode prüft, ob alle freien Symbole der Qll-Matrix substituierbar sind.
Zur Rechenzeitersparnis wird ein Lambdify-Cache geführt. Die Matrix wird anschließend gezielt
über Nicht-Null-Einträge befüllt, um unnötige Auswertung von Nullzellen zu vermeiden.
Die numerische Matrix wird als CSV-Datei in Zwischenergebnisse\\Qll_Numerisch.csv exportiert.
:param Qll_Matrix_Symbolisch: Symbolische Varianz-Kovarianz-Matrix Qll.
:type Qll_Matrix_Symbolisch: sp.Matrix
:param liste_beobachtungen_symbolisch: Liste der symbolischen Beobachtungskennungen.
:type liste_beobachtungen_symbolisch: list
:return: Numerische Varianz-Kovarianz-Matrix Qll als Numpy-Array.
:rtype: np.Matrix
:raises ValueError: Falls Symbole in Qll_Matrix_Symbolisch enthalten sind, für die keine Substitutionen vorhanden sind.
"""
liste_beobachtungen_symbolisch = [str(b).strip() for b in liste_beobachtungen_symbolisch]
liste_beobachtungen_symbolisch = [b for b in liste_beobachtungen_symbolisch if not b.startswith("lA_")]
db_zugriff = Datenbankzugriff(pfad_datenbank)
dict_genauigkeiten = db_zugriff.get_genauigkeiten_dict()
dict_beobachtungenID_instrumenteID = db_zugriff.get_instrumenteID_beobachtungenID_dict()
# Abfragen der Zahlen zu den Symbolen aus der Datenbank
dict_genauigkeiten = self.db_zugriff.get_genauigkeiten_dict()
liste_beobachtungen_tachymeter = db_zugriff.get_beobachtungen_from_beobachtungenid()
liste_beobachtungen_gnss = db_zugriff.get_beobachtungen_gnssbasislinien()
liste_beobachtungen_nivellement = db_zugriff.get_beobachtungen_nivellement()
liste_varianzkomponenten = db_zugriff.get_varianzkomponentenschaetzung()
liste_beobachtungen_tachymeter = self.db_zugriff.get_beobachtungen_from_beobachtungenid()
liste_beobachtungen_gnss = self.db_zugriff.get_beobachtungen_gnssbasislinien()
liste_beobachtungen_nivellement = self.db_zugriff.get_beobachtungen_nivellement()
liste_varianzkomponenten = self.db_zugriff.get_varianzkomponentenschaetzung()
# Erstellen eines Dicts mit der zuordnung beobachtungenID : Distanz
dict_beobachtungenID_distanz = {}
for standpunkt, zielpunkt, beobachtungenID, beobachtungsgruppeID, tachymeter_richtung, tachymeter_zenitwinkel, tachymeter_distanz in liste_beobachtungen_tachymeter:
dict_beobachtungenID_distanz[int(beobachtungenID)] = tachymeter_distanz
# Erstellen eines Dicts mit den Genauigkeiten aus der Tabelle Genauigkeiten
dict_genauigkeiten_neu = {}
for genauigkeitenID, eintrag in dict_genauigkeiten.items():
instrumenteID = int(eintrag[0])
@@ -235,6 +257,7 @@ class StochastischesModell:
substitutionen = {}
# Erstellen eines Dicts mit den konstanten Anteilen der Standardabweichungen aus der Datenbank
dict_konstante_sd = {}
for (instrumenteID, beobachtungsart), (stabw_apriori_konstant,
stabw_apriori_streckenprop) in dict_genauigkeiten_neu.items():
@@ -242,9 +265,11 @@ class StochastischesModell:
if stabw_apriori_konstant is not None:
dict_konstante_sd[instrumenteID] = float(stabw_apriori_konstant)
# Zuordnen des numerischen Varianzfaktors zum entsprechenden Symbol für die Substitution
for (varianzkomponenteID, instrumenteID, beobachtungsgruppe, varianz_varianzkomponentenschaetzung) in liste_varianzkomponenten:
substitutionen[sp.Symbol(f"varkomp_{instrumenteID}_{beobachtungsgruppe.strip()}")] = float(varianz_varianzkomponentenschaetzung)
# Zuordnen der numerischen Genauigkeitsangaben aus der Datenbank zum entsprechenden Symbol für die Substitution
for (instrumenteID, beobachtungsart), (stabw_apriori_konstant,
stabw_apriori_streckenprop) in dict_genauigkeiten_neu.items():
@@ -257,7 +282,6 @@ class StochastischesModell:
elif beobachtungsart == "Geometrisches_Nivellement":
beobachtungsart_kurz = "niv"
if stabw_apriori_konstant is not None:
substitutionen[sp.Symbol(f"stabw_apriori_konstant_{beobachtungsart_kurz}_{instrumenteID}")] = float(stabw_apriori_konstant)
if stabw_apriori_streckenprop is not None:
@@ -266,17 +290,12 @@ class StochastischesModell:
wert = wert / 1000.0
substitutionen[sp.Symbol(f"stabw_apriori_streckenprop_{beobachtungsart_kurz}_{instrumenteID}")] = wert
# --- DEBUG NIV Genauigkeiten (einmalig) ---
for k, v in substitutionen.items():
ks = str(k)
if "stabw_apriori_streckenprop_niv_" in ks or "stabw_apriori_konstant_niv_" in ks:
print("DEBUG", ks, "=", v)
for instrumenteID, wert in dict_konstante_sd.items():
substitutionen[sp.Symbol(f"stabw_apriori_konstant_SD_{instrumenteID}")] = float(wert)
liste_beobachtungen_symbolisch = [str(b) for b in liste_beobachtungen_symbolisch]
# Zuordnen der numerischen gemessenen Distanz aus der Datenbank zum entsprechenden Symbol für die Substitution
for beobachtung_symbolisch in liste_beobachtungen_symbolisch:
aufgeteilt = beobachtung_symbolisch.split("_")
beobachtungenID = int(aufgeteilt[0])
@@ -285,7 +304,7 @@ class StochastischesModell:
if distanz is not None:
substitutionen[sp.Symbol(f"SD_{beobachtungenID}")] = float(distanz)
#GNSS Basislinien
# Zuordnen der numerischen GNSS-Bestandteile aus der Tabelle Beobachtungen zum entsprechenden Symbol für die Substitution
for gnss_beobachtungen in liste_beobachtungen_gnss:
beobachtungenID = gnss_beobachtungen[0]
gnss_s0 = gnss_beobachtungen[6]
@@ -313,7 +332,7 @@ class StochastischesModell:
substitutionen[sp.Symbol(f"niv_anz_wechselpunkte_{beobachtungenID}")] = float(niv_anz_standpkte)
substitutionen[sp.Symbol(f"niv_distanz_{beobachtungenID}")] = float(niv_strecke)
#Qll_numerisch = Qll_Matrix_Symbolisch.xreplace(substitutionen)
# Substituieren mit der Methode lambdify
if (self.func_Qll_numerisch is None) or (set(self.liste_symbole_lambdify) != set(substitutionen.keys())):
self.liste_symbole_lambdify = sorted(substitutionen.keys(), key=lambda s: str(s))
self.func_Qll_numerisch = sp.lambdify(
@@ -323,14 +342,13 @@ class StochastischesModell:
cse=True
)
# Ausgeben der fehlenden Substitutionen
fehlend = sorted(list(Qll_Matrix_Symbolisch.free_symbols - set(substitutionen.keys())), key=lambda s: str(s))
if fehlend:
raise ValueError(
f"Qll_numerisch: Fehlende Substitutionen ({len(fehlend)}): {[str(s) for s in fehlend[:80]]}")
#liste_werte = [substitutionen[s] for s in self.liste_symbole_lambdify]
#Qll_numerisch = np.asarray(self.func_Qll_numerisch(*liste_werte), dtype=float)
# Eingrenzen der Substitution auf nicht 0 Zellen
rows = int(Qll_Matrix_Symbolisch.rows)
cols = int(Qll_Matrix_Symbolisch.cols)
@@ -349,12 +367,6 @@ class StochastischesModell:
eintrag_num = eintrag.xreplace(substitutionen)
if hasattr(eintrag_num, "free_symbols") and len(eintrag_num.free_symbols) > 0:
rest = sorted(list(eintrag_num.free_symbols), key=lambda s: str(s))
raise ValueError(
f"Qll_numerisch: Eintrag [{i},{j}] bleibt symbolisch. Rest-Symbole: {[str(s) for s in rest[:20]]}"
)
Qll_numerisch[i, j] = float(eintrag_num)
Export.matrix_to_csv(
@@ -367,15 +379,28 @@ class StochastischesModell:
return Qll_numerisch
def QAA_symbolisch(self, liste_beobachtungen_symbolisch: list, pfad_datenbank: str) -> np.Matrix:
def QAA_symbolisch(self, liste_beobachtungen_symbolisch: list) -> np.Matrix:
"""Erstellt die symbolische Varianz-Kovarianz-Matrix QAA der Anschlusspunkte (weiche Lagerung).
Es werden ausschließlich Beobachtungen berücksichtigt, deren Kennung mit "lA_" beginnt. Für jede Anschlussbedingung
wird eine (symbolische) Standardabweichung StabwAA_* angesetzt und mit der Varianzkomponente der Beobachtungsgruppe
"Anschlusspunkte" multipliziert.
Die symbolische Matrix wird als CSV-Datei in Zwischenergebnisse\\QAA_Symbolisch.csv exportiert.
:param liste_beobachtungen_symbolisch: Liste der symbolischen Beobachtungskennungen.
:type liste_beobachtungen_symbolisch: list
:return: Symbolische Varianz-Kovarianz-Matrix QAA.
:rtype: sp.Matrix
"""
liste_standardabweichungen_symbole = []
liste_beobachtungen_symbolisch = [str(b) for b in liste_beobachtungen_symbolisch]
liste_beobachtungen_symbolisch = [b for b in liste_beobachtungen_symbolisch if b.startswith("lA_")]
Qll = sp.zeros(len(liste_beobachtungen_symbolisch), len(liste_beobachtungen_symbolisch))
db_zugriff = Datenbankzugriff(pfad_datenbank)
instrumente_id_anschlusspunkte = db_zugriff.get_instrument_liste("Anschlusspunkte")[0][0]
instrumente_id_anschlusspunkte = self.db_zugriff.get_instrument_liste("Anschlusspunkte")[0][0]
# Erstellen der Symbolischen Gleichungen für die QAA-Matrix
for i, beobachtung_symbolisch_i in enumerate(liste_beobachtungen_symbolisch):
aufgeteilt_i = beobachtung_symbolisch_i.split("_")
datumskoordinate = str(aufgeteilt_i[1])
@@ -394,17 +419,32 @@ class StochastischesModell:
Export.matrix_to_csv(r"Zwischenergebnisse\QAA_Symbolisch.csv", liste_beobachtungen_symbolisch, liste_beobachtungen_symbolisch, Qll, "Qll")
return Qll
def QAA_numerisch(self, pfad_datenbank: str, QAA_Matrix_Symbolisch: sp.Matrix, liste_beobachtungen_symbolisch: list) -> np.Matrix:
def QAA_numerisch(self, QAA_Matrix_Symbolisch: sp.Matrix, liste_beobachtungen_symbolisch: list) -> np.Matrix:
"""Erstellt eine numerische Matrix aus einer symbolischen QAA-Matrix.
Es werden die numerischen Standardabweichungen der Anschlussbedingungen sowie die Varianzkomponente der Gruppe
"Anschlusspunkte" aus der Datenbank abgefragt und substituiert. Zur Rechenzeitersparnis wird ein Lambdify-Cache geführt,
der bei Änderung der Symbolmenge neu aufgebaut.
Die numerische Matrix wird als CSV-Datei in Zwischenergebnisse\\QAA_Numerisch.csv exportiert.
:param QAA_Matrix_Symbolisch: Symbolische Varianz-Kovarianz-Matrix QAA.
:type QAA_Matrix_Symbolisch: sp.Matrix
:param liste_beobachtungen_symbolisch: Liste der symbolischen Beobachtungskennungen.
:type liste_beobachtungen_symbolisch: list
:return: Numerische Varianz-Kovarianz-Matrix QAA als Numpy-Array.
:rtype: np.Matrix
"""
# Symbolische Listen
liste_beobachtungen_symbolisch = [str(b).strip() for b in liste_beobachtungen_symbolisch]
liste_beobachtungen_symbolisch = [b for b in liste_beobachtungen_symbolisch if b.startswith("lA_")]
db_zugriff = Datenbankzugriff(pfad_datenbank)
dict_stabwAA_vorinfo = db_zugriff.get_stabw_AA_Netzpunkte()
liste_varianzkomponenten = db_zugriff.get_varianzkomponentenschaetzung()
# Abfrage der numerischen Werte aus der Datenbank
dict_stabwAA_vorinfo = self.db_zugriff.get_stabw_AA_Netzpunkte()
liste_varianzkomponenten = self.db_zugriff.get_varianzkomponentenschaetzung()
# Zuordnen des Symbols zum numerischen Wert
substitutionen = {}
for koordinate, stabwAA in dict_stabwAA_vorinfo.items():
substitutionen[sp.Symbol(str(koordinate).strip())] = float(stabwAA)
@@ -412,11 +452,13 @@ class StochastischesModell:
varianz_varianzkomponentenschaetzung) in liste_varianzkomponenten:
substitutionen[sp.Symbol(f"varkomp_{instrumenteID}_Anschlusspunkte")] = float(varianz_varianzkomponentenschaetzung)
# Speichern der MAtrix in einer Instanzvariablen
if not hasattr(self, "func_QAA_numerisch"):
self.func_QAA_numerisch = None
if not hasattr(self, "liste_symbole_lambdify_QAA"):
self.liste_symbole_lambdify_QAA = []
# Substituieren
if (self.func_QAA_numerisch is None) or (set(self.liste_symbole_lambdify_QAA) != set(substitutionen.keys())):
self.liste_symbole_lambdify_QAA = sorted(substitutionen.keys(), key=lambda s: str(s))
self.func_QAA_numerisch = sp.lambdify(
@@ -440,21 +482,66 @@ class StochastischesModell:
return QAA_numerisch
@staticmethod
def berechne_P(Q_ll):
def berechne_P(Q_ll: np.ndarray) -> np.ndarray:
"""Berechnet die Gewichtsmatrix P aus einer Varianz-Kovarianz-Matrix Qll.
Die Gewichtsmatrix wird als Inverse von Qll gebildet: P = inv(Qll).
:param Q_ll: Varianz-Kovarianz-Matrix der Beobachtungen.
:type Q_ll: np.ndarray
:return: Gewichtsmatrix P.
:rtype: np.ndarray
"""
P = np.linalg.inv(Q_ll)
return P
@staticmethod
def berechne_Q_xx(N):
def berechne_Q_xx(N: np.ndarray) -> np.ndarray:
"""Berechnet die Kofaktormatrix der Unbekannten Qxx aus der Normalgleichungsmatrix N.
Die Kofaktormatrix wird als Inverse der Normalgleichungsmatrix gebildet: Qxx = inv(N).
Vor der Inversion wird geprüft, ob N quadratisch ist.
:param N: Normalgleichungsmatrix.
:type N: np.ndarray
:return: Kofaktormatrix der Unbekannten Qxx.
:rtype: np.ndarray
:raises ValueError: Falls N nicht quadratisch ist.
"""
if N.shape[0] != N.shape[1]:
raise ValueError("N muss eine quadratische Matrix sein")
Qxx = np.linalg.inv(N)
return Qxx
def berechne_Q_ll_dach(A, Q_xx):
@staticmethod
def berechne_Q_ll_dach(A: np.ndarray, Q_xx: np.ndarray) -> np.ndarray:
"""Berechnet die (geschätzte) Varianz-Kovarianz-Matrix der Beobachtungen Qll_dach.
Die Matrix wird gemäß Qll_dach = A @ Qxx @ A.T gebildet.
:param A: Jacobi-Matrix.
:type A: np.ndarray
:param Q_xx: Kofaktormatrix der Unbekannten.
:type Q_xx: np.ndarray
:return: Geschätzte Varianz-Kovarianz-Matrix der Beobachtungen Qll_dach.
:rtype: np.ndarray
"""
Q_ll_dach = A @ Q_xx @ A.T
return Q_ll_dach
def berechne_Qvv(Q_ll, Q_ll_dach):
@staticmethod
def berechne_Qvv(Q_ll: np.ndarray, Q_ll_dach: np.ndarray) -> np.ndarray:
"""Berechnet die Varianz-Kovarianz-Matrix der Residuen Qvv.
Die Residuenkovarianz wird als Differenz aus Beobachtungs-Kovarianz und dem durch das Modell erklärten Anteil gebildet:
Qvv = Qll - Qll_dach.
:param Q_ll: Varianz-Kovarianz-Matrix der Beobachtungen.
:type Q_ll: np.ndarray
:param Q_ll_dach: Geschätzte Varianz-Kovarianz-Matrix der Beobachtungen.
:type Q_ll_dach: np.ndarray
:return: Varianz-Kovarianz-Matrix der Residuen Qvv.
:rtype: np.ndarray
"""
Q_vv = Q_ll - Q_ll_dach
return Q_vv