# 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
"""Utilities for detecting and launching the appropriate simulation backend.
The flow is intentionally simple: walk the config tree **once** to collect its
signals into a :class:`Scan`, resolve the Kit runtime sources from that scan and
the launcher inputs, then validate and launch.
"""
from __future__ import annotations
import argparse
import importlib.metadata
import logging
import os
import sys
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Any
from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg
from isaaclab_ov.physics import OvPhysxCfg
from isaaclab_ov.renderers import OVRTXRendererCfg
from isaaclab_physx.physics import PhysxCfg
from isaaclab_physx.renderers import IsaacRtxRendererCfg
from isaaclab.app.logging_utils import apply_python_logging_level, resolve_python_logging_level
from isaaclab.physics.physics_manager_cfg import PhysicsCfg, PhysxAutoCfg, _resolve_physx_auto_cfg
from isaaclab.renderers.renderer_cfg import RendererCfg
from isaaclab.sensors.camera.camera_cfg import CameraCfg
from isaaclab.utils._device import set_cuda_device
logger = logging.getLogger(__name__)
# Class names of the kitless physics backends (Newton, OvPhysX). Matched by exact
# name so subclasses with distinct names opt out.
_KITLESS_PHYSICS_CFGS = ("NewtonCfg", "OvPhysxCfg")
def add_launcher_args(parser: argparse.ArgumentParser) -> None:
"""Add simulation-launcher CLI arguments (``--device``, ``--viz``, etc.) to *parser*.
Delegates to :meth:`AppLauncher.add_app_launcher_args` so that user scripts
do not need to import ``AppLauncher`` directly.
"""
from isaaclab.app import AppLauncher
AppLauncher.add_app_launcher_args(parser)
[docs]
def make_physics_cfg(physics_cfg_str: str) -> PhysicsCfg:
"""Build a physics config for the requested backend.
Args:
physics_cfg_str: Backend selector: ``"physx"``, ``"isaacsim_physx"``,
``"newton_mjwarp"``, ``"newton_vbd"``, or ``"ovphysx"``. The
``"physx"`` selector is automatic: it resolves to Isaac Sim PhysX
when Kit is required, and to OvPhysX otherwise.
Returns:
A new physics config instance for the requested backend.
Raises:
ValueError: If *physics_cfg_str* does not name a known backend.
"""
if physics_cfg_str == "physx":
return PhysxAutoCfg(isaacsim_physx=PhysxCfg(), ovphysx=OvPhysxCfg())
if physics_cfg_str == "isaacsim_physx":
return PhysxCfg()
if physics_cfg_str == "newton_mjwarp":
return NewtonCfg()
if physics_cfg_str == "newton_vbd":
return NewtonCfg(solver_cfg=VBDSolverCfg())
if physics_cfg_str == "ovphysx":
return OvPhysxCfg()
raise ValueError(
f"Invalid physics config: {physics_cfg_str!r} "
"(expected 'physx', 'isaacsim_physx', 'newton_mjwarp', 'newton_vbd', or 'ovphysx')."
)
"""
Node Predicates.
"""
def _is_ovrtx_renderer(node) -> bool:
"""True when the node is an OVRTX renderer config."""
return isinstance(node, RendererCfg) and getattr(node, "renderer_type", None) == "ovrtx"
def _is_auto_rtx_renderer(node) -> bool:
"""True when the node is an automatic RTX renderer placeholder."""
return getattr(node, "renderer_type", None) == "auto_rtx"
def _is_auto_physx_physics(node) -> bool:
"""True when the node is an automatic PhysX placeholder."""
return isinstance(node, PhysxAutoCfg)
def _is_kit_camera(node) -> bool:
"""True for a CameraCfg whose renderer requires Kit (not Newton)."""
if not isinstance(node, CameraCfg):
return False
renderer_cfg = getattr(node, "renderer_cfg", None)
if renderer_cfg is None:
return True
if _is_auto_rtx_renderer(renderer_cfg):
# ``auto_rtx`` is resolved after the initial scan once physics and
# visualizer intent are known; ie. it may become OVRTX for a kitless run.
return False
if not isinstance(renderer_cfg, RendererCfg):
raise TypeError(
f"CameraCfg.renderer_cfg must be a concrete RendererCfg or None, got {type(renderer_cfg).__name__}."
)
return renderer_cfg.renderer_type in ("default", "isaac_rtx")
"""
Launcher Argument Helpers.
"""
def _get_arg(launcher_args: argparse.Namespace | dict | None, key: str, default: Any = None) -> Any:
"""Read *key* from launcher args, whether a namespace, dict, or ``None``."""
if isinstance(launcher_args, argparse.Namespace):
return getattr(launcher_args, key, default)
if isinstance(launcher_args, dict):
return launcher_args.get(key, default)
return default
def _set_arg(launcher_args: argparse.Namespace | dict | None, key: str, value: Any) -> None:
"""Write *key* on launcher args when it is a namespace or dict."""
if isinstance(launcher_args, argparse.Namespace):
setattr(launcher_args, key, value)
elif isinstance(launcher_args, dict):
launcher_args[key] = value
def _get_visualizer_types(launcher_args: argparse.Namespace | dict | None) -> set[str]:
"""Extract requested visualizer type names (``--visualizer kit,newton,...``)."""
visualizers = _get_arg(launcher_args, "visualizer")
if not visualizers:
return set()
if isinstance(visualizers, str):
visualizers = visualizers.split(",")
return {str(v).strip().lower() for v in visualizers if str(v).strip()}
def _get_livestream_mode(launcher_args: argparse.Namespace | dict | None) -> int:
"""Return effective livestream mode using the same CLI-over-env precedence as AppLauncher."""
livestream_arg = _get_arg(launcher_args, "livestream", -1)
if livestream_arg is not None and int(livestream_arg) >= 0:
return int(livestream_arg)
return int(os.environ.get("LIVESTREAM", 0))
def _ensure_livestream_kit_visualizer(launcher_args: argparse.Namespace | dict | None) -> None:
"""Request the Kit visualizer when livestreaming needs a video-producing viewport."""
if launcher_args is None or _get_livestream_mode(launcher_args) == 0:
return
visualizer_explicit = bool(_get_arg(launcher_args, "visualizer_explicit", False))
visualizers = _get_arg(launcher_args, "visualizer")
if visualizer_explicit and (visualizers is None or "none" in _get_visualizer_types(launcher_args)):
raise ValueError("Livestreaming requires the Kit visualizer. Remove '--viz none' or pass '--viz kit'.")
visualizer_types = _get_visualizer_types(launcher_args)
if "kit" in visualizer_types:
return
requested_visualizers = []
if visualizers:
requested_visualizers = (
[visualizer.strip() for visualizer in visualizers.split(",")]
if isinstance(visualizers, str)
else [str(visualizer).strip() for visualizer in visualizers if str(visualizer).strip()]
)
requested_visualizers.append("kit")
_set_arg(launcher_args, "visualizer", requested_visualizers)
def _get_visualizer_intent(cfg) -> dict[str, bool]:
"""Compute upstream visualizer intent from ``cfg.sim.visualizer_cfgs``."""
# Accept both env_cfg (has .sim.visualizer_cfgs) and a bare SimulationCfg
# (has .visualizer_cfgs directly).
sim = getattr(cfg, "sim", None)
visualizer_cfgs = getattr(sim, "visualizer_cfgs", None) or getattr(cfg, "visualizer_cfgs", None)
if visualizer_cfgs is None:
return {"has_any_visualizers": False, "has_kit_visualizer": False, "has_kit_streaming_view": False}
cfgs = visualizer_cfgs if isinstance(visualizer_cfgs, list) else [visualizer_cfgs]
cfgs = [c for c in cfgs if c is not None]
return {
"has_any_visualizers": len(cfgs) > 0,
"has_kit_visualizer": any(getattr(c, "visualizer_type", None) == "kit" for c in cfgs),
"has_kit_streaming_view": any(
getattr(c, "visualizer_type", None) == "kit" and bool(getattr(c, "streaming_view", False)) for c in cfgs
),
}
"""
The Single Scan.
"""
[docs]
@dataclass
class Scan:
"""Signals gathered from one walk of the config tree (see :func:`scan`).
Every field starts as a plain snapshot computed during that single walk.
Automatic PhysX configurations and RTX placeholders are also recorded so
launch-time resolution can update the physics- and renderer-related fields
without traversing the config tree again. ``needs_kit`` is the headline launch
decision after automatic selections are resolved: a Kit-renderer camera or Isaac
Sim PhysX requires Kit (the launcher additionally forces Kit when
``--visualizer kit`` is requested).
"""
resolved_physics_cfg: PhysicsCfg | None # first physics config in walk order (post --physics override)
effective_cfg: Any # the input config, or its replacement when the config itself was an overridden physics config
visualizer_intent: dict[str, bool]
has_ovrtx: bool
has_kit_camera: bool
has_kit_physics: bool # PhysX (Kit-based)
has_kitless_physics: bool # Newton or OvPhysX
has_ovphysx_physics: bool
needs_kit: bool
def _refresh_physics_scan_flags(config_scan: Scan, concrete_physics_cfgs: list[PhysicsCfg], has_physics: bool) -> None:
"""Refresh physics-derived launch signals from concrete physics configs."""
names = [type(pcfg).__name__ for pcfg in concrete_physics_cfgs]
config_scan.has_kit_physics = "PhysxCfg" in names
config_scan.has_kitless_physics = any(name in _KITLESS_PHYSICS_CFGS for name in names)
config_scan.has_ovphysx_physics = "OvPhysxCfg" in names
config_scan.needs_kit = config_scan.has_kit_camera or config_scan.has_kit_physics or not has_physics
[docs]
def scan(cfg, launcher_args: argparse.Namespace | dict | None = None) -> Scan:
"""Walk *cfg* once, collecting all launch signals and applying ``--physics``.
When the ``physics`` key is present in *launcher_args*, every physics config is
replaced by the requested backend (see :func:`make_physics_cfg`): nested configs
in place, a root config via :attr:`Scan.effective_cfg` (it cannot be mutated in
place). Automatic PhysX configurations and RTX
renderer placeholders (``renderer_type="auto_rtx"``) are also resolved
at this stage using the full *launcher_args* context.
The walk mutates *cfg* in place, and resolving a placeholder consumes it, so
a second walk of the same config observes the same signals and reaches the
same launch decision.
"""
# Livestreaming implies a Kit visualizer; make that visible to auto RTX resolution.
_ensure_livestream_kit_visualizer(launcher_args)
physics_str = _get_arg(launcher_args, "physics", None)
physics_cfgs: list[PhysicsCfg] = []
concrete_physics_cfgs: list[PhysicsCfg] = []
effective_cfg: Any = cfg
has_ovrtx = False
has_auto_rtx = False
has_auto_physx = False
has_kit_camera = False
auto_rtx_locations: list[tuple[Any, Any, bool]] = [] # (parent, key, is_cam_renderer) for each auto RTX placeholder
auto_physx_locations: list[tuple[PhysicsCfg, Any, Any, bool]] = [] # (node, parent, key, is_first_physics)
visited: set[int] = set()
def visit(node, parent, key):
nonlocal effective_cfg, has_ovrtx, has_auto_rtx, has_auto_physx, has_kit_camera
if _is_auto_rtx_renderer(node):
has_auto_rtx = True
auto_rtx_locations.append((parent, key, isinstance(parent, CameraCfg) and key == "renderer_cfg"))
if id(node) in visited:
return
visited.add(id(node))
if isinstance(node, PhysicsCfg):
if physics_str:
node = make_physics_cfg(physics_str)
if parent is not None:
setattr(parent, key, node)
else:
effective_cfg = node
physics_cfgs.append(node)
if _is_auto_physx_physics(node):
has_auto_physx = True
auto_physx_locations.append((node, parent, key, len(physics_cfgs) == 1))
return
else:
concrete_physics_cfgs.append(node)
elif _is_ovrtx_renderer(node):
has_ovrtx = True
elif _is_kit_camera(node):
has_kit_camera = True
try:
children = vars(node)
except TypeError:
return
for name, child in children.items():
if child is None or isinstance(child, (int, float, str, bool)):
continue
visit(child, node, name)
visit(cfg, None, None)
has_physics = bool(physics_cfgs)
config_scan = Scan(
resolved_physics_cfg=physics_cfgs[0] if physics_cfgs else None,
effective_cfg=effective_cfg,
visualizer_intent=_get_visualizer_intent(cfg),
has_ovrtx=has_ovrtx,
has_kit_camera=has_kit_camera,
has_kit_physics=False,
has_kitless_physics=False,
has_ovphysx_physics=False,
needs_kit=False,
)
_refresh_physics_scan_flags(config_scan, concrete_physics_cfgs, has_physics)
# RTX selection depends on the resolved physics backend.
if has_auto_physx:
use_isaac_sim = bool(_get_kit_runtime_sources(config_scan, launcher_args))
for node, parent, key, is_first_physics in auto_physx_locations:
physics_cfg = _resolve_physx_auto_cfg(node, use_isaac_sim)
concrete_physics_cfgs.append(physics_cfg)
if parent is None:
effective_cfg = physics_cfg
config_scan.effective_cfg = physics_cfg
else:
setattr(parent, key, physics_cfg)
if is_first_physics:
config_scan.resolved_physics_cfg = physics_cfg
_refresh_physics_scan_flags(config_scan, concrete_physics_cfgs, has_physics)
# Resolve recorded auto RTX renderer placeholders.
if not has_auto_rtx:
return config_scan
use_isaac_sim = bool(_get_kit_runtime_sources(config_scan, launcher_args))
renderer_factory = IsaacRtxRendererCfg if use_isaac_sim else OVRTXRendererCfg
# Resolve every auto RTX placeholder in place, tracking camera renderers that may require Kit.
has_auto_camera = False
for location in auto_rtx_locations:
if location[0] is None:
raise ValueError("Automatic RTX renderer placeholders cannot be resolved as the root config.")
setattr(location[0], location[1], renderer_factory())
has_auto_camera = has_auto_camera or location[2]
# Update the scan with the resolved auto RTX renderer type and Kit-camera status.
if use_isaac_sim:
config_scan.has_kit_camera = config_scan.has_kit_camera or has_auto_camera
else:
config_scan.has_ovrtx = True
_refresh_physics_scan_flags(config_scan, concrete_physics_cfgs, has_physics)
return config_scan
"""
Launch Decisions (derived purely from a scan).
"""
def _has_kit_visualizer(config_scan: Scan, launcher_args: argparse.Namespace | dict | None) -> bool:
"""Return whether the run requests the Kit visualizer through config or CLI."""
visualizer_types = _get_visualizer_types(launcher_args)
return "kit" in visualizer_types or config_scan.visualizer_intent["has_kit_visualizer"]
def _get_kit_runtime_sources(config_scan: Scan, launcher_args: argparse.Namespace | dict | None) -> tuple[str, ...]:
"""Return the config and launcher components that require Isaac Sim / Kit."""
kit_sources = []
if config_scan.has_kit_physics:
kit_sources.append("Isaac Sim PhysX physics (`PhysxCfg`)")
if config_scan.has_kit_camera:
kit_sources.append('a Kit-based renderer (`IsaacRtxRendererCfg`, `renderer_type="isaac_rtx"`)')
if _has_kit_visualizer(config_scan, launcher_args):
kit_sources.append('the Kit visualizer (`--visualizer kit` / `visualizer_type="kit"`)')
if _get_arg(launcher_args, "experience", ""):
kit_sources.append("an explicit Kit experience")
if _get_livestream_mode(launcher_args) > 0:
kit_sources.append("livestreaming")
if _get_arg(launcher_args, "require_kit", False):
kit_sources.append("the caller's explicit Kit requirement")
if config_scan.needs_kit and not kit_sources:
kit_sources.append("the default Isaac Sim / Kit runtime")
return tuple(kit_sources)
def _format_runtime_sources(sources: tuple[str, ...]) -> str:
"""Format runtime sources as a readable list."""
if len(sources) < 3:
return " and ".join(sources)
return f"{', '.join(sources[:-1])}, and {sources[-1]}"
def _validate_runtime(scan: Scan, kit_sources: tuple[str, ...]) -> None:
"""Raise if the scan and resolved Kit sources form an unsupported combination.
OVRTX is kitless and cannot share a process with Kit-based runtimes (PhysX physics
or any other Kit source); OvPhysX physics is subject to the same process boundary.
"""
if scan.has_ovphysx_physics and kit_sources:
# both would initialize Carbonite in one process; without this guard OvPhysX fails
# deep inside its own library with "Failed to initialize Carbonite and load PhysX plugins"
raise ValueError(
"Invalid backend combination: OvPhysX physics (`OvPhysxCfg`) is kitless and cannot be used together "
f"with Isaac Sim / Kit ({_format_runtime_sources(kit_sources)}).\n"
"\n"
"To fix this, pick one of the following supported combinations:\n"
" * Keep OvPhysX physics and switch to a kitless renderer/visualizer:\n"
" use `OvPhysxCfg` with `OVRTXRendererCfg`\n"
" (and use `--visualizer newton`, `--visualizer rerun`, or `--visualizer viser`, or omit\n"
" the visualizer argument for headless execution.)\n"
" * Keep Isaac Sim / Kit and switch to a Kit-compatible physics backend:\n"
" use `PhysxCfg` with `IsaacRtxRendererCfg`\n"
)
if not scan.has_ovrtx or not kit_sources:
return
raise ValueError(
"Invalid backend combination: the OVRTX renderer (`OVRTXRendererCfg`,"
' `renderer_type="ovrtx"`) is a kitless renderer and cannot be used together'
f" with Isaac Sim / Kit ({_format_runtime_sources(kit_sources)}).\n"
"\n"
"To fix this, pick one of the following supported combinations:\n"
" * Keep Isaac Sim / Kit and switch the renderer:\n"
" use `IsaacRtxRendererCfg`, the Kit-compatible renderer\n"
" * Keep the OVRTX renderer and switch to a kitless physics backend\n"
" (and avoid `--visualizer kit`):\n"
" use `NewtonCfg` or `OvPhysxCfg` with `OVRTXRendererCfg`\n"
)
def _resolve_distributed_device(cfg, launcher_args: argparse.Namespace | dict | None) -> None:
"""Set ``cfg.sim.device`` for distributed training, mirroring AppLauncher's fallback.
When ``--distributed`` restricts each process to one GPU, ``local_rank`` may exceed
the visible device count. The Kit path later overwrites this with ``AppLauncher.device``;
the kitless path relies solely on this value.
"""
if not _get_arg(launcher_args, "distributed", False):
return
import torch
local_rank = int(os.getenv("LOCAL_RANK", "0")) + int(os.getenv("JAX_LOCAL_RANK", "0"))
num_visible_gpus = torch.cuda.device_count()
# Compare against the local device count (not WORLD_SIZE) so multi-node runs work.
device_str = f"cuda:{local_rank}" if local_rank < num_visible_gpus else "cuda:0"
sim_cfg = getattr(cfg, "sim", None)
if sim_cfg is not None:
sim_cfg.device = device_str
set_cuda_device(device_str)
logger.info(
"Distributed device resolved to %s (local_rank=%d, visible_gpus=%d)",
device_str,
local_rank,
num_visible_gpus,
)
[docs]
@contextmanager
def launch_simulation(
cfg,
launcher_args: argparse.Namespace | dict | None = None,
) -> Generator[PhysicsCfg | None, None, None]:
"""Context manager that launches the appropriate simulation runtime for *cfg*.
Walks the config tree once (resolving ``--physics``, validating the
physics/renderer/visualizer combination, and deciding whether Isaac Sim Kit is
needed), then launches ``AppLauncher`` for Kit-based backends (closed on exit) or
does nothing for kitless ones. Cameras are auto-enabled for Kit-renderer sensors.
Yields the resolved physics config, so a script can pass a bare placeholder and
pick the backend from the command line::
with launch_simulation(PhysicsCfg(), args_cli) as physics_cfg:
sim = SimulationContext(SimulationCfg(physics=physics_cfg))
Callers that do not need the value simply omit ``as``.
Args:
cfg: Config tree to scan for backend, renderer, and sensor requirements.
launcher_args: Parsed launcher arguments, typically the script's ``args_cli``. Besides the
arguments added by :func:`add_launcher_args`, the following keys are read when a script
contributes them:
* ``physics``: Backend selector applied to every physics config in *cfg*, see
:func:`make_physics_cfg`.
* ``require_kit``: Whether the caller needs Kit for a reason *cfg* cannot express, e.g.
a tool that reaches a Kit-only extension API. This is additive -- it can only turn a
kitless launch into a Kit one, never the reverse, so a config that already needs Kit
still launches it when the key is absent or ``False``.
"""
if launcher_args is None:
launcher_args = {}
# The single walk: collect every signal, apply the --physics override, and
# resolve the automatic PhysX and RTX placeholders.
config_scan = scan(cfg, launcher_args)
effective_cfg = config_scan.effective_cfg
physics_cfg = config_scan.resolved_physics_cfg
visualizer_types = _get_visualizer_types(launcher_args)
kit_sources = _get_kit_runtime_sources(config_scan, launcher_args)
_validate_runtime(config_scan, kit_sources)
needs_kit = bool(kit_sources)
_set_arg(launcher_args, "visualizer_intent", config_scan.visualizer_intent)
# Kit-based backends apply the Python logging level inside AppLauncher; kitless backends
# never construct it, so honor --verbose / --info here to keep behavior consistent.
if not needs_kit:
apply_python_logging_level(resolve_python_logging_level(launcher_args))
if needs_kit and (config_scan.has_kit_camera or config_scan.visualizer_intent.get("has_kit_streaming_view")):
if not _get_arg(launcher_args, "enable_cameras", False):
logger.info(
"Auto-enabling camera rendering because the scene contains Kit camera sensors "
"or a Kit visualizer with streaming_view=True."
)
_set_arg(launcher_args, "enable_cameras", True)
# Resolve distributed device early, before AppLauncher or physics init.
_resolve_distributed_device(effective_cfg, launcher_args)
visualizer_explicit_none = _get_arg(launcher_args, "visualizer") is None and _get_arg(
launcher_args, "visualizer_explicit", False
)
close_fn: Any = None
if needs_kit:
_ensure_isaac_sim_available()
from isaaclab.utils import has_kit
if not has_kit():
from isaaclab.app import AppLauncher
app_launcher = AppLauncher(launcher_args)
# AppLauncher may refine the device choice; propagate its final value,
# intentionally overwriting the value set by _resolve_distributed_device.
sim_cfg = getattr(effective_cfg, "sim", None)
if sim_cfg is not None and hasattr(app_launcher, "device"):
sim_cfg.device = app_launcher.device
close_fn = app_launcher.app.close
elif visualizer_types or visualizer_explicit_none:
# Kitless path: AppLauncher is skipped, so persist the visualizer selection in
# SettingsManager so SimulationContext._get_cli_visualizer_types() can find it.
from isaaclab.app import AppLauncher
disable_all = visualizer_explicit_none or "none" in visualizer_types
base = vars(launcher_args) if isinstance(launcher_args, argparse.Namespace) else launcher_args
if base is not None:
AppLauncher.sync_visualizer_cli_settings_to_carb(
{**base, "visualizer_explicit": True, "visualizer_disable_all": disable_all}
)
exit_code = 0
try:
# The import stays after the Kit launch decision. With no selected profile this is a
# no-op; with one, it installs process-wide OmniClient routing before user code runs.
from isaaclab.utils.assets import configure_storage_profile
configure_storage_profile()
yield physics_cfg
except Exception:
exit_code = 1
import traceback
traceback.print_exc()
raise
finally:
if close_fn is not None:
if exit_code:
close_fn(exit_code=exit_code)
else:
close_fn()
def _ensure_isaac_sim_available() -> None:
"""Raise ``SystemExit`` with an actionable hint when Isaac Sim / Kit is missing."""
from isaaclab.app import AppLauncher # noqa: PLC0415
if AppLauncher.is_available():
return
isaaclab_path = os.environ.get("ISAACLAB_PATH")
local_sim = os.path.join(isaaclab_path, "_isaac_sim") if isaaclab_path else None
extra_hint = ""
if local_sim and os.path.isdir(local_sim):
launcher, source = ("isaaclab.bat", f'call "{local_sim}\\setup_conda_env.bat"')
if sys.platform != "win32":
launcher, source = ("./isaaclab.sh", f'source "{local_sim}/setup_conda_env.sh"')
extra_hint = (
f" Found a local Isaac Sim at {local_sim} but its environment is not active.\n"
f" Either run via `{launcher} ...` (which sources the Isaac Sim env automatically),\n"
f" or in your current shell run:\n"
f" {source}\n"
)
try:
installed_version = importlib.metadata.version("isaacsim")
except importlib.metadata.PackageNotFoundError:
installed_version = None
if installed_version:
logger.error(
f"\n[ERROR] Isaac Sim {installed_version} is installed, but its full runtime is unavailable.\n"
"\n"
" This environment requires Isaac Sim and Omniverse Kit.\n"
" PhysX backend and Kit visualizer require Isaac Sim.\n"
"\n"
" The current Python environment does not expose the SimulationApp API.\n"
f"{extra_hint}"
" Install the full Isaac Sim runtime from the Isaac Lab directory by running:\n"
" uv run isaaclab -i isaacsim\n"
"\n"
" See https://isaac-sim.github.io/IsaacLab/main/source/setup/installation for details.\n"
)
raise SystemExit(1)
logger.error(
"\n[ERROR] Isaac Sim is not installed or not found on PYTHONPATH.\n"
"\n"
" This environment requires Isaac Sim and Omniverse Kit.\n"
" PhysX backend and Kit visualizer currently requires Isaac Sim.\n"
"\n"
f"{extra_hint}"
" To fix this, ensure Isaac Sim is installed and available in the current environment.\n"
"\n"
" See https://isaac-sim.github.io/IsaacLab/main/source/setup/installation for details.\n"
)
raise SystemExit(1)