Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed
^^^^^

* Fixed OVPhysX actuator joint indices to follow the common actuator indexing contract.
* Fixed OVPhysX initialization alongside Kit by reusing Kit's registered PhysX schema provider.
* Fixed the OVPhysX manager to support both the declared public runtime API and the current runtime API.
Original file line number Diff line number Diff line change
Expand Up @@ -3976,6 +3976,10 @@ def _process_actuators_cfg(self) -> None:
if not joint_ids:
logger.warning("Actuator '%s': no joints matched '%s'", name, act_cfg.joint_names_expr)
continue
if len(joint_names) == self.num_joints:
actuator_joint_ids = slice(None)
else:
actuator_joint_ids = torch.tensor(joint_ids, device=self.device, dtype=torch.int32)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 torch.int32 index tensor may not be accepted by all PyTorch advanced-indexing paths

actuator_joint_ids is created with dtype=torch.int32 and is immediately used to index several .torch tensors at lines 3943–3950. Standard PyTorch advanced indexing expects int64 (LongTensor); older bundled PyTorch versions raise RuntimeError: expected scalar type Long but found Int for int32 index tensors. Using dtype=torch.int64 (or inserting .long() casts at the indexing sites) would eliminate this version dependency.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

act_cfg_copy = act_cfg.copy()
# seed the actuator with the simulation's already-correct DOF defaults
# (USD-authored ``physxJoint:maxJointVelocity`` etc. parsed at scene-load).
Expand All @@ -3985,17 +3989,17 @@ def _process_actuators_cfg(self) -> None:
act = act_cfg_copy.class_type(
act_cfg_copy,
joint_names=joint_names,
joint_ids=joint_ids,
joint_ids=actuator_joint_ids,
num_envs=self._num_instances,
device=self._device,
stiffness=self._data.joint_stiffness.torch[:, joint_ids],
damping=self._data.joint_damping.torch[:, joint_ids],
armature=self._data.joint_armature.torch[:, joint_ids],
friction=self._data.joint_friction_coeff.torch[:, joint_ids],
dynamic_friction=self._data.joint_dynamic_friction_coeff.torch[:, joint_ids],
viscous_friction=self._data.joint_viscous_friction_coeff.torch[:, joint_ids],
effort_limit=self._data.joint_effort_limits.torch[:, joint_ids].clone(),
velocity_limit=self._data.joint_vel_limits.torch[:, joint_ids],
stiffness=self._data.joint_stiffness.torch[:, actuator_joint_ids],
damping=self._data.joint_damping.torch[:, actuator_joint_ids],
armature=self._data.joint_armature.torch[:, actuator_joint_ids],
friction=self._data.joint_friction_coeff.torch[:, actuator_joint_ids],
dynamic_friction=self._data.joint_dynamic_friction_coeff.torch[:, actuator_joint_ids],
viscous_friction=self._data.joint_viscous_friction_coeff.torch[:, actuator_joint_ids],
effort_limit=self._data.joint_effort_limits.torch[:, actuator_joint_ids].clone(),
velocity_limit=self._data.joint_vel_limits.torch[:, actuator_joint_ids],
)
self.actuators[name] = act
self._joint_ids_per_actuator[name] = joint_ids
Expand Down Expand Up @@ -4030,10 +4034,7 @@ def _apply_actuator_model(self) -> None:
from isaaclab.utils.types import ArticulationActions

for name, act in self.actuators.items():
jids = act.joint_indices
if jids is None:
continue
jids_t = jids if isinstance(jids, list) else list(jids)
jids_t = self._joint_ids_per_actuator[name]
all_joints = len(jids_t) == self._num_joints

# Warp -> torch (zero-copy on same device via DLPack).
Expand Down
130 changes: 97 additions & 33 deletions source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import annotations

