Files
sps_skel/doc/Python_Scripts.md
T
Samer AyadiandClaude Opus 5.5 550bdeb464 Weichenknoten zwischen Kreiseln, Track-Mouseover, Parameter in cfg/parameter.ini
TRO-Typen an Kreisel-Beruehrpunkten (lib/kreisel_junctions.py, derive_tros
Schritt 3c): jeder Kreisel als Stadion-Form mit Laufrichtung; jeder
gleichlaufende Beruehrpunkt ist eine Weiche (exportierte ILS Weiche oder
abgeleitet W<a>_<b>), Beruehrpunkte derselben Umlenkung bilden einen
Knoten. Ein Separator steuert ihn, wenn er in Laufrichtung unmittelbar
davor liegt (keine andere Weiche dazwischen). Typ = Separatoren x Weichen.
export.csv: 1Sep1Swi (9/1, 9/8, 8/7), 2Sep1Swi (7/2), 2Sep2Swi (2/3/4),
3Sep2Swi (4/5/6). Neuer Katalogtyp 3Sep2Swi (nicht aus SCL belegt).
Gegenlaeufige Beruehrpunkte (Mubea, Dematic) bleiben bei der alten Regel;
Mubea/HundM05/Dematic unveraendert, Showroom 0018+0019 jetzt 2Sep1Swi.

material_flow --track: Mouseover je Knoten mit Zugehoerigkeit zu
Kreisel/VF/GF (Zuordnung, TrackIds-Kette, Lage laut Geometrie), Weiche mit
beiden Kreiseln, Be-/Entladung mit ihrem Kreisel; Widersprueche mit "!".

Zahlenwerte (Toleranzen, Schwellen, Massstaebe, Groessen) aus allen
lib-Modulen nach cfg/parameter.ini (lib/parameter.py, Abschnitt je Modul,
Ueberschreiben je Anlage ueber %SKEL_CFG%/parameter.ini). Ausgaben aller
Anlagen bytegleich.

tro_flow.py: Docstrings mit Parametern/Rueckgabe, fehlende Typangaben.

Geprueft: 90 Tests gruen; tro_flow/material_flow/scl_skeleton auf 5 CSVs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:20:30 +02:00

58 KiB
Raw Blame History

Python Scripts — CLI Tooling Overview

As of: 2026-10-10

lib/ currently holds eleven Python modules (six of them runnable CLI tools, five supporting libraries), plus one documentation-generator script that lives under doc/TRO_Katalog/tro_graphs/. Together the CLI tools turn a CSV export of the mechanical layout (ILS 2.0) into a material-flow graph, a derived TRO list/diagram, a TRO-JSON (shared model lib/model.py), an FB_Main SCL skeleton generated from that JSON, and an annotated copy of the BricsCAD drawing for visual checking. e_planning.py bridges to the electrical planning: it assigns the sensors/valves of the E-planning drawing to the layout parts, so the skeleton can carry their real names.

This document is the practical "what does each script do and which switches does it take" reference. For the underlying domain model (TRO types, JSON layout concept, I/O-list analysis) see TRO_Typen.md, Json_Layout-Konzept.md, EA-Listen-Analyse.md, BricsCAD_TRO_Symbol.md and doc/TRO_Katalog/TRO_Katalog.md.

1. How the pieces fit together

CSV export (in %SKEL_DATA%)
   │
   ▼
material_flow.py  ──►  <name>_material_flow.dot / .svg / .md
   │   (read_elements, build_graph — reused by tro_flow.py)
   ▼
tro_flow.py       ──►  <name>_tro_flow.dot / .svg, _tro_doc.md, _tro_errors.md
   │   (derives TROs, using the type catalog in tro_catalog.py)
   │
   ├── --json ──►  <name>_tro.json   (model.TroDocument — MAIN PATH)
   │                  │
   │                  ▼
   │               scl_skeleton.py ──►  <name>_FB_Main.scl
   │                  (validates the JSON against model.py; one REGION per TRO;
   │                   E-planning values left as TODO(E-Planung) gaps)
   │                  ▲
   │                  │ --eplan <name>_eplan.json  (optional)
   │               e_planning.py ◄── <drawing>_positionsdraw.json (kabellaengen)
   │                  (assigns BG/MB of the E-planning drawing to the layout
   │                   parts by position + numbering rule; see §5c)
   │
   └── same derivation ──► tro_annotate.py ──► <name>_annotated.dxf  (VIEWER ONLY)
          (also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF
           coordinate offset and tro_catalog.py for marker shape/color per TRO type)

parked:  tro_extract.py   drawing ──► JSON  (not extended; see §5a)

The drawing is a view for checking the result, not a data source: the TRO-JSON is written by tro_flow.py --json straight from the CSV, and tro_annotate.py derives exactly the same TROs to draw them.

  • material_flow.py models the material flow of the mechanical objects only (Gefällestrecke, Strecke, Kreisel).
  • tro_flow.py is its sister program: it derives the control objects (TROs) from the same graph and draws their flow instead.
  • tro_annotate.py takes the TROs from tro_flow.py and burns them into a copy of the BricsCAD drawing as attributed blocks (never touches the original).

Supporting libraries (no CLI of their own, imported by the tools above):

  • lib/tro_catalog.py — the single source of truth for TRO types: short name, Siemens FB block, sub-components and counts, color group, CAD marker shape.
  • lib/model.py — the shared pydantic model of TRO instances and of the TRO-JSON (TroBase, DerivedTro, Connection, TroCandidate, Analysis, TroDocument). Written by tro_flow.py --json, read and validated by scl_skeleton.py. See §6b.
  • lib/dxf_registration.py — derives, verifies and persists the rigid (rotation + offset) transform between the CSV export's coordinate system and the DXF drawing's.
  • lib/tro_ids.py — the per-plant ID file %SKEL_CFG%\<name>_tro_ids.ini: reads it, finds each TRO again by the position of its separators, assigns new IDs, writes/extends the file. Used by tro_flow.py (read and write) and tro_annotate.py (read only). See §4, "TRO-IDs".
  • lib/tro_overrides.py — reads the hand-maintained %SKEL_CFG%\tro_overrides.ini; only material_flow.py --connect still uses it. See §6a.

Separate, documentation-only tool (not part of the generator pipeline):

  • doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py — regenerates the schematic per-type SVGs, the edge-type legend and the per-controller (UH01–UH05) topology graphs embedded in TRO_Katalog.md, built from doc/TRO_Katalog/connect.ini.

2. Running the scripts

Each CLI tool is invoked through its bin/<name>.bat (Windows) / bin/<name>.sh (Linux/macOS) wrapper, which:

  1. calls setenv to set SPS_SKEL, SKEL_DATA, SKEL_RESULTS, SKEL_CFG, SKEL_LIB and prepend SKEL_LIB to PYTHONPATH;
  2. activates .venv if it exists (bin/install_py creates it);
  3. runs the module with py (falling back to python).
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json
bin\scl_skeleton.bat --json mubea_tro.json
bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json
bin\scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend

