export_csv: AS/ES direkt aus ZZ_ASES_MARKER klassifizieren, Kreisel<->Strecke-Kollisionserzeugung entfernen

Stufe 2 (Python-Seite von "sepliste: Bauteil-IDs eintragen"): AS/ES-Kettenenden
sind jetzt ganz normale Items, direkt aus dem dauerhaften ZZ_ASES_MARKER
klassifiziert (TeileId/Position/Boundingbox vom Marker-Block selbst) - keine
Kollisionsberechnung mehr, die sie erfindet.

- lib/export_neighbors.py: compute_strecke_kreisel_schleus entfernt (suchte
  bisher per Kollisionstest den anschliessenden Kreisel und erzeugte dabei bei
  JEDEM Export eine neue TeileId fuer denselben physischen Punkt). AUSSCHLEUS_/
  EINSCHLEUS_TEILEART sind jetzt Teil von KREISEL_UMLAUF_TEILEARTEN - ein AS/ES
  bringt seine eigene Bounding-Box vom Marker mit und wird dadurch, wie jeder
  freie Separator, ueber die normale BBox-Ueberschneidung einem Kreisel
  zugeordnet; die gesonderte "naechster Kreisel per sep_kette"-Suche in
  compute_kreisel_umlauf (dessen sep_kette-Parameter damit entfaellt) ist
  ueberfluessig geworden. compute_sep_kette gibt je Eintrag zusaetzlich das
  Feld "id" weiter (Bauteil-TeileId, siehe vorheriger Commit).
- lib/export_csv.py: process_blocks klassifiziert ZZ_ASES_MARKER direkt (TYP-
  Attribut waehlt Ausschleus-/Einschleuselement); build_strecke_schleus_items
  entfaellt komplett. map_separator_kette_items -> map_sepliste_kette_items:
  ordnet jetzt AS/SEP/ES exakt per ID-Gleichheit zu (sepliste-Feld "id" gegen
  die TeileId der klassifizierten Items) statt per Koordinaten-Naehe - SEP
  faellt bei fehlendem "id"-Feld weiterhin auf den alten Naehe-Abgleich zurueck
  (Altbestand), AS/ES ohne "id" bleiben unaufgeloest.
- Neu: pruefe_ases_in_beiden_containern prueft am Ende von process_blocks, ob
  jede AS-/ES-Zeile sowohl in den TrackIds ihrer eigenen Strecke als auch in
  denen mindestens eines Kreisels auftaucht, und traegt bei Abweichung eine
  Warnung ein (Spalte "Warnungen") - Konsistenzpruefung fuer genau die beiden
  "Container", zu denen ein AS/ES gehoert.
- Aufgeraeumt (tote Routinen): _strecke_ends, _closest_pt_on_segment,
  _distinct_kreisel_ids (nur von den entfernten Funktionen genutzt), sowie der
  temporaere Diagnose-Filter DEBUG_HIGHLIGHT_TEILEIDS/dbg_highlight (an die
  jetzt behobene Koordinaten-Matching-Ambiguitaet gebunden, in seinem eigenen
  Kommentar bereits als Wegwerf-Code markiert). LISP-seitiger Dead-Code-Scan
  (export.lsp, ssg_ks_insert.lsp - die diese Woche bearbeiteten Module):
  keine Funde.

