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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-22 14:48:45 +02:00
co-authored by Claude Sonnet 5
parent 719ffc2385
commit ab3958e0e4
5 changed files with 543 additions and 793 deletions
+248 -298
View File
@@ -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
"<strecke_id>#<lfdnr>" -> 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
"<strecke_id>#<lfdnr>" 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
"<strecke_id>#<lfdnr>"-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 "<strecke_id>#<lfdnr>"-
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