Weichenerkennung: nur Ausschleuselement zaehlt, Regel 2 mit Obergrenze

Zwei Korrekturen zu c788647, beide durch Review gefunden (HundM05):

1. Ein Einschleuselement (Wiedereinschleusen in den Kreisel) ist keine
   Weiche - das ist ein passiver Merge ohne Richtungsentscheidung, nur das
   Ausschleusen (Verlassen der Bahn) legt die Fahrtrichtung fest.
   _find_switch_separators() beruecksichtigt jetzt ausschliesslich
   Ausschleuselemente; ein Separator an einem Einschleuselement (z. B.
   HundM05 Separator 0018) wird nicht mehr als eigener 1Sep1Swi
   herausgeloest, sondern zaehlt normal zum Vario-TRO seines Streckensegments.
   HundM05.csv: 6 -> 5 automatisch erkannte Weichen (deckt sich jetzt mit den
   5 Ausschleuselementen im Layout).

2. Beim Pruefen an Mubea (500573) fiel auf, dass Regel 2 (naechster freier
   Separator, bisher ohne Obergrenze) dort mehrere Separatoren dem naechsten
   noch freien Ausschleus-/Einschleuselement zugeordnet hatte, obwohl das
   3.5-9m entfernt lag - eindeutig keine Weiche. Neue Konstante
   SWITCH_FALLBACK_MM (1500mm) begrenzt Regel 2: grosszuegig genug fuer die
   bei HundM05 beobachtete Kalibrierungs-Drift (bis 1139mm), zu eng fuer
   Mubeas Fehlzuordnungen. mubea.csv: 9 -> 4 automatisch erkannte Weichen
   (die beiden bestaetigten Handkorrekturen in tro_overrides.ini bleiben
   unveraendert).

