import numpy as np import plotly.graph_objects as go from scipy.stats import f as f_dist import pandas as pd class Genauigkeitsmaße: @staticmethod def berechne_s0apost(v: np.ndarray, P: np.ndarray, r: int) -> float: vTPv_matrix = v.T @ P @ v vTPv = float(vTPv_matrix.item()) s0apost = np.sqrt(vTPv / r) return float(s0apost) @staticmethod def helmert_punktfehler(Qxx, s0_apost, unbekannten_liste, dim=3): 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"]) return helmert_punktfehler def standardellipse(Qxx, s0_apost, unbekannten_liste, dim_labels=3): Qxx = np.asarray(Qxx, float) data = [] n_punkte = len(unbekannten_liste) // dim_labels for i in range(n_punkte): sym_x = str(unbekannten_liste[dim_labels * i]) # z.B. "X10009" punkt = sym_x[1:] # -> "10009" ix = dim_labels * i iy = dim_labels * i + 1 # 2x2-Kofaktorblock Qxx_ = Qxx[ix, ix] Qyy_ = Qxx[iy, iy] Qyx_ = Qxx[iy, ix] # Standardabweichungen der Koordinatenkomponenten sx = s0_apost * np.sqrt(Qxx_) sy = s0_apost * np.sqrt(Qyy_) sxy = (s0_apost ** 2) * Qyx_ # k und Eigenwerte (Q_dmax, Q_dmin) k = np.sqrt((Qxx_ - Qyy_) ** 2 + 4 * (Qyx_ ** 2)) Q_dmax = 0.5 * (Qxx_ + Qyy_ + k) Q_dmin = 0.5 * (Qxx_ + Qyy_ - k) # Halbachsen (Standardabweichungen entlang Hauptachsen) s_max = s0_apost * np.sqrt(Q_dmax) s_min = s0_apost * np.sqrt(Q_dmin) # Richtungswinkel theta (Hauptachse) in rad: theta_rad = 0.5 * np.arctan2(2 * Qyx_, (Qxx_ - Qyy_)) # in gon theta_gon = theta_rad * (200 / np.pi) if theta_gon < 0: theta_gon += 200.0 data.append([ punkt, float(sx), float(sy), float(sxy), float(s_max), float(s_min), float(theta_gon) ]) standardellipse = pd.DataFrame(data, columns=["Punkt", "σx", "σy", "σxy", "s_max", "s_min", "θ [gon]"]) return standardellipse def konfidenzellipse(Qxx, s0_apost, unbekannten_liste, R, alpha=0.05): Qxx = np.asarray(Qxx, float) data = [] n_punkte = len(unbekannten_liste) // 3 # X,Y,Z je Punkt angenommen k = float(np.sqrt(2.0 * f_dist.ppf(1.0 - alpha, 2, R))) for i in range(n_punkte): punkt = str(unbekannten_liste[3 * i])[1:] # "X10009" -> "10009" ix = 3 * i iy = 3 * i + 1 Qxx_ = Qxx[ix, ix] Qyy_ = Qxx[iy, iy] Qxy_ = Qxx[iy, ix] # = Qyx # k für Eigenwerte kk = np.sqrt((Qxx_ - Qyy_) ** 2 + 4 * (Qxy_ ** 2)) Q_dmax = 0.5 * (Qxx_ + Qyy_ + kk) Q_dmin = 0.5 * (Qxx_ + Qyy_ - kk) # Standard-Halbachsen (1-sigma) s_max = s0_apost * np.sqrt(Q_dmax) s_min = s0_apost * np.sqrt(Q_dmin) # Orientierung (Hauptachse) in gon theta_rad = 0.5 * np.arctan2(2 * Qxy_, (Qxx_ - Qyy_)) theta_gon = theta_rad * (200 / np.pi) if theta_gon < 0: theta_gon += 200.0 # Konfidenz-Halbachsen a_K = k * s_max b_K = k * s_min data.append([punkt, float(a_K), float(b_K), float(theta_gon)]) konfidenzellipsen = pd.DataFrame(data, columns=["Punkt", "a_K", "b_K", "θ [gon]"]) return konfidenzellipsen 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'/'Y'.") fig = go.Figure() # 2. Beobachtungen (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() 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. 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] 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 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])) 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)", legendgroup="Ellipsen", showlegend=(pt == pts_data[0]), # Nur einmal in der Legende zeigen hoverinfo='skip' )) # 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"Maßstab Ellipsen:
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')))) 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: 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])) 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}
a_K: {row['a_K']:.4f}m
b_K: {row['b_K']:.4f}m" )) # 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'), width=1100, height=900, plot_bgcolor='white') # Maßstabsangabe fig.add_annotation( text=f"Skalierung:
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" ) fig.show() # Aufruf