Creating a Basic Environment#
This tutorial shows you how to create a simple robot learning environment using EmbodiChain’s Gym interface. You’ll learn how to inherit from the base environment class, set up robots and objects, define actions and observations, and run training scenarios.
The Code#
The tutorial corresponds to the random_reach.py script in the scripts/tutorials/gym directory.
Code for random_reach.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
20from embodichain.cli.sim import (
21 add_sim_args_to_parser,
22 add_seed_arg_to_parser,
23 resolve_seed,
24)
25
26
27def build_parser() -> argparse.ArgumentParser:
28 """Build CLI options without initializing simulation resources."""
29 parser = argparse.ArgumentParser(
30 description="Demo for running a random reach environment."
31 )
32 add_sim_args_to_parser(parser)
33 add_seed_arg_to_parser(parser, scope="task environment")
34 return parser
35
36
37if __name__ == "__main__":
38 # Parse before importing optional simulation/planning dependencies.
39 _cli_args = build_parser().parse_args()
40
41
42import torch
43import numpy as np
44import gymnasium as gym
45
46from embodichain.lab.gym.envs import BaseEnv, EnvCfg
47from embodichain.lab.sim import SimulationManagerCfg
48from embodichain.lab.visualization import VisualizationCfg
49from embodichain.lab.sim.types import EnvAction, EnvObs
50from embodichain.lab.sim.shapes import CubeCfg
51from embodichain.lab.sim.objects import RigidObject, Robot
52from embodichain.lab.sim.cfg import (
53 RenderCfg,
54 physics_cfg_for_backend,
55 RobotCfg,
56 RigidObjectCfg,
57 CollisionPropertiesCfg,
58 RigidBodyPhysicsCfg,
59)
60from embodichain.lab.gym.utils.registration import register_env
61
62
63@register_env("RandomReach-v1", override=True)
64class RandomReachEnv(BaseEnv):
65
66 robot_init_qpos = np.array(
67 [1.57079, -1.57079, 1.57079, -1.57079, -1.57079, -3.14159]
68 )
69
70 def __init__(
71 self,
72 num_envs=1,
73 headless=False,
74 device: str | torch.device | None = None,
75 renderer="hybrid",
76 physics_cfg="default",
77 seed: int | None = None,
78 visualization: VisualizationCfg | None = None,
79 **kwargs,
80 ) -> None:
81 env_cfg = EnvCfg(
82 seed=seed,
83 sim_cfg=SimulationManagerCfg(
84 headless=headless,
85 arena_space=2.0,
86 device=device,
87 render_cfg=RenderCfg(renderer=renderer),
88 physics_cfg=physics_cfg_for_backend(physics_cfg),
89 visualization=visualization or VisualizationCfg(),
90 ),
91 num_envs=num_envs,
92 )
93
94 super().__init__(
95 cfg=env_cfg,
96 **kwargs,
97 )
98
99 def _declare_robot(self, **kwargs) -> Robot:
100 from embodichain.data import get_data_path
101
102 file_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
103
104 return self.sim.add_robot(
105 cfg=RobotCfg(
106 uid="ur10",
107 fpath=file_path,
108 init_pos=(0, 0, 1),
109 init_qpos=self.robot_init_qpos,
110 )
111 )
112
113 def _setup_robot(self, **kwargs) -> Robot:
114 robot = self.robot
115 if robot is None:
116 raise RuntimeError("UR10 was not declared before simulation prepare.")
117
118 qpos_limits = robot.body_data.qpos_limits[0].cpu().numpy()
119 self.single_action_space = gym.spaces.Box(
120 low=qpos_limits[:, 0], high=qpos_limits[:, 1], dtype=np.float32
121 )
122
123 return robot
124
125 def _prepare_scene(self, **kwargs) -> None:
126 size = 0.03
127 # Create a kinematic cube object without collision.
128 # Currently, we use this workaround for visualization purposes.
129 self.cube: RigidObject = self.sim.add_rigid_object(
130 cfg=RigidObjectCfg(
131 uid="cube",
132 shape=CubeCfg(size=[size, size, size]),
133 attrs=RigidBodyPhysicsCfg(
134 collision_props=CollisionPropertiesCfg(
135 collision_enabled=False,
136 ),
137 ),
138 init_pos=(0.0, 0.0, 0.5),
139 body_type="kinematic",
140 ),
141 )
142
143 def _update_sim_state(self, **kwargs) -> None:
144 pose = torch.eye(4, device=self.device)
145 pose = pose.unsqueeze_(0).repeat(self.num_envs, 1, 1)
146 pose[:, :3, 3] += torch.rand(self.num_envs, 3, device=self.device) * 0.5 - 0.25
147 self.cube.set_local_pose(pose=pose)
148
149 def _step_action(self, action: EnvAction) -> EnvAction:
150 self.robot.set_qpos(qpos=action)
151 return action
152
153 def _extend_obs(self, obs: EnvObs, **kwargs) -> EnvObs:
154 # You can also use `cube = self.sim.get_rigid_object("cube")` to access obj.
155 # obs["cube_position"] = self.cube.get_local_pose()[:, :3]
156 return obs
157
158
159if __name__ == "__main__":
160 import argparse
161 import time
162
163 from embodichain.cli.sim import (
164 add_sim_args_to_parser,
165 add_seed_arg_to_parser,
166 resolve_seed,
167 )
168 from embodichain.lab.visualization import visualization_cfg_from_args
169
170 parser = build_parser()
171 args = _cli_args
172 seed = resolve_seed(args.seed)
173 print(f"[INFO]: Environment seed: {seed}", flush=True)
174
175 env = gym.make(
176 "RandomReach-v1",
177 seed=seed,
178 num_envs=args.num_envs,
179 headless=args.headless,
180 device=args.device,
181 renderer=args.renderer,
182 physics_cfg=args.physics,
183 visualization=visualization_cfg_from_args(args),
184 )
185
186 for episode in range(10):
187 print("Episode:", episode)
188 env.reset()
189 start_time = time.time()
190 total_steps = 0
191
192 for i in range(100):
193 action = env.action_space.sample()
194 action = torch.as_tensor(
195 action, dtype=torch.float32, device=env.get_wrapper_attr("device")
196 )
197
198 init_pose = env.unwrapped.robot_init_qpos
199 init_pose = (
200 torch.as_tensor(
201 init_pose,
202 dtype=torch.float32,
203 device=env.get_wrapper_attr("device"),
204 )
205 .unsqueeze_(0)
206 .repeat(env.get_wrapper_attr("num_envs"), 1)
207 )
208 action = (
209 init_pose
210 + torch.rand_like(
211 action, dtype=torch.float32, device=env.get_wrapper_attr("device")
212 )
213 * 0.2
214 - 0.1
215 )
216
217 obs, reward, done, truncated, info = env.step(action)
218 total_steps += env.get_wrapper_attr("num_envs")
219
220 end_time = time.time()
221 elapsed_time = end_time - start_time
222 if elapsed_time > 0:
223 fps = total_steps / elapsed_time
224 print(f"Total steps: {total_steps}")
225 print(f"Elapsed time: {elapsed_time:.2f} seconds")
226 print(f"FPS: {fps:.2f}")
227 else:
228 print("Elapsed time is too short to calculate FPS.")
229
230 env.close()
The Code Explained#
This tutorial demonstrates how to create a custom RL environment by inheriting from envs.BaseEnv. The environment implements a simple reach task where a robot arm tries to reach randomly positioned targets.
Environment Registration#
First, we register the environment with the Gymnasium registry using the utils.registration.register_env() decorator:
@register_env("RandomReach-v1", override=True)
class RandomReachEnv(BaseEnv):
The decorator parameters define:
Environment ID:
"RandomReach-v1"- unique identifier for the environmentoverride: Whether to override existing environment with same ID
Environment Initialization#
The __init__ method configures the simulation environment and calls the parent constructor:
def build_parser() -> argparse.ArgumentParser:
"""Build CLI options without initializing simulation resources."""
parser = argparse.ArgumentParser(
description="Demo for running a random reach environment."
)
add_sim_args_to_parser(parser)
add_seed_arg_to_parser(parser, scope="task environment")
return parser
if __name__ == "__main__":
# Parse before importing optional simulation/planning dependencies.
_cli_args = build_parser().parse_args()
import torch
import numpy as np
import gymnasium as gym
from embodichain.lab.gym.envs import BaseEnv, EnvCfg
Key configuration options include:
num_envs: Number of parallel environments to run
headless: Whether to run without GUI (useful for training)
device: Computation device (“cpu” or “cuda”)
Robot Setup#
The _setup_robot method loads and configures the robot for the environment:
def _setup_robot(self, **kwargs) -> Robot:
robot = self.robot
if robot is None:
raise RuntimeError("UR10 was not declared before simulation prepare.")
qpos_limits = robot.body_data.qpos_limits[0].cpu().numpy()
self.single_action_space = gym.spaces.Box(
low=qpos_limits[:, 0], high=qpos_limits[:, 1], dtype=np.float32
)
return robot
This method demonstrates:
URDF Loading: Using data module to access robot URDF files
Robot Configuration: Setting initial position and joint configuration
Action Space Definition: Creating action space based on joint limits
The action space is automatically derived from the robot’s joint limits, ensuring actions stay within valid ranges.
Scene Preparation#
The _prepare_scene() method adds additional objects to the simulation environment:
num_envs=1,
headless=False,
device: str | torch.device | None = None,
renderer="hybrid",
physics_cfg="default",
seed: int | None = None,
visualization: VisualizationCfg | None = None,
**kwargs,
) -> None:
env_cfg = EnvCfg(
seed=seed,
sim_cfg=SimulationManagerCfg(
headless=headless,
In this example, we add a kinematic cube that serves as a visual target. The cube is configured with:
No collision:
enable_collision=Falsefor visualization onlyKinematic body: Can be moved programmatically without physics
Custom size: Small 3cm cube for target visualization
initial position: Initially placed at a fixed location
State Updates#
The _update_sim_state method is called at each simulation step to update object states:
def _update_sim_state(self, **kwargs) -> None:
pose = torch.eye(4, device=self.device)
pose = pose.unsqueeze_(0).repeat(self.num_envs, 1, 1)
pose[:, :3, 3] += torch.rand(self.num_envs, 3, device=self.device) * 0.5 - 0.25
self.cube.set_local_pose(pose=pose)
This method randomizes the cube’s position. The pose is updated for all parallel environments simultaneously.
Note that this method is called after perform action execution and simulation update but before observation collection. For more details, see envs.BaseEnv.step().
Action Execution#
The _step_action method applies actions to the robot:
def _step_action(self, action: EnvAction) -> EnvAction:
self.robot.set_qpos(qpos=action)
return action
In this simple environment, actions directly set joint positions. More complex environments might:
Convert actions to joint torques or velocities
Apply action filtering or scaling
Implement inverse kinematics for end-effector control
Observation Extension#
The default observations include the following keys:
robot: Robot proprioception data (joint positions, velocities, efforts)
sensor (optional): Data from any sensors (e.g., cameras)
The _extend_obs method allows you to add custom observations:
def _extend_obs(self, obs: EnvObs, **kwargs) -> EnvObs:
# You can also use `cube = self.sim.get_rigid_object("cube")` to access obj.
# obs["cube_position"] = self.cube.get_local_pose()[:, :3]
return obs
While commented out in this example, you can add custom data like:
Object positions and orientations
Distance calculations
Custom sensor readings
Task-specific state information
The Code Execution#
To run the environment:
cd /path/to/embodichain
python scripts/tutorials/gym/random_reach.py
You can customize the execution with command-line options:
# Run multiple parallel environments
python scripts/tutorials/gym/random_reach.py --num_envs 4
# Run with GPU acceleration
python scripts/tutorials/gym/random_reach.py --device cuda
# Run in headless mode (no GUI)
python scripts/tutorials/gym/random_reach.py --headless
The script demonstrates:
Environment Creation: Using
gym.make()with custom parametersEpisode Loop: Running multiple episodes with random actions
Performance Monitoring: Calculating frames per second (FPS)
Key Features Demonstrated#
This tutorial showcases several important features of EmbodiChain environments:
Gymnasium Integration: Full compatibility with the Gymnasium API
Parallel Environments: Running multiple environments simultaneously for efficient training
Robot Integration: Easy loading and control of robotic systems
Custom Objects: Adding and manipulating scene objects
Flexible Actions: Customizable action spaces and execution methods
Extensible Observations: Adding task-specific observation data
Tip
Using an AI coding agent? Once you’re ready to create your own task environment, use the /add-task-env skill to scaffold the file with the correct structure, @register_env decorator, base class methods, and test stub. Use /add-test to write tests and /pre-commit-check to verify everything passes CI before committing.
Next Steps#
Creating a Modular Environment — Build advanced config-driven environments with
EmbodiedEnvReinforcement Learning Training — Train RL agents with PPO or GRPO
Embodied Environments — Full environment architecture and manager reference
Writing Custom Functors — Write custom observation, reward, and event functors