Skip to content

Commit e6dc450

Browse files
jpothen8claude
andcommitted
Add constraint-aware safety pipeline: DAgger, safe Pi0 policy, geometry, and scripts
Introduces safety_geometry.py (workspace constraint primitives), safe_pi0_policy.py (safety-filtered Pi0 wrapper with Python 3.14 argparse shim), dagger.py (DAgger training loop), and supporting scripts for rollout recording and training. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 70bbbbd commit e6dc450

25 files changed

Lines changed: 1300 additions & 51 deletions

AGENTS.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,46 @@ uv run python -m sim.scripts.collect_demos --repo-id local/safe-cube-bc \
181181
.venv/bin/mjpython -m sim.scripts.view_env
182182
```
183183

184-
## What's NOT done yet (for the next agent)
184+
## Training pipeline (`SafePI0Policy` + DAgger) — built, NOT yet run
185+
186+
The policy/training half of `project_summary.md` now exists but is **untested**
187+
(no runnable Python env was available when it was written — validate before
188+
trusting it):
189+
190+
- **`sim/safety_geometry.py`** — pure-torch, LeRobot-free primitives: `box_sdf`,
191+
`sdf_clearance`, `safety_loss`, and `FKChain` (differentiable FK via
192+
`pytorch_kinematics`, end link `gripper_frame_link`). Unit-tested in
193+
`sim/tests/test_safety_geometry.py` (`uv run pytest sim/tests -q`). **Run these
194+
first** — a sign error here is cheap to catch now, expensive 40 epochs into a 3B run.
195+
- **`sim/safe_pi0_policy.py`**`SafePI0Config` (registered as `safe_pi0`) +
196+
`SafePI0Policy(PI0Policy)`. Overrides `forward` so one velocity prediction feeds
197+
both `L_flow` and the endpoint-estimate safety term. **Flow convention:** LeRobot's
198+
π0 is `x_t = t·noise + (1−t)·actions` (t=0→data, t=1→noise) — the *opposite* of
199+
`project_summary.md` §5.1. So the endpoint estimate is `â = x_t − t·v` and the
200+
safety loss is weighted by `(1−t)`. Actions are MEAN_STD-normalized before
201+
`forward`, so `â` is un-normalized (via `dataset_stats[ACTION]`, captured in
202+
`__init__`) before FK.
203+
- **`sim/dagger.py` / `sim/scripts/dagger.py`**`PolicyRollout` (checkpoint →
204+
closed-loop `act(obs)` via the stock preprocessor→`select_action`→postprocessor
205+
path) + the collect-round logic, and a CLI orchestrator that runs
206+
collect→retrain rounds (retrain shells out to the stock entrypoint).
207+
- **`sim/scripts/train_safe_pi0.py`** — thin shim: imports the policy module (to
208+
register `safe_pi0` with draccus) then calls the stock `train()`.
209+
- **Privileged-key plumbing (one stock edit):** `src/lerobot/processor/converters.py`
210+
`_extract_complementary_data` now preserves any `privileged.*` batch key. Without
211+
this the preprocessor (`batch = preprocessor(batch)` in the train loop) drops them
212+
before `forward` and the safety loss can't see the cube positions. They ride
213+
through as complementary data → un-normalized, untouched by feature classification
214+
(`dataset_to_policy_features` `continue`s on non-`observation`/`action` keys).
215+
216+
**Before the first real run, verify:** (1) `pytorch_kinematics` is installed and
217+
`FKChain.fk(state[:5])` matches the recorded `privileged.ee_pos` on a sample frame
218+
(FK-frame == world-frame assumption); (2) privileged keys actually arrive in
219+
`policy.forward`'s batch; (3) sweep `λ` (`--policy.safety_weight`) — it's the
220+
central knob.
221+
222+
## Still NOT done
185223

186-
- **`SafePI0Policy`** — the LeRobot subclass with `L = L_flow_matching + λ·L_safety` per `project_summary.md` §5. Privileged keys are already in the recorded datasets (`privileged.cube_positions`, `privileged.cube_half_extents`, `privileged.blue_cube_pos`, `privileged.goal_pos`, `privileged.ee_pos`, `privileged.grasped`) — the policy/loss just needs to read them from the batch and ignore them at inference. Per-cube arrays are flattened to 1D; reshape on load.
187-
- **DAgger driver** — sketched in `sim/README.md`. The primitives it needs are already in place (`SafeCubeEnv`, `ScriptedExpert`, `EpisodeRecorder`, `evaluate.evaluate()`).
188224
- **Domain randomization not yet applied.** `sim/randomize.py` defines `apply_dr(model, rng, cfg)` but `env.reset()` doesn't call it. Wire it in after the scene compiles, before the first physics step, when ready.
189225
- **Real-world fine-tune / hardware deployment** — out of scope for the sim.
190226

sim/README.md

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -98,36 +98,111 @@ from the batch.
9898
}
9999
```
100100

