[REFACTOR] Stufe 2: VF-Eingang/-Ausgang getrennt + Regressionsnetz fuer die Geometrie

Der CAD-Lauf belegt die Schritte 0-4 jetzt auch EMPIRISCH:
results/test_hundm05.dxf entstand nach dem Umbau, das Ergebnis-JSON von
11:18 davor - fuer alle 5 Ketten sind ALLE 37 Attribute identisch,
inklusive der reihenfolgeabhaengigen Komma-Listen L_VF_m, L_GF_m, GF_WINKEL
und ANTRIEBFAHRTRICHTUNG. TEST_VF_SPEC: 28 PASS / 0 FAIL.
results/test_linienzug.dxf zeigt eine vollstaendige Kette (AS+ES, Motor,
Umlenkung, 2 Vario-Kurven, 22 Bausteine, HOEHE_VON = HOEHE_BIS = 4500 bei
DELTA_H 0).

Schritt 5a/5b - vfl-vf-eingang-bauen und vfl-vf-ausgang-bauen: beide Bloecke
(GF1 + Einlauf-Separator + Umlenkstation bzw. Motorstation + GF2) enthalten
keine Frage und sind zusammenhaengend, der Umzug ist wortwoertlich
(Zeilenvergleich: 0 Zeilen entfernt, nur zwei Koepfe, zwei Aufrufe und zwei
"frame)" neu). Die Auswahl gf2-eff (ziel-gf2 im Kettenende-Modus, sonst
L_GF2-bau) bleibt BEWUSST an der Aufrufstelle - genau dort entsteht sonst
still die falsche GF2 hinter dem Motor. Der Separator-Zaehler und die
vfl-acc-*-Aufrufe bleiben an derselben Stelle in derselben Reihenfolge.

Regressionsnetz, damit die restlichen Schritte nicht blind gemacht werden
muessen:
- lib/dxf_vf_abbild.py (neu): liest eine Testzeichnung streamend und bildet
  je Kette Attribute, Bausteinzusammensetzung und Einfuegepunkt ab. Ketten
  werden ueber den EINFUEGEPUNKT der Spec zugeordnet, nicht ueber den
  Blocknamen - die VF-Nummer beginnt in jeder neuen Zeichnung wieder bei 1.
  ID und Bezeichnung sind aus dem Vergleich heraus (pro Zeichnung neu).
  --referenz vergleicht (Exit 1 bei Abweichung), --schreibe-referenz frischt
  die Referenz nach einem GEPRUEFTEN Lauf auf.
- tests/reference/hundm05_attribute.json (neu): die Referenz, erzeugt aus dem
  geprueften Lauf - also dem Stand, der attributgleich mit dem Zustand VOR
  dem Umbau ist.
- tests/test_vf_geometrie.py (neu, 8 Tests): vergleicht automatisch, mit
  einem eigenen Test nur fuer die Komma-Listen (das einzige, was eine
  vertauschte vfl-acc-*-Reihenfolge sichtbar macht) und einer Gegenprobe,
  dass eine verfaelschte Referenz auch erkannt wird.

Das faengt, was der Ergebnis-Record NICHT sieht: er sagt "executed, kein
Prompt, Journal aufgegangen", aber nichts darueber, ob dieselbe Geometrie
herauskam. Ablauf ab jetzt pro Schritt: umbauen -> TEST_HUNDM05 (save dxf)
-> pytest tests/test_vf_geometrie.py.

Offen bleiben 5c, 6, 8, 9, 10 (Schleifenrumpf der VF-Einheit, Daten-Executor,
Einheit-Abschluss, Kettenebene je Glied, vfl-spec-ausfuehren). Die vier
haengen zusammen: 6 und 10 brauchen den aufgeteilten Schleifenrumpf. Halb
umgebaut waere der Code schlechter dran als vor oder nach dem Umbau, darum
als ein Schritt mit CAD-Lauf davor und danach.

