Files
Pier_v1/Code/교각모델링/verify_rhino.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

279 lines
15 KiB
Python

"""Stage 5: verify the built Rhino model against drawing values (run inside Rhino 8).
Independence: expected values are typed from drawing dimension TEXT and DXF entity
coordinates (see EXPECTED / DXF), NOT read from params.json used by the generator.
Writes 검증/verify_result.json (path passed as RESULT_PATH).
Checks C1..C6 follow 설계서 v0.1.01 §4.
"""
import json
import math
import Rhino.Geometry as rg
doc = __rhino_doc__
def ALL_OBJECTS():
"""All live objects incl. hidden/locked (default doc.Objects skips hidden ones; 2026-10-07 P1-FT hidden)."""
from Rhino.DocObjects import ObjectEnumeratorSettings
s = ObjectEnumeratorSettings()
s.HiddenObjects = True
s.LockedObjects = True
return list(doc.Objects.GetObjectList(s))
TOL_MM = 1.0
# Drawing dimension texts (D1 unless noted), converted to mm / EL x 1000.
EXPECTED = {
"footing": dict(dx=9350, dy=7800, z0=26208, z1=26208 + 2200), # '9.350' '7.800' 'EL. 26.208' '2.200'
"lean": dict(dx=9350 + 200, dy=7800 + 200, z0=26208 - 100, z1=26208), # footing +100 each side, 100 thick
"column": dict(d=2000, z0=26208 + 2200, z1=26208 + 2200 + 12003), # '2.000' '12.003'
"coping": dict(dx=12400, dy=3100, z_bottom=43411 - 1500 - 1500, # '12.400' '3.100' 'EL. 43.411' '1.500' x2
z_tip_bottom=43411 - 1500, z_top_left=43411, z_top_right=43698, z_top_center=43555,
flat_half=6200 - 5100 - 0, # 12.400/2 - '5.100' -> 1100
top_half=6200 - 200, radius=12000), # '200', 'R=12.000'
"piles": dict(nx=6, ny=5, pitch=1550, edge=800, d=600, length=24400), # '5@1.550' '4@1.550' '800' 'Φ600' 'L=24.4M'
}
# DXF ARC 1C4BF centre (5207.5, 6039.2) in front-view coords; datum x=12159.4 (CS-CNTL 1C546),
# y=16515.6 <-> EL 40.411 (coping bottom line 1C4BD).
DXF_LEFT_ARC_CENTER = (5207.5 - 12159.4, 40411 + (6039.2 - 16515.6))
DXF_RIGHT_ARC_CENTER = (19111.2 - 12159.4, 40411 + (6039.2 - 16515.6))
def objs(prefix):
return [o for o in ALL_OBJECTS() if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
def brep(o):
return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry)
def vol(b):
return rg.VolumeMassProperties.Compute(b).Volume
def bbox(b):
return b.GetBoundingBox(True)
results = []
def check(cid, name, expected, actual, tol=TOL_MM):
ok = abs(expected - actual) <= tol
results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3),
diff=round(actual - expected, 3), tol=tol, result="PASS" if ok else "FAIL"))
def flag(cid, name, ok, detail):
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
# --- C7 bearing seats (added for model v0.1.01; values typed from D2 ELEVATION TABLE and D1 dims) ---
# NO: (EL.'X', h1, h2, A, B); NO.1-5 front row 3500kN, NO.6-10 rear row 2800kN, girder line = (NO-1)%5+1
TABLE = {1: (43625, 164, 50, 700, 640), 2: (43697, 164, 50, 700, 640), 3: (43768, 164, 50, 700, 790),
4: (43840, 164, 50, 700, 640), 5: (43912, 164, 50, 700, 640), 6: (43600, 139, 50, 700, 540),
7: (43672, 139, 50, 700, 540), 8: (43744, 139, 50, 700, 650), 9: (43816, 139, 50, 700, 540),
10: (43887, 139, 50, 700, 540)}
STEP_TOP = {1: 43411, 2: 43483, 3: 43555, 4: 43627, 5: 43698} # D1 EL texts + drawn steps
STEP_X = {1: (-6000, -3801), 2: (-3801, -1296), 3: (-1296, 1210), 4: (1210, 3715), 5: (3715, 6000)}
ROW_Y = {"front": -(1550 - 852), "rear": 1550 - 950} # D1 평면도 S1/S2 + D2 TABLE S1
PITCH = 7516 / 3 # D1 '3@2.505=7.516'
def fingerprints():
fp = {}
for o in ALL_OBJECTS():
oid = o.Attributes.GetUserString("id")
if oid and not oid.startswith(("P1-BS", "P1-BP", "P1-R")): # rebar (v0.1.02) checked by verify_rebar.py
b = brep(o)
bb = bbox(b)
fp[oid] = [round(v, 3) for v in (bb.Min.X, bb.Min.Y, bb.Min.Z, bb.Max.X, bb.Max.Y, bb.Max.Z,
vol(b) / 1e6)]
return fp
def check_seats(cp):
seats = {int(o.Attributes.GetUserString("id")[5:]): brep(o) for o in objs("P1-BS")}
pads = {int(o.Attributes.GetUserString("id")[5:]): brep(o) for o in objs("P1-BP")}
flag("C7", "pedestal / pad counts", sorted(seats) == list(range(1, 11)) and sorted(pads) == list(range(1, 11)),
f"seats={len(seats)} pads={len(pads)}")
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in list(seats.values()) + list(pads.values())),
f"{sum(b.IsSolid for b in list(seats.values()) + list(pads.values()))}/20")
pad_x = {}
for no, (elx, h1, h2, a, b) in TABLE.items():
g = (no - 1) % 5 + 1
row = "front" if no <= 5 else "rear"
s, p = seats[no], pads[no]
sb, pb = bbox(s), bbox(p)
check("C7", f"NO.{no} pedestal bottom = step {g}", STEP_TOP[g], sb.Min.Z)
check("C7", f"NO.{no} pedestal top = step + h1", STEP_TOP[g] + h1, sb.Max.Z)
check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z)
flag("C7", f"NO.{no} pedestal within step {g} / coping width",
STEP_X[g][0] - TOL_MM <= sb.Min.X and sb.Max.X <= STEP_X[g][1] + TOL_MM
and -1550 - TOL_MM <= sb.Min.Y and sb.Max.Y <= 1550 + TOL_MM,
f"x {sb.Min.X:.1f}..{sb.Max.X:.1f} y {sb.Min.Y:.1f}..{sb.Max.Y:.1f}")
check("C7", f"NO.{no} pedestal half area (m2)", 1.25 * 1.2, vol(s) / h1 / 1e6, tol=1.5 * 0.002)
pv = h2 / 6 * ((a + 200) * (b + 200) + (a + 100) * (b + 100) + 4 * (a + 150) * (b + 150))
check("C7", f"NO.{no} pad volume (prismatoid A,B)", pv, vol(p), tol=pv * 0.001)
c = rg.VolumeMassProperties.Compute(p).Centroid
check("C7", f"NO.{no} pad centre Y (S1)", ROW_Y[row], c.Y, tol=2.0)
pad_x.setdefault(row, {})[g] = c.X
check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0,
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(s, cp, 0.01) or [])), tol=1.0)
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0,
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(p, s, 0.01) or [])), tol=1.0)
for row, xs in pad_x.items():
for g in range(1, 5):
check("C7", f"{row} pad pitch girder {g}-{g + 1}", PITCH, xs[g + 1] - xs[g], tol=1.0)
# Rear-row bearing centres: D1 평면도 dims '1.232' + n x '2.505' from the coping bottom tip (X=-6200)
for g in range(1, 6):
check("C7", f"rear pad centre X girder {g} (dim 1.232+2.505n)", -6200 + 1232 + (g - 1) * PITCH,
pad_x["rear"][g], tol=2.0)
# Pedestal positions: centroid pitch per row (catches a single moved pedestal)
for row, nos in (("front", range(1, 6)), ("rear", range(6, 11))):
cxs = [rg.VolumeMassProperties.Compute(seats[n]).Centroid.X for n in nos]
for g in range(4):
check("C7", f"{row} pedestal pitch girder {g + 1}-{g + 2}", PITCH, cxs[g + 1] - cxs[g], tol=1.0)
if "FINGERPRINT_REF" in globals():
ref = json.load(open(FINGERPRINT_REF, encoding="utf-8"))
now = fingerprints()
worst = max(abs(a - b) for k in ref for a, b in zip(ref[k], now.get(k, [1e9] * 7)))
flag("C7", "existing 34 objects unchanged (bbox mm, volume 1e6 mm3)",
set(ref) == set(now) and worst <= 0.001, f"{len(now)}/{len(ref)} objects, max diff {worst}")
def main():
if "FINGERPRINT_OUT" in globals():
with open(FINGERPRINT_OUT, "w", encoding="utf-8") as f:
json.dump(fingerprints(), f, indent=0)
print("fingerprints written")
return
lc, ft, cl, cp = (brep(objs(p)[0]) for p in ("P1-LC", "P1-FT", "P1-CL", "P1-CP"))
piles = [brep(o) for o in objs("P1-PL")]
# C5 object population
counts = {p: len(objs(p)) for p in ("P1-LC", "P1-FT", "P1-CL", "P1-CP", "P1-PL")}
flag("C5", "object counts", counts == {"P1-LC": 1, "P1-FT": 1, "P1-CL": 1, "P1-CP": 1, "P1-PL": 30},
json.dumps(counts))
# C2 closed solids
all_b = [lc, ft, cl, cp] + piles
flag("C2", "IsSolid all 34", all(b.IsSolid for b in all_b), f"{sum(b.IsSolid for b in all_b)}/34")
# C1 bounding dimensions
e = EXPECTED
for key, b in (("footing", ft), ("lean", lc)):
bb = bbox(b)
check("C1", f"{key} X", e[key]["dx"], bb.Max.X - bb.Min.X)
check("C1", f"{key} Y", e[key]["dy"], bb.Max.Y - bb.Min.Y)
check("C1", f"{key} Zbot", e[key]["z0"], bb.Min.Z)
check("C1", f"{key} Ztop", e[key]["z1"], bb.Max.Z)
check("C1", f"{key} centre X", 0, (bb.Max.X + bb.Min.X) / 2)
check("C1", f"{key} centre Y", 0, (bb.Max.Y + bb.Min.Y) / 2)
bb = bbox(cl)
check("C1", "column diameter X", e["column"]["d"], bb.Max.X - bb.Min.X)
check("C1", "column diameter Y", e["column"]["d"], bb.Max.Y - bb.Min.Y)
check("C1", "column Zbot", e["column"]["z0"], bb.Min.Z)
check("C1", "column Ztop", e["column"]["z1"], bb.Max.Z)
bb = bbox(cp)
check("C1", "coping length X", e["coping"]["dx"], bb.Max.X - bb.Min.X)
check("C1", "coping width Y", e["coping"]["dy"], bb.Max.Y - bb.Min.Y)
check("C1", "coping Zbot", e["coping"]["z_bottom"], bb.Min.Z)
check("C1", "coping Ztop (right end)", e["coping"]["z_top_right"], bb.Max.Z)
p = e["piles"]
xs = sorted({round((bbox(b).Min.X + bbox(b).Max.X) / 2, 1) for b in piles})
ys = sorted({round((bbox(b).Min.Y + bbox(b).Max.Y) / 2, 1) for b in piles})
flag("C1", "pile grid", len(xs) == p["nx"] and len(ys) == p["ny"], f"x={xs} y={ys}")
check("C1", "pile edge X", p["edge"], xs[0] - (-e["footing"]["dx"] / 2))
check("C1", "pile edge Y", p["edge"], ys[0] - (-e["footing"]["dy"] / 2))
for a, b2 in zip(xs, xs[1:]):
check("C1", "pile pitch X", p["pitch"], b2 - a)
for a, b2 in zip(ys, ys[1:]):
check("C1", "pile pitch Y", p["pitch"], b2 - a)
pb = bbox(piles[0])
check("C1", "pile diameter", p["d"], pb.Max.X - pb.Min.X)
check("C1", "pile length", p["length"], pb.Max.Z - pb.Min.Z)
check("C1", "pile head embedment", 100, pb.Max.Z - e["footing"]["z0"])
# C4 coping front section at Y=0 (re-projection to drawing)
plane = rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis)
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, plane, 0.01)
sec = rg.Curve.JoinCurves(curves, 0.1)
flag("C4", "section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)")
c = e["coping"]
def z_at(x, upper):
line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 60000))
zs = sorted(ev.PointA.Z for ev in rg.Intersect.Intersection.CurveCurve(sec[0], line, 0.01, 0.01))
return zs[-1] if upper else zs[0]
check("C4", "bottom flat at X=0", c["z_bottom"], z_at(0, False))
check("C4", "bottom flat at X=+1099", c["z_bottom"], z_at(c["flat_half"] - 1, False))
check("C4", "bottom at X=-6199 (tip)", c["z_tip_bottom"], z_at(-c["top_half"] - 199, False), tol=2.0)
check("C4", "top left step (X=-5000)", c["z_top_left"], z_at(-5000, True))
check("C4", "top centre step (X=0)", c["z_top_center"], z_at(0, True))
check("C4", "top right step (X=5000)", c["z_top_right"], z_at(5000, True))
# soffit arc: sample points between X=1200..6100 must lie on circle centred at DXF arc centre
worst = 0.0
for side, (cx, cz) in ((1, DXF_RIGHT_ARC_CENTER), (-1, DXF_LEFT_ARC_CENTER)):
for k in range(1, 50):
x = side * (c["flat_half"] + 100 + k * 98)
zb = z_at(x, False)
worst = max(worst, abs(math.hypot(x - cx, zb - cz) - c["radius"]))
check("C4", "soffit radius max deviation vs DXF ARC centre (98 samples)", 0, worst)
# C3 joints and overlaps
def inter_vol(a, b):
r = rg.Brep.CreateBooleanIntersection(a, b, 0.01)
return sum(vol(x) for x in r) if r else 0.0
check("C3", "LC top = FT bottom", bbox(lc).Max.Z, bbox(ft).Min.Z, tol=0.01)
check("C3", "FT top = CL bottom", bbox(ft).Max.Z, bbox(cl).Min.Z, tol=0.01)
check("C3", "CL top = CP bottom", bbox(cl).Max.Z, bbox(cp).Min.Z, tol=0.01)
check("C3", "overlap LC-FT (mm3)", 0, inter_vol(lc, ft), tol=1.0)
check("C3", "overlap FT-CL (mm3)", 0, inter_vol(ft, cl), tol=1.0)
check("C3", "overlap CL-CP (mm3)", 0, inter_vol(cl, cp), tol=1.0)
lc_pile = sum(inter_vol(lc, b) for b in piles)
check("C3", "overlap LC-piles (mm3)", 0, lc_pile, tol=1.0)
emb = 30 * math.pi * 300 ** 2 * 100
ft_pile = sum(inter_vol(ft, b) for b in piles)
check("C3", "intended overlap FT-piles (mm3)", emb, ft_pile, tol=emb * 0.001)
# column footprint inside coping flat bottom: coping bottom plane area >= column section
flag("C3", "column within coping flat zone", e["column"]["d"] / 2 <= c["flat_half"]
and e["column"]["d"] / 2 <= c["dy"] / 2, "r=1000 <= 1100 (X), 1550 (Y)")
# C6 volumes vs analytic formulas
check("C6", "footing volume m3", 9.350 * 7.800 * 2.200, vol(ft) / 1e9, tol=9.35 * 7.8 * 2.2 * 0.001)
check("C6", "column volume m3", math.pi * 1.0 ** 2 * 12.003, vol(cl) / 1e9, tol=37.71 * 0.001)
lv = 9.55 * 8.0 * 0.1 - 30 * math.pi * 0.3 ** 2 * 0.1
check("C6", "lean volume m3", lv, vol(lc) / 1e9, tol=lv * 0.001)
# coping: straight-edged polygon area (with arc chords) minus 2 circular segments, x width
poly = [(-1100, 40411), (1100, 40411), (6200, 41911), (6000, 43698), (3715, 43698), (3715, 43627),
(1210, 43627), (1210, 43555), (-1296, 43555), (-1296, 43483), (-3801, 43483), (-3801, 43411),
(-6000, 43411), (-6200, 41911)]
area = 0.5 * abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1])))
chord = math.hypot(6200 - 1100, 41911 - 40411)
theta = 2 * math.asin(chord / 2 / 12000)
seg = 12000 ** 2 / 2 * (theta - math.sin(theta))
cv = (area - 2 * seg) * 3100 / 1e9
check("C6", "coping volume m3", cv, vol(cp) / 1e9, tol=cv * 0.001)
if objs("P1-BS") or objs("P1-BP"):
check_seats(cp)
summary = dict(total=len(results), passed=sum(r["result"] == "PASS" for r in results),
failed=sum(r["result"] == "FAIL" for r in results),
volumes_m3={k: round(vol(b) / 1e9, 3) for k, b in
(("lean", lc), ("footing", ft), ("column", cl), ("coping", cp))},
pile_volume_each_m3=round(vol(piles[0]) / 1e9, 3))
out = dict(summary=summary, results=results)
with open(RESULT_PATH, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=1)
print(json.dumps(summary, ensure_ascii=False))
for r in results:
if r["result"] == "FAIL":
print("FAIL", r)
main()