Expert Data Generation#
EmbodiChain supports two expert-authoring paradigms for synthetic demonstration data:
Handwritten trajectories implement the expert in a registered Python task, using
MotionGeneratordirectly or composing reusable Atomic Skills.Task Program declares the task flow in typed YAML and lets the configured environment integration lower Semantic Calls to the same Atomic Skills.
The two paradigms differ only in how the expert plan is authored. Both produce
lazy DemoSegment objects, and both use the
same Gym executor, env.step() path, validation rules, dataset manager, and
transactional commit boundary.
Paradigm 1: Handwritten Trajectories#
A handwritten expert lives beside its registered task entry point, for example:
embodichain_tasks/embodichain_tasks/manipulation/tableware/stack_blocks_two.py;embodichain_tasks/embodichain_tasks/manipulation/tableware/blocks_ranking_rgb.py.
Both examples build a MotionGenerator backed by TOPPRA and pass it to an
AtomicActionEngine. The engine then plans reusable PickUp and Place
invocations while the task retains control over scene queries, command timing,
segment metadata, and success checks.
Direct Motion Generator or Atomic Skills#
These are two abstraction levels, not two unrelated planning engines:
Use
MotionGenerator.generate()directly when the task already owns the joint or Cartesian waypoints and only needs a planned joint trajectory. WrapPlanResult.positionsas aDemoSegmentaction iterable and combinePlanResult.successwith a physical validator. See Motion Generator for the complete planning API.Use
AtomicActionEnginewhen the behavior matches reusable skills such as PickUp, Place, MoveEndEffector, Pour, or HandOver. The engine owns the shared motion generator, command profiles, typed goals, and projected semantic context. See Atomic actions for the action contracts.
When using MotionGenerator directly, its planned positions are ordered for
the selected control_part. The actions yielded to the environment must
match its active action space. If the environment controls more joints than
the planned arm, merge the arm plan with hold or gripper commands before
yielding it; do not silently rely on incompatible dimensions.
Build the planning services#
The two-block stacking task demonstrates the recommended Atomic Skills setup:
Motion Generator and AtomicActionEngine setup
1 def _initialize_atomic_actions(self) -> None:
2 """Create the right-arm atomic-action engine and object semantics."""
3 from embodichain.lab.sim.atomic_actions import (
4 Affordance,
5 AtomicActionEngine,
6 ControlPartCommandProfile,
7 ObjectSemantics,
8 )
9 from embodichain.lab.sim.motion.motion_generator import (
10 MotionGenCfg,
11 MotionGenerator,
12 )
13 from embodichain.lab.sim.motion.planners import ToppraPlannerCfg
14
15 hand_dof = len(self.robot.get_joint_ids(name=HAND_CONTROL_PART))
16 hand_open_qpos = torch.full(
17 (hand_dof,), HAND_OPEN_QPOS, dtype=torch.float32, device=self.device
18 )
19 hand_close_qpos = torch.full(
20 (hand_dof,), HAND_CLOSE_QPOS, dtype=torch.float32, device=self.device
21 )
22 motion_generator = MotionGenerator(
23 cfg=MotionGenCfg(planner_cfg=ToppraPlannerCfg(robot_uid=self.robot.uid))
24 )
25 self._action_engine: AtomicActionEngine = AtomicActionEngine(
26 motion_generator,
27 control_profiles={
28 HAND_CONTROL_PART: ControlPartCommandProfile.joint_positions(
29 open=hand_open_qpos,
30 grasp=hand_close_qpos,
31 )
32 },
33 )
34 self._stack_block_semantics: ObjectSemantics = ObjectSemantics(
35 affordance=Affordance(),
36 geometry={},
37 label=STACK_BLOCK_UID,
38 entity_id=STACK_BLOCK_UID,
39 )
40
The task constructs the motion generator once, declares command profiles for the gripper, and reuses the engine for every episode. Object semantics are explicit inputs to the skills rather than being inferred from simulator names.
Return semantic demonstration segments#
create_demo_segments() is the preferred handwritten expert API:
1 def create_demo_segments(self, **kwargs: Any) -> tuple[DemoSegment]:
2 """Plan the complete stacking task as exactly one semantic segment."""
3 del kwargs
4 plan_success, trajectory, source_pose, target_pose = self._plan_stack()
5 return (
6 DemoSegment(
7 actions=self._iter_segment_actions(trajectory),
8 name="stack_block_2_on_block_1",
9 target_uid=STACK_BLOCK_UID,
10 instruction="Pick up block 2 and place it on top of block 1.",
11 progress_total_steps=int(trajectory.shape[1]) + SETTLE_STEPS,
12 metadata={
13 "segment_index": 0,
14 "segment_count": 1,
15 "planning_success": plan_success.detach().cpu().tolist(),
16 "source_pose": source_pose.detach().cpu().tolist(),
17 "target_pose": target_pose.detach().cpu().tolist(),
18 "atomic_actions": ["pick_up", "place"],
19 },
20 validator=partial(self._validate_stack, plan_success.detach().clone()),
21 ),
22 )
23
This segment keeps three outcomes separate:
actionsis the controller-command stream consumed by the common runner;planning_successrecords whether PickUp and Place were planned; andvalidatorchecks both planning success and the physical stack after all commands and settling actions have executed.
The task creates typed PickUp and Place requests and threads the projected held-object state from the first plan into the second:
Compile the PickUp and Place trajectory
1 pick_compiled = self._action_engine.compile(
2 (
3 ActionInvocation(
4 skill_id="pick_up",
5 goal=GraspGoal(
6 self._stack_block_semantics,
7 grasp_xpos=grasp_pose,
8 ),
9 binding=pick_binding,
10 motion_policy=MotionPolicy(sample_count=PICK_SAMPLE_INTERVAL),
11 skill_options=PickUpOptions(
12 pre_grasp_distance=0.12,
13 lift_height=0.15,
14 hand_interp_steps=HAND_INTERP_STEPS,
15 ),
16 ),
17 ),
18 self._action_engine.initial_context(
19 scene=SceneSnapshot(
20 timestamp=0.0,
21 version=0,
22 entities={STACK_BLOCK_UID: EntityState(source_pose)},
23 ),
24 control_dt=self.step_dt,
25 ),
26 )
27 pick_success = pick_compiled.plan_success
28 pick_trajectory = pick_compiled.trajectory.positions
29 picked_context = pick_compiled.projected_context
30 pick_trajectory = self._insert_grasp_hold(pick_trajectory)
31
32 target_pose = source_pose.clone()
33 target_pose[:, :3, 3] = base_pose[:, :3, 3]
34 target_pose[:, 2, 3] += BLOCK_HEIGHT
35 held = picked_context.get_held_object(CONTROL_PART)
36 if held is None or not bool(pick_success.all().item()):
37 return (
38 torch.zeros_like(pick_success, dtype=torch.bool),
39 self._ensure_nonempty_trajectory(pick_trajectory),
40 source_pose,
41 target_pose,
42 )
43
44 place_eef_pose = torch.bmm(target_pose, held.object_to_eef)
45 place_compiled = self._action_engine.compile(
46 (
47 ActionInvocation(
48 skill_id="place",
49 goal=PlaceGoal(place_eef_pose),
50 binding=place_binding,
51 motion_policy=MotionPolicy(sample_count=PLACE_SAMPLE_INTERVAL),
52 skill_options=PlaceOptions(
53 lift_height=0.10,
54 hand_interp_steps=HAND_INTERP_STEPS,
55 ),
56 ),
57 ),
58 picked_context,
59 )
60 place_success = place_compiled.plan_success
61 place_trajectory = place_compiled.trajectory.positions
62 trajectory = self._ensure_nonempty_trajectory(
63 torch.cat((pick_trajectory, place_trajectory), dim=1)
64 )
65 return pick_success & place_success, trajectory, source_pose, target_pose
66
For a multi-object episode, yield segments lazily. The
BlocksRankingRGBEnv.create_demo_segments() example yields the red-block
segment first, then queries the updated reference-block pose before planning
the blue-block segment. This avoids planning later subtasks against stale
scene state.
Legacy tasks implementing create_demo_action_list() remain supported and
are wrapped as one segment named legacy. New tasks should implement
create_demo_segments() so subtask boundaries, language, metadata, and
validators remain explicit.
Try the handwritten expert#
The shipped stacking config currently defines the scene and rollout but does not configure a dataset recorder. First smoke-test the expert without writing data:
embodichain run-env \
--gym_config embodichain_tasks/configs/tasks/manipulation/tableware/stack_blocks_two/env.json \
--headless \
--filter_dataset_saving \
--max_episodes 1
To collect it, copy that config, add the recorder from
Configure Dataset Recording, and run the copied config without
--filter_dataset_saving:
embodichain run-env \
--gym_config path/to/stack_blocks_two_recording.json \
--headless \
--max_episodes 5
Paradigm 2: Task Program#
Task Program replaces the task-specific expert method with declarative task intent. It does not introduce a second rollout or recording API: the configured adapter compiles the program, creates lazy demonstration segments, and returns them to the same executor used by handwritten tasks.
The current Pour Water example is a complete configuration-defined task:
env.yamlowns the physical scene, environment values, and dataset recorder, without any Task Program fields;task.cobotmagic.yamlis the runnable deployment that selects the environment, Task Program, embodiment, and execution policy;program.yamlowns embodiment-independent intent and targets;integration.yamlowns task-specific contracts, scene binding, semantic defaults, action options, and allowlisted runtime services;the embodiment component owns the robot, sensors, endpoints, and compatible skill profile.
Compose the deployment#
The runnable deployment is intentionally thin:
1id: PourWater-v1
2
3environment:
4 component: env.yaml
5
6task_program:
7 program: task_program/program.yaml
8 integration: task_program/integration.yaml
9 execution_policy: ../../../../components/execution_policies/trajectory_open_loop_dense.yaml
10
11embodiment:
12 component: ../../../../components/embodiments/cobotmagic.yaml
Its selected env.yaml is reusable outside Task Program because it contains
only physical simulation and ordinary Gym values:
Pour Water reusable environment
1environment_id: pour_water
2max_episodes: 5
3max_episode_steps: 600
4num_envs: 1
5arena_space: 3.0
6
7simulation:
8 light:
9 direct:
10 - uid: light_1
11 light_type: point
12 color: [1.0, 1.0, 1.0]
13 intensity: 50.0
14 init_pos: [2, 0, 2]
15 radius: 10.0
16 background:
17 - uid: table
18 shape:
19 shape_type: Mesh
20 fpath: CircleTableSimple/circle_table_simple.ply
21 compute_uv: true
22 attrs:
23 mass: 10.0
24 static_friction: 0.95
25 dynamic_friction: 0.9
26 restitution: 0.01
27 body_scale: [1, 1, 1]
28 body_type: kinematic
29 init_pos: [0.725, 0.0, 0.825]
30 init_rot: [0, 90, 0]
31 rigid_object:
32 - uid: cup
33 shape:
34 shape_type: Mesh
35 fpath: PaperCup/paper_cup.ply
36 compute_uv: true
37 attrs:
38 mass: 0.01
39 contact_offset: 0.003
40 rest_offset: 0.001
41 restitution: 0.01
42 max_depenetration_velocity: 10.0
43 min_position_iters: 32
44 min_velocity_iters: 8
45 init_pos: [0.75, 0.1, 0.9]
46 body_scale: [0.75, 0.75, 1.0]
47 max_convex_hull_num: 8
48 - uid: bottle
49 shape:
50 shape_type: Mesh
51 fpath: ScannedBottle/kashijia_processed.ply
52 compute_uv: true
53 attrs:
54 mass: 0.01
55 contact_offset: 0.003
56 rest_offset: 0.001
57 restitution: 0.01
58 max_depenetration_velocity: 10.0
59 min_position_iters: 32
60 min_velocity_iters: 8
61 init_pos: [0.75, -0.1, 0.932]
62 body_scale: [1, 1, 1]
63 max_convex_hull_num: 8
64 rigid_object_group: []
65 articulation: []
66
67env:
68 events:
69 random_light:
70 func: randomize_light
71 mode: interval
72 interval_step: 10
73 params:
74 entity_cfg:
75 uid: light_1
76 position_range:
77 - [-0.5, -0.5, 2]
78 - [0.5, 0.5, 2]
79 color_range:
80 - [0.6, 0.6, 0.6]
81 - [1, 1, 1]
82 intensity_range: [50.0, 100.0]
83 random_table_material:
84 func: randomize_visual_material
85 mode: reset
86 params:
87 entity_cfg:
88 uid: table
89 texture_path: BackgroundTexture/100
90 p_original: 0.0
91 p_library: 1.0
92 p_solid: 0.0
93 random_bottle_material:
94 func: randomize_visual_material
95 mode: reset
96 params:
97 entity_cfg:
98 uid: bottle
99 p_original: 0.0
100 p_library: 0.0
101 p_solid: 1.0
102 settle_pour_objects_on_reset:
103 func: wait_for_dynamic_objects_to_settle
104 mode: reset
105 params:
106 entity_cfgs:
107 - uid: bottle
108 - uid: cup
109 min_steps: 10
110 max_steps: 120
111 check_interval_steps: 2
112 required_stable_checks: 3
113 timeout_behavior: raise
114 dataset:
115 lerobot:
116 func: LeRobotRecorder
117 mode: save
118 params:
119 robot_meta:
120 robot_type: CobotMagic
121 instruction:
122 lang: Pour water from bottle to cup
123 extra:
124 scene_type: Commercial
125 task_description: Pour water
126 data_type: sim
127 use_videos: true
128 control_parts: [left_arm, left_eef, right_arm, right_eef]
During config_to_cfg(), component paths are resolved relative to
task.cobotmagic.yaml. The resolver expands the physical environment and
embodiment, checks scene-binding UIDs and semantic contracts, and binds trusted
provider identities into the otherwise embodiment-independent program.
Declare the semantic workflow#
The program describes Pick, transport, Pour, and Place without importing Python callables or naming simulator joints:
Pour Water Task Program
1program_id: pour_water_with_right_arm
2targets:
3 bottle_return_pose:
4 kind: cyclic_pose
5 values:
6 - position: [0.75, -0.1, 0.962]
7 quaternion_wxyz: [1.0, 0.0, 0.0, 0.0]
8program:
9 kind: segment
10 name: pour_and_return_bottle
11 steps:
12 kind: sequence
13 items:
14 - kind: invoke
15 call:
16 kind: pick
17 object: bottle
18 grasp: bottle_grasp
19 - kind: invoke
20 call:
21 kind: registered
22 call_id: simulation.move_held_object
23 arguments:
24 target: cup_pour_pose
25 - kind: invoke
26 call:
27 kind: registered
28 call_id: simulation.pour
29 arguments:
30 object: bottle
31 - kind: invoke
32 call:
33 kind: place
34 object: bottle
35 at:
36 kind: target_ref
37 target: bottle_return_pose
38 post:
39 - kind: wait_stable
40 entity: bottle
41 - kind: wait_stable
42 entity: cup
43 validators:
44 - kind: object_near_target
45 object: bottle
46 target: bottle_return_pose
47 position_tolerance: 0.05
The trusted integration maps bottle and cup to physical scene objects,
selects the primary manipulator, configures action options, and allowlists the
registered transport and pour lowerers:
Pour Water integration
1integration_id: pour_water_v1
2program_id: pour_water_with_right_arm
3
4requires:
5 scene_contract: pour_water_scene_v1
6 embodiment_contract: single_arm_parallel_gripper
7
8scene_binding:
9 contract_id: pour_water_scene_v1
10 registry_id: task_program_pour_water
11 rigid_objects:
12 - entity_id: cup
13 simulation_uid: cup
14 dynamics: dynamic
15 semantic_type: cup
16 - entity_id: bottle
17 simulation_uid: bottle
18 dynamics: dynamic
19 semantic_type: bottle
20 affordances:
21 - entity_id: bottle_grasp
22 kind: antipodal_grasp
23 internal_axis: [1.0, 0.0, 0.0]
24
25profile:
26 defaults:
27 pick_up:
28 primary: primary_manipulator
29 move_held_object:
30 primary: primary_manipulator
31 pour:
32 primary: primary_manipulator
33 place:
34 primary: primary_manipulator
35 action_options:
36 pick:
37 kind: pick_up
38 hand_interp_steps: 11
39 grasp_settle_steps: 25
40 fixed_object_to_eef:
41 - -0.0530918874
42 - 0.4963395894
43 - 0.8665033579
44 - 0.035870254
45 - -0.0525476672
46 - -0.8679134846
47 - 0.493927747
48 - 0.0204655528
49 - 0.9972059727
50 - -0.0193091929
51 - 0.0721606836
52 - 0.0321167707
53 - 0.0
54 - 0.0
55 - 0.0
56 - 1.0
57 simulation.move_held_object:
58 kind: move_held_object
59 simulation.pour:
60 kind: pour
61 rotate_angle: -1.0471975511965976
62 place:
63 kind: place
64 hand_interp_steps: 11
65 release_settle_steps: 15
66 preserve_current_object_orientation: true
67 effect_monitors: {}
68
69runtime_services:
70 registered_semantic_lowerers:
71 - kind: move_held_object
72 target_id: cup_pour_pose
73 reference_entity_id: cup
74 relative_pose:
75 - 1.0
76 - 0.0
77 - 0.0
78 - 0.05
79 - 0.0
80 - 1.0
81 - 0.0
82 - -0.1
83 - 0.0
84 - 0.0
85 - 1.0
86 - 0.125
87 - 0.0
88 - 0.0
89 - 0.0
90 - 1.0
91 - kind: pour
92 object_id: bottle
The program remains provider-independent. Simulation UIDs, grasp affordances, fixed object-to-end-effector transforms, and live target resolution stay in the trusted integration. For a detailed deployment walkthrough, see Configure and Run an Embodied Task Program; for constructing and compiling the language directly in Python, see Authoring a Task Program in Python.
Run the configured Task Program#
Pour Water already configures LeRobotRecorder in env.yaml, so its
runnable deployment can record directly:
embodichain run-env \
--gym_config embodichain_tasks/configs/tasks/manipulation/tableware/pour_water/task.cobotmagic.yaml \
--headless \
--max_episodes 1
The Task Program bridge plans and yields actions lazily. It never calls
env.step(); stepping, annotations, validation, commit, retry, and discard
remain owned by the shared environment rollout.
Configure Dataset Recording#
The expert paradigm and recorder configuration are independent. Add a dataset
functor under env.dataset in an inline Gym config or in a reusable
env.yaml environment component:
max_episodes: 5
max_episode_steps: 600
env:
dataset:
lerobot:
func: LeRobotRecorder
mode: save
save_failed_episodes: false
params:
save_path: outputs/lerobot/expert_demos
robot_meta:
robot_type: CobotMagic
instruction:
lang: Pick and place the object
extra:
scene_type: tabletop
task_description: pick_and_place
data_type: sim
use_videos: true
Important fields are:
max_episodesis the exact number of persisted per-environment episodes, not the number of vector batches.max_episode_stepsmust exceed the longest valid expert execution, including gripper holds and settling actions.save_failed_episodesbelongs besidefuncandmode. It defaults tofalse; when enabled, a failed or truncated attempt with recorded frames is committed withsuccess=falsemetadata and counts towardmax_episodes.params.save_pathis the parent directory for auto-numbered datasets. If omitted, the default is~/.cache/embodichain_datasetsor the value ofEMBODICHAIN_DATASET_ROOT.params.use_videoscontrols RGB dataset videos. It has an effect only when image observations from configured sensors are present.Dataset frequency is derived from
env.step_dtand must be an integer number of frames per second for LeRobot.
The real Pour Water recorder block can be inspected directly:
1 dataset:
2 lerobot:
3 func: LeRobotRecorder
4 mode: save
5 params:
6 robot_meta:
7 robot_type: CobotMagic
8 instruction:
9 lang: Pour water from bottle to cup
10 extra:
11 scene_type: Commercial
12 task_description: Pour water
13 data_type: sim
14 use_videos: true
15 control_parts: [left_arm, left_eef, right_arm, right_eef]
Execution, Validation, and Persistence#
Without --preview or --replay, embodichain run-env performs offline
data generation:
Resolve
create_demo_segments()from the handwritten task or configured Task Program bridge.Execute every yielded action through
env.step(action)and record the resulting transition.Run the segment validator after its action iterable is exhausted.
Check episode termination and final task success.
Commit selected environment rows with an explicit reset, or discard the attempt with
reset(options={"save_data": False}).
Failed attempts are discarded and retried by default, up to
demo_max_attempts (default: 3). Empty plans and exceptions are always
discarded because they do not form a complete dataset transaction. With
save_failed_episodes: true, a failed or truncated attempt is retained only
when every selected row contains recorded frames.
num_envs controls collection parallelism. If max_episodes=10 and
num_envs=4, the runner uses three vector batches and commits only two rows
from the final batch, so it never overshoots the requested episode count.
An episode is the complete task; a segment is one semantic subtask. Do not use
generate_function(num_traj=...) to repeat subtasks: direct callers may pass
only None or 1. Yield multiple DemoSegment objects instead so the
task owns their order, live-state dependencies, and validation.
Useful modes and options are:
--headlessdisables the GUI for collection throughput.--previewopens interactive inspection and does not save a dataset.--filter_dataset_savingexecutes the expert while suppressing structured dataset writes.--num_envsoverrides collection parallelism.--max_episodesoverrides the configured episode target.
See Running Environments with run-env for preview, dataset recording, debug video, trajectory recording, and replay modes, and CLI Reference for the complete argument list.
Recorded Data#
LeRobotRecorder creates an auto-numbered dataset directory containing:
data/for Parquet action, state, and annotation features;videos/for RGB observations whenuse_videosis enabled;meta/for LeRobot metadata, task/subtask mappings, and EmbodiChain episode metadata.
The primary fields include observation.state, action, and
observation.images.{sensor_name}. Segment-aware episodes additionally
record subtask_index and annotation.segment_* boundaries, plus terminal
and truncation annotations. Depth and segmentation observations have their own
numeric or configured sidecar representation; see
Dataset Functors for the complete schema.
Inspect Recorded LeRobot Data#
Use EmbodiChain’s structural preview on the parent directory of auto-numbered datasets:
embodichain preview_lerobot_data \
outputs/lerobot/expert_demos \
--latest \
--episode 0
For Pour Water without an explicit save_path, use the default parent:
embodichain preview_lerobot_data \
~/.cache/embodichain_datasets \
--latest \
--episode 0 \
--expect-segments 1
The command validates frame and timestamp continuity, one episode-level task,
subtask mappings, contiguous segment ranges, terminal annotations, and the
EmbodiChain metadata sidecar. --expect-segments is an optional assertion;
omit it when the expert’s segment count is data-dependent.
Use LeRobot’s lerobot-dataset-viz with the exact auto-numbered dataset
directory when interactive Rerun plots and camera playback are needed. The
EmbodiChain preview focuses on structure and annotations; it does not render
images or time-series plots.
Best Practices#
Keep planning and stepping separate: experts yield actions; the runner calls
env.step().Validate physical outcomes from live simulator state, not only planner success or projected semantic state.
Yield later subtasks lazily when they depend on object poses changed by earlier segments.
Smoke-test with
--filter_dataset_savingbefore a long collection run, then inspect one committed episode before scalingnum_envs.Keep each task’s
env.yaml,task.<embodiment>.yaml, andtask_program/{program,integration}.yamltogether so physical UIDs, contracts, and canonical IDs stay aligned. Keep reusable embodiment and execution-policy components underconfigs/components/.Move reusable motion behavior into Atomic Skills instead of copying task-local trajectory logic across Python tasks or registered Semantic Calls.