TRO-Fluss: Weichenerkennung ohne Bahn-Sperre, Vario/1Sep-Sonderfaelle je Separator
_find_switch_separators: Regel 2 (naechster freier Separator <= 1500mm) lief bisher nur auf einer Bahn, die Regel 1 (<=500mm) schon bestaetigt hatte. Bei HundM05 lagen nach einer CAD-Aktualisierung alle 5 echten Weichen-Separatoren gleichmaessig bei ca. 1013-1016mm - ausserhalb von Regel 1, wodurch die Bahn-Sperre fuer jede Bahn zublieb und keine einzige Weiche mehr erkannt wurde. Die Sperre entfaellt; SWITCH_FALLBACK_MM bleibt die alleinige Schranke gegen Fehlzuordnungen (der eigentliche Schutz vor dem urspruenglichen Mubea-Fehlfund, der 5-9m entfernte Separatoren betraf). derive_tros, angetriebene Strecken (Schritt 2): zwei Faelle stufen einen Separator trotz Host "Strecke" als 1Sep statt Vario ein: - Letzter Separator vor dem Wiedereinschleusen auf die Kreisel-Bahn (in TrackIds-Reihenfolge) - reiner Vereinzeler ohne eigene VarioDrive-Strecke, die Kreisel-Bahn treibt ab dort selbst an (HundM05 VF_3 vor Ort bestaetigt). - Ein nicht-letzter Separator, dessen vorangehendes Segment "horizontal" (keine aktive Antriebsrichtung) gefuehrt wird, dabei aber tatsaechlich an Hoehe verliert (> 30mm) - ein unbetriebenes Gefaelle-Teilstueck innerhalb einer gemischten VF/GF-Streckengruppe (HundM05 VF_1/0035). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
df6d56090e
commit
97965e8ec7
+383
-62
@@ -28,9 +28,32 @@ doc/500573_Mubea/TRO_Identifikation_500573.md:
|
||||
-----------------------------------------------------------------------
|
||||
Gefaellestrecke, >= 2 Linien -> PinStore_Auto (ein Block je Gruppe)
|
||||
Gefaellestrecke, einzelne Linie -> 1Sep
|
||||
Strecke (angetrieben) -> Vario (je Streckensegment)
|
||||
Strecke (angetrieben) -> Vario (je Separator, siehe unten)
|
||||
Kreisel-Bahn -> 1Sep
|
||||
|
||||
Zwei Ausnahmen stufen einen Separator auf einer "Strecke" trotzdem als 1Sep
|
||||
statt Vario ein (Review 2026-09-23, HundM05):
|
||||
|
||||
1. Letzter Separator vor dem Wiedereinschleusen auf die Kreisel-Bahn (in
|
||||
TrackIds-Reihenfolge) - reiner Vereinzeler ohne eigene VarioDrive-Strecke,
|
||||
die Kreisel-Bahn treibt ab dort selbst an. Gilt fuer jede Separatoranzahl
|
||||
>= 2 (z. B. VF_3: erster Separator 0041 = Vario, letzter 0040 = 1Sep); bei
|
||||
genau einem Separator bleibt dieser Vario (einziger Antriebspunkt).
|
||||
2. Eine "Strecke" (Streckengruppe) ist selbst oft eine gemischte VF/GF-Kette:
|
||||
sie enthaelt sowohl angetriebene (VF, jede Antriebfahrtrichtung -
|
||||
horizontal, Auf oder Ab zaehlt gleichermassen als VF) als auch unbetriebene
|
||||
Gefaelle-Teilstuecke (GF) - siehe Merkmal "Antriebfahrtrichtung"/"VF_Winkel"
|
||||
je TrackIds-Segment. Ein (nicht letzter) Separator, dessen unmittelbar
|
||||
vorangehendes Segment als "horizontal" (=keine aktive Antriebsrichtung)
|
||||
gefuehrt wird, dabei aber tatsaechlich an Hoehe verliert (Merkmal "Hoehe"
|
||||
der Anbauteile, > 30mm Gefaelle), sitzt auf einem GF-Teilstueck und wird
|
||||
ebenfalls 1Sep (siehe _incoming_drop). Achtung: diese Hoehen-/
|
||||
Antriebsangaben sind bei mehreren Streckengruppen desselben Typs (z. B.
|
||||
VF_2/VF_3/VF_4 bei HundM05) identisch und damit vermutlich unkorrigierte
|
||||
CAD-Blockvorgaben, nicht echte Einzelmessungen - Regel 1 (Position in der
|
||||
Kette) ist deshalb der robustere Nachweis, Regel 2 nur ein Zusatzfund fuer
|
||||
Faelle mit echten, individuellen Messwerten (wie VF_1).
|
||||
|
||||
Ein Scanner am Separator aendert den Typ NICHT: in connect.ini der
|
||||
Referenzanlage sind Separatoren mit Scanner ueberwiegend 1Sep (z. B.
|
||||
"TRO104 = 1Sep | TRO 104 (1Sep)+Scanner"). 1Sep_SSCC ist dem Sonderfall mit
|
||||
@@ -73,6 +96,7 @@ from material_flow import (
|
||||
LANE_RIGHT,
|
||||
ROTATION_CCW,
|
||||
ROTATION_CW,
|
||||
TRANSPORT_KINDS,
|
||||
UNASSIGNED,
|
||||
Element,
|
||||
Graph,
|
||||
@@ -93,6 +117,7 @@ from tro_catalog import (
|
||||
STYLES,
|
||||
TRO_CATALOG,
|
||||
TroDefinition,
|
||||
TroItem,
|
||||
get_tro,
|
||||
mermaid_classdefs,
|
||||
)
|
||||
@@ -131,6 +156,15 @@ SWITCH_PROXIMITY_MM = 500.0
|
||||
# zugeordnet, auch wenn das 5-9 Meter entfernt lag.
|
||||
SWITCH_FALLBACK_MM = 1500.0
|
||||
|
||||
# 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
|
||||
|
||||
GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
|
||||
|
||||
# Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in
|
||||
@@ -309,18 +343,29 @@ def _find_switch_separators(
|
||||
die Strecke zeigt (in den Daten kommt beides vor, siehe derive_tros).
|
||||
2. Naechstgelegener freier Separator je noch unbelegtem Ausschleuselement,
|
||||
jedes Paar nur einmal vergeben (kleinste Distanz zuerst, hoechstens
|
||||
SWITCH_FALLBACK_MM), und nur auf einer Bahn, die bereits durch Regel 1
|
||||
einen bestaetigten Treffer hat (eine Bahn ohne jeden nahen Treffer hat
|
||||
vermutlich gar keine Weiche, z. B. Kreisel1 Bahn R bei HundM05). Grund
|
||||
fuer den zweiten Durchlauf (HundM05, Review 2026-09-18): an einem
|
||||
Kreisel mit 5 Abzweigen lagen die tatsaechlichen Schalt-Separatoren pro
|
||||
Abzweig unterschiedlich weit vom Uebergabeelement entfernt (418-1139mm
|
||||
in der damaligen Registrierung) - eine einzelne mm-Schwelle (Regel 1)
|
||||
trifft nicht alle. SWITCH_FALLBACK_MM begrenzt das trotzdem: ohne
|
||||
Obergrenze hat Regel 2 an einer groesseren Anlage (Mubea) uebrig
|
||||
gebliebene, tatsaechlich unbeteiligte Separatoren dem naechsten noch
|
||||
freien Ausschleuselement zugeordnet, auch wenn das 5-9 Meter entfernt
|
||||
lag - siehe SWITCH_FALLBACK_MM.
|
||||
SWITCH_FALLBACK_MM) - unabhaengig davon, ob Regel 1 auf derselben Bahn
|
||||
bereits einen Treffer hatte. Grund fuer den zweiten Durchlauf (HundM05,
|
||||
Review 2026-09-18): an einem Kreisel mit 5 Abzweigen lagen die
|
||||
tatsaechlichen Schalt-Separatoren pro Abzweig unterschiedlich weit vom
|
||||
Uebergabeelement entfernt (418-1139mm in der damaligen Registrierung)
|
||||
- eine einzelne mm-Schwelle (Regel 1) trifft nicht alle.
|
||||
SWITCH_FALLBACK_MM begrenzt das trotzdem: ohne Obergrenze hat Regel 2
|
||||
an einer groesseren Anlage (Mubea) uebrig gebliebene, tatsaechlich
|
||||
unbeteiligte Separatoren dem naechsten noch freien Ausschleuselement
|
||||
zugeordnet, auch wenn das 5-9 Meter entfernt lag - siehe
|
||||
SWITCH_FALLBACK_MM.
|
||||
|
||||
Frueher lief Regel 2 nur auf einer Bahn, die Regel 1 bereits bestaetigt
|
||||
hatte (eine Bahn ohne jeden nahen Treffer hat vermutlich gar keine
|
||||
Weiche). Diese Bahn-Sperre wurde entfernt (Review 2026-09-23): bei
|
||||
einer neueren HundM05-Revision lagen ALLE 5 echten Schalt-Separatoren
|
||||
gleichmaessig bei ca. 1013-1016mm - ausserhalb von Regel 1 (500mm), aber
|
||||
innerhalb von SWITCH_FALLBACK_MM. Ohne einen einzigen Regel-1-Treffer
|
||||
blieb die Bahn-Sperre fuer jede Bahn zu, Regel 2 kam nie zum Zug, und
|
||||
keine der 5 Weichen wurde erkannt. SWITCH_FALLBACK_MM bleibt die
|
||||
einzige Schranke gegen Fehlzuordnungen (siehe Mubea-Fall oben) - sollte
|
||||
sich das an einer weiteren Anlage als zu grosszuegig erweisen, braucht
|
||||
es dort eine gezieltere Loesung statt der Bahn-Sperre.
|
||||
"""
|
||||
transfers = [e for e in elements if e.kind == "Ausschleus"]
|
||||
result: dict[str, tuple[Element, float]] = {}
|
||||
@@ -338,15 +383,9 @@ def _find_switch_separators(
|
||||
if best is not None:
|
||||
result[sep.teile_id] = (best[1], best[0])
|
||||
|
||||
# Bahnen mit mindestens einem durch Regel 1 bestaetigten Treffer - nur
|
||||
# dort darf Regel 2 ueberhaupt suchen (siehe Docstring).
|
||||
switch_bahnen = {sep.assignment for sep in separators
|
||||
if sep.teile_id in result and sep.assignment}
|
||||
|
||||
used_transfers = {hit[0].teile_id for hit in result.values()}
|
||||
remaining_seps = [s for s in separators if s.teile_id not in result
|
||||
and s.x is not None and s.y is not None
|
||||
and s.assignment in switch_bahnen]
|
||||
and s.x is not None and s.y is not None]
|
||||
|
||||
candidates: list[tuple[float, Element, Element]] = []
|
||||
for sep in remaining_seps:
|
||||
@@ -369,6 +408,39 @@ def _find_switch_separators(
|
||||
return result
|
||||
|
||||
|
||||
def _incoming_drop(
|
||||
host: Element, sep_id: str, elements_by_id: dict[str, Element]
|
||||
) -> tuple[str | None, float | None]:
|
||||
"""Antriebfahrtrichtung und Hoehendifferenz (mm) des TrackIds-Segments,
|
||||
das unmittelbar vor sep_id in host.track_ids liegt.
|
||||
|
||||
host ist eine "Strecke" (Streckengruppe); ihr Merkmal "Antriebfahrtrichtung"
|
||||
ist eine Kommaliste, ein Eintrag je Segment zwischen zwei aufeinander-
|
||||
folgenden TrackIds-Elementen (siehe derive_tros/Docstring-Tabelle). Fehlt
|
||||
sep_id in der Kette, ist es das erste Element (kein vorangehendes Segment)
|
||||
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.
|
||||
"""
|
||||
chain = host.track_ids
|
||||
if sep_id not in chain:
|
||||
return None, None
|
||||
idx = chain.index(sep_id)
|
||||
if idx == 0:
|
||||
return None, None
|
||||
prev = elements_by_id.get(chain[idx - 1])
|
||||
cur = elements_by_id.get(sep_id)
|
||||
if prev is None or cur is None:
|
||||
return None, None
|
||||
prev_h = _as_float(_merkmal(prev.merkmale, "Höhe", "Hoehe"))
|
||||
cur_h = _as_float(_merkmal(cur.merkmale, "Höhe", "Hoehe"))
|
||||
if prev_h is None or cur_h is None:
|
||||
return None, None
|
||||
drive_list = [d.strip() for d in (host.drive_dir or "").split(",")]
|
||||
drive = drive_list[idx - 1] if idx - 1 < len(drive_list) else None
|
||||
return drive, cur_h - prev_h
|
||||
|
||||
|
||||
def derive_tros(
|
||||
graph: Graph,
|
||||
elements: list[Element],
|
||||
@@ -396,6 +468,7 @@ def derive_tros(
|
||||
scanners = sorted(
|
||||
(e for e in elements if e.kind == "Scanner"), key=lambda e: e.teile_id
|
||||
)
|
||||
elements_by_id = {e.teile_id: e for e in elements}
|
||||
|
||||
# Separatoren an einem Ausschleuselement (siehe _find_switch_separators)
|
||||
# werden VOR der normalen Host-Aufloesung herausgenommen: sie bilden gleich
|
||||
@@ -486,23 +559,78 @@ def derive_tros(
|
||||
)
|
||||
)
|
||||
|
||||
# 2) Angetriebene Strecken -> je Segment ein Vario
|
||||
# 2) Angetriebene Strecken -> je SEPARATOR ein eigener Vario-TRO. Katalog
|
||||
# (TRO_CATALOG in tro_catalog.py) definiert Vario als 1x Separator + 1x
|
||||
# VarioDrive - mehrere Separatoren auf demselben Streckensegment sind
|
||||
# mehrere eigenstaendige Uebergabestellen, keine gemeinsame (Review
|
||||
# 2026-09-21). Ein Segment ganz ohne Separator behaelt weiterhin einen
|
||||
# einzelnen TRO (leere Separatorliste), damit das Antriebssegment selbst
|
||||
# nicht verschwindet.
|
||||
for node_id in sorted(n for n, node in graph.nodes.items() if node.kind == "Strecke"):
|
||||
seps = seps_on([node_id])
|
||||
element = graph.nodes[node_id].element
|
||||
drive = element.drive_dir or "unbekannt"
|
||||
tros.append(
|
||||
Tro(
|
||||
tro_id="",
|
||||
type_name="Vario",
|
||||
label=element.name,
|
||||
hosts=[node_id],
|
||||
separators=seps,
|
||||
scanners=scanners_for(seps),
|
||||
reason=f"angetriebenes Streckensegment (Antriebfahrtrichtung: {drive})",
|
||||
confidence="hoch",
|
||||
reason = f"angetriebenes Streckensegment (Antriebfahrtrichtung: {drive})"
|
||||
|
||||
# Letzter Separator vor dem Wiedereinschleusen auf die Kreisel-Bahn
|
||||
# (in echter TrackIds-Reihenfolge, nicht sortierte Teile-Id) ist ein
|
||||
# reiner Vereinzeler ohne eigene VarioDrive-Strecke: die Kreisel-Bahn
|
||||
# uebernimmt den Antrieb ab dort selbst - bestaetigt am Beispiel VF_3
|
||||
# (Review 2026-09-23): TRO11 (erster Separator 0041, direkt nach dem
|
||||
# Ausschleusen) ist Vario, TRO10 (zweiter/letzter Separator 0040, vor
|
||||
# dem Wiedereinschleusen) ist 1Sep. Gilt unabhaengig von der
|
||||
# Separatoranzahl - siehe auch VF_1 (0034, letzter von vieren). Nur
|
||||
# bei genau einem Separator gibt es kein "erster vs. letzter", der
|
||||
# bleibt Vario (er ist der einzige Antriebspunkt der Strecke).
|
||||
seps_set = set(seps)
|
||||
track_seps = [tid for tid in element.track_ids if tid in seps_set]
|
||||
last_sep_id = track_seps[-1] if len(track_seps) > 1 else None
|
||||
|
||||
for sep_id in seps or [None]:
|
||||
sep_list = [sep_id] if sep_id else []
|
||||
type_name = "Vario"
|
||||
sep_reason = reason
|
||||
confidence = "hoch"
|
||||
if sep_id is not None and sep_id == last_sep_id:
|
||||
type_name = "1Sep"
|
||||
confidence = "mittel"
|
||||
sep_reason = (
|
||||
f"letzter Separator vor dem Wiedereinschleusen von "
|
||||
f"{element.name} auf die Kreisel-Bahn - reiner Vereinzeler, "
|
||||
f"kein eigenes VarioDrive mehr noetig (Review 2026-09-23)"
|
||||
)
|
||||
findings.append(
|
||||
f"{element.name} Separator {sep_id}: letzter Separator vor dem "
|
||||
f"Wiedereinschleusen - als 1Sep statt Vario eingestuft"
|
||||
)
|
||||
elif sep_id is not None:
|
||||
seg_drive, seg_drop = _incoming_drop(element, sep_id, elements_by_id)
|
||||
if seg_drive == "horizontal" and seg_drop is not None and seg_drop <= -GEFAELLE_DROP_MM:
|
||||
type_name = "1Sep"
|
||||
confidence = "mittel"
|
||||
sep_reason = (
|
||||
f"Separator {sep_id} liegt auf einem unbetriebenen "
|
||||
f"Gefaelle-Teilstueck der Streckengruppe {element.name} "
|
||||
f"(Hoehe faellt um {-seg_drop:.0f}mm, keine aktive "
|
||||
f"Antriebfahrtrichtung) - kein Vario trotz Host 'Strecke'"
|
||||
)
|
||||
findings.append(
|
||||
f"{element.name} Separator {sep_id}: {-seg_drop:.0f}mm Gefaelle "
|
||||
f"ohne Antrieb auf dem vorangehenden Segment - als 1Sep statt "
|
||||
f"Vario eingestuft"
|
||||
)
|
||||
tros.append(
|
||||
Tro(
|
||||
tro_id="",
|
||||
type_name=type_name,
|
||||
label=element.name,
|
||||
hosts=[node_id],
|
||||
separators=sep_list,
|
||||
scanners=scanners_for(sep_list),
|
||||
reason=sep_reason,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 3) Separatoren auf Kreisel-Bahnen -> je Separator eine Uebergabestelle
|
||||
for sep_id in sorted(
|
||||
@@ -624,26 +752,35 @@ def derive_tros(
|
||||
|
||||
|
||||
# Typnamen fuer Systemgrenz-Knoten (siehe derive_boundaries) - bewusst NICHT
|
||||
# in TRO_CATALOG: eine BTMT Be-/Entladestation ist kein Steuerobjekt mit
|
||||
# eigenem FB-Baustein, nur der tatsaechliche Materialein-/ausgang.
|
||||
# in TRO_CATALOG: eine Be-/Entladestation (BTMT oder generisches Eingang/
|
||||
# Ausgang-Symbol) ist kein Steuerobjekt mit eigenem FB-Baustein, nur der
|
||||
# tatsaechliche Materialein-/ausgang.
|
||||
BOUNDARY_ENTRY = "Materialeingang"
|
||||
BOUNDARY_EXIT = "Materialausgang"
|
||||
BOUNDARY_TYPES = (BOUNDARY_ENTRY, BOUNDARY_EXIT)
|
||||
|
||||
|
||||
def derive_boundaries(elements: list[Element], graph: Graph) -> list[Tro]:
|
||||
"""BTMT Be-/Entladestationen als sichtbare Systemgrenz-Knoten.
|
||||
"""Be-/Entladestationen als sichtbare Systemgrenz-Knoten.
|
||||
|
||||
Ohne diese Knoten "verschwindet" der Materialfluss stillschweigend an der
|
||||
Station: link_tros() registriert eine Kante nur, wenn die Traversierung
|
||||
einen Knoten erreicht, der ein TRO-Host ist. Eine BTMT-Station ist nie
|
||||
ein TRO-Host (sie hat keinen Separator), also wird sie beim Durchlaufen
|
||||
als passiver Zwischenknoten behandelt - und da eine Entladung (Senke)
|
||||
keine ausgehende Kante mehr hat, bricht die Suche dort einfach ab, ohne
|
||||
eine Kante einzutragen. Eine Beladung (Quelle) wird umgekehrt nie als
|
||||
Ausgangspunkt einer Traversierung betrachtet, weil kein TRO dort beginnt.
|
||||
Damit fehlte bislang genau der Teil des Flusses, den man am Anfang oder
|
||||
Ende eines Durchlaufs durch die Anlage sieht.
|
||||
einen Knoten erreicht, der ein TRO-Host ist. Eine Station ist nie ein
|
||||
TRO-Host (sie hat keinen Separator), also wird sie beim Durchlaufen als
|
||||
passiver Zwischenknoten behandelt - und da eine Entladung/ein Ausgang
|
||||
(Senke) keine ausgehende Kante mehr hat, bricht die Suche dort einfach
|
||||
ab, ohne eine Kante einzutragen. Eine Beladung/ein Eingang (Quelle) wird
|
||||
umgekehrt nie als Ausgangspunkt einer Traversierung betrachtet, weil kein
|
||||
TRO dort beginnt. Damit fehlte bislang genau der Teil des Flusses, den
|
||||
man am Anfang oder Ende eines Durchlaufs durch die Anlage sieht - die
|
||||
Anlage laeuft als Kreislauf, diese Stationen sind ihre einzigen
|
||||
Materialquellen/-senken (siehe auch material_flow.py --chain-graph).
|
||||
|
||||
Erfasst sowohl die konkrete BTMT-Station (Beladung/Entladung) als auch das
|
||||
generische Eingang/Ausgang-Symbol (element.is_station, STATION_KINDS in
|
||||
material_flow.py) - ein durch eine deckungsgleiche BTMT-Station
|
||||
verschlucktes Symbol taucht dank des graph.nodes-Filters unten trotzdem
|
||||
nicht doppelt auf (_merge_station_symbols(), siehe build_graph()).
|
||||
|
||||
Kein eigener TRO-Typ (keine FB_ILS_MTRO_*-Zuordnung, kein Eintrag in
|
||||
TRO_CATALOG) - nur ein sichtbarer Marker fuer den echten Anfang/das echte
|
||||
@@ -653,7 +790,7 @@ def derive_boundaries(elements: list[Element], graph: Graph) -> list[Tro]:
|
||||
"""
|
||||
boundaries: list[Tro] = []
|
||||
for element in elements:
|
||||
if element.kind not in ("Beladung", "Entladung"):
|
||||
if not element.is_station:
|
||||
continue
|
||||
if element.teile_id not in graph.nodes:
|
||||
continue
|
||||
@@ -665,7 +802,7 @@ def derive_boundaries(elements: list[Element], graph: Graph) -> list[Tro]:
|
||||
hosts=[element.teile_id],
|
||||
separators=[],
|
||||
scanners=[],
|
||||
reason="Systemgrenze (BTMT Be-/Entladestation) - kein TRO, "
|
||||
reason="Systemgrenze (Be-/Entladestation) - kein TRO, "
|
||||
"nur Materialein-/ausgang",
|
||||
confidence="hoch",
|
||||
)
|
||||
@@ -754,6 +891,7 @@ def link_tros(
|
||||
for tro in tros:
|
||||
for host in tro.hosts:
|
||||
owners.setdefault(host, []).append(tro.tro_id)
|
||||
by_id = {t.tro_id: t for t in tros}
|
||||
|
||||
forward: dict[str, list[tuple[str, str]]] = {}
|
||||
for edge in graph.edges:
|
||||
@@ -782,26 +920,169 @@ def link_tros(
|
||||
continue
|
||||
queue.extend(forward.get(node, []))
|
||||
|
||||
# Mehrere TROs auf derselben Kreisel-Bahn liegen HINTEREINANDER, nicht
|
||||
# nebeneinander: das Material laeuft die Bahn in Foerderrichtung ab. Sie
|
||||
# werden daher der Reihe nach verkettet statt nur als "gleiche Bahn"
|
||||
# markiert - sonst fehlt z. B. die Kante von der einen Sperre zur naechsten.
|
||||
# Gruppiert wird ueber _tro_bahn() (Separator-Zuordnung), NICHT ueber
|
||||
# owners/host: ein Weichen-TRO liegt zwar physisch auf der Bahn, sein Host
|
||||
# wurde in derive_tros Schritt 3b aber auf die gefuetterte Strecke gelegt
|
||||
# (review 2026-09-21 - sonst fehlt bei HundM05 die Reihenfolge 0008->0010->
|
||||
# 0012->0014->0020 auf Kreisel1 Bahn L komplett, sobald deren Weichen einen
|
||||
# eigenen Host haben und nur noch ein einzelner Separator auf der Bahn
|
||||
# selbst uebrig bleibt).
|
||||
# verkettete_paare (ungerichtet) unterdrueckt die "gleiche Bahn"-Punktlinie
|
||||
# weiter unten - dafuer reicht Ortsgleichheit unabhaengig von der Richtung.
|
||||
# directed_paare (gerichtet) markiert dagegen genau die Kanten, die durch
|
||||
# eine dieser Regeln bewusst gesetzt wurden - fuer den Weichen-
|
||||
# Redundanzfilter weiter unten muss das richtungsscharf sein: TRO12->TRO01
|
||||
# (echte Umlaufkette) darf die entgegengesetzte TRO01->TRO12 (grober,
|
||||
# redundanter Contraction-Walk-Fund) NICHT mitdecken, nur weil es dieselben
|
||||
# zwei TROs sind (Review 2026-09-21, Bug beim ersten Versuch dieses Filters).
|
||||
verkettete_paare: set[frozenset[str]] = set()
|
||||
directed_paare: set[tuple[str, str]] = set()
|
||||
|
||||
# 1. Ground-Truth-Umlaufreihenfolge je Kreisel aus Element.track_ids
|
||||
# (dieselbe nach Drehrichtung-Winkel sortierte Liste, die auch --track/
|
||||
# build_track_graph verwendet - siehe doc/track.md). Laeuft ZUERST, damit
|
||||
# der Positions-Fallback weiter unten weiss, welche Kreisel damit schon
|
||||
# praezise abgedeckt sind. Ersetzt fuer diese Kreisel die aus dem groben
|
||||
# Materialfluss-Graph kontrahierten "Nabe -> jeder erreichbare Zweig"-
|
||||
# Kanten weiter unten: die kennen keine Reihenfolge und zeigen daher u. U.
|
||||
# rueckwaerts (Review 2026-09-21 - TRO11 zeigte auf alle 5 Abzweige
|
||||
# gleichrangig, obwohl es in der echten Umlaufreihenfolge der LETZTE
|
||||
# Separator vor der Entladung ist).
|
||||
elem_owner: dict[str, str] = {}
|
||||
for tro in tros:
|
||||
if tro.type_name in BOUNDARY_TYPES:
|
||||
elem_owner.setdefault(tro.hosts[0], tro.tro_id)
|
||||
else:
|
||||
for sep_id in tro.separators:
|
||||
elem_owner.setdefault(sep_id, tro.tro_id)
|
||||
|
||||
def _add_chain_pair(a: str, b: str) -> None:
|
||||
if a == b:
|
||||
return
|
||||
# Eine Systemgrenze ist nur in EINE Richtung gueltig (siehe
|
||||
# derive_boundaries: Beladung ist Quelle, nie Ziel; Entladung ist
|
||||
# Senke, nie Quelle) - unabhaengig davon, wie TrackIds sie in der
|
||||
# geschlossenen Umlaufliste nacheinander auffuehrt.
|
||||
if by_id[a].type_name == BOUNDARY_EXIT or by_id[b].type_name == BOUNDARY_ENTRY:
|
||||
return
|
||||
pair = (a, b)
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, "bahn")
|
||||
verkettete_paare.add(frozenset(pair))
|
||||
directed_paare.add(pair)
|
||||
trackids_pairs.add(pair)
|
||||
|
||||
trackids_pairs: set[tuple[str, str]] = set()
|
||||
covered_kreisel_bases: set[str] = set()
|
||||
|
||||
# 0. Reihenfolge der Vario-TROs INNERHALB einer Strecke, aus derselben
|
||||
# Element.track_ids-Quelle wie der Kreisel-Umlauf unten, hier aber linear
|
||||
# (AS -> SEP.. -> ES, keine geschlossene Schleife) - siehe build_track_graph()
|
||||
# in material_flow.py, dessen Richtungskorrektur hier repliziert wird. Seit
|
||||
# derive_tros Schritt 2 einen eigenen Vario-TRO je Separator anlegt (Review
|
||||
# 2026-09-21: "jedes Vario-TRO soll genau 1 Separator haben"), muessen
|
||||
# mehrere Varios auf derselben Strecke in echter Reihenfolge verkettet
|
||||
# werden - strecke_first_owned/strecke_last_owned merken sich den jeweils
|
||||
# ersten/letzten fuer die Anschluss-Kanten an Weiche bzw. Wiedereinschleusung.
|
||||
elements_by_id = {e.teile_id: e for e in elements}
|
||||
strecke_first_owned: dict[str, str] = {}
|
||||
strecke_last_owned: dict[str, str] = {}
|
||||
for el in elements:
|
||||
if el.kind not in TRANSPORT_KINDS:
|
||||
continue
|
||||
chain = list(el.track_ids)
|
||||
if len(chain) >= 2:
|
||||
first_el, last_el = elements_by_id.get(chain[0]), elements_by_id.get(chain[-1])
|
||||
if ((first_el and first_el.kind == "Einschleus")
|
||||
or (last_el and last_el.kind == "Ausschleus")):
|
||||
chain = list(reversed(chain))
|
||||
# Ein Separator, der (Zuordnung) zwar auf dieser Strecke steht, aber
|
||||
# selbst eine Weiche ist (z. B. HundM05 Separator 0016 auf VF_5 - siehe
|
||||
# derive_tros Schritt 3b), gehoert NICHT in die Vario-Kette der Strecke:
|
||||
# er ist der Kreisel-Bahn zugehoerig und dort schon (Schritt 1 unten)
|
||||
# verkettet. Ohne diesen Ausschluss wuerde er faelschlich als erster
|
||||
# oder letzter "Vario" gelten (Review 2026-09-21).
|
||||
owned = [
|
||||
elem_owner[tid] for tid in chain if tid in elem_owner
|
||||
and not ((d := by_id[elem_owner[tid]].definition()) and d.has_item(TroItem.SWITCH))
|
||||
]
|
||||
if not owned:
|
||||
# Segment ganz ohne Separator (derive_tros Schritt 2: leere
|
||||
# Separatorliste) - der einzelne Vario-TRO dieses Hosts bleibt
|
||||
# trotzdem Anschlusspunkt fuer Weiche/Wiedereinschleusung.
|
||||
solo = [tid for tid in owners.get(el.teile_id, [])
|
||||
if by_id[tid].type_name == "Vario"]
|
||||
if len(solo) == 1:
|
||||
owned = solo
|
||||
for a, b in zip(owned, owned[1:]):
|
||||
_add_chain_pair(a, b)
|
||||
if owned:
|
||||
strecke_first_owned[el.teile_id] = owned[0]
|
||||
strecke_last_owned[el.teile_id] = owned[-1]
|
||||
|
||||
# Einschleuselement-Id -> das Vario-TRO, das dort real wieder einmuendet
|
||||
# (der LETZTE Vario-TRO der Strecke in echter Reihenfolge, nicht irgendein
|
||||
# beliebiger - Review 2026-09-21).
|
||||
einschleus_vario: dict[str, str] = {}
|
||||
for el in elements:
|
||||
if el.kind != "Einschleus":
|
||||
continue
|
||||
strecke_id = str(_merkmal(el.merkmale, "Strecke", "Förderer") or "")
|
||||
vario_id = strecke_last_owned.get(strecke_id)
|
||||
if vario_id:
|
||||
einschleus_vario[el.teile_id] = vario_id
|
||||
|
||||
for el in elements:
|
||||
if not el.is_circle or len(el.track_ids) < 2:
|
||||
continue
|
||||
chain = list(el.track_ids)
|
||||
if el.rotation in (ROTATION_CW, ROTATION_CCW):
|
||||
chain.append(chain[0]) # Umlauf schliessen
|
||||
|
||||
# Hauptkette: Separatoren/Systemgrenzen in Reihenfolge, Aus-/
|
||||
# Einschleuselemente bewusst uebersprungen (unowned) - das beschreibt
|
||||
# den BYPASS fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03,
|
||||
# wenn dazwischen niemand abbiegt).
|
||||
owned = [(i, elem_owner[tid]) for i, tid in enumerate(chain) if tid in elem_owner]
|
||||
for (_, a), (_, b) in zip(owned, owned[1:]):
|
||||
_add_chain_pair(a, b)
|
||||
if owned:
|
||||
covered_kreisel_bases.add(el.teile_id)
|
||||
|
||||
# Zusaetzlich der reale Wiedereintritt eines abgezweigten Vario-
|
||||
# Segments: vom Einschleuselement zum naechsten Halt DANACH in
|
||||
# derselben Kette - ersetzt fuer dieses Vario-TRO den groben "Strecke
|
||||
# -> Bahn"-Kontraktionsfund (der pauschal auf DEN EINEN TRO zeigt, der
|
||||
# zufaellig noch die Kreisel-Bahn als Host hat, unabhaengig davon,
|
||||
# welche Strecke tatsaechlich zurueckkehrt - Review 2026-09-21: "warum
|
||||
# zeigen alle Varios auf TRO12"). Unabhaengig von der Hauptkette oben,
|
||||
# die an dieser Stelle bewusst durchlaeuft (Bypass, siehe dort).
|
||||
for i, tid in enumerate(chain):
|
||||
vario_id = einschleus_vario.get(tid)
|
||||
if vario_id is None:
|
||||
continue
|
||||
nxt = next((o for j, o in owned if j > i), None)
|
||||
if nxt is None and owned:
|
||||
nxt = owned[0][1] # Umlauf geschlossen - wickelt um
|
||||
if nxt is not None and nxt != vario_id:
|
||||
_add_chain_pair(vario_id, nxt)
|
||||
|
||||
# Nur TROs, fuer die die Umlaufkette tatsaechlich einen praezisen
|
||||
# Nachfolger gefunden hat (precise_sources) - ein TRO ohne brauchbare
|
||||
# TrackIds-Abdeckung behaelt seine bisherigen (ungenaueren, aber nicht
|
||||
# leeren) Kanten aus dem Contraction-Walk als Rueckfall. Angewendet wird
|
||||
# der Filter erst ganz unten, nachdem auch die Weichen-Kanten feststehen.
|
||||
precise_sources = {a for a, _ in trackids_pairs}
|
||||
|
||||
# 2. Positions-Fallback: mehrere TROs auf derselben Kreisel-Bahn liegen
|
||||
# HINTEREINANDER, nicht nebeneinander - der Reihe nach verkettet statt nur
|
||||
# als "gleiche Bahn" markiert. NUR fuer Kreisel, die Schritt 1 NICHT schon
|
||||
# praezise abgedeckt hat (covered_kreisel_bases) - sonst gruppiert dieser
|
||||
# Fallback rein nach CSV-Spalte "Zuordnung" und uebersieht z. B. bei
|
||||
# HundM05 die Separatoren 0016/0018 (auf "0005" zugeordnet, nicht auf der
|
||||
# Bahn) und zieht faelschlich eine direkte Kante 0014->0020 an ihnen
|
||||
# vorbei (Review 2026-09-21).
|
||||
sep_assignment = {e.teile_id: e.assignment for e in elements
|
||||
if e.kind == "Separator" and e.assignment}
|
||||
bahn_gruppen: dict[str, list[Tro]] = {}
|
||||
for tro in tros:
|
||||
bahn = _tro_bahn(tro, graph, sep_assignment)
|
||||
if bahn:
|
||||
if bahn and bahn.split("-")[0] not in covered_kreisel_bases:
|
||||
bahn_gruppen.setdefault(bahn, []).append(tro)
|
||||
|
||||
verkettete_paare: set[frozenset[str]] = set()
|
||||
for bahn, gruppe in bahn_gruppen.items():
|
||||
if len(gruppe) < 2:
|
||||
continue
|
||||
@@ -822,12 +1103,52 @@ def link_tros(
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, "bahn")
|
||||
verkettete_paare.add(frozenset(pair))
|
||||
directed_paare.add(pair)
|
||||
|
||||
# Ein Weichen-TRO auf derselben Strecke wie ein Vario-TRO ist keine blosse
|
||||
# Ortsgleichheit, sondern der zweite der beiden moeglichen Wege an der
|
||||
# Weiche: entweder weiter auf der Kreisel-Bahn zum naechsten Separator
|
||||
# (verkettete_paare oben) oder durch die Weiche hindurch in dieses Segment
|
||||
# - konkret in dessen ERSTEN Vario-TRO (strecke_first_owned oben; bei
|
||||
# mehreren Separatoren auf derselben Strecke sind die weiteren bereits
|
||||
# ueber die Strecken-Kette oben an den ersten angeschlossen). Gerichtete
|
||||
# Kante statt "gleiche Bahn"-Punktlinie, mit dem bereits vorhandenen
|
||||
# "weiche"-Kantenstil (orange, Label "Weiche"). Review 2026-09-21.
|
||||
for tro in tros:
|
||||
definition = tro.definition()
|
||||
if not (definition and definition.has_item(TroItem.SWITCH)) or len(tro.hosts) != 1:
|
||||
continue
|
||||
ziel_id = strecke_first_owned.get(tro.hosts[0])
|
||||
if ziel_id is None or ziel_id == tro.tro_id:
|
||||
continue
|
||||
pair = (tro.tro_id, ziel_id)
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, "weiche")
|
||||
verkettete_paare.add(frozenset(pair))
|
||||
directed_paare.add(pair)
|
||||
|
||||
# Jedes TRO, fuer das oben ein praeziser Nachfolger in der Umlaufkette
|
||||
# gefunden wurde (precise_sources), hat damit seine gesamte reale
|
||||
# Weiterfuehrung bereits vollstaendig beschrieben (Bahn-Fortsetzung und/
|
||||
# oder Weiche). Jede WEITERE von ihm ausgehende Kante stammt aus dem
|
||||
# groben Contraction-Walk ganz oben (ueber denselben Host wie ein anderes
|
||||
# TRO - z. B. TRO01 -> TRO12 zusaetzlich zu TRO01 -> TRO02, oder TRO02 ->
|
||||
# TRO12 zusaetzlich zu TRO02 -> TRO03) und ist redundant bzw. schlicht
|
||||
# falsch: das jeweilige Ziel-TRO berichtet dieselbe Weiterfuehrung bereits
|
||||
# selbst, praeziser. Review 2026-09-21: erst "jede Weiche hat 3 statt 2
|
||||
# ausgehende Kanten", dann "warum zeigen alle Varios auf TRO12" - beides
|
||||
# derselbe Fehler, hier fuer jedes TRO vereinheitlicht statt nur fuer
|
||||
# Weichen. directed_paare (NICHT das ungerichtete verkettete_paare!) -
|
||||
# sonst deckt eine TRO12->TRO01-Umlaufkante faelschlich auch die
|
||||
# Gegenrichtung TRO01->TRO12 ab, nur weil dasselbe TRO-Paar beteiligt ist.
|
||||
edges = {e for e in edges
|
||||
if not (e[0] in precise_sources and e not in directed_paare)}
|
||||
|
||||
# gleicher Host = physisch dieselbe Strecke/Bahn. Ein bereits verkettetes
|
||||
# PAAR (nicht: irgendein verketteter TRO - ein Weichen-TRO kann zugleich
|
||||
# auf der Bahn verkettet UND mit seinem Vario-TRO auf derselben Strecke
|
||||
# colocated sein, das sind zwei verschiedene Beziehungen) braucht die
|
||||
# ungerichtete Notiz nicht zusaetzlich.
|
||||
# PAAR (Bahn-Reihenfolge oben oder Weiche gerade eben) braucht die
|
||||
# ungerichtete "gleiche Bahn"-Notiz nicht zusaetzlich - ein Weichen-TRO
|
||||
# kann durchaus auf der Bahn verkettet UND mit seinem Vario-TRO ueber die
|
||||
# Weiche verbunden sein, das sind zwei unabhaengige Beziehungen.
|
||||
for host, ids in owners.items():
|
||||
for tro_id in ids:
|
||||
tro = next(t for t in tros if t.tro_id == tro_id)
|
||||
|
||||
Reference in New Issue
Block a user