Source code for isaaclab_newton.sensors.joint_wrench.joint_wrench_sensor

# 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

from __future__ import annotations

import logging
from collections.abc import Sequence
from typing import TYPE_CHECKING

import warp as wp
from newton import JointType
from newton.selection import ArticulationView

from pxr import UsdPhysics

from isaaclab.sensors.joint_wrench import BaseJointWrenchSensor
from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source

from isaaclab_newton.physics import NewtonManager

from .joint_wrench_sensor_data import JointWrenchSensorData
from .kernels import joint_wrench_reset_kernel, joint_wrench_to_incoming_joint_frame_kernel

if TYPE_CHECKING:
    from isaaclab.sensors.joint_wrench import JointWrenchSensorCfg

logger = logging.getLogger(__name__)


[docs] class JointWrenchSensor(BaseJointWrenchSensor): """Newton joint reaction wrench sensor. Reads Newton's ``body_parent_f`` (world-frame wrench at child COM) and converts each entry to the ``INCOMING_JOINT_FRAME`` convention (child-side joint frame, child-side joint anchor as reference point) before storing it in per-joint force / torque buffers. :attr:`~isaaclab.sensors.SensorBaseCfg.prim_path` may point at either an articulation root expression or an env-scoped parent prefix. Newton label matching selects the articulations owned by that prefix. ``FREE`` and ``FIXED`` joints are excluded because neither has a meaningful joint anchor. """ cfg: JointWrenchSensorCfg """The configuration parameters.""" __backend_name__: str = "newton" """The name of the backend for the joint wrench sensor."""
[docs] def __init__(self, cfg: JointWrenchSensorCfg): """Initialize the Newton joint-wrench sensor. Requests the ``body_parent_f`` extended state attribute from :class:`NewtonManager` so the model builder allocates it during simulation startup. Args: cfg: The configuration parameters. """ super().__init__(cfg) self._data = JointWrenchSensorData() self._root_view: ArticulationView | None = None self._sim_bind_body_parent_f: wp.array | None = None self._sim_bind_body_q: wp.array | None = None self._sim_bind_body_com: wp.array | None = None self._sim_bind_joint_X_c: wp.array | None = None self._joint_child: wp.array | None = None self._num_joints: int = 0 NewtonManager.request_extended_state_attribute("body_parent_f")
def __str__(self) -> str: """String representation of the sensor instance.""" return ( f"Joint wrench sensor @ '{self.cfg.prim_path}': \n" f"\tbackend : newton\n" f"\tupdate period (s) : {self.cfg.update_period}\n" f"\tnumber of joints : {self._num_joints}\n" ) """ Properties """ @property def body_names(self) -> list[str]: """Ordered names of the bodies whose incoming joint wrench is reported.""" return self._data._body_names @property def data(self) -> JointWrenchSensorData: """The joint-wrench sensor data.""" self._update_outdated_buffers() return self._data """ Operations """ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None = None) -> None: """Reset the sensor buffers for the given environments. Args: env_ids: the environment ids to reset. env_mask: the mask used to reset the environments. Shape is (num_envs).""" if self._data._force is None or self._data._torque is None: return env_mask = self._resolve_indices_and_mask(env_ids, env_mask) super().reset(None, env_mask) wp.launch( joint_wrench_reset_kernel, dim=(self._num_envs, self._num_joints), inputs=[env_mask, self._data._force, self._data._torque], device=self._device, ) """ Implementation """ def _initialize_impl(self) -> None: """PHYSICS_READY callback: builds the articulation view and binds model / state arrays.""" super()._initialize_impl() model = NewtonManager.get_model() state_0 = NewtonManager.get_state_0() def has_articulation_root_api(prim) -> bool: return bool(prim.HasAPI(UsdPhysics.ArticulationRootAPI)) resolve_kwargs = {"predicate": has_articulation_root_api, "expected_num_matches": 1} _, root_prim_path_expr = resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs)[0] self._root_view = ArticulationView( model, path_expr_to_glob(root_prim_path_expr), verbose=False, exclude_joint_types=[JointType.FREE, JointType.FIXED], ) self._num_joints = self._root_view.joint_count if self._num_joints == 0: raise RuntimeError( "Joint wrench sensor matched zero reportable joints (all joints are FREE or FIXED)." f" Check the articulation at '{root_prim_path_expr}'." ) try: body_parent_f = self._root_view.get_attribute("body_parent_f", state_0) except AttributeError as err: raise RuntimeError( f"Joint wrench sensor '{self.cfg.prim_path}': Newton state does not expose" " 'body_parent_f'. Construct the sensor before sim startup so the extended-state" " request is forwarded to the model builder." ) from err self._sim_bind_body_parent_f = body_parent_f[:, 0] self._sim_bind_body_q = self._root_view.get_link_transforms(state_0)[:, 0] self._sim_bind_body_com = self._root_view.get_attribute("body_com", model)[:, 0] self._sim_bind_joint_X_c = self._root_view.get_attribute("joint_X_c", model)[:, 0] # joint_child is per-articulation; topology is identical across envs, # so we take the first-env mapping as the 1-D kernel input. joint_child_full = self._root_view.get_attribute("joint_child", model)[:, 0] joint_child_np = joint_child_full.numpy()[0] if not all(0 <= b < self._sim_bind_body_parent_f.shape[1] for b in joint_child_np): raise RuntimeError(f"joint_child contains out-of-range body indices for '{self.cfg.prim_path}'") self._joint_child = wp.array(joint_child_np, dtype=wp.int32, device=self._device) link_names = list(self._root_view.link_names) self._data._body_names = [link_names[int(b)] for b in joint_child_np] self._data.create_buffers(num_envs=self._num_envs, num_joints=self._num_joints, device=self._device) logger.info(f"Joint wrench sensor initialized: {self._num_envs} envs, {self._num_joints} joints") def _update_buffers_impl(self, env_mask: wp.array) -> None: """Convert Newton's body_parent_f into INCOMING_JOINT_FRAME force and torque buffers. Args: env_mask: A mask containing which environments need to be updated. Shape is (num_envs) """ if self._sim_bind_body_parent_f is None: raise RuntimeError( f"Joint wrench sensor '{self.cfg.prim_path}': not initialized." " Access sensor data only after sim.reset() has been called." ) wp.launch( joint_wrench_to_incoming_joint_frame_kernel, dim=(self._num_envs, self._num_joints), inputs=[ env_mask, self._sim_bind_body_parent_f, self._sim_bind_body_q, self._sim_bind_body_com, self._sim_bind_joint_X_c, self._joint_child, self._timestamp, ], outputs=[self._data._force, self._data._torque], device=self._device, ) def _invalidate_initialize_callback(self, event) -> None: """Drop view, cached sizes, and buffers; re-register the extended-state request. Args: event: An invalidate event. """ super()._invalidate_initialize_callback(event) self._root_view = None self._sim_bind_body_parent_f = None self._sim_bind_body_q = None self._sim_bind_body_com = None self._sim_bind_joint_X_c = None self._joint_child = None self._num_joints = 0 self._data._force = None self._data._torque = None self._data._body_names = [] self._data._force_ta = None self._data._torque_ta = None NewtonManager.request_extended_state_attribute("body_parent_f")