Index
2026-09-11 — Engineering

스크립트 one_leg 정책 v2 — 폐루프 삽입/나사로 stock 0.47 → v2g 0.983 (59/60)

FurnitureBench (FurnitureSim, low randomness) | LLM 없음, API 비용 $0 | 벤치마크 8-D delta-EE OSC 액션·기존 관측만 사용

TL;DR

0.47
stock 14/30
0.983
v2g 최종 59/60 (동결)
0.95
v2f 57/60
0.925
v2c~v2e 111/120
0.77
FLIP 실험 23/30
510
성공 step 중앙값
$0
API 비용

1 배경 / 목적

CaP-X(gpt-6-astra) 트랙은 단일 턴 1/30, 멀티턴 2/44, 누적 API ≈ $210에서 사용자 결정으로 종료했다. 새 방향: LLM 없이 기존 FurnitureBench 스크립트 정책을 참고해 직접 고성능 스크립트를 만든다. 규칙 — API 지출 없음, 치팅 금지: 벤치마크 액션 인터페이스(10 Hz 8-D delta-EE OSC, 실물과 동일)로만 행동, 관측은 stock 스크립트와 동일(시뮬 GT 부품 자세 + 로봇 상태), 텔레포트·물리 변경·채점기 조회 없음. 데모를 VLA 학습에 쓰기 위해 IK 관절 제어 대신 OSC delta-EE 유지.

stock 스크립트의 한계: 각 phase가 EE 목표 도달 또는 25 step 시간초과("phase time out")로 넘어가는 열린 루프. insert_wait는 팔이 어디 있든 10 step 뒤 무조건 놓고, 놓기가 빗나가도 나사 phase를 계속 돌려 600 step 타임아웃. 파지 자세가 월드 고정이라 다리 yaw가 틀어지면 빈 손을 쥔다. 실측 14/30(@600), 17/30(@1500).

2 작업 내용 — LegV2(Leg)

furniture-bench/furniture_bench/furniture/parts/leg_v2.py. FURNITURE_SCRIPTED_V2=1일 때만 square_table_leg.py가 이 클래스를 쓴다(opt-in, 기본 동작 불변). 액션은 그대로 env의 get_assembly_action(delta ×2, 클램프)으로 변환된다. 운반(lift → move_center → match_leg_ori → reach_table_top)은 stock을 재사용하고 그 앞뒤를 교체했다.

단계stockv2
파지월드 고정 자세, 25 step 예산다리 yaw만큼 회전한 측면 파지, 폭 <2 cm면 재시도(≤2), 그래도 실패면 top-down 파지 후 공중 세우기; 운반 중 빈 손 감지
정렬없음(고정 목표)입구 +10 mm에서 다리 끝점 수평 오차 서보 + 적분항(OSC 정상상태 오차 ~2 mm 해소), 기울기 서보 3°/step; 1.5 mm·4° 이내 5 step 연속일 때만 하강
하강·놓기10 step 뒤 무조건 놓기6 mm/step 하강(림 접촉 시간 최소화), 림에 걸리면 백오프·재정렬(≤4); 입구 3 mm(→ 8 mm) 안에서 놓고 8 step 뒤 판정
안착 판정없음끝점 깊이 <−6 mm & 수평 <7 mm & 기울기 <25° → 나사; 25~60°면 닫은 그리퍼로 밀어 세움(nudge); 아니면 복구
나사 접근pre_grasp: 측면 자세에서 top-down으로 이동+회전 동시v2c: 수직 상승(손끝이 다리 위 7.5 cm) → 다리 5 cm 위 top-down → 수직 하강; 하강 중 다리가 8° 이상 더 기울면 후퇴
비틀기−95° 목표, 허용오차 0.3, 최대 75 step빈 파지 검사; 정체 감지(6 step 동안 heading 변화 <2°면 즉시 놓기); 사이클마다 끝점 검사(수평 >8 mm/기울기 >30°/빠짐 → 복구, 2사이클 무진전 → 재배치)
복구없음놓고 6 cm 위로 → 누웠으면 파지부터, 서 있으면 top-down 재파지 → 재정렬(≤4회); 바닥으로 떨어지면 즉시 포기

