latest changes
This commit is contained in:
+266
-20
@@ -58,6 +58,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -69,6 +70,8 @@ from material_flow import (
|
||||
EDGE_UNRESOLVED,
|
||||
LANE_LEFT,
|
||||
LANE_RIGHT,
|
||||
ROTATION_CCW,
|
||||
ROTATION_CW,
|
||||
UNASSIGNED,
|
||||
Element,
|
||||
Graph,
|
||||
@@ -91,6 +94,14 @@ from tro_catalog import (
|
||||
get_tro,
|
||||
mermaid_classdefs,
|
||||
)
|
||||
from tro_overrides import (
|
||||
Overrides,
|
||||
apply_connections,
|
||||
apply_merges,
|
||||
apply_overrides,
|
||||
apply_splits,
|
||||
load_overrides,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
@@ -169,9 +180,13 @@ class Tro:
|
||||
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)
|
||||
# Manuelle Korrekturen aus cfg/tro_overrides.ini (Typkorrekturen, offene Punkte)
|
||||
overrides: Overrides = field(default_factory=Overrides)
|
||||
|
||||
def by_id(self, tro_id: str) -> Tro | None:
|
||||
for tro in self.tros:
|
||||
@@ -228,7 +243,12 @@ def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
|
||||
)
|
||||
|
||||
|
||||
def derive_tros(graph: Graph, elements: list[Element], findings: list[str]) -> list[Tro]:
|
||||
def derive_tros(
|
||||
graph: Graph,
|
||||
elements: list[Element],
|
||||
findings: list[str],
|
||||
overrides: Overrides | None = None,
|
||||
) -> list[Tro]:
|
||||
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden."""
|
||||
separators = sorted(
|
||||
(e for e in elements if e.kind == "Separator"), key=lambda e: e.teile_id
|
||||
@@ -376,12 +396,41 @@ def derive_tros(graph: Graph, elements: list[Element], findings: list[str]) -> l
|
||||
tro.reason = f"{tro.reason}; {len(exits)} abgehende Wege -> Weiche"
|
||||
tro.confidence = "niedrig"
|
||||
|
||||
# 4b) Manuell ergaenzte Uebergabestellen (cfg/tro_overrides.ini, [.split.]).
|
||||
# Vor der Nummernvergabe, damit die neuen TROs in derselben Reihenfolge
|
||||
# eine Nummer bekommen wie die abgeleiteten.
|
||||
if overrides is not None and overrides.splits:
|
||||
def _neuer_tro(spec, hosts, label):
|
||||
return Tro(
|
||||
tro_id="",
|
||||
type_name=spec.type_name,
|
||||
label=label,
|
||||
hosts=hosts,
|
||||
separators=list(spec.separators),
|
||||
scanners=list(spec.scanners),
|
||||
reason=spec.reason or "manuell ergaenzte Uebergabestelle",
|
||||
confidence=spec.confidence,
|
||||
)
|
||||
|
||||
apply_splits(tros, overrides, _neuer_tro)
|
||||
|
||||
# 4c) Zusammenfassungen - nach den Splits, damit auch ein frisch
|
||||
# herausgeloester TRO Teil einer Zusammenfassung sein kann.
|
||||
if overrides is not None and overrides.merges:
|
||||
apply_merges(tros, overrides)
|
||||
|
||||
# 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}"
|
||||
|
||||
# 5b) Manuelle Korrekturen aus cfg/tro_overrides.ini. Sie greifen erst hier,
|
||||
# weil sie ueber die TRO-ID adressiert sind, und noch vor Schritt 6, damit
|
||||
# ein manuell gesetzter Typ den SSCC-Hinweis nicht mehr ausloest.
|
||||
if overrides is not None:
|
||||
apply_overrides(tros, overrides)
|
||||
|
||||
# 6) Scanner am Separator: 1Sep_SSCC laesst sich mechanisch nicht erkennen
|
||||
for tro in tros:
|
||||
if tro.type_name == "1Sep" and tro.scanners:
|
||||
@@ -394,53 +443,129 @@ def derive_tros(graph: Graph, elements: list[Element], findings: list[str]) -> l
|
||||
return tros
|
||||
|
||||
|
||||
def link_tros(graph: Graph, tros: list[Tro]) -> list[tuple[str, str]]:
|
||||
def _tro_edge_kind(edge: Edge) -> str:
|
||||
"""Materialkante -> Kantenart des TRO-Flusses (fuer Einfaerbung)."""
|
||||
if edge.note == "Weiche":
|
||||
return "weiche"
|
||||
if edge.kind == EDGE_CIRCLE:
|
||||
return "umlauf"
|
||||
if edge.kind == EDGE_UNRESOLVED:
|
||||
return "unbestimmt"
|
||||
if edge.note in ("Einschleusung", "Ausschleusung"):
|
||||
return "transfer"
|
||||
return "normal"
|
||||
|
||||
|
||||
def _bahn_richtung(node) -> tuple[float, float] | None:
|
||||
"""Foerderrichtung einer Kreisel-Bahn als Einheitsvektor.
|
||||
|
||||
Der Kreisel ist eine geschlossene Schleife: auf der einen Bahn laeuft das
|
||||
Material hin, auf der anderen zurueck. Welche Richtung das ist, ergibt sich
|
||||
aus der Laengsachse ("Drehung") und dem Umlaufsinn ("Drehrichtung"):
|
||||
|
||||
UZS (im Uhrzeigersinn) Bahn R entgegen der Achse, Bahn L mit ihr
|
||||
GUZ (gegen den UZS) umgekehrt
|
||||
|
||||
Rueckgabe None, wenn eines der beiden Merkmale fehlt - dann bleibt es bei
|
||||
der ungerichteten "gleiche Bahn"-Notiz.
|
||||
"""
|
||||
element = node.element
|
||||
drehung = _as_float(_merkmal(element.merkmale, "Drehung"))
|
||||
umlauf = element.rotation
|
||||
if drehung is None or umlauf not in (ROTATION_CW, ROTATION_CCW):
|
||||
return None
|
||||
rad = math.radians(drehung)
|
||||
achse = (math.cos(rad), math.sin(rad))
|
||||
mit_achse = (node.lane == LANE_LEFT) == (umlauf == ROTATION_CW)
|
||||
return achse if mit_achse else (-achse[0], -achse[1])
|
||||
|
||||
|
||||
def link_tros(
|
||||
graph: Graph, tros: list[Tro]
|
||||
) -> tuple[list[tuple[str, str]], dict[tuple[str, str], str]]:
|
||||
"""
|
||||
TROs verknuepfen.
|
||||
|
||||
Der Materialfluss laeuft von Uebergabestelle zu Uebergabestelle, ggf. durch
|
||||
passive Zwischenknoten (Strecken/Bahnen ohne eigenen TRO). Solche Knoten
|
||||
werden daher durchlaufen ("kontrahiert"), belegte Knoten beenden den Pfad.
|
||||
Jede TRO-Kante bekommt zusaetzlich die Art der *ankommenden* Materialkante
|
||||
(transfer / umlauf / weiche / unbestimmt / normal).
|
||||
"""
|
||||
owners: dict[str, list[str]] = {}
|
||||
for tro in tros:
|
||||
for host in tro.hosts:
|
||||
owners.setdefault(host, []).append(tro.tro_id)
|
||||
|
||||
forward: dict[str, list[str]] = {}
|
||||
forward: dict[str, list[tuple[str, str]]] = {}
|
||||
for edge in graph.edges:
|
||||
forward.setdefault(edge.src, []).append(edge.dst)
|
||||
kind = _tro_edge_kind(edge)
|
||||
forward.setdefault(edge.src, []).append((edge.dst, kind))
|
||||
if edge.kind == EDGE_UNRESOLVED:
|
||||
# unbestimmte Richtung: in beide Richtungen begehbar
|
||||
forward.setdefault(edge.dst, []).append(edge.src)
|
||||
forward.setdefault(edge.dst, []).append((edge.src, kind))
|
||||
|
||||
edges: set[tuple[str, str]] = set()
|
||||
edge_kinds: dict[tuple[str, str], str] = {}
|
||||
for tro in tros:
|
||||
seen = set(tro.hosts)
|
||||
queue = [n for host in tro.hosts for n in forward.get(host, [])]
|
||||
queue = [(nxt, k) for host in tro.hosts for (nxt, k) in forward.get(host, [])]
|
||||
while queue:
|
||||
node = queue.pop()
|
||||
node, arriving = queue.pop()
|
||||
if node in seen:
|
||||
continue
|
||||
seen.add(node)
|
||||
if node in owners:
|
||||
for other in owners[node]:
|
||||
if other != tro.tro_id:
|
||||
edges.add((tro.tro_id, other))
|
||||
pair = (tro.tro_id, other)
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, arriving)
|
||||
continue
|
||||
queue.extend(forward.get(node, []))
|
||||
|
||||
# gleicher Host = physisch dieselbe Strecke/Bahn
|
||||
# Mehrere TROs auf derselben Kreisel-Bahn liegen HINTEREINANDER, nicht
|
||||
# nebeneinander: das Material laeuft die Bahn in Foerderrichtung ab. Sie
|
||||
# werden daher der Reihe nach verkettet statt nur als "gleiche Bahn"
|
||||
# markiert - sonst fehlt z. B. die Kante von der einen Sperre zur naechsten.
|
||||
verkettet: set[str] = set()
|
||||
for host, ids in owners.items():
|
||||
if len(ids) < 2:
|
||||
continue
|
||||
node = graph.nodes.get(host)
|
||||
if node is None or node.kind != "Kreisel" or not node.lane:
|
||||
continue
|
||||
richtung = _bahn_richtung(node)
|
||||
if richtung is None:
|
||||
continue
|
||||
auf_bahn = [t for t in tros if t.tro_id in ids and t.position]
|
||||
if len(auf_bahn) < 2:
|
||||
continue
|
||||
# Projektion auf die Foerderrichtung: kleinster Wert kommt zuerst
|
||||
auf_bahn.sort(key=lambda t: t.position[0] * richtung[0]
|
||||
+ t.position[1] * richtung[1])
|
||||
for vorher, nachher in zip(auf_bahn, auf_bahn[1:]):
|
||||
pair = (vorher.tro_id, nachher.tro_id)
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, "bahn")
|
||||
verkettet.update(pair)
|
||||
|
||||
# gleicher Host = physisch dieselbe Strecke/Bahn. Verkettete Bahnen sind
|
||||
# oben schon gerichtet verbunden und brauchen die ungerichtete Notiz nicht.
|
||||
for host, ids in owners.items():
|
||||
for tro_id in ids:
|
||||
tro = next(t for t in tros if t.tro_id == tro_id)
|
||||
tro.colocated = sorted(set(tro.colocated) | {o for o in ids if o != tro_id})
|
||||
if tro_id in verkettet:
|
||||
continue
|
||||
tro.colocated = sorted(
|
||||
set(tro.colocated) | {o for o in ids if o != tro_id and o not in verkettet}
|
||||
)
|
||||
|
||||
for tro in tros:
|
||||
tro.successors = sorted(dst for src, dst in edges if src == tro.tro_id)
|
||||
tro.predecessors = sorted(src for src, dst in edges if dst == tro.tro_id)
|
||||
|
||||
return sorted(edges)
|
||||
return sorted(edges), edge_kinds
|
||||
|
||||
|
||||
def _centroid(
|
||||
@@ -494,12 +619,36 @@ def locate_tros(tros: list[Tro], elements: list[Element], graph: Graph) -> None:
|
||||
tro.position_from = source if tro.position else "keine Position"
|
||||
|
||||
|
||||
def analyse_tros(graph: Graph, elements: list[Element]) -> Analysis:
|
||||
def analyse_tros(
|
||||
graph: Graph, elements: list[Element], overrides: Overrides | None = None
|
||||
) -> Analysis:
|
||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen."""
|
||||
analysis = Analysis()
|
||||
analysis.tros = derive_tros(graph, elements, analysis.findings)
|
||||
analysis.edges = link_tros(graph, analysis.tros)
|
||||
analysis.overrides = overrides if overrides is not None else Overrides()
|
||||
analysis.tros = derive_tros(graph, elements, analysis.findings, analysis.overrides)
|
||||
# Verorten vor dem Verknuepfen: die Reihenfolge mehrerer TROs auf einer
|
||||
# Kreisel-Bahn wird ueber ihre Koordinate bestimmt.
|
||||
locate_tros(analysis.tros, elements, graph)
|
||||
analysis.edges, analysis.edge_kinds = link_tros(graph, analysis.tros)
|
||||
|
||||
# Vor Ort aufgenommene Topologie schlaegt die abgeleitete. Die Differenz
|
||||
# wird protokolliert, damit sichtbar bleibt, was die Ableitung falsch hatte.
|
||||
if analysis.overrides.connections:
|
||||
abgeleitet = set(analysis.edges)
|
||||
analysis.edges, analysis.edge_kinds = apply_connections(
|
||||
analysis.tros, analysis.overrides
|
||||
)
|
||||
gesetzt = set(analysis.edges)
|
||||
for src, dst in sorted(abgeleitet - gesetzt):
|
||||
analysis.findings.append(
|
||||
f"abgeleitete Verbindung {src} -> {dst} entfaellt - steht nicht "
|
||||
f"in der gesetzten Topologie"
|
||||
)
|
||||
for src, dst in sorted(gesetzt - abgeleitet):
|
||||
analysis.findings.append(
|
||||
f"Verbindung {src} -> {dst} nur gesetzt, aus dem Layout nicht "
|
||||
f"ableitbar"
|
||||
)
|
||||
|
||||
for tro in analysis.tros:
|
||||
definition = tro.definition()
|
||||
@@ -552,6 +701,21 @@ def _legend_pos(analysis: Analysis) -> tuple[float, float]:
|
||||
return (min(xs) - LEGEND_MARGIN, min(ys) - LEGEND_MARGIN)
|
||||
|
||||
|
||||
# Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt,
|
||||
# Weiche orange). "transfer"/"normal" bleiben der durchgezogene Standardfluss.
|
||||
TRO_EDGE_STYLE = {
|
||||
"umlauf": 'color="#8ea9db", style=dashed, tooltip="Kreisel-Umlauf"',
|
||||
"weiche": 'color="#cc8800", penwidth=1.8, xlabel="Weiche", fontcolor="#cc8800", '
|
||||
'tooltip="Weiche (Kreisel-Uebergang)"',
|
||||
"unbestimmt": 'color="#bf8f00", style=dashed, dir=both, tooltip="Richtung unbestimmt"',
|
||||
"transfer": 'tooltip="Uebergabe (Ein-/Ausschleusung)"',
|
||||
}
|
||||
|
||||
|
||||
def _tro_edge_attrs(kind: str | None) -> str:
|
||||
return TRO_EDGE_STYLE.get(kind or "normal", "")
|
||||
|
||||
|
||||
def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str:
|
||||
"""
|
||||
Gerichtetes Flussdiagramm der TROs als Graphviz-Quelle.
|
||||
@@ -601,7 +765,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
|
||||
add("")
|
||||
|
||||
for src, dst in analysis.edges:
|
||||
add(f' "{src}" -> "{dst}" [];')
|
||||
add(f' "{src}" -> "{dst}" [{_tro_edge_attrs(analysis.edge_kinds.get((src, dst)))}];')
|
||||
|
||||
seen: set[frozenset] = set()
|
||||
for tro in analysis.tros:
|
||||
@@ -624,9 +788,13 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
|
||||
)
|
||||
legend_label = ('<<table border="0" cellborder="1" cellspacing="0" cellpadding="3">'
|
||||
+ rows
|
||||
+ '<tr><td align="left">durchgezogen</td>'
|
||||
'<td align="left">Materialfluss</td></tr>'
|
||||
'<tr><td align="left">gepunktet</td>'
|
||||
+ '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>'
|
||||
'<td align="left">Uebergabe / Ein-Ausschleusung</td></tr>'
|
||||
'<tr><td align="left"><font color="#8ea9db">blau gestrichelt</font></td>'
|
||||
'<td align="left">Kreisel-Umlauf</td></tr>'
|
||||
'<tr><td align="left"><font color="#cc8800">orange</font></td>'
|
||||
'<td align="left">Weiche (Kreisel-Uebergang)</td></tr>'
|
||||
'<tr><td align="left">grau gepunktet</td>'
|
||||
'<td align="left">gleiche Bahn / Strecke</td></tr>'
|
||||
"</table>>")
|
||||
if use_coords:
|
||||
@@ -787,6 +955,11 @@ def render_doc(
|
||||
add(f"| TRO-Verbindungen | {len(analysis.edges)} |")
|
||||
add(f"| Nicht verbundene TROs | {len(analysis.isolated)} |")
|
||||
add(f"| Hinweise | {len(analysis.findings)} |")
|
||||
if analysis.overrides.applied:
|
||||
add(f"| Manuelle Typkorrekturen | {len(analysis.overrides.applied)} |")
|
||||
if analysis.overrides.open_points:
|
||||
add(f"| Offene Punkte (noch kein TRO) | "
|
||||
f"{len(analysis.overrides.open_points)} |")
|
||||
add("")
|
||||
add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe |")
|
||||
add("|---|---|---:|---|---|")
|
||||
@@ -873,7 +1046,56 @@ def render_doc(
|
||||
add("_Keine._")
|
||||
add("")
|
||||
|
||||
add("## 6. Ableitungsregeln und Typdefinition")
|
||||
add("## 6. Manuelle Korrekturen und offene Punkte")
|
||||
add("")
|
||||
overrides = analysis.overrides
|
||||
if not overrides and not overrides.warnings:
|
||||
add("_Keine._ Alles unten Stehende ist rein aus dem Layout abgeleitet.")
|
||||
add("")
|
||||
else:
|
||||
add(f"Quelle: `{overrides.source}`" if overrides.source
|
||||
else "_Keine Override-Datei gefunden._")
|
||||
add("")
|
||||
|
||||
if overrides.applied:
|
||||
add("### 6.1 Angewandte Typkorrekturen")
|
||||
add("")
|
||||
add("Diese Typen stammen **nicht** aus dem Layout, sondern aus der "
|
||||
"Begehung bzw. dem Review - im Layout sind sie nicht erkennbar.")
|
||||
add("")
|
||||
add("| Korrektur | Begruendung |")
|
||||
add("|---|---|")
|
||||
for entry in overrides.applied:
|
||||
head, _, tail = entry.partition(" (")
|
||||
add(f"| `{head}` | {tail[:-1] if tail.endswith(')') else tail} |")
|
||||
add("")
|
||||
|
||||
if overrides.open_points:
|
||||
add("### 6.2 Offene Punkte - noch kein TRO")
|
||||
add("")
|
||||
add("Im Review gemeldete Stellen. Die TRO-Struktur ist hier "
|
||||
"**bewusst unveraendert**; die Punkte sind nur markiert (auch in der "
|
||||
"annotierten Zeichnung, Layer `TRO_OPENPOINT`), damit sie in BricsCAD "
|
||||
"geprueft werden koennen.")
|
||||
add("")
|
||||
add("| Punkt | X [mm] | Y [mm] | Z [mm] | erwartet | heute Teil von | Anmerkung |")
|
||||
add("|---|---:|---:|---:|---|---|---|")
|
||||
for point in overrides.open_points:
|
||||
z_text = f"{point.z:.0f}" if point.z is not None else "-"
|
||||
expect = f"`{point.expect}`" if point.expect else "-"
|
||||
owner = f"`{point.belongs_to}`" if point.belongs_to else "-"
|
||||
add(f"| {point.name} | {point.x:.0f} | {point.y:.0f} | {z_text} "
|
||||
f"| {expect} | {owner} | {point.note or '-'} |")
|
||||
add("")
|
||||
|
||||
if overrides.warnings:
|
||||
add("### 6.3 Probleme mit der Override-Datei")
|
||||
add("")
|
||||
for warning in overrides.warnings:
|
||||
add(f"- {warning}")
|
||||
add("")
|
||||
|
||||
add("## 7. Ableitungsregeln und Typdefinition")
|
||||
add("")
|
||||
add("Ein TRO ist die Steuerungslogik **einer Uebergabestelle**. Ausgangspunkt "
|
||||
"ist der Separator und das, was an ihm haengt (Host-Objekt, Scanner).")
|
||||
@@ -974,6 +1196,29 @@ def report(
|
||||
print("FEHLER: TRO-Typen ohne Eintrag in TRO_CATALOG: "
|
||||
+ ", ".join(analysis.unknown_types))
|
||||
|
||||
overrides = analysis.overrides
|
||||
if overrides.applied:
|
||||
print("")
|
||||
print(f"Manuelle Typkorrekturen ({len(overrides.applied)}) aus "
|
||||
f"{overrides.source}:")
|
||||
for entry in overrides.applied:
|
||||
print(f" * {entry}")
|
||||
|
||||
if overrides.open_points:
|
||||
print("")
|
||||
print(f"Offene Punkte ({len(overrides.open_points)}) - nur markiert, "
|
||||
f"TRO-Struktur unveraendert:")
|
||||
for point in overrides.open_points:
|
||||
expect = f" erwartet {point.expect}" if point.expect else ""
|
||||
owner = f", heute Teil von {point.belongs_to}" if point.belongs_to else ""
|
||||
print(f" o {point.name} bei {point.describe_position()}{expect}{owner}")
|
||||
|
||||
if overrides.warnings:
|
||||
print("")
|
||||
print(f"Probleme mit {overrides.source} ({len(overrides.warnings)}):")
|
||||
for warning in overrides.warnings:
|
||||
print(f" ! {warning}")
|
||||
|
||||
if analysis.findings:
|
||||
print("")
|
||||
print(f"Hinweise ({len(analysis.findings)}):")
|
||||
@@ -1071,7 +1316,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f"FEHLER: {csv_file} enthaelt keine Flussobjekte.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
analysis = analyse_tros(graph, elements)
|
||||
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg"))
|
||||
analysis = analyse_tros(graph, elements, overrides)
|
||||
if not analysis.tros:
|
||||
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
||||
f"ableitbar.", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user