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
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
+58 -89
View File
@@ -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 "