latest changes
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
#!/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. `tro_flow.py`
|
||||
und `tro_annotate.py` lesen sie und wenden sie nach der Ableitung an; die Doku
|
||||
weist jede angewandte Korrektur getrennt aus, damit abgeleitet und behauptet
|
||||
unterscheidbar bleiben.
|
||||
|
||||
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**. Sie werden nur
|
||||
in der Doku und in der annotierten Zeichnung markiert, damit sie in BricsCAD
|
||||
geprueft werden koennen, bevor daraus ein TRO wird.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
def apply_splits(tros, overrides: Overrides, make_tro) -> list:
|
||||
"""Neue TROs aus bestehenden herausloesen (vor der Nummernvergabe).
|
||||
|
||||
`make_tro(spec, hosts, label)` erzeugt das projektspezifische Tro-Objekt -
|
||||
so bleibt dieses Modul frei von einem Import aus tro_flow (das umgekehrt
|
||||
schon von hier importiert).
|
||||
"""
|
||||
for spec in overrides.splits:
|
||||
quellen = [t for t in tros if set(spec.separators) & set(t.separators)]
|
||||
if not quellen:
|
||||
overrides.warnings.append(
|
||||
f"Split '{spec.name}': keiner der Separatoren "
|
||||
f"{', '.join(spec.separators)} kommt in diesem Lauf vor - "
|
||||
f"nicht angelegt"
|
||||
)
|
||||
continue
|
||||
|
||||
hosts: list[str] = []
|
||||
for quelle in quellen:
|
||||
quelle.separators = [s for s in quelle.separators
|
||||
if s not in spec.separators]
|
||||
quelle.scanners = [s for s in quelle.scanners if s not in spec.scanners]
|
||||
hosts.extend(quelle.hosts)
|
||||
if not quelle.separators and quelle.type_name not in ("Vario",):
|
||||
overrides.warnings.append(
|
||||
f"Split '{spec.name}': {quelle.label} hat danach keinen "
|
||||
f"Separator mehr - bitte pruefen"
|
||||
)
|
||||
|
||||
ziel_hosts = [spec.host] if spec.host else sorted(set(hosts))[:1]
|
||||
label = spec.label or (
|
||||
f"Separator {', '.join(spec.separators)} "
|
||||
f"(manuell aus {quellen[0].label} herausgeloest)"
|
||||
)
|
||||
tros.append(make_tro(spec, ziel_hosts, label))
|
||||
overrides.applied.append(
|
||||
f"neu {spec.name}: {spec.type_name} auf {', '.join(ziel_hosts)} "
|
||||
f"mit Separator {', '.join(spec.separators)}, herausgeloest aus "
|
||||
f"{', '.join(sorted({q.label for q in quellen}))} "
|
||||
f"({spec.reason or 'manuelle Ergaenzung'})"
|
||||
)
|
||||
return tros
|
||||
|
||||
|
||||
def apply_merges(tros, overrides: Overrides) -> list:
|
||||
"""Mehrere TROs zu einem zusammenfassen (nach den Splits, vor der Nummer).
|
||||
|
||||
Der erste betroffene TRO bleibt bestehen und uebernimmt Bauteile, Hosts und
|
||||
Typ; die uebrigen werden aus der Liste entfernt.
|
||||
"""
|
||||
for spec in overrides.merges:
|
||||
betroffen = [t for t in tros if set(spec.separators) & set(t.separators)]
|
||||
if len(betroffen) < 2:
|
||||
gefunden = ", ".join(t.tro_id or t.label for t in betroffen) or "keiner"
|
||||
overrides.warnings.append(
|
||||
f"Merge '{spec.name}': nur {len(betroffen)} der Separatoren "
|
||||
f"{', '.join(spec.separators)} gefunden ({gefunden}) - "
|
||||
f"nicht zusammengefasst"
|
||||
)
|
||||
continue
|
||||
|
||||
ziel = betroffen[0]
|
||||
quellen = betroffen[1:]
|
||||
for quelle in quellen:
|
||||
ziel.separators = sorted(set(ziel.separators) | set(quelle.separators))
|
||||
ziel.scanners = sorted(set(ziel.scanners) | set(quelle.scanners))
|
||||
ziel.hosts = sorted(set(ziel.hosts) | set(quelle.hosts))
|
||||
tros.remove(quelle)
|
||||
|
||||
if spec.host:
|
||||
# Ein Host bestimmt, wo der zusammengefasste TRO im Fluss haengt;
|
||||
# die uebrigen Bahnen bleiben als weitere Hosts erhalten, damit die
|
||||
# Verknuepfung beide Seiten des Uebergangs sieht.
|
||||
ziel.hosts = [spec.host] + [h for h in ziel.hosts if h != spec.host]
|
||||
ziel.type_name = spec.type_name
|
||||
ziel.confidence = spec.confidence
|
||||
ziel.label = spec.label or ziel.label
|
||||
grund = spec.reason or "manuell zusammengefasst"
|
||||
ziel.reason = f"{ziel.reason}; manuell zusammengefasst ({grund})"
|
||||
overrides.applied.append(
|
||||
f"merge {spec.name}: {len(betroffen)} TROs -> ein {spec.type_name} "
|
||||
f"mit Separatoren {', '.join(ziel.separators)} ({grund})"
|
||||
)
|
||||
return tros
|
||||
|
||||
|
||||
def apply_connections(tros, overrides: Overrides):
|
||||
"""Gesetzte Topologie an die Stelle der abgeleiteten Kanten setzen.
|
||||
|
||||
Ersetzt vollstaendig, nicht ergaenzend: wenn die Verbindungen vor Ort
|
||||
aufgenommen wurden, sind die abgeleiteten damit erledigt - sonst blieben
|
||||
genau die Kanten stehen, die korrigiert werden sollten. Was dabei wegfaellt
|
||||
bzw. hinzukommt, steht anschliessend in overrides.applied.
|
||||
|
||||
Rueckgabe: (edges, edge_kinds) wie von link_tros.
|
||||
"""
|
||||
bekannt = {t.tro_id for t in tros}
|
||||
edges: list[tuple[str, str]] = []
|
||||
kinds: dict[tuple[str, str], str] = {}
|
||||
for spec in overrides.connections:
|
||||
for seite, wert in (("von", spec.src), ("nach", spec.dst)):
|
||||
if wert not in bekannt:
|
||||
overrides.warnings.append(
|
||||
f"Verbindung {spec.src} -> {spec.dst}: {seite} '{wert}' ist "
|
||||
f"kein TRO dieses Laufs - Verbindung ignoriert"
|
||||
)
|
||||
break
|
||||
else:
|
||||
paar = (spec.src, spec.dst)
|
||||
if paar in kinds:
|
||||
continue
|
||||
edges.append(paar)
|
||||
kinds[paar] = spec.kind or "gesetzt"
|
||||
|
||||
for tro in tros:
|
||||
tro.successors = sorted(d for s, d in edges if s == tro.tro_id)
|
||||
tro.predecessors = sorted(s for s, d in edges if d == tro.tro_id)
|
||||
tro.colocated = []
|
||||
|
||||
ohne = [t.tro_id for t in tros if not t.successors and not t.predecessors]
|
||||
if ohne:
|
||||
overrides.warnings.append(
|
||||
"ohne Verbindung in der gesetzten Topologie: " + ", ".join(sorted(ohne))
|
||||
)
|
||||
overrides.applied.append(
|
||||
f"Topologie gesetzt: {len(edges)} Verbindungen aus der Override-Datei "
|
||||
f"ersetzen die abgeleiteten"
|
||||
)
|
||||
return sorted(edges), kinds
|
||||
|
||||
|
||||
def apply_overrides(tros, overrides: Overrides) -> None:
|
||||
"""Typkorrekturen auf die abgeleitete TRO-Liste anwenden.
|
||||
|
||||
Wird aufgerufen, nachdem die TRO-IDs vergeben sind (die Korrekturen sind
|
||||
ueber die ID adressiert) und bevor FB-Baustein und Bauteilliste aus dem
|
||||
Katalog gezogen werden - der Typwechsel zieht beides mit.
|
||||
"""
|
||||
if not overrides.tros:
|
||||
return
|
||||
for section_key, override in sorted(overrides.tros.items()):
|
||||
treffer = [t for t in tros if override.matches(t)]
|
||||
if not treffer:
|
||||
wie = "Separator" if override.by_separator else "TRO-ID"
|
||||
overrides.warnings.append(
|
||||
f"[{section_key}]: kein TRO mit {wie} '{override.key}' in diesem "
|
||||
f"Lauf - Korrektur nicht angewandt"
|
||||
+ ("" if override.by_separator else
|
||||
" (Nummerierung geaendert? Besser ueber SEP<Nummer> adressieren)")
|
||||
)
|
||||
continue
|
||||
if len(treffer) > 1:
|
||||
overrides.warnings.append(
|
||||
f"[{section_key}]: trifft {len(treffer)} TROs "
|
||||
f"({', '.join(t.tro_id for t in treffer)}) - auf alle angewandt"
|
||||
)
|
||||
for tro in treffer:
|
||||
if not override.type_name or override.type_name == tro.type_name:
|
||||
continue
|
||||
vorher = tro.type_name
|
||||
tro.type_name = override.type_name
|
||||
tro.confidence = override.confidence
|
||||
grund = override.reason or "manuelle Korrektur"
|
||||
tro.reason = (f"{tro.reason}; manuell auf {override.type_name} "
|
||||
f"gesetzt ({grund})")
|
||||
overrides.applied.append(
|
||||
f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})"
|
||||
)
|
||||
Reference in New Issue
Block a user