diff --git a/lib/material_flow.py b/lib/material_flow.py index 7c784bb..cbe3fc3 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -11,9 +11,10 @@ erzeugt daraus einen gerichteten Graphen des Materialflusses. Knotenmodell ------------ * Gefaellestrecke / Strecke (Foerderer) -> je ein Knoten -* Kreisel -> zwei Knoten (-L / -R), die als - Kreis miteinander verbunden sind - (L -> R und R -> L) +* Kreisel -> zwei Knoten (-L / -R); die Richtung + zwischen ihnen kommt aus dem Merkmal + "Drehrichtung": UZS = rechts nach + links, GUZ = links nach rechts * Separator / Scanner -> kein eigener Knoten; sie werden ueber "Zuordnung" am Label des uebergeordneten Knotens angezeigt @@ -82,6 +83,14 @@ LANE_NAMES = {LANE_LEFT: "links", LANE_RIGHT: "rechts"} UNASSIGNED = "nicht zugeordnet" +# Drehrichtung eines Kreisels (Merkmal "Drehrichtung"). Sie bestimmt, in welche +# Richtung das Material innerhalb des Kreisels zwischen den beiden Bahnen laeuft: +# UZS im Uhrzeigersinn -> von rechts nach links (R -> L) +# GUZ gegen den Uhrzeigersinn -> von links nach rechts (L -> R) +ROTATION_CW = "UZS" +ROTATION_CCW = "GUZ" +ROTATION_FLOW = {ROTATION_CW: "rechts nach links", ROTATION_CCW: "links nach rechts"} + # "0009-L" -> base 0009, lane L ; "0034" -> base 0034, lane None NEIGHBOUR_RE = re.compile(r"^(?P[^\s-]+)(?:-(?P[LR]))?$") @@ -154,6 +163,11 @@ class Element: def drive_dir(self) -> str: return str(_merkmal(self.merkmale, "Antriebfahrtrichtung") or "").strip() + @property + def rotation(self) -> str: + """Drehrichtung eines Kreisels: UZS (im Uhrzeigersinn) oder GUZ.""" + return str(_merkmal(self.merkmale, "Drehrichtung") or "").strip().upper() + @property def assignment(self) -> str: return str(_merkmal(self.merkmale, "Zuordnung") or "").strip() @@ -561,14 +575,51 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr edges = _orient(sorted(pairs), nodes, warnings) - # Kreisel: die beiden Bahnen bilden den Kreis + # 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 + # Fehlt die Angabe, bleibt der Umlauf beidseitig und wird gemeldet. for element in elements: if not element.is_circle: continue left = f"{element.teile_id}-{LANE_LEFT}" right = f"{element.teile_id}-{LANE_RIGHT}" - edges.append(Edge(left, right, EDGE_CIRCLE)) - edges.append(Edge(right, left, EDGE_CIRCLE)) + rotation = element.rotation + if rotation == ROTATION_CW: + edges.append(Edge(right, left, EDGE_CIRCLE, ROTATION_CW)) + elif rotation == ROTATION_CCW: + edges.append(Edge(left, right, EDGE_CIRCLE, ROTATION_CCW)) + else: + warnings.append( + f"{element.describe()}: Drehrichtung '{element.rotation or '-'}' " + f"unbekannt (erwartet {ROTATION_CW} oder {ROTATION_CCW}) - " + f"Umlauf beidseitig gezeichnet" + ) + edges.append(Edge(left, right, EDGE_CIRCLE, "unbestimmt")) + edges.append(Edge(right, left, EDGE_CIRCLE, "unbestimmt")) + + # 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" + ) return Graph(nodes=nodes, edges=edges, warnings=warnings, unassigned=unassigned, source=source) @@ -633,8 +684,12 @@ def render_dot(graph: Graph) -> str: for index, (teile_id, lanes) in enumerate(sorted(circles.items())): element = lanes[0].element name = _merkmal(element.merkmale, "Name") or element.name + rot = element.rotation + rot_text = (f" {rot} ({ROTATION_FLOW[rot]})" if rot in ROTATION_FLOW + else f" Drehrichtung {rot or '?'}") add(f" subgraph cluster_kreisel_{index} {{") - add(f' label="{_dot_escape(element.kind)} {_dot_escape(str(name))} [{teile_id}]";') + add(f' label="{_dot_escape(element.kind)} {_dot_escape(str(name))} ' + f'[{teile_id}]{_dot_escape(rot_text)}";') add(' style="rounded,filled"; fillcolor="#eef2fa"; color="#2f5597";') add(' fontname="Segoe UI"; fontsize=10; fontcolor="#1f3864"; margin=12;') style = NODE_STYLES.get(element.kind, DEFAULT_NODE_STYLE) @@ -653,8 +708,11 @@ def render_dot(graph: Graph) -> str: for edge in graph.edges: if edge.kind == EDGE_CIRCLE: - attrs = ('[color="#8ea9db", style=dashed, penwidth=1.0, constraint=false, ' - 'arrowsize=0.6, tooltip="Kreisel-Umlauf"]') + # Richtung kommt aus der Drehrichtung und steht als Label an der Kante + label = f', xlabel="{_dot_escape(edge.note)}"' if edge.note else "" + attrs = ('[color="#8ea9db", style=dashed, penwidth=1.2, constraint=false, ' + 'arrowsize=0.9, fontcolor="#5b7fc7", ' + f'tooltip="Kreisel-Umlauf {edge.note}"{label}]') elif edge.kind == EDGE_UNRESOLVED: attrs = ('[color="#bf8f00", style=dashed, dir=both, ' f'tooltip="Richtung {edge.note}"]') @@ -670,7 +728,9 @@ def render_dot(graph: Graph) -> str: '' '' '' - '' + '' '' '' "
durchgezogenMaterialfluss
gestrichelt blauKreisel-Umlauf (L / R)
Kreisel-Umlauf, Richtung aus Drehrichtung ' + f'({ROTATION_CW} = {ROTATION_FLOW[ROTATION_CW]}, ' + f'{ROTATION_CCW} = {ROTATION_FLOW[ROTATION_CCW]})
gestrichelt gelbRichtung nicht bestimmbar
>];") @@ -886,7 +946,14 @@ def render_doc( kind_text = {EDGE_FLOW: "Materialfluss", EDGE_CIRCLE: "Kreisel-Umlauf", EDGE_UNRESOLVED: "Richtung unbestimmt"} for edge in graph.edges: - add(f"| `{edge.src}` | `{edge.dst}` | {kind_text.get(edge.kind, edge.kind)} |") + note = f" ({edge.note})" if edge.note else "" + add(f"| `{edge.src}` | `{edge.dst}` | " + f"{kind_text.get(edge.kind, edge.kind)}{note} |") + add("") + add(f"Die Richtung innerhalb eines Kreisels kommt aus dem Merkmal " + f"`Drehrichtung`: **{ROTATION_CW}** (im Uhrzeigersinn) laesst das Material " + f"{ROTATION_FLOW[ROTATION_CW]} laufen, **{ROTATION_CCW}** (gegen den " + f"Uhrzeigersinn) {ROTATION_FLOW[ROTATION_CCW]}.") add("") add("## 4. Plausibilitaetshinweise")