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
+87 -67
View File
@@ -149,7 +149,7 @@ Konzept zur sepliste — es sind dieselben physischen Symbole, nur aus zwei
Blickwinkeln: `csv:sep-proxies-erzeugen` liefert sie als echte, exportierbare
INSERT-Kopie (mit Bounding-Box und ID); die sepliste liefert dieselben
Separatoren als Eintraege mit Vorgaenger-/Nachfolger-Wissen. Python fuehrt
beides zusammen (siehe `map_separator_kette_items` unten) statt sie doppelt
beides zusammen (siehe `map_sepliste_kette_items` unten) statt sie doppelt
zu zaehlen.
### Das Feld `id` je sepliste-Eintrag
@@ -161,13 +161,15 @@ realen Bauteils, das ihn in der Zeichnung vertritt:
{"typ": "AS", "lfdnr": 1, "x": 5070.1, "y": 1199.2, "id": "0031"}
```
Das ist dieselbe ID, die die zugehoerige temporaere Kopie in ihrer eigenen
JSON-Zeile traegt — die Verbindung "Kettenglied <-> reales Teil" steht damit
schon im Export und muss in Python nicht mehr ueber Koordinaten-Naehe geraten
werden (`map_separator_kette_items`, siehe unten; solange Python das Feld noch
nicht liest, aendert sich dort nichts). Das Feld fehlt, wenn keine Kopie
erzeugt werden konnte oder deren ID-Attribut leer blieb — Altbestand und
Fehlerfaelle verhalten sich damit wie bisher.
Das ist dieselbe ID, die die zugehoerige temporaere Kopie (SEP) bzw. der
dauerhafte `ZZ_ASES_MARKER` (AS/ES) in ihrer eigenen JSON-Zeile traegt — die
Verbindung "Kettenglied <-> reales Teil" steht damit schon im Export und muss
in Python nicht mehr ueber Koordinaten-Naehe geraten werden. `map_sepliste_
kette_items` (siehe Abschnitt 5 unten) loest genau darueber jeden sepliste-
Eintrag exakt per ID-Gleichheit auf. Das Feld fehlt, wenn keine Kopie erzeugt
werden konnte oder deren ID-Attribut leer blieb — fuer SEP faellt das auf den
alten Koordinaten-Abgleich zurueck (Altbestand), fuer AS/ES bleibt der
Eintrag dann unaufgeloest (siehe Abschnitt 3 unten).
### Der AS/ES-Marker (`ZZ_ASES_MARKER`)
@@ -358,10 +360,11 @@ Zu beachten:
- `VF_1` (`0021`) bekommt seine eigene `TrackIds`-Spalte
(`write_separatorliste_merkmale`) aus der `sepliste`: AS -> SEP -> SEP -> ES
in Baureihenfolge, unabhaengig vom Kreisel-Umlauf.
- Die AS/ES-Enden (`0021`=AS, `0024`=ES) sind hier synthetische Zeilen aus
`build_strecke_schleus_items` (Zeilentyp "ILS Ausschleuselement"/
"ILS Einschleuselement") — in diesem gekuerzten Beispiel nicht einzeln
aufgefuehrt, aber ihre TeileIds tauchen in `TrackIds` beider Seiten auf.
- Die AS/ES-Enden (`0021`=AS, `0024`=ES) sind eigene Zeilen (Zeilentyp
"ILS Ausschleuselement"/"ILS Einschleuselement"), direkt aus dem
dauerhaften `ZZ_ASES_MARKER` klassifiziert — in diesem gekuerzten Beispiel
nicht einzeln aufgefuehrt, aber ihre TeileIds tauchen in `TrackIds` beider
Seiten auf.
- Die Separatoren 3 und 5 (`0012`/`0022`) unterscheiden sich nur durch ihre
`Zuordnung`: `0012` haengt am Kreisel, `0022`/`0023` an der VF-Kette — beide
Faelle laufen ueber denselben `pattern_separator`-Blocktyp.
@@ -435,39 +438,46 @@ Partner", je nach Mindestanzahl Partner pro TeileArt).
### 3. Synthetische Zeilen an den Uebergangsstellen
Drei Arten von "Zeilen ohne eigene Zeichnungsgeometrie" werden aus der
Geometrie der bereits klassifizierten Items abgeleitet:
Nur noch EINE Art von "Zeile ohne eigene Zeichnungsgeometrie" wird aus der
Geometrie bereits klassifizierter Items abgeleitet:
- **`compute_kreisel_touch_switches`** — praezise Kapsel-Geometrie (nicht nur
Bounding-Box) findet jeden Punkt, an dem sich zwei echte Kreisel wirklich
beruehren, und erzeugt dort eine `"ILS Weiche"`-Zeile
(`build_kreisel_weiche_items`). Das ist der Verzweigungspunkt, an dem
Material von einem Kreisel zum anderen wechseln kann.
- **`compute_strecke_kreisel_schleus`** — an jedem Streckenende (AS/ES,
ueber K1/K2 bzw. als Fallback die Wrapper-Enden), das einen Kreisel
beruehrt (siehe Schritt 2), entsteht ein `"ILS Ausschleuselement"`
(Anfang) bzw. `"ILS Einschleuselement"` (Ende), leicht zum Kreisel hin
versetzt (`build_strecke_schleus_items`).
- **`map_separator_kette_items`** — ordnet jedem `"SEP"`-Eintrag einer
sepliste das dazu passende, bereits vorhandene Separator-Item zu (siehe
naechster Abschnitt). Erzeugt bewusst KEINE eigenen Zeilen.
AS/ES-Kettenenden werden NICHT mehr synthetisiert (fruehere Version:
`compute_strecke_kreisel_schleus`/`build_strecke_schleus_items` suchten den
anschliessenden Kreisel per Kollisionstest und erfanden dabei eine eigene,
bei jedem Export neue TeileId). Der dauerhafte `ZZ_ASES_MARKER`
(Lisp/export.lsp, siehe oben) ist ein echter Block mit echter Geometrie -
`"ILS Ausschleuselement"`/`"ILS Einschleuselement"` sind darum eine ganz
normale Zeile aus Schritt 1 (Klassifikation), TeileId/Position/Boundingbox
direkt vom Marker-Block selbst.
`map_sepliste_kette_items` (siehe Abschnitt 5 unten) ordnet danach jedem
sepliste-Eintrag (AS/SEP/ES) das dazu passende, bereits vorhandene Item zu.
Erzeugt bewusst KEINE eigenen Zeilen.
### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen
`compute_kreisel_umlauf` ist der zentrale Baustein, der Ketten-Enden,
freie Punkte (Separatoren UND BTMT Beladung/Entladung) und Kreisel-Kreisel-
`compute_kreisel_umlauf` ist der zentrale Baustein, der freie Punkte
(Separatoren, BTMT Beladung/Entladung, AS/ES-Enden) und Kreisel-Kreisel-
Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden
echten Kreisel werden alle Punkte gesammelt, die dort geometrisch liegen:
- freie Punkte (`KREISEL_UMLAUF_TEILEARTEN` in `lib/export_neighbors.py`:
Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang — alles
Stationen/Markierungen direkt auf der Bahn/dem Track), deren Bounding-Box
die Kreisel-Box ueberschneidet,
Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang UND die AS-/ES-
Ein-/Ausschleuselemente — alles Stationen/Markierungen direkt auf der
Bahn/dem Track mit eigener Bounding-Box), deren Bounding-Box die Kreisel-
Box ueberschneidet. Ein AS/ES bringt seine Box vom eigenen `ZZ_ASES_MARKER`
mit — keine gesonderte Naeherungslogik mehr noetig (fruehere Version
suchte hierfuer je AS/ES-sepliste-Eintrag den geometrisch naechstgelegenen
Kreisel, unabhaengig davon ob er wirklich in Reichweite lag),
- die Kreisel-Kreisel-Weichen aus Schritt 3 (in BEIDEN beteiligten
Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist —
man kann von Kreisel A nach B und zurueck),
- die AS/ES-Enden andockender VF_n/GF_n-Ketten (aus der `sepliste`,
zugeordnet zum geometrisch naechstgelegenen Kreisel).
man kann von Kreisel A nach B und zurueck).
Alle Punkte werden nach Winkel um die Kreiselachse sortiert (Richtung ueber
das `DREHRICHTUNG`-Attribut, UZS/GUZ) — das ergibt eine geschlossene,
@@ -484,32 +494,31 @@ echte Nachbarn.
`compute_sep_kette` liest jede `sepliste` und baut daraus einen Schluessel
je Eintrag (`"<strecke_id>#<lfdnr>"`) mit `vorgaenger`/`nachfolger`
(wieder solche Schluessel, `None` am Kettenende). Das ist eine reine
Uebersetzung der LISP-Baureihenfolge — kein Rateverfahren.
(wieder solche Schluessel, `None` am Kettenende) sowie `id` (die TeileId des
vertretenden Items, siehe oben). Das ist eine reine Uebersetzung der
LISP-Baureihenfolge — kein Rateverfahren.
**Warum die Zeilen erst NACH allen Schluesseln aufgeloest werden:** ein
Schluessel kann auf eine Zeile zeigen, die zum Zeitpunkt seiner Vergabe noch
gar nicht existiert (eine SEP in der Mitte einer Kette hat ihre AS als
Vorgaenger, aber die AS-Zeile entsteht erst in `build_strecke_schleus_items`,
das VOR der SEP-Zuordnung laeuft). Darum sammeln
`build_strecke_schleus_items` und `map_separator_kette_items` ihre Treffer
zunaechst nur in einem gemeinsamen `sep_kette_by_key`-dict
(Schluessel -> Item), OHNE die `Nachbarn`-Spalte zu setzen.
`resolve_umlauf_und_sepliste_nachbarn` loest danach in einem einzigen Schritt
sowohl die Kreisel-Umlauf-Punkte als auch die sepliste-Schluessel gegen einen
gemeinsamen Lookup (freie Separatoren nach TeileId, Weichen nach Schluessel,
sepliste-Zeilen nach Schluessel) zu echten TeileIds auf.
**`map_sepliste_kette_items`** loest jeden sepliste-Schluessel auf sein
bereits vorhandenes Item auf — EXAKT per ID-Gleichheit (Feld `id` gegen die
`teileid` der klassifizierten Items), nicht mehr per Koordinaten-Naehe:
AS/ES sind eigene Items (Schritt 3 entfaellt fuer sie), SEP ein bereits
klassifiziertes Separator-Item (der verpackte Separator liegt durch
`csv:sep-proxies-erzeugen` bereits als echtes Item vor — eine zweite, aus
der sepliste erzeugte Zeile waere derselbe physische Separator ein zweites
Mal). Nur wenn das `id`-Feld fehlt (Altbestand-Export ohne Marker/Kopie-ID),
faellt SEP auf den alten Abgleich zurueck: Wrapper-ID (Item-Merkmal
`"Zuordnung"`) plus naechstgelegener rohen INSERT-Punkt (Item-Felder
`_x`/`_y` vs. sepliste-Koordinate). AS/ES ohne `id` bleiben unaufgeloest -
fuer sie existiert kein Item mehr, das sie ersatzweise vertreten koennte.
**`map_separator_kette_items` erzeugt keine neuen Zeilen** (Architektur-
entscheidung, siehe Docstring in `lib/export_csv.py`): der verpackte
Separator liegt durch `csv:sep-proxies-erzeugen` (Phase 1, Schritt 1) bereits
als echtes Separator-Item vor. Eine zweite, aus der sepliste erzeugte Zeile
waere derselbe physische Separator ein zweites Mal. Die Zuordnung
sepliste-Eintrag -> vorhandenes Item laeuft ueber die Wrapper-ID (Item-
Merkmal `"Zuordnung"`, von `csv:sep-proxies-zuordnung-setzen` fest gesetzt)
plus den naechstgelegenen rohen INSERT-Punkt (Item-Felder `_x`/`_y` vs. die
sepliste-Koordinate) — beide stammen aus derselben Bauphase und liegen daher
nah beieinander, auch wenn sie nicht exakt uebereinstimmen.
Die Treffer landen zunaechst nur in `sep_kette_by_key` (Schluessel -> Item),
OHNE die `Nachbarn`-Spalte zu setzen: ein Schluessel kann auf eine Zeile
zeigen, deren eigener Nachbar (Vorgaenger/Nachfolger) zu diesem Zeitpunkt
selbst noch nicht aufgeloest ist. `resolve_umlauf_und_sepliste_nachbarn`
loest danach in einem einzigen Schritt sowohl die Kreisel-Umlauf-Punkte als
auch die sepliste-Schluessel gegen einen gemeinsamen Lookup (freie Punkte
nach TeileId, Weichen nach Schluessel, sepliste-Zeilen nach Schluessel) zu
echten TeileIds auf.
### Die Spalte `"TrackIds"` — Reihenfolge als eigene CSV-Spalte
@@ -517,17 +526,28 @@ Zusaetzlich zur `"Nachbarn"`-Spalte (Vorgaenger/Nachfolger je Zeile) traegt
`write_separatorliste_merkmale` (`lib/export_csv.py`) jeder VF_n/GF_n-Zeile
die eigene Spalte `"TrackIds"` (`item["trackids"]`, Top-Level-Feld wie
`"nachbarn"` — KEIN Merkmal) ein: die TeileIds von AS-Ende, Separatoren UND
ES-Ende in Baureihenfolge, kommagetrennt (z.B. `"0051, 0052, 0053, 0054,
0055"` fuer AS, zwei SEP, ES). AS/ES zaehlen mit, weil sie beim Kreisel
(siehe unten) ebenfalls Teil von dessen `TrackIds` sind — ohne sie fehlten
hier die Kettenenden. Quelle ist die rohe `sepliste` des Items (nicht
`sep_kette`, dessen dict-Reihenfolge nicht garantiert ist). Muss NACH
`map_separator_kette_items`/`build_strecke_schleus_items` laufen, weil erst
dann jeder sepliste-Schluessel (AS/SEP/ES) auf sein reales Item (TeileId)
aufgeloest ist. Beruehrt ein Streckenende geometrisch KEINEN Kreisel,
entsteht dort keine AS/ES-Zeile (`build_strecke_schleus_items`) und damit
auch keine TeileId — dieses Ende fehlt dann in `TrackIds` (kein Fehler,
sondern Folge der fehlenden Kollision).
ES-Ende in Baureihenfolge, kommagetrennt (z.B. `"0056, 0052, 0053, 0057"`
fuer AS, zwei SEP, ES). AS/ES zaehlen mit, weil sie beim Kreisel (siehe
unten) ebenfalls Teil von dessen `TrackIds` sind — ohne sie fehlten hier die
Kettenenden. Quelle ist die rohe `sepliste` des Items (nicht `sep_kette`,
dessen dict-Reihenfolge nicht garantiert ist). Muss NACH
`map_sepliste_kette_items` laufen, weil erst dann jeder sepliste-Schluessel
(AS/SEP/ES) auf sein reales Item (TeileId) aufgeloest ist. Fehlt einem
Eintrag die TeileId (kein `id`-Feld und - nur bei SEP - kein Koordinaten-
Fallback-Treffer), fehlt dieses Kettenglied dann in `TrackIds` (kein Fehler,
sondern Folge der fehlenden Zuordnung).
### Konsistenzpruefung: AS/ES in beiden Containern
`pruefe_ases_in_beiden_containern` (`lib/export_csv.py`, laeuft am Ende von
`process_blocks`, NACH beiden `TrackIds`-Spalten) prueft fuer jede AS-/ES-
Zeile, ob ihre TeileId sowohl in den `TrackIds` ihrer EIGENEN Strecke
(ZUORDNUNG-Attribut des Markers) als auch in den `TrackIds` MINDESTENS eines
Kreisels auftaucht — beide "Container", zu denen ein AS/ES logisch gehoert.
Fehlt einer von beiden, ist das kein Normalfall (z.B. ZUORDNUNG leer/falsch,
Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am
Marker): die Zeile bekommt eine Eintragung in der Spalte `"Warnungen"`
(`item["warnung"]`) UND eine Konsolen-/Debuglog-Meldung.
`write_kreisel_separatorliste_merkmale` ueberträgt dasselbe Prinzip auf
echte Kreisel (`"ILS 2.0 Kreisel"`): Quelle ist hier NICHT die sepliste
@@ -544,8 +564,8 @@ separatorliste_merkmale` loest die rohen Umlauf-Schluessel (TeileId eines
freien Punkts, sep_kette-Schluessel oder Weichen-Key) auf echte TeileIds auf
und schreibt sie kommagetrennt in `item["trackids"]` auf den Kreisel selbst.
Muss darum NACH `resolve_umlauf_und_sepliste_nachbarn` (bzw. nach allen
Zeilen-Registrierungen: `build_kreisel_weiche_items`, `build_strecke_
schleus_items`, `map_separator_kette_items`) laufen.
Zeilen-Registrierungen: `build_kreisel_weiche_items`,
`map_sepliste_kette_items`) laufen.
BTMT Beladung/Entladung sind bewusst NICHT in `SENSOR_TEILEARTEN`
aufgenommen (die bleibt Separator/Scanner vorbehalten, siehe