d414bd6cf3
Steuernder Separator einer ILS Weiche (_find_weiche_separators) ist der letzte freie Separator in Laufrichtung VOR der Weiche, gemessen auf dem Kreisel-Umlauf aus Lage und Drehrichtung (kreisel_junctions.separators_before) - nicht mehr der CSV-Nachbar. Ein Nachbar hinter der Weiche steuert sie nie; Hoechstabstand weiche_vorlauf_max in cfg/parameter.ini [tro_flow]. Mubea (Export 2026-10-16): 2Sep2Swi = 0005 (Ausschleus 0061) + 0036 (Weiche 0130) statt 0005 + 0012; 0012 bleibt 1Sep, Kreisel1 wieder angebunden. Dematic, HundM05, showroom, test_system unveraendert. TRO-JSON: DerivedTro.switches (optional) nennt die Weichen-Bauteile in Slot-Reihenfolge (switch<i> gehoert bei 2Sep-Typen zu separator<i>); derive_slots traegt sie in die switch<n>-Slots, merge_hw_layout bindet das Ventil dort. e_planning.ini: Rollen ventil -> valve (Slot-Rolle der SCL), neue Regel Ausschleus_Weiche.valve fuer Ausschleusventile an Kreisel-Weichen. Mubea: MB0715, MB0724, MB0627, MB0671 statt TODO(E-Planung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
8.5 KiB
Python
199 lines
8.5 KiB
Python
# -*- 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, separators_before
|
|
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
|
|
|
|
|
|
# Mubea-Export 2026-10-16: Kreisel2 (UZS, senkrecht) und Kreisel1 (GUZ,
|
|
# waagerecht) beruehren sich an der ILS Weiche 0130. Auf Kreisel2 liegt 0005
|
|
# davor (steuert schon Ausschleuselement 0061), 0012 dahinter; auf Kreisel1
|
|
# liegt 0036 davor, 0040 dahinter. Die CSV-Nachbarn der Weiche sind 0061/0012.
|
|
def _mubea_weiche(kreisel1: str = "GUZ") -> list[Element]:
|
|
weiche = _el("0130", "Weiche", (400.0, -5478.0))
|
|
weiche.neighbours = [("0061", None), ("0012", None)]
|
|
return [
|
|
_kreisel("0054", (0.0, 4529.5), (800.0, 20815.0), "UZS", 90),
|
|
_kreisel("0129", (3300.0, -5478.0), (5800.0, 800.0), kreisel1, 0),
|
|
_el("0005", "Separator", (400.0, -3978.05)),
|
|
_el("0012", "Separator", (-400.0, -3554.95)),
|
|
_el("0036", "Separator", (1474.95, -5078.0)),
|
|
_el("0040", "Separator", (5355.05, -5878.0)),
|
|
weiche,
|
|
]
|
|
|
|
|
|
def test_separators_before_uses_geometry_not_trackids() -> None:
|
|
vorher, nachher = separators_before(_mubea_weiche(), 400.0, -5478.0)
|
|
assert {u.kreisel: u.separator for u in vorher} == {"0054": "0005", "0129": "0036"}
|
|
assert all(u.distance < 2000.0 for u in vorher)
|
|
assert nachher == ["0012", "0040"]
|
|
|
|
|
|
def test_weiche_controlled_by_free_separator_before_it() -> None:
|
|
from tro_flow import _find_weiche_separators
|
|
|
|
elements = _mubea_weiche()
|
|
frei = [e for e in elements if e.teile_id in ("0012", "0036", "0040")] # 0005: Ausschleus
|
|
findings: list[str] = []
|
|
result = _find_weiche_separators(frei, elements, findings)
|
|
assert {s: w.teile_id for s, w in result.items()} == {"0036": "0130"}
|
|
assert any("CSV-Nachbar 0012 liegt in Laufrichtung hinter" in f for f in findings)
|
|
|
|
|
|
def test_weiche_far_behind_separator_is_not_assigned() -> None:
|
|
# Kreisel1 falsch als UZS: davor laege nur 0040, ~5 m weit weg - kein Raten
|
|
from tro_flow import _find_weiche_separators
|
|
|
|
elements = _mubea_weiche(kreisel1="UZS")
|
|
frei = [e for e in elements if e.teile_id in ("0012", "0036", "0040")]
|
|
findings: list[str] = []
|
|
assert _find_weiche_separators(frei, elements, findings) == {}
|
|
assert any("kein freier Separator in Laufrichtung davor" in f for f in findings)
|