21 KiB
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) (= <csv>_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<n>-Block)
├── SclSwitch (ein stInSwitch<n>-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 <fbType>.scl.j2, hält ein jinja2.Environment)
Auffälligkeiten für ein mögliches Refactoring
— erledigt 2026-10-10: zuOpenPointexistiert zweimalmodel.TroCandidatezusammengefasst (expect→type_name,x/y/z→position);ConnectionSpecdurchmodel.Connectionersetzt;tro_overrides.pyimportiert beide ausmodel.py.- Zwei parallele Graph-Hierarchien in
material_flow.py(Node/Edge/Graphvs.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 generischesGraphBase[NodeT, EdgeT]) wäre möglich, aber angesichts der wenigen Stellen, die beide Typen anfassen, vermutlich mehr Abstraktion als Nutzen. TroItemundTroSymbolsind reine Konstanten-Namespaces (keine Instanzen, keine Methoden außerTroItem.all()), kein Pydantic-Modell wie der Rest vontro_catalog.py. Das ist bewusst so (siehe Docstring: "damit Tippfehler auffallen") und funktioniert wie einEnum-Ersatz — einStrEnumwäre eine nahe liegende, idiomatischere Alternative, falls ein Refactoring ansteht.- Die einzige echte Vererbung (
DerivedTro(TroBase)) ist sauber:TroBaseist bereits so geschnitten, dass es für Systemgrenzen (boundaries) wiederverwendet wird,DerivedTroergä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_blockunditemssind 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 einitems/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 Codetro_id/type_name(populate_by_name). _Positioned(Mixin) wandeltpositionzwischen Tupel und{"x","y","z"}und liefertx/y/zunddescribe_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 <csv>_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 (siehedoc/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 übertro_idoder — stabiler — über einenseparator, 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) undwarnings.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 [<Anlage>.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 einemTroDocumenteinSclFile: 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 einSclTro: 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_destinationsnur bei genau einer Weiche (bei mehreren ist die Aufteilung eine Planungsentscheidung); die Zielliste je Weichenabgang überreachable_via()(Erreichbarkeit im TRO-Graphen, ohne über die Weiche zurückzulaufen).SclName= Wert + Merkertodo. Alles, was aus der E-Planung kommen muss (Separator-/Weichennummern, Sensor-/Aktoradressen, MainTro, Staubereichs-DB …), ist heute ein Platzhalter mittodo=True; die Ausgabe hängt daran den KommentarTODO(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 <fb>.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:
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
<fbType>.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 einSclTemplatesan;render_context()nimmt pro TROtemplates.render(tro), wenntemplates.has(tro.fb), sonstemit_region(). Der SCL-Kopf listet, welche FB-Typen aus Template bzw. generisch kamen.--strictbricht ab (Exit 1, keine SCL), wenn ein FB-Typ der Anlage kein Template hat. EinTemplateError(fehlende Variable, Syntaxfehler) bricht ebenfalls ab — lieber keine SCL als eine mit stillen Lücken.- Ohne
--templateswerden Templates gar nicht geladen; die Ausgabe ist die bisherige (Golden-Filestests/fixtures/scl/*_FB_Main.scl). Mit Templates pinnen*_FB_Main_templates.scldas Ergebnis. bin/scl_skeleton.bat/.shreichen die Schalter nur durch; der Standardaufruf ohne--templatesnutzt 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.