diff --git a/lib/tro_flow.py b/lib/tro_flow.py index f2557c5..c29b02b 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -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)