Weichenknoten zwischen Kreiseln, Track-Mouseover, Parameter in cfg/parameter.ini
TRO-Typen an Kreisel-Beruehrpunkten (lib/kreisel_junctions.py, derive_tros Schritt 3c): jeder Kreisel als Stadion-Form mit Laufrichtung; jeder gleichlaufende Beruehrpunkt ist eine Weiche (exportierte ILS Weiche oder abgeleitet W<a>_<b>), Beruehrpunkte derselben Umlenkung bilden einen Knoten. Ein Separator steuert ihn, wenn er in Laufrichtung unmittelbar davor liegt (keine andere Weiche dazwischen). Typ = Separatoren x Weichen. export.csv: 1Sep1Swi (9/1, 9/8, 8/7), 2Sep1Swi (7/2), 2Sep2Swi (2/3/4), 3Sep2Swi (4/5/6). Neuer Katalogtyp 3Sep2Swi (nicht aus SCL belegt). Gegenlaeufige Beruehrpunkte (Mubea, Dematic) bleiben bei der alten Regel; Mubea/HundM05/Dematic unveraendert, Showroom 0018+0019 jetzt 2Sep1Swi. material_flow --track: Mouseover je Knoten mit Zugehoerigkeit zu Kreisel/VF/GF (Zuordnung, TrackIds-Kette, Lage laut Geometrie), Weiche mit beiden Kreiseln, Be-/Entladung mit ihrem Kreisel; Widersprueche mit "!". Zahlenwerte (Toleranzen, Schwellen, Massstaebe, Groessen) aus allen lib-Modulen nach cfg/parameter.ini (lib/parameter.py, Abschnitt je Modul, Ueberschreiben je Anlage ueber %SKEL_CFG%/parameter.ini). Ausgaben aller Anlagen bytegleich. tro_flow.py: Docstrings mit Parametern/Rueckgabe, fehlende Typangaben. Geprueft: 90 Tests gruen; tro_flow/material_flow/scl_skeleton auf 5 CSVs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c69faf3db
commit
550bdeb464
+11
-12
@@ -58,6 +58,8 @@ from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from parameter import get_float, get_int
|
||||
|
||||
__all__ = [
|
||||
"Transform",
|
||||
"RegistrationError",
|
||||
@@ -84,18 +86,15 @@ ANCHOR_BLOCKS: dict[str, tuple[str, ...]] = {
|
||||
"Scanner": ("SCAN", "Scanner_*"),
|
||||
}
|
||||
|
||||
# Rastergroesse der Abstimmung in mm
|
||||
VOTE_GRID = 100.0
|
||||
# So viele Kandidaten der Abstimmung werden ausgeglichen und bewertet
|
||||
VOTE_CANDIDATES = 6
|
||||
# Nur Paare bis zu diesem Abstand gehen in den Ausgleich ein (exakte Treffer)
|
||||
FIT_TOL = 50.0
|
||||
# Punkte gelten als zugeordnet, wenn sie nach der Verschiebung so nah liegen
|
||||
DEFAULT_MATCH_TOL = 600.0
|
||||
# So viele exakte Treffer muessen es mindestens sein
|
||||
MIN_INLIERS = 5
|
||||
# Ab dieser Drehung wird gewarnt (Grad)
|
||||
ROTATION_WARN_DEG = 0.05
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [dxf_registration] - Bedeutung
|
||||
# und Einheit stehen dort (lib/parameter.py).
|
||||
_P = "dxf_registration"
|
||||
VOTE_GRID = get_float(_P, "vote_grid")
|
||||
VOTE_CANDIDATES = get_int(_P, "vote_candidates")
|
||||
FIT_TOL = get_float(_P, "fit_tol")
|
||||
DEFAULT_MATCH_TOL = get_float(_P, "default_match_tol")
|
||||
MIN_INLIERS = get_int(_P, "min_inliers")
|
||||
ROTATION_WARN_DEG = get_float(_P, "rotation_warn_deg")
|
||||
|
||||
|
||||
class RegistrationError(RuntimeError):
|
||||
|
||||
+7
-2
@@ -60,6 +60,7 @@ from dxf_registration import (
|
||||
save_transform,
|
||||
verify_transform,
|
||||
)
|
||||
from parameter import get_float
|
||||
|
||||
__all__ = [
|
||||
"EPLAN_SCHEMA",
|
||||
@@ -78,6 +79,10 @@ __all__ = [
|
||||
EPLAN_SCHEMA = "sps_skel-eplan-v1"
|
||||
RULES_FILE = "e_planning.ini"
|
||||
|
||||
# Zulaessige Abweichung vom Sollabstand, wenn eine Regel kein toleranz_mm
|
||||
# angibt - aus cfg/parameter.ini [e_planning] (lib/parameter.py).
|
||||
DEFAULT_TOLERANCE_MM = get_float("e_planning", "toleranz_mm")
|
||||
|
||||
_CONFIG = ConfigDict(extra="forbid")
|
||||
|
||||
# "=A1+UH01-KF107" -> Ort/Steuerung "UH01", Geraet "KF107"
|
||||
@@ -153,7 +158,7 @@ class Rule:
|
||||
# Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil.
|
||||
# Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand.
|
||||
target_mm: float | None = None
|
||||
tolerance_mm: float = 150.0
|
||||
tolerance_mm: float = DEFAULT_TOLERANCE_MM
|
||||
|
||||
def score(self, dist: float) -> float | None:
|
||||
"""Rangfolge eines Kandidaten (kleiner = besser), None = nicht zulaessig."""
|
||||
@@ -304,7 +309,7 @@ def load_rules(path: Path) -> tuple[list[Rule], list[str]]:
|
||||
number_offset=int(s.get("nummer_versatz", "0")),
|
||||
prefix=s.get("praefix", "").strip(),
|
||||
target_mm=float(soll) if soll else None,
|
||||
tolerance_mm=float(s.get("toleranz_mm", "150")),
|
||||
tolerance_mm=float(s.get("toleranz_mm", str(DEFAULT_TOLERANCE_MM))),
|
||||
))
|
||||
known = {r.role for r in rules}
|
||||
for r in rules:
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
kreisel_junctions.py - Weichenknoten zwischen Kreiseln aus der Geometrie.
|
||||
|
||||
Zwei Kreisel, die sich beruehren, bilden eine Weichenstelle: am Beruehrpunkt
|
||||
laufen beide Umlaeufe in dieselbe Richtung, ein Traeger kann dort von einem
|
||||
Kreisel auf den anderen wechseln. dxfmakros exportiert solche Stellen als
|
||||
"ILS Weiche" (Merkmal "Erzeugt aus: Kreisel-Beruehrpunkt") - aber nicht
|
||||
immer: in data/export.csv fehlen die Weichen zwischen Kreisel 9/8, 2/3 und
|
||||
3/4, obwohl sich die Kreisel dort beruehren. Darum wird hier jede Weiche aus
|
||||
der Kreisel-Geometrie bestimmt und eine exportierte ILS Weiche nur noch ihrem
|
||||
Beruehrpunkt zugeordnet (Id und Name).
|
||||
|
||||
Geometrie
|
||||
---------
|
||||
Ein Kreisel ist eine Stadion-Form: zwei Umlenkungen (Halbkreise vom Radius
|
||||
r) im Abstand L, verbunden durch die beiden geraden Bahnen. Mittelpunkt ist
|
||||
die CSV-Position, die Laengsachse zeigt in Richtung "Drehung". r und L kommen
|
||||
aus der Boundingbox (kurze Seite = 2r, lange Seite = L + 2r), bei schraeger
|
||||
Lage aus dem Merkmal "Abstand (Kreiselachse A - Kreiselachse) in Meter".
|
||||
Jeder Punkt auf dem Umlauf hat eine Laufposition s (mm) in Drehrichtung
|
||||
("Drehrichtung" UZS/GUZ) - damit ist "stromauf"/"stromab" definiert.
|
||||
|
||||
Weichenknoten und steuernde Separatoren
|
||||
---------------------------------------
|
||||
Beruehrpunkte, die auf derselben Umlenkung eines Kreisels liegen, bilden
|
||||
EINEN Weichenknoten (Kreisel 5 beruehrt mit seiner Umlenkung Kreisel 6 und
|
||||
Kreisel 4 - zwei Weichen, eine Uebergabestelle). Ein Separator steuert den
|
||||
Knoten, wenn er auf einem der beteiligten Kreisel unmittelbar stromauf vor
|
||||
dem Knoten liegt: vom Knoten gegen die Drehrichtung zurueckgegangen, kommt
|
||||
zuerst dieser Separator und keine andere Weiche. Liegt dazwischen eine
|
||||
fremde Weiche, steuert der Separator diese, nicht den Knoten.
|
||||
|
||||
Aus der Anzahl steuernder Separatoren und Weichen ergibt sich der TRO-Typ
|
||||
(tro_flow.derive_tros, Schritt 3c): 1Sep1Swi, 2Sep1Swi, 2Sep2Swi, 3Sep2Swi.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from material_flow import (
|
||||
LANE_LEFT,
|
||||
LANE_RIGHT,
|
||||
ROTATION_CCW,
|
||||
ROTATION_CW,
|
||||
Element,
|
||||
_as_float,
|
||||
_merkmal,
|
||||
)
|
||||
from parameter import get_float, get_int
|
||||
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [kreisel_junctions] - Bedeutung
|
||||
# und Einheit stehen dort (lib/parameter.py).
|
||||
_P = "kreisel_junctions"
|
||||
CONTACT_TOLERANCE = get_float(_P, "contact_tolerance")
|
||||
SEPARATOR_TOLERANCE = get_float(_P, "separator_tolerance")
|
||||
WEICHE_TOLERANCE = get_float(_P, "weiche_tolerance")
|
||||
SAMPLE_STEP = get_float(_P, "sample_step")
|
||||
MIN_SAMPLES = get_int(_P, "min_samples")
|
||||
CONTACT_TIE = get_float(_P, "contact_tie")
|
||||
MAX_CONTACT_LENGTH = get_float(_P, "max_contact_length")
|
||||
END_TOLERANCE = get_float(_P, "end_tolerance")
|
||||
DEFAULT_RADIUS = get_float(_P, "default_radius")
|
||||
DIRECTION_STEP = get_float(_P, "direction_step")
|
||||
GLEICHLAUF_MIN_COS = get_float(_P, "gleichlauf_min_cos")
|
||||
|
||||
# Abschnitte eines Umlaufs: die beiden Umlenkungen und die beiden Geraden.
|
||||
END_PLUS = "+" # Umlenkung in Achsrichtung
|
||||
END_MINUS = "-" # Umlenkung gegen die Achsrichtung
|
||||
STRAIGHT = "gerade"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Loop:
|
||||
"""Umlauf eines Kreisels als Stadion-Form mit Laufrichtung.
|
||||
|
||||
Lokale Koordinaten: a entlang der Achse ("Drehung"), b quer dazu (links
|
||||
positiv). Die Umlenkungen liegen bei a = +half und a = -half.
|
||||
"""
|
||||
|
||||
kreisel: Element
|
||||
cx: float
|
||||
cy: float
|
||||
ux: float # Achsrichtung (Einheitsvektor)
|
||||
uy: float
|
||||
half: float # halber Umlenkungsabstand L/2 (mm)
|
||||
radius: float # Bahnabstand / 2 (mm)
|
||||
clockwise: bool # True = UZS
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""Umfang des Umlaufs (mm)."""
|
||||
return 4 * self.half + 2 * math.pi * self.radius
|
||||
|
||||
def _local(self, x: float, y: float) -> tuple[float, float]:
|
||||
"""Weltpunkt -> lokale Koordinaten (a, b)."""
|
||||
dx, dy = x - self.cx, y - self.cy
|
||||
return dx * self.ux + dy * self.uy, -dx * self.uy + dy * self.ux
|
||||
|
||||
def lane(self, x: float, y: float) -> str:
|
||||
"""Bahn eines Punkts: LANE_LEFT links der Achse, sonst LANE_RIGHT
|
||||
(dieselbe Konvention wie tro_flow._kreisel_bahn)."""
|
||||
_a, b = self._local(x, y)
|
||||
return LANE_LEFT if b > 0 else LANE_RIGHT
|
||||
|
||||
def project(self, x: float, y: float) -> tuple[float, float, str]:
|
||||
"""Naechster Punkt auf dem Umlauf.
|
||||
|
||||
Parameter:
|
||||
x, y: Weltpunkt (mm).
|
||||
|
||||
Rueckgabe:
|
||||
(s, Abstand, Abschnitt): s = Laufposition in Drehrichtung
|
||||
(0 <= s < length), Abstand des Punkts vom Umlauf (mm), Abschnitt
|
||||
END_PLUS, END_MINUS oder STRAIGHT.
|
||||
"""
|
||||
a, b = self._local(x, y)
|
||||
h, r = self.half, self.radius
|
||||
cands: list[tuple[float, float, str]] = []
|
||||
# Gegen den Uhrzeigersinn (lokal): untere Gerade (b=-r) in +a, rechte
|
||||
# Umlenkung, obere Gerade (b=+r) in -a, linke Umlenkung.
|
||||
if -h <= a <= h:
|
||||
cands.append((abs(b + r), a + h, STRAIGHT))
|
||||
cands.append((abs(b - r), 2 * h + math.pi * r + (h - a), STRAIGHT))
|
||||
if a >= h:
|
||||
theta = math.atan2(b, a - h) # -pi/2 .. pi/2
|
||||
cands.append((abs(math.hypot(a - h, b) - r),
|
||||
2 * h + r * (theta + math.pi / 2), END_PLUS))
|
||||
if a <= -h:
|
||||
phi = math.atan2(b, a + h) % (2 * math.pi) # pi/2 .. 3pi/2
|
||||
cands.append((abs(math.hypot(a + h, b) - r),
|
||||
4 * h + math.pi * r + r * (phi - math.pi / 2), END_MINUS))
|
||||
if not cands: # Innenpunkt nahe Achse
|
||||
cands.append((abs(b + r), a + h, STRAIGHT))
|
||||
dist, s_ccw, section = min(cands)
|
||||
if section == STRAIGHT and a >= h - END_TOLERANCE:
|
||||
section = END_PLUS
|
||||
elif section == STRAIGHT and a <= -h + END_TOLERANCE:
|
||||
section = END_MINUS
|
||||
s = (self.length - s_ccw) % self.length if self.clockwise else s_ccw % self.length
|
||||
return s, dist, section
|
||||
|
||||
def point(self, s: float) -> tuple[float, float]:
|
||||
"""Weltpunkt zur Laufposition s (Umkehrung von project)."""
|
||||
s_ccw = (self.length - s) % self.length if self.clockwise else s % self.length
|
||||
h, r = self.half, self.radius
|
||||
if s_ccw < 2 * h:
|
||||
a, b = s_ccw - h, -r
|
||||
elif s_ccw < 2 * h + math.pi * r:
|
||||
theta = (s_ccw - 2 * h) / r - math.pi / 2
|
||||
a, b = h + r * math.cos(theta), r * math.sin(theta)
|
||||
elif s_ccw < 4 * h + math.pi * r:
|
||||
a, b = h - (s_ccw - 2 * h - math.pi * r), r
|
||||
else:
|
||||
phi = (s_ccw - 4 * h - math.pi * r) / r + math.pi / 2
|
||||
a, b = -h + r * math.cos(phi), r * math.sin(phi)
|
||||
return (self.cx + a * self.ux - b * self.uy, self.cy + a * self.uy + b * self.ux)
|
||||
|
||||
def direction(self, s: float) -> tuple[float, float]:
|
||||
"""Laufrichtung (Einheitsvektor) an der Laufposition s."""
|
||||
x0, y0 = self.point(s - DIRECTION_STEP)
|
||||
x1, y1 = self.point(s + DIRECTION_STEP)
|
||||
n = math.hypot(x1 - x0, y1 - y0) or 1.0
|
||||
return (x1 - x0) / n, (y1 - y0) / n
|
||||
|
||||
|
||||
def kreisel_loop(kreisel: Element) -> Loop | None:
|
||||
"""Umlauf eines Kreisels aus Position, Drehung, Boundingbox, Drehrichtung.
|
||||
|
||||
Parameter:
|
||||
kreisel: Element mit kind "Kreisel".
|
||||
|
||||
Rueckgabe:
|
||||
Loop oder None, wenn Position, Drehrichtung oder Abmessungen fehlen.
|
||||
"""
|
||||
if kreisel.x is None or kreisel.y is None:
|
||||
return None
|
||||
if kreisel.rotation not in (ROTATION_CW, ROTATION_CCW):
|
||||
return None
|
||||
drehung = _as_float(_merkmal(kreisel.merkmale, "Drehung")) or 0.0
|
||||
rad = math.radians(drehung)
|
||||
bw, bh = kreisel.bbox_x, kreisel.bbox_y
|
||||
if bw and bh and abs(drehung) % 90 < 1e-6:
|
||||
short, long_ = min(bw, bh), max(bw, bh)
|
||||
radius, half = short / 2, (long_ - short) / 2
|
||||
else:
|
||||
abstand = _as_float(_merkmal(
|
||||
kreisel.merkmale, "Abstand (Kreiselachse A - Kreiselachse) in Meter"))
|
||||
if abstand is None:
|
||||
return None
|
||||
radius, half = DEFAULT_RADIUS, abstand * 1000 / 2
|
||||
return Loop(kreisel, kreisel.x, kreisel.y, math.cos(rad), math.sin(rad),
|
||||
half, radius, kreisel.rotation == ROTATION_CW)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""Beruehrpunkt zweier Kreisel = eine Weiche."""
|
||||
|
||||
weiche_id: str # TeileId der ILS Weiche oder "W<a>_<b>"
|
||||
kreisel: tuple[str, str] # beide Kreisel-Ids, sortiert
|
||||
x: float
|
||||
y: float
|
||||
exported: bool # True = als ILS Weiche im CSV
|
||||
s: dict[str, float] = field(default_factory=dict) # Kreisel -> s
|
||||
section: dict[str, str] = field(default_factory=dict) # Kreisel -> Abschnitt
|
||||
|
||||
|
||||
@dataclass
|
||||
class Junction:
|
||||
"""Weichenknoten: Beruehrpunkte auf derselben Umlenkung, plus Steuerung."""
|
||||
|
||||
contacts: list[Contact]
|
||||
kreisel: list[str] # beteiligte Kreisel
|
||||
separators: list[str] = field(default_factory=list) # steuernde Separatoren
|
||||
sep_kreisel: dict[str, str] = field(default_factory=dict) # Separator -> Kreisel
|
||||
sep_lane: dict[str, str] = field(default_factory=dict) # Separator -> "<Id>-L/R"
|
||||
|
||||
@property
|
||||
def type_name(self) -> str:
|
||||
"""TRO-Typ aus Separator- und Weichenzahl, z. B. "2Sep1Swi"."""
|
||||
return f"{len(self.separators)}Sep{len(self.contacts)}Swi"
|
||||
|
||||
|
||||
def _find_contact(a: Loop, b: Loop) -> tuple[float, float, float] | None:
|
||||
"""Beruehrpunkt (x, y, Laenge des Beruehrbereichs) oder None."""
|
||||
steps = max(int(a.length / SAMPLE_STEP), MIN_SAMPLES)
|
||||
near: list[tuple[float, float, float]] = []
|
||||
for i in range(steps):
|
||||
x, y = a.point(i * a.length / steps)
|
||||
_s, dist, _sec = b.project(x, y)
|
||||
if dist <= CONTACT_TOLERANCE:
|
||||
near.append((dist, x, y))
|
||||
if not near:
|
||||
return None
|
||||
# Bei tangentialer Beruehrung sind viele Abtastpunkte fast gleich nah -
|
||||
# der Mittelpunkt der naechsten liegt am eigentlichen Beruehrpunkt.
|
||||
best = min(d for d, _x, _y in near)
|
||||
mitte = [(x, y) for d, x, y in near if d <= best + CONTACT_TIE]
|
||||
x = sum(p[0] for p in mitte) / len(mitte)
|
||||
y = sum(p[1] for p in mitte) / len(mitte)
|
||||
return x, y, len(near) * a.length / steps
|
||||
|
||||
|
||||
def find_contacts(elements: list[Element], findings: list[str]) -> tuple[dict[str, Loop], list[Contact]]:
|
||||
"""Alle Beruehrpunkte zwischen Kreiseln, mit Zuordnung der ILS Weichen.
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports (Kreisel, ILS Weichen).
|
||||
findings: Fundliste; Hinweise zu fehlenden/gegenlaeufigen Weichen.
|
||||
|
||||
Rueckgabe:
|
||||
(Kreisel-Id -> Loop, Beruehrpunkte). Ein Beruehrpunkt ohne
|
||||
exportierte ILS Weiche bekommt die Id "W<KreiselA>_<KreiselB>".
|
||||
"""
|
||||
loops = {e.teile_id: lp for e in elements if e.kind == "Kreisel"
|
||||
and (lp := kreisel_loop(e)) is not None}
|
||||
weichen = [e for e in elements if e.kind == "Weiche"]
|
||||
ids = sorted(loops)
|
||||
contacts: list[Contact] = []
|
||||
for i, ka in enumerate(ids):
|
||||
for kb in ids[i + 1:]:
|
||||
hit = _find_contact(loops[ka], loops[kb])
|
||||
if hit is None:
|
||||
continue
|
||||
x, y, laenge = hit
|
||||
if laenge > MAX_CONTACT_LENGTH:
|
||||
findings.append(
|
||||
f"Kreisel {ka}/{kb}: beruehren sich auf {laenge:.0f}mm Laenge "
|
||||
f"(parallele Bahnen?) - keine Weiche angenommen")
|
||||
continue
|
||||
c = Contact("", (ka, kb), x, y, False)
|
||||
for k in (ka, kb):
|
||||
c.s[k], _d, c.section[k] = loops[k].project(x, y)
|
||||
da = loops[ka].direction(c.s[ka])
|
||||
db = loops[kb].direction(c.s[kb])
|
||||
if da[0] * db[0] + da[1] * db[1] < GLEICHLAUF_MIN_COS:
|
||||
findings.append(
|
||||
f"Kreisel {ka}/{kb}: Beruehrpunkt bei {x:.0f}, {y:.0f} laeuft "
|
||||
f"gegenlaeufig (Drehrichtung pruefen) - kein Weichenknoten")
|
||||
continue
|
||||
contacts.append(c)
|
||||
|
||||
for c in contacts:
|
||||
passend = [w for w in weichen if w.x is not None and w.y is not None
|
||||
and math.hypot(w.x - c.x, w.y - c.y) <= WEICHE_TOLERANCE]
|
||||
if passend:
|
||||
w = min(passend, key=lambda w: math.hypot(w.x - c.x, w.y - c.y))
|
||||
c.weiche_id, c.exported = w.teile_id, True
|
||||
else:
|
||||
c.weiche_id = f"W{c.kreisel[0]}_{c.kreisel[1]}"
|
||||
findings.append(
|
||||
f"Kreisel {c.kreisel[0]}/{c.kreisel[1]}: Beruehrpunkt bei "
|
||||
f"{c.x:.0f}, {c.y:.0f} ohne ILS Weiche im Export - als Weiche "
|
||||
f"{c.weiche_id} angenommen")
|
||||
# ILS Weichen ohne passenden Beruehrpunkt (z. B. an gegenlaeufigen
|
||||
# Kreiseln) bleiben unberuehrt - sie wertet tro_flow wie bisher ueber ihre
|
||||
# CSV-Nachbarn aus (_find_weiche_separators).
|
||||
return loops, contacts
|
||||
|
||||
|
||||
def _group(contacts: list[Contact]) -> list[list[Contact]]:
|
||||
"""Beruehrpunkte auf derselben Umlenkung eines Kreisels zusammenfassen."""
|
||||
parent = list(range(len(contacts)))
|
||||
|
||||
def root(i: int) -> int:
|
||||
while parent[i] != i:
|
||||
parent[i] = parent[parent[i]]
|
||||
i = parent[i]
|
||||
return i
|
||||
|
||||
seen: dict[tuple[str, str], int] = {}
|
||||
for i, c in enumerate(contacts):
|
||||
for k, section in c.section.items():
|
||||
if section == STRAIGHT:
|
||||
continue
|
||||
j = seen.setdefault((k, section), i)
|
||||
parent[root(i)] = root(j)
|
||||
groups: dict[int, list[Contact]] = {}
|
||||
for i, c in enumerate(contacts):
|
||||
groups.setdefault(root(i), []).append(c)
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def find_junctions(
|
||||
elements: list[Element],
|
||||
findings: list[str],
|
||||
separators: list[Element] | None = None,
|
||||
) -> list[Junction]:
|
||||
"""Weichenknoten zwischen Kreiseln mit ihren steuernden Separatoren.
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports.
|
||||
findings: Fundliste (siehe find_contacts).
|
||||
separators: Separatoren, die steuern duerfen; None = alle.
|
||||
|
||||
Rueckgabe:
|
||||
Weichenknoten, sortiert nach erster Weichen-Id. Knoten ohne
|
||||
steuernden Separator sind enthalten (separators leer) - der Aufrufer
|
||||
entscheidet, was damit geschieht.
|
||||
"""
|
||||
loops, contacts = find_contacts(elements, findings)
|
||||
if separators is None:
|
||||
separators = [e for e in elements if e.kind == "Separator"]
|
||||
|
||||
# Separator -> (Kreisel, s): auf dem naechstgelegenen Umlauf
|
||||
sep_pos: dict[str, tuple[str, float]] = {}
|
||||
sep_lane: dict[str, str] = {}
|
||||
for sep in separators:
|
||||
if sep.x is None or sep.y is None:
|
||||
continue
|
||||
best: tuple[float, str, float] | None = None
|
||||
for k, lp in loops.items():
|
||||
s, dist, _sec = lp.project(sep.x, sep.y)
|
||||
if dist <= SEPARATOR_TOLERANCE and (best is None or dist < best[0]):
|
||||
best = (dist, k, s)
|
||||
if best is not None:
|
||||
sep_pos[sep.teile_id] = (best[1], best[2])
|
||||
sep_lane[sep.teile_id] = f"{best[1]}-{loops[best[1]].lane(sep.x, sep.y)}"
|
||||
|
||||
junctions: list[Junction] = []
|
||||
for group in _group(contacts):
|
||||
kreisel = sorted({k for c in group for k in c.kreisel})
|
||||
j = Junction(sorted(group, key=lambda c: c.weiche_id), kreisel)
|
||||
in_group = {id(c) for c in group}
|
||||
for k in kreisel:
|
||||
# Ereignisse auf Kreisel k: (s, Art, Id); Art "sep" oder "swi"
|
||||
events = [(s, "sep", sid) for sid, (kk, s) in sep_pos.items() if kk == k]
|
||||
events += [(c.s[k], "own" if id(c) in in_group else "swi", c.weiche_id)
|
||||
for c in contacts if k in c.s]
|
||||
events.sort()
|
||||
n = len(events)
|
||||
for idx, (_s, art, _id) in enumerate(events):
|
||||
if art != "own":
|
||||
continue
|
||||
# gegen die Laufrichtung zurueck bis zum ersten fremden Ereignis
|
||||
for step in range(1, n):
|
||||
_ps, part, pid = events[(idx - step) % n]
|
||||
if part == "own":
|
||||
continue
|
||||
if part == "sep" and pid not in j.separators:
|
||||
j.separators.append(pid)
|
||||
j.sep_kreisel[pid] = k
|
||||
j.sep_lane[pid] = sep_lane[pid]
|
||||
break
|
||||
j.separators.sort()
|
||||
junctions.append(j)
|
||||
junctions.sort(key=lambda j: j.contacts[0].weiche_id)
|
||||
return junctions
|
||||
+178
-37
@@ -79,6 +79,8 @@ from dataclasses import dataclass, field, replace
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from parameter import get_float, get_int
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -129,10 +131,10 @@ STATION_PATTERNS = (
|
||||
("Ausgang", ("ausgang",)),
|
||||
)
|
||||
|
||||
# Max. Abstand (mm), ab dem ein generisches Eingang/Ausgang-Symbol als
|
||||
# "deckungsgleich" mit einer BTMT-Station gilt und verschluckt wird. Die
|
||||
# beobachteten Paare liegen bei 256-431 mm, echte getrennte Stationen >1000 mm.
|
||||
STATION_MERGE_MM = 600.0
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [material_flow] - Bedeutung
|
||||
# und Einheit stehen dort (lib/parameter.py).
|
||||
_P = "material_flow"
|
||||
STATION_MERGE_MM = get_float(_P, "station_merge_mm")
|
||||
|
||||
# Transfer-/Weichenelemente aus dem erweiterten Export. Sie bilden KEINE eigenen
|
||||
# Knoten, liefern aber die *gerichtete* Verbindung zwischen den Transportobjekten:
|
||||
@@ -199,11 +201,8 @@ NODE_STYLES = {
|
||||
}
|
||||
DEFAULT_NODE_STYLE = dict(fillcolor="#f2f2f2", fontcolor="#404040", color="#808080")
|
||||
|
||||
# Ab dieser Anzahl paralleler Gefaellestrecken zwischen denselben zwei
|
||||
# Kreisel-Bahnen (gleicher Zulauf, gleicher Ablauf, gleiche Steigung) werden
|
||||
# sie im Materialfluss-Diagramm zu einem Block zusammengefasst - sonst wird
|
||||
# eine grosse Linienbank (Speicher) unlesbar breit.
|
||||
MIN_GEFAELLE_GROUP = 2
|
||||
# Parallele Gefaellestrecken je Block im Materialfluss-Diagramm (parameter.ini)
|
||||
MIN_GEFAELLE_GROUP = get_int(_P, "min_gefaelle_group")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1893,13 +1892,11 @@ def _node_label(node: Node) -> str:
|
||||
return _dot_label(lines)
|
||||
|
||||
|
||||
# Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in
|
||||
# Millimeter. 0.25 pt/mm (1:4) - wie in tro_flow.py, damit Material- und TRO-Graph
|
||||
# im selben Massstab lagerichtig sind.
|
||||
COORD_SCALE = 0.25
|
||||
# Die beiden Bahnen (L/R) eines Kreisels teilen sich eine Position; im
|
||||
# lagerichtigen Layout leicht versetzen, damit sie nicht uebereinander liegen.
|
||||
LANE_NUDGE_MM = 350.0
|
||||
# Lagerichtiges Layout (--use-cords): Massstab, Bahnversatz, Legendenabstand
|
||||
# (parameter.ini [material_flow])
|
||||
COORD_SCALE = get_float(_P, "coord_scale")
|
||||
LANE_NUDGE_MM = get_float(_P, "lane_nudge_mm")
|
||||
LEGEND_OFFSET_MM = get_float(_P, "legend_offset_mm")
|
||||
|
||||
|
||||
def _node_xy(node: "Node") -> tuple[float, float] | None:
|
||||
@@ -2110,7 +2107,7 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
xs = [xy[0] for n in graph.nodes.values() if (xy := _node_xy(n))]
|
||||
ys = [xy[1] for n in graph.nodes.values() if (xy := _node_xy(n))]
|
||||
if xs and ys:
|
||||
lx, ly = min(xs), min(ys) - 3000.0 # etwas unterhalb der Anlage
|
||||
lx, ly = min(xs), min(ys) - LEGEND_OFFSET_MM # unterhalb der Anlage
|
||||
legend_pos = (f' pos="{lx * COORD_SCALE:.2f},'
|
||||
f'{ly * COORD_SCALE:.2f}",')
|
||||
|
||||
@@ -2198,10 +2195,157 @@ def _track_node_html_label(node: TrackNode) -> str:
|
||||
)
|
||||
|
||||
|
||||
def render_track_dot(track: TrackGraph) -> str:
|
||||
# Container-Arten fuer die Zugehoerigkeit im Mouseover (--track)
|
||||
CONTAINER_TEXT = {"Kreisel": "Kreisel", "Strecke": "VF", "Gefaellestrecke": "GF"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TooltipContext:
|
||||
"""Nachschlagedaten fuer die Mouseover-Texte des --track-Graphen."""
|
||||
|
||||
by_id: dict[str, Element]
|
||||
containers: dict[str, list[Element]] # Element-Id -> Container mit ihm in TrackIds
|
||||
loops: dict[str, object] # Kreisel-Id -> kreisel_junctions.Loop
|
||||
contacts: dict[str, tuple[str, str]] # Weichen-Id -> beruehrende Kreisel (Geometrie)
|
||||
|
||||
|
||||
def _tooltip_context(elements: list[Element]) -> _TooltipContext:
|
||||
"""Container-Mitgliedschaft und Kreisel-Geometrie fuer die Mouseover-Texte.
|
||||
|
||||
kreisel_junctions baut auf diesem Modul auf; der Import steht deshalb hier
|
||||
in der Funktion (beim Aufruf ist material_flow laengst geladen).
|
||||
"""
|
||||
from kreisel_junctions import find_contacts
|
||||
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
containers: dict[str, list[Element]] = {}
|
||||
for c in elements:
|
||||
if c.kind in CONTAINER_TEXT:
|
||||
for tid in c.track_ids:
|
||||
containers.setdefault(tid, []).append(c)
|
||||
loops, contacts = find_contacts(elements, [])
|
||||
return _TooltipContext(by_id, containers, loops,
|
||||
{c.weiche_id: c.kreisel for c in contacts})
|
||||
|
||||
|
||||
def _container_text(ctx: _TooltipContext, ref: str) -> str:
|
||||
"""'0009-L' -> 'Kreisel Kreisel9 (0009) Bahn L', '0014' -> 'VF VF_2 (0014)'."""
|
||||
base, _sep, lane = ref.partition("-")
|
||||
el = ctx.by_id.get(base)
|
||||
if el is None:
|
||||
return f"{ref} (unbekannt)"
|
||||
art = CONTAINER_TEXT.get(el.kind, el.kind)
|
||||
name = el.name if el.name.lower().startswith(art.lower()) else f"{art} {el.name}"
|
||||
text = f"{name} ({base})"
|
||||
return f"{text} Bahn {lane}" if lane else text
|
||||
|
||||
|
||||
def _geometry_text(ctx: _TooltipContext, el: Element, station: bool = False
|
||||
) -> tuple[str, str | None]:
|
||||
"""Lage auf einem Kreisel aus der Geometrie.
|
||||
|
||||
Parameter:
|
||||
ctx: Nachschlagedaten (_tooltip_context).
|
||||
el: das Bauteil.
|
||||
station: True fuer Be-/Entladung - sie steht neben dem Umlauf, darum
|
||||
zaehlt dort der naechste Kreisel unabhaengig vom Abstand.
|
||||
|
||||
Rueckgabe:
|
||||
(Text, Kreisel-Id oder None, wenn das Bauteil auf keinem Kreisel liegt).
|
||||
"""
|
||||
if el.x is None or el.y is None or not ctx.loops:
|
||||
return "Geometrie: keine Position", None
|
||||
dist, k = min((lp.project(el.x, el.y)[1], k) for k, lp in ctx.loops.items())
|
||||
kreisel = _container_text(ctx, k)
|
||||
if station:
|
||||
return f"Geometrie: naechster Kreisel {kreisel}, {dist:.0f}mm entfernt", k
|
||||
from kreisel_junctions import SEPARATOR_TOLERANCE
|
||||
|
||||
if dist <= SEPARATOR_TOLERANCE:
|
||||
return f"Geometrie: liegt auf {kreisel} Bahn {ctx.loops[k].lane(el.x, el.y)}", k
|
||||
return f"Geometrie: auf keinem Kreisel, naechster {kreisel} {dist:.0f}mm", None
|
||||
|
||||
|
||||
def _track_node_tooltip(node: TrackNode, ctx: _TooltipContext | None) -> str:
|
||||
"""Mouseover-Text eines Knotens: Zugehoerigkeit zu Kreisel/VF/GF.
|
||||
|
||||
Separator: Zuordnung (CSV), TrackIds-Kette(n), Lage laut Geometrie
|
||||
Weiche: die beiden erzeugenden Kreisel (Merkmale Kreisel A/B),
|
||||
Beruehrart, TrackIds-Kette(n), Beruehrpunkt laut Geometrie
|
||||
Be-/Entladung: Kreisel, auf dessen Umlauf sie liegt (TrackIds), Geometrie
|
||||
Widersprechen sich die Quellen, kommt eine Zeile mit "!" dazu.
|
||||
"""
|
||||
el = node.element
|
||||
if el is None:
|
||||
return _track_node_label(node)
|
||||
lines = [f"{el.teile_art} {el.teile_id} '{el.name}' (Zeile {el.row})"]
|
||||
if ctx is None:
|
||||
return _dot_label(lines)
|
||||
ketten = ctx.containers.get(el.teile_id, [])
|
||||
ketten_ids = {c.teile_id for c in ketten}
|
||||
ketten_text = ", ".join(_container_text(ctx, c.teile_id) for c in ketten) or "-"
|
||||
warn: list[str] = []
|
||||
|
||||
if node.kind == "Weiche":
|
||||
a = str(_merkmal(el.merkmale, "Kreisel A") or "")
|
||||
b = str(_merkmal(el.merkmale, "Kreisel B") or "")
|
||||
lines.append(f"Kreisel A: {_container_text(ctx, a) if a else '-'}")
|
||||
lines.append(f"Kreisel B: {_container_text(ctx, b) if b else '-'}")
|
||||
for key in ("Beruehrung", "Achse von Kreisel", "Erzeugt aus"):
|
||||
if (v := _merkmal(el.merkmale, key)) is not None:
|
||||
lines.append(f"{key}: {v}")
|
||||
lines.append(f"TrackIds-Kette: {ketten_text}")
|
||||
geo = ctx.contacts.get(el.teile_id)
|
||||
if geo is None:
|
||||
lines.append("Geometrie: an keinem gleichlaufenden Kreisel-Beruehrpunkt")
|
||||
warn.append("Weiche ohne stimmigen Beruehrpunkt (Drehrichtung pruefen)")
|
||||
else:
|
||||
lines.append(f"Geometrie: Beruehrpunkt Kreisel {geo[0]} / {geo[1]}")
|
||||
if a and b and set(geo) != {a, b}:
|
||||
warn.append(f"Kreisel A/B ({a}/{b}) passen nicht zur Geometrie")
|
||||
else:
|
||||
zuordnung = el.assignment
|
||||
if zuordnung and zuordnung != UNASSIGNED:
|
||||
lines.append(f"Zuordnung: {_container_text(ctx, zuordnung)}")
|
||||
elif node.kind == "Separator":
|
||||
lines.append("Zuordnung: -")
|
||||
lines.append(f"TrackIds-Kette: {ketten_text}")
|
||||
station = node.kind in ("Beladung", "Entladung")
|
||||
geo_text, geo_k = _geometry_text(ctx, el, station=station)
|
||||
lines.append(geo_text)
|
||||
zu_base = zuordnung.partition("-")[0] if zuordnung else ""
|
||||
zu_el = ctx.by_id.get(zu_base)
|
||||
if zu_el is not None and ketten_ids and zu_base not in ketten_ids:
|
||||
warn.append("Zuordnung und TrackIds-Kette widersprechen sich")
|
||||
if zu_el is not None and zu_el.kind == "Kreisel" and geo_k and geo_k != zu_base:
|
||||
warn.append(f"Zuordnung Kreisel {zu_base}, liegt aber auf Kreisel {geo_k}")
|
||||
kreisel_ketten = {c.teile_id for c in ketten if c.kind == "Kreisel"}
|
||||
if station and geo_k and kreisel_ketten and geo_k not in kreisel_ketten:
|
||||
warn.append(f"TrackIds-Kette nennt {', '.join(sorted(kreisel_ketten))}, "
|
||||
f"naechster Kreisel ist aber {geo_k}")
|
||||
if not ketten and el.kind not in CONTAINER_TEXT:
|
||||
warn.append("in keiner TrackIds-Kette")
|
||||
if (geo_k is None and not station
|
||||
and not (zu_el is not None and zu_el.kind == "Kreisel")):
|
||||
# Bauteil einer VF/GF: der naechste Kreisel ist dort ohne Belang
|
||||
lines[-1] = "Geometrie: liegt auf keinem Kreisel"
|
||||
lines.extend(f"! {w}" for w in warn)
|
||||
return _dot_label(lines)
|
||||
|
||||
|
||||
def render_track_dot(track: TrackGraph, elements: list[Element] | None = None) -> str:
|
||||
"""DOT des Bahnzusammenhang-Graphen (--track); siehe build_track_graph()
|
||||
und doc/track.md. Gerendert ueber dieselben graph_to_svg/dot_to_svg wie
|
||||
render_dot()."""
|
||||
render_dot().
|
||||
|
||||
Parameter:
|
||||
track: der Bahnzusammenhang-Graph.
|
||||
elements: alle Elemente des CSV-Exports; damit bekommt jeder Knoten
|
||||
einen Mouseover-Text (tooltip, im SVG als Hover-Titel) mit seiner
|
||||
Zugehoerigkeit zu Kreisel/VF/GF (siehe _track_node_tooltip).
|
||||
None = nur die Bezeichnung.
|
||||
"""
|
||||
ctx = _tooltip_context(elements) if elements else None
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
|
||||
@@ -2228,7 +2372,8 @@ def render_track_dot(track: TrackGraph) -> str:
|
||||
label_attr = f"label={_track_node_html_label(node)}"
|
||||
else:
|
||||
label_attr = f'label="{_track_node_label(node)}"'
|
||||
add(f' "{node_id}" [{label_attr}, {attrs}{extra}];')
|
||||
tooltip = f', tooltip="{_track_node_tooltip(node, ctx)}"'
|
||||
add(f' "{node_id}" [{label_attr}, {attrs}{extra}{tooltip}];')
|
||||
add("")
|
||||
|
||||
for edge in track.edges:
|
||||
@@ -2411,18 +2556,14 @@ BBS_DEFAULT_STYLE = dict(fill="#f2f2f2", stroke="#808080", text="#404040")
|
||||
BBS_DETAIL_KINDS = ("Bogen", "Streckenelement", "Motorstation", "Umlenkstation")
|
||||
BBS_MODES = ("flow", "all")
|
||||
|
||||
# Legende (--show-bbs): Kaestchen- und Schriftgroesse in mm.
|
||||
BBS_LEGEND_BOX_MM = 250.0
|
||||
BBS_LEGEND_FONT_MM = 180.0
|
||||
|
||||
# Rand um die Anlage herum und minimale Schriftgroesse, jeweils in mm.
|
||||
BBS_MARGIN_MM = 500.0
|
||||
BBS_MIN_FONT_MM = 40.0
|
||||
|
||||
# Darstellung der Anschlusspunkte K1-K4 (--show-kx): Groesse des Kreuzes und
|
||||
# der Beschriftung in mm, Verbindungslinie K1->K2->K3->K4 in Blau.
|
||||
KX_CROSS_MM = 120.0
|
||||
KX_FONT_MM = 90.0
|
||||
# Groessen fuer --show-bbs/--show-kx (parameter.ini [material_flow]); die
|
||||
# Verbindungslinie K1->K2->K3->K4 ist blau.
|
||||
BBS_LEGEND_BOX_MM = get_float(_P, "bbs_legend_box_mm")
|
||||
BBS_LEGEND_FONT_MM = get_float(_P, "bbs_legend_font_mm")
|
||||
BBS_MARGIN_MM = get_float(_P, "bbs_margin_mm")
|
||||
BBS_MIN_FONT_MM = get_float(_P, "bbs_min_font_mm")
|
||||
KX_CROSS_MM = get_float(_P, "kx_cross_mm")
|
||||
KX_FONT_MM = get_float(_P, "kx_font_mm")
|
||||
KX_CROSS_COLOR = "#c00000"
|
||||
KX_LINK_COLOR = "#1f5fd0"
|
||||
|
||||
@@ -2708,10 +2849,10 @@ def _container_children(element: Element, by_id: dict[str, Element]) -> list[Ele
|
||||
# Keine Anbauteile (Separator, Scanner, Weiche, ...) und keine Ein-/
|
||||
# Ausgabestationen - die kommen in einem spaeteren Schritt dazu.
|
||||
|
||||
CHAIN_MAX_W = 1400.0
|
||||
CHAIN_MAX_H = 850.0
|
||||
CHAIN_MARGIN = 70.0
|
||||
CHAIN_TITLE_H = 60.0
|
||||
CHAIN_MAX_W = get_float(_P, "chain_max_w") # Groessen: parameter.ini
|
||||
CHAIN_MAX_H = get_float(_P, "chain_max_h")
|
||||
CHAIN_MARGIN = get_float(_P, "chain_margin")
|
||||
CHAIN_TITLE_H = get_float(_P, "chain_title_h")
|
||||
|
||||
CHAIN_EDGE_COLOR = {
|
||||
EDGE_FLOW: "#2f5597",
|
||||
@@ -3793,7 +3934,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
track = build_track_graph(elements, by_id, graph, warnings)
|
||||
if args.track:
|
||||
track_dot_target = results / f"{stem}_track.dot"
|
||||
track_dot_target.write_text(render_track_dot(track), encoding="utf-8")
|
||||
track_dot_target.write_text(render_track_dot(track, elements), encoding="utf-8")
|
||||
track_dot_file = track_dot_target
|
||||
if args.tosvg:
|
||||
track_svg_target = results / f"{stem}_track.svg"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
parameter.py - Zahlenwerte der Bibliotheken aus cfg/parameter.ini.
|
||||
|
||||
Toleranzen, Schwellen, Massstaebe und Zeichnungsgroessen stehen nicht im Code,
|
||||
sondern in einer INI-Datei: ein Abschnitt je Modul, der Schluessel ist der
|
||||
Name der Konstanten in Kleinbuchstaben ([kreisel_junctions]
|
||||
contact_tolerance -> kreisel_junctions.CONTACT_TOLERANCE).
|
||||
|
||||
Gelesen wird zuerst die Grunddatei des Repositorys (<Projekt>/cfg/
|
||||
parameter.ini, versioniert, muss vollstaendig sein). Liegt in %SKEL_CFG% eine
|
||||
weitere parameter.ini, ueberschreibt sie einzelne Werte - so kann eine Anlage
|
||||
abweichende Toleranzen bekommen, ohne die Grunddatei zu aendern.
|
||||
|
||||
Fehlt die Grunddatei oder ein Schluessel, bricht der Import mit
|
||||
ParameterError ab: einen stillen Ersatzwert im Code gibt es bewusst nicht,
|
||||
sonst stuende die Zahl doch wieder im Code.
|
||||
|
||||
Unterste Schicht: importiert nichts aus lib/, damit jedes Modul es nutzen kann.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
PARAMETER_FILE = "parameter.ini"
|
||||
|
||||
__all__ = ["PARAMETER_FILE", "ParameterError", "files", "get_float", "get_int"]
|
||||
|
||||
|
||||
class ParameterError(RuntimeError):
|
||||
"""Grunddatei fehlt oder ein Wert fehlt bzw. ist keine Zahl."""
|
||||
|
||||
|
||||
def _base_file() -> Path:
|
||||
"""Grunddatei im Repository: <Projekt>/cfg/parameter.ini."""
|
||||
return Path(__file__).resolve().parent.parent / "cfg" / PARAMETER_FILE
|
||||
|
||||
|
||||
def files() -> list[Path]:
|
||||
"""Gelesene Dateien in Lesereihenfolge (Grunddatei, ggf. %SKEL_CFG%)."""
|
||||
result = [_base_file()]
|
||||
cfg = os.environ.get("SKEL_CFG")
|
||||
if cfg:
|
||||
override = Path(cfg) / PARAMETER_FILE
|
||||
if override.is_file() and override.resolve() != result[0].resolve():
|
||||
result.append(override)
|
||||
return result
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _parser() -> configparser.ConfigParser:
|
||||
"""Alle Parameterdateien einlesen (einmal je Prozess)."""
|
||||
base = _base_file()
|
||||
if not base.is_file():
|
||||
raise ParameterError(f"Parameterdatei fehlt: {base}")
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
|
||||
for path in files():
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
parser.read_file(handle, source=str(path))
|
||||
return parser
|
||||
|
||||
|
||||
def _raw(section: str, key: str) -> str:
|
||||
"""Rohwert; ParameterError, wenn Abschnitt oder Schluessel fehlt."""
|
||||
parser = _parser()
|
||||
if not parser.has_option(section, key):
|
||||
raise ParameterError(
|
||||
f"[{section}] {key} fehlt in {', '.join(str(p) for p in files())}")
|
||||
return parser.get(section, key)
|
||||
|
||||
|
||||
def get_float(section: str, key: str) -> float:
|
||||
"""Zahl aus [section] key.
|
||||
|
||||
Parameter:
|
||||
section: Abschnitt, in der Regel der Modulname (z. B. "tro_flow").
|
||||
key: Schluessel, der Konstantenname in Kleinbuchstaben.
|
||||
|
||||
Rueckgabe:
|
||||
Der Wert als float.
|
||||
"""
|
||||
raw = _raw(section, key)
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine Zahl") from None
|
||||
|
||||
|
||||
def get_int(section: str, key: str) -> int:
|
||||
"""Ganze Zahl aus [section] key (Anzahlen, Schrittweiten)."""
|
||||
raw = _raw(section, key)
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine ganze Zahl") from None
|
||||
+6
-5
@@ -108,6 +108,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 model import Analysis
|
||||
from parameter import get_float
|
||||
from tro_flow import (
|
||||
BOUNDARY_ENTRY,
|
||||
BOUNDARY_EXIT,
|
||||
@@ -131,11 +132,11 @@ BLOCK_PREFIX = "TRO_SYM_"
|
||||
# SSG_BLOCKEDIT in dxfmakros ihn ueber "CONNECTION_*" erkennt.
|
||||
CONNECTION_BLOCK = "CONNECTION_ARROW"
|
||||
|
||||
# Zeichnungsgroessen in mm, wenn nicht anders angegeben
|
||||
DEFAULT_MARKER = 300.0
|
||||
DEFAULT_TEXT = 220.0
|
||||
# Restfehler, ab dem der Lagebezug abgelehnt wird
|
||||
DEFAULT_MAX_RESIDUAL = 600.0
|
||||
# Zeichnungsgroessen und Restfehler-Grenze des Lagebezugs aus cfg/parameter.ini,
|
||||
# Abschnitt [tro_annotate] (lib/parameter.py)
|
||||
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
||||
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
||||
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
||||
|
||||
|
||||
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||
|
||||
@@ -619,6 +619,17 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 2,
|
||||
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#f2a65a"),
|
||||
# ACHTUNG - NICHT aus SCL belegt, wie 2Sep2Swi. Aufgenommen am 2026-10-14
|
||||
# fuer die Testanlage data/export.csv: Weichenknoten, an dem die Umlenkung
|
||||
# eines Kreisels zwei andere beruehrt (Kreisel 5 an 4 und 6) - drei
|
||||
# Separatoren laufen in Laufrichtung auf die zwei Weichen zu (siehe
|
||||
# lib/kreisel_junctions.py). Scanner je Separator analog zu 2Sep2Swi.
|
||||
# FB_ILS_MTRO_3Sep2Swi existiert noch nicht in der ILSLib.
|
||||
TroDefinition("3Sep2Swi", "FB_ILS_MTRO_3Sep2Swi", {
|
||||
TroItem.SEPARATOR: 3,
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 3,
|
||||
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#d9822b"),
|
||||
)
|
||||
|
||||
_BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG}
|
||||
|
||||
+415
-59
@@ -78,6 +78,49 @@ Hinweis ausgegeben.
|
||||
Zusaetzlich: hat ein Host 2 bzw. 3 abgehende Flusskanten, wird 1Sep zu
|
||||
1Sep1Swi bzw. 1Sep2Swi aufgewertet (Weiche).
|
||||
|
||||
Wo im Code der Typ entsteht
|
||||
---------------------------
|
||||
Einstieg ist analyse_tros(); den TRO-Typ (DerivedTro.type_name) setzt aber
|
||||
ausschliesslich derive_tros(), Schritt fuer Schritt:
|
||||
|
||||
Schritt Bauteil-Muster im CSV -> TRO-Typ
|
||||
-----------------------------------------------------------------------
|
||||
1 >= 2 Gefaellestrecken mit Separator, PinStore_Auto
|
||||
gemeinsamer Zu-/Ablauf (_group_lanes,
|
||||
_speicher_bloecke)
|
||||
1 einzelne Gefaellestrecke mit Separator 1Sep
|
||||
2 Separator in der TrackIds-Kette einer Vario / 1Sep
|
||||
Strecke (alternierendes Muster, (abwechselnd)
|
||||
_incoming_drop)
|
||||
2 Separator mit Zuordnung auf eine Strecke, 1Sep
|
||||
aber nicht in deren TrackIds-Kette
|
||||
3 Separator auf einer Kreisel-Bahn 1Sep
|
||||
3b Separator vor einem Ausschleuselement 1Sep1Swi
|
||||
(_find_switch_separators, _weichen_*)
|
||||
3b' derselbe Separator vor einem zweiten 1Sep2Swi
|
||||
Ausschleuselement ohne Separator dazwischen
|
||||
3c Weichenknoten zwischen Kreiseln: n Separa- 1Sep1Swi, 2Sep1Swi,
|
||||
toren in Laufrichtung unmittelbar vor m 2Sep2Swi, 3Sep2Swi
|
||||
Beruehrpunkten derselben Umlenkung
|
||||
(_kreisel_knoten, lib/kreisel_junctions.py)
|
||||
3c' Separator an einer ILS Weiche, die kein 1Sep1Swi
|
||||
Weichenknoten aus 3c ist
|
||||
(_find_weiche_separators)
|
||||
3d 3b-TRO und 3c'-TRO direkt hintereinander 2Sep2Swi
|
||||
in der TrackIds-Kette eines Kreisels
|
||||
4 1Sep mit 2 bzw. 3 abgehenden Flusskanten 1Sep1Swi / 1Sep2Swi
|
||||
|
||||
Alle anderen Funktionen (link_tros, locate_tros, derive_boundaries, ...)
|
||||
aendern den Typ nicht mehr. cfg/tro_overrides.ini wird hier seit
|
||||
2026-10-05 nicht mehr angewendet (siehe tro_overrides.py) - ein falsch
|
||||
erkannter Typ wird in derive_tros() korrigiert, nicht per INI.
|
||||
|
||||
Nicht abgeleitet werden bisher: 1Sep_SSCC (nur Hinweis, Schritt 6),
|
||||
EmptyCarrBuffer und LoadingBoom. Schritt 3c braucht eine stimmige
|
||||
Drehrichtung beider Kreisel: laufen sie am Beruehrpunkt gegeneinander
|
||||
(Mubea, Dematic - vermutlich Exportfehler), bleibt es fuer diese ILS Weiche
|
||||
bei 3c' und 3d.
|
||||
|
||||
Typen, FB-Bausteine, Bauteile und Farben kommen ausschliesslich aus
|
||||
lib/tro_catalog.py.
|
||||
|
||||
@@ -108,6 +151,7 @@ import csv
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
@@ -123,6 +167,7 @@ from material_flow import (
|
||||
UNASSIGNED,
|
||||
Element,
|
||||
Graph,
|
||||
Node,
|
||||
_as_float,
|
||||
_dot_escape,
|
||||
_dot_label,
|
||||
@@ -133,7 +178,9 @@ from material_flow import (
|
||||
read_elements,
|
||||
resolve_input,
|
||||
)
|
||||
from kreisel_junctions import Junction, find_junctions
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from parameter import get_float, get_int
|
||||
from tro_ids import (
|
||||
DEFAULT_TOLERANCE,
|
||||
TroIds,
|
||||
@@ -159,42 +206,19 @@ from tro_catalog import (
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Ab dieser Anzahl zusammenhaengender Schwerkraftlinien (Gefaellestrecke mit je
|
||||
# einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen.
|
||||
MIN_STORE_LANES = 2
|
||||
|
||||
# Mindest-Hoehenverlust (mm) auf dem der Reihe nach vorangehenden TrackIds-
|
||||
# Segment, damit ein Separator auf einer "Strecke" trotzdem als 1Sep statt
|
||||
# Vario gilt (siehe _incoming_drop). Muss nur "horizontal" gefuehrte, aber
|
||||
# tatsaechlich Gefaelle-Segmente erwischen (HundM05 Separator 0035: -104mm auf
|
||||
# 3,8m), nicht das Rauschen eines echten flachen VF-Laufs (HundM05 Separator
|
||||
# 0036: +26mm auf 0,5m). 30mm liegt bewusst weit unter dem echten Befund und
|
||||
# weit ueber dem Rauschen.
|
||||
GEFAELLE_DROP_MM = 30.0
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [tro_flow] - Bedeutung und
|
||||
# Einheit stehen dort (lib/parameter.py).
|
||||
_P = "tro_flow"
|
||||
MIN_STORE_LANES = get_int(_P, "min_store_lanes")
|
||||
GEFAELLE_DROP_MM = get_float(_P, "gefaelle_drop_mm")
|
||||
COORD_SCALE = get_float(_P, "coord_scale")
|
||||
COORD_PFEIL_LAENGE = get_int(_P, "coord_pfeil_laenge")
|
||||
COORD_MAX_LUECKE = get_int(_P, "coord_max_luecke")
|
||||
LEGEND_MARGIN = get_int(_P, "legend_margin")
|
||||
LEGEND_DROP = get_int(_P, "legend_drop")
|
||||
|
||||
GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
|
||||
|
||||
# Zeichnungsmassstab fuer --use-cords: Graphviz rechnet in Punkt, die Anlage in
|
||||
# Millimeter. Seit die Symbole eine feste Groesse haben (COORD_SYMBOL_SIZE)
|
||||
# statt mit ihrem Label zu wachsen, reichen 0.1 pt/mm: ~11 x 20 m Anlage ergeben
|
||||
# etwa 1100 x 2000 pt - bildschirmfuellend lesbar. Vorher 0.25 (riesige
|
||||
# Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01).
|
||||
COORD_SCALE = 0.1
|
||||
|
||||
# Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
|
||||
# (mm) ab dem angehaengten TRO (siehe _station_positionen).
|
||||
COORD_PFEIL_LAENGE = 1300
|
||||
|
||||
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
|
||||
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
|
||||
# TRO) werden auf diesen Wert gekuerzt - Reihenfolge und Nachbarschaft der
|
||||
# Knoten bleiben erhalten, nur der Leerraum verschwindet (Review 2026-10-01:
|
||||
# "zu verstreut, zu viel Weissraum"). --true-scale schaltet das ab.
|
||||
COORD_MAX_LUECKE = 1500
|
||||
|
||||
# Abstand der Legende zum Anlagenrand in mm
|
||||
LEGEND_MARGIN = 3000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TRO-Ermittlung
|
||||
@@ -207,6 +231,17 @@ def _group_lanes(lanes: list[str], graph: Graph) -> list[list[str]]:
|
||||
|
||||
Linien, die sich einen Zu- oder Ablaufknoten teilen, gehoeren zum selben
|
||||
Speicher. So entstehen bei zwei getrennten Speichern auch zwei TROs.
|
||||
|
||||
Parameter:
|
||||
lanes: Knoten-Ids der Gefaellestrecken, die einen Separator tragen
|
||||
(derive_tros Schritt 1).
|
||||
graph: Materialflussgraph aus material_flow.build_graph; nur
|
||||
graph.edges wird gelesen (Nachbarschaft der Linien).
|
||||
|
||||
Rueckgabe:
|
||||
Liste von Gruppen, jede eine sortierte Liste von Linien-Ids; die
|
||||
Gruppen sind nach ihrer ersten Linie sortiert. Eine Gruppe mit
|
||||
>= MIN_STORE_LANES Linien wird in derive_tros zu PinStore_Auto.
|
||||
"""
|
||||
adjacency: dict[str, set[str]] = {}
|
||||
for lane in lanes:
|
||||
@@ -246,6 +281,18 @@ def _kreisel_bahn(sep: Element, elements: list[Element], graph: Graph) -> str |
|
||||
der Achsrichtung L, rechts R - dieselbe Konvention wie die Zuordnungen,
|
||||
die der Export richtig setzt (0038 unterhalb von Kreisel2 mit Drehung 180
|
||||
-> L, 0047 oberhalb von Kreisel3 -> R).
|
||||
|
||||
Parameter:
|
||||
sep: der Separator (Element.kind "Separator").
|
||||
elements: alle Elemente des CSV-Exports; durchsucht werden die
|
||||
Kreisel und ihre track_ids.
|
||||
graph: Materialflussgraph; die gefundene Bahn muss dort ein Knoten
|
||||
sein (z. B. "0002-L").
|
||||
|
||||
Rueckgabe:
|
||||
Knoten-Id der Bahn ("<Kreisel-Id>-L" bzw. "-R") oder None, wenn der
|
||||
Separator in keiner Kreiselkette steht, Position bzw. "Drehung"
|
||||
fehlen oder die Bahn kein Graphknoten ist.
|
||||
"""
|
||||
for k in elements:
|
||||
if k.kind != "Kreisel" or sep.teile_id not in k.track_ids:
|
||||
@@ -278,6 +325,16 @@ def _weichen_nach_umlauf(
|
||||
Schluss mit dem Ausschleuselement 0057 von VF_1 "benachbart" (TRO03 als
|
||||
Weiche in VF_1), 0046 liegt hinter dem letzten Ausschleuselement von
|
||||
Speicherblock C. Beide sind gewoehnliche Kreisel-Separatoren (Schritt 3).
|
||||
|
||||
Parameter:
|
||||
switch_hits: Ergebnis von _find_switch_separators
|
||||
(Separator-Id -> (Ausschleuselement, Abstand mm)).
|
||||
elements: alle Elemente des CSV-Exports (Kreisel mit track_ids).
|
||||
findings: Fundliste; je verworfenem Paar wird ein Eintrag angehaengt.
|
||||
|
||||
Rueckgabe:
|
||||
Gefiltertes switch_hits. Paare ausserhalb einer Kreiselkette bleiben
|
||||
ungeprueft erhalten.
|
||||
"""
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
kreisel = [e for e in elements if e.kind == "Kreisel" and e.track_ids]
|
||||
@@ -324,6 +381,18 @@ def _weichen_aus_umlauf(
|
||||
Ausschleuselemente einer Gefaellestrecke bleiben aussen vor: das sind
|
||||
Speicherlinien, ihr Steuer-Separator entsteht in _speicher_bloecke nur
|
||||
aus der CSV-Nachbarschaft (Mubea 0044 bleibt eigener 1Sep vor dem Block).
|
||||
|
||||
Parameter:
|
||||
switch_hits: bisherige Paare (Separator-Id -> (Ausschleuselement,
|
||||
Abstand mm)), meist aus _weichen_nach_umlauf.
|
||||
kandidaten: Separatoren, die ueberhaupt Weiche sein duerfen (in
|
||||
derive_tros: ohne die eigenen VF-Kettenseparatoren).
|
||||
elements: alle Elemente des CSV-Exports.
|
||||
findings: Fundliste; je ergaenztem Paar wird ein Eintrag angehaengt.
|
||||
|
||||
Rueckgabe:
|
||||
Neues dict: switch_hits plus die ergaenzten Paare. Jedes
|
||||
Ausschleuselement gehoert hoechstens einem Separator.
|
||||
"""
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
erlaubt = {s.teile_id for s in kandidaten}
|
||||
@@ -381,8 +450,16 @@ def _speicher_bloecke(
|
||||
Mubea 0044 (TRO05) ist durch ein Einschleuselement vom Block getrennt,
|
||||
kein Nachbar eines Ausschleuselements und bleibt deshalb eigener 1Sep.
|
||||
|
||||
Rueckgabe: (Linie -> Block, Steuer-Separator -> Block). Linien ohne
|
||||
Ausschleuselement in einer Kreisel-Kette fehlen in der ersten Zuordnung.
|
||||
Parameter:
|
||||
lanes: Knoten-Ids der Gefaellestrecken mit Separator.
|
||||
elements: alle Elemente des CSV-Exports (Ausschleuselemente mit
|
||||
Merkmal "Strecke"/"Foerderer", Kreisel mit track_ids).
|
||||
switch_hits: Weichen-Separatoren (Separator-Id -> (Ausschleuselement,
|
||||
Abstand mm)), siehe _find_switch_separators.
|
||||
|
||||
Rueckgabe: (Linie -> Block, Steuer-Separator -> Block). Ein Block ist
|
||||
(Kreisel-Id, laufende Nummer ab 1). Linien ohne Ausschleuselement in
|
||||
einer Kreisel-Kette fehlen in der ersten Zuordnung.
|
||||
"""
|
||||
lane_set = set(lanes)
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
@@ -427,7 +504,17 @@ def _speicher_bloecke(
|
||||
|
||||
|
||||
def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
|
||||
"""Abgehende Flusskanten eines TRO-Hosts (ohne Kreisel-Umlauf)."""
|
||||
"""Abgehende Flusskanten eines TRO-Hosts (ohne Kreisel-Umlauf).
|
||||
|
||||
Parameter:
|
||||
hosts: Knoten-Ids, die zusammen den TRO tragen (DerivedTro.hosts).
|
||||
graph: Materialflussgraph; gezaehlt werden nur Kanten der Art
|
||||
EDGE_FLOW, die aus hosts hinaus fuehren.
|
||||
|
||||
Rueckgabe:
|
||||
Sortierte, eindeutige Ziel-Knoten-Ids. 2 bzw. 3 Ziele werten einen
|
||||
1Sep in derive_tros Schritt 4 zu 1Sep1Swi bzw. 1Sep2Swi auf.
|
||||
"""
|
||||
return sorted(
|
||||
{
|
||||
edge.dst
|
||||
@@ -438,7 +525,20 @@ def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
|
||||
|
||||
|
||||
def _default_resolve_host(sep: Element, findings: list[str], graph: Graph) -> str | None:
|
||||
"""Host eines Separators aus der CSV-Spalte 'Zuordnung' (Default-Resolver)."""
|
||||
"""Host eines Separators aus der CSV-Spalte 'Zuordnung' (Default-Resolver).
|
||||
|
||||
Parameter:
|
||||
sep: der Separator; gelesen wird sep.assignment (Merkmal
|
||||
"Zuordnung", z. B. "0002-L" oder die Id einer Strecke).
|
||||
findings: Fundliste; ist die Zuordnung leer oder kein Graphknoten,
|
||||
wird ein Eintrag angehaengt.
|
||||
graph: Materialflussgraph, gegen dessen Knoten geprueft wird.
|
||||
|
||||
Rueckgabe:
|
||||
Knoten-Id des Hosts oder None (Separator bekommt dann keinen TRO).
|
||||
Die Art dieses Knotens (Kreisel/Strecke/Gefaellestrecke) entscheidet
|
||||
in derive_tros, in welchem Schritt der Separator landet.
|
||||
"""
|
||||
host = sep.assignment
|
||||
if host and host != UNASSIGNED and host in graph.nodes:
|
||||
return host
|
||||
@@ -503,6 +603,18 @@ def _find_switch_separators(
|
||||
|
||||
Die Distanz im Rueckgabewert ist nur noch Beiwerk fuer die Doku ("XXXmm
|
||||
von Ausschleuselement YYYY entfernt"), kein Kriterium mehr.
|
||||
|
||||
Parameter:
|
||||
separators: zu pruefende Separatoren (in derive_tros ohne die
|
||||
eigenen VF-Kettenseparatoren).
|
||||
elements: alle Elemente des CSV-Exports (Ausschleuselemente und
|
||||
Kreisel mit track_ids).
|
||||
|
||||
Rueckgabe:
|
||||
Separator-Id -> (Ausschleuselement, Abstand mm); je Separator das
|
||||
erste passende Element aus seinen Nachbarn. Wird in derive_tros
|
||||
Schritt 3b zu 1Sep1Swi (oder zum Steuer-Separator eines
|
||||
PinStore_Auto, siehe _speicher_bloecke).
|
||||
"""
|
||||
transfers = {e.teile_id: e for e in elements if e.kind == "Ausschleus"}
|
||||
kreisel_by_id = {e.teile_id: e for e in elements if e.kind == "Kreisel"}
|
||||
@@ -555,6 +667,22 @@ def _find_weiche_separators(
|
||||
bestaetigt (Review 2026-08-12 und 2026-10-01). Ist nur ein Separator
|
||||
benachbart, gilt dieser. Bleibt es mehrdeutig, wird nichts geraten,
|
||||
sondern ein Finding geschrieben.
|
||||
|
||||
Grenze: einer Weiche wird hoechstens EIN Separator zugeordnet. Weichen
|
||||
mit mehreren Separatoren (2Sep1Swi, 3Sep2Swi, ...) ermittelt vorher
|
||||
_kreisel_knoten aus Geometrie und Laufrichtung; hier kommen nur die
|
||||
ILS Weichen an, die dort keinen Knoten bilden.
|
||||
|
||||
Parameter:
|
||||
separators: zu pruefende Separatoren (in derive_tros ohne die
|
||||
Ausschleus-Weichen-Separatoren aus switch_hits).
|
||||
elements: alle Elemente des CSV-Exports (ILS Weichen und Scanner).
|
||||
findings: Fundliste; je mehrdeutiger Weiche wird ein Eintrag
|
||||
angehaengt.
|
||||
|
||||
Rueckgabe:
|
||||
Separator-Id -> ILS Weiche (Element.kind "Weiche"). Wird in
|
||||
derive_tros Schritt 3c' zu 1Sep1Swi.
|
||||
"""
|
||||
by_id = {s.teile_id: s for s in separators}
|
||||
mit_scanner = {
|
||||
@@ -578,6 +706,48 @@ def _find_weiche_separators(
|
||||
return result
|
||||
|
||||
|
||||
def _kreisel_knoten(
|
||||
elements: list[Element], findings: list[str], frei: set[str]
|
||||
) -> list[Junction]:
|
||||
"""Weichenknoten zwischen Kreiseln, die einen eigenen TRO bilden.
|
||||
|
||||
Geometrie und Steuerregel siehe lib/kreisel_junctions.py: ein Separator
|
||||
steuert einen Knoten, wenn er auf einem beteiligten Kreisel in
|
||||
Laufrichtung unmittelbar davor liegt (keine andere Weiche dazwischen).
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports.
|
||||
findings: Fundliste; je verworfenem Knoten ein Eintrag.
|
||||
frei: Separator-Ids, die einen Knoten steuern duerfen. Steuert ein
|
||||
anderer Separator (Ausschleus-Weiche aus 3b, eigener VF-
|
||||
Kettenseparator) mit, bleibt der Knoten den bisherigen Regeln
|
||||
(3b, 3c', 3d) ueberlassen.
|
||||
|
||||
Rueckgabe:
|
||||
Die Knoten, deren Typ (j.type_name) im Katalog steht und die nur von
|
||||
freien Separatoren gesteuert werden.
|
||||
"""
|
||||
result: list[Junction] = []
|
||||
for j in find_junctions(elements, findings):
|
||||
weichen = ", ".join(c.weiche_id for c in j.contacts)
|
||||
if not j.separators:
|
||||
if any(not c.exported for c in j.contacts):
|
||||
findings.append(
|
||||
f"Weichenknoten Kreisel {'/'.join(j.kreisel)} ({weichen}): kein "
|
||||
f"Separator in Laufrichtung davor - kein TRO")
|
||||
continue
|
||||
if not set(j.separators) <= frei:
|
||||
continue
|
||||
if get_tro(j.type_name) is None:
|
||||
findings.append(
|
||||
f"Weichenknoten Kreisel {'/'.join(j.kreisel)} ({weichen}): "
|
||||
f"{len(j.separators)} Separatoren und {len(j.contacts)} Weichen - "
|
||||
f"Typ {j.type_name} fehlt im Katalog, kein TRO")
|
||||
continue
|
||||
result.append(j)
|
||||
return result
|
||||
|
||||
|
||||
def _incoming_drop(
|
||||
host: Element, sep_id: str, elements_by_id: dict[str, Element]
|
||||
) -> tuple[str | None, float | None]:
|
||||
@@ -591,6 +761,18 @@ def _incoming_drop(
|
||||
oder fehlt einem der beiden Enden das Merkmal "Hoehe" (z. B. Aus-/
|
||||
Einschleuselemente tragen keins), liefert die Funktion (None, None) - der
|
||||
Aufrufer behandelt das als "kein Gefaelle-Nachweis", bleibt also bei Vario.
|
||||
|
||||
Parameter:
|
||||
host: die Streckengruppe (Element.kind "Strecke") mit track_ids und
|
||||
drive_dir.
|
||||
sep_id: TeileId des Separators in host.track_ids.
|
||||
elements_by_id: TeileId -> Element, fuer die Hoehen der beiden
|
||||
Segmentenden.
|
||||
|
||||
Rueckgabe:
|
||||
(Antriebfahrtrichtung des Segments, Hoehendifferenz in mm; negativ =
|
||||
Gefaelle). "horizontal" mit <= -GEFAELLE_DROP_MM stuft in
|
||||
derive_tros Schritt 2 einen Vario auf 1Sep herab.
|
||||
"""
|
||||
chain = host.track_ids
|
||||
if sep_id not in chain:
|
||||
@@ -677,23 +859,67 @@ def derive_tros(
|
||||
) -> list[DerivedTro]:
|
||||
"""TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden.
|
||||
|
||||
Host eines Separators aus der CSV-Spalte "Zuordnung" (_default_resolve_host),
|
||||
abgehende Wege eines TRO-Hosts aus graph.edges (_switch_exits).
|
||||
**Hier - und nur hier - wird der TRO-Typ bestimmt** (DerivedTro.type_name,
|
||||
Uebersicht im Modul-Docstring "Wo im Code der Typ entsteht").
|
||||
|
||||
sepliste_counts (optional, siehe load_sepliste_counts): Container-Id ->
|
||||
Anzahl 'SEP'-Eintraege aus dem Roh-JSON. Reine Gegenprobe - stimmt die aus
|
||||
der CSV abgeleitete Separatorenzahl eines Strecke/Gefaellestrecke-Hosts
|
||||
nicht mit dieser Zahl ueberein, wird ein Finding angehaengt; die TRO-Daten
|
||||
selbst bleiben unangetastet (CSV bleibt die massgebliche Quelle).
|
||||
Vorbereitung (vor den Schritten):
|
||||
a) Weichen-Separatoren: _find_switch_separators ->
|
||||
_weichen_nach_umlauf -> _weichen_aus_umlauf (Ausschleuselemente),
|
||||
_kreisel_knoten (Weichenknoten zwischen Kreiseln) und
|
||||
_find_weiche_separators (uebrige ILS Weichen). Diese Separatoren
|
||||
bekommen keinen Host aus der Zuordnung.
|
||||
b) Uebrige Separatoren -> Host-Knoten (_default_resolve_host, bei
|
||||
Gefaellestrecken ggf. korrigiert durch _kreisel_bahn).
|
||||
c) Scanner -> Separator ueber das Merkmal "Naechster Separator".
|
||||
|
||||
tro_ids (optional): ID-Datei der Anlage. Ohne sie werden die TROs entlang
|
||||
des Materialflusses neu durchnummeriert; mit ihr behalten wiedergefundene
|
||||
TROs ihre ID (Ergebnis in tro_ids.result).
|
||||
Schritte (setzen type_name):
|
||||
1 Gefaellestrecken: PinStore_Auto (>= MIN_STORE_LANES Linien) bzw.
|
||||
1Sep (einzelne Linie).
|
||||
2 Strecken: je Separator Vario oder 1Sep (alternierendes Muster,
|
||||
Gefaelle-Pruefung ueber _incoming_drop).
|
||||
3 Kreisel-Bahnen: je Separator 1Sep.
|
||||
3b Separator vor Ausschleuselement: 1Sep1Swi.
|
||||
3b' zweites Ausschleuselement desselben Separators: 1Sep2Swi.
|
||||
3c Weichenknoten zwischen Kreiseln (_kreisel_knoten): Typ aus der
|
||||
Zahl der Separatoren unmittelbar davor und der Weichen, z. B.
|
||||
2Sep1Swi, 3Sep2Swi.
|
||||
3c' Separator an einer uebrigen ILS Weiche: 1Sep1Swi.
|
||||
3d 3b- und 3c'-TRO direkt hintereinander am Kreisel: 2Sep2Swi.
|
||||
4 1Sep mit 2/3 abgehenden Flusskanten: 1Sep1Swi/1Sep2Swi.
|
||||
5 Sortieren entlang des Materialflusses, IDs vergeben.
|
||||
6 Hinweis bei 1Sep mit Scanner (evtl. 1Sep_SSCC, Typ bleibt).
|
||||
|
||||
Parameter:
|
||||
graph: Materialflussgraph aus material_flow.build_graph (Knoten =
|
||||
Kreisel-Bahnen "<Id>-L/-R", Strecken, Gefaellestrecken,
|
||||
Stationen; Kanten = Materialfluss).
|
||||
elements: alle Elemente des CSV-Exports (material_flow.read_elements);
|
||||
Separatoren, Scanner, Aus-/Einschleuselemente, ILS Weichen und
|
||||
Kreisel werden daraus gelesen.
|
||||
findings: Fundliste, an die jeder Schritt Hinweise anhaengt (wird
|
||||
veraendert, nicht ersetzt).
|
||||
sepliste_counts: optional, siehe load_sepliste_counts: Container-Id
|
||||
-> Anzahl 'SEP'-Eintraege aus dem Roh-JSON. Reine Gegenprobe -
|
||||
stimmt die aus der CSV abgeleitete Separatorenzahl eines
|
||||
Strecke/Gefaellestrecke-Hosts nicht mit dieser Zahl ueberein,
|
||||
wird ein Finding angehaengt; die TRO-Daten selbst bleiben
|
||||
unangetastet (CSV bleibt die massgebliche Quelle).
|
||||
tro_ids: optional, ID-Datei der Anlage. Ohne sie werden die TROs
|
||||
entlang des Materialflusses neu durchnummeriert; mit ihr behalten
|
||||
wiedergefundene TROs ihre ID (Ergebnis in tro_ids.result).
|
||||
|
||||
Rueckgabe:
|
||||
Die TROs in Materialfluss-Reihenfolge, mit tro_id, type_name, label,
|
||||
hosts, separators, scanners, reason und confidence. Position,
|
||||
Vorgaenger/Nachfolger fuellt erst analyse_tros (locate_tros,
|
||||
link_tros).
|
||||
"""
|
||||
def resolve_host(sep: Element, findings: list[str]) -> str | None:
|
||||
"""Host-Knoten eines Separators (siehe _default_resolve_host)."""
|
||||
return _default_resolve_host(sep, findings, graph)
|
||||
|
||||
def count_exits(hosts: list[str]) -> list[str]:
|
||||
"""Abgehende Flussziele der Hosts (siehe _switch_exits)."""
|
||||
return _switch_exits(hosts, graph)
|
||||
|
||||
separators = sorted(
|
||||
@@ -732,17 +958,26 @@ def derive_tros(
|
||||
_weichen_nach_umlauf(
|
||||
_find_switch_separators(switch_candidates, elements), elements, findings),
|
||||
switch_candidates, elements, findings)
|
||||
# Weichenknoten zwischen Kreiseln aus Geometrie und Laufrichtung (siehe
|
||||
# _kreisel_knoten, Schritt 3c) - ihre Separatoren und Weichen sind damit
|
||||
# vergeben und laufen nicht mehr durch _find_weiche_separators.
|
||||
knoten = _kreisel_knoten(
|
||||
elements, findings, frei={s.teile_id for s in switch_candidates} - set(switch_hits))
|
||||
knoten_seps = {s for j in knoten for s in j.separators}
|
||||
knoten_weichen = {c.weiche_id for j in knoten for c in j.contacts}
|
||||
# Separatoren an einer echten ILS Weiche (siehe _find_weiche_separators) -
|
||||
# wie switch_hits vor der Host-Aufloesung herausgenommen, Schritt 3c.
|
||||
# wie switch_hits vor der Host-Aufloesung herausgenommen, Schritt 3c'.
|
||||
weiche_hits = _find_weiche_separators(
|
||||
[s for s in switch_candidates if s.teile_id not in switch_hits],
|
||||
elements, findings,
|
||||
[s for s in switch_candidates
|
||||
if s.teile_id not in switch_hits and s.teile_id not in knoten_seps],
|
||||
[e for e in elements if e.teile_id not in knoten_weichen], findings,
|
||||
)
|
||||
|
||||
# Separator -> Host-Knoten
|
||||
sep_host: dict[str, str] = {}
|
||||
for sep in separators:
|
||||
if sep.teile_id in switch_hits or sep.teile_id in weiche_hits:
|
||||
if (sep.teile_id in switch_hits or sep.teile_id in weiche_hits
|
||||
or sep.teile_id in knoten_seps):
|
||||
continue
|
||||
host = resolve_host(sep, findings)
|
||||
# Steht er in der Umlaufkette eines Kreisels, zeigt die Zuordnung aber
|
||||
@@ -764,7 +999,8 @@ def derive_tros(
|
||||
sep_scanners: dict[str, list[str]] = {}
|
||||
for scanner in scanners:
|
||||
target = str(_merkmal(scanner.merkmale, "Naechster Separator") or "").strip()
|
||||
if target in sep_host or target in switch_hits or target in weiche_hits:
|
||||
if (target in sep_host or target in switch_hits or target in weiche_hits
|
||||
or target in knoten_seps):
|
||||
sep_scanners.setdefault(target, []).append(scanner.teile_id)
|
||||
mounted = scanner.assignment
|
||||
target_host = sep_host.get(target)
|
||||
@@ -784,9 +1020,11 @@ def derive_tros(
|
||||
)
|
||||
|
||||
def seps_on(hosts: list[str]) -> list[str]:
|
||||
"""Sortierte Separator-Ids, deren Host in hosts liegt."""
|
||||
return sorted(s for s, h in sep_host.items() if h in hosts)
|
||||
|
||||
def scanners_for(seps: list[str]) -> list[str]:
|
||||
"""Sortierte Scanner-Ids, die an einem der Separatoren lesen."""
|
||||
found: list[str] = []
|
||||
for sep in seps:
|
||||
found.extend(sep_scanners.get(sep, []))
|
||||
@@ -1106,7 +1344,35 @@ def derive_tros(
|
||||
f"Barcode-Scanner am Eingang, im Layout fehlt einer vor Separator "
|
||||
f"{tro.separators[0]}")
|
||||
|
||||
# 3c) Separatoren an einer echten ILS Weiche -> 1Sep1Swi (siehe
|
||||
# 3c) Weichenknoten zwischen Kreiseln -> nSepmSwi (siehe _kreisel_knoten):
|
||||
# n = Separatoren, die in Laufrichtung unmittelbar vor dem Knoten liegen,
|
||||
# m = Weichen (Beruehrpunkte) des Knotens. Host je Separator ist die Bahn,
|
||||
# auf der er physisch liegt - nicht die Zuordnung (export.csv 0015:
|
||||
# Zuordnung 0003-L, liegt aber auf Kreisel 0002 Bahn R).
|
||||
for j in knoten:
|
||||
weichen = [c.weiche_id for c in j.contacts]
|
||||
vor = ", ".join(f"{s} ({j.sep_lane[s]})" for s in j.separators)
|
||||
hosts = list(dict.fromkeys(j.sep_lane[s] for s in j.separators
|
||||
if j.sep_lane[s] in graph.nodes))
|
||||
tros.append(DerivedTro(
|
||||
tro_id="",
|
||||
type_name=j.type_name,
|
||||
label=(f"Weichenknoten Kreisel {'/'.join(j.kreisel)}: "
|
||||
f"Weiche{'n' if len(weichen) > 1 else ''} {', '.join(weichen)}"),
|
||||
hosts=hosts or [j.sep_kreisel[s] for s in j.separators],
|
||||
separators=list(j.separators),
|
||||
scanners=scanners_for(j.separators),
|
||||
reason=(f"Separator{'en' if len(j.separators) > 1 else ''} {vor} "
|
||||
f"in Laufrichtung unmittelbar vor "
|
||||
f"{'den Weichen' if len(weichen) > 1 else 'der Weiche'} "
|
||||
f"{', '.join(weichen)} (Beruehrpunkt"
|
||||
f"{'e' if len(weichen) > 1 else ''} der Kreisel "
|
||||
f"{'/'.join(j.kreisel)})"),
|
||||
confidence="hoch" if all(c.exported for c in j.contacts) else "mittel",
|
||||
))
|
||||
|
||||
# 3c') Separatoren an einer ILS Weiche, die kein Weichenknoten aus 3c ist
|
||||
# (z. B. gegenlaeufige Drehrichtung im Export) -> 1Sep1Swi (siehe
|
||||
# _find_weiche_separators). Host wie bei 3b: der aufgeloeste Zuordnung-Host
|
||||
# des Separators, sonst die Weiche selbst.
|
||||
weiche_tro: dict[str, DerivedTro] = {} # Weichen-Id -> 1Sep1Swi
|
||||
@@ -1232,15 +1498,26 @@ def _flow_rank(graph: Graph) -> dict[str, int]:
|
||||
return rank
|
||||
|
||||
|
||||
def _flow_key(graph: Graph, elements_by_id: dict[str, Element]):
|
||||
def _flow_key(
|
||||
graph: Graph, elements_by_id: dict[str, Element]
|
||||
) -> Callable[[DerivedTro], tuple[int, int, str]]:
|
||||
"""Sortierschluessel fuer TROs entlang des Materialflusses.
|
||||
|
||||
Erst der Rang des Host-Knotens, dann die Lage des Separators in der
|
||||
TrackIds-Kette des Hosts (Kreisel: Umlaufrichtung, Strecke: Baureihenfolge).
|
||||
|
||||
Parameter:
|
||||
graph: Materialflussgraph (fuer _flow_rank).
|
||||
elements_by_id: TeileId -> Element, fuer die track_ids des Hosts.
|
||||
|
||||
Rueckgabe:
|
||||
Funktion fuer sorted()/list.sort(key=...), die je TRO
|
||||
(Host-Rang, Index in der TrackIds-Kette, erster Separator) liefert.
|
||||
"""
|
||||
rank = _flow_rank(graph)
|
||||
|
||||
def key(tro: DerivedTro) -> tuple:
|
||||
def key(tro: DerivedTro) -> tuple[int, int, str]:
|
||||
"""(Host-Rang, Index in der TrackIds-Kette, erster Separator)."""
|
||||
host_rank = min((rank.get(h, len(rank)) for h in tro.hosts),
|
||||
default=len(rank))
|
||||
sep = tro.separators[0] if tro.separators else ""
|
||||
@@ -1328,7 +1605,7 @@ def _tro_edge_kind(edge: Edge) -> str:
|
||||
return "normal"
|
||||
|
||||
|
||||
def _bahn_richtung(node) -> tuple[float, float] | None:
|
||||
def _bahn_richtung(node: Node) -> tuple[float, float] | None:
|
||||
"""Foerderrichtung einer Kreisel-Bahn als Einheitsvektor.
|
||||
|
||||
Der Kreisel ist eine geschlossene Schleife: auf der einen Bahn laeuft das
|
||||
@@ -1414,7 +1691,8 @@ def _chain_mit_ungelisteten_separatoren(
|
||||
if dx == 0 and dy == 0:
|
||||
return chain
|
||||
|
||||
def _t(pos) -> float:
|
||||
def _t(pos: tuple[float, float, float]) -> float:
|
||||
"""Projektion von pos auf die Richtung anfang -> ende (unskaliert)."""
|
||||
return (pos[0] - anfang.position[0]) * dx + (pos[1] - anfang.position[1]) * dy
|
||||
|
||||
# Kettenelemente ohne Position behalten ihren Platz: sie erben den Wert des
|
||||
@@ -1507,6 +1785,7 @@ def link_tros(
|
||||
elem_owner.setdefault(sep_id, tro.tro_id)
|
||||
|
||||
def _add_chain_pair(a: str, b: str) -> None:
|
||||
"""Gerichtete Kante TRO a -> TRO b aus der TrackIds-Kette eintragen."""
|
||||
if a == b:
|
||||
return
|
||||
# Keine Sperre fuer Systemgrenzen noetig: sie sind kein Kettenglied
|
||||
@@ -1863,7 +2142,23 @@ def analyse_tros(
|
||||
) -> Analysis:
|
||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
||||
|
||||
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu nummerieren.
|
||||
Gemeinsamer Einstieg fuer tro_flow.py und tro_annotate.py. Der TRO-Typ
|
||||
wird NICHT hier bestimmt, sondern in derive_tros (erster Aufruf unten);
|
||||
danach kommen nur noch Systemgrenzen (derive_boundaries), Koordinaten
|
||||
(locate_tros) und Verbindungen (link_tros) dazu.
|
||||
|
||||
Parameter:
|
||||
graph: Materialflussgraph aus material_flow.build_graph.
|
||||
elements: alle Elemente des CSV-Exports (material_flow.read_elements).
|
||||
sepliste_counts: optional, Separatorenzahl je Container aus dem
|
||||
Roh-JSON (load_sepliste_counts) - nur Gegenprobe.
|
||||
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu
|
||||
nummerieren.
|
||||
|
||||
Rueckgabe:
|
||||
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
|
||||
(Typnamen ohne Katalogeintrag), isolated (TROs ohne Verbindung) und
|
||||
den sortierten findings.
|
||||
"""
|
||||
analysis = Analysis()
|
||||
analysis.tros = derive_tros(graph, elements, analysis.findings,
|
||||
@@ -1892,6 +2187,12 @@ def analyse_tros(
|
||||
|
||||
|
||||
def _tro_label(tro: DerivedTro) -> str:
|
||||
"""Mehrzeiliges DOT-Label eines TRO-Knotens.
|
||||
|
||||
Zeilen: ID und Typ, Bezeichnung, FB-Baustein, Bauteile laut Katalog,
|
||||
Separatoren (ab 7 gekuerzt), Scanner, Koordinate - jeweils nur, wenn
|
||||
vorhanden.
|
||||
"""
|
||||
lines = [f"{tro.tro_id} ({tro.type_name})", tro.label]
|
||||
if tro.fb_block:
|
||||
lines.append(tro.fb_block)
|
||||
@@ -1981,6 +2282,7 @@ def _station_positionen(
|
||||
je_tro.setdefault(anker, []).append(station)
|
||||
|
||||
def _abstand(a: float, b: float) -> float:
|
||||
"""Kleinster Winkelabstand (rad) zweier Richtungen."""
|
||||
d = abs(a - b) % (2 * math.pi)
|
||||
return min(d, 2 * math.pi - d)
|
||||
|
||||
@@ -2060,7 +2362,7 @@ def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float,
|
||||
return (0.0, 0.0)
|
||||
xs = [p[0] for p in zeichen.values()]
|
||||
ys = [p[1] for p in zeichen.values()]
|
||||
return (max(xs) + LEGEND_MARGIN, max(ys) - 1000)
|
||||
return (max(xs) + LEGEND_MARGIN, max(ys) - LEGEND_DROP)
|
||||
|
||||
|
||||
def _zeichen_positionen(
|
||||
@@ -2099,6 +2401,7 @@ STATION_FARBE = {
|
||||
|
||||
|
||||
def _tro_edge_attrs(kind: str | None) -> str:
|
||||
"""DOT-Kantenattribute zur Kantenart (TRO_EDGE_STYLE); None = "normal"."""
|
||||
return TRO_EDGE_STYLE.get(kind or "normal", "")
|
||||
|
||||
|
||||
@@ -2118,6 +2421,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
|
||||
zeichen = _zeichen_positionen(analysis, compact) if use_coords else {}
|
||||
|
||||
def _pos(node: DerivedTro) -> str:
|
||||
"""DOT-Attribut ', pos="x,y"' des Knotens oder ''."""
|
||||
if use_coords:
|
||||
p = zeichen.get(node.tro_id)
|
||||
return (f', pos="{p[0] * COORD_SCALE:.2f},{p[1] * COORD_SCALE:.2f}"'
|
||||
@@ -2336,6 +2640,16 @@ def render_connect_ini(analysis: Analysis, section: str, source: str) -> str:
|
||||
|
||||
|
||||
def render_errors(analysis: Analysis, csv_file: Path) -> str:
|
||||
"""Fehlerbericht <name>_tro_errors.md als Markdown-Text.
|
||||
|
||||
Parameter:
|
||||
analysis: Ergebnis von analyse_tros (isolated, unknown_types).
|
||||
csv_file: Eingabe-CSV, nur fuer Titel und Quellenangabe.
|
||||
|
||||
Rueckgabe:
|
||||
Der Markdown-Text. Ob er geschrieben wird (nur bei isolierten TROs
|
||||
oder unbekannten Typen), entscheidet write_error_file.
|
||||
"""
|
||||
out = [GENERATED_MARKER, f"# TRO-Fehler - {csv_file.name}", ""]
|
||||
out.append(f"**Erzeugt:** {datetime.now().strftime('%Y-%m-%d %H:%M')} ")
|
||||
out.append(f"**Quelle:** `{csv_file}` ")
|
||||
@@ -2406,10 +2720,16 @@ def write_error_file(analysis: Analysis, csv_file: Path, path: Path) -> bool:
|
||||
|
||||
|
||||
def _mermaid_text(text: str) -> str:
|
||||
"""Text fuer ein Mermaid-Knotenlabel entschaerfen (\" und [] ersetzen)."""
|
||||
return text.replace('"', "'").replace("[", "(").replace("]", ")")
|
||||
|
||||
|
||||
def _tro_mermaid(analysis: Analysis) -> list[str]:
|
||||
"""TRO-Fluss als Mermaid-Flowchart (Zeilen inklusive ```-Zaun).
|
||||
|
||||
Knoten je TRO (Farbe nach Katalogtyp) und Systemgrenze, Kanten aus
|
||||
analysis.edges, gestrichelt TROs auf derselben Bahn (colocated).
|
||||
"""
|
||||
out = ["```mermaid", "flowchart LR"]
|
||||
out.extend(f" {line}" for line in mermaid_classdefs())
|
||||
|
||||
@@ -2447,6 +2767,20 @@ def render_doc(
|
||||
svg_file: Path | None,
|
||||
error_file: Path | None,
|
||||
) -> str:
|
||||
"""TRO-Dokumentation <name>_tro_doc.md (--doc) als Markdown-Text.
|
||||
|
||||
Parameter:
|
||||
analysis: Ergebnis von analyse_tros.
|
||||
graph: Materialflussgraph (Host-Namen und -Arten fuer die Tabellen).
|
||||
csv_file: Eingabe-CSV (Titel, Quellenangabe).
|
||||
dot_file: erzeugte DOT-Datei, wird verlinkt.
|
||||
svg_file: erzeugtes SVG oder None (ohne --tosvg).
|
||||
error_file: Fehlerdatei oder None (fehlerfreier Lauf).
|
||||
|
||||
Rueckgabe:
|
||||
Der Markdown-Text: TRO-Liste mit Typ und Begruendung, Mermaid-Fluss,
|
||||
Verbindungen, Typdefinition aus dem Katalog, Findings.
|
||||
"""
|
||||
out: list[str] = [GENERATED_MARKER]
|
||||
add = out.append
|
||||
|
||||
@@ -2650,6 +2984,17 @@ def report(
|
||||
warnings: list[str],
|
||||
engine: str = "dot",
|
||||
) -> None:
|
||||
"""Konsolenbericht eines Laufs: TRO-Zahl je Typ, Findings, Warnungen,
|
||||
erzeugte Dateien.
|
||||
|
||||
Parameter:
|
||||
analysis: Ergebnis von analyse_tros.
|
||||
csv_file: Eingabe-CSV.
|
||||
dot_file: erzeugte DOT-Datei.
|
||||
svg_file / error_file / doc_file: erzeugte Dateien oder None.
|
||||
warnings: Warnungen aus material_flow (Einlesen, Graphaufbau).
|
||||
engine: Graphviz-Programm des SVG ("dot" oder "neato", mit -n).
|
||||
"""
|
||||
types_used: dict[str, int] = {}
|
||||
for tro in analysis.tros:
|
||||
types_used[tro.type_name] = types_used.get(tro.type_name, 0) + 1
|
||||
@@ -2709,6 +3054,7 @@ def report(
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"""Kommandozeile auswerten; argv=None liest sys.argv[1:]."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="tro_flow",
|
||||
description="Ermittelt aus einem CSV-Export der Anlagenobjekte die "
|
||||
@@ -2866,6 +3212,16 @@ def report_ids(tro_ids: TroIds, status: str) -> None:
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI-Einstieg: CSV lesen, Graph bauen, analyse_tros, Ausgaben schreiben.
|
||||
|
||||
Parameter:
|
||||
argv: Argumente ohne Programmnamen; None = sys.argv[1:].
|
||||
|
||||
Rueckgabe:
|
||||
Exitcode: 0 = Erfolg, 1 = Eingabe- oder Ausgabefehler, 2 = SVG nicht
|
||||
erzeugt, 3 = TRO-Pruefung fehlgeschlagen (isolierte TROs oder
|
||||
unbekannte Typen, siehe Fehlerdatei).
|
||||
"""
|
||||
args = parse_args(argv)
|
||||
|
||||
csv_file = resolve_input(args.file)
|
||||
|
||||
+4
-3
@@ -37,6 +37,8 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from parameter import get_float
|
||||
|
||||
__all__ = [
|
||||
"SECTION",
|
||||
"DEFAULT_TOLERANCE",
|
||||
@@ -57,9 +59,8 @@ ID_FILE_SUFFIX = "_tro_ids.ini"
|
||||
PREFIX = "TRO"
|
||||
|
||||
# Abstand (mm) zwischen Eintrag und Separator, bis zu dem ein TRO als
|
||||
# wiedergefunden gilt. Separatoren derselben Bahn liegen deutlich weiter
|
||||
# auseinander; ein verschobenes Bauteil faellt so als "neu" auf.
|
||||
DEFAULT_TOLERANCE = 100.0
|
||||
# wiedergefunden gilt - aus cfg/parameter.ini [tro_ids] (lib/parameter.py).
|
||||
DEFAULT_TOLERANCE = get_float("tro_ids", "default_tolerance")
|
||||
|
||||
# Eine ID wird zum SCL-Bezeichner (Instanz #<ID>, "DB_TRO_TEST".<ID>).
|
||||
_ID_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
|
||||
Reference in New Issue
Block a user