Creating a Modular Environment#

This tutorial demonstrates how to create sophisticated robotic environments using EmbodiChain’s modular architecture. You’ll learn how to use the advanced envs.EmbodiedEnv class with configuration-driven setup, event managers, observation managers, and randomization systems.

The Code#

The tutorial corresponds to the modular_env.py script in the scripts/tutorials/gym directory.

Code for modular_env.py
  1# ----------------------------------------------------------------------------
  2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
  3#
  4# Licensed under the Apache License, Version 2.0 (the "License");
  5# you may not use this file except in compliance with the License.
  6# You may obtain a copy of the License at
  7#
  8#     http://www.apache.org/licenses/LICENSE-2.0
  9#
 10# Unless required by applicable law or agreed to in writing, software
 11# distributed under the License is distributed on an "AS IS" BASIS,
 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13# See the License for the specific language governing permissions and
 14# limitations under the License.
 15# ----------------------------------------------------------------------------
 16
 17from __future__ import annotations
 18
 19import argparse
 20import random
 21from copy import deepcopy
 22
 23from embodichain.cli.sim import (
 24    add_sim_args_to_parser,
 25    add_seed_arg_to_parser,
 26    resolve_seed,
 27)
 28
 29
 30def build_parser() -> argparse.ArgumentParser:
 31    """Build CLI options without initializing simulation resources."""
 32    parser = argparse.ArgumentParser()
 33    add_sim_args_to_parser(parser)
 34    add_seed_arg_to_parser(parser, scope="task environment")
 35    return parser
 36
 37
 38if __name__ == "__main__":
 39    # Parse before importing optional simulation/planning dependencies.
 40    _cli_args = build_parser().parse_args()
 41
 42
 43import torch
 44
 45from typing import List, Dict, Any
 46
 47import embodichain.lab.gym.envs.managers.randomization as rand
 48import embodichain.lab.gym.envs.managers.events as events
 49import embodichain.lab.gym.envs.managers.observations as obs
 50
 51from embodichain.lab.gym.envs.managers import (
 52    EventCfg,
 53    SceneEntityCfg,
 54    ObservationCfg,
 55)
 56from embodichain.lab.gym.envs import EmbodiedEnv, EmbodiedEnvCfg
 57from embodichain.lab.gym.utils.registration import register_env
 58from embodichain_robots import DexforceW1Cfg
 59from embodichain.lab.sim.sensors import StereoCameraCfg, SensorCfg
 60from embodichain.lab.sim.shapes import MeshCfg
 61from embodichain.lab.sim.cfg import (
 62    RenderCfg,
 63    physics_cfg_for_backend,
 64    LightCfg,
 65    ArticulationCfg,
 66    RobotCfg,
 67    RigidObjectCfg,
 68    RigidBodyPhysicsCfg,
 69)
 70from embodichain.data import get_data_path
 71from embodichain.utils import configclass
 72from embodichain.utils.file import get_all_files_in_directory
 73from embodichain.lab.gym.envs.managers.event_manager import _derive_functor_seed
 74
 75
 76class _ReplacePreselectedFork(events.replace_assets_from_group):
 77    """Consume the first reset draw without replacing an already selected fork."""
 78
 79    def __call__(
 80        self,
 81        env: EmbodiedEnv,
 82        env_ids: torch.Tensor | None,
 83        entity_cfg: SceneEntityCfg,
 84        folder_path: str,
 85    ) -> None:
 86        initial_path = env._initial_fork_path
 87        env._initial_fork_path = None
 88        if initial_path is not None:
 89            # Preview the actual event stream: reset(seed=...) may have changed
 90            # it since construction. Leave fallback sampling to the base term.
 91            preview = random.Random()
 92            preview.setstate(random.getstate())
 93            if (
 94                preview.choice(self._asset_group_path) == initial_path
 95                and self.asset_cfg.shape.fpath == initial_path
 96            ):
 97                random.choice(self._asset_group_path)
 98                # As with a real replacement, keep the template separate from
 99                # the live object modified by later mass randomization terms.
