#!/usr/bin/env python3 """Step 3 deterministic checks for the solid model (Task 8, 대산2본선3D변환). Usage: python3 verify_solids.py 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))