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:
2026-09-10 15:29:08 +02:00
parent 47724fcfe2
commit 90bfbd89b6
2 changed files with 178 additions and 3 deletions
+118
View File
@@ -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"