# -*- coding: utf-8 -*- """Tests fuer lib/kreisel_junctions.py - Weichenknoten zwischen Kreiseln. Die Geometrie stammt aus der Testanlage data/export.csv (9 Kreisel, 10 Separatoren, 5 ILS Weichen; drei Beruehrpunkte fehlen im Export als Weiche). """ from __future__ import annotations import math import pytest from kreisel_junctions import find_contacts, find_junctions, kreisel_loop from material_flow import Element def _el(teile_id: str, kind: str, pos: tuple[float, float], bbox: tuple[float, float] | None = None, **merkmale: object) -> Element: return Element( number=teile_id, kind=kind, teile_art=kind, teile_id=teile_id, name=f"{kind} {teile_id}", plan_square="", neighbours=[], track_ids=[], merkmale=dict(merkmale), row=0, position=(pos[0], pos[1], 2000.0), bbox=(bbox[0], bbox[1], 1.0) if bbox else None, ) def _kreisel(teile_id: str, pos: tuple[float, float], bbox: tuple[float, float], drehrichtung: str, drehung: float) -> Element: return _el(teile_id, "Kreisel", pos, bbox, Drehrichtung=drehrichtung, Drehung=drehung) # (Id, Position, Boundingbox, Drehrichtung, Drehung) aus data/export.csv KREISEL = [ ("0001", (-2011.74, 994.84), (3100.00, 800.00), "UZS", 0), ("0002", (4940.03, -4953.25), (800.00, 5300.00), "UZS", 90), ("0003", (5739.13, -9850.80), (800.00, 6095.71), "GUZ", 90), ("0004", (6533.60, -3258.73), (800.00, 8689.42), "UZS", 90), ("0005", (11425.39, 1485.99), (10583.58, 800.00), "GUZ", 0), ("0006", (6533.60, 4145.31), (800.00, 4518.63), "UZS", 90), ("0007", (4940.59, -453.25), (800.00, 3700.00), "GUZ", 90), ("0008", (2440.85, 505.79), (4200.00, 800.00), "UZS", 0), ("0009", (-61.76, 1425.92), (800.00, 4800.00), "GUZ", 90), ] SEPARATOREN = [ ("0010", (-461.76, 1526.13)), ("0011", (1244.39, 105.79)), ("0012", (3699.43, 905.79)), ("0013", (4540.59, -1493.47)), ("0014", (4540.03, -3169.65)), ("0015", (5340.03, -6534.21)), ("0016", (6933.60, -6618.23)), ("0017", (6133.60, 302.58)), ("0018", (7149.18, 1885.99)), ("0019", (6933.60, 2908.49)), ] WEICHEN = [ ("0022", (-461.76, 994.84)), ("0023", (4540.59, 505.79)), ("0024", (4940.31, -2303.25)), ("0025", (6533.60, 1885.99)), ("0026", (6533.60, 1085.78)), ] @pytest.fixture def testanlage() -> list[Element]: return ([_kreisel(*k) for k in KREISEL] + [_el(i, "Separator", p) for i, p in SEPARATOREN] + [_el(i, "Weiche", p) for i, p in WEICHEN]) @pytest.mark.parametrize("drehrichtung", ["UZS", "GUZ"]) def test_loop_point_project_roundtrip(drehrichtung: str) -> None: loop = kreisel_loop(_kreisel("K", (100.0, 200.0), (800.0, 3000.0), drehrichtung, 90)) assert loop is not None for i in range(40): s = i * loop.length / 40 x, y = loop.point(s) s2, dist, _sec = loop.project(x, y) assert dist < 1e-6 assert min(abs(s2 - s), loop.length - abs(s2 - s)) < 1e-6 def test_loop_direction_follows_drehrichtung() -> None: # Vertikaler Kreisel (Drehung 90), Bahnen bei x = -400 und x = +400: # UZS laeuft auf der linken (westlichen) Bahn nach Norden. uzs = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "UZS", 90)) guz = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "GUZ", 90)) s_uzs, _d, _sec = uzs.project(-400.0, 0.0) s_guz, _d, _sec = guz.project(-400.0, 0.0) assert uzs.direction(s_uzs) == pytest.approx((0.0, 1.0), abs=1e-6) assert guz.direction(s_guz) == pytest.approx((0.0, -1.0), abs=1e-6) def test_contacts_match_exported_weichen(testanlage: list[Element]) -> None: findings: list[str] = [] _loops, contacts = find_contacts(testanlage, findings) by_pair = {c.kreisel: c for c in contacts} assert sorted(by_pair) == [ ("0001", "0009"), ("0002", "0003"), ("0002", "0007"), ("0003", "0004"), ("0004", "0005"), ("0005", "0006"), ("0007", "0008"), ("0008", "0009"), ] assert by_pair[("0002", "0007")].weiche_id == "0024" assert by_pair[("0005", "0006")].weiche_id == "0025" # Im Export fehlende Weichen bekommen eine abgeleitete Id und ein Finding assert by_pair[("0008", "0009")].weiche_id == "W0008_0009" assert not by_pair[("0008", "0009")].exported assert sum("ohne ILS Weiche im Export" in f for f in findings) == 3 def test_junction_types_testanlage(testanlage: list[Element]) -> None: junctions = find_junctions(testanlage, []) result = sorted((j.type_name, tuple(j.separators), tuple(c.weiche_id for c in j.contacts)) for j in junctions) assert result == [ ("1Sep1Swi", ("0010",), ("0022",)), ("1Sep1Swi", ("0011",), ("W0008_0009",)), ("1Sep1Swi", ("0012",), ("0023",)), ("2Sep1Swi", ("0013", "0014"), ("0024",)), ("2Sep2Swi", ("0015", "0016"), ("W0002_0003", "W0003_0004")), ("3Sep2Swi", ("0017", "0018", "0019"), ("0025", "0026")), ] def test_separator_behind_other_weiche_does_not_control(testanlage: list[Element]) -> None: # 0010 liegt auf Kreisel 9 vor Weiche 0022; die Weiche 9/8 folgt erst # danach - sie steuert 0011 von Kreisel 8 aus, nicht 0010. junctions = find_junctions(testanlage, []) j98 = next(j for j in junctions if j.contacts[0].weiche_id == "W0008_0009") assert j98.separators == ["0011"] assert j98.sep_lane["0011"] == "0008-R" def test_separator_lane_is_physical_not_zuordnung(testanlage: list[Element]) -> None: # 0015 ist im Export Kreisel 0003 zugeordnet, liegt aber auf Kreisel 0002 junctions = find_junctions(testanlage, []) j = next(j for j in junctions if "0015" in j.separators) assert j.sep_kreisel["0015"] == "0002" assert j.sep_lane["0015"] == "0002-R" def test_counter_rotating_contact_is_no_weiche() -> None: # Zwei UZS-Kreisel Stirn an Stirn laufen am Beruehrpunkt gegeneinander a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0) b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "UZS", 0) findings: list[str] = [] _loops, contacts = find_contacts([a, b], findings) assert contacts == [] assert any("gegenlaeufig" in f for f in findings) def test_co_rotating_end_contact_is_weiche() -> None: a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0) b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "GUZ", 0) _loops, contacts = find_contacts([a, b], []) assert len(contacts) == 1 assert math.hypot(contacts[0].x - 1500.0, contacts[0].y) < 5.0