From ab3958e0e44b859abaeb16cedf6fa181bd0ca9c1 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Tue, 22 Sep 2026 14:48:45 +0200 Subject: [PATCH] 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 --- doc/csv-export.md | 154 ++++---- lib/export_csv.py | 546 +++++++++++++--------------- lib/export_neighbors.py | 326 +++-------------- tests/test_export_kreisel_umlauf.py | 140 ++++--- tests/test_export_separatorliste.py | 170 ++++----- 5 files changed, 543 insertions(+), 793 deletions(-) diff --git a/doc/csv-export.md b/doc/csv-export.md index e05fca0..ee33597 100644 --- a/doc/csv-export.md +++ b/doc/csv-export.md @@ -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 (`"#"`) 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 diff --git a/lib/export_csv.py b/lib/export_csv.py index 1c3a019..0b83f1c 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -41,14 +41,15 @@ from export_neighbors import ( compute_sensor_zuordnung, compute_scanner_nearest_separator, compute_kreisel_touch_switches, - compute_strecke_kreisel_schleus, compute_sep_kette, compute_kreisel_umlauf, - dbg_highlight, SENSOR_TEILEARTEN, WEICHE_TEILEART, GF_VF_TEILEARTEN, + KREISEL_SPLIT_TEILEART, KREISEL_UMLAUF_TEILEARTEN, + AUSSCHLEUS_TEILEART, + EINSCHLEUS_TEILEART, ) BLOCKPATTERNS = load_patterns() @@ -534,174 +535,44 @@ def _strecke_typ_feld(teileart): return "Strecke" if teileart == "ILS 2.0 Gefaellestrecke" else "Förderer" -def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette=None, - sep_kette_by_key=None, dbg=None): - """Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine - "ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile - (Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus - fuer Position/Boundingbox - reine Geometrie, unveraendert). +def map_sepliste_kette_items(items, sep_kette, dbg=None): + """Ordnet jeden sepliste-Eintrag (AS/SEP/ES, siehe compute_sep_kette) + einem bereits klassifizierten Item zu und liefert sie unter ihrem + sep_kette-Schluessel zurueck - fuer die Nachbar-Aufloesung in + resolve_umlauf_und_sepliste_nachbarn sowie die TrackIds-Spalte der + besitzenden Strecke (siehe write_separatorliste_merkmale). - Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung - (max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst. - Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, an der - Kreiselachse ausgerichtete Default-Box. + ERZEUGT KEINE neuen Zeilen: AS/ES liegen bereits als eigene Items vor + (direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe + process_blocks), SEP als die beim Export ausgepackte temporaere + Separator_SP-Kopie (csv:sep-proxies-erzeugen, export.lsp). Beide Wege + wuerden sonst dieselben physischen Bauteile DOPPELT zaehlen. - sep_kette = Rueckgabe von export_neighbors.compute_sep_kette (keys - "#" -> Eintrag mit typ/vorgaenger/nachfolger). Traegt - die zugehoerige Strecke eine "sepliste" (neue LISP-XDATA, siehe - ssg-sepliste-xdata-schreiben), bleibt "Nachbarn" hier zunaechst LEER und - der sep_kette-Schluessel dieser Zeile wird stattdessen in - sep_kette_by_key (dict, vom Aufrufer bereitgestellt und HIER befuellt) - eingetragen - process_blocks loest daraus NACH der Registrierung ALLER - SEP/AS/ES-Schluessel (auch der von map_separator_kette_items) die - tatsaechlichen TeileIds auf (eigener Block am Ende von process_blocks, - direkt nach beiden Aufrufen). Ein sep_kette-Schluessel ist naemlich KEINE TeileId, - sondern nur waehrend des Baus eindeutig - - ihn direkt in die CSV zu schreiben waere ein Bug (zeigte frueher auf - "#" statt auf den eigentlichen Nachbarn). Die - Kreisel-/Foerderer-Zugehoerigkeit steht bei sepliste-Zeilen stattdessen - in den Merkmalen ("Kreisel" + "Förderer"/"Strecke" je Typ, analog zum - "Zuordnung"-Feld der Separatoren). Fehlt die sepliste (Altbestand-Wrapper - ohne die neue XDATA), bleibt der bisherige Nachbarn-Inhalt - [kreisel_id, strecke_id] unveraendert erhalten (sofort aufgeloest, kein - sep_kette-Schluessel involviert). + Zuordnung: EXAKTER ID-Abgleich. Seit csv:sepliste-ids-setzen/ + csv:asmark-ids-setzen (Lisp/export.lsp) traegt praktisch jeder sepliste- + Eintrag bereits die TeileId des ihn vertretenden Items als Feld "id" - + das Item mit GENAU dieser teileid IST der gesuchte Partner. Kein Rate-/ + Naeheverfahren mehr noetig, keine der frueheren Ambiguitaeten (ein + einzelner fehlerhafter sepliste-Koordinatenwert konnte vorher per + Naeherungssuche einen FALSCHEN Nachbarn waehlen und einen anderen + Separator ganz ohne Zuordnung lassen - siehe Memory + sepliste-lfdnr2-koordinaten-bug.md). - Rueckgabe: fortgeschriebene Elementnummer. + Fallback (kein "id"-Feld - Altbestand-Export ohne Marker/Kopie-ID, oder + deren Zuweisung ist in Lisp fehlgeschlagen): NUR fuer SEP-Eintraege + weiterhin per naechstem INSERT-Punkt gegen die per Zuordnung + vorgefilterten Separator-Items. AS/ES ohne "id" bleiben unaufgeloest - + fuer sie existiert seit dem Wegfall der Kreisel<->Strecke-Kollisions- + erzeugung (frueher compute_strecke_kreisel_schleus/ + build_strecke_schleus_items) ohnehin kein Item mehr, das sie vertreten + koennte. + + Rueckgabe: sep_kette_by_key (neues dict). """ - if sep_kette is None: - sep_kette = {} - if sep_kette_by_key is None: - sep_kette_by_key = {} - schleus, warnings = compute_strecke_kreisel_schleus( - items, durchmesser_mm=KREISEL_DURCHMESSER_MM, - element_box_mm=ELEMENT_BOX_MM, dbg=dbg) + sep_kette_by_key = {} + by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")} - strecke_by_id = {it.get("teileid", ""): it for it in items - if it.get("teileart", "") in GF_VF_TEILEARTEN} - - # Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile - # schreiben (Spalte "Warnungen") und in die Konsole ausgeben. - if warnings: - for sid, text in warnings.items(): - if sid in strecke_by_id: - strecke_by_id[sid]["warnung"] = text - print(f"[export_csv] WARNUNG: Strecke {sid} {text}") - - naechste_id = max_numeric_teileid(items) - elem_nr = start_elem_nr - zaehler = {} - for sp in schleus: - elem_nr += 1 - teileart = sp["teileart"] - zaehler[teileart] = zaehler.get(teileart, 0) + 1 - planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg) - if planquadrat_cfg is not None else "") - - strecke = strecke_by_id.get(sp["strecke_id"]) - sepliste = strecke.get("sepliste") if strecke else None - merkmale = {"Erzeugt aus": "Kreisel-Strecke-Kollision"} - sep_kette_key = None - marker_id = "" - if sepliste: - # Neue XDATA vorhanden: Nachbarn nur noch aus der Ketten-internen - # Vorgaenger/Nachfolger-Beziehung (AS/ES ist immer das erste bzw. - # letzte Sepliste-Element seiner Strecke). Die Keys (vorgaenger/ - # nachfolger) sind sep_kette-interne Schluessel, KEINE fertigen - # TeileIds - erst nach dem Bau ALLER SEP/AS/ES-Zeilen dieser Kette - # aufloesbar (siehe process_blocks, Abschnitt nach beiden Aufrufen, - # analog zum Kreisel-Umlauf/Weichen-Muster). - sepliste_ende = sepliste[0] if sp["ende_label"] == "Anfang" else sepliste[-1] - lfdnr = sepliste_ende["lfdnr"] - # ZZ_ASES_MARKER (csv:asmark-aktualisieren, Lisp/export.lsp) traegt - # bereits eine STABILE, ueber Exportlaeufe hinweg gleichbleibende - # ID fuer genau dieses Kettenende (schon 4-stellig formatiert, - # ssg-id-format) - im sepliste-Eintrag als Feld "id" gespiegelt. - # Diese ID unten WIEDERVERWENDEN statt eine neue zu minten: der - # Marker bekommt in process_blocks bewusst KEINE eigene CSV-Zeile - # (siehe pattern_ases_marker), seine ID bliebe sonst fuer immer - # ungenutzt, waehrend hier bei jedem Exportlauf zusaetzlich eine - # neue, hoehere ID fuer denselben physischen Punkt erfunden wuerde. - marker_id = str(sepliste_ende.get("id", "")).strip() - sep_kette_key = f"{sp['strecke_id']}#{lfdnr}" - nachbarn = [] # in process_blocks nachgetragen - merkmale["Kreisel"] = sp["kreisel_id"] - merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = sp["strecke_id"] - else: - nachbarn = [sp["kreisel_id"], sp["strecke_id"]] - merkmale["Kreisel"] = sp["kreisel_id"] - merkmale["Strecke"] = sp["strecke_id"] - merkmale["Streckenende"] = sp["ende_label"] - - if marker_id: - # Marker-ID uebernehmen (schon 4-stellig formatiert - NICHT - # erneut formatieren) und den laufenden Zaehler ggf. hochziehen, - # damit ein SPAETERER Mint in dieser Schleife (Altbestand-Wrapper - # ohne Marker/sepliste) nie mit einer bereits wiederverwendeten - # Marker-ID kollidiert - unabhaengig davon, in welcher Reihenfolge - # Lisp Marker- vs. Separator-Proxy-IDs vergeben hat. - teileid = marker_id - if marker_id.isdigit(): - naechste_id = max(naechste_id, int(marker_id)) - else: - # Altbestand-Wrapper ohne Marker-ID (oder Marker-ID-Zuweisung in - # Lisp fehlgeschlagen) - unveraendertes bisheriges Verhalten. - naechste_id += 1 - teileid = f"{naechste_id:04d}" - - item = { - "nr": elem_nr, - "teileart": teileart, - "teileid": teileid, - "bezeichnung": f"{teileart} :{zaehler[teileart]}", - "planquadrat": planquadrat, - "position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}', - "boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}', - "insertpoint": "", - "k1": "", "k2": "", "k3": "", "k4": "", - "nachbarn": ", ".join(nachbarn), - "fehler": "", - "merkmale": merkmale, - } - items.append(item) - if sep_kette_key is not None: - sep_kette_by_key[sep_kette_key] = item - return elem_nr - - -def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None): - """Ordnet jeden in einer VF_n/GF_n-Kette eingebetteten Separator - (sepliste-Eintrag typ "SEP", siehe compute_sep_kette) dem BEREITS - vorhandenen Separator-Item zu und traegt es unter seinem sep_kette- - Schluessel in sep_kette_by_key ein - fuer die Nachbar-Aufloesung in - resolve_umlauf_und_sepliste_nachbarn. - - WICHTIG - erzeugt KEINE neuen Zeilen (frueher tat das - build_separator_kette_items): die in einer Kette verpackten Separatoren - liegen bereits als ECHTE Separator_SP-INSERTs in der Zeichnung - der - LISP-Export fuegt fuer jedes verpackte Symbol eine temporaere Kopie ein - (csv:sep-proxies-erzeugen, export.lsp), die den normalen freien-Separator- - Pfad in process_blocks durchlaeuft (mit echter vla-getboundingbox-Box, ID - und fest gesetzter ZUORDNUNG = Wrapper-ID ueber "zuordnung_fix"). Beide - Wege wuerden sonst dieselben physischen Separatoren DOPPELT zaehlen (26 - Proxy-INSERTs + 26 sepliste-Zeilen = 64 statt 38). Die sepliste liefert - hier nur noch die REIHENFOLGE (Vorgaenger/Nachfolger), nicht die Zeilen. - - Zuordnung sepliste-SEP -> vorhandenes Separator-Item: ueber die - Wrapper-ID (Item-Merkmal "Zuordnung" == sep_kette-Eintrag "strecke_id", - fixiert durch den LISP-Proxy) UND den naechstgelegenen INSERT-Punkt - (Item-Felder "_x"/"_y" vs. sepliste-Koordinate x/y - beide sind der rohe - Sub-INSERT-Einfuegepunkt aus derselben Bauphase, daher deckungsgleich; - verifiziert 26/26 <300mm). Reicht der Wrapper mehrere Separatoren, trennt - der INSERT-Punkt sie; bei nur einem ist die Wrapper-ID schon eindeutig. - - Ein sepliste-SEP ohne passendes Item (sollte nicht vorkommen, da der Proxy - jeden verpackten Separator einfuegt) wird uebersprungen (dbg-Hinweis). - """ - if sep_kette_by_key is None: - sep_kette_by_key = {} - # Vorhandene Separator-Items nach Wrapper-Zuordnung gruppieren (nur die mit - # fixierter Zuordnung = die Proxy-Kopien verpackter Ketten-Separatoren; - # frei platzierte Separatoren tragen keine sepliste und stoeren nicht). + # Kandidatenpool nur noch fuer den Koordinaten-Fallback (SEP ohne "id"). sep_items_by_zuordnung = {} for it in items: if it.get("teileart", "") != "ILS 2.0 Separator": @@ -711,96 +582,68 @@ def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None) sep_items_by_zuordnung.setdefault(zuordnung, []).append(it) if dbg: - dbg("=== map_separator_kette_items: Kandidatenpools je Zuordnung ===") - for zuordnung, kandidaten in sep_items_by_zuordnung.items(): - dbg(f" Zuordnung {zuordnung!r}: " - f"{[k.get('teileid', '') for k in kandidaten]}") - dbg("") - dbg("=== map_separator_kette_items: sepliste-SEP -> Separator-Item ===") - - # id(item) -> Liste der sep_kette-Keys, die genau dieses Item als - # naechsten Kandidaten gewaehlt haben - Diagnose der NICHT-exklusiven - # Zuordnung (jeder Key sucht unabhaengig im vollen Pool, siehe Docstring): - # waehlen zwei Keys denselben Kandidaten, bleibt ein anderer Kandidat im - # selben Pool ohne jeden sep_kette-Eintrag und damit ohne Nachbarn/TrackIds. - gewaehlt_von = {} - # Zuordnungen, fuer die es ueberhaupt sepliste-SEP-Schluessel gibt. Nur in - # DIESEN Pools ist ein nicht gewaehlter Kandidat auffaellig; frei auf einem - # Kreisel platzierte Separatoren tragen keine sepliste und bekommen ihre - # Nachbarn aus dem Kreisel-Umlauf (siehe resolve_umlauf_und_sepliste_nachbarn). - zuordnungen_mit_sepliste = set() + dbg("=== map_sepliste_kette_items: sepliste-Eintrag -> Item ===") + per_id = 0 + per_fallback = 0 + ohne_treffer = 0 for key, eintrag in sep_kette.items(): + marker_id = eintrag.get("id", "") + if marker_id: + item = by_teileid.get(marker_id) + if item is not None: + sep_kette_by_key[key] = item + per_id += 1 + if dbg: + dbg(f" {key} (typ={eintrag.get('typ')!r}) -> Item per ID " + f"{marker_id!r} (TeileId {item.get('teileid', '')!r})") + continue + if dbg: + dbg(f" {key}: id={marker_id!r} gesetzt, aber KEIN Item mit " + f"dieser TeileId gefunden") + + # Fallback nur fuer SEP ohne (aufloesbares) "id"-Feld (Altbestand). if eintrag.get("typ") != "SEP": + ohne_treffer += 1 continue - zuordnungen_mit_sepliste.add(eintrag["strecke_id"]) kandidaten = sep_items_by_zuordnung.get(eintrag["strecke_id"], []) if not kandidaten: + ohne_treffer += 1 if dbg: - dbg(f" sepliste-SEP {key}: kein Separator-Item mit Zuordnung " + dbg(f" {key}: kein Separator-Item mit Zuordnung " f"{eintrag['strecke_id']!r} gefunden - uebersprungen") continue - # Naechster INSERT-Punkt zur sepliste-Koordinate. sx, sy = eintrag.get("x", 0.0), eintrag.get("y", 0.0) - entfernungen = sorted( - ((math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy), it) - for it in kandidaten), - key=lambda p: p[0]) - best_d, best = entfernungen[0] + best = min(kandidaten, + key=lambda it: math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy)) sep_kette_by_key[key] = best - gewaehlt_von.setdefault(id(best), []).append(key) + per_fallback += 1 if dbg: - rangliste = ", ".join(f"{it.get('teileid', '')!r}={d:.0f}mm" - for d, it in entfernungen) - dbg(f" sepliste-SEP {key} (x={sx:.0f},y={sy:.0f}) -> Separator-Item " - f"TeileId {best.get('teileid', '')!r} (d={best_d:.0f}mm); " - f"Kandidaten sortiert: [{rangliste}]" - f"{dbg_highlight(best.get('teileid', ''))}") + dbg(f" {key} (x={sx:.0f},y={sy:.0f}) -> Naehe-Fallback auf " + f"Separator-Item TeileId {best.get('teileid', '')!r}") if dbg: - dbg("") - mehrfach = {iid: keys for iid, keys in gewaehlt_von.items() if len(keys) > 1} - if mehrfach: - dbg(" WARNUNG: folgende Separator-Items wurden von MEHREREN " - "sepliste-Keys unabhaengig als naechster Kandidat gewaehlt " - "(nicht-exklusive Zuordnung):") - for keys in mehrfach.values(): - teileid = sep_kette_by_key[keys[0]].get("teileid", "") - dbg(f" TeileId {teileid!r} gewaehlt von Keys: {keys}" - f"{dbg_highlight(teileid)}") - gewaehlte_ids = set(gewaehlt_von.keys()) - for zuordnung, kandidaten in sep_items_by_zuordnung.items(): - if zuordnung not in zuordnungen_mit_sepliste: - continue - for it in kandidaten: - if id(it) not in gewaehlte_ids: - teileid = it.get("teileid", "") - dbg(f" Separator-Item TeileId {teileid!r} (Zuordnung " - f"{zuordnung!r}) wurde von KEINEM sepliste-Key gewaehlt " - f"- bleibt ohne sep_kette-Eintrag, damit ohne Nachbarn/" - f"TrackIds{dbg_highlight(teileid)}") + dbg(f" Zuordnung: {per_id} per ID, {per_fallback} per Naehe-Fallback, " + f"{ohne_treffer} ohne Treffer") return sep_kette_by_key def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key, dbg=None): - """Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items/ - build_strecke_schleus_items/map_separator_kette_items zunaechst leer - gelassen (bzw. deren Item nur registriert) haben, weil ihr(e) Nachbar(n) - beim eigenen Bau/der Registrierung noch nicht als TeileId feststand(en) - (Weiche vs. Separator im Kreisel-Umlauf, oder eine sepliste-Zeile, deren - Nachfolger erst danach registriert wird). + """Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items + zunaechst leer gelassen bzw. map_sepliste_kette_items nur registriert hat, + weil ihr(e) Nachbar(n) beim eigenen Bau/der Registrierung noch nicht als + TeileId feststand(en) (Weiche vs. Separator im Kreisel-Umlauf, oder eine + sepliste-Zeile, deren Nachfolger erst danach registriert wird). - Muss NACH allen drei Aufrufen laufen (ihre TeileIds stehen erst dann - fest). items wird IN-PLACE veraendert (item["nachbarn"]). + Muss NACH build_kreisel_weiche_items UND map_sepliste_kette_items laufen + (deren TeileIds stehen erst dann fest). items wird IN-PLACE veraendert + (item["nachbarn"]). kreisel_umlauf = Rueckgabe von export_neighbors.compute_kreisel_umlauf. sep_kette = Rueckgabe von export_neighbors.compute_sep_kette. - sep_kette_by_key = dict sep_kette-Schluessel -> Item (befuellt von - build_strecke_schleus_items UND - map_separator_kette_items - DIESELBE dict fuer beide, - siehe process_blocks: ein sep_kette-Nachbar kann in der - jeweils anderen Funktion registriert worden sein). + sep_kette_by_key = Rueckgabe von map_sepliste_kette_items (dict + sep_kette-Schluessel -> Item). weiche_by_key = dict switches-"key" -> erzeugtes Item (von build_kreisel_weiche_items). """ @@ -837,8 +680,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, if item is None: if dbg: dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: " - f"KEIN Item in alle_schluessel gefunden - " - f"uebersprungen{dbg_highlight(punkt_key)}") + f"KEIN Item in alle_schluessel gefunden - uebersprungen") continue nachbarn = [] for nachbar_key in (vorgaenger, nachfolger): @@ -849,20 +691,17 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, nachbarn.append(nachbar_item.get("teileid", "")) elif dbg: dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: " - f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel " - f"aufloesbar{dbg_highlight(item.get('teileid', ''))}") + f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel aufloesbar") teileid = item.get("teileid", "") if nachbarn: item["nachbarn"] = ", ".join(nachbarn) if dbg: dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} " - f"(TeileId {teileid!r}): nachbarn={nachbarn}" - f"{dbg_highlight(teileid)}") + f"(TeileId {teileid!r}): nachbarn={nachbarn}") elif dbg: dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} " f"(TeileId {teileid!r}): KEINE Nachbarn aufgeloest " - f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})" - f"{dbg_highlight(teileid)}") + f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})") if dbg: dbg("") @@ -878,7 +717,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, if dbg: dbg(f" sepliste-Key {key!r} (typ={eintrag.get('typ')!r}): " f"KEIN Item in sep_kette_by_key - uebersprungen " - f"(siehe map_separator_kette_items){dbg_highlight(key)}") + f"(siehe map_sepliste_kette_items)") continue nachbarn = [] for nachbar_key in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")): @@ -894,8 +733,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, dbg(f" sepliste-Key {key!r} (TeileId {teileid!r}): " f"vorgaenger={eintrag.get('vorgaenger')!r} " f"nachfolger={eintrag.get('nachfolger')!r} -> " - f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}" - f"{dbg_highlight(teileid)}") + f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}") def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None): @@ -909,18 +747,18 @@ def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None): Baureihenfolge - siehe compute_sep_kette) statt sep_kette selbst: dessen dict-Reihenfolge ist nicht garantiert, die sepliste-Liste dagegen schon. AS/ES zaehlen HIER mit (anders als beim "Separatorlist"-Merkmal frueher) - - sie sind die Schleus-Enden der Kette (siehe build_strecke_schleus_items) - und bekommen dort eine eigene TeileId ("ILS Ausschleuselement"/"ILS - Einschleuselement"), analog zu den AS/ES-Enden, die beim Kreisel bereits - ueber compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie - fehlten hier die Kettenenden, obwohl sie beim Kreisel schon drin sind. + sie sind eigene Items ("ILS Ausschleuselement"/"ILS Einschleuselement", + direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe + process_blocks), analog zu den AS/ES-Enden, die beim Kreisel bereits ueber + compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie fehlten + hier die Kettenenden, obwohl sie beim Kreisel schon drin sind. - Der eigentliche Separator wurde bereits von map_separator_kette_items ueber - die sepliste-Koordinate (x/y) auf sein reales Separator-Item (TeileId) - aufgeloest, AS/ES von build_strecke_schleus_items - beide unter demselben - "#"-Schluessel in sep_kette_by_key eingetragen; hier - wird dieser Schluessel nur noch je Strecke eingesammelt. Muss darum NACH - map_separator_kette_items/build_strecke_schleus_items laufen. + Sowohl der Separator als auch AS/ES wurden bereits von + map_sepliste_kette_items ueber die sepliste-ID (Feld "id") auf ihr reales + Item (TeileId) aufgeloest, unter demselben "#"- + Schluessel in sep_kette_by_key eingetragen; hier wird dieser Schluessel + nur noch je Strecke eingesammelt. Muss darum NACH map_sepliste_kette_items + laufen. """ for item in items: if item.get("teileart", "") not in GF_VF_TEILEARTEN: @@ -960,8 +798,8 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke Schluessel oder Weichen-"key") auf die TeileId des jeweils zugehoerigen Items aufgeloest. Derselbe Lookup wie in resolve_umlauf_und_sepliste_nachbarn, daher muss diese Funktion NACH - map_separator_kette_items/build_kreisel_weiche_items/ - build_strecke_schleus_items laufen (deren TeileIds stehen erst dann fest). + map_sepliste_kette_items/build_kreisel_weiche_items laufen (deren + TeileIds stehen erst dann fest). """ freipunkt_by_id = {it.get("teileid", ""): it for it in items if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN} @@ -987,6 +825,67 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke kreisel_item["trackids"] = ", ".join(teileids) +def pruefe_ases_in_beiden_containern(items, dbg=None): + """Prueft, ob jede AS-/ES-Zeile (AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART) + in BEIDEN Containern auftaucht, zu denen sie gehoert: + - in den TrackIds ihrer EIGENEN Strecke (VF_n/GF_n, ueber das + ZUORDNUNG-Attribut des Markers - siehe Item-Feld "_zuordnung") + - in den TrackIds MINDESTENS eines Kreisels (der beruehrte Carrier, + geometrisch ueber compute_kreisel_umlauf/KREISEL_UMLAUF_TEILEARTEN + ermittelt - nur "ILS 2.0 Kreisel", Eckrad nimmt am Umlauf nicht teil, + siehe compute_kreisel_umlauf/KREISEL_SPLIT_TEILEART) + + Muss NACH write_separatorliste_merkmale UND + write_kreisel_separatorliste_merkmale laufen (beide TrackIds-Spalten + muessen bereits stehen). + + Ein AS/ES, das in einem der beiden Container fehlt, zeigt einen echten + Fehler in der Zuordnungskette an (z.B. ZUORDNUNG-Attribut leer/falsch, + Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am + Marker) - kein Normalfall. Traegt in diesem Fall eine Warnung in + item["warnung"] ein (erscheint in der CSV-Spalte "Warnungen") und gibt sie + zusaetzlich auf der Konsole/im Debuglog aus. + + Rueckgabe: Liste der betroffenen TeileIds (fuer Tests/Diagnose), leer bei + vollstaendiger Zuordnung. + """ + strecke_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in GF_VF_TEILEARTEN} + kreisel_trackid_sets = [ + {t.strip() for t in (it.get("trackids", "") or "").split(",") if t.strip()} + for it in items if it.get("teileart", "") == KREISEL_SPLIT_TEILEART + ] + + auffaellig = [] + for it in items: + if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART): + continue + teileid = it.get("teileid", "") + fehlend = [] + + strecke = strecke_by_id.get(it.get("_zuordnung", "")) + if strecke is None: + fehlend.append("eigene Strecke (ZUORDNUNG unaufloesbar)") + else: + strecke_ids = {t.strip() for t in (strecke.get("trackids", "") or "").split(",") if t.strip()} + if teileid not in strecke_ids: + fehlend.append("eigene Strecke") + + if not any(teileid in ids for ids in kreisel_trackid_sets): + fehlend.append("Kreisel") + + if fehlend: + auffaellig.append(teileid) + warnung = f"nicht in TrackIds von: {', '.join(fehlend)}" + bisherige = it.get("warnung", "") + it["warnung"] = f"{bisherige}; {warnung}" if bisherige else warnung + meldung = f"[export_csv] WARNUNG: {it.get('teileart', '')} {teileid}: {warnung}" + print(meldung) + if dbg: + dbg(f" {meldung}") + return auffaellig + + def process_blocks(blocks, lookup, dbg=None): # x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe # export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls @@ -1010,6 +909,8 @@ def process_blocks(blocks, lookup, dbg=None): eingang_count = 0 ausgang_count = 0 geruest_count = 0 + ausschleus_count = 0 + einschleus_count = 0 # Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade) cnt_wk = cnt_einzel = cnt_delta = cnt_doppel = cnt_stern = 0 @@ -1025,15 +926,54 @@ def process_blocks(blocks, lookup, dbg=None): # ZZ_ASES_MARKER (siehe cfg/export.cfg pattern_ases_marker sowie # csv:asmark-aktualisieren in Lisp/export.lsp): der persistente - # Marker, den Lisp an jedes AS-/ES-Kettenende setzt, damit dessen ID - # ueber Exportlaeufe hinweg stabil bleibt. Bekommt hier bewusst KEINE - # eigene CSV-Zeile - die sichtbare AS/ES-Zeile fuer denselben - # physischen Punkt liefert weiterhin build_strecke_schleus_items - # (reine Geometrie); die Marker-ID wird dort ueber das sepliste-Feld - # "id" wiederverwendet (siehe dort). Eine eigene Zeile hier waere - # doppelt gezaehlt und haette ohnehin keine sinnvolle Geometrie/ - # Boundingbox aus dem kleinen Marker-Rechteck selbst. + # Marker, den Lisp an jedes AS-/ES-Kettenende setzt. Er IST die + # AS-/ES-Zeile - TeileId, Position und Boundingbox kommen direkt vom + # Marker-Block selbst (echtes INSERT, echte vla-getboundingbox-Box), + # kein synthetischer Bau-Schritt mehr noetig (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). "Kreisel"-Merkmal und + # ID-basierte sep_kette-Zuordnung tragen unten zwei kurze Nachlaeufe + # in process_blocks nach, sobald alle Items klassifiziert sind (die + # eigene Kette bzw. der beruehrte Kreisel koennen an DIESER Stelle + # noch unklassifiziert sein). if matches_any(bname, BLOCKPATTERNS.get("pattern_ases_marker", [])): + attribs = block.get("attribs", {}) + typ = attribs.get("TYP", "") + teileart = {"AS": AUSSCHLEUS_TEILEART, "ES": EINSCHLEUS_TEILEART}.get(typ) + if teileart is None: + # Sollte nicht vorkommen (TYP wird von csv:asmark-aktualisieren + # immer gesetzt) - defensiv ueberspringen statt abzustuerzen. + print(f"[export_csv] WARNUNG: {bname} ohne gueltiges TYP-Attribut " + f"(TYP={typ!r}) - uebersprungen") + continue + elem_nr += 1 + if typ == "AS": + ausschleus_count += 1 + zaehler_n = ausschleus_count + else: + einschleus_count += 1 + zaehler_n = einschleus_count + items.append({ + "nr": elem_nr, + "teileart": teileart, + "teileid": attribs.get("ID") or format_sensor_id("ASES", elem_nr), + "bezeichnung": f"{teileart} :{zaehler_n}", + "planquadrat": planquadrat_column(block, planquadrat_cfg), + "merkmale": { + "Streckenende": "Anfang" if typ == "AS" else "Ende", + "Element": attribs.get("ELEMENT", ""), + "ARTINR": attribs.get("ARTINR", ""), + }, + # Private Felder (nicht in der CSV) fuer die beiden Nachlaeufe + # weiter unten in process_blocks: "_zuordnung" = ID des + # eigenen VF_n/GF_n-Wrappers (fuer das "Förderer"/"Strecke"- + # Merkmal), "_lfdnr" wird hier nicht gebraucht (die sep_kette- + # Zuordnung laeuft exakt ueber die TeileId, siehe + # map_sepliste_kette_items). + "_zuordnung": attribs.get("ZUORDNUNG", ""), + **bbox_columns(block), + }) continue # Omniflo Bogen oder Weiche @@ -1108,12 +1048,8 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": bezeichnung_oder_fallback(block, f"VarioFoerderer :{vf_count}"), "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_variofoerderer_merkmale(block), - # x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende- - # Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus) - "x": block.get("x", 0.0), - "y": block.get("y", 0.0), # Sepliste-XDATA (AS/SEP/ES-Reihenfolge) durchreichen - Quelle - # fuer compute_sep_kette/map_separator_kette_items (Ketten- + # fuer compute_sep_kette/map_sepliste_kette_items (Ketten- # interne Separatoren + AS/ES-Nachbarn). Ohne diese Weitergabe # liefe die gesamte sepliste-basierte Nachbarverkettung ins # Leere (das Feld stand im JSON, wurde aber nicht ans Item @@ -1134,10 +1070,6 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": bezeichnung_oder_fallback(block, f"Gefaellestrecke :{gf_count}"), "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_variofoerderer_merkmale(block), - # x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende- - # Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus) - "x": block.get("x", 0.0), - "y": block.get("y", 0.0), # Sepliste-XDATA durchreichen (siehe VF_N oben). "sepliste": block.get("sepliste"), **bbox_columns(block), @@ -1232,9 +1164,9 @@ def process_blocks(blocks, lookup, dbg=None): # ueberschreiben darf - siehe build_sensor_merkmale. "_zuordnung_fix": bool(block.get("zuordnung_fix")), # Roher INSERT-Punkt (nicht der bbox-Mittelpunkt) - dient - # map_separator_kette_items zum Zuordnen dieses (Proxy-) - # Separators zu seinem sepliste-Eintrag (Match ueber - # Wrapper-ID + naechster INSERT-Punkt, siehe dort). + # map_sepliste_kette_items als Koordinaten-FALLBACK, falls der + # sepliste-Eintrag (noch) kein "id"-Feld traegt (Altbestand; + # der Normalfall loest exakt per ID auf, siehe dort). "_x": block.get("x", 0.0), "_y": block.get("y", 0.0), **cols, }) @@ -1433,34 +1365,46 @@ def process_blocks(blocks, lookup, dbg=None): items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM, element_box_mm=ELEMENT_BOX_MM, dbg=dbg) - # Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren (per Zuordnung - # oben bereits einem Kreisel zugeordnet), die AS/ES-Enden andockender - # Ketten (aus sep_kette) UND die Kreisel-Kreisel-Weichen (touch_switches), - # sortiert nach Winkel um die Kreiselachse (siehe compute_kreisel_umlauf - - # DREHRICHTUNG-Attribut bestimmt den Umlaufsinn). + # Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren, BTMT-Stationen + # UND die AS/ES-Enden andockender Ketten (alle zusammen KREISEL_UMLAUF_ + # TEILEARTEN, einheitlich per eigener Bounding-Box zugeordnet - ein AS/ES + # bringt seine Box vom ZZ_ASES_MARKER direkt mit) sowie die Kreisel- + # Kreisel-Weichen (touch_switches), sortiert nach Winkel um die + # Kreiselachse (siehe compute_kreisel_umlauf - DREHRICHTUNG-Attribut + # bestimmt den Umlaufsinn). kreisel_umlauf = compute_kreisel_umlauf( - items, sep_kette, NEIGHBOR_TOLERANCE_MM, + items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM, touch_switches=touch_switches, dbg=dbg) # Neue Zeilen, deren "Nachbarn"-Spalte aus einem sep_kette-Schluessel - # aufgeloest werden muss, erzeugen: Kreisel-Kreisel-Weichen, dann Aus-/ - # Einschleuselemente (AS/ES). Ein sep_kette-Schluessel kann auf eine Zeile - # zeigen, die ERST DANACH registriert wird, darum erst NACH allen - # Registrierungen aufloesen (siehe unten) statt inline - frueherer Bug - # schrieb sonst den rohen Schluessel ("0026#3") statt der TeileId. + # aufgeloest werden muss, erzeugen: nur noch die Kreisel-Kreisel-Weichen + # (AS/ES sind bereits als eigene Items klassifiziert, siehe process_blocks + # oben). Ein sep_kette-Schluessel kann auf eine Zeile zeigen, die ERST + # DANACH registriert wird, darum erst NACH allen Registrierungen aufloesen + # (siehe unten) statt inline - frueherer Bug schrieb sonst den rohen + # Schluessel ("0026#3") statt der TeileId. elem_nr, weiche_by_key = build_kreisel_weiche_items( items, planquadrat_cfg, elem_nr, touch_switches, dbg=dbg) - sep_kette_by_key = {} - elem_nr = build_strecke_schleus_items( - items, planquadrat_cfg, elem_nr, sep_kette=sep_kette, - sep_kette_by_key=sep_kette_by_key, dbg=dbg) - # Die in einer Kette verpackten Separatoren erzeugen KEINE eigenen Zeilen - - # sie liegen bereits als (Proxy-)Separator_SP-Items vor (freier-Separator- - # Pfad oben). map_separator_kette_items ordnet ihnen nur ihren sepliste- - # Schluessel zu, damit die Nachbar-Reihenfolge (AS -> SEP -> ES) auf die - # vorhandenen Items angewandt werden kann (kein Doppel-Export). - map_separator_kette_items( - items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg) + + # "Förderer"/"Strecke"-Merkmal an jedem AS/ES nachtragen: die eigene Kette + # (ZUORDNUNG-Attribut des Markers, siehe "_zuordnung") kann bei der + # Klassifikation des Markers selbst noch unklassifiziert gewesen sein - + # der FeldNAME haengt von deren TeileArt ab (VF -> "Förderer", + # GF -> "Strecke", siehe _strecke_typ_feld), der WERT ist einfach die + # ZUORDNUNG selbst (keine Geometrie mehr noetig). + strecke_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in GF_VF_TEILEARTEN} + for it in items: + if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART): + continue + strecke = strecke_by_id.get(it.get("_zuordnung", "")) + if strecke is not None: + it["merkmale"][_strecke_typ_feld(strecke.get("teileart", ""))] = \ + strecke.get("teileid", "") + + # sep_kette-Schluessel (AS/SEP/ES) auf ihre bereits vorhandenen Items + # aufloesen - exakt per ID, siehe map_sepliste_kette_items. + sep_kette_by_key = map_sepliste_kette_items(items, sep_kette, dbg=dbg) # Spalte "TrackIds" je Strecke (Reihenfolge der SEP-TeileIds aus der # sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest). @@ -1477,6 +1421,12 @@ def process_blocks(blocks, lookup, dbg=None): write_kreisel_separatorliste_merkmale( items, kreisel_umlauf, sep_kette_by_key, weiche_by_key, dbg=dbg) + # Konsistenzpruefung: jedes AS/ES muss jetzt sowohl in den TrackIds seiner + # eigenen Strecke als auch in denen eines Kreisels auftauchen (siehe + # Docstring) - Abweichungen deuten auf einen echten Fehler in der + # Zuordnungskette hin und werden ueber "Warnungen" sichtbar gemacht. + pruefe_ases_in_beiden_containern(items, dbg=dbg) + # Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit # Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen # (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 38cbc95..826cc47 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -88,26 +88,21 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"} # pattern_ausgang in cfg/export.cfg) werden hier wie BTMT als frei platzierte # Umlauf-Punkte gefuehrt - so tauchen sie, wenn sie auf einer Kreiselbahn # liegen, in dessen "TrackIds"-Spalte auf (Nutzeranforderung: wie -# Separator/AS/ES behandeln). Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"- -# Praefix (siehe export_csv.py) - dieselben Strings muessen hier stehen. +# Separator/AS/ES behandeln). AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART (AS/ES- +# Kettenenden, siehe unten): seit sie als dauerhafter ZZ_ASES_MARKER real in +# der Zeichnung stehen (statt wie frueher nur ueber eine Kreisel<->Strecke- +# Kollisionsberechnung synthetisiert zu werden, siehe git-Historie +# compute_strecke_kreisel_schleus), reicht ihre EIGENE Bounding-Box fuer die +# ganz normale BBox-Ueberschneidung mit einem Kreisel - keine gesonderte +# Naeherungs-/Kollisionslogik mehr noetig (siehe compute_kreisel_umlauf, das +# vorher zusaetzlich sep_kette nach der naechstgelegenen Kreisel-Achse absuchte). +# Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-Praefix (siehe +# export_csv.py) - dieselben Strings muessen hier stehen. KREISEL_UMLAUF_TEILEARTEN = { "ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche", - "Eingang", "Ausgang", + "Eingang", "Ausgang", "ILS Ausschleuselement", "ILS Einschleuselement", } -# Temporaerer Diagnose-Filter fuers Debuglog (siehe [Nachbarschaft] debug_log -# in cfg/export.cfg): markiert Zeilen zu diesen TeileIds im .dbg-Log -# besonders, um die fehlende Nachbarn-/TrackIds-Zuordnung einzelner -# Separatoren (0025/0029/0031/0035) nachzuvollziehen. Nach Abschluss der -# Diagnose wieder auf einen leeren Set zuruecksetzen oder entfernen. -DEBUG_HIGHLIGHT_TEILEIDS = {"0025", "0029", "0031", "0035"} - - -def dbg_highlight(teileid): - """Suffix fuer eine Debuglog-Zeile, wenn teileid in - DEBUG_HIGHLIGHT_TEILEIDS steht - sonst leerer String.""" - return " <<<< ZIEL-ID" if str(teileid) in DEBUG_HIGHLIGHT_TEILEIDS else "" - # Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann- # station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0 # Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox. @@ -117,9 +112,10 @@ KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel" # (compute_kreisel_touch_switches / export_csv.py). WEICHE_TEILEART = "ILS Weiche" -# TeileArten der automatisch an den Strecken-Enden (Kollision Kreisel <-> -# Gefaellestrecke/Foerderer) erzeugten Ein-/Ausschleuselemente -# (compute_strecke_kreisel_schleus / export_csv.py). +# TeileArten der Ein-/Ausschleuselemente an den Strecken-Enden (AS/ES) einer +# Gefaellestrecke/eines Foerderers - siehe process_blocks (export_csv.py): +# direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (dessen Attribut +# TYP="AS"/"ES" waehlt die TeileArt), keine gesonderte Erzeugung mehr hier. AUSSCHLEUS_TEILEART = "ILS Ausschleuselement" EINSCHLEUS_TEILEART = "ILS Einschleuselement" @@ -1096,39 +1092,12 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, # --------------------------------------------------------------------------- -# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente +# Kreisel <-> Strecken-Enden: verfeinerte Kollisionsbox fuer compute_neighbor_ +# ids (die "Nachbarn"-Spalte einer GF/VF-Strecke soll nur an ihrem echten AS-/ +# ES-Ende auf einen Kreisel reagieren, nicht auf die gesamte, oft lange +# Wrapper-Bounding-Box). # --------------------------------------------------------------------------- -def _closest_pt_on_segment(a, b, p): - """Naechstgelegener Punkt auf der 2D-Strecke [a,b] zum Punkt p.""" - abx, aby = b[0] - a[0], b[1] - a[1] - denom = abx * abx + aby * aby - if denom <= 1e-9: - return (a[0], a[1]) - t = _clamp(((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / denom, 0.0, 1.0) - return (a[0] + abx * t, a[1] + aby * t) - - -def _strecke_ends(bbox, insert_xy): - """Die beiden Enden einer GF/VF-Strecke als (Anfang, Ende) in Welt-x/y. - - Enden = Mitten der beiden kurzen Stirnseiten der (achsparallelen) Bounding- - Box entlang ihrer laengeren Ausdehnung. "Anfang" ist das Ende, das dem - Block-Einfuegepunkt (insert_xy = Kettenanfang/startpunkt, siehe - ssg-block-wrap-welt) naeher liegt; "Ende" das andere. - """ - cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0) - dx, dy = bbox.get("dx", 0.0), bbox.get("dy", 0.0) - if dx >= dy: - e0, e1 = (cx - dx / 2.0, cy), (cx + dx / 2.0, cy) - else: - e0, e1 = (cx, cy - dy / 2.0), (cx, cy + dy / 2.0) - ix, iy = insert_xy - if math.hypot(e0[0] - ix, e0[1] - iy) <= math.hypot(e1[0] - ix, e1[1] - iy): - return e0, e1 - return e1, e0 - - def _asel_kos_xy(item): """(k1_xy, k2_xy) einer GF/VF-Strecke = Welt-(x,y) von AS (K1/KS_EIN) und ES (K2/KS_AUS), dekodiert aus item["k1"]/["k2"] (csv:trans-encode, siehe @@ -1177,188 +1146,6 @@ def _asel_collision_boxes(item, half_tol, box_dims): return boxes -def _distinct_kreisel_ids(nachbarn): - """Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R, - 0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe - kreisel_half_bboxes) wird entfernt, Reihenfolge bleibt erhalten.""" - ids = [] - for part in (nachbarn or "").split(","): - p = part.strip() - if not p: - continue - if p.endswith("-L") or p.endswith("-R"): - p = p[:-2] - if p not in ids: - ids.append(p) - return ids - - -def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, - element_box_mm=None, dbg=None): - """Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und - Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum - anschliessenden Kreisel. - - Grundlage sind NICHT eigene Abstandsschwellen, sondern die bereits per - Bounding-Box ermittelten Kreisel-Nachbarn der Strecke (Spalte "Nachbarn", - siehe compute_neighbor_ids). Jeder der beiden Streckenenden (Anfang = dem - Block-Einfuegepunkt naeheres Ende, siehe _strecke_ends) wird der unter - diesen Nachbarn naechstgelegene Kreisel zugeordnet: - - Am ANFANG entsteht ein Ausschleuselement (AUSSCHLEUS_TEILEART), - - am ENDE ein Einschleuselement (EINSCHLEUS_TEILEART). - Beruehrt eine Strecke nur EINEN Kreisel (z.B. ein Foerderer, der zum selben - Kreisel zurueckfuehrt), wird dieser beiden Enden zugeordnet. Beruehrt sie - KEINEN Kreisel, entstehen keine Elemente. - - Eine Strecke sollte genau zwei Kreisel-Bounding-Boxen beruehren (ein Anfang - + ein Ende). Beruehrt ihre BBox MEHR als zwei (mit exakten 3D-BBoxen sollte - das nicht vorkommen - hier wird in x/y ohne Hoehe geprueft), entsteht eine - Warnung: die beiden Enden bekommen weiterhin ihren jeweils naechsten - Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt. - - Position eines Elements = das AS-/ES-Koordinatensystem (K1/K2, siehe - csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der - Foerderachse, siehe element_box_mm) nach aussen versetzt: das - Ausschleuselement (AS/Anfang) VOR das AS-KOS (entgegen der Achse), das - Einschleuselement (ES/Ende) HINTER das ES-KOS (in Achsrichtung) - beide - also zum anschliessenden Kreisel hin, sodass das Element realistisch - zwischen Streckenende und Kreisel liegt. Fehlen K1/K2 (Altbestand ohne - AS/ES-Export), werden die Streckenenden aus der Wrapper-BBox genommen - (_strecke_ends). Die feste Box (element_box_mm) wird mit ihrer Laengsseite - senkrecht zur Foerderachse ausgerichtet (Welt-AABB, _oriented_box_aabb). - - element_box_mm = (laenge, breite, hoehe) in mm; None -> load_element_box_mm(). - - Rueckgabe: (result, warnings) - result = Liste von dicts: - {"teileart": AUSSCHLEUS_/EINSCHLEUS_TEILEART, - "x","y","z": Kollisionsposition (mm), - "dx","dy","dz": Welt-AABB der ausgerichteten Box (mm), - "kreisel_id": TeileId des anschliessenden Kreisels, - "strecke_id": TeileId der GF/VF, - "ende_label": "Anfang" | "Ende"} - warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2 - Kreisel-BBox-Beruehrungen. - """ - if element_box_mm is None: - element_box_mm = load_element_box_mm() - box_l, box_b, box_h = element_box_mm - kreisel = {} - for it in items: - if it.get("teileart", "") == KREISEL_SPLIT_TEILEART: - kreisel[it.get("teileid", "")] = _kreisel_capsule(it, durchmesser_mm) - - if dbg: - dbg("") - dbg("=== Strecken-Enden <-> Kreisel -> Ein-/Ausschleuselemente ===") - dbg(f"Echte Kreisel: {len(kreisel)}; Zuordnung ueber die BBox-Nachbarn " - f"der Strecke (naechster Kreisel je Ende), erwartet 2 Kreisel/Strecke") - - result = [] - warnings = {} - for it in items: - if it.get("teileart", "") not in GF_VF_TEILEARTEN: - continue - bbox = it.get("_bbox") - if not bbox: - continue - sid = it.get("teileid", "") - nb_ids = [k for k in _distinct_kreisel_ids(it.get("nachbarn", "")) if k in kreisel] - if not nb_ids: - if dbg: - dbg(f" Strecke {sid}: kein Kreisel-Nachbar -> keine Schleuselemente") - continue - - insert_xy = (it.get("x", bbox.get("cx", 0.0)) or 0.0, - it.get("y", bbox.get("cy", 0.0)) or 0.0) - # Streckenenden bevorzugt aus den AS/ES-Koordinatensystemen (K1=AS, - # K2=ES, siehe csv:vfgf-k-kos-strings) - das ist die tatsaechliche - # Position der Ein-/Ausschleuspunkte. "Anfang" bleibt das dem Block- - # Einfuegepunkt naehere Ende. Fehlen K1/K2 (Altbestand ohne AS/ES- - # Export), Fallback auf die Stirnseiten-Mitten der Wrapper-BBox. - k1_xy, k2_xy = _asel_kos_xy(it) - if k1_xy is not None and k2_xy is not None: - ix, iy = insert_xy - if math.hypot(k1_xy[0] - ix, k1_xy[1] - iy) <= math.hypot(k2_xy[0] - ix, k2_xy[1] - iy): - anfang_end, ende_end = k1_xy, k2_xy - else: - anfang_end, ende_end = k2_xy, k1_xy - else: - anfang_end, ende_end = _strecke_ends(bbox, insert_xy) - # Foerderachse (Einheitsvektor Anfang->Ende) fuer den Halbe-Box-Versatz - # der Schleuselemente. Ohne verwertbare Richtung (Enden fallen zusammen) - # bleibt der Versatz 0. - adx, ady = ende_end[0] - anfang_end[0], ende_end[1] - anfang_end[1] - alen = math.hypot(adx, ady) - axis_u = (adx / alen, ady / alen) if alen > 1e-9 else (0.0, 0.0) - # Versatz = halbe Box-Ausdehnung ENTLANG der Foerderachse. Die feste Box - # (element_box_mm) liegt mit ihrer Breite laengs der Achse (siehe - # _oriented_box_aabb), also ist die halbe Achs-Ausdehnung Breite/2. - halbe_box = box_b / 2.0 - # Ausrichtung der Schleuselement-Box: entlang der Foerderachse (nicht - # mehr der Kreiselachse) - das Element sitzt am AS/ES-KOS in - # Foerderrichtung, konsistent mit den AS/ES-Kollisionsboxen. - achs_rot = math.atan2(ady, adx) if alen > 1e-9 else 0.0 - scz = bbox.get("cz", 0.0) - - def _nearest(end_pt): - best = None - for kid in nb_ids: - cap = kreisel[kid] - p = _closest_pt_on_segment(cap["a"], cap["b"], end_pt) - d = math.hypot(p[0] - end_pt[0], p[1] - end_pt[1]) - if best is None or d < best[3]: - best = (kid, cap, p, d) - return best - - a_best = _nearest(anfang_end) - e_best = _nearest(ende_end) - - if len(nb_ids) > 2: - genutzt = {a_best[0], e_best[0]} - extra = [k for k in nb_ids if k not in genutzt] - warnings[sid] = ( - f"beruehrt {len(nb_ids)} Kreisel-BoundingBoxen " - f"(erwartet 2 = Anfang + Ende): {', '.join(nb_ids)}" - + (f"; ueberzaehlig/ignoriert: {', '.join(extra)}" if extra else "") - + " - mit exakten 3D-BoundingBoxen sollte das nicht auftreten") - if dbg: - dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}") - - # Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine - # HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der - # Streckenmitte, zum anschliessenden Kreisel hin) - damit das Element - # realistisch zwischen Streckenende und Kreisel liegt statt exakt auf - # dem KOS. Vorzeichen: Ausschleuselement (AS/Anfang) "vor das AS-KOS" - # = entgegen der Achse (-u), Einschleuselement (ES/Ende) "hinter das - # ES-KOS" = in Achsrichtung (+u). - for best, teileart, label, end_pt, vorzeichen in ( - (a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end, -1.0), - (e_best, EINSCHLEUS_TEILEART, "Ende", ende_end, 1.0)): - kid, cap, p, d = best - pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box, - end_pt[1] + vorzeichen * axis_u[1] * halbe_box) - dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h) - result.append({ - "teileart": teileart, - "x": pos[0], "y": pos[1], "z": (cap["z"] + scz) / 2.0, - "dx": dx, "dy": dy, "dz": dz, - "kreisel_id": kid, "strecke_id": sid, "ende_label": label, - }) - if dbg: - dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse " - f"{d:.0f}mm) -> {teileart} bei ({pos[0]:.0f}, {pos[1]:.0f}, " - f"{(cap['z'] + scz) / 2.0:.0f}) = KOS ({end_pt[0]:.0f}, {end_pt[1]:.0f}) " - f"{'+' if vorzeichen > 0 else '-'} halbe Box ({halbe_box:.0f}mm) entlang Achse, " - f"Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f})") - - if dbg: - dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; " - f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}") - - return result, warnings - - # --------------------------------------------------------------------------- # Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft # --------------------------------------------------------------------------- @@ -1376,15 +1163,19 @@ def compute_sep_kette(items, dbg=None): Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige) Schluessel-ID "#" - das ist die ID, mit der - build_strecke_schleus_items/map_separator_kette_items die spaeteren - globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger - sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits - in Baureihenfolge vorliegt) - kein Rateverfahren noetig. + map_sepliste_kette_items die spaeteren globalen TeileIds verknuepft + (siehe export_csv.py). Vorgaenger/Nachfolger sind schlicht der vorherige/ + naechste Eintrag in der Sepliste (die bereits in Baureihenfolge vorliegt) + - kein Rateverfahren noetig. - Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger", + Rueckgabe: dict {key: {"typ", "x", "y", "id", "strecke_id", "vorgaenger", "nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt - compute_kreisel_umlauf). + compute_kreisel_umlauf). "id" ist die TeileId des Bauteils, das diesen + Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften + ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere + Separator_SP-Kopie - siehe csv:sepliste-ids-setzen, Lisp/export.lsp) - + leerer String, wenn (noch) keine ID zugewiesen wurde (Altbestand). """ result = {} for item in items: @@ -1401,6 +1192,7 @@ def compute_sep_kette(items, dbg=None): "typ": entry.get("typ", ""), "x": entry.get("x", 0.0), "y": entry.get("y", 0.0), + "id": str(entry.get("id", "")).strip(), "strecke_id": sid, "vorgaenger": keys[i - 1] if i > 0 else None, "nachfolger": keys[i + 1] if i < len(keys) - 1 else None, @@ -1411,16 +1203,18 @@ def compute_sep_kette(items, dbg=None): return result -def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, +def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0, touch_switches=None, dbg=None): """Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort - liegenden Punkte - frei platzierte Separatoren UND BTMT-Beladung/ - -Entladung (KREISEL_UMLAUF_TEILEARTEN, per BBox-Ueberschneidung - wie compute_sensor_zuordnung einer Kreiselhaelfte zugeordnet), die AS/ES- - Enden aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe - wie compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel- - Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine - geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut). + liegenden Punkte - frei platzierte Separatoren, BTMT-Beladung/-Entladung + UND die AS/ES-Enden aller andockenden VF_n/GF_n-Ketten (alle zusammen + KREISEL_UMLAUF_TEILEARTEN, einheitlich per BBox-Ueberschneidung einer + Kreiselhaelfte/eines Kreisels zugeordnet - wie compute_sensor_zuordnung; + ein AS/ES bringt seine eigene Bounding-Box vom dauerhaften ZZ_ASES_MARKER + mit, seit es als reales Item existiert - keine gesonderte Naeherungslogik + mehr noetig) UND die Kreisel-Kreisel-Weichen (touch_switches, siehe + compute_kreisel_touch_switches) - in eine geschlossene Umlauf-Reihenfolge + (zirkulaer, siehe DREHRICHTUNG-Attribut). Eine Kreisel-Kreisel-Weiche liegt (anders als ein Separator oder ein AS/ES-Ende) auf GENAU EINEM Punkt, gehoert aber zu ZWEI Kreiseln - man @@ -1441,17 +1235,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, "TrackIds"-Spalte (siehe export_csv.py write_kreisel_separatorliste_merkmale). items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_ - zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES- - Punkte: x/y + strecke_id). touch_switches = Rueckgabe von - compute_kreisel_touch_switches (None/leer = wie bisher ohne Weichen im - Umlauf). + zuordnung. touch_switches = Rueckgabe von compute_kreisel_touch_switches + (None/leer = wie bisher ohne Weichen im Umlauf). Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key, nachfolger_key), ...]} in Umlaufreihenfolge, beginnend bei Winkel 0 Grad - - punkt_key ist die TeileId eines freien Punkts (Separator oder BTMT- - Beladung/-Entladung, siehe KREISEL_UMLAUF_TEILEARTEN), ein - sep_kette-Schluessel (AS/ES-Enden) oder ein touch_switches-"key" (Kreisel- - Kreisel-Weiche). Kreisel ohne zugeordnete Punkte liefern eine leere Liste. + punkt_key ist die TeileId eines freien Punkts (Separator, BTMT-Beladung/ + -Entladung ODER AS/ES-Ende, siehe KREISEL_UMLAUF_TEILEARTEN) oder ein + touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel ohne zugeordnete + Punkte liefern eine leere Liste. """ half_tol = tolerance_mm / 2.0 kreisel = {} @@ -1498,8 +1290,7 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, if not bbox: if dbg: dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}): " - f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden" - f"{dbg_highlight(teileid)}") + f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden") continue bounds = _bounds(bbox, half_tol) matched_tid = None @@ -1513,28 +1304,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, if matched_tid is not None: dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) " f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) " - f"-> Kreisel {matched_tid}{dbg_highlight(teileid)}") + f"-> Kreisel {matched_tid}") else: dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) " f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) " - f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})" - f"{dbg_highlight(teileid)}") - - # AS/ES-Enden jeder Kette mit sepliste: naechstgelegener Kreisel (wie - # compute_strecke_kreisel_schleus._nearest), aber direkt aus sep_kette - # statt ueber die grobe BBox-Nachbarschaft der Strecke. - for key, eintrag in sep_kette.items(): - if eintrag["typ"] not in ("AS", "ES"): - continue - p = (eintrag["x"], eintrag["y"]) - best_tid, best_d = None, None - for tid, (cap, _dr) in kreisel.items(): - q = _closest_pt_on_segment(cap["a"], cap["b"], p) - d = math.hypot(q[0] - p[0], q[1] - p[1]) - if best_d is None or d < best_d: - best_tid, best_d = tid, d - if best_tid is not None: - punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"])) + f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})") # Kreisel-Kreisel-Weichen: die beiden beruehrenden Kreisel-IDs stehen # bereits in sw["nachbarn"] (siehe compute_kreisel_touch_switches) - kein @@ -1623,6 +1397,6 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, f"{' -> '.join(p[0] for p in sortiert)}") for punkt_key, vorgaenger, nachfolger in umlauf: dbg(f" Punkt {punkt_key!r}: vorgaenger={vorgaenger!r} " - f"nachfolger={nachfolger!r}{dbg_highlight(punkt_key)}") + f"nachfolger={nachfolger!r}") return result diff --git a/tests/test_export_kreisel_umlauf.py b/tests/test_export_kreisel_umlauf.py index f3e2b39..50e8ab6 100644 --- a/tests/test_export_kreisel_umlauf.py +++ b/tests/test_export_kreisel_umlauf.py @@ -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") diff --git a/tests/test_export_separatorliste.py b/tests/test_export_separatorliste.py index c6faf50..ad3a82a 100644 --- a/tests/test_export_separatorliste.py +++ b/tests/test_export_separatorliste.py @@ -1,15 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -test_export_separatorliste.py - Merkmal "Separatorlist" einer Strecke. +test_export_separatorliste.py - "TrackIds"-Spalte einer Strecke (VF_n/GF_n). Laeuft komplett ohne BricsCAD (reine Python-Logik, siehe lib/export_csv.py). -Eine Strecke (VF_n/GF_n) traegt in ihrer "sepliste"-XDATA nur x/y je AS-/SEP-/ -ES-Eintrag (Rohkoordinaten aus der Zeichnung, kein TeileId). write_separator- -liste_merkmale() soll daraus, ueber die bereits von map_separator_kette_items -per x/y-Abgleich aufgeloesten sep_kette_by_key-Eintraege, das Merkmal -"Separatorlist" der Strecke fuellen: die TeileIds ihrer SEP-Eintraege (NICHT -AS/ES) in Baureihenfolge, kommagetrennt (z.B. "0052, 0053, 0054"). +Eine Strecke (VF_n/GF_n) traegt in ihrer "sepliste"-XDATA je AS-/SEP-/ES- +Eintrag x/y sowie (seit csv:asmark-ids-setzen/csv:sepliste-ids-setzen, +Lisp/export.lsp) ein Feld "id" - die TeileId des Bauteils, das diesen +Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften +ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere +Separator_SP-Kopie). write_separatorliste_merkmale() soll daraus, ueber die +bereits von map_sepliste_kette_items per exaktem ID-Abgleich aufgeloesten +sep_kette_by_key-Eintraege, die Spalte "TrackIds" der Strecke fuellen: die +TeileIds von AS-Ende, SEP-Eintraegen und ES-Ende in Baureihenfolge, +kommagetrennt (z.B. "0056, 0052, 0053, 0057"). """ import os @@ -17,11 +21,9 @@ import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) -from export_neighbors import compute_sep_kette, compute_kreisel_umlauf # noqa: E402 +from export_neighbors import compute_sep_kette # noqa: E402 from export_csv import ( # noqa: E402 - build_strecke_schleus_items, - map_separator_kette_items, - max_numeric_teileid, + map_sepliste_kette_items, write_separatorliste_merkmale, ) @@ -40,13 +42,27 @@ def _kreisel(teileid, x0, y0, rotation, abstand=1000.0): } +def _ases(teileart, teileid, x, y, zuordnung): + """AS-/ES-Item, wie es process_blocks direkt aus einem ZZ_ASES_MARKER + klassifiziert - siehe test_export_kreisel_umlauf.py fuer dasselbe + Fixture-Muster.""" + return { + "teileart": teileart, + "teileid": teileid, + "_zuordnung": zuordnung, + "merkmale": {}, + "nachbarn": "", + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0}, + } + + def _strecke_mit_zwei_separatoren(): """Ein Kreisel + eine Gefaellestrecke mit sepliste AS(1) -> SEP(2) -> - SEP(3) -> ES(4), PLUS die zwei bereits vorhandenen Proxy-Separator-Items - (wie sie der freie-Separator-Pfad in process_blocks erzeugt: TeileId, - Merkmal "Zuordnung" = Wrapper-ID, INSERT-Punkt _x/_y an der jeweiligen - sepliste-SEP-Koordinate) - siehe TestSeplisteNachbarnAufgeloest in - test_export_kreisel_umlauf.py fuer dasselbe Fixture-Muster.""" + SEP(3) -> ES(4), PLUS die vier bereits vorhandenen Items, die diese + Eintraege vertreten (zwei Proxy-Separator-Items wie sie der freie- + Separator-Pfad in process_blocks erzeugt, sowie das AS-/ES-Item wie aus + dem ZZ_ASES_MARKER klassifiziert) - jeder sepliste-Eintrag traegt bereits + die TeileId seines Items als Feld "id".""" k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) strecke = { "teileart": "ILS 2.0 Gefaellestrecke", @@ -57,10 +73,10 @@ def _strecke_mit_zwei_separatoren(): "k1": "", "k2": "", "merkmale": {"Anzahl_Separator": "2", "Anzahl_Scanner": "0"}, "sepliste": [ - {"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0}, - {"typ": "SEP", "lfdnr": 2, "x": 4000.0, "y": 0.0}, - {"typ": "SEP", "lfdnr": 3, "x": 6000.0, "y": 0.0}, - {"typ": "ES", "lfdnr": 4, "x": 8500.0, "y": 0.0}, + {"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0, "id": "0056"}, + {"typ": "SEP", "lfdnr": 2, "x": 4000.0, "y": 0.0, "id": "0052"}, + {"typ": "SEP", "lfdnr": 3, "x": 6000.0, "y": 0.0, "id": "0053"}, + {"typ": "ES", "lfdnr": 4, "x": 8500.0, "y": 0.0, "id": "0057"}, ], } proxy_sep_1 = { @@ -79,7 +95,9 @@ def _strecke_mit_zwei_separatoren(): "_bbox": {"cx": 6000.0, "cy": 0.0, "cz": 0.0, "dx": 150.0, "dy": 210.0}, "nachbarn": "", } - return [k, strecke, proxy_sep_1, proxy_sep_2] + as_item = _ases("ILS Ausschleuselement", "0056", 1500.0, 0.0, "0010") + es_item = _ases("ILS Einschleuselement", "0057", 8500.0, 0.0, "0010") + return [k, strecke, proxy_sep_1, proxy_sep_2, as_item, es_item] class TestSeparatorlisteMerkmal: @@ -87,53 +105,36 @@ class TestSeparatorlisteMerkmal: def test_separatorlist_in_baureihenfolge(self): items = _strecke_mit_zwei_separatoren() sep_kette = compute_sep_kette(items) - compute_kreisel_umlauf( # AS/ES muessen als Punkte existieren, sonst - items, sep_kette, tolerance_mm=50.0, # unveraendert fuer diesen Test - 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) write_separatorliste_merkmale(items, sep_kette_by_key) - # write_separatorliste_merkmale fuellt seit dem Umbau das Top-Level-Feld + # write_separatorliste_merkmale fuellt das Top-Level-Feld # item["trackids"] (CSV-Spalte "TrackIds") mit AS-Ende + Separatoren + - # ES-Ende in Baureihenfolge - NICHT mehr das Merkmal "Separatorlist" mit - # nur den SEP-Eintraegen. AS/ES bekommen ueber build_strecke_schleus_items - # eigene TeileIds (hier 0054/0055), die SEP-Proxies sind 0052/0053. + # ES-Ende in Baureihenfolge. AS/ES tragen ihre eigene TeileId (0056/ + # 0057), die SEP-Proxies 0052/0053 - alle exakt per sepliste-Feld + # "id" aufgeloest, kein Minten mehr noetig. strecke = next(it for it in items if it.get("teileid") == "0010") - assert strecke.get("trackids") == "0054, 0052, 0053, 0055", ( + assert strecke.get("trackids") == "0056, 0052, 0053, 0057", ( f'trackids = {strecke.get("trackids")!r}, ' - f'erwartet "0054, 0052, 0053, 0055" (AS, SEP, SEP, ES in Baureihenfolge)') + f'erwartet "0056, 0052, 0053, 0057" (AS, SEP, SEP, ES in Baureihenfolge)') def test_vertauschte_proxy_reihenfolge_in_items_aendert_nichts(self): """Die Reihenfolge in "items" (bzw. in welcher Reihenfolge die Proxy- - Separatoren im Export auftauchen) darf die Separatorlist-Reihenfolge - nicht beeinflussen - massgeblich ist allein die lfdnr aus der - sepliste, nicht die Item-Liste.""" + Separatoren im Export auftauchen) darf die TrackIds-Reihenfolge nicht + beeinflussen - massgeblich ist allein die lfdnr aus der sepliste, + nicht die Item-Liste.""" items = _strecke_mit_zwei_separatoren() - k, strecke, proxy_1, proxy_2 = items - items = [k, strecke, proxy_2, proxy_1] # vertauscht + k, strecke, proxy_1, proxy_2, as_item, es_item = items + items = [k, strecke, proxy_2, proxy_1, es_item, as_item] # vertauscht sep_kette = compute_sep_kette(items) - compute_kreisel_umlauf( - items, sep_kette, 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) write_separatorliste_merkmale(items, sep_kette_by_key) strecke = next(it for it in items if it.get("teileid") == "0010") - assert strecke.get("trackids") == "0054, 0052, 0053, 0055" + assert strecke.get("trackids") == "0056, 0052, 0053, 0057" def test_strecke_ohne_sepliste_bleibt_unveraendert(self): """Altbestand ohne "sepliste"-XDATA darf kein leeres/kaputtes @@ -148,57 +149,32 @@ class TestSeparatorlisteMerkmal: assert "Separatorlist" not in strecke["merkmale"] -class TestMarkerIdWiederverwendung: - """build_strecke_schleus_items darf die ID eines dauerhaften - ZZ_ASES_MARKER (siehe csv:asmark-aktualisieren, Lisp/export.lsp; im - sepliste-Eintrag als Feld "id" gespiegelt) nicht ignorieren und - stattdessen eine neue erfinden - genau das erzeugte die vom Nutzer - gemeldete TeileId-Luecke (Marker-ID blieb ungenutzt, gleichzeitig - entstand fuer denselben Punkt eine zusaetzliche, hoehere ID).""" +class TestMapSeplisteKetteItemsFallback: + """map_sepliste_kette_items faellt fuer SEP-Eintraege OHNE "id"-Feld + (Altbestand-Export vor der Marker-Funktion) weiterhin auf den + Naehe-Abgleich zurueck - AS/ES ohne "id" bleiben dagegen unaufgeloest + (fuer sie existiert seit dem Wegfall der Kreisel<->Strecke-Kollisions- + erzeugung kein Item mehr, das sie vertreten koennte).""" - def test_marker_id_wiederverwendet_statt_neu_gemintet(self): + def test_sep_ohne_id_faellt_auf_naehe_zurueck(self): items = _strecke_mit_zwei_separatoren() strecke = next(it for it in items if it.get("teileid") == "0010") - # AS = sepliste[0] (lfdnr=1), ES = sepliste[-1] (lfdnr=4) - siehe - # ssg-sepliste-sammeln (Lisp/ssg_ks_insert.lsp): AS immer zuerst, - # ES immer zuletzt. Bewusst NIEDRIGER als die Proxy-Separator-IDs - # 0052/0053 gewaehlt - genau wie im real beobachteten HundM05-Export - # (Marker 0024-0033, Separator-Proxies 0034-0045). - strecke["sepliste"][0]["id"] = "0024" - strecke["sepliste"][-1]["id"] = "0033" + for entry in strecke["sepliste"]: + if entry["typ"] == "SEP": + entry["id"] = "" 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) + sep_kette_by_key = map_sepliste_kette_items(items, sep_kette) - as_item = sep_kette_by_key["0010#1"] - es_item = sep_kette_by_key["0010#4"] - assert as_item["teileid"] == "0024", ( - f'AS-Zeile hat teileid={as_item["teileid"]!r}, erwartet die ' - f'wiederverwendete Marker-ID "0024"') - assert es_item["teileid"] == "0033", ( - f'ES-Zeile hat teileid={es_item["teileid"]!r}, erwartet die ' - f'wiederverwendete Marker-ID "0033"') + assert sep_kette_by_key["0010#2"]["teileid"] == "0052" + assert sep_kette_by_key["0010#3"]["teileid"] == "0053" - # Kein spaeterer Mint in DIESEM Lauf darf eine wiederverwendete - # Marker-ID erneut vergeben - der laufende Zaehler muss mindestens - # auf die hoechste wiederverwendete ID hochgezogen worden sein. - assert max_numeric_teileid(items) >= 33 - - def test_strecke_ohne_marker_id_mintet_wie_bisher(self): - """Fehlt das "id"-Feld (Altbestand-Wrapper vor der Marker-Funktion, - oder Marker-Zuweisung in Lisp fehlgeschlagen), bleibt das Mint- - Verhalten unveraendert - dieselben TeileIds wie vor diesem Fix.""" + def test_as_ohne_id_bleibt_unaufgeloest(self): items = _strecke_mit_zwei_separatoren() - 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) + strecke = next(it for it in items if it.get("teileid") == "0010") + strecke["sepliste"][0]["id"] = "" - as_item = sep_kette_by_key["0010#1"] - es_item = sep_kette_by_key["0010#4"] - assert as_item["teileid"] == "0054" - assert es_item["teileid"] == "0055" + sep_kette = compute_sep_kette(items) + sep_kette_by_key = map_sepliste_kette_items(items, sep_kette) + + assert "0010#1" not in sep_kette_by_key