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:
1 parent
133db4e3f7
commit
3638249c43
9 files changed
+1272
No files matched your search
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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'))
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
Reference in new issue
Block a user