Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/actions/validate-kitless-image/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,20 @@ runs:
print("ovrtx renderer initialized")
'

# The Newton training probe is the shared one architecture CI also runs, so
# the command and its assertions live in one place. pytest goes only into
# this disposable layer; reinstalling the workspace would fail against the
# read-only source mount.
# Files are discovered by grep, not by pointing pytest at a directory: this image has no
# Isaac Sim, and collecting a directory imports every module in it to read its markers,
# which most of the suite cannot survive. Adding coverage is a `kitless` marker, nothing
# here. pytest goes only into this disposable layer; reinstalling the workspace would fail
# against the read-only source mount.
docker run --rm --gpus all --network host \
"${mount_args[@]}" \
"$IMAGE_TAG" \
bash -lc '
set -euo pipefail
uv pip install --python "$VIRTUAL_ENV/bin/python" pytest
python -m pytest \
source/isaaclab/test/install_ci/misc/cartpole_training_smoke.py::test_train_cartpole_state_completes \
source/isaaclab_newton/test/test_mock_interfaces \
-q -p no:cacheprovider
mapfile -t suite < <(grep -rl --include="*.py" "pytest\.mark\.kitless" source/)
[ "${#suite[@]}" -gt 0 ] || { echo "::error::no tests carry the kitless marker"; exit 1; }
python -m pytest -m kitless "${suite[@]}" -q -p no:cacheprovider
'

runtime_identity="$(docker run --rm --entrypoint bash "$IMAGE_TAG" \
Expand Down
11 changes: 6 additions & 5 deletions docker/Dockerfile.kitless
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ COPY pyproject.toml uv.lock VERSION LICENSE LICENSE-mimic README.md ./
COPY source/ source/
COPY isaaclab.sh ./

# Same entry point as Dockerfile.base. The selector is explicit because OVRTX
# lives in the `ov` feature a bare --install excludes; OVPhysX stays out until
# its OmniClient pin is compatible. The venv pins python3.12 to match the
# runtime stage's libpython3.12, and isaaclab.sh resolves VIRTUAL_ENV first.
# Same entry point as Dockerfile.base. Selectors are explicit: OVRTX lives in the `ov`
# feature a bare --install excludes, and `importers` carries the standalone URDF/MJCF
# importers that replace the Isaac Sim ones. OVPhysX stays out until its OmniClient pin
# is compatible. The venv pins python3.12 to match the runtime stage's libpython3.12,
# and isaaclab.sh resolves VIRTUAL_ENV first.
RUN uv venv --python /usr/bin/python3.12 --seed --no-managed-python "${VIRTUAL_ENV}" \
&& chmod +x "${ISAACLAB_PATH}/isaaclab.sh" \
&& "${ISAACLAB_PATH}/isaaclab.sh" --install newton,rl[all],ov[ovrtx] \
&& "${ISAACLAB_PATH}/isaaclab.sh" --install newton,rl[all],ov[ovrtx],importers \
&& python -c "import importlib.metadata as m; \
names = {d.metadata['Name'].lower() for d in m.distributions()}; \
assert 'isaacsim' not in names; \
Expand Down
6 changes: 3 additions & 3 deletions docs/source/api/lab/isaaclab.sim.converters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Mesh Converter
:members:
:inherited-members:
:show-inheritance:
:exclude-members: __init__
:exclude-members: __init__, PhysicsVariant


URDF Converter
Expand All @@ -53,7 +53,7 @@ URDF Converter
:members:
:inherited-members:
:show-inheritance:
:exclude-members: __init__
:exclude-members: __init__, PhysicsVariant

MJCF Converter
--------------
Expand All @@ -67,4 +67,4 @@ MJCF Converter
:members:
:inherited-members:
:show-inheritance:
:exclude-members: __init__
:exclude-members: __init__, PhysicsVariant
25 changes: 15 additions & 10 deletions docs/source/how-to/import_new_asset.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use the Omniverse Kit to edit the asset and export it to other file formats. Isa
these importers by default. They can also be enabled manually in Omniverse Kit.

