diff --git a/docs/conf.py b/docs/conf.py index 792ee6eeecb..ee9e2bfe429 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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), } diff --git a/source/isaaclab_ovphysx/changelog.d/marcodiiga-ovphysx-kinematic-fk.rst b/source/isaaclab_ovphysx/changelog.d/marcodiiga-ovphysx-kinematic-fk.rst new file mode 100644 index 00000000000..8de50d4b158 --- /dev/null +++ b/source/isaaclab_ovphysx/changelog.d/marcodiiga-ovphysx-kinematic-fk.rst @@ -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. diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py index 3ada3b36e63..36fa68871d3 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py @@ -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) @@ -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 @@ -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. @@ -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 diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py index 1b4e8cd6e25..7b94421057c 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py @@ -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: @@ -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. @@ -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( @@ -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 @@ -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 @@ -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. diff --git a/source/isaaclab_ovphysx/setup.py b/source/isaaclab_ovphysx/setup.py index 4898806285d..6ff12076bf0 100644 --- a/source/isaaclab_ovphysx/setup.py +++ b/source/isaaclab_ovphysx/setup.py @@ -16,7 +16,7 @@ INSTALL_REQUIRES: list[str] = [] EXTRAS_REQUIRE = { - "ovphysx": ["ovphysx"], + "ovphysx": ["ovphysx>=0.4.2"], } setup( diff --git a/source/isaaclab_ovphysx/test/assets/test_articulation.py b/source/isaaclab_ovphysx/test/assets/test_articulation.py index ad17be4eb7a..f6c6aa9fb54 100644 --- a/source/isaaclab_ovphysx/test/assets/test_articulation.py +++ b/source/isaaclab_ovphysx/test/assets/test_articulation.py @@ -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*. @@ -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. diff --git a/source/isaaclab_ovphysx/test/assets/test_articulation_data.py b/source/isaaclab_ovphysx/test/assets/test_articulation_data.py index 16bb99a4d6c..2604c903a2b 100644 --- a/source/isaaclab_ovphysx/test/assets/test_articulation_data.py +++ b/source/isaaclab_ovphysx/test/assets/test_articulation_data.py @@ -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)