[FIX] Materialfluss: Kreisel als geschlossener Umlauf

Jeder Kreisel bekam nur EINE Bahnkante aus der Drehrichtung (UZS R->L,
GUZ L->R). Die Gegenbahn hatte damit keinen Abgang bzw. Zugang, und der
Sackgassen-Check meldete an fast jedem Kreisel "Sackgasse"/"nicht
erreichbar", obwohl die Anlage durchlaufbar ist.

- build_graph: immer L->R UND R->L; die Gegenkante traegt
  CIRCLE_RETURN_NOTE und wird in render_dot nicht gezeichnet
- Sackgassen-Pruefung ersetzt durch Erreichbarkeit im Gesamtgraphen
  (von Beladung erreichbar / Entladung erreichbar); unbestimmte Kanten
  zaehlen in beide Richtungen
- Weiche: Merkmal "Beruehrung" (Kreis/Kreis bzw. Tangente) hat Vorrang
  vor der Geometrie, die am Kappenuebergang per Rundung entschied
- Test test_kreisel_umlauf_graph.py mit Fixture test_system_export.csv

TRO-Graph (nach Hosts) fuer dematic/hundm_05/mubea/showroom unveraendert,
HundM05 komplett unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-15 15:40:53 +02:00
co-authored by Claude Opus 5.5
parent 550bdeb464
commit c4122b4497
3 changed files with 243 additions and 28 deletions
+95 -28
View File
@@ -186,6 +186,10 @@ NEIGHBOUR_RE = re.compile(r"^(?P<base>[^\s-]+)(?:-(?P<lane>[LR]))?$")
EDGE_FLOW = "flow"
EDGE_CIRCLE = "circle"
EDGE_UNRESOLVED = "unresolved"
# Notiz der Gegenkante eines Kreisel-Umlaufs (EDGE_CIRCLE entgegen der
# Hauptkante, siehe build_graph) - fuer den Fluss gleichwertig, wird aber
# nicht gezeichnet.
CIRCLE_RETURN_NOTE = "Rueckfuehrung"
# Darstellung je Objektart
NODE_STYLES = {
@@ -1330,7 +1334,21 @@ def _apply_transfers(
touch_a = _kreisel_touch_lane(el_a, point, elements) if el_a and point else None
touch_b = _kreisel_touch_lane(el_b, point, elements) if el_b and point else None
if touch_a is not None and touch_b is not None and (not touch_a[1] or not touch_b[1]):
# Beruehrart aus dem Export (dxfmakros, Merkmal "Beruehrung") hat
# Vorrang vor der Geometrie: ein Kreis/Kreis-Punkt liegt genau auf dem
# Uebergang Endkappe -> Gerade, dort entschied in _kreisel_touch_lane
# die Rundung der CSV-Werte (test_system: 4891.79 gegen Schwelle
# 4891.79) und machte die Weiche faelschlich "unbestimmt".
beruehrung = str(_merkmal(weiche_el.merkmale, "Beruehrung") or "").strip()
if beruehrung == "Kreis/Kreis":
laengsseite = False
elif "Tangente" in beruehrung:
laengsseite = True
else:
laengsseite = (touch_a is not None and touch_b is not None
and (not touch_a[1] or not touch_b[1]))
if laengsseite and touch_a is not None and touch_b is not None:
# Mindestens eine Seite liegt an der Laengsseite: Bahn direkt aus
# der Geometrie, Richtung bleibt unbestimmt (ohne Schaltzustand
# der Weiche ist dort kein Eingang/Ausgang ableitbar).
@@ -1473,6 +1491,63 @@ def _merge_station_symbols(
return swallowed
def _reachability_warnings(nodes: dict[str, Node], edges: list[Edge]) -> list[str]:
"""Bahnen, die von keiner Beladung aus erreichbar sind bzw. von denen
aus keine Entladung erreichbar ist.
Ein Kreisel ist ein geschlossener Umlauf (siehe build_graph), eine Bahn
hat darum lokal immer Zu- und Abgang - ob das Material sie erreicht bzw.
wieder verlassen kann, ist eine Frage des ganzen Graphen. Kanten mit
unbestimmter Richtung (EDGE_UNRESOLVED, z. B. eine Weiche an der
Laengsseite) gelten in beide Richtungen als begehbar. Ohne Be- bzw.
Entladestation im Layout entfaellt die jeweilige Pruefung.
"""
succ: dict[str, set[str]] = {}
pred: dict[str, set[str]] = {}
for edge in edges:
succ.setdefault(edge.src, set()).add(edge.dst)
pred.setdefault(edge.dst, set()).add(edge.src)
if edge.kind == EDGE_UNRESOLVED:
succ.setdefault(edge.dst, set()).add(edge.src)
pred.setdefault(edge.src, set()).add(edge.dst)
def reach(starts: list[str], step: dict[str, set[str]]) -> set[str]:
seen = set(starts)
stack = list(starts)
while stack:
for nxt in step.get(stack.pop(), ()):
if nxt not in seen:
seen.add(nxt)
stack.append(nxt)
return seen
entries = sorted(n for n, node in nodes.items()
if node.element.is_station and node.element.is_entry)
exits = sorted(n for n, node in nodes.items()
if node.element.is_station and node.element.is_exit)
from_entry = reach(entries, succ) if entries else None
to_exit = reach(exits, pred) if exits else None
result: list[str] = []
for node_id in sorted(nodes):
node = nodes[node_id]
if node.lane is None:
continue
devices = node.devices.get("Separator", [])
detail = f" (Separatoren {', '.join(sorted(devices))})" if devices else ""
if from_entry is not None and node_id not in from_entry:
result.append(
f"Bahn {node_id}{detail}: von keiner Beladung ({', '.join(entries)}) "
f"aus erreichbar"
)
if to_exit is not None and node_id not in to_exit:
result.append(
f"Bahn {node_id}{detail}: keine Entladung ({', '.join(exits)}) "
f"erreichbar - Sackgasse"
)
return result
def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Graph:
# Generische Eingang/Ausgang-Symbole, die ueber einer BTMT-Station liegen,
# verschlucken (nur die konkrete Station bleibt Knoten).
@@ -1496,9 +1571,16 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
edges = _orient(sorted(pairs), nodes, warnings)
# Kreisel: Richtung zwischen den beiden Bahnen aus der Drehrichtung
# UZS (im Uhrzeigersinn) -> von rechts nach links, R -> L
# GUZ (gegen den Uhrzeigersinn) -> von links nach rechts, L -> R
# Kreisel: geschlossener Umlauf - das Ende von L laeuft an der einen
# Endkappe in den Anfang von R, das Ende von R an der anderen in den
# Anfang von L. Darum IMMER zwei Kanten L -> R und R -> L. Die Drehrichtung
# legt nur fest, welche davon die "Hauptkante" mit dem Kuerzel ist:
# UZS (im Uhrzeigersinn) -> Hauptkante R -> L
# GUZ (gegen den Uhrzeigersinn) -> Hauptkante L -> R
# Die Gegenkante traegt CIRCLE_RETURN_NOTE und wird in render_dot nicht
# gezeichnet (der Umlauf-Pfeil steht fuer beide). Frueher fehlte sie: jeder
# Kreisel hatte dann eine Bahn ohne Abgang und eine ohne Zugang, und die
# Pruefung unten meldete an fast jedem Kreisel Sackgasse + unerreichbar.
# Fehlt die Angabe, bleibt der Umlauf beidseitig und wird gemeldet.
for element in elements:
if not element.is_circle:
@@ -1508,8 +1590,10 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
rotation = element.rotation
if rotation == ROTATION_CW:
edges.append(Edge(right, left, EDGE_CIRCLE, ROTATION_CW))
edges.append(Edge(left, right, EDGE_CIRCLE, CIRCLE_RETURN_NOTE))
elif rotation == ROTATION_CCW:
edges.append(Edge(left, right, EDGE_CIRCLE, ROTATION_CCW))
edges.append(Edge(right, left, EDGE_CIRCLE, CIRCLE_RETURN_NOTE))
else:
warnings.append(
f"{element.describe()}: Drehrichtung '{element.rotation or '-'}' "
@@ -1525,28 +1609,7 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
# Kreisel<->Strecke gerichtet aus den Transferelementen (massgeblich)
edges = _apply_transfers(elements, by_id, nodes, edges, warnings)
# Sackgassen und unerreichbare Bahnen melden. Sie entstehen, wenn ein
# Kreisel nur in einer Richtung durchlaufen wird (Drehrichtung) und die
# Gegenrichtung nicht ueber aeussere Verbindungen geschlossen ist.
outgoing = {edge.src for edge in edges}
incoming = {edge.dst for edge in edges}
for node_id in sorted(nodes):
node = nodes[node_id]
if node.lane is None:
continue
devices = node.devices.get("Separator", [])
detail = f" (Separatoren {', '.join(sorted(devices))})" if devices else ""
if node_id not in outgoing:
warnings.append(
f"Bahn {node_id}{detail}: keine abgehende Verbindung - Sackgasse. "
f"Drehrichtung {node.element.rotation or '?'} laesst das Material "
f"nur in eine Richtung laufen"
)
if node_id not in incoming:
warnings.append(
f"Bahn {node_id}{detail}: keine ankommende Verbindung - fuer das "
f"Material nicht erreichbar"
)
warnings.extend(_reachability_warnings(nodes, edges))
return Graph(nodes=nodes, edges=edges, warnings=warnings, unassigned=unassigned, source=source)
@@ -2078,6 +2141,8 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
seen_group_edges: set[tuple[str, str]] = set()
for edge in graph.edges:
if edge.kind == EDGE_CIRCLE and edge.note == CIRCLE_RETURN_NOTE:
continue # Umlauf-Pfeil der Hauptkante steht fuer beide
src = group_of.get(edge.src, edge.src)
dst = group_of.get(edge.dst, edge.dst)
if edge.src in grouped_ids or edge.dst in grouped_ids:
@@ -3499,7 +3564,8 @@ def render_doc(
devices[kind] = devices.get(kind, 0) + len(ids)
flow = sum(1 for e in graph.edges if e.kind == EDGE_FLOW)
circle = sum(1 for e in graph.edges if e.kind == EDGE_CIRCLE)
circle = sum(1 for e in graph.edges
if e.kind == EDGE_CIRCLE and e.note != CIRCLE_RETURN_NOTE)
open_dir = sum(1 for e in graph.edges if e.kind == EDGE_UNRESOLVED)
add("## 1. Kurzfassung")
@@ -3622,7 +3688,8 @@ def report(
kinds[node.kind] = kinds.get(node.kind, 0) + 1
flow = sum(1 for e in graph.edges if e.kind == EDGE_FLOW)
circle = sum(1 for e in graph.edges if e.kind == EDGE_CIRCLE)
circle = sum(1 for e in graph.edges
if e.kind == EDGE_CIRCLE and e.note != CIRCLE_RETURN_NOTE)
open_dir = sum(1 for e in graph.edges if e.kind == EDGE_UNRESOLVED)
devices = 0
+30
View File
@@ -0,0 +1,30 @@
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
1;"ILS 2.0 Kreisel";"0001";"Kreisel1";"A/7-B/8";1;"-2011.74, 994.84, 2000.00";"3100.00, 800.00, 1.00";"yab0Dy4YHoSAAAAAAAAAAAAA";"";"";"";"";"0009-L";"0023, 0021";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel1", "Geruest fuer Einzelmodul": true}
2;"ILS 2.0 Kreisel";"0002";"Kreisel2";"E/3-E/6";1;"4938.26, -4953.25, 2000.00";"800.00, 5300.00, 1.00";"S1oUi/vOHoSAAAAAAAAAWoJ4";"";"";"";"";"0007-L, 0007-R, 0003-L";"0015, 0029, 0014, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.5", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel2", "Geruest fuer Einzelmodul": true}
3;"ILS 2.0 Kreisel";"0003";"Kreisel3";"E/1-E/4";1;"5738.26, -9850.80, 2000.00";"800.00, 6095.71, 1.00";"V48UgAABHoSAAAAAAAAAWoJ4";"";"";"";"";"0004-L, 0002-R";"0029, 0028";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "5.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel3", "Geruest fuer Einzelmodul": true}
4;"ILS 2.0 Kreisel";"0004";"Kreisel4";"E/3-F/7";1;"6538.26, -3258.73, 2000.00";"800.00, 8689.42, 1.00";"Y8QUi/sQHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-R, 0003-R";"0016, 0028, 0017, 0027";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "7.89", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel4", "Geruest fuer Einzelmodul": true}
5;"ILS 2.0 Kreisel";"0005";"Kreisel5";"E/7-K/8";1;"11430.05, 1485.99, 2000.00";"10583.58, 800.00, 1.00";"XamUFqymHoSAAAAAAAAAAAAA";"";"";"";"";"0006-L, 0006-R, 0004-L, 0004-R";"0018, 0026, 0027, 0020";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "9.78", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 0, "Name": "Kreisel5", "Geruest fuer Einzelmodul": true}
6;"ILS 2.0 Kreisel";"0006";"Kreisel6";"E/8-F/10";1;"6538.26, 4145.31, 2000.00";"800.00, 4518.63, 1.00";"Y8QUHMcmHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-L";"0019, 0026";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.72", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel6", "Geruest fuer Einzelmodul": true}
7;"ILS 2.0 Kreisel";"0007";"Kreisel7";"E/6-E/8";1;"4938.26, -453.25, 2000.00";"800.00, 3700.00, 1.00";"S1oU3NruHoSAAAAAAAAAWoJ4";"";"";"";"";"0008-L, 0008-R, 0002-L, 0002-R";"0024, 0013, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.9", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel7", "Geruest fuer Einzelmodul": true}
8;"ILS 2.0 Kreisel";"0008";"Kreisel8";"B/7-E/7";1;"2438.26, 505.79, 2000.00";"4200.00, 800.00, 1.00";"BSlUB7e+HoSAAAAAAAAAAAAA";"";"";"";"";"0009-R, 0007-L";"0024, 0011, 0022, 0012";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.4", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel8", "Geruest fuer Einzelmodul": true}
9;"ILS 2.0 Kreisel";"0009";"Kreisel9";"B/6-B/9";1;"-61.74, 1425.92, 2000.00";"800.00, 4800.00, 1.00";"/w7U8SMAHoSAAAAAAAAAWoJ4";"";"";"";"";"0001-L, 0001-R, 0008-L, 0008-R";"0010, 0023, 0022";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel9", "Geruest fuer Einzelmodul": true}
10;"ILS 2.0 Separator";"0010";"Separator :10";"B/8";1;"-461.74, 1526.13, 2001.50";"150.00, 209.90, 3.00";"+PRUFl9EHoSAAAAAAAAAWoJ4";"/+33ADlFAE4g";"";"";"";"0022, 0023";"";"";"";{"Zuordnung": "0009-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
11;"ILS 2.0 Separator";"0011";"Separator :9";"C/7";1;"1241.80, 105.79, 1.50";"209.90, 150.00, 3.00";"EgiaAZ0+AAAAAAAAAAAAAAAA";"AC4qAAQhAAAA";"";"";"";"0024, 0022";"";"";"";{"Zuordnung": "0008-R", "ARTINR": "200000077", "Höhe": "0", "Drehung": 0, "Typ": ""}
12;"ILS 2.0 Separator";"0012";"Separator :8";"D/7";1;"3696.84, 905.79, 2001.50";"209.90, 150.00, 3.00";"OVL2DdI+HoSAAAAAAAAAf///";"AJK/ACNhAE4g";"";"";"";"0022, 0024";"";"";"";{"Zuordnung": "0008-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 180, "Typ": ""}
13;"ILS 2.0 Separator";"0013";"Separator :7";"E/6";1;"4538.26, -1493.47, 2001.50";"150.00, 209.90, 3.00";"RT+U6Ev0HoSAAAAAAAAAWoJ4";"ALFG/8NSAE4g";"";"";"";"0024, 0025";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
14;"ILS 2.0 Separator";"0014";"Separator :6";"E/5";1;"4538.26, -3169.65, 2001.50";"150.00, 209.90, 3.00";"RT+U0Iy8HoSAAAAAAAAApX2I";"ALFG/4aHAE4g";"";"";"";"0029, 0025";"";"";"";{"Zuordnung": "0002-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
15;"ILS 2.0 Separator";"0015";"Separator :5";"E/4";1;"5338.26, -6534.21, 2001.50";"150.00, 209.90, 3.00";"UXSUm2GQHoSAAAAAAAAAWoJ4";"ANCG/v5rAE4g";"";"";"";"0025, 0029";"";"";"";{"Zuordnung": "0002-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
16;"ILS 2.0 Separator";"0016";"Separator :4";"F/4";1;"6938.26, -6618.23, 2001.50";"150.00, 209.90, 3.00";"ad6UmhlcHoSAAAAAAAAAWoJ4";"AQ8G/vsjAE4g";"";"";"";"0027, 0028";"";"";"";{"Zuordnung": "0004-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
17;"ILS 2.0 Separator";"0017";"Separator :3";"E/7";1;"6138.26, 302.58, 2001.50";"150.00, 209.90, 3.00";"XamUBYgiHoSAAAAAAAAApX2I";"AO/GAA4pAE4g";"";"";"";"0028, 0027";"";"";"";{"Zuordnung": "0004-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
18;"ILS 2.0 Separator";"0018";"Separator :2";"F/8";1;"7153.84, 1885.99, 2001.50";"209.90, 150.00, 3.00";"bD6CHMcmHoSAAAAAAAAAAAAA";"ARUaAEmrAE4g";"";"";"";"0020, 0026";"";"";"";{"Zuordnung": "0005-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 0, "Typ": ""}
19;"ILS 2.0 Separator";"0019";"Separator :1";"F/8";1;"6938.26, 2908.49, 2001.50";"150.00, 209.90, 3.00";"ad6UK3ccHoSAAAAAAAAAWoJ4";"AQ8GAG9FAE4g";"";"";"";"0026, 0026";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
20;"ILS 2.0 BTMT Beladung";"0020";"BTMT Beladung :1";"I/7-J/8";1;"14378.91, 1186.94, 1955.31";"645.71, 397.50, 316.02";"f///EJImHoSAAAAAAAAAAAAA";"AiYwADU7AE6r";"Aj4eACprAE4g";"";"";"0027, 0018";"";"";"";{"ARTINR": "829662012", "Höhe": "2000", "Drehung": 0}
21;"ILS 2.0 BTMT Entladung";"0021";"BTMT Entladung :1";"A/7";1;"-2501.71, 212.89, 1847.09";"1468.14, 950.36, 467.01";"3bjiCN/+HoSAAAAAAAAAf/+r";"/7UPABabAE4f";"/4Hu//aMAD/1";"";"";"0023, 0023";"";"";"";{"ARTINR": "400100156", "Höhe": "2000", "Drehung": 179}
22;"ILS Weiche";"0022";"ILS Weiche :1";"B/7";1;"338.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0011, 0012";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0008", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
23;"ILS Weiche";"0023";"ILS Weiche :2";"B/7";1;"-461.74, 994.84, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0021, 0021";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0001", "Achse von Kreisel": "0001", "Beruehrung": "Kreis/Tangente"}
24;"ILS Weiche";"0024";"ILS Weiche :3";"E/7";1;"4538.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0025, 0013";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0008", "Kreisel B": "0007", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
25;"ILS Weiche";"0025";"ILS Weiche :4";"E/6";1;"4938.26, -2303.25, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0014, 0015";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0007", "Kreisel B": "0002", "Achse von Kreisel": "0007", "Beruehrung": "Kreis/Kreis"}
26;"ILS Weiche";"0026";"ILS Weiche :5";"F/8";1;"6538.26, 1885.99, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0018, 0027";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0006", "Kreisel B": "0005", "Achse von Kreisel": "0006", "Beruehrung": "Kreis/Kreis"}
27;"ILS Weiche";"0027";"ILS Weiche :6";"F/7";1;"6538.26, 1085.78, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0017, 0016";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0005", "Kreisel B": "0004", "Achse von Kreisel": "0005", "Beruehrung": "Kreis/Kreis"}
28;"ILS Weiche";"0028";"ILS Weiche :7";"E/3";1;"6138.26, -7203.05, 2000.00";"100.25, 250.10, 30.00";"";"";"";"";"";"0029, 0029";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0004", "Kreisel B": "0003", "Achse von Kreisel": "0004", "Beruehrung": "Kreis/Kreis"}
29;"ILS Weiche";"0029";"ILS Weiche :8";"E/3";1;"5338.26, -7202.95, 2000.00";"100.19, 250.07, 30.00";"";"";"";"";"";"0015, 0014";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0003", "Kreisel B": "0002", "Achse von Kreisel": "0003", "Beruehrung": "Kreis/Kreis"}
Can't render this file because it contains an unexpected character in line 2 and column 171.
+118
View File
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""Tests fuer den Kreisel-Umlauf im Materialflussgraphen (lib/material_flow.py).
Ein Kreisel ist ein geschlossener Umlauf: das Ende von L laeuft in den Anfang
von R und das Ende von R in den Anfang von L. Frueher bekam jeder Kreisel nur
EINE Bahnkante (aus der Drehrichtung) - jede Bahn ohne Gegenkante galt dann
als Sackgasse bzw. unerreichbar, obwohl die Anlage vollstaendig durchlaufbar
ist. Fixture: Testanlage test_system (dxfmakros results/export.csv, 9 Kreisel,
8 Kreisel-Kreisel-Weichen, 1 Beladung, 1 Entladung).
"""
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
import pytest
from material_flow import (
CIRCLE_RETURN_NOTE,
EDGE_CIRCLE,
EDGE_FLOW,
EDGE_UNRESOLVED,
Edge,
Element,
Node,
_reachability_warnings,
build_graph,
read_elements,
)
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
@pytest.fixture(scope="module")
def elements() -> list[Element]:
return read_elements(FIXTURE, [])
def _graph(elements: list[Element]):
warnings: list[str] = []
return build_graph(elements, FIXTURE.name, warnings), warnings
def test_jeder_kreisel_hat_beide_bahnkanten(elements):
graph, _ = _graph(elements)
for el in (e for e in elements if e.is_circle):
left, right = f"{el.teile_id}-L", f"{el.teile_id}-R"
circle = [e for e in graph.edges if e.kind == EDGE_CIRCLE
and {e.src, e.dst} == {left, right}]
assert {(e.src, e.dst) for e in circle} == {(left, right), (right, left)}
# genau eine Hauptkante mit dem Drehrichtungs-Kuerzel
assert sorted(e.note for e in circle) == sorted([el.rotation, CIRCLE_RETURN_NOTE])
def test_keine_sackgassen_in_durchlaufbarer_anlage(elements):
_, warnings = _graph(elements)
bahn = [w for w in warnings if w.startswith("Bahn ")]
assert bahn == []
def test_kreis_kreis_weiche_ist_gerichtet(elements):
"""Weiche 0006<->0005 ist "Kreis/Kreis" - liegt genau auf dem Uebergang
Endkappe/Gerade, die Geometrie allein entschied dort per Rundung auf
"Laengsseite, unbestimmt". Das Merkmal "Beruehrung" hat Vorrang."""
graph, _ = _graph(elements)
weiche = [e for e in graph.edges if e.note == "Weiche"
and {e.src.split("-")[0], e.dst.split("-")[0]} == {"0005", "0006"}]
assert weiche
assert all(e.kind == EDGE_FLOW for e in weiche)
def test_ohne_weichen_meldet_unerreichbare_bahnen(elements):
"""Gegenprobe: ohne die Kreisel-Kreisel-Weichen (und ohne die rohen
Kreisel-Nachbarn, die sonst als unbestimmte Kanten verbinden) ist nur der
Kreisel der Beladung erreichbar - die Pruefung muss das melden."""
ohne = [replace(e, neighbours=[]) if e.is_circle else e
for e in elements if e.kind != "Weiche"]
_, warnings = _graph(ohne)
assert any("von keiner Beladung" in w for w in warnings)
assert any("keine Entladung" in w for w in warnings)
def _el(teile_id: str, kind: str) -> 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={}, row=0, position=(0.0, 0.0, 0.0), bbox=None,
)
def test_unbestimmte_kante_in_beide_richtungen_begehbar():
"""Eine Weiche mit unbestimmter Richtung (dir=both) darf nicht nur in
src->dst-Richtung zaehlen."""
k1, k2 = _el("0001", "Kreisel"), _el("0002", "Kreisel")
nodes = {
"0001-L": Node("0001-L", "Kreisel", k1, lane="L"),
"0001-R": Node("0001-R", "Kreisel", k1, lane="R"),
"0002-L": Node("0002-L", "Kreisel", k2, lane="L"),
"0002-R": Node("0002-R", "Kreisel", k2, lane="R"),
"B": Node("B", "Beladung", _el("B", "Beladung")),
"E": Node("E", "Entladung", _el("E", "Entladung")),
}
edges = [
Edge("0001-L", "0001-R", EDGE_CIRCLE, "GUZ"),
Edge("0001-R", "0001-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
Edge("0002-L", "0002-R", EDGE_CIRCLE, "GUZ"),
Edge("0002-R", "0002-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
# Beladung an Kreisel 2, Entladung an Kreisel 1, Weiche zeigt "falsch herum"
Edge("B", "0002-L", EDGE_FLOW),
Edge("0001-L", "E", EDGE_FLOW),
Edge("0001-R", "0002-R", EDGE_UNRESOLVED, "Weiche"),
]
assert _reachability_warnings(nodes, edges) == []
# gerichtet waere Kreisel 1 von der Beladung aus nicht erreichbar
edges[-1] = Edge("0001-R", "0002-R", EDGE_FLOW, "Weiche")
assert any("0001-L" in w and "von keiner Beladung" in w
for w in _reachability_warnings(nodes, edges))