Isaac Lab's URDF and MJCF converter utilities first use the importer APIs from
Isaac Sim when the full runtime is installed. In kit-less environments, install
the standalone importer wheel as described in
Isaac Sim when the full runtime is installed. In kit-less environments they use the
standalone importers that ship with Isaac Lab, as described in
:ref:`installation-standalone-importers` below.
The Kit visualizer and GUI import dialogs still require an Omniverse Kit runtime.

Expand All @@ -43,18 +43,23 @@ For more details on instanceable assets, please check the Isaac Sim `documentati
Standalone URDF/MJCF importers
------------------------------

The URDF and MJCF converter scripts can run without Isaac Sim when the standalone
``isaacsim-asset-isolated`` wheel is installed in the active environment. The wheel is not
published on PyPI, so replace ``PACKAGE_INDEX_URL`` with the package index that hosts it:
The URDF and MJCF converter scripts run without Isaac Sim, using the standalone
``isaacsim-asset-isolated`` importers. They ship in the ``importers`` extra, which is not
installed by default because it cannot share an environment with Isaac Sim -- both provide the
same ``isaacsim.asset`` importers. Install it from a source checkout:

.. code-block:: bash

uv pip install "isaacsim-asset-isolated>=6.0,<6.1" \
--extra-index-url "PACKAGE_INDEX_URL"
./isaaclab.sh --install 'newton,rl[rsl-rl],importers'

After installing the wheel, run conversion in the kit-less environment. Optionally pass
``--viz newton`` (or ``rerun`` / ``viser``) to preview the converted asset in a kit-less
Isaac Lab visualizer:
or into an environment built from the published wheel:

.. code-block:: bash

uv pip install 'isaaclab[importers]'

Run conversion in the kit-less environment. Optionally pass ``--viz newton`` (or ``rerun`` /
``viser``) to preview the converted asset in a kit-less Isaac Lab visualizer:

.. code-block:: bash

Expand Down
45 changes: 36 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ dependencies = [
"pin ; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')",
"pin-pink==3.3.0 ; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')",
"daqp==0.8.5 ; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')",
# OpenUSD (kit-less mode). Both packages vendor a complete ``pxr`` runtime, so
# they must never co-install: usd-core supplies pxr where it has wheels (x86_64),
# usd-exchange supplies it on aarch64 (where usd-core has no wheel).
"usd-core>=25.11,<26.0 ; platform_machine == 'x86_64' or platform_machine == 'AMD64'",
"usd-exchange>=2.2 ; platform_machine == 'aarch64'",
# OpenUSD (kit-less mode). Use the same provider required by the URDF and MJCF importer
# packages so another complete ``pxr`` runtime cannot co-install with it. Two providers in
# 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",
# 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 -----
Expand All @@ -88,9 +88,19 @@ dependencies = [
# ----- newton (default physics engine) -----
# Loose bound so the wheel co-resolves with isaacsim's newton[sim]==1.2.0 pin; the
# exact git commit is forced via [tool.uv].override-dependencies (uv sync only).
# The importers extra carries the mesh-processing deps (coacd, fast-simplification,
# ...) that honoring USD-authored ``physics:approximation`` requires.
"newton[sim,importers]>=1.2.0",
"newton[sim]>=1.2.0",
# Import and mesh-processing packages used by Newton, including the ones that honoring
# USD-authored ``physics:approximation`` requires. Keep these explicit instead of selecting
# newton[importers], whose standalone USD dependency would overlap with usd-exchange.
"requests>=2.25.0",
"scipy>=1.11.0",
"coacd>=1.0.7",
"fast-simplification>=0.1.11",
"alphashape>=1.3.1",
"meshio>=5.3.5",
"pycollada>=0.9",
"resolve-robotics-uri-py>=0.4.0",
"open3d>=0.19.0 ; python_version < '3.13' and (sys_platform != 'linux' or platform_machine != 'aarch64')",
# Loose bound co-resolves with isaacsim's ==0.2.0; the override below forces >=0.4.1.
"newton-usd-schemas>=0.2.0",
"PyOpenGL-accelerate>=3.1.0",
Expand Down Expand Up @@ -150,6 +160,19 @@ rerun = [

isaacsim = ["isaacsim[all,extscache]==6.0.1.0"]

# Packages that cannot share an environment with Isaac Sim, because they provide the same
# import paths it does. This is not "everything a kit-less setup installs" -- Newton, the RL
# frameworks and the visualizers work with and without Isaac Sim and stay in the base set.
# Membership rule: it belongs here only when co-installing it with Isaac Sim breaks imports.
#
# * isaacsim-asset-isolated -- standalone URDF/MJCF importers. Contributes to the
# ``isaacsim`` namespace, so alongside a real Isaac Sim it displaces the Kit extension
# serving the same import and conversion fails with ``No module named 'isaacsim.asset'``.
#
# Runtime-only incompatibilities (ovrtx, ovphysx) do NOT belong here: they install fine
# beside Isaac Sim and are rejected at launch by ``sim_launcher._validate_runtime``.
importers = ["isaacsim-asset-isolated>=6.0,<6.1"]

ov = ["ovphysx==0.5.9", "ovrtx>=0.4.0,<0.5.0", "ovstage==0.1.0.346039"]
ovphysx = ["ovphysx==0.5.9", "ovstage==0.1.0.346039"]
ovrtx = ["ovrtx>=0.4.0,<0.5.0", "ovstage==0.1.0.346039"]
Expand Down Expand Up @@ -200,6 +223,7 @@ leapp = [
# ``test_version_single_source`` fails CI if they drift.
[tool.isaaclab.versions]
isaacsim = "6.0.1.0"
usd_exchange = "2.3.0"
torch = "2.11.0"
torchvision = "0.26.0"
torchaudio = "2.11.0"
Expand Down Expand Up @@ -343,6 +367,7 @@ markers = [
"benchmark: test covers the Isaac Lab benchmark framework and infrastructure",
"rendering: test exercises the rendering / camera / visualizer pipeline",
"smoke: tests for core installation, task, and RL functionality",
"importers: test must pass inside the Kit-less container, which has no Isaac Sim runtime",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Register the kitless marker

The new converter tests and container selection use pytest.mark.kitless, but this configuration registers importers instead. This produces unknown-marker warnings and makes collection fail under --strict-markers.

Suggested change
"importers: test must pass inside the Kit-less container, which has no Isaac Sim runtime",
"kitless: test must pass inside the Kit-less container, which has no Isaac Sim runtime",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f6bf12. The marker is now registered as kitless, matching the six test files and the -m kitless selection in validate-kitless-image. It had been renamed along with the extra by a bulk rename that should only have touched the extra name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Implementation — Registered marker name does not match tests

The tests and .github/actions/validate-kitless-image/action.yml use pytest.mark.kitless (grep for pytest\.mark\.kitless, then pytest -m kitless), but the marker registered here is named importers. So kitless stays unregistered and raises PytestUnknownMarkWarning on every marked module (failing under --strict-markers), while the registered importers marker selects nothing. Rename the registration to kitless.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f6bf12 — registered as kitless. Caused by a bulk rename of the extra (kitlessimporters) that also caught the marker string, leaving all six marked modules with an unregistered marker.

]

# Add pypi.nvidia.com so that `uv pip install isaaclab[isaacsim]` works without --extra-index-url.
Expand Down Expand Up @@ -388,7 +413,7 @@ override-dependencies = [
"numpy>=2",
"mujoco~=3.11.0",
"mujoco-warp~=3.11.0",
"newton[sim,importers] @ git+https://github.com/newton-physics/newton.git@release-1.5",
"newton[sim] @ git+https://github.com/newton-physics/newton.git@release-1.5",
# Force the Newton-matched schemas over isaacsim's ==0.2.0 pin.
"newton-usd-schemas>=0.4.1",
"torch==2.11.0",
Expand All @@ -408,6 +433,8 @@ conflicts = [
[{ extra = "teleop" }, { extra = "ovphysx" }],
[{ extra = "isaacsim" }, { extra = "ov" }],
[{ extra = "isaacsim" }, { extra = "ovphysx" }],
# both supply the ``isaacsim.asset`` importers; together the wheel shadows the Kit extension
[{ extra = "isaacsim" }, { extra = "importers" }],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Design Architecture — teleop does not inherit the importers conflict

The teleop extra installs isaacsim[all,extscache] directly, which is why the table already mirrors (isaacsim, ov) with (teleop, ov). Only (isaacsim, importers) was added, so uv sync --extra teleop --extra importers resolves and ./isaaclab.sh -i teleop,importers passes the new check, co-installing isaacsim-asset-isolated beside a real Isaac Sim — exactly the shadowing this extra exists to prevent. Add (teleop, importers).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f6bf12: added [{ extra = "teleop" }, { extra = "importers" }]. Correct — teleop lists isaacsim[all,extscache]==6.0.1.0 directly, so it inherits the incompatibility, and the table already mirrored every isaacsim pair with a teleop one.

Note that after merging develop the ovphysx pairs were removed upstream, so the table is now teleop×ov, isaacsim×ov, isaacsim×importers, teleop×importers.

]
python-preference = "only-managed"
package = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Added
^^^^^

* Added an ``importers`` extra carrying the standalone URDF and MJCF importers, so conversion works
without Isaac Sim. It cannot be combined with the ``isaacsim`` extra.

* Added :attr:`~isaaclab.sim.converters.AssetConverterBaseCfg.physics_variant` to choose which
``"Physics"`` variant the URDF and MJCF converters select.

Fixed
^^^^^

* Fixed URDF and MJCF conversion producing assets with no joints, articulation roots, or mass
properties.

* Fixed MJCF conversion failing with ``Cannot find a valid schema for 'MjcSceneAPI'``.

* Fixed installation failures caused by overlapping standalone USD providers by using
``usd-exchange`` on all supported platforms and installing required Newton mesh-processing
packages directly.

* Fixed :meth:`~isaaclab.utils.dict.class_to_dict` expanding enum values into their internal
members.

Changed
^^^^^^^

* Changed :func:`~isaaclab.sim.utils.select_usd_variants` to raise for a variant set in
:obj:`~isaaclab.sim.utils.REQUIRED_VARIANT_SETS` that is absent or lacks the requested variant.

* Changed ``./isaaclab.sh --install`` to reject extras that ``pyproject.toml`` declares conflicting.
27 changes: 27 additions & 0 deletions source/isaaclab/isaaclab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""Package containing the core framework."""

import importlib.metadata
import importlib.util
import os
import sys

Expand Down Expand Up @@ -79,7 +80,33 @@ def _should_demote(path: str) -> bool:
os.environ["PYTHONPATH"] = os.pathsep.join(env_clean + env_demoted)


def _expose_mujoco_usd_schemas():
"""Put the MuJoCo USD schemas on OpenUSD's plugin search path.

``mujoco-usd-converter`` ships them as a codeless schema plugin that it registers when the
package is imported. OpenUSD builds its schema registry once, on the first schema query, and
ignores plugins registered after that -- so MJCF conversion fails with "Cannot find a valid
schema for ``MjcSceneAPI``" whenever anything touched a schema first. Adding the plugin to the
search path lets OpenUSD find it while building the registry, so importing the converter late
no longer matters.

OpenUSD reads the search path while building the registry, so this only helps while the
registry is still unbuilt. A host that queries a schema before importing Isaac Lab has to put
the plugin directory on ``PXR_PLUGINPATH_NAME`` itself.
"""
spec = importlib.util.find_spec("mujoco_usd_converter")
if spec is None or spec.origin is None:
return
plugins = os.path.join(os.path.dirname(spec.origin), "plugins")
if not os.path.isdir(plugins):
return
search_path = os.environ.get("PXR_PLUGINPATH_NAME", "")
if plugins not in search_path.split(os.pathsep):
os.environ["PXR_PLUGINPATH_NAME"] = os.pathsep.join(filter(None, (search_path, plugins)))


_deprioritize_prebundle_paths()
_expose_mujoco_usd_schemas()


try:
Expand Down
55 changes: 53 additions & 2 deletions source/isaaclab/isaaclab/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,15 +674,17 @@ def _install_isaacsim() -> None:
# core set.
VALID_EXTRA_FEATURES: set[str] = {
"contrib",
"importers",
"newton",
"ov",
"rl",
"tetrahedralization",
"visualizer",
}

# Extra features excluded from the automatic ``-i all`` / ``-i`` install.
MANUAL_EXTRA_FEATURES: set[str] = {"contrib", "ov", "tetrahedralization"}
# Extra features excluded from the automatic ``-i all`` / ``-i`` install. ``importers`` is
# here because it cannot coexist with Isaac Sim, so it must be asked for deliberately.
MANUAL_EXTRA_FEATURES: set[str] = {"contrib", "importers", "ov", "tetrahedralization"}


def split_install_items(install_type: str) -> list[str]:
Expand Down Expand Up @@ -817,6 +819,11 @@ def _install_extra_feature(feature_name: str, selector: str = "") -> None:
"""
if feature_name == "contrib":
_install_contrib_extra_dependencies(selector)
elif feature_name == "importers":
if selector:
print_warning(f"'importers' does not support selectors (got '{selector}').")
print_info("Installing the kit-less stand-ins for Isaac Sim (URDF/MJCF importers)...")
_install_root_extra("importers")
elif feature_name == "newton":
if selector:
print_warning(f"'newton' does not support selectors (got '{selector}').")
Expand Down Expand Up @@ -1097,6 +1104,48 @@ def _repoint_prebundle_packages() -> None:
)


def _requested_root_extras(
install_isaacsim: bool, optional_submodules: list[str], extra_features: list[tuple[str, str]]
) -> set[str]:
"""Return the root extras this install would apply.

Tokens are not extras: ``ov[ovrtx]`` installs ``ovrtx``, not ``ov``.
"""
extras = set(optional_submodules)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion · Implementation — Submodule tokens are not mapped to root extras

set(optional_submodules) stores the CLI token, but OPTIONAL_SUBMODULE_ROOT_EXTRAS maps mimic to the teleop and mimic extras. So -i mimic,ov[ovphysx] installs the Isaac-Sim-bearing teleop extra while the declared {teleop, ovphysx} conflict never matches, leaving one of the pairs this function claims to enforce unenforced. Expand submodule tokens through OPTIONAL_SUBMODULE_ROOT_EXTRAS first.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f6bf12: optional submodule tokens are expanded through OPTIONAL_SUBMODULE_ROOT_EXTRAS before the conflict check, so -i mimic contributes both mimic and teleop.

Verified at the time: mimic + ov[ovphysx] was rejected. After merging develop the ovphysx conflict pairs no longer exist upstream, so that specific pair is legal again — the check reads [tool.uv].conflicts rather than hardcoding, so it tracked the change.

if install_isaacsim:
extras.add("isaacsim")
for feature, selector in extra_features:
if feature == "ov":
chosen = {item.strip().lower() for item in selector.split(",") if item.strip()}
extras |= {"ovphysx", "ovrtx"} if "all" in chosen else chosen
else:
extras.add(feature)
return extras


def _reject_conflicting_extras(requested: set[str]) -> None:
"""Reject extras that ``[tool.uv].conflicts`` declares incompatible.

That table only binds a resolver, and each feature here installs in its own pip pass, so
nothing else would catch the combination.

Args:
requested: Names of the extras this install would apply.

Raises:
SystemExit: When the request covers every extra of a declared conflict.
"""
conflicts = _load_root_pyproject().get("tool", {}).get("uv", {}).get("conflicts", [])
for conflict in conflicts:
extras = {entry["extra"] for entry in conflict if "extra" in entry}
if len(extras) > 1 and extras <= requested:
names = ", ".join(f"'{extra}'" for extra in sorted(extras))
raise SystemExit(
f"error: {names} cannot be installed together; the root pyproject.toml declares"
" them conflicting. Drop one of the tokens."
)


def command_install(install_type: str = "all") -> None:
"""Install Isaac Lab extensions and optional extras.

Expand Down Expand Up @@ -1195,6 +1244,8 @@ def append_submodules_once(package_dirs: tuple[str, ...]) -> None:
valid = sorted(OPTIONAL_ISAACLAB_SUBMODULES) + sorted(VALID_EXTRA_FEATURES) + ["isaacsim"]
print_warning(f"Unknown install token '{name}'. Valid values: {', '.join(valid)}. Skipping.")

_reject_conflicting_extras(_requested_root_extras(install_isaacsim, requested_optional_submodules, extra_features))

# Configure extra package indexes for NVIDIA and MuJoCo wheels.
os.environ.setdefault("UV_EXTRA_INDEX_URL", "https://pypi.nvidia.com")
os.environ.setdefault("PIP_EXTRA_INDEX_URL", "https://pypi.nvidia.com")
Expand Down
Loading
Loading