Expert Data Generation#

EmbodiChain supports two expert-authoring paradigms for synthetic demonstration data:

  1. Handwritten trajectories implement the expert in a registered Python task, using MotionGenerator directly or composing reusable Atomic Skills.

  2. 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.

Choose an Expert-Authoring Paradigm#

Handwritten trajectories and Task Program#

Concern

Handwritten trajectory

Task Program

Authoring surface

A registered Python task module implementing create_demo_segments().

program.yaml plus a trusted integration, reusable physical environment, embodiment, execution policy, and runnable task deployment.

Planning level

Call MotionGenerator for task-specific waypoints, or construct typed ActionInvocation values for AtomicActionEngine.

Declare Pick, Place, HandOver, registered Semantic Calls, control flow, post-policies, and validators; the adapter lowers them to Atomic Skills.

Best fit

Custom geometry, unusual control logic, experimental skills, or logic that needs unrestricted Python.

Reusable object-centric workflows whose task intent should remain independent of a particular robot and simulator assembly.

Responsibility

The task owns planning, command assembly, lazy segment boundaries, and physical validation.

The program owns intent; trusted integration and deployment components own physical identities, resources, policies, and services.

Environment entry

A task-specific registered EmbodiedEnv subclass.

A supported configuration-defined task can use the common EmbodiedEnv without a task-specific Python module.

Use handwritten Python while the behavior itself is still changing rapidly or does not have a reusable semantic contract. Use Task Program when the behavior can be expressed as stable semantic operations and should be portable across compatible embodiments. A common progression is to prototype in Python, move reusable motion behavior into Atomic Skills, and then expose stable task flows through Task Program.

The Shared Rollout Contract#

The rollout path is deliberately shared:

registered Python task                 configured Task Program
create_demo_segments()                 compile + Gym bridge
             \                           /
              +---- Iterable[DemoSegment]
                             |
                  execute_demo_episode()
                             |
                      env.step(action)
                             |
               observation/action buffers
                             |
            reset(commit) or reset(discard)
                             |
                    LeRobot dataset

A DemoSegment contains an action iterable and may also contain a stable name, target, instruction, JSON-compatible metadata, and a validator. Its actions may be lazy: the next segment can be planned only after the previous segment has executed and changed the scene.

The expert planner must not call env.step() itself. The common executor owns stepping so observations, actions, terminal signals, segment annotations, and dataset frames stay causally aligned.

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. Wrap PlanResult.positions as a DemoSegment action iterable and combine PlanResult.success with a physical validator. See Motion Generator for the complete planning API.

  • Use AtomicActionEngine when 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:

  • actions is the controller-command stream consumed by the common runner;

  • planning_success records whether PickUp and Place were planned; and

  • validator checks 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.yaml owns the physical scene, environment values, and dataset recorder, without any Task Program fields;

  • task.cobotmagic.yaml is the runnable deployment that selects the environment, Task Program, embodiment, and execution policy;

  • program.yaml owns embodiment-independent intent and targets;

  • integration.yaml owns 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_episodes is the exact number of persisted per-environment episodes, not the number of vector batches.

  • max_episode_steps must exceed the longest valid expert execution, including gripper holds and settling actions.

  • save_failed_episodes belongs beside func and mode. It defaults to false; when enabled, a failed or truncated attempt with recorded frames is committed with success=false metadata and counts toward max_episodes.

  • params.save_path is the parent directory for auto-numbered datasets. If omitted, the default is ~/.cache/embodichain_datasets or the value of EMBODICHAIN_DATASET_ROOT.

  • params.use_videos controls RGB dataset videos. It has an effect only when image observations from configured sensors are present.

  • Dataset frequency is derived from env.step_dt and 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:

  1. Resolve create_demo_segments() from the handwritten task or configured Task Program bridge.

  2. Execute every yielded action through env.step(action) and record the resulting transition.

  3. Run the segment validator after its action iterable is exhausted.

  4. Check episode termination and final task success.

  5. 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:

  • --headless disables the GUI for collection throughput.

  • --preview opens interactive inspection and does not save a dataset.

  • --filter_dataset_saving executes the expert while suppressing structured dataset writes.

  • --num_envs overrides collection parallelism.

  • --max_episodes overrides 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 when use_videos is 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_saving before a long collection run, then inspect one committed episode before scaling num_envs.

  • Keep each task’s env.yaml, task.<embodiment>.yaml, and task_program/{program,integration}.yaml together so physical UIDs, contracts, and canonical IDs stay aligned. Keep reusable embodiment and execution-policy components under configs/components/.

  • Move reusable motion behavior into Atomic Skills instead of copying task-local trajectory logic across Python tasks or registered Semantic Calls.