# Klassenhierarchie Bestandsaufnahme aller Python-Klassen in `lib/` (Stand 2026-10-10). Kurzes Ergebnis vorweg: es gibt praktisch **keine Vererbung** in diesem Code (eine Ausnahme: `DerivedTro(TroBase)`, plus die Standard-Exception `RegistrationError(RuntimeError)`). Alles andere sind flache, eigenständige Klassen — entweder `@dataclass` (veränderlicher Laufzeit-/Analysezustand) oder Pydantic `BaseModel` (validierte Daten, die auch als JSON ein-/ausgehen). Eine klassische "ist-ein"-Hierarchie zu suchen wäre hier der falsche Blickwinkel; interessanter ist, **wer wen enthält/referenziert** und in welchem Modul-Layer eine Klasse sitzt (siehe Layer-Kommentar oben in `model.py`). ## Modul-Layer (wer darf wen importieren) ``` tro_catalog.py (Typ-Ebene: was ein TRO-Typ ist) | v model.py (Instanz-Ebene: ein konkreter TRO, die TRO-JSON) | v tro_flow.py (Ableitung: CSV-Graph -> Analysis -> TroDocument) | +----------------------+ v v scl_context.py tro_annotate.py (Ausgabe: annotierte Zeichnung) (TroDocument -> SclFile, Render-Kontext) | v scl_skeleton.py (Ausgabe: SclFile -> FB_Main.scl, generisch oder per Jinja-Template) material_flow.py dxf_registration.py (eigenständig, ohne Abhängigkeit auf obige) tro_overrides.py (importiert model.Connection / model.TroCandidate) ``` `tro_flow.py` und `tro_annotate.py` definieren keine eigenen Klassen (nur Funktionen). `scl_skeleton.py` hat genau eine (`SclTemplates`, Jinja-Anbindung). `material_flow.py` hat Graph-Dataclasses — das ist CSV-Rohmodell, nicht TRO-Modell. ## Vererbung (ASCII) ``` RuntimeError (stdlib) BaseModel (pydantic, stdlib: pydantic) | | +-- RegistrationError +-- _Positioned (model.py, Mixin) (dxf_registration.py) | +-- TroBase | +-- DerivedTro +-- TroCandidate ``` Das ist die gesamte `class X(Y)`-Liste im Projekt — alle anderen Klassen unten haben `object`/`BaseModel` direkt als einzige Basis und erscheinen hier nur noch als Gruppen nach Datei. ## Klassen nach Datei, mit Komposition ("enthält") ``` lib/tro_catalog.py [Typ-Katalog, keine Instanzdaten] ├── TroItem (Konstanten-Klasse: Bauteilarten wie SEPARATOR, SWITCH) ├── TroSymbol (Konstanten-Klasse: Zeichensymbole wie CIRCLE, TRIANGLE) ├── TroStyle(BaseModel) Farbe/Darstellung einer Typgruppe └── TroDefinition(BaseModel) └─ style: TroStyle (1 TroDefinition hat genau 1 TroStyle) lib/model.py [Instanz-Ebene / TRO-JSON] ├── TroBase(BaseModel) │ └─ definition() --> TroDefinition (gelesen aus tro_catalog, nicht gespeichert) │ └── DerivedTro(TroBase) [einzige echte Vererbung im Projekt] ├── _Positioned(BaseModel) (Mixin: position {x,y,z}, describe_position) │ ├── TroBase, TroCandidate ├── Connection(BaseModel) (from/to/kind zwischen zwei TRO-IDs) ├── TroCandidate(_Positioned) (gemeldete Stelle, an der ein TRO erwartet wird) ├── Analysis(BaseModel) │ ├─ tros: list[DerivedTro] │ ├─ isolated: list[DerivedTro] │ └─ boundaries: list[DerivedTro] └── TroDocument(BaseModel) (= _tro.json) ├─ tros: list[DerivedTro] ├─ boundaries: list[DerivedTro] ├─ connections: list[Connection] └─ candidates: list[TroCandidate] lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass] ├── Element (1 CSV-Zeile) ├── Node │ └─ element: Element ├── Edge ├── Graph │ ├─ nodes: dict[str, Node] │ ├─ edges: list[Edge] │ └─ unassigned: list[Element] ├── TrackNode │ └─ element: Element | None ├── TrackEdge └── TrackGraph ├─ nodes: dict[str, TrackNode] └─ edges: list[TrackEdge] lib/tro_overrides.py [manuelle Korrekturen aus cfg/tro_overrides.ini, @dataclass] ├── TroOverride (Typkorrektur an einem abgeleiteten TRO: tro_id ODER separator) ├── SplitSpec (TRO aus einem bestehenden heraustrennen) ├── MergeSpec (mehrere TROs zu einem zusammenfassen) └── Overrides (Sammelcontainer für eine CSV-Datei) ├─ tros: dict[str, TroOverride] ├─ splits: list[SplitSpec] ├─ merges: list[MergeSpec] ├─ connections: list[model.Connection] └─ candidates: list[model.TroCandidate] lib/dxf_registration.py [CSV<->DXF Koordinatentransform] ├── RegistrationError(RuntimeError) └── Transform(BaseModel) lib/scl_context.py [Render-Kontext für die SCL, BaseModel] ├── SclName (Wert + Merker todo: noch aus der E-Planung offen?) ├── SclSeparator (ein stInSeparator-Block) ├── SclSwitch (ein stInSwitch-Block + FC_Direction-Aufruf) ├── SclSwitchExit (Weichenabgang + darüber erreichbare TROs) ├── SclScanner (ein stInBarcodeReader-Block) ├── SclJam (Staubereich an Ein-/Ausgang) ├── SclTro (alles für eine REGION) │ ├─ separators: list[SclSeparator] │ ├─ switches: list[SclSwitch] │ ├─ switch_exits: list[SclSwitchExit] │ ├─ scanners: list[SclScanner] │ ├─ jam_entries / jam_exits: list[SclJam] │ └─ main_tro, safety, ... : SclName └── SclFile (die ganze Datei) └─ tros: list[SclTro] (in Fliessrichtung) lib/scl_skeleton.py [SCL-Ausgabe] └── SclTemplates (Ordner mit .scl.j2, hält ein jinja2.Environment) ``` ## Auffälligkeiten für ein mögliches Refactoring 1. ~~`OpenPoint` existiert zweimal~~ — erledigt 2026-10-10: zu `model.TroCandidate` zusammengefasst (`expect` → `type_name`, `x/y/z` → `position`); `ConnectionSpec` durch `model.Connection` ersetzt; `tro_overrides.py` importiert beide aus `model.py`. 2. **Zwei parallele Graph-Hierarchien** in `material_flow.py` (`Node/Edge/Graph` vs. `TrackNode/TrackEdge/TrackGraph`) teilen sich keine Basisklasse, obwohl Struktur und Feldnamen sich stark ähneln (`node_id, kind, ...` / `src, dst, ...`). Eine gemeinsame leichte Basis (z. B. ein generisches `GraphBase[NodeT, EdgeT]`) wäre möglich, aber angesichts der wenigen Stellen, die beide Typen anfassen, vermutlich mehr Abstraktion als Nutzen. 3. **`TroItem` und `TroSymbol` sind reine Konstanten-Namespaces** (keine Instanzen, keine Methoden außer `TroItem.all()`), kein Pydantic-Modell wie der Rest von `tro_catalog.py`. Das ist bewusst so (siehe Docstring: "damit Tippfehler auffallen") und funktioniert wie ein `Enum`-Ersatz — ein `StrEnum` wäre eine nahe liegende, idiomatischere Alternative, falls ein Refactoring ansteht. 4. Die einzige echte Vererbung (`DerivedTro(TroBase)`) ist sauber: `TroBase` ist bereits so geschnitten, dass es für Systemgrenzen (`boundaries`) wiederverwendet wird, `DerivedTro` ergänzt nur das, was die CSV-Ableitung zusätzlich weiß. Kein Handlungsbedarf. **Fazit:** Die Code-Basis nutzt Vererbung bewusst kaum — die Architektur trennt stattdessen über Module (Typ-Katalog vs. Instanz vs. Ableitung vs. Ausgabe, siehe `model.py`-Kopfkommentar) und über Komposition. Ein Refactoring in Richtung "mehr Vererbung" wäre hier eher kontraproduktiv. Die doppelte `OpenPoint`-Definition (1) ist behoben; offen bleibt nur optional `StrEnum` statt Konstanten-Klassen (3). ## Die komplexen Klassen im Einzelnen Die folgenden Abschnitte beschreiben je Klasse, **wozu** sie da ist, **was darin passiert** und **wer sie erzeugt bzw. liest**. Einfache Datenhalter ohne eigene Logik — `Connection`, `TroCandidate`, `Edge`, `TrackEdge`, `TroItem`/`TroSymbol` und die kleinen Blöcke `SclSeparator`, `SclSwitch`, `SclSwitchExit`, `SclScanner`, `SclJam` — sind im Baum oben ausreichend beschrieben und bekommen keinen eigenen Abschnitt. ### `TroDefinition` (+ `TroStyle`) — `lib/tro_catalog.py` **Wozu:** Beschreibt einen TRO-**Typ**, nicht eine Stelle in der Anlage: Kurzname (`1Sep1Swi`), Siemens-FB (`FB_ILS_MTRO_1Sep1Swi`), Bauteile (`{"Separator": 1, "Switch": 1}`), Farbgruppe und CAD-Symbol. Der Katalog aller Definitionen ist die ausführbare Form von `doc/TRO_Typen.md`. **Was passiert darin:** Pydantic mit `validate_assignment` und `extra="forbid"` — eine Definition kann nie ungültig werden, auch nicht über die `set_*`-Methoden. `_clean_items` wirft Bauteile mit Anzahl ≤ 0 heraus. Ein eigener `__init__` erlaubt die alte Positionsform `TroDefinition("1Sep", "FB_...", {...})`. Dazu kommen Abfragen (`get_items`, `has_item`, `get_item_fb`, `get_total_items`) und Darstellungshelfer (`get_dot_attrs`, `mermaid_class/_classdef`, `describe_items`). `TroStyle` liefert nur die Farben einer Gruppe für Graphviz, Mermaid und CAD. **Wer nutzt sie:** Über `get_tro(name)`/`catalog()` jeder, der vom Typnamen auf FB oder Bauteile kommen muss — vor allem `TroBase.definition()`; außerdem `tro_flow` (Typprüfung), `tro_annotate` (Farbe/Symbol, `--emit-lisp`) und die Doku-Grafiken. ### `TroBase` / `DerivedTro` — `lib/model.py` **Wozu:** Ein konkreter TRO in einer Anlage (Instanz). `TroBase` ist der Kern, den jeder TRO hat, egal woher er stammt: `tro_id`, `type_name`, `separators`, `predecessors`/`successors`, `position`. `DerivedTro` ergänzt, was `tro_flow` beim Ableiten aus dem CSV zusätzlich weiß: `hosts`, `scanners`, `reason`, `colocated`, `position_from`/`position_points`. Systemgrenzen (`SYSn`) sind ebenfalls `DerivedTro`. **Was passiert darin:** - `fb_block` und `items` sind **berechnete** Felder: bei jedem Zugriff aus dem Katalog gelesen (`definition()` → `get_tro(type_name)`), nie gespeichert. In der JSON stehen sie trotzdem (`fbType`, `items`), damit sie lesbar ist. - `_check_catalog_fields` (Validator vor dem Einlesen): steht in einer JSON ein `items`/`fbType`, das nicht zum Typ passt, ist das ein Fehler — "den Typ ändern, nicht die Bauteile". Danach werden die Felder verworfen. - Aliase: in der JSON `id`/`type`, im Code `tro_id`/`type_name` (`populate_by_name`). - `_Positioned` (Mixin) wandelt `position` zwischen Tupel und `{"x","y","z"}` und liefert `x`/`y`/`z` und `describe_position()`. - `DerivedTro.connected`: hat der TRO überhaupt Nachbarn? **Wer nutzt sie:** `tro_flow.derive_tros`/`derive_boundaries` erzeugen sie, `scl_context` liest sie (als `TroBase`), `tro_annotate` zeichnet sie. ### `Analysis` — `lib/model.py` **Wozu:** Das Arbeitsergebnis eines Ableitungslaufs, bevor daraus eine Datei wird. Enthält mehr als die JSON: Kanten als Tupel, Kantenarten, isolierte TROs, unbekannte Typen, Befunde, die verwendete Methode. **Was passiert darin:** Fast nur Daten; `by_id()` sucht über TROs und Systemgrenzen. Befüllt wird sie in `tro_flow.analyse_tros` in fester Reihenfolge: `derive_tros` → `derive_boundaries` → `locate_tros` (Position, nötig für die Reihenfolge auf einer Kreiselbahn) → `link_tros` (Kanten und Kantenarten) → Prüfung gegen den Katalog. **Wer nutzt sie:** `tro_flow` (Bericht, Diagramm, `--json`) und `tro_annotate` (zeichnet direkt aus der `Analysis`, ohne Umweg über die JSON). ### `TroDocument` — `lib/model.py` **Wozu:** Die TRO-JSON `_tro.json` (Schema `sps_skel-tro-v3`) — die Schnittstelle zwischen Ableitung (`tro_flow --json`) und SCL-Ausgabe (`scl_skeleton`). Weil beide Seiten dieselbe Klasse benutzen, kann das Format nicht unbemerkt auseinanderlaufen. **Was passiert darin:** `from_analysis()` macht aus einer `Analysis` ein Dokument (Kanten + Kantenarten → `Connection`-Liste). `to_json()` schreibt mit den JSON-Aliasen, `from_json()` liest und prüft. `extra="forbid"` sorgt dafür, dass ein umbenannter oder falscher Schlüssel als Fehler auffällt statt still einen Standardwert zu bekommen; `scl_skeleton.main` meldet bei falschem `schema` gezielt "mit tro_flow neu erzeugen". **Wer nutzt sie:** geschrieben von `tro_flow --json`, gelesen von `scl_skeleton.load_document` → `scl_context.build_context`. ### `Element`, `Graph`, `TrackGraph` — `lib/material_flow.py` **Wozu:** Das Rohmodell aus dem CSV-Export (ILS 2.0), noch ohne TRO-Begriff. - `Element` = eine CSV-Zeile: Art, ID, Name, Nachbarn, `track_ids`, Merkmale, Position, Bounding-Box, Anschlusspunkte K1..K4. - `Graph` (`Node`/`Edge`) = grober **Materialfluss-Graph**: Transportobjekte (Strecke, Gefällestrecke, Kreisel-Bahn) als Knoten, Geräte (Separatoren, Weichen, Scanner …) an die Knoten gehängt (`Node.devices`), Stationen und Übergaben als Kanten. - `TrackGraph` (`TrackNode`/`TrackEdge`) = feiner **Bahnzusammenhang**: jedes Bahnelement (AS, SEP, ES, Weiche, BTMT …) ist ein Knoten, verkettet in Bau- bzw. Drehrichtung (siehe `doc/track.md`). **Was passiert darin:** `Element` hat viele abgeleitete Properties: Klassifizierung (`is_transport`, `is_circle`, `is_device`, `is_station`, `is_transfer`, `is_entry`/`is_exit`), Geometrie (`x/y/z`, `bbox_x/y`, `height`, `height_from/to`, `rise`), Antrieb (`drive_dir`, `rotation`). Die Graphklassen selbst sind reine Container; die Logik steckt in den Funktionen `build_graph()` (Knoten bilden, Geräte zuordnen, Kanten sammeln, Fliessrichtung aus Gefälle/Antrieb, Stationen und Übergaben einhängen) und `build_track_graph()` (Ketten aus der Spalte `TrackIds`; ein Element in zwei Containern verbindet diese automatisch). `TrackNode` kann ohne `element` existieren (Platzhalter für eine nicht auflösbare Bahn, generisches Ein-/Ausgang-Symbol). **Wer nutzt sie:** `material_flow` selbst (DOT/SVG/connect.ini/Bericht) und `tro_flow.analyse_tros(graph, elements)` als Eingang der TRO-Ableitung; `tro_annotate` für Ankerpunkte des Lagebezugs. ### `Overrides` (+ `TroOverride`, `SplitSpec`, `MergeSpec`) — `lib/tro_overrides.py` **Wozu:** Von Hand gepflegte, vor Ort bestätigte Korrekturen aus `cfg/tro_overrides.ini` für das, was die Ableitung nicht erkennen kann. - `TroOverride`: Typ eines TRO korrigieren. Adressiert über `tro_id` **oder** — stabiler — über einen `separator`, weil TRO-Nummern sich zwischen Läufen verschieben, Separatornummern aus dem Export nicht (`matches()`). - `SplitSpec`: Separatoren/Scanner aus einem TRO herauslösen und einen neuen TRO bilden (Übergabestelle, deren Zuordnung auf die Strecke statt auf die Kreiselbahn zeigt). - `MergeSpec`: mehrere TROs zu einem Steuerobjekt zusammenfassen (z. B. zwei Sperren beiderseits eines Kreisel-Berührpunkts). - `Overrides`: Sammelcontainer je CSV-Datei, dazu gesetzte Topologie (`connections`, ersetzt die abgeleiteten Kanten vollständig), erwartete TROs (`candidates`), Protokoll (`applied`) und `warnings`. `bool(overrides)` ist True, sobald irgendeine Korrektur vorhanden ist. **Was passiert darin:** `load_overrides(csv_name)` liest den INI-Abschnitt der CSV-Datei und baut daraus die Objekte; Lesefehler landen in `warnings` statt den Lauf abzubrechen. **Wer nutzt sie:** Stand heute **nur** `material_flow --connect` (`render_connect_ini` schreibt sie als `[.manual]`-Abschnitt). `tro_flow` wendet sie (noch) nicht an — sie fliessen also nicht in die TRO-JSON und nicht in die SCL. ### `Transform` (+ `RegistrationError`) — `lib/dxf_registration.py` **Wozu:** Der Lagebezug zwischen CSV-Export und BricsCAD-Zeichnung: eine starre Transformation `csv = R(rotation) · dxf + (dx, dy)`. Ohne sie landen die TRO-Marker in `tro_annotate` an der falschen Stelle. **Was passiert darin:** `to_csv()`/`to_dxf()` rechnen Punkte hin und zurück, `describe()` fasst Verschiebung, Drehung, Punktzahl und Restfehler zusammen. Die Ermittlung steckt in Funktionen: `derive_transform()` ordnet punktartige Objekte gleicher Rolle (Separator, Scanner) aus CSV und DXF zu, stimmt über Verschiebungen ab, gleicht aus und wählt den besten Kandidaten; `verify_transform()` rechnet die Restfehler nach und schreibt sie ins Objekt; `load_transform()`/`save_transform()` lesen/schreiben `cfg/dxf_registration.json`. `RegistrationError` heisst: der Bezug liess sich nicht **nachweisbar** bestimmen — dann lieber abbrechen als falsch zeichnen. **Wer nutzt sie:** `tro_annotate.resolve_transform` (Reihenfolge: `--offset` → abgelegter Bezug → neu ermitteln, jeweils mit Prüfung), dazu das geparkte `tro_extract`. ### `SclFile`, `SclTro`, `SclName` — `lib/scl_context.py` **Wozu:** Der **Render-Kontext** für das FB_Main-Gerüst. Alles, was aus der TRO-JSON *abgeleitet* wird, steht hier fertig drin; die Ausgabe (`emit_*`-Funktionen oder Jinja-Templates) rechnet nichts mehr selbst, sie liest nur noch. So können generischer Aufbau und Templates nicht unterschiedlich ableiten. **Was passiert darin:** - `build_context(document, source, start_id)` baut aus einem `TroDocument` ein `SclFile`: Kopfdaten plus die TROs in Fliessrichtung. - `flow_order()` bestimmt die Reihenfolge der REGIONs: Start bei `--start`, sonst beim TRO ohne Vorgänger, sonst beim TRO, den eine Station (`SYSn`) speist, sonst wird der Kreislauf willkürlich aufgetrennt — der Grund steht im SCL-Kopf (`start_reason`). Danach Breitensuche; nicht erreichte TROs werden hinten angehängt, damit keiner verloren geht. - `_tro_context()` baut je TRO ein `SclTro`: so viele Separator-, Weichen- und Scannerblöcke, wie der Katalogtyp Bauteile hat (`items`); je Vorgänger ein Staubereich am Eingang, je Nachfolger einer am Ausgang; `to1_destinations` nur bei genau einer Weiche (bei mehreren ist die Aufteilung eine Planungsentscheidung); die Zielliste je Weichenabgang über `reachable_via()` (Erreichbarkeit im TRO-Graphen, ohne über die Weiche zurückzulaufen). - `SclName` = Wert + Merker `todo`. Alles, was aus der E-Planung kommen muss (Separator-/Weichennummern, Sensor-/Aktoradressen, MainTro, Staubereichs-DB …), ist heute ein Platzhalter mit `todo=True`; die Ausgabe hängt daran den Kommentar `TODO(E-Planung)`. Spätere Namensregeln ersetzen nur den Wert und löschen den Merker — die Ausgabe ändert sich dafür nicht. **Wer nutzt sie:** erzeugt in `scl_skeleton.render()`/`main()`, gelesen von `render_context()`, den `emit_*`-Funktionen und den Templates (als `t`). ### `SclTemplates` — `lib/scl_skeleton.py` **Wozu:** Die Anbindung der parametrisierten SCL-Aufrufstellen (`lib/scl_templates/*.scl.j2`) an die Ausgabe. Ein Objekt steht für einen Template-Ordner. **Was passiert darin:** Der Konstruktor baut ein `jinja2.Environment` mit `StrictUndefined` (ein Tippfehler im Template wird zum Fehler statt zu einer leeren SCL-Stelle), `trim_blocks`/`lstrip_blocks` und der globalen Variable `TODO`. `has(fb)` prüft, ob `.scl.j2` existiert; `render(tro)` rendert das Template mit `t = SclTro` und fasst danach mehrfache Leerzeilen zusammen (statt das mit `{%- -%}` im Template zu tun, was die Einrückung bricht). #### Template ↔ TRO: Zuordnung und Einstieg in die Produktion **Zuordnung — es gibt keine Tabelle, nur eine Namenskonvention: Dateiname = FB-Typ.** Die Kette: ```text DerivedTro.type_name "1Sep1Swi" (TRO-JSON, Feld "type") -> tro_catalog.get_tro() TroDefinition -> TroDefinition.fb_block "FB_ILS_MTRO_1Sep1Swi" -> TroBase.fb_block (berechnet, nicht gespeichert) -> SclTro.fb (scl_context._tro_context) -> SclTemplates.has/render lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2 ``` Ein neues Template für einen weiteren Typ heisst also einfach `.scl.j2` und wird automatisch für alle TROs dieses Typs verwendet. Heute gibt es nur den Piloten `FB_ILS_MTRO_1Sep1Swi.scl.j2` (plus `_macros.scl.j2` mit gemeinsamen Makros). Alle anderen Typen laufen über den generischen Aufbau (`emit_region()`). **Einstieg — nur per Schalter (opt-in):** - `scl_skeleton --templates [DIR]` (ohne DIR: `lib/scl_templates`). `main()` legt dann ein `SclTemplates` an; `render_context()` nimmt pro TRO `templates.render(tro)`, wenn `templates.has(tro.fb)`, sonst `emit_region()`. Der SCL-Kopf listet, welche FB-Typen aus Template bzw. generisch kamen. - `--strict` bricht ab (Exit 1, keine SCL), wenn ein FB-Typ der Anlage kein Template hat. Ein `TemplateError` (fehlende Variable, Syntaxfehler) bricht ebenfalls ab — lieber keine SCL als eine mit stillen Lücken. - Ohne `--templates` werden Templates gar nicht geladen; die Ausgabe ist die bisherige (Golden-Files `tests/fixtures/scl/*_FB_Main.scl`). Mit Templates pinnen `*_FB_Main_templates.scl` das Ergebnis. - `bin/scl_skeleton.bat`/`.sh` reichen die Schalter nur durch; der Standardaufruf ohne `--templates` nutzt also **keine** Templates. **Nicht Produktion:** `doc/TRO_Katalog/scl_templates/*.scl` sind die realen Referenz-Aufrufstellen. Gegen sie prüft `tests/test_scl_templates.py` jedes Template im Round-Trip; in die erzeugte SCL fliessen sie nicht ein.