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 environment

  • override: 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:

  1. URDF Loading: Using data module to access robot URDF files

  2. Robot Configuration: Setting initial position and joint configuration

  3. 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=False for visualization only

  • Kinematic 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:

  1. Environment Creation: Using gym.make() with custom parameters

  2. Episode Loop: Running multiple episodes with random actions

  3. Performance Monitoring: Calculating frames per second (FPS)

Key Features Demonstrated#

This tutorial showcases several important features of EmbodiChain environments:

  1. Gymnasium Integration: Full compatibility with the Gymnasium API

  2. Parallel Environments: Running multiple environments simultaneously for efficient training

  3. Robot Integration: Easy loading and control of robotic systems

  4. Custom Objects: Adding and manipulating scene objects

  5. Flexible Actions: Customizable action spaces and execution methods

  6. 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#