100                self.asset_cfg = deepcopy(self.asset_cfg)
101                return
102        super().__call__(env, env_ids, entity_cfg, folder_path)
103
104
105@configclass
106class ExampleEventCfg:
107
108    replace_obj: EventCfg = EventCfg(
109        func=_ReplacePreselectedFork,
110        mode="reset",
111        params={
112            "entity_cfg": SceneEntityCfg(
113                uid="fork",
114            ),
115            "folder_path": get_data_path("TableWare/tableware/fork/"),
116        },
117    )
118
119    randomize_fork_mass: EventCfg = EventCfg(
120        func=rand.randomize_rigid_object_mass,
121        mode="reset",
122        params={
123            "entity_cfg": SceneEntityCfg(
124                uid="fork",
125            ),
126            "mass_range": (0.1, 2.0),
127        },
128    )
129
130    randomize_table_mat: EventCfg = EventCfg(
131        func=rand.randomize_visual_material,
132        mode="interval",
133        interval_step=25,
134        params={
135            "entity_cfg": SceneEntityCfg(
136                uid="table",
137            ),
138            "random_texture_prob": 0.5,
139            "texture_path": get_data_path("CocoBackground/coco"),
140            "base_color_range": [[0.2, 0.2, 0.2], [1.0, 1.0, 1.0]],
141        },
142    )
143
144
145@configclass
146class ObsCfg:
147
148    obj_pose: ObservationCfg = ObservationCfg(
149        func=obs.get_rigid_object_pose,
150        mode="add",
151        name="fork_pose",
152        params={"entity_cfg": SceneEntityCfg(uid="fork")},
153    )
154
155
156@configclass
157class ExampleCfg(EmbodiedEnvCfg):
158
159    # Define the robot configuration using DexforceW1Cfg
160    robot: RobotCfg = DexforceW1Cfg.from_dict(
161        {
162            "uid": "dexforce_w1",
163            "version": "v021",
164            "init_pos": [0.0, 0, 0.0],
165        }
166    )
167
168    # Define the sensor configuration using StereoCameraCfg
169    sensor: List[SensorCfg] = [
170        StereoCameraCfg(
171            uid="eye_in_head",
172            width=960,
173            height=540,
174            enable_mask=True,
175            enable_depth=True,
176            left_to_right_pos=(0.06, 0, 0),
177            intrinsics=(450, 450, 480, 270),
178            intrinsics_right=(450, 450, 480, 270),
179            extrinsics=StereoCameraCfg.ExtrinsicsCfg(
180                parent="eyes",
181            ),
182        )
183    ]
184
185    background: List[RigidObjectCfg] = [
186        RigidObjectCfg(
187            uid="table",
188            shape=MeshCfg(
189                fpath=get_data_path("CircleTableSimple/circle_table_simple.ply"),
190                compute_uv=True,
191            ),
192            attrs=RigidBodyPhysicsCfg.from_dict(
193                {
194                    "mass_props": {"mass": 10.0},
195                    "material_props": {
196                        "static_friction": 0.95,
197                        "dynamic_friction": 0.85,
198                        "restitution": 0.01,
199                    },
200                }
201            ),
202            body_type="kinematic",
203            init_pos=(0.80, 0, 0.8),
204            init_rot=(0, 90, 0),
205        ),
206    ]
207
208    rigid_object: List[RigidObjectCfg] = [
209        RigidObjectCfg(
210            uid="fork",
211            shape=MeshCfg(
212                fpath=get_data_path("TableWare/tableware/fork/standard_fork_scale.ply"),
213            ),
214            body_scale=(0.75, 0.75, 1.0),
215            init_pos=(0.8, 0, 1.0),
216        ),
217    ]
218
219    articulation_cfg: List[ArticulationCfg] = [
220        ArticulationCfg(
221            uid="drawer",
222            fpath="SlidingBoxDrawer/SlidingBoxDrawer.urdf",
223            init_pos=(0.5, 0.0, 0.85),
224        )
225    ]
226
227    events = ExampleEventCfg()
228
229    observations = ObsCfg()
230
231
232def _preselect_initial_fork(
233    cfg: EmbodiedEnvCfg,
234) -> tuple[EmbodiedEnvCfg, str | None]:
235    """Prepare this tutorial's first reset asset before scene materialization.
236
237    Use the same named event stream and sorted asset list as EventManager.
238    Keep unseeded runs and customized replacement terms on their normal path.
239    """
240    term = getattr(cfg.events, "replace_obj", None)
241    if (
242        cfg.seed is None
243        or cfg.seed < 0
244        or term is None
245        or term.func is not _ReplacePreselectedFork
246        or term.mode != "reset"
247    ):
248        return cfg, None
249    folder_path = term.params["folder_path"]
250    if not folder_path.endswith("/"):
251        return cfg, None
252    paths = sorted(get_all_files_in_directory(get_data_path(folder_path)))
253    if not paths:
254        raise ValueError(f"No fork assets found in {folder_path!r}.")
255    stream_seed = _derive_functor_seed(cfg.seed, "call", "reset", "replace_obj", 0)
256    path = random.Random(stream_seed).choice(paths)
257    prepared = deepcopy(cfg)
258    uid = term.params["entity_cfg"].uid
259    asset_cfg = next(asset for asset in prepared.rigid_object if asset.uid == uid)
260    asset_cfg.shape.fpath = path
261    return prepared, path
262
263
264@register_env("ModularEnv-v1", max_episode_steps=100, override=True)
265class ModularEnv(EmbodiedEnv):
266    """
267    An example of a modular environment that inherits from EmbodiedEnv
268    and uses custom event and observation managers.
269    """
270
271    def __init__(self, cfg: EmbodiedEnvCfg, **kwargs):
272        cfg, self._initial_fork_path = _preselect_initial_fork(cfg)
273        super().__init__(cfg, **kwargs)
274
275
276if __name__ == "__main__":
277    import gymnasium as gym
278    import argparse
279
280    from embodichain.lab.sim import SimulationManagerCfg
281    from embodichain.cli.sim import (
282        add_sim_args_to_parser,
283        add_seed_arg_to_parser,
284        resolve_seed,
285    )
286    from embodichain.lab.visualization import visualization_cfg_from_args
287
288    parser = build_parser()
289    args = _cli_args
290    seed = resolve_seed(args.seed)
291    print(f"[INFO]: Environment seed: {seed}", flush=True)
292
293    env_cfg = ExampleCfg(
294        seed=seed,
295        sim_cfg=SimulationManagerCfg(
296            render_cfg=RenderCfg(renderer=args.renderer),
297            headless=args.headless,
298            device=args.device,
299            num_envs=args.num_envs,
300            physics_cfg=physics_cfg_for_backend(args.physics),
301            visualization=visualization_cfg_from_args(args),
302        ),
303        num_envs=args.num_envs,
304    )
305
306    # Create the Gym environment
307    env = gym.make("ModularEnv-v1", cfg=env_cfg)
308
309    for i in range(5):
310        obs, info = env.reset()
311
312        for i in range(100):
313            action = torch.zeros(env.action_space.shape, dtype=torch.float32)
314            obs, reward, done, truncated, info = env.step(action)

