File size: 19,174 Bytes
bd08baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# actaug β€” perturbed-object action re-earning for robocasa episodes

Standalone handoff package. **No imports from any other project directory** β€” only
installed libraries (robocasa / robosuite / robomimic / numpy / pandas / zmq /
torch / imageio). Reference implementations it was distilled from (read-only):
`/home/nvidia/jonghoon/idm_eval/scripts/gripper_state_replay_sandbox/replay/`
(`gripper_state_replay.py` hybrid mode, `grip_perturb.py`).

Physical location `/lp-dev/jonghoon/actaug` (symlinked at
`/home/nvidia/jonghoon/actaug`).

## What it does

For an arbitrary episode of a robocasa LeRobot export (per-episode
`extras/episode_XXXXXX/{model.xml.gz, states.npz, ep_meta.json}` + 12-d actions in
`data/chunk-XXX/episode_XXXXXX.parquet`):

1. **Reset** the MuJoCo sim to the recorded initial state β€” the episode's own
   compiled `model.xml` + flattened state via `EnvRobosuite.reset_to`, so the
   scene / layout / cameras are bit-identical to the source episode.
2. **Perturb** the manipulated object (`obj_joint0` free joint): yaw about the
   WORLD vertical + xy translation, then a short physics settle. Random draws:
   yaw ~ U[-rot_deg, rot_deg], translation direction uniform on the circle,
   radius ~ U[trans_min_m, trans_m].
3. **Replay** the recorded actions until `grasp_t - pre_buffer`
   (`grasp_t` = first recorded gripper-close command).
4. **Policy handoff**: GR00T-N1.5 grasps (chunks of `exec_h=16` env steps,
   stop when the object is lifted > 3 cm or budget runs out), then the recorded
   transport actions resume at `grasp_t + resume_offset`.
5. **Sample N times** per perturbation. Each attempt is fully seeded: the obs
   dict carries `_policy_action_seed` (honored by myGR00T `policy.get_action`),
   so attempts are diverse AND reproducible.
6. **Filter on task success** (`env.is_success()["task"]`). Successful attempts
   are dumped under `accepted/` with executed `actions.npy` (parquet order),
   `states.npz` (16-d observation.state layout), and mp4s from the SAME three
   cameras as the dataset (`robot0_agentview_left/right`, `robot0_eye_in_hand`,
   256Γ—256) + a side-by-side `3cam.mp4` + `meta.json`.

## Two grasp-re-earning algorithms (pick via YAML `mode:`)

**`hybrid` (unconstrained)** β€” GT actions to `grasp_t βˆ’ pre_buffer`, then the
policy commands the FULL action space (EEF pose + gripper through the OSC
controller) until the object lifts, then recorded transport resumes. Handles
translated objects (the policy can chase them). `configs/unconstrained.yaml`.

**`constrained`** β€” the sim_action_aug algorithm
(`sim_action_aug/code/rollout/r02_rollout.py`, `ws/ROLLOUT_REPORT.md` Β§3)
ported to robocasa: every arm joint NOT listed in `constrained.policy_joints`
(+ mobile base + torso) is PINNED to the recorded MuJoCo state path every
physics substep (zero drift, the grip_state/grip_policy machinery). The policy
owns ONLY the listed joints (default `[6, 7]`, i.e. wrist pitch + roll, like
the sim_action_aug pipeline) and the gripper:

- **each joint j in `policy_joints`**: `recorded + servo` β€” the policy's delta
  rotation projected onto joint j's world axis, capped `step_cap` rad/step and
  `servo_caps[j]` rad total (defaults: 6β†’0.30, 7β†’0.60);
- **joint 7** (when listed) additionally: `recorded + dj7_init + servo` β€” `dj7_init` is a searched
  grid (`constrained.dj7_init_deg`, slewed in over ≀48 frames instead of
  teleported, per r02 phase R), with the (βˆ’Ο€/2, Ο€/2] wrap (2-jaw symmetry)
  and `|dj7_init + servo| ≀ j7_total_cap`;
- **the gripper**: `action.gripper_close` in one of three interpretations β€”
  `bit` (binarized, robocasa-native), `threshold` (latch full close once
  β‰₯ `grip_tau`; this is what unlocked ep 0009 in sim_action_aug),
  `continuous` (ctrl interpolated open→close), `recorded` (diagnostic: the
  recorded close bit, = grip_state).

