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 Berechnungen import Berechnungen
from Datenbank import Datenbankzugriff
from Export import Export
from Koordinatentransformationen import Transformationen

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()
        self.dict_punkt_symbole = {}
        self.liste_symbole_lambdify = sorted(self.substitutionen_dict.keys(), key=lambda s: str(s))
        self.func_beob0 = None
        self.func_A0 = None
        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:
        """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"]

        # 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 = self.db_zugriff.get_puntknummern_beobachtungen_tachymeter(beobachtungsart)

                for beobachtungenID, beobachtungsgruppeID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
                    liste_beobachtungen_rohdaten_tachymeter.append(
                        (beobachtungsart, beobachtungenID, beobachtungsgruppeID, standpunkt, zielpunkt)
                    )

                    if standpunkt not in liste_punktnummern:
                        liste_punktnummern.append(standpunkt)
                    if zielpunkt not in liste_punktnummern:
                        liste_punktnummern.append(zielpunkt)

                    if beobachtungsart == "tachymeter_richtung":
                        if beobachtungsgruppeID not in liste_orientierungsunbekannte:
                            liste_orientierungsunbekannte.append(beobachtungsgruppeID)

            #GNSS Block
            if beobachtungsart == "gnss_basislinien":
                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()
                    liste_beobachtungen_rohdaten_gnssbasislinien.append(("gnssbx", beobachtungenID, standpunkt, zielpunkt))
                    if standpunkt not in liste_punktnummern:
                        liste_punktnummern.append(standpunkt)
                    if zielpunkt not in liste_punktnummern:
                        liste_punktnummern.append(zielpunkt)

                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()
                    liste_beobachtungen_rohdaten_gnssbasislinien.append(("gnssby", beobachtungenID, standpunkt, zielpunkt))
                    if standpunkt not in liste_punktnummern:
                        liste_punktnummern.append(standpunkt)
                    if zielpunkt not in liste_punktnummern:
                        liste_punktnummern.append(zielpunkt)

                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()
                    liste_beobachtungen_rohdaten_gnssbasislinien.append(("gnssbz", beobachtungenID, standpunkt, zielpunkt))
                    if standpunkt not in liste_punktnummern:
                        liste_punktnummern.append(standpunkt)
                    if zielpunkt not in liste_punktnummern:
                        liste_punktnummern.append(zielpunkt)


            if beobachtungsart == "geometrisches_nivellement":
                liste_id_standpunkt_zielpunkt = self.db_zugriff.get_nivellement_beobachtungen_punktnummern()

                for beobachtungenID, standpunkt, zielpunkt in liste_id_standpunkt_zielpunkt:
                    standpunkt = str(standpunkt).strip()
                    zielpunkt = str(zielpunkt).strip()
                    liste_beobachtungen_rohdaten_nivellement.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)

        # Erstellen der Symbole für die Unbekannten (X, Y, Z und Orientierung)
        liste_unbekannte = []

        for punkt in liste_punktnummern:
            X, Y, Z = sp.symbols(f"X{punkt} Y{punkt} Z{punkt}")
            self.dict_punkt_symbole[punkt] = (X, Y, Z)
            liste_unbekannte.append(X)
            liste_unbekannte.append(Y)
            liste_unbekannte.append(Z)

        dict_orientierung_symbole = {}
        for orientierungsunbekannte in liste_orientierungsunbekannte:
            O = sp.symbols(f"O{orientierungsunbekannte}")
            dict_orientierung_symbole[orientierungsunbekannte] = O
            liste_unbekannte.append(O)

        # Erstellen der Symbolischen Gleichungen für die Jacobimatrix
        liste_beobachtungsgleichungen_distanz =[]
        liste_zeilenbeschriftungen_distanz = []

        liste_A_richtung_zeilen = []
        liste_zeilenbeschriftungen_richtung = []

        liste_A_zenitwinkel_zeilen = []
        liste_zeilenbeschriftungen_zenitwinkel = []

        liste_beobachtungsgleichungen_gnssbasislinien = []
        liste_zeilenbeschriftungen_gnssbasislinien = []

        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}")

                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":
                    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)))
                    d_r_dX_sp = - d_r_dX_zp
                    d_r_dY_zp = (
                            (sp.sin(B_sp) * sp.sin(L_sp) * sp.sin(azimut_berechnet) + sp.cos(L_sp) * sp.cos(azimut_berechnet)) / (
                                s_berechnet * sp.sin(zw_berechnet)))
                    d_r_dY_sp = - d_r_dY_zp
                    d_r_dZ_zp = ((-sp.cos(B_sp) * sp.sin(azimut_berechnet) / (s_berechnet * sp.sin(zw_berechnet))))
                    d_r_dZ_sp = - d_r_dZ_zp
                    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])
                        elif punkt == zielpunkt:
                            zeile_A_Matrix.extend([d_r_dX_zp, d_r_dY_zp, d_r_dZ_zp])
                        else:
                            zeile_A_Matrix.extend([0, 0, 0])

                    for orientierung in liste_orientierungsunbekannte:
                        if orientierung == beobachtungsgruppeID:
                            zeile_A_Matrix.append(d_r_dO_sp)
                        else:
                            zeile_A_Matrix.append(0)

                    liste_A_richtung_zeilen.append(zeile_A_Matrix)
                    liste_zeilenbeschriftungen_richtung.append(
                        f"{beobachtungenID}_R_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}"
                    )


                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))
                    d_r_dY_sp = - d_r_dY_zp
                    d_r_dZ_zp = ((Z_zp - Z_sp) * sp.cos(zw_berechnet) - s_berechnet * sp.sin(B_sp)) / (s_berechnet ** 2 * sp.sin(zw_berechnet))
                    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])
                        elif punkt == zielpunkt:
                            zeile_A_Matrix.extend([d_r_dX_zp, d_r_dY_zp, d_r_dZ_zp])
                        else:
                            zeile_A_Matrix.extend([0, 0, 0])

                    for orientierung in liste_orientierungsunbekannte:
                        zeile_A_Matrix.append(0)

                    liste_A_zenitwinkel_zeilen.append(zeile_A_Matrix)
                    liste_zeilenbeschriftungen_zenitwinkel.append(
                        f"{beobachtungenID}_ZW_{beobachtungsgruppeID}_{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]

                if beobachtungsart == "gnssbx":
                    beobachtungsgleichung_bx = X_zp - X_sp
                    liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_bx)
                    liste_zeilenbeschriftungen_gnssbasislinien.append(
                        f"{beobachtungenID}_gnssbx_{standpunkt}_{zielpunkt}")

                if beobachtungsart == "gnssby":
                    beobachtungsgleichung_by = Y_zp - Y_sp
                    liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_by)
                    liste_zeilenbeschriftungen_gnssbasislinien.append(
                        f"{beobachtungenID}_gnssby_{standpunkt}_{zielpunkt}")

                if beobachtungsart == "gnssbz":
                    beobachtungsgleichung_bz = Z_zp - Z_sp
                    liste_beobachtungsgleichungen_gnssbasislinien.append(beobachtungsgleichung_bz)
                    liste_zeilenbeschriftungen_gnssbasislinien.append(
                        f"{beobachtungenID}_gnssbz_{standpunkt}_{zielpunkt}")


        # Geometrisches Nivellement
        if liste_beobachtungen_rohdaten_nivellement != []:
            for beobachtungsart, beobachtungenID, standpunkt, zielpunkt in liste_beobachtungen_rohdaten_nivellement:
                # 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}")

                if beobachtungsart == "geometrisches_nivellement":

                    d_r_dX_zp = sp.cos(B_zp) * sp.cos(L_zp)
                    d_r_dX_sp = -sp.cos(B_sp) * sp.cos(L_sp)
                    d_r_dY_zp = sp.cos(B_zp) * sp.sin(L_zp)
                    d_r_dY_sp = -sp.cos(B_sp) * sp.sin(L_sp)
                    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:
                            zeile_A_Matrix.extend([d_r_dX_sp, d_r_dY_sp, d_r_dZ_sp])
                        elif punkt == zielpunkt:
                            zeile_A_Matrix.extend([d_r_dX_zp, d_r_dY_zp, d_r_dZ_zp])
                        else:
                            zeile_A_Matrix.extend([0, 0, 0])

                    for orientierung in liste_orientierungsunbekannte:
                        zeile_A_Matrix.append(0)

                    liste_A_nivellement_zeilen.append(zeile_A_Matrix)
                    liste_zeilenbeschriftungen_nivellement.append(
                        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)
            A_dist = f_matrix_dist.jacobian(unbekanntenvektor)
        else:
            A_dist = None

        if liste_A_richtung_zeilen:
            A_richtung = sp.Matrix(liste_A_richtung_zeilen)
        else:
            A_richtung = None

        if liste_A_zenitwinkel_zeilen:
            A_zenitwinkel = sp.Matrix(liste_A_zenitwinkel_zeilen)
        else:
            A_zenitwinkel = None

        if liste_beobachtungsgleichungen_gnssbasislinien:
            f_matrix_gnss = sp.Matrix(liste_beobachtungsgleichungen_gnssbasislinien)
            unbekanntenvektor = sp.Matrix(liste_unbekannte)
            A_gnssbasislinien = f_matrix_gnss.jacobian(unbekanntenvektor)
        else:
            A_gnssbasislinien = None

        if liste_A_nivellement_zeilen:
            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:
            A_gesamt = A_dist
            liste_zeilenbeschriftungen_gesamt.extend(liste_zeilenbeschriftungen_distanz)

        if A_richtung is not None:
            if A_gesamt is None:
                A_gesamt = A_richtung
            else:
                A_gesamt = A_gesamt.col_join(A_richtung)
            liste_zeilenbeschriftungen_gesamt.extend(liste_zeilenbeschriftungen_richtung)

        if A_zenitwinkel is not None:
            if A_gesamt is None:
                A_gesamt = A_zenitwinkel
            else:
                A_gesamt = A_gesamt.col_join(A_zenitwinkel)
            liste_zeilenbeschriftungen_gesamt.extend(liste_zeilenbeschriftungen_zenitwinkel)

        if A_gnssbasislinien is not None:
            if A_gesamt is None:
                A_gesamt = A_gnssbasislinien
            else:
                A_gesamt = A_gesamt.col_join(A_gnssbasislinien)
            liste_zeilenbeschriftungen_gesamt.extend(liste_zeilenbeschriftungen_gnssbasislinien)

        if A_nivellement is not None:
            if A_gesamt is None:
                A_gesamt = A_nivellement
            else:
                A_gesamt = A_gesamt.col_join(A_nivellement)
            liste_zeilenbeschriftungen_gesamt.extend(liste_zeilenbeschriftungen_nivellement)

        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":
            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]
                idx_rechts = []
                for name in liste_unbekannte_datumsfestlegung:
                    for i, sym in enumerate(liste_unbekannte_alt):
                        if str(sym) == name and i not in idx_rechts:
                            idx_rechts.append(i)
                            break

                idx_links = [i for i in range(len(liste_unbekannte_alt)) if i not in idx_rechts]

                vertauschung = idx_links + idx_rechts

                A_gesamt = A_gesamt[:, vertauschung]
                liste_unbekannte = [liste_unbekannte_alt[i] for i in vertauschung]

            # 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)
                einheitsmatrix = sp.eye(anzhl_einheitsmatrix)
                A_weiche_Lagerung = nullenmatrix.row_join(einheitsmatrix)
                A_gesamt = A_gesamt.col_join(A_weiche_Lagerung)
                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_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":
            if self.func_A0 is None:
                self.func_A0 = sp.lambdify(
                    self.liste_symbole_lambdify,
                    A_symbolisch,
                    modules="numpy",
                    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:
                    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 = np.asarray(self.func_A0(*liste_werte), dtype=float)

            Export.matrix_to_csv(fr"Zwischenergebnisse\{iterationsnummer}Jacobi_Matrix_Numerisch.csv", liste_unbekannte,
                                 liste_zeilenbeschriftungen_gesamt, A_numerisch, "Beobachtung")

            return A_numerisch
        else:
            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)
        Export.matrix_to_csv(r"Zwischenergebnisse\Beobachtungsvektor_Numerisch.csv", [""], liste_beobachtungsvektor_symbolisch, beobachtungsvektor_numerisch, "Beobachtungsvektor")
        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":
                continue

            if aufgeteilt [1] == "SD" or aufgeteilt [1] == "R" or aufgeteilt [1] == "ZW":
                standpunkt = str(aufgeteilt[3])
                zielpunkt = str(aufgeteilt[4])

                if standpunkt not in liste_punktnummern:
                    liste_punktnummern.append(standpunkt)
                if zielpunkt not in liste_punktnummern:
                    liste_punktnummern.append(zielpunkt)

            if aufgeteilt[1] == "gnssbx" or aufgeteilt[1] == "gnssby" or aufgeteilt[1] == "gnssbz":
                standpunkt = str(aufgeteilt[2])
                zielpunkt = str(aufgeteilt[3])

                if standpunkt not in liste_punktnummern:
                    liste_punktnummern.append(standpunkt)
                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)

        for beobachtung_symbolisch in liste_beobachtungsvektor_symbolisch:
            beobachtung_symbolisch = str(beobachtung_symbolisch).strip()
            aufgeteilt = beobachtung_symbolisch.split("_")
            if aufgeteilt[0] == "lA":
                anschlusspunkt = str(aufgeteilt[1])
                liste_beobachtungsgleichungen.append(sp.Symbol(anschlusspunkt))
                continue

            if aufgeteilt[1] == "SD" or aufgeteilt[1] == "R" or aufgeteilt[1] == "ZW":
                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

                if beobachtungsart == "SD":
                    s_geom = sp.sqrt(dX ** 2 + dY ** 2 + dZ ** 2)
                    liste_beobachtungsgleichungen.append(s_geom)

                elif beobachtungsart == "R":
                    r_sp_zp = sp.Symbol(f"richtung_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
                    liste_beobachtungsgleichungen.append(r_sp_zp)

                elif beobachtungsart == "ZW":
                    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":
                beobachtungsart = aufgeteilt[1]
                standpunkt = str(aufgeteilt[2]).strip()
                zielpunkt = str(aufgeteilt[3]).strip()

                X_sp, Y_sp, Z_sp = self.dict_punkt_symbole[standpunkt]
                X_zp, Y_zp, Z_zp = self.dict_punkt_symbole[zielpunkt]

                dX = X_zp - X_sp
                dY = Y_zp - Y_sp
                dZ = Z_zp - Z_sp

                if beobachtungsart == "gnssbx":
                    liste_beobachtungsgleichungen.append(dX)
                if beobachtungsart == "gnssby":
                    liste_beobachtungsgleichungen.append(dY)
                if beobachtungsart == "gnssbz":
                    liste_beobachtungsgleichungen.append(dZ)

            if aufgeteilt[1] == "niv":
                standpunkt = str(aufgeteilt[2]).strip()
                zielpunkt = str(aufgeteilt[3]).strip()

                nh_sp = sp.Symbol(f"NH{standpunkt}")
                nh_zp = sp.Symbol(f"NH{zielpunkt}")

                niv_sp_zp = nh_zp - nh_sp

                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(self, liste_beobachtungsvektor_symbolisch: list,
                                               beobachtungsvektor_naeherung_symbolisch: sp.Matrix,
                                               iterationsnummer: int = 0) -> ndarray[tuple[int, int], Any]:
        """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.func_beob0 = sp.lambdify(
                self.liste_symbole_lambdify,
                beobachtungsvektor_naeherung_symbolisch,
                modules="numpy",
                cse=True
            )

        liste_werte = [self.substitutionen_dict[s] for s in self.liste_symbole_lambdify]
        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,
                             "Beobachtungsvektor")

        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")
        return(unbekanntenvektor_symbolisch)

    def unbekanntenvektor_numerisch(self, liste_unbekanntenvektor_symbolisch: list,
                                    unbekanntenvektor_symbolisch: sp.Matrix,
                                    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

        # Ü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)

        else:
            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

        # 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.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:
        """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 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)
            iY = idx.get(f"Y{pn}", None)
            iZ = idx.get(f"Z{pn}", None)

            if iX is None or iY is None or iZ is None:
                continue

            dict_koordinaten[pn] = sp.Matrix([
                float(unbekanntenvektor_numerisch[iX, 0]),
                float(unbekanntenvektor_numerisch[iY, 0]),
                float(unbekanntenvektor_numerisch[iZ, 0]),
            ])

        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.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

        for i, name in enumerate(liste_beobachtungsvektor_symbolisch):
            if "_R_" in str(name):
                dl[i] = np.arctan2(np.sin(dl[i]), np.cos(dl[i]))

        Export.matrix_to_csv(
            fr"Zwischenergebnisse\{iterationsnummer}_dl.csv",
            [""],
            liste_beobachtungsvektor_symbolisch,
            dl.reshape(-1, 1),
            "dl"
        )

        return dl

    def dict_substitutionen_uebergeordnetes_system(self,
                                                   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 = self.db_zugriff.get_koordinaten("naeherung_us")
        else:
            dict_koordinaten = self.unbekanntenvektor_numerisch_to_dict_unbekanntenvektor(
                self.liste_unbekanntenvektor_symbolisch,
                unbekanntenvektor_aus_iteration
            )

        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)

        # 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 = 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:
            punktnummern_niv.add(str(pn_sp).strip())
            punktnummern_niv.add(str(pn_zp).strip())

        dict_koordinaten_niv = {}
        for pn in punktnummern_niv:
            if pn in dict_koordinaten:
                dict_koordinaten_niv[pn] = dict_koordinaten[pn]

        dict_koordinaten_utm = self.trafos.ecef_to_utm(
            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}")

            substitutionen[X_sym] = float(vektor[0][0])
            substitutionen[Y_sym] = float(vektor[0][1])
            substitutionen[Z_sym] = float(vektor[0][2])
            substitutionen[B_sym] = float(vektor[1])
            substitutionen[L_Sym] = float(vektor[2])

        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)

            azimut_sym = sp.symbols(f"azimut_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
            substitutionen[azimut_sym] = float(azimut)

            zenitwinkel_sym = sp.symbols(f"zw_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
            substitutionen[zenitwinkel_sym] = float(zenitwinkel)

            schraegstrecke_sym = sp.symbols(f"strecke_berechnet_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")
            substitutionen[schraegstrecke_sym] = float(schraegstrecke)

        for punktnummer, koordinaten_utm in dict_koordinaten_utm.items():
            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}")
            s = sp.symbols(f"{beobachtungenID}_SD_{beobachtungsgruppeID}_{standpunkt}_{zielpunkt}")

            if tachymeter_richtung is None and tachymeter_zenitwinkel is None and tachymeter_distanz is None:
                continue

            substitutionen[alpha] = float(tachymeter_richtung)
            substitutionen[zw] = float(tachymeter_zenitwinkel)
            substitutionen[s] = float(tachymeter_distanz)

        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()
            punktnummer_sp = str(punktnummer_sp).strip()
            punktnummer_zp = str(punktnummer_zp).strip()

            bx = sp.symbols(f"{beobachtungenID}_gnssbx_{punktnummer_sp}_{punktnummer_zp}")
            by = sp.symbols(f"{beobachtungenID}_gnssby_{punktnummer_sp}_{punktnummer_zp}")
            bz = sp.symbols(f"{beobachtungenID}_gnssbz_{punktnummer_sp}_{punktnummer_zp}")

            if gnss_bx is None and gnss_by is None and gnss_bz is None:
                continue

            substitutionen[bx] = float(gnss_bx)
            substitutionen[by] = float(gnss_by)
            substitutionen[bz] = float(gnss_bz)

        for beobachtungenID, punktnummer_sp, punktnummer_zp, niv_dh, niv_strecke, niv_anz_standpkte in liste_beobachtungen_nivellemente:
            beobachtungenID = str(beobachtungenID).strip()
            punktnummer_sp = str(punktnummer_sp).strip()
            punktnummer_zp = str(punktnummer_zp).strip()

            niv = sp.symbols(f"{beobachtungenID}_niv_{punktnummer_sp}_{punktnummer_zp}")

            if niv_dh is None:
                continue

            substitutionen[niv] = float(niv_dh)

        if unbekanntenvektor_aus_iteration is not None:
            dict_O = self.unbekanntenvektor_numerisch_to_dict_orientierungen(
                self.liste_unbekanntenvektor_symbolisch,
                unbekanntenvektor_aus_iteration
            )
            for orientierungs_id, wert in dict_O.items():
                substitutionen[sp.Symbol(f"O{orientierungs_id}")] = float(wert)
        else:
            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:
                    substitutionen[O_sym] = orientierung

        return substitutionen

    def unbekanntenvektor_numerisch_to_dict_orientierungen(self, liste_unbekanntenvektor_symbolisch: list,
                                                           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):
            name = str(symbol)
            if name.startswith("O"):
                orientierungs_id = name[1:]
                dict_O[orientierungs_id] = float(unbekanntenvektor_numerisch[i, 0])

        return dict_O