Files
sps_skel/lib/tro_overrides.py
T
Samer AyadiandClaude Opus 5.5 e0defad011 tro_flow: --use-overrides, --from-connect und --geometry entfernt
TROs, Typen und Verbindungen kommen nur noch aus dem Layout. Fehler der
Ableitung werden im Code behoben, nicht in einer Korrekturdatei.

- tro_flow: Optionen --use-overrides/--no-overrides, --from-connect,
  --geometry, --geo-tol-mm entfernt; analyse_tros/derive_tros ohne
  overrides/resolve_host/count_exits-Parameter, Analysis.overrides entfernt,
  Override-Schritte 4b/4c/5b, Doku-Abschnitt "6. Manuelle Korrekturen" und
  die Override-Zeilen im Bericht entfernt; connect.ini ist reine Ausgabe
- lib/tro_geometry.py geloescht (nur von --geometry genutzt), shapely aus
  requirements.txt
- tro_overrides: apply_splits/_merges/_connections/_overrides und
  load_connect_ini entfernt - nur noch load_overrides fuer
  material_flow.py --connect
- doc/Python_Scripts.md nachgezogen

Gegenprobe: DOT (beide Layouts) und connect.ini fuer HundM05 und Mubea
unveraendert (28 bzw. 26 Verbindungen), Doku nur ohne den leeren
Abschnitt 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:02:16 +02:00

