# 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
import isaaclab.utils.math as math_utils
from isaaclab.sensors.imu import BaseImu
from isaaclab.sim.utils.queries import path_expr_to_glob
from isaaclab_physx.physics import PhysxManager as SimulationManager
from .imu_data import ImuData
from .kernels import imu_reset_kernel, imu_update_kernel
if TYPE_CHECKING:
from isaaclab.sensors.imu import ImuCfg
logger = logging.getLogger(__name__)
[docs]
class Imu(BaseImu):
"""The PhysX Inertial Measurement Unit (IMU) sensor.
This sensor models a real IMU that measures angular velocity (gyroscope) and
linear acceleration (accelerometer) in the sensor's body frame. Unlike the PVA
sensor, it does not provide pose, linear velocity, angular acceleration, or
projected gravity.
Like a real accelerometer, the linear acceleration readings always include the
contribution of gravity. The gravity vector is queried from the simulation at
initialization.
The sensor can be attached to any prim path with a rigid ancestor in its tree.
If the provided path is not a rigid body, the closest rigid-body ancestor is used
for simulation queries. The fixed transform from that ancestor to the target prim
is computed once during initialization and composed with the configured sensor offset.
.. note::
Linear acceleration is computed using numerical differentiation from velocities.
Consequently, the IMU sensor accuracy depends on the chosen physics timestep.
For sufficient accuracy, we recommend keeping the timestep at least 200 Hz.
"""
cfg: ImuCfg
"""The configuration parameters."""
__backend_name__: str = "physx"
"""The name of the backend for the IMU sensor."""
[docs]
def __init__(self, cfg: ImuCfg):
"""Initializes the IMU sensor.
Args:
cfg: The configuration parameters.
"""
super().__init__(cfg)
self._data = ImuData()
self._rigid_parent_expr: str | None = None
self._raw_transforms: wp.array | None = None
self._raw_velocities: wp.array | None = None
self._raw_coms: wp.array | None = None
self._update_cmd: wp.Launch | None = None
self._update_env_mask: wp.array | None = None
self._use_recorded_launch: bool = False
def __str__(self) -> str:
"""Returns: A string containing information about the instance."""
return (
f"Imu sensor @ '{self.cfg.prim_path}': \n"
f"\tview type : {self._view.__class__}\n"
f"\tupdate period (s) : {self.cfg.update_period}\n"
f"\tnumber of sensors : {self._view.count}\n"
)
"""
Properties
"""
@property
def data(self) -> ImuData:
self._update_outdated_buffers()
return self._data
@property
def num_instances(self) -> int:
return self._view.count
"""
Operations
"""
def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None = None):
env_mask = self._resolve_indices_and_mask(env_ids, env_mask)
super().reset(None, env_mask)
wp.launch(
imu_reset_kernel,
dim=self._num_envs,
inputs=[
env_mask,
self._data._ang_vel_b,
self._data._lin_acc_b,
self._prev_lin_vel_w,
],
device=self._device,
)
"""
Implementation.
"""
def _initialize_impl(self):
"""Initializes the sensor handles and internal buffers.
- If the target prim path is a rigid body, build the view directly on it.
- Otherwise find the closest rigid-body ancestor, cache the fixed transform from that ancestor
to the target prim, and build the view on the ancestor expression.
"""
super()._initialize_impl()
self._physics_sim_view = SimulationManager.get_physics_sim_view()
self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr()
self._view = self._physics_sim_view.create_rigid_body_view(path_expr_to_glob(self._rigid_parent_expr))
# Query world gravity and compute accelerometer bias (real IMUs always measure gravity)
gravity = self._physics_sim_view.get_gravity()
gravity_bias = torch.tensor((-gravity[0], -gravity[1], -gravity[2]), device=self._device)
gravity_bias_torch = gravity_bias.repeat(self._view.count, 1)
self._gravity_bias_w = wp.from_torch(gravity_bias_torch.contiguous(), dtype=wp.vec3f)
self._initialize_buffers_impl()
# Compose the configured offset with the fixed ancestor->target transform (done once)
if fixed_pos_b is not None and fixed_quat_b is not None:
fixed_p = torch.tensor(fixed_pos_b, device=self._device).repeat(self._view.count, 1)
fixed_q = torch.tensor(fixed_quat_b, device=self._device).repeat(self._view.count, 1)
cfg_p = wp.to_torch(self._offset_pos_b).clone()
cfg_q = wp.to_torch(self._offset_quat_b).clone()
composed_p = fixed_p + math_utils.quat_apply(fixed_q, cfg_p)
composed_q = math_utils.quat_mul(fixed_q, cfg_q)
self._offset_pos_b = wp.from_torch(composed_p.contiguous(), dtype=wp.vec3f)
self._offset_quat_b = wp.from_torch(composed_q.contiguous(), dtype=wp.quatf)
self._use_recorded_launch = wp.get_device(self._device).is_cuda
def _update_buffers_impl(self, env_mask: wp.array | None = None):
"""Fills the buffers of the sensor data."""
env_mask = self._resolve_indices_and_mask(None, env_mask)
# Refresh the PhysX buffers every update, but create their typed Warp views only once:
# the getters lazily allocate their output buffers and refresh the same memory in place
# on every call, so the cached views (and the recorded launch that consumes them) stay
# valid. A re-backed buffer would silently freeze the sensor data, so fail loudly.
transforms = self._view.get_transforms()
velocities = self._view.get_velocities()
coms = self._view.get_coms()
if self._raw_transforms is None:
self._raw_transforms = transforms.view(wp.transformf)
self._raw_velocities = velocities.view(wp.spatial_vectorf)
self._raw_coms = coms.view(wp.transformf)
elif (
transforms.ptr != self._raw_transforms.ptr
or velocities.ptr != self._raw_velocities.ptr
or coms.ptr != self._raw_coms.ptr
):
raise RuntimeError(
f"A PhysX rigid body buffer of the sensor at '{self.cfg.prim_path}' was re-allocated"
" after its warp view was cached. The cached views and the recorded launch require"
" pointer-stable buffers refreshed in place."
)
wp.copy(self._coms_buffer, self._raw_coms)
if self._use_recorded_launch:
if self._update_cmd is None:
try:
self._update_cmd = self._launch_update(env_mask, record_cmd=True)
self._update_env_mask = env_mask
except Exception as exc:
self._use_recorded_launch = False
logger.warning(
f"Failed to record the update of the IMU at '{self.cfg.prim_path}'."
f" Falling back to eager kernel launches. Reason: {exc}"
)
if self._update_cmd is not None:
if env_mask is not self._update_env_mask:
self._update_cmd.set_param_by_name("env_mask", env_mask)
self._update_env_mask = env_mask
self._update_cmd.launch()
return
self._launch_update(env_mask)
def _launch_update(self, env_mask: wp.array, record_cmd: bool = False) -> wp.Launch | None:
"""Launch or record the kernel that updates the IMU data."""
return wp.launch(
imu_update_kernel,
dim=self._num_envs,
inputs=[
env_mask,
self._raw_transforms,
self._raw_velocities,
self._coms_buffer,
self._offset_pos_b,
self._offset_quat_b,
self._gravity_bias_w,
self._timestamp,
self._timestamp_last_update,
self._prev_lin_vel_w,
self._data._ang_vel_b,
self._data._lin_acc_b,
],
device=self._device,
record_cmd=record_cmd,
)
def _initialize_buffers_impl(self):
"""Create buffers for storing data."""
self._data.create_buffers(num_envs=self._view.count, device=self._device)
self._prev_lin_vel_w = wp.zeros(self._view.count, dtype=wp.vec3f, device=self._device)
offset_pos_torch = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1)
offset_quat_torch = torch.tensor(list(self.cfg.offset.rot), device=self._device).repeat(self._view.count, 1)
self._offset_pos_b = wp.from_torch(offset_pos_torch.contiguous(), dtype=wp.vec3f)
self._offset_quat_b = wp.from_torch(offset_quat_torch.contiguous(), dtype=wp.quatf)
self._coms_buffer = wp.zeros(self._view.count, dtype=wp.transformf, device=self._device)
def _invalidate_initialize_callback(self, event):
"""Invalidate the sensor and release cached PhysX and launch state."""
super()._invalidate_initialize_callback(event)
self._view = None
self._raw_transforms = None
self._raw_velocities = None
self._raw_coms = None
self._update_cmd = None
self._update_env_mask = None