[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:
co-authored by
Claude Opus 5.5
parent
550bdeb464
commit
c4122b4497
+95
-28
@@ -186,6 +186,10 @@ NEIGHBOUR_RE = re.compile(r"^(?P<base>[^\s-]+)(?:-(?P<lane>[LR]))?$")
|
|||||||
EDGE_FLOW = "flow"
|
EDGE_FLOW = "flow"
|
||||||
EDGE_CIRCLE = "circle"
|
EDGE_CIRCLE = "circle"
|
||||||
EDGE_UNRESOLVED = "unresolved"
|
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
|
# Darstellung je Objektart
|
||||||
NODE_STYLES = {
|
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_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
|
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
|
# Mindestens eine Seite liegt an der Laengsseite: Bahn direkt aus
|
||||||
# der Geometrie, Richtung bleibt unbestimmt (ohne Schaltzustand
|
# der Geometrie, Richtung bleibt unbestimmt (ohne Schaltzustand
|
||||||
# der Weiche ist dort kein Eingang/Ausgang ableitbar).
|
# der Weiche ist dort kein Eingang/Ausgang ableitbar).
|
||||||
@@ -1473,6 +1491,63 @@ def _merge_station_symbols(
|
|||||||
return swallowed
|
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:
|
def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Graph:
|
||||||
# Generische Eingang/Ausgang-Symbole, die ueber einer BTMT-Station liegen,
|
# Generische Eingang/Ausgang-Symbole, die ueber einer BTMT-Station liegen,
|
||||||
# verschlucken (nur die konkrete Station bleibt Knoten).
|
# 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)
|
edges = _orient(sorted(pairs), nodes, warnings)
|
||||||
|
|
||||||
# Kreisel: Richtung zwischen den beiden Bahnen aus der Drehrichtung
|
# Kreisel: geschlossener Umlauf - das Ende von L laeuft an der einen
|
||||||
# UZS (im Uhrzeigersinn) -> von rechts nach links, R -> L
|
# Endkappe in den Anfang von R, das Ende von R an der anderen in den
|
||||||
# GUZ (gegen den Uhrzeigersinn) -> von links nach rechts, L -> R
|
# 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.
|
# Fehlt die Angabe, bleibt der Umlauf beidseitig und wird gemeldet.
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if not element.is_circle:
|
if not element.is_circle:
|
||||||
@@ -1508,8 +1590,10 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
|
|||||||
rotation = element.rotation
|
rotation = element.rotation
|
||||||
if rotation == ROTATION_CW:
|
if rotation == ROTATION_CW:
|
||||||
edges.append(Edge(right, left, EDGE_CIRCLE, 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:
|
elif rotation == ROTATION_CCW:
|
||||||
edges.append(Edge(left, right, EDGE_CIRCLE, ROTATION_CCW))
|
edges.append(Edge(left, right, EDGE_CIRCLE, ROTATION_CCW))
|
||||||
|
edges.append(Edge(right, left, EDGE_CIRCLE, CIRCLE_RETURN_NOTE))
|
||||||
else:
|
else:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"{element.describe()}: Drehrichtung '{element.rotation or '-'}' "
|
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)
|
# Kreisel<->Strecke gerichtet aus den Transferelementen (massgeblich)
|
||||||
edges = _apply_transfers(elements, by_id, nodes, edges, warnings)
|
edges = _apply_transfers(elements, by_id, nodes, edges, warnings)
|
||||||
|
|
||||||
# Sackgassen und unerreichbare Bahnen melden. Sie entstehen, wenn ein
|
warnings.extend(_reachability_warnings(nodes, edges))
|
||||||
# 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
return Graph(nodes=nodes, edges=edges, warnings=warnings, unassigned=unassigned, source=source)
|
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()
|
seen_group_edges: set[tuple[str, str]] = set()
|
||||||
for edge in graph.edges:
|
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)
|
src = group_of.get(edge.src, edge.src)
|
||||||
dst = group_of.get(edge.dst, edge.dst)
|
dst = group_of.get(edge.dst, edge.dst)
|
||||||
if edge.src in grouped_ids or edge.dst in grouped_ids:
|
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)
|
devices[kind] = devices.get(kind, 0) + len(ids)
|
||||||
|
|
||||||
flow = sum(1 for e in graph.edges if e.kind == EDGE_FLOW)
|
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)
|
open_dir = sum(1 for e in graph.edges if e.kind == EDGE_UNRESOLVED)
|
||||||
|
|
||||||
add("## 1. Kurzfassung")
|
add("## 1. Kurzfassung")
|
||||||
@@ -3622,7 +3688,8 @@ def report(
|
|||||||
kinds[node.kind] = kinds.get(node.kind, 0) + 1
|
kinds[node.kind] = kinds.get(node.kind, 0) + 1
|
||||||
|
|
||||||
flow = sum(1 for e in graph.edges if e.kind == EDGE_FLOW)
|
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)
|
open_dir = sum(1 for e in graph.edges if e.kind == EDGE_UNRESOLVED)
|
||||||
|
|
||||||
devices = 0
|
devices = 0
|
||||||
|
|||||||
+30
@@ -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.
|
@@ -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))
|
||||||
Reference in New Issue
Block a user