The Code Explained#

This tutorial showcases EmbodiChain’s most powerful environment creation approach using the envs.EmbodiedEnv class. Unlike the basic environment tutorial, this approach uses declarative configuration classes and manager systems for maximum flexibility and reusability.

Event Configuration#

Events define automated behaviors that occur during simulation. There are three types of supported modes:

  • startup: triggers once when the environment is initialized

  • reset: triggers every time the environment is reset

  • interval: triggers at fixed step intervals during simulation

The ExampleEventCfg demonstrates three types of events:



if __name__ == "__main__":
    # Parse before importing optional simulation/planning dependencies.
    _cli_args = build_parser().parse_args()


import torch

from typing import List, Dict, Any

import embodichain.lab.gym.envs.managers.randomization as rand
import embodichain.lab.gym.envs.managers.events as events
import embodichain.lab.gym.envs.managers.observations as obs

from embodichain.lab.gym.envs.managers import (
    EventCfg,
    SceneEntityCfg,
    ObservationCfg,
)
from embodichain.lab.gym.envs import EmbodiedEnv, EmbodiedEnvCfg
from embodichain.lab.gym.utils.registration import register_env
from embodichain_robots import DexforceW1Cfg
from embodichain.lab.sim.sensors import StereoCameraCfg, SensorCfg
from embodichain.lab.sim.shapes import MeshCfg
from embodichain.lab.sim.cfg import (
    RenderCfg,
    physics_cfg_for_backend,
    LightCfg,
    ArticulationCfg,
    RobotCfg,
    RigidObjectCfg,
    RigidBodyPhysicsCfg,
)
from embodichain.data import get_data_path
from embodichain.utils import configclass
from embodichain.utils.file import get_all_files_in_directory
from embodichain.lab.gym.envs.managers.event_manager import _derive_functor_seed


