Files
Pier_v1/RAG/design-model-box-sample_2/tools/audit_balanced_body_ties.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

78 lines
4.8 KiB
Python

"""Read-only saved-model audit of shared body-tie placement and preservation."""
import argparse
from collections import Counter
import hashlib
import json
import math
from pathlib import Path
import sys
root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root/'src/backend' if (root/'src/backend').is_dir() else root))
from quantity_review_validation import bend_paths
from tie_end_spacing import inspect_visible_ends
def audit(job_path, baseline_path):
job=json.loads(Path(job_path).read_text());before=json.loads(Path(baseline_path).read_text())
plan=job['result'];old=before['result'];reports=[];changed=set()
for group in plan['rebar_groups']:
distribution=group.get('quantity_distribution',{})
rhythm=distribution.get('rhythm',{})
if 'end_balance_required' not in rhythm:continue
changed.add(group['id'])
records=distribution['records'];paths=group['instance_paths_m']
ends=inspect_visible_ends(plan,group,[b['path'] for b in bend_paths(group)])
if not ends['pass'] and rhythm.get('end_spacing_priority') == 'equal_ends':
raise ValueError('visible concrete-to-tie end spacing differs: '+json.dumps(ends))
counts=Counter(r['lane'] for r in records)
repeat_errors=[]
for lane in counts:
xs=sorted(r['x_m'] for r in records if r['lane']==lane)
repeat_errors.extend(abs(b-a-rhythm['repeat_m']) for a,b in zip(xs,xs[1:]))
phase_errors=[]
for side in rhythm['side_lanes']:
events=sorted((r['x_m'],r['lane']) for r in records if r['lane'] in side)
for (x,lane),(y,next_lane) in zip(events,events[1:]):
if lane==next_lane:raise ValueError('staggered lane repeat')
phase_errors.append(abs(y-x-rhythm['phase_m']))
lengths=[sum(math.dist(a,b) for a,b in zip(path,path[1:])) for path in paths]
detail=group['detail_shape_binding']['fabrication']['points_m']
expected=[math.dist(a,b) for a,b in zip(detail,detail[1:])]
for path in paths:
measured=[math.dist(a,b) for a,b in zip(path,path[1:])]
if len(measured)!=len(expected) or max(abs(a-b) for a,b in zip(measured,expected))>.001:
raise ValueError('fabrication segments changed')
if len(paths)!=group['schedule_count']:raise ValueError('drawing count changed')
if max(repeat_errors+phase_errors+[0])>1e-6:raise ValueError('fixed rhythm changed')
if max(abs(v-group['unit_length_m']) for v in lengths)>.001:raise ValueError('fabrication length changed')
if not group['placement_geometry_check'].get('pass'):raise ValueError('geometry gate missing')
if not all(r['contact']['pass'] for r in records):raise ValueError('raw contact failed')
if not all(r['pass'] for r in distribution['rounded_contact']):raise ValueError('rounded contact failed')
a,b=rhythm['side_lengths_m'];n,m=rhythm['side_counts']
if abs(a-b)>1e-5 and (a-b)*(n-m)<=0:raise ValueError('long side not more populated')
reports.append({'block':group['rebar_block'],'mark':group['mark'],'count':len(paths),
'side_counts':rhythm['side_counts'],'side_lengths_m':rhythm['side_lengths_m'],
'repeat_m':rhythm['repeat_m'],'phase_m':rhythm['phase_m'],
'end_balance_required':rhythm['end_balance_required'],'end_margins_m':rhythm['end_margins_m'],
'visible_end_check':ends,
'skew_contact_bars':sum('skew' in r.get('contact_domains',[]) for r in records),
'max_unit_length_error_m':max(abs(v-group['unit_length_m']) for v in lengths),
'geometry_and_contacts_pass':True})
def unchanged(groups):
return Counter(json.dumps({k:v for k,v in g.items() if k!='rebar_assembly_label'},sort_keys=True)
for g in groups if g.get('id') not in changed)
if plan['concrete_components']!=old['concrete_components']:raise ValueError('concrete changed')
if unchanged(plan['rebar_groups'])!=unchanged(old['rebar_groups']):raise ValueError('unrelated rebar changed')
return {'job':Path(job_path).stem,'baseline':str(baseline_path),'geometryRevision':job.get('geometryRevision'),
'sha256':hashlib.sha256(Path(job_path).read_bytes()).hexdigest(),'groups':reports,
'unrelated_rebar_and_concrete_preserved':True,'engineering_approval':False}
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('job');parser.add_argument('baseline');parser.add_argument('--output',required=True)
args=parser.parse_args();report=audit(args.job,args.baseline)
with open(args.output,'x') as stream:json.dump(report,stream,ensure_ascii=False,indent=2)
print(json.dumps({'groups':len(report['groups']),'bars':sum(g['count'] for g in report['groups']),
'unrelated_preserved':True},ensure_ascii=False))