Commit f2bc4bf2 authored by ooctipus's avatar ooctipus Committed by Kelly Guo

Fixes isaaclab.scene.reset_to to properly accept None as valid argument (#2970)

As reported by issue, isaaclab.scene.reset_to currently does not run
well with env_id = None, even though None is one of its default values.

This PR make sure it supports and and added tests that tensor input and
None should both work well

Fixes #2878

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

<!-- As you go through the list, delete the ones that are not
applicable. -->

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

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] 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

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->
parent 5bcf0e71
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.44.5"
version = "0.44.6"
# Description
title = "Isaac Lab framework for Robot Learning"
......
Changelog
---------
0.44.5 (2025-07-28)
0.44.6 (2025-07-28)
~~~~~~~~~~~~~~~~~~~
Changed
......@@ -9,7 +9,18 @@ Changed
* Tweak default behavior for rendering preset modes.
0.44.4 (2025-07-18)
0.44.5 (2025-07-28)
~~~~~~~~~~~~~~~~~~~
Fixed
^^^^^
* Fixed :meth:`isaaclab.scene.reset_to` to properly accept None as valid argument.
* Added tests to verify that argument types.
0.44.4 (2025-07-22)
~~~~~~~~~~~~~~~~~~~
Added
......@@ -91,7 +102,7 @@ Added
Added
^^^^^
* Added :attr:`omni.isaac.lab.sensors.ContactSensorData.force_matrix_w_history` that tracks the history of the filtered contact forces in the world frame.
* Added :attr:`~isaaclab.sensors.ContactSensorData.force_matrix_w_history` that tracks the history of the filtered contact forces in the world frame.
0.42.24 (2025-06-25)
......
......@@ -138,7 +138,8 @@ class InteractiveScene:
self.env_prim_paths = self.cloner.generate_paths(f"{self.env_ns}/env", self.cfg.num_envs)
# create source prim
self.stage.DefinePrim(self.env_prim_paths[0], "Xform")
# allocate env indices
self._ALL_INDICES = torch.arange(self.cfg.num_envs, dtype=torch.long, device=self.device)
# when replicate_physics=False, we assume heterogeneous environments and clone the xforms first.
# this triggers per-object level cloning in the spawner.
if not self.cfg.replicate_physics:
......@@ -515,7 +516,7 @@ class InteractiveScene:
"""
# resolve env_ids
if env_ids is None:
env_ids = slice(None)
env_ids = self._ALL_INDICES
# articulations
for asset_name, articulation in self._articulations.items():
asset_state = state["articulation"][asset_name]
......
......@@ -12,6 +12,8 @@ simulation_app = AppLauncher(headless=True).app
"""Rest everything follows."""
import torch
import pytest
import isaaclab.sim as sim_utils
......@@ -20,7 +22,6 @@ from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg
from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
from isaaclab.sensors import ContactSensorCfg
from isaaclab.sim import build_simulation_context
from isaaclab.terrains import TerrainImporterCfg
from isaaclab.utils import configclass
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
......@@ -29,15 +30,9 @@ from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
class MySceneCfg(InteractiveSceneCfg):
"""Example scene configuration."""
# terrain - flat terrain plane
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="plane",
)
# articulation
robot = ArticulationCfg(
prim_path="/World/Robot",
prim_path="/World/envs/env_.*/Robot",
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/SimpleArticulation/revolute_articulation.usd"
),
......@@ -47,7 +42,7 @@ class MySceneCfg(InteractiveSceneCfg):
)
# rigid object
rigid_obj = RigidObjectCfg(
prim_path="/World/RigidObj",
prim_path="/World/envs/env_.*/RigidObj",
spawn=sim_utils.CuboidCfg(
size=(0.5, 0.5, 0.5),
rigid_props=sim_utils.RigidBodyPropertiesCfg(
......@@ -59,23 +54,23 @@ class MySceneCfg(InteractiveSceneCfg):
),
)
# sensor
sensor = ContactSensorCfg(
prim_path="/World/Robot",
)
# extras - light
light = AssetBaseCfg(
prim_path="/World/light",
spawn=sim_utils.DistantLightCfg(),
)
@pytest.fixture
def setup_scene(request):
"""Create simulation context with the specified device."""
device = request.getfixturevalue("device")
with build_simulation_context(device=device, auto_add_lighting=True, add_ground_plane=True) as sim:
sim._app_control_on_stop_handle = None
def make_scene(num_envs: int, env_spacing: float = 1.0):
scene_cfg = MySceneCfg(num_envs=num_envs, env_spacing=env_spacing)
return scene_cfg
@pytest.fixture(scope="module")
def setup_scene():
"""Fixture to set up scene parameters."""
sim_dt = 0.001
scene_cfg = MySceneCfg(num_envs=1, env_spacing=1)
return sim_dt, scene_cfg
yield make_scene, sim
sim.stop()
sim.clear()
sim.clear_all_callbacks()
sim.clear_instance()
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
......@@ -86,11 +81,24 @@ def test_scene_entity_isolation(device, setup_scene):
The scene at index 0 of the list will have all of its entities cleared manually, and
the test compares that the data held in the scene at index 1 remained intact.
"""
sim_dt, scene_cfg = setup_scene
make_scene, sim = setup_scene
scene_cfg = make_scene(num_envs=1)
# set additional light to test 'extras' attribute of the scene
setattr(
scene_cfg,
"light",
AssetBaseCfg(
prim_path="/World/light",
spawn=sim_utils.DistantLightCfg(),
),
)
# set additional sensor to test 'sensors' attribute of the scene
setattr(scene_cfg, "sensor", ContactSensorCfg(prim_path="/World/envs/env_.*/Robot"))
scene_list = []
# create two InteractiveScene instances
for _ in range(2):
with build_simulation_context(device=device, dt=sim_dt) as _:
with build_simulation_context(device=device, dt=sim.get_physics_dt()) as _:
scene = InteractiveScene(scene_cfg)
scene_list.append(scene)
scene_0 = scene_list[0]
......@@ -109,3 +117,101 @@ def test_scene_entity_isolation(device, setup_scene):
assert scene_0.sensors != scene_1.sensors
assert scene_0.extras == dict()
assert scene_0.extras != scene_1.extras
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_relative_flag(device, setup_scene):
make_scene, sim = setup_scene
scene_cfg = make_scene(num_envs=4)
scene = InteractiveScene(scene_cfg)
sim.reset()
# test relative == False produces different result than relative == True
assert_state_different(scene.get_state(is_relative=False), scene.get_state(is_relative=True))
# test is relative == False
prev_state = scene.get_state(is_relative=False)
scene["robot"].write_joint_state_to_sim(
position=torch.rand_like(scene["robot"].data.joint_pos), velocity=torch.rand_like(scene["robot"].data.joint_pos)
)
next_state = scene.get_state(is_relative=False)
assert_state_different(prev_state, next_state)
scene.reset_to(prev_state, is_relative=False)
assert_state_equal(prev_state, scene.get_state(is_relative=False))
# test is relative == True
prev_state = scene.get_state(is_relative=True)
scene["robot"].write_joint_state_to_sim(
position=torch.rand_like(scene["robot"].data.joint_pos), velocity=torch.rand_like(scene["robot"].data.joint_pos)
)
next_state = scene.get_state(is_relative=True)
assert_state_different(prev_state, next_state)
scene.reset_to(prev_state, is_relative=True)
assert_state_equal(prev_state, scene.get_state(is_relative=True))
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_reset_to_env_ids_input_types(device, setup_scene):
make_scene, sim = setup_scene
scene_cfg = make_scene(num_envs=4)
scene = InteractiveScene(scene_cfg)
sim.reset()
# test env_ids = None
prev_state = scene.get_state()
scene["robot"].write_joint_state_to_sim(
position=torch.rand_like(scene["robot"].data.joint_pos), velocity=torch.rand_like(scene["robot"].data.joint_pos)
)
scene.reset_to(prev_state, env_ids=None)
assert_state_equal(prev_state, scene.get_state())
# test env_ids = torch tensor
scene["robot"].write_joint_state_to_sim(
position=torch.rand_like(scene["robot"].data.joint_pos), velocity=torch.rand_like(scene["robot"].data.joint_pos)
)
scene.reset_to(prev_state, env_ids=torch.arange(scene.num_envs, device=scene.device))
assert_state_equal(prev_state, scene.get_state())
def assert_state_equal(s1: dict, s2: dict, path=""):
"""
Recursively assert that s1 and s2 have the same nested keys
and that every tensor leaf is exactly equal.
"""
assert set(s1.keys()) == set(s2.keys()), f"Key mismatch at {path}: {s1.keys()} vs {s2.keys()}"
for k in s1:
v1, v2 = s1[k], s2[k]
subpath = f"{path}.{k}" if path else k
if isinstance(v1, dict):
assert isinstance(v2, dict), f"Type mismatch at {subpath}"
assert_state_equal(v1, v2, path=subpath)
else:
# leaf: should be a torch.Tensor
assert isinstance(v1, torch.Tensor) and isinstance(v2, torch.Tensor), f"Expected tensors at {subpath}"
if not torch.equal(v1, v2):
diff = (v1 - v2).abs().max()
pytest.fail(f"Tensor mismatch at {subpath}, max abs diff = {diff}")
def assert_state_different(s1: dict, s2: dict, path=""):
"""
Recursively scan s1 and s2 (which must have identical keys) and
succeed as soon as you find one tensor leaf that differs.
If you reach the end with everything equal, fail the test.
"""
assert set(s1.keys()) == set(s2.keys()), f"Key mismatch at {path}: {s1.keys()} vs {s2.keys()}"
for k in s1:
v1, v2 = s1[k], s2[k]
subpath = f"{path}.{k}" if path else k
if isinstance(v1, dict):
# recurse; if any nested call returns (i.e. finds a diff), we propagate success
try:
assert_state_different(v1, v2, path=subpath)
return
except AssertionError:
continue
else:
assert isinstance(v1, torch.Tensor) and isinstance(v2, torch.Tensor), f"Expected tensors at {subpath}"
if not torch.equal(v1, v2):
return # found a difference → success
pytest.fail(f"No differing tensor found in nested state at {path}")
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