Stabile TRO-IDs: ID-Datei je Anlage, Wiederfinden ueber die Lage
- lib/tro_ids.py: cfg/<csv>_tro_ids.ini lesen/schreiben, TROs ueber die Lage ihrer Separatoren wiederfinden, neue IDs vergeben, Weggefallene melden - tro_flow: Nummerierung entlang des Materialflusses, Schalter --ids, --renumber, --id-tolerance; Bericht der IDs - tro_annotate: liest dieselbe ID-Datei (nur lesend) - Tests tests/test_tro_ids.py, Doku Python_Scripts.md, Konzept (Stufe 1 erledigt) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef57a2218e
commit
dd2d98f2ee
+186
-8
@@ -90,6 +90,14 @@ Ausgabe (alles in SKEL_RESULTS)
|
||||
fuer scl_skeleton.py
|
||||
<name>_tro_errors.md nur wenn TRO-Fehler gefunden wurden
|
||||
|
||||
TRO-IDs (ID-Datei in SKEL_CFG)
|
||||
------------------------------
|
||||
Die IDs kommen aus %SKEL_CFG%/<name>_tro_ids.ini (lib/tro_ids.py): jeder TRO
|
||||
wird ueber die Lage seiner Separatoren wiedergefunden und behaelt seine ID,
|
||||
auch wenn die SPS-Programmierer sie umbenannt haben. Fehlt die Datei, werden
|
||||
die TROs entlang des Materialflusses durchnummeriert und die Datei angelegt;
|
||||
neue TROs werden angehaengt. --renumber nummeriert alles neu.
|
||||
|
||||
Aufruf ueber bin/tro_flow.bat bzw. bin/tro_flow.sh.
|
||||
"""
|
||||
|
||||
@@ -126,6 +134,16 @@ from material_flow import (
|
||||
resolve_input,
|
||||
)
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from tro_ids import (
|
||||
DEFAULT_TOLERANCE,
|
||||
TroIds,
|
||||
assign_ids,
|
||||
ids_path,
|
||||
insert_entries,
|
||||
other_sections,
|
||||
render_ids,
|
||||
render_new_entries,
|
||||
)
|
||||
from tro_catalog import (
|
||||
CATALOG_AS_OF,
|
||||
ITEM_FB,
|
||||
@@ -655,6 +673,7 @@ def derive_tros(
|
||||
findings: list[str],
|
||||
*,
|
||||
sepliste_counts: dict[str, int] | None = None,
|
||||
tro_ids: TroIds | None = None,
|
||||
) -> list[DerivedTro]:
|
||||
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden.
|
||||
|
||||
@@ -666,6 +685,10 @@ def derive_tros(
|
||||
der CSV abgeleitete Separatorenzahl eines Strecke/Gefaellestrecke-Hosts
|
||||
nicht mit dieser Zahl ueberein, wird ein Finding angehaengt; die TRO-Daten
|
||||
selbst bleiben unangetastet (CSV bleibt die massgebliche Quelle).
|
||||
|
||||
tro_ids (optional): ID-Datei der Anlage. Ohne sie werden die TROs entlang
|
||||
des Materialflusses neu durchnummeriert; mit ihr behalten wiedergefundene
|
||||
TROs ihre ID (Ergebnis in tro_ids.result).
|
||||
"""
|
||||
def resolve_host(sep: Element, findings: list[str]) -> str | None:
|
||||
return _default_resolve_host(sep, findings, graph)
|
||||
@@ -1156,11 +1179,16 @@ def derive_tros(
|
||||
tro.reason = f"{tro.reason}; {len(exits)} abgehende Wege -> Weiche"
|
||||
tro.confidence = "niedrig"
|
||||
|
||||
# 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}"
|
||||
# 5) IDs: Reihenfolge entlang des Materialflusses, wiedergefundene TROs
|
||||
# behalten ihre ID aus der ID-Datei (siehe lib/tro_ids.py)
|
||||
tros.sort(key=_flow_key(graph, elements_by_id))
|
||||
anchors = separator_anchors(elements)
|
||||
if tro_ids is None:
|
||||
assign_ids(tros, anchors, [], renumber=True)
|
||||
else:
|
||||
tro_ids.result = assign_ids(
|
||||
tros, anchors, tro_ids.entries,
|
||||
tolerance=tro_ids.tolerance, renumber=tro_ids.renumber)
|
||||
|
||||
# 6) Scanner am Separator: 1Sep_SSCC laesst sich mechanisch nicht erkennen
|
||||
for tro in tros:
|
||||
@@ -1174,6 +1202,56 @@ def derive_tros(
|
||||
return tros
|
||||
|
||||
|
||||
def separator_anchors(elements: list[Element]) -> dict[str, tuple[float, float]]:
|
||||
"""Separator-TeileId -> (x, y) in mm, Bezugslage fuer die ID-Datei."""
|
||||
return {e.teile_id: (e.position[0], e.position[1])
|
||||
for e in elements if e.kind == "Separator" and e.position}
|
||||
|
||||
|
||||
def _flow_rank(graph: Graph) -> dict[str, int]:
|
||||
"""Knoten -> Rang entlang des Materialflusses (Tiefensuche).
|
||||
|
||||
Start an den Materialeingaengen (Beladung/Eingang), danach an jedem noch
|
||||
nicht erreichten Knoten - eine geschlossene Anlage ohne Station wird so
|
||||
trotzdem vollstaendig erfasst. Gleichrangige Nachfolger nach Knotennamen.
|
||||
"""
|
||||
succ: dict[str, list[str]] = {}
|
||||
for edge in graph.edges:
|
||||
succ.setdefault(edge.src, []).append(edge.dst)
|
||||
starts = sorted(n for n, node in graph.nodes.items() if node.element.is_entry)
|
||||
rank: dict[str, int] = {}
|
||||
for start in starts + sorted(graph.nodes):
|
||||
stack = [start]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if node in rank:
|
||||
continue
|
||||
rank[node] = len(rank)
|
||||
stack.extend(n for n in sorted(succ.get(node, []), reverse=True)
|
||||
if n not in rank)
|
||||
return rank
|
||||
|
||||
|
||||
def _flow_key(graph: Graph, elements_by_id: dict[str, Element]):
|
||||
"""Sortierschluessel fuer TROs entlang des Materialflusses.
|
||||
|
||||
Erst der Rang des Host-Knotens, dann die Lage des Separators in der
|
||||
TrackIds-Kette des Hosts (Kreisel: Umlaufrichtung, Strecke: Baureihenfolge).
|
||||
"""
|
||||
rank = _flow_rank(graph)
|
||||
|
||||
def key(tro: DerivedTro) -> tuple:
|
||||
host_rank = min((rank.get(h, len(rank)) for h in tro.hosts),
|
||||
default=len(rank))
|
||||
sep = tro.separators[0] if tro.separators else ""
|
||||
host = elements_by_id.get(tro.hosts[0]) if tro.hosts else None
|
||||
track = host.track_ids if host else []
|
||||
in_track = track.index(sep) if sep in track else len(track)
|
||||
return (host_rank, in_track, sep)
|
||||
|
||||
return key
|
||||
|
||||
|
||||
# Typnamen fuer Systemgrenz-Knoten (siehe derive_boundaries) - bewusst NICHT
|
||||
# in TRO_CATALOG: eine Be-/Entladestation (BTMT oder generisches Eingang/
|
||||
# Ausgang-Symbol) ist kein Steuerobjekt mit eigenem FB-Baustein, nur der
|
||||
@@ -1781,11 +1859,15 @@ def analyse_tros(
|
||||
elements: list[Element],
|
||||
*,
|
||||
sepliste_counts: dict[str, int] | None = None,
|
||||
tro_ids: TroIds | None = None,
|
||||
) -> Analysis:
|
||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen."""
|
||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
||||
|
||||
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu nummerieren.
|
||||
"""
|
||||
analysis = Analysis()
|
||||
analysis.tros = derive_tros(graph, elements, analysis.findings,
|
||||
sepliste_counts=sepliste_counts)
|
||||
sepliste_counts=sepliste_counts, tro_ids=tro_ids)
|
||||
analysis.boundaries = derive_boundaries(elements, graph)
|
||||
# Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen:
|
||||
# link_tros() registriert eine Kante nur zu einem Knoten, der Host eines
|
||||
@@ -2638,6 +2720,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
" 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"
|
||||
" tro_flow.bat --file mubea_export.csv --ids mubea_tro_ids.ini --json\n"
|
||||
" tro_flow.bat --file mubea.csv --renumber\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -2694,9 +2778,93 @@ 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(
|
||||
"--ids",
|
||||
metavar="DATEI",
|
||||
help="ID-Datei der Anlage (Name in %%SKEL_CFG%% oder vollstaendiger "
|
||||
"Pfad). Standard: <name>_tro_ids.ini in %%SKEL_CFG%%. Fehlt sie, "
|
||||
"wird sie angelegt.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--renumber",
|
||||
action="store_true",
|
||||
help="Alle TRO-IDs neu entlang des Materialflusses vergeben und die "
|
||||
"Abschnitt [tro] der ID-Datei neu schreiben (fruehe Projektphase - "
|
||||
"umbenannte IDs gehen verloren, andere Abschnitte bleiben).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--id-tolerance",
|
||||
dest="id_tolerance",
|
||||
type=float,
|
||||
default=DEFAULT_TOLERANCE,
|
||||
metavar="MM",
|
||||
help="Hoechster Abstand (mm) zwischen Eintrag der ID-Datei und "
|
||||
"Separator, bis zu dem ein TRO als wiedergefunden gilt. "
|
||||
"Standard: %(default)s",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def resolve_ids_file(arg: str | None, csv_file: Path) -> Path:
|
||||
"""Pfad der ID-Datei: --ids (Name in SKEL_CFG oder Pfad), sonst Standard."""
|
||||
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||
if not arg:
|
||||
return ids_path(csv_file.stem, cfg_dir)
|
||||
path = Path(arg)
|
||||
if path.is_absolute() or path.parent != Path("."):
|
||||
return path
|
||||
return cfg_dir / path
|
||||
|
||||
|
||||
def write_ids_file(tro_ids: TroIds, analysis: Analysis, elements: list[Element],
|
||||
source: str) -> str:
|
||||
"""ID-Datei anlegen, neu schreiben oder neue TROs anhaengen.
|
||||
|
||||
Rueckgabe: kurze Beschreibung fuer den Bericht.
|
||||
"""
|
||||
result = tro_ids.result
|
||||
if result is None:
|
||||
return "unveraendert"
|
||||
anchors = separator_anchors(elements)
|
||||
if result.renumbered:
|
||||
tro_ids.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
rest = other_sections(tro_ids.path)
|
||||
text = render_ids(analysis.tros, anchors, source)
|
||||
tro_ids.path.write_text(text + (f"\n{rest}\n" if rest else ""),
|
||||
encoding="utf-8")
|
||||
return "neu geschrieben" if tro_ids.exists else "angelegt"
|
||||
if result.new:
|
||||
insert_entries(tro_ids.path,
|
||||
render_new_entries(analysis.tros, anchors, result.new, source))
|
||||
return f"{len(result.new)} neue(r) TRO(s) angehaengt"
|
||||
return "unveraendert"
|
||||
|
||||
|
||||
def report_ids(tro_ids: TroIds, status: str) -> None:
|
||||
"""Abschnitt TRO-IDs im Konsolenbericht."""
|
||||
result = tro_ids.result
|
||||
if result is None:
|
||||
return
|
||||
print(f"TRO-IDs = {tro_ids.path} ({status})")
|
||||
if result.renumbered:
|
||||
grund = "--renumber" if tro_ids.renumber else "keine ID-Datei"
|
||||
print(f" neu entlang des Materialflusses nummeriert "
|
||||
f"({grund})")
|
||||
return
|
||||
print(f" wiedergefunden: {len(result.kept)}, "
|
||||
f"neu: {len(result.new)}, weggefallen: {len(result.missing)} "
|
||||
f"(Toleranz {tro_ids.tolerance:g} mm)")
|
||||
if result.new:
|
||||
print(f" + neu (vorlaeufige ID, in der ID-Datei umbenennbar): "
|
||||
f"{', '.join(result.new)}")
|
||||
for entry in result.missing:
|
||||
merged = result.merged_into.get(entry.tro_id)
|
||||
grund = (f"Separator gehoert jetzt zu {merged}" if merged else
|
||||
f"kein Separator innerhalb {tro_ids.tolerance:g} mm")
|
||||
print(f" - weggefallen: {entry.tro_id} bei {entry.x:.1f}, {entry.y:.1f} "
|
||||
f"({grund}; Eintrag bleibt stehen, ID wird nicht wiederverwendet)")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
|
||||
@@ -2739,11 +2907,20 @@ def main(argv: list[str] | None = None) -> int:
|
||||
connect_target = results / f"{stem}_tro_connect.ini"
|
||||
json_target = results / f"{stem}_tro.json"
|
||||
|
||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts)
|
||||
try:
|
||||
tro_ids = TroIds.load(resolve_ids_file(args.ids, csv_file),
|
||||
tolerance=args.id_tolerance, renumber=args.renumber)
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||
tro_ids=tro_ids)
|
||||
if not analysis.tros:
|
||||
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
||||
f"ableitbar.", file=sys.stderr)
|
||||
return 1
|
||||
ids_status = write_ids_file(tro_ids, analysis, elements, csv_file.name)
|
||||
|
||||
dot_file = results / f"{stem}_tro_flow.dot"
|
||||
svg_file = results / f"{stem}_tro_flow.svg"
|
||||
@@ -2806,6 +2983,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
report(analysis, csv_file, dot_file, created_svg, error_file, doc_file,
|
||||
warnings, engine=engine)
|
||||
report_ids(tro_ids, ids_status)
|
||||
if connect_file:
|
||||
print(f"connect.ini = {connect_file}")
|
||||
if json_file:
|
||||
|
||||
Reference in New Issue
Block a user