Scripted demonstrations (eden datagen)¶
eden datagen rolls a task's scripted demonstrator out in parallel envs and records every episode to HDF5 — the
same layout as a teleoperated recording, so eden lerobot convert reads either. The demonstrators are a port of
OCBench's scripted policies (eden/extensions/scripted_demos/), built to
produce data that moves like a person — smooth, randomly paced, with retries and deliberate mistakes — rather than
a rule-based controller with noise on top. This page covers running the generator, what it records, and how to
write a demonstrator for a new task.
Run it¶
eden datagen --task robosuite_stack --num_episodes 1000 --num_envs 32 --output_dir data/demos
eden datagen --task robosuite_stack --robot ur5e_2f85 --export succeeded # OCBench's own arm, keep successes
eden datagen --task robosuite_lift --p_mistake 0.0 # no deliberate mistakes
eden datagen --task robosuite_pick_place --camera_only # sees through the task camera
eden datagen --task robosuite_door --latch no_latch # a task variant flag
eden datagen --task simpler_put_carrot_on_plate \
--override 'env_options: {use_gjk_collision: false}' # SIMPLER: see below
| Flag | Default | Meaning |
|---|---|---|
--task |
— | A task with a registered demonstrator (below). Its variant flags (--robot, --latch, --object, …) are accepted too; eden datagen --task <name> --help lists them. |
--num_episodes |
100 |
Episodes to end before stopping. |
--num_envs |
16 |
Parallel envs. |
--output_dir / --filename |
data/demos / the task name |
The dataset is <output_dir>/<filename>.hdf5. An existing file of that name is replaced. |
--export |
all |
all keeps every episode, tagged with its success; succeeded keeps only successes; separate writes failures to <filename>_failed.hdf5. |
--seed |
0 |
Seeds the env and the demonstrator. |
--p_mistake |
the demonstrator's own | Probability that a subgoal is a deliberate mistake. |
--episode_length_s |
the demonstrator's own, else 90 | The generator's episode time limit. |
--camera_only |
off | Run without the simulator's object state (demonstrators that support it). |
--show_viewer, --cpu |
off | Open the Genesis viewer; use the CPU backend. |
--config, --override |
— | Layer a config file or inline YAML onto the task config, as for eden train. |
The run ends by printing a report: episodes, successes, how many were cut short, steps, wall time, and subgoals
started by kind (goal, clear, mistake) — or, for a tool routine, motions ended by <tool>:<status>. For a
tool routine the trace of the first unsuccessful episode follows; it is the place to start when the success rate
is low. Logs go under logs/datagen/<task>/.
Tasks with a demonstrator¶
SCRIPTED_DEMO_REGISTRY (eden/extensions/scripted_demos/registry.py) maps a task name to its demonstrator
factory. A task without one is refused with the list of tasks that have one.
| Task | Demonstrator | Variant flags, camera-only |
|---|---|---|
robosuite_lift, robosuite_stack |
keyframe (LiftController, TowerController) |
--robot |
robosuite_pick_place |
tool routine (eden/tasks/robosuite/pick_place/routine.py) |
--object; camera-only |
robosuite_door |
tool routine (door/routine.py) |
--latch |
robosuite_wipe |
tool routine (wipe/routine.py) |
|
robosuite_tool_hang |
tool routine (tool_hang/routine.py) |
|
robosuite_two_arm_transport |
two-arm routines (two_arm_transport/routine.py) |
|
simpler_stack_green_cube_on_yellow_cube |
keyframe (TowerController) |
|
simpler_put_carrot_on_plate, simpler_put_spoon_on_table_cloth, simpler_put_eggplant_in_basket |
tool routine (eden/extensions/scripted_demos/put_on_routine.py) |
camera-only |
polaris_block_stack_kitchen_batched, polaris_food_bussing_batched, polaris_pan_clean_batched, polaris_move_latte_cup_batched, polaris_organize_tools_batched, polaris_tape_into_container_batched |
tool routine (put-on, bound in eden/tasks/polaris/scripted_demos.py) |
|
whiteboard_write_text, whiteboard_write_in_colour, whiteboard_draw_table |
the task's own ScriptedDrawer |
|
art_calligraphy_dai, art_calligraphy_san, art_calligraphy_ei, art_calligraphy_lettering_minimum |
the task's own ScriptedWriter |
|
art_painting_sunset, art_painting_fuji_study, art_painting_fuji |
the task's own ScriptedPainter |
The whiteboard and art demonstrators move a held tool through the arm's IK alone, with no gripper action and no
randomness; they accept only --p_mistake 0. See Whiteboard and Art.
What an episode records¶
The generator replaces the task's recorder options with this set, written with HDF5FileHandler:
| Key | Recorder | Content |
|---|---|---|
obs/<group> |
ObservationRecorder |
Every observation group at the start of each step — what the step's action was chosen from. |
action |
ActionRecorder |
The action sent to the task's own action terms, so it replays as-is. |
state/{qpos,dofs_vel,pos,quat} |
StateRecorder |
The arm's state; a two-arm task records both arms, concatenated in scene order. eden lerobot convert reads this as observation.state. |
reward, done, terminated, timeout, timestamp |
RLStepRecorder |
Per-step RL buffers. |
initial_state/entities/... |
InitialStateRecorder |
Every entity's state after reset, for replay. |
attribute success |
— | The task's success metric when the episode ended. |
An episode ends when the demonstrator has finished the task (its success is the task's success metric at that
step), when the task ends it (a termination, e.g. an object off the table), or at the generator's time limit — the
generator enforces that limit itself, because a task need not declare a time-out. Episodes still running when
--num_episodes is reached are dropped, not written cut short. The env is built in eval mode. The layout and its
conversion are described in Datasets and recording.
The generator refuses a task with no success metric (no metric term with contributes_to_success=True), since
its episodes could not be labelled, and it refuses export_mode=EXPORT_NONE.
SIMPLER tasks record no observations
SIMPLER tasks are evaluated from the camera and declare no observation groups, so their episodes have no
obs/* keys (and no images).
What makes the data demo-like¶
| Property | Where | What it does |
|---|---|---|
| Smooth, non-Markovian motion | trajectory.py |
Each subgoal is a keyframe plan executed on a per-env clock: Hermite splines that come to rest at stop keyframes (before closing, at the place, at the end) and pass through the others; rotation and gripper ease with a quintic smoothstep. |
| Human speed profile | trajectory.py |
Segments are stretched so a minimum-jerk profile never exceeds a peak tool speed: T >= 1.875 d / v_peak. |
| Speed varies per episode | controller.py |
One segment_dt per episode, uniform in speed_range (default (0.7, 1.5) s). Tool routines draw a speed factor from their own speed_range (default (0.8, 1.25)). |
| Varied grasps and paths | primitives.py |
Grasp face (usually the nearest), wrist tilt up to max_tilt (25° by default), approach heights, contact offsets, an optional pause before lifting, when the fingers close, and a carry midpoint that bows sideways and sometimes overshoots. |
| Recovers from failure | controller.py |
A pick whose object does not follow the hand is re-planned from wherever the hand is (max_pick_retries=2), every random offset scaled by 0.75 per retry. |
| Suboptimal high-level plans | block_world.py |
With probability p_mistake (default 0.1) a subgoal is replaced by a wrong one — a block moved to a random free spot or onto another — and the demonstrator works its way back. |
| Unsaturated actions | rig.py + the task binding |
See Speed and saturation. |
The robot it drives¶
A demonstrator drives an arm with a two-finger gripper through the task's own action terms. It reads what it
needs from the scene entry holding the robot (gripper_rig() in rig.py); two shapes qualify:
| Scene entry | Arm / fingers | IK link | Tool point |
|---|---|---|---|
a ManipulatorOptions composite (robosuite's rigs, DROID, UR5e + Robotiq) |
the arm entity / the welded gripper entity | the composite's ee_link |
the gripper's tcp_offset, carried through the weld |
a RobotOptions whose asset has its own fingers (SIMPLER's WidowX250) |
every actuated DOF except those of open_dofs_pos / the DOFs of open_dofs_pos |
ee_links_name[0] |
the robot's tcp_offset, in that link's frame |
A robot of the second shape must declare open_dofs_pos, ee_links_name and tcp_offset; a missing one is
refused by name. tcp_offset is the pad midpoint with the fingers open — measure it off the built finger pads
(the WidowX's is (0.0935, 0, 0) in its IK link). Its direction is the approach axis: +z for a Franka Hand or
a Robotiq mount, +x for the WidowX.
The action terms are found by what they drive, not by name (find_rig_actions()): one
DifferentialIKController on the arm's IK link and one BinaryJointController on the fingers. robosuite names
them robot_arm / robot_gripper, SIMPLER and PolaRiS ee_controller / gripper_controller. Every action term
of the task must be one of the two (for one arm, or for each arm of a two-arm task).
How it drives the robot¶
The demonstrator plans the tool point and converts it to the task's action every step (RigDriver in
rig.py): a 6-D world-frame delta of the IK link's pose, at most max_step_translation per axis (default
0.05 m) and max_step_rotation (default 0.3 rad) per step — OCBench's ee_action_delta clip — and 2g − 1
for the binary gripper. The IK term squashes its input with tanh, so the recorded arm action is
atanh(delta / scale), with the ratio capped at 0.9 (MAX_COMMAND_RATIO).
Object poses come from the scene in their authored frames (get_pos(relative=True)), never from the noisy policy
observations. The keyframe controller also handles what OCBench did not need on Eden's robots:
- Headings respect the wrist. A Panda's last joint turns ±2.9 rad and the task's damped-least-squares IK has
no posture term, so the arm drifts in its null space. Every heading is moved by a symmetry of what it grasps (a
quarter turn for a cube, a half turn for the gripper) into the range the last joint can still reach
(
yaw_window()). - Plans start from the hand's actual orientation, not its heading alone (the robosuite Panda's home pose is tilted).
- The plan waits at the grasp and the release. Contact gates hold the plan clock until the hand is within
contact_gate_tolerance(1 cm) of the gate — or has come to rest against contact within 4 cm of it (slower than 1 cm/s for 0.6 s:SETTLE_SPEED,SETTLE_TIME,SETTLE_REACH), the way a person lowers a cube onto another until it stops. - A pick is judged by whether the object follows the hand: the check waits until the hand has risen
three quarters of the way to its hover and fails the pick only if the object rose less than half as much
(
PICK_CHECK_RISE,PICK_FOLLOW_FRACTION). - The fingertips, not the tool point, stay above the table. The workspace floor is for the fingertips: the
driver measures how far the fingers reach past the tool point on the built rig (
tip_reach) and raises the tool's floor by it. - The gripper never closes during the descent: closing ramps over the contact segment or the pause before lifting only, which suits a binary gripper.
Speed and saturation¶
The task's action realizes only part of each command per step (damped IK into a position-PD arm), so an arm asked to follow a fast plan lags it, and the command saturates — the bang-bang data the demonstrators exist to avoid. Each binding sizes its peak speed and step clip to what its action realizes:
- robosuite and SIMPLER bindings use a peak speed of
0.12m/s, PolaRiS0.15m/s; OCBench's own default (PickPlaceParams.peak_speed) is0.3. - SIMPLER's WidowX has a generous IK scale (0.5 m per action unit) but trails its plan at 5 Hz, so its binding
lifts
max_step_translationto0.1m, caps the wrist tilt at0.1rad, and uses Bridge-scale hover heights (_DESCENT_HEIGHT = (0.04, 0.12)).
A new binding should measure how often its arm commands sit at the clip before choosing peak_speed and
max_step_translation.
Writing a demonstrator¶
Register a factory under the task's name, next to the task (eden/tasks/<suite>/scripted_demos.py), and list
that module in SCRIPTED_DEMO_REGISTRY's populated_by. A factory is (env, *, seed, p_mistake) ->
demonstrator; one that also takes privileged has a camera-only mode, and one whose episodes need longer than
90 s declares it as an attribute (my_factory.episode_length_s = 200.0).
Keyframe demonstrators¶
A keyframe demonstrator is a batched state machine: fast, but a new kind of task means a new subclass. Block
worlds reuse TowerController or LiftController (block_world.py):
from eden.extensions.scripted_demos.block_world import TowerController
from eden.extensions.scripted_demos.primitives import PickPlaceParams
from eden.extensions.scripted_demos.registry import SCRIPTED_DEMO_REGISTRY
@SCRIPTED_DEMO_REGISTRY.register(name="my_three_block_tower")
def my_three_block_tower(env, *, seed=0, p_mistake=0.1):
return TowerController(
env, ["base", "middle", "top"], table_z=0.8,
spot_bounds=((-0.12, -0.15), (0.12, 0.15)),
workspace=((-0.35, -0.35, 0.805), (0.35, 0.35, 1.25)),
effector_bounds=((-0.2, -0.15, 0.92), (0.05, 0.15, 1.1)),
p_mistake=p_mistake, seed=seed, params=PickPlaceParams(peak_speed=0.12),
)
Any other pick-and-place task subclasses ScriptedPickPlaceController and implements
choose_subgoals(env_ids, objects) -> list[Subgoal | None], returning None for an env whose task is complete.
Tool routines¶
A tool routine is a scripted policy written for one robot as a plain Python generator, against a handful
of robot tools — the form a person or a coding agent writes a demonstrator in. RoutineDemonstrator
(routines.py) runs one instance per env, batched:
import math
from eden.extensions.scripted_demos.registry import SCRIPTED_DEMO_REGISTRY
from eden.extensions.scripted_demos.routines import RoutineDemonstrator
def pick_and_lift(tools):
for attempt in range(3):
cube = tools.object_footprint("cube") # or tools.locate("cube") through a camera
yaw = cube.yaw + math.pi / 2 - tools.finger_axis_yaw # close across the short side
yield tools.open_gripper()
yield tools.move_to(cube.top_center + (0, 0, 0.10), yaw)
yield tools.move_to((*cube.top_center[:2], cube.top_z - 0.02), yaw)
yield tools.close_gripper()
yield tools.move_by((0, 0, 0.15))
if tools.holding():
return
tools.note(f"attempt {attempt} missed (closure {tools.gripper_closure():.2f})")
@SCRIPTED_DEMO_REGISTRY.register(name="my_lift")
def my_lift(env, *, seed=0, p_mistake=0.0):
return RoutineDemonstrator(env, pick_and_lift, seed=seed, workspace=((-0.35, -0.35, 0.805), (0.35, 0.35, 1.25)))
A motion is yielded, and the routine gets an Outcome back when the motion has ended. Its status is one of
reached, contact, done, timeout, blocked or no_contact, and outcome.ok is true for the first three,
so checks and retries are ordinary Python. Routines resume only at those boundaries; every motion in flight is
evaluated as batched torch and turned into the task's actions by the same RigDriver the keyframe controller
uses.
Tool (tools.py) |
Kind | What it does or reads |
|---|---|---|
move_to(pos, yaw), move_by(offset) |
motion | Straight, minimum-jerk path at the episode's speed; yaw is turned by a half turn into the range the last joint reaches. Ends reached within tolerance (default 1 cm) or timeout (timeout_s, default 2 s past the path's own duration). |
move_to(pos, rot=tools.aim(approach, closing)) |
motion | The same to a full effector rotation (a hand that leans or points sideways), judged on the whole rotation. |
move_until_contact(direction, max_distance) |
motion | Constant speed until a finger presses on something that is not the robot (contact), the hand is held back without one (blocked), or the distance is used up (no_contact). |
close_gripper(), open_gripper(), wait(s) |
motion | Hold the arm, set the fingers; done. |
tool_pos(), tool_yaw(), tool_rotation(), gripper_closure(), holding() |
robot | The robot's own state. holding(): commanded closed and stopped short of closing (holding_closure, default 0.9). |
heading_window() |
robot | The lowest and highest heading the last joint can turn the hand to from here. |
object_footprint(name), grasped(name) |
privileged | Off the simulator: the footprint of the collision mesh; two finger links touching the object. |
link_pose(name, link=None) |
privileged | A link's (or the root's) pose in the asset's authored frame — for an articulated fixture's parts, or a kinematic entity with no collision mesh. |
locate(name, camera=None) |
camera | The footprint of the pixels showing name, back-projected through depth: what the camera sees, occlusion included. The segmentation stands in for a perfect text-prompted segmenter. |
shared, rng, note(text) |
episode | A per-episode dict (shared between arms), the episode's random stream, and a line in the trace. |
A Footprint is the same whichever way it was measured — center, yaw (of the long axis), half_length,
half_width, top_z, bottom_z, and top_center (where a hand coming from above meets it, off the centre when
the object leans) — so a routine written against it runs privileged or camera-only unchanged.
Camera-only. RoutineDemonstrator(..., privileged=False, cameras=("agentview",)) refuses object_footprint
and grasped with PrivilegedToolError, naming the camera tool to use instead, so a demonstrator meant for
deployment cannot read simulator state by accident. With several envs each camera must render every env:
eden datagen --camera_only sets env_options.env_separate_rigid for you; built by hand, the demonstrator refuses
a shared camera. Camera-only runs render every env's view at each routine boundary, so they are slower.
The trace. Every ended motion and every tools.note(...) is a line of demo.trace(env_id):
36.75 s # bread -> goal quadrant (0.149, 0.083)
37.25 s move_to(0.214, -0.311, 0.953, yaw=-1.06) -> reached in 1.50 s (off target 10 mm)
38.75 s move_to(0.214, -0.311, 0.844, yaw=-1.06) -> timeout in 3.50 s (off target 185 mm)
47.05 s # bread: pick 1 missed (closure 1.00)
A routine written for one task lives with that task, in eden/tasks/<suite>/<task>/routine.py, as a factory that
returns the routine; the suite's scripted_demos.py binds it to the scene. A routine several suites share lives
in eden/extensions/scripted_demos/ (put_on_routine.py, used by SIMPLER and PolaRiS). Each routine module's
docstring records the design decisions its rollouts forced — read it before changing the routine.
Two-arm routines¶
A two-arm task is two routines, one per arm, each written against its own Tools and run side by side by
MultiArmDemonstrator (multi_arm.py). Each arm resumes its routine when its own motion ends; they coordinate
through tools.shared, a dict both routines of an env share for the episode:
from eden.extensions.scripted_demos.multi_arm import MultiArmDemonstrator
def giver(tools):
... # carry the object to the hand-off pose
tools.shared["offered"] = True
while not tools.shared.get("taken"):
yield tools.wait(0.1)
yield tools.open_gripper()
def taker(tools):
while not tools.shared.get("offered"):
yield tools.wait(0.1)
... # close on the object
tools.shared["taken"] = True
demo = MultiArmDemonstrator(
env, {"robot0": giver, "robot1": taker},
workspace={"robot0": ((-0.45, -0.75, 0.805), (0.45, 0.2, 1.3)), "robot1": ((-0.45, -0.2, 0.805), (0.45, 0.75, 1.3))},
)
Options that differ per arm are dicts keyed by every arm's scene entry; other options apply to both. A
single-arm RoutineDemonstrator on a two-arm task is refused, naming the other arm's terms. An env's task is
complete when both routines have returned; an arm that finishes first holds still. The trace interleaves both
arms' lines, each prefixed with its arm, and the report counts motions per arm (robot0/move_to:reached).
Running a demonstrator by hand¶
Every demonstrator exposes reset(envs_idx=None), update_subgoals() (returns a per-env "done" mask) and
act() (returns the action dict). The generator's loop, reduced:
from eden.extensions.scripted_demos.registry import SCRIPTED_DEMO_REGISTRY
demo = SCRIPTED_DEMO_REGISTRY.get("robosuite_lift")(env, seed=0)
demo.reset()
while True:
done = demo.update_subgoals()
if done.any():
env.reset(done)
demo.reset(done)
env.step(demo.act())
if env.reset_buf.any(): # the env ended some episodes itself
demo.reset(env.reset_buf)
Reset the demonstrator for every env the env resets: a routine that keeps running after the env has reset the
scene under it acts on a scene it no longer knows. generate_demos() in eden/extensions/scripted_demos/generate.py
is the full version, with the recorder, success labels and the time limit.
Suite notes¶
These are the constraints found while making each suite's demonstrators succeed; the routine module docstrings carry the full reasoning.
robosuite¶
- PickPlace carries objects over the tallest thing in the way, not just the bin rims; places on the robot's
side of the target cell (the far cell centres are at the edge of the Panda's reach); grasps up to 4.5 cm deep
(
GRASP_DEPTH) so the milk carton is not held by its gable top; and waits for the reset's drop to settle before looking. - Door leans the hand 30° away from the panel (
LEAN: a hand pointing straight down has its palm in the panel), presses the handle down until the latch clears the frame, then pulls with the rigid back finger and turns the hand with the door. Door positions are read withlink_pose, since the door is re-posed every reset. - Wipe feels for the table with
move_until_contactat low speed and sweeps just above the touch height: the pad is position-controlled, so commanding it into the table pushes back with tens of newtons per millimetre, and robosuite's excess-force penalty starts at 60 N. It levels the hand first, and stops at every marker. - ToolHang pinches each object above its centre of mass and expects it to turn in the pinch (there is no
torsional friction on the pads), re-measuring the grasp after each turn. The wrench's contacts are stiffened
(
GRASPED_SOL_PARAMS). - TwoArmTransport hands the hammer across in the air, because neither arm reaches both the start bin and the
target bin. robot0 lets go only after robot1's fingers have closed (
takenontools.shared). The robosuite Panda rig uses robosuite's gripper actuation, and the carried objects carry stiffeneddefault_geoms_sol_paramsso they do not creep out of the pinch. The DROID rig (--robot droid) is not covered by this routine.
SIMPLER¶
- Run with MPR collision:
--override 'env_options: {use_gjk_collision: false}'. Genesis's GJK narrowphase can report a non-finite contact between thin convex geoms (the WidowX's finger pads are thin boxes), which a long scripted rollout eventually hits. The generator stops with this fix in its message; catching the error and resetting does not help, because every env's solver state is invalid after it. - Every SIMPLER task enables torsional friction with a 2 cm contact patch on the fingers: without it, a spoon pinched at its thin handle spins about the line between the pads.
RoutineDemonstratorkeeps commanding a move's target through the waits and gripper motions that follow it, because the WidowX's damped IK closes the last centimetre slowly and the pads would otherwise close high.- The put-on routine looks until the object has stopped before picking: eggplants roll across the basin after the reset.
PolaRiS¶
- The demonstrators are registered on the
*_batchedtasks:eden datagenneeds the RL surface and many envs, and the single-env PolaRiS tasks are teleop and inference scenes. - Success is PolaRiS's own rubric (
eden/tasks/polaris/rubric.py, therubricmetric of every PolaRiS task): reach, lift and placement criteria mapped ontoEeNearEntity,object_bottom_above_heightandobject_a_within_b_xy, withRubricMetricTermkeeping PolaRiS's bookkeeping. - Objects wider than the Robotiq 2F-85's stroke (the latte cup, the tape roll) are gripped by their wall
(
grasp="rim"). A thin wall barely stops the fingers, so the PolaRiS demonstrators useholding_closure=0.97. Goal(container=True)drops the object anywhere inside a container, avoiding what already lies in it (avoid=) and staying within reach (max_reach=);clutter=names entities a pinch keeps its fingers off.- The batched tasks render with the batch renderer, which privileged datagen does not use. On a GPU it cannot
launch on, swap in the rasterizer before building:
config.renderer_options = genesis.options.renderers.RendererOptions(). The PolaRiS demonstrators are privileged only: their camera-only mode has not been run.
See also¶
- Datasets and recording — the HDF5 layout and
eden lerobot convert. - Teleoperation — recording human demonstrations of the same tasks.
- Grasp sampling — learned grasp poses.
- robosuite, SIMPLER, PolaRiS — the suites these demonstrators script.