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
+7 -2
View File
@@ -102,7 +102,7 @@ from dxf_registration import (
from material_flow import build_graph, env_dir, read_elements, resolve_input from material_flow import build_graph, env_dir, read_elements, resolve_input
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
from tro_flow import Analysis, analyse_tros from tro_flow import Analysis, analyse_tros
from tro_overrides import load_overrides from tro_overrides import Overrides, load_overrides
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Konstanten # Konstanten
@@ -782,6 +782,10 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument("--emit-lisp", action="store_true", parser.add_argument("--emit-lisp", action="store_true",
help="cad/tro_types.lsp aus dem Typkatalog neu schreiben " help="cad/tro_types.lsp aus dem Typkatalog neu schreiben "
"(Auswahlliste des Dialogs TROEDIT) und beenden.") "(Auswahlliste des Dialogs TROEDIT) und beenden.")
parser.add_argument("--use-overrides", action="store_true",
help="Manuelle Korrekturen aus cfg/tro_overrides.ini "
"anwenden. Standard ist AUS - wie bei tro_flow, "
"damit Marker und Flussdiagramm zusammenpassen.")
return parser.parse_args(argv) return parser.parse_args(argv)
@@ -834,7 +838,8 @@ def main(argv: list[str] | None = None) -> int:
print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr) print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr)
return 1 return 1
graph = build_graph(elements, csv_file.name, read_warnings) graph = build_graph(elements, csv_file.name, read_warnings)
overrides = load_overrides(csv_file.name, cfg_dir) overrides = (load_overrides(csv_file.name, cfg_dir) if args.use_overrides
else Overrides())
analysis = analyse_tros(graph, elements, overrides) analysis = analyse_tros(graph, elements, overrides)
if not analysis.tros: if not analysis.tros:
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr) print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
+54 -16
View File
@@ -231,6 +231,11 @@ class TroStyle(BaseModel):
Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist
violett. violett.
Innerhalb einer Gruppe hat jeder Typ zusaetzlich eine eigene Fuellfarbe
(TroDefinition.fill, ein Ton im Farbton der Gruppe); der Rand bleibt die
Gruppenfarbe (stroke). So ist jeder Typ an Farbe UND Form eindeutig, und
die Gruppe bleibt am Farbton/Rand lesbar.
""" """
model_config = ConfigDict(validate_assignment=True, extra="forbid", model_config = ConfigDict(validate_assignment=True, extra="forbid",
@@ -337,6 +342,12 @@ class TroDefinition(BaseModel):
min_length=1, min_length=1,
description="Zeichensymbol des Markers im CAD, z. B. 'triangle'", description="Zeichensymbol des Markers im CAD, z. B. 'triangle'",
) )
fill: str = Field(
default="",
description="Typ-eigene Fuellfarbe als Hex; leer = Fuellfarbe der Farbgruppe. "
"Der Rand bleibt immer in der Gruppenfarbe, damit die Gruppe "
"trotz eigener Typfarbe erkennbar bleibt.",
)
def __init__( def __init__(
self, self,
@@ -345,6 +356,7 @@ class TroDefinition(BaseModel):
items: dict[str, int] | None = None, items: dict[str, int] | None = None,
style: TroStyle | None = None, style: TroStyle | None = None,
symbol: str | None = None, symbol: str | None = None,
fill: str | None = None,
**data, **data,
) -> None: ) -> None:
# Positionsargumente auf Feldnamen abbilden, damit die bisherige # Positionsargumente auf Feldnamen abbilden, damit die bisherige
@@ -359,6 +371,8 @@ class TroDefinition(BaseModel):
data["style"] = style data["style"] = style
if symbol is not None: if symbol is not None:
data["symbol"] = symbol data["symbol"] = symbol
if fill is not None:
data["fill"] = fill
super().__init__(**data) super().__init__(**data)
@field_validator("items", mode="after") @field_validator("items", mode="after")
@@ -420,14 +434,32 @@ class TroDefinition(BaseModel):
"""Kurzname der Farbgruppe, z. B. 'vario'.""" """Kurzname der Farbgruppe, z. B. 'vario'."""
return self.style.group return self.style.group
def get_fill(self) -> str:
"""Fuellfarbe dieses Typs: die typ-eigene, sonst die der Farbgruppe."""
return self.fill or self.style.fill
def get_dot_attrs(self) -> dict[str, str]: def get_dot_attrs(self) -> dict[str, str]:
"""Graphviz-Knotenattribute dieses Typs: Fuell-/Randfarbe der Farbgruppe """Graphviz-Knotenattribute dieses Typs: typ-eigene Fuellfarbe, Rand in
plus die typ-eigene Form (siehe TroSymbol/dot_shape_attrs) - Farbe der Farbe der Gruppe, plus die typ-eigene Form (siehe TroSymbol/
gruppiert verwandte Typen, Form unterscheidet den einzelnen Typ.""" dot_shape_attrs) - Rand und Farbton gruppieren verwandte Typen,
Fuellung und Form unterscheiden den einzelnen Typ."""
attrs = self.style.dot_attrs() attrs = self.style.dot_attrs()
attrs["fillcolor"] = self.get_fill()
attrs.update(dot_shape_attrs(self.symbol)) attrs.update(dot_shape_attrs(self.symbol))
return attrs return attrs
def mermaid_class(self) -> str:
"""Mermaid-Klassenname dieses Typs (Typnamen beginnen teils mit einer
Ziffer, die Mermaid als Klassenname nicht erlaubt)."""
return f"t_{self.name}"
def mermaid_classdef(self) -> str:
"""Mermaid-classDef-Zeile fuer diesen Typ (eigene Fuellung, Gruppenrand)."""
return (
f"classDef {self.mermaid_class()} fill:{self.get_fill()},"
f"stroke:{self.style.stroke},color:{self.style.font}"
)
# -- set --------------------------------------------------------------- # -- set ---------------------------------------------------------------
# #
# Jede Zuweisung laeuft durch die Pydantic-Validierung, weil # Jede Zuweisung laeuft durch die Pydantic-Validierung, weil
@@ -453,6 +485,10 @@ class TroDefinition(BaseModel):
"""Zeichensymbol des Markers setzen.""" """Zeichensymbol des Markers setzen."""
self.symbol = symbol self.symbol = symbol
def set_fill(self, fill: str) -> None:
"""Typ-eigene Fuellfarbe setzen ('' = Farbe der Gruppe)."""
self.fill = fill
def set_item(self, item: str, count: int) -> None: def set_item(self, item: str, count: int) -> None:
""" """
Anzahl einer Bauteilart setzen. Anzahl einer Bauteilart setzen.
@@ -503,29 +539,29 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
# (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState). # (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState).
TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", { TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", {
TroItem.SEPARATOR: 1, TroItem.SEPARATOR: 1,
}, STYLE_SEP, TroSymbol.CIRCLE), }, STYLE_SEP, TroSymbol.CIRCLE, "#d4e8ff"),
# laut TRO_Typen.md: Basis + 1x Switch-Modul # laut TRO_Typen.md: Basis + 1x Switch-Modul
TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", { TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", {
TroItem.SEPARATOR: 1, TroItem.SEPARATOR: 1,
TroItem.SWITCH: 1, TroItem.SWITCH: 1,
}, STYLE_SWI, TroSymbol.TRIANGLE), }, STYLE_SWI, TroSymbol.TRIANGLE, "#ffe4a0"),
# laut TRO_Typen.md: Basis + 2x Switch-Modul + Scanner-Modul # laut TRO_Typen.md: Basis + 2x Switch-Modul + Scanner-Modul
TroDefinition("1Sep2Swi", "FB_ILS_MTRO_1Sep2Swi", { TroDefinition("1Sep2Swi", "FB_ILS_MTRO_1Sep2Swi", {
TroItem.SEPARATOR: 1, TroItem.SEPARATOR: 1,
TroItem.SWITCH: 2, TroItem.SWITCH: 2,
TroItem.SCANNER: 1, TroItem.SCANNER: 1,
}, STYLE_SWI, TroSymbol.PENTAGON), }, STYLE_SWI, TroSymbol.PENTAGON, "#ffc766"),
# laut TRO_Typen.md: Basis + Scanner-Modul + SSCC-Modul (Messsensor + WCS) # laut TRO_Typen.md: Basis + Scanner-Modul + SSCC-Modul (Messsensor + WCS)
TroDefinition("1Sep_SSCC", "FB_ILS_MTRO_1Sep_SSCC", { TroDefinition("1Sep_SSCC", "FB_ILS_MTRO_1Sep_SSCC", {
TroItem.SEPARATOR: 1, TroItem.SEPARATOR: 1,
TroItem.SCANNER: 1, TroItem.SCANNER: 1,
TroItem.SSCC: 1, TroItem.SSCC: 1,
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE), }, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE, "#ffd4d4"),
# laut TRO_Typen.md: Basis + Vario-Antriebsmodul # laut TRO_Typen.md: Basis + Vario-Antriebsmodul
TroDefinition("Vario", "FB_ILS_MTRO_Vario", { TroDefinition("Vario", "FB_ILS_MTRO_Vario", {
TroItem.SEPARATOR: 1, TroItem.SEPARATOR: 1,
TroItem.VARIO_DRIVE: 1, TroItem.VARIO_DRIVE: 1,
}, STYLE_VARIO, TroSymbol.RECT), }, STYLE_VARIO, TroSymbol.RECT, "#e8d4f0"),
# laut TRO_Typen.md: Basis (Pin-Separator) + 19x Storage-Line + Scanner # laut TRO_Typen.md: Basis (Pin-Separator) + 19x Storage-Line + Scanner
# + Encoder + StorageRemoval-Schnittstelle # + Encoder + StorageRemoval-Schnittstelle
TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", { TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", {
@@ -533,7 +569,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroItem.STORAGE_LINE: 19, TroItem.STORAGE_LINE: 19,
TroItem.SCANNER: 1, TroItem.SCANNER: 1,
TroItem.ENCODER: 1, TroItem.ENCODER: 1,
}, STYLE_STORE, TroSymbol.SQUARE), }, STYLE_STORE, TroSymbol.SQUARE, "#d4f0d4"),
# aus SCL belegt (FB_ILS_MTRO_Vario_workStation.scl, VAR ab Zeile 77): # aus SCL belegt (FB_ILS_MTRO_Vario_workStation.scl, VAR ab Zeile 77):
# fbSeparator1, fbConvVario, fbSepWait, fbBarcodeReader. # fbSeparator1, fbConvVario, fbSepWait, fbBarcodeReader.
# Der Vario-Antrieb ist hier die WorkStation-Variante des STRO-Bausteins. # Der Vario-Antrieb ist hier die WorkStation-Variante des STRO-Bausteins.
@@ -543,7 +579,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroItem.WORK_STATION: 1, TroItem.WORK_STATION: 1,
TroItem.ACCUMULATE: 1, TroItem.ACCUMULATE: 1,
TroItem.SCANNER: 1, TroItem.SCANNER: 1,
}, STYLE_VARIO, TroSymbol.HEXAGON), }, STYLE_VARIO, TroSymbol.HEXAGON, "#caa6e0"),
# aus SCL belegt (FB_EmptyCarrBuffer.scl): arfbSeparatorStorage als # aus SCL belegt (FB_EmptyCarrBuffer.scl): arfbSeparatorStorage als
# Array[1..5] of FB_ILS_STRO_Sep + 5x fbCarrierWaitStore. # Array[1..5] of FB_ILS_STRO_Sep + 5x fbCarrierWaitStore.
# Kein eigener Eingangs-Separator - der "inTro" ist ein separater 1Sep-TRO # Kein eigener Eingangs-Separator - der "inTro" ist ein separater 1Sep-TRO
@@ -551,7 +587,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", { TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", {
TroItem.STORAGE_LINE: 5, TroItem.STORAGE_LINE: 5,
TroItem.ACCUMULATE: 5, TroItem.ACCUMULATE: 5,
}, STYLE_STORE, TroSymbol.DIAMOND), }, STYLE_STORE, TroSymbol.DIAMOND, "#a6dca6"),
# aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor, # aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor,
# fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche - # fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche -
# der einzige nicht additive Typ. # der einzige nicht additive Typ.
@@ -559,7 +595,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroItem.BOOM: 1, TroItem.BOOM: 1,
TroItem.FOOT: 1, TroItem.FOOT: 1,
TroItem.TILT_SENSOR: 1, TroItem.TILT_SENSOR: 1,
}, STYLE_EXT, TroSymbol.ARROW), }, STYLE_EXT, TroSymbol.ARROW, "#d9d9d9"),
# aus SCL belegt (FB_ILS_MTRO_2Sep1Swi.scl, VAR ab Zeile 115): # aus SCL belegt (FB_ILS_MTRO_2Sep1Swi.scl, VAR ab Zeile 115):
# fbSeparator1, fbSeparator2, fbSwitch1, fbBarcodeReader1, fbBarcodeReader2. # fbSeparator1, fbSeparator2, fbSwitch1, fbBarcodeReader1, fbBarcodeReader2.
# Die Mapping-Tabelle in TRO_Typen.md nennt die beiden Scanner nicht - # Die Mapping-Tabelle in TRO_Typen.md nennt die beiden Scanner nicht -
@@ -568,7 +604,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroItem.SEPARATOR: 2, TroItem.SEPARATOR: 2,
TroItem.SWITCH: 1, TroItem.SWITCH: 1,
TroItem.SCANNER: 2, TroItem.SCANNER: 2,
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN), }, STYLE_SWI, TroSymbol.TRIANGLE_DOWN, "#ffd6ad"),
# ACHTUNG - NICHT aus SCL belegt. Alle Typen oberhalb stammen aus # ACHTUNG - NICHT aus SCL belegt. Alle Typen oberhalb stammen aus
# vorhandenem Code bzw. der Mapping-Tabelle in TRO_Typen.md; dieser hier # vorhandenem Code bzw. der Mapping-Tabelle in TRO_Typen.md; dieser hier
# wurde am 2026-08-12 auf Anforderung aus dem Projekt 500573 (Mubea) # wurde am 2026-08-12 auf Anforderung aus dem Projekt 500573 (Mubea)
@@ -582,7 +618,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
TroItem.SEPARATOR: 2, TroItem.SEPARATOR: 2,
TroItem.SWITCH: 2, TroItem.SWITCH: 2,
TroItem.SCANNER: 2, TroItem.SCANNER: 2,
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN), }, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#f2a65a"),
) )
_BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG} _BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG}
@@ -610,5 +646,7 @@ def catalog() -> dict[str, TroDefinition]:
def mermaid_classdefs() -> list[str]: def mermaid_classdefs() -> list[str]:
"""classDef-Zeilen fuer ein Mermaid-Diagramm, eine je Farbgruppe.""" """classDef-Zeilen fuer ein Mermaid-Diagramm: eine je Farbgruppe (u. a.
return [style.mermaid_classdef() for style in STYLES] 'ext' fuer die Systemgrenzen) plus eine je Typ (t_<Name>)."""
return ([style.mermaid_classdef() for style in STYLES]
+ [definition.mermaid_classdef() for definition in TRO_CATALOG])
+218 -7
View File
@@ -142,6 +142,7 @@ from tro_overrides import (
apply_merges, apply_merges,
apply_overrides, apply_overrides,
apply_splits, apply_splits,
load_connect_ini,
load_overrides, load_overrides,
) )
@@ -1013,6 +1014,62 @@ def _tro_bahn(tro: Tro, graph: Graph, sep_assignment: dict[str, str]) -> str | N
return None 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( def link_tros(
graph: Graph, tros: list[Tro], elements: list[Element] graph: Graph, tros: list[Tro], elements: list[Element]
) -> tuple[list[tuple[str, str]], dict[tuple[str, str], str]]: ) -> 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) # er ist der Kreisel-Bahn zugehoerig und dort schon (Schritt 1 unten)
# verkettet. Ohne diesen Ausschluss wuerde er faelschlich als erster # verkettet. Ohne diesen Ausschluss wuerde er faelschlich als erster
# oder letzter "Vario" gelten (Review 2026-09-21). # oder letzter "Vario" gelten (Review 2026-09-21).
chain = _chain_mit_ungelisteten_separatoren(el, chain, elements, elements_by_id)
owned = [ owned = [
elem_owner[tid] for tid in chain if tid in elem_owner 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)) 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 # Kantenarten. Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0
# mitten in der Anlage - daher unterhalb links vom Anlagenrand. # mitten in der Anlage - daher unterhalb links vom Anlagenrand.
rows = "".join( 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>' f'<td align="left">{d.describe_symbol()}</td></tr>'
for d in TRO_CATALOG 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" 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 # Fehlerdatei
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1669,9 +1811,9 @@ def _tro_mermaid(analysis: Analysis) -> list[str]:
for tro in analysis.tros: for tro in analysis.tros:
definition = tro.definition() 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)}" 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: for boundary in analysis.boundaries:
label = f"{boundary.tro_id}<br/>{boundary.type_name}<br/>{_mermaid_text(boundary.label)}" 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 " add(f"Typen, FB-Bausteine, Bauteile und Farben stammen vollstaendig aus "
f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):") f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):")
add("") add("")
add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol |") add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol | Fuellfarbe |")
add("|---:|---|---|---|---|---|") add("|---:|---|---|---|---|---|---|")
for index, definition in enumerate(TRO_CATALOG, start=1): for index, definition in enumerate(TRO_CATALOG, start=1):
add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` " add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` "
f"| {definition.describe_items()} | {definition.get_group()} " f"| {definition.describe_items()} | {definition.get_group()} "
f"| {definition.describe_symbol()} |") f"| {definition.describe_symbol()} | `{definition.get_fill()}` |")
add("") add("")
add("| Bauteil | Sub-FB |") add("| Bauteil | Sub-FB |")
add("|---|---|") 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 " help="Dokumentation der TROs als Markdown in %%SKEL_RESULTS%% erzeugen "
"(TRO-Liste, Verbindungen, Typdefinition).", "(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( parser.add_argument(
"--geometry", "--geometry",
action="store_true", action="store_true",
@@ -2102,6 +2263,21 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"Anschlusspunkte als derselbe Uebergabepunkt gelten. " "Anschlusspunkte als derselbe Uebergabepunkt gelten. "
"Standard: %(default)s", "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) return parser.parse_args(argv)
@@ -2165,7 +2341,28 @@ def main(argv: list[str] | None = None) -> int:
if raw_json_file is not None: if raw_json_file is not None:
sepliste_counts = load_sepliste_counts(raw_json_file, warnings) sepliste_counts = load_sepliste_counts(raw_json_file, warnings)
if args.use_overrides:
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg")) 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, analysis = analyse_tros(graph, elements, overrides,
resolve_host=resolve_host, count_exits=count_exits, resolve_host=resolve_host, count_exits=count_exits,
method=method, sepliste_counts=sepliste_counts) method=method, sepliste_counts=sepliste_counts)
@@ -2174,7 +2371,6 @@ def main(argv: list[str] | None = None) -> int:
f"ableitbar.", file=sys.stderr) f"ableitbar.", file=sys.stderr)
return 1 return 1
stem = csv_file.stem + ("_geo" if args.geometry else "")
dot_file = results / f"{stem}_tro_flow.dot" dot_file = results / f"{stem}_tro_flow.dot"
svg_file = results / f"{stem}_tro_flow.svg" svg_file = results / f"{stem}_tro_flow.svg"
doc_target = results / f"{stem}_tro_doc.md" doc_target = results / f"{stem}_tro_doc.md"
@@ -2194,6 +2390,14 @@ def main(argv: list[str] | None = None) -> int:
encoding="utf-8", 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) has_errors = write_error_file(analysis, csv_file, error_target)
error_file = error_target if has_errors else None 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, report(analysis, csv_file, dot_file, created_svg, error_file, doc_file,
warnings, engine=engine) 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: if svg_error:
print(f"FEHLER: SVG nicht erzeugt: {svg_error}", file=sys.stderr) print(f"FEHLER: SVG nicht erzeugt: {svg_error}", file=sys.stderr)
+101
View File
@@ -490,3 +490,104 @@ def apply_overrides(tros, overrides: Overrides) -> None:
overrides.applied.append( overrides.applied.append(
f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})" f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})"
) )
# ---------------------------------------------------------------------------
# connect.ini (TRO-Ebene) zurueck einlesen - tro_flow.py --from-connect
# ---------------------------------------------------------------------------
# Vertrauensangabe fuer Typen, die aus einer (evtl. von Hand geaenderten)
# connect.ini stammen - in der Doku von Layout und Begehung unterscheidbar.
CONNECT_CONFIDENCE = "connect.ini"
def _connect_pair(raw: str) -> tuple[str, str, str] | None:
"""'<von> -> <nach> | <JamArea> | <kind>' -> (von, nach, kind); None wenn
kein Pfeil. Die JamArea wird (noch) nicht ausgewertet."""
pfeil, _, rest = str(raw).partition("|")
src, sep, dst = (t.strip() for t in pfeil.partition("->"))
if not sep or not src or not dst:
return None
felder = [f.strip() for f in rest.split("|")]
kind = felder[1] if len(felder) > 1 else ""
return src, dst, kind
def load_connect_ini(path: Path, base: Overrides | None = None) -> Overrides:
"""
Eine von tro_flow.py --connect geschriebene connect.ini als Korrekturen lesen.
Die Datei wird dabei nicht als eigenes Modell behandelt, sondern auf die
vorhandenen Korrekturarten abgebildet - so laeuft sie durch dieselbe
Pipeline wie cfg/tro_overrides.ini:
[<Anlage>.nodes] TRO-ID = <Typ> | ... -> Typkorrektur je TRO-ID
[<Anlage>.connections] cNNN = von -> nach | JamArea | kind
[<Anlage>.externals] eNNN = SYSn -> TRO | ... bzw. TRO -> SYSn | ...
-> zusammen die gesetzte Topologie, die die
abgeleiteten Kanten vollstaendig ersetzt
[<Anlage>.tros] (Vorgaenger/Nachfolger je TRO) ist nur eine Uebersicht und
wird ignoriert - massgeblich sind die Verbindungen.
`base` (die Korrekturen aus cfg/tro_overrides.ini) bleibt erhalten; Typen
aus der connect.ini gewinnen, die Topologie der connect.ini ersetzt eine
dort gesetzte.
"""
result = base if base is not None else Overrides()
parser = configparser.ConfigParser(inline_comment_prefixes=(";",),
interpolation=None)
parser.optionxform = str # TRO-IDs nicht in Kleinschreibung wandeln
# utf-8-sig: von Hand gespeicherte Dateien (Windows-Editoren, PowerShell)
# tragen oft ein BOM, an dem configparser sonst in Zeile 1 scheitert.
try:
parser.read(path, encoding="utf-8-sig")
except configparser.Error as exc:
raise ValueError(f"{path.name} nicht lesbar: {exc}") from exc
connections: list[ConnectionSpec] = []
for section in parser.sections():
art = section.rsplit(".", 1)[-1].lower()
values = parser[section]
if art == "nodes":
for tro_id, raw in values.items():
type_name = str(raw).partition("|")[0].strip()
# Systemgrenzen (SYSn) sind kein TRO, ihr "Typ" ist nur Anzeige
if tro_id.upper().startswith("SYS") or not type_name:
continue
if get_tro(type_name) is None:
result.warnings.append(
f"{path.name} [{section}] {tro_id}: Typ '{type_name}' steht "
f"nicht im TRO-Katalog - Typ bleibt wie abgeleitet"
)
continue
result.tros[tro_id.upper()] = TroOverride(
key=tro_id.upper(), type_name=type_name,
reason=f"aus {path.name}", confidence=CONNECT_CONFIDENCE,
)
elif art in ("connections", "externals"):
for key, raw in values.items():
pair = _connect_pair(raw)
if pair is None:
result.warnings.append(
f"{path.name} [{section}] {key}: '{raw}' ist keine "
f"Verbindung '<von> -> <nach>'"
)
continue
src, dst, kind = pair
if "EXTERN" in (src.upper(), dst.upper()):
result.warnings.append(
f"{path.name} [{section}] {key}: EXTERN wird nicht "
f"ausgewertet - Systemgrenze als SYSn angeben"
)
continue
connections.append(ConnectionSpec(src=src, dst=dst, kind=kind))
if connections:
result.connections = connections
else:
result.warnings.append(
f"{path.name}: keine Verbindungen gefunden - Topologie bleibt wie abgeleitet"
)
result.applied.append(f"connect.ini eingelesen: {path}")
return result