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>
This commit is contained in:
Samer Ayadi
2026-10-14 17:20:30 +02:00
co-authored by Claude Opus 5.5
parent 0c69faf3db
commit 550bdeb464
17 changed files with 1634 additions and 125 deletions
+415 -59
View File
@@ -78,6 +78,49 @@ Hinweis ausgegeben.
Zusaetzlich: hat ein Host 2 bzw. 3 abgehende Flusskanten, wird 1Sep zu
1Sep1Swi bzw. 1Sep2Swi aufgewertet (Weiche).
Wo im Code der Typ entsteht
---------------------------
Einstieg ist analyse_tros(); den TRO-Typ (DerivedTro.type_name) setzt aber
ausschliesslich derive_tros(), Schritt fuer Schritt:
Schritt Bauteil-Muster im CSV -> TRO-Typ
-----------------------------------------------------------------------
1 >= 2 Gefaellestrecken mit Separator, PinStore_Auto
gemeinsamer Zu-/Ablauf (_group_lanes,
_speicher_bloecke)
1 einzelne Gefaellestrecke mit Separator 1Sep
2 Separator in der TrackIds-Kette einer Vario / 1Sep
Strecke (alternierendes Muster, (abwechselnd)
_incoming_drop)
2 Separator mit Zuordnung auf eine Strecke, 1Sep
aber nicht in deren TrackIds-Kette
3 Separator auf einer Kreisel-Bahn 1Sep
3b Separator vor einem Ausschleuselement 1Sep1Swi
(_find_switch_separators, _weichen_*)
3b' derselbe Separator vor einem zweiten 1Sep2Swi
Ausschleuselement ohne Separator dazwischen
3c Weichenknoten zwischen Kreiseln: n Separa- 1Sep1Swi, 2Sep1Swi,
toren in Laufrichtung unmittelbar vor m 2Sep2Swi, 3Sep2Swi
Beruehrpunkten derselben Umlenkung
(_kreisel_knoten, lib/kreisel_junctions.py)
3c' Separator an einer ILS Weiche, die kein 1Sep1Swi
Weichenknoten aus 3c ist
(_find_weiche_separators)
3d 3b-TRO und 3c'-TRO direkt hintereinander 2Sep2Swi
in der TrackIds-Kette eines Kreisels
4 1Sep mit 2 bzw. 3 abgehenden Flusskanten 1Sep1Swi / 1Sep2Swi
Alle anderen Funktionen (link_tros, locate_tros, derive_boundaries, ...)
aendern den Typ nicht mehr. cfg/tro_overrides.ini wird hier seit
2026-10-05 nicht mehr angewendet (siehe tro_overrides.py) - ein falsch
erkannter Typ wird in derive_tros() korrigiert, nicht per INI.
Nicht abgeleitet werden bisher: 1Sep_SSCC (nur Hinweis, Schritt 6),
EmptyCarrBuffer und LoadingBoom. Schritt 3c braucht eine stimmige
Drehrichtung beider Kreisel: laufen sie am Beruehrpunkt gegeneinander
(Mubea, Dematic - vermutlich Exportfehler), bleibt es fuer diese ILS Weiche
bei 3c' und 3d.
Typen, FB-Bausteine, Bauteile und Farben kommen ausschliesslich aus
lib/tro_catalog.py.
@@ -108,6 +151,7 @@ import csv
import json
import math
import sys
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
@@ -123,6 +167,7 @@ from material_flow import (
UNASSIGNED,
Element,
Graph,
Node,
_as_float,
_dot_escape,
_dot_label,
@@ -133,7 +178,9 @@ from material_flow import (
read_elements,
resolve_input,
)
from kreisel_junctions import Junction, find_junctions
from model import Analysis, DerivedTro, TroDocument
from parameter import get_float, get_int
from tro_ids import (
DEFAULT_TOLERANCE,
TroIds,
@@ -159,42 +206,19 @@ from tro_catalog import (
# Konstanten
# ---------------------------------------------------------------------------
# Ab dieser Anzahl zusammenhaengender Schwerkraftlinien (Gefaellestrecke mit je
# einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen.
MIN_STORE_LANES = 2
# Mindest-Hoehenverlust (mm) auf dem der Reihe nach vorangehenden TrackIds-
# Segment, damit ein Separator auf einer "Strecke" trotzdem als 1Sep statt
# Vario gilt (siehe _incoming_drop). Muss nur "horizontal" gefuehrte, aber
# tatsaechlich Gefaelle-Segmente erwischen (HundM05 Separator 0035: -104mm auf
# 3,8m), nicht das Rauschen eines echten flachen VF-Laufs (HundM05 Separator
# 0036: +26mm auf 0,5m). 30mm liegt bewusst weit unter dem echten Befund und
# weit ueber dem Rauschen.
GEFAELLE_DROP_MM = 30.0
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [tro_flow] - Bedeutung und
# Einheit stehen dort (lib/parameter.py).
_P = "tro_flow"
MIN_STORE_LANES = get_int(_P, "min_store_lanes")
GEFAELLE_DROP_MM = get_float(_P, "gefaelle_drop_mm")
COORD_SCALE = get_float(_P, "coord_scale")
COORD_PFEIL_LAENGE = get_int(_P, "coord_pfeil_laenge")
COORD_MAX_LUECKE = get_int(_P, "coord_max_luecke")
LEGEND_MARGIN = get_int(_P, "legend_margin")
LEGEND_DROP = get_int(_P, "legend_drop")
GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
# Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in
# Millimeter. Seit die Symbole eine feste Groesse haben (COORD_SYMBOL_SIZE)
# statt mit ihrem Label zu wachsen, reichen 0.1 pt/mm: ~11 x 20 m Anlage ergeben
# etwa 1100 x 2000 pt - bildschirmfuellend lesbar. Vorher 0.25 (riesige
# Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01).
COORD_SCALE = 0.1
# Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
# (mm) ab dem angehaengten TRO (siehe _station_positionen).
COORD_PFEIL_LAENGE = 1300
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
# TRO) werden auf diesen Wert gekuerzt - Reihenfolge und Nachbarschaft der
# Knoten bleiben erhalten, nur der Leerraum verschwindet (Review 2026-10-01:
# "zu verstreut, zu viel Weissraum"). --true-scale schaltet das ab.
COORD_MAX_LUECKE = 1500
# Abstand der Legende zum Anlagenrand in mm
LEGEND_MARGIN = 3000
# ---------------------------------------------------------------------------
# TRO-Ermittlung
@@ -207,6 +231,17 @@ def _group_lanes(lanes: list[str], graph: Graph) -> list[list[str]]:
Linien, die sich einen Zu- oder Ablaufknoten teilen, gehoeren zum selben
Speicher. So entstehen bei zwei getrennten Speichern auch zwei TROs.
Parameter:
lanes: Knoten-Ids der Gefaellestrecken, die einen Separator tragen
(derive_tros Schritt 1).
graph: Materialflussgraph aus material_flow.build_graph; nur
graph.edges wird gelesen (Nachbarschaft der Linien).
Rueckgabe:
Liste von Gruppen, jede eine sortierte Liste von Linien-Ids; die
Gruppen sind nach ihrer ersten Linie sortiert. Eine Gruppe mit
>= MIN_STORE_LANES Linien wird in derive_tros zu PinStore_Auto.
"""
adjacency: dict[str, set[str]] = {}
for lane in lanes:
@@ -246,6 +281,18 @@ def _kreisel_bahn(sep: Element, elements: list[Element], graph: Graph) -> str |
der Achsrichtung L, rechts R - dieselbe Konvention wie die Zuordnungen,
die der Export richtig setzt (0038 unterhalb von Kreisel2 mit Drehung 180
-> L, 0047 oberhalb von Kreisel3 -> R).
Parameter:
sep: der Separator (Element.kind "Separator").
elements: alle Elemente des CSV-Exports; durchsucht werden die
Kreisel und ihre track_ids.
graph: Materialflussgraph; die gefundene Bahn muss dort ein Knoten
sein (z. B. "0002-L").
Rueckgabe:
Knoten-Id der Bahn ("<Kreisel-Id>-L" bzw. "-R") oder None, wenn der
Separator in keiner Kreiselkette steht, Position bzw. "Drehung"
fehlen oder die Bahn kein Graphknoten ist.
"""
for k in elements:
if k.kind != "Kreisel" or sep.teile_id not in k.track_ids:
@@ -278,6 +325,16 @@ def _weichen_nach_umlauf(
Schluss mit dem Ausschleuselement 0057 von VF_1 "benachbart" (TRO03 als
Weiche in VF_1), 0046 liegt hinter dem letzten Ausschleuselement von
Speicherblock C. Beide sind gewoehnliche Kreisel-Separatoren (Schritt 3).
Parameter:
switch_hits: Ergebnis von _find_switch_separators
(Separator-Id -> (Ausschleuselement, Abstand mm)).
elements: alle Elemente des CSV-Exports (Kreisel mit track_ids).
findings: Fundliste; je verworfenem Paar wird ein Eintrag angehaengt.
Rueckgabe:
Gefiltertes switch_hits. Paare ausserhalb einer Kreiselkette bleiben
ungeprueft erhalten.
"""
by_id = {e.teile_id: e for e in elements}
kreisel = [e for e in elements if e.kind == "Kreisel" and e.track_ids]
@@ -324,6 +381,18 @@ def _weichen_aus_umlauf(
Ausschleuselemente einer Gefaellestrecke bleiben aussen vor: das sind
Speicherlinien, ihr Steuer-Separator entsteht in _speicher_bloecke nur
aus der CSV-Nachbarschaft (Mubea 0044 bleibt eigener 1Sep vor dem Block).
Parameter:
switch_hits: bisherige Paare (Separator-Id -> (Ausschleuselement,
Abstand mm)), meist aus _weichen_nach_umlauf.
kandidaten: Separatoren, die ueberhaupt Weiche sein duerfen (in
derive_tros: ohne die eigenen VF-Kettenseparatoren).
elements: alle Elemente des CSV-Exports.
findings: Fundliste; je ergaenztem Paar wird ein Eintrag angehaengt.
Rueckgabe:
Neues dict: switch_hits plus die ergaenzten Paare. Jedes
Ausschleuselement gehoert hoechstens einem Separator.
"""
by_id = {e.teile_id: e for e in elements}
erlaubt = {s.teile_id for s in kandidaten}
@@ -381,8 +450,16 @@ def _speicher_bloecke(
Mubea 0044 (TRO05) ist durch ein Einschleuselement vom Block getrennt,
kein Nachbar eines Ausschleuselements und bleibt deshalb eigener 1Sep.
Rueckgabe: (Linie -> Block, Steuer-Separator -> Block). Linien ohne
Ausschleuselement in einer Kreisel-Kette fehlen in der ersten Zuordnung.
Parameter:
lanes: Knoten-Ids der Gefaellestrecken mit Separator.
elements: alle Elemente des CSV-Exports (Ausschleuselemente mit
Merkmal "Strecke"/"Foerderer", Kreisel mit track_ids).
switch_hits: Weichen-Separatoren (Separator-Id -> (Ausschleuselement,
Abstand mm)), siehe _find_switch_separators.
Rueckgabe: (Linie -> Block, Steuer-Separator -> Block). Ein Block ist
(Kreisel-Id, laufende Nummer ab 1). Linien ohne Ausschleuselement in
einer Kreisel-Kette fehlen in der ersten Zuordnung.
"""
lane_set = set(lanes)
by_id = {e.teile_id: e for e in elements}
@@ -427,7 +504,17 @@ def _speicher_bloecke(
def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
"""Abgehende Flusskanten eines TRO-Hosts (ohne Kreisel-Umlauf)."""
"""Abgehende Flusskanten eines TRO-Hosts (ohne Kreisel-Umlauf).
Parameter:
hosts: Knoten-Ids, die zusammen den TRO tragen (DerivedTro.hosts).
graph: Materialflussgraph; gezaehlt werden nur Kanten der Art
EDGE_FLOW, die aus hosts hinaus fuehren.
Rueckgabe:
Sortierte, eindeutige Ziel-Knoten-Ids. 2 bzw. 3 Ziele werten einen
1Sep in derive_tros Schritt 4 zu 1Sep1Swi bzw. 1Sep2Swi auf.
"""
return sorted(
{
edge.dst
@@ -438,7 +525,20 @@ def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
def _default_resolve_host(sep: Element, findings: list[str], graph: Graph) -> str | None:
"""Host eines Separators aus der CSV-Spalte 'Zuordnung' (Default-Resolver)."""
"""Host eines Separators aus der CSV-Spalte 'Zuordnung' (Default-Resolver).
Parameter:
sep: der Separator; gelesen wird sep.assignment (Merkmal
"Zuordnung", z. B. "0002-L" oder die Id einer Strecke).
findings: Fundliste; ist die Zuordnung leer oder kein Graphknoten,
wird ein Eintrag angehaengt.
graph: Materialflussgraph, gegen dessen Knoten geprueft wird.
Rueckgabe:
Knoten-Id des Hosts oder None (Separator bekommt dann keinen TRO).
Die Art dieses Knotens (Kreisel/Strecke/Gefaellestrecke) entscheidet
in derive_tros, in welchem Schritt der Separator landet.
"""
host = sep.assignment
if host and host != UNASSIGNED and host in graph.nodes:
return host
@@ -503,6 +603,18 @@ def _find_switch_separators(
Die Distanz im Rueckgabewert ist nur noch Beiwerk fuer die Doku ("XXXmm
von Ausschleuselement YYYY entfernt"), kein Kriterium mehr.
Parameter:
separators: zu pruefende Separatoren (in derive_tros ohne die
eigenen VF-Kettenseparatoren).
elements: alle Elemente des CSV-Exports (Ausschleuselemente und
Kreisel mit track_ids).
Rueckgabe:
Separator-Id -> (Ausschleuselement, Abstand mm); je Separator das
erste passende Element aus seinen Nachbarn. Wird in derive_tros
Schritt 3b zu 1Sep1Swi (oder zum Steuer-Separator eines
PinStore_Auto, siehe _speicher_bloecke).
"""
transfers = {e.teile_id: e for e in elements if e.kind == "Ausschleus"}
kreisel_by_id = {e.teile_id: e for e in elements if e.kind == "Kreisel"}
@@ -555,6 +667,22 @@ def _find_weiche_separators(
bestaetigt (Review 2026-08-12 und 2026-10-01). Ist nur ein Separator
benachbart, gilt dieser. Bleibt es mehrdeutig, wird nichts geraten,
sondern ein Finding geschrieben.
Grenze: einer Weiche wird hoechstens EIN Separator zugeordnet. Weichen
mit mehreren Separatoren (2Sep1Swi, 3Sep2Swi, ...) ermittelt vorher
_kreisel_knoten aus Geometrie und Laufrichtung; hier kommen nur die
ILS Weichen an, die dort keinen Knoten bilden.
Parameter:
separators: zu pruefende Separatoren (in derive_tros ohne die
Ausschleus-Weichen-Separatoren aus switch_hits).
elements: alle Elemente des CSV-Exports (ILS Weichen und Scanner).
findings: Fundliste; je mehrdeutiger Weiche wird ein Eintrag
angehaengt.
Rueckgabe:
Separator-Id -> ILS Weiche (Element.kind "Weiche"). Wird in
derive_tros Schritt 3c' zu 1Sep1Swi.
"""
by_id = {s.teile_id: s for s in separators}
mit_scanner = {
@@ -578,6 +706,48 @@ def _find_weiche_separators(
return result
def _kreisel_knoten(
elements: list[Element], findings: list[str], frei: set[str]
) -> list[Junction]:
"""Weichenknoten zwischen Kreiseln, die einen eigenen TRO bilden.
Geometrie und Steuerregel siehe lib/kreisel_junctions.py: ein Separator
steuert einen Knoten, wenn er auf einem beteiligten Kreisel in
Laufrichtung unmittelbar davor liegt (keine andere Weiche dazwischen).
Parameter:
elements: alle Elemente des CSV-Exports.
findings: Fundliste; je verworfenem Knoten ein Eintrag.
frei: Separator-Ids, die einen Knoten steuern duerfen. Steuert ein
anderer Separator (Ausschleus-Weiche aus 3b, eigener VF-
Kettenseparator) mit, bleibt der Knoten den bisherigen Regeln
(3b, 3c', 3d) ueberlassen.
Rueckgabe:
Die Knoten, deren Typ (j.type_name) im Katalog steht und die nur von
freien Separatoren gesteuert werden.
"""
result: list[Junction] = []
for j in find_junctions(elements, findings):
weichen = ", ".join(c.weiche_id for c in j.contacts)
if not j.separators:
if any(not c.exported for c in j.contacts):
findings.append(
f"Weichenknoten Kreisel {'/'.join(j.kreisel)} ({weichen}): kein "
f"Separator in Laufrichtung davor - kein TRO")
continue
if not set(j.separators) <= frei:
continue
if get_tro(j.type_name) is None:
findings.append(
f"Weichenknoten Kreisel {'/'.join(j.kreisel)} ({weichen}): "
f"{len(j.separators)} Separatoren und {len(j.contacts)} Weichen - "
f"Typ {j.type_name} fehlt im Katalog, kein TRO")
continue
result.append(j)
return result
def _incoming_drop(
host: Element, sep_id: str, elements_by_id: dict[str, Element]
) -> tuple[str | None, float | None]:
@@ -591,6 +761,18 @@ def _incoming_drop(
oder fehlt einem der beiden Enden das Merkmal "Hoehe" (z. B. Aus-/
Einschleuselemente tragen keins), liefert die Funktion (None, None) - der
Aufrufer behandelt das als "kein Gefaelle-Nachweis", bleibt also bei Vario.
Parameter:
host: die Streckengruppe (Element.kind "Strecke") mit track_ids und
drive_dir.
sep_id: TeileId des Separators in host.track_ids.
elements_by_id: TeileId -> Element, fuer die Hoehen der beiden
Segmentenden.
Rueckgabe:
(Antriebfahrtrichtung des Segments, Hoehendifferenz in mm; negativ =
Gefaelle). "horizontal" mit <= -GEFAELLE_DROP_MM stuft in
derive_tros Schritt 2 einen Vario auf 1Sep herab.
"""
chain = host.track_ids
if sep_id not in chain:
@@ -677,23 +859,67 @@ def derive_tros(
) -> list[DerivedTro]:
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden.
Host eines Separators aus der CSV-Spalte "Zuordnung" (_default_resolve_host),
abgehende Wege eines TRO-Hosts aus graph.edges (_switch_exits).
**Hier - und nur hier - wird der TRO-Typ bestimmt** (DerivedTro.type_name,
Uebersicht im Modul-Docstring "Wo im Code der Typ entsteht").
sepliste_counts (optional, siehe load_sepliste_counts): Container-Id ->
Anzahl 'SEP'-Eintraege aus dem Roh-JSON. Reine Gegenprobe - stimmt die aus
der CSV abgeleitete Separatorenzahl eines Strecke/Gefaellestrecke-Hosts
nicht mit dieser Zahl ueberein, wird ein Finding angehaengt; die TRO-Daten
selbst bleiben unangetastet (CSV bleibt die massgebliche Quelle).
Vorbereitung (vor den Schritten):
a) Weichen-Separatoren: _find_switch_separators ->
_weichen_nach_umlauf -> _weichen_aus_umlauf (Ausschleuselemente),
_kreisel_knoten (Weichenknoten zwischen Kreiseln) und
_find_weiche_separators (uebrige ILS Weichen). Diese Separatoren
bekommen keinen Host aus der Zuordnung.
b) Uebrige Separatoren -> Host-Knoten (_default_resolve_host, bei
Gefaellestrecken ggf. korrigiert durch _kreisel_bahn).
c) Scanner -> Separator ueber das Merkmal "Naechster Separator".
tro_ids (optional): ID-Datei der Anlage. Ohne sie werden die TROs entlang
des Materialflusses neu durchnummeriert; mit ihr behalten wiedergefundene
TROs ihre ID (Ergebnis in tro_ids.result).
Schritte (setzen type_name):
1 Gefaellestrecken: PinStore_Auto (>= MIN_STORE_LANES Linien) bzw.
1Sep (einzelne Linie).
2 Strecken: je Separator Vario oder 1Sep (alternierendes Muster,
Gefaelle-Pruefung ueber _incoming_drop).
3 Kreisel-Bahnen: je Separator 1Sep.
3b Separator vor Ausschleuselement: 1Sep1Swi.
3b' zweites Ausschleuselement desselben Separators: 1Sep2Swi.
3c Weichenknoten zwischen Kreiseln (_kreisel_knoten): Typ aus der
Zahl der Separatoren unmittelbar davor und der Weichen, z. B.
2Sep1Swi, 3Sep2Swi.
3c' Separator an einer uebrigen ILS Weiche: 1Sep1Swi.
3d 3b- und 3c'-TRO direkt hintereinander am Kreisel: 2Sep2Swi.
4 1Sep mit 2/3 abgehenden Flusskanten: 1Sep1Swi/1Sep2Swi.
5 Sortieren entlang des Materialflusses, IDs vergeben.
6 Hinweis bei 1Sep mit Scanner (evtl. 1Sep_SSCC, Typ bleibt).
Parameter:
graph: Materialflussgraph aus material_flow.build_graph (Knoten =
Kreisel-Bahnen "<Id>-L/-R", Strecken, Gefaellestrecken,
Stationen; Kanten = Materialfluss).
elements: alle Elemente des CSV-Exports (material_flow.read_elements);
Separatoren, Scanner, Aus-/Einschleuselemente, ILS Weichen und
Kreisel werden daraus gelesen.
findings: Fundliste, an die jeder Schritt Hinweise anhaengt (wird
veraendert, nicht ersetzt).
sepliste_counts: optional, siehe load_sepliste_counts: Container-Id
-> Anzahl 'SEP'-Eintraege aus dem Roh-JSON. Reine Gegenprobe -
stimmt die aus der CSV abgeleitete Separatorenzahl eines
Strecke/Gefaellestrecke-Hosts nicht mit dieser Zahl ueberein,
wird ein Finding angehaengt; die TRO-Daten selbst bleiben
unangetastet (CSV bleibt die massgebliche Quelle).
tro_ids: optional, ID-Datei der Anlage. Ohne sie werden die TROs
entlang des Materialflusses neu durchnummeriert; mit ihr behalten
wiedergefundene TROs ihre ID (Ergebnis in tro_ids.result).
Rueckgabe:
Die TROs in Materialfluss-Reihenfolge, mit tro_id, type_name, label,
hosts, separators, scanners, reason und confidence. Position,
Vorgaenger/Nachfolger fuellt erst analyse_tros (locate_tros,
link_tros).
"""
def resolve_host(sep: Element, findings: list[str]) -> str | None:
"""Host-Knoten eines Separators (siehe _default_resolve_host)."""
return _default_resolve_host(sep, findings, graph)
def count_exits(hosts: list[str]) -> list[str]:
"""Abgehende Flussziele der Hosts (siehe _switch_exits)."""
return _switch_exits(hosts, graph)
separators = sorted(
@@ -732,17 +958,26 @@ def derive_tros(
_weichen_nach_umlauf(
_find_switch_separators(switch_candidates, elements), elements, findings),
switch_candidates, elements, findings)
# Weichenknoten zwischen Kreiseln aus Geometrie und Laufrichtung (siehe
# _kreisel_knoten, Schritt 3c) - ihre Separatoren und Weichen sind damit
# vergeben und laufen nicht mehr durch _find_weiche_separators.
knoten = _kreisel_knoten(
elements, findings, frei={s.teile_id for s in switch_candidates} - set(switch_hits))
knoten_seps = {s for j in knoten for s in j.separators}
knoten_weichen = {c.weiche_id for j in knoten for c in j.contacts}
# Separatoren an einer echten ILS Weiche (siehe _find_weiche_separators) -
# wie switch_hits vor der Host-Aufloesung herausgenommen, Schritt 3c.
# wie switch_hits vor der Host-Aufloesung herausgenommen, Schritt 3c'.
weiche_hits = _find_weiche_separators(
[s for s in switch_candidates if s.teile_id not in switch_hits],
elements, findings,
[s for s in switch_candidates
if s.teile_id not in switch_hits and s.teile_id not in knoten_seps],
[e for e in elements if e.teile_id not in knoten_weichen], findings,
)
# Separator -> Host-Knoten
sep_host: dict[str, str] = {}
for sep in separators:
if sep.teile_id in switch_hits or sep.teile_id in weiche_hits:
if (sep.teile_id in switch_hits or sep.teile_id in weiche_hits
or sep.teile_id in knoten_seps):
continue
host = resolve_host(sep, findings)
# Steht er in der Umlaufkette eines Kreisels, zeigt die Zuordnung aber
@@ -764,7 +999,8 @@ def derive_tros(
sep_scanners: dict[str, list[str]] = {}
for scanner in scanners:
target = str(_merkmal(scanner.merkmale, "Naechster Separator") or "").strip()
if target in sep_host or target in switch_hits or target in weiche_hits:
if (target in sep_host or target in switch_hits or target in weiche_hits
or target in knoten_seps):
sep_scanners.setdefault(target, []).append(scanner.teile_id)
mounted = scanner.assignment
target_host = sep_host.get(target)
@@ -784,9 +1020,11 @@ def derive_tros(
)
def seps_on(hosts: list[str]) -> list[str]:
"""Sortierte Separator-Ids, deren Host in hosts liegt."""
return sorted(s for s, h in sep_host.items() if h in hosts)
def scanners_for(seps: list[str]) -> list[str]:
"""Sortierte Scanner-Ids, die an einem der Separatoren lesen."""
found: list[str] = []
for sep in seps:
found.extend(sep_scanners.get(sep, []))
@@ -1106,7 +1344,35 @@ def derive_tros(
f"Barcode-Scanner am Eingang, im Layout fehlt einer vor Separator "
f"{tro.separators[0]}")
# 3c) Separatoren an einer echten ILS Weiche -> 1Sep1Swi (siehe
# 3c) Weichenknoten zwischen Kreiseln -> nSepmSwi (siehe _kreisel_knoten):
# n = Separatoren, die in Laufrichtung unmittelbar vor dem Knoten liegen,
# m = Weichen (Beruehrpunkte) des Knotens. Host je Separator ist die Bahn,
# auf der er physisch liegt - nicht die Zuordnung (export.csv 0015:
# Zuordnung 0003-L, liegt aber auf Kreisel 0002 Bahn R).
for j in knoten:
weichen = [c.weiche_id for c in j.contacts]
vor = ", ".join(f"{s} ({j.sep_lane[s]})" for s in j.separators)
hosts = list(dict.fromkeys(j.sep_lane[s] for s in j.separators
if j.sep_lane[s] in graph.nodes))
tros.append(DerivedTro(
tro_id="",
type_name=j.type_name,
label=(f"Weichenknoten Kreisel {'/'.join(j.kreisel)}: "
f"Weiche{'n' if len(weichen) > 1 else ''} {', '.join(weichen)}"),
hosts=hosts or [j.sep_kreisel[s] for s in j.separators],
separators=list(j.separators),
scanners=scanners_for(j.separators),
reason=(f"Separator{'en' if len(j.separators) > 1 else ''} {vor} "
f"in Laufrichtung unmittelbar vor "
f"{'den Weichen' if len(weichen) > 1 else 'der Weiche'} "
f"{', '.join(weichen)} (Beruehrpunkt"
f"{'e' if len(weichen) > 1 else ''} der Kreisel "
f"{'/'.join(j.kreisel)})"),
confidence="hoch" if all(c.exported for c in j.contacts) else "mittel",
))
# 3c') Separatoren an einer ILS Weiche, die kein Weichenknoten aus 3c ist
# (z. B. gegenlaeufige Drehrichtung im Export) -> 1Sep1Swi (siehe
# _find_weiche_separators). Host wie bei 3b: der aufgeloeste Zuordnung-Host
# des Separators, sonst die Weiche selbst.
weiche_tro: dict[str, DerivedTro] = {} # Weichen-Id -> 1Sep1Swi
@@ -1232,15 +1498,26 @@ def _flow_rank(graph: Graph) -> dict[str, int]:
return rank
def _flow_key(graph: Graph, elements_by_id: dict[str, Element]):
def _flow_key(
graph: Graph, elements_by_id: dict[str, Element]
) -> Callable[[DerivedTro], tuple[int, int, str]]:
"""Sortierschluessel fuer TROs entlang des Materialflusses.
Erst der Rang des Host-Knotens, dann die Lage des Separators in der
TrackIds-Kette des Hosts (Kreisel: Umlaufrichtung, Strecke: Baureihenfolge).
Parameter:
graph: Materialflussgraph (fuer _flow_rank).
elements_by_id: TeileId -> Element, fuer die track_ids des Hosts.
Rueckgabe:
Funktion fuer sorted()/list.sort(key=...), die je TRO
(Host-Rang, Index in der TrackIds-Kette, erster Separator) liefert.
"""
rank = _flow_rank(graph)
def key(tro: DerivedTro) -> tuple:
def key(tro: DerivedTro) -> tuple[int, int, str]:
"""(Host-Rang, Index in der TrackIds-Kette, erster Separator)."""
host_rank = min((rank.get(h, len(rank)) for h in tro.hosts),
default=len(rank))
sep = tro.separators[0] if tro.separators else ""
@@ -1328,7 +1605,7 @@ def _tro_edge_kind(edge: Edge) -> str:
return "normal"
def _bahn_richtung(node) -> tuple[float, float] | None:
def _bahn_richtung(node: Node) -> tuple[float, float] | None:
"""Foerderrichtung einer Kreisel-Bahn als Einheitsvektor.
Der Kreisel ist eine geschlossene Schleife: auf der einen Bahn laeuft das
@@ -1414,7 +1691,8 @@ def _chain_mit_ungelisteten_separatoren(
if dx == 0 and dy == 0:
return chain
def _t(pos) -> float:
def _t(pos: tuple[float, float, float]) -> float:
"""Projektion von pos auf die Richtung anfang -> ende (unskaliert)."""
return (pos[0] - anfang.position[0]) * dx + (pos[1] - anfang.position[1]) * dy
# Kettenelemente ohne Position behalten ihren Platz: sie erben den Wert des
@@ -1507,6 +1785,7 @@ def link_tros(
elem_owner.setdefault(sep_id, tro.tro_id)
def _add_chain_pair(a: str, b: str) -> None:
"""Gerichtete Kante TRO a -> TRO b aus der TrackIds-Kette eintragen."""
if a == b:
return
# Keine Sperre fuer Systemgrenzen noetig: sie sind kein Kettenglied
@@ -1863,7 +2142,23 @@ def analyse_tros(
) -> Analysis:
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu nummerieren.
Gemeinsamer Einstieg fuer tro_flow.py und tro_annotate.py. Der TRO-Typ
wird NICHT hier bestimmt, sondern in derive_tros (erster Aufruf unten);
danach kommen nur noch Systemgrenzen (derive_boundaries), Koordinaten
(locate_tros) und Verbindungen (link_tros) dazu.
Parameter:
graph: Materialflussgraph aus material_flow.build_graph.
elements: alle Elemente des CSV-Exports (material_flow.read_elements).
sepliste_counts: optional, Separatorenzahl je Container aus dem
Roh-JSON (load_sepliste_counts) - nur Gegenprobe.
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu
nummerieren.
Rueckgabe:
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
(Typnamen ohne Katalogeintrag), isolated (TROs ohne Verbindung) und
den sortierten findings.
"""
analysis = Analysis()
analysis.tros = derive_tros(graph, elements, analysis.findings,
@@ -1892,6 +2187,12 @@ def analyse_tros(
def _tro_label(tro: DerivedTro) -> str:
"""Mehrzeiliges DOT-Label eines TRO-Knotens.
Zeilen: ID und Typ, Bezeichnung, FB-Baustein, Bauteile laut Katalog,
Separatoren (ab 7 gekuerzt), Scanner, Koordinate - jeweils nur, wenn
vorhanden.
"""
lines = [f"{tro.tro_id} ({tro.type_name})", tro.label]
if tro.fb_block:
lines.append(tro.fb_block)
@@ -1981,6 +2282,7 @@ def _station_positionen(
je_tro.setdefault(anker, []).append(station)
def _abstand(a: float, b: float) -> float:
"""Kleinster Winkelabstand (rad) zweier Richtungen."""
d = abs(a - b) % (2 * math.pi)
return min(d, 2 * math.pi - d)
@@ -2060,7 +2362,7 @@ def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float,
return (0.0, 0.0)
xs = [p[0] for p in zeichen.values()]
ys = [p[1] for p in zeichen.values()]
return (max(xs) + LEGEND_MARGIN, max(ys) - 1000)
return (max(xs) + LEGEND_MARGIN, max(ys) - LEGEND_DROP)
def _zeichen_positionen(
@@ -2099,6 +2401,7 @@ STATION_FARBE = {
def _tro_edge_attrs(kind: str | None) -> str:
"""DOT-Kantenattribute zur Kantenart (TRO_EDGE_STYLE); None = "normal"."""
return TRO_EDGE_STYLE.get(kind or "normal", "")
@@ -2118,6 +2421,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
zeichen = _zeichen_positionen(analysis, compact) if use_coords else {}
def _pos(node: DerivedTro) -> str:
"""DOT-Attribut ', pos="x,y"' des Knotens oder ''."""
if use_coords:
p = zeichen.get(node.tro_id)
return (f', pos="{p[0] * COORD_SCALE:.2f},{p[1] * COORD_SCALE:.2f}"'
@@ -2336,6 +2640,16 @@ def render_connect_ini(analysis: Analysis, section: str, source: str) -> str:
def render_errors(analysis: Analysis, csv_file: Path) -> str:
"""Fehlerbericht <name>_tro_errors.md als Markdown-Text.
Parameter:
analysis: Ergebnis von analyse_tros (isolated, unknown_types).
csv_file: Eingabe-CSV, nur fuer Titel und Quellenangabe.
Rueckgabe:
Der Markdown-Text. Ob er geschrieben wird (nur bei isolierten TROs
oder unbekannten Typen), entscheidet write_error_file.
"""
out = [GENERATED_MARKER, f"# TRO-Fehler - {csv_file.name}", ""]
out.append(f"**Erzeugt:** {datetime.now().strftime('%Y-%m-%d %H:%M')} ")
out.append(f"**Quelle:** `{csv_file}` ")
@@ -2406,10 +2720,16 @@ def write_error_file(analysis: Analysis, csv_file: Path, path: Path) -> bool:
def _mermaid_text(text: str) -> str:
"""Text fuer ein Mermaid-Knotenlabel entschaerfen (\" und [] ersetzen)."""
return text.replace('"', "'").replace("[", "(").replace("]", ")")
def _tro_mermaid(analysis: Analysis) -> list[str]:
"""TRO-Fluss als Mermaid-Flowchart (Zeilen inklusive ```-Zaun).
Knoten je TRO (Farbe nach Katalogtyp) und Systemgrenze, Kanten aus
analysis.edges, gestrichelt TROs auf derselben Bahn (colocated).
"""
out = ["```mermaid", "flowchart LR"]
out.extend(f" {line}" for line in mermaid_classdefs())
@@ -2447,6 +2767,20 @@ def render_doc(
svg_file: Path | None,
error_file: Path | None,
) -> str:
"""TRO-Dokumentation <name>_tro_doc.md (--doc) als Markdown-Text.
Parameter:
analysis: Ergebnis von analyse_tros.
graph: Materialflussgraph (Host-Namen und -Arten fuer die Tabellen).
csv_file: Eingabe-CSV (Titel, Quellenangabe).
dot_file: erzeugte DOT-Datei, wird verlinkt.
svg_file: erzeugtes SVG oder None (ohne --tosvg).
error_file: Fehlerdatei oder None (fehlerfreier Lauf).
Rueckgabe:
Der Markdown-Text: TRO-Liste mit Typ und Begruendung, Mermaid-Fluss,
Verbindungen, Typdefinition aus dem Katalog, Findings.
"""
out: list[str] = [GENERATED_MARKER]
add = out.append
@@ -2650,6 +2984,17 @@ def report(
warnings: list[str],
engine: str = "dot",
) -> None:
"""Konsolenbericht eines Laufs: TRO-Zahl je Typ, Findings, Warnungen,
erzeugte Dateien.
Parameter:
analysis: Ergebnis von analyse_tros.
csv_file: Eingabe-CSV.
dot_file: erzeugte DOT-Datei.
svg_file / error_file / doc_file: erzeugte Dateien oder None.
warnings: Warnungen aus material_flow (Einlesen, Graphaufbau).
engine: Graphviz-Programm des SVG ("dot" oder "neato", mit -n).
"""
types_used: dict[str, int] = {}
for tro in analysis.tros:
types_used[tro.type_name] = types_used.get(tro.type_name, 0) + 1
@@ -2709,6 +3054,7 @@ def report(
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Kommandozeile auswerten; argv=None liest sys.argv[1:]."""
parser = argparse.ArgumentParser(
prog="tro_flow",
description="Ermittelt aus einem CSV-Export der Anlagenobjekte die "
@@ -2866,6 +3212,16 @@ def report_ids(tro_ids: TroIds, status: str) -> None:
def main(argv: list[str] | None = None) -> int:
"""CLI-Einstieg: CSV lesen, Graph bauen, analyse_tros, Ausgaben schreiben.
Parameter:
argv: Argumente ohne Programmnamen; None = sys.argv[1:].
Rueckgabe:
Exitcode: 0 = Erfolg, 1 = Eingabe- oder Ausgabefehler, 2 = SVG nicht
erzeugt, 3 = TRO-Pruefung fehlgeschlagen (isolierte TROs oder
unbekannte Typen, siehe Fehlerdatei).
"""
args = parse_args(argv)
csv_file = resolve_input(args.file)