import atexit
import inspect
import logging
import os
import re
Expand Down Expand Up @@ -272,8 +273,10 @@ def _ensure_physx_schemas_registered(cls) -> None:
runs it must be registered manually before the wheel can match
``PhysxContactReportAPI`` and friends on the stage. The wheel
bundles the plugin under ``ovphysx/plugins/usd/PhysxSchema``. This
method is idempotent — :meth:`pxr.Plug.Registry.RegisterPlugins`
is a no-op once the plugin is registered.
method is idempotent and leaves an existing Kit ``physxSchema``
provider authoritative. Registering the wheel's provider after Kit's
provider raises duplicate-type errors even though both plugins share
the same name.
"""
if cls._physx_schemas_registered:
return
Expand All @@ -285,11 +288,15 @@ def _ensure_physx_schemas_registered(cls) -> None:
from pxr import Plug # noqa: PLC0415
except Exception:
return
registry = Plug.Registry()
if any(plugin.name == "physxSchema" for plugin in registry.GetAllPlugins()):
cls._physx_schemas_registered = True
return
plugin_root = os.path.join(os.path.dirname(ovphysx.__file__), "plugins", "usd")
for sub in ("PhysxSchema/resources", "PhysxSchemaAddition/resources"):
path = os.path.join(plugin_root, sub)
if os.path.isdir(path):
Plug.Registry().RegisterPlugins(path)
registry.RegisterPlugins(path)
cls._physx_schemas_registered = True

@classmethod
Expand Down Expand Up @@ -350,10 +357,25 @@ def step(cls) -> None:
if cls._physx is None:
return
dt = cls.get_physics_dt()
cls._physx.step_sync(dt=dt)
cls._step_physx(cls._physx, dt=dt, sim_time=PhysicsManager._sim_time)
cls._physx.update_articulations_kinematic()
PhysicsManager._sim_time += dt

@staticmethod
def _step_physx(physx: Any, dt: float, sim_time: float) -> None:
"""Step either the declared legacy runtime or the trusted current runtime."""
if hasattr(physx, "reset_stage"):
physx.step_sync(dt=dt)
else:
physx.step_sync(dt=dt, sim_time=sim_time)

@staticmethod
def _reset_physx_stage(physx: Any) -> None:
"""Clear the loaded stage through the runtime's available reset API."""
reset = physx.reset_stage if hasattr(physx, "reset_stage") else physx.reset
operation = reset()
physx.wait_op(operation)

@classmethod
def close(cls) -> None:
"""Release ovphysx resources and clean up."""
Expand All @@ -378,7 +400,7 @@ def close(cls) -> None:
def _release_physx(cls) -> None:
"""Soft-reset the ovphysx runtime stage; keep the C++ instance alive.

Calls ``physx.reset_stage()`` to clear the loaded scene, but does **not**
Clears the loaded scene through the runtime's reset API, but does **not**
drop the Python reference. The cached :class:`ovphysx.PhysX` is reused by
the next :class:`~isaaclab.sim.SimulationContext` via the reuse path in
:meth:`_warmup_and_load`. Safe to call multiple times.
Expand All @@ -395,8 +417,7 @@ def _release_physx(cls) -> None:
namespace-isolated Carbonite (different soname / hidden visibility).
"""
if cls._physx is not None:
op = cls._physx.reset_stage()
cls._physx.wait_op(op)
cls._reset_physx_stage(cls._physx)

@classmethod
def get_physx_instance(cls) -> Any:
Expand Down Expand Up @@ -583,13 +604,12 @@ def _warmup_and_load(cls) -> None:
cls._locked_device = ovphysx_device
else:
# Reuse path: the cached PhysX may still hold the prior stage (the
# wheel allows only one loaded USD at a time). ``physx.reset_stage()``
# is idempotent on an already-cleared stage and required when this is
# wheel allows only one loaded USD at a time). Clearing the stage is
# idempotent on an already-cleared stage and required when this is
# a second :meth:`_warmup_and_load` within the same SimulationContext
# (e.g. when a caller manually clears ``_warmup_done`` to force a
# re-warmup).
op = cls._physx.reset_stage()
cls._physx.wait_op(op)
cls._reset_physx_stage(cls._physx)

usd_handle, op_idx = cls._physx.add_usd(stage_file)
cls._physx.wait_op(op_idx)
Expand Down Expand Up @@ -661,28 +681,7 @@ def _construct_physx(cls, ovphysx_device: str, gpu_index: int) -> None:
_sys.modules.update(_hidden_pxr)

ovphysx = import_ovphysx()
ovphysx.PhysX.set_cpu_mode(ovphysx_device == "cpu")

carbonite_overrides = {
"/physics/physxDispatcher": True,
"/physics/updateToUsd": False,
"/physics/updateVelocitiesToUsd": False,
"/physics/updateParticlesToUsd": False,
}
if ovphysx_device == "gpu":
carbonite_overrides.update(
{
"/physics/suppressReadback": True,
"/physics/suppressFabricUpdate": True,
}
)
physx_kwargs = {
"config": ovphysx.PhysXConfig(num_threads=8, carbonite_overrides=carbonite_overrides),
}
if ovphysx_device == "gpu":
physx_kwargs["active_cuda_gpus"] = str(gpu_index)

cls._physx = ovphysx.PhysX(**physx_kwargs)
cls._physx = cls._create_physx_instance(ovphysx, ovphysx_device, gpu_index)

# FIXME(malesiani): re-evaluate this when carbonite ships an isolated copy.
# At process exit, two Carbonite instances are in memory:
Expand Down Expand Up @@ -717,6 +716,71 @@ def _atexit_release_and_exit():
atexit.register(_atexit_release_and_exit)
cls._atexit_registered = True

@staticmethod
def _create_physx_instance(ovphysx: Any, ovphysx_device: str, gpu_index: int) -> Any:
"""Create a PhysX instance for the declared or current OVPhysX runtime API.

Args:
ovphysx: Imported OVPhysX runtime module.
ovphysx_device: Physics device, either ``"cpu"`` or ``"gpu"``.
gpu_index: CUDA device ordinal selected for GPU physics.

Returns:
The configured ``ovphysx.PhysX`` instance.
"""

carbonite_overrides = {
"/physics/physxDispatcher": True,
"/physics/updateToUsd": False,
"/physics/updateVelocitiesToUsd": False,
"/physics/updateParticlesToUsd": False,
}
if ovphysx_device == "gpu":
carbonite_overrides.update(
{
"/physics/suppressReadback": True,
"/physics/suppressFabricUpdate": True,
}
)
if hasattr(ovphysx.PhysX, "set_cpu_mode"):
ovphysx.PhysX.set_cpu_mode(ovphysx_device == "cpu")
physx_kwargs = {
"config": ovphysx.PhysXConfig(num_threads=8, carbonite_overrides=carbonite_overrides),
}
if ovphysx_device == "gpu":
physx_kwargs["active_cuda_gpus"] = str(gpu_index)
return ovphysx.PhysX(**physx_kwargs)

physx_kwargs = {"device": ovphysx_device}
try:
physx_parameters = inspect.signature(ovphysx.PhysX).parameters
except (TypeError, ValueError):
# C-extension constructors may not expose a Python-visible signature
physx_parameters = {}
if "active_cuda_gpus" in physx_parameters and ovphysx_device == "gpu":
physx_kwargs["active_cuda_gpus"] = str(gpu_index)
physx_kwargs["config"] = ovphysx.PhysXConfig(
carbonite_overrides={
"/physics/suppressReadback": True,
"/physics/suppressFabricUpdate": True,
}
)
elif "gpu_index" in physx_parameters:
physx_kwargs["gpu_index"] = gpu_index

physx = ovphysx.PhysX(**physx_kwargs)
if hasattr(physx, "set_setting"):
physx.set_setting("/persistent/physics/numThreads", "8")
physx.set_setting("/physics/physxDispatcher", "true")
physx.set_setting("/physics/updateToUsd", "false")
physx.set_setting("/physics/updateVelocitiesToUsd", "false")
physx.set_setting("/physics/updateParticlesToUsd", "false")
else:
# the declared legacy runtime exposes no generic settings API, so
# only the thread count can be applied post-construction
physx.set_config_int32(ovphysx.ConfigInt32.NUM_THREADS, 8)
Comment on lines +772 to +781

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing carbonite overrides in legacy path when set_setting is unavailable

The else branch only applies NUM_THREADS via set_config_int32, silently skipping physxDispatcher, updateToUsd, updateVelocitiesToUsd, and updateParticlesToUsd. The PhysXConfig passed to the constructor (lines 746–751) only contains suppressReadback and suppressFabricUpdate, so these four settings are never applied.

If the declared legacy wheel defaults updateToUsd to True, physics state would be written back to the USD stage on every step — causing severe performance degradation and potentially inconsistent state. The original code (before this PR) applied all four overrides unconditionally via carbonite_overrides. The new test for the legacy API (test_manager_supports_declared_legacy_runtime_api) only asserts that set_config_int32(NUM_THREADS, 8) is called, leaving this gap unverified.

return physx

@staticmethod
def _configure_physx_scene_prim(scene_prim, cfg, device: str) -> None:
"""Apply PhysxSceneAPI schema and device-specific scene attributes to the
Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab_ovphysx/test/assets/test_articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def test_initialization_floating_base_non_root(sim, num_articulations, device, a
for actuator_name, actuator in articulation.actuators.items():
is_implicit_model_cfg = isinstance(articulation_cfg.actuators[actuator_name], ImplicitActuatorCfg)
assert actuator.is_implicit_model == is_implicit_model_cfg
assert actuator.joint_indices == slice(None)

# Simulate physics
for _ in range(10):
Expand Down Expand Up @@ -509,6 +510,9 @@ def test_initialization_fixed_base(sim, num_articulations, device):
for actuator_name, actuator in articulation.actuators.items():
is_implicit_model_cfg = isinstance(articulation_cfg.actuators[actuator_name], ImplicitActuatorCfg)
assert actuator.is_implicit_model == is_implicit_model_cfg
assert isinstance(actuator.joint_indices, torch.Tensor)
assert actuator.joint_indices.dtype == torch.int32
assert actuator.joint_indices.device == torch.device(device)

# Simulate physics
for _ in range(10):
Expand Down
Loading
Loading