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 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-25 15:06:25 +02:00
co-authored by Claude Sonnet 5
parent e4bfad4dcc
commit 058c60e23d
2 changed files with 88 additions and 89 deletions
+30
View File
@@ -1152,6 +1152,30 @@ def _attach_stations(
return edges 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: 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.""" """Die konkrete Kreisel-Bahn (…-L/…-R), an der die Strecke haengt."""
for base, lane in strecke.neighbours: for base, lane in strecke.neighbours:
@@ -1217,6 +1241,12 @@ def _apply_transfers(
# "Strecke" (VF) oder "Foerderer"/"Förderer" (GF/Streckengruppe). # "Strecke" (VF) oder "Foerderer"/"Förderer" (GF/Streckengruppe).
s = str(_merkmal(el.merkmale, "Strecke", "Foerderer", "Förderer") or "").strip() 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: if k and s:
role = "aus" if el.kind == "Ausschleus" else "ein" role = "aus" if el.kind == "Ausschleus" else "ein"
per_strecke.setdefault(s, {})[role] = k per_strecke.setdefault(s, {})[role] = k
+58 -89
View File
@@ -153,24 +153,6 @@ from tro_overrides import (
# einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen. # einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen.
MIN_STORE_LANES = 2 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- # Mindest-Hoehenverlust (mm) auf dem der Reihe nach vorangehenden TrackIds-
# Segment, damit ein Separator auf einer "Strecke" trotzdem als 1Sep statt # Segment, damit ein Separator auf einer "Strecke" trotzdem als 1Sep statt
# Vario gilt (siehe _incoming_drop). Muss nur "horizontal" gefuehrte, aber # 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( def _find_switch_separators(
separators: list[Element], elements: list[Element], tol_mm: float separators: list[Element], elements: list[Element]
) -> dict[str, tuple[Element, float]]: ) -> dict[str, tuple[Element, float]]:
"""Separator-Id -> (Ausschleuselement der Weiche, Abstand mm). """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 zusaetzliches Bauteil des TROs (siehe derive_tros/scanners_for) - viele
Separatoren tragen einen Scanner, ohne eine Weiche zu sein. Separatoren tragen einen Scanner, ohne eine Weiche zu sein.
Zwei Durchlaeufe, um trotzdem jedem Ausschleuselement genau einen Frueher ueber eine reine Distanzschwelle ermittelt (<=500mm, mit einer
Separator zuzuordnen: 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 Stattdessen wird die im CSV-Export bereits vorhandene topologische
Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil Nachbarschaft verwendet (Spalte "Nachbarn", Element.neighbours): dxfmakros
(anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von traegt dort den tatsaechlich anliegenden Nachbarn ein (CAD-Topologie),
dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am nicht nur den geometrisch naechsten - an HundM05 steht das echte
Streckenanfang) uebernimmt dort faktisch die Rolle der Weiche. Sitzt Ausschleuselement trotz 1013-1016mm Abstand direkt in den Nachbarn des
ein Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig Schalt-Separators, an Mubea NICHT in den Nachbarn der zu Unrecht per
davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf Fallback getroffenen Separatoren. Ein Separator ist eine Weiche, wenn:
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.
Frueher lief Regel 2 nur auf einer Bahn, die Regel 1 bereits bestaetigt 1. ein Ausschleuselement unter seinen Nachbarn steht, UND
hatte (eine Bahn ohne jeden nahen Treffer hat vermutlich gar keine 2. dieses Ausschleuselement zur selben Kreisel-Bahn gehoert wie der
Weiche). Diese Bahn-Sperre wurde entfernt (Review 2026-09-23): bei Separator (gleiche "Zuordnung", z. B. beide auf Kreisel "0002" -
einer neueren HundM05-Revision lagen ALLE 5 echten Schalt-Separatoren Element.track_ids des betreffenden Kreisels enthaelt das
gleichmaessig bei ca. 1013-1016mm - ausserhalb von Regel 1 (500mm), aber Ausschleuselement). Das ist nicht nur eine Absicherung gegen einen
innerhalb von SWITCH_FALLBACK_MM. Ohne einen einzigen Regel-1-Treffer ungenau exportierten "Nachbarn"-Eintrag, sondern noetig: ein
blieb die Bahn-Sperre fuer jede Bahn zu, Regel 2 kam nie zum Zug, und Ausschleuselement markiert nicht nur den Kreisel->Strecke-Uebergang,
keine der 5 Weichen wurde erkannt. SWITCH_FALLBACK_MM bleibt die sondern an einem PinStore_Auto-Speicherblock auch den Ablauf jeder
einzige Schranke gegen Fehlzuordnungen (siehe Mubea-Fall oben) - sollte einzelnen Schwerkraftlinie zum gemeinsamen Sammler - jeder
sich das an einer weiteren Anlage als zu grosszuegig erweisen, braucht Linien-Separator hat dort ein Ausschleuselement als CSV-Nachbarn, ohne
es dort eine gezieltere Loesung statt der Bahn-Sperre. 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]] = {} result: dict[str, tuple[Element, float]] = {}
for sep in separators: 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 continue
best: tuple[float, Element] | None = None for base, _lane in sep.neighbours:
for transfer in transfers: transfer = transfers.get(base)
if transfer.x is None or transfer.y is None: if transfer is None:
continue continue
d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) if transfer.teile_id not in kreisel.track_ids:
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:
continue continue
d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) d = (
if d <= SWITCH_FALLBACK_MM: math.hypot(transfer.x - sep.x, transfer.y - sep.y)
candidates.append((d, sep, transfer)) if None not in (sep.x, sep.y, transfer.x, transfer.y)
candidates.sort(key=lambda c: c[0]) else 0.0
)
claimed_seps: set[str] = set() result[sep.teile_id] = (transfer, d)
for d, sep, transfer in candidates: break
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)
return result return result
@@ -572,7 +541,7 @@ def derive_tros(
# unten (3b) ihre eigene 1Sep1Swi-Uebergabestelle und duerfen nicht zugleich # unten (3b) ihre eigene 1Sep1Swi-Uebergabestelle und duerfen nicht zugleich
# als gewoehnlicher Separator ihres Zuordnung-Hosts (Kreisel-Bahn oder # als gewoehnlicher Separator ihres Zuordnung-Hosts (Kreisel-Bahn oder
# Strecke - beides kommt vor) mitgezaehlt werden. # 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 # Separator -> Host-Knoten
sep_host: dict[str, str] = {} sep_host: dict[str, str] = {}
@@ -1933,10 +1902,10 @@ def render_doc(
add("| Gefaellestrecke, einzelne Linie | `1Sep` |") add("| Gefaellestrecke, einzelne Linie | `1Sep` |")
add("| Strecke (angetrieben) | `Vario` (je Streckensegment) |") add("| Strecke (angetrieben) | `Vario` (je Streckensegment) |")
add("| Kreisel-Bahn | `1Sep` |") add("| Kreisel-Bahn | `1Sep` |")
add(f"| Separator <= {SWITCH_PROXIMITY_MM:.0f}mm (oder naechstgelegen bis " add("| Separator mit Ausschleuselement unter seinen CSV-Nachbarn (gleiche "
f"{SWITCH_FALLBACK_MM:.0f}mm) von einem Ausschleuselement " "Kreisel-Bahn) | `1Sep1Swi` (Element uebernimmt die fehlende Weiche; "
"| `1Sep1Swi` (Element uebernimmt die fehlende Weiche; ein Einschleuselement " "ein Einschleuselement zaehlt nicht - passiver Merge ohne "
"zaehlt nicht - passiver Merge ohne Richtungsentscheidung) |") "Richtungsentscheidung) |")
add("| zusaetzlich: 2 bzw. 3 abgehende Wege | Aufwertung zu `1Sep1Swi` / `1Sep2Swi` |") add("| zusaetzlich: 2 bzw. 3 abgehende Wege | Aufwertung zu `1Sep1Swi` / `1Sep2Swi` |")
add("") add("")
add("Ein **Scanner** am Separator aendert den Typ nicht. In der Referenzanlage " add("Ein **Scanner** am Separator aendert den Typ nicht. In der Referenzanlage "