Verifiziert: 117 pytest-Tests gruen, vf_linienzug.lsp lint-sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 14:10:56 +02:00
parent 02303ad7fc
commit e91f1d00d0
7 changed files with 819 additions and 14 deletions
+4
View File
@@ -170,6 +170,10 @@ tests/
# format (in alltests.json abgeschaltet,
# baut sonst dieselbe Geometrie zweimal)
test_hm_recformat.py # pytest-Validierung dazu
test_vf_geometrie.py # pytest: die GEBAUTE Geometrie gegen
# reference/hundm05_attribute.json - Attribute,
# Bausteine, Einfuegepunkt (faengt eine
# vertauschte vfl-acc-*-Reihenfolge)
test_vf_spec.lsp # LISP-Unittest: Spec-Uebersetzer (vsp-journal),
# je Glied-Typ + die 5 echten HundM-Ketten
# gegen ihr aufgezeichnetes Journal
+297
View File
@@ -0,0 +1,297 @@
{
"VF_hundm05_LZ_01": {
"block": "VF_1",
"insert": [
4912.239,
1230.886,
2154.0
],
"bausteine": {
"AS_Element_90_links_3D": 1,
"ES_Element_90_links_3D": 1,
"Staustrecke_SP_1000_mm_3D": 7,
"Staustrecke_Separator_SP_300_mm_3D": 3,
"Vario_Bogen_ab_3_TEF_rechts_3D": 2,
"Vario_Bogen_auf_3_TEF_rechts_3D": 2,
"Vario_Kurve_links_30_TEF_innen_3D": 1,
"Vario_Kurve_rechts_30_TEF_innen_3D": 1,
"Vario_Kurve_rechts_90_TEF_innen_3D": 2,
"Vario_Motorstation_500mm_rechts_3D": 2,
"Vario_Umlenkstation_500mm_rechts_3D": 2
},
"bausteine_gesamt": 24,
"attribs": {
"ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,horizontal,horizontal",
"ANZAHL_GF": "3",
"ANZAHL_SCANNER": "0",
"ANZAHL_SEPARATOR": "3",
"ANZAHL_VF": "4",
"ARTINR": "6220",
"DELTA_H_mm": "463",
"DELTA_L_mm": "1934",
"GERUEST_EINZELMODUL": "1",
"GERUEST_TYP": "Schoenenberger Geruest",
"GF_Bogen_L_30": "0",
"GF_Bogen_L_60": "0",
"GF_Bogen_L_90": "0",
"GF_Bogen_R_30": "0",
"GF_Bogen_R_60": "0",
"GF_Bogen_R_90": "0",
"GF_WINKEL": "3,3,3",
"HOEHE_BIS_mm": "1690",
"HOEHE_VON_mm": "2154",
"L_GF_m": "1.826,0.4,0.4",
"L_VF_m": "1.859,1.434,0.1,1.83",
"MONTAGEHOEHE_m": "1.922",
"MOTORSEITE": "rechts,rechts",
"SEITE_AS": "links",
"SEITE_ES": "links",
"TYP": "Streckengruppe",
"VF_Bogen_A_30": "0",
"VF_Bogen_A_60": "0",
"VF_Bogen_A_90": "0",
"VF_Bogen_I_30": "2",
"VF_Bogen_I_60": "0",
"VF_Bogen_I_90": "2",
"VF_WINKEL": "0,0,0,0",
"WINKEL_AS": "90",
"WINKEL_ES": "90"
}
},
"VF_hundm05_LZ_02": {
"block": "VF_2",
"insert": [
8571.951,
1276.061,
2494.931
],
"bausteine": {
"AS_Element_90_links_3D": 1,
"ES_Element_90_links_3D": 1,
"Staustrecke_SP_1000_mm_3D": 7,
"Staustrecke_Separator_SP_300_mm_3D": 2,
"Vario_Bogen_ab_21_TEF_rechts_3D": 1,
"Vario_Bogen_ab_3_TEF_rechts_3D": 2,
"Vario_Bogen_auf_21_TEF_rechts_3D": 1,
"Vario_Bogen_auf_3_TEF_rechts_3D": 2,
"Vario_Kurve_rechts_90_TEF_innen_3D": 2,
"Vario_Motorstation_500mm_rechts_3D": 1,
"Vario_Umlenkstation_500mm_rechts_3D": 1
},
"bausteine_gesamt": 21,
"attribs": {
"ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal",
"ANZAHL_GF": "3",
"ANZAHL_SCANNER": "0",
"ANZAHL_SEPARATOR": "2",
"ANZAHL_VF": "4",
"ARTINR": "6220",
"DELTA_H_mm": "107",
"DELTA_L_mm": "1808",
"GERUEST_EINZELMODUL": "1",
"GERUEST_TYP": "Schoenenberger Geruest",
"GF_Bogen_L_30": "0",
"GF_Bogen_L_60": "0",
"GF_Bogen_L_90": "0",
"GF_Bogen_R_30": "0",
"GF_Bogen_R_60": "0",
"GF_Bogen_R_90": "0",
"GF_WINKEL": "3,3,3",
"HOEHE_BIS_mm": "2387",
"HOEHE_VON_mm": "2494",
"L_GF_m": "2.727,0.4,0.418",
"L_VF_m": "0.177,0.339,0.548,0.548",
"MONTAGEHOEHE_m": "2.441",
"MOTORSEITE": "rechts",
"SEITE_AS": "links",
"SEITE_ES": "links",
"TYP": "Streckengruppe",
"VF_Bogen_A_30": "0",
"VF_Bogen_A_60": "0",
"VF_Bogen_A_90": "0",
"VF_Bogen_I_30": "0",
"VF_Bogen_I_60": "0",
"VF_Bogen_I_90": "2",
"VF_WINKEL": "0,0,21,0",
"WINKEL_AS": "90",
"WINKEL_ES": "90"
}
},
"VF_hundm05_LZ_03": {
"block": "VF_3",
"insert": [
12916.466,
1276.061,
2494.931
],
"bausteine": {
"AS_Element_90_links_3D": 1,
"ES_Element_90_links_3D": 1,
"Staustrecke_SP_1000_mm_3D": 7,
"Staustrecke_Separator_SP_300_mm_3D": 2,
"Vario_Bogen_ab_21_TEF_rechts_3D": 1,
"Vario_Bogen_ab_3_TEF_rechts_3D": 2,
"Vario_Bogen_auf_21_TEF_rechts_3D": 1,
"Vario_Bogen_auf_3_TEF_rechts_3D": 2,
"Vario_Kurve_rechts_90_TEF_innen_3D": 2,
"Vario_Motorstation_500mm_rechts_3D": 1,
"Vario_Umlenkstation_500mm_rechts_3D": 1
},
"bausteine_gesamt": 21,
"attribs": {
"ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal",
"ANZAHL_GF": "3",
"ANZAHL_SCANNER": "0",
"ANZAHL_SEPARATOR": "2",
"ANZAHL_VF": "4",
"ARTINR": "6220",
"DELTA_H_mm": "107",
"DELTA_L_mm": "1808",
"GERUEST_EINZELMODUL": "1",
"GERUEST_TYP": "Schoenenberger Geruest",
"GF_Bogen_L_30": "0",
"GF_Bogen_L_60": "0",
"GF_Bogen_L_90": "0",
"GF_Bogen_R_30": "0",
"GF_Bogen_R_60": "0",
"GF_Bogen_R_90": "0",
"GF_WINKEL": "3,3,3",
"HOEHE_BIS_mm": "2387",
"HOEHE_VON_mm": "2494",
"L_GF_m": "2.727,0.4,0.418",
"L_VF_m": "0.177,0.339,0.548,0.548",
"MONTAGEHOEHE_m": "2.441",
"MOTORSEITE": "rechts",
"SEITE_AS": "links",
"SEITE_ES": "links",
"TYP": "Streckengruppe",
"VF_Bogen_A_30": "0",
"VF_Bogen_A_60": "0",
"VF_Bogen_A_90": "0",
"VF_Bogen_I_30": "0",
"VF_Bogen_I_60": "0",
"VF_Bogen_I_90": "2",
"VF_WINKEL": "0,0,21,0",
"WINKEL_AS": "90",
"WINKEL_ES": "90"
}
},
"VF_hundm05_LZ_04": {
"block": "VF_4",
"insert": [
17201.778,
1205.361,
2154.0
],
"bausteine": {
"AS_Element_90_links_3D": 1,
"ES_Element_90_links_3D": 1,
"Staustrecke_SP_1000_mm_3D": 7,
"Staustrecke_Separator_SP_300_mm_3D": 2,
"Vario_Bogen_ab_39_TEF_rechts_3D": 1,
"Vario_Bogen_ab_3_TEF_rechts_3D": 2,
"Vario_Bogen_auf_39_TEF_rechts_3D": 1,
"Vario_Bogen_auf_3_TEF_rechts_3D": 2,
"Vario_Kurve_rechts_90_TEF_innen_3D": 2,
"Vario_Motorstation_500mm_rechts_3D": 1,
"Vario_Umlenkstation_500mm_rechts_3D": 1
},
"bausteine_gesamt": 21,
"attribs": {
"ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal",
"ANZAHL_GF": "3",
"ANZAHL_SCANNER": "0",
"ANZAHL_SEPARATOR": "2",
"ANZAHL_VF": "4",
"ARTINR": "6220",
"DELTA_H_mm": "220",
"DELTA_L_mm": "1788",
"GERUEST_EINZELMODUL": "1",
"GERUEST_TYP": "Schoenenberger Geruest",
"GF_Bogen_L_30": "0",
"GF_Bogen_L_60": "0",
"GF_Bogen_L_90": "0",
"GF_Bogen_R_30": "0",
"GF_Bogen_R_60": "0",
"GF_Bogen_R_90": "0",
"GF_WINKEL": "3,3,3",
"HOEHE_BIS_mm": "2374",
"HOEHE_VON_mm": "2154",
"L_GF_m": "2.727,0.4,0.418",
"L_VF_m": "0.177,0.339,0.356,0.548",
"MONTAGEHOEHE_m": "2.264",
"MOTORSEITE": "rechts",
"SEITE_AS": "links",
"SEITE_ES": "links",
"TYP": "Streckengruppe",
"VF_Bogen_A_30": "0",
"VF_Bogen_A_60": "0",
"VF_Bogen_A_90": "0",
"VF_Bogen_I_30": "0",
"VF_Bogen_I_60": "0",
"VF_Bogen_I_90": "2",
"VF_WINKEL": "0,0,39,0",
"WINKEL_AS": "90",
"WINKEL_ES": "90"
}
},
"VF_hundm05_LZ_05": {
"block": "VF_5",
"insert": [
21490.274,
1195.74,
2494.931
],
"bausteine": {
"AS_Element_90_links_3D": 1,
"ES_Element_90_links_3D": 1,
"Staustrecke_SP_1000_mm_3D": 7,
"Staustrecke_Separator_SP_300_mm_3D": 2,
"Vario_Bogen_ab_21_TEF_rechts_3D": 1,
"Vario_Bogen_ab_3_TEF_rechts_3D": 2,
"Vario_Bogen_auf_21_TEF_rechts_3D": 1,
"Vario_Bogen_auf_3_TEF_rechts_3D": 2,
"Vario_Kurve_rechts_90_TEF_innen_3D": 2,
"Vario_Motorstation_500mm_rechts_3D": 1,
"Vario_Umlenkstation_500mm_rechts_3D": 1
},
"bausteine_gesamt": 21,
"attribs": {
"ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal",
"ANZAHL_GF": "3",
"ANZAHL_SCANNER": "0",
"ANZAHL_SEPARATOR": "2",
"ANZAHL_VF": "4",
"ARTINR": "6220",
"DELTA_H_mm": "107",
"DELTA_L_mm": "1808",
"GERUEST_EINZELMODUL": "1",
"GERUEST_TYP": "Schoenenberger Geruest",
"GF_Bogen_L_30": "0",
"GF_Bogen_L_60": "0",
"GF_Bogen_L_90": "0",
"GF_Bogen_R_30": "0",
"GF_Bogen_R_60": "0",
"GF_Bogen_R_90": "0",
"GF_WINKEL": "3,3,3",
"HOEHE_BIS_mm": "2387",
"HOEHE_VON_mm": "2494",
"L_GF_m": "2.727,0.4,0.418",
"L_VF_m": "0.177,0.339,0.548,0.548",
"MONTAGEHOEHE_m": "2.441",
"MOTORSEITE": "rechts",
"SEITE_AS": "links",
"SEITE_ES": "links",
"TYP": "Streckengruppe",
"VF_Bogen_A_30": "0",
"VF_Bogen_A_60": "0",
"VF_Bogen_A_90": "0",
"VF_Bogen_I_30": "0",
"VF_Bogen_I_60": "0",
"VF_Bogen_I_90": "2",
"VF_WINKEL": "0,0,21,0",
"WINKEL_AS": "90",
"WINKEL_ES": "90"
}
}
}
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_vf_geometrie.py - Regressionsnetz fuer die GEBAUTE Geometrie der
HundM-Ketten.
Warum zusaetzlich zu tests/test_hundm05.py: der Ergebnis-Record sagt
"executed, kein Prompt, Journal aufgegangen" - er sagt NICHT, ob dieselbe
Geometrie herauskam wie vorher. Genau das ist bei der Funktionstrennung in
Stufe 2 (doc/TODO-plan-vf-interactive.md) das Risiko: die Reihenfolge der
dL-Subtraktionen und der vfl-acc-*-Aufrufe IST Geometrie, und eine falsche
Reihenfolge faellt in der Zeichnung nicht auf - nur im Sivas-Export.
Verglichen wird gegen tests/reference/hundm05_attribute.json:
- alle Attribute des VF_n-Blocks, insbesondere die reihenfolgeabhaengigen
Komma-Listen L_VF_m / L_GF_m / GF_WINKEL / ANTRIEBFAHRTRICHTUNG
- die Bausteinzusammensetzung des Blocks (faengt eine falsche
Blockvariante, die kein Attribut verraet)
- der Einfuegepunkt
Die laufende ID und der Blockname sind bewusst NICHT Teil des Vergleichs:
die VF-Nummer beginnt in jeder neuen Zeichnung wieder bei 1.
Braucht eine gebaute Zeichnung (TEST_HUNDM05 mit "save": "dxf") und wird
sonst uebersprungen. Referenz auffrischen - nur nach geprueftem Lauf:
python lib/dxf_vf_abbild.py <dxf> --spec tests/testdata/hundm05.json \
--referenz tests/reference/hundm05_attribute.json \
--schreibe-referenz
"""
import json
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
import dxf_vf_abbild as dva # noqa: E402
HIER = os.path.dirname(__file__)
SPEC = os.path.join(HIER, "testdata", "hundm05.json")
REFERENZ = os.path.join(HIER, "reference", "hundm05_attribute.json")
# SSG_RUN_ALL_TESTS legt die Zeichnung in tests/output ab; ein Lauf mit
# gesetztem DXFM_RESULTS speichert sie nach results/.
DXF_ORTE = [os.path.join(HIER, "output", "hundm05_tests.dxf"),
os.path.join(HIER, "..", "results", "test_hundm05.dxf")]
@pytest.fixture(scope="module")
def gebautes_abbild():
for pfad in DXF_ORTE:
if os.path.isfile(pfad):
return dva.abbild(pfad, SPEC, ("VF_",))
pytest.skip("keine gebaute HundM05-Zeichnung gefunden (%s) - "
"TEST_HUNDM05 in BricsCAD ausfuehren"
% " bzw. ".join(os.path.basename(p) for p in DXF_ORTE))
@pytest.fixture(scope="module")
def referenz():
if not os.path.isfile(REFERENZ):
pytest.skip("%s fehlt - mit lib/dxf_vf_abbild.py "
"--schreibe-referenz anlegen" % REFERENZ)
with open(REFERENZ, encoding="utf-8") as fh:
return json.load(fh)
def test_referenz_deckt_alle_specs(referenz):
"""Jede Kette der Spec muss in der Referenz stehen - sonst laeuft der
Vergleich unbemerkt ins Leere."""
if not os.path.isfile(SPEC):
pytest.skip("Spec fehlt")
with open(SPEC, encoding="utf-8") as fh:
specs = [o["spec_id"] for o in json.load(fh) if "spec_id" in o]
fehlt = sorted(set(specs) - set(referenz))
assert not fehlt, (
"Ketten ohne Referenz: %s. Nach einem geprueften Lauf die Referenz "
"mit lib/dxf_vf_abbild.py --schreibe-referenz auffrischen." % fehlt)
def test_alle_ketten_gebaut(gebautes_abbild, referenz):
fehlt = sorted(set(referenz) - set(gebautes_abbild))
assert not fehlt, "in der Zeichnung nicht gebaut: %s" % fehlt
@pytest.mark.parametrize("feld", ["bausteine", "bausteine_gesamt"])
def test_bausteine_wie_referenz(gebautes_abbild, referenz, feld):
"""Die Bausteinliste faengt, was kein Attribut verraet: eine falsche
Blockvariante (z.B. auf_3 statt ab_3, links statt rechts)."""
for kette, soll in sorted(referenz.items()):
if kette not in gebautes_abbild:
continue
ist = gebautes_abbild[kette]
assert ist[feld] == soll[feld], \
f"{kette}: {feld} ist {ist[feld]}, Referenz {soll[feld]}"
def test_attribute_wie_referenz(gebautes_abbild, referenz):
for kette, soll in sorted(referenz.items()):
if kette not in gebautes_abbild:
continue
ist = gebautes_abbild[kette]["attribs"]
for tag in sorted(set(ist) | set(soll["attribs"])):
assert ist.get(tag) == soll["attribs"].get(tag), \
f'{kette}: Attribut {tag} ist {ist.get(tag)!r}, ' \
f'Referenz {soll["attribs"].get(tag)!r}'
def test_reihenfolge_listen_wie_referenz(gebautes_abbild, referenz):
"""Eigener Test fuer die Komma-Listen: sie sind das einzige, was eine
vertauschte vfl-acc-*-Reihenfolge sichtbar macht - in der Zeichnung
unsichtbar, im Sivas-Export falsch."""
listen = ("L_VF_m", "L_GF_m", "GF_WINKEL", "ANTRIEBFAHRTRICHTUNG")
for kette, soll in sorted(referenz.items()):
if kette not in gebautes_abbild:
continue
ist = gebautes_abbild[kette]["attribs"]
for tag in listen:
if tag not in soll["attribs"]:
continue
assert ist.get(tag) == soll["attribs"][tag], \
f'{kette}: {tag} ist {ist.get(tag)!r}, ' \
f'Referenz {soll["attribs"][tag]!r} - Reihenfolge der ' \
f'vfl-acc-*-Aufrufe geaendert?'
def test_einfuegepunkt_wie_referenz(gebautes_abbild, referenz):
for kette, soll in sorted(referenz.items()):
if kette not in gebautes_abbild:
continue
for achse, (a, b) in enumerate(
zip(gebautes_abbild[kette]["insert"], soll["insert"])):
assert abs(a - b) < 1.0, \
f"{kette}: Einfuegepunkt Achse {achse} ist {a}, Referenz {b}"
def test_vergleichsfunktion_meldet_abweichung(gebautes_abbild, referenz):
"""Gegenprobe: der Vergleich muss eine verfaelschte Referenz erkennen -
sonst bewiesen die Tests oben nur, dass beide Seiten gleich gelesen
werden."""
kette = sorted(referenz)[0]
gift = json.loads(json.dumps(referenz))
gift[kette]["attribs"]["DELTA_L_mm"] = "999999"
assert dva.vergleiche(gebautes_abbild, gift), \
"verfaelschte Referenz wurde NICHT erkannt"
assert not dva.vergleiche(gebautes_abbild, referenz), \
"echte Referenz wird als abweichend gemeldet"