class _ReplacePreselectedFork(events.replace_assets_from_group):

Asset Replacement Event

The replace_obj event demonstrates dynamic asset swapping:

Light Randomization Event

The randomize_light event creates dynamic lighting conditions:

  • Function: envs.managers.randomization.rendering.randomize_light()

  • Mode: "interval" - triggers every 5 steps

  • Parameters: Randomizes position, color, and intensity within specified ranges

Material Randomization Event

The randomize_table_mat event varies visual appearance:

  • Function: envs.managers.randomization.rendering.randomize_visual_material()

  • Mode: "interval" - triggers every 10 steps

  • Features: Random textures from COCO dataset and base color variations

For more randomization events, please refer to Event Functors.

Observation Configuration#

The default observation from envs.EmbodiedEnv includes: - robot: robot proprioceptive data (joint positions, velocities, efforts) - sensor: all available sensor data (images, depth, segmentation, etc.)

However, users always need to define some custom observation for specified learning tasks. To handle this, the observation manager system allows users to declaratively specify additional observations.

    def __call__(
        self,
        env: EmbodiedEnv,
        env_ids: torch.Tensor | None,
        entity_cfg: SceneEntityCfg,
        folder_path: str,
    ) -> None:
        initial_path = env._initial_fork_path
        env._initial_fork_path = None

This configuration:

For details documentation, see envs.managers.cfg.ObservationCfg.

Environment Configuration#

The main environment configuration inherits from envs.EmbodiedEnvCfg and defines all scene components:

Robot Configuration

    robot: RobotCfg = DexforceW1Cfg.from_dict(
        {
            "uid": "dexforce_w1",
            "version": "v021",
            "init_pos": [0.0, 0, 0.0],
        }
    )

Uses the pre-configured DexforceW1Cfg with customizations:

  • Version: Specific robot variant (v021)

  • Position: Initial placement in the scene

Sensor Configuration


@configclass
class ExampleEventCfg:

    replace_obj: EventCfg = EventCfg(
        func=_ReplacePreselectedFork,
        mode="reset",
        params={
            "entity_cfg": SceneEntityCfg(
                uid="fork",
            ),
            "folder_path": get_data_path("TableWare/tableware/fork/"),
        },
    )

Configures a stereo camera system using StereoCameraCfg:

  • Resolution: 960x540 pixels for realistic visual input

  • Features: Depth sensing and segmentation masks enabled

  • Stereo Setup: 6cm baseline between left and right cameras

  • Mounting: Attached to robot’s “eyes” frame

Lighting Configuration

        func=rand.randomize_rigid_object_mass,
        mode="reset",
        params={
            "entity_cfg": SceneEntityCfg(
                uid="fork",
            ),
            "mass_range": (0.1, 2.0),
        },
    )

    randomize_table_mat: EventCfg = EventCfg(

Defines scene illumination with configurable lights:

  • Types: Supports "point", "sun", "direction", "spot", "rect", and "mesh".

  • Global lights: "sun" and "direction" are global scene lights (single instance, infinite distance).

  • Properties: Configurable color, intensity, position, and direction.

  • UID: Named reference for event system manipulation.

Rigid Objects

        mode="interval",
        interval_step=25,
        params={
            "entity_cfg": SceneEntityCfg(
                uid="table",
            ),
            "random_texture_prob": 0.5,
            "texture_path": get_data_path("CocoBackground/coco"),
            "base_color_range": [[0.2, 0.2, 0.2], [1.0, 1.0, 1.0]],
        },
    )