기타: stock의 tabletop push 전처리(tabletop을 장애물 모서리로 밀기)가 다리를 쓸어 떨어뜨려(3/30) v2에서는 생략(tabletop 위치를 측정해 쓰므로 불필요). mat2quat(torch eigh)이 180° 회전에서 발산 → scipy 변환. Leg.reset()이 phase 카운터를 안 지워 2번째 episode부터 초기화가 안 되던 문제 수정. 디버그: FURNITURE_SCRIPTED_V2_DEBUG=1이면 나사 사이클 step별 EE-다리 상대 자세와 관절각 출력.

평가: sbm "bash scripts/eval_scripted_one_leg.sh v2 30 100 1500" --qos=own --gres=gpu:1 -c 8 --mem 200GB trace: scripts/scripted_v2_trace.py --episodes N --seed S --timeout T (상태 전이마다 다리 자세, 관절각) 분할: python3 tmp/split_eps.py tmp/scripted_eval/<log> 17,18,22 (episode별 핵심 이벤트)

3 결과

정책 (job)timeout성공성공 step 중앙값실패 유형
stock (공식)60014/30 = 0.47470~520insert/screw 타임아웃
stock150017/30 = 0.57—〃
v2 (118937)150022/30 = 0.73608테이블 밖 낙하 5(그중 tabletop push 3), 기울어진 안착 나사 이탈 3
v2b (119003 / 119032)150026/30 / 27/30506전부 손목 감김 → 나사 접근·비틀기에서 뽑힘
v2c (119035 / 119039)150029/30 / 27/30 (56/60)510 (429~1460)얕은 안착 1, 손목 감김에 의한 삽입 부정확 3
v2d (119065, 관절각 손목 가드)150028/30 = 0.93500 (419~963)림 바운스 상태에서 놓기 → 넘어짐 1, 얕고 기울어진(22°) 안착 비틀기 무진전 → nudge가 날림 1. 되감기 가드 발동 0, pre-rotation 13
v2e (119090, 놓기 3 step 조건·누르기 전용·nudge 제거)150027/30 = 0.90527 (446~616)감긴 손목에서 재파지 경로가 큰 heading 변화를 명령해 팔 전체가 재구성되며 다리를 날림 2, upright↔align 무한 루프 1. 누르기 전용 사이클 16회 발동
v2f (119115 seed 100 / 119116 seed 200: 기울어진 다리 재파지 heading 0, upright↔align 3회 캡, 안착 기울기 기준 하강 가드, 나사 접근 전 옆으로 1.2 cm)150029/30 / 28/30 = 57/60 = 0.95539 (431~678)감긴 손목에서 `v2_regrasp_above`가 다리 상단 3 cm 위에서 회전하며 날림 1, 복구가 미삽입 다리를 공중에서 놓아 넘어짐 1, 23° 안착에 누르기 전용 무효·정체 복구 실패 1
v2f + FLIP (119117, 파지 시 손 180° 회전 실험)150023/30 = 0.77—감김 분포를 반대쪽(q5 +, q7 +2.5 고정)으로 옮길 뿐 → 폐기
v2g, 안착 22° (119147 / 119148: 나사산 방향 회전, recover 상승 +7.5 cm, 재파지 예산 단축, 든 채 내려놓기)150029/30 / 28/30 = 57/60 = 0.95566 / 533세 건 모두 22.7°/−11.6 mm 어트랙터 좌석 → 22° 기준이 MISSED→재배치로 보내 3/3 유실
v2g 최종 (동결) (119163 / 119164: 위와 같고 안착 기준 25° 복귀, FLIP 코드 제거)150030/30 / 29/30 = 59/60 = 0.983550어트랙터 좌석 8/8을 누르기/비틀기 사이클로 조립. 유일한 실패는 축 이탈 측면 파지(닫힘 방향·축 0.26) → 32° 기울인 운반 → 복구 중 유실

