chore(verify): store Step 3 solid-model verification scripts and geometry results

- scripts/verify_solids_step3.py: deterministic checks M1-M6, S1-S2, G1 (9/9 PASS)
- scripts/verify_geom/: independent DXF-vs-STEP section deviation scripts (agent run)
- data/verify_geom_hd2_results.json: Hausdorff results at 4+745, 5+100, 5+470
Judgement: DOC/05_검증/검증_본선3D솔리드모델_2026-10-22_v0.1.00.md (FAIL 3 items to Step 4)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5Cun3RHQKna9qxE6ybZ4A
This commit is contained in:
Claude committed 2026-10-22 00:51:01 +00:00
1 parent 133db4e3f7
commit 3638249c43
9 files changed
+1272

No files matched your search

+656
View File
@@ -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
]
}
}
+42
View File
@@ -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 d<bd: bd=d;best=j
if bd<0.02:
used.add(best); mapping[c['id']]=best
else:
mapping[c['id']]=None
print("matched", sum(v is not None for v in mapping.values()), "of", len(comps), "unused solids", len(solids)-len(used))
print("first 10 mapping", list(mapping.items())[:10])
pickle.dump({'mapping':mapping}, open('/tmp/verify_daesan2_run/mapping.pkl','wb'))
# faces of solid 0 (LIN_01)
for idx in [mapping['LIN_01_P-3']]:
s=solids[idx]
fs=s.faces()
print("solid",idx,"nfaces",len(fs), "vol", round(s.volume,3))
rows=[]
for f in fs:
gt=f.geom_type
c=f.center(); a=f.area
try:
n=f.normal_at(c)
except Exception as ex:
n=None
rows.append((gt, round(a,1), [round(x,3) for x in (c.X,c.Y,c.Z)], None if n is None else [round(x,4) for x in (n.X,n.Y,n.Z)]))
rows.sort(key=lambda r: -r[1])
for r in rows[:25]: print(r)
+69
View File
@@ -0,0 +1,69 @@
import sys
exec(open('/tmp/verify_daesan2_run/scripts/hd2.py').read().split('allres={}')[0])
from scipy.spatial import cKDTree
import numpy as np
extra={
'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'DRN_topL': seg(9026.3,15694.0,9476.3,15703.0), 'DRN_topR': seg(18776.3,15519.0,19226.3,15510.0),
}
REF.update(extra)
def dd(A,B):
d1,_=cKDTree(A).query(B); d2,_=cKDTree(B).query(A); return d1,d2
for st in ['4745.0005','5100.0','5469.9995']:
print("=== station", st, "sigma=+1 slope(dxf)", round(FIT[st]['slope_top'],5))
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,1); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,1)
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,1); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1)
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
# LIN full arcs (no clipping) and clipped to model extent
arcs=['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']
for k in arcs:
d,_=cKDTree(L).query(REF[k]); print(f" LIN {k:18s} max={d.max():7.1f} mm (at y_min_ref={REF[k][:,1].min():.0f})")
ymin_L=L[L[:,0]<C0[0]][:,1].min(); ymin_R=L[L[:,0]>C0[0]][:,1].min()
lr=np.vstack([REF[k] for k in arcs]); d,_=cKDTree(L).query(lr)
inside=((lr[:,0]<C0[0])&(lr[:,1]>=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(); eymR=E[E[:,0]>C0[0]][:,1].min()
er=np.vstack([REF[k] for k in ea]); d,_=cKDTree(E).query(er)
ins=((er[:,0]<C0[0])&(er[:,1]>=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]<x0+200)
if m.sum()==0: continue
xm=bw[m,0].mean(); tops.append((xm,bw[m,1].max())); bots.append((xm,bw[m,1].min()))
tops=np.array(tops); bots=np.array(bots)
et=np.abs(tops[:,1]-np.interp(tops[:,0],xs,top_line)); eb=np.abs(bots[:,1]-np.interp(bots[:,0],xs,bot_line))
print(f" BASE top envelope vs drawn top (15484->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")
+23
View File
@@ -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]<x0+25)
if m.sum()==0: continue
tt.append(bw[m,1].max()-top_line(x0)); bb.append(bw[m,1].min()-bot_line(x0))
tt=np.array(tt); bb=np.array(bb)
print(f"{st}: BASE top (pavement-bottom interface), interior 150mm from ends: model-drawn max|dev|={np.abs(tt).max():.1f} mm, mean={tt.mean():.1f} mm (signed mean)")
print(f"{st}: BASE bottom (drawn excavation floor 15334->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}")
+102
View File
@@ -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 d<best[0]: best=(d, Sb[k]+f*(Sb[k+1]-Sb[k]), k, f)
return best
sol_info={}
for i,s in enumerate(solids):
c=s.center(); d,st,k,f=station_of(np.array([c.X,c.Y]))
cid=solid_comp[i]; sol_info[i]=dict(cid=cid, group=comps[cid]['group'], st=st, lat=d)
# 3) pair within group by sorted mid
assign={}
groups=collections.defaultdict(list)
for i,v in sol_info.items(): groups[v['group']].append(i)
maxres=0
for g,idxs in groups.items():
cs=[c for c in comps.values() if c['group']==g]
idxs_s=sorted(idxs, key=lambda i: sol_info[i]['st'])
cs_s=sorted(cs, key=lambda c: (c['from_sta']+c['to_sta'])/2)
if len(idxs_s)!=len(cs_s): print("COUNT MISMATCH", g, len(idxs_s), len(cs_s))
for i,c in zip(idxs_s, cs_s):
mid=(c['from_sta']+c['to_sta'])/2
res=abs(sol_info[i]['st']-mid); maxres=max(maxres,res)
assign[i]=dict(id=c['id'], group=g, frm=c['from_sta'], to=c['to_sta'], mid_chain=sol_info[i]['st'], lat=sol_info[i]['lat'])
print("max residual solid-centre station vs comp mid (m):", round(maxres,3))
# lateral offset sanity
print("max lateral offset of solid centroid from chain (m):", round(max(v['lat'] for v in assign.values()),3))
# 4) slice
def slice_solid(idx, Q, T):
nrm=np.array([T[0],T[1],0.0])
pl=gp_Pln(gp_Pnt(float(Q[0]),float(Q[1]),0.0), gp_Dir(float(nrm[0]),float(nrm[1]),0.0))
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
polys=[]
exp=TopExp_Explorer(sec.Shape(), TopAbs_EDGE)
while exp.More():
e=Shape.cast(exp.Current()); L=e.length; N=max(2,int(L/0.02))
polys.append([[p.X,p.Y,p.Z] for p in (e.position_at(j/N) for j in range(N+1))])
exp.Next()
return polys
def plane_at(s):
k=int(np.searchsorted(Sb, s, side='right')-1); k=min(max(k,0),len(B)-2)
f=(s-Sb[k])/(Sb[k+1]-Sb[k])
Q=(1-f)*B[k]+f*B[k+1]; T=(B[k+1]-B[k]); T=T/np.linalg.norm(T)
return Q, T
out={}
for s in [4745.0005, 5100.0, 5469.9995]:
Q,T=plane_at(s); eh=np.array([-T[1],T[0]])
print(f"== station {s}: Q=({Q[0]:.3f},{Q[1]:.3f}) T=({T[0]:.5f},{T[1]:.5f})")
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'solids':{}}
cnt=collections.Counter()
for i,a in assign.items():
if not (a['frm'] <= s <= a['to']): continue
polys=slice_solid(i,Q,T)
uv=[]
for pl_ in polys:
arr=np.array(pl_); u=(arr[:,:2]-Q)@eh; uv.append(np.stack([u,arr[:,2]],1).tolist())
out[str(s)]['solids'][str(i)]={'id':a['id'],'group':a['group'],'polys':uv}
cnt[a['group']]+=1
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
if len(allp): print(f" {a['group']:<12} {a['id']:<16} nloops={len(uv)} u[{allp[:,0].min():.3f},{allp[:,0].max():.3f}] v[{allp[:,1].min():.3f},{allp[:,1].max():.3f}]")
else: print(" EMPTY", a['group'], a['id'])
print(" counts", dict(cnt))
json.dump(out, open('/tmp/verify_daesan2_run/sections_final.json','w'))
json.dump({str(i):a for i,a in assign.items()}, open('/tmp/verify_daesan2_run/assign.json','w'), ensure_ascii=False)
print("saved")
+110
View File
@@ -0,0 +1,110 @@
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
SEC=json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
FIT=json.load(open('/tmp/verify_daesan2_run/fits.json'))
C0=(14126.3,15305.0)
def pts(st, pred):
out=[]
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])
return np.vstack(out)
def arc(cx,cy,r,a0,a1,step=1.0):
if a1<a0: a1+=360
n=max(2,int(r*math.radians(a1-a0)/step)); th=np.radians(np.linspace(a0,a1,n))
return np.c_[cx+r*np.cos(th), cy+r*np.sin(th)]
def seg(x0,y0,x1,y1,step=1.0):
n=max(2,int(math.hypot(x1-x0,y1-y0)/step)); t=np.linspace(0,1,n); return np.c_[x0+(x1-x0)*t,y0+(y1-y0)*t]
def circ(cx,cy,r,step=1.0):
n=max(8,int(2*math.pi*r/step)); th=np.linspace(0,2*math.pi,n,endpoint=False); return np.c_[cx+r*np.cos(th),cy+r*np.sin(th)]
REF={
'LIN_crown_outer': arc(*C0,7282,40,140), 'LIN_crown_inner': arc(*C0,6982,40,140),
'LIN_haunchL_outer': arc(11959.9,17122.8,4454,140,204.67), 'LIN_haunchL_inner': arc(11621.3,17406.9,3712,140,195.56),
'LIN_haunchR_outer': arc(16105.0,16965.3,4699,336.35,40), 'LIN_haunchR_inner': arc(16479.6,17279.7,3910,344.38,40),
'LIN_footL': np.vstack([seg(8171.3,16410.9,8471.3,16410.9), seg(8471.3,16410.9,8621.3,16410.9), seg(8621.3,16410.9,8651.3,16000.9), seg(8651.3,16000.9,8771.3,15825.9), seg(8771.3,15825.9,8771.3,15750.9)]),
'LIN_footR': np.vstack([seg(20181.3,16226.9,19781.3,16226.9), seg(19781.3,16226.9,19631.3,16226.9), seg(19631.3,16226.9,19601.3,15816.9), seg(19601.3,15816.9,19481.3,15641.9), seg(19481.3,15641.9,19481.3,15566.9)]),
'EXC_crown': arc(*C0,7402,40,140), 'EXC_haunchL': arc(11959.9,17122.8,4574,140,203.98), 'EXC_haunchR': arc(16105.0,16965.3,4819,336.97,40),
'PAV_top': seg(9526.3,15764.0,18726.3,15580.0), 'PAV_bot': seg(9526.3,15484.0,18726.3,15300.0), 'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'BASE_top': seg(9526.3,15484.0,18726.3,15300.0), 'BASE_bot': seg(9526.3,15334.0,18726.3,15150.0),
'DRN_wallL1': seg(9101.3,14864.0,9101.3,15695.5), 'DRN_wallL2': seg(9401.3,14864.0,9401.3,15701.5), 'DRN_botL': seg(9101.3,14864.0,9401.3,14864.0), 'DRN_pipeL': circ(9251.3,15064.0,150),
'DRN_wallR1': seg(18851.3,14680.0,18851.3,15517.5), 'DRN_wallR2': seg(19151.3,14680.0,19151.3,15511.5), 'DRN_botR': seg(18851.3,14680.0,19151.3,14680.0), 'DRN_pipeR': circ(19001.3,14880.0,150),
}
def to_dxf(P, st, sig):
uc,vc,_=FIT[st]['outer']
return np.c_[C0[0]+sig*(P[:,0]-uc)*1000.0, C0[1]+(P[:,1]-vc)*1000.0]
def dist_stats(M, R, label):
"""M model pts (dxf frame), R reference pts. Returns d(ref->model) 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: x<C0x, R: x>C0x) and above model's lowest y on that side minus 50mm
sm = M[(M[:,0]<C0[0])] if side=='L' else M[(M[:,0]>C0[0])]
sr = R[(R[:,0]<C0[0])] if side=='L' else 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]<ymin,1].min())),round(float(sr[sr[:,1]<ymin,1].max()))]
return keep, dropped, rng
out={}
# Lining: arcs and foot lines; clip below model extent per side
lin_ref=np.vstack([REF[k] for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']])
keepL,dL,rL=side_clip(lin_ref[lin_ref[:,0]<C0[0]],L,'L'); keepR,dR_,rR=side_clip(lin_ref[lin_ref[:,0]>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)
+44
View File
@@ -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]<b)
if m.sum()>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'))
+77
View File
@@ -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")
+149
View File
@@ -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> <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))