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:
co-authored by
Claude Opus 5.5
parent
1479519bdc
commit
02db9a95b0
+7
-2
@@ -102,7 +102,7 @@ from dxf_registration import (
|
||||
from material_flow import build_graph, env_dir, read_elements, resolve_input
|
||||
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
|
||||
from tro_flow import Analysis, analyse_tros
|
||||
from tro_overrides import load_overrides
|
||||
from tro_overrides import Overrides, load_overrides
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
@@ -782,6 +782,10 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument("--emit-lisp", action="store_true",
|
||||
help="cad/tro_types.lsp aus dem Typkatalog neu schreiben "
|
||||
"(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)
|
||||
|
||||
|
||||
@@ -834,7 +838,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
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)
|
||||
if not analysis.tros:
|
||||
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
||||
|
||||
+54
-16
@@ -231,6 +231,11 @@ class TroStyle(BaseModel):
|
||||
|
||||
Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist
|
||||
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",
|
||||
@@ -337,6 +342,12 @@ class TroDefinition(BaseModel):
|
||||
min_length=1,
|
||||
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__(
|
||||
self,
|
||||
@@ -345,6 +356,7 @@ class TroDefinition(BaseModel):
|
||||
items: dict[str, int] | None = None,
|
||||
style: TroStyle | None = None,
|
||||
symbol: str | None = None,
|
||||
fill: str | None = None,
|
||||
**data,
|
||||
) -> None:
|
||||
# Positionsargumente auf Feldnamen abbilden, damit die bisherige
|
||||
@@ -359,6 +371,8 @@ class TroDefinition(BaseModel):
|
||||
data["style"] = style
|
||||
if symbol is not None:
|
||||
data["symbol"] = symbol
|
||||
if fill is not None:
|
||||
data["fill"] = fill
|
||||
super().__init__(**data)
|
||||
|
||||
@field_validator("items", mode="after")
|
||||
@@ -420,14 +434,32 @@ class TroDefinition(BaseModel):
|
||||
"""Kurzname der Farbgruppe, z. B. 'vario'."""
|
||||
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]:
|
||||
"""Graphviz-Knotenattribute dieses Typs: Fuell-/Randfarbe der Farbgruppe
|
||||
plus die typ-eigene Form (siehe TroSymbol/dot_shape_attrs) - Farbe
|
||||
gruppiert verwandte Typen, Form unterscheidet den einzelnen Typ."""
|
||||
"""Graphviz-Knotenattribute dieses Typs: typ-eigene Fuellfarbe, Rand in
|
||||
der Farbe der Gruppe, plus die typ-eigene Form (siehe TroSymbol/
|
||||
dot_shape_attrs) - Rand und Farbton gruppieren verwandte Typen,
|
||||
Fuellung und Form unterscheiden den einzelnen Typ."""
|
||||
attrs = self.style.dot_attrs()
|
||||
attrs["fillcolor"] = self.get_fill()
|
||||
attrs.update(dot_shape_attrs(self.symbol))
|
||||
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 ---------------------------------------------------------------
|
||||
#
|
||||
# Jede Zuweisung laeuft durch die Pydantic-Validierung, weil
|
||||
@@ -453,6 +485,10 @@ class TroDefinition(BaseModel):
|
||||
"""Zeichensymbol des Markers setzen."""
|
||||
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:
|
||||
"""
|
||||
Anzahl einer Bauteilart setzen.
|
||||
@@ -503,29 +539,29 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
# (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState).
|
||||
TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
}, STYLE_SEP, TroSymbol.CIRCLE),
|
||||
}, STYLE_SEP, TroSymbol.CIRCLE, "#d4e8ff"),
|
||||
# laut TRO_Typen.md: Basis + 1x Switch-Modul
|
||||
TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SWITCH: 1,
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE),
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE, "#ffe4a0"),
|
||||
# laut TRO_Typen.md: Basis + 2x Switch-Modul + Scanner-Modul
|
||||
TroDefinition("1Sep2Swi", "FB_ILS_MTRO_1Sep2Swi", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 1,
|
||||
}, STYLE_SWI, TroSymbol.PENTAGON),
|
||||
}, STYLE_SWI, TroSymbol.PENTAGON, "#ffc766"),
|
||||
# laut TRO_Typen.md: Basis + Scanner-Modul + SSCC-Modul (Messsensor + WCS)
|
||||
TroDefinition("1Sep_SSCC", "FB_ILS_MTRO_1Sep_SSCC", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SCANNER: 1,
|
||||
TroItem.SSCC: 1,
|
||||
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE),
|
||||
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE, "#ffd4d4"),
|
||||
# laut TRO_Typen.md: Basis + Vario-Antriebsmodul
|
||||
TroDefinition("Vario", "FB_ILS_MTRO_Vario", {
|
||||
TroItem.SEPARATOR: 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
|
||||
# + Encoder + StorageRemoval-Schnittstelle
|
||||
TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", {
|
||||
@@ -533,7 +569,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.STORAGE_LINE: 19,
|
||||
TroItem.SCANNER: 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):
|
||||
# fbSeparator1, fbConvVario, fbSepWait, fbBarcodeReader.
|
||||
# Der Vario-Antrieb ist hier die WorkStation-Variante des STRO-Bausteins.
|
||||
@@ -543,7 +579,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.WORK_STATION: 1,
|
||||
TroItem.ACCUMULATE: 1,
|
||||
TroItem.SCANNER: 1,
|
||||
}, STYLE_VARIO, TroSymbol.HEXAGON),
|
||||
}, STYLE_VARIO, TroSymbol.HEXAGON, "#caa6e0"),
|
||||
# aus SCL belegt (FB_EmptyCarrBuffer.scl): arfbSeparatorStorage als
|
||||
# Array[1..5] of FB_ILS_STRO_Sep + 5x fbCarrierWaitStore.
|
||||
# Kein eigener Eingangs-Separator - der "inTro" ist ein separater 1Sep-TRO
|
||||
@@ -551,7 +587,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", {
|
||||
TroItem.STORAGE_LINE: 5,
|
||||
TroItem.ACCUMULATE: 5,
|
||||
}, STYLE_STORE, TroSymbol.DIAMOND),
|
||||
}, STYLE_STORE, TroSymbol.DIAMOND, "#a6dca6"),
|
||||
# aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor,
|
||||
# fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche -
|
||||
# der einzige nicht additive Typ.
|
||||
@@ -559,7 +595,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.BOOM: 1,
|
||||
TroItem.FOOT: 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):
|
||||
# fbSeparator1, fbSeparator2, fbSwitch1, fbBarcodeReader1, fbBarcodeReader2.
|
||||
# Die Mapping-Tabelle in TRO_Typen.md nennt die beiden Scanner nicht -
|
||||
@@ -568,7 +604,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.SEPARATOR: 2,
|
||||
TroItem.SWITCH: 1,
|
||||
TroItem.SCANNER: 2,
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN),
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN, "#ffd6ad"),
|
||||
# ACHTUNG - NICHT aus SCL belegt. Alle Typen oberhalb stammen aus
|
||||
# vorhandenem Code bzw. der Mapping-Tabelle in TRO_Typen.md; dieser hier
|
||||
# wurde am 2026-08-12 auf Anforderung aus dem Projekt 500573 (Mubea)
|
||||
@@ -582,7 +618,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.SEPARATOR: 2,
|
||||
TroItem.SWITCH: 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}
|
||||
@@ -610,5 +646,7 @@ def catalog() -> dict[str, TroDefinition]:
|
||||
|
||||
|
||||
def mermaid_classdefs() -> list[str]:
|
||||
"""classDef-Zeilen fuer ein Mermaid-Diagramm, eine je Farbgruppe."""
|
||||
return [style.mermaid_classdef() for style in STYLES]
|
||||
"""classDef-Zeilen fuer ein Mermaid-Diagramm: eine je Farbgruppe (u. a.
|
||||
'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])
|
||||
|
||||
+219
-8
@@ -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)
|
||||
|
||||
@@ -490,3 +490,104 @@ def apply_overrides(tros, overrides: Overrides) -> None:
|
||||
overrides.applied.append(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user