model.py: gemeinsames TRO-Modell; Hauptweg CSV -> tro_flow --json -> scl_skeleton
- lib/model.py (neu, pydantic): TroBase -> DerivedTro, Connection, OpenPoint, Analysis (aus tro_flow verschoben), TroDocument = TRO-JSON (Schema sps_skel-tro-v2). items/fbType werden immer ueber den Typ aus tro_catalog gelesen, nie gespeichert; abweichende Werte in einer JSON sind ein Fehler. JSON-Schluessel wie bisher (id, type, fbType ...), extra="forbid". - tro_flow: Tro/Analysis aus model.py; neuer Schalter --json schreibt <name>_tro.json (TroDocument) - scl_skeleton: liest die JSON ueber TroDocument statt per Dict-Zugriff, Bauteile ueber TroItem-Konstanten; ungueltige JSON -> Exit 1 ohne SCL, alte tro_extract-JSON (v1) mit Hinweis auf tro_flow --json - tro_annotate: nur noch Ansicht; derselbe analyse_tros-Aufruf wie tro_flow (inkl. sepliste-Gegenprobe), damit Zeichnung und JSON uebereinstimmen - tro_extract: geparkt (unveraendert) - tests/test_model.py + conftest.py, pytest in requirements.txt aktiviert - Doku: Python_Scripts.md (§6b model.py), Commandlines.md, CLAUDE.md, Aufrufkommentare in bin/ (scl_skeleton.bat zeigte tro_flow-Aufrufe) Geprueft: alle tro_flow/tro_annotate-Ausgaben (8 CSVs) bis auf Zeitstempel identisch zum vorherigen Stand; scl_skeleton alt/neu auf derselben JSON byte-identisch (export, mubea, HundM05); 10 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
90cafec99c
commit
97a0db6707
+51
-113
@@ -86,6 +86,8 @@ Ausgabe (alles in SKEL_RESULTS)
|
||||
<name>_tro_flow.dot immer (mit --use-cords lagerichtige pos-Attribute)
|
||||
<name>_tro_flow.svg mit --tosvg (dot, mit --use-cords neato -n)
|
||||
<name>_tro_doc.md mit --doc
|
||||
<name>_tro.json mit --json: TRO-JSON (model.TroDocument), Eingabe
|
||||
fuer scl_skeleton.py
|
||||
<name>_tro_errors.md nur wenn TRO-Fehler gefunden wurden
|
||||
|
||||
Aufruf ueber bin/tro_flow.bat bzw. bin/tro_flow.sh.
|
||||
@@ -98,7 +100,6 @@ import csv
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
@@ -124,13 +125,13 @@ from material_flow import (
|
||||
read_elements,
|
||||
resolve_input,
|
||||
)
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from tro_catalog import (
|
||||
CATALOG_AS_OF,
|
||||
ITEM_FB,
|
||||
STYLE_EXT,
|
||||
STYLES,
|
||||
TRO_CATALOG,
|
||||
TroDefinition,
|
||||
TroItem,
|
||||
get_tro,
|
||||
mermaid_classdefs,
|
||||
@@ -177,82 +178,6 @@ COORD_MAX_LUECKE = 1500
|
||||
LEGEND_MARGIN = 3000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Datenmodell
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tro:
|
||||
"""Eine ermittelte Uebergabestelle."""
|
||||
|
||||
tro_id: str
|
||||
type_name: str
|
||||
label: str
|
||||
hosts: list[str]
|
||||
separators: list[str]
|
||||
scanners: list[str]
|
||||
reason: str
|
||||
confidence: str
|
||||
fb_block: str = ""
|
||||
items: dict[str, int] = field(default_factory=dict)
|
||||
successors: list[str] = field(default_factory=list)
|
||||
predecessors: list[str] = field(default_factory=list)
|
||||
colocated: list[str] = field(default_factory=list)
|
||||
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde
|
||||
position: tuple[float, float, float] | None = None
|
||||
position_from: str = ""
|
||||
position_points: int = 0
|
||||
|
||||
@property
|
||||
def x(self) -> float | None:
|
||||
return self.position[0] if self.position else None
|
||||
|
||||
@property
|
||||
def y(self) -> float | None:
|
||||
return self.position[1] if self.position else None
|
||||
|
||||
@property
|
||||
def z(self) -> float | None:
|
||||
return self.position[2] if self.position else None
|
||||
|
||||
def describe_position(self) -> str:
|
||||
"""Koordinate als Text, z. B. '1030, 4700'."""
|
||||
if not self.position:
|
||||
return "-"
|
||||
return f"{self.position[0]:.0f}, {self.position[1]:.0f}"
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return bool(self.successors or self.predecessors or self.colocated)
|
||||
|
||||
def definition(self) -> TroDefinition | None:
|
||||
"""Typdefinition aus tro_catalog (None wenn Typ unbekannt)."""
|
||||
return get_tro(self.type_name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Analysis:
|
||||
tros: list[Tro] = field(default_factory=list)
|
||||
edges: list[tuple[str, str]] = field(default_factory=list)
|
||||
# (von, nach) -> Kantenart (transfer / umlauf / weiche / unbestimmt / normal)
|
||||
edge_kinds: dict[tuple[str, str], str] = field(default_factory=dict)
|
||||
isolated: list[Tro] = field(default_factory=list)
|
||||
unknown_types: list[str] = field(default_factory=list)
|
||||
findings: list[str] = field(default_factory=list)
|
||||
# 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.
|
||||
boundaries: list[Tro] = field(default_factory=list)
|
||||
|
||||
def by_id(self, tro_id: str) -> Tro | None:
|
||||
for tro in self.tros + self.boundaries:
|
||||
if tro.tro_id == tro_id:
|
||||
return tro
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TRO-Ermittlung
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -730,7 +655,7 @@ def derive_tros(
|
||||
findings: list[str],
|
||||
*,
|
||||
sepliste_counts: dict[str, int] | None = None,
|
||||
) -> list[Tro]:
|
||||
) -> list[DerivedTro]:
|
||||
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden.
|
||||
|
||||
Host eines Separators aus der CSV-Spalte "Zuordnung" (_default_resolve_host),
|
||||
@@ -862,7 +787,7 @@ def derive_tros(
|
||||
f"sepliste (Roh-JSON) - Gegenprobe weicht ab, bitte pruefen"
|
||||
)
|
||||
|
||||
tros: list[Tro] = []
|
||||
tros: list[DerivedTro] = []
|
||||
|
||||
# 1) Schwerkraftlinien (Gefaellestrecke mit Separator) -> Speicherblock
|
||||
lanes = sorted(
|
||||
@@ -887,7 +812,7 @@ def derive_tros(
|
||||
speicher_steuer.update(steuer)
|
||||
alle_seps = steuer + seps
|
||||
tros.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name="PinStore_Auto",
|
||||
label=f"Speicher ({len(group)} Schwerkraftlinien)",
|
||||
@@ -907,7 +832,7 @@ def derive_tros(
|
||||
for lane in group:
|
||||
lane_seps = seps_on([lane])
|
||||
tros.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name="1Sep",
|
||||
label=graph.nodes[lane].element.name,
|
||||
@@ -973,7 +898,7 @@ def derive_tros(
|
||||
# Streckengruppe genau 1 echter Kettenseparator, alle
|
||||
# uebrigen Zuordnung-Treffer sind eigenstaendige Vereinzeler).
|
||||
tros.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name="1Sep",
|
||||
label=element.name,
|
||||
@@ -1034,7 +959,7 @@ def derive_tros(
|
||||
f"Vario eingestuft (widerspricht alternierendem Muster)"
|
||||
)
|
||||
tros.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name=type_name,
|
||||
label=element.name,
|
||||
@@ -1055,7 +980,7 @@ def derive_tros(
|
||||
node = graph.nodes[host]
|
||||
lane_name = _merkmal(node.element.merkmale, "Name") or node.element.name
|
||||
tros.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name="1Sep",
|
||||
label=f"Separator {sep_id} auf {lane_name} Bahn {node.lane}",
|
||||
@@ -1079,7 +1004,7 @@ def derive_tros(
|
||||
# vor), und die Strecke hat bereits reale, gerichtete Kanten zum Kreisel
|
||||
# (_apply_transfers) - darueber haengt sich die Verknuepfung (link_tros)
|
||||
# von selbst ein, ohne einen weiteren synthetischen Host zu erfinden.
|
||||
as_switch_tro: dict[str, Tro] = {} # Ausschleuselement-Id -> 1Sep1Swi
|
||||
as_switch_tro: dict[str, DerivedTro] = {} # Ausschleuselement-Id -> 1Sep1Swi
|
||||
for sep_id, (transfer, distance) in sorted(switch_hits.items()):
|
||||
if sep_id in speicher_steuer:
|
||||
continue # Teil seines Speichers (Schritt 1)
|
||||
@@ -1088,7 +1013,7 @@ def derive_tros(
|
||||
own_scanners = sorted(sep_scanners.get(sep_id, []))
|
||||
art = "Ausschleuselement"
|
||||
as_switch_tro[transfer.teile_id] = tro = (
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name="1Sep1Swi",
|
||||
label=f"Separator {sep_id} an {art} {transfer.teile_id} "
|
||||
@@ -1161,10 +1086,10 @@ def derive_tros(
|
||||
# 3c) Separatoren an einer echten ILS Weiche -> 1Sep1Swi (siehe
|
||||
# _find_weiche_separators). Host wie bei 3b: der aufgeloeste Zuordnung-Host
|
||||
# des Separators, sonst die Weiche selbst.
|
||||
weiche_tro: dict[str, Tro] = {} # Weichen-Id -> 1Sep1Swi
|
||||
weiche_tro: dict[str, DerivedTro] = {} # Weichen-Id -> 1Sep1Swi
|
||||
for sep_id, weiche in sorted(weiche_hits.items()):
|
||||
host = resolve_host(elements_by_id[sep_id], findings)
|
||||
weiche_tro[weiche.teile_id] = tro = Tro(
|
||||
weiche_tro[weiche.teile_id] = tro = DerivedTro(
|
||||
tro_id="",
|
||||
type_name="1Sep1Swi",
|
||||
label=f"Separator {sep_id} an ILS Weiche {weiche.teile_id}",
|
||||
@@ -1194,7 +1119,7 @@ def derive_tros(
|
||||
continue
|
||||
tros.remove(as_tro)
|
||||
tros.remove(w_tro)
|
||||
tros.append(Tro(
|
||||
tros.append(DerivedTro(
|
||||
tro_id="",
|
||||
type_name="2Sep2Swi",
|
||||
label=(f"Uebergabestelle {kreisel.name}: Ausschleuselement "
|
||||
@@ -1258,7 +1183,7 @@ BOUNDARY_EXIT = "Materialausgang"
|
||||
BOUNDARY_TYPES = (BOUNDARY_ENTRY, BOUNDARY_EXIT)
|
||||
|
||||
|
||||
def derive_boundaries(elements: list[Element], graph: Graph) -> list[Tro]:
|
||||
def derive_boundaries(elements: list[Element], graph: Graph) -> list[DerivedTro]:
|
||||
"""Be-/Entladestationen als sichtbare Systemgrenz-Knoten.
|
||||
|
||||
Ohne diese Knoten "verschwindet" der Materialfluss stillschweigend an der
|
||||
@@ -1286,14 +1211,14 @@ def derive_boundaries(elements: list[Element], graph: Graph) -> list[Tro]:
|
||||
getrennt von den TRO-IDs, damit auf den ersten Blick klar bleibt: das
|
||||
sind keine Steuerobjekte.
|
||||
"""
|
||||
boundaries: list[Tro] = []
|
||||
boundaries: list[DerivedTro] = []
|
||||
for element in elements:
|
||||
if not element.is_station:
|
||||
continue
|
||||
if element.teile_id not in graph.nodes:
|
||||
continue
|
||||
boundaries.append(
|
||||
Tro(
|
||||
DerivedTro(
|
||||
tro_id="",
|
||||
type_name=BOUNDARY_ENTRY if element.is_entry else BOUNDARY_EXIT,
|
||||
label=element.name,
|
||||
@@ -1349,7 +1274,7 @@ def _bahn_richtung(node) -> tuple[float, float] | None:
|
||||
return achse if mit_achse else (-achse[0], -achse[1])
|
||||
|
||||
|
||||
def _tro_bahn(tro: Tro, graph: Graph, sep_assignment: dict[str, str]) -> str | None:
|
||||
def _tro_bahn(tro: DerivedTro, graph: Graph, sep_assignment: dict[str, str]) -> str | None:
|
||||
"""Kreisel-Bahn, auf der ein TRO physisch sitzt - fuer die Positions-
|
||||
Verkettung in link_tros(). Meist einfach der TRO-Host (wenn er direkt auf
|
||||
der Bahn liegt, z. B. ein gewoehnlicher Kreisel-Separator). Bei einem
|
||||
@@ -1430,7 +1355,7 @@ def _chain_mit_ungelisteten_separatoren(
|
||||
|
||||
|
||||
def link_tros(
|
||||
graph: Graph, tros: list[Tro], elements: list[Element]
|
||||
graph: Graph, tros: list[DerivedTro], elements: list[Element]
|
||||
) -> tuple[list[tuple[str, str]], dict[tuple[str, str], str]]:
|
||||
"""
|
||||
TROs verknuepfen.
|
||||
@@ -1692,7 +1617,7 @@ def link_tros(
|
||||
# vorbei (Review 2026-09-21).
|
||||
sep_assignment = {e.teile_id: e.assignment for e in elements
|
||||
if e.kind == "Separator" and e.assignment}
|
||||
bahn_gruppen: dict[str, list[Tro]] = {}
|
||||
bahn_gruppen: dict[str, list[DerivedTro]] = {}
|
||||
for tro in tros:
|
||||
bahn = _tro_bahn(tro, graph, sep_assignment)
|
||||
if bahn and bahn.split("-")[0] not in covered_kreisel_bases:
|
||||
@@ -1814,7 +1739,7 @@ def _centroid(
|
||||
)
|
||||
|
||||
|
||||
def locate_tros(tros: list[Tro], elements: list[Element], graph: Graph) -> None:
|
||||
def locate_tros(tros: list[DerivedTro], elements: list[Element], graph: Graph) -> None:
|
||||
"""
|
||||
Jedem TRO eine Koordinate geben.
|
||||
|
||||
@@ -1872,15 +1797,8 @@ def analyse_tros(
|
||||
locate_tros(linked, elements, graph)
|
||||
analysis.edges, analysis.edge_kinds = link_tros(graph, linked, elements)
|
||||
|
||||
for tro in analysis.tros:
|
||||
definition = tro.definition()
|
||||
if definition:
|
||||
tro.fb_block = definition.get_fb_block()
|
||||
tro.items = definition.get_items()
|
||||
else:
|
||||
analysis.unknown_types.append(tro.type_name)
|
||||
|
||||
analysis.unknown_types = sorted(set(analysis.unknown_types))
|
||||
analysis.unknown_types = sorted(
|
||||
{tro.type_name for tro in analysis.tros if tro.definition() is None})
|
||||
analysis.isolated = [tro for tro in analysis.tros if not tro.connected]
|
||||
analysis.findings.sort()
|
||||
return analysis
|
||||
@@ -1891,7 +1809,7 @@ def analyse_tros(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _tro_label(tro: Tro) -> str:
|
||||
def _tro_label(tro: DerivedTro) -> str:
|
||||
lines = [f"{tro.tro_id} ({tro.type_name})", tro.label]
|
||||
if tro.fb_block:
|
||||
lines.append(tro.fb_block)
|
||||
@@ -1968,7 +1886,7 @@ def _station_positionen(
|
||||
echten Station entscheidet nur bei Gleichstand. Station ohne angehaengten
|
||||
TRO: echte Koordinate."""
|
||||
tro_by_id = {t.tro_id: t for t in analysis.tros}
|
||||
je_tro: dict[str, list[Tro]] = {}
|
||||
je_tro: dict[str, list[DerivedTro]] = {}
|
||||
ergebnis: dict[str, tuple[float, float]] = {}
|
||||
for station in analysis.boundaries:
|
||||
nachbarn = (station.successors if station.type_name == BOUNDARY_ENTRY
|
||||
@@ -2017,7 +1935,7 @@ def _station_positionen(
|
||||
return ergebnis
|
||||
|
||||
|
||||
def _tro_kurzinfo(tro: Tro) -> str:
|
||||
def _tro_kurzinfo(tro: DerivedTro) -> str:
|
||||
"""Zweizeilige Beschriftung neben dem Symbol: Typ und Bauteile."""
|
||||
if tro.type_name in BOUNDARY_TYPES:
|
||||
return _dot_label([tro.type_name, tro.label])
|
||||
@@ -2030,12 +1948,12 @@ def _tro_kurzinfo(tro: Tro) -> str:
|
||||
return _dot_label([tro.type_name, teile])
|
||||
|
||||
|
||||
def _tro_tooltip(tro: Tro) -> str:
|
||||
def _tro_tooltip(tro: DerivedTro) -> str:
|
||||
"""Vollstaendige Angaben als SVG-Tooltip (Zeilenumbruch per )."""
|
||||
return _tro_label(tro).replace("\\n", " ")
|
||||
|
||||
|
||||
def _coord_node_attrs(tro: Tro, base: dict[str, str]) -> str:
|
||||
def _coord_node_attrs(tro: DerivedTro, base: dict[str, str]) -> str:
|
||||
"""Knotenattribute fuer --use-cords: feste Groesse, kurzes Label im Symbol,
|
||||
Typ/Bauteile als xlabel daneben, alle Details als Tooltip."""
|
||||
attrs = dict(base)
|
||||
@@ -2069,7 +1987,7 @@ def _zeichen_positionen(
|
||||
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: TROs auf
|
||||
ihrer Anlagenkoordinate, mit compact die Leerstrecken gekuerzt
|
||||
(_kompakte_positionen); die Stationsbeschriftungen danach relativ zum
|
||||
angehaengten TRO (_station_positionen). Reine Darstellung - Tro.position
|
||||
angehaengten TRO (_station_positionen). Reine Darstellung - DerivedTro.position
|
||||
und die Tooltip-Koordinaten bleiben echt."""
|
||||
roh: dict[str, tuple[float, float]] = {}
|
||||
for tro in analysis.tros:
|
||||
@@ -2117,7 +2035,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
|
||||
add = out.append
|
||||
zeichen = _zeichen_positionen(analysis, compact) if use_coords else {}
|
||||
|
||||
def _pos(node: Tro) -> str:
|
||||
def _pos(node: DerivedTro) -> str:
|
||||
if use_coords:
|
||||
p = zeichen.get(node.tro_id)
|
||||
return (f', pos="{p[0] * COORD_SCALE:.2f},{p[1] * COORD_SCALE:.2f}"'
|
||||
@@ -2719,6 +2637,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
" tro_flow.bat --file mubea.csv --tosvg\n"
|
||||
" tro_flow.bat --file mubea.csv --tosvg --doc\n"
|
||||
" tro_flow.bat --file mubea.csv --tosvg --use-cords\n"
|
||||
" tro_flow.bat --file mubea.csv --json\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -2761,6 +2680,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"schreiben: Knoten (TRO = Typ), Verbindungen, Systemgrenzen und je "
|
||||
"TRO Vorgaenger/Nachfolger.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="TRO-JSON als <name>_tro.json nach %%SKEL_RESULTS%% schreiben "
|
||||
"(Modell lib/model.py, TroDocument) - Eingabe fuer scl_skeleton.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--true-scale",
|
||||
dest="true_scale",
|
||||
@@ -2812,6 +2737,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
stem = csv_file.stem
|
||||
connect_target = results / f"{stem}_tro_connect.ini"
|
||||
json_target = results / f"{stem}_tro.json"
|
||||
|
||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts)
|
||||
if not analysis.tros:
|
||||
@@ -2847,6 +2773,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
connect_file = connect_target
|
||||
|
||||
json_file = None
|
||||
if args.json:
|
||||
document = TroDocument.from_analysis(
|
||||
analysis, source=csv_file.name,
|
||||
note="Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die "
|
||||
"Zeichnung ist nur Ansicht, keine Datenquelle.",
|
||||
)
|
||||
json_target.write_text(document.to_json(), encoding="utf-8")
|
||||
json_file = json_target
|
||||
|
||||
has_errors = write_error_file(analysis, csv_file, error_target)
|
||||
error_file = error_target if has_errors else None
|
||||
|
||||
@@ -2872,6 +2808,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
warnings, engine=engine)
|
||||
if connect_file:
|
||||
print(f"connect.ini = {connect_file}")
|
||||
if json_file:
|
||||
print(f"TRO-JSON = {json_file}")
|
||||
|
||||
if svg_error:
|
||||
print(f"FEHLER: SVG nicht erzeugt: {svg_error}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user