580 lines
26 KiB
Python
580 lines
26 KiB
Python
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 s₀
|
||
- Berechnung des Helmertschen Punktfehlers (2D/3D),
|
||
- Berechnung der Standardellipse (Helmertschen Fehlerellipse),
|
||
- Berechnung der Konfidenzellipse auf Basis eines Konfidenzniveaus (alpha) mit Skalierung über die F-Verteilung,
|
||
- Berechnung von Konfidenzellipsen im lokalen ENU-System durch Transformation von Qxx → 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 s₀.
|
||
|
||
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: numpy.ndarray
|
||
:param P: Gewichtsmatrix der Beobachtungen.
|
||
:type P: numpy.ndarray
|
||
:param r: Redundanz bzw. Anzahl der Freiheitsgrade der Ausgleichung.
|
||
:type r: int
|
||
:return: a-posteriori Standardabweichung der Gewichtseinheit s₀.
|
||
: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, s0_apost, unbekannten_liste):
|
||
"""
|
||
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 s₀ 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: numpy.ndarray
|
||
:param s0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
|
||
:type s0_apost: float
|
||
:param unbekannten_liste: Liste der Unbekannten.
|
||
:type unbekannten_liste: list
|
||
:return: Tabelle mit Standardabweichungen und Helmertschem Punktfehler je Punkt.
|
||
:rtype: pandas.DataFrame
|
||
:raises ValueError: Wenn eine ungültige Dimension (nicht 2 oder 3) eingegeben wird.
|
||
"""
|
||
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 = s0_apost * np.sqrt(qx)
|
||
sy = s0_apost * np.sqrt(qy)
|
||
sz = s0_apost * np.sqrt(qz) if dim == 3 else 0
|
||
|
||
sP = s0_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:.4f} [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, s0_apost, unbekannten_liste):
|
||
"""
|
||
Berechnet Achsenlängen des Fehlerellipsoids (a, b, c) sowie den 2D-Richtungswinkel θ aus Qxx und s₀ 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.
|
||
|
||
Zusätzlich wird zur zweidimensionalen Darstellung ein Richtungswinkel θ in der X/Y-Ebene
|
||
aus den Kovarianzanteilen (qxx, qyy, qyx) berechnet (Winkel in gon).
|
||
|
||
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: numpy.ndarray
|
||
:param s0_apost: A-posteriori Standardabweichung der Gewichtseinheit s₀.
|
||
:type s0_apost: float
|
||
:param unbekannten_liste: Liste der Unbekannten.
|
||
:type unbekannten_liste: list
|
||
:return: Tabelle mit Standardabweichungen (σx, σy, σz), Ellipsoid-Halbachsen (a, b, c) und Richtungswinkel θ je Punkt.
|
||
: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 = s0_apost * np.sqrt(Q_sub[0, 0])
|
||
sy = s0_apost * np.sqrt(Q_sub[1, 1])
|
||
sz = s0_apost * np.sqrt(Q_sub[2, 2])
|
||
|
||
# Eigenwertzerlegung
|
||
eigenwerte, eigenvektoren = np.linalg.eigh(Q_sub)
|
||
eigenwerte = np.sort(eigenwerte)[::-1]
|
||
|
||
# Halbachsen
|
||
s_a = s0_apost * np.sqrt(eigenwerte[0]) # Große Halbachse a
|
||
s_b = s0_apost * np.sqrt(eigenwerte[1]) # Mittlere Halbachse b
|
||
s_c = s0_apost * np.sqrt(eigenwerte[2]) # Kleine Halbachse c
|
||
|
||
# Richtungswinkel theta in gon:
|
||
qyx = Q_sub[1, 0]
|
||
qxx = Q_sub[0, 0]
|
||
qyy = Q_sub[1, 1]
|
||
t_gon = 0.5 * np.arctan2(2 * qyx, qxx - qyy) * (200 / np.pi)
|
||
if t_gon < 0:
|
||
t_gon += 200
|
||
|
||
daten.append([
|
||
pid, float(sx), float(sy), float(sz),
|
||
float(s_a), float(s_b), float(s_c), float(t_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]"])
|
||
standardellipsoid["σx [m]"] = standardellipsoid["σx [m]"].astype(float).round(4)
|
||
standardellipsoid["σy [m]"] = standardellipsoid["σy [m]"].astype(float).round(4)
|
||
standardellipsoid["σz [m]"] = standardellipsoid["σz [m]"].astype(float).round(4)
|
||
standardellipsoid["Halbachse a [m]"] = standardellipsoid["Halbachse a [m]"].astype(float).round(4)
|
||
standardellipsoid["Halbachse b [m]"] = standardellipsoid["Halbachse b [m]"].astype(float).round(4)
|
||
standardellipsoid["Halbachse c [m]"] = standardellipsoid["Halbachse c [m]"].astype(float).round(4)
|
||
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, s0_apost, unbekannten_liste, R, ausgabe_erfolgt):
|
||
"""
|
||
Berechnet die Konfidenzellipse für Punkte aus Qxx und einem Konfidenzniveau.
|
||
|
||
Auf Basis der Kovarianz-Matrix der Unbekannten Qxx und der a-posteriori
|
||
Standardabweichung der Gewichtseinheit s₀ werden für jeden Punkt die Parameter
|
||
der Konfidenzellipse berechnet. Das Konfidenzniveau wird mittels einer Eingabe
|
||
über alpha gewählt (Standard: 0.05 für 95%).
|
||
|
||
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste (z.B. X1, Y1).
|
||
Optional wird die Tabelle ausgegeben und als Excel-Datei exportiert, abhängig von
|
||
ausgabe_erfolgt.
|
||
|
||
:param Qxx: Kofaktor-Matrix der geschätzten Unbekannten.
|
||
:type Qxx: numpy.ndarray
|
||
:param s0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
|
||
:type s0_apost: float
|
||
:param unbekannten_liste: Liste der Unbekannten.
|
||
:type unbekannten_liste: list
|
||
:param R: Redundanz (Freiheitsgrade) für die F-Verteilung.
|
||
:type R: int
|
||
:param ausgabe_erfolgt: Steuert, ob eine Ausgabe/Dateischreibung erfolgen soll (False = Ausgabe).
|
||
:type ausgabe_erfolgt: bool
|
||
:return: Tabelle der Konfidenzellipse je Punkt, verwendetes alpha.
|
||
:rtype: tuple[pandas.DataFrame, float]
|
||
:raises ValueError: Wenn alpha nicht in (0, 1) liegt oder nicht in float umgewandelt werden kann.
|
||
"""
|
||
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 = s0_apost * np.sqrt(Q_sub[0, 0])
|
||
sy = s0_apost * np.sqrt(Q_sub[1, 1])
|
||
sz = s0_apost * np.sqrt(Q_sub[2, 2])
|
||
|
||
# Eigenwertzerlegung
|
||
eigenwerte, eigenvektoren = np.linalg.eigh(Q_sub)
|
||
eigenwerte = np.sort(eigenwerte)[::-1]
|
||
|
||
# Halbachsen des Konfidenzellipoid
|
||
A_K = k * s0_apost * np.sqrt(eigenwerte[0])
|
||
B_K = k * s0_apost * np.sqrt(eigenwerte[1])
|
||
C_K = k * s0_apost * np.sqrt(eigenwerte[2])
|
||
|
||
# Richtungswinkel theta in gon:
|
||
qyx = Q_sub[1, 0]
|
||
qxx = Q_sub[0, 0]
|
||
qyy = Q_sub[1, 1]
|
||
t_gon = 0.5 * np.arctan2(2 * qyx, qxx - qyy) * (200 / np.pi)
|
||
if t_gon < 0:
|
||
t_gon += 200
|
||
|
||
daten.append([
|
||
pid, float(sx), float(sy), float(sz),
|
||
float(A_K), float(B_K), float(C_K), float(t_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]"])
|
||
if ausgabe_erfolgt == False:
|
||
display(HTML(konfidenzellipoid.to_html(index=False)))
|
||
konfidenzellipoid.to_excel(r"Netzqualitaet\Konfidenzellipoid.xlsx", index=False)
|
||
return konfidenzellipoid, alpha
|
||
|
||
|
||
@staticmethod
|
||
def konfidenzellipsen_enu(a, b, ausgabe_parameterschaetzung, liste_unbekannte, ausgleichungsergebnis, s0apost, r_gesamt):
|
||
"""
|
||
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 "konfidenzellipsoid" 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 s0apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
|
||
:type s0apost: 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]
|
||
:raises KeyError: Wenn ``ausgabe_parameterschaetzung`` keinen Eintrag ``"Q_xx"`` enthält.
|
||
"""
|
||
|
||
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) Konfidenzellipoid im ENU-System
|
||
Konfidenzellipse_ENU, alpha = Genauigkeitsmaße.konfidenzellipsoid(
|
||
Qxx_enu,
|
||
s0apost,
|
||
liste_unbekannte,
|
||
r_gesamt,
|
||
ausgabe_erfolgt = True
|
||
)
|
||
|
||
# 3) Spaltennamen anpassen
|
||
Konfidenzellipse_ENU = Konfidenzellipse_ENU.rename(columns={
|
||
"σx [m]": "σE [m]",
|
||
"σy [m]": "σN [m]",
|
||
"σz [m]": "σU [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["σU [m]"] = Konfidenzellipse_ENU["σU [m]"].round(6)
|
||
Konfidenzellipse_ENU["Halbachse a_k [m]"] = Konfidenzellipse_ENU["Halbachse a_k [m]"].round(4)
|
||
Konfidenzellipse_ENU["Halbachse b_k [m]"] = Konfidenzellipse_ENU["Halbachse b_k [m]"].round(4)
|
||
Konfidenzellipse_ENU["Halbachse c_k [m]"] = Konfidenzellipse_ENU["Halbachse c_k [m]"].round(4)
|
||
Konfidenzellipse_ENU["θ_EN [gon]"] = Konfidenzellipse_ENU["θ_EN [gon]"].round(4)
|
||
|
||
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,
|
||
unbekannten_labels,
|
||
beobachtungs_labels,
|
||
df_konf_ellipsen_enu,
|
||
skalierung,
|
||
n_ellipse_pts=60,
|
||
titel="Netzplot im ENU-System mit Konfidenzellipsen"
|
||
):
|
||
|
||
"""
|
||
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 (als separate Traces/Legenden-Einträge),
|
||
- Konfidenzellipsen je Punkt (Halbachsen und Richtungswinkel),
|
||
- Netzpunkte mit Punkt-ID und Koordinaten im Hover-Text angezeigt.
|
||
|
||
Die Ellipsen werden zur besseren Sichtbarkeit mit dem Faktor "skalierung" vergrößert.
|
||
Der Richtungswinkel wird in gon erwartet und intern nach Radiant umgerechnet.
|
||
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
|
||
:raises ValueError: Wenn weder "θ_EN [gon]" noch "θ [gon]" im DataFrame vorhanden ist.
|
||
"""
|
||
|
||
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 [m]"]) * skalierung
|
||
b = float(row["Halbachse b_k [m]"]) * skalierung
|
||
theta = float(row[theta_col]) * np.pi / 200.0 # gon->rad
|
||
|
||
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}<br>E={E:.4f} m<br>N={N:.4f} m<br>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"<b>Maßstab Ellipsen:</b><br>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, dateiname=r"Netzqualitaet\netzplot_ellipsen_zoom_ansicht.png"):
|
||
"""
|
||
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
|
||
:raises OSError: Wenn die Bilddatei nicht geschrieben werden kann (z.B. fehlender Pfad oder fehlendes Kaleido).
|
||
"""
|
||
|
||
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.") |