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>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

@@ -0,0 +1,153 @@
"""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]