Verifiziert an den echten HundM05-Daten (results/export_raw.json, 45 Bloecke):
TeileId-Bereich luecken- und duplikatfrei 1-45 (vorher 1-55 mit Luecke 24-33 +
Zusatz-IDs 46-55), alle 10 AS/ES-IDs korrekt in ihrer Strecke UND im Kreisel
06 TrackIds, keine Warnungen der neuen Pruefung. Zwei Testdateien auf die neue
Architektur umgestellt (test_export_kreisel_umlauf.py: AS/ES als Item statt
Kollisions-Erzeugung, neue Klasse TestAsEsImKreiselUmlauf fuer die BBox-
Teilnahme; test_export_separatorliste.py: id-basierte Zuordnung, Klasse
TestMarkerIdWiederverwendung durch TestMapSeplisteKetteItemsFallback ersetzt
- die alte testete inzwischen entfernte Mint-Logik). Volle Suite: 224 bestanden
(4 vorbestehende, unabhaengige Fixture-Failures unveraendert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-22 14:48:45 +02:00
co-authored by Claude Sonnet 5
parent 719ffc2385
commit ab3958e0e4
5 changed files with 543 additions and 793 deletions
+85 -55
View File
@@ -22,8 +22,7 @@ from export_neighbors import ( # noqa: E402
compute_kreisel_umlauf,
)
from export_csv import ( # noqa: E402
build_strecke_schleus_items,
map_separator_kette_items,
map_sepliste_kette_items,
resolve_umlauf_und_sepliste_nachbarn,
write_kreisel_separatorliste_merkmale,
write_separatorliste_merkmale,
@@ -53,6 +52,20 @@ def _separator(teileid, x, y):
}
def _ases(teileart, teileid, x, y, zuordnung):
"""AS-/ES-Item, wie es process_blocks direkt aus einem ZZ_ASES_MARKER
klassifiziert: eigene Bounding-Box vom Marker-Rechteck, "_zuordnung" =
ZUORDNUNG-Attribut (die eigene Strecke), teilnahmefaehig am Kreisel-
Umlauf ueber KREISEL_UMLAUF_TEILEARTEN wie ein freier Separator."""
return {
"teileart": teileart,
"teileid": teileid,
"_zuordnung": zuordnung,
"merkmale": {},
"_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0},
}
def _btmt(teileart, teileid, x, y):
"""BTMT-Beladung/-Entladung-Stationsblock ("ILS 2.0 BTMT Beladung"/
"ILS 2.0 BTMT Entladung") - frei auf der Bahn platziert wie ein
@@ -105,7 +118,7 @@ class TestKreiselKreiselWeicheImUmlauf:
items = _zwei_beruehrende_kreisel()
switches = compute_kreisel_touch_switches(items, tolerance_mm=0.0,
durchmesser_mm=DURCHMESSER_MM)
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=0.0,
durchmesser_mm=DURCHMESSER_MM,
touch_switches=switches)
weiche_key = switches[0]["key"]
@@ -130,7 +143,7 @@ class TestKreiselKreiselWeicheImUmlauf:
assert len(switches) == 1
weiche_key = switches[0]["key"]
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=50.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=50.0,
durchmesser_mm=DURCHMESSER_MM,
touch_switches=switches)
@@ -166,7 +179,7 @@ class TestKreiselBoundsRotationsUnabhaengig:
k = self._langer_kreisel_90grad()
sep = _separator("9001", 0.0, 10000.0) # liegt mitten auf der Achse
items = [k, sep]
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=200.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0,
durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0002"]]
assert "9001" in keys, "Separator auf der gedrehten Kreiselachse wurde nicht erkannt"
@@ -179,7 +192,7 @@ class TestKreiselBoundsRotationsUnabhaengig:
k = self._langer_kreisel_90grad()
sep = _separator("0065", 958.0, 5100.0)
items = [k, sep]
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=200.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0,
durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0002"]]
assert "0065" not in keys, (
@@ -191,26 +204,24 @@ class TestSeplisteNachbarnAufgeloest:
"""Regressionstest fuer einen dritten realen Mubea-Befund: die meisten
Separatoren blieben trotz sepliste-XDATA komplett ohne "Nachbarn".
Architektur (nach dem Umbau "Proxy liefert Zeilen, sepliste nur
Reihenfolge"): die in einer Kette verpackten Separatoren liegen bereits als
echte (Proxy-)Separator_SP-Items vor (der LISP-Export fuegt je verpacktem
Symbol eine temporaere Kopie ein). map_separator_kette_items erzeugt daher
KEINE neuen Zeilen mehr, sondern ordnet jedem sepliste-SEP das vorhandene
Separator-Item zu (Match ueber Wrapper-ID + INSERT-Punkt) und traegt es in
sep_kette_by_key ein; die AS/ES-Zeilen kommen weiter aus
build_strecke_schleus_items. resolve_umlauf_und_sepliste_nachbarn setzt
dann die Nachbarn als ECHTE TeileIds (nicht die rohen sep_kette-Schluessel,
Architektur: SEP-Eintraege liegen bereits als echte (Proxy-)Separator_SP-
Items vor (der LISP-Export fuegt je verpacktem Symbol eine temporaere
Kopie ein), AS-/ES-Eintraege als eigene Items, direkt aus dem dauerhaften
ZZ_ASES_MARKER klassifiziert (siehe export_csv.py process_blocks) - beide
tragen bereits ihre TeileId als sepliste-Feld "id". map_sepliste_kette_
items erzeugt daher KEINE neuen Zeilen, sondern ordnet jedem sepliste-
Eintrag sein vorhandenes Item exakt per ID zu und traegt es in
sep_kette_by_key ein. resolve_umlauf_und_sepliste_nachbarn setzt dann die
Nachbarn als ECHTE TeileIds (nicht die rohen sep_kette-Schluessel,
frueherer Bug)."""
def _sepliste_kette(self):
"""Ein Kreisel + eine Gefaellestrecke mit sepliste AS(1) -> SEP(2) ->
ES(3) PLUS das bereits vorhandene Proxy-Separator-Item fuer den SEP
(wie es der freie-Separator-Pfad in process_blocks erzeugt: TeileId,
Merkmal "Zuordnung" = Wrapper-ID, INSERT-Punkt _x/_y an der sepliste-
SEP-Koordinate). "nachbarn" auf der Strecke simuliert das Ergebnis von
compute_neighbor_ids (BBox-Ueberschneidung Strecke <-> Kreisel) - die
Vorbedingung, unter der compute_strecke_kreisel_schleus die AS/ES-
Schleuselemente erzeugt."""
ES(3), PLUS die drei bereits vorhandenen Items, die diese Eintraege
vertreten (AS/ES ueber den ZZ_ASES_MARKER, SEP ueber den freien-
Separator-Pfad in process_blocks) - jeder sepliste-Eintrag traegt
bereits die TeileId seines Items als Feld "id" (siehe
csv:asmark-ids-setzen/csv:sepliste-ids-setzen, Lisp/export.lsp)."""
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
strecke = {
"teileart": "ILS 2.0 Gefaellestrecke",
@@ -220,13 +231,14 @@ class TestSeplisteNachbarnAufgeloest:
"_bbox": {"cx": 5000.0, "cy": 0.0, "cz": 0.0, "dx": 8000.0, "dy": 300.0},
"k1": "", "k2": "",
"sepliste": [
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0},
{"typ": "SEP", "lfdnr": 2, "x": 5000.0, "y": 0.0},
{"typ": "ES", "lfdnr": 3, "x": 8500.0, "y": 0.0},
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0, "id": "0056"},
{"typ": "SEP", "lfdnr": 2, "x": 5000.0, "y": 0.0, "id": "0055"},
{"typ": "ES", "lfdnr": 3, "x": 8500.0, "y": 0.0, "id": "0057"},
],
}
# Proxy-Separator-Item (bereits vorhanden), Zuordnung = Wrapper-ID 0010,
# INSERT-Punkt an der sepliste-SEP-Koordinate (5000, 0).
# SEP: Proxy-Separator-Item (INSERT-Punkt an der sepliste-SEP-
# Koordinate - nur als Fallback-Feld gefuehrt, die Zuordnung selbst
# laeuft exakt per ID).
proxy_sep = {
"teileart": "ILS 2.0 Separator",
"teileid": "0055",
@@ -235,7 +247,12 @@ class TestSeplisteNachbarnAufgeloest:
"_bbox": {"cx": 5000.0, "cy": 0.0, "cz": 0.0, "dx": 150.0, "dy": 210.0},
"nachbarn": "",
}
return [k, strecke, proxy_sep]
# AS/ES: eigene Items wie aus dem ZZ_ASES_MARKER klassifiziert.
as_item = _ases("ILS Ausschleuselement", "0056", 1500.0, 0.0, "0010")
as_item["nachbarn"] = ""
es_item = _ases("ILS Einschleuselement", "0057", 8500.0, 0.0, "0010")
es_item["nachbarn"] = ""
return [k, strecke, proxy_sep, as_item, es_item]
def test_keine_rohen_sep_kette_schluessel_in_nachbarn(self):
from export_neighbors import compute_sep_kette
@@ -246,20 +263,15 @@ class TestSeplisteNachbarnAufgeloest:
touch_switches = []
weiche_by_key = {}
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
items, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
touch_switches=touch_switches)
sep_kette_by_key = {}
build_strecke_schleus_items(
items, None, 100, sep_kette=sep_kette,
sep_kette_by_key=sep_kette_by_key)
map_separator_kette_items(
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
resolve_umlauf_und_sepliste_nachbarn(
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key)
# AS/ES (neu erzeugt) + der bereits vorhandene Proxy-Separator.
# AS/ES + der bereits vorhandene Proxy-Separator.
sepliste_items = [it for it in items
if it.get("teileart") in
("ILS 2.0 Separator", "ILS Ausschleuselement", "ILS Einschleuselement")]
@@ -282,14 +294,9 @@ class TestSeplisteNachbarnAufgeloest:
items = self._sepliste_kette()
sep_kette = compute_sep_kette(items)
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
items, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
touch_switches=[])
sep_kette_by_key = {}
build_strecke_schleus_items(
items, None, 100, sep_kette=sep_kette,
sep_kette_by_key=sep_kette_by_key)
map_separator_kette_items(
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
resolve_umlauf_und_sepliste_nachbarn(
items, kreisel_umlauf, sep_kette, sep_kette_by_key, {})
@@ -299,6 +306,8 @@ class TestSeplisteNachbarnAufgeloest:
# Der SEP-Schluessel muss auf das vorhandene Proxy-Separator-Item zeigen.
assert sep_item["teileid"] == "0055", "sepliste-SEP nicht auf Proxy-Separator gemappt"
assert as_item["teileid"] == "0056", "sepliste-AS nicht auf sein Item gemappt"
assert es_item["teileid"] == "0057", "sepliste-ES nicht auf sein Item gemappt"
sep_nachbarn = {n.strip() for n in sep_item["nachbarn"].split(",") if n.strip()}
assert sep_nachbarn == {as_item["teileid"], es_item["teileid"]}, (
@@ -313,12 +322,7 @@ class TestSeplisteNachbarnAufgeloest:
from export_neighbors import compute_sep_kette
items = self._sepliste_kette()
sep_kette = compute_sep_kette(items)
sep_kette_by_key = {}
build_strecke_schleus_items(
items, None, 100, sep_kette=sep_kette,
sep_kette_by_key=sep_kette_by_key)
map_separator_kette_items(
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
write_separatorliste_merkmale(items, sep_kette_by_key)
strecke = next(it for it in items if it.get("teileid") == "0010")
@@ -363,7 +367,7 @@ class TestKreiselUmlaufStartetBeiNullGrad:
# 170/80 Grad - der kleinste (und damit Listenanfang) ist S280 (80
# Grad), NICHT S010 (350 Grad, liegt kurz VOR dem vollen Umlauf).
items = self._kreisel_mit_vier_separatoren("UZS")
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=0.0,
durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0001"]]
assert keys[0] == "S280", f"Umlauf startet nicht bei 0 Grad (UZS): {keys}"
@@ -374,7 +378,7 @@ class TestKreiselUmlaufStartetBeiNullGrad:
# monoton mit 10/100/190/280 Grad - der Umlauf startet beim kleinsten
# (S010) und laeuft aufsteigend weiter.
items = self._kreisel_mit_vier_separatoren("GUZ")
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0,
umlauf = compute_kreisel_umlauf(items, tolerance_mm=0.0,
durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0001"]]
assert keys[0] == "S010", f"Umlauf startet nicht bei 0 Grad (GUZ): {keys}"
@@ -396,7 +400,7 @@ class TestKreiselTrackidsSpalte:
items = [k, sep_a, sep_b]
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
write_kreisel_separatorliste_merkmale(
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
@@ -408,7 +412,7 @@ class TestKreiselTrackidsSpalte:
k["merkmale"] = {}
items = [k]
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette={}, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM)
items, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM)
write_kreisel_separatorliste_merkmale(
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
assert "trackids" not in items[0]
@@ -425,7 +429,7 @@ class TestKreiselTrackidsSpalte:
items = [k, sep, bel, ent]
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in kreisel_umlauf["0001"]]
assert set(keys) == {"0020", "0030", "0031"}, (
f"BTMT Beladung/Entladung fehlen im Kreisel-Umlauf: {keys}")
@@ -447,7 +451,7 @@ class TestKreiselTrackidsSpalte:
items = [k, sep, ein, aus]
kreisel_umlauf = compute_kreisel_umlauf(
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in kreisel_umlauf["0001"]]
assert set(keys) == {"0020", "0040", "0041"}, (
f"Eingang/Ausgang fehlen im Kreisel-Umlauf: {keys}")
@@ -455,3 +459,29 @@ class TestKreiselTrackidsSpalte:
write_kreisel_separatorliste_merkmale(
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
assert k["trackids"] == "0041, 0020, 0040"
class TestAsEsImKreiselUmlauf:
"""Neu (Marker-Architektur): AS/ES sind jetzt ein ganz normaler
KREISEL_UMLAUF_TEILEARTEN-Eintrag - ihre eigene Bounding-Box (vom
ZZ_ASES_MARKER) entscheidet, ob sie im Umlauf eines Kreisels auftauchen,
genau wie ein freier Separator. Keine gesonderte Naeherungslogik mehr."""
def test_as_in_reichweite_taucht_im_umlauf_auf(self):
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
as_item = _ases("ILS Ausschleuselement", "0056", 300.0, 300.0, "0010")
items = [k, as_item]
umlauf = compute_kreisel_umlauf(
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0001"]]
assert "0056" in keys, "AS-Item mit eigener BBox wurde nicht im Umlauf erkannt"
def test_es_ausserhalb_der_reichweite_fehlt_im_umlauf(self):
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
es_item = _ases("ILS Einschleuselement", "0057", 20000.0, 0.0, "0010")
items = [k, es_item]
umlauf = compute_kreisel_umlauf(
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
keys = [p[0] for p in umlauf["0001"]]
assert "0057" not in keys, (
"ES-Item weit ausserhalb der Kreiselbox wurde faelschlich erkannt")