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:
Mode:
"reset"- triggers at environment resetPurpose: Randomly selects different fork models from a folder
Light Randomization Event
The randomize_light event creates dynamic lighting conditions:
Function:
envs.managers.randomization.rendering.randomize_light()Mode:
"interval"- triggers every 5 stepsParameters: 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 stepsFeatures: 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:
Function:
envs.managers.observations.get_rigid_object_pose()Mode:
"add"- appends data to observation dictionaryName: Custom key for the observation data
Target: Tracks the fork object’s pose in the scene
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:
Configuration: Creates an instance of
ExampleCfgRegistration: Uses the registered environment ID
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:
Configuration-Driven Design: Declarative environment specification
Manager Systems: Modular event and observation handling
Asset Management: Dynamic loading and randomization
Sensor Integration: Realistic camera systems with stereo vision
Physics Simulation: Complex articulated and rigid body dynamics
Visual Randomization: Automated domain randomization
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_envdecorator, base class methods,__init__.pyupdate, and test stub./add-functor — Add observation, reward, event, or randomization functors with the correct signature and module placement.
Next Steps#
Configure and Run an Embodied Task Program — Compose a modular environment with a declarative Task Program, embodiment, scene binding, and execution policy.
Expert Data Expansion — Record expert demonstrations from modular environments.