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
|
||||
------------
|
||||
* 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<base>[^\s-]+)(?:-(?P<lane>[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:
|
||||
'<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"><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>'
|
||||
'<td align="left">Richtung nicht bestimmbar</td></tr>'
|
||||
"</table>>];")
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user