--track: Bahnzusammenhang aus der TrackIds-Spalte statt aus Nachbarn
build_track_graph baut die Fein-Adjazenz jetzt direkt aus der geordneten TrackIds-Spalte je Container (Element.track_ids): Kreisel als zirkulaerer Umlauf in Drehrichtung (Export-Sortierung), Strecke/Gefaelle als lineare Kette AS -> SEP.. -> ES. Gemeinsame AS/ES-/Weiche-Knoten verketten die Container automatisch - die Rueckmeldungs-/Rollen-Heuristik aus der Nachbarn-Spalte (_transfer_kreisel_lane, _wire_weiche, _orient_separator_links) entfaellt komplett. _attach_track_symbols misst offene Bahnenden jetzt am feinen Graphen statt am groben; das behebt die vereinzelten Falsch-Eingaenge an voll verketteten Bahnen (HundM05: Bahn 0001-0004, 0006-R). Doku in doc/Python_Scripts.md und doc/track.md nachgezogen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
db4e975adf
commit
516f30278b
+190
-369
@@ -121,6 +121,13 @@ TRANSFER_PATTERNS = (
|
||||
# liefern (siehe TRANSPORT_KINDS/CIRCLE_KINDS oben).
|
||||
TRACK_KINDS = ("Separator",) + TRANSFER_KINDS
|
||||
|
||||
# Container, die in der CSV-Spalte "TrackIds" eine geordnete Liste ihrer
|
||||
# enthaltenen Bahnelemente fuehren (siehe Element.track_ids): Kreisel bringt den
|
||||
# zirkulaeren Umlauf in Drehrichtung mit, Strecke/Gefaellestrecke die lineare
|
||||
# Kette AS -> SEP.. -> ES in Baureihenfolge. Aus diesen Listen wird der
|
||||
# --track-Graph direkt aufgebaut (build_track_graph).
|
||||
TRACK_CONTAINER_KINDS = CIRCLE_KINDS + TRANSPORT_KINDS
|
||||
|
||||
LANE_LEFT = "L"
|
||||
LANE_RIGHT = "R"
|
||||
LANE_NAMES = {LANE_LEFT: "links", LANE_RIGHT: "rechts"}
|
||||
@@ -176,6 +183,12 @@ class Element:
|
||||
name: str
|
||||
plan_square: str
|
||||
neighbours: list[tuple[str, str | None]]
|
||||
# Geordnete Element-Liste eines Containers (Kreisel/VF/GF) aus der CSV-Spalte
|
||||
# "TrackIds": bei VF/GF die Kette AS -> SEP.. -> ES in Baureihenfolge, bei
|
||||
# Kreisel der zirkulaere Umlauf aller auf ihm liegenden Punkte in
|
||||
# Drehrichtung (Separator/AS/ES/Weiche/BTMT/Eingang/Ausgang). Leer fuer
|
||||
# Nicht-Container. Erzeugt von dxfmakros/export_csv.py, siehe doc/track.md.
|
||||
track_ids: list[str]
|
||||
merkmale: dict
|
||||
row: int
|
||||
position: tuple[float, float, float] | None = None
|
||||
@@ -466,6 +479,11 @@ def parse_neighbours(raw: str) -> list[tuple[str, str | None]]:
|
||||
return result
|
||||
|
||||
|
||||
def parse_track_ids(raw: str) -> list[str]:
|
||||
"""'0118, 0059, 0119' -> ['0118', '0059', '0119'] (Reihenfolge erhalten)."""
|
||||
return [token.strip() for token in (raw or "").split(",") if token.strip()]
|
||||
|
||||
|
||||
def _match_kind(patterns, *texts: str) -> str | None:
|
||||
"""Erste Musterklasse, deren Schluesselwort in den Texten vorkommt."""
|
||||
haystack = " ".join(t.lower() for t in texts if t)
|
||||
@@ -534,6 +552,7 @@ def read_elements(path: Path, warnings: list[str]) -> list[Element]:
|
||||
name=(row.get("Bezeichnung") or "").strip(),
|
||||
plan_square=(row.get("Planquadrat") or "").strip(),
|
||||
neighbours=parse_neighbours(row.get("Nachbarn") or ""),
|
||||
track_ids=parse_track_ids(row.get("TrackIds") or ""),
|
||||
merkmale=merkmale,
|
||||
row=offset,
|
||||
position=parse_position(row.get("Position") or ""),
|
||||
@@ -1123,244 +1142,26 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
|
||||
# Bahnzusammenhang-Graph (--track, siehe doc/track.md)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Feinerer Graph als build_graph(): Separator/Ausschleus/Einschleus/Weiche
|
||||
# werden hier zu eigenen Knoten (statt nur Kanten/Zuordnungen zu liefern wie
|
||||
# im normalen Materialfluss). Baut auf dem fertigen groben Graph auf (fuer
|
||||
# Kreisel-Drehrichtung ueber _weiche_lane_roles und die bereits geloeste
|
||||
# Stations-Anbindung aus _attach_stations) und auf der CSV-Spalte "Nachbarn"
|
||||
# der vier TRACK_KINDS selbst - die die bestehende Pipeline bisher komplett
|
||||
# ignoriert (_collect_links ueberspringt sie, siehe dort).
|
||||
# Feinerer Graph als build_graph(): Separator/Ausschleus/Einschleus/Weiche und
|
||||
# die Ein-/Ausgangspunkte (BTMT, Eingang/Ausgang) werden hier zu eigenen Knoten
|
||||
# (statt nur Kanten/Zuordnungen zu liefern wie im normalen Materialfluss).
|
||||
#
|
||||
# Kernbefund (doc/track.md Abschnitt 1): ein Ausschleus-/Einschleuselement
|
||||
# nennt in seiner eigenen Nachbarn-Spalte konsequent nur die Foerderer-/
|
||||
# Streckenseite; die Kreisel-Seite ist nur ueber die Rueckmeldung des dortigen
|
||||
# Separators auffindbar (der das Transferelement seinerseits als Nachbarn
|
||||
# nennt). Die Fein-Adjazenz ist daher die Vereinigung aller Nennungen, nicht
|
||||
# nur die eigene Nachbarn-Liste eines Elements.
|
||||
# Quelle der Fein-Adjazenz ist die CSV-Spalte "TrackIds" (Element.track_ids):
|
||||
# jeder Container (Kreisel/Strecke/Gefaellestrecke) fuehrt die geordnete Liste
|
||||
# der auf ihm liegenden Bahnelemente. Der dxfmakros-Export berechnet diese
|
||||
# Reihenfolge bereits (Kreisel: Umlauf nach Winkel in Drehrichtung; Strecke/
|
||||
# Gefaelle: Kette AS -> SEP.. -> ES in Baureihenfolge) - siehe
|
||||
# dxfmakros/doc/csv-export.md. build_track_graph zieht daraus je Container eine
|
||||
# gerichtete Kette; ein Element, das in zwei Listen steht (ein AS/ES in seiner
|
||||
# Strecke UND im Kreisel-Umlauf, eine Weiche in beiden Kreiseln), ist derselbe
|
||||
# Knoten und verkettet die Container automatisch. Die frueher noetige
|
||||
# Rekonstruktion der Reihenfolge aus der "Nachbarn"-Spalte (Rueckmeldungs-/
|
||||
# Rollen-Heuristik) entfaellt damit.
|
||||
|
||||
SYMBOL_ENTRY = "Eingang"
|
||||
SYMBOL_EXIT = "Ausgang"
|
||||
|
||||
|
||||
def _transfer_kreisel_lane(el: Element, by_id: dict[str, Element], nodes: dict[str, Node]) -> str | None:
|
||||
"""
|
||||
Kreisel-Bahn (…-L/…-R) eines Ausschleus-/Einschleuselements.
|
||||
|
||||
Die Merkmale von AS/ES nennen nur die blanke Kreisel-TeileId (Merkmal
|
||||
"Kreisel"), nie die Bahn. Welche der beiden Bahnen gemeint ist, steht
|
||||
stattdessen in den Nachbarn der zugehoerigen Strecke/Gefaellestrecke
|
||||
(Merkmal "Strecke" bzw. "Förderer") - dieselbe Aufloesung, die
|
||||
_apply_transfers fuer die groben Kreisel<->Strecke-Kanten schon nutzt.
|
||||
"""
|
||||
kreisel_id = str(_merkmal(el.merkmale, "Kreisel") or "").strip()
|
||||
if not kreisel_id:
|
||||
return None
|
||||
strecke_id = str(_merkmal(el.merkmale, "Strecke", "Förderer") or "").strip()
|
||||
strecke_el = by_id.get(strecke_id)
|
||||
if strecke_el is not None:
|
||||
lane = _kreisel_lane_node(kreisel_id, strecke_el, nodes)
|
||||
if lane:
|
||||
return lane
|
||||
for lane_suffix in (LANE_LEFT, LANE_RIGHT): # Fallback: irgendeine Bahn
|
||||
if f"{kreisel_id}-{lane_suffix}" in nodes:
|
||||
return f"{kreisel_id}-{lane_suffix}"
|
||||
return kreisel_id if kreisel_id in nodes else None
|
||||
|
||||
|
||||
def _wire_weiche(
|
||||
el: Element,
|
||||
by_id: dict[str, Element],
|
||||
graph: Graph,
|
||||
own: dict[str, list[str]],
|
||||
track: TrackGraph,
|
||||
warnings: list[str],
|
||||
placeholder,
|
||||
add_edge,
|
||||
) -> None:
|
||||
"""
|
||||
Weiche als 4-Wege-Kreuzung verdrahten (Kreisel A <-> Kreisel B, in beide
|
||||
Richtungen; siehe doc/track.md Abschnitt 2).
|
||||
|
||||
Pro Kreisel liefert das bereits vorhandene _weiche_lane_roles() die
|
||||
Eingangs-/Ausgangsbahn aus der Drehrichtung. Die eigenen Nachbarn der
|
||||
Weiche (bis zu 2) werden per Zuordnung auf diese vier Rollen verteilt;
|
||||
ein Nachbar, dessen Zuordnung zu keiner der vier Bahnen passt (z. B. weil
|
||||
er - wie in den Mubea-Testdaten - foelschlich einer Strecke statt der
|
||||
schaltenden Kreisel-Bahn zugeordnet ist, vgl. tro_overrides.py-Fall
|
||||
"split"), bekommt per Ausschlussverfahren die letzte freie Rolle,
|
||||
zusammen mit einem Hinweis. Rollen ohne passenden Nachbarn haengen am
|
||||
Bahn-Platzhalter der jeweiligen Bahn.
|
||||
"""
|
||||
kreisel_a = str(_merkmal(el.merkmale, "Kreisel A") or "").strip()
|
||||
kreisel_b = str(_merkmal(el.merkmale, "Kreisel B") or "").strip()
|
||||
roles_a = _weiche_lane_roles(kreisel_a, by_id, graph.nodes) if kreisel_a else None
|
||||
roles_b = _weiche_lane_roles(kreisel_b, by_id, graph.nodes) if kreisel_b else None
|
||||
if roles_a is None and kreisel_a:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Drehrichtung von Kreisel {kreisel_a} unbekannt - "
|
||||
f"Bahnrollen auf dieser Seite nicht bestimmbar"
|
||||
)
|
||||
if roles_b is None and kreisel_b:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Drehrichtung von Kreisel {kreisel_b} unbekannt - "
|
||||
f"Bahnrollen auf dieser Seite nicht bestimmbar"
|
||||
)
|
||||
lanes: dict[str, str | None] = {
|
||||
"eingang_a": roles_a[0] if roles_a else None,
|
||||
"ausgang_a": roles_a[1] if roles_a else None,
|
||||
"eingang_b": roles_b[0] if roles_b else None,
|
||||
"ausgang_b": roles_b[1] if roles_b else None,
|
||||
}
|
||||
|
||||
candidates = list(own.get(el.teile_id, []))
|
||||
matched: dict[str, str] = {}
|
||||
unmatched: list[str] = []
|
||||
for cand in candidates:
|
||||
cand_el = by_id.get(cand)
|
||||
lane = cand_el.assignment if cand_el else None
|
||||
role = next((r for r, lane_id in lanes.items() if lane_id and lane_id == lane), None)
|
||||
if role:
|
||||
matched[role] = cand
|
||||
else:
|
||||
unmatched.append(cand)
|
||||
|
||||
remaining_roles = [r for r in lanes if r not in matched and lanes[r] is not None]
|
||||
if len(unmatched) == 1 and len(remaining_roles) == 1:
|
||||
role = remaining_roles[0]
|
||||
matched[role] = unmatched[0]
|
||||
cand_el = by_id.get(unmatched[0])
|
||||
warnings.append(
|
||||
f"{el.describe()}: Nachbar {unmatched[0]} (Zuordnung "
|
||||
f"'{cand_el.assignment if cand_el else '-'}') passt zu keiner der "
|
||||
f"erwarteten Bahnen - durch Ausschlussverfahren als {role} angenommen"
|
||||
)
|
||||
elif unmatched:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Nachbar(n) {unmatched} nicht eindeutig einer Bahn "
|
||||
f"zuzuordnen (offene Rollen: {remaining_roles or '-'}) - nicht verbunden"
|
||||
)
|
||||
|
||||
def endpoint(role: str) -> str | None:
|
||||
if role in matched:
|
||||
return matched[role]
|
||||
lane_id = lanes[role]
|
||||
return placeholder(lane_id, "Weiche-Seite ohne passenden Fein-Nachbarn") if lane_id else None
|
||||
|
||||
add_edge(endpoint("ausgang_a"), el.teile_id, "Weiche")
|
||||
add_edge(el.teile_id, endpoint("eingang_b"), "Weiche")
|
||||
add_edge(endpoint("ausgang_b"), el.teile_id, "Weiche")
|
||||
add_edge(el.teile_id, endpoint("eingang_a"), "Weiche")
|
||||
|
||||
|
||||
def _orient_separator_links(
|
||||
fine: dict[str, Element],
|
||||
own: dict[str, list[str]],
|
||||
mentioned_by: dict[str, set[str]],
|
||||
track: TrackGraph,
|
||||
by_id: dict[str, Element],
|
||||
warnings: list[str],
|
||||
add_edge,
|
||||
suppressed_pairs: set[frozenset] = frozenset(),
|
||||
) -> None:
|
||||
"""
|
||||
Reine Separator-Separator-Kanten richten (doc/track.md Abschnitt 3).
|
||||
|
||||
Jede AS-/ES-/Weiche-Kante ist zu diesem Zeitpunkt bereits gerichtet
|
||||
(siehe build_track_graph) und "verankert" die anliegenden Separatoren.
|
||||
Von dort aus wird die Kette wie eine verkettete Liste abgearbeitet: hat
|
||||
ein Separator genau eine noch offene Separator-Separator-Verbindung und
|
||||
schon eine bekannte (Anker- oder bereits geloeste) Seite, bekommt die
|
||||
offene Seite zwangslaeufig die jeweils andere Rolle. Was am Ende keinen
|
||||
Anker erreicht, faellt auf Hoehe/Antriebsrichtung des zugeordneten
|
||||
Objekts zurueck (wie im groben Graph), sonst bleibt es unbestimmt.
|
||||
|
||||
suppressed_pairs schliesst Paare aus, die zwar gegenseitig als Nachbarn
|
||||
gelistet sind, aber beide zugleich eigener Nachbar DESSELBEN AS/ES/Weiche-
|
||||
Ankers sind (siehe build_track_graph) - das sind zwei unabhaengige Speichen
|
||||
desselben Uebergabepunkts (beide nur deshalb nah beieinander, weil sie am
|
||||
selben Knoten haengen), keine direkte Foerderverbindung zueinander. Ohne
|
||||
diesen Ausschluss wuerde z. B. in mubea_export.csv faelschlich eine Kante
|
||||
zwischen den beiden Weiche-Nachbarn 0040 (Kreisel 0001-R) und 0036 (an
|
||||
Strecke 0014, siehe _wire_weiche) gezeichnet.
|
||||
"""
|
||||
separators = {tid for tid, el in fine.items() if el.kind == "Separator"}
|
||||
adjacency: dict[str, set[str]] = {
|
||||
tid: {
|
||||
n
|
||||
for n in (set(own.get(tid, [])) | mentioned_by.get(tid, set()))
|
||||
if n in separators and frozenset((tid, n)) not in suppressed_pairs
|
||||
}
|
||||
for tid in separators
|
||||
}
|
||||
|
||||
# touches[sep]: bereits bekannte Nachbarn dieses Separators und ihre Rolle
|
||||
# ("upstream" = Material kommt von dort, "downstream" = Material geht dorthin).
|
||||
touches: dict[str, dict[str, str]] = {tid: {} for tid in separators}
|
||||
for e in track.edges:
|
||||
if e.dst in separators:
|
||||
touches[e.dst][e.src] = "upstream"
|
||||
if e.src in separators:
|
||||
touches[e.src][e.dst] = "downstream"
|
||||
|
||||
pending: dict[str, set[str]] = {
|
||||
tid: {n for n in adjacency[tid] if n not in touches[tid]} for tid in separators
|
||||
}
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for tid in separators:
|
||||
if len(pending[tid]) != 1 or len(touches[tid]) == 0:
|
||||
continue
|
||||
other = next(iter(pending[tid]))
|
||||
resolved_dir = next(iter(touches[tid].values()))
|
||||
other_dir = "upstream" if resolved_dir == "downstream" else "downstream"
|
||||
if other_dir == "downstream":
|
||||
add_edge(tid, other, "Bahn")
|
||||
else:
|
||||
add_edge(other, tid, "Bahn")
|
||||
touches[tid][other] = other_dir
|
||||
touches[other][tid] = "upstream" if other_dir == "downstream" else "downstream"
|
||||
pending[tid].discard(other)
|
||||
pending[other].discard(tid)
|
||||
changed = True
|
||||
|
||||
seen: set[frozenset] = set()
|
||||
for tid in separators:
|
||||
for other in pending[tid]:
|
||||
pair = frozenset((tid, other))
|
||||
if pair in seen:
|
||||
continue
|
||||
seen.add(pair)
|
||||
# Hoehenvergleich nur, wenn beide Separatoren DERSELBEN Zuordnung
|
||||
# angehoeren (echtes Kettenmittelstueck, z. B. 0050<->0051 auf
|
||||
# Strecke 0013). Ein Nachbar-Paar mit UNTERSCHIEDLICHER Zuordnung
|
||||
# und ganz ohne erreichbaren Anker (z. B. 0043 auf Kreisel 0010-R
|
||||
# <-> 0045 auf Strecke 0013) ist derselbe Fall wie 0036/0040 bzw.
|
||||
# 0005/0078 - ein Uebergang, der eigentlich ueber ein AS/ES/Weiche
|
||||
# laufen sollte, hier aber keins hat; ohne dessen Rollenaufloesung
|
||||
# waere ein Hoehenvergleich (der hier zudem gleiche Z liefert)
|
||||
# geraten statt abgeleitet.
|
||||
same_host = bool(fine[tid].assignment) and fine[tid].assignment == fine[other].assignment
|
||||
host = by_id.get(fine[tid].assignment.split("-")[0]) if same_host and fine[tid].assignment else None
|
||||
uphill, reason = drive_uphill(host) if host is not None else (None, "kein zugeordnetes Objekt")
|
||||
za, zb = fine[tid].z, fine[other].z
|
||||
if uphill is not None and za is not None and zb is not None and za != zb:
|
||||
src, dst = (tid, other) if (za < zb) == uphill else (other, tid)
|
||||
add_edge(src, dst, "Hoehe")
|
||||
warnings.append(
|
||||
f"Separator {tid} <-> {other}: kein Anker (AS/ES/Weiche) in der "
|
||||
f"Kette erreichbar - Richtung ueber Hoehe des zugeordneten Objekts "
|
||||
f"({reason}) angenommen"
|
||||
)
|
||||
else:
|
||||
add_edge(tid, other, "unbestimmt")
|
||||
warnings.append(
|
||||
f"Separator {tid} <-> {other}: kein Anker und keine Hoehe - "
|
||||
f"Richtung unbestimmt, beidseitig markiert"
|
||||
)
|
||||
|
||||
|
||||
def _attach_track_stations(
|
||||
elements: list[Element], graph: Graph, track: TrackGraph, placeholder, add_edge
|
||||
) -> None:
|
||||
@@ -1369,7 +1170,12 @@ def _attach_track_stations(
|
||||
|
||||
Die Zielbahn wurde bereits von _attach_stations() im groben Graph
|
||||
bestimmt (naechster Kreisel + Eingangs-/Ausgangsbahn aus der Drehrichtung,
|
||||
siehe dort) - hier nur wiederverwendet statt neu berechnet.
|
||||
siehe dort) - hier nur wiederverwendet statt neu berechnet. Eine Station,
|
||||
die bereits als Knoten in einer TrackIds-Kette liegt, wird nur noch an ihre
|
||||
Kreisel-*Bahn* (den groben Lane-/Transportknoten) gebrueckt; grobe
|
||||
Station<->Station-Kanten (ein Artefakt der Abstands-Anbindung im groben
|
||||
Graph) werden hier NICHT uebernommen, sonst entstuenden verwirrende
|
||||
Rueckkanten zwischen zwei BTMT-Punkten, die im Umlauf schon geordnet sind.
|
||||
"""
|
||||
for element in elements:
|
||||
if not element.is_station:
|
||||
@@ -1379,33 +1185,50 @@ def _attach_track_stations(
|
||||
continue
|
||||
track.nodes.setdefault(sid, TrackNode(sid, element.kind, element))
|
||||
for edge in graph.edges:
|
||||
partner = edge.dst if edge.src == sid else edge.src if edge.dst == sid else None
|
||||
if partner is None:
|
||||
continue
|
||||
partner_node = graph.nodes.get(partner)
|
||||
if partner_node is not None and partner_node.element.is_station:
|
||||
continue # Station<->Station-Kante des groben Graphen ueberspringen
|
||||
if edge.src == sid:
|
||||
add_edge(sid, placeholder(edge.dst), "Station")
|
||||
elif edge.dst == sid:
|
||||
add_edge(placeholder(edge.src), sid, "Station")
|
||||
add_edge(sid, placeholder(partner), "Station")
|
||||
else:
|
||||
add_edge(placeholder(partner), sid, "Station")
|
||||
|
||||
|
||||
def _attach_track_symbols(
|
||||
graph: Graph, track: TrackGraph, placeholder, add_edge, warnings: list[str]
|
||||
track: TrackGraph, add_edge, warnings: list[str]
|
||||
) -> None:
|
||||
"""
|
||||
Offene Bahnenden ohne BTMT-Station als generisches Symbol Eingang/Ausgang
|
||||
markieren - dieselben Kandidaten, die render_connect_ini() heute nur als
|
||||
Kommentarzeile auflistet (kein Transportobjekt liefert eine ankommende
|
||||
bzw. abgehende Kante), hier aber als echter Graph-Knoten.
|
||||
markieren - aber anhand des FEINEN Graphen (track), nicht des groben.
|
||||
|
||||
Ein echter System-Ein-/Ausgang ist ein reales Bahnelement (Separator, AS/ES,
|
||||
Station), das im --track-Graphen keinen Vorgaenger (Eingang) bzw. keinen
|
||||
Nachfolger (Ausgang) hat. Muss darum NACH allen Ketten- und Stations-Kanten
|
||||
laufen. Der grobe Graph taugt hier NICHT als Massstab: er kann eine Bahn als
|
||||
offenes Ende fuehren, obwohl ihre AS/ES-Enden im Kreisel-Umlauf laengst
|
||||
verkettet sind (die Richtung im groben Graph blieb nur 'unbestimmt') - genau
|
||||
das erzeugte frueher die vereinzelten Falsch-Eingaenge (z. B. Bahn 0001-0004
|
||||
in HundM05). Platzhalter- und Symbol-Knoten selbst zaehlen nicht als Bahn.
|
||||
"""
|
||||
incoming = {e.dst for e in graph.edges}
|
||||
outgoing = {e.src for e in graph.edges}
|
||||
incoming = {e.dst for e in track.edges}
|
||||
outgoing = {e.src for e in track.edges}
|
||||
symbol_no = 0
|
||||
for node_id in sorted(graph.nodes):
|
||||
node = graph.nodes[node_id]
|
||||
if node.element.is_station:
|
||||
for node_id in sorted(track.nodes):
|
||||
node = track.nodes[node_id]
|
||||
# Nur echte Bahnelemente betrachten: keine Platzhalter, keine schon
|
||||
# gesetzten Symbole, keine BTMT-Stationen (die sind selbst die Grenze).
|
||||
if node.placeholder or node.kind in (SYMBOL_ENTRY, SYMBOL_EXIT):
|
||||
continue
|
||||
if node.element is not None and node.element.is_station:
|
||||
continue
|
||||
if node_id not in incoming:
|
||||
symbol_no += 1
|
||||
sym_id = f"SYM_EIN_{symbol_no:02d}"
|
||||
track.nodes[sym_id] = TrackNode(sym_id, SYMBOL_ENTRY)
|
||||
add_edge(sym_id, placeholder(node_id), "Symbol")
|
||||
add_edge(sym_id, node_id, "Symbol")
|
||||
warnings.append(
|
||||
f"{node_id}: keine ankommende Verbindung und keine BTMT-Station - "
|
||||
f"im --track-Graph als Symbol {SYMBOL_ENTRY} markiert"
|
||||
@@ -1414,32 +1237,59 @@ def _attach_track_symbols(
|
||||
symbol_no += 1
|
||||
sym_id = f"SYM_AUS_{symbol_no:02d}"
|
||||
track.nodes[sym_id] = TrackNode(sym_id, SYMBOL_EXIT)
|
||||
add_edge(placeholder(node_id), sym_id, "Symbol")
|
||||
add_edge(node_id, sym_id, "Symbol")
|
||||
warnings.append(
|
||||
f"{node_id}: keine abgehende Verbindung und keine BTMT-Station - "
|
||||
f"im --track-Graph als Symbol {SYMBOL_EXIT} markiert"
|
||||
)
|
||||
|
||||
|
||||
# Kanten-Notiz je Container-Art: eine Kette innerhalb einer Strecke/
|
||||
# Gefaellestrecke ist eine Weiterfahrt auf derselben Bahn, ein Schritt im
|
||||
# Kreisel-Umlauf gehoert zur zirkulaeren Kreiselbahn.
|
||||
TRACK_NOTE_STRECKE = "Bahn"
|
||||
TRACK_NOTE_KREISEL = "Umlauf"
|
||||
|
||||
|
||||
def build_track_graph(
|
||||
elements: list[Element], by_id: dict[str, Element], graph: Graph, warnings: list[str]
|
||||
) -> TrackGraph:
|
||||
"""
|
||||
Feiner Bahnzusammenhang-Graph fuer --track (siehe doc/track.md).
|
||||
|
||||
Nutzt den bereits fertig aufgeloesten groben Graph fuer Kreisel-
|
||||
Drehrichtung (_weiche_lane_roles) und die bereits geloeste Stations-
|
||||
Anbindung (graph.edges); die Fein-Adjazenz selbst kommt aus der CSV-Spalte
|
||||
"Nachbarn" der Separator-/Ausschleus-/Einschleus-/Weiche-Zeilen (siehe
|
||||
Modul-Docstring oben). warnings wird mit dem Aufruf von build_graph()
|
||||
geteilt, damit Fein-Hinweise im selben Bericht erscheinen.
|
||||
Baut die Fein-Adjazenz direkt aus der CSV-Spalte "TrackIds" auf: jeder
|
||||
Container (Kreisel, Strecke, Gefaellestrecke) fuehrt dort die geordnete
|
||||
Liste der auf ihm liegenden Bahnelemente (Element.track_ids). Daraus wird
|
||||
je Container eine gerichtete Kette gezogen:
|
||||
|
||||
* Kreisel -> zirkulaerer Umlauf in Drehrichtung (die Liste ist bereits
|
||||
nach Winkel in DREHRICHTUNG sortiert); aufeinanderfolgende
|
||||
Eintraege werden verbunden, das Ende schliesst an den Anfang
|
||||
zurueck (der Umlauf ist tatsaechlich geschlossen).
|
||||
* Strecke/ -> lineare Kette AS -> SEP.. -> ES; die Richtung kommt aus den
|
||||
Gefaelle Kettenenden (Ausschleus = Anfang, Einschleus = Ende), nicht
|
||||
aus der Listenreihenfolge.
|
||||
|
||||
Elemente, die in zwei Containern gelistet sind (ein Ausschleus-/
|
||||
Einschleuselement steht in seiner Strecke UND im Umlauf des angedockten
|
||||
Kreisels; eine Kreisel-Kreisel-Weiche steht in beiden Kreiseln), sind
|
||||
derselbe Knoten und verketten die Container automatisch - ohne die frueher
|
||||
noetige Rueckmeldungs-/Rollen-Heuristik aus der "Nachbarn"-Spalte.
|
||||
|
||||
warnings wird mit build_graph() geteilt, damit Fein-Hinweise im selben
|
||||
Bericht erscheinen.
|
||||
"""
|
||||
track = TrackGraph(source=graph.source)
|
||||
fine = {e.teile_id: e for e in elements if e.kind in TRACK_KINDS}
|
||||
seen_edges: set[tuple[str, str]] = set()
|
||||
|
||||
def add_edge(src: str | None, dst: str | None, note: str = "") -> None:
|
||||
if src is not None and dst is not None:
|
||||
track.edges.append(TrackEdge(src, dst, note))
|
||||
# Dieselbe gerichtete Kante kann aus zwei Containern kommen (ein Paar, das
|
||||
# z. B. im Kreisel-Umlauf UND am Ende einer andockenden Strecke direkt
|
||||
# aufeinanderfolgt) - nur einmal ziehen, mit der zuerst gesehenen Notiz.
|
||||
if src is None or dst is None or src == dst or (src, dst) in seen_edges:
|
||||
return
|
||||
seen_edges.add((src, dst))
|
||||
track.edges.append(TrackEdge(src, dst, note))
|
||||
|
||||
def placeholder(host_id: str | None, reason: str = "") -> str | None:
|
||||
if not host_id:
|
||||
@@ -1451,123 +1301,92 @@ def build_track_graph(
|
||||
)
|
||||
return host_id
|
||||
|
||||
for el in fine.values():
|
||||
def ensure_node(tid: str) -> str | None:
|
||||
"""Knoten fuer ein in TrackIds gelistetes Element sicherstellen."""
|
||||
if tid in track.nodes:
|
||||
return tid
|
||||
el = by_id.get(tid)
|
||||
if el is None:
|
||||
warnings.append(f"TrackIds nennt unbekanntes Element {tid} - uebersprungen")
|
||||
return None
|
||||
track.nodes[tid] = TrackNode(tid, el.kind, el)
|
||||
return tid
|
||||
|
||||
# 1. Strecke/Gefaellestrecke: lineare Kette AS -> SEP.. -> ES.
|
||||
for el in elements:
|
||||
if el.kind not in TRANSPORT_KINDS or not el.track_ids:
|
||||
continue
|
||||
chain = list(el.track_ids)
|
||||
# Richtung an den Kettenenden ausrichten: Einschleus vorne bedeutet, die
|
||||
# Liste laeuft entgegen dem Fluss - umdrehen. Ausschleus am Ende ebenso.
|
||||
first_el, last_el = by_id.get(chain[0]), by_id.get(chain[-1])
|
||||
first_kind = first_el.kind if first_el else ""
|
||||
last_kind = last_el.kind if last_el else ""
|
||||
if first_kind == "Einschleus" or last_kind == "Ausschleus":
|
||||
chain = list(reversed(chain))
|
||||
prev = None
|
||||
for tid in chain:
|
||||
node = ensure_node(tid)
|
||||
if node is None:
|
||||
continue
|
||||
if prev is not None:
|
||||
add_edge(prev, node, TRACK_NOTE_STRECKE)
|
||||
prev = node
|
||||
|
||||
# 2. Kreisel: zirkulaerer Umlauf in Drehrichtung (Liste bereits sortiert).
|
||||
# Ohne bekannte Drehrichtung ist die Umlaufrichtung nicht gesichert - dann
|
||||
# wird die Kette offen (ohne Rueckschluss) gezogen und gemeldet.
|
||||
for el in elements:
|
||||
if not el.is_circle or not el.track_ids:
|
||||
continue
|
||||
chain = [n for tid in el.track_ids if (n := ensure_node(tid)) is not None]
|
||||
if len(chain) < 2:
|
||||
continue
|
||||
for a, b in zip(chain, chain[1:]):
|
||||
add_edge(a, b, TRACK_NOTE_KREISEL)
|
||||
if el.rotation in (ROTATION_CW, ROTATION_CCW):
|
||||
add_edge(chain[-1], chain[0], TRACK_NOTE_KREISEL) # Umlauf schliessen
|
||||
else:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Drehrichtung '{el.rotation or '-'}' unbekannt - "
|
||||
f"Kreisel-Umlauf im --track-Graph nicht geschlossen"
|
||||
)
|
||||
|
||||
# 3. Verwaiste Bahnelemente (in keiner TrackIds-Liste eines Containers, z. B.
|
||||
# ein frei platzierter Separator ohne Kette): an ihren Zuordnung-
|
||||
# Platzhalter haengen statt sie unverbunden zu lassen.
|
||||
listed = {tid for el in elements for tid in el.track_ids}
|
||||
for el in elements:
|
||||
if el.kind not in TRACK_KINDS or el.teile_id in listed:
|
||||
continue
|
||||
track.nodes.setdefault(el.teile_id, TrackNode(el.teile_id, el.kind, el))
|
||||
|
||||
# Eigene Nachbarn je Feinelement (nur Feinelemente behalten, siehe
|
||||
# TRACK_KINDS) plus die Umkehr-Sicht (wer nennt mich als Nachbar) -
|
||||
# zusammen die vereinigte Fein-Adjazenz (doc/track.md Abschnitt 1).
|
||||
own: dict[str, list[str]] = {
|
||||
tid: [b for b, _lane in el.neighbours if b in fine] for tid, el in fine.items()
|
||||
}
|
||||
mentioned_by: dict[str, set[str]] = {tid: set() for tid in fine}
|
||||
for tid, targets in own.items():
|
||||
for target in targets:
|
||||
mentioned_by[target].add(tid)
|
||||
|
||||
# Ausschleus/Einschleus: eigener Nachbar = Foerderer-/Streckenseite,
|
||||
# Rueckmeldung = Kreisel-Seite (doc/track.md Abschnitt 1-2). Eine
|
||||
# Rueckmeldung wird nur akzeptiert, wenn ihre eigene Zuordnung auch
|
||||
# tatsaechlich zur erwarteten Bahn (_transfer_kreisel_lane) passt - sonst
|
||||
# ist es wie beim Weiche-Fall 0036/0040 nur eine raeumlich nahe, aber
|
||||
# fachlich andere Kette (z. B. 0005 auf Strecke 0014, das per Rueckmeldung
|
||||
# faelschlich als Kreisel-Seite mehrerer Storage-Bank-Einschleuselemente
|
||||
# auf 0002-R erscheint), keine echte Weiterfahrt auf derselben Bahn.
|
||||
for el in fine.values():
|
||||
if el.kind not in ("Ausschleus", "Einschleus"):
|
||||
continue
|
||||
strecke_side = list(own.get(el.teile_id, []))
|
||||
if not strecke_side:
|
||||
warnings.append(f"{el.describe()}: keine Foerderer-/Streckenseite in eigenen Nachbarn gefunden")
|
||||
elif len(strecke_side) > 1:
|
||||
warnings.append(f"{el.describe()}: mehrere Streckenseiten-Nachbarn {strecke_side} - alle verbunden")
|
||||
|
||||
expected_lane = _transfer_kreisel_lane(el, by_id, graph.nodes)
|
||||
raw_kreisel_side = sorted(mentioned_by.get(el.teile_id, set()) - set(strecke_side))
|
||||
kreisel_side: list[str] = []
|
||||
for cand in raw_kreisel_side:
|
||||
cand_el = by_id.get(cand)
|
||||
if expected_lane and cand_el is not None and cand_el.assignment == expected_lane:
|
||||
kreisel_side.append(cand)
|
||||
else:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Rueckmeldung von {cand} (Zuordnung "
|
||||
f"'{cand_el.assignment if cand_el else '-'}') passt nicht zur erwarteten "
|
||||
f"Bahn {expected_lane or '?'} - als Kreisel-seitiger Nachbar verworfen"
|
||||
)
|
||||
if not kreisel_side:
|
||||
if expected_lane:
|
||||
kreisel_side = [placeholder(expected_lane, "keine passende Rueckmeldung eines Fein-Elements gefunden")]
|
||||
else:
|
||||
warnings.append(f"{el.describe()}: Kreisel-Bahn nicht aufloesbar - keine Kreisel-seitige Kante")
|
||||
elif len(kreisel_side) > 1:
|
||||
warnings.append(f"{el.describe()}: mehrere Kreisel-seitige Nachbarn {kreisel_side} - alle verbunden")
|
||||
|
||||
if el.kind == "Ausschleus":
|
||||
for k in kreisel_side:
|
||||
add_edge(k, el.teile_id, "Bahn")
|
||||
for s in strecke_side:
|
||||
add_edge(el.teile_id, s, "Zweig")
|
||||
else: # Einschleus
|
||||
for s in strecke_side:
|
||||
add_edge(s, el.teile_id, "Zweig")
|
||||
for k in kreisel_side:
|
||||
add_edge(el.teile_id, k, "Bahn")
|
||||
|
||||
# Weiche: 4-Wege-Kreuzung zwischen zwei Kreiseln (doc/track.md Abschnitt 2).
|
||||
for el in fine.values():
|
||||
if el.kind == "Weiche":
|
||||
_wire_weiche(el, by_id, graph, own, track, warnings, placeholder, add_edge)
|
||||
|
||||
# Zwei eigene Nachbarn DESSELBEN AS/ES/Weiche-Ankers sind zwei unabhaengige
|
||||
# Speichen desselben Uebergabepunkts, keine direkte Kette zueinander (siehe
|
||||
# _orient_separator_links-Docstring) - von der Separator-Separator-
|
||||
# Orientierung ausschliessen, bevor sie dort faelschlich verkettet werden.
|
||||
suppressed_pairs: set[frozenset] = set()
|
||||
for tid, el in fine.items():
|
||||
if el.kind not in ("Ausschleus", "Einschleus", "Weiche"):
|
||||
continue
|
||||
candidates = own.get(tid, [])
|
||||
for i, a in enumerate(candidates):
|
||||
for b in candidates[i + 1:]:
|
||||
suppressed_pairs.add(frozenset((a, b)))
|
||||
|
||||
# Reine Separator-Separator-Verbindungen orientieren (doc/track.md Abschnitt 3).
|
||||
_orient_separator_links(
|
||||
fine, own, mentioned_by, track, by_id, warnings, add_edge, suppressed_pairs
|
||||
)
|
||||
|
||||
# Verwaiste Separatoren (keine Nachbarn im Export, z. B. 0037/0038 in
|
||||
# mubea_export.csv) an ihren Zuordnung-Platzhalter haengen statt sie
|
||||
# unverbunden zu lassen - Reihenfolge dort bewusst offen.
|
||||
for el in fine.values():
|
||||
if el.kind != "Separator" or own.get(el.teile_id) or mentioned_by.get(el.teile_id):
|
||||
continue
|
||||
host = el.assignment if el.assignment and el.assignment != UNASSIGNED else None
|
||||
ph = placeholder(host, "Separator ohne Nachbarn im Export")
|
||||
ph = placeholder(host, "nicht in einer TrackIds-Kette enthalten")
|
||||
if ph:
|
||||
add_edge(ph, el.teile_id, "unbestimmt")
|
||||
warnings.append(
|
||||
f"{el.describe()}: keine Nachbarn zu anderen Bahnelementen im Export - "
|
||||
f"{el.describe()}: in keiner TrackIds-Kette enthalten - "
|
||||
f"Reihenfolge auf {host} ungeklaert"
|
||||
)
|
||||
else:
|
||||
warnings.append(f"{el.describe()}: keine Nachbarn und keine Zuordnung - im --track-Graph isoliert")
|
||||
warnings.append(
|
||||
f"{el.describe()}: in keiner TrackIds-Kette und ohne Zuordnung - "
|
||||
f"im --track-Graph isoliert"
|
||||
)
|
||||
|
||||
# Systemgrenzen: BTMT-Stationen (wiederverwendet) bzw. generisches
|
||||
# Eingang/Ausgang-Symbol an offenen Bahnenden ohne Station.
|
||||
# 4. Systemgrenzen: BTMT-Stationen (wiederverwendet) bzw. generisches
|
||||
# Eingang/Ausgang-Symbol an offenen Bahnenden ohne Station.
|
||||
_attach_track_stations(elements, graph, track, placeholder, add_edge)
|
||||
_attach_track_symbols(graph, track, placeholder, add_edge, warnings)
|
||||
_attach_track_symbols(track, add_edge, warnings)
|
||||
|
||||
# Letzte Sicherung: ein Fein-Element, das nach alledem keine einzige Kante
|
||||
# hat (z. B. weil ein Weiche-Nachbar zu keiner Rolle passte, siehe
|
||||
# _wire_weiche, UND seine einzige sonstige Nachbarschaft als Speiche
|
||||
# desselben Ankers ausgeschlossen wurde), sichtbar melden statt es
|
||||
# stillschweigend unverbunden im Graph stehen zu lassen.
|
||||
# Letzte Sicherung: ein gelistetes Element ohne jede Kante sichtbar melden.
|
||||
touched = {e.src for e in track.edges} | {e.dst for e in track.edges}
|
||||
for tid, el in fine.items():
|
||||
if tid not in touched:
|
||||
warnings.append(f"{el.describe()}: im --track-Graph ohne jede Kante - Zuordnung/Nachbarn widerspruechlich")
|
||||
for tid in sorted(listed):
|
||||
el = by_id.get(tid)
|
||||
if el is not None and el.kind in TRACK_KINDS and tid not in touched:
|
||||
warnings.append(
|
||||
f"{el.describe()}: im --track-Graph ohne jede Kante - TrackIds widerspruechlich"
|
||||
)
|
||||
|
||||
return track
|
||||
|
||||
@@ -1880,12 +1699,12 @@ TRACK_NODE_STYLES: dict[str, dict[str, str]] = {
|
||||
}
|
||||
|
||||
TRACK_EDGE_ATTRS: dict[str, str] = {
|
||||
# Schritte innerhalb der TrackIds-Kette eines Containers (build_track_graph):
|
||||
"Bahn": '[color="#2f5597", tooltip="Weiterfahrt auf derselben Bahn (Strecke/Gefaelle)"]',
|
||||
"Umlauf": '[color="#8ea9db", tooltip="Kreisel-Umlauf (Reihenfolge in Drehrichtung)"]',
|
||||
"unbestimmt": '[color="#bf8f00", style=dashed, dir=both, tooltip="Richtung unbestimmt"]',
|
||||
"Zweig": '[color="#e69138", tooltip="Verzweigung/Zusammenfuehrung an AS/ES"]',
|
||||
"Weiche": '[color="#a64d79", penwidth=1.6, tooltip="Weiche (Kreuzung zweier Kreisel)"]',
|
||||
"Symbol": '[color="#999999", style=dotted, tooltip="offenes Bahnende ohne BTMT-Station"]',
|
||||
"Station": '[color="#338833", tooltip="Anbindung BTMT Be-/Entladung"]',
|
||||
"Hoehe": '[color="#548235", style=dashed, tooltip="Richtung ueber Hoehe angenommen"]',
|
||||
}
|
||||
|
||||
|
||||
@@ -1944,14 +1763,16 @@ def render_track_dot(track: TrackGraph) -> str:
|
||||
add(' legende [shape=plaintext, style="", fillcolor="none", label=<')
|
||||
add('<table border="0" cellborder="0" cellspacing="2" cellpadding="2">')
|
||||
add('<tr><td align="left">Raute</td><td align="left">Weiche (Kreuzung zweier Kreisel)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#e69138">orange</font></td>'
|
||||
'<td align="left">Zweig an Ausschleus-/Einschleuselement</td></tr>')
|
||||
add('<tr><td align="left"><font color="#2f5597">blau</font></td>'
|
||||
'<td align="left">Weiterfahrt auf derselben Bahn (Strecke/Gefaelle-Kette)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#8ea9db">hellblau</font></td>'
|
||||
'<td align="left">Kreisel-Umlauf (Reihenfolge in Drehrichtung)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#338833">gruen</font></td>'
|
||||
'<td align="left">Anbindung BTMT Be-/Entladung</td></tr>')
|
||||
add('<tr><td align="left">gestrichelter Rand</td>'
|
||||
'<td align="left">Bahn-Platzhalter - Reihenfolge/Position offen (Teil B)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#548235">gestrichelt gruen</font></td>'
|
||||
'<td align="left">Richtung ueber Hoehe angenommen (kein Anker erreichbar)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
||||
'<td align="left">Richtung unbestimmt</td></tr>')
|
||||
'<td align="left">Richtung unbestimmt (Element nicht in einer TrackIds-Kette)</td></tr>')
|
||||
add("</table>>];")
|
||||
add(" }")
|
||||
add("}")
|
||||
|
||||
Reference in New Issue
Block a user