Unverified Commit 17f12fdf authored by oahmednv's avatar oahmednv Committed by GitHub

Raises exceptions from initialization callbacks inside SimContext (#2166)

# Description


Handling exceptions when raised inside the initialization callbacks

Fixes #1025 

## Type of change

- Bug fix (non-breaking change which fixes an issue)


## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------
Signed-off-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
parent 104805f2
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.39.6"
version = "0.39.7"
# Description
title = "Isaac Lab framework for Robot Learning"
......
Changelog
---------
0.39.7 (2025-05-19)
~~~~~~~~~~~~~~~~~~~
Fixed
^^^^^^
* Raising exceptions in step, render and reset if they occurred inside the initialization callbacks
of assets and sensors.used from the experience files and the double definition is removed.
0.39.6 (2025-01-30)
~~~~~~~~~~~~~~~~~~~
......
......@@ -5,6 +5,7 @@
from __future__ import annotations
import builtins
import inspect
import re
import torch
......@@ -16,7 +17,7 @@ from typing import TYPE_CHECKING, Any
import isaacsim.core.utils.prims as prim_utils
import omni.kit.app
import omni.timeline
from isaacsim.core.simulation_manager import SimulationManager
from isaacsim.core.simulation_manager import IsaacEvents, SimulationManager
import isaaclab.sim as sim_utils
......@@ -102,6 +103,9 @@ class AssetBase(ABC):
lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event),
order=10,
)
self._prim_deletion_callback_id = SimulationManager.register_callback(
self._on_prim_deletion, event=IsaacEvents.PRIM_DELETION
)
# add handle for debug visualization (this is set to a valid handle inside set_debug_vis)
self._debug_vis_handle = None
# set initial state of debug visualization
......@@ -109,17 +113,8 @@ class AssetBase(ABC):
def __del__(self):
"""Unsubscribe from the callbacks."""
# clear physics events handles
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_vis_handle:
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
# clear events handles
self._clear_callbacks()
"""
Properties
......@@ -288,10 +283,15 @@ class AssetBase(ABC):
called whenever the simulator "plays" from a "stop" state.
"""
if not self._is_initialized:
# obtain simulation related information
self._backend = SimulationManager.get_backend()
self._device = SimulationManager.get_physics_sim_device()
# initialize the asset
self._initialize_impl()
try:
self._initialize_impl()
except Exception as e:
if builtins.ISAACLAB_CALLBACK_EXCEPTION is None:
builtins.ISAACLAB_CALLBACK_EXCEPTION = e
# set flag
self._is_initialized = True
......@@ -301,3 +301,37 @@ class AssetBase(ABC):
if self._debug_vis_handle is not None:
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
def _on_prim_deletion(self, prim_path: str) -> None:
"""Invalidates and deletes the callbacks when the prim is deleted.
Args:
prim_path: The path to the prim that is being deleted.
Note:
This function is called when the prim is deleted.
"""
if prim_path == "/":
self._clear_callbacks()
return
result = re.match(
pattern="^" + "/".join(self.cfg.prim_path.split("/")[: prim_path.count("/") + 1]) + "$", string=prim_path
)
if result:
self._clear_callbacks()
def _clear_callbacks(self) -> None:
"""Clears the callbacks."""
if self._prim_deletion_callback_id:
SimulationManager.deregister_callback(self._prim_deletion_callback_id)
self._prim_deletion_callback_id = None
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_vis_handle:
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
......@@ -15,7 +15,7 @@ import omni.kit.app
import omni.log
import omni.physics.tensors.impl.api as physx
import omni.timeline
from isaacsim.core.simulation_manager import SimulationManager
from isaacsim.core.simulation_manager import IsaacEvents, SimulationManager
from pxr import UsdPhysics
import isaaclab.sim as sim_utils
......@@ -67,7 +67,7 @@ class RigidObjectCollection(AssetBase):
self.cfg = cfg.copy()
# flag for whether the asset is initialized
self._is_initialized = False
self._prim_paths = []
# spawn the rigid objects
for rigid_object_cfg in self.cfg.rigid_objects.values():
# check if the rigid object path is valid
......@@ -88,7 +88,7 @@ class RigidObjectCollection(AssetBase):
matching_prims = sim_utils.find_matching_prims(rigid_object_cfg.prim_path)
if len(matching_prims) == 0:
raise RuntimeError(f"Could not find prim with path {rigid_object_cfg.prim_path}.")
self._prim_paths.append(rigid_object_cfg.prim_path)
# stores object names
self._object_names_list = []
......@@ -106,7 +106,9 @@ class RigidObjectCollection(AssetBase):
lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event),
order=10,
)
self._prim_deletion_callback_id = SimulationManager.register_callback(
self._on_prim_deletion, event=IsaacEvents.PRIM_DELETION
)
self._debug_vis_handle = None
"""
......@@ -688,3 +690,23 @@ class RigidObjectCollection(AssetBase):
super()._invalidate_initialize_callback(event)
# set all existing views to None to invalidate them
self._root_physx_view = None
def _on_prim_deletion(self, prim_path: str) -> None:
"""Invalidates and deletes the callbacks when the prim is deleted.
Args:
prim_path: The path to the prim that is being deleted.
Note:
This function is called when the prim is deleted.
"""
if prim_path == "/":
self._clear_callbacks()
return
for prim_path_expr in self._prim_paths:
result = re.match(
pattern="^" + "/".join(prim_path_expr.split("/")[: prim_path.count("/") + 1]) + "$", string=prim_path
)
if result:
self._clear_callbacks()
return
......@@ -11,7 +11,9 @@ Each sensor class should inherit from this class and implement the abstract meth
from __future__ import annotations
import builtins
import inspect
import re
import torch
import weakref
from abc import ABC, abstractmethod
......@@ -20,6 +22,7 @@ from typing import TYPE_CHECKING, Any
import omni.kit.app
import omni.timeline
from isaacsim.core.simulation_manager import IsaacEvents, SimulationManager
import isaaclab.sim as sim_utils
......@@ -71,6 +74,9 @@ class SensorBase(ABC):
lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event),
order=10,
)
self._prim_deletion_callback_id = SimulationManager.register_callback(
self._on_prim_deletion, event=IsaacEvents.PRIM_DELETION
)
# add handle for debug visualization (this is set to a valid handle inside set_debug_vis)
self._debug_vis_handle = None
# set initial state of debug visualization
......@@ -79,16 +85,7 @@ class SensorBase(ABC):
def __del__(self):
"""Unsubscribe from the callbacks."""
# clear physics events handles
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_vis_handle:
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
self._clear_callbacks()
"""
Properties
......@@ -270,7 +267,11 @@ class SensorBase(ABC):
called whenever the simulator "plays" from a "stop" state.
"""
if not self._is_initialized:
self._initialize_impl()
try:
self._initialize_impl()
except Exception as e:
if builtins.ISAACLAB_CALLBACK_EXCEPTION is None:
builtins.ISAACLAB_CALLBACK_EXCEPTION = e
self._is_initialized = True
def _invalidate_initialize_callback(self, event):
......@@ -280,6 +281,40 @@ class SensorBase(ABC):
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
def _on_prim_deletion(self, prim_path: str) -> None:
"""Invalidates and deletes the callbacks when the prim is deleted.
Args:
prim_path: The path to the prim that is being deleted.
Note:
This function is called when the prim is deleted.
"""
if prim_path == "/":
self._clear_callbacks()
return
result = re.match(
pattern="^" + "/".join(self.cfg.prim_path.split("/")[: prim_path.count("/") + 1]) + "$", string=prim_path
)
if result:
self._clear_callbacks()
def _clear_callbacks(self) -> None:
"""Clears the callbacks."""
if self._prim_deletion_callback_id:
SimulationManager.deregister_callback(self._prim_deletion_callback_id)
self._prim_deletion_callback_id = None
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_vis_handle:
self._debug_vis_handle.unsubscribe()
self._debug_vis_handle = None
"""
Helper functions.
"""
......
......@@ -211,7 +211,11 @@ class SimulationContext(_SimulationContext):
# you can reproduce the issue by commenting out this line and running the test `test_articulation.py`.
self._gravity_tensor = torch.tensor(self.cfg.gravity, dtype=torch.float32, device=self.cfg.device)
# add a callback to keep rendering when a stop is triggered through different GUI commands like (save as)
# define a global variable to store the exceptions raised in the callback stack
builtins.ISAACLAB_CALLBACK_EXCEPTION = None
# add callback to deal the simulation app when simulation is stopped.
# this is needed because physics views go invalid once we stop the simulation
if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL:
timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream()
self._app_control_on_stop_handle = timeline_event_stream.create_subscription_to_pop_by_type(
......@@ -513,6 +517,11 @@ class SimulationContext(_SimulationContext):
def reset(self, soft: bool = False):
self._disable_app_control_on_stop_handle = True
# check if we need to raise an exception that was raised in a callback
if builtins.ISAACLAB_CALLBACK_EXCEPTION is not None:
exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION
builtins.ISAACLAB_CALLBACK_EXCEPTION = None
raise exception_to_raise
super().reset(soft=soft)
# app.update() may be changing the cuda device in reset, so we force it back to our desired device here
if "cuda" in self.device:
......@@ -537,6 +546,11 @@ class SimulationContext(_SimulationContext):
render: Whether to render the scene after stepping the physics simulation.
If set to False, the scene is not rendered and only the physics simulation is stepped.
"""
# check if we need to raise an exception that was raised in a callback
if builtins.ISAACLAB_CALLBACK_EXCEPTION is not None:
exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION
builtins.ISAACLAB_CALLBACK_EXCEPTION = None
raise exception_to_raise
# check if the simulation timeline is paused. in that case keep stepping until it is playing
if not self.is_playing():
# step the simulator (but not the physics) to have UI still active
......@@ -570,6 +584,11 @@ class SimulationContext(_SimulationContext):
Args:
mode: The rendering mode. Defaults to None, in which case the current rendering mode is used.
"""
# check if we need to raise an exception that was raised in a callback
if builtins.ISAACLAB_CALLBACK_EXCEPTION is not None:
exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION
builtins.ISAACLAB_CALLBACK_EXCEPTION = None
raise exception_to_raise
# check if we need to change the render mode
if mode is not None:
self.set_render_mode(mode)
......@@ -840,3 +859,8 @@ def build_simulation_context(
# Clear the stage
sim.clear_all_callbacks()
sim.clear_instance()
# check if we need to raise an exception that was raised in a callback
if builtins.ISAACLAB_CALLBACK_EXCEPTION is not None:
exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION
builtins.ISAACLAB_CALLBACK_EXCEPTION = None
raise exception_to_raise
......@@ -609,9 +609,8 @@ def test_out_of_range_default_joint_pos(sim, num_articulations, device, add_grou
assert ctypes.c_long.from_address(id(articulation)).value == 1
# Play sim
sim.reset()
# Check if articulation is initialized
assert not articulation.is_initialized
with pytest.raises(ValueError):
sim.reset()
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
......@@ -633,9 +632,8 @@ def test_out_of_range_default_joint_vel(sim, device):
assert ctypes.c_long.from_address(id(articulation)).value == 1
# Play sim
sim.reset()
# Check if articulation is initialized
assert not articulation.is_initialized
with pytest.raises(ValueError):
sim.reset()
@pytest.mark.parametrize("num_articulations", [1, 2])
......@@ -1062,14 +1060,11 @@ def test_setting_velocity_limit_implicit(sim, num_articulations, device, vel_lim
device=device,
)
# Play sim
sim.reset()
if vel_limit_sim is not None and vel_limit is not None:
# Case 1: during initialization, the actuator will raise a ValueError and fail to
# initialize when both these attributes are set.
# note: The Exception is not caught with self.assertRaises or try-except
assert len(articulation.actuators) == 0
with pytest.raises(ValueError):
sim.reset()
return
sim.reset()
# read the values set into the simulation
physx_vel_limit = articulation.root_physx_view.get_dof_max_velocities().to(device)
......@@ -1170,12 +1165,11 @@ def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_li
device=device,
)
# Play sim
sim.reset()
if effort_limit_sim is not None and effort_limit is not None:
# during initialization, the actuator will raise a ValueError and fail to initialize
assert len(articulation.actuators) == 0
with pytest.raises(ValueError):
sim.reset()
return
sim.reset()
# obtain the physx effort limits
physx_effort_limit = articulation.root_physx_view.get_dof_max_forces().to(device=device)
......@@ -1610,9 +1604,8 @@ def test_body_incoming_joint_wrench_b_single_joint(sim, num_articulations, devic
self.assertEqual(ctypes.c_long.from_address(id(articulation)).value, 1)
# Play sim
sim.reset()
# Check if articulation is initialized
self.assertFalse(articulation._is_initialized)
with pytest.raises(RuntimeError):
sim.reset()
if __name__ == "__main__":
......
......@@ -173,10 +173,8 @@ def test_initialization_on_device_cpu():
assert ctypes.c_long.from_address(id(cube_object)).value == 1
# Play sim
sim.reset()
# Check if object is initialized
assert not cube_object.is_initialized
with pytest.raises(RuntimeError):
sim.reset()
@pytest.mark.parametrize("num_cubes", [1, 2])
......@@ -214,10 +212,8 @@ def test_initialization_with_no_deformable_body(sim, num_cubes):
assert ctypes.c_long.from_address(id(cube_object)).value == 1
# Play sim
sim.reset()
# Check if object is initialized
assert not cube_object.is_initialized
with pytest.raises(RuntimeError):
sim.reset()
@pytest.mark.parametrize("num_cubes", [1, 2])
......
......@@ -168,10 +168,8 @@ def test_initialization_with_no_rigid_body(num_cubes, device):
assert ctypes.c_long.from_address(id(cube_object)).value == 1
# Play sim
sim.reset()
# Check if object is initialized
assert not cube_object.is_initialized
with pytest.raises(RuntimeError):
sim.reset()
@pytest.mark.parametrize("num_cubes", [1, 2])
......@@ -187,10 +185,8 @@ def test_initialization_with_articulation_root(num_cubes, device):
assert ctypes.c_long.from_address(id(cube_object)).value == 1
# Play sim
sim.reset()
# Check if object is initialized
assert not cube_object.is_initialized
with pytest.raises(RuntimeError):
sim.reset()
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
......
......@@ -203,10 +203,8 @@ def test_initialization_with_no_rigid_body(sim, num_cubes, device):
assert ctypes.c_long.from_address(id(object_collection)).value == 1
# Play sim
sim.reset()
# Check if object is initialized
assert not object_collection.is_initialized
with pytest.raises(RuntimeError):
sim.reset()
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
......
......@@ -80,7 +80,7 @@ class FrankaTeddyBearLiftEnvCfg(FrankaCubeLiftEnvCfg):
)
# Make the end effector less stiff to not hurt the poor teddy bear
self.scene.robot.actuators["panda_hand"].effort_limit = 50.0
self.scene.robot.actuators["panda_hand"].effort_limit_sim = 50.0
self.scene.robot.actuators["panda_hand"].stiffness = 40.0
self.scene.robot.actuators["panda_hand"].damping = 10.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment