File size: 11,450 Bytes
b71d051 | 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 | ---
license: apache-2.0
library_name: openpi
pipeline_tag: robotics
tags:
- robotics
- manipulation
- pi0
- pi05
- vla
- embodichain
- robosynchallenge
- bimanual
---
# click_bell β Ο0.5 policy for RoboSynChallenge
A Ο0.5 (pi05) policy finetuned on the **RoboSynChallenge `click_bell`** task: a bimanual
CobotMagic robot must press a desk bell placed at a random position on the table.
Trained from `pi05_base` on 1,000 synthetic demonstrations
(`RoboSynChallenge/cobotmagic_Sim_click_bell`) for 20,000 steps.
- **Checkpoint**: step 19999 (final)
- **Framework**: [openpi](https://github.com/Physical-Intelligence/openpi) (JAX / Orbax)
- **Simulator**: [EmbodiChain](https://dexforce.github.io/EmbodiChain/) 0.2.3 + dexsim 0.4.3
- **Benchmark**: [RoboSynChallenge](https://github.com/EDEM-AI/RoboSynChallenge)
---
## Results
Evaluated in simulation on an RTX 4090. Every number below is from a run of this exact
checkpoint; nothing is copied from a paper or leaderboard.
### Headline
| Button position range | `clear` (no randomization) | `random` (full domain randomization) |
| --- | --- | --- |
| Repo default β xβ[0.40, 0.85], yβ[β0.30, +0.30] | 13/20 = **65%** | 14/20 = **70%** |
| Right-arm reachable β xβ[0.40, 0.72], yβ[β0.30, +0.06] | 30/30 = **100%** | 28/30 = **93.3%** |
**The gap between those two rows is not a model limitation.** The default sampling range
contains a large region that the right arm physically cannot reach, and this policy only
ever uses the right arm (see [Known limitations](#known-limitations)). Restricted to
positions the arm can actually reach, the policy solves the task essentially perfectly
without domain randomization, and loses only ~7 points with it.
### Efficiency
| Metric | `clear` | `random` |
| --- | --- | --- |
| Steps to success (median) | 60 | 60 |
| Inference calls per episode (median) | 6 | 6 |
| Inference latency (median) | 0.387 s | 0.397 s |
Successful episodes are tightly clustered at 60 steps / 6 inference calls β the policy
either solves it in one smooth approach or not at all. There is no "retry until it works"
behaviour.
### Training-step comparison
Same seeds, same configuration, three wrist+head camera streams recorded:
| | step 10000 | step 19999 |
| --- | --- | --- |
| `clear` | 13/20 = 65% | 13/20 = 65% |
| `random` | 10/20 = 50% | 14/20 = 70% |
Doubling training steps did **not** change `clear` at all, and improved `random`. A
per-episode breakdown shows exactly where the gain came from: failures of the type
"end-effector hovers 2β10 mm above the bell without pressing" dropped from 4 to 2, and
"pressed but not deep enough" dropped from 2 to 0. In other words the extra training
bought **press precision under visual perturbation** and nothing else.
Caveat: at n=20 these differences are not statistically separable β see
[Reproducibility](#reproducibility).
---
## Usage
This is an Orbax checkpoint for openpi. It is not a `transformers` model and will not
load with `AutoModel`.
```
click_bell/
βββ params/ # Orbax PyTree β model weights
βββ assets/
β βββ RoboSynChallenge/
β βββ cobotmagic_Sim_click_bell/
β βββ norm_stats.json # normalization stats (required)
βββ _CHECKPOINT_METADATA
```
### Download
```bash
hf download puheliang/click_bell --local-dir ./click_bell_19999
```
### Load
```python
from openpi.training import config as _config
from openpi.policies import policy_config as _policy_config
train_config = _config.get_config("pi05_base_robosynchallenge_full")
policy = _policy_config.create_trained_policy(train_config, "./click_bell_19999")
action_chunk = policy.infer({
"observation/image": img_head, # (H, W, 3) uint8
"observation/left_wrist_image": img_left_wrist, # (H, W, 3) uint8
"observation/right_wrist_image": img_right_wrist, # (H, W, 3) uint8
"observation/state": qpos, # joint positions
"prompt": "Click the bell",
})["actions"]
```
Images must be **HWC uint8**, not CHW. Norm stats are read automatically from
`assets/<asset_id>/` inside the checkpoint directory, where `asset_id` comes from the
train config (`RoboSynChallenge/cobotmagic_Sim_click_bell`).
`train_state/` (optimizer state, 31 GB) is **not** included β this checkpoint is for
inference and evaluation, not for resuming training.
### Evaluate in RoboSynChallenge
```bash
# place under policy/pi05/checkpoints/<train_config>/<model_name>/19999/
bash policy/pi05/eval.sh click_bell random \
pi05_base_robosynchallenge_full pi05_click_bell_baseline 0 \
--checkpoint_id 19999 --max_episodes 30 --headless true \
--eval_video_obs_keys cam_high,cam_left_wrist,cam_right_wrist
```
The checkpoint directory **must be named with the bare step number** (`19999`) β the
adapter does `int(checkpoint_id)`.
---
## Task definition
Success is decided purely by the physical displacement of the button
(`robosynchallenge/tasks/click_bell/click_bell.py`):
```python
press_depth = -button_qpos[:, 0]
success = press_depth >= 0.0048 # button joint travel is [-0.005, 0]
self._button_pressed |= success # latched for the episode
```
The threshold is **4.8 mm out of 5.0 mm of total travel (96%)** β a light touch does not
count. Success is latched: one qualifying frame marks the episode successful.
### `clear` vs `random`
Both settings randomize the **bell position identically** over
xβ[0.40, 0.85], yβ[β0.30, +0.30]. The difference is 10 additional perturbations present
only in `random`:
| Perturbation | Applied |
| --- | --- |
| Light position / colour / intensity (10β30, a 3Γ range) | every 10 steps |
| Material of table, robot, floor, button (50% chance of random texture) | every 10 steps |
| Head camera intrinsics (Β±50 px focal) and extrinsics (Β±2 cm, Β±10Β°) | each reset |
| Robot initial EEF pose (Β±1 cm) and joint angles (Β±0.05 rad) | each reset |
| Two distractor objects (cup / fork / spoon), random pose, β₯8 cm from the bell | each reset |
Note that lighting and materials are re-randomized **within** an episode, so the policy
must cope with appearance changing mid-trajectory.
---
## Known limitations
These were found by instrumenting all 110 evaluation episodes and are, in our view, more
useful than the headline number.
### 1. The policy only ever uses the right arm
Across **110/110 episodes it never once used the left arm** β at step 10000 and at step
19999 alike. Success is therefore strongly asymmetric:
| Bell position | Success rate |
| --- | --- |
| y < β0.10 (right side) | **90%** |
| y β [β0.10, +0.18) | 65% |
| y > +0.18 (left side) | **20%** |
The root cause is upstream, in the expert data generator
(`robosynchallenge/tasks/click_bell/action_bank.py:57`), flagged by its own authors:
```python
# FIXME FIXME FIXME FIXME
logger.log_warning("CAUTION====THIS FUNC generate_left_arm_aim_qpos IS WRONG!!!! PLEASE FIX IT!!!!")
```
The right-arm equivalent is marked `# DONE`. Worse, the collection loop
(`scripts/run_env.py:_generate_function`) silently **resets and re-randomizes** whenever
action generation fails, leaving no record:
```python
valid = generate_and_execute_action_list(...)
if not valid:
_, _ = env.reset(options={"save_data": False}) # try a different scene
break
```
So left-side scenes are systematically dropped from the dataset rather than sampled and
failed. The policy never sees a left-arm demonstration and cannot invent one. **More
training will not fix this β the data has to be regenerated after the function is fixed.**
### 2. Part of the default evaluation range is unreachable
An IK sweep of the right arm's press pose over a grid of bell positions gives a clean
diagonal boundary (`O` reachable, `.` not):
```
y=-0.30 -0.00 +0.30
x=0.40 O O O O O O O O O O O O O O O O O O O O .
x=0.50 O O O O O O O O O O O O O O O O O O O . .
x=0.60 O O O O O O O O O O O O O O O O . . . . .
x=0.70 O O O O O O O O O O O O O . . . . . . . .
x=0.80 O O O O O O O . . . . . . . . . . . . . .
x=0.84 . . . . . . . . . . . . . . . . . . . . .
```
Roughly **35% of the default sampling area is out of the right arm's workspace**. Note
this is a property of the *config shipped for data collection*, not an official benchmark
specification β RoboSynChallenge's official ranking is on held-out physical robots, and
the repository ships no evaluation config of its own.
### 3. Failure breakdown
Of 16 failures over 40 instrumented episodes at step 10000:
| Cause | Count | Whose problem |
| --- | --- | --- |
| Hovers 2β10 mm above the bell, never contacts | 9 | the policy |
| Right arm cannot reach (all had y > +0.18) | 5 | task configuration |
| Pressed 4.3β4.7 mm, threshold is 4.8 mm | 2 | success criterion |
Successes and failures separate almost perfectly by a single number β the closest
approach of the right end-effector to the bell:
| | Closest approach |
| --- | --- |
| 24 successes | 2.0 β **2.8** cm |
| 16 failures | **2.7** β 24.4 cm |
Press depth is bimodal, not continuous: successes all bottom out at 5.00 mm, and 14 of 16
failures never leave the 0.95 mm resting value. The task is all-or-nothing.
### 4. Reproducibility
Because the threshold sits at 96% of total travel, marginal episodes flip between runs
from GPU floating-point non-determinism alone. With the same seed we observed episodes
going `fail β success` and `success β fail` across repeats, and `random` scored 60% / 45%
/ 50% / 70% across four 20-episode runs of the same configuration.
`clear` reproduced exactly (13/20 three times). **Use β₯50 episodes when comparing
checkpoints**, or the noise will exceed the effect.
---
## Reproducing the evaluation
The upstream `policy/pi05/` adapter is out of date relative to its own `openpi` copy and
to `scripts/eval_policy.py`; evaluation cannot run without these four fixes (all present
in `policy/pi0/`, which is current):
| File | Problem | Fix |
| --- | --- | --- |
| `pi_model.py` | passes `robotwin_repo_id`, which this openpi does not accept | drop it β openpi resolves norm stats from `data_config.asset_id` |
| `pi_model.py` | `__init__` does not accept `pytorch_device`, but `deploy_policy.py` passes it | add the parameter and forward it |
| `pi_model.py` | builds an aloha-style observation (`{"state", "images"}`, CHW) | build flat `observation/*` keys in HWC, as `EmbodiChainInputs` expects |
| `deploy_policy.py` | `eval()` returns 2 values, `eval_policy.py` unpacks 3 | also return `truncated` |
One further trap: in `scripts/eval_policy.py` the episode loop's `finally` calls
`env.close()`, which terminates the process **before Python prints the traceback**, and
the exit code is 0. Every one of the bugs above therefore presented as "the run finished
normally but the robot never moved." Printing the exception inside the loop before
`env.close()` runs is what made them findable.
---
## Citation
```bibtex
@misc{click_bell_pi05,
title = {click_bell: a pi0.5 policy for the RoboSynChallenge bell-pressing task},
author = {puheliang},
year = {2026},
url = {https://huggingface.co/puheliang/click_bell}
}
```
Base model: `pi05_base` (Physical Intelligence).
Benchmark and training data: [RoboSynChallenge](https://github.com/EDEM-AI/RoboSynChallenge) (EDEM-AI).
|