From 058c60e23d5f2c2a8ae52a2ef21c8a2702aaafe4 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Fri, 25 Sep 2026 15:06:25 +0200 Subject: [PATCH] TRO-Fluss: Weichenerkennung ueber CSV-Nachbarn statt Distanzschwelle _find_switch_separators() ermittelt Weichen-Separatoren jetzt topologisch (Ausschleuselement unter den CSV-Nachbarn, gleiche Kreisel-Bahn) statt ueber eine Distanzschwelle - die Fallback-Schwelle paarte an Mubea unbeteiligte Separatoren faelschlich mit dem naechsten freien Ausschleuselement, u.a. 20 PinStore_Auto-Speicherlinien wurden so faelschlich als 1Sep1Swi erkannt. Materialfluss: _sole_kreisel_neighbour() als Fallback in _apply_transfers() fuer AS/ES-Elemente ohne eigenes Merkmal "Kreisel" (Mubea), aufloesbar wenn die Strecke genau einen Kreisel-Nachbarn hat. Co-Authored-By: Claude Sonnet 5 --- lib/material_flow.py | 30 +++++++++ lib/tro_flow.py | 147 +++++++++++++++++-------------------------- 2 files changed, 88 insertions(+), 89 deletions(-) diff --git a/lib/material_flow.py b/lib/material_flow.py index 77f3189..618a247 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -1152,6 +1152,30 @@ def _attach_stations( return edges +def _sole_kreisel_neighbour(strecke_id: str, by_id: dict[str, Element]) -> str: + """Kreisel-Basis-Id, wenn die Strecke genau einen Kreisel als Nachbarn hat, sonst ''. + + Fallback fuer _apply_transfers, wenn ein Ausschleus-/Einschleuselement + selbst kein Merkmal "Kreisel" traegt (z. B. Mubea: die dortigen AS/ES- + Bloecke haben dieses Merkmal grundsaetzlich nicht, siehe Review + 2026-09-25). Bewusst nur bei GENAU einem Kreisel-Nachbarn: bei zwei + verschiedenen Kreiseln (der Normalfall) waere sonst nicht entscheidbar, + welcher zu "aus" (Ausschleusung) und welcher zu "ein" (Einschleusung) + gehoert - dort bleibt es beim bisherigen Hoehenvergleich in _orient(), der + dort zuverlaessig funktioniert (unterschiedliche Kreisel, unterschiedliche + Hoehe). Nur bei einer Ruecklauf-Strecke (beide Enden am selben Kreisel, + siehe unten) ist der einzige Nachbar ohnehin fuer beide Rollen richtig. + """ + strecke = by_id.get(strecke_id) + if strecke is None: + return "" + bases = { + base for base, _lane in strecke.neighbours + if (partner := by_id.get(base)) is not None and partner.is_circle + } + return next(iter(bases)) if len(bases) == 1 else "" + + def _kreisel_lane_node(kreisel_base: str, strecke: Element, nodes: dict[str, Node]) -> str | None: """Die konkrete Kreisel-Bahn (…-L/…-R), an der die Strecke haengt.""" for base, lane in strecke.neighbours: @@ -1217,6 +1241,12 @@ def _apply_transfers( # "Strecke" (VF) oder "Foerderer"/"Förderer" (GF/Streckengruppe). s = str(_merkmal(el.merkmale, "Strecke", "Foerderer", "Förderer") or "").strip() + if not k and s: + # AS/ES ohne eigenes Merkmal "Kreisel" (z. B. Mubea) - nur bei + # einem einzigen Kreisel-Nachbarn der Strecke aufloesbar, siehe + # _sole_kreisel_neighbour. + k = _sole_kreisel_neighbour(s, by_id) + if k and s: role = "aus" if el.kind == "Ausschleus" else "ein" per_strecke.setdefault(s, {})[role] = k diff --git a/lib/tro_flow.py b/lib/tro_flow.py index 617e2e1..a923292 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -153,24 +153,6 @@ from tro_overrides import ( # einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen. MIN_STORE_LANES = 2 -# Toleranz (mm), innerhalb derer ein Separator als "sitzt an einem -# Ausschleuselement" gilt (siehe _find_switch_separators, Regel 1). Kalibriert -# an hundmM05_1.csv: die tatsaechlich am Uebergang montierten Separatoren -# liegen 250-350mm vom Element entfernt, die naechsten unbeteiligten (auf der -# eigenen Streckenmitte montierten) Separatoren erst ab ca. 885mm - die -# Grenze liegt bewusst in der Mitte dieser Luecke. -SWITCH_PROXIMITY_MM = 500.0 - -# Obergrenze (mm) fuer Regel 2 in _find_switch_separators (naechster freier -# Separator ohne Treffer aus Regel 1). Grosszuegiger als SWITCH_PROXIMITY_MM, -# aber bewusst NICHT unbegrenzt: an HundM05 (Review 2026-09-18) lagen die noch -# echten Weichen bis 1139mm vom Ausschleuselement entfernt - das deckt dieser -# Wert. Ohne diese Grenze hat sich Regel 2 an einer groesseren Anlage (Mubea, -# 500573) als Fehlerquelle erwiesen: uebrig gebliebene, tatsaechlich -# unbeteiligte Separatoren wurden dem naechsten noch freien Ausschleuselement -# 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 @@ -330,7 +312,7 @@ def _default_resolve_host(sep: Element, findings: list[str], graph: Graph) -> st def _find_switch_separators( - separators: list[Element], elements: list[Element], tol_mm: float + separators: list[Element], elements: list[Element] ) -> dict[str, tuple[Element, float]]: """Separator-Id -> (Ausschleuselement der Weiche, Abstand mm). @@ -345,80 +327,67 @@ def _find_switch_separators( zusaetzliches Bauteil des TROs (siehe derive_tros/scanners_for) - viele Separatoren tragen einen Scanner, ohne eine Weiche zu sein. - Zwei Durchlaeufe, um trotzdem jedem Ausschleuselement genau einen - Separator zuzuordnen: + Frueher ueber eine reine Distanzschwelle ermittelt (<=500mm, mit einer + grosszuegigeren Fallback-Schwelle bis 1500mm fuer weiter entfernte + Faelle). Das erwies sich als nicht plausibel genug in beide Richtungen + (Review 2026-09-25): die Fallback-Schwelle paarte an Mubea wiederholt + voellig unbeteiligte Separatoren mit dem naechstgelegenen noch freien + Ausschleuselement, ohne echten Bezug zwischen beiden - vor Ort geprueft, + keine Weiche an der Stelle. Eine reine Nahdistanz ohne Fallback haette + umgekehrt an HundM05 alle 5 echten Weichen verfehlt, die dort + gleichmaessig bei 1013-1016mm liegen (Review 2026-09-18) - Distanz allein + ist also weder an Mubea noch an HundM05 ein verlaessliches Kriterium. - 1. Geometrische Naehe (Distanz <= tol_mm). HundM05 hat an seinen - Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil - (anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von - dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am - Streckenanfang) uebernimmt dort faktisch die Rolle der Weiche. Sitzt - ein Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig - davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf - 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) - 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. + Stattdessen wird die im CSV-Export bereits vorhandene topologische + Nachbarschaft verwendet (Spalte "Nachbarn", Element.neighbours): dxfmakros + traegt dort den tatsaechlich anliegenden Nachbarn ein (CAD-Topologie), + nicht nur den geometrisch naechsten - an HundM05 steht das echte + Ausschleuselement trotz 1013-1016mm Abstand direkt in den Nachbarn des + Schalt-Separators, an Mubea NICHT in den Nachbarn der zu Unrecht per + Fallback getroffenen Separatoren. Ein Separator ist eine Weiche, wenn: - 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. + 1. ein Ausschleuselement unter seinen Nachbarn steht, UND + 2. dieses Ausschleuselement zur selben Kreisel-Bahn gehoert wie der + Separator (gleiche "Zuordnung", z. B. beide auf Kreisel "0002" - + Element.track_ids des betreffenden Kreisels enthaelt das + Ausschleuselement). Das ist nicht nur eine Absicherung gegen einen + ungenau exportierten "Nachbarn"-Eintrag, sondern noetig: ein + Ausschleuselement markiert nicht nur den Kreisel->Strecke-Uebergang, + sondern an einem PinStore_Auto-Speicherblock auch den Ablauf jeder + einzelnen Schwerkraftlinie zum gemeinsamen Sammler - jeder + Linien-Separator hat dort ein Ausschleuselement als CSV-Nachbarn, ohne + damit eine echte Weiche zu sein (Review 2026-09-25, Mubea: 20 + Speicherlinien faelschlich als 1Sep1Swi einsortiert, weil ihre + Kreisel-Zuordnung fehlt und Kriterium 2 dafuer faelschlich nicht griff). + Ist die Kreisel-Zuordnung des Separators nicht aufloesbar (er sitzt gar + nicht auf einer Kreisel-Bahn), ist er deshalb KEIN Weichenkandidat - + Kriterium 1 allein reicht nicht. + + Die Distanz im Rueckgabewert ist nur noch Beiwerk fuer die Doku ("XXXmm + von Ausschleuselement YYYY entfernt"), kein Kriterium mehr. """ - transfers = [e for e in elements if e.kind == "Ausschleus"] + 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"} result: dict[str, tuple[Element, float]] = {} for sep in separators: - if sep.x is None or sep.y is None: + sep_kreisel_id = sep.assignment.split("-", 1)[0] if sep.assignment else "" + kreisel = kreisel_by_id.get(sep_kreisel_id) + if kreisel is None: continue - best: tuple[float, Element] | None = None - for transfer in transfers: - if transfer.x is None or transfer.y is None: + for base, _lane in sep.neighbours: + transfer = transfers.get(base) + if transfer is None: continue - d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) - if d <= tol_mm and (best is None or d < best[0]): - best = (d, transfer) - if best is not None: - result[sep.teile_id] = (best[1], best[0]) - - 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] - - candidates: list[tuple[float, Element, Element]] = [] - for sep in remaining_seps: - for transfer in transfers: - if transfer.teile_id in used_transfers or transfer.x is None or transfer.y is None: + if transfer.teile_id not in kreisel.track_ids: continue - d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) - if d <= SWITCH_FALLBACK_MM: - candidates.append((d, sep, transfer)) - candidates.sort(key=lambda c: c[0]) - - claimed_seps: set[str] = set() - for d, sep, transfer in candidates: - if sep.teile_id in claimed_seps or transfer.teile_id in used_transfers: - continue - result[sep.teile_id] = (transfer, d) - claimed_seps.add(sep.teile_id) - used_transfers.add(transfer.teile_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.teile_id] = (transfer, d) + break return result @@ -572,7 +541,7 @@ 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 = _find_switch_separators(switch_candidates, elements, SWITCH_PROXIMITY_MM) + switch_hits = _find_switch_separators(switch_candidates, elements) # Separator -> Host-Knoten sep_host: dict[str, str] = {} @@ -1933,10 +1902,10 @@ def render_doc( add("| Gefaellestrecke, einzelne Linie | `1Sep` |") add("| Strecke (angetrieben) | `Vario` (je Streckensegment) |") add("| Kreisel-Bahn | `1Sep` |") - add(f"| Separator <= {SWITCH_PROXIMITY_MM:.0f}mm (oder naechstgelegen bis " - f"{SWITCH_FALLBACK_MM:.0f}mm) von einem Ausschleuselement " - "| `1Sep1Swi` (Element uebernimmt die fehlende Weiche; ein Einschleuselement " - "zaehlt nicht - passiver Merge ohne Richtungsentscheidung) |") + add("| Separator mit Ausschleuselement unter seinen CSV-Nachbarn (gleiche " + "Kreisel-Bahn) | `1Sep1Swi` (Element uebernimmt die fehlende Weiche; " + "ein Einschleuselement zaehlt nicht - passiver Merge ohne " + "Richtungsentscheidung) |") add("| zusaetzlich: 2 bzw. 3 abgehende Wege | Aufwertung zu `1Sep1Swi` / `1Sep2Swi` |") add("") add("Ein **Scanner** am Separator aendert den Typ nicht. In der Referenzanlage "