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:
2026-10-16 13:42:16 +02:00
parent 39b445a7c4
commit d414bd6cf3
8 changed files with 297 additions and 46 deletions
+18 -3
View File
@@ -11,6 +11,9 @@
; ;
; Jeder Abschnitt "<Art>.<Rolle>" ist eine Regel; sie werden in dieser ; Jeder Abschnitt "<Art>.<Rolle>" ist eine Regel; sie werden in dieser
; Reihenfolge angewendet, ein E-Bauteil wird hoechstens einmal vergeben. ; Reihenfolge angewendet, ein E-Bauteil wird hoechstens einmal vergeben.
; <Rolle> ist zugleich die Slot-Rolle der TRO-JSON (sensor, stopper, valve,
; exitSensor): nur bei gleichem Namen landet die Zuordnung im Slot und damit
; in der SCL, sonst bleibt sie "ohne_slot" (z. B. Ausschleus.stau).
; layout Muster fuer die Layout-Art (Separator, Ausschleus, ; layout Muster fuer die Layout-Art (Separator, Ausschleus,
; Einschleus, Weiche, Scanner, Beladung, Entladung). ; Einschleus, Weiche, Scanner, Beladung, Entladung).
; Standard: der Teil vor dem Punkt im Abschnittsnamen. ; Standard: der Teil vor dem Punkt im Abschnittsnamen.
@@ -67,7 +70,8 @@ max_mm = 1600
soll_mm = 1390 soll_mm = 1390
; Ausschleusventil (Mubea: konstant ~1414 mm, Nummer = Stausensor + 1) ; Ausschleusventil (Mubea: konstant ~1414 mm, Nummer = Stausensor + 1)
[Ausschleus.ventil] ; -> Slot switch<n>.valve des TRO, dessen switches das Element nennt (xOutSwitch)
[Ausschleus.valve]
verw = AE ABZW* verw = AE ABZW*
praefix = MB praefix = MB
nummer_von = Ausschleus.stau nummer_von = Ausschleus.stau
@@ -75,8 +79,19 @@ nummer_versatz = 1
max_mm = 1600 max_mm = 1600
soll_mm = 1414 soll_mm = 1414
; Weichenventil an einer Kreisel-Weiche ; Ausschleusventil an einer Kreisel-Weiche (Ausschleuselement einer Strecke,
[Weiche.ventil] ; nicht einer Speicherlinie): liegt nicht im festen Raster der Speicherlinien
; und hat keinen Stausensor mit Nummer - 1 (Mubea MB0627/MB0715/MB0671:
; 350-900 mm neben dem Element). Laeuft nach [Ausschleus.valve], nimmt also
; nur, was dort uebrig bleibt: das naechste "AE ABZW*"-Ventil.
[Ausschleus_Weiche.valve]
layout = Ausschleus
verw = AE ABZW*
praefix = MB
max_mm = 1000
; Weichenventil an einer Kreisel-Weiche -> Slot switch<n>.valve
[Weiche.valve]
verw = ABZWEIG-*, WEICHE* verw = ABZWEIG-*, WEICHE*
praefix = MB praefix = MB
max_mm = 1000 max_mm = 1000
+7
View File
@@ -72,6 +72,13 @@ min_store_lanes = 2
; auf 0,5m). 30mm liegt bewusst weit unter dem Befund und ueber dem Rauschen. ; auf 0,5m). 30mm liegt bewusst weit unter dem Befund und ueber dem Rauschen.
gefaelle_drop_mm = 30.0 gefaelle_drop_mm = 30.0
; Steuernder Separator einer ILS Weiche (_find_weiche_separators): der letzte
; Separator in Laufrichtung vor der Weiche, auf dem Kreisel-Umlauf gemessen
; (Lage + Drehrichtung, nicht TrackIds). Liegt er weiter davor, steuert er die
; Weiche nicht. Mubea 0036 -> Weiche: ~1300mm; die naechste Alternative bei
; falscher Drehrichtung von Kreisel1 (0040) laege ~5200mm davor.
weiche_vorlauf_max = 3000.0
; Zeichnungsmassstab fuer --use-cords in pt/mm: Graphviz rechnet in Punkt, die ; Zeichnungsmassstab fuer --use-cords in pt/mm: Graphviz rechnet in Punkt, die
; Anlage in Millimeter. Seit die Symbole eine feste Groesse haben, reichen ; Anlage in Millimeter. Seit die Symbole eine feste Groesse haben, reichen
; 0.1 pt/mm: ~11 x 20 m Anlage ergeben etwa 1100 x 2000 pt. Vorher 0.25 ; 0.1 pt/mm: ~11 x 20 m Anlage ergeben etwa 1100 x 2000 pt. Vorher 0.25
+41 -8
View File
@@ -324,8 +324,30 @@ any other switch. The type follows from the count: separators × switches. The
host of each separator is the lane it physically lies on, not its `Zuordnung`. host of each separator is the lane it physically lies on, not its `Zuordnung`.
If the two Kreisel run against each other at the touch point (both `UZS` at a If the two Kreisel run against each other at the touch point (both `UZS` at a
pulley-to-pulley touch, as in Mubea and Dematic — probably an export error), no pulley-to-pulley touch, as in Mubea and Dematic — probably an export error), no
junction is formed, a finding is written, and the ILS Weiche is handled by the junction is formed, a finding is written, and the ILS Weiche is handled by
older neighbour rule (`_find_weiche_separators`, step 3c'). `_find_weiche_separators` (step 3c'). The same happens when a junction separator
already controls an Ausschleuselement (step 3b) — step 3d then merges both.
**Controlling separator of an ILS Weiche** (`_find_weiche_separators`, step 3c').
Measured on the Kreisel loop from position and `Drehrichtung`, **not** from
`TrackIds` or the `Nachbarn` column (`kreisel_junctions.separators_before`):
1. For each Kreisel the Weiche lies on, only the **last separator before** it
in the direction of travel counts. A separator behind the Weiche never
controls it, even if it is a CSV neighbour (finding).
2. It must be free (not already the controller of an Ausschleuselement, not a
VF-chain separator) and at most `weiche_vorlauf_max` mm of travel before the
Weiche (`cfg/parameter.ini [tro_flow]`, default 3000).
3. If several remain (one per Kreisel): CSV neighbour first, then one with a
scanner, then the shortest travel distance. A choice outside the CSV
neighbours is reported as a finding.
Only when the Weiche lies on no Kreisel loop (no geometry or `Drehrichtung`) does
the old neighbour rule apply (neighbour with scanner, else the only neighbour).
Mubea export 2026-10-16: Weiche 0130 has neighbours 0061/0012, but 0012 lies
behind it on Kreisel2 and 0005 (before it) already controls Ausschleuselement
0061; the controller is 0036, 1303 mm before it on Kreisel1, so step 3d gives
`2Sep2Swi` 0005 + 0036 and 0012 stays `1Sep`.
A scanner mounted on a separator does **not** change the type (stays `1Sep`); A scanner mounted on a separator does **not** change the type (stays `1Sep`);
the `1Sep_SSCC` special case can't be recognized from the mechanical layout the `1Sep_SSCC` special case can't be recognized from the mechanical layout
@@ -583,12 +605,14 @@ layout is `anlage.layout`; names come from `bindings[]` by TRO-ID + slot + role
|---|---| |---|---|
| `separator<n>` `sensor` / `stopper` / `partSensor` | `stSenInSep`, `xOutStopper<n>`, `stSenPart` | | `separator<n>` `sensor` / `stopper` / `partSensor` | `stSenInSep`, `xOutStopper<n>`, `stSenPart` |
| `line<n>` `sensor` / `stopper` / `jamSensor` | PinStore storage lines | | `line<n>` `sensor` / `stopper` / `jamSensor` | PinStore storage lines |
| `switch<n>` `valve` | `xOutSw<n>ExTo3` | | `switch<n>` `valve` | `xOutSw<n>ExTo3` — the slot names the switch part from `switches` of the TRO-JSON (Ausschleuselement or ILS Weiche; `switch<i>` belongs to `separator<i>` in 2Sep types) |
| `drive` `motor` / `motorProtection` / `sen*` | Vario motor, `xInMotorProtection`, `stSenCarrInPos` … (motor/protection fall back to the conveyor of the TRO's host) | | `drive` `motor` / `motorProtection` / `sen*` | Vario motor, `xInMotorProtection`, `stSenCarrInPos` … (motor/protection fall back to the conveyor of the TRO's host) |
| `conveyors[]` on the TRO's host | `xInCarouselRun` = `#fbCarousel<NNNN>.xOutConveyorFullSpeed` (Kreisel only); jam DB `"DB_JamArea<NNNN>".stJam<NNNN>_____` — still `TODO`, the running number `<k>` is not fixed yet | | `conveyors[]` on the TRO's host | `xInCarouselRun` = `#fbCarousel<NNNN>.xOutConveyorFullSpeed` (Kreisel only); jam DB `"DB_JamArea<NNNN>".stJam<NNNN>_____` — still `TODO`, the running number `<k>` is not fixed yet |
Not filled yet: jam sensors at the exits (`stInSenJam<n>`, the `Ausschleus.stau` Not filled yet: jam sensors at the exits (`stInSenJam<n>`, the `Ausschleus.stau`
bindings have no slot), scanners, separator/switch/MainTro numbers. The bindings have no slot), scanners, separator/switch/MainTro numbers. Switch
valves are filled since 2026-10-16 (Mubea: MB0627, MB0715, MB0724, MB0671 for
TRO14/18/09). The
templates are unchanged — a known name simply drops its marker. Afterwards templates are unchanged — a known name simply drops its marker. Afterwards
every inserted name is checked against the hardware planning (`hwFile` of the every inserted name is checked against the hardware planning (`hwFile` of the
plant JSON in `%SKEL_RESULTS%`, or `--hw NAME`): names missing there are listed plant JSON in `%SKEL_RESULTS%`, or `--hw NAME`): names missing there are listed
@@ -660,7 +684,14 @@ Steps:
| `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest | | `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest |
Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`, Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`,
`Ausschleus.stau`, `Ausschleus.ventil`, `Weiche.ventil`. `VERW` texts and the `Ausschleus.stau`, `Ausschleus.valve` (storage lines: fixed ~1414 mm, number =
jam sensor + 1), `Ausschleus_Weiche.valve` (`layout = Ausschleus`; switch
outfeeds of a Strecke: nearest `AE ABZW*` valve within 1000 mm, only what the
rule before left over), `Weiche.valve`. The role after the dot is the slot role
of the TRO-JSON (`sensor`, `stopper`, `valve`, `exitSensor`): only then does
`merge_hw_layout` put the match into a slot; any other role (`stau`) stays
`ohne_slot`. Several rules may fill the same role if their section names
differ (`layout` names the kind). `VERW` texts and the
numbering rule are project-specific — in the reference SCL (UH01..UH05) numbering rule are project-specific — in the reference SCL (UH01..UH05)
sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`. sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`.
4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the 4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the
@@ -784,8 +815,10 @@ Steps:
part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per
storage line (`line1` … `lineN`, same split as `scl_context._store`); storage line (`line1` … `lineN`, same split as `scl_context._store`);
`separator1` and `line1` can be the same part and both get the binding. `separator1` and `line1` can be the same part and both get the binding.
Parts without a slot (outfeed elements, switches — `tro_flow` does not keep The `switch<n>` slots name the TRO's switch parts (`switches`: the
them on the TRO yet) stay visible as `ohne_slot`. Ausschleuselement or ILS Weiche it controls), so their valve binds there.
Parts without a slot (storage-line outfeeds, roles like `stau` that no slot
has) stay visible as `ohne_slot`.
4. **Conveyors**: the E-planning names (`K0102`) and the layout names 4. **Conveyors**: the E-planning names (`K0102`) and the layout names
(`Kreisel3`) do not match, so every field device whose text names a conveyor (`Kreisel3`) do not match, so every field device whose text names a conveyor
votes for the nearest layout conveyor (distance to its bounding box, at most votes for the nearest layout conveyor (distance to its bounding box, at most
@@ -874,7 +907,7 @@ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
| Class | What it is | | Class | What it is |
|---|---| |---|---|
| `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. | | `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. |
| `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`, `slots`. Also used for the system boundaries `SYSn` (no slots). | | `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `switches` (switch parts in slot order, `switch<i>` with `separator<i>`; since 2026-10-16, optional), `reason`, `colocated`, `positionFrom`, `positionPoints`, `slots`. Also used for the system boundaries `SYSn` (no slots). |
| `Slot` | A place on a TRO where hardware belongs: `slot` (`separator1`, `switch1`, `scanner1`, `drive`, `line<n>` for the storage lines of a store — the stable key together with the TRO-ID), `layoutId` (TeileId in the current export, for tracing only; empty for switches, whose element `tro_flow` does not keep yet), `roles` the hardware planning must fill (`sensor`, `stopper`, `jamSensor?` …; `?` = optional). Built by `derive_slots()` from the catalog type. | | `Slot` | A place on a TRO where hardware belongs: `slot` (`separator1`, `switch1`, `scanner1`, `drive`, `line<n>` for the storage lines of a store — the stable key together with the TRO-ID), `layoutId` (TeileId in the current export, for tracing only; empty for switches, whose element `tro_flow` does not keep yet), `roles` the hardware planning must fill (`sensor`, `stopper`, `jamSensor?` …; `?` = optional). Built by `derive_slots()` from the catalog type. |
| `JamArea` | A jam area = the queue in front of a separator, the entry of exactly one TRO: `id` (`J<Separator>`, e.g. `J0042`), `tro`, `entry` (1, 2 for 2Sep types), `separator`, `host`, `predecessors` (all TROs/stations feeding it — merges have several), `note`, `position`. Built by `derive_jam_areas()`; the SCL name `"DB_JamArea<NNNN>".stJam<NNNN>_<k>` needs the conveyor number from the hardware planning and is not stored. | | `JamArea` | A jam area = the queue in front of a separator, the entry of exactly one TRO: `id` (`J<Separator>`, e.g. `J0042`), `tro`, `entry` (1, 2 for 2Sep types), `separator`, `host`, `predecessors` (all TROs/stations feeding it — merges have several), `note`, `position`. Built by `derive_jam_areas()`; the SCL name `"DB_JamArea<NNNN>".stJam<NNNN>_<k>` needs the conveyor number from the hardware planning and is not stored. |
| `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). | | `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). |
+84 -15
View File
@@ -324,6 +324,89 @@ def _group(contacts: list[Contact]) -> list[list[Contact]]:
return list(groups.values()) 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( def find_junctions(
elements: list[Element], elements: list[Element],
findings: list[str], findings: list[str],
@@ -344,21 +427,7 @@ def find_junctions(
loops, contacts = find_contacts(elements, findings) loops, contacts = find_contacts(elements, findings)
if separators is None: if separators is None:
separators = [e for e in elements if e.kind == "Separator"] separators = [e for e in elements if e.kind == "Separator"]
sep_pos, sep_lane = separator_positions(loops, separators)
# 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] = [] junctions: list[Junction] = []
for group in _group(contacts): for group in _group(contacts):
+11 -3
View File
@@ -91,6 +91,8 @@ __all__ = [
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived). Seit 2026-10-15 # 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 # 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. # 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" SCHEMA = "sps_skel-tro-v4"
# Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout) # Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout)
@@ -315,6 +317,11 @@ class DerivedTro(TroBase):
label: str label: str
hosts: list[str] hosts: list[str]
scanners: 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 reason: str
colocated: list[str] = Field(default_factory=list) colocated: list[str] = Field(default_factory=list)
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde # 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]: def derive_slots(tro: DerivedTro) -> list[Slot]:
"""Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO. """Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO.
Weichen tragen noch keine Layout-ID - tro_flow speichert das Weichen- Weichen-Slots tragen das Weichen- bzw. Ausschleuselement aus
bzw. Ausschleuselement heute nicht am TRO. Ein Speicher bekommt je ``tro.switches`` (leer, wenn tro_flow keins kennt). Ein Speicher bekommt je
Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und
``line1`` koennen dasselbe Bauteil sein (wie im PinStore-Template). ``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, slots.append(Slot(slot=f"line{n}", layout_id=sep,
roles=list(_STORAGE_LINE_ROLES))) roles=list(_STORAGE_LINE_ROLES)))
for i in range(items.get(TroItem.SWITCH, 0)): 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)): for i in range(items.get(TroItem.SCANNER, 0)):
slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i), slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i),
roles=list(_SCANNER_ROLES))) roles=list(_SCANNER_ROLES)))
+73 -15
View File
@@ -104,8 +104,10 @@ ausschliesslich derive_tros(), Schritt fuer Schritt:
Beruehrpunkten derselben Umlenkung Beruehrpunkten derselben Umlenkung
(_kreisel_knoten, lib/kreisel_junctions.py) (_kreisel_knoten, lib/kreisel_junctions.py)
3c' Separator an einer ILS Weiche, die kein 1Sep1Swi 3c' Separator an einer ILS Weiche, die kein 1Sep1Swi
Weichenknoten aus 3c ist Weichenknoten aus 3c ist: letzter freier
(_find_weiche_separators) Separator in Laufrichtung davor (Lage +
Drehrichtung, nicht TrackIds/Nachbarn;
_find_weiche_separators)
3d 3b-TRO und 3c'-TRO direkt hintereinander 2Sep2Swi 3d 3b-TRO und 3c'-TRO direkt hintereinander 2Sep2Swi
in der TrackIds-Kette eines Kreisels in der TrackIds-Kette eines Kreisels
4 1Sep mit 2 bzw. 3 abgehenden Flusskanten 1Sep1Swi / 1Sep2Swi 4 1Sep mit 2 bzw. 3 abgehenden Flusskanten 1Sep1Swi / 1Sep2Swi
@@ -178,7 +180,7 @@ from material_flow import (
read_elements, read_elements,
resolve_input, 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 model import Analysis, DerivedTro, LayoutPart, TroDocument
from parameter import get_float, get_int from parameter import get_float, get_int
from tro_ids import ( from tro_ids import (
@@ -211,6 +213,7 @@ from tro_catalog import (
_P = "tro_flow" _P = "tro_flow"
MIN_STORE_LANES = get_int(_P, "min_store_lanes") MIN_STORE_LANES = get_int(_P, "min_store_lanes")
GEFAELLE_DROP_MM = get_float(_P, "gefaelle_drop_mm") 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_SCALE = get_float(_P, "coord_scale")
COORD_PFEIL_LAENGE = get_int(_P, "coord_pfeil_laenge") COORD_PFEIL_LAENGE = get_int(_P, "coord_pfeil_laenge")
COORD_MAX_LUECKE = get_int(_P, "coord_max_luecke") 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": erzeugt sie z. B. am Beruehrpunkt zweier Kreisel (Merkmal "Erzeugt aus":
"Kreisel-Beruehrpunkt"; Mubea 0125 zwischen Kreisel2 und Kreisel1). "Kreisel-Beruehrpunkt"; Mubea 0125 zwischen Kreisel2 und Kreisel1).
Steuernder Separator ist der Separator unter den CSV-Nachbarn der Weiche, Steuernder Separator ist der Separator, der in Laufrichtung unmittelbar
der einen Scanner traegt (Scanner-Merkmal "Naechster Separator"). Mubea VOR der Weiche liegt - der Traeger haelt dort, wird gelesen und die
0125: Nachbarn 0040 und 0036, nur 0036 hat einen Scanner (0041) - vor Ort Weiche entscheidet. Liegt die Weiche auf Kreiseln mit Drehrichtung
bestaetigt (Review 2026-08-12 und 2026-10-01). Ist nur ein Separator (kreisel_junctions.separators_before), gilt:
benachbart, gilt dieser. Bleibt es mehrdeutig, wird nichts geraten,
sondern ein Finding geschrieben. 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 Grenze: einer Weiche wird hoechstens EIN Separator zugeordnet. Weichen
mit mehreren Separatoren (2Sep1Swi, 3Sep2Swi, ...) ermittelt vorher mit mehreren Separatoren (2Sep1Swi, 3Sep2Swi, ...) ermittelt vorher
@@ -674,15 +694,16 @@ def _find_weiche_separators(
ILS Weichen an, die dort keinen Knoten bilden. ILS Weichen an, die dort keinen Knoten bilden.
Parameter: 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). Ausschleus-Weichen-Separatoren aus switch_hits).
elements: alle Elemente des CSV-Exports (ILS Weichen und Scanner). elements: alle Elemente des CSV-Exports (ILS Weichen, Kreisel,
findings: Fundliste; je mehrdeutiger Weiche wird ein Eintrag Separatoren, Scanner).
angehaengt. findings: Fundliste; je verworfenem Nachbarn, je Zuordnung ausserhalb
der Nachbarn und je nicht zuordenbarer Weiche ein Eintrag.
Rueckgabe: Rueckgabe:
Separator-Id -> ILS Weiche (Element.kind "Weiche"). Wird in 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} by_id = {s.teile_id: s for s in separators}
mit_scanner = { mit_scanner = {
@@ -691,7 +712,37 @@ def _find_weiche_separators(
} }
result: dict[str, Element] = {} result: dict[str, Element] = {}
for weiche in (e for e in elements if e.kind == "Weiche"): 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] gewaehlt = [s for s in kandidaten if s in mit_scanner]
if len(gewaehlt) != 1: if len(gewaehlt) != 1:
gewaehlt = kandidaten if len(kandidaten) == 1 else [] 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], hosts=[strecke_id] if strecke_id in graph.nodes else [transfer.teile_id],
separators=[sep_id], separators=[sep_id],
scanners=own_scanners, scanners=own_scanners,
switches=[transfer.teile_id],
reason=( reason=(
f"{distance:.0f}mm von {art} {transfer.teile_id} entfernt - " f"{distance:.0f}mm von {art} {transfer.teile_id} entfernt - "
f"kein eigenes Weichen-Bauteil im Layout, das {art} " f"kein eigenes Weichen-Bauteil im Layout, das {art} "
@@ -1324,6 +1376,7 @@ def derive_tros(
as_switch_tro[as_id] = tro as_switch_tro[as_id] = tro
weichen = weichen_je_tro[id(tro)] weichen = weichen_je_tro[id(tro)]
weichen.append(as_id) weichen.append(as_id)
tro.switches.append(as_id)
if strecke_id in graph.nodes and strecke_id not in tro.hosts: if strecke_id in graph.nodes and strecke_id not in tro.hosts:
tro.hosts.append(strecke_id) tro.hosts.append(strecke_id)
tro.label = (f"Separator {steuer} an Ausschleuselementen " 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], hosts=hosts or [j.sep_kreisel[s] for s in j.separators],
separators=list(j.separators), separators=list(j.separators),
scanners=scanners_for(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} " reason=(f"Separator{'en' if len(j.separators) > 1 else ''} {vor} "
f"in Laufrichtung unmittelbar vor " f"in Laufrichtung unmittelbar vor "
f"{'den Weichen' if len(weichen) > 1 else 'der Weiche'} " 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], hosts=[host] if host is not None else [weiche.teile_id],
separators=[sep_id], separators=[sep_id],
scanners=sorted(sep_scanners.get(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)", reason=f"steuert die ILS Weiche {weiche.teile_id} (echtes Weichen-Bauteil)",
confidence="hoch", confidence="hoch",
) )
@@ -1416,6 +1472,8 @@ def derive_tros(
hosts=list(as_tro.hosts), hosts=list(as_tro.hosts),
separators=as_tro.separators + w_tro.separators, separators=as_tro.separators + w_tro.separators,
scanners=sorted(as_tro.scanners + w_tro.scanners), 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 " reason=(f"Ausschleuselement {as_id} (Separator "
f"{', '.join(as_tro.separators)}) und ILS Weiche {w_id} " f"{', '.join(as_tro.separators)}) und ILS Weiche {w_id} "
f"(Separator {', '.join(w_tro.separators)}) liegen in " f"(Separator {', '.join(w_tro.separators)}) liegen in "
+48 -1
View File
@@ -11,7 +11,7 @@ import math
import pytest import pytest
from kreisel_junctions import find_contacts, find_junctions, kreisel_loop from kreisel_junctions import find_contacts, find_junctions, kreisel_loop, separators_before
from material_flow import Element from material_flow import Element
@@ -149,3 +149,50 @@ def test_co_rotating_end_contact_is_weiche() -> None:
_loops, contacts = find_contacts([a, b], []) _loops, contacts = find_contacts([a, b], [])
assert len(contacts) == 1 assert len(contacts) == 1
assert math.hypot(contacts[0].x - 1500.0, contacts[0].y) < 5.0 assert math.hypot(contacts[0].x - 1500.0, contacts[0].y) < 5.0
# Mubea-Export 2026-10-16: Kreisel2 (UZS, senkrecht) und Kreisel1 (GUZ,
# waagerecht) beruehren sich an der ILS Weiche 0130. Auf Kreisel2 liegt 0005
# davor (steuert schon Ausschleuselement 0061), 0012 dahinter; auf Kreisel1
# liegt 0036 davor, 0040 dahinter. Die CSV-Nachbarn der Weiche sind 0061/0012.
def _mubea_weiche(kreisel1: str = "GUZ") -> list[Element]:
weiche = _el("0130", "Weiche", (400.0, -5478.0))
weiche.neighbours = [("0061", None), ("0012", None)]
return [
_kreisel("0054", (0.0, 4529.5), (800.0, 20815.0), "UZS", 90),
_kreisel("0129", (3300.0, -5478.0), (5800.0, 800.0), kreisel1, 0),
_el("0005", "Separator", (400.0, -3978.05)),
_el("0012", "Separator", (-400.0, -3554.95)),
_el("0036", "Separator", (1474.95, -5078.0)),
_el("0040", "Separator", (5355.05, -5878.0)),
weiche,
]
def test_separators_before_uses_geometry_not_trackids() -> None:
vorher, nachher = separators_before(_mubea_weiche(), 400.0, -5478.0)
assert {u.kreisel: u.separator for u in vorher} == {"0054": "0005", "0129": "0036"}
assert all(u.distance < 2000.0 for u in vorher)
assert nachher == ["0012", "0040"]
def test_weiche_controlled_by_free_separator_before_it() -> None:
from tro_flow import _find_weiche_separators
elements = _mubea_weiche()
frei = [e for e in elements if e.teile_id in ("0012", "0036", "0040")] # 0005: Ausschleus
findings: list[str] = []
result = _find_weiche_separators(frei, elements, findings)
assert {s: w.teile_id for s, w in result.items()} == {"0036": "0130"}
assert any("CSV-Nachbar 0012 liegt in Laufrichtung hinter" in f for f in findings)
def test_weiche_far_behind_separator_is_not_assigned() -> None:
# Kreisel1 falsch als UZS: davor laege nur 0040, ~5 m weit weg - kein Raten
from tro_flow import _find_weiche_separators
elements = _mubea_weiche(kreisel1="UZS")
frei = [e for e in elements if e.teile_id in ("0012", "0036", "0040")]
findings: list[str] = []
assert _find_weiche_separators(frei, elements, findings) == {}
assert any("kein freier Separator in Laufrichtung davor" in f for f in findings)
+15 -1
View File
@@ -127,10 +127,24 @@ def test_pflichtrollen_ohne_bmk_sind_offen_optionale_nicht():
anlage = run() anlage = run()
roles = {(b.slot, b.role): b.status for b in anlage.bindings if b.tro == "TRO06"} roles = {(b.slot, b.role): b.status for b in anlage.bindings if b.tro == "TRO06"}
assert roles == {("separator1", "sensor"): "offen", ("separator1", "stopper"): "offen"} assert roles == {("separator1", "sensor"): "offen", ("separator1", "stopper"): "offen"}
# die Weiche hat noch keine Layout-ID -> Ventil offen # ohne switches (TRO-JSON vor 2026-10-16) keine Layout-ID -> Ventil offen
assert anlage.binding("TRO02", "switch1", "valve").status == "offen" assert anlage.binding("TRO02", "switch1", "valve").status == "offen"
def test_weichen_slot_bekommt_ventil_des_ausschleuselements():
# TRO02 steuert Ausschleuselement 0030 (tro_flow: switches) -> switch1 traegt
# 0030, das Ausschleusventil (Nummer = Stausensor + 1) landet im Slot.
doc = layout()
tro = doc.by_id("TRO02")
tro.switches = ["0030"]
tro.slots = derive_slots(tro)
hw = hardware()
hw.devices.append(_dev("MB0602", "AE ABZW", (0.0, 2000.0 + 1414.0), direction="out"))
valve = run(doc=doc, hw=hw).binding("TRO02", "switch1", "valve")
assert (valve.bmk, valve.layout_id, valve.rule, valve.status) == (
"MB0602", "0030", "nummer", "zugeordnet")
def test_bauteil_ohne_slot_bleibt_sichtbar(): def test_bauteil_ohne_slot_bleibt_sichtbar():
loose = [b for b in run().bindings if not b.tro] loose = [b for b in run().bindings if not b.tro]
assert [(b.role, b.layout_id, b.bmk, b.status) for b in loose] == [ assert [(b.role, b.layout_id, b.bmk, b.status) for b in loose] == [