Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"torch": ("https://docs.pytorch.org/docs/2.11/", None),
"isaacsim": ("https://docs.isaacsim.omniverse.nvidia.com/6.0.0/py/", None),
"gymnasium": ("https://gymnasium.farama.org/", None),
"warp": ("https://nvidia.github.io/warp/", None),
"warp": ("https://nvidia.github.io/warp/latest/", None),
"omniverse": ("https://docs.omniverse.nvidia.com/dev-guide/latest", None),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed
^^^^^

* Fixed same-frame OVPhysX articulation link-pose reads after joint-position
writes by requiring ``ovphysx>=0.4.2`` and calling
``PhysX.update_articulations_kinematic()`` before reading link poses.
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ def _create_buffers(self) -> None: # noqa: C901
self._body_com_pose_w = TimestampedBuffer((N, L), dev, wp.transformf)
self._body_com_vel_w = TimestampedBuffer((N, L), dev, wp.spatial_vectorf)
self._body_com_acc_w = TimestampedBuffer((N, L), dev, wp.spatial_vectorf)
self._body_incoming_joint_wrench_buf = TimestampedBuffer((N, L), dev, wp.spatial_vectorf)
# -- Joint state buffers
self._joint_pos_buf = TimestampedBuffer((N, D), dev, wp.float32)
self._joint_vel_buf = TimestampedBuffer((N, D), dev, wp.float32)
Expand Down Expand Up @@ -1747,6 +1748,7 @@ def _pin_proxy_arrays(self) -> None:
self._body_com_vel_w_ta: ProxyArray | None = None
self._body_com_acc_w_ta: ProxyArray | None = None
self._body_com_pose_b_ta: ProxyArray | None = None
self._body_incoming_joint_wrench_b_ta: ProxyArray | None = None
# Body properties
self._body_mass_ta: ProxyArray | None = None
self._body_inertia_ta: ProxyArray | None = None
Expand Down Expand Up @@ -1928,6 +1930,16 @@ def _read_binding_into_buf(self, tensor_type: int, buf: TimestampedBuffer) -> No
self._get_binding(tensor_type).read(view)
buf.timestamp = self._sim_timestamp

def _update_articulations_kinematic(self) -> None:
"""Refresh ovphysx articulation FK before same-frame link-pose reads."""
from isaaclab_ovphysx.physics.ovphysx_manager import OvPhysxManager

physx = OvPhysxManager.get_physx_instance()
if physx is None:
return
update_fk = OvPhysxManager._require_kinematic_fk(physx)
update_fk()

def _read_transform_binding(self, tensor_type: int, buf: TimestampedBuffer) -> None:
"""Read a pose binding (float32 view of transformf buffer), skipping if fresh.

Expand All @@ -1947,6 +1959,8 @@ def _read_transform_binding(self, tensor_type: int, buf: TimestampedBuffer) -> N
view = self._get_read_view(tensor_type, buf.data, 7)
if view is None:
return
if tensor_type == TT.LINK_POSE:
self._update_articulations_kinematic()
self._binding_read(tensor_type, binding, view)
buf.timestamp = self._sim_timestamp

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ def reset(cls, soft: bool = False) -> None:

@classmethod
def forward(cls) -> None:
"""No-op -- ovphysx does not have a fabric/rendering pipeline."""
pass
"""Refresh ovphysx kinematic articulation state for same-frame reads."""
if cls._physx is None:
return
update_fk = cls._require_kinematic_fk(cls._physx)
update_fk()

@classmethod
def step(cls) -> None:
Expand Down Expand Up @@ -187,6 +190,25 @@ def get_physx_instance(cls) -> Any:
# Internal helpers
# ------------------------------------------------------------------

@staticmethod
def _kinematic_fk_requirement_message(version: str | None = None) -> str:
"""Return a user-facing error for wheels without kinematic FK support."""
detected = f" Detected ovphysx {version}." if version else ""
return (
"IsaacLab OVPhysX requires a local ovphysx wheel with "
"PhysX.update_articulations_kinematic() (>=0.4.2). Rebuild ovphysx "
"and install the updated wheel into IsaacLab's ovphysx launcher environment."
f"{detected}"
)

@classmethod
def _require_kinematic_fk(cls, physx: Any, version: str | None = None) -> Any:
"""Return the ovphysx kinematic FK updater, or raise a clear version error."""
update_fk = getattr(physx, "update_articulations_kinematic", None)
if update_fk is None:
raise RuntimeError(cls._kinematic_fk_requirement_message(version))
return update_fk

@classmethod
def _warmup_and_load(cls) -> None:
"""Export the USD stage and load it into the ovphysx runtime.
Expand Down Expand Up @@ -214,9 +236,10 @@ def _warmup_and_load(cls) -> None:
parts = device_str.split(":")
gpu_index = int(parts[1]) if len(parts) > 1 else 0
ovphysx_device = "gpu"
active_cuda_gpus = str(gpu_index)
else:
gpu_index = 0
ovphysx_device = "cpu"
active_cuda_gpus = None

