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) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 14:08:58 +02:00
parent 0392114e10
commit 5dcaa8b8fa
+89 -13
View File
@@ -168,6 +168,30 @@ class Element:
"""Drehrichtung eines Kreisels: UZS (im Uhrzeigersinn) oder GUZ.""" """Drehrichtung eines Kreisels: UZS (im Uhrzeigersinn) oder GUZ."""
return str(_merkmal(self.merkmale, "Drehrichtung") or "").strip().upper() 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 @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()
@@ -461,24 +485,52 @@ def _endpoints(
return [f"{base}-{LANE_LEFT}", f"{base}-{LANE_RIGHT}"] 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) True = Fluss laeuft zum hoechsten Nachbarn hin (bergauf)
False = Fluss laeuft vom hoechsten Nachbarn weg (bergab) False = Fluss laeuft vom hoechsten Nachbarn weg (bergab)
None = nicht bestimmbar 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": if element.kind == "Gefaellestrecke":
return False return False, "Gefaellestrecke ohne Hoehenangabe - laeuft bergab"
direction = element.drive_dir.lower()
if direction.startswith("auf"): return None, (
return True f"weder Hoehe_Von/Bis noch Antriebfahrtrichtung verwertbar "
if direction.startswith("ab"): f"(Antrieb '{element.drive_dir}', Hoehen "
return False f"{element.height_from}/{element.height_to})"
warnings.append(
f"{element.describe()}: Antriebfahrtrichtung fehlt oder unbekannt "
f"('{element.drive_dir}') - Richtung nicht bestimmbar"
) )
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( def _orient(
@@ -645,8 +697,16 @@ def _node_label(node: Node) -> str:
if height is not None: if height is not None:
lines.append(f"h = {height:.3f} m") lines.append(f"h = {height:.3f} m")
if element.kind == "Strecke" and element.drive_dir: if element.is_transport:
lines.append(f"Antrieb: {element.drive_dir}") 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: for kind in DEVICE_KINDS:
ids = node.devices.get(kind) 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(): for kind, ids in node.devices.items():
counted.setdefault(node_id, {})[kind] = len(ids) 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: for element in elements:
if not (element.is_transport or element.is_circle): if not (element.is_transport or element.is_circle):
continue continue