+
+ This environment does not support RL training or playback. The command runs it with the
+ zero-action agent instead.
+
@@ -135,7 +145,6 @@ Available Tasks
-
diff --git a/docs/source/setup/installation/include/pip_extras_note.rst b/docs/source/setup/installation/include/pip_extras_note.rst
index 2623a0180bf8..e97ef912b9bc 100644
--- a/docs/source/setup/installation/include/pip_extras_note.rst
+++ b/docs/source/setup/installation/include/pip_extras_note.rst
@@ -1,5 +1,6 @@
.. note::
- The ``isaaclab`` pip wheel bundles all Isaac Lab extensions. Install with
- ``[all]`` for the full workflow: it carries Isaac Sim, both OV backends, every RL
- library, and every visualizer.
+ The ``isaaclab`` pip wheel bundles all Isaac Lab extensions. The ``[all]`` extra is the
+ curated ``ov``, ``rl-games``, ``sb3``, ``skrl``, ``rsl-rl``, ``rerun``, and ``viser`` list.
+ It does not include Isaac Sim or the standalone importers; request ``[isaacsim]`` or
+ ``[importers]`` separately.
diff --git a/docs/source/setup/installation/include/src_clone_isaaclab.rst b/docs/source/setup/installation/include/src_clone_isaaclab.rst
index ef8a0e6a25bb..794ad2be60c1 100644
--- a/docs/source/setup/installation/include/src_clone_isaaclab.rst
+++ b/docs/source/setup/installation/include/src_clone_isaaclab.rst
@@ -24,7 +24,7 @@ We provide helper executables at the repository root — ``./isaaclab.sh`` (Linu
./isaaclab.sh --help
- usage: isaaclab.sh [-h] [-i [INSTALL]] [-f] [-p ...] [-s ...] [-t ...] [-o ...] [-v] [-d] [-n ...] [-c [CONDA]] [-u [UV]]
+ usage: isaaclab.sh [-h] [-i [INSTALL]] [-f] [-p ...] [-s ...] [-t ...] [-o ...] [-v] [-d] [-n ...] [-c [CONDA]] [-u [UV]] [--isaacsim_source PATH]
Isaac Lab CLI
@@ -55,6 +55,9 @@ We provide helper executables at the repository root — ``./isaaclab.sh`` (Linu
-c [CONDA], --conda [CONDA]
Create a new conda environment for Isaac Lab. Default name is 'env_isaaclab'.
-u [UV], --uv [UV] Create a new uv environment for Isaac Lab. Default name is 'env_isaaclab'.
+ --isaacsim_source PATH
+ Incrementally build the Isaac Sim source checkout at PATH and link its live release
+ tree as '_isaac_sim'. Python commands keep using the active uv environment.
.. tab-item:: :icon:`fa-brands fa-windows` Windows
:sync: windows
@@ -63,7 +66,7 @@ We provide helper executables at the repository root — ``./isaaclab.sh`` (Linu
isaaclab.bat --help
- usage: isaaclab.bat [-h] [-i [INSTALL]] [-f] [-p ...] [-s ...] [-t ...] [-o ...] [-v] [-d] [-n ...] [-c [CONDA]] [-u [UV]]
+ usage: isaaclab.bat [-h] [-i [INSTALL]] [-f] [-p ...] [-s ...] [-t ...] [-o ...] [-v] [-d] [-n ...] [-c [CONDA]] [-u [UV]] [--isaacsim_source PATH]
Isaac Lab CLI
@@ -94,3 +97,6 @@ We provide helper executables at the repository root — ``./isaaclab.sh`` (Linu
-c [CONDA], --conda [CONDA]
Create a new conda environment for Isaac Lab. Default name is 'env_isaaclab'.
-u [UV], --uv [UV] Create a new uv environment for Isaac Lab. Default name is 'env_isaaclab'.
+ --isaacsim_source PATH
+ Incrementally build the Isaac Sim source checkout at PATH and link its live release
+ tree as '_isaac_sim'. Python commands keep using the active uv environment.
diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst
index 0249906c4c71..8029915584b6 100644
--- a/docs/source/setup/installation/index.rst
+++ b/docs/source/setup/installation/index.rst
@@ -225,15 +225,15 @@ Install ``uv``, clone Isaac Lab, and start a workflow:
option includes the selected optional integration in the command's environment. Place it
before ``isaaclab``; for example, ``--extra ov`` installs both ovphysx and ovrtx
backends. Pass a comma-separated list or repeat ``--extra``. No extras conflict, so
-any combination resolves into one environment. The ``--extra all`` shortcut installs a
-curated set of backends, RL libraries, and visualizers. It does not include the specialized
-extras ``rlinf``, ``mimic``, ``teleop``, ``tetrahedralization``, ``video``, and ``leapp``;
-request them by name:
+any combination resolves into one environment. The ``--extra all`` shortcut installs the
+curated ``ov``, ``rl-games``, ``sb3``, ``skrl``, ``rsl-rl``, ``rerun``, and ``viser`` extras.
+It does not include Isaac Sim or the specialized ``rlinf``, ``mimic``, ``teleop``,
+``tetrahedralization``, ``video``, and ``leapp`` extras; request them by name:
.. code-block:: bash
uv run --extra all isaaclab train --rl_library rsl_rl \
- --task Isaac-Cartpole-Direct physics=isaacsim_physx
+ --task Isaac-Cartpole-Direct physics=ovphysx
See :ref:`installation-optional-extras` for the available extras.
@@ -574,9 +574,10 @@ or temporary work.
Optional extras
~~~~~~~~~~~~~~~
-Add extras to the package requirement when your project needs them. For a standalone environment,
-use ``uv pip install "isaaclab[]"``; for a uv project, use
-``uv add "isaaclab[]"``.
+Add extras only when your project needs them. Most extras work with
+``uv pip install "isaaclab[]"`` in a standalone environment or
+``uv add "isaaclab[]"`` in a uv project. The ``importers`` and ``isaacsim`` extras
+have dedicated commands below.
.. list-table::
:header-rows: 1
@@ -601,20 +602,45 @@ use ``uv pip install "isaaclab[]"``; for a uv project, use
- Mesh tetrahedralization / video recording.
* - ``leapp``
- LEAP model export support.
+ * - ``importers``
+ - Standalone URDF and MJCF conversion without Isaac Sim.
* - ``all``
- - A curated set of backends, RL libraries, and visualizers: ``isaacsim``, ``ov``, ``rl-games``,
- ``sb3``, ``skrl``, ``rsl-rl``, ``rerun``, and ``viser``.
+ - The curated ``ov``, ``rl-games``, ``sb3``, ``skrl``, ``rsl-rl``, ``rerun``, and ``viser``
+ extras. Isaac Sim is not included.
* - ``test``
- Developer test and documentation tooling.
-Extras can be combined freely: none of them conflict, so any set of extras -- including
-the Isaac Sim and OV backend stacks together -- resolves into a single environment.
-Use ``all`` to install the curated set of backends, RL libraries, and visualizers listed
-above with one flag. The specialized extras (``rlinf``, ``mimic``, ``teleop``,
-``tetrahedralization``, ``video``, ``leapp``) and the developer ``test`` tooling are not
-part of ``all``; request them by name.
+Use ``all`` for the curated list above. Isaac Sim, standalone importers, specialized extras
+(``rlinf``, ``mimic``, ``teleop``, ``tetrahedralization``, ``video``, ``leapp``), and the
+developer ``test`` tooling remain opt-in.
-.. isaaclab-uv-wheel-install::
+.. _installation-importers-extra:
+
+Installing the ``importers`` extra
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Install this extra to convert URDF and MJCF files without Isaac Sim.
+
+.. warning::
+
+ Use the full command below. Without the overrides, the importer extra can downgrade packages
+ used by the base Isaac Lab install. The overrides keep Isaac Lab's tested versions.
+
+.. isaaclab-uv-importers-wheel-install::
+
+Installing the ``isaacsim`` extra
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Isaac Sim 6.0 pins dependencies that conflict with Isaac Lab. Install the ``isaacsim`` extra with
+the tested overrides:
+
+.. isaaclab-uv-isaacsim-wheel-install::
+
+Add other extras inside the brackets when needed; for example, use
+``isaaclab[isaacsim,all]`` to include the curated ``all`` list.
+
+Installing CUDA-enabled PyTorch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Install the CUDA-enabled PyTorch build appropriate for your system architecture:
@@ -777,98 +803,132 @@ On Windows, enable `long-path support
`__
before building.
+Choose how to connect the Isaac Sim source build to Isaac Lab:
+
.. tab-set::
- :sync-group: installation-platform
+ :sync-group: isaacsim-source-installation-method
- .. tab-item:: :icon:`fa-brands fa-linux` Linux (x86_64)
- :sync: linux-x86_64
+ .. tab-item:: uv (Recommended)
+ :sync: uv
- .. code-block:: bash
+ Clone Isaac Sim next to the Isaac Lab checkout. From the Isaac Lab root, run the source-build
+ command. It incrementally builds Isaac Sim and links the live release tree as ``_isaac_sim``:
- git clone https://github.com/isaac-sim/IsaacSim.git
- cd IsaacSim
- ./build.sh
- export ISAACSIM_PATH="${PWD}/_build/linux-x86_64/release"
- export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh"
- ${ISAACSIM_PATH}/isaac-sim.sh
- ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')"
- ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.experimental.api/add_cubes.py
+ .. code-block:: text
- .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64)
- :sync: linux-aarch64
+ git clone https://github.com/isaac-sim/IsaacSim.git ../IsaacSim
+ uv run isaaclab --isaacsim_source ../IsaacSim
- .. code-block:: bash
+ Isaac Lab runs the active ``uv`` environment through Isaac Sim's generated Python launcher.
+ This loads Kit and extensions directly from the source build without creating wheels or
+ changing ``pyproject.toml`` and ``uv.lock``. Run Isaac Lab against the source build with:
- git clone https://github.com/isaac-sim/IsaacSim.git
- cd IsaacSim
- ./build.sh
- export ISAACSIM_PATH="${PWD}/_build/linux-aarch64/release"
- export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh"
- ${ISAACSIM_PATH}/isaac-sim.sh
- ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')"
- ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.experimental.api/add_cubes.py
+ .. code-block:: text
- .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64)
- :sync: windows-x86_64
+ uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Direct physics=isaacsim_physx
- .. code-block:: batch
+ After changing Isaac Sim source, run the same ``--isaacsim_source`` command again. The native
+ build is incremental, and the link continues to expose the updated build immediately; no
+ wheel packaging or dependency resolution step is required.
- git clone https://github.com/isaac-sim/IsaacSim.git
- cd IsaacSim
- build.bat
- set ISAACSIM_PATH="%cd%\_build\windows-x86_64\release"
- set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat"
- %ISAACSIM_PATH%\isaac-sim.bat
- %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')"
- %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.experimental.api\add_cubes.py
+ .. tab-item:: isaaclab.sh / isaaclab.bat
+ :sync: isaaclab-script
-Return to the workspace containing the ``IsaacSim`` checkout, then clone Isaac Lab, link it to the
-source build, install, and verify:
+ Build and verify Isaac Sim for your platform:
-.. code-block:: text
+ .. tab-set::
+ :sync-group: installation-platform
- cd ..
+ .. tab-item:: :icon:`fa-brands fa-linux` Linux (x86_64)
+ :sync: linux-x86_64
-.. isaaclab-clone-commands::
+ .. code-block:: bash
-.. tab-set::
- :sync-group: installation-platform
+ git clone https://github.com/isaac-sim/IsaacSim.git
+ cd IsaacSim
+ ./build.sh
+ export ISAACSIM_PATH="${PWD}/_build/linux-x86_64/release"
+ export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh"
+ ${ISAACSIM_PATH}/isaac-sim.sh
+ ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')"
+ ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.experimental.api/add_cubes.py
- .. tab-item:: :icon:`fa-brands fa-linux` Linux (x86_64)
- :sync: linux-x86_64
+ .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64)
+ :sync: linux-aarch64
- .. code-block:: bash
+ .. code-block:: bash
- cd IsaacLab
- ln -s ${ISAACSIM_PATH} _isaac_sim
- sudo apt install cmake build-essential
- ./isaaclab.sh -i
- ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py --viz kit
+ git clone https://github.com/isaac-sim/IsaacSim.git
+ cd IsaacSim
+ ./build.sh
+ export ISAACSIM_PATH="${PWD}/_build/linux-aarch64/release"
+ export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh"
+ ${ISAACSIM_PATH}/isaac-sim.sh
+ ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')"
+ ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.experimental.api/add_cubes.py
- .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64)
- :sync: linux-aarch64
+ .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64)
+ :sync: windows-x86_64
- .. code-block:: bash
+ .. code-block:: batch
- cd IsaacLab
- ln -s ${ISAACSIM_PATH} _isaac_sim
- sudo apt install cmake build-essential python3.12-dev libgl1-mesa-dev libx11-dev \
- libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev
- ./isaaclab.sh -i
- ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py --viz kit
+ git clone https://github.com/isaac-sim/IsaacSim.git
+ cd IsaacSim
+ build.bat
+ set ISAACSIM_PATH="%cd%\_build\windows-x86_64\release"
+ set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat"
+ %ISAACSIM_PATH%\isaac-sim.bat
+ %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')"
+ %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.experimental.api\add_cubes.py
- .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64)
- :sync: windows-x86_64
+ Return to the workspace containing the ``IsaacSim`` checkout, then clone Isaac Lab:
- .. code-block:: batch
+ .. code-block:: text
- cd IsaacLab
- mklink /D _isaac_sim %ISAACSIM_PATH%
- isaaclab.bat -i
- isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py --viz kit
+ cd ..
+
+ .. isaaclab-clone-commands::
+
+ Link Isaac Lab to the source build, install, and verify:
+
+ .. tab-set::
+ :sync-group: installation-platform
+
+ .. tab-item:: :icon:`fa-brands fa-linux` Linux (x86_64)
+ :sync: linux-x86_64
+
+ .. code-block:: bash
+
+ cd IsaacLab
+ ln -s ${ISAACSIM_PATH} _isaac_sim
+ sudo apt install cmake build-essential
+ ./isaaclab.sh -i
+ ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py --viz kit
+
+ .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64)
+ :sync: linux-aarch64
+
+ .. code-block:: bash
+
+ cd IsaacLab
+ ln -s ${ISAACSIM_PATH} _isaac_sim
+ sudo apt install cmake build-essential python3.12-dev libgl1-mesa-dev libx11-dev \
+ libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev
+ ./isaaclab.sh -i
+ ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py --viz kit
+
+ .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64)
+ :sync: windows-x86_64
+
+ .. code-block:: batch
+
+ cd IsaacLab
+ mklink /D _isaac_sim %ISAACSIM_PATH%
+ isaaclab.bat -i
+ isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py --viz kit
-The tutorial command should open a black simulator viewport. Use the binary-installation
-troubleshooting links above if the source build does not launch.
+ The tutorial command should open a black simulator viewport. Use the binary-installation
+ troubleshooting links above if the source build does not launch.
.. _installation-method-container:
diff --git a/docs/source/setup/quickstart.rst b/docs/source/setup/quickstart.rst
index 3713595a3602..285163920317 100644
--- a/docs/source/setup/quickstart.rst
+++ b/docs/source/setup/quickstart.rst
@@ -49,9 +49,10 @@ Training outputs, including checkpoints, are saved under ``logs/``. Add
Extras make optional capabilities available; task selectors choose which capabilities the task uses
For example, ``--extra ovphysx`` makes the OV PhysX integration available, while
``physics=ovphysx`` selects it for the task. You can combine extras as needed. The ``--extra all``
- shortcut installs a curated set of backends, RL libraries, and visualizers.
- Specialized extras such as ``rlinf``, ``mimic``, ``teleop``, ``tetrahedralization``, ``video``,
- and ``leapp`` are not included; add them explicitly when needed. See
+ shortcut installs the curated ``ov``, ``rl-games``, ``sb3``, ``skrl``, ``rsl-rl``, ``rerun``,
+ and ``viser`` extras. Isaac Sim, standalone importers, and specialized extras such as ``rlinf``,
+ ``mimic``, ``teleop``, ``tetrahedralization``, ``video``, and ``leapp`` are not included; add
+ them explicitly. See
:ref:`installation-optional-extras` for the complete list.
Choose an RL library
diff --git a/pyproject.toml b/pyproject.toml
index 0ee6fb31762c..f7fd71b6ed39 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -71,10 +71,6 @@ dependencies = [
# one environment overwrite each other's files, and removing either then breaks ``pxr``.
# usd-exchange 2.3.0 vendors USD 25.5, matching the Isaac Sim 6.0 wheel stack.
"usd-exchange==2.3.0",
- # Standalone URDF/MJCF importers, so conversion works without Isaac Sim. They install
- # alongside it without displacing it: Kit serves ``isaacsim.asset`` from its extension
- # roots when the runtime is present, and the standalone packages serve it otherwise.
- "isaacsim-asset-isolated>=6.0,<6.1",
# avoid broken hf-xet pre-release cached on NVIDIA Artifactory
"hf-xet>=1.4.1,<2.0.0 ; platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'aarch64'",
# ----- tasks -----
@@ -118,6 +114,11 @@ tetrahedralization = ["pytetwild[all]>=0.3.0,<0.4"]
video = ["moviepy>=1.0.3,<2.0.0.dev0"]
+importers = [
+ "isaacsim-asset-isolated>=6.0,<6.1",
+ "tinyobjloader==2.0.0rc13",
+]
+
test = [
"pytest",
"pytest-mock",
@@ -193,12 +194,8 @@ rlinf = [
leapp = [
"leapp>=0.5.2",
]
-# Every backend, RL library, and visualizer in one flag. No extra is forked in
-# [tool.uv].conflicts, so any combination resolves into a single environment. The
-# specialized extras (rlinf, mimic, teleop, tetrahedralization, video, leapp)
-# and the developer ``test`` tooling stay opt-in by name.
all = [
- "isaaclab-dev[sb3,skrl,rl-games,rsl-rl,viser,rerun,isaacsim,ov]",
+ "isaaclab-dev[sb3,skrl,rl-games,rsl-rl,viser,rerun,ov]",
]
# Single source of truth for externally-pinned versions, read by docs/conf.py, the
@@ -390,8 +387,8 @@ environments = [
"sys_platform == 'win32' and platform_machine == 'AMD64'",
]
# Isaac Lab owns the Newton, MuJoCo, and torch versions. Overrides replace requirements
-# for every requester, unlike constraints, which only intersect. The ``isaacsim`` extra
-# pins older Newton, MuJoCo, and torch versions, so these overrides prevent downgrades.
+# for every requester, unlike constraints, which only intersect. The ``isaacsim`` and
+# ``importers`` extras pin older versions of these packages, so the overrides prevent downgrades.
# Torch routes through [tool.uv.sources]. Values mirror [tool.isaaclab.versions] where applicable.
override-dependencies = [
"numpy>=2",
diff --git a/skills/user/install-isaac-lab/reference.md b/skills/user/install-isaac-lab/reference.md
index 0501cea5f9ac..d58ee01b010b 100644
--- a/skills/user/install-isaac-lab/reference.md
+++ b/skills/user/install-isaac-lab/reference.md
@@ -59,12 +59,25 @@ Read `docs/source/setup/installation/index.rst` "System requirements" from the c
Run the docs-defined minimal verification command after every install, before larger tests. The command varies by route:
-- Automatic uv (`installation-method-uv`), legacy installer (`installation-legacy-installer`), managed Python env (`installation-method-python-env`), Isaac Lab wheel (`installation-method-wheel`), and Isaac Sim source build (`installation-method-source`) verify Isaac Lab via the tutorial script documented in the section's included verification snippet:
+- Automatic uv (`installation-method-uv`), legacy installer (`installation-legacy-installer`), managed Python env (`installation-method-python-env`), and Isaac Lab wheel (`installation-method-wheel`) verify Isaac Lab via the tutorial script documented in the section's included verification snippet:
```bash
uv run python scripts/tutorials/00_sim/create_empty.py --viz kit
```
+- Isaac Sim source build (`installation-method-source`) runs the same script against the locally built Isaac Sim wheels:
+
+```bash
+uv run --extra isaacsim-local python scripts/tutorials/00_sim/create_empty.py --viz kit
+```
+
+ This only uses the local build when `pyproject.toml` carries both edits that
+ `uv run isaaclab --isaacsim_source ` writes: `find-links = ["_isaac_sim_wheels"]` under
+ `[tool.uv]`, and an `isaacsim-local` extra pinning the exact version from
+ `_isaac_sim_wheels/isaacsim-*.whl`. Without the pin, uv resolves the published wheels from
+ `pypi.nvidia.com` instead, because source builds carry pre-release local versions that sort below
+ the release.
+
- Downloaded Isaac Sim package (`installation-method-binary`) uses the bundled-Python verification documented in the section (launch via `${ISAACSIM_PATH}/isaac-sim.sh`, then run the tutorial script from the checkout).
- Docker (`installation-method-container`) runs the same tutorial verification inside the container as documented in `docs/source/features/docker_cloud.rst`.
diff --git a/skills/user/setup-troubleshooting/SKILL.md b/skills/user/setup-troubleshooting/SKILL.md
index ad17f2cc1e7d..5c78c73590e5 100644
--- a/skills/user/setup-troubleshooting/SKILL.md
+++ b/skills/user/setup-troubleshooting/SKILL.md
@@ -20,7 +20,7 @@ Do not duplicate installation or troubleshooting docs in this skill. The officia
1. Identify the install mode: automatic uv, legacy installer script, managed Python environment, Python package, downloaded Isaac Sim package, source build, Docker, cloud, or backend-specific setup. For a new full-feature Isaac Sim setup, prefer the automatic uv installation guide.
2. Identify OS, Python environment, GPU/driver context, Isaac Sim source, and target backend.
3. Read the matching installation guide and troubleshooting reference before prescribing commands.
-4. From the Isaac Lab checkout, use documented uv commands such as `uv run python`, `uv run isaaclab train`, and `uv run isaaclab play` for Python, verification, and RL entry points. XR teleoperation entry points are `uv run --extra teleop isaaclab teleop run|record|replay`; `teleop` cannot be combined with the `mimic` or `all` extras in one command.
+4. From the Isaac Lab checkout, use documented uv commands such as `uv run python`, `uv run isaaclab train`, and `uv run isaaclab play` for Python, verification, and RL entry points. The `all` extra is the curated `ov`, `rl-games`, `sb3`, `skrl`, `rsl-rl`, `rerun`, and `viser` list; it excludes Isaac Sim and the standalone URDF/MJCF importers. Install the `importers` extra with the documented resolver overrides. XR teleoperation entry points are `uv run --extra teleop isaaclab teleop run|record|replay`; `teleop` cannot be combined with the `mimic` or `all` extras in one command.
5. Use suffixless task names in verification and training commands.
6. Ask for the smallest relevant error output when the failure mode is unclear.
7. Prefer a minimal verification command before running examples, training, or rendering workflows.
diff --git a/source/isaaclab/changelog.d/all-extra-without-isaacsim.major.rst b/source/isaaclab/changelog.d/all-extra-without-isaacsim.major.rst
new file mode 100644
index 000000000000..84998e9b74fc
--- /dev/null
+++ b/source/isaaclab/changelog.d/all-extra-without-isaacsim.major.rst
@@ -0,0 +1,12 @@
+Changed
+^^^^^^^
+
+* **Breaking:** Changed the ``isaaclab[all]`` extra to exclude Isaac Sim. Install
+ ``isaaclab[isaacsim]`` with the documented resolver overrides when Isaac Sim is required.
+* **Breaking:** Moved the standalone URDF/MJCF importers from the base wheel to the
+ ``isaaclab[importers]`` extra. Use the documented override command when installing it.
+
+Fixed
+^^^^^
+
+* Fixed Newton actuator imports with the minimum Newton versions supported by the wheel.
diff --git a/source/isaaclab/changelog.d/normalize-colorized-semantic-segmentation.rst b/source/isaaclab/changelog.d/normalize-colorized-semantic-segmentation.rst
new file mode 100644
index 000000000000..6e374ee6bd4c
--- /dev/null
+++ b/source/isaaclab/changelog.d/normalize-colorized-semantic-segmentation.rst
@@ -0,0 +1,5 @@
+Fixed
+^^^^^
+
+* Fixed :func:`isaaclab.envs.mdp.image` so colorized semantic-segmentation observations are
+ converted to normalized ``float32`` tensors when ``normalize=True``.
diff --git a/source/isaaclab/changelog.d/uv-friendly-isaacsim-source-build.minor.rst b/source/isaaclab/changelog.d/uv-friendly-isaacsim-source-build.minor.rst
new file mode 100644
index 000000000000..8a6fc94e69a7
--- /dev/null
+++ b/source/isaaclab/changelog.d/uv-friendly-isaacsim-source-build.minor.rst
@@ -0,0 +1,8 @@
+Added
+^^^^^
+
+* Added the ``--isaacsim_source`` CLI option, which incrementally builds Isaac Sim from a source checkout,
+ links its live release tree into the repository as ``_isaac_sim``, and runs Python commands with
+ the active environment through Isaac Sim's generated launcher. This avoided rebuilding and
+ installing Python wheels after every incremental native build and left ``pyproject.toml`` and
+ ``uv.lock`` unchanged.
diff --git a/source/isaaclab/isaaclab/actuators/newton/adapter.py b/source/isaaclab/isaaclab/actuators/newton/adapter.py
index 7498334c01ac..9dc50f833ae2 100644
--- a/source/isaaclab/isaaclab/actuators/newton/adapter.py
+++ b/source/isaaclab/isaaclab/actuators/newton/adapter.py
@@ -475,7 +475,24 @@ def __init__(self, num_envs: int, num_joints: int, device: str):
get_actuator_parameter = ArticulationView.get_actuator_parameter
set_actuator_parameter = ArticulationView.set_actuator_parameter
_get_actuator_dof_mapping = ArticulationView._get_actuator_dof_mapping
- _resolve_world_mask = ArticulationView._resolve_world_mask
+
+ def _resolve_world_mask(self, mask: Sequence[bool] | wp.array | None) -> wp.array:
+ """Normalize a world mask independently of the installed Newton version."""
+ if mask is None:
+ return self.full_mask
+ if isinstance(mask, wp.array):
+ if mask.dtype is not wp.bool:
+ raise ValueError(f"Expected Boolean mask, got dtype {mask.dtype}")
+ if mask.shape != (self.world_count,):
+ raise ValueError(f"Expected mask shape ({self.world_count},), got {mask.shape}")
+ if mask.device != self.device:
+ raise ValueError(f"Expected mask on device {self.device}, got {mask.device}")
+ return mask
+
+ try:
+ return wp.array(mask, dtype=wp.bool, shape=(self.world_count,), device=self.device, copy=False)
+ except Exception as error:
+ raise ValueError(f"Expected Boolean mask with shape ({self.world_count},)") from error
@dataclass(frozen=True)
diff --git a/source/isaaclab/isaaclab/cli/__init__.py b/source/isaaclab/isaaclab/cli/__init__.py
index 56549fdadc88..3f90fc5d3554 100644
--- a/source/isaaclab/isaaclab/cli/__init__.py
+++ b/source/isaaclab/isaaclab/cli/__init__.py
@@ -17,6 +17,7 @@
)
from .commands.misc import (
command_build_docs,
+ command_build_isaacsim,
command_new,
command_run_docker,
command_run_isaacsim,
@@ -262,6 +263,14 @@ def cli() -> None:
const="env_isaaclab",
help="Create a new uv environment for Isaac Lab. Default name is 'env_isaaclab'.",
)
+ parser.add_argument(
+ "--isaacsim_source",
+ metavar="PATH",
+ help=(
+ "Incrementally build the Isaac Sim source checkout at PATH and link its live release\n"
+ "tree as '_isaac_sim'. Python commands keep using the active uv environment."
+ ),
+ )
args = parser.parse_args()
@@ -277,6 +286,9 @@ def cli() -> None:
elif args.uv:
command_setup_uv(args.uv)
+ elif args.isaacsim_source:
+ command_build_isaacsim(args.isaacsim_source)
+
elif args.vscode:
command_vscode_settings()
diff --git a/source/isaaclab/isaaclab/cli/commands/misc.py b/source/isaaclab/isaaclab/cli/commands/misc.py
index e1bff31762ab..44e684413c14 100644
--- a/source/isaaclab/isaaclab/cli/commands/misc.py
+++ b/source/isaaclab/isaaclab/cli/commands/misc.py
@@ -5,7 +5,10 @@
"""Misc commands"""
+import platform
import shutil
+import sys
+from pathlib import Path
from ..utils import (
ISAACLAB_ROOT,
@@ -118,6 +121,83 @@ def command_build_docs() -> None:
print_info(f"Open with: xdg-open {index_path}")
+def command_build_isaacsim(source_path: str) -> None:
+ """Build Isaac Sim from source and make it usable through ``uv`` (--isaacsim_source).
+
+ Runs Isaac Sim's incremental build and links its release tree into Isaac Lab as ``_isaac_sim``.
+ Python commands launched through the Isaac Lab CLI use the active environment's interpreter
+ through Isaac Sim's ``python.sh`` or ``python.bat`` wrapper, so they load the live build without
+ packaging or installing it as wheels.
+
+ Args:
+ source_path: Path to an Isaac Sim source checkout.
+ """
+ isaacsim_root = Path(source_path).expanduser().resolve()
+ build_script = isaacsim_root / ("build.bat" if is_windows() else "build.sh")
+
+ if not build_script.is_file():
+ print_error(f"'{isaacsim_root}' is not an Isaac Sim source checkout ({build_script.name} not found).")
+ print_info("Clone it first with: git clone https://github.com/isaac-sim/IsaacSim.git")
+ raise SystemExit(1)
+
+ print_info("Incrementally building Isaac Sim from source. This may take a while...")
+ run_command([str(build_script)], cwd=isaacsim_root)
+
+ release_dir = _resolve_isaacsim_release_dir(isaacsim_root)
+ python_launcher = release_dir / ("python.bat" if is_windows() else "python.sh")
+ if not python_launcher.is_file():
+ print_error(f"The Isaac Sim build did not produce {python_launcher}.")
+ raise SystemExit(1)
+
+ link_path = ISAACLAB_ROOT / "_isaac_sim"
+ if link_path.is_symlink() or link_path.exists():
+ if link_path.is_symlink():
+ link_path.unlink()
+ else:
+ print_error(f"{link_path} exists and is not a symbolic link. Remove it and re-run.")
+ raise SystemExit(1)
+ try:
+ link_path.symlink_to(release_dir, target_is_directory=True)
+ except OSError as error:
+ print_error(f"Could not link {link_path} to {release_dir}: {error}")
+ if is_windows():
+ print_info("Enable Windows Developer Mode or run from an elevated terminal, then retry.")
+ raise SystemExit(1) from error
+ print_info(f"Linked {link_path} -> {release_dir}")
+ _repoint_source_build_prebundles()
+
+ print_info("Isaac Sim is ready. Python commands now use the live source build through '_isaac_sim'.")
+ print_info("Run Isaac Lab against it with:")
+ print_info(" uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Direct physics=isaacsim_physx")
+
+
+def _resolve_isaacsim_release_dir(isaacsim_root: Path) -> Path:
+ """Resolve the platform-specific Isaac Sim release directory."""
+ machine = platform.machine().lower()
+ targets = {
+ ("linux", "amd64"): "linux-x86_64",
+ ("linux", "x86_64"): "linux-x86_64",
+ ("linux", "aarch64"): "linux-aarch64",
+ ("linux", "arm64"): "linux-aarch64",
+ ("win32", "amd64"): "windows-x86_64",
+ ("win32", "x86_64"): "windows-x86_64",
+ }
+ target = targets.get((sys.platform, machine))
+ if target is None:
+ print_error(f"Isaac Sim source builds are not supported on platform '{sys.platform}' with machine '{machine}'.")
+ raise SystemExit(1)
+ return isaacsim_root / "_build" / target / "release"
+
+
+def _repoint_source_build_prebundles() -> None:
+ """Keep Isaac Sim's prebundled packages from shadowing the active environment."""
+ # ``install`` imports ``command_vscode_settings`` from this module, so defer this import until
+ # both command modules are initialized. Reuse the same protection as the legacy installer.
+ from .install import _repoint_prebundle_packages
+
+ _repoint_prebundle_packages()
+
+
def command_run_docker(args: list[str]) -> None:
"""Run the docker container helper script (docker/container.py).
diff --git a/source/isaaclab/isaaclab/cli/utils.py b/source/isaaclab/isaaclab/cli/utils.py
index ced6877c3b10..11f15432c05b 100644
--- a/source/isaaclab/isaaclab/cli/utils.py
+++ b/source/isaaclab/isaaclab/cli/utils.py
@@ -6,6 +6,7 @@
import os
import platform
import shutil
+import site
import subprocess
import sys
import time
@@ -597,7 +598,37 @@ def run_python_command(
[subprocess.CompletedProcess] Result returned by ``subprocess.run``.
"""
- cmd = [extract_python_exe()]
+ python_exe = extract_python_exe()
+ cmd = [python_exe]
+
+ # A source build linked at ``_isaac_sim`` must load its live Kit and extension paths, but the
+ # dependencies managed by uv should still come from the active environment. Isaac Sim's Python
+ # launcher supports exactly this combination through its ``PYTHONEXE`` override. The shell
+ # wrappers already configure ``ISAAC_PATH`` before starting this CLI, so only direct invocations
+ # such as ``uv run isaaclab train`` need to delegate through the launcher here.
+ command_env = os.environ if env is None else env
+ configured_isaac_path = command_env.get("ISAAC_PATH")
+ local_sim = DEFAULT_ISAAC_SIM_PATH
+ python_launcher = local_sim / ("python.bat" if is_windows() else "python.sh")
+ isaac_env_active = (
+ configured_isaac_path is not None and Path(configured_isaac_path).resolve() == local_sim.resolve()
+ )
+ if local_sim.is_dir() and python_launcher.is_file() and not isaac_env_active:
+ env = dict(command_env)
+ env["PYTHONEXE"] = python_exe
+ source_paths = [
+ local_sim / "python_packages",
+ local_sim / "exts" / "isaacsim.simulation_app",
+ local_sim / "kit" / "kernel" / "py",
+ local_sim / "kit" / "plugins" / "bindings-python",
+ Path(site.getsitepackages()[0]),
+ ]
+ existing_pythonpath = env.get("PYTHONPATH")
+ python_paths = [str(path) for path in source_paths if path.is_dir()]
+ if existing_pythonpath:
+ python_paths.append(existing_pythonpath)
+ env["PYTHONPATH"] = os.pathsep.join(python_paths)
+ cmd = [str(python_launcher)]
if is_module:
cmd.append("-m")
diff --git a/source/isaaclab/isaaclab/utils/images.py b/source/isaaclab/isaaclab/utils/images.py
index 480245ed6540..8917e9d915e4 100644
--- a/source/isaaclab/isaaclab/utils/images.py
+++ b/source/isaaclab/isaaclab/utils/images.py
@@ -52,11 +52,13 @@ def normalize_camera_image(
Dispatch (in order of check):
- - :func:`is_rgb_like` and ``images.dtype == torch.uint8`` and contiguous 4D: routes to the
+ - :func:`is_rgb_like` or colorized ``"semantic_segmentation"`` (``uint8``) and contiguous
+ 4D: routes to the
fused Warp kernel via :func:`~isaaclab.utils.warp.ops.normalize_image_uint8`. ``out`` and
``channel_dim`` are forwarded so callers can reuse a pre-allocated float32 buffer and
select the image layout.
- - :func:`is_rgb_like` and any other dtype/shape: pure-PyTorch ``(x.float() / 255.0) - mean``
+ - :func:`is_rgb_like` or colorized ``"semantic_segmentation"`` and any other dtype/shape:
+ pure-PyTorch ``(x.float() / 255.0) - mean``
with the same math. ``out`` is ignored on this branch; ``channel_dim`` selects the spatial
reduction axes.
- :func:`is_depth_like`: in-place ``images[images == inf] = 0``. ``images`` is returned as-is.
@@ -65,21 +67,23 @@ def normalize_camera_image(
Args:
images: The camera-observation tensor. Shape and dtype vary by ``data_type``; the
- RGB-like Warp fast path requires 4D contiguous uint8 with the channel axis at
- position ``channel_dim``.
+ RGB-like and colorized semantic-segmentation Warp fast paths require 4D contiguous uint8
+ with the channel axis at position ``channel_dim``.
data_type: The camera data-type string. Drives the dispatch.
out: Optional pre-allocated float32 output for the RGB-like Warp fast path. Reused
across steps to eliminate per-step allocation. Ignored on the PyTorch fallback
and on non-RGB branches. Defaults to None.
- channel_dim: Position of the channel axis for the RGB-like branches. ``-1`` (BHWC,
- default) or ``-3`` / ``1`` (BCHW). Ignored on non-RGB branches.
+ channel_dim: Position of the channel axis for the RGB-like and colorized semantic-
+ segmentation branches. ``-1`` (BHWC, default) or ``-3`` / ``1`` (BCHW). Ignored on
+ other branches.
Returns:
- The normalized tensor. For RGB-like input this is a fresh (or pre-allocated) float32
- tensor; for depth-like input it is ``images`` itself (mutated in place); for
- normals-like input it is a new tensor; for anything else, ``images`` unchanged.
+ The normalized tensor. For RGB-like and colorized semantic-segmentation input this is a
+ fresh (or pre-allocated) float32 tensor; for depth-like input it is ``images`` itself
+ (mutated in place); for normals-like input it is a new tensor; for anything else,
+ ``images`` unchanged.
"""
- if is_rgb_like(data_type):
+ if is_rgb_like(data_type) or (data_type == "semantic_segmentation" and images.dtype == torch.uint8):
if images.dtype == torch.uint8 and images.ndim == 4 and images.is_contiguous():
return normalize_image_uint8(images, channel_dim=channel_dim, out=out)
# PyTorch fallback for callers that pre-floated or pass a strided view.
diff --git a/source/isaaclab/test/cli/test_install.py b/source/isaaclab/test/cli/test_install.py
index e2ddb89738b1..9f6e44fb43e3 100644
--- a/source/isaaclab/test/cli/test_install.py
+++ b/source/isaaclab/test/cli/test_install.py
@@ -19,6 +19,7 @@
extract_python_exe,
get_pip_command,
run_command,
+ run_python_command,
)
pytestmark = pytest.mark.unit
@@ -61,6 +62,47 @@ def test_run_command_retries_a_failed_process():
sleep.assert_called_once_with(3.0)
+def test_run_python_command_uses_live_isaac_sim_with_active_python(tmp_path):
+ """Direct uv launches must combine the live source build with the active Python."""
+ local_sim = tmp_path / "_isaac_sim"
+ local_sim.mkdir()
+ python_launcher = local_sim / "python.sh"
+ python_launcher.touch()
+ active_python = str(tmp_path / ".venv" / "bin" / "python")
+
+ with (
+ mock.patch("isaaclab.cli.utils.DEFAULT_ISAAC_SIM_PATH", local_sim),
+ mock.patch("isaaclab.cli.utils.extract_python_exe", return_value=active_python),
+ mock.patch("isaaclab.cli.utils.run_command") as run,
+ mock.patch.dict(os.environ, {}, clear=True),
+ ):
+ run_python_command("train.py", ["--task", "Cartpole"])
+
+ command = run.call_args.args[0]
+ assert command[0] == str(python_launcher)
+ assert Path(command[1]).name == "train.py"
+ assert command[2:] == ["--task", "Cartpole"]
+ assert run.call_args.kwargs["env"]["PYTHONEXE"] == active_python
+
+
+def test_run_python_command_does_not_wrap_an_active_isaac_sim_environment(tmp_path):
+ """The legacy wrapper path must not source the same Isaac Sim environment twice."""
+ local_sim = tmp_path / "_isaac_sim"
+ local_sim.mkdir()
+ (local_sim / "python.sh").touch()
+ active_python = str(tmp_path / ".venv" / "bin" / "python")
+
+ with (
+ mock.patch("isaaclab.cli.utils.DEFAULT_ISAAC_SIM_PATH", local_sim),
+ mock.patch("isaaclab.cli.utils.extract_python_exe", return_value=active_python),
+ mock.patch("isaaclab.cli.utils.run_command") as run,
+ mock.patch.dict(os.environ, {"ISAAC_PATH": str(local_sim)}, clear=True),
+ ):
+ run_python_command("script.py", [])
+
+ assert run.call_args.args[0] == [active_python, "script.py"]
+
+
# ---------------------------------------------------------------------------
# get_pip_command
# ---------------------------------------------------------------------------
diff --git a/source/isaaclab/test/cli/test_misc_commands.py b/source/isaaclab/test/cli/test_misc_commands.py
index 330cb8d1674d..abcb8b040108 100644
--- a/source/isaaclab/test/cli/test_misc_commands.py
+++ b/source/isaaclab/test/cli/test_misc_commands.py
@@ -64,3 +64,59 @@ def test_build_docs_explains_how_to_install_uv():
mock.call("uv could not be found. Please install uv and try again."),
mock.call("https://docs.astral.sh/uv/getting-started/installation/"),
]
+
+
+def test_build_isaacsim_links_incremental_build_without_packaging(tmp_path):
+ """The source workflow must link the live build without creating Python wheels."""
+ isaacsim_root = tmp_path / "IsaacSim"
+ build_script = isaacsim_root / "build.sh"
+ build_script.parent.mkdir()
+ build_script.touch()
+ release_dir = isaacsim_root / "_build" / "linux-x86_64" / "release"
+ release_dir.mkdir(parents=True)
+ (release_dir / "python.sh").touch()
+
+ workspace = tmp_path / "IsaacLab"
+ workspace.mkdir()
+
+ with (
+ mock.patch.object(misc, "ISAACLAB_ROOT", workspace),
+ mock.patch.object(misc, "run_command") as run_command,
+ mock.patch.object(misc, "_repoint_source_build_prebundles") as repoint_prebundles,
+ mock.patch.object(misc.sys, "platform", "linux"),
+ mock.patch.object(misc.platform, "machine", return_value="x86_64"),
+ ):
+ misc.command_build_isaacsim(str(isaacsim_root))
+
+ run_command.assert_called_once_with([str(build_script)], cwd=isaacsim_root)
+ repoint_prebundles.assert_called_once_with()
+ assert (workspace / "_isaac_sim").resolve() == release_dir
+
+
+@pytest.mark.parametrize(
+ ("sys_platform", "machine", "target"),
+ [
+ ("linux", "x86_64", "linux-x86_64"),
+ ("linux", "aarch64", "linux-aarch64"),
+ ("win32", "AMD64", "windows-x86_64"),
+ ],
+)
+def test_build_isaacsim_resolves_release_directory(tmp_path, sys_platform, machine, target):
+ """The source workflow must select the current platform's live release tree."""
+ with (
+ mock.patch.object(misc.sys, "platform", sys_platform),
+ mock.patch.object(misc.platform, "machine", return_value=machine),
+ ):
+ result = misc._resolve_isaacsim_release_dir(tmp_path)
+
+ assert result == tmp_path / "_build" / target / "release"
+
+
+def test_build_isaacsim_rejects_unsupported_platform(tmp_path):
+ """The source workflow must reject platforms Isaac Sim cannot build."""
+ with (
+ mock.patch.object(misc.sys, "platform", "darwin"),
+ mock.patch.object(misc.platform, "machine", return_value="arm64"),
+ pytest.raises(SystemExit, match="1"),
+ ):
+ misc._resolve_isaacsim_release_dir(tmp_path)
diff --git a/source/isaaclab/test/cli/test_source_package_metadata.py b/source/isaaclab/test/cli/test_source_package_metadata.py
index 29c6844730c4..722b4063f416 100644
--- a/source/isaaclab/test/cli/test_source_package_metadata.py
+++ b/source/isaaclab/test/cli/test_source_package_metadata.py
@@ -59,10 +59,15 @@ def test_resolved_environment_has_no_second_usd_provider():
assert "usd-exchange" in locked
-def test_standalone_importers_ship_as_base_dependencies():
- """The standalone URDF/MJCF importers install by default, so conversion works without Isaac Sim."""
+def test_standalone_importers_are_opt_in():
+ """Standalone URDF/MJCF importers must not constrain the base environment."""
with (_repo_root() / "pyproject.toml").open("rb") as f:
pyproject = tomllib.load(f)
- assert "isaacsim-asset-isolated>=6.0,<6.1" in pyproject["project"]["dependencies"]
- assert "importers" not in pyproject["project"]["optional-dependencies"]
+ project = pyproject["project"]
+ assert "isaacsim-asset-isolated>=6.0,<6.1" not in project["dependencies"]
+ assert "tinyobjloader==2.0.0rc13" not in project["dependencies"]
+ assert project["optional-dependencies"]["importers"] == [
+ "isaacsim-asset-isolated>=6.0,<6.1",
+ "tinyobjloader==2.0.0rc13",
+ ]
diff --git a/source/isaaclab/test/cli/test_uv_run_pyproject.py b/source/isaaclab/test/cli/test_uv_run_pyproject.py
index 198f70740c37..b860d84c3c04 100644
--- a/source/isaaclab/test/cli/test_uv_run_pyproject.py
+++ b/source/isaaclab/test/cli/test_uv_run_pyproject.py
@@ -59,6 +59,7 @@ def test_uv_run_exposes_centralized_feature_extras():
"ovrtx",
"mimic",
"teleop",
+ "importers",
"rlinf",
"tetrahedralization",
"all",
@@ -82,27 +83,20 @@ def test_uv_run_exposes_centralized_feature_extras():
assert any(dep.startswith("ovstage") for dep in optional_dependencies["ovrtx"])
-def test_all_extra_aggregates_backends_rl_libraries_and_visualizers():
- """``all`` is the single flag for every backend, RL library, and visualizer.
-
- Nothing is forked in ``[tool.uv].conflicts``, so Isaac Sim and both OV backends fit in
- one environment alongside every RL library and visualizer. The specialized workflows stay
- opt-in by name -- they are large, narrowly used, or both.
- """
+def test_all_extra_aggregates_curated_ov_rl_and_visualizer_extras():
+ """``all`` aggregates only the curated OV, RL, and visualizer extras."""
optional = _root_pyproject()["project"]["optional-dependencies"]
- # ``all`` is a single self-reference listing the extras it aggregates.
assert len(optional["all"]) == 1
aggregated = set(re.fullmatch(r"isaaclab-dev\[(.+)\]", optional["all"][0]).group(1).split(","))
- assert aggregated == {"isaacsim", "ov", "sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun"}
+ assert aggregated == {"ov", "sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun"}
- # ``ov`` pulls both OV backends, so naming it covers ``ovphysx`` and ``ovrtx`` too.
reachable = aggregated | {"ovphysx", "ovrtx"}
- # Everything else is requested by name. A newly added extra lands in this diff and
- # has to be classified deliberately -- into ``all`` or into this list.
assert set(optional) - reachable - {"all"} == {
"rlinf",
+ "isaacsim",
+ "importers",
"mimic",
"teleop",
"tetrahedralization",
diff --git a/source/isaaclab/test/cli/test_wheel_builder_metadata.py b/source/isaaclab/test/cli/test_wheel_builder_metadata.py
index ceeaac018bdd..7ac6515aee45 100644
--- a/source/isaaclab/test/cli/test_wheel_builder_metadata.py
+++ b/source/isaaclab/test/cli/test_wheel_builder_metadata.py
@@ -86,24 +86,39 @@ def test_wheel_builder_includes_isaacsim_extra(tmp_path):
assert any(dep.startswith("isaacsim[") for dep in optional_dependencies["isaacsim"])
-def test_wheel_builder_expands_all_extra_into_concrete_requirements(tmp_path):
- """``isaaclab[all]`` must ship the aggregated requirements, not a self-reference.
+def test_wheel_builder_keeps_standalone_importers_explicit(tmp_path):
+ """The wheel must expose standalone importers only through their explicit extra."""
+ generated = _generate_wheel_pyproject(tmp_path)
+ project = generated["project"]
- At the root, ``all`` is the self-reference ``isaaclab-dev[...]``. The generator
- inlines it, so the published wheel carries the concrete third-party requirements
- for every backend, RL library, and visualizer.
- """
+ assert "isaacsim-asset-isolated>=6.0,<6.1" not in project["dependencies"]
+ assert "tinyobjloader==2.0.0rc13" not in project["dependencies"]
+ assert project["optional-dependencies"]["importers"] == [
+ "isaacsim-asset-isolated>=6.0,<6.1",
+ "tinyobjloader==2.0.0rc13",
+ ]
+
+
+def test_wheel_builder_expands_all_extra_into_concrete_requirements(tmp_path):
+ """``isaaclab[all]`` must contain concrete curated requirements."""
generated = _generate_wheel_pyproject(tmp_path)
optional_dependencies = generated["project"]["optional-dependencies"]
all_extra = optional_dependencies["all"]
assert not any(dep.lower().startswith("isaaclab") for dep in all_extra)
- # Sampled across what ``all`` aggregates: Isaac Sim, both OV backends, the RL
- # libraries, and the visualizers.
- for prefix in ("isaacsim[", "ovphysx", "ovrtx", "ovstage", "stable-baselines3", "skrl", "viser", "rerun-sdk"):
+ for prefix in ("ovphysx", "ovrtx", "ovstage", "stable-baselines3", "skrl", "viser", "rerun-sdk"):
assert any(dep.startswith(prefix) for dep in all_extra), f"'{prefix}' missing from the 'all' extra"
- # The specialized extras and the developer tooling stay opt-in by name.
- for prefix in ("ray", "robomimic", "isaacteleop", "pytetwild", "moviepy", "leapp", "pytest"):
+ for prefix in (
+ "isaacsim[",
+ "isaacsim-asset-isolated",
+ "ray",
+ "robomimic",
+ "isaacteleop",
+ "pytetwild",
+ "moviepy",
+ "leapp",
+ "pytest",
+ ):
assert not any(dep.startswith(prefix) for dep in all_extra), f"'{prefix}' must not be in the 'all' extra"
diff --git a/source/isaaclab/test/install_ci/misc/test_wheel_builder_smoke.py b/source/isaaclab/test/install_ci/misc/test_wheel_builder_smoke.py
index fe4699682742..a32dc83a117b 100644
--- a/source/isaaclab/test/install_ci/misc/test_wheel_builder_smoke.py
+++ b/source/isaaclab/test/install_ci/misc/test_wheel_builder_smoke.py
@@ -33,12 +33,7 @@
@pytest.mark.smoke
class Test_Wheel_Builder_Smoke(UV_Mixin):
- """Test building the isaaclab wheel and installing it in a uv environment.
-
- The extras are named individually rather than using the aggregate ``all``: this is a
- fast smoke test of the built wheel, and ``all`` would pull Isaac Sim and both OV
- backends in. ``test_uv_pip_install_isaaclab_all_trains_cartpole`` covers ``[all]``.
- """
+ """Test building and installing the Isaac Lab wheel with selected RL extras."""
_wheel: str = ""
_extras: str = "[sb3,skrl,rsl-rl]"
diff --git a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py
index 986e329a5f20..3defd6fca820 100644
--- a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py
+++ b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py
@@ -7,8 +7,7 @@
Setup:
- (wheel supplied by runner: tools/run_install_ci.py --build-wheel or --wheel )
- ./isaaclab.sh -u
- - uv pip install [all] --overrides uv_pip/uv-overrides.txt
- --extra-index-url https://pypi.nvidia.com --index-strategy unsafe-best-match --prerelease=allow
+ - uv --no-config pip install [all]
- uv pip install --reinstall-package torch --reinstall-package torchvision
torch== torchvision== --index-url
(versions read from [tool.isaaclab.versions] in the root pyproject.)
@@ -16,6 +15,8 @@
Reinstall AFTER the wheel install: unsafe-best-match re-resolves torch from PyPI to CPU.)
- (aarch64 only) export LD_PRELOAD=/lib/aarch64-linux-gnu/libgomp.so.1
Tests:
+ - python -c "import importlib.metadata as m; assert m.version('newton') == '1.5.0'"
+ -> verify the wheel resolves Newton 1.5
- uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Direct --num_envs 16
presets=newton_mjwarp --max_iterations 5; uv run isaaclab train --rl_library rsl_rl
--task Isaac-Cartpole-Camera-Direct --num_envs 16 presets=newton_mjwarp,newton_renderer --max_iterations 2
@@ -27,7 +28,7 @@
import shutil
import pytest
-from utils import UV_Mixin, aarch64_isaacsim_env, cuda_torch_index_url, pinned_torch_specs
+from utils import UV_Mixin, cuda_torch_index_url, pinned_torch_specs
@pytest.mark.install_path_uv_pip
@@ -45,45 +46,25 @@ def setup_class(cls):
@pytest.mark.slow
@pytest.mark.gpu
@pytest.mark.timeout(4800)
- def test_uv_pip_install_isaaclab_all_trains_cartpole(
- self, isaaclab_root, wheel, uv_overrides, cartpole_smoke_script
- ):
+ def test_uv_pip_install_isaaclab_all_trains_cartpole(self, isaaclab_root, wheel, cartpole_smoke_script):
"""Install the runner-supplied wheel with ``[all]`` via ``uv pip``, then train."""
try:
- # 1. Create the uv env and install the wheel with the aggregate [all] extra, which
- # carries Isaac Sim, both OV backends, every RL library, and every visualizer.
- # This mirrors the documented wheel install (isaaclab-uv-wheel-install directive).
self.create_uv_env(isaaclab_root)
- # uv pip install "isaaclab[all]" --extra-index-url https://pypi.nvidia.com
- # --index-strategy unsafe-best-match --prerelease=allow
- # NOTE: --index-strategy unsafe-best-match re-resolves torch from PyPI (CPU build),
- # overriding any pre-installed CUDA torch. So install isaaclab FIRST, then
- # force-reinstall the CUDA torch from cu128/cu130 below.
result = self.run_in_uv_env(
- [
- "uv",
- "pip",
- "install",
- f"{wheel}[all]",
- "--overrides",
- str(uv_overrides),
- "--extra-index-url",
- "https://pypi.nvidia.com",
- "--index-strategy",
- "unsafe-best-match",
- "--prerelease=allow",
- ],
- cwd=isaaclab_root,
- timeout=1800,
+ ["uv", "--no-config", "pip", "install", f"{wheel}[all]"], cwd=isaaclab_root, timeout=1800
)
assert result.returncode == 0, f"uv pip install {wheel}[all] failed:\n{result.stdout}\n{result.stderr}"
- # 2. uv pip install --reinstall-package torch --reinstall-package torchvision
- # torch== torchvision== --index-url
- # (versions from [tool.isaaclab.versions]; cu128 on x86_64, cu130 on aarch64,
- # e.g. GB10 / DGX Spark with CUDA capability 12.x).
- # --reinstall-package forces uv to swap the CPU torch installed above with the CUDA build.
+ result = self.run_in_uv_env(
+ ["python", "-c", "import importlib.metadata as m; assert m.version('newton') == '1.5.0'"],
+ cwd=isaaclab_root,
+ )
+ assert result.returncode == 0, (
+ f"isaaclab[all] did not resolve Newton 1.5:\n{result.stdout}\n{result.stderr}"
+ )
+
+ # Restore the CUDA build selected for this architecture.
result = self.run_in_uv_env(
[
"uv",
@@ -102,11 +83,9 @@ def test_uv_pip_install_isaaclab_all_trains_cartpole(
)
assert result.returncode == 0, f"uv pip install CUDA torch failed:\n{result.stdout}\n{result.stderr}"
- # 3. Run the shared state and camera Cartpole smoke in the installed environment.
result = self.run_in_uv_env(
[str(self.python), str(cartpole_smoke_script)],
cwd=isaaclab_root,
- env=aarch64_isaacsim_env(),
timeout=3000,
)
assert result.returncode == 0, f"Cartpole smoke failed:\n{result.stdout}\n{result.stderr}"
diff --git a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_isaacsim_imports_simulation_context.py b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_isaacsim_imports_simulation_context.py
index eb034a69fd50..aeb9893f2987 100644
--- a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_isaacsim_imports_simulation_context.py
+++ b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_isaacsim_imports_simulation_context.py
@@ -7,8 +7,8 @@
Setup:
- (wheel supplied by runner: tools/run_install_ci.py --build-wheel or --wheel )
- ./isaaclab.sh -u
- - uv pip install [isaacsim] --overrides uv_pip/uv-overrides.txt
- --extra-index-url https://pypi.nvidia.com --index-strategy unsafe-best-match --prerelease=allow
+ - uv --no-config pip install [isaacsim] --overrides uv_pip/uv-overrides.txt
+ --extra-index-url https://pypi.nvidia.com --index-strategy unsafe-best-match
- uv pip install --reinstall-package torch --reinstall-package torchvision
torch== torchvision== --index-url
(versions read from [tool.isaaclab.versions] in the root pyproject.)
@@ -56,6 +56,7 @@ def _install_wheel(self, isaaclab_root, wheel, uv_overrides):
result = self.run_in_uv_env(
[
"uv",
+ "--no-config",
"pip",
"install",
f"{cls._wheel}[isaacsim]",
@@ -65,7 +66,6 @@ def _install_wheel(self, isaaclab_root, wheel, uv_overrides):
"https://pypi.nvidia.com",
"--index-strategy",
"unsafe-best-match",
- "--prerelease=allow",
],
cwd=isaaclab_root,
timeout=1800,
diff --git a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_rl_tasks_imports_rl_tasks.py b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_rl_tasks_imports_rl_tasks.py
index af8fd4a7a8b0..91cf3f812ede 100644
--- a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_rl_tasks_imports_rl_tasks.py
+++ b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_rl_tasks_imports_rl_tasks.py
@@ -24,12 +24,7 @@
@pytest.mark.install_path_uv_pip
class Test_Uv_Pip_Install_Isaaclab_Rl_Tasks_Imports_Rl_Tasks(UV_Mixin):
- """``uv pip install [sb3,skrl,rsl-rl]``: verify RL imports without Isaac Sim.
-
- The extras are named individually on purpose. ``test_install_rl_tasks_omits_isaacsim``
- asserts Isaac Sim is absent, and the aggregate ``all`` extra carries it -- switching to
- ``[all]`` would make that assertion fail.
- """
+ """Verify RL imports without Isaac Sim."""
_wheel: str = ""
_extras: str = "[sb3,skrl,rsl-rl]"
@@ -44,7 +39,6 @@ def _install_wheel(self, isaaclab_root, wheel):
cls = self.__class__
cls._wheel = str(wheel)
- # Create the uv env and install the RL extras (no isaacsim, no NVIDIA flags).
self.create_uv_env(isaaclab_root)
cls.env_path = self.env_path
cls.python = self.python
@@ -87,9 +81,7 @@ def test_install_rl_tasks_makes_isaaclab_tasks_importable(self):
def test_install_rl_tasks_omits_isaacsim(self):
"""The Isaac Sim runtime is absent after installing the RL extras (isaacsim extra not requested).
- ``import isaacsim`` is not the check: the standalone importers ship in the base
- dependencies and contribute an ``isaacsim.asset`` portion, so the namespace package
- resolves without the runtime. Ask the distribution instead.
+ Ask the distribution directly so this remains independent of namespace-package behavior.
"""
result = self.run_in_uv_env(
["python", "-c", "import importlib.metadata as m; m.version('isaacsim')"],
diff --git a/source/isaaclab/test/utils/test_images.py b/source/isaaclab/test/utils/test_images.py
index 26dc770b0118..9a7c045e6479 100644
--- a/source/isaaclab/test/utils/test_images.py
+++ b/source/isaaclab/test/utils/test_images.py
@@ -146,6 +146,23 @@ def test_bchw_float_input_takes_pytorch_fallback(self, device):
torch.testing.assert_close(out, expected)
+class TestNormalizeCameraImageColorizedSegmentation:
+ """Colorized segmentation dispatch."""
+
+ def test_colorized_semantic_segmentation_is_normalized(self, device):
+ """RGBA uint8 semantic segmentation produces a float32 normalized image."""
+ from isaaclab.utils.images import normalize_camera_image
+
+ torch.manual_seed(0)
+ src = torch.randint(0, 255, (2, 8, 8, 4), dtype=torch.uint8, device=device)
+ out = normalize_camera_image(src, "semantic_segmentation")
+
+ expected = src.float() / 255.0
+ expected = expected - torch.mean(expected, dim=(1, 2), keepdim=True)
+ torch.testing.assert_close(out, expected, atol=1e-5, rtol=1e-5)
+ assert out.dtype == torch.float32
+
+
class TestNormalizeCameraImageDepth:
"""Depth-like dispatch: in-place ``inf -> 0``."""
diff --git a/source/isaaclab_ov/changelog.d/ovphysx-force-matrix-history-reset.rst b/source/isaaclab_ov/changelog.d/ovphysx-force-matrix-history-reset.rst
new file mode 100644
index 000000000000..919df66444e1
--- /dev/null
+++ b/source/isaaclab_ov/changelog.d/ovphysx-force-matrix-history-reset.rst
@@ -0,0 +1,5 @@
+Fixed
+^^^^^
+
+* Cleared ``ContactSensorData.force_matrix_w_history`` when resetting an
+ OVPhysX contact sensor.
diff --git a/source/isaaclab_ov/changelog.d/ovstage-performance-improvements.rst b/source/isaaclab_ov/changelog.d/ovstage-performance-improvements.rst
new file mode 100644
index 000000000000..ef4c016bd89b
--- /dev/null
+++ b/source/isaaclab_ov/changelog.d/ovstage-performance-improvements.rst
@@ -0,0 +1,11 @@
+Changed
+^^^^^^^
+
+* Changed the OVRTX ovstage path to write object transforms, camera transforms and deformable or
+ particle points straight from their Warp GPU buffers as CUDA DLTensors, removing the per-frame
+ host copies that ``ovstage 0.1.0`` required.
+* Changed those writes to be ordered by handing ovstage the producing Warp stream
+ (``write_attribute(cuda_stream=...)``), replacing the device-wide ``wp.synchronize_device`` with
+ stream-scoped producer ordering, and matching the legacy OVRTX binding path. The write is still
+ awaited, so the calling thread can block; the gain is the removed host copy and the narrower
+ synchronization scope, not a nonblocking handoff.
diff --git a/source/isaaclab_ov/isaaclab_ov/physics/ovphysx_manager.py b/source/isaaclab_ov/isaaclab_ov/physics/ovphysx_manager.py
index d818ab8fffbc..03c953559883 100644
--- a/source/isaaclab_ov/isaaclab_ov/physics/ovphysx_manager.py
+++ b/source/isaaclab_ov/isaaclab_ov/physics/ovphysx_manager.py
@@ -37,6 +37,7 @@
from isaaclab_ov._clone import CloneTransform, clone_transforms_from_positions
from isaaclab_ov._runtime import import_ovphysx
+from isaaclab_ov.stage import create_ovstage
if TYPE_CHECKING:
from isaaclab.sim.simulation_context import SimulationContext
@@ -596,7 +597,7 @@ def _attach_ovstage(cls, stage_usda: str) -> None:
"""Populate an OVStage from USDA text and attach it to the runtime."""
import ovstage # noqa: PLC0415
- stage = ovstage.Stage("isaaclab")
+ stage = create_ovstage("isaaclab")
try:
ovstage.population.open_usd_from_string(
stage,
diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
index eb006605d3e0..b4ae36de5acc 100644
--- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
+++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
@@ -33,22 +33,12 @@
logger = logging.getLogger(__name__)
import numpy as np
+import ovstage
import torch
import warp as wp
import isaaclab.utils.warp # noqa: F401 # initializes Warp runtime
-# ovstage is optional: when present the renderer uses the split-ownership model
-# (ovstage owns scene data, ovrtx owns rendering). When absent it falls back to
-# the legacy renderer-owned scene APIs (deprecated in ovrtx 0.4).
-_OVSTAGE_AVAILABLE = False
-try:
- import ovstage
-
- _OVSTAGE_AVAILABLE = True
-except ModuleNotFoundError:
- pass
-
# The ovrtx C library links to its own version of the USD libraries. Having
# the pxr Python package available can cause the C library to load an
# incompatible version of libusd, potentially leading to undefined behavior.
@@ -81,6 +71,13 @@
from isaaclab.sim import SimulationContext
from isaaclab.utils.warp.warp_math import convert_camera_frame_orientation_convention_wp
+from isaaclab_ov.stage import (
+ create_ovstage,
+ points_tensor_from_warp,
+ xform_tensor_from_numpy,
+ xform_tensor_from_warp,
+)
+
from .ovrtx_annotator_utils import (
build_instance_id_to_labels_and_semantics,
build_semantic_id_to_labels,
@@ -137,41 +134,6 @@
_DISABLE_LINUX_CUDA_CPU_SYNC_ENV = "ISAAC_LAB_OVRTX_DISABLE_LINUX_CUDA_CPU_SYNC"
-if _OVSTAGE_AVAILABLE:
- # DLDataType for a 4×4 double matrix (omni:xform column). ovstage stores omni:xform
- # as one 16-lane float64 element per prim; wp.mat44d maps to the same layout via __dlpack__.
- _OVSTAGE_XFORM_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLFloat, bits=64, lanes=16)
-
- def _xform_tensor_from_numpy(xforms: np.ndarray) -> Any:
- """Wrap a ``(N, 4, 4)`` float64 array as a 16-lane DLTensor for ``omni:xform`` writes.
-
- Args:
- xforms: Array of shape ``(N, 4, 4)`` with dtype ``float64``.
-
- Returns:
- A :class:`ovstage.DLTensor` with shape ``[N]`` and ``lanes=16``.
- """
- flat = np.ascontiguousarray(xforms, dtype=np.float64).reshape(-1)
- return ovstage.make_dltensor(flat, dtype=_OVSTAGE_XFORM_DTYPE, shape=[xforms.shape[0]])
-
- # DLDataType for a float32 3-vector (``points`` column). ovstage stores ``point3f[] points``
- # as one 3-lane float32 element per vertex; a warp ``vec3f`` array exports as ``(N, 3)`` lanes=1
- # via DLPack, so a lanes=3 override on a host numpy array is required to match the column.
- _OVSTAGE_POINT_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLFloat, bits=32, lanes=3)
-
- def _points_tensor_from_numpy(points: np.ndarray) -> Any:
- """Wrap an ``(N, 3)`` float32 array as a 3-lane DLTensor for ``points`` writes.
-
- Args:
- points: Array of shape ``(N, 3)`` with dtype ``float32``.
-
- Returns:
- A :class:`ovstage.DLTensor` with shape ``[N]`` and ``lanes=3``.
- """
- flat = np.ascontiguousarray(points, dtype=np.float32).reshape(-1)
- return ovstage.make_dltensor(flat, dtype=_OVSTAGE_POINT_DTYPE, shape=[points.shape[0]])
-
-
def ovrtx_use_ovstage_enabled() -> bool:
"""Return whether the ovstage scene-ownership path should be used.
@@ -180,20 +142,10 @@ def ovrtx_use_ovstage_enabled() -> bool:
Raises:
ValueError: If the environment variable is set to anything other than ``0`` or ``1``.
- RuntimeError: If the environment variable is ``1`` but ovstage is not importable. Falling
- back to the legacy path here would silently ignore an explicit request and make the
- renderer look like it had honoured it.
"""
value = os.environ.get(_USE_OVSTAGE_ENV, "0").strip()
if value not in {"0", "1"}:
raise ValueError(f"Invalid value for environment variable `{_USE_OVSTAGE_ENV}`: {value}. Expected 0 or 1.")
- if value == "1" and not _OVSTAGE_AVAILABLE:
- raise RuntimeError(
- f"`{_USE_OVSTAGE_ENV}=1` requests the ovstage scene-ownership path, but the 'ovstage' "
- "package is not installed. Run your command with: uv run --extra ovrtx "
- "(or, manually: python -m pip install --extra-index-url https://pypi.nvidia.com "
- "'ovstage>=0.1.0,<0.2.0')."
- )
return value == "1"
@@ -1721,9 +1673,6 @@ def close(self) -> None:
# :meth:`_render_ovstage` already bars all writes at ordinals <= N, so accumulate the
# ``Operation`` objects and ``stage.release_op(op.op_id)`` after it. They must outlive the
# barrier — an ``Operation`` is its buffer's only keepalive. Saves caller-side blocking only.
- # - Direct zero-copy warp DLpack writes are rejected in ovstage 0.1.0 because
- # ``omni:xform``/``points`` are lanes=16/3 while warp's DLPack export is always lanes=1.
- # ovstage 0.1.1 will address this.
# ---------------------------------------------------------------------------
def _init_fields_ovstage(self) -> None:
@@ -1741,6 +1690,8 @@ def _init_fields_ovstage(self) -> None:
self._particle_paths_list = None
self._cable_points_query = None
self._cable_paths_list = None
+ # DLTensor descriptors aliasing ``_cable_point_slices``; rebuilt only when cables rebind.
+ self._cable_point_tensors: list = []
def _initialize_from_spec_ovstage(self, spec: CameraRenderSpec) -> None:
"""Initialize the OVRTX renderer with internal environment cloning (ovstage path).
@@ -1785,7 +1736,7 @@ def _initialize_from_spec_ovstage(self, spec: CameraRenderSpec) -> None:
logger.info("Loading USD into OvRTX via ovstage...")
self._ovstage_exit_stack = contextlib.ExitStack()
- self._stage = self._ovstage_exit_stack.enter_context(ovstage.Stage("isaaclab.ovrtx"))
+ self._stage = self._ovstage_exit_stack.enter_context(create_ovstage("isaaclab.ovrtx"))
self._stage_paths = self._ovstage_exit_stack.enter_context(ovstage.PathDictionary(self._stage))
# Ordinal 0 is the empty/unwritten state in ovstage; the first write must use >= 1.
self._current_ordinal += 1
@@ -1891,7 +1842,7 @@ def _clone_sources_ovstage(self):
env_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(env_root_xforms),
+ tensors=xform_tensor_from_numpy(env_root_xforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
@@ -2065,7 +2016,7 @@ def _setup_deformable_bindings_ovstage(self, num_envs: int) -> None:
self._deformable_points_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(identity_xforms),
+ tensors=xform_tensor_from_numpy(identity_xforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
@@ -2076,10 +2027,10 @@ def _setup_deformable_bindings_ovstage(self, num_envs: int) -> None:
def _setup_cable_bindings_ovstage(self) -> None:
"""Setup ovstage ``points`` bindings for Newton cables (``UsdGeom.BasisCurves``).
- Mirrors :meth:`_setup_cable_bindings_legacy`, except that the per-frame write goes through a
- host copy: ovstage 0.1.0's ``make_dltensor`` accepts the lanes=3 dtype override only on
- numpy arrays, so a warp ``vec3f`` slice is rejected against the ``points`` column. The
- endpoint kernel still runs on device; only the handover is host-side.
+ Mirrors :meth:`_setup_cable_bindings_legacy`: the endpoint kernel writes device memory and
+ the per-frame handover is zero-copy. The per-curve slices and their DLTensor descriptors are
+ built once here rather than per frame, because the layout is fixed for the lifetime of the
+ binding — only the contents of ``_cable_points`` change each step.
"""
discovered = self._discover_cable_segment_bindings()
if discovered is None:
@@ -2105,12 +2056,18 @@ def _setup_cable_bindings_ovstage(self) -> None:
self._cable_points_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(identity_xforms),
+ tensors=xform_tensor_from_numpy(identity_xforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
self._allocate_cable_device_buffers(flat_shape_ids, offsets, counts)
+ # The descriptors alias these slices, so both must outlive every write that uses them.
+ self._cable_point_slices = [
+ self._cable_points[offset + curve : offset + curve + segment_count + 1]
+ for curve, (offset, segment_count) in enumerate(zip(offsets, counts, strict=True))
+ ]
+ self._cable_point_tensors = [points_tensor_from_warp(points) for points in self._cable_point_slices]
def _setup_particle_bindings_ovstage(self) -> None:
"""Setup OVRTX bindings for Newton particle clouds (ovstage path)."""
@@ -2160,7 +2117,7 @@ def _setup_particle_bindings_ovstage(self) -> None:
self._particle_points_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(identity_xforms),
+ tensors=xform_tensor_from_numpy(identity_xforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
@@ -2192,17 +2149,21 @@ def _update_transforms_ovstage(self) -> None:
inputs=[object_transforms, self._object_newton_indices, body_q, self._object_scales],
device=self._device,
)
- # Synchronize then copy to CPU numpy: ovstage's make_dltensor only accepts the lanes=16
- # dtype override on numpy arrays, not DLPack producers. wp.mat44d exports as (N,4,4) lanes=1
- # via DLPack, which conflicts with the lanes=16 omni:xform column created at population time.
- wp.synchronize_device(self._device)
+ # The tensor is handed over zero-copy, so ovstage reads ``object_transforms`` in place and
+ # must not do so until the kernel above has landed. Passing the producing Warp stream as
+ # ``cuda_stream`` gives producer ordering: ovstage drains the work already queued on that
+ # stream before it touches the tensor. That replaces the device-wide
+ # ``wp.synchronize_device()`` with stream-scoped ordering and removes the host copy; it is
+ # not a nonblocking handoff, and the ``.wait()`` below can still block the calling thread.
+ # A GPU-side wait would need the event-based API instead.
self._stage.write_attribute(
self._object_xform_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(object_transforms.numpy().reshape(-1, 4, 4)),
+ tensors=xform_tensor_from_warp(object_transforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
+ cuda_stream=wp.get_stream(self._device).cuda_stream,
).wait()
def _update_geometries_ovstage(self) -> None:
@@ -2221,19 +2182,10 @@ def _update_geometries_ovstage(self) -> None:
if particle_q is None:
raise RuntimeError("Newton state has no particle_q but particle geometry queries exist")
- # ovstage write_attribute needs one DLPack tensor per prim, not one flat ``particle_q``
- # plus offsets. Synchronize then copy to CPU numpy once (shared by both queries below):
- # the ``points`` column is ``point3f[]`` (lanes=3), and ovstage's make_dltensor only
- # accepts the lanes=3 dtype override on numpy arrays, not DLPack producers. A warp
- # ``vec3f`` slice exports as ``(N, 3)`` lanes=1, which is rejected as a type mismatch
- # against the lanes=3 column.
- wp.synchronize_device(self._device)
- particle_np = particle_q.numpy()
-
if self._deformable_points_query is not None:
self._write_particle_q_slices_ovstage(
self._deformable_points_query,
- particle_np,
+ particle_q,
self._deformable_particle_offsets,
self._deformable_particle_counts,
)
@@ -2241,7 +2193,7 @@ def _update_geometries_ovstage(self) -> None:
if self._particle_points_query is not None:
self._write_particle_q_slices_ovstage(
self._particle_points_query,
- particle_np,
+ particle_q,
self._particle_visual_offsets,
self._particle_visual_counts,
)
@@ -2252,7 +2204,7 @@ def _update_geometries_ovstage(self) -> None:
def _write_particle_q_slices_ovstage(
self,
query: Any,
- particle_np: np.ndarray,
+ particle_q: wp.array,
particle_offsets: list[int],
particle_counts: list[int],
) -> None:
@@ -2260,15 +2212,22 @@ def _write_particle_q_slices_ovstage(
Args:
query: ovstage query selecting the prims whose ``points`` attribute is written.
- particle_np: Host copy of Newton particle positions [m], shape ``[total_particles, 3]``.
- particle_offsets: Start index of each prim's slice into Newton's ``particle_q``.
+ particle_q: Flat world-space particle positions [m], shape ``[total_particles]``,
+ dtype ``wp.vec3f``. Slices are passed zero-copy as CUDA DLTensors.
+ particle_offsets: Start index of each prim's slice into :paramref:`particle_q`.
particle_counts: Number of particles in each prim's slice.
"""
particle_slices = [
- _points_tensor_from_numpy(particle_np[particle_offset : particle_offset + particle_count])
+ points_tensor_from_warp(particle_q[particle_offset : particle_offset + particle_count])
for particle_offset, particle_count in zip(particle_offsets, particle_counts, strict=True)
]
+ # The slices alias ``particle_q`` and are handed over zero-copy, so ovstage must not read
+ # them until the Warp kernels that wrote ``particle_q`` have finished. Passing the producing
+ # Warp stream as ``cuda_stream`` gives producer ordering: ovstage drains the work already
+ # queued on that stream before it touches the slices. That replaces the device-wide
+ # ``wp.synchronize_device()`` with stream-scoped ordering and removes the host copy; it is
+ # not a nonblocking handoff, and the ``.wait()`` below can still block the calling thread.
self._stage.write_attribute(
query,
"points",
@@ -2276,32 +2235,26 @@ def _write_particle_q_slices_ovstage(
tensors=particle_slices,
is_array=True,
semantic=ovstage.AttributeSemantic.POINT,
+ cuda_stream=wp.get_stream(self._device).cuda_stream,
).wait()
def _write_cable_points_ovstage(self) -> None:
"""Recompute world-space cable curve points on device and write them through ovstage."""
self._compute_cable_points_world()
- # ovstage write_attribute needs one DLPack tensor per prim, not one flat ``particle_q``
- # plus offsets. Synchronize then copy to CPU numpy once (shared by both queries below):
- # the ``points`` column is ``point3f[]`` (lanes=3), and ovstage's make_dltensor only
- # accepts the lanes=3 dtype override on numpy arrays, not DLPack producers. A warp
- # ``vec3f`` slice exports as ``(N, 3)`` lanes=1, which is rejected as a type mismatch
- # against the lanes=3 column.
- points_np = self._cable_points.numpy()
- cable_slices = []
- point_offset = 0
- for segment_count in self._cable_segment_counts:
- cable_slices.append(_points_tensor_from_numpy(points_np[point_offset : point_offset + segment_count + 1]))
- point_offset += segment_count + 1
-
+ # The cached descriptors alias ``_cable_points`` and are handed over zero-copy, so ovstage
+ # must not read them until the kernel above has landed. Passing the producing Warp stream as
+ # ``cuda_stream`` gives producer ordering: ovstage drains the work already queued on that
+ # stream before it touches the slices. That keeps the handover off the host; it is not a
+ # nonblocking handoff, and the ``.wait()`` below can still block the calling thread.
self._stage.write_attribute(
self._cable_points_query,
"points",
ordinal=self._current_ordinal,
- tensors=cable_slices,
+ tensors=self._cable_point_tensors,
is_array=True,
semantic=ovstage.AttributeSemantic.POINT,
+ cuda_stream=wp.get_stream(self._device).cuda_stream,
).wait()
def _update_camera_ovstage(
@@ -2328,15 +2281,15 @@ def _update_camera_ovstage(
device=self._device,
)
if self._camera_xform_query is not None:
- # Synchronize then copy to CPU numpy: same lanes=16 constraint as object transforms above.
- wp.synchronize_device(self._device)
+ # Stream-ordered zero-copy handoff, as for the object transforms above.
self._stage.write_attribute(
self._camera_xform_query,
"omni:xform",
ordinal=self._current_ordinal,
- tensors=_xform_tensor_from_numpy(camera_transforms.numpy().reshape(-1, 4, 4)),
+ tensors=xform_tensor_from_warp(camera_transforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
+ cuda_stream=wp.get_stream(self._device).cuda_stream,
).wait()
def _render_ovstage(self, render_data: OVRTXRenderData) -> None:
@@ -2430,6 +2383,10 @@ def _safe_destroy_path_list(path_list, name: str) -> None:
self._particle_visual_counts = []
self._cable_segment_counts = []
self._cable_max_points = 0
+ # Descriptors alias ``_cable_points``; drop them before the buffer so no cached
+ # DLTensor can outlive the device memory it points at.
+ self._cable_point_tensors = []
+ self._cable_point_slices = []
self._cable_points = None
self._cable_shape_ids = None
self._cable_offsets = None
diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py
index 757fe06a1681..9a92a878e134 100644
--- a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py
+++ b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py
@@ -409,6 +409,7 @@ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None
self._data._net_forces_w,
self._data._net_forces_w_history,
self._data._force_matrix_w,
+ self._data._force_matrix_w_history,
],
outputs=[
self._data._current_air_time,
diff --git a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/kernels.py b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/kernels.py
index 437355dc470d..a4de413a82f5 100644
--- a/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/kernels.py
+++ b/source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/kernels.py
@@ -97,6 +97,7 @@ def reset_contact_sensor_kernel(
net_forces_w: wp.array2d(dtype=wp.vec3f),
net_forces_w_history: wp.array3d(dtype=wp.vec3f),
force_matrix_w: wp.array3d(dtype=wp.vec3f),
+ force_matrix_w_history: wp.array4d(dtype=wp.vec3f),
# outputs
current_air_time: wp.array2d(dtype=wp.float32),
last_air_time: wp.array2d(dtype=wp.float32),
@@ -116,6 +117,8 @@ def reset_contact_sensor_kernel(
net_forces_w: Net forces array. Shape is (num_envs, num_sensors).
net_forces_w_history: Net forces history array. Shape is (num_envs, history_length, num_sensors).
force_matrix_w: Force matrix array. Shape is (num_envs, num_sensors, num_filter_objects).
+ force_matrix_w_history: Force matrix history array. Shape is
+ (num_envs, history_length, num_sensors, num_filter_objects).
current_air_time: Current air time array. Shape is (num_envs, num_sensors).
last_air_time: Last air time array. Shape is (num_envs, num_sensors).
current_contact_time: Current contact time array. Shape is (num_envs, num_sensors).
@@ -142,6 +145,11 @@ def reset_contact_sensor_kernel(
for f in range(num_filter_objects):
force_matrix_w[env, sensor, f] = wp.vec3f(0.0)
+ if force_matrix_w_history:
+ for i in range(history_length):
+ for f in range(num_filter_objects):
+ force_matrix_w_history[env, i, sensor, f] = wp.vec3f(0.0)
+
# Reset air/contact time tracking
if current_air_time:
current_air_time[env, sensor] = 0.0
diff --git a/source/isaaclab_ov/isaaclab_ov/stage.py b/source/isaaclab_ov/isaaclab_ov/stage.py
new file mode 100644
index 000000000000..e054e7f1476e
--- /dev/null
+++ b/source/isaaclab_ov/isaaclab_ov/stage.py
@@ -0,0 +1,103 @@
+# 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
+
+"""Shared helpers for creating ovstage stages and describing their attribute columns.
+
+``ovstage`` is a hard dependency of ``isaaclab_ov``, so it is imported unconditionally here.
+"""
+
+from __future__ import annotations
+
+import numpy as np
+import ovstage
+import warp as wp
+
+# DLDataType for a 4x4 double matrix (``omni:xform`` column). ovstage stores omni:xform as one
+# 16-lane float64 element per prim; wp.mat44d maps to the same layout via __dlpack__.
+OVSTAGE_XFORM_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLFloat, bits=64, lanes=16)
+
+# DLDataType for a float32 3-vector (``points`` column). ovstage stores ``point3f[] points`` as one
+# 3-lane float32 element per vertex.
+OVSTAGE_POINT_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLFloat, bits=32, lanes=3)
+
+
+def create_ovstage(name: str) -> ovstage.Stage:
+ """Create an ovstage stage using Isaac Lab's process-wide stage configuration.
+
+ ovstage's hierarchy computation model drives its automatic world-transform updates. It is
+ process-scoped rather than per-stage: ovstage applies it when the first process reference is
+ acquired and raises if a later stage asks for a conflicting model while another stage is live.
+ Every Isaac Lab stage is therefore created through this helper so the whole process agrees on
+ one model.
+
+ :attr:`~ovstage.HierarchyComputationModel.CPU_INCREMENTAL` is requested explicitly rather than
+ left implicit, so the model in force is visible at the call site.
+ :attr:`~ovstage.HierarchyComputationModel.GPU_INCREMENTAL` is currently not working - objects
+ are out-of-place. Needs investigation
+
+ Args:
+ name: Instance name used for ovstage diagnostics.
+
+ Returns:
+ The created :class:`ovstage.Stage`.
+ """
+ config = ovstage.StageConfig(
+ runtime_default_hierarchy_computation_model=ovstage.HierarchyComputationModel.CPU_INCREMENTAL
+ )
+ return ovstage.Stage(name, config=config)
+
+
+def xform_tensor_from_numpy(xforms: np.ndarray) -> ovstage.DLTensor:
+ """Wrap a ``(N, 4, 4)`` float64 host array as a 16-lane DLTensor for ``omni:xform`` writes.
+
+ Args:
+ xforms: Array of shape ``(N, 4, 4)`` with dtype ``float64``.
+
+ Returns:
+ A :class:`ovstage.DLTensor` with shape ``[N]`` and ``lanes=16``.
+ """
+ flat = np.ascontiguousarray(xforms, dtype=np.float64).reshape(-1)
+ return ovstage.make_dltensor(flat, dtype=OVSTAGE_XFORM_DTYPE, shape=[xforms.shape[0]])
+
+
+def xform_tensor_from_warp(xforms: wp.array) -> ovstage.DLTensor:
+ """Describe a warp ``mat44d`` array as a 16-lane DLTensor for ``omni:xform`` writes.
+
+ The array is consumed zero-copy through DLPack: a warp ``mat44d`` exports as ``(N, 4, 4)``
+ ``lanes=1``, and ovstage folds the trailing matrix axes into the ``lanes=16`` the column
+ expects. A device array therefore reaches ovstage without a host round-trip.
+
+ The caller owns the data: the returned tensor must stay alive until the consuming write
+ completes, and that write must be ordered against the kernels that produced
+ :paramref:`xforms` — pass their Warp stream as ``write_attribute(cuda_stream=...)``.
+
+ Args:
+ xforms: Warp array of shape ``[N]`` and dtype :class:`warp.mat44d`.
+
+ Returns:
+ A :class:`ovstage.DLTensor` with shape ``[N]`` and ``lanes=16``.
+ """
+ return ovstage.make_dltensor(xforms, dtype=OVSTAGE_XFORM_DTYPE)
+
+
+def points_tensor_from_warp(points: wp.array) -> ovstage.DLTensor:
+ """Describe a warp ``vec3f`` array as a 3-lane DLTensor for ``points`` writes.
+
+ The array is consumed zero-copy through DLPack: a warp ``vec3f`` exports as ``(N, 3)``
+ ``lanes=1``, and ovstage folds the trailing component axis into the ``lanes=3`` the
+ ``point3f[]`` column expects. A device array therefore reaches ovstage without a host
+ round-trip.
+
+ The caller owns the data: the returned tensor must stay alive until the consuming write
+ completes, and that write must be ordered against the kernels that produced
+ :paramref:`points` — pass their Warp stream as ``write_attribute(cuda_stream=...)``.
+
+ Args:
+ points: Warp array of shape ``[N]`` and dtype :class:`warp.vec3f`.
+
+ Returns:
+ A :class:`ovstage.DLTensor` with shape ``[N]`` and ``lanes=3``.
+ """
+ return ovstage.make_dltensor(points, dtype=OVSTAGE_POINT_DTYPE)
diff --git a/source/isaaclab_ov/test/physics/test_ovphysx_scene_data_backend.py b/source/isaaclab_ov/test/physics/test_ovphysx_scene_data_backend.py
index a8e32ad9f61a..bad5af8750ce 100644
--- a/source/isaaclab_ov/test/physics/test_ovphysx_scene_data_backend.py
+++ b/source/isaaclab_ov/test/physics/test_ovphysx_scene_data_backend.py
@@ -455,6 +455,7 @@ def test_manager_logs_when_serialized_stage_has_no_envs(caplog):
def test_manager_attaches_and_releases_owned_ovstage(monkeypatch):
"""The manager owns OVStage from population through PhysX release."""
+ import isaaclab_ov.physics.ovphysx_manager as om_mod
from isaaclab_ov.physics import OvPhysxManager
events = []
@@ -491,7 +492,6 @@ def release(self):
events.append(("release",))
fake_ovstage = ModuleType("ovstage")
- fake_ovstage.Stage = FakeStage
fake_ovstage.PopulationDomain = SimpleNamespace(ALL="all")
fake_ovstage.population = SimpleNamespace(
open_usd_from_string=lambda stage, usda, ordinal, domains: events.append(
@@ -499,6 +499,9 @@ def release(self):
)
)
monkeypatch.setitem(sys.modules, "ovstage", fake_ovstage)
+ # The manager builds its stage through the shared helper so every stage in the process gets
+ # the same ovstage configuration; that is the seam to fake, not ``ovstage.Stage``.
+ monkeypatch.setattr(om_mod, "create_ovstage", FakeStage)
previous_physx = OvPhysxManager._physx
previous_ovstage = getattr(OvPhysxManager, "_ovstage", None)
@@ -535,6 +538,7 @@ def release(self):
def test_manager_destroys_ovstage_when_population_fails(monkeypatch):
"""A failed in-memory population does not leak its OVStage allocation."""
+ import isaaclab_ov.physics.ovphysx_manager as om_mod
from isaaclab_ov.physics import OvPhysxManager
destroyed = []
@@ -550,10 +554,10 @@ def fail_population(*args, **kwargs):
raise RuntimeError("population failed")
fake_ovstage = ModuleType("ovstage")
- fake_ovstage.Stage = FakeStage
fake_ovstage.PopulationDomain = SimpleNamespace(ALL="all")
fake_ovstage.population = SimpleNamespace(open_usd_from_string=fail_population)
monkeypatch.setitem(sys.modules, "ovstage", fake_ovstage)
+ monkeypatch.setattr(om_mod, "create_ovstage", FakeStage)
previous_ovstage = getattr(OvPhysxManager, "_ovstage", None)
OvPhysxManager._ovstage = None
diff --git a/source/isaaclab_ov/test/sensors/test_contact_sensor_kernels.py b/source/isaaclab_ov/test/sensors/test_contact_sensor_kernels.py
new file mode 100644
index 000000000000..43090133ca33
--- /dev/null
+++ b/source/isaaclab_ov/test/sensors/test_contact_sensor_kernels.py
@@ -0,0 +1,59 @@
+# 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
+
+"""Unit tests for OvPhysx contact-sensor Warp kernels."""
+
+import numpy as np
+import warp as wp
+from isaaclab_ov.sensors.contact_sensor.kernels import reset_contact_sensor_kernel
+
+
+def test_reset_contact_sensor_kernel_clears_selected_force_matrix_history():
+ """Reset clears filtered-force history only for selected environments."""
+ num_envs = 2
+ num_sensors = 1
+ history_length = 2
+ num_filter_shapes = 1
+ device = "cpu"
+ env_mask = wp.array([True, False], dtype=wp.bool, device=device)
+
+ net_forces_w = wp.zeros((num_envs, num_sensors), dtype=wp.vec3f, device=device)
+ net_forces_w_history = wp.zeros((num_envs, history_length, num_sensors), dtype=wp.vec3f, device=device)
+ force_matrix_w = wp.zeros((num_envs, num_sensors, num_filter_shapes), dtype=wp.vec3f, device=device)
+ force_matrix_w_history = wp.array(
+ np.ones((num_envs, history_length, num_sensors, num_filter_shapes, 3), dtype=np.float32),
+ dtype=wp.vec3f,
+ device=device,
+ )
+ current_air_time = wp.zeros((num_envs, num_sensors), dtype=wp.float32, device=device)
+ last_air_time = wp.zeros((num_envs, num_sensors), dtype=wp.float32, device=device)
+ current_contact_time = wp.zeros((num_envs, num_sensors), dtype=wp.float32, device=device)
+ last_contact_time = wp.zeros((num_envs, num_sensors), dtype=wp.float32, device=device)
+
+ wp.launch(
+ reset_contact_sensor_kernel,
+ dim=(num_envs, num_sensors),
+ inputs=[
+ history_length,
+ num_filter_shapes,
+ env_mask,
+ net_forces_w,
+ net_forces_w_history,
+ force_matrix_w,
+ force_matrix_w_history,
+ ],
+ outputs=[
+ current_air_time,
+ last_air_time,
+ current_contact_time,
+ last_contact_time,
+ None,
+ None,
+ ],
+ device=device,
+ )
+
+ np.testing.assert_array_equal(force_matrix_w_history.numpy()[0], 0.0)
+ np.testing.assert_array_equal(force_matrix_w_history.numpy()[1], 1.0)
diff --git a/source/isaaclab_ov/test/test_ovrtx_clone_plan.py b/source/isaaclab_ov/test/test_ovrtx_clone_plan.py
index e167ba979d9e..dc1256b06065 100644
--- a/source/isaaclab_ov/test/test_ovrtx_clone_plan.py
+++ b/source/isaaclab_ov/test/test_ovrtx_clone_plan.py
@@ -276,7 +276,7 @@ def _record_xforms(value: np.ndarray) -> str:
xforms.append(value.copy())
return "root_xforms"
- monkeypatch.setattr("isaaclab_ov.renderers.ovrtx_renderer._xform_tensor_from_numpy", _record_xforms)
+ monkeypatch.setattr("isaaclab_ov.renderers.ovrtx_renderer.xform_tensor_from_numpy", _record_xforms)
renderer._clone_sources_ovstage()
diff --git a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py
index 75ee17a08a8c..88d9f233485e 100644
--- a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py
+++ b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py
@@ -27,6 +27,9 @@
if not _MISSING_MODULES:
import isaaclab_ov.renderers.ovrtx_renderer as ovrtx_renderer_module # noqa: E402
+
+ # ovstage is an unconditional dependency of isaaclab_ov, so it is importable here.
+ import ovstage # noqa: E402
from isaaclab_newton.physics import NewtonManager # noqa: E402
from isaaclab_ov.renderers import OVRTXRendererCfg # noqa: E402
from isaaclab_ov.renderers.ovrtx_renderer import OVRTXRenderer # noqa: E402
@@ -546,3 +549,44 @@ def _capture_launch(*args, **kwargs):
# only guard against that -- the downgrade does not raise, it just renders from a stale copy.
assert renderer._cable_points_binding.write_kwargs["data_access"] is DataAccess.ASYNC
assert renderer._cable_points_binding.write_kwargs["cuda_stream"] == 1234
+
+
+@pytest.mark.skipif(not wp.get_cuda_device_count(), reason="requires a CUDA device")
+def test_write_particle_q_slices_ovstage_passes_device_slices_zero_copy():
+ """The ovstage points write hands ``particle_q`` slices to ovstage as CUDA DLTensors, without a host copy."""
+ renderer, _backend = _make_renderer_without_backend(device="cuda:0")
+ particle_q = wp.array(
+ [
+ wp.vec3f(-1.0, -1.0, -1.0),
+ wp.vec3f(1.0, 2.0, 3.0),
+ wp.vec3f(4.0, 5.0, 6.0),
+ wp.vec3f(7.0, 8.0, 9.0),
+ ],
+ dtype=wp.vec3f,
+ device="cuda:0",
+ )
+ writes: list[dict] = []
+
+ def _write(query, attribute, **kwargs):
+ writes.append({"query": query, "attribute": attribute, **kwargs})
+ return SimpleNamespace(wait=lambda: None)
+
+ renderer._stage = SimpleNamespace(write_attribute=_write)
+ renderer._current_ordinal = 7
+
+ renderer._write_particle_q_slices_ovstage("points_query", particle_q, [1], [3])
+
+ assert len(writes) == 1
+ assert writes[0]["attribute"] == "points"
+ assert writes[0]["is_array"] is True
+ # The slices alias ``particle_q``, so ovstage is handed the producing Warp stream to order its
+ # read against, rather than the caller blocking the host on a device synchronize.
+ assert writes[0]["cuda_stream"] == wp.get_stream("cuda:0").cuda_stream
+ tensors = writes[0]["tensors"]
+ assert len(tensors) == 1
+ # A zero-copy device view: the descriptor points straight at the slice's own CUDA buffer with
+ # the trailing component axis folded into point3f's three lanes.
+ assert tensors[0].device.device_type.value == ovstage.DLDeviceType.kDLCUDA
+ assert tensors[0].data == particle_q[1:4].ptr
+ assert tensors[0].shape_tuple == (3,)
+ assert tensors[0].dtype.lanes == 3
diff --git a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py
index e4aa92214740..9585105d41ee 100644
--- a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py
+++ b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py
@@ -370,26 +370,15 @@ def test_ovrtx_use_ovstage_defaults_to_disabled(monkeypatch):
assert ovrtx_use_ovstage_enabled() is False
-def test_ovrtx_use_ovstage_enabled_when_requested_and_available(monkeypatch):
- """Setting the variable to 1 selects the ovstage path when ovstage is importable."""
+def test_ovrtx_use_ovstage_enabled_when_requested(monkeypatch):
+ """Setting the variable to 1 selects the ovstage path."""
monkeypatch.setenv("ISAAC_LAB_OVRTX_USE_OVSTAGE", "1")
- monkeypatch.setattr(ovrtx_renderer_module, "_OVSTAGE_AVAILABLE", True)
assert ovrtx_use_ovstage_enabled() is True
-def test_ovrtx_use_ovstage_raises_when_requested_but_unavailable(monkeypatch):
- """An explicit opt-in must fail loudly rather than silently falling back to the legacy path."""
- monkeypatch.setenv("ISAAC_LAB_OVRTX_USE_OVSTAGE", "1")
- monkeypatch.setattr(ovrtx_renderer_module, "_OVSTAGE_AVAILABLE", False)
-
- with pytest.raises(RuntimeError, match="uv run --extra ovrtx"):
- ovrtx_use_ovstage_enabled()
-
-
def test_ovrtx_use_ovstage_rejects_non_boolean_values(monkeypatch):
"""Values other than 0/1 are a configuration error, not a silent disable."""
monkeypatch.setenv("ISAAC_LAB_OVRTX_USE_OVSTAGE", "true")
- monkeypatch.setattr(ovrtx_renderer_module, "_OVSTAGE_AVAILABLE", True)
with pytest.raises(ValueError, match="Expected 0 or 1"):
ovrtx_use_ovstage_enabled()
diff --git a/source/isaaclab_rl/changelog.d/mh-fix-leapp-openusd-thread-limit.skip b/source/isaaclab_rl/changelog.d/mh-fix-leapp-openusd-thread-limit.skip
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/source/isaaclab_rl/test/export/test_leapp_export_flow.py b/source/isaaclab_rl/test/export/test_leapp_export_flow.py
index 8615d7738bfc..854382cc8a7c 100644
--- a/source/isaaclab_rl/test/export/test_leapp_export_flow.py
+++ b/source/isaaclab_rl/test/export/test_leapp_export_flow.py
@@ -11,6 +11,7 @@
from __future__ import annotations
+import os
import subprocess
import sys
import tempfile
@@ -27,8 +28,10 @@
_SUBPROCESS_TIMEOUT = 600
_CHECKPOINT_BATCH_TIMEOUT = 1200
_OUTPUT_TAIL_CHARS = 5000
-# TODO: Remove once usd-core>=26.5 is the minimum. Earlier OpenUSD releases
-# can corrupt the heap while parsing the Newton Franka payload concurrently.
+# TODO: Remove once usd-core>=26.5 is the minimum. Earlier OpenUSD releases can
+# corrupt the heap while parsing the Newton Franka payload concurrently. OpenUSD
+# reads PXR_WORK_THREAD_LIMIT during process startup, before AppLauncher can apply
+# its matching SimulationApp limit.
_LEAPP_TEST_CPU_THREAD_LIMIT = 1
@@ -132,6 +135,7 @@ def _run_checked(
list(cmd),
cwd=_REPO_ROOT,
capture_output=True,
+ env={**os.environ, "PXR_WORK_THREAD_LIMIT": str(_LEAPP_TEST_CPU_THREAD_LIMIT)},
text=True,
timeout=timeout,
)
@@ -266,6 +270,15 @@ def test_initialized_checkpoints(initialized_checkpoints: Path):
assert not missing, f"Missing initialized checkpoints for: {', '.join(missing)}"
+def test_openusd_thread_limit_is_set_before_subprocess_startup():
+ """Assert LEAPP subprocesses start with OpenUSD concurrency disabled."""
+ result = _run_checked(
+ [sys.executable, "-c", "import os; print(os.environ['PXR_WORK_THREAD_LIMIT'])"],
+ label="OpenUSD thread-limit probe",
+ )
+ assert result.stdout.strip() == str(_LEAPP_TEST_CPU_THREAD_LIMIT)
+
+
@pytest.mark.parametrize(("backend", "task_name"), _export_cases())
def test_leapp_export_flow(backend: ExportFlowBackend, task_name: str, initialized_checkpoints: Path):
"""Export one backend/task pair using the shared initialized checkpoint."""
diff --git a/tools/environ_docs.py b/tools/environ_docs.py
index 69403e9916d2..c3f9edd9a038 100644
--- a/tools/environ_docs.py
+++ b/tools/environ_docs.py
@@ -92,6 +92,30 @@ class EnvironmentDocRow:
rl_libraries: dict[str, list[str]]
presets: dict[PresetTarget, list[str]] | None
agent_preset_compatibility: dict[str, tuple[str, ...]] = field(default_factory=dict)
+ supports_warp_frontend: bool = False
+
+
+def _supports_warp_frontend(task_name: str, workflow: str, presets: dict[PresetTarget, list[str]] | None) -> bool:
+ """Return whether a task can run through ``--frontend warp``."""
+ if presets is None or "newton_mjwarp" not in presets.get(PresetTarget.PHYSICS, []):
+ return False
+
+ try:
+ from isaaclab_experimental.envs.frontend import FrontendIncompatibleError, WarpFrontend
+
+ from isaaclab_tasks.utils.hydra import resolve_presets
+ from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry
+
+ cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point")
+ cfg = resolve_presets(cfg, selected=("newton_mjwarp",))
+ if workflow == "Direct":
+ try:
+ return WarpFrontend._resolve_direct_warp_class(task_name, cfg) is not None
+ except FrontendIncompatibleError:
+ return False
+ return WarpFrontend.check_compatibility(cfg) is None
+ except (ImportError, gym.error.Error):
+ return False
def is_training_task(task_id: str) -> bool:
@@ -526,6 +550,7 @@ def collect_environment_doc_rows(
for agent, presets in spec.kwargs.get("agent_preset_compatibility", {}).items()
if agent in spec.kwargs
},
+ supports_warp_frontend=_supports_warp_frontend(spec.id, workflow, preset_map),
)
)
@@ -632,6 +657,12 @@ def render_environment_browser_task_rows(
rendered_values += f", {json.dumps(row.agent_preset_compatibility, sort_keys=True)}"
if preview_image:
rendered_values += f", {json.dumps(preview_image)}"
+ if row.supports_warp_frontend:
+ if not row.agent_preset_compatibility and not preview_image:
+ rendered_values += ", {}"
+ if not preview_image:
+ rendered_values += ', ""'
+ rendered_values += ", true"
lines.append(f" [{rendered_values}],")
lines.append(" ];")
return "\n".join(lines)
diff --git a/tools/test/test_environ_docs.py b/tools/test/test_environ_docs.py
index cd3b7e846e51..df41aec9a860 100644
--- a/tools/test/test_environ_docs.py
+++ b/tools/test/test_environ_docs.py
@@ -382,6 +382,7 @@ def test_environment_browser_rows_include_concrete_core_and_contributed_selector
"rsl_rl_cfg_entry_point": ("rgb",),
"rsl_rl_feature_cfg_entry_point": ("resnet18", "theia_tiny"),
},
+ supports_warp_frontend=True,
),
EnvironmentDocRow(
task_name="IsaacContrib-Cartpole",
@@ -391,7 +392,13 @@ def test_environment_browser_rows_include_concrete_core_and_contributed_selector
),
]
rows.reverse()
- rendered = render_environment_browser_task_rows(rows, {"IsaacContrib-Cartpole": "tasks/classic/cartpole.jpg"})
+ rendered = render_environment_browser_task_rows(
+ rows,
+ {
+ "Isaac-Cartpole": "tasks/classic/cartpole.jpg",
+ "IsaacContrib-Cartpole": "tasks/classic/cartpole.jpg",
+ },
+ )
original = (
f" {ENVIRONMENT_BROWSER_TASKS_START_MARKER}\n"
" const taskRows = [];\n"
@@ -410,6 +417,7 @@ def test_environment_browser_rows_include_concrete_core_and_contributed_selector
assert '"IsaacContrib-Cartpole"' in updated
assert '"ovphysx"' in updated
assert '"tasks/classic/cartpole.jpg"' in updated
+ assert '"tasks/classic/cartpole.jpg", true' in updated
assert updated.index('"Isaac-Cartpole"') < updated.index('"IsaacContrib-Cartpole"')
assert "const preserved = true;" in updated
diff --git a/uv.lock b/uv.lock
index 686ea79ec84a..a70b4d0e001c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1764,7 +1764,7 @@ wheels = [
[[package]]
name = "isaaclab"
-version = "16.2.3"
+version = "16.4.0"
source = { editable = "source/isaaclab" }
[[package]]
@@ -1774,12 +1774,16 @@ source = { editable = "source/isaaclab_assets" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "isaaclab-contrib", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "isaaclab-newton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "isaaclab-physx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
[package.metadata]
requires-dist = [
{ name = "isaaclab", editable = "source/isaaclab" },
{ name = "isaaclab-contrib", editable = "source/isaaclab_contrib" },
+ { name = "isaaclab-newton", editable = "source/isaaclab_newton" },
+ { name = "isaaclab-physx", editable = "source/isaaclab_physx" },
]
[[package]]
@@ -1823,7 +1827,6 @@ dependencies = [
{ name = "isaaclab-tasks", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "isaaclab-tasks-experimental", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "isaaclab-visualizers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
- { name = "isaacsim-asset-isolated", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "lazy-loader", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "matplotlib", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "meshio", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -1870,7 +1873,6 @@ dependencies = [
all = [
{ name = "aiohttp", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "gym", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
- { name = "isaacsim", extra = ["all", "extscache"], marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "onnxscript", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "ovphysx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "ovrtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -1886,6 +1888,10 @@ all = [
{ name = "tqdm", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "viser", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
+importers = [
+ { name = "isaacsim-asset-isolated", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "tinyobjloader", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+]
isaacsim = [
{ name = "isaacsim", extra = ["all", "extscache"], marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
@@ -2010,7 +2016,7 @@ requires-dist = [
{ name = "isaaclab", editable = "source/isaaclab" },
{ name = "isaaclab-assets", editable = "source/isaaclab_assets" },
{ name = "isaaclab-contrib", editable = "source/isaaclab_contrib" },
- { name = "isaaclab-dev", extras = ["sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun", "isaacsim", "ov"], marker = "extra == 'all'" },
+ { name = "isaaclab-dev", extras = ["sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun", "ov"], marker = "extra == 'all'" },
{ name = "isaaclab-experimental", editable = "source/isaaclab_experimental" },
{ name = "isaaclab-mimic", marker = "extra == 'mimic'", editable = "source/isaaclab_mimic" },
{ name = "isaaclab-mimic", marker = "extra == 'teleop'", editable = "source/isaaclab_mimic" },
@@ -2025,7 +2031,7 @@ requires-dist = [
{ name = "isaaclab-visualizers", editable = "source/isaaclab_visualizers" },
{ name = "isaacsim", extras = ["all", "extscache"], marker = "extra == 'isaacsim'", specifier = "==6.0.1.0" },
{ name = "isaacsim", extras = ["all", "extscache"], marker = "extra == 'teleop'", specifier = "==6.0.1.0" },
- { name = "isaacsim-asset-isolated", specifier = ">=6.0,<6.1" },
+ { name = "isaacsim-asset-isolated", marker = "extra == 'importers'", specifier = ">=6.0,<6.1" },
{ name = "isaacteleop", extras = ["retargeters", "ui", "cloudxr"], marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'teleop'", specifier = "~=1.4.0" },
{ name = "junitparser", marker = "extra == 'test'" },
{ name = "lazy-loader", specifier = ">=0.4" },
@@ -2094,6 +2100,7 @@ requires-dist = [
{ name = "starlette", specifier = ">=0.46.0,<0.50" },
{ name = "tensorboard" },
{ name = "timm", marker = "extra == 'rlinf'", specifier = ">=1.0.14" },
+ { name = "tinyobjloader", marker = "extra == 'importers'", specifier = "==2.0.0rc13" },
{ name = "torch", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (sys_platform != 'linux' and sys_platform != 'win32')", specifier = ">=2.11" },
{ name = "torch", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'", specifier = ">=2.11", index = "https://download.pytorch.org/whl/cu130" },
{ name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.11", index = "https://download.pytorch.org/whl/cu128" },
@@ -2115,7 +2122,7 @@ requires-dist = [
{ name = "viser", marker = "extra == 'viser'", specifier = ">=1.0.16" },
{ name = "warp-lang", specifier = "==1.16.0" },
]
-provides-extras = ["tetrahedralization", "video", "test", "sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun", "isaacsim", "ov", "ovphysx", "ovrtx", "mimic", "teleop", "rlinf", "leapp", "all"]
+provides-extras = ["tetrahedralization", "video", "importers", "test", "sb3", "skrl", "rl-games", "rsl-rl", "viser", "rerun", "isaacsim", "ov", "ovphysx", "ovrtx", "mimic", "teleop", "rlinf", "leapp", "all"]
[[package]]
name = "isaaclab-experimental"
@@ -2147,7 +2154,7 @@ requires-dist = [
[[package]]
name = "isaaclab-newton"
-version = "5.2.0"
+version = "5.3.0"
source = { editable = "source/isaaclab_newton" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -2158,7 +2165,7 @@ requires-dist = [{ name = "isaaclab", editable = "source/isaaclab" }]
[[package]]
name = "isaaclab-ov"
-version = "2.0.4"
+version = "2.1.0"
source = { editable = "source/isaaclab_ov" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -2173,7 +2180,7 @@ requires-dist = [
[[package]]
name = "isaaclab-physx"
-version = "5.0.1"
+version = "5.1.0"
source = { editable = "source/isaaclab_physx" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -2212,7 +2219,7 @@ requires-dist = [
[[package]]
name = "isaaclab-tasks"
-version = "16.5.0"
+version = "17.0.0"
source = { editable = "source/isaaclab_tasks" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
@@ -2253,7 +2260,7 @@ requires-dist = [{ name = "isaaclab", editable = "source/isaaclab" }]
[[package]]
name = "isaaclab-visualizers"
-version = "1.6.0"
+version = "1.7.0"
source = { editable = "source/isaaclab_visualizers" }
dependencies = [
{ name = "isaaclab", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },