Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
205 changes: 205 additions & 0 deletions docs/source/features/hydra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,208 @@ the post init update is as follows:

Here, when modifying ``env.decimation`` or ``env.sim.dt``, the user needs to give the updated ``env.sim.render_interval``,
``env.scene.height_scanner.update_period``, and ``env.scene.contact_forces.update_period`` as input as well.


Preset System
-------------

The preset system lets you swap out entire config sections with a single command line argument.
Instead of overriding individual fields, you select a named preset that **completely replaces** the
config section (no field merging).

Presets are defined directly on config classes using a ``presets`` attribute. The system recursively
discovers all presets from nested configs automatically.


Override Order
^^^^^^^^^^^^^^

The override order is strict and deterministic:

**By type** (earlier stages are overwritten by later ones):

1. **Presets** — Global preset selections (``presets=noise_less``)
2. **Groups** — Path preset selections (``env.actions.arm_action=relative_joint_position``)
3. **Single field** — Scalar overrides (``env.sim.dt=0.001``)

**Within the same type:**

- **Lower depth > higher depth** — Parent paths are applied before child paths (e.g. ``env.actions`` before ``env.actions.arm_action``)
- **Left > right** — Arguments are processed left-to-right; for the same target, the rightmost override wins

**Concrete sequence:**

1. Auto-default presets for paths not explicitly selected
2. Global presets (``presets=name``)
3. Path presets (``env.actions=name``), sorted by depth ascending
4. Scalar overrides (both preset-path and global)


Defining Presets
^^^^^^^^^^^^^^^^

There are three styles for defining presets:

**Style 1: Inheritance** - Default values from base class, presets for alternatives:

.. code-block:: python

@configclass
class FrankaArmActionCfg(mdp.JointPositionActionCfg):
"""Franka arm action config with presets for different action types."""

presets = {
"joint_position_to_limit": mdp.JointPositionToLimitsActionCfg(
asset_name="robot", joint_names=["panda_joint.*"]
),
"relative_joint_position": mdp.RelativeJointPositionActionCfg(
asset_name="robot", joint_names=["panda_joint.*"], scale=0.2
),
}

**Style 2: Inner class** - Self-contained with nested preset definitions:

.. code-block:: python

@configclass
class PhysxCfg:
"""Simulation config with physics backend presets."""

backend: str = "physx"

@configclass
class Newton:
backend: str = "newton"

presets = {"newton": Newton()}

**Style 3: Preset-only with auto-default** - Pure composition, no default fields:

.. code-block:: python

@configclass
class ObservationsCfg:
"""Observation specifications with presets."""

presets = {
"default": DefaultObservationsCfg(),
"noise_less": NoiselessObservationsCfg(),
}

With Style 3, the ``"default"`` preset is automatically applied when no preset is selected.


Using Presets
^^^^^^^^^^^^^

**Path presets** - Select a specific preset for one config path:

.. code-block:: bash

# Use relative joint position action
python train.py --task=Isaac-Reach-Franka-v0 \
env.actions.arm_action=relative_joint_position

# Use noiseless observations
python train.py --task=Isaac-Reach-Franka-v0 \
env.observations=noise_less

**Path preset + scalar override** - Select preset then modify a field:

.. code-block:: bash

python train.py --task=Isaac-Reach-Franka-v0 \
env.actions.arm_action=relative_joint_position \
env.actions.arm_action.scale=0.5

**Global presets** - Apply the same preset name everywhere it exists:

.. code-block:: bash

# Apply "noise_less" preset to all configs that define it
# (e.g., env.observations in Isaac-Reach-Franka-v0)
python train.py --task=Isaac-Reach-Franka-v0 \
presets=noise_less

**Multiple global presets** - Apply several non-conflicting presets (each applies to configs that define it):

.. code-block:: bash

# Apply noise_less to env.observations and relative_joint_position to env.actions.arm_action
python train.py --task=Isaac-Reach-Franka-v0 \
presets=noise_less,relative_joint_position

**Combined** - Global presets + path presets + scalar overrides:

.. code-block:: bash

python train.py --task=Isaac-Reach-Franka-v0 \
presets=noise_less \
env.actions.arm_action=relative_joint_position \
env.actions.arm_action.scale=0.5 \
env.sim.dt=0.002


Global Preset Conflict Detection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If multiple global presets define the same path, an error is raised:

.. code-block:: bash

# ERROR: both "fast" and "noise_less" define env.observations
python train.py --task=Isaac-Reach-Franka-v0 \
presets=fast,noise_less

