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:
co-authored by
Claude Sonnet 5
parent
e4bfad4dcc
commit
058c60e23d
@@ -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
@@ -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 "
|
||||||
|
|||||||
Reference in New Issue
Block a user