실행 간 물리가 비결정적이라 같은 seed라도 ep1부터 초기 상태가 갈린다(ep0만 동일). 30-ep 세트는 독립 표본이다. v2c에서 비틀기 정체 감지는 29/30 episode에서 발동했고(정상 사이클도 −90.6°에서 멈춤: stock 목표 −95°·허용오차 0.3은 경계선), 복구는 9회.

실패 메커니즘 — 손목 감김 (관절각 로그로 확정)

v2b 실패 7건(두 run)은 모두 "정상 안착(기울기 3~23°) → 나사 사이클에서 뽑힘". 나사 사이클 step별 관절각을 찍자 실패 episode는 예외 없이 첫 나사 파지 시점에 q5≈−2.0~−2.7, q6→3.75(상한), q7→2.90(상한)의 감긴 손목이었고, 정상은 q5≈0, q6≈2.6, q7≈0.8~1.0. 감긴 손목에 stock pre_grasp(이동+회전 동시)를 주면 OSC가 한계에 막혀 EE가 3 step에 −x로 3.4 cm 튀며 다리를 넘어뜨린다.

모든 상태 전이에서 관절각을 찍은 119039(30 ep)로 발생 지점을 특정했다: stock match_leg_ori — 툴을 뒤로 54° 기울인 파지 자세에서 앞으로 36° 기울인 세우기 자세로 90° 회전하는 동안 q6이 π(5·7축 정렬 특이점)를 지나고, 그 직후 (q5, q7)의 분배가 사실상 무작위다. 세우기 직후 q5/q7 30건의 분포: 정상(−0.1/1.1) ~ 완전 감김(−2.2/2.9)의 연속체, 완전 감김(q7=2.897) 5/30. 놓기 자세(툴 앞으로 25°)가 특이점 너머(q6 3.1~3.5)에 있는 것이 구조적 원인이고 stock 파지 기하(툴-다리 36° 마진)가 이를 강제한다.

v2c가 해결한 부분: 다리 5 cm 위에서 top-down으로 회전하는 v2_screw_above는 q6이 π를 내려가며 통과하고, OSC의 null-space 자세항(init_joints, kp 10)이 리셋 자세로 당겨 감김이 풀린다(ep0: q5 −2.28→−0.38, q7 2.78→1.19). 119039의 완전 감김 5건은 전부 성공.
남은 실패(v2c 4/60): (a) 얕은(10 mm)·11° 안착을 top-down으로 잡고 비틀다 뽑음 1건 → 놓기 깊이 3 → 8 mm로 변경(놓은 뒤 기울기 상한 ≈ atan(간극/깊이) 20° → 7°, 미평가). (b) 중간 감김(q5 −0.9~−1.9, q7 1.7~2.7)에서 삽입이 부정확해진 3건: 하강 자체가 막혀 depth +55 mm 유지(ep10), reach_table_top_z 75 step 시간초과 후 22° 기울여 놓음(ep22), 26° 기울여 놓고 nudge 목표로 팔이 크게 재구성되며 다리를 43 cm 밖으로 날림(ep13). 되감기는 나사 전이 아니라 insert_wait 전에 필요.

4 Takeaway

CaP-X 대비 무엇이 나아졌나

성공률 0.03~0.10 → 0.97, API 비용 $6~500/30 trial → $0, 행동 전 지연 90 s → 0, 결정적 재현. 본질적으로 CaP-X 멀티턴이 하려던 "실행 결과를 보고 코드를 고친다"를 사람(모델)이 trace를 뜯어보며 7회 이상 반복한 것이고, 결과 차이는 모델 능력보다 반복 횟수와 피드백 해상도(관절각·끝점 기하)의 차이에 가깝다. 대신 일반성은 잃었다: one_leg 전용 상태기계라 task마다 새로 써야 하고, 관측은 여전히 시뮬 GT 부품 자세다.