# ValueError: Conflicting global presets: 'fast' and 'noise_less'
# both define preset for 'env.observations'


Real-World Example
^^^^^^^^^^^^^^^^^^

The Franka Reach environment demonstrates presets in practice:

.. literalinclude:: ../../../source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/joint_pos_env_cfg.py
:language: python
:start-at: @configclass
:end-before: ##

This allows users to switch action types:

.. code-block:: bash

# Default: JointPositionActionCfg (from inheritance)
python train.py --task=Isaac-Reach-Franka-v0

# Switch to relative joint position
python train.py --task=Isaac-Reach-Franka-v0 \
env.actions.arm_action=relative_joint_position

# Switch to joint position with limits
python train.py --task=Isaac-Reach-Franka-v0 \
env.actions.arm_action=joint_position_to_limit


Summary
^^^^^^^

.. list-table::
:widths: 25 35 40
:header-rows: 1

* - Override Type
- Syntax
- Effect
* - Scalar
- ``env.sim.dt=0.001``
- Modify single field
* - Path preset
- ``env.actions.arm_action=relative``
- Replace entire section
* - Global preset
- ``presets=noise_less``
- Apply everywhere matching
* - Combined
- ``presets=noise_less env.sim.dt=0.001``
- Global + scalar overrides
4 changes: 2 additions & 2 deletions docs/source/migration/migrating_to_isaaclab_3-0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -683,10 +683,10 @@ The previous ``write_*_to_sim(data, env_ids)`` methods have been removed.
robot.write_root_pose_to_sim(pose_data, env_ids)

# After (Isaac Lab 3.x) — indexed variant (partial data)
robot.write_root_pose_to_sim_index(pose_data, env_ids)
robot.write_root_pose_to_sim_index(root_pose=pose_data, env_ids=env_ids)

# After (Isaac Lab 3.x) — mask variant (full data, boolean mask)
robot.write_root_pose_to_sim_mask(pose_data, env_mask)
robot.write_root_pose_to_sim_mask(root_pose=pose_data, env_mask=env_mask)

.. list-table:: Affected write methods (RigidObject / Articulation)
:header-rows: 1
Expand Down
56 changes: 56 additions & 0 deletions docs/source/refs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,62 @@ without triggering circular imports.
It is important to note that this is the sole instance within our codebase where circular imports are used
and are acceptable. In all other scenarios, we adhere to best practices and recommend that you do the same.

Lazy Loading and ``__init__.py`` Structure
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Isaac Lab uses the `lazy_loader <https://pypi.org/project/lazy-loader/>`__ package
(``lazy.attach``) to defer the loading of heavy modules. This is critical because
certain backend modules (``pxr``, ``omni``, ``carb``, ``isaacsim``) must not be
imported before ``SimulationApp`` is initialized. Importing them too early causes
crashes such as ``free(): invalid pointer``.

**Configuration vs. Implementation separation**

The codebase follows a clear separation between *configuration classes* (pure data,
no runtime dependencies) and *implementation classes* (require backend modules at
runtime). In ``__init__.py`` files, this separation is expressed as:

* **Cfg classes** are imported eagerly at the top of the file.
* **Implementation classes** are deferred via ``lazy.attach()``.

This pattern makes the architectural intent explicit: configuration classes are
lightweight and always available, while implementation classes are only loaded when
actually used.

.. code:: python

# Example: isaaclab/assets/articulation/__init__.py

import lazy_loader as lazy

from .articulation_cfg import ArticulationCfg # safe -- pure data, no backend deps

__getattr__, __dir__, __all__ = lazy.attach(
__name__,
submod_attrs={
"base_articulation": ["BaseArticulation"],
"base_articulation_data": ["BaseArticulationData"],
"articulation": ["Articulation"], # deferred -- may chain into backend
"articulation_data": ["ArticulationData"],
},
)
__all__ += ["ArticulationCfg"]

**Rules for deciding eager vs. lazy**

1. A ``Cfg`` class can be eagerly imported only if its module (and every module it
transitively imports) is free of backend imports (``pxr``, ``omni``, ``carb``,
``isaacsim``).
2. If a ``Cfg`` module imports an implementation class (e.g. for ``class_type``
defaults), verify that the implementation module is also backend-free before
making the ``Cfg`` eager.
3. If there is any doubt, keep the ``Cfg`` in the ``lazy.attach()`` block. Safety
over style.
4. For callable fields such as ``func`` in spawner configs, prefer string references
(e.g. ``"isaaclab.sim.spawners.shapes.shapes:spawn_sphere"``) over direct
function references. Strings are inert data and never trigger accidental imports.