Transport = relative replay: everything pinned, dj7 frozen at hand-off,
gripper held closed until the recorded release frame, then the recorded
opening schedule (never re-closes), plus 30 settle frames.

**rel-6D weld (on by default, `constrained.rel6d`)** β€” sim_action_aug's simfix,
required here too: a pinned (infinite-impedance) arm carry slips out of the
friction grasp (the old robocasa pin-sweep peaked at ~33% success; reproduced
on ep0: the pinch engages, then the bottle squeezes out during the lift, with
recorded gripper timing and no policy at all). So an INACTIVE
`<weld body1="gripper0_right_eef" body2="<obj root>"/>` is injected into the
episode XML; it engages at the CURRENT relative pose once both jaw pads touch
the object while it is lifted β‰₯ `weld_lift_m` under a close command, and it
drops the moment the gripper is commanded open β€” the release is a genuine
physical fall. Engage/release frames are logged per attempt (`weld_engage_t`,
`weld_release_t`). Tuning matters: with a 15 mm engage gate + soft solref 0.02
the bottle sagged ~19 mm in the jaw, caught the cabinet-shelf edge on the way
in, and fell out at release; `weld_lift_m: 0.004` + `weld_solref: "0.005 1"`
(the defaults) fixed it β€” recorded-gripper baseline then reproduces the GT
success exactly.

`configs/constrained.yaml`. Caveat: the pinned arm cannot chase a TRANSLATED
object β€” use it for yaw-dominant perturbations / small translations, and
`hybrid` for the 3–7 cm displacement regime.

Note: attempts count = `n_perturbs Γ— len(dj7_init_deg) Γ— n_samples` for
constrained, `n_perturbs Γ— n_samples` for the others. The wrist servo uses the
policy's raw delta-rotation command (controller units) β€” direction is what
matters; the per-step/total caps bound the magnitude exactly as in r02.

## YAML reference (every key)

A YAML passed as `--config` sets argparse defaults; any explicit CLI flag
overrides it. Top-level keys mirror `rollout.py` flags; the `constrained:`
block is passed to `run_constrained_attempt` verbatim.

### Top-level keys (both algorithms)

