tro_flow: Weichen-Separator nach Laufrichtung, Weichen-Slots mit Bauteil
Steuernder Separator einer ILS Weiche (_find_weiche_separators) ist der letzte freie Separator in Laufrichtung VOR der Weiche, gemessen auf dem Kreisel-Umlauf aus Lage und Drehrichtung (kreisel_junctions.separators_before) - nicht mehr der CSV-Nachbar. Ein Nachbar hinter der Weiche steuert sie nie; Hoechstabstand weiche_vorlauf_max in cfg/parameter.ini [tro_flow]. Mubea (Export 2026-10-16): 2Sep2Swi = 0005 (Ausschleus 0061) + 0036 (Weiche 0130) statt 0005 + 0012; 0012 bleibt 1Sep, Kreisel1 wieder angebunden. Dematic, HundM05, showroom, test_system unveraendert. TRO-JSON: DerivedTro.switches (optional) nennt die Weichen-Bauteile in Slot-Reihenfolge (switch<i> gehoert bei 2Sep-Typen zu separator<i>); derive_slots traegt sie in die switch<n>-Slots, merge_hw_layout bindet das Ventil dort. e_planning.ini: Rollen ventil -> valve (Slot-Rolle der SCL), neue Regel Ausschleus_Weiche.valve fuer Ausschleusventile an Kreisel-Weichen. Mubea: MB0715, MB0724, MB0627, MB0671 statt TODO(E-Planung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+84
-15
@@ -324,6 +324,89 @@ def _group(contacts: list[Contact]) -> list[list[Contact]]:
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def separator_positions(
|
||||
loops: dict[str, Loop], separators: list[Element]
|
||||
) -> tuple[dict[str, tuple[str, float]], dict[str, str]]:
|
||||
"""Separatoren auf ihren Umlauf legen - nach Lage, nicht nach TrackIds.
|
||||
|
||||
Parameter:
|
||||
loops: Kreisel-Id -> Loop (siehe find_contacts).
|
||||
separators: die zu legenden Separatoren.
|
||||
|
||||
Rueckgabe:
|
||||
(Separator -> (Kreisel, s), Separator -> "<Kreisel>-L/R"). Ein
|
||||
Separator liegt auf dem naechsten Umlauf, wenn er hoechstens
|
||||
separator_tolerance davon entfernt ist; sonst fehlt er.
|
||||
"""
|
||||
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)}"
|
||||
return sep_pos, sep_lane
|
||||
|
||||
|
||||
@dataclass
|
||||
class Upstream:
|
||||
"""Letzter Separator vor einem Punkt auf einem Umlauf."""
|
||||
|
||||
kreisel: str
|
||||
separator: str
|
||||
distance: float # Laufweg Separator -> Punkt in Drehrichtung (mm)
|
||||
|
||||
|
||||
def separators_before(
|
||||
elements: list[Element], x: float, y: float
|
||||
) -> tuple[list[Upstream], list[str]]:
|
||||
"""Je Kreisel, auf dem der Punkt (x, y) liegt, der letzte Separator davor.
|
||||
|
||||
"Davor" heisst: in Drehrichtung des Kreisels kommt zuerst dieser
|
||||
Separator, dann der Punkt - gemessen auf dem Umlauf aus Geometrie und
|
||||
Drehrichtung (kreisel_loop), nicht ueber die TrackIds-Reihenfolge. So
|
||||
bleibt das Ergebnis richtig, auch wenn ein Export die Kette anders
|
||||
sortiert oder einen Separator gar nicht in TrackIds fuehrt.
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports (Kreisel, Separatoren).
|
||||
x, y: der Punkt, z. B. eine ILS Weiche (mm).
|
||||
|
||||
Rueckgabe:
|
||||
(je Kreisel ein Upstream, sortiert nach Laufweg; Separatoren auf
|
||||
diesen Kreiseln, die auf dem Umlauf naeher HINTER dem Punkt liegen
|
||||
als davor - nur fuer Hinweise).
|
||||
Der Punkt liegt auf einem Kreisel, wenn er hoechstens
|
||||
weiche_tolerance von dessen Umlauf entfernt ist.
|
||||
"""
|
||||
loops = {e.teile_id: lp for e in elements if e.kind == "Kreisel"
|
||||
and (lp := kreisel_loop(e)) is not None}
|
||||
sep_pos, _lane = separator_positions(
|
||||
loops, [e for e in elements if e.kind == "Separator"])
|
||||
before: list[Upstream] = []
|
||||
after: list[str] = []
|
||||
for k, lp in loops.items():
|
||||
s_point, dist, _sec = lp.project(x, y)
|
||||
if dist > WEICHE_TOLERANCE:
|
||||
continue
|
||||
auf_k = [(sid, s) for sid, (kk, s) in sep_pos.items() if kk == k]
|
||||
if not auf_k:
|
||||
continue
|
||||
vor = min(auf_k, key=lambda e: (s_point - e[1]) % lp.length)
|
||||
d_vor = (s_point - vor[1]) % lp.length
|
||||
before.append(Upstream(k, vor[0], d_vor))
|
||||
after += [sid for sid, s in auf_k
|
||||
if (s - s_point) % lp.length < (s_point - s) % lp.length]
|
||||
before.sort(key=lambda u: u.distance)
|
||||
return before, sorted(after)
|
||||
|
||||
|
||||
def find_junctions(
|
||||
elements: list[Element],
|
||||
findings: list[str],
|
||||
@@ -344,21 +427,7 @@ def find_junctions(
|
||||
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)}"
|
||||
sep_pos, sep_lane = separator_positions(loops, separators)
|
||||
|
||||
junctions: list[Junction] = []
|
||||
for group in _group(contacts):
|
||||
|
||||
+11
-3
@@ -91,6 +91,8 @@ __all__ = [
|
||||
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived). Seit 2026-10-15
|
||||
# traegt v4 optional `parts` (Lage der Layout-Bauteile fuer merge_hw_layout); eine
|
||||
# v4-Datei ohne `parts` liest sich weiter, nur merge_hw_layout braucht sie.
|
||||
# Seit 2026-10-16 traegt jeder TRO optional `switches` (Weichen-Bauteile, fuellt
|
||||
# die switch<n>-Slots); fehlt es, bleiben die Slots wie bisher ohne layoutId.
|
||||
SCHEMA = "sps_skel-tro-v4"
|
||||
|
||||
# Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout)
|
||||
@@ -315,6 +317,11 @@ class DerivedTro(TroBase):
|
||||
label: str
|
||||
hosts: list[str]
|
||||
scanners: list[str]
|
||||
# Weichen-Bauteile (Ausschleuselement oder ILS Weiche), Reihenfolge wie die
|
||||
# Katalog-Slots switch1, switch2 ...; switch<i> gehoert bei 2Sep-Typen zu
|
||||
# separator<i>. "" = Weiche ohne Layout-Bauteil (nicht exportierter
|
||||
# Beruehrpunkt, Aufwertung nach Abgaengen). Fehlt in Dateien vor 2026-10-16.
|
||||
switches: list[str] = Field(default_factory=list)
|
||||
reason: str
|
||||
colocated: list[str] = Field(default_factory=list)
|
||||
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde
|
||||
@@ -622,8 +629,8 @@ def storage_lines(tro: TroBase) -> list[str]:
|
||||
def derive_slots(tro: DerivedTro) -> list[Slot]:
|
||||
"""Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO.
|
||||
|
||||
Weichen tragen noch keine Layout-ID - tro_flow speichert das Weichen-
|
||||
bzw. Ausschleuselement heute nicht am TRO. Ein Speicher bekommt je
|
||||
Weichen-Slots tragen das Weichen- bzw. Ausschleuselement aus
|
||||
``tro.switches`` (leer, wenn tro_flow keins kennt). Ein Speicher bekommt je
|
||||
Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und
|
||||
``line1`` koennen dasselbe Bauteil sein (wie im PinStore-Template).
|
||||
"""
|
||||
@@ -637,7 +644,8 @@ def derive_slots(tro: DerivedTro) -> list[Slot]:
|
||||
slots.append(Slot(slot=f"line{n}", layout_id=sep,
|
||||
roles=list(_STORAGE_LINE_ROLES)))
|
||||
for i in range(items.get(TroItem.SWITCH, 0)):
|
||||
slots.append(Slot(slot=f"switch{i + 1}", roles=list(_SWITCH_ROLES)))
|
||||
slots.append(Slot(slot=f"switch{i + 1}", layout_id=_at(tro.switches, i),
|
||||
roles=list(_SWITCH_ROLES)))
|
||||
for i in range(items.get(TroItem.SCANNER, 0)):
|
||||
slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i),
|
||||
roles=list(_SCANNER_ROLES)))
|
||||
|
||||
+73
-15
@@ -104,8 +104,10 @@ ausschliesslich derive_tros(), Schritt fuer Schritt:
|
||||
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)
|
||||
Weichenknoten aus 3c ist: letzter freier
|
||||
Separator in Laufrichtung davor (Lage +
|
||||
Drehrichtung, nicht TrackIds/Nachbarn;
|
||||
_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
|
||||
@@ -178,7 +180,7 @@ from material_flow import (
|
||||
read_elements,
|
||||
resolve_input,
|
||||
)
|
||||
from kreisel_junctions import Junction, find_junctions
|
||||
from kreisel_junctions import Junction, find_junctions, separators_before
|
||||
from model import Analysis, DerivedTro, LayoutPart, TroDocument
|
||||
from parameter import get_float, get_int
|
||||
from tro_ids import (
|
||||
@@ -211,6 +213,7 @@ from tro_catalog import (
|
||||
_P = "tro_flow"
|
||||
MIN_STORE_LANES = get_int(_P, "min_store_lanes")
|
||||
GEFAELLE_DROP_MM = get_float(_P, "gefaelle_drop_mm")
|
||||
WEICHE_VORLAUF_MAX = get_float(_P, "weiche_vorlauf_max")
|
||||
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")
|
||||
@@ -661,12 +664,29 @@ def _find_weiche_separators(
|
||||
erzeugt sie z. B. am Beruehrpunkt zweier Kreisel (Merkmal "Erzeugt aus":
|
||||
"Kreisel-Beruehrpunkt"; Mubea 0125 zwischen Kreisel2 und Kreisel1).
|
||||
|
||||
Steuernder Separator ist der Separator unter den CSV-Nachbarn der Weiche,
|
||||
der einen Scanner traegt (Scanner-Merkmal "Naechster Separator"). Mubea
|
||||
0125: Nachbarn 0040 und 0036, nur 0036 hat einen Scanner (0041) - vor Ort
|
||||
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.
|
||||
Steuernder Separator ist der Separator, der in Laufrichtung unmittelbar
|
||||
VOR der Weiche liegt - der Traeger haelt dort, wird gelesen und die
|
||||
Weiche entscheidet. Liegt die Weiche auf Kreiseln mit Drehrichtung
|
||||
(kreisel_junctions.separators_before), gilt:
|
||||
|
||||
1. Je Kreisel, auf dem die Weiche liegt, zaehlt nur der letzte Separator
|
||||
vor ihr - bestimmt aus Lage und Drehrichtung, nicht aus TrackIds
|
||||
oder der CSV-Spalte "Nachbarn". Ein Separator NACH der Weiche steuert
|
||||
sie nie, auch wenn er ihr CSV-Nachbar ist (Mubea-Export 2026-10-16:
|
||||
Nachbarn der Weiche 0130 sind 0061/0012, 0012 liegt auf Kreisel2
|
||||
hinter ihr; gesteuert wird sie von 0036 auf Kreisel1 davor).
|
||||
2. Er muss frei sein (in separators, also nicht schon Steuer-Separator
|
||||
eines Ausschleuselements oder eigener VF-Kettenseparator) und
|
||||
hoechstens weiche_vorlauf_max (cfg/parameter.ini [tro_flow]) Laufweg
|
||||
vor der Weiche liegen.
|
||||
3. Bleiben mehrere (je Kreisel einer): zuerst ein CSV-Nachbar, dann einer
|
||||
mit Scanner (Scanner-Merkmal "Naechster Separator"), dann der mit dem
|
||||
kuerzesten Laufweg.
|
||||
|
||||
Ohne Kreisel-Geometrie (Weiche auf keinem Umlauf, Drehrichtung fehlt)
|
||||
gilt die alte Regel: der Separator unter den CSV-Nachbarn mit Scanner,
|
||||
sonst der einzige benachbarte. 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
|
||||
@@ -674,15 +694,16 @@ def _find_weiche_separators(
|
||||
ILS Weichen an, die dort keinen Knoten bilden.
|
||||
|
||||
Parameter:
|
||||
separators: zu pruefende Separatoren (in derive_tros ohne die
|
||||
separators: Separatoren, die steuern duerfen (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.
|
||||
elements: alle Elemente des CSV-Exports (ILS Weichen, Kreisel,
|
||||
Separatoren, Scanner).
|
||||
findings: Fundliste; je verworfenem Nachbarn, je Zuordnung ausserhalb
|
||||
der Nachbarn und je nicht zuordenbarer Weiche ein Eintrag.
|
||||
|
||||
Rueckgabe:
|
||||
Separator-Id -> ILS Weiche (Element.kind "Weiche"). Wird in
|
||||
derive_tros Schritt 3c' zu 1Sep1Swi.
|
||||
derive_tros Schritt 3c' zu 1Sep1Swi (oder in 3d Teil eines 2Sep2Swi).
|
||||
"""
|
||||
by_id = {s.teile_id: s for s in separators}
|
||||
mit_scanner = {
|
||||
@@ -691,7 +712,37 @@ def _find_weiche_separators(
|
||||
}
|
||||
result: dict[str, Element] = {}
|
||||
for weiche in (e for e in elements if e.kind == "Weiche"):
|
||||
kandidaten = sorted({base for base, _lane in weiche.neighbours if base in by_id})
|
||||
nachbarn = sorted({base for base, _lane in weiche.neighbours if base in by_id})
|
||||
vorher, nachher = (separators_before(elements, weiche.x, weiche.y)
|
||||
if None not in (weiche.x, weiche.y) else ([], []))
|
||||
if vorher:
|
||||
for sid in (n for n in nachbarn if n in nachher):
|
||||
findings.append(
|
||||
f"ILS Weiche {weiche.teile_id}: CSV-Nachbar {sid} liegt in "
|
||||
f"Laufrichtung hinter der Weiche - steuert sie nicht")
|
||||
frei = [u for u in vorher
|
||||
if u.separator in by_id and u.distance <= WEICHE_VORLAUF_MAX]
|
||||
if not frei:
|
||||
davor = ", ".join(f"{u.separator} auf {u.kreisel} ({u.distance:.0f}mm)"
|
||||
for u in vorher)
|
||||
findings.append(
|
||||
f"ILS Weiche {weiche.teile_id}: kein freier Separator in "
|
||||
f"Laufrichtung davor (letzte davor: {davor}; hoechstens "
|
||||
f"{WEICHE_VORLAUF_MAX:.0f}mm) - keinem TRO zugeordnet")
|
||||
continue
|
||||
frei.sort(key=lambda u: (u.separator not in nachbarn,
|
||||
u.separator not in mit_scanner, u.distance))
|
||||
u = frei[0]
|
||||
if u.separator not in nachbarn:
|
||||
findings.append(
|
||||
f"ILS Weiche {weiche.teile_id}: gesteuert von Separator "
|
||||
f"{u.separator} ({u.distance:.0f}mm in Laufrichtung davor auf "
|
||||
f"Kreisel {u.kreisel}), obwohl kein CSV-Nachbar (Nachbarn: "
|
||||
f"{', '.join(b for b, _l in weiche.neighbours) or '-'})")
|
||||
result[u.separator] = weiche
|
||||
continue
|
||||
|
||||
kandidaten = nachbarn
|
||||
gewaehlt = [s for s in kandidaten if s in mit_scanner]
|
||||
if len(gewaehlt) != 1:
|
||||
gewaehlt = kandidaten if len(kandidaten) == 1 else []
|
||||
@@ -1282,6 +1333,7 @@ def derive_tros(
|
||||
hosts=[strecke_id] if strecke_id in graph.nodes else [transfer.teile_id],
|
||||
separators=[sep_id],
|
||||
scanners=own_scanners,
|
||||
switches=[transfer.teile_id],
|
||||
reason=(
|
||||
f"{distance:.0f}mm von {art} {transfer.teile_id} entfernt - "
|
||||
f"kein eigenes Weichen-Bauteil im Layout, das {art} "
|
||||
@@ -1324,6 +1376,7 @@ def derive_tros(
|
||||
as_switch_tro[as_id] = tro
|
||||
weichen = weichen_je_tro[id(tro)]
|
||||
weichen.append(as_id)
|
||||
tro.switches.append(as_id)
|
||||
if strecke_id in graph.nodes and strecke_id not in tro.hosts:
|
||||
tro.hosts.append(strecke_id)
|
||||
tro.label = (f"Separator {steuer} an Ausschleuselementen "
|
||||
@@ -1362,6 +1415,8 @@ def derive_tros(
|
||||
hosts=hosts or [j.sep_kreisel[s] for s in j.separators],
|
||||
separators=list(j.separators),
|
||||
scanners=scanners_for(j.separators),
|
||||
# nicht exportierte Beruehrpunkte (W<a>_<b>) sind kein Layout-Bauteil
|
||||
switches=[c.weiche_id if c.exported else "" for c in j.contacts],
|
||||
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'} "
|
||||
@@ -1385,6 +1440,7 @@ def derive_tros(
|
||||
hosts=[host] if host is not None else [weiche.teile_id],
|
||||
separators=[sep_id],
|
||||
scanners=sorted(sep_scanners.get(sep_id, [])),
|
||||
switches=[weiche.teile_id],
|
||||
reason=f"steuert die ILS Weiche {weiche.teile_id} (echtes Weichen-Bauteil)",
|
||||
confidence="hoch",
|
||||
)
|
||||
@@ -1416,6 +1472,8 @@ def derive_tros(
|
||||
hosts=list(as_tro.hosts),
|
||||
separators=as_tro.separators + w_tro.separators,
|
||||
scanners=sorted(as_tro.scanners + w_tro.scanners),
|
||||
# switch1 = Ausschleuselement (separator1), switch2 = Weiche
|
||||
switches=as_tro.switches + w_tro.switches,
|
||||
reason=(f"Ausschleuselement {as_id} (Separator "
|
||||
f"{', '.join(as_tro.separators)}) und ILS Weiche {w_id} "
|
||||
f"(Separator {', '.join(w_tro.separators)}) liegen in "
|
||||
|
||||
Reference in New Issue
Block a user