Weichenknoten zwischen Kreiseln, Track-Mouseover, Parameter in cfg/parameter.ini
TRO-Typen an Kreisel-Beruehrpunkten (lib/kreisel_junctions.py, derive_tros Schritt 3c): jeder Kreisel als Stadion-Form mit Laufrichtung; jeder gleichlaufende Beruehrpunkt ist eine Weiche (exportierte ILS Weiche oder abgeleitet W<a>_<b>), Beruehrpunkte derselben Umlenkung bilden einen Knoten. Ein Separator steuert ihn, wenn er in Laufrichtung unmittelbar davor liegt (keine andere Weiche dazwischen). Typ = Separatoren x Weichen. export.csv: 1Sep1Swi (9/1, 9/8, 8/7), 2Sep1Swi (7/2), 2Sep2Swi (2/3/4), 3Sep2Swi (4/5/6). Neuer Katalogtyp 3Sep2Swi (nicht aus SCL belegt). Gegenlaeufige Beruehrpunkte (Mubea, Dematic) bleiben bei der alten Regel; Mubea/HundM05/Dematic unveraendert, Showroom 0018+0019 jetzt 2Sep1Swi. material_flow --track: Mouseover je Knoten mit Zugehoerigkeit zu Kreisel/VF/GF (Zuordnung, TrackIds-Kette, Lage laut Geometrie), Weiche mit beiden Kreiseln, Be-/Entladung mit ihrem Kreisel; Widersprueche mit "!". Zahlenwerte (Toleranzen, Schwellen, Massstaebe, Groessen) aus allen lib-Modulen nach cfg/parameter.ini (lib/parameter.py, Abschnitt je Modul, Ueberschreiben je Anlage ueber %SKEL_CFG%/parameter.ini). Ausgaben aller Anlagen bytegleich. tro_flow.py: Docstrings mit Parametern/Rueckgabe, fehlende Typangaben. Geprueft: 90 Tests gruen; tro_flow/material_flow/scl_skeleton auf 5 CSVs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c69faf3db
commit
550bdeb464
@@ -0,0 +1,151 @@
|
||||
# -*- 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
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/parameter.py und cfg/parameter.ini."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import parameter
|
||||
from parameter import ParameterError, get_float, get_int
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BASE = ROOT / "cfg" / parameter.PARAMETER_FILE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frisch(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Parameter-Cache leeren, damit SKEL_CFG im Test wirkt - und danach wieder."""
|
||||
parameter._parser.cache_clear()
|
||||
yield monkeypatch
|
||||
parameter._parser.cache_clear()
|
||||
|
||||
|
||||
def _base() -> configparser.ConfigParser:
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
|
||||
parser.read(BASE, encoding="utf-8")
|
||||
return parser
|
||||
|
||||
|
||||
def test_base_values_are_read(frisch: pytest.MonkeyPatch) -> None:
|
||||
frisch.delenv("SKEL_CFG", raising=False)
|
||||
assert get_float("kreisel_junctions", "contact_tolerance") == 50.0
|
||||
assert get_int("tro_flow", "min_store_lanes") == 2
|
||||
|
||||
|
||||
def test_skel_cfg_overrides_single_value(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
(tmp_path / parameter.PARAMETER_FILE).write_text(
|
||||
"[kreisel_junctions]\ncontact_tolerance = 75 ; Anlage X\n", encoding="utf-8")
|
||||
frisch.setenv("SKEL_CFG", str(tmp_path))
|
||||
assert get_float("kreisel_junctions", "contact_tolerance") == 75.0
|
||||
# nicht ueberschriebene Werte kommen weiter aus der Grunddatei
|
||||
assert get_float("kreisel_junctions", "separator_tolerance") == 100.0
|
||||
|
||||
|
||||
def test_missing_key_is_an_error(frisch: pytest.MonkeyPatch) -> None:
|
||||
frisch.delenv("SKEL_CFG", raising=False)
|
||||
with pytest.raises(ParameterError, match=r"\[tro_flow\] gibt_es_nicht fehlt"):
|
||||
get_float("tro_flow", "gibt_es_nicht")
|
||||
|
||||
|
||||
def test_non_number_is_an_error(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
(tmp_path / parameter.PARAMETER_FILE).write_text(
|
||||
"[tro_flow]\nmin_store_lanes = zwei\n", encoding="utf-8")
|
||||
frisch.setenv("SKEL_CFG", str(tmp_path))
|
||||
with pytest.raises(ParameterError, match="keine ganze Zahl"):
|
||||
get_int("tro_flow", "min_store_lanes")
|
||||
|
||||
|
||||
def test_every_key_is_used_by_its_module() -> None:
|
||||
"""Jeder Schluessel der Grunddatei wird in lib/<Abschnitt>.py gelesen -
|
||||
keine verwaisten Werte, die nichts mehr steuern."""
|
||||
parser = _base()
|
||||
for section in parser.sections():
|
||||
source = (ROOT / "lib" / f"{section}.py").read_text(encoding="utf-8")
|
||||
for key in parser.options(section):
|
||||
assert re.search(rf'"{key}"', source), f"[{section}] {key} wird nicht gelesen"
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer die Mouseover-Texte des --track-Graphen (material_flow.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from material_flow import TrackNode, _tooltip_context, _track_node_tooltip
|
||||
from test_kreisel_junctions import KREISEL, SEPARATOREN, WEICHEN, _el, _kreisel
|
||||
|
||||
|
||||
def _anlage() -> list:
|
||||
kreisel = [_kreisel(*k) for k in KREISEL]
|
||||
for k in kreisel:
|
||||
k.name = f"Kreisel{int(k.teile_id)}" # wie im Export
|
||||
kreisel[2].track_ids = ["0015"] # Kreisel 0003 laut Export
|
||||
kreisel[4].track_ids = ["0025", "0026", "0020"] # Kreisel 0005
|
||||
seps = []
|
||||
for teile_id, pos in SEPARATOREN:
|
||||
zuordnung = {"0015": "0003-L", "0010": "0009-L"}.get(teile_id, "")
|
||||
seps.append(_el(teile_id, "Separator", pos, Zuordnung=zuordnung))
|
||||
weichen = [_el(i, "Weiche", p) for i, p in WEICHEN]
|
||||
weichen[0].merkmale.update({"Kreisel A": "0009", "Kreisel B": "0001"})
|
||||
beladung = _el("0020", "Beladung", (14374.25, 1186.94))
|
||||
return kreisel + seps + weichen + [beladung]
|
||||
|
||||
|
||||
def _tooltip(elements: list, teile_id: str, kind: str) -> list[str]:
|
||||
ctx = _tooltip_context(elements)
|
||||
el = next(e for e in elements if e.teile_id == teile_id)
|
||||
return _track_node_tooltip(TrackNode(teile_id, kind, el), ctx).split("\\n")
|
||||
|
||||
|
||||
def test_separator_shows_zuordnung_kette_and_geometry_conflict() -> None:
|
||||
lines = _tooltip(_anlage(), "0015", "Separator")
|
||||
assert "Zuordnung: Kreisel3 (0003) Bahn L" in lines
|
||||
assert "TrackIds-Kette: Kreisel3 (0003)" in lines
|
||||
assert "Geometrie: liegt auf Kreisel2 (0002) Bahn R" in lines
|
||||
assert "! Zuordnung Kreisel 0003, liegt aber auf Kreisel 0002" in lines
|
||||
|
||||
|
||||
def test_separator_without_conflict_has_no_warning() -> None:
|
||||
lines = _tooltip(_anlage(), "0010", "Separator")
|
||||
assert "Geometrie: liegt auf Kreisel9 (0009) Bahn L" in lines
|
||||
assert not any(line.startswith("! Zuordnung") for line in lines)
|
||||
|
||||
|
||||
def test_weiche_shows_both_kreisel() -> None:
|
||||
lines = _tooltip(_anlage(), "0022", "Weiche")
|
||||
assert "Kreisel A: Kreisel9 (0009)" in lines
|
||||
assert "Kreisel B: Kreisel1 (0001)" in lines
|
||||
assert "Geometrie: Beruehrpunkt Kreisel 0001 / 0009" in lines
|
||||
|
||||
|
||||
def test_beladung_shows_its_kreisel() -> None:
|
||||
lines = _tooltip(_anlage(), "0020", "Beladung")
|
||||
assert "TrackIds-Kette: Kreisel5 (0005)" in lines
|
||||
assert lines[-1].startswith("Geometrie: naechster Kreisel Kreisel5 (0005)")
|
||||
Reference in New Issue
Block a user