Skip to content

Commit 9659a5c

Browse files
Changes Quaternion convention from WXYZ to XYZW (#4437)
# Description Changes the quaternion convention in IsaacLab to match that of: - ROS - PhysX - Newton - Scipy - Low level USD Fixes isaac-sim/IsaacLab-Internal#744 This PR brings massive changes throughout the code base. Almost all users should expect their code to be impacted by these changes. We provide a tool that should help users navigate these changes. `./scripts/tools/find_quaternions.py` provides users with an automated script to search throughout the code base some quaternions that do not differ between the current code and a given commit. This can be used to track if your code has changed the order of its hard-coded quaternions. For instance running the following will show all the quaternions that might need to be updated inside your extension. The dry-run flag will make it so that the script does not attempt to modify the quaternions. ``` ./scripts/tools/find_quaternions.py --path PATH_TO_YOUR_EXTENSION --all-quats --fix --context 4 --dry-run ``` If you want to modify the quaternions the script comes with an interactive mode where it prompts you with changes to be made, to do so remove the `--dry-run` flag: ``` ./scripts/tools/find_quaternions.py --path PATH_TO_YOUR_EXTENSION --all-quats --fix --context 4 ``` The `--context` flag provides more or less context before and after the line where a quaternion was found. Here is a sample of the interactive mode: ```shell Searching 968 Python, 11 JSON, and 6 RST files... Comparing against: main Found 178 files changed from main Found 69 potential quaternions to review: ==================================================================================================== ──────────────────────────────────────────────────────────────────────────────── 📍 source/isaaclab_assets/isaaclab_assets/robots/anymal.py:173 [AMBIGUOUS] ──────────────────────────────────────────────────────────────────────────────── 169 | # Configuration - Sensors. 170 | ## 171 | 172 | ANYMAL_LIDAR_CFG = VELODYNE_VLP_16_RAYCASTER_CFG.replace( >>> 173 | offset=RayCasterCfg.OffsetCfg(pos=(-0.310, 0.000, 0.159), rot=(0.0, 0.0, 0.0, 1.0)) 174 | ) 175 | """Configuration for the Velodyne VLP-16 sensor mounted on the ANYmal robot's base.""" ──────────────────────────────────────────────────────────────────────────────── Change: [0.0, 0.0, 0.0, 1.0] → [0.0, 0.0, 1.0, 0.0] Result: offset=RayCasterCfg.OffsetCfg(pos=(-0.310, 0.000, 0.159), rot=(0.0, 0.0, 1.0, 0.0)) Apply this fix? [Y/n/a/q]: Y ✅ Fixed! ──────────────────────────────────────────────────────────────────────────────── 📍 source/isaaclab_mimic/test/test_curobo_planner_franka.py:42 [AMBIGUOUS] ──────────────────────────────────────────────────────────────────────────────── 38 | 39 | # Predefined EE goals for the test 40 | # Each entry is a tuple of: (goal specification, goal ID) 41 | predefined_ee_goals_and_ids = [ >>> 42 | ({"pos": [0.70, -0.25, 0.25], "quat": [0.0, 0.707, 0.0, 0.707]}, "Behind wall, left"), 43 | ({"pos": [0.70, 0.25, 0.25], "quat": [0.0, 0.707, 0.0, 0.707]}, "Behind wall, right"), 44 | ({"pos": [0.65, 0.0, 0.45], "quat": [1.0, 0.0, 0.0, 0.0]}, "Behind wall, center, high"), 45 | ({"pos": [0.80, -0.15, 0.35], "quat": [0.0, 0.5, 0.0, 0.866]}, "Behind wall, far left"), 46 | ({"pos": [0.80, 0.15, 0.35], "quat": [0.0, 0.5, 0.0, 0.866]}, "Behind wall, far right"), ──────────────────────────────────────────────────────────────────────────────── Change: [0.0, 0.707, 0.0, 0.707] → [0.707, 0.0, 0.707, 0.0] Result: ({"pos": [0.70, -0.25, 0.25], "quat": [0.707, 0.0, 0.707, 0.0]}, "Behind wall, left"), Apply this fix? [Y/n/a/q]: Y ✅ Fixed! ──────────────────────────────────────────────────────────────────────────────── 📍 source/isaaclab_mimic/test/test_curobo_planner_franka.py:43 [AMBIGUOUS] ──────────────────────────────────────────────────────────────────────────────── 39 | # Predefined EE goals for the test 40 | # Each entry is a tuple of: (goal specification, goal ID) 41 | predefined_ee_goals_and_ids = [ 42 | ({"pos": [0.70, -0.25, 0.25], "quat": [0.707, 0.0, 0.707, 0.0]}, "Behind wall, left"), >>> 43 | ({"pos": [0.70, 0.25, 0.25], "quat": [0.0, 0.707, 0.0, 0.707]}, "Behind wall, right"), 44 | ({"pos": [0.65, 0.0, 0.45], "quat": [1.0, 0.0, 0.0, 0.0]}, "Behind wall, center, high"), 45 | ({"pos": [0.80, -0.15, 0.35], "quat": [0.0, 0.5, 0.0, 0.866]}, "Behind wall, far left"), 46 | ({"pos": [0.80, 0.15, 0.35], "quat": [0.0, 0.5, 0.0, 0.866]}, "Behind wall, far right"), 47 | ] ──────────────────────────────────────────────────────────────────────────────── Change: [0.0, 0.707, 0.0, 0.707] → [0.707, 0.0, 0.707, 0.0] Result: ({"pos": [0.70, 0.25, 0.25], "quat": [0.707, 0.0, 0.707, 0.0]}, "Behind wall, right"), Apply this fix? [Y/n/a/q]: Y ✅ Fixed! ``` ## Type of change - Breaking change (existing functionality will not work without user modification) - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Kelly Guo <[email protected]> Co-authored-by: Kelly Guo <[email protected]>
1 parent 2c60472 commit 9659a5c

File tree

188 files changed

+2162
-1059
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

188 files changed

+2162
-1059
lines changed

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Guidelines for modifications:
1919

2020
---
2121

22+
* Antoine Richard
2223
* Antonio Serrano-Muñoz
2324
* Ben Johnston
2425
* Brian McCann

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Table of Contents
150150
source/migration/migrating_from_isaacgymenvs
151151
source/migration/migrating_from_omniisaacgymenvs
152152
source/migration/migrating_from_orbit
153+
source/migration/migrating_to_isaaclab_3-0
153154

154155
.. toctree::
155156
:maxdepth: 1

docs/source/how-to/make_fixed_prim.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ For instance, to spawn an ANYmal robot and make it static in the simulation worl
100100
),
101101
)
102102
anymal_spawn_cfg.func(
103-
"/World/ANYmal", anymal_spawn_cfg, translation=(0.0, 0.0, 0.8), orientation=(1.0, 0.0, 0.0, 0.0)
103+
"/World/ANYmal", anymal_spawn_cfg, translation=(0.0, 0.0, 0.8), orientation=(0.0, 0.0, 0.0, 1.0)
104104
)
105105
106106
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
.. _migrating-to-isaaclab-3-0:
2+
3+
Migrating to IsaacLab 3.0
4+
=========================
5+
6+
.. currentmodule:: isaaclab
7+
8+
IsaacLab 3.0 introduces a significant change to quaternion ordering throughout the codebase.
9+
This guide explains what changed, why it matters, and how to update your code.
10+
11+
12+
What Changed: Quaternion Format
13+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14+
15+
**The quaternion format changed from WXYZ to XYZW.**
16+
17+
+------------------+----------------------------------+----------------------------------+
18+
| Component | Old Format (WXYZ) | New Format (XYZW) |
19+
+==================+==================================+==================================+
20+
| Order | ``(w, x, y, z)`` | ``(x, y, z, w)`` |
21+
+------------------+----------------------------------+----------------------------------+
22+
| Identity | ``(1.0, 0.0, 0.0, 0.0)`` | ``(0.0, 0.0, 0.0, 1.0)`` |
23+
+------------------+----------------------------------+----------------------------------+
24+
25+
26+
Why This Change?
27+
----------------
28+
29+
The new XYZW format aligns with:
30+
31+
- **Warp**: NVIDIA's spatial computing framework
32+
- **PhysX**: PhysX physics engine
33+
- **Newton**: Newton multi-solver framework
34+
35+
This alignment removes the need for internal quaternion conversions, making the code simpler,
36+
faster, and less error-prone.
37+
38+
39+
What You Need to Update
40+
~~~~~~~~~~~~~~~~~~~~~~~
41+
42+
Any hard-coded quaternion values in your code need to be converted from WXYZ to XYZW.
43+
This includes:
44+
45+
1. **Configuration files** - ``rot`` parameters in asset configs
46+
2. **Task definitions** - Goal poses, initial states
47+
3. **Controller parameters** - Target orientations
48+
4. **Documentation** - Code examples with quaternions
49+
50+
Also, if you were relying on the :func:`~isaaclab.utils.math.convert_quat` function to convert quaternions, this should
51+
no longer be needed. (This would happen if you were pulling values from the views directly.)
52+
53+
Example: Updating Asset Configuration
54+
-------------------------------------
55+
56+
**Before (WXYZ):**
57+
58+
.. code-block:: python
59+
60+
from isaaclab.assets import AssetBaseCfg
61+
62+
cfg = AssetBaseCfg(
63+
init_state=AssetBaseCfg.InitialStateCfg(
64+
pos=(0.0, 0.0, 0.5),
65+
rot=(1.0, 0.0, 0.0, 0.0), # OLD: w, x, y, z
66+
),
67+
)
68+
69+
**After (XYZW):**
70+
71+
.. code-block:: python
72+
73+
from isaaclab.assets import AssetBaseCfg
74+
75+
cfg = AssetBaseCfg(
76+
init_state=AssetBaseCfg.InitialStateCfg(
77+
pos=(0.0, 0.0, 0.5),
78+
rot=(0.0, 0.0, 0.0, 1.0), # NEW: x, y, z, w
79+
),
80+
)
81+
82+
83+
Using the Quaternion Finder Tool
84+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
85+
86+
We provide a tool to help you find and fix quaternions in your codebase automatically. This is not a bulletproof tool,
87+
but it should help you find most of the quaternions that need to be updated. You *should* review the results manually.
88+
89+
.. warning::
90+
Do not run the tool on the whole codebase! If you run the tool on our own packages (isaaclab, or isaaclab_tasks for
91+
instance) it will find all the quaternions that we already converted. This tool is only meant to be used on your own
92+
codebase with no overlap with our own packages.
93+
94+
Finding Quaternions
95+
-------------------
96+
97+
Run the tool to scan your code for potential quaternions:
98+
99+
.. code-block:: bash
100+
101+
# Scan the 'source' directory (default)
102+
python scripts/tools/find_quaternions.py
103+
104+
# Scan a specific path
105+
python scripts/tools/find_quaternions.py --path my_project/
106+
107+
# Compare against a different branch
108+
python scripts/tools/find_quaternions.py --base develop
109+
110+
.. tip::
111+
We recommend always running the tool with a custom base branch *and* a specific path.
112+
113+
114+
The tool will show you:
115+
116+
- Quaternions that haven't been updated (marked as ``UNCHANGED``)
117+
- Whether each looks like a WXYZ identity quaternion (``WXYZ_IDENTITY``)
118+
- Whether the format is likely WXYZ (``LIKELY_WXYZ``)
119+
120+
121+
Understanding the Output
122+
------------------------
123+
124+
.. code-block:: text
125+
126+
my_project/robot_cfg.py:42:8 ⚠ UNCHANGED [WXYZ_IDENTITY]
127+
Values: [1.0, 0.0, 0.0, 0.0]
128+
Source: rot=(1.0, 0.0, 0.0, 0.0),
129+
130+
This tells you:
131+
132+
- **File and line**: ``my_project/robot_cfg.py:42``
133+
- **Status**: ``UNCHANGED`` means this line hasn't been modified yet
134+
- **Flag**: ``WXYZ_IDENTITY`` means it's the identity quaternion in old WXYZ format
135+
- **Values**: The actual quaternion values found
136+
- **Source**: The line of code for context
137+
138+
139+
Filtering Results
140+
-----------------
141+
142+
Focus on specific types of quaternions:
143+
144+
.. code-block:: bash
145+
146+
# Only show identity quaternions [1, 0, 0, 0]
147+
python scripts/tools/find_quaternions.py --check-identity
148+
149+
# Only show quaternions likely in WXYZ format
150+
python scripts/tools/find_quaternions.py --likely-wxyz
151+
152+
# Show ALL potential quaternions (ignore format heuristics)
153+
python scripts/tools/find_quaternions.py --all-quats
154+
155+
156+
Fixing Quaternions Automatically
157+
--------------------------------
158+
159+
The tool can automatically convert quaternions from WXYZ to XYZW:
160+
161+
.. code-block:: bash
162+
163+
# Interactive mode: prompts before each fix
164+
python scripts/tools/find_quaternions.py --fix
165+
166+
# Only fix identity quaternions (safest option)
167+
python scripts/tools/find_quaternions.py --fix-identity-only
168+
169+
# Preview changes without applying them
170+
python scripts/tools/find_quaternions.py --fix --dry-run
171+
172+
# Apply all fixes without prompting
173+
python scripts/tools/find_quaternions.py --fix --force
174+
175+
176+
Interactive Fix Example
177+
-----------------------
178+
179+
When running with ``--fix``, you'll see something like:
180+
181+
.. code-block:: text
182+
183+
────────────────────────────────────────────────────────────────────────────────
184+
📍 my_project/robot_cfg.py:42 [WXYZ_IDENTITY]
185+
────────────────────────────────────────────────────────────────────────────────
186+
40 | init_state=AssetBaseCfg.InitialStateCfg(
187+
41 | pos=(0.0, 0.0, 0.5),
188+
>>> 42 | rot=(1.0, 0.0, 0.0, 0.0),
189+
43 | ),
190+
44 | )
191+
────────────────────────────────────────────────────────────────────────────────
192+
Change: [1.0, 0.0, 0.0, 0.0] → [0.0, 0.0, 0.0, 1.0]
193+
Result: rot=(0.0, 0.0, 0.0, 1.0),
194+
Apply this fix? [Y/n/a/q]:
195+
196+
Options:
197+
198+
- **Y** (yes): Apply this fix
199+
- **n** (no): Skip this one
200+
- **a** (all): Apply all remaining fixes without asking
201+
- **q** (quit): Stop fixing
202+
203+
204+
How the Tool Works
205+
------------------
206+
207+
The tool uses several techniques to find quaternions:
208+
209+
1. **Python files**: Parses the code using AST (Abstract Syntax Tree) to find
210+
4-element tuples and lists with numeric values.
211+
212+
2. **JSON files**: Uses regex to find 4-element arrays.
213+
214+
3. **RST documentation**: Searches for quaternion-like patterns in docs.
215+
216+
To identify if something is a quaternion, the tool checks:
217+
218+
- Is it exactly 4 numeric values?
219+
- Does the sum of squares ≈ 1? (unit quaternion property)
220+
- Does it match known patterns like identity quaternions?
221+
222+
To determine if it's in WXYZ format:
223+
224+
- Is the first value 1.0 and rest are 0? (WXYZ identity)
225+
- Is the first value a common cos(θ/2) value like 0.707, 0.866, etc.?
226+
- Is the pattern consistent with first-element being the scalar part?
227+
228+
229+
Best Practices for Migration
230+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
231+
232+
1. **Start with a clean git state** - Commit your work before running fixes.
233+
234+
2. **Run the tool first without ``--fix``** - Review what will be changed.
235+
236+
3. **Fix identity quaternions first** - They're the most common and safest:
237+
238+
.. code-block:: bash
239+
240+
python scripts/tools/find_quaternions.py --fix-identity-only
241+
242+
4. **Review non-identity quaternions manually** - Some 4-element lists might
243+
not be quaternions (e.g., RGBA colors, bounding boxes).
244+
245+
5. **Test your code** - Run your simulations to verify everything works correctly.
246+
247+
6. **Check documentation** - Update any docs or comments that mention quaternion format.
248+
249+
API Changes
250+
~~~~~~~~~~~
251+
252+
The ``convert_quat`` function has been removed
253+
----------------------------------------------
254+
255+
Previously, IsaacLab had a utility function to convert between quaternion formats:
256+
257+
.. code-block:: python
258+
259+
# OLD - No longer needed
260+
from isaaclab.utils.math import convert_quat
261+
quat_xyzw = convert_quat(quat_wxyz, "xyzw")
262+
263+
Since everything now uses XYZW natively, this function is no longer needed.
264+
If you were using it, simply remove the conversion calls.
265+
266+
267+
Math utility functions now expect XYZW
268+
--------------------------------------
269+
270+
All quaternion functions in :mod:`isaaclab.utils.math` now expect and return
271+
quaternions in XYZW format:
272+
273+
- :func:`~isaaclab.utils.math.quat_mul`
274+
- :func:`~isaaclab.utils.math.quat_apply`
275+
- :func:`~isaaclab.utils.math.quat_from_euler_xyz`
276+
- :func:`~isaaclab.utils.math.euler_xyz_from_quat`
277+
- :func:`~isaaclab.utils.math.quat_from_matrix`
278+
- :func:`~isaaclab.utils.math.matrix_from_quat`
279+
- And all other quaternion utilities
280+
281+
282+
Need Help?
283+
~~~~~~~~~~
284+
285+
If you encounter issues during migration:
286+
287+
1. Check the `IsaacLab GitHub Issues <https://github.com/isaac-sim/IsaacLab/issues>`_
288+
2. Review the `CHANGELOG <https://github.com/isaac-sim/IsaacLab/blob/main/source/isaaclab/docs/CHANGELOG.rst>`_
289+
3. Join the community on `Discord <https://discord.gg/nvidiaomniverse>`_

