Source code for isaaclab_newton.sensors.pva.pva

# 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 torch
import warp as wp

from pxr import UsdGeom

import isaaclab.utils.math as math_utils
from isaaclab.markers import VisualizationMarkers
from isaaclab.sensors.pva import BasePva

from isaaclab_newton.physics import NewtonManager

from .kernels import pva_reset_kernel, pva_update_kernel
from .pva_data import PvaData

if TYPE_CHECKING:
    from isaaclab.sensors.pva import PvaCfg

logger = logging.getLogger(__name__)


[docs] class Pva(BasePva): """Newton Pose Velocity Acceleration (PVA) sensor. Reads body transforms, velocities, and accelerations directly from Newton's simulation state (``body_q``, ``body_qd``, ``body_qdd``) to provide world-frame pose and body-frame velocities/accelerations. """ cfg: PvaCfg """The configuration parameters.""" __backend_name__: str = "newton" """The name of the backend for the PVA sensor."""
[docs] def __init__(self, cfg: PvaCfg): """Initializes the Newton PVA sensor. Registers a site request and the ``body_qdd`` state attribute with :class:`NewtonManager`. The site is injected into prototype builders before replication so it ends up in each world. Args: cfg: The configuration parameters. """ super().__init__(cfg) self._data = PvaData() self._site_indices: wp.array | None = None self._newton_model = None offset_xform = wp.transform(cfg.offset.pos, cfg.offset.rot) self._site_label = NewtonManager.cl_register_site(cfg.prim_path, offset_xform) NewtonManager.request_extended_state_attribute("body_qdd") logger.info(f"Pva '{cfg.prim_path}': site registered (label='{self._site_label}')")
def __str__(self) -> str: """String representation of the sensor instance.""" return ( f"Pva sensor @ '{self.cfg.prim_path}': \n" f"\tbackend : newton\n" f"\tupdate period (s) : {self.cfg.update_period}\n" f"\tnumber of sensors : {self._num_envs}\n" ) """ Properties """ @property def data(self) -> PvaData: """The PVA 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): """Reset the sensor for the given environments. Zeroes out all PVA buffers for the specified environments. Args: env_ids: Environment indices to reset. Defaults to all environments. env_mask: Boolean mask of environments to reset. Mutually exclusive with *env_ids*. """ env_mask = self._resolve_indices_and_mask(env_ids, env_mask) super().reset(None, env_mask) wp.launch( pva_reset_kernel, dim=self._num_envs, inputs=[ env_mask, self._data._pose_w, self._data._pos_w, self._data._quat_w, self._data._projected_gravity_b, self._data._lin_vel_b, self._data._ang_vel_b, self._data._lin_acc_b, self._data._ang_acc_b, ], device=self._device, ) """ Implementation """ def _initialize_impl(self): """PHYSICS_READY callback: resolves site indices and stores model reference.""" super()._initialize_impl() site_map = NewtonManager._cl_site_index_map num_envs = self._num_envs if self._site_label not in site_map: raise ValueError( f"Pva '{self.cfg.prim_path}': site label '{self._site_label}' " "not found in NewtonManager._cl_site_index_map." ) global_idx, per_world = site_map[self._site_label] if per_world is None: site_indices = [global_idx] * num_envs else: if len(per_world) != num_envs: raise ValueError( f"Pva '{self.cfg.prim_path}': site has {len(per_world)} world entries, expected {num_envs}." ) site_indices: list[int] = [] for env_idx, world_sites in enumerate(per_world): if len(world_sites) != 1: raise ValueError( f"Pva '{self.cfg.prim_path}': pattern matched {len(world_sites)} " f"bodies in env {env_idx}, expected exactly 1." ) site_indices.append(world_sites[0]) self._site_indices = wp.array(site_indices, dtype=int, device=self._device) self._newton_model = NewtonManager._model self._data.create_buffers(num_envs=num_envs, device=self._device) logger.info(f"Pva initialized: {num_envs} envs") def _update_buffers_impl(self, env_mask: wp.array): """Reads Newton body state and computes all PVA quantities.""" if self._newton_model is None: raise RuntimeError( f"Pva '{self.cfg.prim_path}': sensor not initialized. " "Access sensor data only after sim.reset() has been called." ) state = NewtonManager._state_0 wp.launch( pva_update_kernel, dim=self._num_envs, inputs=[ env_mask, self._site_indices, self._newton_model.shape_body, self._newton_model.shape_transform, self._newton_model.body_com, self._newton_model.gravity, self._newton_model.body_world, state.body_q, state.body_qd, state.body_qdd, self._timestamp, ], outputs=[ self._data._pose_w, self._data._pos_w, self._data._quat_w, self._data._projected_gravity_b, self._data._lin_vel_b, self._data._ang_vel_b, self._data._lin_acc_b, self._data._ang_acc_b, ], device=self._device, ) def _set_debug_vis_impl(self, debug_vis: bool): if debug_vis: if not hasattr(self, "acceleration_visualizer"): self.acceleration_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) self.acceleration_visualizer.set_visibility(True) else: if hasattr(self, "acceleration_visualizer"): self.acceleration_visualizer.set_visibility(False) def _debug_vis_callback(self, event): if self._newton_model is None: return # base position (offset upward for visibility) base_pos_w = self._data.pos_w.torch.clone() base_pos_w[:, 2] += 0.5 # arrow scale default_scale = self.acceleration_visualizer.cfg.markers["arrow"].scale arrow_scale = torch.tensor(default_scale, device=self.device).repeat(self._data.lin_acc_b.torch.shape[0], 1) # arrow direction from acceleration; filter out bodies with effectively zero accel (no defined direction) up_axis = UsdGeom.GetStageUpAxis(self.stage) pos_w_torch = self._data.pos_w.torch accel_w = math_utils.quat_apply(self._data.quat_w.torch, self._data.lin_acc_b.torch) valid_indices = (torch.linalg.norm(accel_w, dim=-1) > 1e-5).nonzero(as_tuple=True)[0] if valid_indices.numel() == 0: return pos_filtered = pos_w_torch.index_select(0, valid_indices) accel_filtered = accel_w.index_select(0, valid_indices) rotation_matrix = math_utils.create_rotation_matrix_from_view( pos_filtered, pos_filtered + accel_filtered, up_axis=up_axis, device=self._device, ) quat_opengl = math_utils.quat_from_matrix(rotation_matrix) quat_w = math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "world") self.acceleration_visualizer.visualize( base_pos_w.index_select(0, valid_indices), quat_w, arrow_scale.index_select(0, valid_indices), ) def _invalidate_initialize_callback(self, event): """Clears references for re-initialization and re-registers with NewtonManager.""" super()._invalidate_initialize_callback(event) self._newton_model = None self._site_indices = None # Zero out data buffers so stale data is not served between STOP and reinit. for buf in [ self._data._pose_w, self._data._pos_w, self._data._quat_w, self._data._projected_gravity_b, self._data._lin_vel_b, self._data._ang_vel_b, self._data._lin_acc_b, self._data._ang_acc_b, ]: if buf is not None: buf.zero_() # Re-register so a subsequent start_simulation picks them up. offset_xform = wp.transform(self.cfg.offset.pos, self.cfg.offset.rot) self._site_label = NewtonManager.cl_register_site(self.cfg.prim_path, offset_xform) NewtonManager.request_extended_state_attribute("body_qdd")