tro_flow: Weichen aus der Umlaufreihenfolge, 1Sep2Swi, keine "gleiche Bahn"-Notiz an Weichen-TROs

- _weichen_aus_umlauf: ein Separator, auf den in Laufrichtung ein Ausschleuselement folgt (ohne Separator dazwischen), ist dessen Weiche - auch ohne CSV-Nachbarschaft (Dematic 0043 -> VF_5, Nachbarn nur Einschleuselemente); Speicherlinien ausgenommen
- Schritt 3b': weitere Ausschleuselemente bis zum naechsten Separator gehoeren zum selben Separator, zwei -> 1Sep2Swi (Dematic 0048 -> VF_4 + VF_6), Finding bei fehlendem Scanner bzw. mehr als zwei Weichen
- Weichenkante je Host; Weichen-TRO ohne Bahn-Nachfolger (einziger Separator seines Kreisels) zaehlt als praezise Quelle - entfernt die Contraction-Walk-Kanten an VF_4/VF_5/VF_6 vorbei
- keine gestrichelte "gleiche Bahn"-Notiz an Weichen-TROs (Host ist die Strecke, in die sie abzweigen, nicht ihre Bahn)
- HundM05 (28 Kanten) und Mubea (26) inhaltlich unveraendert, --use-cords-Ausgabe byte-identisch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-08 11:35:59 +02:00
co-authored by Claude Opus 5.5
parent e91b754d90
commit 90cafec99c
+137 -12
View File
@@ -362,6 +362,56 @@ def _weichen_nach_umlauf(
return result
def _weichen_aus_umlauf(
switch_hits: dict[str, tuple[Element, float]],
kandidaten: list[Element],
elements: list[Element],
findings: list[str],
) -> dict[str, tuple[Element, float]]:
"""Weichen-Separatoren ergaenzen, die die CSV-Nachbarn nicht zeigen.
Ein Separator, auf den in Laufrichtung ein Ausschleuselement folgt, ohne
Separator dazwischen, steuert diese Weiche - auch wenn das Element nicht
in seinen "Nachbarn" steht (dort stehen nur die unmittelbaren Ring-
Nachbarn, oft ein Einschleuselement). Dematic/Arvato Kreisel1 (Review
2026-10-08): 0043 -> Ausschleuselement 0071 (VF_5), Nachbarn von 0043
sind nur die Einschleuselemente von VF_4/VF_6. Weitere Elemente bis zum
naechsten Separator ergaenzt derive_tros Schritt 3b' (1Sep2Swi).
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).
"""
by_id = {e.teile_id: e for e in elements}
erlaubt = {s.teile_id for s in kandidaten}
belegt = {t.teile_id for t, _d in switch_hits.values()}
result = dict(switch_hits)
for kreisel in (e for e in elements if e.kind == "Kreisel" and e.track_ids):
ids = list(kreisel.track_ids)
ring = kreisel.rotation in (ROTATION_CW, ROTATION_CCW)
for i, as_id in enumerate(ids):
transfer = by_id.get(as_id)
if transfer is None or transfer.kind != "Ausschleus" or as_id in belegt:
continue
strecke = by_id.get(str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or ""))
if strecke is not None and strecke.kind == "Gefaellestrecke":
continue
davor = ids[:i][::-1] + (ids[i + 1:][::-1] if ring else [])
sep_id = next((t for t in davor
if (el := by_id.get(t)) and el.kind == "Separator"), None)
if sep_id is None or sep_id in result or sep_id not in erlaubt:
continue
sep = by_id[sep_id]
d = (math.hypot(transfer.x - sep.x, transfer.y - sep.y)
if None not in (sep.x, sep.y, transfer.x, transfer.y) else 0.0)
result[sep_id] = (transfer, d)
belegt.add(as_id)
findings.append(
f"Separator {sep_id}: steuert Ausschleuselement {as_id} (folgt in "
f"Laufrichtung ohne Separator dazwischen, kein CSV-Nachbar) - Weiche")
return result
def _speicher_bloecke(
lanes: list[str],
elements: list[Element],
@@ -730,8 +780,10 @@ def derive_tros(
# unten (3b) ihre eigene 1Sep1Swi-Uebergabestelle und duerfen nicht zugleich
# als gewoehnlicher Separator ihres Zuordnung-Hosts (Kreisel-Bahn oder
# Strecke - beides kommt vor) mitgezaehlt werden.
switch_hits = _weichen_nach_umlauf(
_find_switch_separators(switch_candidates, elements), elements, findings)
switch_hits = _weichen_aus_umlauf(
_weichen_nach_umlauf(
_find_switch_separators(switch_candidates, elements), elements, findings),
switch_candidates, elements, findings)
# Separatoren an einer echten ILS Weiche (siehe _find_weiche_separators) -
# wie switch_hits vor der Host-Aufloesung herausgenommen, Schritt 3c.
weiche_hits = _find_weiche_separators(
@@ -1054,6 +1106,58 @@ def derive_tros(
)
tros.append(tro)
# 3b') Weitere Weichen desselben Separators. Ein Separator steuert ALLE
# Ausschleuselemente, die in Laufrichtung bis zum naechsten Separator auf
# ihn folgen - nicht nur das eine, das in seinen CSV-Nachbarn steht
# (_find_switch_separators). Dematic/Arvato Kreisel4 (GUZ): 0048 ->
# Ausschleuselement 0069 (VF_4) -> Ausschleuselement 0077 (VF_6); 0077 ist
# kein CSV-Nachbar von 0048, wird aber von ihm gesteuert (Review
# 2026-10-07). Mit zwei Weichen wird der TRO zum 1Sep2Swi (Katalog: 1x
# Separator, 2x Switch, 1x Scanner). Ausschleuselemente von Speicherlinien
# gehoeren dem PinStore_Auto (Schritt 1) und zaehlen hier nicht.
speicher_hosts = {h for t in tros if t.type_name == "PinStore_Auto" for h in t.hosts}
sep_switch_tro = {t.separators[0]: t for t in as_switch_tro.values() if t.separators}
weichen_je_tro: dict[int, list[str]] = {
id(t): [a for a, tt in as_switch_tro.items() if tt is t] for t in as_switch_tro.values()}
for kreisel in (e for e in elements if e.kind == "Kreisel" and e.track_ids):
ids = list(kreisel.track_ids)
ring = kreisel.rotation in (ROTATION_CW, ROTATION_CCW)
for i, as_id in enumerate(ids):
transfer = elements_by_id.get(as_id)
if transfer is None or transfer.kind != "Ausschleus" or as_id in as_switch_tro:
continue
strecke_id = str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or "")
if strecke_id in speicher_hosts:
continue
davor = ids[:i][::-1] + (ids[i + 1:][::-1] if ring else [])
steuer = next((t for t in davor if (el := elements_by_id.get(t))
and el.kind == "Separator"), None)
tro = sep_switch_tro.get(steuer or "")
if tro is None:
continue
as_switch_tro[as_id] = tro
weichen = weichen_je_tro[id(tro)]
weichen.append(as_id)
if strecke_id in graph.nodes and strecke_id not in tro.hosts:
tro.hosts.append(strecke_id)
tro.label = (f"Separator {steuer} an Ausschleuselementen "
f"{', '.join(weichen)} (Strecken {', '.join(tro.hosts)})")
tro.reason += (f"; steuert auch Ausschleuselement {as_id} - folgt in "
f"Laufrichtung ohne Separator dazwischen")
if len(weichen) == 2:
tro.type_name = "1Sep2Swi"
else:
findings.append(
f"Separator {steuer}: {len(weichen)} Ausschleuselemente "
f"({', '.join(weichen)}) ohne Separator dazwischen - der "
f"Katalog kennt hoechstens 1Sep2Swi, bitte pruefen")
for tro in sep_switch_tro.values():
if tro.type_name == "1Sep2Swi" and not tro.scanners:
findings.append(
f"{tro.label}: 1Sep2Swi ohne Scanner - der Katalog verlangt einen "
f"Barcode-Scanner am Eingang, im Layout fehlt einer vor Separator "
f"{tro.separators[0]}")
# 3c) Separatoren an einer echten ILS Weiche -> 1Sep1Swi (siehe
# _find_weiche_separators). Host wie bei 3b: der aufgeloeste Zuordnung-Host
# des Separators, sonst die Weiche selbst.
@@ -1625,18 +1729,31 @@ def link_tros(
# 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.
# Je Host eine Weichenkante: ein 1Sep2Swi hat zwei Strecken (Schritt 3b'
# in derive_tros). Hat ein Weichen-TRO keinen Nachfolger auf der Bahn
# (einziger Separator seines Kreisels - Dematic Kreisel1/Kreisel4: ohne
# Abzweig laeuft das Material auf dem Kreisel zu ihm selbst zurueck),
# beschreiben die Weichenkanten seine Weiterfuehrung trotzdem vollstaendig;
# er zaehlt dann wie ein Bahn-TRO als praezise Quelle, die groben
# Contraction-Walk-Kanten entfallen (sie fuehrten an VF_5 vorbei direkt
# auf Kreisel2, Review 2026-10-08).
for tro in tros:
definition = tro.definition()
if not (definition and definition.has_item(TroItem.SWITCH)) or len(tro.hosts) != 1:
if not (definition and definition.has_item(TroItem.SWITCH)):
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)
weichen_kanten = 0
for host in tro.hosts:
ziel_id = strecke_first_owned.get(host)
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)
weichen_kanten += 1
if tro.hosts and weichen_kanten == len(tro.hosts):
precise_sources.add(tro.tro_id)
# Jedes TRO, fuer das oben ein praeziser Nachfolger in der Umlaufkette
# gefunden wurde (precise_sources), hat damit seine gesamte reale
@@ -1660,11 +1777,19 @@ def link_tros(
# 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.
# Weichen-TROs bekommen keine "gleiche Bahn"-Notiz: ihr Host ist die
# Strecke, IN die sie abzweigen - sie selbst sitzen auf der Kreisel-Bahn,
# nicht auf dieser Strecke. Die Notiz verband sonst einen 1Sep2Swi mit
# jedem TRO beider Strecken (Dematic TRO13 mit VF_4 und VF_6, Review
# 2026-10-08); die Verbindung zeigt bereits die Weichenkante.
weichen_tros = {t.tro_id for t in tros
if (d := t.definition()) and d.has_item(TroItem.SWITCH)}
for host, ids in owners.items():
for tro_id in ids:
tro = next(t for t in tros if t.tro_id == tro_id)
others = {o for o in ids if o != tro_id
and frozenset((tro_id, o)) not in verkettete_paare}
and frozenset((tro_id, o)) not in verkettete_paare
and not {tro_id, o} & weichen_tros}
if others:
tro.colocated = sorted(set(tro.colocated) | others)