(--use-cords lays both graphs out at the real plant coordinates; drop it for Graphviz's computed left-to-right arrangement.)

Conventions shared by the CLI tools:

  • --file / --dxf accept either a bare filename — resolved against %SKEL_DATA% — or a full path.
  • All generated output is written to %SKEL_RESULTS%.
  • tro_annotate.py and e_planning.py additionally persist the verified coordinate offset (CSV↔DXF, CSV↔E-planning drawing) to %SKEL_CFG%\dxf_registration.json.
  • Every run prints a ==== bordered report to stdout (counts, warnings, output file paths); non-fatal findings are printed as ?/! lines and also do not stop the run.
  • Requires the packages pinned in requirements.txt (pydantic>=2.0.0; ezdxf>=1.4.0 for tro_annotate.py only) — run bin\install_py.bat once.
  • --tosvg needs Graphviz's dot (or neato, for tro_flow.py --use-cords) on PATH, or the GRAPHVIZ_DOT environment variable pointing at the dot executable. bin/material_flow.bat/.sh and bin/tro_flow.bat/.sh check for the right engine up front when --tosvg is passed and print an early WARNUNG if it's missing (GRAPHVIZ_DOT only covers dot, not neato) — this is just a heads-up before the CSV is even parsed; the Python script still runs and reports the same failure in detail (exit code 2) if the SVG step is reached.

3. material_flow.py — material-flow graph of the mechanical layout

Reads the CSV export and builds a directed graph of the mechanical objects:

  • Gefällestrecke / Strecke (conveyor segments) → one node each.
  • Kreisel (rotary/turntable) → two nodes (-L / -R), linked by the Drehrichtung feature (UZS = clockwise = right→left, GUZ = counter-clockwise = left→right).
  • Separator / Scanner → no node of their own; shown via Zuordnung (assignment) on the label of their owning node.
  • TrackIds (added Sept. 2026): a Kreisel/Strecke/Gefällestrecke row lists the TeileIds of every fine element (Separator, Scanner, Weiche, Ein-/Ausschleuselement, BTMT Be-/Entladung) that belongs to it. Zuordnung on the fine element itself stays authoritative whenever it resolves to a real node — it's more precise than TrackIds for a separator sitting right at a Kreisel↔Strecke boundary, which TrackIds lists under both neighbouring containers (normal, not a conflict). TrackIds only fills gaps Zuordnung can't resolve: a bare Kreisel id with no lane suffix (attached to both lanes then), a missing/empty Zuordnung, or (for BTMT stations, which never had a reliable Zuordnung) the nearest-neighbour distance guess this replaces. An older export without the TrackIds column behaves exactly as before. See _build_track_owners/ _track_owner/_attach_devices/_attach_stations in lib/material_flow.py.
  • Einschleuselement / Ausschleuselement / Weiche (from the richer export) → no node of their own; they supply the direction of the Kreisel↔Strecke edges (Ausschleusung = Kreisel→Strecke at the strecke's Anfang, Einschleusung = Strecke→Kreisel at its Ende, read from their Kreisel/Strecke/Streckenende Merkmale) and mark the Weiche on a Kreisel↔Kreisel link. A strecke whose two ends touch the same Kreisel (return loop) is split across that Kreisel's two lanes. This makes Kreisel↔Strecke direction deterministic instead of height-inferred.
  • BTMT Be-/Entladestation (entry/exit points) → one node each, recognised by TeileArt/Bezeichnung containing Beladung/BTMT-Beladung/Eingang (→ entry, a material source) or Entladung/SC_Entladung/Abwurf/Ausgang (→ exit, a sink). Some exports name these same entry/exit points plainly Eingang/Ausgang instead of the BTMT-specific terms — same role, recognised the same way. An entry gets an outgoing edge, an exit an incoming edge, to the object its owning Kreisel/Strecke names it under in TrackIds (a Kreisel picks the Eingangs-/ Ausgangsbahn from Drehrichtung, same as _weiche_lane_roles); failing that, to the object named in its own Nachbarn (ignoring another station — BTMT Nachbarn often just names nearby separators or the paired station, not a real flow target); failing that, the nearest Kreisel, then the nearest object by 2-D distance, each with a note printed. Recognition is sps_skel-side only — nothing in dxfmakros changes; it simply activates when such a row appears in the CSV.

Note on BTMT / entry-exit data. dxfmakros' EXPORTCSV currently does not emit the BTMT stations (their block names match no export pattern; documented as "Export noch offen"). Until that is addressed, a freshly exported mubea.csv will contain the 61 conveyor items but no BTMT rows, so no entry/exit nodes appear. The recognition above is ready for when BTMT rows are present.

In the rendered graph, parallel Gefällestrecken between the same two Kreisel lanes with the same slope (Hoehe_Von_mm/Hoehe_Bis_mm) — a typical line-storage bank — are drawn as a single Gefällestrecke-styled box with a double border (peripheries=2) instead of one box per line. Its label states the id range and count of the merged lines plus the combined Separator/Scanner id ranges, e.g. Gefaellestrecke 0017 - 0036 (20x) / Separator (20): 0037 - 0056. This is purely a rendering simplification (_group_parallel_gefaelle in lib/material_flow.py) — the underlying graph, --doc report and --connect output are unaffected and still list every line individually.

Flow direction between a Kreisel and a Strecke/Gefällestrecke is set from the Ausschleus-/Einschleuselement at each end (_apply_transfers): the Ausschleuselement is the Kreisel outlet (Kreisel → Strecke, start), the Einschleuselement the inlet (Strecke → Kreisel, end). A conveyor whose two ends dock on the same Kreisel (a return line — Eingang and Ausgang on one Kreisel) is drawn as two edges — one onto each lane (0006-L → VF on the way out, VF → 0006-R on the way back) — so it is never flat/undetermined. The transfer element names its conveyor via the Strecke or Foerderer/Förderer attribute (both are read).

Where no transfer element resolves the direction, drive_uphill applies the physics rule: only a pure Gefällestrecke takes its direction from the height (Hoehe_Von_mm/Hoehe_Bis_mm → flows downhill; a GF with no height runs downhill by definition). A Förderer (VF) can convey up or down and a Strecke is often a mixed GF/VF chain, so height is not a direction indicator for them — they use an unambiguous Antriebfahrtrichtung (exactly "Auf"/"Ab", not a mixed list like "horizontal,horizontal,Auf") and otherwise stay undetermined (direction is expected to come from the AS/ES ends). Only what nothing resolves is drawn bidirectional and reported as a warning.

Entry/exit stations are hung on the Kreisel lane they belong to (_attach_stations): first the Kreisel whose TrackIds umlauf lists the station, then the entry-/exit-lane from its Drehrichtung (UZS → entry lane R / exit lane L, GUZ reversed). Sources (Beladung/Eingang) get Station → lane, sinks (Entladung/Ausgang) lane → Station. Neighbours that aren't flow objects (another station, a separator) are skipped, so two BTMT points no longer get wired to each other.

Eingang/Ausgang are the generic symbols for the concrete BTMT stations Beladung/Entladung (same source/sink role). When a symbol lies almost on top of a BTMT station of the same role (≤ STATION_MERGE_MM = 600 mm, x/y from the Position column), only one node is drawn: the concrete BTMT station is kept, the generic symbol is swallowed (_merge_station_symbols). A standalone Eingang/Ausgang with no nearby BTMT partner is drawn as its own green/red node. --doc additionally cross-checks stated Anzahl_Separator/Anzahl_Scanner counts against what was actually attached.

--track builds a second, finer-grained graph in addition to the one above: Separator, Ausschleus-/Einschleuselement, Weiche and the entry/exit points (BTMT Be-/Entladung, Eingang/Ausgang) each become their own node (they supply no node in the graph described above), showing the actual branch/merge/crossing topology between Kreisel rather than just the mechanical objects.

The ordering comes directly from the TrackIds CSV column (Element.track_ids): every container — Kreisel, Strecke, Gefällestrecke — carries the ordered list of the track elements lying on it, computed by the dxfmakros export (dxfmakros/doc/csv-export.md). build_track_graph draws one directed chain per container from that list:

  • Kreisel → the circular umlauf in Drehrichtung order (the list is already angle-sorted); consecutive entries are linked and the last closes back onto the first (a real, closed loop). Edges are drawn light-blue ("Umlauf").
  • Strecke / Gefällestrecke → the linear chain AS → SEP… → ES in build order, oriented by its ends (Ausschleuselement = start, Einschleuselement = end). Edges are drawn blue ("Bahn").
  • An element listed in two containers (an AS/ES sits in its Strecke and in the docked Kreisel's umlauf; a Kreisel-Kreisel Weiche sits in both Kreisel) is the same node and stitches the containers together automatically — no Nachbarn-based role/feedback heuristic any more.
  • A track element that appears in no TrackIds list (e.g. a free Separator the export didn't chain) is hung off a dashed placeholder for its Zuordnung with an "unbestimmt" edge and a warning — order on that Bahn left open. If the CSV has no TrackIds column at all (older export), every track element falls into this case.

See doc/track.md for the fuller derivation notes.

Switch Argument Default Meaning
--file NAME export.csv CSV input file, resolved against %SKEL_DATA%, or a full path.
--tosvg — off Also render the .dot file to .svg via Graphviz dot.
--use-cords (alias --use-coords) — off Place every node at its plant coordinate and render with neato -n (fixed positions) instead of dot, so the material graph matches the plant layout — the same option tro_flow.py has. The two Kreisel lanes (-L/-R) are nudged apart by LANE_NUDGE_MM; scale is COORD_SCALE (0.25 pt/mm, identical to tro_flow.py). Only has an effect together with --tosvg; fails (exit 1) if any flow node has no coordinate.
--doc — off Also write a Markdown report (<name>_material_flow.md) with object list, connections and plausibility findings.
--connect — off Also write the topology as <name>_connect.ini (item/flow level): […nodes] (Kreisel lanes, Strecke, Gefällestrecke), […connections] with a kind per edge (normal/umlauf/weiche/einschleusung/ausschleusung), and […externals] — the BTMT entry/exit if present, otherwise the open lane-ends listed as entry candidates. A hand-editable bridge into the TRO step, styled after doc/TRO_Katalog/connect.ini.
--show-bbs [flow|all] flow off Also write <name>_bbs.svg: a plain 2-D top-down view of every CSV row (not just flow nodes — Separator/Scanner/Ein-/Ausschleuselement too), one axis-aligned rectangle per row from the Position column (its center, X/Y) and the Boundingbox column (its width/depth, X/Y — the Z extent is ignored), with the row's Bezeichnung centered in it. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (_kreisel_lane_rects() — short axis divided in half, Zuordnung-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with --show-kx) still belong to the Kreisel as a whole and are drawn once, not per half. No rotation, no Graphviz — hand-built SVG; the viewBox stays in plant millimetres (Y flipped so plant "up" renders as screen "up") but the printed page size is scaled to DIN A3 (landscape or portrait, whichever matches the layout's aspect ratio) instead of the plant's real-world extent. Larger objects are drawn first so small ones (Separator, Scanner, …) stay visible on top. A legend column right of the plant lists every drawn kind with its row count, and each rectangle carries a <title> tooltip (row, TeileId, TeileArt, name). Mode flow (default, also bare --show-bbs) skips the VF/GF chain detail rows exported by dxfmakros since Sept. 2026 (Bogen, Streckenelement, Motorstation, Umlenkstation — BBS_DETAIL_KINDS), because they fully overlap their Strecke/Gefällestrecke box; all draws every row, detail kinds in their own stronger colours. dxfmakros calls this via bin/show_exportcsv.bat (mode all).
--show-kx — off Only valid together with --show-bbs (else exit 1). Additionally draw each object's connection coordinate systems K1–K4 into the _bbs.svg: the K1–K4 columns are 12-char base64 strings (three 24-bit fixed-point values x,y,z, factor 10, csv:trans-encode in dxfmakros/Lisp/export.lsp) decoded to a plant-mm X/Y. Each present K-point is drawn as a small red cross (two lines) with a small K1…K4 label, and consecutive points are joined K1→K2→K3→K4 by a blue line. Empty K columns are skipped, so an object with only K1/K2 gets two crosses and one link.
--track — off Additionally write the finer Separator/Ausschleus/Einschleus/Weiche graph described above (<name>_track.dot, and <name>_track.svg with --tosvg). Does not change the normal _material_flow.* output. See doc/track.md. Every node has a mouseover text (tooltip, shown on hover in the SVG): for a separator its Zuordnung, the container whose TrackIds list it (Kreisel/VF/GF) and the Kreisel lane it lies on by geometry (lib/kreisel_junctions.py); for an ILS Weiche the two Kreisel that created it (Kreisel A/B) and the touch point by geometry; for Beladung/Entladung the Kreisel they sit on. Contradictions between these sources get a line starting with ! (e.g. export.csv 0015: Zuordnung Kreisel 0003, lies on Kreisel 0002).
--chain-graph — off Write a "chain-/Lageplan" of the grobe Objekte (big items): every Kreisel split into its Bahn L/R halves (_kreisel_lane_rects(), same split as --show-bbs), every Strecke/Gefällestrecke ("VF"/"GF"), and every Ein-/Ausgabestation (BTMT Beladung/Entladung, or the generic Eingang/Ausgang symbol where no BTMT station absorbed it — see _merge_station_symbols()) — the plant runs as a closed loop, so these stations are its only material sources/sinks and are drawn like any other node, not skipped. Connected by the already-resolved top-level graph.edges (material flow, Kreisel-Umlauf, unresolved). Rendering method and output file are chosen by --graphtype.
--graphtype coarsebox|compound|subgraph coarsebox Only valid together with --chain-graph (else exit 1). coarsebox: containers only (as above), to real scale, hand-built <name>_chain.svg, no Graphviz — the original behaviour. compound: the same to-scale picture, plus each container's Anbauteile (Separator, Scanner, Ausschleus-/Einschleuselement, Weiche) drawn nested inside it at their real position (_container_children() reads them off Element.track_ids, the same source build_track_graph()/--track uses — not a re-derivation from Nachbarn like the old data/gen_chain_svg.py prototype), linked by the already-resolved TrackGraph.edges; still <name>_chain.svg, no Graphviz. subgraph: the same containment expressed as nested Graphviz clusters (subgraph cluster_* — one per Kreisel, nested one per Bahn L/R, plus one per Strecke/Gefällestrecke, each containing its Anbauteile as child nodes) — schematic (dot auto-layout), not to scale; writes <name>_chain.dot always, and <name>_chain.svg additionally with --tosvg (needs Graphviz dot, unlike the other two graph types).

Output (in %SKEL_RESULTS%): <name>_material_flow.dot (always, with pos attributes when --use-cords was used), <name>_material_flow.svg (with --tosvg), <name>_material_flow.md (with --doc), <name>_connect.ini (with --connect), <name>_bbs.svg (with --show-bbs), <name>_track.dot / <name>_track.svg (with --track), <name>_chain.svg (with --chain-graph --graphtype coarsebox|compound, or --graphtype subgraph --tosvg), <name>_chain.dot (with --chain-graph --graphtype subgraph).

Exit codes: 0 ok · 1 input/CLI error · 2 SVG rendering failed (Graphviz).

4. tro_flow.py — derive and draw the TROs

Sister program to material_flow.py — same --file/--tosvg/--doc switches, but instead of the mechanical objects it derives the TROs (Transfer Route Objects, i.e. control-logic units) from the same graph and draws their flow. Derivation rules (see doc/500573_Mubea/TRO_Identifikation_500573.md for the source analysis):

Host of the separator TRO type
Gefällestrecke, ≥ 2 lines sharing an in-/outfeed PinStore_Auto (one block per line group)
Gefällestrecke, single line 1Sep
Strecke (driven) Vario (one per segment)
Kreisel lane 1Sep
Separator before an Ausschleuselement (one / two in a row) 1Sep1Swi / 1Sep2Swi
Junction between Kreisel: n separators directly upstream of m touch points on one pulley 1Sep1Swi, 2Sep1Swi, 2Sep2Swi, 3Sep2Swi
— additionally: 2 or 3 outgoing paths upgraded to 1Sep1Swi / 1Sep2Swi

Junctions between Kreisel (lib/kreisel_junctions.py, step 3c in derive_tros). Every Kreisel is modelled as a stadium (two pulleys plus two straight lanes, from position, Drehung and bounding box) with its direction of travel (Drehrichtung). Wherever two Kreisel touch and run the same way, there is a switch. An exported ILS Weiche is matched to it by position; a touch point without one gets the ID W<KreiselA>_<KreiselB> and a finding (in export.csv three are missing: 2/3, 3/4, 8/9). Touch points on the same pulley form one junction (Kreisel 5 touches 4 and 6 with its west pulley → 2 switches). A separator controls a junction if, walking against the direction of travel from the junction on one of its Kreisel, it is the first thing reached — before any other switch. The type follows from the count: separators × switches. The host of each separator is the lane it physically lies on, not its Zuordnung. If the two Kreisel run against each other at the touch point (both UZS at a pulley-to-pulley touch, as in Mubea and Dematic — probably an export error), no junction is formed, a finding is written, and the ILS Weiche is handled by the older neighbour rule (_find_weiche_separators, step 3c').

A scanner mounted on a separator does not change the type (stays 1Sep); the 1Sep_SSCC special case can't be recognized from the mechanical layout alone and is only raised as a finding. TRO types, FB blocks, sub-components and colors all come from lib/tro_catalog.py — nothing is hard-coded here.

Switch Argument Default Meaning
--file NAME export.csv CSV input file, resolved against %SKEL_DATA%, or a full path.
--tosvg — off Also render the TRO flow diagram to .svg.
--use-cords (alias --use-coords) — off Place every TRO at the centroid of the components it was built from and render with neato -n (fixed positions) instead of dot's computed layout, so the diagram matches the plant layout. Only has an effect together with --tosvg; fails if any TRO has no coordinate.
--doc — off Also write a Markdown report (<name>_tro_doc.md) with the TRO list, connections, findings and the full type catalog.
--connect — off Also write the TRO topology as <name>_tro_connect.ini (nodes, connections, system boundaries, predecessors/successors per TRO). Pure output, never read back.
--true-scale — off Only with --use-cords: draw to scale. By default empty stretches longer than 1500 mm between nodes are shortened (order and neighbourhood are kept).
--json — off Also write the TRO-JSON <name>_tro.json (schema sps_skel-tro-v4, model TroDocument in lib/model.py): TROs with their slots, system boundaries (SYSn), connections with their kind, jamAreas, findings. Input for scl_skeleton.py.
--ids DATEI <name>_tro_ids.ini ID file of the plant (name in %SKEL_CFG%, or a path). Use it to run another export of the same plant against the same IDs, e.g. --file mubea_export.csv --ids mubea_tro_ids.ini.
--renumber — off Number all TROs afresh along the material flow and rewrite the [tro] section of the ID file (early project phase). Renamed IDs are lost; other sections of the file are kept.
--id-tolerance MM (float) 100.0 Maximum distance between an ID-file entry and a separator of a TRO for the TRO to count as found again.

TROs, types and connections always come from the layout alone; cfg/tro_overrides.ini is not read. Errors in the derivation are fixed in lib/tro_flow.py, not in a correction file.

TRO-IDs (lib/tro_ids.py). The IDs belong to the SPS programmers and must survive a new layout export; the layout TeileId changes between exports, the position does not. So every run reads %SKEL_CFG%\<name>_tro_ids.ini:

[tro]
TRO01 = -400.0, 13920.0    ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
K0302_1 = 10310.0, -440.1  ; renamed by hand
  • Each entry is ID = x, y (mm, CSV coordinates) of the TRO's anchor separator. An entry finds the TRO with a separator within --id-tolerance (closest first, one entry per TRO), and that TRO keeps the ID — renamed IDs included. An ID must be unique and a valid SCL identifier; SYSn is reserved for system boundaries.
  • No file yet: the TROs are numbered along the material flow (depth-first from the loading stations over the material-flow graph, on a carousel or Strecke in TrackIds order) and the file is created.
  • A TRO not found gets the next free TRO<n> and is appended to [tro]; an entry without a TRO is reported ("weggefallen", with the TRO that now holds its separator, e.g. after two TROs merged into a 2Sep2Swi) and stays in the file; its ID is not reused.
  • The console report lists found / new / dropped IDs.

Output (in %SKEL_RESULTS%): <name>_tro_flow.dot (always, with pos attributes when --use-cords was used), <name>_tro_flow.svg (with --tosvg), <name>_tro_doc.md (with --doc), <name>_tro.json (with --json), <name>_tro_errors.md (only if TROs are unconnected or of an unknown type). In %SKEL_CFG%: <name>_tro_ids.ini (created, extended or, with --renumber, rewritten — see above).

Exit codes: 0 ok · 1 input/CLI error · 2 SVG rendering failed (Graphviz) · 3 TRO validation errors found (error file written).

5. tro_annotate.py — burn the TROs into a copy of the CAD drawing

Viewer only. The drawing shows the result for a visual check in BricsCAD; it is no longer a data source for the SCL (that is tro_flow.py --json).

Takes the CSV export, the original BricsCAD drawing (DXF) and the TROs derived with exactly the same call as tro_flow.py (including the separator cross-check from <name>_raw.json), and writes a copy of the drawing with:

  • one colored marker block per TRO, each type with its own shape (circle, triangle, square, diamond, …; see TroSymbol in tro_catalog.py) so types are distinguishable even without color;
  • visible attributes ID, TYPE (and FB_BLOCK with --fb); hidden attributes ITEMS, CONFIDENCE, SEPARATORS for downstream tooling;
  • optional directional flow arrows between TROs (--flow) and a legend (--legend).

The original drawing is never modified. Everything this tool adds lives on its own TRO_-prefixed layers and carries XDATA under the app id SPS_SKEL_TRO; a re-run removes exactly those tagged objects (not by layer name) and rebuilds them, so hand-drawn content on the same layers survives.

Once at least one marker of a given type has been annotated, the dxfmakros BricsCAD menu (SSG_LIB > TRO) can insert further markers of that type by hand — TRO_INSERT reuses the TRO_SYM_<Typ> block definition this tool created and only asks for a point, an id and (optionally) a different type; TRO_EDIT changes ID/TYPE on an existing marker. Neither command draws new shapes — a type with no prior annotated instance in the drawing must be run through this tool at least once first.

CSV and DXF live in different coordinate systems; the offset between them is handled by dxf_registration.py — derived once, verified on every run, and cached per CSV/DXF filename pair in %SKEL_CFG%\dxf_registration.json unless overridden.

Switch Argument Default Meaning
--file NAME export.csv CSV export, resolved against %SKEL_DATA%, or a full path.
--dxf NAME — Original drawing as DXF (path, or name resolved against %SKEL_DATA%). Required unless --emit-lisp is used alone. DWG is not supported.
--out NAME <csv-stem>_annotated.dxf Output filename in %SKEL_RESULTS%.
--flow — off Draw directional flow arrows between the TROs.
--fb — off Show the Siemens FB block name as a visible attribute on the marker.
--legend — off Draw a legend of the color groups.
--marker-size MM (float) 300.0 Marker radius in mm.
--text-height MM (float) 220.0 Text height in mm.
--ids DATEI <csv-stem>_tro_ids.ini ID file as for tro_flow.py, read only: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as tro_flow.py would.
--offset DX,DY — Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is 0,0.
--re-register — off Re-derive the coordinate offset even if a verified one is already cached in %SKEL_CFG%.
--check — off Dry run: only analyze and report, write no output file.
--emit-lisp — off Regenerate cad/tro_types.lsp (the type list for the TRO_INSERT/TRO_EDIT dialogs) from tro_catalog.py and exit — no --dxf needed. Written under %DXFM_LISP% if set (next to the dxfmakros SSG_LIB modules that provide the dialogs), otherwise under <SPS_SKEL>/cad.

Layers always carry the prefix TRO_. Like tro_flow.py, it never reads cfg/tro_overrides.ini, so the markers always match the flow diagram.

Output (in %SKEL_RESULTS%, unless --check): <name>_annotated.dxf, <name>_registration.json (the coordinate transform used, with its proof).

Exit codes: 0 ok · 1 input/CLI error · 4 coordinate registration could not be proven (or its residual error exceeds 600 mm) — resolve with --offset.

5a. tro_extract.py — read the TROs back out of the drawing into JSON

Parked since 2026-10-09. The TRO-JSON for scl_skeleton.py now comes from tro_flow.py --json. tro_extract.py is kept but not extended; its output (schema sps_skel-tro-extract-v1) is not accepted by scl_skeleton.py any more, which says so and asks for tro_flow --json. If hand edits in the drawing are ever needed again, it should write the same model.TroDocument format.

The counter-direction to tro_annotate.py:

tro_annotate.py   CSV + derivation  ->  markers in the drawing
tro_extract.py    drawing           ->  JSON

It reads only the drawing, never the CSV. So what comes back is what is actually in the DXF — including anything adjusted by hand in BricsCAD (markers moved, type changed via TRO_EDIT). That is the point: after a review, the drawing is the current state, not the export.

It finds the objects tro_annotate.py tagged with XDATA (AppID SPS_SKEL_TRO): the TRO_SYM_<type> block references (attributes ID/TYPE/ITEMS/CONFIDENCE/SEPARATORS, plus FB_BLOCK when annotated with --fb), the flow arrows ("<from>-><to>"), and the open-point markers ("OFFEN:<name>"). Each TRO also gets predecessors/successors computed from the arrows.

Coordinates appear twice: positionDxf is the drawing coordinate, position the plant coordinate. The conversion uses the registration written by tro_annotate.py (<stem>_registration.json; the _annotated suffix and the file's own dxf_file field are both taken into account when locating it). If no registration is found, position is left out rather than guessed, and a warning says so.

Switches: --dxf NAME (default export_annotated.dxf, looked up in %SKEL_RESULTS%), --out NAME (default <drawing>_tro.json). Exit codes: 0 ok, 1 input error, 2 no TRO markers in the drawing.

fbType is worth one note: unless the drawing was annotated with --fb, the FB block is not an attribute in the DXF. It is then looked up from tro_catalog.py and marked "fbTypeFrom": "tro_catalog" — so a consumer can tell a value that came out of the drawing from one that was resolved.

5b. scl_skeleton.py — generate the FB_Main skeleton as SCL

Last step of the chain:

material_flow.py  objects    -> material flow
tro_flow.py       flow       -> TROs -> <name>_tro.json (--json)
scl_skeleton.py   JSON       -> FB_Main.scl

The JSON is read through model.TroDocument and validated: an unknown or renamed key, or items/fbType that do not match the TRO type in tro_catalog.py, stops the run with exit code 1 and the validation error — no SCL is written. A file with a different schema (e.g. the old tro_extract.py output) gets a short message asking for tro_flow --json. Part counts (items) and the FB type are taken from the catalog via the TRO type.

Produces a skeleton, not a running program: one REGION per TRO with the instance call and every parameter line in the right order, following the real call sites in doc/TRO_Katalog/scl_templates/*.scl.

It will not compile as delivered, and that is deliberate. Every value that has to come from the electrical planning is a visible gap marked TODO(E-Planung) — never an invented value, because a plausible-looking wrong sensor address is far worse than an empty one. Grep for that marker and nothing is missed.

filled from the layout left as TODO(E-Planung)
REGION order (material flow), instance list, FB type per TRO sensor/actuator addresses (cInBG, MB, MA)
number of separator / switch / scanner parameter blocks separator and switch numbers, cMainTro
nTo1Destinations when the TRO has exactly one switch jam-area DB names (DB_JamArea…)
number of jam entries (one per own jam area, not per predecessor) and which jam area each exit leads to (J<Separator> in the comment)
destination list per switch exit (graph reachability) times marked "pruefen"
cross-reference to the layout parts (separator numbers, coordinate)

Two derivations worth knowing about. The destination list per switch exit is computed by walking the TRO graph from each exit without going back through the switch — that is exactly what FC_Direction needs to map destination to direction, and it does not require the I/O list. And nTo1Destinations is only auto-filled for a single-switch TRO; where several switches share the exits, how they divide them is a planning decision, so it stays a gap rather than a guess.

Switches: --json NAME (default export_tro.json, looked up in %SKEL_RESULTS%; written by tro_flow.py --json), --out NAME (default <source>_FB_Main.scl), --eplan NAME (see below), --start TRO. The last one matters while the plant has no loading/unloading station: the flow is then a closed loop with no entry, so the order has to be broken somewhere arbitrary. The chosen start and the reason are recorded in the file header.

Stations (SYSn). Loading/unloading stations from tro_flow.py appear in the JSON under boundaries and as SYSn neighbours of the TROs, not in tros. They get no REGION and no instance. A TRO fed by a SYSn is the start of the order ("Materialeingang ueber SYSn"), so --start is no longer needed once the export contains a station. In the jam-area comments they are marked "(Station, kein TRO)", and an exit station shows up as a destination in the per-switch lists.

Internal structure. scl_skeleton.py works in two stages: lib/scl_context.py derives everything from the TRO-JSON into a render context (build_context → SclFile / SclTro: flow order, separator/switch/scanner blocks, jam entries/exits, destination lists, names), and scl_skeleton.py only turns that context into text (render_context). Every name or address is an SclName (value + todo flag); today all of them are placeholders with todo=True. This split prepares the move to parameterised templates and per-project naming rules described in SCL_Templates.md.

Templates (opt-in, --templates [DIR]). With this switch every TRO whose FB has a template in lib/scl_templates/<fbType>.scl.j2 (Jinja2) is rendered from it; the others keep the generic text above. The header and the console summary name which FB types came from a template. Templates exist for the seven types with a real reference call site (1Sep, 1Sep1Swi, 1Sep2Swi, 1Sep_SSCC, Vario, Vario_workStation, PinStore_Auto); each follows its call site in doc/TRO_Katalog/scl_templates/ line for line, and every value that is still open carries TODO(E-Planung). The line decisions are proposals to be confirmed by a PLC engineer (doc/SCL_Templates.md §6). 2Sep1Swi, EmptyCarrBuffer, LoadingBoom and 2Sep2Swi have no call site yet and stay generic. --strict stops (exit 1, no SCL) if any FB type has no template. Without --templates the output is unchanged.

Names from the E-planning (opt-in, --eplan NAME). Reads the assignment <csv>_eplan.json written by e_planning.py (§5c; bare name looked up in %SKEL_RESULTS%). For every separator it assigns, the separator sensor (stSenInSep := "DB_Inputs".Sensors["cInBG0642"]) and the stopper valve (xOutStopper<n> => "MB0643") are filled in and lose their TODO(E-Planung) marker (SclName.todo = False); everything else stays open. The header names the E-planning file. An assignment made for a different CSV (csv_file ≠ the TRO-JSON's source) stops the run with exit 1 — separator numbers are only unique within one export. Without --eplan the output is unchanged.

Tests: tests/test_scl_context.py (the derivations one by one), tests/test_scl_templates.py (each template rendered with the real values of its reference call site must equal that call site, comments and whitespace ignored) and tests/test_scl_skeleton_golden.py (the whole output of mubea and HundM05 byte for byte against tests/fixtures/scl/*_FB_Main.scl and *_FB_Main_templates.scl, timestamp masked; an intended change is written back with SKEL_UPDATE_GOLDEN=1).

5c. e_planning.py — assign the E-planning to the layout parts

The bridge between layout and electrical planning (background and findings: E-Planung_Bruecke.md). The layout knows which separators, switches and outfeed elements exist and where; the E-planning knows what their sensors and valves are called. The I/O list alone has no topology — but the E-planning is drawn as a layer over the hardware layout, every sensor or actuator symbol sits next to its part, and the cable-length tool (kabellaengen/lib/getpositions.py) writes those symbols with position and attributes to <drawing>_positionsdraw.json.

<name>.csv (layout)  +  <drawing>_positionsdraw.json (E-planning)
   │
   ▼
e_planning.py  ──►  <name>_eplan.json  (EplanDocument, schema sps_skel-eplan-v1)
               ──►  <name>_eplan.md    (report: assignments, gaps, drives)
   │
   ▼
scl_skeleton.py --eplan <name>_eplan.json

Steps:

  1. Read sensors (field devices) and schaltschrank_elemente (cabinet) of the positionsdraw file into EDevice (BMK, VERW, BEZEICHNUNG, KENNZEICHNUNG → controller UH01 / I/O station KF107, position). A BMK listed twice keeps the entry that has a VERW.

  2. Registration E-planning drawing → CSV with dxf_registration.py: the E symbols matching [Registrierung] anker (default IN SEP, the sensor that sits on the separator) against the CSV separators. Stored under the key <csv>|<positionsdraw> in %SKEL_CFG%\dxf_registration.json and re-verified on every run; --re-register forces a new derivation.

  3. Rules from %SKEL_CFG%\e_planning.ini, one section <kind>.<role> per rule, applied in file order; each E device is given out at most once, each role of a layout part filled at most once:

    key meaning
    layout layout kind pattern (default: the part before the dot): Separator, Ausschleus, Einschleus, Weiche, Scanner, Beladung, Entladung
    verw, praefix pattern(s) for the E attribute VERW, BMK prefix (BG, MB)
    max_mm spatial: maximum distance after registration
    soll_mm, toleranz_mm spatial: the symbol always sits this far from the part; candidates are ranked by their deviation from it, not by the smallest distance
    nummer_von, nummer_versatz numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest

    Shipped rules (tuned on Mubea): Separator.sensor, Separator.stopper, Ausschleus.stau, Ausschleus.ventil, Weiche.ventil. VERW texts and the numbering rule are project-specific — in the reference SCL (UH01..UH05) sensor and stopper share the number (BG3242/MB3242), i.e. offset 0.

  4. Drives: motor MAnnnn ↔ motor protection FCnnnn (same number), for the report.

Uncertain cases stay open on purpose: a gap is better than a plausible wrong name. The report lists, per role, the layout parts without a match, the field devices without a match, and distance and runner-up distance of every match (close values = check by hand).

Switches: --file NAME (CSV, default export.csv), --eplan NAME (positionsdraw JSON, required; bare name resolved against %SKEL_DATA%, or a full path), --rules FILE (default %SKEL_CFG%\e_planning.ini), --re-register. Exit 0 also when not everything was assigned; 1 on input errors or when the registration cannot be proven.

Today only separator sensor and stopper reach the SCL. Jam sensors and outfeed/switch valves are assigned in the JSON/report, but the TRO-JSON does not yet say which outfeed element or switch belongs to which TRO exit, so scl_skeleton cannot place them. The same holds for the storage lines of a PinStore TRO (arInSep[n]): their sensors are assigned but the generator does not emit per-line parameters yet.

Result on Mubea (mubea.csv + Mubea_positionsdraw.json): registration dx +50613.6 / dy +8555.8 mm; separator sensor 21/38, stopper 22/38 (all 20 storage lines exact, 0–1 mm), jam sensor 20/23, outfeed valve 20/23, switch valve 1/1. The open separators are on the carousels K0102/K0104/K0105, where the E-planning drawing and the CSV apparently differ.

Tests: tests/test_e_planning.py (reading, rules, numbering vs. distance on a storage-line grid, target distance, uniqueness, registration, drives, the path into scl_context and scl_skeleton --eplan).

6. tro_catalog.py — TRO type catalog (library, no CLI)

The single source of truth for everything needed to generate or draw a TRO type: short name (as used in FB_Main/connect.ini), Siemens FB block, sub-components and their counts, color group (for Graphviz/Mermaid/CAD) and CAD marker shape. Twelve types are defined in TRO_CATALOG. Ten of them are verified against data/*.scl where the source is vendored locally, and against TRO_Typen.md's mapping table otherwise (see the module docstring, CATALOG_AS_OF, for provenance per type).

2Sep2Swi and 3Sep2Swi are the exceptions — they are not backed by any SCL. It was added on 2026-08-12 for project 500573 (Mubea), to model the transfer point at the Kreisel2/Kreisel1 touch as a single control object with 2 separators, 2 scanners and 2 switches. FB_ILS_MTRO_2Sep2Swi does not exist in this repository or in doc/TRO_Katalog/scl_templates/, and has to be created in the ILSLib — otherwise a future SCL generator will emit a call to a missing block. The closest verified type is 2Sep1Swi (2 separators, 2 scanners, 1 switch), which matches the hardware actually present in that export. 3Sep2Swi (3 separators, 3 scanners, 2 switches) was added on 2026-10-14 for the test plant data/export.csv, where one Kreisel pulley touches two others; FB_ILS_MTRO_3Sep2Swi does not exist either.

Everything is a Pydantic v2 model (TroDefinition/TroStyle), so it validates on every assignment and comes with model_dump()/model_validate() for the planned JSON layout model for free. Consumed by model.py, tro_flow.py, tro_annotate.py and scl_skeleton.py; not run directly.

6b. model.py — shared model of TRO instances and the TRO-JSON (library, no CLI)

tro_catalog.py says what a TRO type is; model.py says what one concrete TRO in a plant, and the whole TRO-JSON, look like. It imports only the catalog.

tro_catalog.py   type level      TroDefinition (items, FB block, color, symbol)
     ▲ get_tro()
model.py         instance level  TroBase ─► DerivedTro, Slot, JamArea, Connection,
                                 TroCandidate, Analysis, TroDocument
     ▲
tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
Class What it is
TroBase Core of every TRO: id, type, confidence, separators, predecessors, successors, position ({x,y,z} in JSON). definition(), fb_block (JSON fbType) and items are read from the catalog on every access, never stored — so they follow a type change and cannot go stale.
DerivedTro TroBase + what tro_flow.py knows from the derivation: label, hosts, scanners, reason, colocated, positionFrom, positionPoints, slots. Also used for the system boundaries SYSn (no slots).
Slot A place on a TRO where hardware belongs: slot (separator1, switch1, scanner1, drive — the stable key together with the TRO-ID), layoutId (TeileId in the current export, for tracing only; empty for switches, whose element tro_flow does not keep yet), roles the hardware planning must fill (sensor, stopper, jamSensor? …; ? = optional). Built by derive_slots() from the catalog type.
JamArea A jam area = the queue in front of a separator, the entry of exactly one TRO: id (J<Separator>, e.g. J0042), tro, entry (1, 2 for 2Sep types), separator, host, predecessors (all TROs/stations feeding it — merges have several), note, position. Built by derive_jam_areas(); the SCL name "DB_JamArea<NNNN>".stJam<NNNN>_<k> needs the conveyor number from the hardware planning and is not stored.
Connection Directed edge from → to with its kind (transfer / umlauf / weiche / unbestimmt / normal / bahn).
TroCandidate A spot reported in a review where a TRO is expected but not derived (formerly OpenPoint): name, type (the expected catalog type), position, belongsTo (the TRO that holds the part today), note. Not a TRO — no separators, no connections, no SCL. Built by tro_overrides.py from [<csv>.openpoint.<Name>]; empty in the TRO-JSON for now.
Analysis Working result of tro_flow.analyse_tros (moved here from tro_flow.py).
TroDocument The TRO-JSON: schema (sps_skel-tro-v4), source, created, note, tros, boundaries, connections, candidates, jamAreas, findings. from_analysis() (derives slots and jam areas), to_json(), from_json(), jam_areas_or_derived() (a v3 file without jamAreas still renders).

TroBase and TroCandidate share the position handling (JSON {x,y,z}, x/y/z, describe_position()) through the private mixin _Positioned. The same data has the same field name in every class, including tro_overrides.py: tro_id, type_name, separators, scanners, hosts, label, reason, confidence, position.

All models validate on creation and assignment and reject unknown keys (extra="forbid"). JSON keys keep the camelCase names of the TRO-JSON (id, type, fbType, belongsTo …); Python code uses the field names (tro_id, type_name, fb_block, belongs_to) — both are accepted on input. Schema v3 differs from v2 only in candidates replacing openPoints; v4 adds slots per TRO and jamAreas. A v3 file still reads (both default to empty; scl_skeleton then derives the jam areas while rendering).

Tests: tests/test_model.py (round trip, catalog-backed fields, rejection of wrong keys/items, SCL rendering from a document) — run with python -m pytest tests/.

6a. tro_overrides.py — manual corrections to the derived TROs (library, no CLI)

Since 2026-10-05 only material_flow.py --connect reads this file. tro_flow.py and tro_annotate.py no longer apply corrections: the sections were keyed on running TRO numbers and went stale with every new export. Errors in the TRO derivation are fixed in lib/tro_flow.py instead. The format below is kept for material_flow.py.

tro_flow.py derives TRO types from the mechanical layout alone. Some things are simply not in the CSV export: a switch that the export does not carry, a scanner that lifts the type to 1Sep_SSCC, or a spot that a walkthrough identified as its own transfer point. Those findings come from a site review, and they must survive the next run — the .dot/.svg/.md/.ini outputs are all regenerated from scratch every time, so editing them is not an option.

They therefore live in %SKEL_CFG%\tro_overrides.ini (hand-maintained, never generated). Section names are prefixed with the CSV file they apply to, so several plants — and several exports of the same plant — can coexist:

[export.csv.tro.SEP0011]       ; type correction, keyed on separator 0011
type       = 1Sep1Swi          ; a type from lib/tro_catalog.py
reason     = Weiche vor Ort bestaetigt (Review 2026-08-10)
confidence = bestaetigt        ; default when omitted

[mubea.csv.openpoint.Qualitaetskontrolle]   ; explicitly NOT a TRO yet
x = -1026.00                   ; plant coordinates in mm (CSV system)
y = 5250.05
z = 1478.50
expect     = 1Sep1Swi          ; type expected here
belongs_to = TRO09             ; TRO that owns the part today
note       = ...

A fourth kind folds several derived TROs into one control object:

[export.csv.merge.Beruehrpunkt_K2_K1_gesamt]
type       = 2Sep2Swi
separators = 0005, 0057                  ; at least two, addressed by separator
host       = 0002-R                      ; primary flow node; the others are kept
label      = ...
reason     = ...

A third kind creates a TRO the derivation missed entirely:

[export.csv.split.Beruehrpunkt_K2_K1]   ; pull a separator out of its TRO
type       = 1Sep1Swi
separators = 0057                        ; comma-separated
scanners   =                             ; optional, follow the separators
host       = 0001-L                      ; flow node the new TRO sits on
label      = ...
reason     = ...

Three kinds of entry, deliberately different in weight:

  • tro — changes the type of an already-derived TRO. Applied after the TRO numbers are assigned (the sections address them by ID) and before the FB block and component list are pulled from the catalog, so the type change drags both along. A manually set type also suppresses the automatic "check whether 1Sep_SSCC is needed" hint for that TRO.
  • split — creates a new TRO from separators taken out of an existing one. For transfer points the derivation misses because the separator's Zuordnung points at a conveyor (Strecke/Gefaellestrecke) instead of the Kreisel lane it actually switches on. Applied before the numbers are assigned, so the new TROs slot into the normal ordering — which means adding a split renumbers everything after it. That is exactly why type corrections should be keyed SEP<number>, not TRO<number>.
  • merge — folds several TROs into one. Runs after the splits, so a freshly split-out TRO can be part of a merge. The first affected TRO survives and absorbs the others' separators, scanners and hosts; host picks which flow node is primary, the rest stay attached so the linking still sees both sides of a transition. Like splits, this renumbers.
  • openpoint — a spot reported in a review that is not turned into a TRO. The structure stays untouched. Read as model.TroCandidate (expect → type_name, x/y/z → position, missing z = 0).

In Python the entries are TroOverride (tro_id or separator, from TRO<n> / SEP<n>), SplitSpec/MergeSpec (hosts is a list; the INI key stays host, comma-separated), model.Connection for [<csv>.connections], and model.TroCandidate — field names as in model.py.

Consumer: material_flow.py --connect (writes the [<plant>.manual] section into connect.ini). Keep in mind: TRO IDs are running numbers of one run and are only valid for the CSV named in the section. Swap the data basis and the numbers shift.

7. dxf_registration.py — CSV↔DXF coordinate registration (library, no CLI)

Derives the rigid transform (rotation + offset, no scaling) between the CSV export's and the DXF drawing's coordinate systems, by voting on the offset between same-role anchor points (Separator and Scanner blocks, see ANCHOR_BLOCKS), refining the best candidates via a Procrustes fit, and picking the candidate with the most exact matches. Raises RegistrationError if fewer than MIN_INLIERS (5) exact matches are found for any candidate. Persists/loads the verified transform per CSV/DXF filename pair under %SKEL_CFG%\dxf_registration.json. Used exclusively by tro_annotate.py; not run directly.

ANCHOR_BLOCKS holds name patterns (fnmatch, case-insensitive), not fixed names, because two naming worlds occur in this project:

Drawing Separator blocks Scanner blocks
original customer drawing (500573_60_1.dxf) S-SP, S-LP SCAN
macro-generated (Mubea.dxf, from dxfmakros) Separator_SP_2D/_3D Scanner_2D/_3D

Staustrecke_Separator_SP_* is deliberately not an anchor: it is a conveyor module with a built-in separator, so its insertion point is the module origin rather than the separator, which would feed a second cluster — offset by exactly that distance — into the vote.

Note that the two roles can carry slightly different systematic offsets (in Mubea.dxf: separators land on +100/0, scanners on +400/0), so a mixed fit shows a larger residual_max than either role alone. That is expected; the mean residual is the number to watch, and only pairs within FIT_TOL (50 mm) enter the Procrustes fit.

7a. parameter.py — numeric settings from cfg/parameter.ini (library, no CLI)

Tolerances, thresholds, scales and drawing sizes of the libraries are not written in the code but read from cfg/parameter.ini: one section per module, the key is the constant's name in lower case ([kreisel_junctions] contact_tolerance → kreisel_junctions.CONTACT_TOLERANCE). Meaning and unit of every value are documented as comments in the INI file.

  • The base file in the repository is read first and must be complete — a missing key or a non-number stops the import with ParameterError; there are deliberately no fallback values in the code.
  • A second parameter.ini in %SKEL_CFG% (when it points somewhere else) overrides single values, e.g. a different contact tolerance for one plant.
  • Read by kreisel_junctions, tro_flow, tro_ids, material_flow, dxf_registration, tro_annotate and e_planning (fallback for toleranz_mm in e_planning.ini). tests/test_parameter.py checks that every key is read by its module.
  • Not in the INI, on purpose: format constants (KX_TRANS_FAKTOR, the coding of the CSV columns K1–K4), mathematical epsilons, and style tables (colours, fonts, COORD_SYMBOL_SIZE, symbol proportions in the drawing code).

Changing a value that drives the derivation (a tolerance or threshold) can change TRO types and IDs — compare the outputs of all plants afterwards.

8. gen_tro_graphs.py — documentation image generator (doc tooling, not part of the generator)

Lives under doc/TRO_Katalog/tro_graphs/, not lib/ — it produces the pictures embedded in TRO_Katalog.md, not generator output. Two families:

  1. one schematic SVG per TRO type showing the block with its possible connection edges (normal/dir2/dir3/bypass/finger/plc/extern, each with its own color and line style), plus a shared edge_legend.svg;
  2. one Graphviz topology graph per controller (uh01..uh05), built from doc/TRO_Katalog/connect.ini, rendered to SVG.

No command-line switches — run it directly and re-run after editing connect.ini:

py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py

Needs Graphviz dot on PATH (or at the default Windows install location); without it the .dot files are still written, just not rendered to .svg.