| key | default | meaning |
|---|---|---|
| `mode` | hybrid | `gt` (pure replay sanity) / `hybrid` / `policy` (end-to-end) / `constrained` |
| `dataset` | PickPlaceCounterToCabinet export | robocasa LeRobot export root |
| `episode` | β€” (required) | episode index |
| `perturb` | "" | explicit `"dyaw_deg,dx_m,dy_m"`; overrides random draws |
| `n_perturbs` | 0 | number of RANDOM perturbations (0 + no `perturb` = nominal) |
| `rot_deg` / `rot_min_deg` | 60 / 0 | yaw magnitude ~ U[rot_min_deg, rot_deg], sign random |
| `trans_m` / `trans_min_m` | 0.05 / 0 | translation radius ~ U[trans_min_m, trans_m], direction uniform |
| `seed` | 0 | seeds BOTH the perturbation draws and the per-attempt policy seeds |
| `n_samples` | 1 | **N**: policy samples per initial condition (rejection sampling) |
| `pre_buffer` | 10 | **the buffer**: policy inference starts at `grasp_t βˆ’ pre_buffer` frames (`grasp_t` = first recorded gripper-close). hybrid-mode flag; constrained mode reads it from its own block (below) |
| `policy_budget` | 200 | hybrid only: max policy env-steps to achieve the lift |
| `resume_offset` | 0 | hybrid only: recorded transport resumes at `grasp_t + resume_offset` |
| `exec_h` | 16 | hybrid only: env steps executed per policy chunk (inference stride) |
| `host` / `port` | 127.0.0.1 / 8801 | GR00T server address |
| `save` | success | `success` (dump accepted only) / `all` (+ rejected/) / `none` |
| `fps` | 20 | output video fps (= dataset rate; don't change unless re-timing) |
| `out` | outputs/ep\<N\>_\<mode\> | output dir |

### `constrained:` block

| key | default | meaning |
|---|---|---|
| `pre_buffer` (alias `start_off`) | 4 | **the buffer**: inference starts at `grasp_t βˆ’ pre_buffer`; the dj7_init ramp is scheduled BEFORE this point so it never eats into the policy window |
| `k_post` | 12 | policy keeps control until `grasp_t + k_post`, then hand-off |
| `replan` | 8 | **inference stride**: control steps between policy queries; each query returns a 16-step chunk, of which the first `replan` steps are executed |
| `policy_joints` | [6, 7] | **the joint constraint**: 1-indexed arm joints the policy may servo; everything else (joints, base, torso) stays pinned to the recording. Gripper is ALWAYS the policy's unless `gripper_mode: recorded` |
| `step_cap` | 0.05 | **inference speed**: max servo change per 20 Hz control step (rad) for every policy joint; also paces the dj7_init ramp. 0.05 β‰ˆ 1 rad/s (rate-matched to sim_action_aug) |
| `servo_caps` | {6: 0.30, 7: 0.60} | total authority per joint (rad) β€” how FAR, not how fast |
| `servo_cap_default` | 0.30 | cap for listed joints missing from `servo_caps` |
| `j7_total_cap` | Ο€ | joint 7 only: bound on `dj7_init + servo` |
| `dj7_init_deg` | [0] | searched initial wrist-roll offsets (deg); grid Γ— `n_samples` = attempts per perturbation. Use `[0, 90, -90]` for 90Β°+ object rotations |
| `gripper_mode` | threshold | `bit` / `threshold` / `continuous` / `recorded` (see algorithm section) |
| `grip_tau` | 0.35 | threshold mode: latch full close once policy's close fraction β‰₯ Ο„ (use ~0.25 for the base model) |
| `embodiment` | robocasa | `robocasa` (finetuned head) / `oxe_droid` (BASE model via DROID head) |
| `oxe_grip_flip` | false | oxe_droid: invert gripper polarity if the base model's convention is reversed |
| `rel6d` | true | the grasp weld (mandatory for reliable carry β€” see weld section) |
| `weld_lift_m` | 0.004 | weld engages once both pads touch + object lifted this much + close commanded |
| `weld_solref` | "0.005 1" | weld stiffness injected into the XML |
| `weld_open_frac` / `weld_open_floor` | 0.5 / 0.10 | weld releases when the close command drops below `max(fracΒ·held, floor)` |
| `settle_frames` | 30 | pinned hold after the last frame so the final pose is a rest pose |
| `qvel_mode` | finitediff | qvel written for pinned joints each substep (`zero` = no injected velocity) |

## Code documentation (what each file does)

### `actaug_core.py` β€” the shared library (no CLI)
Everything both algorithms need, importable, side-effect free:
- **Episode I/O**: `load_episode(root, idx)` β†’ dict with the compiled
  `model.xml`, the full flattened MuJoCo `states` (TΓ—(1+nq+nv)), `ep_meta`,
  the 12-d parquet `actions`, the language `instruction` (tasks.jsonl), and
  `env_args`. `grasp_frame(actions)` = first recorded gripper-close.
- **Env**: `make_env(env_args)` builds the robomimic `EnvRobosuite` (cached per
  env name β€” construction is minutes); `reset_episode(env, ep)` = `reset_to`
  with the episode's own XML + state 0.
- **Perturbation**: `apply_object_perturbation(env, dyaw, dx, dy)` rotates the
  object's free joint about the world vertical and translates it in-sim;
  `sample_perturbation(rng, ...)` draws random ones; `settle(env)` lets physics
  rest after.
- **Policy bridges**: `groot_obs` / `groot_obs_oxe` build the obs dict
  (3 camera renders + proprio + instruction + `_policy_action_seed`) for the
  finetuned robocasa head / the base model's DROID head;
  `policy_chunk_to_env` / `oxe_chunk_to_env` decode a returned action chunk
  into robosuite env order; `to_env_action` / `env_to_parquet_action` convert
  between parquet layout and env layout.
- **PolicyClient**: minimal ZMQ REQ client for the myGR00T inference server
  (torch.save wire format).
- **Predicates & output**: `obj_z`, `is_success`, `render3`, `groot_state_vec`
  (16-d observation.state), `write_mp4`.

### `rollout.py` β€” the CLI driver
Parses CLI + `--config` YAML, loads the episode, builds the env, draws the
perturbations, then loops `perturbation Γ— dj7_grid Γ— n_samples`:
- `run_attempt(...)` implements `gt` / `policy` / `hybrid` (GT approach via
  `env.step` β†’ `run_policy` chunks until lift β†’ GT transport), recording every
  executed step;
- for `mode: constrained` it injects the weld into the episode XML
  (`inject_weld`), resets + perturbs, and delegates to
  `constrained.run_constrained_attempt`;
- `save_dump(...)` writes each kept attempt: `actions.npy` (parquet order),
  `states.npz`, `left/right/wrist.mp4` + `3cam.mp4`, `obj_track.npz`
  (executed-vs-recorded object positions, constrained only), `meta.json`;
  `results.csv` is re-flushed after every attempt.

### `constrained.py` β€” the constrained algorithm
- `inject_weld(xml, solref)`: adds the inactive
  `<weld body1="gripper0_right_eef" body2="<object root>"/>` equality to the
  episode XML (pure text transform, idempotent).
- `PinRig(env)`: all model addressing for one compiled scene β€” arm/base/torso
  joint qpos/dof addresses, finger actuators (+ open/close ctrl targets),
  object vs gripper geom sets, left/right pad geom sets, joint world axes,
  weld eq id. Methods: `pin_step` (one control frame: lerp the pinned joints
  across physics substeps while gripper + object integrate), `grip_ctrl`,
  `grip_contact`, `pad_contacts`, `weld_engage/release/relpose/active`.
- `run_constrained_attempt(env, ep, client, dj7_init_deg, seed, cfg, record)`:
  the four phases β€” pinned approach (with the dj7_init ramp), policy window
  (per-joint servo + gripper, chunk every `replan` steps, per-call action
  seed), relative-replay transport (frozen offsets, recorded opening), settle β€”
  plus the weld engage/release bookkeeping and the result row.

### `compose_2x3.py` β€” comparison videos
`compose_2x3.py <episode> <dump_dir> <out.mp4>`: row 1 = the ORIGINAL
episode's three dataset videos, row 2 = the augmented rollout's same three
views; the shorter row freezes on its last frame.

### Shell scripts
- `launch_server.sh [gpu] [port] [model] [embodiment] [data_config]` β€”
  detached GR00T inference server (defaults = the finetuned 60k checkpoint;
  pass the HF snapshot + `oxe_droid oxe_droid` for the base model).
- `run_sanity.sh [gpu] [port] [ep]` β€” GT replay sanity β†’ hybrid nominal β†’
  3 random perturbations Γ— 3 samples.

### `configs/`
`unconstrained.yaml` (hybrid preset), `constrained.yaml` (fully commented
constrained preset), `asym_base_j6.yaml` / `asym_bigrot.yaml` (the base-model
asymmetric-object example runs, standard and 90–150Β° rotation).

## Interpreters / environment

- sim client: `/lp-dev/jonghoon/mimicgen_augment/envs/mimicgen/bin/python`
  with `MUJOCO_GL=egl MUJOCO_EGL_DEVICE_ID=<gpu>` (osmesa is broken on this box).
- policy server: `/data/nvidia/gripper_augmentator/conda-envs/mygr00t/bin/python`
  running `myGR00T/scripts/inference_service.py` (see `launch_server.sh`;
  `HF_HUB_OFFLINE=1` is mandatory). Default checkpoint:
  `/lp-dev/jonghoon/myGR00T_outputs/pnpcountertocab_all502_gbs64_wandb_60k_save5k_20260427_193901/checkpoint-60000`
  (GR00T-N1.5 finetuned on all 502 PnPCounterToCabinet episodes β€” the same
  dataset these rollouts perturb).
- **BASE (non-finetuned) GR00T-N1.5**: the base checkpoint has NO robocasa
  embodiment head (only gr1 / oxe_droid / agibot_genie1), so it is served via
  its **oxe_droid (DROID) head** with a best-effort obs/action mapping
  (`groot_obs_oxe` / `oxe_chunk_to_env` in actaug_core, same approach as the
  old gripper_state_replay code):
  `bash launch_server.sh <gpu> 8802 <hf-snapshot-dir> oxe_droid oxe_droid`
  and set `constrained.embodiment: oxe_droid` (+ optionally lower
  `grip_tau` to ~0.25 β€” the base policy's close commands are weaker).
  Expect lower sample efficiency; that's what `n_samples` is for.
- default dataset:
  `/lp-dev/jonghoon/robocasa_full/pickplace_target_human/PickPlaceCounterToCabinet`
  (502 eps, PandaOmron, HYBRID_MOBILE_BASE, fps 20).

## Usage

```bash
# one-time server (GPU 5, port 8801)
bash launch_server.sh 5 8801

SIMPY=/lp-dev/jonghoon/mimicgen_augment/envs/mimicgen/bin/python
export MUJOCO_GL=egl MUJOCO_EGL_DEVICE_ID=2

# sanity: unperturbed GT replay must succeed
$SIMPY rollout.py --episode 0 --mode gt

# the pipeline: 5 random perturbations x 8 policy samples, keep successes
$SIMPY rollout.py --episode 0 --mode hybrid \
    --n_perturbs 5 --n_samples 8 --rot_deg 60 --trans_m 0.05 \
    --pre_buffer 10 --policy_budget 200 --port 8801 --seed 7

# explicit perturbation (yaw +45deg, +3cm x, -2cm y)
$SIMPY rollout.py --episode 0 --mode hybrid --perturb "45,0.03,-0.02" \
    --n_samples 8 --port 8801

# constrained algorithm, all knobs from YAML (CLI flags override YAML values)
$SIMPY rollout.py --config configs/constrained.yaml --episode 0

# or everything at once
bash run_sanity.sh 2 8801 0
```

Outputs: `outputs/<run>/results.csv` (one row per attempt: perturbation, seed,
peak lift, grasped, success, wall time) + `accepted/pertXX_sYY/` dumps.
`--save all` also dumps failures under `rejected/`.

## Controlling how FAST the policy moves (inference speed)

The policy-driven motion speed is set entirely by per-control-step caps in the
`constrained:` block β€” the sim always runs at the dataset's 20 Hz, so these
caps ARE the deg/s of the wrist during the inference window:

| key | meaning | default | rad/s @20 Hz |
|---|---|---|---|
| `step_cap` | max servo change per control step, every policy joint; also paces the `dj7_init` ramp | 0.05 | 1.0 (β‰ˆ57Β°/s) |
| `servo_caps: {j: cap}` | TOTAL authority per joint (how far, not how fast) | 6β†’0.30, 7β†’0.60 | β€” |
| `replan` | control steps between policy queries (reaction latency, not speed) | 8 | β€” |

History: the port initially reused r02's 0.10 rad/step, but r02 ran at 9.2 Hz β€”
at robocasa's 20 Hz that is ~2 rad/s and the wrist visibly whips. 0.05 is
rate-matched to the original. Raise/lower `step_cap` in the YAML to make the
policy's wrist motion faster/slower; the gripper close speed is the actuator's
own dynamics (same as GT) and is not affected.

For `hybrid` mode the analogous knob is `exec_h` (env steps executed per chunk)
β€” the policy's actions are executed at the recorded control rate either way.

## Knobs that matter

- `--pre_buffer` β€” how early the policy takes over. 10 works for ≀5 cm
  translations; increase (or use `--mode policy`) for larger displacements,
  since the recorded approach aims at the ORIGINAL object location.
- `--policy_budget` β€” max policy env-steps to achieve the lift (default 200).
- `--resume_offset` β€” where recorded transport resumes. 0 = at the recorded
  grasp frame. The transport is delta-eef, so it carries the object from
  wherever the policy lifted it.
- Perturbation ranges: prior sweeps (`grip_perturb.py`) used yaw up to Β±180Β°,
  translations ≀ 7 cm, dz ≀ 2 cm. Large yaws are fine for rotationally
  symmetric objects; translations > ~7 cm start leaving the reachable
  counter region.

## Gotchas

- Env construction takes ~2–4 min (kitchen scene compile) β€” it is cached per
  process; batch many attempts/episodes per process launch.
- `EnvRobosuite.reset_to` internally remaps the export's absolute asset paths
  (`/root/robocasa/...`, `/opt/conda/...`) to the local robocasa/robosuite
  installs β€” only the mimicgen env's robocasa checkout has all assets; the
  `robocasa_calib` env historically had a geom mismatch. Use the mimicgen python.
- GPU 4 is often another user's β€” check `nvidia-smi` before picking GPUs.
- `obj_pos` observable + `obj_joint0` joint exist in all robocasa kitchen envs;
  success predicate is the env's own `is_success()["task"]`.