From 5dcaa8b8fa47b5ba58103ae87fee5f4957f4afe3 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 30 Jul 2026 14:08:58 +0200 Subject: [PATCH] Take the conveying direction from the element's own height change The new export fills Hoehe_Von_mm / Hoehe_Bis_mm, so the direction of a transport element no longer has to be guessed from its neighbours. drive_uphill() now decides in this order: 1. Hoehe_Von_mm -> Hoehe_Bis_mm: the rise of the element itself. Only the sign is read, since the values are absolute on a Gefaellestrecke (1941 -> 1466) but relative on a Strecke (0 -> 474). 2. Antriebfahrtrichtung "Auf" (up) or "Ab" (down). 3. A Gefaellestrecke without either runs downhill by definition. Previously a Gefaellestrecke was assumed downhill outright and everything else depended on Antriebfahrtrichtung alone; now both are backed by the height data and the fallback order is explicit. The node label states what was read and what follows from it, e.g. "0 -> 474 mm (+474) = aufwaerts", so the arrow can be checked against the export without opening the CSV. Adds a plausibility check for the case where Antriebfahrtrichtung and the height change disagree. The height wins and the conflict is reported, rather than one of the two silently deciding. Co-Authored-By: Claude Opus 5 (1M context) --- lib/material_flow.py | 102 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/lib/material_flow.py b/lib/material_flow.py index cbe3fc3..ffaae0d 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -168,6 +168,30 @@ class Element: """Drehrichtung eines Kreisels: UZS (im Uhrzeigersinn) oder GUZ.""" return str(_merkmal(self.merkmale, "Drehrichtung") or "").strip().upper() + @property + def height_from(self) -> float | None: + """Hoehe am Anfang des Objekts in mm (Merkmal Hoehe_Von_mm).""" + return _as_float(_merkmal(self.merkmale, "Hoehe_Von_mm", "Höhe_Von_mm")) + + @property + def height_to(self) -> float | None: + """Hoehe am Ende des Objekts in mm (Merkmal Hoehe_Bis_mm).""" + return _as_float(_merkmal(self.merkmale, "Hoehe_Bis_mm", "Höhe_Bis_mm")) + + @property + def rise(self) -> float | None: + """ + Hoehenaenderung in Foerderrichtung in mm. + + Positiv = das Objekt foerdert nach oben, negativ = nach unten. Die Werte + koennen absolut sein (Gefaellestrecke: 1941 -> 1466) oder relativ zum + Objekt (Strecke: 0 -> 474); nur das Vorzeichen wird ausgewertet. + """ + start, end = self.height_from, self.height_to + if start is None or end is None: + return None + return end - start + @property def assignment(self) -> str: return str(_merkmal(self.merkmale, "Zuordnung") or "").strip() @@ -461,24 +485,52 @@ def _endpoints( return [f"{base}-{LANE_LEFT}", f"{base}-{LANE_RIGHT}"] -def _flow_uphill(element: Element, warnings: list[str]) -> bool | None: +def drive_uphill(element: Element) -> tuple[bool | None, str]: """ + Foerdert das Objekt nach oben? + True = Fluss laeuft zum hoechsten Nachbarn hin (bergauf) False = Fluss laeuft vom hoechsten Nachbarn weg (bergab) None = nicht bestimmbar + + Bewertet wird in dieser Reihenfolge: + 1. Hoehe_Von_mm / Hoehe_Bis_mm - die Hoehenaenderung des Objekts selbst + 2. Antriebfahrtrichtung "Auf" (nach oben) bzw. "Ab" (nach unten) + 3. Gefaellestrecke ohne Angaben laeuft per Definition bergab + + Der zweite Rueckgabewert nennt die verwendete Quelle fuer Bericht und + Zeichnung. """ + rise = element.rise + if rise is not None and rise != 0: + direction = "Auf" if rise > 0 else "Ab" + return rise > 0, ( + f"Hoehe_Von {element.height_from:.0f} mm -> Hoehe_Bis " + f"{element.height_to:.0f} mm ({direction}, {rise:+.0f} mm)" + ) + + drive = element.drive_dir.lower() + if drive.startswith("auf"): + return True, f"Antriebfahrtrichtung '{element.drive_dir}' (nach oben)" + if drive.startswith("ab"): + return False, f"Antriebfahrtrichtung '{element.drive_dir}' (nach unten)" + if element.kind == "Gefaellestrecke": - return False - direction = element.drive_dir.lower() - if direction.startswith("auf"): - return True - if direction.startswith("ab"): - return False - warnings.append( - f"{element.describe()}: Antriebfahrtrichtung fehlt oder unbekannt " - f"('{element.drive_dir}') - Richtung nicht bestimmbar" + return False, "Gefaellestrecke ohne Hoehenangabe - laeuft bergab" + + return None, ( + f"weder Hoehe_Von/Bis noch Antriebfahrtrichtung verwertbar " + f"(Antrieb '{element.drive_dir}', Hoehen " + f"{element.height_from}/{element.height_to})" ) - return None + + +def _flow_uphill(element: Element, warnings: list[str]) -> bool | None: + """Wie drive_uphill, meldet aber unbestimmbare Faelle als Warnung.""" + uphill, reason = drive_uphill(element) + if uphill is None: + warnings.append(f"{element.describe()}: {reason} - Richtung nicht bestimmbar") + return uphill def _orient( @@ -645,8 +697,16 @@ def _node_label(node: Node) -> str: if height is not None: lines.append(f"h = {height:.3f} m") - if element.kind == "Strecke" and element.drive_dir: - lines.append(f"Antrieb: {element.drive_dir}") + if element.is_transport: + uphill = drive_uphill(element)[0] + arrow = {True: "aufwaerts", False: "abwaerts", None: "Richtung offen"}[uphill] + if element.rise is not None: + lines.append(f"{element.height_from:.0f} -> {element.height_to:.0f} mm " + f"({element.rise:+.0f}) = {arrow}") + elif element.drive_dir: + lines.append(f"Antrieb: {element.drive_dir} = {arrow}") + else: + lines.append(arrow) for kind in DEVICE_KINDS: ids = node.devices.get(kind) @@ -818,6 +878,22 @@ def check_elements(graph: Graph, elements: list[Element]) -> list[str]: for kind, ids in node.devices.items(): counted.setdefault(node_id, {})[kind] = len(ids) + # Widerspruch zwischen Hoehenangabe und Antriebfahrtrichtung + for element in elements: + if not element.is_transport: + continue + rise, drive = element.rise, element.drive_dir.lower() + if rise is None or rise == 0 or not drive: + continue + stated_up = drive.startswith("auf") + if stated_up != (rise > 0): + findings.append( + f"{element.kind} {element.teile_id} '{element.name}': " + f"Antriebfahrtrichtung '{element.drive_dir}' passt nicht zu " + f"Hoehe_Von {element.height_from:.0f} -> Hoehe_Bis " + f"{element.height_to:.0f} mm ({rise:+.0f}) - die Hoehenangabe gilt" + ) + for element in elements: if not (element.is_transport or element.is_circle): continue