diff --git a/data/verify_geom_hd2_results.json b/data/verify_geom_hd2_results.json new file mode 100644 index 0000000..cd4e10e --- /dev/null +++ b/data/verify_geom_hd2_results.json @@ -0,0 +1,656 @@ +{ + "1|4745.0005": { + "LIN_arcs": { + "ref_to_model_max": 99.05839911556747, + "ref_to_model_mean": 12.265421914456372, + "model_to_ref_max": 2699.228422004956, + "model_to_ref_mean": 139.42217237180967, + "n_ref": 66562, + "n_model": 2539, + "clipped_below_model_extent": { + "L": 427, + "R": 438, + "yrange_L": [ + 15264, + 15659 + ], + "yrange_R": [ + 15080, + 15488 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 199.81630323321525, + "ref_to_model_mean": 31.495823939407906, + "model_to_ref_max": 8330.781218615124, + "model_to_ref_mean": 3658.6932294230833, + "n_ref": 2395, + "n_model": 2539 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.5, + "LIN_haunchL_outer": 492.3, + "LIN_haunchL_inner": 12.9, + "LIN_haunchR_outer": 461.1, + "LIN_haunchR_inner": 15.5 + }, + "EXC_arcs": { + "ref_to_model_max": 49.97333468210581, + "ref_to_model_mean": 13.22944506104266, + "model_to_ref_max": 139.10210757850402, + "model_to_ref_mean": 69.63490871239516, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 13.6729631110277, + "ref_to_model_mean": 10.97265592073033, + "model_to_ref_max": 9.766634709250443, + "model_to_ref_mean": 9.376172149516487, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 106.89691611711953, + "ref_to_model_mean": 46.68072152026932, + "model_to_ref_max": 270.29560834777436, + "model_to_ref_mean": 47.30323588291019, + "n_ref": 18402, + "n_model": 934 + }, + "BASE_model_extent_x": [ + 7715, + 20584 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15760 + ], + "DRN": { + "ref_to_model_max": 14.457886611257248, + "ref_to_model_mean": 7.048062010678219, + "model_to_ref_max": 148.08739517488308, + "model_to_ref_mean": 9.751422269720287, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 10.4, + "DRN_wallL2": 11.2, + "DRN_botL": 14.4, + "DRN_pipeL": 14.5, + "DRN_wallR1": 10.2, + "DRN_wallR2": 11.1, + "DRN_botR": 14.4, + "DRN_pipeR": 14.5 + }, + "DRN_model_xrange": [ + 9099, + 19149 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + }, + "1|5100.0": { + "LIN_arcs": { + "ref_to_model_max": 98.98374727460279, + "ref_to_model_mean": 12.286715807049747, + "model_to_ref_max": 2699.208225118645, + "model_to_ref_mean": 139.4446947488841, + "n_ref": 66562, + "n_model": 2539, + "clipped_below_model_extent": { + "L": 427, + "R": 438, + "yrange_L": [ + 15264, + 15659 + ], + "yrange_R": [ + 15080, + 15488 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 199.75630963156448, + "ref_to_model_mean": 31.508170955828962, + "model_to_ref_max": 8330.792297541451, + "model_to_ref_mean": 3658.67650320109, + "n_ref": 2395, + "n_model": 2539 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.4, + "LIN_haunchL_outer": 492.3, + "LIN_haunchL_inner": 12.9, + "LIN_haunchR_outer": 461.1, + "LIN_haunchR_inner": 15.5 + }, + "EXC_arcs": { + "ref_to_model_max": 49.90231927152876, + "ref_to_model_mean": 13.25015537105612, + "model_to_ref_max": 139.12313900485057, + "model_to_ref_mean": 69.6577719572834, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 14.418186911034798, + "ref_to_model_mean": 10.996432167000822, + "model_to_ref_max": 10.776825902966216, + "model_to_ref_mean": 9.399395181664575, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 106.95235216721005, + "ref_to_model_mean": 46.704840227446, + "model_to_ref_max": 270.3072089440827, + "model_to_ref_mean": 47.3306581098095, + "n_ref": 18402, + "n_model": 934 + }, + "BASE_model_extent_x": [ + 7715, + 20583 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15759 + ], + "DRN": { + "ref_to_model_max": 14.490884181941974, + "ref_to_model_mean": 7.0658785237396495, + "model_to_ref_max": 148.0563566649249, + "model_to_ref_mean": 9.77795072083027, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 10.4, + "DRN_wallL2": 11.3, + "DRN_botL": 14.4, + "DRN_pipeL": 14.5, + "DRN_wallR1": 10.2, + "DRN_wallR2": 11.2, + "DRN_botR": 14.4, + "DRN_pipeR": 14.4 + }, + "DRN_model_xrange": [ + 9099, + 19149 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + }, + "1|5469.9995": { + "LIN_arcs": { + "ref_to_model_max": 99.05839911694011, + "ref_to_model_mean": 12.265421913408609, + "model_to_ref_max": 2699.2284220029933, + "model_to_ref_mean": 139.55032916231897, + "n_ref": 66562, + "n_model": 2540, + "clipped_below_model_extent": { + "L": 427, + "R": 438, + "yrange_L": [ + 15264, + 15659 + ], + "yrange_R": [ + 15080, + 15488 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 199.8163032523252, + "ref_to_model_mean": 31.495823938300035, + "model_to_ref_max": 8330.781218614657, + "model_to_ref_mean": 3657.3796663221337, + "n_ref": 2395, + "n_model": 2540 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.5, + "LIN_haunchL_outer": 492.3, + "LIN_haunchL_inner": 12.9, + "LIN_haunchR_outer": 461.1, + "LIN_haunchR_inner": 15.5 + }, + "EXC_arcs": { + "ref_to_model_max": 49.973334683068344, + "ref_to_model_mean": 13.229445060405267, + "model_to_ref_max": 139.10210757277747, + "model_to_ref_mean": 69.63490871114456, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 193.59943538166334, + "ref_to_model_mean": 85.90038197968867, + "model_to_ref_max": 193.58867016575806, + "model_to_ref_mean": 85.49583497432751, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 194.0339970575608, + "ref_to_model_mean": 76.70070967162879, + "model_to_ref_max": 270.2956083490425, + "model_to_ref_mean": 71.6861767350549, + "n_ref": 18402, + "n_model": 925 + }, + "BASE_model_extent_x": [ + 7715, + 20584 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15760 + ], + "DRN": { + "ref_to_model_max": 14.45788661735982, + "ref_to_model_mean": 7.048062004823568, + "model_to_ref_max": 148.08739519731785, + "model_to_ref_mean": 9.751422260123833, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 10.4, + "DRN_wallL2": 11.2, + "DRN_botL": 14.4, + "DRN_pipeL": 14.5, + "DRN_wallR1": 10.2, + "DRN_wallR2": 11.1, + "DRN_botR": 14.4, + "DRN_pipeR": 14.5 + }, + "DRN_model_xrange": [ + 9099, + 19149 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + }, + "-1|4745.0005": { + "LIN_arcs": { + "ref_to_model_max": 165.93842403641, + "ref_to_model_mean": 19.342371436798466, + "model_to_ref_max": 2694.4903611935015, + "model_to_ref_mean": 148.5127579371637, + "n_ref": 66566, + "n_model": 2539, + "clipped_below_model_extent": { + "L": 245, + "R": 616, + "yrange_L": [ + 15264, + 15488 + ], + "yrange_R": [ + 15080, + 15658 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 278.1041972578194, + "ref_to_model_mean": 86.14276227224678, + "model_to_ref_max": 8325.718000790263, + "model_to_ref_mean": 3664.9850644439653, + "n_ref": 2395, + "n_model": 2539 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.5, + "LIN_haunchL_outer": 278.9, + "LIN_haunchL_inner": 79.7, + "LIN_haunchR_outer": 676.9, + "LIN_haunchR_inner": 165.9 + }, + "EXC_arcs": { + "ref_to_model_max": 110.0366585359107, + "ref_to_model_mean": 20.147468133684814, + "model_to_ref_max": 230.36912980232339, + "model_to_ref_mean": 77.01959791893609, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 193.58697559581455, + "ref_to_model_mean": 85.90449213019785, + "model_to_ref_max": 193.50070137422574, + "model_to_ref_mean": 85.49177355782385, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 186.2456896549011, + "ref_to_model_mean": 65.25754410234194, + "model_to_ref_max": 241.51176893122732, + "model_to_ref_mean": 79.35668872937082, + "n_ref": 18402, + "n_model": 934 + }, + "BASE_model_extent_x": [ + 7669, + 20538 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15760 + ], + "DRN": { + "ref_to_model_max": 191.09289892056788, + "ref_to_model_mean": 43.78263107043471, + "model_to_ref_max": 230.1686793175862, + "model_to_ref_mean": 53.43022849360874, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 185.1, + "DRN_wallL2": 191.1, + "DRN_botL": 142.3, + "DRN_pipeL": 149.8, + "DRN_wallR1": 174.4, + "DRN_wallR2": 174.4, + "DRN_botR": 174.7, + "DRN_pipeR": 124.6 + }, + "DRN_model_xrange": [ + 9103, + 19153 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + }, + "-1|5100.0": { + "LIN_arcs": { + "ref_to_model_max": 165.86208125776304, + "ref_to_model_mean": 19.353584271599697, + "model_to_ref_max": 2694.466152098678, + "model_to_ref_mean": 148.51959481437646, + "n_ref": 66566, + "n_model": 2539, + "clipped_below_model_extent": { + "L": 245, + "R": 616, + "yrange_L": [ + 15264, + 15488 + ], + "yrange_R": [ + 15080, + 15658 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 278.0451437760574, + "ref_to_model_mean": 86.11198124109171, + "model_to_ref_max": 8325.7291267321, + "model_to_ref_mean": 3664.9615785773476, + "n_ref": 2395, + "n_model": 2539 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.4, + "LIN_haunchL_outer": 278.9, + "LIN_haunchL_inner": 79.6, + "LIN_haunchR_outer": 676.9, + "LIN_haunchR_inner": 165.9 + }, + "EXC_arcs": { + "ref_to_model_max": 109.97882644641383, + "ref_to_model_mean": 20.162782046328307, + "model_to_ref_max": 230.32659770807805, + "model_to_ref_mean": 77.03356500761437, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 192.60836907668372, + "ref_to_model_mean": 85.5974876462991, + "model_to_ref_max": 192.52054570763823, + "model_to_ref_mean": 85.17944998313874, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 185.37579093848115, + "ref_to_model_mean": 65.10503094806941, + "model_to_ref_max": 241.49813213935548, + "model_to_ref_mean": 79.21916715369449, + "n_ref": 18402, + "n_model": 934 + }, + "BASE_model_extent_x": [ + 7669, + 20538 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15759 + ], + "DRN": { + "ref_to_model_max": 191.07984589599806, + "ref_to_model_mean": 43.782144626512356, + "model_to_ref_max": 230.1425275457013, + "model_to_ref_mean": 53.43922451031213, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 185.1, + "DRN_wallL2": 191.1, + "DRN_botL": 142.3, + "DRN_pipeL": 149.8, + "DRN_wallR1": 174.4, + "DRN_wallR2": 174.4, + "DRN_botR": 174.7, + "DRN_pipeR": 124.6 + }, + "DRN_model_xrange": [ + 9103, + 19153 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + }, + "-1|5469.9995": { + "LIN_arcs": { + "ref_to_model_max": 165.93842404388747, + "ref_to_model_mean": 19.342371439075382, + "model_to_ref_max": 2694.4903611915724, + "model_to_ref_mean": 148.67066912972504, + "n_ref": 66566, + "n_model": 2540, + "clipped_below_model_extent": { + "L": 245, + "R": 616, + "yrange_L": [ + 15264, + 15488 + ], + "yrange_R": [ + 15080, + 15658 + ] + } + }, + "LIN_footlines": { + "ref_to_model_max": 278.10419727094626, + "ref_to_model_mean": 86.14276227398764, + "model_to_ref_max": 8325.718000790119, + "model_to_ref_mean": 3663.701800289308, + "n_ref": 2395, + "n_model": 2540 + }, + "LIN_perarc_ref2model_max_mm": { + "LIN_crown_outer": 21.5, + "LIN_crown_inner": 17.5, + "LIN_haunchL_outer": 278.9, + "LIN_haunchL_inner": 79.7, + "LIN_haunchR_outer": 676.9, + "LIN_haunchR_inner": 165.9 + }, + "EXC_arcs": { + "ref_to_model_max": 110.03665854392911, + "ref_to_model_mean": 20.147468133951584, + "model_to_ref_max": 230.36912981558459, + "model_to_ref_mean": 77.01959791929141, + "n_ref": 35303, + "n_model": 2271, + "clipped_below_model_extent": { + "L": 375, + "R": 566, + "yrange_L": [ + 15264, + 15612 + ], + "yrange_R": [ + 15080, + 15612 + ] + } + }, + "EXC_crown_ref2model_max_mm": 21.8, + "PAV": { + "ref_to_model_max": 13.753593831242364, + "ref_to_model_mean": 10.921448830959804, + "model_to_ref_max": 9.778010359538655, + "model_to_ref_mean": 9.31009466082442, + "n_ref": 18962, + "n_model": 952 + }, + "BASE_drawn_span": { + "ref_to_model_max": 140.76940169714493, + "ref_to_model_mean": 48.22264276831631, + "model_to_ref_max": 241.5117689307402, + "model_to_ref_mean": 55.15185176197466, + "n_ref": 18402, + "n_model": 925 + }, + "BASE_model_extent_x": [ + 7669, + 20538 + ], + "BASE_model_ymin_ymax": [ + 15067, + 15760 + ], + "DRN": { + "ref_to_model_max": 191.09289891938243, + "ref_to_model_mean": 43.782631065576105, + "model_to_ref_max": 230.16867930371887, + "model_to_ref_mean": 53.430228486469154, + "n_ref": 5820, + "n_model": 373 + }, + "DRN_perref_ref2model_max_mm": { + "DRN_wallL1": 185.1, + "DRN_wallL2": 191.1, + "DRN_botL": 142.3, + "DRN_pipeL": 149.8, + "DRN_wallR1": 174.4, + "DRN_wallR2": 174.4, + "DRN_botR": 174.7, + "DRN_pipeR": 124.6 + }, + "DRN_model_xrange": [ + 9103, + 19153 + ], + "DRN_model_yrange": [ + 14670, + 15690 + ] + } +} \ No newline at end of file diff --git a/scripts/verify_geom/faces.py b/scripts/verify_geom/faces.py new file mode 100644 index 0000000..9c6c152 --- /dev/null +++ b/scripts/verify_geom/faces.py @@ -0,0 +1,42 @@ +import sys, time, pickle +sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages'] +from build123d import import_step, Face +import numpy as np +shape = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step") +solids = shape.solids() +pickle_out = {} +import json +meta=json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json')) +comps=[c for c in meta['components'] if c['group']!='시설한계(참조선)'] +print("non-clearance comps", len(comps)) +# match by volume greedily in meta order +used=set(); mapping={} +for c in comps: + best=None;bd=1e9 + for j,s in enumerate(solids): + if j in used: continue + d=abs(s.volume-c['volume_m3']) + if dC0[0]][:,1].min() + lr=np.vstack([REF[k] for k in arcs]); d,_=cKDTree(L).query(lr) + inside=((lr[:,0]=ymin_L))|((lr[:,0]>=C0[0])&(lr[:,1]>=ymin_R)) + print(f" LIN within model extent (y>={ymin_L:.0f}/{ymin_R:.0f}) max={d[inside].max():.1f} mm; drawn part below model extent (not modelled) n={int((~inside).sum())} pts, y range {lr[~inside,1].min():.0f}-{lr[~inside,1].max():.0f}") + fl=np.vstack([REF['LIN_footL'],REF['LIN_footR']]); a,b=dd(L,fl); print(f" LIN foot lines (CS-CONC-MAJR wall/foot) ref->model max={a.max():.1f} model->ref(near band) ") + # EXC + ea=['EXC_crown','EXC_haunchL','EXC_haunchR'] + eymL=E[E[:,0]C0[0]][:,1].min() + er=np.vstack([REF[k] for k in ea]); d,_=cKDTree(E).query(er) + ins=((er[:,0]=eymL))|((er[:,0]>=C0[0])&(er[:,1]>=eymR)) + print(f" EXC arcs within model extent (y>={eymL:.0f}/{eymR:.0f}) max={d[ins].max():.1f} mm; crown max={cKDTree(E).query(REF['EXC_crown'])[0].max():.1f}; below model extent n={int((~ins).sum())} y {er[~ins,1].min():.0f}-{er[~ins,1].max():.0f}") + # PAV model vs drawn top/bottom/sides + pref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']]) + Pm=P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)] + a,b=dd(Pm,pref); print(f" PAV ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (top/bottom/sides)") + # BASE: top envelope vs base top line; bottom envelope vs base bottom line, within road span + bw=B[(B[:,0]>=9526.3)&(B[:,0]<=18726.3)] + xs=np.linspace(9526.3,18726.3,921) + top_line=15484.0+(xs-9526.3)*(15300-15484)/9200.0; bot_line=15334.0+(xs-9526.3)*(15150-15334)/9200.0 + # model envelope per x-bin (1 m bins for robust): max y (top) and min y (bottom) + tops=[];bots=[] + for x0 in np.arange(9526.3,18726.3,200): + m=(bw[:,0]>=x0)&(bw[:,0]15300): max={et.max():.1f} mm, mean={et.mean():.1f}; bottom envelope vs drawn bottom (15334->15150): max={eb.max():.1f} mm, mean={eb.mean():.1f}") + print(f" BASE model bottom envelope at span ends: x={bots[0,0]:.0f} y={bots[0,1]:.1f} / x={bots[-1,0]:.0f} y={bots[-1,1]:.1f}; top envelope ends y={tops[0,1]:.1f}/{tops[-1,1]:.1f}") + print(f" BASE model extent outside road span: x {B[:,0].min():.0f}..{B[:,0].max():.0f}, y {B[:,1].min():.0f}..{B[:,1].max():.0f}") + # DRN + dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR','DRN_topL','DRN_topR']]) + dm=D[(D[:,0]>8900)&(D[:,0]<19300)] + a,b=dd(dm,dref); print(f" DRN ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (walls, bottoms, pipes, tops)") + # vertical datum check at tunnel centre and road centre + fit=FIT[st]; uc,vc,_=fit['outer'] + top_c=fit['slope_top']*(0-uc)+0 ; + # top envelope at u for DXF tunnel centre (u=uc) and road centre (x=9776.3 -> u=uc-4.35) + uu=P[:,0]; + def top_env(xdxf): + um=uc+(xdxf-C0[0])/1000.0 + m=np.abs(pts(st,lambda g,i:g=='노면(계획고)')[:,0]-um)<0.05 + return pts(st,lambda g,i:g=='노면(계획고)')[m,1].max() + Pm_raw=pts(st,lambda g,i:g=='노면(계획고)') + for lab,xdxf,ydxf in [('tunnel centre x=14126.3',14126.3,15672.0),('road centre x=9776.3',9776.3,15759.0)]: + um=uc+(xdxf-C0[0])/1000.0 + m=np.abs(Pm_raw[:,0]-um)<0.05 + yt=(Pm_raw[m,1].max()-vc)*1000; yb=(Pm_raw[m,1].min()-vc)*1000 + print(f" datum {lab}: model top={yt:.1f} mm, bottom={yb:.1f} mm above lining centre | DXF top at this x = {ydxf-15305:.1f} mm above lining centre") diff --git a/scripts/verify_geom/final2.py b/scripts/verify_geom/final2.py new file mode 100644 index 0000000..47e2201 --- /dev/null +++ b/scripts/verify_geom/final2.py @@ -0,0 +1,23 @@ +import sys +exec(open('/tmp/verify_daesan2_run/scripts/final.py').read().split("for st in ['4745.0005','5100.0','5469.9995']:")[0]) +for st in ['4745.0005','5100.0']: + B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1); D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1) + bw=B[(B[:,0]>=9526.3+150)&(B[:,0]<=18726.3-150)] + xs=np.arange(9676.3,18576.3,50.0) + top_line=lambda x: 15484.0+(x-9526.3)*(15300-15484)/9200.0 + bot_line=lambda x: 15334.0+(x-9526.3)*(15150-15334)/9200.0 + tt=[];bb=[] + for x0 in xs: + m=(bw[:,0]>=x0-25)&(bw[:,0]15150), interior: max|dev|={np.abs(bb).max():.1f} mm, mean={bb.mean():.1f} mm (signed; negative=model below drawing)") + # drains body vs lid + body=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']]) + lid=np.vstack([REF['DRN_topL'],REF['DRN_topR']]) + dm=D[(D[:,0]>8900)&(D[:,0]<19300)] + a,_=cKDTree(dm).query(body); b,_=cKDTree(dm).query(lid) + print(f"{st}: DRN body (walls/bottom/pipe) ref->model max={a.max():.1f} mm; lid (450-wide top line) ref->model max={b.max():.1f} mm") + print(f"{st}: DRN model x-extent per ditch: left {dm[dm[:,0]<14126,0].min():.0f}-{dm[dm[:,0]<14126,0].max():.0f}, right {dm[dm[:,0]>14126,0].min():.0f}-{dm[dm[:,0]>14126,0].max():.0f}") diff --git a/scripts/verify_geom/final_slice.py b/scripts/verify_geom/final_slice.py new file mode 100644 index 0000000..9d21668 --- /dev/null +++ b/scripts/verify_geom/final_slice.py @@ -0,0 +1,102 @@ +import sys, json, pickle, collections +sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages'] +import numpy as np +from build123d import import_step, Shape +from OCP.BRepAlgoAPI import BRepAlgoAPI_Section +from OCP.gp import gp_Pln, gp_Pnt, gp_Dir +from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_EDGE +solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids() +mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping'] +caps = json.load(open('/tmp/verify_daesan2_run/caps.json')) +ch = json.load(open('/tmp/verify_daesan2_run/chain.json')) +meta = json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json')) +comps = {c['id']: c for c in meta['components'] if c['group']!='시설한계(참조선)'} +solid_comp = {v:k for k,v in mp.items()} +# 1) boundary points from lining chain +pts = [] +for k in ch['lin']: + pts.append(np.array(ch['starts'][k])); pts.append(np.array(ch['ends'][k])) +P = np.array(pts)[:, :2] +uniq = [] +for p in P: + if not any(np.linalg.norm(p-q) < 1e-3 for q in uniq): uniq.append(p) +uniq = np.array(uniq) +print("unique boundary points", len(uniq), "(expect 41)") +D = P[-1]-P[0] if False else (np.array(ch['ends'][ch['lin'][-1]])[:2]-np.array(ch['starts'][ch['lin'][0]])[:2]) +proj = uniq@D/np.linalg.norm(D) +order = np.argsort(proj); B = uniq[order] +cum = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(B,axis=0),axis=1))]) +Sb = 4745 + cum +M = sorted(set([c['from_sta'] for c in comps.values()] + [5480])) +print("boundary count", len(Sb), "meta boundary count", len(M)) +print("max |station_chain - meta_boundary| (m):", round(float(np.max(np.abs(Sb-np.array(M)))),3) if len(Sb)==len(M) else 'count mismatch') +# 2) station for each solid by centroid projection +def station_of(p): + best=None + for k in range(len(B)-1): + a,b=B[k],B[k+1]; t=b-a; L2=t@t + f=np.clip(((p-a)@t)/L2,0,1); q=a+f*t; d=np.linalg.norm(p-q) + if best is None or dmodel) max, d(model->ref) max within band.""" + if len(M)==0 or len(R)==0: return None + tM=cKDTree(M); tR=cKDTree(R) + dR,_=tM.query(R); dM,_=tR.query(M) + return dict(ref_to_model_max=float(dR.max()), ref_to_model_mean=float(dR.mean()), model_to_ref_max=float(dM.max()), model_to_ref_mean=float(dM.mean()), n_ref=len(R), n_model=len(M)) +def run(sig, st, feats): + res={} + L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,sig); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,sig) + P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,sig); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,sig) + D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,sig) + def side_clip(R, M, side): + # keep reference points on given side (L: xC0x) and above model's lowest y on that side minus 50mm + sm = M[(M[:,0]C0[0])] + sr = R[(R[:,0]C0[0])] + if len(sm)==0: return sr, 0, None + ymin=sm[:,1].min()-0.05*1000 + keep=sr[sr[:,1]>=ymin] + dropped=len(sr)-len(keep) + rng=None if dropped==0 else [round(float(sr[sr[:,1]C0[0]],L,'R') + crown=REF['LIN_crown_outer']; + out['LIN_arcs']=dist_stats(L, np.vstack([crown, REF['LIN_crown_inner'], keepL, keepR]), 'LIN') + out['LIN_arcs']['clipped_below_model_extent']={'L':dL,'R':dR_,'yrange_L':rL,'yrange_R':rR} + out['LIN_footlines']=dist_stats(L, np.vstack([REF['LIN_footL'],REF['LIN_footR']]), 'LIN_foot') + # per-arc max deviations (ref->model), by arc name + perarc={} + for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']: + R=REF[k]; tM=cKDTree(L); d,_=tM.query(R); perarc[k]=round(float(d.max())*1,1) + out['LIN_perarc_ref2model_max_mm']=perarc + # Excavation + exc_ref=np.vstack([REF['EXC_crown'],REF['EXC_haunchL'],REF['EXC_haunchR']]) + keepE_L,eL,reL=side_clip(exc_ref,E,'L'); keepE_R,eR,reR=side_clip(exc_ref,E,'R') + out['EXC_arcs']=dist_stats(E, np.vstack([REF['EXC_crown'],keepE_L,keepE_R]),'EXC') + out['EXC_arcs']['clipped_below_model_extent']={'L':eL,'R':eR,'yrange_L':reL,'yrange_R':reR} + tE=cKDTree(E); d,_=tE.query(REF['EXC_crown']); out['EXC_crown_ref2model_max_mm']=round(float(d.max()),1) + # Pavement: top/bottom/sides + pav_ref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']]) + out['PAV']=dist_stats(P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)], pav_ref,'PAV') + # Base: model points restricted to road width (drawn span) + Bw=B[(B[:,0]>=9526.3-1)&(B[:,0]<=18726.3+1)] + out['BASE_drawn_span']=dist_stats(Bw, np.vstack([REF['BASE_top'],REF['BASE_bot']]),'BASE') + out['BASE_model_extent_x']=[round(float(B[:,0].min())),round(float(B[:,0].max()))] + out['BASE_model_ymin_ymax']=[round(float(B[:,1].min())),round(float(B[:,1].max()))] + # Drains: both ditches vs both references + dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']]) + dm=D[(D[:,0]>8900)&(D[:,0]<19300)] + out['DRN']=dist_stats(dm, dref,'DRN') + # per-ditch: ref walls/bottom/pipe to model + tD=cKDTree(dm) + out['DRN_perref_ref2model_max_mm']={k: round(float(tD.query(REF[k])[0].max()),1) for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']} + out['DRN_model_xrange']=[round(float(dm[:,0].min())),round(float(dm[:,0].max()))] + out['DRN_model_yrange']=[round(float(dm[:,1].min())),round(float(dm[:,1].max()))] + return out +allres={} +for sig in [1,-1]: + for st in ['4745.0005','5100.0','5469.9995']: + r=run(sig,st,None); allres[f"{sig}|{st}"]=r + print(f"=== sigma={sig} st={st} slope={FIT[st]['slope_top']*sig:.5f}") + for k in ['LIN_arcs','EXC_arcs','PAV','BASE_drawn_span','DRN']: + v=r[k]; + if v is None: print(' ',k,'none'); continue + print(f" {k:16s} ref->model max={v['ref_to_model_max']*1000:7.1f}mm mean={v['ref_to_model_mean']*1000:6.1f} | model->ref max={v['model_to_ref_max']*1000:7.1f}mm mean={v['model_to_ref_mean']*1000:6.1f}") + if 'clipped_below_model_extent' in v: print(' clipped', v['clipped_below_model_extent']) + print(' LIN foot lines model->ref / ref->model:', None if r['LIN_footlines'] is None else (round(r['LIN_footlines']['model_to_ref_max']*1000,1), round(r['LIN_footlines']['ref_to_model_max']*1000,1))) + print(' LIN per-arc ref->model max mm:', r['LIN_perarc_ref2model_max_mm']) + print(' EXC crown ref->model max mm:', r['EXC_crown_ref2model_max_mm']) + print(' BASE model x-range', r['BASE_model_extent_x'], 'y-range', r['BASE_model_ymin_ymax']) + print(' DRN per-ref ref->model max mm:', r['DRN_perref_ref2model_max_mm'], 'model x', r['DRN_model_xrange'], 'y', r['DRN_model_yrange']) +json.dump(allres, open('/tmp/verify_daesan2_run/hd2_results.json','w'), indent=1, default=str) diff --git a/scripts/verify_geom/measure.py b/scripts/verify_geom/measure.py new file mode 100644 index 0000000..656361e --- /dev/null +++ b/scripts/verify_geom/measure.py @@ -0,0 +1,44 @@ +import sys, json, math +sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages'] +import numpy as np +from scipy.spatial import cKDTree +from scipy.optimize import least_squares +import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt +SEC = json.load(open('/tmp/verify_daesan2_run/sections_final.json')) +def pts_of(st, pred): + out=[]; ids=[] + for sid,v in SEC[st]['solids'].items(): + if pred(v['group'], v['id']): + for poly in v['polys']: + out.append(np.array(poly)[:, :2]); ids.append(v['id']) + return (np.vstack(out) if out else np.zeros((0,2))), ids +def fit_circle(P): + x,y=P[:,0],P[:,1] + A=np.c_[2*x,2*y,np.ones(len(x))]; b=x*x+y*y + c,_,_,_=np.linalg.lstsq(A,b,rcond=None) + xc,yc=c[0],c[1]; r=math.sqrt(c[2]+xc*xc+yc*yc) + return xc,yc,r +# ---- lining crown fit per station +fits={} +for st in SEC: + P,_=pts_of(st, lambda g,i: g=='라이닝') + u0=(P[:,0].min()+P[:,0].max())/2; v0=P[:,1].max()-7.282 + d=np.hypot(P[:,0]-u0,P[:,1]-v0) + sel_o=P[(np.abs(d-7.282)<0.03)&(P[:,1]-v0>4.0)] + sel_i=P[(np.abs(d-6.982)<0.03)&(P[:,1]-v0>4.0)] + co=fit_circle(sel_o); ci=fit_circle(sel_i) + fits[st]={'outer':co,'inner':ci,'n_o':len(sel_o),'n_i':len(sel_i)} + print(f"st {st}: outer n={len(sel_o)} centre=({co[0]:.4f},{co[1]:.4f}) R={co[2]:.4f} | inner n={len(sel_i)} centre=({ci[0]:.4f},{ci[1]:.4f}) R={ci[2]:.4f}") +# ---- pavement slope (top/bottom envelopes) per station +for st in SEC: + P,_=pts_of(st, lambda g,i: g=='노면(계획고)') + bins=np.arange(P[:,0].min(),P[:,0].max()+0.25,0.25) + tops=[];bots=[] + for a,b in zip(bins[:-1],bins[1:]): + m=(P[:,0]>=a)&(P[:,0]0: tops.append((P[m,0].mean(),P[m,1].max())); bots.append((P[m,0].mean(),P[m,1].min())) + tops=np.array(tops); bots=np.array(bots) + mt=np.polyfit(tops[:,0],tops[:,1],1); mb=np.polyfit(bots[:,0],bots[:,1],1) + fits[st]['slope_top']=mt[0]; fits[st]['slope_bot']=mb[0] + print(f"st {st}: PAV top slope dv/du={mt[0]:.5f} (S={mt[0]*100:.3f}%), bottom slope={mb[0]:.5f}; top@u=0 {mt[1]:.4f}, bottom@u=0 {mb[1]:.4f}; u-range [{P[:,0].min():.3f},{P[:,0].max():.3f}]") +json.dump(fits, open('/tmp/verify_daesan2_run/fits.json','w')) diff --git a/scripts/verify_geom/slice.py b/scripts/verify_geom/slice.py new file mode 100644 index 0000000..791b4a7 --- /dev/null +++ b/scripts/verify_geom/slice.py @@ -0,0 +1,77 @@ +import sys, json, pickle, math +sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages'] +from build123d import import_step, Edge, Shape +from OCP.BRepAlgoAPI import BRepAlgoAPI_Section +from OCP.gp import gp_Pln, gp_Pnt, gp_Dir +from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_EDGE +from OCP.TopoDS import TopoDS +import numpy as np +solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids() +mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping'] +caps = json.load(open('/tmp/verify_daesan2_run/caps.json')) +ch = json.load(open('/tmp/verify_daesan2_run/chain.json')) +lin = ch['lin'] +starts = {k: np.array(v) for k,v in ch['starts'].items()} +ends = {k: np.array(v) for k,v in ch['ends'].items()} +# station knots +S = [caps[k]['from_sta'] for k in lin] + [caps[lin[-1]]['to_sta']] +P = [starts[k] for k in lin] + [ends[lin[-1]]] +def plane_at(s): + for i in range(len(lin)): + if S[i] <= s <= S[i+1]: + f=(s-S[i])/(S[i+1]-S[i]) + Q=(1-f)*P[i]+f*P[i+1] + T=(P[i+1]-P[i])[:2]; T=T/np.linalg.norm(T) + return Q, T, lin[i] + raise ValueError(s) +def slice_solid(idx, Q, T): + nrm=np.array([T[0],T[1],0.0]) + pl=gp_Pln(gp_Pnt(*Q), gp_Dir(*nrm)) + face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face() + sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build() + sh=sec.Shape() + polys=[] + exp=TopExp_Explorer(sh, TopAbs_EDGE) + while exp.More(): + e=Shape.cast(exp.Current()) + L=e.length; N=max(2,int(L/0.02)) + pts=[e.position_at(i/N) for i in range(N+1)] + polys.append([[p.X,p.Y,p.Z] for p in pts]) + exp.Next() + return polys +out={} +stations=[4745.0005, 5100.0, 5469.9995] +for s in stations: + Q,T,lk=plane_at(s) + eh=np.array([-T[1],T[0]]) + print(f"== station {s}: comp {lk}, Q=({Q[0]:.4f},{Q[1]:.4f},{Q[2]:.4f}) T=({T[0]:.5f},{T[1]:.5f}) ") + out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'comps':{}} + for cid,info in caps.items(): + if not (info['from_sta'] <= s <= info['to_sta']): continue + polys=slice_solid(mp[cid], Q, T) + uv=[] + for pl_ in polys: + arr=np.array(pl_) + dx=arr[:,:2]-Q[:2] + u=dx@eh; v=arr[:,2] + uv.append(np.stack([u,v],1).tolist()) + out[str(s)]['comps'][cid]={'group':info['group'],'polys':uv} + allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2)) + if info['group'] in ('라이닝','굴착면','노면(계획고)','기층·인버트(가정)','공동구(가정)') or cid.startswith('DRN') or 'DRN' in cid or 'HI' in cid: + pass + # summary per group + import collections + summ=collections.defaultdict(list) + for cid,v in out[str(s)]['comps'].items(): + summ[v['group']].append(cid) + for g,cids in summ.items(): + allp=[p for cid in cids for poly in out[str(s)]['comps'][cid]['polys'] for p in poly] + A=np.array(allp) if allp else np.zeros((0,2)) + if len(A): + print(f" {g}: n_comp={len(cids)} n_pts={len(A)} u[{A[:,0].min():.3f},{A[:,0].max():.3f}] v[{A[:,1].min():.3f},{A[:,1].max():.3f}] ids={cids[:4]}") + else: + print(f" {g}: n_comp={len(cids)} EMPTY ids={cids[:4]}") +json.dump(out, open('/tmp/verify_daesan2_run/sections_raw.json','w')) +print("saved sections_raw.json") diff --git a/scripts/verify_solids_step3.py b/scripts/verify_solids_step3.py new file mode 100644 index 0000000..56210d8 --- /dev/null +++ b/scripts/verify_solids_step3.py @@ -0,0 +1,149 @@ +#!/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))