Orient the carousel arrows from Drehrichtung (UZS / GUZ)
Until now the two lanes of a Kreisel were joined by a pair of edges in both directions, because the travel direction was unknown. The CSV does carry it in the Drehrichtung merkmal, so the arrow inside a Kreisel is now directed: UZS clockwise -> material runs right to left, R -> L GUZ counter-clockwise -> material runs left to right, L -> R If the merkmal is missing or holds anything else, the circuit stays bidirectional and that is reported as a warning, so an unknown value cannot silently invent a direction. The direction is visible in the drawing: the edge carries the rotation as its label and the Kreisel cluster header states it in words, e.g. "Kreisel Kreisel3 [0009] UZS (rechts nach links)". Since a Kreisel is now traversed one way only, lanes can end up without an outgoing or without an incoming connection. build_graph reports both cases, naming the separators that sit on the affected lane, because such a lane means carriers can either not leave or not reach it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+78
-11
@@ -11,9 +11,10 @@ erzeugt daraus einen gerichteten Graphen des Materialflusses.
|
|||||||
Knotenmodell
|
Knotenmodell
|
||||||
------------
|
------------
|
||||||
* Gefaellestrecke / Strecke (Foerderer) -> je ein Knoten
|
* Gefaellestrecke / Strecke (Foerderer) -> je ein Knoten
|
||||||
* Kreisel -> zwei Knoten (-L / -R), die als
|
* Kreisel -> zwei Knoten (-L / -R); die Richtung
|
||||||
Kreis miteinander verbunden sind
|
zwischen ihnen kommt aus dem Merkmal
|
||||||
(L -> R und R -> L)
|
"Drehrichtung": UZS = rechts nach
|
||||||
|
links, GUZ = links nach rechts
|
||||||
* Separator / Scanner -> kein eigener Knoten; sie werden
|
* Separator / Scanner -> kein eigener Knoten; sie werden
|
||||||
ueber "Zuordnung" am Label des
|
ueber "Zuordnung" am Label des
|
||||||
uebergeordneten Knotens angezeigt
|
uebergeordneten Knotens angezeigt
|
||||||
@@ -82,6 +83,14 @@ LANE_NAMES = {LANE_LEFT: "links", LANE_RIGHT: "rechts"}
|
|||||||
|
|
||||||
UNASSIGNED = "nicht zugeordnet"
|
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
|
# "0009-L" -> base 0009, lane L ; "0034" -> base 0034, lane None
|
||||||
NEIGHBOUR_RE = re.compile(r"^(?P<base>[^\s-]+)(?:-(?P<lane>[LR]))?$")
|
NEIGHBOUR_RE = re.compile(r"^(?P<base>[^\s-]+)(?:-(?P<lane>[LR]))?$")
|
||||||
|
|
||||||
@@ -154,6 +163,11 @@ class Element:
|
|||||||
def drive_dir(self) -> str:
|
def drive_dir(self) -> str:
|
||||||
return str(_merkmal(self.merkmale, "Antriebfahrtrichtung") or "").strip()
|
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
|
@property
|
||||||
def assignment(self) -> str:
|
def assignment(self) -> str:
|
||||||
return str(_merkmal(self.merkmale, "Zuordnung") or "").strip()
|
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)
|
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:
|
for element in elements:
|
||||||
if not element.is_circle:
|
if not element.is_circle:
|
||||||
continue
|
continue
|
||||||
left = f"{element.teile_id}-{LANE_LEFT}"
|
left = f"{element.teile_id}-{LANE_LEFT}"
|
||||||
right = f"{element.teile_id}-{LANE_RIGHT}"
|
right = f"{element.teile_id}-{LANE_RIGHT}"
|
||||||
edges.append(Edge(left, right, EDGE_CIRCLE))
|
rotation = element.rotation
|
||||||
edges.append(Edge(right, left, EDGE_CIRCLE))
|
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)
|
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())):
|
for index, (teile_id, lanes) in enumerate(sorted(circles.items())):
|
||||||
element = lanes[0].element
|
element = lanes[0].element
|
||||||
name = _merkmal(element.merkmale, "Name") or element.name
|
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" 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(' style="rounded,filled"; fillcolor="#eef2fa"; color="#2f5597";')
|
||||||
add(' fontname="Segoe UI"; fontsize=10; fontcolor="#1f3864"; margin=12;')
|
add(' fontname="Segoe UI"; fontsize=10; fontcolor="#1f3864"; margin=12;')
|
||||||
style = NODE_STYLES.get(element.kind, DEFAULT_NODE_STYLE)
|
style = NODE_STYLES.get(element.kind, DEFAULT_NODE_STYLE)
|
||||||
@@ -653,8 +708,11 @@ def render_dot(graph: Graph) -> str:
|
|||||||
|
|
||||||
for edge in graph.edges:
|
for edge in graph.edges:
|
||||||
if edge.kind == EDGE_CIRCLE:
|
if edge.kind == EDGE_CIRCLE:
|
||||||
attrs = ('[color="#8ea9db", style=dashed, penwidth=1.0, constraint=false, '
|
# Richtung kommt aus der Drehrichtung und steht als Label an der Kante
|
||||||
'arrowsize=0.6, tooltip="Kreisel-Umlauf"]')
|
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:
|
elif edge.kind == EDGE_UNRESOLVED:
|
||||||
attrs = ('[color="#bf8f00", style=dashed, dir=both, '
|
attrs = ('[color="#bf8f00", style=dashed, dir=both, '
|
||||||
f'tooltip="Richtung {edge.note}"]')
|
f'tooltip="Richtung {edge.note}"]')
|
||||||
@@ -670,7 +728,9 @@ def render_dot(graph: Graph) -> str:
|
|||||||
'<table border="0" cellborder="0" cellspacing="2" cellpadding="2">'
|
'<table border="0" cellborder="0" cellspacing="2" cellpadding="2">'
|
||||||
'<tr><td align="left">durchgezogen</td><td align="left">Materialfluss</td></tr>'
|
'<tr><td align="left">durchgezogen</td><td align="left">Materialfluss</td></tr>'
|
||||||
'<tr><td align="left"><font color="#8ea9db">gestrichelt blau</font></td>'
|
'<tr><td align="left"><font color="#8ea9db">gestrichelt blau</font></td>'
|
||||||
'<td align="left">Kreisel-Umlauf (L / R)</td></tr>'
|
'<td align="left">Kreisel-Umlauf, Richtung aus Drehrichtung '
|
||||||
|
f'({ROTATION_CW} = {ROTATION_FLOW[ROTATION_CW]}, '
|
||||||
|
f'{ROTATION_CCW} = {ROTATION_FLOW[ROTATION_CCW]})</td></tr>'
|
||||||
'<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
'<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
||||||
'<td align="left">Richtung nicht bestimmbar</td></tr>'
|
'<td align="left">Richtung nicht bestimmbar</td></tr>'
|
||||||
"</table>>];")
|
"</table>>];")
|
||||||
@@ -886,7 +946,14 @@ def render_doc(
|
|||||||
kind_text = {EDGE_FLOW: "Materialfluss", EDGE_CIRCLE: "Kreisel-Umlauf",
|
kind_text = {EDGE_FLOW: "Materialfluss", EDGE_CIRCLE: "Kreisel-Umlauf",
|
||||||
EDGE_UNRESOLVED: "Richtung unbestimmt"}
|
EDGE_UNRESOLVED: "Richtung unbestimmt"}
|
||||||
for edge in graph.edges:
|
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("")
|
||||||
|
|
||||||
add("## 4. Plausibilitaetshinweise")
|
add("## 4. Plausibilitaetshinweise")
|
||||||
|
|||||||
Reference in New Issue
Block a user