Repository navigation
[OVPHYSX] Add IMU sensor #5421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[OVPHYSX] Add IMU sensor #5421
Changes from all commits
a2b4f4b
d429600
a4a4cd4
34b99ef
9683bdb
3ce1991
8d9e4bd
e2047bb
162e702
00a8e55
a55662b
9b9709e
59d7843
4efc6c0
788ccba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| Added | ||
| ^^^^^ | ||
|
|
||
| * Added :class:`~isaaclab_ovphysx.sensors.Imu` and | ||
| :class:`~isaaclab_ovphysx.sensors.ImuData` implementing the | ||
| :class:`~isaaclab.sensors.imu.BaseImu` / | ||
| :class:`~isaaclab.sensors.imu.BaseImuData` contracts on the OVPhysX | ||
| backend. Reports angular velocity and proper linear acceleration in | ||
| the sensor body frame using ovphysx tensor bindings on the rigid-body | ||
| ancestor of the sensor prim path. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # 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 | ||
|
|
||
| """Sub-module for OVPhysX IMU sensor.""" | ||
|
|
||
| from isaaclab.utils.module import lazy_export | ||
|
|
||
| lazy_export() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # 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 | ||
|
|
||
| __all__ = [ | ||
| "Imu", | ||
| "ImuData", | ||
| ] | ||
|
|
||
| from .imu import Imu | ||
| from .imu_data import ImuData |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| # 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 | ||
|
|
||
| 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 | ||
|
|
||
| import isaaclab_ovphysx.tensor_types as TT | ||
| from isaaclab_ovphysx.physics import OvPhysxManager 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 | ||
|
|
||
|
|
||
| class Imu(BaseImu): | ||
| """The OVPhysX 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 = "ovphysx" | ||
| """The name of the backend for the IMU sensor.""" | ||
|
|
||
| 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 | ||
|
|
||
| def __str__(self) -> str: | ||
| """Returns: A string containing information about the instance.""" | ||
| return ( | ||
| f"Imu sensor @ '{self.cfg.prim_path}': \n" | ||
| f"\tbinding pattern : {self._rigid_parent_expr}\n" | ||
| f"\tupdate period (s) : {self.cfg.update_period}\n" | ||
| f"\tnumber of sensors : {self._num_bodies}\n" | ||
| ) | ||
|
|
||
| """ | ||
| Properties | ||
| """ | ||
|
|
||
| @property | ||
| def data(self) -> ImuData: | ||
| self._update_outdated_buffers() | ||
| return self._data | ||
|
|
||
| @property | ||
| def num_instances(self) -> int: | ||
| return self._num_bodies | ||
|
|
||
| """ | ||
| 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, | ||
| ) | ||
|
|
||
| def update(self, dt: float, force_recompute: bool = False): | ||
| self._dt = dt | ||
| super().update(dt, force_recompute) | ||
|
|
||
| """ | ||
| Implementation. | ||
| """ | ||
|
|
||
| def _initialize_impl(self): | ||
| """Initializes the sensor handles and internal buffers. | ||
|
|
||
| - If the target prim path is a rigid body, bind directly to it. | ||
| - Otherwise find the closest rigid-body ancestor, cache the fixed transform | ||
| from that ancestor to the target prim, and bind to the ancestor pattern. | ||
| """ | ||
| super()._initialize_impl() | ||
|
|
||
| physx_instance = SimulationManager.get_physx_instance() | ||
| if physx_instance is None: | ||
| raise RuntimeError("OvPhysxManager has not been initialized yet.") | ||
|
|
||
| self._rigid_parent_expr, fixed_pos_b, fixed_quat_b = self._resolve_rigid_body_ancestor_expr() | ||
|
|
||
| # Translate the regex-style path expression to an ovphysx fnmatch glob. | ||
| pattern = self._rigid_parent_expr.replace(".*", "*") | ||
|
|
||
| self._pose_binding = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=TT.RIGID_BODY_POSE) | ||
| self._vel_binding = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=TT.RIGID_BODY_VELOCITY) | ||
| self._com_binding = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=TT.RIGID_BODY_COM_POSE) | ||
| self._num_bodies = self._pose_binding.count | ||
|
|
||
| if self._num_bodies != self._num_envs: | ||
| raise ValueError( | ||
| f"OvPhysx Imu: pattern '{pattern}' matched {self._num_bodies} rigid bodies; expected exactly one" | ||
| f" body per environment (num_envs={self._num_envs}). Check that the prim path or its rigid-body" | ||
| " ancestor is unique per env." | ||
| ) | ||
|
|
||
| gravity = SimulationManager.get_gravity() | ||
| gravity_bias = torch.tensor((-gravity[0], -gravity[1], -gravity[2]), device=self._device) | ||
| gravity_bias_torch = gravity_bias.repeat(self._num_bodies, 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._num_bodies, 1) | ||
| fixed_q = torch.tensor(fixed_quat_b, device=self._device).repeat(self._num_bodies, 1) | ||
|
|
||
| cfg_p = wp.to_torch(self._offset_pos_b).clone() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is clone strictly needed? |
||
| 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) | ||
|
Comment on lines
+158
to
+168
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the round trip feel a bit weird. Would adding warp kernel for doing this be beneficial? Broadcast seems savable. Or it can be potentially done in _initialize_buffers_impl.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This side is matching the PhysX sensor. I agree it's odd. |
||
|
|
||
| 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) | ||
|
|
||
| # ovphysx ``binding.read(dst)`` writes into the pre-allocated dst buffer; | ||
| # ``_*_view`` are float32 aliases of the structured-dtype buffers below. | ||
| self._pose_binding.read(self._transforms_view) | ||
| self._vel_binding.read(self._velocities_view) | ||
| # RIGID_BODY_COM_POSE is a CPU tensor type in the OVPhysX wheel. | ||
| # For GPU simulations, stage on CPU then copy into the kernel buffer. | ||
| self._com_binding.read(self._coms_read_view) | ||
| if self._coms_read_view is not self._coms_gpu_view: | ||
| wp.copy(self._coms_gpu_view, self._coms_read_view) | ||
|
|
||
| wp.launch( | ||
| imu_update_kernel, | ||
| dim=self._num_envs, | ||
| inputs=[ | ||
| env_mask, | ||
| self._transforms, | ||
| self._velocities, | ||
| self._coms_buffer, | ||
| self._offset_pos_b, | ||
| self._offset_quat_b, | ||
| self._gravity_bias_w, | ||
| 1.0 / self._dt, | ||
| self._timestamp, | ||
| self._prev_lin_vel_w, | ||
| self._data._ang_vel_b, | ||
| self._data._lin_acc_b, | ||
| ], | ||
| device=self._device, | ||
| ) | ||
|
|
||
| def _initialize_buffers_impl(self): | ||
| """Create buffers for storing data.""" | ||
| self._data.create_buffers(num_envs=self._num_bodies, device=self._device) | ||
|
|
||
| self._prev_lin_vel_w = wp.zeros(self._num_bodies, dtype=wp.vec3f, device=self._device) | ||
|
|
||
| offset_pos_torch = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._num_bodies, 1) | ||
| offset_quat_torch = torch.tensor(list(self.cfg.offset.rot), device=self._device).repeat(self._num_bodies, 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) | ||
|
|
||
| # Structured-dtype buffers consumed by the kernel. | ||
| self._transforms = wp.zeros(self._num_bodies, dtype=wp.transformf, device=self._device) | ||
| self._velocities = wp.zeros(self._num_bodies, dtype=wp.spatial_vectorf, device=self._device) | ||
| self._coms_buffer = wp.zeros(self._num_bodies, dtype=wp.transformf, device=self._device) | ||
|
|
||
| self._transforms_view = wp.array( | ||
| ptr=self._transforms.ptr, | ||
| shape=self._pose_binding.shape, | ||
| dtype=wp.float32, | ||
| device=self._device, | ||
| copy=False, | ||
| ) | ||
| self._velocities_view = wp.array( | ||
| ptr=self._velocities.ptr, | ||
| shape=self._vel_binding.shape, | ||
| dtype=wp.float32, | ||
| device=self._device, | ||
| copy=False, | ||
| ) | ||
| self._coms_gpu_view = wp.array( | ||
| ptr=self._coms_buffer.ptr, | ||
| shape=self._com_binding.shape, | ||
| dtype=wp.float32, | ||
| device=self._device, | ||
| copy=False, | ||
| ) | ||
| if self._device == "cpu": | ||
| self._coms_read_view = self._coms_gpu_view | ||
| else: | ||
| self._coms_read_view = wp.zeros(self._com_binding.shape, dtype=wp.float32, device="cpu", pinned=True) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 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 warp as wp | ||
|
|
||
| from isaaclab.sensors.imu import BaseImuData | ||
| from isaaclab.utils.warp import ProxyArray | ||
|
|
||
|
|
||
| class ImuData(BaseImuData): | ||
| """Data container for the OVPhysX IMU sensor.""" | ||
|
|
||
| @property | ||
| def ang_vel_b(self) -> ProxyArray: | ||
| """IMU frame angular velocity relative to the world expressed in IMU frame [rad/s]. | ||
|
|
||
| Shape is (num_instances,), dtype = wp.vec3f. In torch this resolves to (num_instances, 3). | ||
| """ | ||
| if self._ang_vel_b_ta is None: | ||
| self._ang_vel_b_ta = ProxyArray(self._ang_vel_b) | ||
| return self._ang_vel_b_ta | ||
|
|
||
| @property | ||
| def lin_acc_b(self) -> ProxyArray: | ||
| """Linear acceleration (proper) in the IMU frame [m/s^2]. | ||
|
|
||
| Zero in freefall, +g upward at rest. | ||
|
|
||
| Shape is (num_instances,), dtype = wp.vec3f. In torch this resolves to (num_instances, 3). | ||
| """ | ||
| if self._lin_acc_b_ta is None: | ||
| self._lin_acc_b_ta = ProxyArray(self._lin_acc_b) | ||
| return self._lin_acc_b_ta | ||
|
|
||
| def create_buffers(self, num_envs: int, device: str) -> None: | ||
| """Create internal buffers for sensor data. | ||
|
|
||
| Args: | ||
| num_envs: Number of environments. | ||
| device: Device for tensor storage. | ||
| """ | ||
| self._num_envs = num_envs | ||
| self._device = device | ||
| self._ang_vel_b = wp.zeros(num_envs, dtype=wp.vec3f, device=device) | ||
| self._lin_acc_b = wp.zeros(num_envs, dtype=wp.vec3f, device=device) | ||
|
|
||
| # -- Pinned ProxyArray cache (one per read property, lazily created on first access) | ||
| self._ang_vel_b_ta: ProxyArray | None = None | ||
| self._lin_acc_b_ta: ProxyArray | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imu/__init__.pyandimu/__init__.pyiThe
sensors/imu/directory is missing its__init__.py(and the corresponding.pyistub). Without__init__.py,sensors/imu/is not a Python package, so thelazy_export()mechanism insensors/__init__.pywill fail when it tries to resolvefrom .imu import Imu, ImuData— Python will raiseModuleNotFoundError: No module named 'isaaclab_ovphysx.sensors.imu'on first access of either symbol.Every peer package follows this exact pattern:
sensors/contact_sensor/has both__init__.py(callinglazy_export()) and__init__.pyi(declaringContactSensor,ContactSensorCfg,ContactSensorData), and the PhysXsensors/imu/has the same pair. The OVPhysXimu/package needs matching files to be importable at runtime.