324 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
tro_overrides.py - manuelle Korrekturen an der abgeleiteten TRO-Liste.
`tro_flow.py` leitet die TROs allein aus dem mechanischen Layout (CSV-Export)
ab. Manches steht dort aber nicht drin: eine Weiche, die im Export fehlt, ein
Scanner, der den Typ auf `1Sep_SSCC` hebt, oder eine Stelle, die vor Ort als
eigener TRO erkannt wurde. Solche Erkenntnisse kommen aus der Begehung bzw.
aus dem Review der erzeugten Diagramme.
Damit sie den naechsten Lauf ueberleben, stehen sie in einer INI-Datei
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien.
Seit 2026-10-05 nur noch von `material_flow.py --connect` gelesen (Abschnitt
`[<Anlage>.manual]` der connect.ini). `tro_flow.py` und `tro_annotate.py`
wenden keine Korrekturen mehr an: die Abschnitte hingen an laufenden
TRO-Nummern und veralteten mit jedem Export - Fehler der Ableitung werden
seitdem im Code behoben (lib/tro_flow.py), nicht in der INI.
Aufbau der INI (Sektionsnamen mit dem CSV-Dateinamen als Praefix, damit
mehrere Anlagen in einer Datei stehen koennen):
[<csv>.tro.<TRO-ID>] Korrektur an einem abgeleiteten TRO
type = 1Sep1Swi neuer Katalogtyp
reason = ... Begruendung (kommt in die Doku)
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO
x = 529.95 Anlagenkoordinate in mm
y = -3895.27
z = 1468
expect = 1Sep1Swi hier erwarteter TRO-Typ
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
note = ... Erlaeuterung
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
"""
from __future__ import annotations
import configparser
import os
from dataclasses import dataclass, field
from pathlib import Path
from tro_catalog import get_tro
OVERRIDE_FILE = "tro_overrides.ini"
# Vertrauensangabe fuer manuell gesetzte Typen. Bewusst ein eigener Wert - so
# ist in der Doku sichtbar, dass der Typ nicht aus dem Layout stammt.
DEFAULT_CONFIDENCE = "bestaetigt"
@dataclass
class TroOverride:
"""Korrektur an einem abgeleiteten TRO.
Adressiert entweder ueber die TRO-ID (`TRO03`) oder - stabiler - ueber die
Nummer eines seiner Separatoren (`SEP0011`). Die TRO-IDs sind laufende
Nummern eines Laufs und verschieben sich, sobald TROs dazukommen oder
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
"""
key: str
by_separator: bool = False
type_name: str = ""
reason: str = ""
confidence: str = ""
def matches(self, tro) -> bool:
if self.by_separator:
return self.key in tro.separators
return self.key == tro.tro_id
@dataclass
class SplitSpec:
"""Ein neuer TRO, herausgeloest aus einem bereits abgeleiteten.
Fuer Uebergabestellen, die die Ableitung nicht als eigenstaendig erkennt,
weil die "Zuordnung" des Separators auf ein Transportobjekt zeigt (Strecke,
Gefaellestrecke) statt auf die Kreisel-Bahn, an der er tatsaechlich
schaltet. Die genannten Separatoren/Scanner werden ihrem bisherigen TRO
entnommen und bilden einen neuen.
"""
name: str
type_name: str
separators: list[str] = field(default_factory=list)
scanners: list[str] = field(default_factory=list)
host: str = ""
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class ConnectionSpec:
"""Eine vor Ort bestaetigte Verbindung zwischen zwei TROs."""
src: str
dst: str
kind: str = ""
@dataclass
class MergeSpec:
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
Fuer Stellen, die mechanisch aus mehreren Bauteilgruppen bestehen,
steuerungstechnisch aber eine einzige Uebergabestelle sind - etwa zwei
Sperren beiderseits eines Kreisel-Beruehrpunkts. Adressiert wird ueber die
Separatoren, weil deren Nummern ueber Exporte hinweg stabiler sind als die
laufenden TRO-Nummern.
"""
name: str
type_name: str
separators: list[str] = field(default_factory=list)
host: str = ""
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class OpenPoint:
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
name: str
x: float
y: float
z: float | None = None
expect: str = ""
belongs_to: str = ""
note: str = ""
def describe_position(self) -> str:
return f"{self.x:.0f}, {self.y:.0f}"
@dataclass
class Overrides:
"""Alle Korrekturen zu einer CSV-Datei."""
source: Path | None = None
tros: dict[str, TroOverride] = field(default_factory=dict)
splits: list[SplitSpec] = field(default_factory=list)
merges: list[MergeSpec] = field(default_factory=list)
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
connections: list[ConnectionSpec] = field(default_factory=list)
open_points: list[OpenPoint] = field(default_factory=list)
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
applied: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
def __bool__(self) -> bool:
return bool(self.tros or self.splits or self.merges or self.connections
or self.open_points)
def override_path(cfg_dir: Path | None = None) -> Path:
"""Pfad zur Override-Datei: %SKEL_CFG%, sonst <projekt>/cfg."""
if cfg_dir is not None:
return cfg_dir / OVERRIDE_FILE
env = os.environ.get("SKEL_CFG")
if env:
return Path(env) / OVERRIDE_FILE
return Path(__file__).resolve().parent.parent / "cfg" / OVERRIDE_FILE
def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
try:
return float(str(raw).strip())
except (TypeError, ValueError):
warnings.append(f"{where}: '{raw}' ist keine Zahl - Eintrag ignoriert")
return None
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
path = override_path(cfg_dir)
result = Overrides()
if not path.is_file():
return result
result.source = path
parser = configparser.ConfigParser(inline_comment_prefixes=(";",))
# Schluessel sind Kleinschreibung, Sektionsnamen bleiben wie geschrieben.
parser.read(path, encoding="utf-8")
prefix = csv_name.lower()
for section in parser.sections():
# [<csv>.connections] - gesetzte Topologie, eigene Form ohne <name>
if section.lower() == f"{prefix}.connections":
for key, raw in parser[section].items():
pfeil, _, rest = str(raw).partition("|")
src, sep, dst = (t.strip() for t in pfeil.partition("->"))
if not sep or not src or not dst:
result.warnings.append(
f"[{section}] {key}: '{raw}' ist keine Verbindung "
f"'<von> -> <nach>'"
)
continue
result.connections.append(
ConnectionSpec(src=src, dst=dst, kind=rest.strip())
)
continue
parts = section.split(".")
# <csv>.<art>.<name> - der CSV-Name kann selbst einen Punkt enthalten
if len(parts) < 3:
continue
kind, name = parts[-2].lower(), parts[-1]
owner = ".".join(parts[:-2]).lower()
if owner != prefix:
continue
values = parser[section]
if kind == "tro":
type_name = values.get("type", "").strip()
if type_name and get_tro(type_name) is None:
result.warnings.append(
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
f"- Korrektur ignoriert"
)
continue
key = name.upper()
by_sep = key.startswith("SEP")
result.tros[key] = TroOverride(
key=key[3:] if by_sep else key,
by_separator=by_sep,
type_name=type_name,
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
)
elif kind == "split":
type_name = values.get("type", "").strip()
if get_tro(type_name) is None:
result.warnings.append(
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
f"- neuer TRO nicht angelegt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
if not seps:
result.warnings.append(
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
)
continue
result.splits.append(
SplitSpec(
name=name,
type_name=type_name,
separators=seps,
scanners=[s.strip() for s in values.get("scanners", "").split(",")
if s.strip()],
host=values.get("host", "").strip(),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
)
)
elif kind == "merge":
type_name = values.get("type", "").strip()
if get_tro(type_name) is None:
result.warnings.append(
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
f"- Zusammenfassung nicht durchgefuehrt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
if len(seps) < 2:
result.warnings.append(
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
)
continue
result.merges.append(
MergeSpec(
name=name,
type_name=type_name,
separators=seps,
host=values.get("host", "").strip(),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
)
)
elif kind == "openpoint":
x = _as_float(values.get("x", ""), f"[{section}] x", result.warnings)
y = _as_float(values.get("y", ""), f"[{section}] y", result.warnings)
if x is None or y is None:
continue
z_raw = values.get("z", "").strip()
expect = values.get("expect", "").strip()
if expect and get_tro(expect) is None:
result.warnings.append(
f"[{section}]: erwarteter Typ '{expect}' steht nicht im "
f"TRO-Katalog"
)
result.open_points.append(
OpenPoint(
name=name,
x=x,
y=y,
z=_as_float(z_raw, f"[{section}] z", result.warnings)
if z_raw
else None,
expect=expect,
belongs_to=values.get("belongs_to", "").strip(),
note=values.get("note", "").strip(),
)
)
else:
result.warnings.append(
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro' oder "
f"'openpoint'"
)
result.open_points.sort(key=lambda p: p.name)
return result