YAM Pick-Duster 200 β B-spline Diffusion Policy (end-effector)
A B-spline Policy UNet diffusion policy trained on
Dimios45/yam-pick-duster-200-ee:
200 teleop demos of an I2RT YAM arm picking a blue duster and placing it in a red box.
This model emits absolute Cartesian poses and requires IK on the robot. The joint-space
counterpart β same 200 takes, no IK, and the one recommended for deployment β is
Dimios45/yam-pick-duster-200-bspline-joint.
The pair exists to compare action spaces on identical demonstrations.
Read this before deploying. The dataset card states these poses are MuJoCo FK and disagree with bspline-policy's pyroki/URDF by up to ~9 mm, and advises: "do not deploy a policy trained on this data through its Cartesian path without resolving the discrepancy β drive the arm in joint space instead." Their suggested mitigations are
EEFollowerwith lowmu,target_frame="tcp", and reach clamping.
Supersedes the 50-episode
yam-pick-duster-bspline-ee.
Action and observation space
raw actions (7,) = [pos(3), rotvec(3), gripper(1)]
-> dataset expands to (10,) = [pos(3), rot6d(6), gripper(1)]
gripper: 0 = open, 1 = closed
| key | shape | notes |
|---|---|---|
wrist_image |
(3, 84, 84) |
RGB, RAW 640x480 resized β not cropped |
top_image |
(3, 84, 84) |
RGB, overhead |
arm_pos |
(3,) |
TCP position, metres |
arm_quat |
(4,) |
xyzw, w >= 0 hemisphere |
gripper_pos |
(1,) |
0 = open, 1 = closed |
rotation_rep: rotation_6d with abs_action: True. infer_action_meta resolves this to
single_yam_rot6d. Network output is (16, 11): column 0 is the knot vector in units of
10 Hz frames, columns 1-10 are control points.
TCP frame is the link_6 flange with a fixed 90 degree z-rotation, ~13.5 cm from the fingertip.
Files
| file | size | use |
|---|---|---|
deploy_ema.ckpt |
426 MB | Inference. EMA weights only. |
epoch0400_full.ckpt |
1.5 GB | model + ema_model + optimizer, for resuming/fine-tuning. |
Training
| data | 200 episodes, 30,462 frames @ 10 Hz -> 30,262 B-spline chunks |
| hardware | 1x RTX 4090, ~2.6 h, 23 s/epoch, 473 batches/epoch |
| epochs / batch | 401 / 64 |
| optimizer | AdamW, lr 1e-4, cosine, 500 warmup, EMA |
| scheduler | DDIM, 100 train timesteps, 16 inference steps, epsilon prediction |
| B-spline | degree 3, chunk_size 10, max_error 0.002, absolute knots |
Loss: 0.408 (ep0) -> 0.015 (50) -> 0.011 (100) -> 0.008 (150) -> 0.006 (200) -> 0.005 (250) -> 0.003 (300-350) -> 0.002 (400), final in-epoch 0.00157.
The 401-epoch budget was chosen on gradient-step count (~190k steps), not copied from the
50-episode run. checkpoint_every: 50 with 401 (not 400) epochs so the fully-annealed final
epoch is actually written.
These numbers are not comparable to the joint-space model's: different action space, units, and normalizer. Only the shape of each curve is meaningful on its own.
Inference latency
Measured on an idle GPU, batch 1, two 84x84 cameras:
| DDIM steps | RTX 4090 | CPU (i9-13900K, 8 threads) |
|---|---|---|
| 4 | 13.4 ms | 57.6 ms |
| 8 | 24.2 ms | 91.0 ms |
| 16 | 45.8 ms | 168.1 ms |
A chunk spans ~1.1 s of wall time at 10 Hz, so all settings clear comfortably even on a NUC-class CPU (expect 2-3x the CPU column). 16 steps is a safe default here.
Rollout
cd ~/bspline-policy
export PYTHONPATH=$PWD/bspline_policy:$PWD/diffusion_policy:$PWD/real_env/yam_teleop
hf download Dimios45/yam-pick-duster-200-bspline-ee deploy_ema.ckpt --local-dir ./ckpt
# terminal 1 - arm server
sudo ip link set can_follower_r up type can bitrate 1000000
python real_env/yam_teleop/yam_server.py --channel can_follower_r
# terminal 2 - rollout
python real_env/yam_teleop/rollout_local_policy.py \
--env yam-vrkit --policy bspline \
--ckpt-path ./ckpt/deploy_ema.ckpt \
--diffusion-policy-dir $PWD/diffusion_policy \
--control-freq 100 \
--data-freq 10 \
--origin-time-scale 10 \
--num-inference-steps 16 \
--predict-before-end 0.3 \
--speed-up-times 1.0 \
--save --output-dir data/rollouts_ee_200
--env yam-vrkit is not in upstream bspline-policy. rollout_local_policy.py accepts
only x5, tidybot2, and yam, so this fails at argparse until a matching env class is
added. Substituting --env yam routes through RealEnv + yam_server's pyroki velocity IK β
which is exactly the Cartesian path the dataset card warns about above.
Flags that are not optional
| flag | value | why |
|---|---|---|
--origin-time-scale |
10 | Knots are in data-frame units; must equal the training rate. The joint-space counterpart is 25 Hz β never copy this flag between the two models. Using 25 here runs the arm at 2.5x speed. |
--data-freq |
10 | Must match the above. |
--control-freq |
100 | Matches YAM_CONTROL_HZ. |
--speed-up-times |
start at 1.0 | Velocity scales linearly, acceleration quadratically. |
--predict-before-end |
0.3 | Must exceed inference latency. |
A missing top_image key does not raise: policy_local_bspline.py:625-629 substitutes a
black frame, so an unwired or dead camera yields a half-blind policy rather than an error.
Cameras, gripper, data notes
Trained uncropped (RAW 640x480 -> 84x84); reproduce exactly at deployment. Gripper is 0 = open, 1 = closed β verify on hardware first (see the joint-space card for the one-line check). All 200 episodes kept, including the 2 aborted takes and 14 with re-grasps.
Reproducing
cd real_env/yam_teleop && python convert_to_robomimic_hdf5.py \
--input-dir ~/data/yam-200-ee --output-path ../../data/yam_ee_200.hdf5
cd bspline_policy && python train.py \
--config-name=yam_pick_ee_200_bspline \
hydra.run.dir=../outputs/yam_pick_ee_200 \
training.resume=false logging.mode=offline \
checkpoint.topk.k=999 dataloader.persistent_workers=True
Give this task config a cache_suffix distinct from every other run, or it will silently load
a stale zarr cache.
Citation
@article{han2026b,
title={B-spline Policy: Accelerating Manipulation Policies via B-spline Action Representations},
author={Han, Xiaoshen and Xiong, Haoyu and Chen, Haonan and Liu, Chaoqi and
Torralba, Antonio and Zhu, Yuke and Du, Yilun},
journal={arXiv preprint arXiv:2607.09648},
year={2026}
}