101-
## DAgger loop sketch
101+
## End-to-end runbook (collect → BC warm-start → DAgger)
102102

103-
The pieces above are the primitives — the DAgger loop is a small driver:
103+
The policy + training code now exists (`sim/safe_pi0_policy.py`, `sim/dagger.py`,
104+
`sim/scripts/`). It was written without a runnable environment, so **it is
105+
untested** — do step 0 before anything else.
106+
107+
### 0. Setup + validate the geometry (cheap, do it first)
108+
109+
```bash
110+
uv sync --locked --extra all # base + policy deps
111+
uv pip install pytorch_kinematics # differentiable FK for the safety loss
112+
113+
# A sign error in the SDF/FK is cheap to catch now, expensive 40 epochs into a
114+
# 3B-param fine-tune. These tests are torch-only (no MuJoCo / VLA needed).
115+
uv run pytest sim/tests -q
116+
```
117+
118+
Then spot-check the FK frame against ground truth on one recorded frame: load a
119+
sample, run `FKChain.fk(state[:5])`, and confirm it matches that frame's
120+
`privileged.ee_pos` (the loss assumes FK's base_link frame == the world frame —
121+
true while the URDF base sits at the origin).
122+
123+
### 1. Collect data
124+
125+
The safety term needs **both** success and failure data (failures are its
126+
negative class). Make a clean set for the BC warm start and a larger mixed set
127+
that contains failures:
128+
129+
```bash
130+
# Clean successes — BC warm start
131+
uv run python -m sim.scripts.collect_demos \
132+
--repo-id local/safe-cube-bc --root data/safe_cube_bc \
133+
--n-episodes 100 --successes-only
134+
135+
# Mixed set (keeps red-contact / drop / timeout failures)
136+
uv run python -m sim.scripts.collect_demos \
137+
--repo-id local/safe-cube-mixed --root data/safe_cube_mixed \
138+
--n-episodes 200
139+
```
140+
141+
### 2. BC warm start (fine-tune π0 with the safety loss on)
142+
143+
```bash
144+
uv run python -m sim.scripts.train_safe_pi0 \
145+
--policy.type=safe_pi0 \
146+
--policy.pretrained_path=lerobot/pi0_base \
147+
--policy.safety_weight=1.0 \
148+
--dataset.repo_id=local/safe-cube-mixed --dataset.root=data/safe_cube_mixed \
149+
--output_dir=outputs/safe_pi0_bc \
150+
--batch_size=8 --steps=20000 \
151+
--policy.gradient_checkpointing=true
152+
```
153+
154+
`--policy.safety_weight` (λ) is the central knob — sweep it. Too small → safety
155+
ignored; too large → the policy collapses to pure avoidance and stops doing the
156+
task. The evolving safety term makes the loss landscape non-stationary, so
157+
**early-stop at peak rollout success, not at min loss.** Checkpoint lands at
158+
`outputs/safe_pi0_bc/checkpoints/last/pretrained_model`.
159+
160+
### 3. DAgger (collect on the policy's own state distribution → retrain)
161+
162+
`sim/scripts/dagger.py` runs the full collect→retrain loop. Round 0 is pure
163+
expert; later rounds load the previous checkpoint, mix in policy actions
164+
(`alpha = alpha0 ** round`), and **always label with the expert** — which is what
165+
drags training onto deployment-distribution states and produces fresh failures.
166+
167+
```bash
168+
uv run python -m sim.scripts.dagger \
169+
--repo-id local/safe-cube-dagger --root data/safe_cube_dagger \
170+
--output-root outputs/safe_pi0_dagger \
171+
--rounds 3 --episodes-per-round 40 \
172+
--base-policy outputs/safe_pi0_bc/checkpoints/last/pretrained_model \
173+
--safety-weight 1.0 --steps 8000 --batch-size 8
174+
```
175+
176+
Use `--no-train` to only collect, or `--print-only` to see the constructed
177+
training commands without running them.
178+
179+
### 4. Evaluate
180+
181+
`sim.evaluate.evaluate()` reports success / red-contact / clearance / ceiling
182+
rates. Wrap a checkpoint with `sim.dagger.PolicyRollout` and pass its `.act` as
183+
the `policy` argument:
104184