@configclass
class ObsCfg:

    obj_pose: ObservationCfg = ObservationCfg(
        func=obs.get_rigid_object_pose,
        mode="add",
        name="fork_pose",
        params={"entity_cfg": SceneEntityCfg(uid="fork")},
    )


@configclass
class ExampleCfg(EmbodiedEnvCfg):

Multiple objects demonstrate different physics properties:

Table Configuration:

  • Shape: Custom PLY mesh with UV mapping

  • Physics: Kinematic body (movable but not affected by forces)

  • Material: Friction and restitution properties for realistic contact

Fork Configuration:

  • Shape: Detailed mesh from asset library

  • Scale: Proportionally scaled for scene consistency

  • Physics: Dynamic body affected by gravity and collisions

Articulated Objects

    # Define the robot configuration using DexforceW1Cfg
    robot: RobotCfg = DexforceW1Cfg.from_dict(
        {
            "uid": "dexforce_w1",
            "version": "v021",
            "init_pos": [0.0, 0, 0.0],
        }
    )

    # Define the sensor configuration using StereoCameraCfg
    sensor: List[SensorCfg] = [

Demonstrates complex mechanisms with moving parts:

  • URDF: Sliding drawer with joints and constraints

  • Positioning: Placed on table surface for interaction

Environment Implementation#

The actual environment class is remarkably simple due to the configuration-driven approach:

@register_env("ModularEnv-v1", max_episode_steps=100, override=True)
class ModularEnv(EmbodiedEnv):
    """
    An example of a modular environment that inherits from EmbodiedEnv
    and uses custom event and observation managers.
    """

    def __init__(self, cfg: EmbodiedEnvCfg, **kwargs):
        cfg, self._initial_fork_path = _preselect_initial_fork(cfg)
        super().__init__(cfg, **kwargs)

The envs.EmbodiedEnv base class automatically:

  • Loads all configured scene components

  • Sets up observation and action spaces

  • Initializes event and observation managers

  • Handles environment lifecycle (reset, step, etc.)

The Code Execution#

To run the modular environment:

cd /path/to/embodichain
python scripts/tutorials/gym/modular_env.py

The script demonstrates the complete workflow:

  1. Configuration: Creates an instance of ExampleCfg

  2. Registration: Uses the registered environment ID

  3. Execution: Runs episodes with zero actions to observe automatic behaviors

Manager System Benefits#

The manager-based architecture provides several key advantages:

Event Managers

  • Modularity: Reusable event functions across environments

  • Timing Control: Flexible scheduling (reset, interval, condition-based)

  • Parameter Binding: Type-safe configuration with validation

  • Extensibility: Easy to add custom event behaviors

Observation Managers

  • Flexible Data: Any simulation data can become an observation

  • Processing Pipeline: Built-in normalization and transformation

  • Dynamic Composition: Runtime observation space modification

  • Performance: Efficient data collection and GPU acceleration

Key Features Demonstrated#

This tutorial showcases the most advanced features of EmbodiChain environments:

  1. Configuration-Driven Design: Declarative environment specification

  2. Manager Systems: Modular event and observation handling

  3. Asset Management: Dynamic loading and randomization

  4. Sensor Integration: Realistic camera systems with stereo vision

  5. Physics Simulation: Complex articulated and rigid body dynamics

  6. Visual Randomization: Automated domain randomization

  7. Extensible Architecture: Easy customization and extension points

This tutorial demonstrates the full power of EmbodiChain’s modular environment system, providing the foundation for creating sophisticated robotic learning scenarios.

Tip

Using an AI coding agent? These skills can help you build on this tutorial:

  • /add-task-env — Scaffold a new task environment with the correct file structure, @register_env decorator, base class methods, __init__.py update, and test stub.

  • /add-functor — Add observation, reward, event, or randomization functors with the correct signature and module placement.

Next Steps#