#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ test_export_kreisel_umlauf.py - Kreisel-Kreisel-Weiche im Umlauf. Laeuft komplett ohne BricsCAD (reine Python-Geometrie, siehe lib/export_neighbors.py). Regressionstest fuer einen realen Mubea-Befund: zwei sich beruehrende Kreisel erzeugen eine "ILS Weiche" am Beruehrpunkt (compute_kreisel_touch_switches). Diese Weiche ist der Verzweigungspunkt zwischen beiden Kreiseln - ein Separator direkt davor muss sie als Nachbarn bekommen, nicht (wie vor dem Fix) einen Punkt vom jeweils ANDEREN Kreisel-Ende, weil die Weiche in compute_kreisel_umlauf schlicht fehlte. """ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) from export_neighbors import ( # noqa: E402 compute_kreisel_touch_switches, compute_kreisel_umlauf, ) from export_csv import ( # noqa: E402 build_strecke_schleus_items, map_separator_kette_items, resolve_umlauf_und_sepliste_nachbarn, ) DURCHMESSER_MM = 800.0 RADIUS_MM = DURCHMESSER_MM / 2.0 def _kreisel(teileid, x0, y0, rotation, abstand=1000.0): return { "teileart": "ILS 2.0 Kreisel", "teileid": teileid, "x": x0, "y": y0, "z": 0.0, "rotation": rotation, "attribs": {"ABSTAND": str(abstand), "DREHRICHTUNG": "UZS"}, "_bbox": {"cx": x0, "cy": y0, "cz": 0.0, "dx": abstand + DURCHMESSER_MM, "dy": DURCHMESSER_MM}, } def _separator(teileid, x, y): return { "teileart": "ILS 2.0 Separator", "teileid": teileid, "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 150.0, "dy": 100.0}, } def _zwei_beruehrende_kreisel(): """Zwei Kreisel, deren AN8-Enden sich exakt beruehren (End-an-End, wie im Mubea-Fall: 'Kreis/Kreis'-Beruehrung an der jeweiligen AN8-Kappe). Kreisel 1 liegt entlang +X ab x=0 (AN8-Mitte bei radius=400, SP8-Mitte bei radius+abstand=1400). Kreisel 2 liegt entlang -X ab x=800 (AN8-Mitte bei 800-400=400) - beide AN8-Kappen beruehren sich bei x=400 (Kreis/Kreis). """ k1 = _kreisel("0001", 0.0, 0.0, 0.0) k2 = _kreisel("0002", 800.0, 0.0, 180.0) return [k1, k2] class TestKreiselKreiselWeicheImUmlauf: def test_touch_switch_erkannt(self): items = _zwei_beruehrende_kreisel() switches = compute_kreisel_touch_switches(items, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM) assert len(switches) == 1 sw = switches[0] assert sorted(sw["nachbarn"]) == ["0001", "0002"] assert sw["key"] == "WEICHE#0001#0002" def test_weiche_in_beiden_umlaeufen(self): """Die Weiche muss als Punkt in BEIDEN Kreisel-Umlaeufen auftauchen - sie ist der gemeinsame Verzweigungspunkt (K1->K2 und K2->K1).""" 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, durchmesser_mm=DURCHMESSER_MM, touch_switches=switches) weiche_key = switches[0]["key"] for tid in ("0001", "0002"): keys = [p[0] for p in umlauf[tid]] assert weiche_key in keys, f"Weiche fehlt im Umlauf von Kreisel {tid}" def test_separator_bekommt_weiche_als_nachbar_nicht_anderen_kreisel(self): """Realer Mubea-Befund: ein Separator, der unmittelbar VOR der Kreisel-Kreisel-Weiche liegt (auf der AN8-Seite von Kreisel 1), muss die Weiche als Umlauf-Nachbarn bekommen - nicht einen Punkt, der eigentlich zu Kreisel 2 gehoert (das war der Bug: die Weiche fehlte in punkte_je_kreisel, wodurch beide Separatoren am Beruehrpunkt auf denselben (falschen) Nachbarn verwiesen).""" items = _zwei_beruehrende_kreisel() # Separator dicht vor der Beruehrung (x=390), gehoert zu Kreisel 1. sep = _separator("0036", 390.0, 0.0) items.append(sep) switches = compute_kreisel_touch_switches(items, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM) assert len(switches) == 1 weiche_key = switches[0]["key"] umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM, touch_switches=switches) punkte_k1 = {p[0]: (p[1], p[2]) for p in umlauf["0001"]} assert "0036" in punkte_k1, "Separator 0036 fehlt im Umlauf von Kreisel 1" vorgaenger, nachfolger = punkte_k1["0036"] assert weiche_key in (vorgaenger, nachfolger), ( f"Separator 0036 sollte die Kreisel-Kreisel-Weiche ({weiche_key}) " f"als Umlauf-Nachbarn haben, hat aber vorgaenger={vorgaenger!r}, " f"nachfolger={nachfolger!r}") class TestKreiselBoundsRotationsUnabhaengig: """Regressionstest fuer einen zweiten realen Mubea-Befund: ein um 90 Grad gedrehter, sehr langer Kreisel (Achse entlang Welt-Y statt Welt-X) bekam in compute_kreisel_umlauf eine falsch orientierte Matching-Box (schmal in Y, ueberbreit in X statt umgekehrt) - kreisel_bounds baute die Box bisher IMMER mit dx=durchmesser+achs_len/dy=durchmesser, unabhaengig von der tatsaechlichen Drehung. Echte anliegende Separatoren (weit entfernt in X, aber auf der Kreiselachse in Y) fielen dadurch durch, waehrend entfernte, zufaellig in der ueberbreiten X-Box liegende Separatoren faelschlich als Kreisel-Nachbarn erkannt wurden (Mubea Kreisel 0002, 20m lang, Drehung 90).""" def _langer_kreisel_90grad(self): # Kreisel 0002 aus dem Mubea-Datensatz: x=0, y=4529.5, Drehung 90, # ABSTAND=20020mm -> Achse verlaeuft entlang Welt-Y von y~4930 bis # y~24950, nicht entlang Welt-X. return _kreisel("0002", 0.0, 4529.5, 90.0, abstand=20020.0) def test_separator_auf_der_gedrehten_achse_wird_erkannt(self): """Ein Separator, der WIRKLICH auf der (gedrehten) Kreiselachse liegt (x nahe 0, y zwischen AN8- und SP8-Mitte), muss erkannt werden.""" 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, durchmesser_mm=DURCHMESSER_MM) keys = [p[0] for p in umlauf["0002"]] assert "9001" in keys, "Separator auf der gedrehten Kreiselachse wurde nicht erkannt" def test_separator_weit_neben_der_achse_wird_nicht_erkannt(self): """Ein Separator, der (wie im Mubea-Fall Separator 0065/0066) 958mm seitlich der schmalen Kreiselbreite (durchmesser=800mm, Radius 400mm) liegt, gehoert NICHT zum Kreisel - er darf nicht ueber eine faelschlich breite X-Box eingefangen werden.""" 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, durchmesser_mm=DURCHMESSER_MM) keys = [p[0] for p in umlauf["0002"]] assert "0065" not in keys, ( "Separator 958mm neben der Kreiselachse wurde faelschlich als " "Kreisel-Nachbar erkannt (rotationsabhaengiger BBox-Bug)") 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, 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.""" k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) strecke = { "teileart": "ILS 2.0 Gefaellestrecke", "teileid": "0010", "x": 5000.0, "y": 0.0, "z": 0.0, "nachbarn": "0001", "_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}, ], } # Proxy-Separator-Item (bereits vorhanden), Zuordnung = Wrapper-ID 0010, # INSERT-Punkt an der sepliste-SEP-Koordinate (5000, 0). proxy_sep = { "teileart": "ILS 2.0 Separator", "teileid": "0055", "merkmale": {"Zuordnung": "0010"}, "_x": 5000.0, "_y": 0.0, "_bbox": {"cx": 5000.0, "cy": 0.0, "cz": 0.0, "dx": 150.0, "dy": 210.0}, "nachbarn": "", } return [k, strecke, proxy_sep] def test_keine_rohen_sep_kette_schluessel_in_nachbarn(self): from export_neighbors import compute_sep_kette items = self._sepliste_kette() sep_kette = compute_sep_kette(items) assert set(sep_kette) == {"0010#1", "0010#2", "0010#3"} touch_switches = [] weiche_by_key = {} kreisel_umlauf = compute_kreisel_umlauf( items, sep_kette, 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) 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. sepliste_items = [it for it in items if it.get("teileart") in ("ILS 2.0 Separator", "ILS Ausschleuselement", "ILS Einschleuselement")] assert len(sepliste_items) == 3, "AS/SEP/ES nicht alle vorhanden" for it in sepliste_items: for nachbar in it["nachbarn"].split(","): nachbar = nachbar.strip() if not nachbar: continue assert "#" not in nachbar, ( f'{it["teileart"]} {it["teileid"]}: "Nachbarn" enthaelt einen ' f'unaufgeloesten sep_kette-Schluessel ({nachbar!r}) statt ' f'einer TeileId') def test_sep_zeile_hat_as_und_es_als_nachbarn(self): """Der Proxy-Separator (SEP-Position) muss nach der Aufloesung sowohl die TeileId der AS- als auch der ES-Zeile als Nachbarn tragen.""" from export_neighbors import compute_sep_kette 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, 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) resolve_umlauf_und_sepliste_nachbarn( items, kreisel_umlauf, sep_kette, sep_kette_by_key, {}) as_item = sep_kette_by_key["0010#1"] sep_item = sep_kette_by_key["0010#2"] es_item = sep_kette_by_key["0010#3"] # Der SEP-Schluessel muss auf das vorhandene Proxy-Separator-Item zeigen. assert sep_item["teileid"] == "0055", "sepliste-SEP nicht auf Proxy-Separator gemappt" sep_nachbarn = {n.strip() for n in sep_item["nachbarn"].split(",") if n.strip()} assert sep_nachbarn == {as_item["teileid"], es_item["teileid"]}, ( f'SEP-Zeile hat Nachbarn {sep_nachbarn!r}, erwartet ' f'{{{as_item["teileid"]!r}, {es_item["teileid"]!r}}}')