scripts/benchmarks/benchmark_view_comparison.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
from isaacsim.core.simulation_manager import SimulationManager
7474

7575
import isaaclab.sim as sim_utils
76-
import isaaclab.utils.math as math_utils
7776
from isaaclab.sim.views import XformPrimView
7877

7978

@@ -168,8 +167,6 @@ def benchmark_view(view_type: str, num_iterations: int) -> tuple[dict[str, float
168167
transforms = view.get_transforms()
169168
positions = transforms[:, :3]
170169
orientations = transforms[:, 3:7]
171-
# Convert quaternion from xyzw to wxyz
172-
orientations = math_utils.convert_quat(orientations, to="wxyz")
173170
timing_results["get_world_poses"] = (time.perf_counter() - start_time) / num_iterations
174171

175172
# Store initial world poses
@@ -184,9 +181,7 @@ def benchmark_view(view_type: str, num_iterations: int) -> tuple[dict[str, float
184181
if view_type in ("xform", "xform_fabric"):
185182
view.set_world_poses(new_positions, orientations)
186183
else: # physx
187-
# Convert quaternion from wxyz to xyzw for PhysX
188-
orientations_xyzw = math_utils.convert_quat(orientations, to="xyzw")
189-
new_transforms = torch.cat([new_positions, orientations_xyzw], dim=-1)
184+
new_transforms = torch.cat([new_positions, orientations], dim=-1)
190185
view.set_transforms(new_transforms, indices=all_indices)
191186
timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations
192187

@@ -196,7 +191,7 @@ def benchmark_view(view_type: str, num_iterations: int) -> tuple[dict[str, float
196191
else: # physx
197192
transforms_after = view.get_transforms()
198193
positions_after_set = transforms_after[:, :3]
199-
orientations_after_set = math_utils.convert_quat(transforms_after[:, 3:7], to="wxyz")
194+
orientations_after_set = transforms_after[:, 3:7]
200195
computed_results["world_positions_after_set"] = positions_after_set.clone()
201196
computed_results["world_orientations_after_set"] = orientations_after_set.clone()
202197

scripts/demos/bin_packing.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,7 @@ def reset_object_collections(
171171

172172
# Compose new orientations by applying the sampled euler noise in quaternion space.
173173
orientations_delta = math_utils.quat_from_euler_xyz(samples[..., 3], samples[..., 4], samples[..., 5])
174-
orientations = math_utils.convert_quat(orientations, to="wxyz")
175174
orientations = math_utils.quat_mul(orientations, orientations_delta)
176-
orientations = math_utils.convert_quat(orientations, to="xyzw")
177175

178176
# velocities
179177
new_velocities = sel_view_states[:, 7:13]
@@ -239,7 +237,7 @@ def build_grocery_defaults(
239237
grid_y, grid_x = torch.meshgrid(y, x, indexing="ij")
240238
grid_z = CACHE_HEIGHT * torch.ones_like(grid_x)
241239
# We can then create the poses for the cached groceries.
242-
ref_quat = torch.tensor([[1.0, 0.0, 0.0, 0.0]], device=device).repeat(num_x_objects * num_y_objects, 1)
240+
ref_quat = torch.tensor([[0.0, 0.0, 0.0, 1.0]], device=device).repeat(num_x_objects * num_y_objects, 1)
243241
positions = torch.stack((grid_x.flatten(), grid_y.flatten(), grid_z.flatten()), dim=-1)
244242
poses = torch.cat((positions, ref_quat), dim=-1)
245243
# Duplicate across environments, cap at max_num_objects

scripts/environments/state_machine/lift_cube_sm.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,6 @@ def reset_idx(self, env_ids: Sequence[int] = None):
221221

222222
def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_pose: torch.Tensor) -> torch.Tensor:
223223
"""Compute the desired state of the robot's end-effector and the gripper."""
224-
# convert all transformations from (w, x, y, z) to (x, y, z, w)
225-
ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]]
226-
object_pose = object_pose[:, [0, 1, 2, 4, 5, 6, 3]]
227-
des_object_pose = des_object_pose[:, [0, 1, 2, 4, 5, 6, 3]]
228224

229225
# convert to warp
230226
ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform)
@@ -250,10 +246,8 @@ def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_p
250246
device=self.device,
251247
)
252248

253-
# convert transformations back to (w, x, y, z)
254-
des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]]
255249
# convert to torch
256-
return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1)
250+
return torch.cat([self.des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1)
257251

258252

259253
def main():

0 commit comments

Comments
 (0)