if cls._locked_device is not None and ovphysx_device != cls._locked_device:
raise RuntimeError(
Expand All @@ -237,7 +260,7 @@ def _warmup_and_load(cls) -> None:
logger.info("OvPhysxManager: exported USD stage to %s", stage_file)

if cls._physx is None:
cls._construct_physx(ovphysx_device, gpu_index)
cls._construct_physx(ovphysx_device, active_cuda_gpus)
cls._locked_device = ovphysx_device
else:
# Reuse path: the cached PhysX may still hold the prior stage (the
Expand Down Expand Up @@ -288,7 +311,7 @@ def _warmup_and_load(cls) -> None:
cls._warmup_done = True

@classmethod
def _construct_physx(cls, ovphysx_device: str, gpu_index: int) -> None:
def _construct_physx(cls, ovphysx_device: str, active_cuda_gpus: str | None) -> None:
"""Bootstrap the ``ovphysx`` wheel and create the :class:`ovphysx.PhysX` instance.

Runs once per process. Configures worker threads, registers the
Expand All @@ -314,7 +337,14 @@ def _construct_physx(cls, ovphysx_device: str, gpu_index: int) -> None:

import ovphysx

cls._physx = ovphysx.PhysX(device=ovphysx_device, gpu_index=gpu_index)
ovphysx_version = getattr(ovphysx, "__version__", None)
try:
cls._physx = ovphysx.PhysX(device=ovphysx_device, active_cuda_gpus=active_cuda_gpus)
except TypeError as exc:
if "active_cuda_gpus" in str(exc):
raise RuntimeError(cls._kinematic_fk_requirement_message(ovphysx_version)) from exc
raise
cls._require_kinematic_fk(cls._physx, ovphysx_version)

# Without worker threads the stepper runs simulate()+fetchResults()
# synchronously, blocking the calling thread for the full GPU step time.
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab_ovphysx/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
INSTALL_REQUIRES: list[str] = []

EXTRAS_REQUIRE = {
"ovphysx": ["ovphysx"],
"ovphysx": ["ovphysx>=0.4.2"],
}

setup(
Expand Down
15 changes: 0 additions & 15 deletions source/isaaclab_ovphysx/test/assets/test_articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,6 @@
"docs/superpowers/specs/2026-04-28-ovphysx-wheel-gaps-for-marco.md."
)

_FK_ON_DEMAND_GAP_REASON = (
"GPU-only: PhysX's data-class getters call "
"``SimulationView.update_articulations_kinematic`` before reading link "
"transforms (see ``isaaclab_physx.assets.articulation_data:735``), so body "
"poses reflect new joint positions immediately after "
"``write_joint_position_to_sim_*`` without a sim step. The OVPhysX wheel's "
"``ovphysx.PhysX`` class does not expose an equivalent FK-on-demand API "
"(omni.physics.tensors has it; the OVPhysX wrapper does not surface it), "
"so body-pose bindings remain stale on a GPU sim until the next ``step``. "
"On a CPU sim the same write path happens to update the bindings synchronously "
"(no async stream involved), so the test xpasses there — hence ``strict=False``. "
"See docs/superpowers/specs/2026-04-28-ovphysx-wheel-gaps-for-marco.md."
)


def _read_binding_to_torch(articulation: Articulation, tensor_type: int, device: str | torch.device) -> torch.Tensor:
"""Read an OVPhysX TensorBinding into a torch tensor on *device*.
Expand Down Expand Up @@ -2154,7 +2140,6 @@ def test_setting_invalid_articulation_root_prim_path(sim, device):
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
@pytest.mark.parametrize("gravity_enabled", [False])
@pytest.mark.isaacsim_ci
@pytest.mark.xfail(reason=_FK_ON_DEMAND_GAP_REASON, strict=False)
def test_write_joint_state_data_consistency(sim, num_articulations, device, gravity_enabled):
"""Test the setters for root_state using both the link frame and center of mass as reference frame.

Expand Down
16 changes: 14 additions & 2 deletions source/isaaclab_ovphysx/test/assets/test_articulation_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,20 @@ class TestArticulationData:
def test_joint_acc_uses_inverse_dt(self):
"""Finite-difference joint acceleration should divide by ``dt``."""
mock_bindings = MockOvPhysxBindingSet(num_instances=1, num_joints=2, num_bodies=1)
data = ArticulationData(mock_bindings.bindings, device="cpu")
data._create_buffers()
data = ArticulationData(
mock_bindings.bindings,
device="cpu",
num_instances=1,
num_bodies=1,
num_joints=2,
num_fixed_tendons=0,
num_spatial_tendons=0,
body_names=["body_0"],
joint_names=["joint_0", "joint_1"],
fixed_tendon_names=[],
spatial_tendon_names=[],
)
data.is_primed = True

mock_bindings.bindings[TT.DOF_VELOCITY]._data[...] = np.array([[1.0, -2.0]], dtype=np.float32)

Expand Down
Loading