Type-hinting
^^^^^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion scripts/benchmarks/benchmark_load_robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
wp.to_torch(robot.data.default_joint_vel).clone(),
)
joint_pos += torch.rand_like(joint_pos) * 0.1
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# clear internal buffers
scene.reset()
# Apply random action
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/arms.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula
robot.write_root_velocity_to_sim(root_state[:, 7:])
# set joint positions
joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone()
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# clear internal buffers
robot.reset()
print("[INFO]: Resetting robots state...")
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/bipeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def run_simulator(sim: sim_utils.SimulationContext, robots: list[Articulation],
wp.to_torch(robot.data.default_joint_pos),
wp.to_torch(robot.data.default_joint_vel),
)
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
root_state = wp.to_torch(robot.data.default_root_state).clone()
root_state[:, :3] += origins[index]
robot.write_root_pose_to_sim(root_state[:, :7])
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/hands.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula
wp.to_torch(robot.data.default_joint_pos).clone(),
wp.to_torch(robot.data.default_joint_vel).clone(),
)
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# reset the internal state
robot.reset()
print("[INFO]: Resetting robots state...")
Expand Down
4 changes: 2 additions & 2 deletions scripts/demos/haply_teleoperation.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def run_simulator(
joint_pos = robot.data.default_joint_pos.clone()
joint_pos[0, :7] = torch.tensor([0.0, -0.569, 0.0, -2.81, 0.0, 3.037, 0.741], device=robot.device)
joint_vel = robot.data.default_joint_vel.clone()
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)

for _ in range(10):
scene.write_data_to_sim()
Expand Down Expand Up @@ -253,7 +253,7 @@ def run_simulator(
joint_pos = robot.data.default_joint_pos.clone()
joint_pos[0, :7] = torch.tensor([0.0, -0.569, 0.0, -2.81, 0.0, 3.037, 0.741], device=robot.device)
joint_vel = robot.data.default_joint_vel.clone()
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)

cube_state = cube.data.default_root_state.clone()
cube_state[:, :3] += scene.env_origins
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/multi_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def run_simulator(sim: SimulationContext, scene: InteractiveScene):
wp.to_torch(robot.data.default_joint_pos).clone(),
wp.to_torch(robot.data.default_joint_vel).clone(),
)
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# clear internal buffers
scene.reset()
print("[INFO]: Resetting scene state...")
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/pick_and_place.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def _reset_idx(self, env_ids: Sequence[int] | None):
self.joint_pos[env_ids] = joint_pos
self.joint_vel[env_ids] = joint_vel

self.pick_and_place.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids)
self.pick_and_place.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel, joint_ids=None, env_ids=env_ids)

def _set_debug_vis_impl(self, debug_vis: bool):
# create markers if necessary for the first tome
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/quadcopter.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def main():
count = 0
# reset dof state
joint_pos, joint_vel = wp.to_torch(robot.data.default_joint_pos), wp.to_torch(robot.data.default_joint_vel)
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
default_root_state = wp.to_torch(robot.data.default_root_state)
robot.write_root_pose_to_sim(default_root_state[:, :7])
robot.write_root_velocity_to_sim(default_root_state[:, 7:])
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/quadrupeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula
robot.write_root_velocity_to_sim(root_state[:, 7:])
# joint state
joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone()
robot.write_joint_state_to_sim(joint_pos, joint_vel)
robot.write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# reset the internal state
robot.reset()
print("[INFO]: Resetting robots state...")
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/sensors/cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
wp.to_torch(scene["robot"].data.default_joint_vel).clone(),
)
joint_pos += torch.rand_like(joint_pos) * 0.1
scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel)
scene["robot"].write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# clear internal buffers
scene.reset()
print("[INFO]: Resetting robot state...")
Expand Down
2 changes: 1 addition & 1 deletion scripts/demos/sensors/contact_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
wp.to_torch(scene["robot"].data.default_joint_vel).clone(),
)
joint_pos += torch.rand_like(joint_pos) * 0.1
scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel)
scene["robot"].write_joint_state_to_sim(position=joint_pos, velocity=joint_vel)
# clear internal buffers
scene.reset()
print("[INFO]: Resetting robot state...")
Expand Down
Loading