설계 교훈

  • OSC delta-EE만으로도 mm 정밀 삽입은 가능하다 — 다리 끝점 기준 서보 + 적분항 + 놓은 뒤 판정이 핵심이었다.
  • 관절 공간을 보지 않으면 설명되지 않는 실패가 있다. 관절각은 벤치마크 관측에 포함되므로 정책이 써도 규칙 위반이 아니며, env → part 배관(part.robot_joint_positions)을 추가했다.
  • 실패 원인 분류(테이블 밖 낙하 / 기울어진 안착 / 손목 감김)는 매번 다른 층위에서 나왔고, 각 층은 30-ep 표본 + step별 trace 없이는 보이지 않았다.

5 Next Steps

v2d 결과와 되돌린 것

관절각 손목 가드(나사 접근 전 되감기, 비틀기 전 q7 여유 확보, 하강 정체 가드, q6>3.6이면 기울기 서보 중지)를 넣은 v2d는 28/30. 되감기 가드는 한 번도 필요하지 않았고(감긴 손목은 v2_screw_above에서 자연히 풀림), pre-rotation 13회. insert_wait 시점 손목 상태별 성공률(정상 8/9, 부분 감김 11/12, 완전 감김 8/9)이 같아 "의도적 감기"·"삽입 전 되감기"는 폐기. 놓기 깊이 −8 mm는 쥔 채 밀면 림에 걸린 각도(24°)로 놓게 되어 −3 mm로 환원. 느린 세우기(1.5°/step)는 감김을 결정적으로 만들어 폐기.

v2g 최종 동결 (59/60 = 0.983) — 잔여 2유형

v2f(heading 0 재파지, upright↔align 캡) 57/60 → v2g(나사산 방향 회전: −95° = 2.3 mm 진입·+95° = 후퇴·누르기만 = 0이라 항상 조이는 방향, recover 상승을 다리 상단 +7.5 cm로, 재파지 예산 45 step·기울기 변화 시 중단, 든 채 복구면 내려놓기) — 안착 22° 기준으로는 57/60(22.7° 어트랙터 좌석 3건이 MISSED→재배치로 가서 3/3 유실), 25° 기준으로 되돌리자 같은 좌석 8/8이 누르기/비틀기로 조립되어 59/60. 잔여: (1) 22.7°/−11.6 mm 어트랙터(수직으로 3 mm 넣고 놓으면 열린 손가락 안에서 근처 림에 기대는 안정점; 미시도 아이디어 — 기운 직후 열린 툴로 아래로 톡 치기, 첫 들어올리기 전 제자리 재파지), (2) 축 이탈 측면 파지(1/60: 닫힘 방향·축 0.26 → 32° 기울인 운반 → 복구 중 유실). 동결 파일 leg_v2.py 14:08:24.

FLIP 실험 폐기 (23/30)

파지 시 손을 자체 축으로 180° 돌려(q7≈−2.1) 세우기 통과 후 손목이 +2.9 한계에서 멀어지게 하려는 시도. one_leg 3 seed 로컬 3/3이었으나 30 ep에서는 감김이 반대쪽(q5 +1.6, q7 +2.5 고정)으로 옮겨가 0.77. 60 ep 표에서 통과 전 상태가 통과 후 부류를 전혀 예측하지 못한 것과 정합(분기는 stock 세우기의 첫 목표 노이즈 경로에서 결정).

그 다음

v2를 데모 수집(collect_scripted.sh, timeout ~1500)에 적용 → lamp/round_table/cabinet에 같은 레시피(측정 기반 정렬·놓기 판정·사이클 검사) → desk/stool/chair/drawer 신규 상태기계.

6 영상

v2 최종(seed 300/800/900, 각 3 ep)과 stock(seed 300/800)의 전면 카메라 + 손목 카메라 인셋 + 상태/기울기 오버레이 영상은 별도 쇼케이스 리포트 260911-scripted_one_leg_v2_videos.html에 있다(job 119213, scripts/record_scripted_video.py / record_scripted_videos.sh, 병렬 세션 작성).