- 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>
154 lines
10 KiB
Python
154 lines
10 KiB
Python
"""params <-> expected consistency (C9, 2026-10-20): values that describe the SAME physical quantity in params.json (what the model is built from) and
|
|
expected.json (what the checkers compare against) must agree. The Rhino gates find every mismatch too, but only after ~150 s; this check finds the
|
|
stale ones (a value that changed in one file but not in the other) before any gate is run. Origin: Ungok P3 / P4 -- the first gate runs failed almost
|
|
entirely on values that were updated in params but not in expected (coping steps, pile pitch, hoop chain, B9 grid, tier EL ...) or the other way round.
|
|
|
|
Not an independent check: when both files were written from the same drawing reading they agree trivially. It only says "the two files tell one story".
|
|
Rules apply to the Ungok layout (coping steps, footing rebar rows ...); a rule whose paths are missing in either file is skipped, so other pier types
|
|
are not affected. Also contains the point-list rule of improvement #6: `coping_rebar.c14.hooks_up` must name existing `c14.points` (a point list keyed by
|
|
coordinates breaks silently when the points move).
|
|
"""
|
|
import re
|
|
|
|
TOL = 1.5 # mm (drawing values are rounded to 1 mm, CAD vertices to 0.1)
|
|
|
|
|
|
def _get(doc, path):
|
|
x = doc
|
|
for p in path.split("."):
|
|
if isinstance(x, list):
|
|
if not re.fullmatch(r"-?\d+", p) or int(p) >= len(x):
|
|
return None
|
|
x = x[int(p)]
|
|
elif isinstance(x, dict):
|
|
if p not in x:
|
|
return None
|
|
x = x[p]
|
|
else:
|
|
return None
|
|
return x
|
|
|
|
|
|
def _num(v):
|
|
"""number of a value that may be a string like '11.856' (schedule cells are metres) -> None when not numeric."""
|
|
if isinstance(v, bool) or v is None:
|
|
return None
|
|
if isinstance(v, (int, float)):
|
|
return float(v)
|
|
try:
|
|
return float(str(v).replace(",", ""))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _rule(rid, text, p, e, tol=TOL, f=None, g=None):
|
|
"""one pairwise rule: params path p, expected path e; f / g transform the values before comparing."""
|
|
return dict(id=rid, text=text, p=p, e=e, tol=tol, f=f, g=g)
|
|
|
|
|
|
def _steps(n=5):
|
|
for i in range(n):
|
|
yield i
|
|
|
|
|
|
def rules(params, expected):
|
|
R = []
|
|
A = R.append
|
|
# footing, lean, column, piles
|
|
for a, b, t in (("size_x", "dx", "기초 X"), ("size_y", "dy", "기초 Y"), ("z_bottom", "z0", "기초 하면 EL"), ("thickness", "thickness", "기초 두께")):
|
|
A(_rule("footing." + a, t, "footing." + a, "G1.footing." + b))
|
|
A(_rule("column.height", "기둥 높이", "column.height", "G1.column.height"))
|
|
A(_rule("column.d", "기둥 지름", "column.diameter", "G1.column.d"))
|
|
for a, b, t in (("count_x", "nx", "말뚝 X 개수"), ("count_y", "ny", "말뚝 Y 개수"), ("length", "length", "말뚝 길이"), ("diameter", "d", "말뚝 지름"), ("edge", "edge", "말뚝 연단")):
|
|
A(_rule("piles." + a, t, "piles." + a, "G1.piles." + b, tol=0 if a.startswith("count") else TOL))
|
|
pp = (params.get("piles") or {})
|
|
for ax in ("x", "y"):
|
|
pit = f"piles.pitch_{ax}" if f"pitch_{ax}" in pp else "piles.pitch"
|
|
A(_rule(f"piles.pitch_{ax}", f"말뚝 {ax.upper()} 피치", pit, f"G1.piles.pitch_{ax}"))
|
|
# coping outline: steps, soffit flat
|
|
for i in _steps():
|
|
A(_rule(f"coping.step{i + 1}.z", f"코핑 윗면 {i + 1}단 높이", f"coping.top_steps.{i}.z", f"G1.seats.step_top.{i + 1}"))
|
|
A(_rule(f"coping.step{i + 1}.z/G4", f"코핑 윗면 {i + 1}단 높이(G4)", f"coping.top_steps.{i}.z", f"G4.step_top.{i + 1}"))
|
|
A(_rule(f"coping.step{i + 1}.x0", f"코핑 {i + 1}단 시작 x", f"coping.top_steps.{i}.x0", f"G1.seats.step_x.{i + 1}.0"))
|
|
A(_rule(f"coping.step{i + 1}.x1", f"코핑 {i + 1}단 끝 x", f"coping.top_steps.{i}.x1", f"G1.seats.step_x.{i + 1}.1"))
|
|
for nm, i in (("left", 0), ("center", 2), ("right", 4)):
|
|
A(_rule(f"coping.z_top_{nm}", f"코핑 윗면 {nm} 높이", f"coping.top_steps.{i}.z", f"G1.coping.z_top_{nm}"))
|
|
A(_rule("coping.dx", "코핑 길이", "coping.length_bottom_tip_to_tip", "G1.coping.dx"))
|
|
A(_rule("coping.dy", "코핑 폭", "coping.width_y", "G1.coping.dy"))
|
|
A(_rule("coping.flat", "코핑 하면 평평부 높이", "coping.z_bottom_flat", "G1.dxf_arc_centres.datum_el"))
|
|
A(_rule("coping.outline.flat", "코핑 외곽 하면 높이", "coping.z_bottom_flat", "G1.coping_outline.0.1"))
|
|
# bearings
|
|
A(_rule("bearing.pitch", "받침 피치", "bearing_seats.pitch_x", "G1.seats.pitch_chain", tol=1.0, g=lambda v: v[1] / v[0] if isinstance(v, list) and len(v) == 2 and v[0] else None))
|
|
A(_rule("bearing.row_y.front", "받침 앞열 y", "bearing_seats.pad_rows.front.centre_rel_girder5.1", "G1.seats.row_y.front", tol=1.5))
|
|
A(_rule("bearing.row_y.rear", "받침 뒤열 y", "bearing_seats.pad_rows.rear.centre_rel_girder5.1", "G1.seats.row_y.rear", tol=1.5))
|
|
# coping rebar
|
|
for t, ms in (("1", ("C1", "C4")), ("2", ("C2", "C5")), ("3", ("C3", "C6"))):
|
|
for m in ms:
|
|
A(_rule(f"coping_rebar.tier{t}.{m}", f"코핑 상단 철근 {m} 높이", f"coping_rebar.top.tiers.{t}.z", f"G4.tier_z.{m}"))
|
|
A(_rule("coping_rebar.c15", "C15 높이", "coping_rebar.c15.z", "G4.c15_top"))
|
|
A(_rule("coping_rebar.c16", "C16 높이", "coping_rebar.c16.z", "G4.c16_top"))
|
|
A(_rule("coping_rebar.c10_bottom", "C10 스터럽 하단 높이", "coping_rebar.bottom.z_flat", "G4.c10_bottom"))
|
|
lv = (params.get("coping_rebar") or {}).get("loops", {}).get("levels")
|
|
if lv:
|
|
A(_rule("coping_rebar.loop.first", "루프 C12 시작 높이", "coping_rebar.loops.levels.0", "G4.loop_z.C12.start"))
|
|
for k in range(3):
|
|
A(_rule(f"coping_rebar.loop.c13.{k}", f"루프 C13 {k + 1}번째 높이", f"coping_rebar.loops.levels.{6 + k}", f"G4.loop_z.C13.{k}"))
|
|
# column rebar chains
|
|
A(_rule("column_rebar.hoop.radius", "후프 반경", "column_rebar.hoops.radius", "G2.r_hoop"))
|
|
A(_rule("column_rebar.hoop.count", "후프 개수", "column_rebar.hoops.levels", "G2.hoop_chain.groups",
|
|
tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda gs: 1 + sum(int(n) for n, _ in gs) if isinstance(gs, list) else None))
|
|
A(_rule("column_rebar.tie.count", "결속철근 단 수", "column_rebar.ties.levels", "G2.tie_chain.groups",
|
|
tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda gs: 1 + sum(int(n) for n, _ in gs) if isinstance(gs, list) else None))
|
|
A(_rule("column_rebar.p1.top", "주철근 P1 직선 길이", "column_rebar.main_bars.p1.straight_len", "G2.p1_top_parts.2"))
|
|
# footing rebar rows (positions include the two end bars)
|
|
for lay, row in (("bottom_Y1", "Y_BOT"), ("bottom_X2", "X_BOT"), ("bottom_Y3", "Y_250"), ("bottom_X4", "X_250"), ("top_Y", "Y_250"), ("top_X", "X_250")):
|
|
A(_rule(f"footing_rebar.{lay}.count", f"기초 철근 {lay} 개수", f"footing_rebar.layers.{lay}.pos", f"G3.rows.{row}.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None, g=lambda n: n + 2 if isinstance(n, (int, float)) else None))
|
|
A(_rule(f"footing_rebar.{lay}.end", f"기초 철근 {lay} 끝", f"footing_rebar.layers.{lay}.pos.-1", f"G3.rows.{row}.ends.1"))
|
|
A(_rule("footing_rebar.b9.x", "B9 격자 x 개수", "footing_rebar.b9.grid_x", "G3.b9_grid.x.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None))
|
|
A(_rule("footing_rebar.b9.y", "B9 격자 y 개수", "footing_rebar.b9.grid_y", "G3.b9_grid.y.n", tol=0, f=lambda v: len(v) if isinstance(v, list) else None))
|
|
A(_rule("footing_rebar.b9.x0", "B9 격자 x 시작", "footing_rebar.b9.grid_x.0", "G3.b9_grid.x.start"))
|
|
A(_rule("footing_rebar.b9.y0", "B9 격자 y 시작", "footing_rebar.b9.grid_y.0", "G3.b9_grid.y.start"))
|
|
A(_rule("footing_rebar.b10.x", "B10 x", "footing_rebar.b10.size_x", "G3.b10_size.0"))
|
|
A(_rule("footing_rebar.b10.y", "B10 y", "footing_rebar.b10.size_y", "G3.b10_size.1"))
|
|
# schedule copies
|
|
for m in ("P1", "P2", "P3", "P4", "P5", "P6"):
|
|
A(_rule(f"schedule.{m}.n", f"재료표 {m} 개수", f"column_rebar.schedule.{m}.n", f"G2.schedule_n.{m}", tol=0))
|
|
A(_rule(f"schedule.{m}.L", f"재료표 {m} 길이", f"column_rebar.schedule.{m}.L", f"G2.schedule_L.{m}", f=lambda v: None if _num(v) is None else _num(v) * 1000))
|
|
for i in range(1, 13):
|
|
m = f"B{i}"
|
|
sch = (params.get("footing_rebar") or {}).get("schedule", {}).get(m)
|
|
if sch is None:
|
|
continue
|
|
# raw schedule rows: B9 has an extra count column in newer drawings; counts are compared where the row has one
|
|
A(_rule(f"schedule.{m}.n", f"재료표 {m} 개수", f"footing_rebar.schedule.{m}.3", f"G3.schedule.{m}.1", tol=0, f=lambda v: _num(v) if _num(v) is not None and float(v) == int(float(v)) else None))
|
|
return R
|
|
|
|
|
|
def check(params, expected):
|
|
"""-> list of {id, text, p, e, pv, ev, ok} for every rule whose two values exist, plus the point-list rule."""
|
|
out = []
|
|
for r in rules(params, expected):
|
|
pv, ev = _get(params, r["p"]), _get(expected, r["e"])
|
|
if r["f"]:
|
|
pv = r["f"](pv)
|
|
if r["g"]:
|
|
ev = r["g"](ev)
|
|
a, b = _num(pv), _num(ev)
|
|
if a is None or b is None:
|
|
continue
|
|
out.append(dict(id=r["id"], text=r["text"], p=r["p"], e=r["e"], pv=a, ev=b, ok=abs(a - b) <= r["tol"]))
|
|
# improvement #6: a list of points addressed by coordinates must name existing points
|
|
c14 = _get(params, "coping_rebar.c14")
|
|
if isinstance(c14, dict) and isinstance(c14.get("hooks_up"), list) and isinstance(c14.get("points"), list):
|
|
pts = {(round(x, 1), round(z, 1)) for x, z in c14["points"]}
|
|
for i, h in enumerate(c14["hooks_up"]):
|
|
ok = (round(h[0], 1), round(h[1], 1)) in pts
|
|
out.append(dict(id=f"coping_rebar.c14.hooks_up.{i}", text="c14 갈고리 위 지정 점이 c14 점 목록에 있다", p=f"coping_rebar.c14.hooks_up.{i}",
|
|
e="coping_rebar.c14.points", pv=f"({h[0]}, {h[1]})", ev="목록에 없음" if not ok else "있음", ok=ok))
|
|
return out
|
|
|
|
|
|
def bundles_of(path):
|
|
"""'footing.size_x' -> 'footing' (the section key of a dotted path)."""
|
|
return path.split(".", 1)[0]
|