BTMT Beladung/Entladung als Ein-/Ausgang des Materialfluss-Graphen erkannt
export_graph.py behandelte BTMT-Stationen bisher wie ein generisches Durchlaufelement mit zwei Achs-Enden - dadurch entstanden mehrere, teils widerspruechliche Kanten und die Rolle blieb "Element" statt "Eingang"/"Ausgang". BTMT hat aber physisch nur EINEN Anschluss zum Foerdersystem (sitzt als dritter Zweig einer Weiche auf der Kreisel- Durchgangsbahn, der Kreisel bleibt dabei ein einzelner Verteiler-Knoten): Beladung speist fest Material ein (kein Vorgaenger im Fluss), Entladung nimmt es fest raus (kein Nachfolger). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+60
-3
@@ -9,7 +9,8 @@ Aufruf:
|
|||||||
Erzeugt neben <basispfad>.json (vollstaendiges Graph-Modell) auch
|
Erzeugt neben <basispfad>.json (vollstaendiges Graph-Modell) auch
|
||||||
<basispfad>.dot (GraphViz) und <basispfad>.mmd (Mermaid) zur Visualisierung.
|
<basispfad>.dot (GraphViz) und <basispfad>.mmd (Mermaid) zur Visualisierung.
|
||||||
|
|
||||||
KNOTEN = ein Anlagenelement je CSV-Zeile (Kreisel, Gefaellestrecke, Strecke).
|
KNOTEN = ein Anlagenelement je CSV-Zeile (Kreisel, Gefaellestrecke, Strecke,
|
||||||
|
BTMT Beladung/Entladung als Ein-/Ausgang des Graphen).
|
||||||
KANTEN = eine Materialuebergabe zwischen zwei Elementen.
|
KANTEN = eine Materialuebergabe zwischen zwei Elementen.
|
||||||
|
|
||||||
Warum nicht die CSV-Spalte "Nachbarn"?
|
Warum nicht die CSV-Spalte "Nachbarn"?
|
||||||
@@ -42,9 +43,25 @@ Lisp/export.lsp), wird die Richtung heuristisch abgeleitet:
|
|||||||
"Ab" = abwaerts) gegen die Partnerhoehen.
|
"Ab" = abwaerts) gegen die Partnerhoehen.
|
||||||
* Kreisel: Verteilknoten. Richtung ergibt sich aus den angeschlossenen
|
* Kreisel: Verteilknoten. Richtung ergibt sich aus den angeschlossenen
|
||||||
Elementen, nicht aus dem Kreisel selbst.
|
Elementen, nicht aus dem Kreisel selbst.
|
||||||
|
* BTMT Beladung/Entladung: FEST gerichtet (Beladung -> Partner, Partner ->
|
||||||
|
Entladung), unabhaengig von Hoehe/Antrieb - siehe naechster Abschnitt.
|
||||||
|
|
||||||
Jede Kante traegt daher ein Feld "richtung_quelle": "ks" (exakt, aus K1/K2),
|
Jede Kante traegt daher ein Feld "richtung_quelle": "ks" (exakt, aus K1/K2),
|
||||||
"gefaelle", "antrieb" oder "unbestimmt".
|
"gefaelle", "antrieb", "btmt" oder "unbestimmt".
|
||||||
|
|
||||||
|
Ein-/Ausgaenge des Graphen: BTMT Beladung/Entladung
|
||||||
|
----------------------------------------------------
|
||||||
|
BTMT Beladung/Entladung sind die Ein- bzw. Ausgaenge des gesamten
|
||||||
|
Materialfluss-Graphen - eine Beladestation speist NEUES Material ein (kein
|
||||||
|
Vorgaenger im Fluss), eine Entladestation nimmt es endgueltig raus (kein
|
||||||
|
Nachfolger). Physisch sitzen sie AUF der Durchgangsbahn eines Kreisels
|
||||||
|
(der Kreisel laeuft an dieser Stelle durch UND die BTMT-Station zweigt dort
|
||||||
|
zusaetzlich ab/ein - wie eine Weiche mit einem fixen dritten Zweig). Der
|
||||||
|
Kreisel bleibt im Graphmodell trotzdem EIN Knoten (Rolle "Verteiler", keine
|
||||||
|
Aufteilung in Segmente); BTMT wird als eigener Knoten mit GENAU EINEM
|
||||||
|
Anschlusspunkt (Elementmitte statt zwei Achs-Enden, siehe _enden) und einer
|
||||||
|
fest gerichteten Kante zum Kreisel/Partner dargestellt (Rolle "Eingang"
|
||||||
|
bzw. "Ausgang", siehe _rolle/BTMT_ARTEN).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
@@ -68,6 +85,15 @@ KREISEL_ARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"}
|
|||||||
GEFAELLE_ARTEN = {"ILS 2.0 Gefaellestrecke"}
|
GEFAELLE_ARTEN = {"ILS 2.0 Gefaellestrecke"}
|
||||||
STRECKEN_ARTEN = {"ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"}
|
STRECKEN_ARTEN = {"ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"}
|
||||||
|
|
||||||
|
# Ein-/Ausgaenge des Materialfluss-Graphen: BTMT Beladung speist Material
|
||||||
|
# EIN (Graph-Quelle, kein Vorgaenger im Fluss), BTMT Entladung nimmt es
|
||||||
|
# AUS (Graph-Senke, kein Nachfolger). Anders als Kreisel/Strecken haben
|
||||||
|
# beide nur EINEN Anschluss zum Foerdersystem, keine zwei Enden einer
|
||||||
|
# Durchlaufachse - siehe _enden().
|
||||||
|
BTMT_BELADUNG_ARTEN = {"ILS 2.0 BTMT Beladung"}
|
||||||
|
BTMT_ENTLADUNG_ARTEN = {"ILS 2.0 BTMT Entladung"}
|
||||||
|
BTMT_ARTEN = BTMT_BELADUNG_ARTEN | BTMT_ENTLADUNG_ARTEN
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Dekodierung der Base64-Koordinatenstrings
|
# Dekodierung der Base64-Koordinatenstrings
|
||||||
@@ -193,7 +219,17 @@ def _footprint(element):
|
|||||||
|
|
||||||
|
|
||||||
def _enden(element):
|
def _enden(element):
|
||||||
"""Die beiden Enden der Laengsachse als Anschlusspunkte im Grundriss."""
|
"""Anschlusspunkte eines Elements im Grundriss.
|
||||||
|
|
||||||
|
BTMT Beladung/Entladung haben nur EINEN Anschluss zum Foerdersystem
|
||||||
|
(Elementmitte) - anders als eine Durchlaufstrecke keine zwei Enden
|
||||||
|
einer Laengsachse. Mit der generischen Zwei-Enden-Logik wuerden sie
|
||||||
|
faelschlich als Durchlaufelement mit zwei Uebergabestellen behandelt
|
||||||
|
(siehe derive_flow/BTMT_ARTEN).
|
||||||
|
"""
|
||||||
|
if element["teileart"] in BTMT_ARTEN:
|
||||||
|
m = element["mitte"]
|
||||||
|
return {"a": (m["x"], m["y"])}
|
||||||
minx, maxx, miny, maxy = element["footprint"]
|
minx, maxx, miny, maxy = element["footprint"]
|
||||||
m = element["mitte"]
|
m = element["mitte"]
|
||||||
if element["achse"] == "x":
|
if element["achse"] == "x":
|
||||||
@@ -262,6 +298,12 @@ def derive_flow(elemente, anschluesse):
|
|||||||
* Foerderer mit Antriebfahrtrichtung "Auf" -> vom tieferen zum hoeheren,
|
* Foerderer mit Antriebfahrtrichtung "Auf" -> vom tieferen zum hoeheren,
|
||||||
"Ab" -> umgekehrt.
|
"Ab" -> umgekehrt.
|
||||||
Liegen K1/K2 vor, gewinnt diese exakte Angabe.
|
Liegen K1/K2 vor, gewinnt diese exakte Angabe.
|
||||||
|
|
||||||
|
BTMT Beladung/Entladung sind die Ein-/Ausgaenge des gesamten Graphen
|
||||||
|
(siehe BTMT_ARTEN/_enden): ihr einziger Anschluss wird FEST gerichtet
|
||||||
|
(Beladung -> Partner, Partner -> Entladung), unabhaengig von Hoehe/
|
||||||
|
Antriebfahrtrichtung - eine Beladestation speist immer ein, eine
|
||||||
|
Entladestation nimmt immer aus, nie umgekehrt.
|
||||||
"""
|
"""
|
||||||
# Anschluesse je Streckenelement sammeln
|
# Anschluesse je Streckenelement sammeln
|
||||||
je_element = {}
|
je_element = {}
|
||||||
@@ -273,6 +315,15 @@ def derive_flow(elemente, anschluesse):
|
|||||||
el = elemente[idx]
|
el = elemente[idx]
|
||||||
art = el["teileart"]
|
art = el["teileart"]
|
||||||
|
|
||||||
|
if art in BTMT_BELADUNG_ARTEN:
|
||||||
|
for anschluss in liste:
|
||||||
|
kanten.append(_kante(idx, anschluss["b"], "btmt", anschluss["abstand"]))
|
||||||
|
continue
|
||||||
|
if art in BTMT_ENTLADUNG_ARTEN:
|
||||||
|
for anschluss in liste:
|
||||||
|
kanten.append(_kante(anschluss["b"], idx, "btmt", anschluss["abstand"]))
|
||||||
|
continue
|
||||||
|
|
||||||
# Exakte Richtung, sobald der Export K1/K2 liefert
|
# Exakte Richtung, sobald der Export K1/K2 liefert
|
||||||
if el["ks_ein"] and el["ks_aus"]:
|
if el["ks_ein"] and el["ks_aus"]:
|
||||||
kanten.extend(_kanten_aus_ks(elemente, idx, liste))
|
kanten.extend(_kanten_aus_ks(elemente, idx, liste))
|
||||||
@@ -433,6 +484,10 @@ def _rolle(el):
|
|||||||
return "Schwerkraftstrecke"
|
return "Schwerkraftstrecke"
|
||||||
if el["teileart"] in STRECKEN_ARTEN:
|
if el["teileart"] in STRECKEN_ARTEN:
|
||||||
return "Foerderer"
|
return "Foerderer"
|
||||||
|
if el["teileart"] in BTMT_BELADUNG_ARTEN:
|
||||||
|
return "Eingang"
|
||||||
|
if el["teileart"] in BTMT_ENTLADUNG_ARTEN:
|
||||||
|
return "Ausgang"
|
||||||
return "Element"
|
return "Element"
|
||||||
|
|
||||||
|
|
||||||
@@ -440,6 +495,8 @@ STIL = {
|
|||||||
"Verteiler": ("box3d", "#1f6feb"),
|
"Verteiler": ("box3d", "#1f6feb"),
|
||||||
"Schwerkraftstrecke": ("box", "#8250df"),
|
"Schwerkraftstrecke": ("box", "#8250df"),
|
||||||
"Foerderer": ("box", "#1a7f37"),
|
"Foerderer": ("box", "#1a7f37"),
|
||||||
|
"Eingang": ("invhouse", "#bf8700"),
|
||||||
|
"Ausgang": ("house", "#bf8700"),
|
||||||
"Element": ("ellipse", "#57606a"),
|
"Element": ("ellipse", "#57606a"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_export_graph.py - Materialfluss-Graph (lib/export_graph.py).
|
||||||
|
Laeuft komplett ohne BricsCAD (reine Python-Logik).
|
||||||
|
|
||||||
|
Regressionstest fuer einen realen Befund: BTMT Beladung/Entladung sind die
|
||||||
|
Ein-/Ausgaenge des gesamten Materialfluss-Graphen (Beladung speist neu ein,
|
||||||
|
Entladung nimmt endgueltig raus), wurden aber bisher wie ein generisches
|
||||||
|
Durchlaufelement mit ZWEI Enden behandelt (_enden nahm die laengere
|
||||||
|
Boundingbox-Seite als Achse) - dadurch bekamen sie mehrere, teils
|
||||||
|
widerspruechliche Kanten und die Rolle "Element" statt "Eingang"/"Ausgang".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
|
||||||
|
|
||||||
|
from export_graph import load_elements, build_graph # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _write_csv(path, rows):
|
||||||
|
header = ["Elementnummer", "TeileArt", "TeileId", "Bezeichnung", "Planquadrat",
|
||||||
|
"Anzahl", "Position", "Boundingbox", "Insertpoint", "K1", "K2", "K3",
|
||||||
|
"K4", "Nachbarn", "TrackIds", "Fehler", "Warnungen", "Merkmale"]
|
||||||
|
with open(path, "w", encoding="utf-8", newline="") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=header, delimiter=";")
|
||||||
|
writer.writeheader()
|
||||||
|
for row in rows:
|
||||||
|
full = {k: "" for k in header}
|
||||||
|
full.update(row)
|
||||||
|
writer.writerow(full)
|
||||||
|
|
||||||
|
|
||||||
|
def _zeile(teileart, teileid, x, y, z, dx, dy, dz, bezeichnung=""):
|
||||||
|
return {
|
||||||
|
"TeileArt": teileart,
|
||||||
|
"TeileId": teileid,
|
||||||
|
"Bezeichnung": bezeichnung or teileart,
|
||||||
|
"Position": f"{x:.2f}, {y:.2f}, {z:.2f}",
|
||||||
|
"Boundingbox": f"{dx:.2f}, {dy:.2f}, {dz:.2f}",
|
||||||
|
"Merkmale": "{}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestBtmtEinAusgangDesGraphen:
|
||||||
|
"""Kreisel (5000x800mm, Achse entlang X) mit einer BTMT-Beladung UND
|
||||||
|
einer BTMT-Entladung, beide mittig auf der Kreiselflaeche platziert
|
||||||
|
(wie physisch: sie sitzen AUF der Kreiselbahn, keine eigenstaendige
|
||||||
|
Durchlaufstrecke)."""
|
||||||
|
|
||||||
|
def _csv_pfad(self, tmp_path):
|
||||||
|
rows = [
|
||||||
|
_zeile("ILS 2.0 Kreisel", "0001", 2500.0, 0.0, 1500.0, 5000.0, 800.0, 1.0),
|
||||||
|
_zeile("ILS 2.0 BTMT Beladung", "0002", 1000.0, 0.0, 1500.0, 400.0, 650.0, 300.0),
|
||||||
|
_zeile("ILS 2.0 BTMT Entladung", "0003", 4000.0, 0.0, 1500.0, 900.0, 1450.0, 450.0),
|
||||||
|
]
|
||||||
|
pfad = os.path.join(tmp_path, "graph_test.csv")
|
||||||
|
_write_csv(pfad, rows)
|
||||||
|
return pfad
|
||||||
|
|
||||||
|
def test_beladung_hat_nur_einen_anschluss(self, tmp_path):
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
beladung = next(e for e in elemente if e["teileart"] == "ILS 2.0 BTMT Beladung")
|
||||||
|
assert list(beladung["enden"].keys()) == ["a"], (
|
||||||
|
"BTMT Beladung darf nur EINEN Anschlusspunkt haben (Elementmitte), "
|
||||||
|
f"hat aber {list(beladung['enden'].keys())}")
|
||||||
|
|
||||||
|
def test_entladung_hat_nur_einen_anschluss(self, tmp_path):
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
entladung = next(e for e in elemente if e["teileart"] == "ILS 2.0 BTMT Entladung")
|
||||||
|
assert list(entladung["enden"].keys()) == ["a"]
|
||||||
|
|
||||||
|
def test_beladung_ist_reiner_eingang(self, tmp_path):
|
||||||
|
"""Beladung speist Material EIN - kein Vorgaenger im Fluss (grad_ein=0),
|
||||||
|
genau ein Nachfolger (grad_aus=1), Rolle "Eingang"."""
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
graph = build_graph(elemente)
|
||||||
|
knoten = next(n for n in graph["knoten"] if n["teileart"] == "ILS 2.0 BTMT Beladung")
|
||||||
|
assert knoten["rolle"] == "Eingang"
|
||||||
|
assert knoten["grad_ein"] == 0, "Beladung darf keinen Vorgaenger im Fluss haben"
|
||||||
|
assert knoten["grad_aus"] == 1, "Beladung muss genau einen Nachfolger haben"
|
||||||
|
|
||||||
|
def test_entladung_ist_reiner_ausgang(self, tmp_path):
|
||||||
|
"""Entladung nimmt Material AUS - kein Nachfolger im Fluss (grad_aus=0),
|
||||||
|
genau ein Vorgaenger (grad_ein=1), Rolle "Ausgang"."""
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
graph = build_graph(elemente)
|
||||||
|
knoten = next(n for n in graph["knoten"] if n["teileart"] == "ILS 2.0 BTMT Entladung")
|
||||||
|
assert knoten["rolle"] == "Ausgang"
|
||||||
|
assert knoten["grad_ein"] == 1, "Entladung muss genau einen Vorgaenger haben"
|
||||||
|
assert knoten["grad_aus"] == 0, "Entladung darf keinen Nachfolger im Fluss haben"
|
||||||
|
|
||||||
|
def test_kante_beladung_zeigt_zum_kreisel(self, tmp_path):
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
graph = build_graph(elemente)
|
||||||
|
beladung_idx = next(i for i, e in enumerate(elemente)
|
||||||
|
if e["teileart"] == "ILS 2.0 BTMT Beladung")
|
||||||
|
kreisel_idx = next(i for i, e in enumerate(elemente)
|
||||||
|
if e["teileart"] == "ILS 2.0 Kreisel")
|
||||||
|
kante = next(k for k in graph["kanten"] if beladung_idx in (k["von"], k["nach"]))
|
||||||
|
assert kante["von"] == beladung_idx and kante["nach"] == kreisel_idx
|
||||||
|
assert kante["gerichtet"] is True
|
||||||
|
assert kante["richtung_quelle"] == "btmt"
|
||||||
|
|
||||||
|
def test_kante_entladung_kommt_vom_kreisel(self, tmp_path):
|
||||||
|
elemente = load_elements(self._csv_pfad(tmp_path))
|
||||||
|
graph = build_graph(elemente)
|
||||||
|
entladung_idx = next(i for i, e in enumerate(elemente)
|
||||||
|
if e["teileart"] == "ILS 2.0 BTMT Entladung")
|
||||||
|
kreisel_idx = next(i for i, e in enumerate(elemente)
|
||||||
|
if e["teileart"] == "ILS 2.0 Kreisel")
|
||||||
|
kante = next(k for k in graph["kanten"] if entladung_idx in (k["von"], k["nach"]))
|
||||||
|
assert kante["von"] == kreisel_idx and kante["nach"] == entladung_idx
|
||||||
|
assert kante["gerichtet"] is True
|
||||||
|
assert kante["richtung_quelle"] == "btmt"
|
||||||
Reference in New Issue
Block a user