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>
This commit is contained in:
Samer Ayadi
2026-10-05 16:02:16 +02:00
co-authored by Claude Opus 5.5
parent a5fab7e2dc
commit e0defad011
5 changed files with 49 additions and 935 deletions
+19 -263
View File
@@ -98,7 +98,6 @@ import csv
import json
import math
import sys
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
@@ -136,15 +135,6 @@ from tro_catalog import (
get_tro,
mermaid_classdefs,
)
from tro_overrides import (
Overrides,
apply_connections,
apply_merges,
apply_overrides,
apply_splits,
load_connect_ini,
load_overrides,
)
# ---------------------------------------------------------------------------
# Konstanten
@@ -250,9 +240,7 @@ class Analysis:
isolated: list[Tro] = field(default_factory=list)
unknown_types: list[str] = field(default_factory=list)
findings: list[str] = field(default_factory=list)
# Manuelle Korrekturen aus cfg/tro_overrides.ini (Typkorrekturen, offene Punkte)
overrides: Overrides = field(default_factory=Overrides)
# Wie Host/Fanout ermittelt wurden - siehe derive_tros(resolve_host, count_exits)
# Wie Host/Fanout ermittelt wurden (Doku/Bericht)
method: str = "Materialfluss-Graph (Zuordnung/Nachbarn)"
# Systemgrenzen (BTMT Be-/Entladestationen) - siehe derive_boundaries; kein
# TRO, nur damit der tatsaechliche Materialein-/ausgang sichtbar wird.
@@ -547,20 +535,13 @@ def derive_tros(
graph: Graph,
elements: list[Element],
findings: list[str],
overrides: Overrides | None = None,
*,
resolve_host: Callable[[Element, list[str]], str | None] | None = None,
count_exits: Callable[[list[str]], list[str]] | None = None,
sepliste_counts: dict[str, int] | None = None,
) -> list[Tro]:
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden.
resolve_host(sep, findings) -> Host-Knoten-Id oder None; Default liest die
CSV-Spalte "Zuordnung" (siehe _default_resolve_host). count_exits(hosts) ->
Liste abgehender Nachbarn eines TRO-Hosts; Default liest graph.edges (siehe
_switch_exits). tro_geometry.py::make_geometric_resolvers() liefert ein
Paar, das beides stattdessen direkt aus den Bauteil-Weltkoordinaten sucht
(STRtree) - siehe tro_flow.py --geometry.
Host eines Separators aus der CSV-Spalte "Zuordnung" (_default_resolve_host),
abgehende Wege eines TRO-Hosts aus graph.edges (_switch_exits).
sepliste_counts (optional, siehe load_sepliste_counts): Container-Id ->
Anzahl 'SEP'-Eintraege aus dem Roh-JSON. Reine Gegenprobe - stimmt die aus
@@ -568,8 +549,11 @@ def derive_tros(
nicht mit dieser Zahl ueberein, wird ein Finding angehaengt; die TRO-Daten
selbst bleiben unangetastet (CSV bleibt die massgebliche Quelle).
"""
resolve_host = resolve_host or (lambda sep, findings: _default_resolve_host(sep, findings, graph))
count_exits = count_exits or (lambda hosts: _switch_exits(hosts, graph))
def resolve_host(sep: Element, findings: list[str]) -> str | None:
return _default_resolve_host(sep, findings, graph)
def count_exits(hosts: list[str]) -> list[str]:
return _switch_exits(hosts, graph)
separators = sorted(
(e for e in elements if e.kind == "Separator"), key=lambda e: e.teile_id
@@ -969,41 +953,12 @@ def derive_tros(
tro.reason = f"{tro.reason}; {len(exits)} abgehende Wege -> Weiche"
tro.confidence = "niedrig"
# 4b) Manuell ergaenzte Uebergabestellen (cfg/tro_overrides.ini, [.split.]).
# Vor der Nummernvergabe, damit die neuen TROs in derselben Reihenfolge
# eine Nummer bekommen wie die abgeleiteten.
if overrides is not None and overrides.splits:
def _neuer_tro(spec, hosts, label):
return Tro(
tro_id="",
type_name=spec.type_name,
label=label,
hosts=hosts,
separators=list(spec.separators),
scanners=list(spec.scanners),
reason=spec.reason or "manuell ergaenzte Uebergabestelle",
confidence=spec.confidence,
)
apply_splits(tros, overrides, _neuer_tro)
# 4c) Zusammenfassungen - nach den Splits, damit auch ein frisch
# herausgeloester TRO Teil einer Zusammenfassung sein kann.
if overrides is not None and overrides.merges:
apply_merges(tros, overrides)
# 5) Stabile, laufende Nummern in Reihenfolge der Host-Knoten
tros.sort(key=lambda t: (t.hosts[0], t.separators[0] if t.separators else ""))
width = max(2, len(str(len(tros))))
for index, tro in enumerate(tros, start=1):
tro.tro_id = f"TRO{index:0{width}d}"
# 5b) Manuelle Korrekturen aus cfg/tro_overrides.ini. Sie greifen erst hier,
# weil sie ueber die TRO-ID adressiert sind, und noch vor Schritt 6, damit
# ein manuell gesetzter Typ den SSCC-Hinweis nicht mehr ausloest.
if overrides is not None:
apply_overrides(tros, overrides)
# 6) Scanner am Separator: 1Sep_SSCC laesst sich mechanisch nicht erkennen
for tro in tros:
if tro.type_name == "1Sep" and tro.scanners:
@@ -1597,20 +1552,12 @@ def locate_tros(tros: list[Tro], elements: list[Element], graph: Graph) -> None:
def analyse_tros(
graph: Graph,
elements: list[Element],
overrides: Overrides | None = None,
*,
resolve_host: Callable[[Element, list[str]], str | None] | None = None,
count_exits: Callable[[list[str]], list[str]] | None = None,
method: str | None = None,
sepliste_counts: dict[str, int] | None = None,
) -> Analysis:
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen."""
analysis = Analysis()
analysis.overrides = overrides if overrides is not None else Overrides()
if method is not None:
analysis.method = method
analysis.tros = derive_tros(graph, elements, analysis.findings, analysis.overrides,
resolve_host=resolve_host, count_exits=count_exits,
analysis.tros = derive_tros(graph, elements, analysis.findings,
sepliste_counts=sepliste_counts)
analysis.boundaries = derive_boundaries(elements, graph)
# Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen:
@@ -1623,25 +1570,6 @@ def analyse_tros(
locate_tros(linked, elements, graph)
analysis.edges, analysis.edge_kinds = link_tros(graph, linked, elements)
# Vor Ort aufgenommene Topologie schlaegt die abgeleitete. Die Differenz
# wird protokolliert, damit sichtbar bleibt, was die Ableitung falsch hatte.
if analysis.overrides.connections:
abgeleitet = set(analysis.edges)
analysis.edges, analysis.edge_kinds = apply_connections(
linked, analysis.overrides
)
gesetzt = set(analysis.edges)
for src, dst in sorted(abgeleitet - gesetzt):
analysis.findings.append(
f"abgeleitete Verbindung {src} -> {dst} entfaellt - steht nicht "
f"in der gesetzten Topologie"
)
for src, dst in sorted(gesetzt - abgeleitet):
analysis.findings.append(
f"Verbindung {src} -> {dst} nur gesetzt, aus dem Layout nicht "
f"ableitbar"
)
for tro in analysis.tros:
definition = tro.definition()
if definition:
@@ -2020,29 +1948,26 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
# ---------------------------------------------------------------------------
# connect.ini (TRO-Ebene) - --connect schreibt, --from-connect liest
# connect.ini (TRO-Ebene) - --connect schreibt
# ---------------------------------------------------------------------------
def render_connect_ini(analysis: Analysis, section: str, source: str) -> str:
"""
TRO-Topologie als connect.ini, angelehnt an doc/TRO_Katalog/connect.ini
(hmf-connect-v1). Zurueck eingelesen von tro_overrides.load_connect_ini
(--from-connect): geaenderte Typen und Verbindungen gehen dann in DOT/SVG.
(hmf-connect-v1). Reine Ausgabe - wird von keinem Lauf zurueckgelesen.
Abweichung vom Referenzformat: Systemgrenzen stehen nicht als EXTERN,
sondern mit ihrer ID (SYSn), damit die Datei verlustfrei zurueckgelesen
werden kann. [<Anlage>.tros] ist eine reine Uebersicht (Vorgaenger/
Nachfolger je TRO) und wird beim Einlesen ignoriert.
sondern mit ihrer ID (SYSn). [<Anlage>.tros] ist eine reine Uebersicht
(Vorgaenger/Nachfolger je TRO).
"""
out: list[str] = []
add = out.append
add("; ============================================================")
add("; connect.ini - Routing / Zusammenhalt der TROs (TRO-Ebene)")
add(f"; Automatisch erzeugt von lib/tro_flow.py aus '{source}'.")
add("; Wird bei jedem Lauf mit --connect neu geschrieben. Eine von Hand")
add("; geaenderte Fassung wird mit --from-connect gezeichnet; dauerhafte")
add("; Korrekturen gehoeren nach cfg/tro_overrides.ini.")
add("; Wird bei jedem Lauf mit --connect neu geschrieben - nicht von Hand")
add("; aendern, Korrekturen gehoeren in die Ableitung (lib/tro_flow.py).")
add(";")
add("; [<Anlage>.nodes] TRO = <Typ> | <Bezeichnung> | <Bauteile>")
add("; SYSn = Materialeingang/-ausgang (kein TRO)")
@@ -2250,11 +2175,6 @@ def render_doc(
add(f"| TRO-Verbindungen | {len(analysis.edges)} |")
add(f"| Nicht verbundene TROs | {len(analysis.isolated)} |")
add(f"| Hinweise | {len(analysis.findings)} |")
if analysis.overrides.applied:
add(f"| Manuelle Typkorrekturen | {len(analysis.overrides.applied)} |")
if analysis.overrides.open_points:
add(f"| Offene Punkte (noch kein TRO) | "
f"{len(analysis.overrides.open_points)} |")
add("")
add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe | Symbol |")
add("|---|---|---:|---|---|---|")
@@ -2360,56 +2280,7 @@ def render_doc(
add("_Keine._")
add("")
add("## 6. Manuelle Korrekturen und offene Punkte")
add("")
overrides = analysis.overrides
if not overrides and not overrides.warnings:
add("_Keine._ Alles unten Stehende ist rein aus dem Layout abgeleitet.")
add("")
else:
add(f"Quelle: `{overrides.source}`" if overrides.source
else "_Keine Override-Datei gefunden._")
add("")
if overrides.applied:
add("### 6.1 Angewandte Typkorrekturen")
add("")
add("Diese Typen stammen **nicht** aus dem Layout, sondern aus der "
"Begehung bzw. dem Review - im Layout sind sie nicht erkennbar.")
add("")
add("| Korrektur | Begruendung |")
add("|---|---|")
for entry in overrides.applied:
head, _, tail = entry.partition(" (")
add(f"| `{head}` | {tail[:-1] if tail.endswith(')') else tail} |")
add("")
if overrides.open_points:
add("### 6.2 Offene Punkte - noch kein TRO")
add("")
add("Im Review gemeldete Stellen. Die TRO-Struktur ist hier "
"**bewusst unveraendert**; die Punkte sind nur markiert (auch in der "
"annotierten Zeichnung, Layer `TRO_OPENPOINT`), damit sie in BricsCAD "
"geprueft werden koennen.")
add("")
add("| Punkt | X [mm] | Y [mm] | Z [mm] | erwartet | heute Teil von | Anmerkung |")
add("|---|---:|---:|---:|---|---|---|")
for point in overrides.open_points:
z_text = f"{point.z:.0f}" if point.z is not None else "-"
expect = f"`{point.expect}`" if point.expect else "-"
owner = f"`{point.belongs_to}`" if point.belongs_to else "-"
add(f"| {point.name} | {point.x:.0f} | {point.y:.0f} | {z_text} "
f"| {expect} | {owner} | {point.note or '-'} |")
add("")
if overrides.warnings:
add("### 6.3 Probleme mit der Override-Datei")
add("")
for warning in overrides.warnings:
add(f"- {warning}")
add("")
add("## 7. Ableitungsregeln und Typdefinition")
add("## 6. Ableitungsregeln und Typdefinition")
add("")
add("Ein TRO ist die Steuerungslogik **einer Uebergabestelle**. Ausgangspunkt "
"ist der Separator und das, was an ihm haengt (Host-Objekt, Scanner).")
@@ -2516,29 +2387,6 @@ def report(
print("FEHLER: TRO-Typen ohne Eintrag in TRO_CATALOG: "
+ ", ".join(analysis.unknown_types))
overrides = analysis.overrides
if overrides.applied:
print("")
print(f"Manuelle Typkorrekturen ({len(overrides.applied)}) aus "
f"{overrides.source}:")
for entry in overrides.applied:
print(f" * {entry}")
if overrides.open_points:
print("")
print(f"Offene Punkte ({len(overrides.open_points)}) - nur markiert, "
f"TRO-Struktur unveraendert:")
for point in overrides.open_points:
expect = f" erwartet {point.expect}" if point.expect else ""
owner = f", heute Teil von {point.belongs_to}" if point.belongs_to else ""
print(f" o {point.name} bei {point.describe_position()}{expect}{owner}")
if overrides.warnings:
print("")
print(f"Probleme mit {overrides.source} ({len(overrides.warnings)}):")
for warning in overrides.warnings:
print(f" ! {warning}")
if analysis.findings:
print("")
print(f"Hinweise ({len(analysis.findings)}):")
@@ -2611,35 +2459,6 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"schreiben: Knoten (TRO = Typ), Verbindungen, Systemgrenzen und je "
"TRO Vorgaenger/Nachfolger.",
)
parser.add_argument(
"--from-connect",
dest="from_connect",
nargs="?",
const="",
default=None,
metavar="INI",
help="Typen und Verbindungen aus einer (von Hand geaenderten) connect.ini "
"uebernehmen und damit DOT/SVG/Doku erzeugen. Ohne Wert: "
"<name>_tro_connect.ini in %%SKEL_RESULTS%%. Die connect.ini wird "
"dabei nicht ueberschrieben.",
)
parser.add_argument(
"--geometry",
action="store_true",
help="Separator-Host und Weichen-Fanout per STRtree direkt aus den "
"Bauteil-Weltkoordinaten bestimmen (lib/tro_geometry.py) statt "
"aus den CSV-Spalten 'Zuordnung'/'Nachbarn' - unabhaengig davon, "
"ob diese Spalten fuer die Eingabedatei luecken haben.",
)
parser.add_argument(
"--geo-tol-mm",
type=float,
default=tro_geometry_default_tolerance_mm(),
metavar="MM",
help="Nur mit --geometry: Toleranz (mm), innerhalb derer zwei "
"Anschlusspunkte als derselbe Uebergabepunkt gelten. "
"Standard: %(default)s",
)
parser.add_argument(
"--true-scale",
dest="true_scale",
@@ -2648,34 +2467,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
f"Leerstrecken ueber {COORD_MAX_LUECKE} mm zwischen den Knoten "
"(Reihenfolge und Nachbarschaft bleiben erhalten).",
)
parser.add_argument(
"--use-overrides",
dest="use_overrides",
action="store_true",
help="Manuelle Korrekturen aus cfg/tro_overrides.ini anwenden. Standard "
"ist AUS: TROs, Typen und Verbindungen kommen rein aus dem Layout "
"(die INI-Abschnitte haengen an laufenden TRO-Nummern und veralten "
"mit jedem neuen Export).",
)
parser.add_argument(
"--no-overrides",
dest="use_overrides",
action="store_false",
help=argparse.SUPPRESS,
)
return parser.parse_args(argv)
def tro_geometry_default_tolerance_mm() -> float:
"""Default fuer --geo-tol-mm; importiert tro_geometry nur bei Bedarf, damit
--help/normale Laeufe ohne --geometry kein shapely brauchen."""
try:
from tro_geometry import DEFAULT_TOLERANCE_MM
except ImportError:
return 50.0
return DEFAULT_TOLERANCE_MM
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
@@ -2709,48 +2503,15 @@ def main(argv: list[str] | None = None) -> int:
print(f"FEHLER: {csv_file} enthaelt keine Flussobjekte.", file=sys.stderr)
return 1
resolve_host = count_exits = None
method = None
if args.geometry:
try:
from tro_geometry import make_geometric_resolvers
except ImportError as exc:
print(f"FEHLER: --geometry braucht shapely (pip install shapely): {exc}",
file=sys.stderr)
return 1
resolve_host, count_exits = make_geometric_resolvers(elements, tol_mm=args.geo_tol_mm)
method = f"geometrisch (STRtree, Toleranz {args.geo_tol_mm:.0f}mm)"
sepliste_counts: dict[str, int] = {}
raw_json_file = find_sibling_raw_json(csv_file)
if raw_json_file is not None:
sepliste_counts = load_sepliste_counts(raw_json_file, warnings)
if args.use_overrides:
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg"))
else:
overrides = Overrides()
stem = csv_file.stem + ("_geo" if args.geometry else "")
stem = csv_file.stem
connect_target = results / f"{stem}_tro_connect.ini"
connect_source: Path | None = None
if args.from_connect is not None:
connect_source = Path(args.from_connect) if args.from_connect else connect_target
if not connect_source.is_absolute() and connect_source.parent == Path("."):
connect_source = results / connect_source
if not connect_source.is_file():
print(f"FEHLER: connect.ini nicht gefunden: {connect_source}", file=sys.stderr)
print(" Erst mit --connect erzeugen.", file=sys.stderr)
return 1
try:
overrides = load_connect_ini(connect_source, overrides)
except ValueError as exc:
print(f"FEHLER: {exc}", file=sys.stderr)
return 1
analysis = analyse_tros(graph, elements, overrides,
resolve_host=resolve_host, count_exits=count_exits,
method=method, sepliste_counts=sepliste_counts)
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts)
if not analysis.tros:
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
f"ableitbar.", file=sys.stderr)
@@ -2777,7 +2538,7 @@ def main(argv: list[str] | None = None) -> int:
)
connect_file = None
if args.connect and connect_source is None:
if args.connect:
connect_target.write_text(
render_connect_ini(analysis, section=stem, source=csv_file.name),
encoding="utf-8",
@@ -2809,11 +2570,6 @@ def main(argv: list[str] | None = None) -> int:
warnings, engine=engine)
if connect_file:
print(f"connect.ini = {connect_file}")
if connect_source:
print(f"Gezeichnet aus = {connect_source}")
if args.connect and connect_source is not None:
print("HINWEIS: --connect zusammen mit --from-connect - die connect.ini "
"wird nicht ueberschrieben.")
if svg_error:
print(f"FEHLER: SVG nicht erzeugt: {svg_error}", file=sys.stderr)