Files
CMPlanning/3d-drawing/scripts/verify_solids_step3.py
T

150 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Step 3 deterministic checks for the solid model (Task 8, 대산2본선3D변환).
Usage: python3 verify_solids.py <project_dir> <pattern_csv>
project_dir : Code/3d-drawing folder (contains model/ and data/)
pattern_csv : data/pattern_stations_dangjin.csv
Checks (each prints PASS/FAIL with counts, method and population):
M1 meta counts per group M2 pattern runs contiguous 4745..5480
M3 runs == CSV rows (in range) M4 component ids and from/to/pattern match runs
M5 meta volume sum == verify json M6 pattern-run ids (PAV/BASE/DRN/HBOX/IBOX) present
S1 STEP re-import: solid count, validity, total volume vs meta (0.1 %)
S2 lining vs excavation overlap per run (volume, threshold 0.05 m2 x run length)
G1 GLB reload: geometry count and non-empty meshes
"""
import csv
import json
import os
import sys
from collections import Counter, defaultdict
PROJ, CSV = sys.argv[1], sys.argv[2]
S0, S1 = 4745, 5480
results = []
def rec(code, verdict, method, note=""):
results.append((code, verdict, method, note))
print(f"{code} {verdict} | {method} | {note}")
meta = json.load(open(os.path.join(PROJ, "model/tunnel_solids_meta.json"), encoding="utf-8"))
verify = json.load(open(os.path.join(PROJ, "data/verify_solids.json"), encoding="utf-8"))
comps = meta["components"]
runs = meta["pattern_runs"]
solids_meta = [c for c in comps if not c.get("reference")]
refs = [c for c in comps if c.get("reference")]
# M1 counts per group
cnt = Counter(c["group"] for c in solids_meta)
exp = {"라이닝": 40, "굴착면": 40, "노면(계획고)": 40, "기층·인버트(가정)": 40, "배수(측구)": 80, "공동구(가정)": 80}
bad = {g: (cnt.get(g, 0), n) for g, n in exp.items() if cnt.get(g, 0) != n}
rec("M1", "PASS" if not bad and len(solids_meta) == 320 and len(refs) == 6 else "FAIL",
f"meta components 전수 {len(comps)}", f"solids {len(solids_meta)} (expect 320), refs {len(refs)} (expect 6), group mismatch {bad}")
# M2 contiguity
runs_sorted = sorted(runs, key=lambda r: r["from"])
gaps = [(a["to"], b["from"]) for a, b in zip(runs_sorted, runs_sorted[1:]) if a["to"] != b["from"]]
ok2 = runs_sorted[0]["from"] == S0 and runs_sorted[-1]["to"] == S1 and not gaps
rec("M2", "PASS" if ok2 else "FAIL", f"pattern_runs 전수 {len(runs)}",
f"range {runs_sorted[0]['from']}..{runs_sorted[-1]['to']}, gaps/overlaps {gaps}")
# M3 runs vs CSV rows inside the model range
rows = list(csv.DictReader(open(CSV, encoding="utf-8")))
csv_in = [r for r in rows if float(r["sta_to"]) > S0 and float(r["sta_from"]) < S1]
# clip each CSV interval to the model range (row 95 runs 5468..5495 and is cut at 5480 by design)
csv_set = Counter((max(int(float(r["sta_from"])), S0), min(int(float(r["sta_to"])), S1), r["lining_pattern"]) for r in csv_in)
run_set = Counter((r["from"], r["to"], r["pattern"]) for r in runs)
mism = (csv_set - run_set) + (run_set - csv_set)
rec("M3", "PASS" if not mism else "FAIL",
f"CSV 전수 {len(rows)} 행 중 범위 내 {len(csv_in)} 행과 pattern_runs {len(runs)} 행 대조",
f"mismatch {dict(mism)}")
# M4 component from/to/pattern equal their run
run_by_id = {r["id"]: r for r in runs}
bad4 = []
for c in solids_meta:
if c["group"] == "라이닝":
r = run_by_id.get(c["id"])
if not r or (r["from"], r["to"], r["pattern"]) != (c["from_sta"], c["to_sta"], c["pattern"]):
bad4.append(c["id"])
rec("M4", "PASS" if not bad4 else "FAIL", f"라이닝 전수 {cnt['라이닝']} 대 run",
f"mismatch {bad4[:5]}")
# M5 volume sum
vol = sum(c["volume_m3"] for c in solids_meta)
ok5 = abs(vol - verify["volume_sum_m3"]) < 0.001
rec("M5", "PASS" if ok5 else "FAIL", f"meta 부피 합 전수 {len(solids_meta)} 개",
f"sum {vol:.3f} vs verify {verify['volume_sum_m3']}")
# M6 per-run component ids
need = ["LIN", "EXC", "PAV", "BASE", "DRNL", "DRNR", "HBOX", "IBOX"]
ids = {c["id"] for c in solids_meta}
miss = []
for i, r in enumerate(runs_sorted):
for pre in need:
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if pre in ("DRNL", "DRNR"):
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if cid not in ids and pre not in ("DRNL", "DRNR"):
miss.append(cid)
drn = [c for c in solids_meta if c["group"] == "배수(측구)"]
rec("M6", "PASS" if not miss and len(drn) == 80 else "FAIL",
f"run {len(runs_sorted)} x 부품 종류 {len(need)} 대조 (배수 L/R 포함 {len(drn)})",
f"missing {miss[:5]}")
# S1 STEP re-import
import build123d as bd # noqa: E402
step_path = os.path.join(PROJ, "model/tunnel_solids.step")
shape = bd.importers.import_step(step_path) if hasattr(bd, "importers") else bd.import_step(step_path)
solids = shape.solids()
n_valid = sum(1 for s in solids if s.is_valid)
vol_step = sum(s.volume for s in solids)
rel = abs(vol_step - vol) / vol
rec("S1", "PASS" if len(solids) == 320 and n_valid == len(solids) and rel < 0.001 else "FAIL",
f"STEP 재임포트 솔리드 전수 {len(solids)}, is_valid 전수 {n_valid}, 부피 재계산 합 vs meta",
f"count {len(solids)}, valid {n_valid}, vol_step {vol_step:.3f} vs meta {vol:.3f} (rel {rel:.2e})")
# S2 overlap lining vs excavation per run, using the STEP solids
def centroid(s):
return s.center()
lin = [s for s in solids]
# classify STEP solids by volume match to meta (only for overlap sampling)
meta_vol = sorted([(c["volume_m3"], c["group"], c["id"], c["from_sta"], c["to_sta"]) for c in solids_meta])
sample_ids = [r["id"] for r in runs_sorted[:: max(1, len(runs_sorted) // 4)]][:4]
lines = []
ok_overlap = True
for rid in sample_ids:
r = run_by_id[rid]
k = rid.split("_", 1)[1]
idx = [i for i, rr in enumerate(runs_sorted) if rr["id"] == rid][0] + 1
lin_c = next(c for c in solids_meta if c["id"] == f"LIN_{idx:02d}_{r['pattern']}")
exc_c = next(c for c in solids_meta if c["id"] == f"EXC_{idx:02d}_{r['pattern']}")
def find(vol_target):
best = min(solids, key=lambda s: abs(s.volume - vol_target))
assert abs(best.volume - vol_target) < 1e-3, "no STEP solid matches meta volume"
return best
sl = find(lin_c["volume_m3"]); se = find(exc_c["volume_m3"])
inter = sl.intersect(se)
iv = sum(x.volume for x in inter) if inter is not None else 0.0
length = r["to"] - r["from"]
thr = 0.05 * length
lines.append(f"{rid}: ovl {iv:.4f} m3 (thr {thr:.2f})")
if iv > thr:
ok_overlap = False
rec("S2", "PASS" if ok_overlap else "FAIL", f"샘플 run {len(sample_ids)}건 라이닝∩굴착 부피 (부피 매칭 솔리드)",
"; ".join(lines))
# G1 GLB reload
import trimesh # noqa: E402
glb = trimesh.load(os.path.join(PROJ, "model/tunnel_solids.glb"), force="scene")
geoms = list(glb.geometry.values())
empty = [g for g in geoms if len(g.faces) == 0]
rec("G1", "PASS" if len(geoms) == verify["glb_geometries"] and not empty else "FAIL",
f"GLB 로드 지오메트리 전수 {len(geoms)}", f"empty {len(empty)}, expect {verify['glb_geometries']}")
fails = [r for r in results if r[1] == "FAIL"]
print("SUMMARY", len(results), "checks, FAIL", len(fails))