tro_flow: Speicherbloecke je Steuer-Separator, Ring-Schluss bis zum ersten Kettenglied, Weichen nur in Laufrichtung
- _speicher_bloecke: Schwerkraftlinien nach Umlaufreihenfolge in Bloecke schneiden (Separator dazwischen = neuer Block); der Weichen-Separator vor dem Block ist Steuer-Separator des PinStore_Auto statt eigener 1Sep1Swi (Dematic: 3 Speicher 10/7/10 statt einem mit 27) - Umlauf schliessen bis zum ersten Element mit TRO, nicht nur ueber Stationen hinweg (Dematic Kreisel2 beginnt mit Einschleuselement: TRO01 -> Speicher fehlte; Mubea TRO06 -> TRO10 statt Fallback TRO06 -> TRO18) - _weichen_nach_umlauf: Separator steuert ein Ausschleuselement nur als letzter Separator davor in Laufrichtung (Dematic 0047/0046 waren ueber den Ring-Schluss bzw. hinter dem Block "benachbart") - _kreisel_bahn: Kreisel-Separator mit Gefaellestrecke als Zuordnung auf die Kreisel-Bahn haengen (Dematic 0046 zaehlte als Speicherlinie) - HundM05 unveraendert, Mubea 26 Kanten (eine korrigiert) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e0defad011
commit
a741dd0157
+186
-9
@@ -290,6 +290,149 @@ def _group_lanes(lanes: list[str], graph: Graph) -> list[list[str]]:
|
|||||||
return [group["lanes"] for group in groups]
|
return [group["lanes"] for group in groups]
|
||||||
|
|
||||||
|
|
||||||
|
def _kreisel_bahn(sep: Element, elements: list[Element], graph: Graph) -> str | None:
|
||||||
|
"""Kreisel-Bahn-Knoten eines Separators, der in der Umlaufkette
|
||||||
|
(Element.track_ids) eines Kreisels steht - sonst None.
|
||||||
|
|
||||||
|
dxfmakros traegt bei Kreisel-Separatoren als "Zuordnung" oft die naechste
|
||||||
|
Strecke statt der Bahn ein (Dematic/Arvato: 0043 -> VF_5, 0048 -> VF_4,
|
||||||
|
0046 -> Gefaellestrecke 0037; nur 0038/0047 stehen auf "0002-L"/"0003-R").
|
||||||
|
Bei einer Gefaellestrecke zaehlt er dann faelschlich als Speicherlinien-
|
||||||
|
Separator (Aufrufer in derive_tros wendet es nur dort an).
|
||||||
|
Die Bahn ergibt sich aus der Seite der Kreisel-Achse ("Drehung"): links
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
for k in elements:
|
||||||
|
if k.kind != "Kreisel" or sep.teile_id not in k.track_ids:
|
||||||
|
continue
|
||||||
|
drehung = _as_float(_merkmal(k.merkmale, "Drehung"))
|
||||||
|
if drehung is None or None in (sep.x, sep.y, k.x, k.y):
|
||||||
|
return None
|
||||||
|
rad = math.radians(drehung)
|
||||||
|
kreuz = math.cos(rad) * (sep.y - k.y) - math.sin(rad) * (sep.x - k.x)
|
||||||
|
bahn = f"{k.teile_id}-{LANE_LEFT if kreuz > 0 else LANE_RIGHT}"
|
||||||
|
return bahn if bahn in graph.nodes else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _weichen_nach_umlauf(
|
||||||
|
switch_hits: dict[str, tuple[Element, float]],
|
||||||
|
elements: list[Element],
|
||||||
|
findings: list[str],
|
||||||
|
) -> dict[str, tuple[Element, float]]:
|
||||||
|
"""Weichen-Separatoren verwerfen, die in Laufrichtung NACH ihrem
|
||||||
|
Ausschleuselement liegen.
|
||||||
|
|
||||||
|
_find_switch_separators paart ueber die CSV-Nachbarn - die kennen aber
|
||||||
|
keine Laufrichtung, und am Ring ist auch das erste mit dem letzten
|
||||||
|
Element der TrackIds-Liste benachbart. Ein Separator steuert ein
|
||||||
|
Ausschleuselement nur, wenn er der LETZTE Separator davor ist (Element.
|
||||||
|
track_ids des Kreisels ist in Laufrichtung sortiert, bei gesetzter
|
||||||
|
Drehrichtung ein geschlossener Ring). Dematic/Arvato (Review 2026-10-06):
|
||||||
|
0047 sitzt auf Kreisel3 am Kopf der Gegenbahn und war ueber den Ring-
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
by_id = {e.teile_id: e for e in elements}
|
||||||
|
kreisel = [e for e in elements if e.kind == "Kreisel" and e.track_ids]
|
||||||
|
result: dict[str, tuple[Element, float]] = {}
|
||||||
|
for sep_id, (transfer, distance) in switch_hits.items():
|
||||||
|
k = next((k for k in kreisel
|
||||||
|
if transfer.teile_id in k.track_ids and sep_id in k.track_ids), None)
|
||||||
|
if k is None:
|
||||||
|
result[sep_id] = (transfer, distance)
|
||||||
|
continue
|
||||||
|
ids = list(k.track_ids)
|
||||||
|
i = ids.index(transfer.teile_id)
|
||||||
|
ring = k.rotation in (ROTATION_CW, ROTATION_CCW)
|
||||||
|
davor = ids[:i][::-1] + (ids[i + 1:][::-1] if ring else [])
|
||||||
|
vorgaenger = next((t for t in davor
|
||||||
|
if (el := by_id.get(t)) and el.kind == "Separator"), None)
|
||||||
|
if vorgaenger == sep_id:
|
||||||
|
result[sep_id] = (transfer, distance)
|
||||||
|
else:
|
||||||
|
findings.append(
|
||||||
|
f"Separator {sep_id}: CSV-Nachbar von Ausschleuselement "
|
||||||
|
f"{transfer.teile_id}, liegt in Laufrichtung aber nicht davor "
|
||||||
|
f"(letzter Separator davor: {vorgaenger or '-'}) - keine Weiche, "
|
||||||
|
f"gewoehnlicher Kreisel-Separator")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _speicher_bloecke(
|
||||||
|
lanes: list[str],
|
||||||
|
elements: list[Element],
|
||||||
|
switch_hits: dict[str, tuple[Element, float]],
|
||||||
|
) -> tuple[dict[str, tuple[str, int]], dict[str, tuple[str, int]]]:
|
||||||
|
"""Schwerkraftlinien je Kreisel in Speicherbloecke schneiden.
|
||||||
|
|
||||||
|
_group_lanes fasst alle Linien mit gemeinsamem Zu-/Ablauf zusammen - an
|
||||||
|
Dematic/Arvato haengen aber alle 27 Linien an denselben zwei Kreiseln und
|
||||||
|
bilden doch DREI Bloecke (10/7/10), je von einem eigenen Separator auf der
|
||||||
|
Kreisel-Bahn gesteuert, dazwischen kehren VF_1..VF_3 zurueck (Review
|
||||||
|
2026-10-06). Grenze ist darum die Umlaufreihenfolge (Element.track_ids):
|
||||||
|
die Ausschleuselemente der Linien folgen dort lueckenlos aufeinander, ein
|
||||||
|
Separator dazwischen beginnt einen neuen Block. Mubea Kreisel3 (20
|
||||||
|
Ausschleuselemente ohne Separator dazwischen) bleibt ein Block.
|
||||||
|
|
||||||
|
Steuer-Separator eines Blocks ist der Weichen-Separator (switch_hits,
|
||||||
|
_find_switch_separators), dessen Ausschleuselement zu einer Linie des
|
||||||
|
Blocks gehoert: er steht auf der Kreisel-Bahn direkt davor und hat das
|
||||||
|
erste Ausschleuselement des Blocks als CSV-Nachbarn (Dematic 0040 ->
|
||||||
|
0083). Er gehoert in den PinStore_Auto-TRO (Katalog: 1x Separator +
|
||||||
|
Scanner + Encoder + Speicherlinien), nicht in einen eigenen 1Sep1Swi -
|
||||||
|
das Ausschleuselement einer Speicherlinie ist keine Weiche einer Strecke.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
lane_set = set(lanes)
|
||||||
|
by_id = {e.teile_id: e for e in elements}
|
||||||
|
as_lane: dict[str, str] = {}
|
||||||
|
for e in elements:
|
||||||
|
if e.kind != "Ausschleus":
|
||||||
|
continue
|
||||||
|
strecke = str(_merkmal(e.merkmale, "Strecke", "Förderer") or "")
|
||||||
|
if strecke in lane_set:
|
||||||
|
as_lane[e.teile_id] = strecke
|
||||||
|
|
||||||
|
lane_block: dict[str, tuple[str, int]] = {}
|
||||||
|
as_block: dict[str, tuple[str, int]] = {}
|
||||||
|
for kreisel in (e for e in elements if e.kind == "Kreisel"):
|
||||||
|
ids = list(kreisel.track_ids)
|
||||||
|
if not any(t in as_lane for t in ids):
|
||||||
|
continue
|
||||||
|
# Am ersten Separator beginnen, damit ein Block nicht ueber das
|
||||||
|
# Listenende hinweg in zwei Teile zerfaellt (die Kette ist ein Ring).
|
||||||
|
start = next((i for i, t in enumerate(ids)
|
||||||
|
if (el := by_id.get(t)) and el.kind == "Separator"), 0)
|
||||||
|
ids = ids[start:] + ids[:start]
|
||||||
|
nummer, offen = 0, False
|
||||||
|
for t in ids:
|
||||||
|
el = by_id.get(t)
|
||||||
|
if el is not None and el.kind == "Separator":
|
||||||
|
offen = False
|
||||||
|
elif t in as_lane:
|
||||||
|
if not offen:
|
||||||
|
nummer += 1
|
||||||
|
offen = True
|
||||||
|
block = (kreisel.teile_id, nummer)
|
||||||
|
as_block[t] = block
|
||||||
|
lane_block.setdefault(as_lane[t], block)
|
||||||
|
|
||||||
|
steuer: dict[str, tuple[str, int]] = {}
|
||||||
|
for sep_id, (transfer, _d) in switch_hits.items():
|
||||||
|
block = as_block.get(transfer.teile_id)
|
||||||
|
if block is not None:
|
||||||
|
steuer[sep_id] = block
|
||||||
|
return lane_block, steuer
|
||||||
|
|
||||||
|
|
||||||
def _switch_exits(hosts: list[str], graph: Graph) -> list[str]:
|
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)."""
|
||||||
return sorted(
|
return sorted(
|
||||||
@@ -587,7 +730,8 @@ def derive_tros(
|
|||||||
# unten (3b) ihre eigene 1Sep1Swi-Uebergabestelle und duerfen nicht zugleich
|
# unten (3b) ihre eigene 1Sep1Swi-Uebergabestelle und duerfen nicht zugleich
|
||||||
# als gewoehnlicher Separator ihres Zuordnung-Hosts (Kreisel-Bahn oder
|
# als gewoehnlicher Separator ihres Zuordnung-Hosts (Kreisel-Bahn oder
|
||||||
# Strecke - beides kommt vor) mitgezaehlt werden.
|
# Strecke - beides kommt vor) mitgezaehlt werden.
|
||||||
switch_hits = _find_switch_separators(switch_candidates, elements)
|
switch_hits = _weichen_nach_umlauf(
|
||||||
|
_find_switch_separators(switch_candidates, elements), elements, findings)
|
||||||
# Separatoren an einer echten ILS Weiche (siehe _find_weiche_separators) -
|
# 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(
|
weiche_hits = _find_weiche_separators(
|
||||||
@@ -601,6 +745,17 @@ def derive_tros(
|
|||||||
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:
|
||||||
continue
|
continue
|
||||||
host = resolve_host(sep, findings)
|
host = resolve_host(sep, findings)
|
||||||
|
# Steht er in der Umlaufkette eines Kreisels, zeigt die Zuordnung aber
|
||||||
|
# auf eine Gefaellestrecke, ist das nur die naechstgelegene Linie
|
||||||
|
# (siehe _kreisel_bahn) - sonst zaehlte er als Speicherlinien-
|
||||||
|
# Separator (Dematic 0046 in Speicherblock C). Bewusst NUR fuer
|
||||||
|
# Gefaellestrecken: Kreisel-Separatoren mit einer VF-Strecke als
|
||||||
|
# Zuordnung bleiben wie bisher (HundM05 0018 laut Review 2026-09-21
|
||||||
|
# Teil seiner Strecke; ein Umhaengen nummerierte HundM05/Mubea um).
|
||||||
|
if host is not None and graph.nodes[host].kind == "Gefaellestrecke":
|
||||||
|
bahn = _kreisel_bahn(sep, elements, graph)
|
||||||
|
if bahn is not None:
|
||||||
|
host = bahn
|
||||||
if host is not None:
|
if host is not None:
|
||||||
sep_host[sep.teile_id] = host
|
sep_host[sep.teile_id] = host
|
||||||
|
|
||||||
@@ -661,20 +816,37 @@ def derive_tros(
|
|||||||
lanes = sorted(
|
lanes = sorted(
|
||||||
{h for h in sep_host.values() if graph.nodes[h].kind == "Gefaellestrecke"}
|
{h for h in sep_host.values() if graph.nodes[h].kind == "Gefaellestrecke"}
|
||||||
)
|
)
|
||||||
|
# Speicherbloecke nach Umlaufreihenfolge und Steuer-Separator schneiden
|
||||||
|
# (siehe _speicher_bloecke). Steuer-Separatoren eines gebildeten Speichers
|
||||||
|
# bekommen in 3b keinen eigenen 1Sep1Swi mehr.
|
||||||
|
lane_block, steuer_block = _speicher_bloecke(lanes, elements, switch_hits)
|
||||||
|
speicher_steuer: set[str] = set()
|
||||||
|
teilgruppen: list[list[str]] = []
|
||||||
for group in _group_lanes(lanes, graph):
|
for group in _group_lanes(lanes, graph):
|
||||||
|
nach_block: dict = {}
|
||||||
|
for lane in group:
|
||||||
|
nach_block.setdefault(lane_block.get(lane), []).append(lane)
|
||||||
|
teilgruppen.extend(sorted(nach_block.values(), key=lambda g: g[0]))
|
||||||
|
for group in teilgruppen:
|
||||||
seps = seps_on(group)
|
seps = seps_on(group)
|
||||||
if len(group) >= MIN_STORE_LANES:
|
if len(group) >= MIN_STORE_LANES:
|
||||||
|
bloecke = {lane_block.get(lane) for lane in group} - {None}
|
||||||
|
steuer = sorted(s for s, b in steuer_block.items() if b in bloecke)
|
||||||
|
speicher_steuer.update(steuer)
|
||||||
|
alle_seps = steuer + seps
|
||||||
tros.append(
|
tros.append(
|
||||||
Tro(
|
Tro(
|
||||||
tro_id="",
|
tro_id="",
|
||||||
type_name="PinStore_Auto",
|
type_name="PinStore_Auto",
|
||||||
label=f"Speicher ({len(group)} Schwerkraftlinien)",
|
label=f"Speicher ({len(group)} Schwerkraftlinien)",
|
||||||
hosts=group,
|
hosts=group,
|
||||||
separators=seps,
|
separators=alle_seps,
|
||||||
scanners=scanners_for(seps),
|
scanners=scanners_for(alle_seps),
|
||||||
reason=(
|
reason=(
|
||||||
f"{len(group)} Gefaellestrecken mit je eigenem Separator, "
|
f"{len(group)} Gefaellestrecken mit je eigenem Separator, "
|
||||||
f"gemeinsamer Zu-/Ablauf - Muster eines Linienspeichers"
|
f"gemeinsamer Zu-/Ablauf - Muster eines Linienspeichers"
|
||||||
|
+ (f"; Steuer-Separator {', '.join(steuer)} auf der "
|
||||||
|
f"Kreisel-Bahn davor" if steuer else "")
|
||||||
),
|
),
|
||||||
confidence="hoch",
|
confidence="hoch",
|
||||||
)
|
)
|
||||||
@@ -857,6 +1029,8 @@ def derive_tros(
|
|||||||
# von selbst ein, ohne einen weiteren synthetischen Host zu erfinden.
|
# von selbst ein, ohne einen weiteren synthetischen Host zu erfinden.
|
||||||
as_switch_tro: dict[str, Tro] = {} # Ausschleuselement-Id -> 1Sep1Swi
|
as_switch_tro: dict[str, Tro] = {} # Ausschleuselement-Id -> 1Sep1Swi
|
||||||
for sep_id, (transfer, distance) in sorted(switch_hits.items()):
|
for sep_id, (transfer, distance) in sorted(switch_hits.items()):
|
||||||
|
if sep_id in speicher_steuer:
|
||||||
|
continue # Teil seines Speichers (Schritt 1)
|
||||||
strecke_id = str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or "")
|
strecke_id = str(_merkmal(transfer.merkmale, "Strecke", "Förderer") or "")
|
||||||
ende = str(_merkmal(transfer.merkmale, "Streckenende") or "")
|
ende = str(_merkmal(transfer.merkmale, "Streckenende") or "")
|
||||||
own_scanners = sorted(sep_scanners.get(sep_id, []))
|
own_scanners = sorted(sep_scanners.get(sep_id, []))
|
||||||
@@ -1339,13 +1513,16 @@ def link_tros(
|
|||||||
laenge = len(chain)
|
laenge = len(chain)
|
||||||
geschlossen = el.rotation in (ROTATION_CW, ROTATION_CCW)
|
geschlossen = el.rotation in (ROTATION_CW, ROTATION_CCW)
|
||||||
if geschlossen:
|
if geschlossen:
|
||||||
# Umlauf schliessen: erstes Element hinten anhaengen. Stationen am
|
# Umlauf schliessen: den Listenanfang hinten anhaengen, und zwar
|
||||||
# Listenanfang sind kein Kettenglied - ueber sie hinweg bis zum
|
# bis einschliesslich des ersten Kettenglieds (Element mit TRO).
|
||||||
# ersten anderen Element verlaengern, sonst fehlt der Schluss
|
# Alles davor - Stationen, aber auch Ein-/Ausschleuselemente ohne
|
||||||
# (HundM05 Kreisel1 beginnt mit den Beladungen 0021/0022: ohne das
|
# eigenen TRO - ist kein Glied; endete die Verlaengerung dort, fehlte
|
||||||
# fehlte TRO19 -> TRO20).
|
# der Schluss (HundM05 Kreisel1 beginnt mit den Beladungen 0021/
|
||||||
|
# 0022: ohne das fehlte TRO19 -> TRO20; Dematic Kreisel2 beginnt
|
||||||
|
# mit dem Einschleuselement 0060: es fehlte TRO01 -> Speicher,
|
||||||
|
# Review 2026-10-06).
|
||||||
k = 0
|
k = 0
|
||||||
while k < laenge - 1 and chain[k] in station_hosts:
|
while k < laenge - 1 and chain[k] not in ketten_owner:
|
||||||
k += 1
|
k += 1
|
||||||
chain.extend(chain[:k + 1])
|
chain.extend(chain[:k + 1])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user