Registering an Environment#
In the previous tutorial, we learned how to create a custom cartpole environment. We manually
created an instance of the environment from the class named by its configuration’s class_type.
Environment creation in the previous tutorial
# create environment configuration
env_cfg = parse_env_cfg(
"Isaac-Cartpole", device=args_cli.device, num_envs=args_cli.num_envs, overrides=hydra_overrides
)
# Launch the simulator runtime that the configuration needs
with launch_simulation(env_cfg, args_cli):
# setup RL environment
env = instantiate(env_cfg)
While straightforward, this approach is not scalable as we have a large suite of environments.
In this tutorial, we will show how to use the gymnasium.register() method to register
environments with the gymnasium registry. This allows us to create the environment through
the gymnasium.make() function.
Environment creation in this tutorial
env_cfg, _ = resolve_task_config(args_cli.task, "")
apply_env_overrides(args_cli, env_cfg)
pre_launch_video_config(env_cfg, args_cli)
# reject unsupported configurations before launching Kit or initializing a native physics backend
try:
validate(env_cfg)
except (TypeError, ValueError) as exc:
raise SystemExit(f"Invalid environment configuration: {exc}") from None
with launch_simulation(env_cfg, args_cli), contextlib.ExitStack() as cleanup:
log_dir = os.path.abspath(os.path.join("logs", f"{policy}_agent", normalize_task_name(args_cli.task)))
apply_video_recording(env_cfg, log_dir, args_cli, subdir="play")
env = gym.make(args_cli.task, cfg=env_cfg)
The Code#
The tutorial corresponds to the random_agent.py script in the scripts/environments directory. The
script is a thin wrapper that calls into the isaaclab_rl.entrypoints module, where the actual
implementation lives.
Code for simple_agents.py
1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2# All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""Checkpoint-free playback workflows for Isaac Lab environments.
7
8The zero and random agents are variations of playback that need no trained checkpoint:
9the policy either infers finite zero or hold actions or samples uniform random actions.
10"""
11
12from __future__ import annotations
13
14import argparse
15import contextlib
16import logging
17import os
18import sys
19from collections.abc import Callable
20from typing import Any, Literal
21
22import gymnasium as gym
23import torch
24
25from isaaclab.app import add_launcher_args, launch_simulation
26from isaaclab.envs.utils.spaces import sample_space
27from isaaclab.utils import math as math_utils
28from isaaclab.utils import validate
29
30import isaaclab_tasks # noqa: F401
31from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli
32
33from .common import (
34 add_video_args,
35 apply_env_overrides,
36 apply_video_recording,
37 close_env,
38 enable_cameras_for_video,
39 normalize_task_name,
40 pre_launch_video_config,
41 video_playback_steps,
42)
43
44logger = logging.getLogger(__name__)
45
46# PLACEHOLDER: Extension template (do not remove this comment)
47with contextlib.suppress(ImportError):
48 import isaaclab_tasks_experimental # noqa: F401
49
50PolicyName = Literal["zero", "random"]
51"""Action policies supported by the checkpoint-free agents."""
52
53_DESCRIPTIONS: dict[str, str] = {
54 "zero": "Zero agent for Isaac Lab environments.",
55 "random": "Random agent for Isaac Lab environments.",
56}
57
58_SEED = 42
59
60
61def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
62 """Run an Isaac Lab environment with a checkpoint-free policy.
63
64 Args:
65 argv: Command-line arguments excluding the executable name. Reads ``sys.argv`` when omitted.
66 policy: Action policy to apply, either inferred zero actions or uniform random actions.
67
68 Raises:
69 ValueError: If the requested policy is not supported.
70 """
71 if policy not in _DESCRIPTIONS:
72 raise ValueError(f"Unsupported policy {policy!r}. Expected one of: {sorted(_DESCRIPTIONS)}.")
73
74 args_cli = _parse_args(argv, policy)
75 torch.manual_seed(_SEED)
76
77 env_cfg, _ = resolve_task_config(args_cli.task, "")
78 apply_env_overrides(args_cli, env_cfg)
79 pre_launch_video_config(env_cfg, args_cli)
80 # reject unsupported configurations before launching Kit or initializing a native physics backend
81 try:
82 validate(env_cfg)
83 except (TypeError, ValueError) as exc:
84 raise SystemExit(f"Invalid environment configuration: {exc}") from None
85
86 with launch_simulation(env_cfg, args_cli), contextlib.ExitStack() as cleanup:
87 log_dir = os.path.abspath(os.path.join("logs", f"{policy}_agent", normalize_task_name(args_cli.task)))
88 apply_video_recording(env_cfg, log_dir, args_cli, subdir="play")
89 env = gym.make(args_cli.task, cfg=env_cfg)
90 cleanup.callback(lambda: close_env(env))
91 logger.info(f"Gym observation space: {env.observation_space}")
92 logger.info(f"Gym action space: {env.action_space}")
93 env.reset()
94 if policy == "zero":
95 action_policy = create_zero_action_policy(env)
96 else:
97 action_policy = create_random_action_policy(env)
98 logger.info(f"{policy.capitalize()} agent is running, press Ctrl+C to exit...")
99
100 budgets = [n for n in (args_cli.max_steps, video_playback_steps(args_cli, env_cfg)) if n is not None]
101 max_steps = min(budgets, default=None)
102
103 # keep running while any visualizer is open and the step budget is not exhausted
104 sim = env.unwrapped.sim
105 step = 0
106 while sim.is_running():
107 if max_steps is not None and step >= max_steps:
108 break
109 step += 1
110 with torch.inference_mode():
111 env.step(action_policy())
112
113
114def create_zero_action_policy(env: gym.Env) -> Callable[[], Any]:
115 """Create a policy that emits finite actions for passive environment playback.
116
117 Manager-based environments infer hold commands for absolute task-space action terms and use literal zeros for all
118 other terms. Direct-workflow environments use zero-filled samples of their declared Gymnasium spaces, including
119 composite and multi-agent spaces.
120 """
121 unwrapped = env.unwrapped
122 action_manager = getattr(unwrapped, "action_manager", None)
123 if action_manager is not None:
124 return _create_manager_zero_action_policy(action_manager, unwrapped)
125
126 if hasattr(unwrapped, "action_spaces"):
127 actions = {
128 agent: sample_space(space, unwrapped.device, batch_size=unwrapped.num_envs, fill_value=0)
129 for agent, space in unwrapped.action_spaces.items()
130 }
131 return lambda: actions
132
133 actions = sample_space(unwrapped.single_action_space, unwrapped.device, batch_size=unwrapped.num_envs, fill_value=0)
134 return lambda: actions
135
136
137def create_random_action_policy(env: gym.Env) -> Callable[[], torch.Tensor]:
138 """Create a policy that samples uniform random actions in ``[-1, 1]``."""
139 device = env.unwrapped.device
140 return lambda: 2 * torch.rand(env.action_space.shape, device=device) - 1
141
142
143def _create_manager_zero_action_policy(action_manager: Any, env: Any) -> Callable[[], torch.Tensor]:
144 """Create a zero-action policy from the active action terms."""
145 actions = torch.zeros_like(action_manager.action)
146 term_policies = []
147 index = 0
148 for term_name in action_manager.active_terms:
149 term = action_manager.get_term(term_name)
150 term_policy = _create_action_term_zero_policy(term, env)
151 if term_policy is not None:
152 term_policies.append((slice(index, index + term.action_dim), term_policy))
153 index += term.action_dim
154
155 def policy() -> torch.Tensor:
156 actions.zero_()
157 for action_slice, term_policy in term_policies:
158 actions[:, action_slice] = term_policy()
159 if not torch.isfinite(actions).all():
160 raise RuntimeError("Zero agent inferred non-finite actions from the current environment state.")
161 return actions
162
163 return policy
164
165
166def _create_action_term_zero_policy(term: Any, env: Any) -> Callable[[], torch.Tensor] | None:
167 """Create the specialized zero-action policy required by an action term."""
168 term_types = {cls.__name__ for cls in type(term).__mro__}
169
170 if "PinkInverseKinematicsAction" in term_types:
171 controlled_frame_ids, controlled_frame_names = term._asset.find_bodies(
172 list(term.cfg.target_eef_link_names.values()), preserve_order=True
173 )
174 if len(controlled_frame_ids) != len(term.cfg.target_eef_link_names):
175 raise ValueError(
176 "Expected one controlled body for every Pink IK target. Resolved "
177 f"{controlled_frame_names} from {list(term.cfg.target_eef_link_names.values())}."
178 )
179 if len(controlled_frame_ids) != term._num_frame_tasks:
180 raise ValueError(
181 f"Pink IK has {term._num_frame_tasks} variable frame tasks but "
182 f"{len(controlled_frame_ids)} controlled bodies were configured."
183 )
184
185 def pink_policy() -> torch.Tensor:
186 frame_poses = term._asset.data.body_link_pose_w.torch[:, controlled_frame_ids].clone()
187 frame_poses[..., :3] -= env.scene.env_origins.unsqueeze(1)
188 hand_joint_positions = term._asset.data.joint_pos.torch[:, term._hand_joint_ids]
189 return torch.cat((frame_poses.flatten(start_dim=1), hand_joint_positions), dim=-1)
190
191 return pink_policy
192
193 if "DifferentialInverseKinematicsAction" in term_types and not term.cfg.controller.use_relative_mode:
194
195 def differential_ik_policy() -> torch.Tensor:
196 ee_pos, ee_quat = term._compute_frame_pose()
197 command = ee_pos if term.cfg.controller.command_type == "position" else torch.cat((ee_pos, ee_quat), dim=-1)
198 return _unscale_action(command - term._offset, term._scale)
199
200 return differential_ik_policy
201
202 if "RMPFlowAction" in term_types and not term.cfg.use_relative_mode:
203
204 def rmpflow_policy() -> torch.Tensor:
205 ee_pos, ee_quat = term._compute_frame_pose()
206 return _unscale_action(torch.cat((ee_pos, ee_quat), dim=-1), term._scale)
207
208 return rmpflow_policy
209
210 if "OperationalSpaceControllerAction" in term_types and term._pose_abs_idx is not None:
211 term_actions = torch.zeros_like(term.raw_actions)
212
213 def operational_space_policy() -> torch.Tensor:
214 term_actions.zero_()
215 term._compute_ee_pose()
216 term._compute_task_frame_pose()
217 if term._task_frame_pose_b is None:
218 ee_pos_task = term._ee_pose_b[:, :3]
219 ee_quat_task = term._ee_pose_b[:, 3:7]
220 else:
221 ee_pos_task, ee_quat_task = math_utils.subtract_frame_transforms(
222 term._task_frame_pose_b[:, :3],
223 term._task_frame_pose_b[:, 3:7],
224 term._ee_pose_b[:, :3],
225 term._ee_pose_b[:, 3:7],
226 )
227 term_actions[:, term._pose_abs_idx : term._pose_abs_idx + 3] = _unscale_action(
228 ee_pos_task, term._position_scale
229 )
230 term_actions[:, term._pose_abs_idx + 3 : term._pose_abs_idx + 7] = _unscale_action(
231 ee_quat_task, term._orientation_scale
232 )
233 return term_actions
234
235 return operational_space_policy
236
237 return None
238
239
240def _unscale_action(command: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
241 """Map a processed command back to policy-action coordinates without division by zero."""
242 return torch.where(scale != 0.0, command / scale, torch.zeros_like(command))
243
244
245def _parse_args(argv: list[str] | None, policy: PolicyName) -> argparse.Namespace:
246 """Parse the command line of a checkpoint-free agent and hand the remainder to Hydra."""
247 parser = argparse.ArgumentParser(description=_DESCRIPTIONS[policy])
248 parser.add_argument(
249 "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations."
250 )
251 parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
252 parser.add_argument("--task", type=str, default=None, help="Name of the task.")
253 parser.add_argument(
254 "--max_steps", type=int, default=None, help="Number of environment steps to run. Runs unbounded when omitted."
255 )
256 add_video_args(parser, action=f"the {policy} agent run")
257 add_launcher_args(parser)
258 # let task configs select the simulation device
259 parser.set_defaults(device=None)
260 args_cli, hydra_args = setup_preset_cli(parser, argv)
261 enable_cameras_for_video(args_cli)
262 sys.argv = [sys.argv[0]] + hydra_args
263 return args_cli
The Code Explained#
The envs.ManagerBasedRLEnv class inherits from the gymnasium.Env class to follow
a standard interface. However, unlike the traditional Gym environments, the envs.ManagerBasedRLEnv
implements a vectorized environment. This means that multiple environment instances
are running simultaneously in the same process, and all the data is returned in a batched
fashion.
Similarly, the envs.DirectRLEnv class also inherits from the gymnasium.Env class
for the direct workflow. For envs.DirectMARLEnv, although it does not inherit
from Gymnasium, it can be registered and created in the same way.
Using the gym registry#
To register an environment, we use the gymnasium.register() method. This method takes
in the environment name, the entry point to the environment class, and the entry point to the
environment configuration class.
Note
The gymnasium registry is a global registry. Hence, it is important to ensure that the
environment names are unique. Otherwise, the registry will throw an error when registering
the environment.
Manager-Based Environments#
For manager-based environments, the following shows the registration
call for the cartpole environment in the isaaclab_tasks.core.cartpole sub-package:
from . import agents
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerCfg",
"torchrl_cfg_entry_point": f"{agents.__name__}.torchrl_ppo_cfg:CartpolePPOCfg",
"default_agent": "rsl_rl",
"rsl_rl_with_symmetry_cfg_entry_point": (
f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerWithSymmetryCfg"
),
"skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml",
"sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml",
},
)
gym.register(
id="Isaac-Cartpole-Camera",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
The id argument is the name of the environment. As a convention, we name all the environments
with the prefix Isaac- to make it easier to search for them in the registry. The name of the
environment is typically followed by the name of the task, and then the name of the robot.
For instance, for legged locomotion with ANYmal C on flat terrain, the environment is called
IsaacContrib-Velocity-Flat-AnymalC. The version number v<N> is typically used to specify different
variations of the same environment. Otherwise, the names of the environments can become too long
and difficult to read.
The entry_point argument is the entry point to the environment class. The entry point is a string
of the form <module>:<class>. In the case of the cartpole environment, the entry point is
isaaclab.envs:ManagerBasedRLEnv. The entry point is used to import the environment class
when creating the environment instance.
The env_cfg_entry_point argument specifies the default configuration for the environment. The default
configuration is loaded using the isaaclab_tasks.utils.parse_env_cfg() function.
It is then passed to the gymnasium.make() function to create the environment instance.
The configuration entry point can be both a YAML file or a python configuration class.
Direct Environments#
For direct-based environments, the environment registration follows a similar pattern. Instead of
registering the environment’s entry point as the ManagerBasedRLEnv class,
we register the environment’s entry point as the implementation class of the environment.
Additionally, we add the suffix -Direct to the environment name to differentiate it from the
manager-based environments.
As an example, the following shows the registration call for the cartpole environment in the
isaaclab_tasks.core.cartpole sub-package:
from . import agents
"sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml",
},
)
gym.register(
id="Isaac-Cartpole-Camera-Direct",
entry_point=f"{__name__}.cartpole_direct_camera_env:CartpoleCameraEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.cartpole_direct_camera_env_cfg:CartpoleCameraEnvCfg",
"rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpoleCameraDirectPPORunnerCfg",
Creating the environment#
To inform the gym registry with all the environments provided by the isaaclab_tasks
extension, we must import the module at the start of the script. This will execute the __init__.py
file which iterates over all the sub-packages and registers their respective environments.
import isaaclab_tasks # noqa: F401
In this tutorial, the task name is read from the command line. The task name is used to parse the default configuration as well as to create the environment instance. In addition, other parsed command line arguments such as the number of environments, the simulation device, and whether to render, are used to override the default configuration.
env_cfg, _ = resolve_task_config(args_cli.task, "")
apply_env_overrides(args_cli, env_cfg)
pre_launch_video_config(env_cfg, args_cli)
# reject unsupported configurations before launching Kit or initializing a native physics backend
try:
validate(env_cfg)
except (TypeError, ValueError) as exc:
raise SystemExit(f"Invalid environment configuration: {exc}") from None
with launch_simulation(env_cfg, args_cli), contextlib.ExitStack() as cleanup:
log_dir = os.path.abspath(os.path.join("logs", f"{policy}_agent", normalize_task_name(args_cli.task)))
apply_video_recording(env_cfg, log_dir, args_cli, subdir="play")
env = gym.make(args_cli.task, cfg=env_cfg)
Once creating the environment, the rest of the execution follows the standard resetting and stepping.
The Code Execution#
Now that we have gone through the code, let’s run the script and see the result:
uv run python scripts/environments/random_agent.py --task Isaac-Cartpole --num_envs 32 --viz kit
This should open a stage with everything similar to the Creating a Manager-Based RL Environment tutorial.
To stop the simulation, you can either close the window, or press Ctrl+C in the terminal.
In addition, you can also change the simulation device from GPU to CPU by setting the value of the --device flag explicitly:
uv run python scripts/environments/random_agent.py --task Isaac-Cartpole --num_envs 32 --device cpu --viz kit
With the --device cpu flag, the simulation will run on the CPU. This is useful for debugging the simulation.
However, the simulation will run much slower than on the GPU.