diff --git a/scripts/demos/pick_and_place.py b/scripts/demos/pick_and_place.py index e23ee1a2450c..7d7cb8ed9710 100644 --- a/scripts/demos/pick_and_place.py +++ b/scripts/demos/pick_and_place.py @@ -230,8 +230,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=["/World/ground"]) # add articulation to scene self.scene.articulations["pick_and_place"] = self.pick_and_place diff --git a/scripts/tutorials/06_deploy/anymal_c_env.py b/scripts/tutorials/06_deploy/anymal_c_env.py index c3999cb5b8b5..b7775617f01b 100644 --- a/scripts/tutorials/06_deploy/anymal_c_env.py +++ b/scripts/tutorials/06_deploy/anymal_c_env.py @@ -68,8 +68,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) light_cfg.func("/World/Light", light_cfg) diff --git a/source/isaaclab_tasks/changelog.d/limit-direct-physx-filtering.rst b/source/isaaclab_tasks/changelog.d/limit-direct-physx-filtering.rst new file mode 100644 index 000000000000..2ce3b2fda94b --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/limit-direct-physx-filtering.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Limited explicit collision filtering in manually cloned direct environments to Isaac Sim PhysX and + CPU OvPhysX so GPU OvPhysX uses its native environment-ID isolation. diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py index 5d25326c2923..a3f392eee2b4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py @@ -77,8 +77,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) # add lights light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py index e85b6835292f..a9d379f71a06 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env.py @@ -103,8 +103,10 @@ def _setup_scene(self): plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions() self.scene.articulations["robot"] = self._robot diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env.py index fccda95dc46a..b545be718946 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env.py @@ -69,8 +69,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=["/World/ground"]) # add articulation to scene diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py index 0585fa8b3c85..a327e39edbf6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py @@ -56,8 +56,10 @@ def _setup_scene(self): plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[]) # add articulation and sensors to scene diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py index 2378183bcf21..579e67d7e474 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py @@ -43,8 +43,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[]) # add articulation to scene diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py index ce78692d54fa..f3e274103ec9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py @@ -96,8 +96,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=["/World/ground"]) # add articulation to scene - we must register to scene to randomize with EventManager self.scene.articulations["right_robot"] = self.right_hand diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py index 2416a95df9ab..f308468588fb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py @@ -69,8 +69,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) # add articulation and the feet wrench sensor to scene self.scene.articulations["robot"] = self.robot diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env.py index f0e97fded0c9..cd64a48809b5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_env.py @@ -47,8 +47,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[]) # add articulation to scene self.scene.articulations["robot"] = self.robot diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index 025a5c692b3b..c4b83a175c63 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py @@ -57,8 +57,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=[]) # add articulation to scene - we must register to scene to randomize with EventManager self.scene.articulations["robot"] = self.hand diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py index c2c47e66491b..520029ee8ec1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py @@ -106,8 +106,10 @@ def _setup_scene(self): pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos) cloner.replicate(plan, stage=self.scene.stage) - # PhysX replication requires explicit collision filtering between environments. - if "physx" in self.scene.physics_backend: + # Isaac Sim PhysX always needs USD filtering; OvPhysX needs it on CPU. + if self.scene.physics_backend == "physxmanager" or ( + self.scene.physics_backend == "ovphysxmanager" and self.device == "cpu" + ): self.scene.filter_collisions(global_prim_paths=["/World/ground"]) # add articulation to scene - we must register to scene to randomize with EventManager self.scene.articulations["robot"] = self.hand diff --git a/source/isaaclab_tasks/test/core/test_cartpole_direct_physx_actuation.py b/source/isaaclab_tasks/test/core/test_cartpole_direct_physx_actuation.py new file mode 100644 index 000000000000..429e74fee99b --- /dev/null +++ b/source/isaaclab_tasks/test/core/test_cartpole_direct_physx_actuation.py @@ -0,0 +1,108 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test direct-workflow Cartpole isolation and CUDA actuation with both PhysX backends.""" + +from __future__ import annotations + +import argparse +import importlib.util +import subprocess +import sys + +import pytest + + +def _run_cartpole_diagnostic(physics_backend: str, device: str) -> None: + """Run the Cartpole diagnostic in an isolated simulator process.""" + from isaaclab.app import launch_simulation + + from isaaclab_tasks.core.cartpole.cartpole_direct_env_cfg import CartpoleEnvCfg + from isaaclab_tasks.utils.hydra import resolve_presets + + cfg = resolve_presets(CartpoleEnvCfg(), (physics_backend,)) + cfg.scene.num_envs = 4 + cfg.sim.device = device + cfg.seed = 0 + cfg.initial_cart_position_range = (0.0, 0.0) + cfg.initial_cart_velocity_range = (0.0, 0.0) + cfg.initial_pole_angle_range = (0.0, 0.0) + cfg.initial_pole_velocity_range = (0.0, 0.0) + + launcher_args = { + "device": device, + "headless": True, + "visualizer": None, + "visualizer_explicit": True, + } + with launch_simulation(cfg, launcher_args): + import gymnasium as gym + import torch + + import isaaclab_tasks # noqa: F401, PLC0415 + + env = gym.make("Isaac-Cartpole-Direct", cfg=cfg) + try: + has_collision_groups = bool(env.unwrapped.scene.stage.GetPrimAtPath("/World/collisions")) + expected_collision_groups = physics_backend == "isaacsim_physx" or device == "cpu" + assert has_collision_groups == expected_collision_groups, ( + f"Unexpected collision-group state for {physics_backend} on {device}: {has_collision_groups}." + ) + + if device == "cuda:0": + env.reset() + actions = torch.ones(env.action_space.shape, device=env.unwrapped.device) + with torch.inference_mode(): + for _ in range(20): + env.step(actions) + + cart_idx = env.unwrapped._cart_dof_idx[0] + cart_vel = env.unwrapped.cartpole.data.joint_vel.torch[:, cart_idx] + assert torch.all(cart_vel > 1.0), f"Expected every cart to accelerate, got {cart_vel.tolist()}." + torch.testing.assert_close(cart_vel, cart_vel[0].expand_as(cart_vel)) + print(f"CARTPOLE_DIAGNOSTIC_PASSED {physics_backend} {device}") + finally: + env.close() + + +@pytest.mark.isaacsim_ci +@pytest.mark.integration +@pytest.mark.parametrize( + "physics_backend,device", + ( + pytest.param("isaacsim_physx", "cuda:0", id="isaacsim-physx-cuda"), + pytest.param("isaacsim_physx", "cpu", id="isaacsim-physx-cpu"), + pytest.param("ovphysx", "cuda:0", id="ovphysx-cuda"), + pytest.param("ovphysx", "cpu", id="ovphysx-cpu"), + ), +) +def test_cartpole_direct_physx_isolation_matches_backend_device(physics_backend: str, device: str): + """Verify backend-specific collision isolation and consistent CUDA actuation.""" + if physics_backend == "isaacsim_physx": + from isaaclab.app import AppLauncher + + if not AppLauncher.is_available(): + pytest.skip("Isaac Sim is not installed") + if physics_backend == "ovphysx" and importlib.util.find_spec("ovphysx") is None: + pytest.skip("OVPhysX is not installed") + + result = subprocess.run( + [sys.executable, __file__, "--physics_backend", physics_backend, "--device", device], + capture_output=True, + text=True, + timeout=180, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"Cartpole diagnostic failed:\n{output}" + assert "Traceback (most recent call last):" not in output, f"Cartpole diagnostic failed:\n{output}" + assert f"CARTPOLE_DIAGNOSTIC_PASSED {physics_backend} {device}" in output, f"Cartpole diagnostic failed:\n{output}" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--physics_backend", choices=("isaacsim_physx", "ovphysx"), required=True) + parser.add_argument("--device", choices=("cpu", "cuda:0"), required=True) + args = parser.parse_args() + _run_cartpole_diagnostic(args.physics_backend, args.device)