TRO-Fluss: tro_overrides.ini standardmaessig aus, ungelistete Strecken-Separatoren lagerichtig verketten

- tro_flow/tro_annotate: cfg/tro_overrides.ini nur noch mit --use-overrides
  (die Abschnitte haengen an laufenden TRO-Nummern und am CSV-Namen und
  veralten mit jedem Export; [export.csv.connections] ersetzte alle
  abgeleiteten Kanten und liess TRO13-20 unverbunden)
- link_tros: Separatoren mit Zuordnung auf eine Strecke, die in keiner
  TrackIds-Liste stehen, per Projektion AS->ES in die Strecken-Kette
  einsortieren (Mubea VF_3: 0037/0038 erzeugten je 9 Fehlkanten;
  mubea 38 -> 22 Verbindungen, HundM05 unveraendert)
- tro_catalog: typ-eigene Fuellfarbe je TRO-Typ, Mermaid-classDefs je Typ
- tro_overrides/tro_flow: connect.ini zurueck einlesen (--from-connect)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-01 10:50:40 +02:00
co-authored by Claude Opus 5.5
parent 1479519bdc
commit 02db9a95b0
4 changed files with 381 additions and 26 deletions
+219 -8
View File
@@ -142,6 +142,7 @@ from tro_overrides import (
apply_merges,
apply_overrides,
apply_splits,
load_connect_ini,
load_overrides,
)
@@ -1013,6 +1014,62 @@ def _tro_bahn(tro: Tro, graph: Graph, sep_assignment: dict[str, str]) -> str | N
return None
def _chain_mit_ungelisteten_separatoren(
strecke: Element,
chain: list[str],
elements: list[Element],
elements_by_id: dict[str, Element],
) -> list[str]:
"""Separatoren der Strecke, die in KEINER TrackIds-Liste stehen, lagerichtig
in die (bereits AS -> ES orientierte) Kette einsortieren.
Hintergrund (Mubea, VF_3): Separatoren 0037/0038 sind per Zuordnung der
Strecke 0015 zugeordnet und liegen physisch zwischen 0062 und 0063, fehlen
aber in deren TrackIds. Ohne Platz in der Kette bekommen ihre TROs keinen
praezisen Nachfolger, behalten die groben Contraction-Walk-Kanten und
zeigen auf jeden TRO hinter Kreisel2 Bahn R (je 9 Fehlkanten).
Einsortiert wird ueber die Projektion auf die Gerade erstes -> letztes
Kettenelement. Steht ein Separator in irgendeiner TrackIds-Liste (z. B.
eines Kreisels), gehoert er dorthin und bleibt hier draussen.
"""
if len(chain) < 2:
return chain
gelistet = {tid for e in elements for tid in e.track_ids}
fehlend = [
e for e in elements
if e.kind == "Separator" and e.assignment == strecke.teile_id
and e.teile_id not in gelistet and e.position
]
if not fehlend:
return chain
anfang = elements_by_id.get(chain[0])
ende = elements_by_id.get(chain[-1])
if not (anfang and ende and anfang.position and ende.position):
return chain
dx = ende.position[0] - anfang.position[0]
dy = ende.position[1] - anfang.position[1]
if dx == 0 and dy == 0:
return chain
def _t(pos) -> float:
return (pos[0] - anfang.position[0]) * dx + (pos[1] - anfang.position[1]) * dy
# Kettenelemente ohne Position behalten ihren Platz: sie erben den Wert des
# Vorgaengers, damit die Sortierung stabil bleibt.
werte: list[tuple[float, int, str]] = []
letzter = float("-inf")
for i, tid in enumerate(chain):
el = elements_by_id.get(tid)
if el is not None and el.position:
letzter = _t(el.position)
werte.append((letzter, i, tid))
for e in fehlend:
werte.append((_t(e.position), len(chain), e.teile_id))
werte.sort(key=lambda w: (w[0], w[1]))
return [tid for _, _, tid in werte]
def link_tros(
graph: Graph, tros: list[Tro], elements: list[Element]
) -> tuple[list[tuple[str, str]], dict[tuple[str, str], str]]:
@@ -1133,6 +1190,7 @@ def link_tros(
# er ist der Kreisel-Bahn zugehoerig und dort schon (Schritt 1 unten)
# verkettet. Ohne diesen Ausschluss wuerde er faelschlich als erster
# oder letzter "Vario" gelten (Review 2026-09-21).
chain = _chain_mit_ungelisteten_separatoren(el, chain, elements, elements_by_id)
owned = [
elem_owner[tid] for tid in chain if tid in elem_owner
and not ((d := by_id[elem_owner[tid]].definition()) and d.has_item(TroItem.SWITCH))
@@ -1550,7 +1608,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
# Kantenarten. Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0
# mitten in der Anlage - daher unterhalb links vom Anlagenrand.
rows = "".join(
f'<tr><td bgcolor="{d.get_style().fill}" align="left"> {d.get_name()} </td>'
f'<tr><td bgcolor="{d.get_fill()}" color="{d.get_style().stroke}" align="left"> {d.get_name()} </td>'
f'<td align="left">{d.describe_symbol()}</td></tr>'
for d in TRO_CATALOG
)
@@ -1584,6 +1642,90 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
return "\n".join(out) + "\n"
# ---------------------------------------------------------------------------
# connect.ini (TRO-Ebene) - --connect schreibt, --from-connect liest
# ---------------------------------------------------------------------------
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.
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.
"""
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(";")
add("; [<Anlage>.nodes] TRO = <Typ> | <Bezeichnung> | <Bauteile>")
add("; SYSn = Materialeingang/-ausgang (kein TRO)")
add("; [<Anlage>.connections] cNNN = <von> -> <nach> | <JamArea> | <kind>")
add("; kind: normal | transfer | bahn | weiche | umlauf | unbestimmt | gesetzt")
add("; [<Anlage>.externals] eNNN = SYSn -> <TRO> | <JamArea> | <kind> (Zulauf)")
add("; eNNN = <TRO> -> SYSn | <JamArea> | <kind> (Ablauf)")
add("; [<Anlage>.tros] TRO = <Typ> | prev: ... | next: ...")
add("; nur Uebersicht - beim Einlesen ignoriert")
add("; ============================================================")
add("")
add(f"[{section}.nodes]")
for tro in analysis.tros:
teile = []
if tro.separators:
teile.append(f"Sep {', '.join(tro.separators)}")
if tro.scanners:
teile.append(f"Scan {', '.join(tro.scanners)}")
add(f"{tro.tro_id} = {tro.type_name} | {tro.label}"
+ (f" | {'; '.join(teile)}" if teile else ""))
for boundary in analysis.boundaries:
add(f"{boundary.tro_id} = {boundary.type_name} | {boundary.label}")
add("")
boundary_ids = {b.tro_id for b in analysis.boundaries}
intern = [e for e in analysis.edges
if e[0] not in boundary_ids and e[1] not in boundary_ids]
extern = [e for e in analysis.edges
if e[0] in boundary_ids or e[1] in boundary_ids]
add(f"[{section}.connections]")
for i, (src, dst) in enumerate(intern, start=1):
kind = analysis.edge_kinds.get((src, dst)) or "normal"
add(f"c{i:03d} = {src} -> {dst} | | {kind}")
add("")
add(f"[{section}.externals]")
if not extern and boundary_ids:
add(f"; Systemgrenzen ohne Verbindung: {', '.join(sorted(boundary_ids))}")
elif not extern:
add("; keine Be-/Entladestation im Layout - Materialein-/ausgang offen")
for i, (src, dst) in enumerate(extern, start=1):
kind = analysis.edge_kinds.get((src, dst)) or "normal"
add(f"e{i:03d} = {src} -> {dst} | | {kind}")
add("")
add(f"[{section}.tros]")
for tro in analysis.tros:
prev = ", ".join(tro.predecessors) or "-"
nxt = ", ".join(tro.successors) or "-"
line = f"{tro.tro_id} = {tro.type_name} | prev: {prev} | next: {nxt}"
if tro.colocated:
line += f" | gleiche Bahn: {', '.join(tro.colocated)}"
add(line)
add("")
return "\n".join(out) + "\n"
# ---------------------------------------------------------------------------
# Fehlerdatei
# ---------------------------------------------------------------------------
@@ -1669,9 +1811,9 @@ def _tro_mermaid(analysis: Analysis) -> list[str]:
for tro in analysis.tros:
definition = tro.definition()
group = definition.get_group() if definition else "ext"
css_class = definition.mermaid_class() if definition else "ext"
label = f"{tro.tro_id}<br/>{tro.type_name}<br/>{_mermaid_text(tro.label)}"
out.append(f' {tro.tro_id}["{label}"]:::{group}')
out.append(f' {tro.tro_id}["{label}"]:::{css_class}')
for boundary in analysis.boundaries:
label = f"{boundary.tro_id}<br/>{boundary.type_name}<br/>{_mermaid_text(boundary.label)}"
@@ -1921,12 +2063,12 @@ def render_doc(
add(f"Typen, FB-Bausteine, Bauteile und Farben stammen vollstaendig aus "
f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):")
add("")
add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol |")
add("|---:|---|---|---|---|---|")
add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol | Fuellfarbe |")
add("|---:|---|---|---|---|---|---|")
for index, definition in enumerate(TRO_CATALOG, start=1):
add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` "
f"| {definition.describe_items()} | {definition.get_group()} "
f"| {definition.describe_symbol()} |")
f"| {definition.describe_symbol()} | `{definition.get_fill()}` |")
add("")
add("| Bauteil | Sub-FB |")
add("|---|---|")
@@ -2085,6 +2227,25 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help="Dokumentation der TROs als Markdown in %%SKEL_RESULTS%% erzeugen "
"(TRO-Liste, Verbindungen, Typdefinition).",
)
parser.add_argument(
"--connect",
action="store_true",
help="TRO-Topologie als <name>_tro_connect.ini nach %%SKEL_RESULTS%% "
"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",
@@ -2102,6 +2263,21 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"Anschlusspunkte als derselbe Uebergabepunkt gelten. "
"Standard: %(default)s",
)
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)
@@ -2165,7 +2341,28 @@ def main(argv: list[str] | None = None) -> int:
if raw_json_file is not None:
sepliste_counts = load_sepliste_counts(raw_json_file, warnings)
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg"))
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 "")
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)
@@ -2174,7 +2371,6 @@ def main(argv: list[str] | None = None) -> int:
f"ableitbar.", file=sys.stderr)
return 1
stem = csv_file.stem + ("_geo" if args.geometry else "")
dot_file = results / f"{stem}_tro_flow.dot"
svg_file = results / f"{stem}_tro_flow.svg"
doc_target = results / f"{stem}_tro_doc.md"
@@ -2194,6 +2390,14 @@ def main(argv: list[str] | None = None) -> int:
encoding="utf-8",
)
connect_file = None
if args.connect and connect_source is None:
connect_target.write_text(
render_connect_ini(analysis, section=stem, source=csv_file.name),
encoding="utf-8",
)
connect_file = connect_target
has_errors = write_error_file(analysis, csv_file, error_target)
error_file = error_target if has_errors else None
@@ -2217,6 +2421,13 @@ def main(argv: list[str] | None = None) -> int:
report(analysis, csv_file, dot_file, created_svg, error_file, doc_file,
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)