import numpy as np
import plotly.graph_objects as go
from scipy.stats import f
import pandas as pd
from IPython.display import HTML
from IPython.display import display
import Berechnungen
import Einheitenumrechnung
class Genauigkeitsmaße:
"""Berechnung von Genauigkeitsmaße zur Bewertung der erreichten Netzqualität.
Die Klasse stellt Methoden zur Verfügung für:
- Berechnung der a posteriori Standardabweichung der Gewichtseinheit σ̂₀
- Berechnung des Helmertschen Punktfehlers (2D/3D),
- Berechnung der Standardellipsoide,
- Berechnung der Konfidenzellipsoide auf Basis eines Konfidenzniveaus (alpha) mit Skalierung über die F-Verteilung,
- Berechnung der Konfidenzellipsen auf Basis eines Konfidenzniveaus (alpha) mit Skalierung über die F-Verteilung,
- Berechnung von Konfidenzellipsen im lokalen ENU-System durch Transformation von Qxx zu Qxx_ENU,
inkl. Ausgabe/Export tabellarischer Ergebnisse.
"""
@staticmethod
def berechne_sigma0apost(v: np.ndarray, P: np.ndarray, r: int, print_ausgabe = True) -> float:
"""
Berechnet die a-posteriori Standardabweichung der Gewichtseinheit σ̂₀.
Die a posteriori Standardabweichung dient als Qualitätsmaß für die
Ausgleichung nach der Methode der kleinsten Quadrate. Dabei beschreibt
r die Redundanz (Freiheitsgrade).
:param v: Residuenvektor der Beobachtungen.
:type v: np.ndarray
:param P: Gewichtsmatrix der Beobachtungen.
:type P: np.ndarray
:param r: Redundanz bzw. Anzahl der Freiheitsgrade der Ausgleichung.
:type r: int
:return: a-posteriori Standardabweichung der Gewichtseinheit σ̂₀.
:rtype: float
"""
vTPv_matrix = v.T @ P @ v
vTPv = vTPv_matrix.item()
sigma0apost = np.sqrt(vTPv / r)
if print_ausgabe:
print(f"σ̂₀ a posteriori beträgt: {sigma0apost:.4f}")
return sigma0apost
@staticmethod
def helmert_punktfehler(Qxx: np.ndarray, sigma0_apost: float, unbekannten_liste: list) -> pd.DataFrame:
"""
Berechnet den Helmertschen Punktfehler (2D/3D) anhand der Standardabweichungen der Koordinaten der Punkte.
Aus der Kofaktor-Matrix der Unbekannten Qxx werden die Kofaktoren punktweise ausgelesen. Durch Multiplikation
mit der a posteriori Standardabweichung der Gewichtseinheit σ̂₀ werden die Standardabweichungen je Koordinate
(σx, σy, σz) sowie der Helmert'sche Punktfehler σP berechnet:
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste.
Die Dimension (2D/3D) wird interaktiv per Eingabe abgefragt. Zusätzlich werden die
Ergebnisse als Tabelle ausgegeben und in eine Excel-Datei exportiert.
:param Qxx: Kofaktor-Matrix der Unbekannten.
:type Qxx: np.ndarray
:param sigma0_apost: a-posteriori Standardabweichung der Gewichtseinheit σ̂₀.
:type sigma0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:return: Tabelle mit Standardabweichungen und Helmertschem Punktfehler je Punkt.
:rtype: pandas.DataFrame
"""
dim = int(input("Helmertscher Punktfehler (2 = 2D, 3 = 3D): "))
diagQ = np.diag(Qxx)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = []
for n in namen_str:
if n.upper().startswith('X'):
punkt_ids.append(n[1:])
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())
qx = diagQ[idx_x]
qy = diagQ[idx_y]
qz = 0.0
if dim == 3:
try:
idx_z = next(i for i, n in enumerate(namen_str) if n.upper() == f"Z{pid}".upper())
qz = diagQ[idx_z]
except StopIteration:
qz = 0.0
sx = sigma0_apost * np.sqrt(qx)
sy = sigma0_apost * np.sqrt(qy)
sz = sigma0_apost * np.sqrt(qz) if dim == 3 else 0
sP = sigma0_apost * np.sqrt(qx + qy + qz)
daten.append([pid, float(sx), float(sy), float(sz), float(sP)])
except:
continue
helmert_punktfehler = pd.DataFrame(daten, columns=["Punkt", "σ̂x", "σ̂y", "σ̂z", f"σ̂P_{dim}D"])
mittel_sP = helmert_punktfehler[f"σ̂P_{dim}D"].mean()
print(f"Mittlerer Helmert-Punktfehler über alle Punkte: {mittel_sP:.6f} [m]")
display(HTML(helmert_punktfehler.to_html(index=False)))
helmert_punktfehler.to_excel(r"Netzqualitaet\Standardabweichungen_Helmertscher_Punktfehler.xlsx",index=False)
return helmert_punktfehler
@staticmethod
def standardellipsoid(Qxx: np.ndarray, sigma0_apost: float, unbekannten_liste: list) -> pd.DataFrame:
"""
Berechnet das Standardellipsoid aus Qxx und σ̂₀ a posteriori.
Für jeden Punkt wird aus der Kofaktor-Matrix der Unbekannten Qxx die 3×3-Submatrix
der Koordinaten (X, Y, Z) gebildet. Daraus werden zunächst die Standardabweichungen
σx, σy, σz bestimmt. Anschließend werden über eine Eigenwertzerlegung der Submatrix
die Halbachsen des (punktbezogenen) Fehlerellipsoids berechnet:
- Halbachse a aus dem größten Eigenwert,
- Halbachse b aus dem mittleren Eigenwert,
- Halbachse c aus dem kleinsten Eigenwert.
Die Orientierung des Ellipsoids im Raum wird über die zugehörigen Eigenvektoren bestimmt.
Aus der Modalmatrix M, bestehend aus den sortierten Eigenvektoren, werden die drei Eulerwinkel α, β und γ
berechnet.
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste.
Die Ergebnisse werden tabellarisch ausgegeben und in eine Excel-Datei exportiert.
:param Qxx: Kofaktor-Matrix der Unbekannten.
:type Qxx: np.ndarray
:param sigma0_apost: A-posteriori Standardabweichung der Gewichtseinheit σ̂₀.
:type sigma0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:return: Tabelle mit Standardabweichungen, Ellipsoid-Halbachsen, Eulerwinkel.
:rtype: pandas.DataFrame
"""
Qxx = np.asarray(Qxx, float)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = []
for n in namen_str:
if n.upper().startswith('X'):
punkt_ids.append(n[1:])
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())
indices = [idx_x, idx_y, idx_z]
Q_sub = Qxx[np.ix_(indices, indices)]
# Standardabweichungen
sx = sigma0_apost * np.sqrt(Q_sub[0, 0])
sy = sigma0_apost * np.sqrt(Q_sub[1, 1])
sz = sigma0_apost * np.sqrt(Q_sub[2, 2])
# Eigenwertzerlegung
eigenwerte, eigenvektoren = np.linalg.eigh(Q_sub)
reihenfolge = np.argsort(eigenwerte)[::-1]
eigenwerte = eigenwerte[reihenfolge]
eigenwerte = np.clip(eigenwerte, 0.0, None)
M = eigenvektoren[:, reihenfolge]
if np.linalg.det(M) < 0:
M[:, 2] *= -1
# Halbachsen des Standardellipsoids
s_a = sigma0_apost * np.sqrt(eigenwerte[0]) # Große Halbachse a
s_b = sigma0_apost * np.sqrt(eigenwerte[1]) # Mittlere Halbachse b
s_c = sigma0_apost * np.sqrt(eigenwerte[2]) # Kleine Halbachse c
# Eulerwinkel aus M
beta = np.arcsin(-M[2, 0])
alpha = np.arctan2(M[2, 1], M[2, 2])
gamma = np.arctan2(M[1, 0], M[0, 0])
alpha_gon = (alpha * 200 / np.pi)% 400.0
beta_gon = (beta * 200 / np.pi)% 400.0
gamma_gon = (gamma * 200 / np.pi)% 400.0
daten.append([
pid,
float(sx), float(sy), float(sz),
float(s_a), float(s_b), float(s_c),
float(alpha_gon), float(beta_gon), float(gamma_gon)
])
except:
continue
standardellipsoid = pd.DataFrame(daten, columns=["Punkt", "σ̂x [m]", "σ̂y [m]", "σ̂z [m]", "Halbachse a [m]", "Halbachse b [m]", "Halbachse c [m]", "α [gon]", "β [gon]", "γ [gon]"])
standardellipsoid["σ̂x [m]"] = standardellipsoid["σ̂x [m]"].astype(float).round(6)
standardellipsoid["σ̂y [m]"] = standardellipsoid["σ̂y [m]"].astype(float).round(6)
standardellipsoid["σ̂z [m]"] = standardellipsoid["σ̂z [m]"].astype(float).round(6)
standardellipsoid["Halbachse a [m]"] = standardellipsoid["Halbachse a [m]"].astype(float).round(6)
standardellipsoid["Halbachse b [m]"] = standardellipsoid["Halbachse b [m]"].astype(float).round(6)
standardellipsoid["Halbachse c [m]"] = standardellipsoid["Halbachse c [m]"].astype(float).round(6)
standardellipsoid["α [gon]"] = standardellipsoid["α [gon]"].astype(float).round(3)
standardellipsoid["β [gon]"] = standardellipsoid["β [gon]"].astype(float).round(3)
standardellipsoid["γ [gon]"] = standardellipsoid["γ [gon]"].astype(float).round(3)
display(HTML(standardellipsoid.to_html(index=False)))
standardellipsoid.to_excel(r"Netzqualitaet\Standardellipsoid.xlsx", index=False)
return standardellipsoid
@staticmethod
def konfidenzellipsoid(Qxx: np.ndarray, sigma0_apost: float, unbekannten_liste: list, R: int) -> tuple[pd.DataFrame, float]:
"""
Berechnet das Konfidenzellipsoid aus Qxx, σ̂₀ a posteriori und einem Konfidenzniveau.
Auf Basis der Kovarianz-Matrix der Unbekannten Qxx und der a posteriori
Standardabweichung der Gewichtseinheit σ̂₀ werden für jeden Punkt die Parameter
des Konfidenzellipsoids berechnet. Das Konfidenzniveau wird mittels einer Eingabe
über alpha festgelegt.
Für jeden Punkt wird die 3×3-Submatrix der Koordinaten aus Qxx gebildet.
Über eine Eigenwertzerlegung dieser Submatrix werden die drei Hauptachsen des
Ellipsoids bestimmt. Die Halbachsen des Konfidenzellipsoids ergeben sich aus:
- Aₖ: große Halbachse des Konfidenzellipsoids,
- Bₖ: mittlere Halbachse des Konfidenzellipsoids,
- Cₖ: kleine Halbachse des Konfidenzellipsoids,
unter Verwendung eines Faktors aus der F-Verteilung in Abhängigkeit vom Konfidenzniveau und den Freiheitsgraden.
Die Orientierung des Ellipsoids im Raum wird über die zugehörigen Eigenvektoren bestimmt.
Aus der Modalmatrix M, bestehend aus den sortierten Eigenvektoren, werden die drei Eulerwinkel α, β und γ
berechnet.
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste.
Optional wird die Tabelle ausgegeben und als Excel-Datei exportiert.
:param Qxx: Kofaktor-Matrix der geschätzten Unbekannten.
:type Qxx: np.ndarray
:param sigma0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type sigma0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:param R: Redundanz (Freiheitsgrade) für die F-Verteilung.
:type R: int
:return: Tabelle der Konfidenzellipse je Punkt, verwendetes alpha.
:rtype: tuple[pandas.DataFrame, float]
"""
alpha_input = input("Irrtumswahrscheinlichkeit α wählen (z.B. 0.05, 0.01) [Standard=0.05]: ")
if alpha_input.strip() == "":
alpha = 0.05
else:
alpha = float(alpha_input)
print(f"→ Verwende alpha = {alpha} (Konfidenz = {(1 - alpha) * 100:.1f}%)")
Qxx = np.asarray(Qxx, float)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = [n[1:] for n in namen_str if n.upper().startswith('X')]
# Faktor fürs Konfidenzellipoid (F-Verteilung)
k = float(np.sqrt(3.0 * f.ppf(1.0 - alpha, 3, R)))
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())
indices = [idx_x, idx_y, idx_z]
Q_sub = Qxx[np.ix_(indices, indices)]
# Standardabweichungen
sx = sigma0_apost * np.sqrt(Q_sub[0, 0])
sy = sigma0_apost * np.sqrt(Q_sub[1, 1])
sz = sigma0_apost * np.sqrt(Q_sub[2, 2])
# Eigenwertzerlegung
eigenwerte, eigenvektoren = np.linalg.eigh(Q_sub)
order = np.argsort(eigenwerte)[::-1]
eigenwerte = eigenwerte[order]
eigenwerte = np.clip(eigenwerte, 0.0, None)
M = eigenvektoren[:, order]
if np.linalg.det(M) < 0:
M[:, 2] *= -1
# Halbachsen des Konfidenzellipoid
A_K = k * sigma0_apost * np.sqrt(eigenwerte[0])
B_K = k * sigma0_apost * np.sqrt(eigenwerte[1])
C_K = k * sigma0_apost * np.sqrt(eigenwerte[2])
# Eulerwinkel aus M
beta = np.arcsin(-M[2, 0])
alpha_ = np.arctan2(M[2, 1], M[2, 2])
gamma = np.arctan2(M[1, 0], M[0, 0])
alpha_gon = (alpha_ * 200.0 / np.pi) % 400.0
beta_gon = (beta * 200.0 / np.pi) % 400.0
gamma_gon = (gamma * 200.0 / np.pi) % 400.0
daten.append([
pid,
float(sx), float(sy), float(sz),
float(A_K), float(B_K), float(C_K),
float(alpha_gon), float(beta_gon), float(gamma_gon)
])
except:
continue
konfidenzellipoid = pd.DataFrame(daten, columns=["Punkt", "σ̂x [m]", "σ̂y [m]", "σ̂z [m]", "Halbachse a_k [m]", "Halbachse b_k [m]", "Halbachse c_k [m]", "α [gon]", "β [gon]", "γ [gon]"])
konfidenzellipoid["σ̂x [m]"] = konfidenzellipoid["σ̂x [m]"].astype(float).round(6)
konfidenzellipoid["σ̂y [m]"] = konfidenzellipoid["σ̂y [m]"].astype(float).round(6)
konfidenzellipoid["σ̂z [m]"] = konfidenzellipoid["σ̂z [m]"].astype(float).round(6)
konfidenzellipoid["Halbachse a_k [m]"] = konfidenzellipoid["Halbachse a_k [m]"].astype(float).round(6)
konfidenzellipoid["Halbachse b_k [m]"] = konfidenzellipoid["Halbachse b_k [m]"].astype(float).round(6)
konfidenzellipoid["Halbachse c_k [m]"] = konfidenzellipoid["Halbachse c_k [m]"].astype(float).round(6)
konfidenzellipoid["α [gon]"] = konfidenzellipoid["α [gon]"].astype(float).round(3)
konfidenzellipoid["β [gon]"] = konfidenzellipoid["β [gon]"].astype(float).round(3)
konfidenzellipoid["γ [gon]"] = konfidenzellipoid["γ [gon]"].astype(float).round(3)
display(HTML(konfidenzellipoid.to_html(index=False)))
konfidenzellipoid.to_excel(r"Netzqualitaet\Konfidenzellipoid.xlsx", index=False)
return konfidenzellipoid, alpha
@staticmethod
def konfidenzellipsen(Qxx: np.ndarray, sigma0_apost: float, unbekannten_liste: list, r_gesamt: int) -> pd.DataFrame:
"""
Berechnet Konfidenzellipsen für Punkte aus der Kofaktor-Matrix Qxx.
Für jeden Punkt werden aus der 2×2-Submatrix der Koordinaten die Parameter der Konfidenzellipse bestimmt.
Grundlage ist die a posteriori Standardabweichung der Gewichtseinheit sowie ein Konfidenzniveau, das über
die Irrtumswahrscheinlichkeit α festgelegt wird.
Die Halbachsen der Konfidenzellipse werden aus den Eigenwerten der Kovarianzmatrix berechnet.
Der Skalierungsfaktor ergibt sich aus der F-Verteilung.Der Richtungswinkel θ wird aus dem
Eigenvektor der großen Halbachse bestimmt und in gon ausgegeben.
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste.
:param Qxx: Kofaktor-Matrix der geschätzten Unbekannten.
:type Qxx: numpy.ndarray
:param sigma0_apost: a posteriori Standardabweichung der Gewichtseinheit.
:type sigma0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:param r_gesamt: Redundanz (Freiheitsgrade) des Netzes.
:type r_gesamt: int
:return: Tabelle der Konfidenzellipsen je Punkt.
:rtype: pd.DataFrame
"""
# Irrtumswahrscheinlichkeit alpha
alpha_input = input("Irrtumswahrscheinlichkeit α wählen (z.B. 0.05, 0.01) [Standard=0.05]: ")
if alpha_input.strip() == "":
alpha = 0.05
else:
alpha = float(alpha_input)
print(f"→ Verwende alpha = {alpha} (Konfidenz = {(1 - alpha) * 100:.1f}%)")
Qxx = np.asarray(Qxx, float)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = [n[1:] for n in namen_str if n.upper().startswith("X")]
# Faktor für die Konfidenzellipe (F-Verteilung)
k2 = float(np.sqrt(2.0 * f.ppf(1.0 - alpha, 2, r_gesamt)))
for pid in punkt_ids:
try:
idx_1 = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
idx_2 = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())
index = [idx_1, idx_2]
Q_sub = Qxx[np.ix_(index, index)]
# Standardabweichungen
sigma1 = sigma0_apost * np.sqrt(Q_sub[0, 0])
sigma2 = sigma0_apost * np.sqrt(Q_sub[1, 1])
# Eigenwertzerlegung 2D
eigenwerte, eigenvektoren = np.linalg.eigh(Q_sub)
reihenfolge = np.argsort(eigenwerte)[::-1]
eigenwerte = eigenwerte[reihenfolge]
eigenwerte = np.clip(eigenwerte, 0.0, None)
eigenvektoren = eigenvektoren[:, reihenfolge]
# Halbachsen der Konfidenzellipse
a = k2 * sigma0_apost * np.sqrt(eigenwerte[0])
b = k2 * sigma0_apost * np.sqrt(eigenwerte[1])
# Winkel aus Eigenvektor der großen Halbachse
v = eigenvektoren[:, 0]
t_gon = (np.arctan2(v[1], v[0]) * (200.0 / np.pi)) % 400.0
daten.append([pid, float(sigma1), float(sigma2), float(a), float(b), float(t_gon)])
except StopIteration:
continue
Konfidenzellipse = pd.DataFrame(daten, columns=[
"Punkt", "σ̂x [m]", "σ̂y [m]",
"Halbachse a_k [m]", "Halbachse b_k [m]", "θ [gon]"
])
return Konfidenzellipse
@staticmethod
def konfidenzellipsen_enu(a: float, b: float, ausgabe_parameterschaetzung: dict, liste_unbekannte: list, ausgleichungsergebnis: dict, sigma0apost: float, r_gesamt: int) -> tuple[pd.DataFrame, np.ndarray]:
"""
Berechnet Konfidenzellipsen im lokalen ENU-System aus einer ins ENU-System transformierten Qxx-Matrix.
Die Funktion transformiert zunächst die Kofaktor-Matrix der Unbekannten Qxx
in ein East-North-Up-System (ENU) bezogen auf den Schwerpunkt der verwendeten
Punkte (B0, L0). Anschließend wird auf Basis der transformierten Matrix die
Konfidenzellipse über die Funktion "konfidenzellipsen" bestimmt.
Zum Schluss werden Spaltennamen an die ENU-Notation angepasst, Werte gerundet,
tabellarisch ausgegeben und als Excel-Datei exportiert.
:param a: Große Halbachse a des Referenzellipsoids (z.B. WGS84/GRS80) in Metern.
:type a: float
:param b: Große Halbachse b des Referenzellipsoids (z.B. WGS84/GRS80) in Metern.
:type b: float
:param ausgabe_parameterschaetzung: Dictonary der Ergebnisse der Parameterschätzung, muss "Q_xx" enthalten.
:type ausgabe_parameterschaetzung: dict
:param liste_unbekannte: Liste der Unbekannten.
:type liste_unbekannte: list
:param ausgleichungsergebnis: Dictionary der geschätzten Punktkoordinaten (XYZ) zur ENU-Referenzbildung.
:type ausgleichungsergebnis: dict
:param sigma0apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type sigma0apost: float
:param r_gesamt: Redundanz (Freiheitsgrade) für die Konfidenzberechnung.
:type r_gesamt: int
:return: Tabelle der Konfidenzellipse im ENU-System, Rotationsmatrix R0 der ENU-Transformation.
:rtype: tuple[pandas.DataFrame, numpy.ndarray]
"""
berechnungen = Berechnungen.Berechnungen(a, b)
# 1) Qxx ins ENU-System transformieren
Qxx_enu, (B0, L0), R0 = Berechnungen.ENU.transform_Qxx_zu_QxxENU(
Qxx=ausgabe_parameterschaetzung["Q_xx"],
unbekannten_liste= liste_unbekannte,
berechnungen=berechnungen,
dict_xyz= ausgleichungsergebnis,
)
print(
f"ENU-Referenz (Schwerpunkt): B0={Einheitenumrechnung.Einheitenumrechnung.rad_to_gon_Decimal(B0):.8f} rad, L0={Einheitenumrechnung.Einheitenumrechnung.rad_to_gon_Decimal(L0):.8f} rad")
# 2) Konfidenzellipsen im ENU-System
Konfidenzellipse_ENU = Genauigkeitsmaße.konfidenzellipsen(
Qxx_enu,
sigma0apost,
liste_unbekannte,
r_gesamt
)
# 3) Spaltennamen anpassen
Konfidenzellipse_ENU = Konfidenzellipse_ENU.rename(columns={
"σ̂x [m]": "σ̂E [m]",
"σ̂y [m]": "σ̂N [m]",
"Halbachse a_k [m]": "Halbachse a_k_EN [m]",
"Halbachse b_k [m]": "Halbachse b_k_EN [m]",
"θ [gon]": "θ_EN [gon]"
})
# 4) Runden und Anzeigen
Konfidenzellipse_ENU["σ̂E [m]"] = Konfidenzellipse_ENU["σ̂E [m]"].round(6)
Konfidenzellipse_ENU["σ̂N [m]"] = Konfidenzellipse_ENU["σ̂N [m]"].round(6)
Konfidenzellipse_ENU["Halbachse a_k_EN [m]"] = Konfidenzellipse_ENU["Halbachse a_k_EN [m]"].round(6)
Konfidenzellipse_ENU["Halbachse b_k_EN [m]"] = Konfidenzellipse_ENU["Halbachse b_k_EN [m]"].round(6)
Konfidenzellipse_ENU["θ_EN [gon]"] = Konfidenzellipse_ENU["θ_EN [gon]"].round(3)
display(HTML(Konfidenzellipse_ENU.to_html(index=False)))
# 5) Export
Konfidenzellipse_ENU.to_excel(r"Netzqualitaet\Konfidenzellipse_ENU.xlsx", index=False)
return Konfidenzellipse_ENU, R0
class Plot:
"""Visualisierung geodätischer Netze und Genauigkeitsmaße.
Die Klasse stellt Methoden zur Verfügung für:
- grafische Darstellung von geodätischen Netzen im lokalen ENU-System,
- Visualisierung von Beobachtungen als Verbindungslinien,
- Darstellung von Konfidenzellipsen,
- interaktive Netzdarstellung mit Plotly inklusive Hover-Informationen,
- Skalierung und Layout-Anpassung zur anschaulichen Präsentation von
Lagegenauigkeiten.
Die Klasse dient ausschließlich der Ergebnisvisualisierung und nimmt keine
numerischen Berechnungen vor.
"""
@staticmethod
def netzplot_ellipsen(Koord_ENU: dict[str, tuple[float, float, float]], unbekannten_labels: list, beobachtungs_labels: list, df_konf_ellipsen_enu: pd.DataFrame, skalierung: float, n_ellipse_pts: int = 60, titel: str = "Netzplot im ENU-System mit Konfidenzellipsen") -> go.Figure:
"""
Erstellt einen Netzplot im ENU-System inklusive Konfidenzellipsen, Netzpunkten und Beobachtungslinien.
Die Funktion visualisiert das geodätische Netz im East-North-Up-System (ENU) mit Plotly.
Dabei werden:
- Beobachtungen als Verbindungslinien zwischen Punkten dargestellt,
- Konfidenzellipsen je Punkt,
- Netzpunkte mit Punkt-ID und Koordinaten im Hover-Text angezeigt.
Die Ellipsen werden zur besseren Sichtbarkeit mit dem Faktor "skalierung" vergrößert.
Zusätzlich wird eine PNG-Grafik über "plot.write_image(...)" exportiert.
:param Koord_ENU: Dictionary der Punktkoordinaten im ENU-System.
:type Koord_ENU: dict
:param unbekannten_labels: Liste der Unbekannten zur Ableitung der Punkt-IDs.
:type unbekannten_labels: list
:param beobachtungs_labels: Liste der Beobachtungen zur Ableitung von Verbindungslinien.
:type beobachtungs_labels: list
:param df_konf_ellipsen_enu: DataFrame mit Konfidenzellipsenparametern je Punkt.
:type df_konf_ellipsen_enu: pandas.DataFrame
:param skalierung: Faktor zur visuellen Vergrößerung der Ellipsen im Plot.
:type skalierung: float
:param n_ellipse_pts: Anzahl der Stützpunkte zur Approximation der Ellipse.
:type n_ellipse_pts: int
:param titel: Titel des Plots.
:type titel: str
:return: Plotly-Figure-Objekt mit Netz, Beobachtungen und Ellipsen.
:rtype: plotly.graph_objects.Figure
"""
namen = [str(s).strip() for s in unbekannten_labels]
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("Spalte 'θ_EN [gon]' oder 'θ [gon]' fehlt im DataFrame.")
punkt_ids = sorted({nm[1:] for nm in namen if nm and nm[0].upper() in ("X", "Y", "Z")})
plot = go.Figure()
# 1) Darstellungen der Beobachtungen
beob_typen = {
'GNSS-Basislinien': {'pattern': 'gnss', 'color': 'rgba(255, 100, 0, 0.4)'},
'Tachymeter-Beob': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
}
for typ, info in beob_typen.items():
x_linie, y_linie = [], []
for beob in beobachtungs_labels:
bl_str = str(beob).lower()
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
beob_text = str(beob)
punkte = []
for pid in punkt_ids:
if (f"_{pid}" in beob_text) or beob_text.startswith(f"{pid}_"):
if pid in Koord_ENU:
punkte.append(pid)
if len(punkte) >= 2:
punkt1, punkt2 = punkte[0], punkte[1]
x_linie.extend([Koord_ENU[punkt1][0], Koord_ENU[punkt2][0], None]) # E
y_linie.extend([Koord_ENU[punkt1][1], Koord_ENU[punkt2][1], None]) # N
if x_linie:
plot.add_trace(go.Scatter(x=x_linie, y=y_linie, mode='lines', name=typ,
line=dict(color=info['color'], width=1)))
# 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
a = float(row["Halbachse a_k_EN [m]"]) * skalierung
b = float(row["Halbachse b_k_EN [m]"]) * skalierung
theta = float(row["θ_EN [gon]"]) * np.pi / 200.0
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]
plot.add_trace(go.Scatter(
x=E0 + xr, y=N0 + yr,
mode="lines",
line=dict(color="red", width=1.5),
name=f"Ellipsen (×{skalierung})",
legendgroup="Ellipsen",
showlegend=first,
hoverinfo="skip"
))
first = False
# 3) Darstellung der Punkte
xs, ys, texts, hovers = [], [], [], []
for pid in punkt_ids:
if pid not in Koord_ENU:
continue
E, N, U = Koord_ENU[pid]
xs.append(E);
ys.append(N);
texts.append(pid)
hovers.append(f"Punkt {pid}
E={E:.4f} m
N={N:.4f} m
U={U:.4f} m")
plot.add_trace(go.Scatter(
x=xs, y=ys, mode="markers+text",
text=texts, textposition="top center",
marker=dict(size=8, color="black"),
name="Netzpunkte",
hovertext=hovers, hoverinfo="text"
))
plot.update_layout(
title=f"{titel} (Ellipsen ×{skalierung})",
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,
template="plotly_white",
plot_bgcolor="white"
)
plot.add_annotation(
text=f"Maßstab Ellipsen:
Dargestellte Größe = Konfidenzellipse × {skalierung}",
align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.05,
bgcolor="white", bordercolor="black", borderwidth=1
)
plot.write_image(r"Netzqualitaet\netzplot_ellipsen_volle_ausdehnung.png")
return plot
def plot_speichere_aktuelle_ansicht(plot: go.Figure, dateiname: str = r"Netzqualitaet\netzplot_ellipsen_zoom_ansicht.png") -> None:
"""
Speichert die aktuell dargestellte Plot-Ansicht als Bilddatei.
Die Funktion übernimmt ein vorhandenes Plotly-Figure-Objekt und exportiert die momentan im Layout
eingestellte Ansicht (inklusive Zoom- oder Achsenbereich) als PNG-Datei. Dazu wird das aktuelle
Layout übernommen und in ein neues Figure-Objekt kopiert, welches anschließend mit
"write_image" gespeichert wird.
Zusätzlich werden die aktuellen Achsenbereiche (E/N) in der Konsole ausgegeben, um den gespeicherten
Ausschnitt zu dokumentieren.
:param plot: Plotly-Figure-Objekt mit dem darzustellenden Netzplot.
:type plot: plotly.graph_objects.Figure
:param dateiname: Dateipfad für die Ausgabedatei (Standard: PNG im Ordner Netzqualitaet).
:type dateiname: str
:return: None
:rtype: None
"""
aktuelles_layout = plot.layout
export_plot = go.Figure(plot.data, layout=aktuelles_layout)
export_plot.write_image(dateiname, scale=2)
print(f"✅ Aktuelle Ansicht wurde erfolgreich gespeichert: {dateiname}")
x_bereich = aktuelles_layout.xaxis.range
y_bereich = aktuelles_layout.yaxis.range
if x_bereich and y_bereich:
print(
f"Ausschnitt: E[{x_bereich[0]:.2f} bis {x_bereich[1]:.2f}], N[{y_bereich[0]:.2f} bis {y_bereich[1]:.2f}]")
else:
print("Hinweis: Es wurde die Standardansicht (kein Zoom) gespeichert.")