105185
```python
106-
from sim import SafeCubeEnv, EnvConfig, ExpertConfig
107-
from sim.expert import ScriptedExpert
108-
from sim.recorder import EpisodeRecorder
109-
110-
env = SafeCubeEnv(EnvConfig(...))
111-
expert = ScriptedExpert(env=env, cfg=ExpertConfig())
112-
113-
for j in range(N_DAGGER_ROUNDS):
114-
alpha = ALPHA0 ** j # mixing weight: expert-heavy -> policy-heavy
115-
for ep in range(EPISODES_PER_ROUND):
116-
obs, info = env.reset(seed=...)
117-
rec.begin_episode(task=...)
118-
expert.reset()
119-
while not done:
120-
expert_action = expert.act(info)
121-
policy_action = policy.act(obs) # your π0 inference
122-
use_expert = rng.random() < alpha
123-
action = expert_action if use_expert else policy_action
124-
# Always label with the expert action (DAgger).
125-
rec.add(obs, expert_action, info)
126-
obs, _, terminated, truncated, info = env.step(action)
127-
rec.save_episode()
128-
train_one_pass(policy, dataset, L_task + lam * L_safety)
186+
from sim import SafeCubeEnv, EnvConfig
187+
from sim.dagger import PolicyRollout
188+
from sim.evaluate import evaluate
189+
190+
env = SafeCubeEnv(EnvConfig())
191+
roll = PolicyRollout(
192+
checkpoint="outputs/safe_pi0_dagger/round_2/checkpoints/last/pretrained_model",
193+
dataset_repo_id="local/safe-cube-dagger", dataset_root="data/safe_cube_dagger",
194+
)
195+
task = "pick up the blue cube ... place it on the goal patch"
196+
res = evaluate(env=env, policy=lambda obs: roll.act(obs, task), n_episodes=50)
197+
print(res.summary())
129198
```
130199

200+
### 5. Deploy
201+
202+
Identical to a vanilla π0: `image → π0 → action`. No SDF, no cube positions, no
203+
filter — the safety preference lives in the weights. (On real hardware, keep the
204+
thin clearance watchdog from `project_summary.md` §7.4 as insurance.)
205+
131206
## Knobs worth tuning first
132207

133208
| Knob | Where | Effect |
-1.79 MB
Binary file not shown.
-460 KB
Binary file not shown.
-1.08 MB
Binary file not shown.
-1 MB
Binary file not shown.
-1.35 MB
Binary file not shown.
-863 KB
Binary file not shown.
-845 KB
Binary file not shown.
-932 KB
Binary file not shown.

0 commit comments

Comments
 (0)