From 5764df1ac8ecc09da6f91dab7103162dbb71d4f9 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Sat, 10 Oct 2026 11:31:05 +0200 Subject: [PATCH] Doku: README auf Hauptweg CSV -> TRO-JSON -> SCL, Klassenhierarchie ausgebaut Co-Authored-By: Claude Opus 5.5 --- README.md | 259 ++++++++++++++++++------------ doc/Klassenhierarchie.md | 334 +++++++++++++++++++++++++++++++++++---- 2 files changed, 461 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index e61b43c..f49893d 100644 --- a/README.md +++ b/README.md @@ -9,61 +9,110 @@ ## Stand des Projekts -Aus einem CSV-Export der mechanischen Planung (ILS 2.0) werden automatisch +Die Kette **CSV → TRO-JSON → SCL** läuft in einer ersten Version durch. Aus +einem CSV-Export der mechanischen Planung (ILS 2.0) entstehen automatisch: -- der **Materialfluss-Graph** der mechanischen Objekte (Strecken, Gefällestrecken, - Kreisel), +- der **Materialfluss-Graph** der mechanischen Objekte (Strecken, + Gefällestrecken, Kreisel), - die daraus abgeleitete **TRO-Liste** samt eigenem Flussdiagramm, -- eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem Marker-Symbol pro - TRO (danach in BricsCAD von Hand nachbearbeitbar — TROs verschieben, - ergänzen, Typ ändern), -- daraus zurückgelesen ein **JSON-Modell der TROs** (liest nur die Zeichnung, - erfasst also auch von Hand vorgenommene Korrekturen), und +- die **TRO-JSON** `_tro.json` (Schema `sps_skel-tro-v3`, Modell + `lib/model.py`) — die Schnittstelle zwischen Ableitung und SCL-Erzeugung, - daraus ein **SCL-Gerüst für `FB_Main`** — ein `REGION` pro TRO mit Instanzaufruf und allen Parametern in der richtigen Reihenfolge, aber bewusst nicht lauffähig: jeder Wert, der aus der E-Planung stammen muss (Sensor-/Aktoradressen, Separator-/Weichennummern, Staubereiche, ungeprüfte - Zeiten), ist als `TODO(E-Planung)` markiert statt geraten. + Zeiten), ist als `TODO(E-Planung)` markiert statt geraten. Optional + (`--templates`) kommen die REGIONs aus Jinja2-Templates + (`lib/scl_templates/.scl.j2`, Pilot: `FB_ILS_MTRO_1Sep1Swi`), +- zur Sichtkontrolle eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem + Marker-Symbol pro TRO. Die Zeichnung ist nur Ansicht, keine Datenquelle für + die SCL. -Diese fünf Werkzeuge liegen als CLI-Tools in `lib/` und sind über -`bin/`-Wrapper aufrufbar (siehe unten). Details, Domänenmodell und die -verbleibende Roadmap stehen in `doc/`, allen voran `doc/Python_Scripts.md`. +Details stehen in `doc/`, allen voran +[`doc/Python_Scripts.md`](doc/Python_Scripts.md) (alle Tools und Schalter) und +[`doc/Klassenhierarchie.md`](doc/Klassenhierarchie.md) (Klassen und wozu sie da +sind). -## Roadmap +## Ablauf -Offen ist vor allem die JSON-Layout-Datei nach dem in -`doc/HundM/Json_Layout-Konzept.md` beschriebenen Schema (`plc`, -`controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`, -`emptyCarrBuffers[]`, `routing`, `jamAreas[]`, `scanners[]`, `connections[]`, -`destinations[]`) als von Hand editierbares Zwischenformat vor der -SCL-Erzeugung, sowie die Generierung der übrigen Bausteine je Steuerung -(`FB_CallSensors`, `FC_Direction`, `FC_Call_Jams`) — `scl_skeleton.py` deckt -bisher nur `FB_Main` ab. Siehe `CLAUDE.md` (Abschnitt "Roadmap") für den -aktuellen Stand der Planung dazu. +```text +data/.csv (ILS-2.0-Export) + │ + ▼ +material_flow ──► _material_flow.dot/.svg/.md Materialfluss der Mechanik + │ + ▼ +tro_flow --json ──► _tro.json (+ .dot/.svg/.md) TROs ableiten + │ │ + │ ▼ + │ scl_skeleton ──► _FB_Main.scl SCL-Gerüst + │ + └── gleiche Ableitung ──► tro_annotate ──► _annotated.dxf (nur Ansicht) + (braucht zusätzlich die DXF-Zeichnung) +``` + +Alle Ausgaben landen in `results/` (`%SKEL_RESULTS%`). + +## Welche Skripte gibt es und wofür? + +### Umgebung (`bin/`, je `.bat` für Windows und `.sh` für Linux/macOS) + +Diese vier Skripte verwalten nur Umgebung und venv, sie rufen kein +Python-Programm auf. + +| Skript | Wofür | +| --- | --- | +| `setenv` | Setzt `SPS_SKEL` und die `SKEL_*`-Variablen, hängt `SKEL_LIB` an `PYTHONPATH`, legt fehlende Ordner an und gibt alles zur Kontrolle aus. `.sh` muss gesourct werden. | +| `install_py` | Einmalig: legt `.venv` an und installiert `requirements.txt`. Bricht ab, wenn `.venv` schon existiert. | +| `activate_venv` | Je Sitzung: aktiviert `.venv` und zeigt Python-Version und Pakete. `.sh` muss gesourct werden. | +| `get_cmd` | Öffnet eine neue Shell mit gesetzten Umgebungsvariablen. | + +### CLI-Tools (`lib/.py`, aufgerufen über `bin/.bat`/`.sh`) + +Jeder Wrapper ruft `setenv`, aktiviert `.venv` (falls vorhanden) und startet +das Modul mit allen übergebenen Argumenten. + +| Tool | Wofür | Wichtige Schalter | +| --- | --- | --- | +| `material_flow` | Liest den CSV-Export und baut den gerichteten **Materialfluss-Graphen** der mechanischen Objekte (Strecke, Gefällestrecke, Kreisel); Separatoren/Scanner hängen an ihrem Objekt. Grundlage für `tro_flow`. | `--file`, `--tosvg`, `--use-cords`, `--doc`, `--connect`, `--track`, `--show-bbs`, `--chain-graph` | +| `tro_flow` | Leitet aus demselben Graphen die **TROs** ab (Typ aus dem Trägerobjekt des Separators, Weichen aus den Abgängen), verknüpft sie und zeichnet ihren Fluss. Mit `--json` entsteht die TRO-JSON — der Hauptweg zur SCL. | `--file`, `--json`, `--tosvg`, `--use-cords`, `--doc`, `--connect` | +| `scl_skeleton` | Liest die TRO-JSON (geprüft gegen `lib/model.py`) und schreibt das **`FB_Main`-Gerüst** als SCL. Mit `--templates` aus den Jinja2-Templates, sonst generisch. | `--json`, `--out`, `--start`, `--templates [DIR]`, `--strict` | +| `tro_annotate` | Zeichnet dieselben TROs als Marker (Farbe/Form je Typ, Attribute `ID`/`TYPE`/`FB_BLOCK`) in eine **Kopie** der BricsCAD-Zeichnung — zur Sichtkontrolle. Bestimmt dafür den CSV↔DXF-Lagebezug und legt ihn in `cfg/dxf_registration.json` ab. Erzeugt mit `--emit-lisp` die Typliste `cad/tro_types.lsp` für den `TROEDIT`-Dialog. | `--file`, `--dxf`, `--flow`, `--fb`, `--legend`, `--offset`, `--re-register`, `--check`, `--emit-lisp` | +| `tro_extract` | **Geparkt.** Liest TROs aus der annotierten Zeichnung zurück in das alte JSON-Format `sps_skel-tro-extract-v1`, das `scl_skeleton` nicht mehr annimmt. Nicht gelöscht, nicht weiterentwickelt. | `--dxf` | + +### Bibliotheken (`lib/`, ohne eigenen Aufruf) + +| Modul | Wofür | +| --- | --- | +| `tro_catalog.py` | Katalog der TRO-**Typen**: Kurzname, Siemens-FB, Bauteile, Farbgruppe, CAD-Symbol. Einzige Quelle für FB-Typ und Bauteile. | +| `model.py` | Gemeinsames Modell der TRO-**Instanzen** und der TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`, `TroDocument`). | +| `scl_context.py` | Wandelt die TRO-JSON in den Render-Kontext für die SCL (Reihenfolge, Bauteilblöcke, Staubereiche, Weichenziele, offene Namen). | +| `scl_templates/` | Jinja2-Templates je FB-Typ (`.scl.j2`) plus gemeinsame Makros; nur mit `scl_skeleton --templates`. | +| `dxf_registration.py` | Ermittelt, prüft und speichert die Transformation zwischen CSV- und DXF-Koordinaten. | +| `tro_overrides.py` | Liest die Handkorrekturen aus `cfg/tro_overrides.ini`; heute nur von `material_flow --connect` genutzt. | + +### Sonstige Skripte + +| Skript | Wofür | +| --- | --- | +| `doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | Doku-Werkzeug, nicht Teil der Pipeline: erzeugt die Typ-Schaubilder und Topologie-Graphen für `doc/TRO_Katalog/TRO_Katalog.md` aus `connect.ini`. Aufruf: `py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | +| `cad/tro_types.lsp` | Generiertes BricsCAD-LISP (Typliste für den `TROEDIT`-Dialog), erzeugt von `tro_annotate --emit-lisp`. | +| `tests/` | pytest-Tests: TRO-Modell, Render-Kontext, Golden-Files der SCL-Ausgabe (`tests/fixtures/scl/`), Templates gegen die Referenz-Aufrufstellen. | ## Projektstruktur ```text sps_skel/ - bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool - setenv.bat/.sh Umgebungsvariablen setzen - install_py.bat/.sh venv erstellen + pip install - activate_venv.bat/.sh venv aktivieren - get_cmd.bat/.sh Shell mit Umgebung oeffnen - material_flow.bat/.sh Wrapper fuer lib/material_flow.py - tro_flow.bat/.sh Wrapper fuer lib/tro_flow.py - tro_annotate.bat/.sh Wrapper fuer lib/tro_annotate.py - tro_extract.bat/.sh Wrapper fuer lib/tro_extract.py - scl_skeleton.bat/.sh Wrapper fuer lib/scl_skeleton.py - cad/ generiertes BricsCAD-LISP (tro_types.lsp) fuer den TROEDIT-Dialog - cfg/ Konfigurationsdateien (INI/JSON); dxf_registration.json - data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git) - doc/ Dokumentation, siehe doc/Python_Scripts.md - examples/ Beispieldateien - lib/ Python-Quellcode (CLI-Tools + Bibliotheken) - log/ Log-Dateien (nicht im Git) - results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.dxf, nicht im Git) - tests/ Unit Tests + bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool + cad/ generiertes BricsCAD-LISP (tro_types.lsp) für den TROEDIT-Dialog + cfg/ Konfiguration: dxf_registration.json, tro_overrides.ini + data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git) + doc/ Dokumentation, siehe doc/Python_Scripts.md + examples/ Beispieldateien (noch leer) + lib/ Python-Quellcode (CLI-Tools, Bibliotheken, scl_templates/) + log/ Log-Dateien (nicht im Git) + results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.json/.scl/.dxf, nicht im Git) + tests/ Unit Tests (pytest) .gitignore LICENSE README.md @@ -72,94 +121,104 @@ sps_skel/ ## Umgebungsvariablen -| Variable | Beschreibung | -|----------------|---------------------------| -| `SPS_SKEL` | Wurzelverzeichnis | -| `SKEL_BIN` | Skriptverzeichnis | -| `SKEL_LIB` | Python-Quellcode | -| `SKEL_CFG` | Konfigurationsdateien | -| `SKEL_DATA` | Eingabedaten | -| `SKEL_DOC` | Dokumentation | -| `SKEL_LOG` | Log-Dateien | -| `SKEL_RESULTS` | Ergebnisse | -| `SKEL_EXAMPLES` | Beispieldateien | -| `SKEL_TESTS` | Unit Tests | -| `PYTHONPATH` | Erweitert um `SKEL_LIB` | +| Variable | Beschreibung | +|-----------------|-------------------------| +| `SPS_SKEL` | Wurzelverzeichnis | +| `SKEL_BIN` | Skriptverzeichnis | +| `SKEL_LIB` | Python-Quellcode | +| `SKEL_CFG` | Konfigurationsdateien | +| `SKEL_DATA` | Eingabedaten | +| `SKEL_DOC` | Dokumentation | +| `SKEL_LOG` | Log-Dateien | +| `SKEL_RESULTS` | Ergebnisse | +| `SKEL_EXAMPLES` | Beispieldateien | +| `SKEL_TESTS` | Unit Tests | +| `PYTHONPATH` | Erweitert um `SKEL_LIB` | ## Installation ### Voraussetzungen -- Python 3.10 oder hoeher -- Graphviz (`dot`) auf `PATH` fuer `--tosvg`, optional aber empfohlen -- fuer `tro_annotate.py`: `ezdxf` (wird per `requirements.txt` installiert) +- Python 3.10 oder höher +- Pakete aus `requirements.txt`: `pydantic`, `jinja2` (SCL-Templates), `ezdxf` + (nur `tro_annotate`), `pytest` +- Graphviz (`dot`, für `--use-cords` bei `tro_flow` auch `neato`) auf `PATH` + für `--tosvg` — optional, aber empfohlen -### Setup (Windows) +### Setup ```bat -bin\install_py.bat +bin\install_py.bat # Windows ``` -### Setup (Linux / macOS) - ```sh -bash bin/install_py.sh +bash bin/install_py.sh # Linux / macOS ``` ## Nutzung -### Umgebung setzen +### Umgebung ```bat -bin\setenv.bat # Windows -source bin/setenv.sh # Linux / macOS +bin\setenv.bat # Windows: nur Variablen setzen +bin\activate_venv.bat # Windows: venv aktivieren +bin\get_cmd.bat # Windows: neue Shell mit Umgebung ``` -### Shell mit gesetzten Variablen oeffnen - -```bat -bin\get_cmd.bat # Windows -source bin/get_cmd.sh # Linux / macOS +```sh +source bin/setenv.sh +source bin/activate_venv.sh +bash bin/get_cmd.sh ``` -### venv aktivieren +### Typischer Durchlauf + +`--file`/`--dxf` nehmen einen Dateinamen (aufgelöst gegen `%SKEL_DATA%`) oder +einen vollen Pfad; `--json` sucht in `%SKEL_RESULTS%`. ```bat -bin\activate_venv.bat # Windows -source bin/activate_venv.sh # Linux / macOS -``` - -### CLI-Tools ausführen - -Jedes Tool wird über seinen `bin/.bat`/`.sh`-Wrapper aufgerufen; dieser -setzt die Umgebung, aktiviert `.venv` und ruft dann das Python-Modul auf. -`--file`/`--dxf` akzeptieren entweder einen Dateinamen (wird gegen -`%SKEL_DATA%` aufgelöst) oder einen vollen Pfad; Ausgaben landen in -`%SKEL_RESULTS%`. - -```bat -REM 1) Materialfluss der mechanischen Objekte +REM 1) Materialfluss der mechanischen Objekte (optional, zur Kontrolle) bin\material_flow.bat --file mubea.csv --tosvg --doc -REM 2) TRO-Liste + eigenes Flussdiagramm ableiten -bin\tro_flow.bat --file mubea.csv --tosvg --doc +REM 2) TROs ableiten und als TRO-JSON schreiben +bin\tro_flow.bat --file mubea.csv --tosvg --doc --json -REM 3) TROs als Marker-Symbole in eine Kopie der CAD-Zeichnung einbrennen +REM 3) FB_Main-SCL-Geruest erzeugen (generisch bzw. aus Templates) +bin\scl_skeleton.bat --json mubea_tro.json +bin\scl_skeleton.bat --json mubea_tro.json --templates + +REM 4) Optional: TROs zur Sichtkontrolle in eine Kopie der Zeichnung bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend - -REM (Zeichnung optional in BricsCAD von Hand nachbearbeiten) - -REM 4) TROs aus der (ggf. bearbeiteten) Zeichnung zurueck in JSON lesen -bin\tro_extract.bat --dxf export_annotated.dxf - -REM 5) FB_Main-SCL-Geruest aus dem JSON erzeugen -bin\scl_skeleton.bat --json export_tro.json --start TRO07 ``` -Alle Switches, Ausgabedateien und Exit-Codes der drei Tools sind in -[`doc/Python_Scripts.md`](doc/Python_Scripts.md) dokumentiert; das -Domänenmodell (TRO-Typen, geplantes JSON-Layout, I/O-Listen-Analyse) findet -sich in den übrigen Dokumenten unter `doc/`. +`--start TRO07` bei `scl_skeleton` ist nur nötig, solange die Anlage ein +geschlossener Kreis ohne Be-/Entladestation ist. Danach im erzeugten +`*_FB_Main.scl` nach `TODO(E-Planung)` suchen — das sind die Stellen, die aus +der E-Planung gefüllt werden müssen. + +### Tests + +```bat +python -m pytest tests/ +``` + +mit aktivem `.venv`. Die Golden-Files in `tests/fixtures/scl/` legen die +SCL-Ausgabe byteweise fest; neu schreiben (`SKEL_UPDATE_GOLDEN=1`) nur bei einer +geprüften, gewollten Änderung. + +## Roadmap + +Noch offen: + +- das volle **JSON-Layout-Schema** aus `doc/HundM/Json_Layout-Konzept.md` + (`plc`, `controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `jamAreas[]`, + `routing`, …) als Zwischenformat — die TRO-JSON ist bisher TRO-zentriert; +- die übrigen Bausteine je Steuerung (`FB_CallSensors`, `FC_Direction`, + `FC_Call_Jams` als eigene Dateien) — erzeugt wird bisher nur `FB_Main`; +- Templates für die weiteren FB-Typen (siehe `doc/SCL_Templates.md`); +- typabhängige Zeit-Defaults statt `TODO(E-Planung): pruefen`. + +Details: Abschnitt "Roadmap" in `CLAUDE.md`. ## Lizenz diff --git a/doc/Klassenhierarchie.md b/doc/Klassenhierarchie.md index d0cf7ac..3175e21 100644 --- a/doc/Klassenhierarchie.md +++ b/doc/Klassenhierarchie.md @@ -1,6 +1,6 @@ # Klassenhierarchie -Bestandsaufnahme aller Python-Klassen in `lib/` (Stand 2026-10-09). Kurzes +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 @@ -21,27 +21,33 @@ tro_catalog.py (Typ-Ebene: was ein TRO-Typ ist) v tro_flow.py (Ableitung: CSV-Graph -> Analysis -> TroDocument) | - +----------------+ - v v -scl_skeleton.py tro_annotate.py (Ausgabe: SCL-Skeleton / annotierte Zeichnung) + +----------------------+ + 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 tro_overrides.py (eigenständig, ohne - Abhängigkeit auf obige) +material_flow.py dxf_registration.py (eigenständig, ohne Abhängigkeit auf obige) +tro_overrides.py (importiert model.Connection / model.TroCandidate) ``` -`tro_flow.py`, `scl_skeleton.py`, `tro_annotate.py`, `material_flow.py` selbst -definieren keine eigenen Klassen mehr (nur Funktionen) — `material_flow.py` -ist die Ausnahme: seine Graph-Dataclasses sind CSV-Rohmodell, nicht TRO-Modell. +`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 +-- TroBase + +-- RegistrationError +-- _Positioned (model.py, Mixin) (dxf_registration.py) | - +-- DerivedTro - (model.py) + +-- TroBase + | +-- DerivedTro + +-- TroCandidate ``` Das ist die gesamte `class X(Y)`-Liste im Projekt — alle anderen Klassen unten @@ -62,8 +68,10 @@ 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) -├── OpenPoint(BaseModel) (gemeldete, noch nicht modellierte Stelle) +├── TroCandidate(_Positioned) (gemeldete Stelle, an der ein TRO erwartet wird) ├── Analysis(BaseModel) │ ├─ tros: list[DerivedTro] │ ├─ isolated: list[DerivedTro] @@ -72,7 +80,7 @@ lib/model.py [Instanz-Ebene / TRO-JSON] ├─ tros: list[DerivedTro] ├─ boundaries: list[DerivedTro] ├─ connections: list[Connection] - └─ open_points: list[OpenPoint] + └─ candidates: list[TroCandidate] lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass] ├── Element (1 CSV-Zeile) @@ -91,35 +99,47 @@ lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass └─ edges: list[TrackEdge] lib/tro_overrides.py [manuelle Korrekturen aus cfg/tro_overrides.ini, @dataclass] -├── TroOverride (Typkorrektur an einem abgeleiteten TRO) +├── TroOverride (Typkorrektur an einem abgeleiteten TRO: tro_id ODER separator) ├── SplitSpec (TRO aus einem bestehenden heraustrennen) -├── ConnectionSpec (bestätigte Verbindung zwischen zwei TROs) ├── MergeSpec (mehrere TROs zu einem zusammenfassen) -├── OpenPoint (eigene Kopie, NICHT dieselbe Klasse wie model.OpenPoint!) └── Overrides (Sammelcontainer für eine CSV-Datei) ├─ tros: dict[str, TroOverride] ├─ splits: list[SplitSpec] ├─ merges: list[MergeSpec] - ├─ connections: list[ConnectionSpec] - └─ open_points: list[OpenPoint] (tro_overrides.OpenPoint, s.o.) + ├─ 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** mit fast identischen Feldern - (`name, x, y, z, expect, belongs_to/belongsTo, note` + - `describe_position()`): einmal in `model.py` (Pydantic, JSON-Alias - `belongsTo`), einmal in `tro_overrides.py` (Dataclass, kein Alias). Das ist - duplizierte Definition, kein Vererbungsfall — `tro_overrides.py` baut seine - `OpenPoint`-Liste und wandelt sie vermutlich später in `model.OpenPoint` um - (oder sollte das tun; laut Roadmap im Projekt-`CLAUDE.md` ist "`tro_overrides.py` - auf die gemeinsamen `model.Connection`/`model.OpenPoint`-Klassen umstellen" - ohnehin schon als offener Punkt vermerkt). Kandidat: `tro_overrides.py` - importiert künftig `model.OpenPoint` statt eine eigene Kopie zu pflegen. +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, @@ -141,6 +161,256 @@ lib/dxf_registration.py [CSV<->DXF Koordinatentransform] **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 einzig konkret -lohnenden Punkte sind die doppelte `OpenPoint`-Definition (1) und optional -`StrEnum` statt Konstanten-Klassen (3). +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.