Beide Projekte weiterhin ohne Fehler, keine nicht verbundenen TROs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 10:46:30 +02:00
parent c7886477ff
commit fa96a6e666
+57 -46
View File
@@ -113,14 +113,24 @@ 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 Aus-/ # Toleranz (mm), innerhalb derer ein Separator als "sitzt an einem
# Einschleuselement" gilt (siehe _find_switch_separators). Kalibriert an # Ausschleuselement" gilt (siehe _find_switch_separators, Regel 1). Kalibriert
# hundmM05_1.csv: die tatsaechlich am Uebergang montierten Separatoren liegen # an hundmM05_1.csv: die tatsaechlich am Uebergang montierten Separatoren
# 250-350mm vom Element entfernt, die naechsten unbeteiligten (auf der # liegen 250-350mm vom Element entfernt, die naechsten unbeteiligten (auf der
# eigenen Streckenmitte montierten) Separatoren erst ab ca. 885mm - die # eigenen Streckenmitte montierten) Separatoren erst ab ca. 885mm - die
# Grenze liegt bewusst in der Mitte dieser Luecke. # Grenze liegt bewusst in der Mitte dieser Luecke.
SWITCH_PROXIMITY_MM = 500.0 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
GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->" GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
# Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in # Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in
@@ -273,43 +283,46 @@ 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], tol_mm: float
) -> dict[str, tuple[Element, float]]: ) -> dict[str, tuple[Element, float]]:
"""Separator-Id -> (Aus-/Einschleuselement der Weiche, Abstand mm). """Separator-Id -> (Ausschleuselement der Weiche, Abstand mm).
Ein Separator ist eine Weiche, wenn ihm ein ILS Aus- oder Einschleuselement Ein Separator ist eine Weiche, wenn ihm ein ILS Ausschleuselement
zugeordnet werden kann - das Element ist die Weiche (siehe unten), der zugeordnet werden kann - das Element ist die Weiche (siehe unten), der
Separator nur der Steuerpunkt davor. Ein eventueller Scanner am selben Separator nur der Steuerpunkt davor. NICHT das Einschleuselement: das
Separator ist dabei kein Kriterium, sondern nur ein zusaetzliches Bauteil Wiedereinschleusen ist ein passiver Merge ohne Richtungsentscheidung, nur
des TROs (siehe derive_tros/scanners_for) - viele Separatoren tragen einen das Ausschleusen (Verlassen der Kreisel-Bahn) ist eine Weichenstelle - vor
Scanner, ohne eine Weiche zu sein. Ort bestaetigt (HundM05, Review 2026-09-21): ein Separator an einem
Einschleuselement ist keine eigene Uebergabestelle. Ein eventueller
Scanner am selben Separator ist ebenfalls kein Kriterium, sondern nur ein
zusaetzliches Bauteil des TROs (siehe derive_tros/scanners_for) - viele
Separatoren tragen einen Scanner, ohne eine Weiche zu sein.
Zwei Durchlaeufe, um trotzdem jedem Aus-/Einschleuselement genau einen Zwei Durchlaeufe, um trotzdem jedem Ausschleuselement genau einen
Separator zuzuordnen: Separator zuzuordnen:
1. Geometrische Naehe (Distanz <= tol_mm). HundM05 hat an seinen 1. Geometrische Naehe (Distanz <= tol_mm). HundM05 hat an seinen
Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil
(anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von (anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von
dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am
Streckenanfang) bzw. Einschleuselement (Strecke -> Kreisel, am Streckenanfang) uebernimmt dort faktisch die Rolle der Weiche. Sitzt
Streckenende) uebernimmt dort faktisch die Rolle der Weiche. Sitzt ein ein Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig
Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig
davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf
die Strecke zeigt (in den Daten kommt beides vor, siehe derive_tros). die Strecke zeigt (in den Daten kommt beides vor, siehe derive_tros).
2. Naechstgelegener freier Separator je noch unbelegtem Aus-/ 2. Naechstgelegener freier Separator je noch unbelegtem Ausschleuselement,
Einschleuselement, jedes Paar nur einmal vergeben (kleinste Distanz jedes Paar nur einmal vergeben (kleinste Distanz zuerst, hoechstens
zuerst), auf derselben Bahn und mit derselben Rolle (Aus- oder SWITCH_FALLBACK_MM), und nur auf einer Bahn, die bereits durch Regel 1
Einschleusung) wie ein bereits durch Regel 1 bestaetigter Treffer - einen bestaetigten Treffer hat (eine Bahn ohne jeden nahen Treffer hat
verhindert, dass ein Separator versehentlich mit dem naeher liegenden vermutlich gar keine Weiche, z. B. Kreisel1 Bahn R bei HundM05). Grund
Einschleuselement einer anderen Strecke verkoppelt wird. Grund fuer den fuer den zweiten Durchlauf (HundM05, Review 2026-09-18): an einem
zweiten Durchlauf (HundM05, Review 2026-09-18): an einem Kreisel mit 5 Kreisel mit 5 Abzweigen lagen die tatsaechlichen Schalt-Separatoren pro
Abzweigen lagen die tatsaechlichen Schalt-Separatoren pro Abzweig Abzweig unterschiedlich weit vom Uebergabeelement entfernt (418-1139mm
unterschiedlich weit vom Uebergabeelement entfernt (418-1139mm in der in der damaligen Registrierung) - eine einzelne mm-Schwelle (Regel 1)
aktuellen Registrierung) - eine einzelne mm-Schwelle (Regel 1) trifft trifft nicht alle. SWITCH_FALLBACK_MM begrenzt das trotzdem: ohne
nicht alle. Durch die eindeutige Vergabe (jedes Element nur einmal) Obergrenze hat Regel 2 an einer groesseren Anlage (Mubea) uebrig
bleibt ein Separator ohne eigenes Uebergabeelement (z. B. ein gebliebene, tatsaechlich unbeteiligte Separatoren dem naechsten noch
gewoehnlicher Zwischenseparator auf der Bahn) trotzdem unbelegt, auch freien Ausschleuselement zugeordnet, auch wenn das 5-9 Meter entfernt
ohne feste mm-Grenze. lag - siehe SWITCH_FALLBACK_MM.
""" """
transfers = [e for e in elements if e.kind in ("Ausschleus", "Einschleus")] transfers = [e for e in elements if e.kind == "Ausschleus"]
result: dict[str, tuple[Element, float]] = {} result: dict[str, tuple[Element, float]] = {}
for sep in separators: for sep in separators:
@@ -325,28 +338,24 @@ def _find_switch_separators(
if best is not None: if best is not None:
result[sep.teile_id] = (best[1], best[0]) result[sep.teile_id] = (best[1], best[0])
# Rolle je Kreisel-Anschluss aus einem bereits bestaetigten Treffer lesen - # Bahnen mit mindestens einem durch Regel 1 bestaetigten Treffer - nur
# keine Annahme, welche Bahn (-L/-R) Aus- bzw. Einschleusung uebernimmt. # dort darf Regel 2 ueberhaupt suchen (siehe Docstring).
bahn_role: dict[str, str] = {} switch_bahnen = {sep.assignment for sep in separators
for sep in separators: if sep.teile_id in result and sep.assignment}
hit = result.get(sep.teile_id)
if hit and sep.assignment:
bahn_role.setdefault(sep.assignment, hit[0].kind)
used_transfers = {hit[0].teile_id for hit in result.values()} 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 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 and s.x is not None and s.y is not None
and bahn_role.get(s.assignment or "") is not None] and s.assignment in switch_bahnen]
candidates: list[tuple[float, Element, Element]] = [] candidates: list[tuple[float, Element, Element]] = []
for sep in remaining_seps: for sep in remaining_seps:
role = bahn_role[sep.assignment or ""]
for transfer in transfers: for transfer in transfers:
if (transfer.kind != role or transfer.teile_id in used_transfers if transfer.teile_id in used_transfers or transfer.x is None or transfer.y is None:
or transfer.x is None or transfer.y is None):
continue continue
d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) d = math.hypot(transfer.x - sep.x, transfer.y - sep.y)
candidates.append((d, sep, transfer)) if d <= SWITCH_FALLBACK_MM:
candidates.append((d, sep, transfer))
candidates.sort(key=lambda c: c[0]) candidates.sort(key=lambda c: c[0])
claimed_seps: set[str] = set() claimed_seps: set[str] = set()
@@ -388,7 +397,7 @@ def derive_tros(
(e for e in elements if e.kind == "Scanner"), key=lambda e: e.teile_id (e for e in elements if e.kind == "Scanner"), key=lambda e: e.teile_id
) )
# Separatoren an einem Aus-/Einschleuselement (siehe _find_switch_separators) # Separatoren an einem Ausschleuselement (siehe _find_switch_separators)
# werden VOR der normalen Host-Aufloesung herausgenommen: sie bilden gleich # werden VOR der normalen Host-Aufloesung herausgenommen: sie bilden gleich
# 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
@@ -520,7 +529,7 @@ def derive_tros(
) )
) )
# 3b) Separatoren an einem Aus-/Einschleuselement -> eigene 1Sep1Swi- # 3b) Separatoren an einem Ausschleuselement -> eigene 1Sep1Swi-
# Uebergabestelle. Das Element uebernimmt die Rolle der (hier fehlenden) # Uebergabestelle. Das Element uebernimmt die Rolle der (hier fehlenden)
# Weiche; siehe _find_switch_separators. Host bewusst die Strecke (nicht # Weiche; siehe _find_switch_separators. Host bewusst die Strecke (nicht
# die Kreisel-Bahn): beide Elementarten tragen "Strecke" oder "Foerderer" # die Kreisel-Bahn): beide Elementarten tragen "Strecke" oder "Foerderer"
@@ -532,7 +541,7 @@ def derive_tros(
strecke_id = str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or "") strecke_id = str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or "")
ende = str(_merkmal(transfer.merkmale, "Streckenende") or "") ende = str(_merkmal(transfer.merkmale, "Streckenende") or "")
own_scanners = sorted(sep_scanners.get(sep_id, [])) own_scanners = sorted(sep_scanners.get(sep_id, []))
art = "Ausschleuselement" if transfer.kind == "Ausschleus" else "Einschleuselement" art = "Ausschleuselement"
tros.append( tros.append(
Tro( Tro(
tro_id="", tro_id="",
@@ -1391,8 +1400,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 von einem Aus-/Einschleuselement " add(f"| Separator <= {SWITCH_PROXIMITY_MM:.0f}mm (oder naechstgelegen bis "
"| `1Sep1Swi` (Element uebernimmt die fehlende Weiche) |") f"{SWITCH_FALLBACK_MM:.0f}mm) von einem Ausschleuselement "
"| `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("| 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 "