Unverified Commit d7fac055 authored by Shaoshu Su's avatar Shaoshu Su Committed by GitHub

Improves the implementation of euler_xyz_from_quat (#2365)

## Description

Previously, `euler_xyz_from_quat` returned Euler angles in the [0, 2π)
range, which is uncommon in robotics applications. As a result, several
users implemented their own workarounds to adjust the angle range to
(−π, π]. Examples include:

- **Isaac-RL-Two-wheel-Legged-Bot**: Custom unwrapping implemented in
[`rewards.py#L17`](https://github.com/jaykorea/Isaac-RL-Two-wheel-Legged-Bot/blob/057f81ed3aa4aff91551fce5c54256e47cece29a/lab/flamingo/tasks/constraint_based/locomotion/velocity/mdp/rewards.py#L17).
- **lorenzo-bianchi/IsaacLab**: Manual angle shifting in
[`quadcopter_env_v1.py#L219`](https://github.com/lorenzo-bianchi/IsaacLab/blob/247f66db3691046ecdfecb311268eccc729048ec/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/quadcopter_env_v1.py#L219).
- **kscalelabs/klab**: Custom angle normalization in
[`imu.py#L17`](https://github.com/kscalelabs/klab/blob/edf749f5177ba296a076d13380cd0fb1e5846e3f/exts/zbot2/zbot2/tasks/locomotion/velocity/mdp/imu.py#L17).

To address this issue, we have updated the default angle range from [0,
2π) to (−π, π] by removing the `% (2 * torch.pi)` operation at the
return statement, since the [atan2
function](https://en.wikipedia.org/wiki/Atan2) naturally outputs angles
within the (−π, π] range.

We also introduced a new parameter, `wrap_to_2pi: bool = False`. Setting
this parameter to `True` will maintain the previous behavior:

- **Default (`wrap_to_2pi=False`)**: Angles returned in the range (−π,
π].
- **Optional (`wrap_to_2pi=True`)**: Angles wrapped in the original [0,
2π) range.


Additionally, multiple test samples have been added to evaluate and
ensure performance and accuracy across different scenarios.

### What’s Changed

- **Default behavior** updated to (−π, π] (non-breaking).  
- **New argument** `wrap_to_2pi` for optional wrapping.  
- **Unit tests** added for both wrapped and unwrapped outputs.

---


Fixes https://github.com/isaac-sim/IsaacLab/issues/2364


## Type of Change

- **Enhancement** (non-breaking addition of functionality)
---

## Checklist

- [x] Ran `./isaaclab.sh --format` and all pre-commit checks pass  
- [x] Updated docstrings to describe `wrap_to_2pi` parameter  
- [x] Added tests covering both wrapped and unwrapped outputs  
- [x] No new warnings introduced  
- [ ] Updated changelog and bumped version in `config/extension.toml`  
- [x] Confirmed my name is listed in `CONTRIBUTORS.md`

---------
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
parent 25f7a5da
...@@ -103,6 +103,7 @@ Guidelines for modifications: ...@@ -103,6 +103,7 @@ Guidelines for modifications:
* Rosario Scalise * Rosario Scalise
* Ryley McCarroll * Ryley McCarroll
* Shafeef Omar * Shafeef Omar
* Shaoshu Su
* Shundo Kishi * Shundo Kishi
* Stefan Van de Mosselaer * Stefan Van de Mosselaer
* Stephan Pleines * Stephan Pleines
......
...@@ -429,7 +429,9 @@ def matrix_from_euler(euler_angles: torch.Tensor, convention: str) -> torch.Tens ...@@ -429,7 +429,9 @@ def matrix_from_euler(euler_angles: torch.Tensor, convention: str) -> torch.Tens
@torch.jit.script @torch.jit.script
def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: def euler_xyz_from_quat(
quat: torch.Tensor, wrap_to_2pi: bool = False
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Convert rotations given as quaternions to Euler angles in radians. """Convert rotations given as quaternions to Euler angles in radians.
Note: Note:
...@@ -437,6 +439,9 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, ...@@ -437,6 +439,9 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor,
Args: Args:
quat: The quaternion orientation in (w, x, y, z). Shape is (N, 4). quat: The quaternion orientation in (w, x, y, z). Shape is (N, 4).
wrap_to_2pi (bool): Whether to wrap output Euler angles into [0, 2π). If
False, angles are returned in the default range (−π, π]. Defaults to
False.
Returns: Returns:
A tuple containing roll-pitch-yaw. Each element is a tensor of shape (N,). A tuple containing roll-pitch-yaw. Each element is a tensor of shape (N,).
...@@ -459,7 +464,9 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, ...@@ -459,7 +464,9 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor,
cos_yaw = 1 - 2 * (q_y * q_y + q_z * q_z) cos_yaw = 1 - 2 * (q_y * q_y + q_z * q_z)
yaw = torch.atan2(sin_yaw, cos_yaw) yaw = torch.atan2(sin_yaw, cos_yaw)
return roll % (2 * torch.pi), pitch % (2 * torch.pi), yaw % (2 * torch.pi) # TODO: why not wrap_to_pi here ? if wrap_to_2pi:
return roll % (2 * torch.pi), pitch % (2 * torch.pi), yaw % (2 * torch.pi)
return roll, pitch, yaw
@torch.jit.script @torch.jit.script
......
...@@ -847,3 +847,57 @@ def test_interpolate_rotations(): ...@@ -847,3 +847,57 @@ def test_interpolate_rotations():
# Assert that the result is almost equal to the expected quaternion # Assert that the result is almost equal to the expected quaternion
np.testing.assert_array_almost_equal(result_axis_angle.cpu(), expected, decimal=DECIMAL_PRECISION) np.testing.assert_array_almost_equal(result_axis_angle.cpu(), expected, decimal=DECIMAL_PRECISION)
def test_euler_xyz_from_quat():
"""Test euler_xyz_from_quat function.
This test checks the output from the :meth:`~isaaclab.utils.math_utils.euler_xyz_from_quat` function
against the expected output for various quaternions.
The test includes quaternions representing different rotations around the x, y, and z axes.
The test is performed for both the default output range (-π, π] and the wrapped output range [0, 2π).
"""
quats = [
torch.Tensor([[1.0, 0.0, 0.0, 0.0]]), # 0° around x, y, z
torch.Tensor([
[0.9238795, 0.3826834, 0.0, 0.0], # 45° around x
[0.9238795, 0.0, -0.3826834, 0.0], # -45° around y
[0.9238795, 0.0, 0.0, -0.3826834], # -45° around z
]),
torch.Tensor([
[0.7071068, -0.7071068, 0.0, 0.0], # -90° around x
[0.7071068, 0.0, 0.0, -0.7071068], # -90° around z
]),
torch.Tensor([
[0.3826834, -0.9238795, 0.0, 0.0], # -135° around x
[0.3826834, 0.0, 0.0, -0.9238795], # -135° around y
]),
]
expected_euler_angles = [
torch.Tensor([[0.0, 0.0, 0.0]]), # identity
torch.Tensor([
[torch.pi / 4, 0.0, 0.0], # 45° about x
[0.0, -torch.pi / 4, 0.0], # -45° about y
[0.0, 0.0, -torch.pi / 4], # -45° about z
]),
torch.Tensor([
[-torch.pi / 2, 0.0, 0.0], # -90° about x
[0.0, 0.0, -torch.pi / 2], # -90° about z
]),
torch.Tensor([
[-3 * torch.pi / 4, 0.0, 0.0], # -135° about x
[0.0, 0.0, -3 * torch.pi / 4], # -135° about y
]),
]
# Test 1: default no-wrap range from (-π, π]
for quat, expected in zip(quats, expected_euler_angles):
output = torch.stack(math_utils.euler_xyz_from_quat(quat), dim=-1)
torch.testing.assert_close(output, expected)
# Test 2: wrap to [0, 2π)
for quat, expected in zip(quats, expected_euler_angles):
wrapped = expected % (2 * torch.pi)
output = torch.stack(math_utils.euler_xyz_from_quat(quat, wrap_to_2pi=True), dim=-1)
torch.testing.assert_close(output, wrapped)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment