Skip to content
91 changes: 91 additions & 0 deletions scripts/demos/demo_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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

"""
This script contains helper functions for the demos.
"""

import textwrap
from contextlib import contextmanager


@contextmanager
def resolve_backend_and_visualizer(args, newton_cfg=None):
"""Resolve physics + visualizer cfgs from ``--physics`` / ``--visualizer``.

Yields ``(physics_cfg, visualizer_cfg)``. Kit is launched when required by
either the visualizer or the physics backend, and closed automatically on exit.
"""

# Resolve physics cfg
if args.physics == "physx":
from isaaclab_physx.physics import PhysxCfg

physics_cfg = PhysxCfg()
elif args.physics == "newton_mjwarp":
from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg

DEFAULT_NEWTON_CFG = NewtonCfg(
solver_cfg=MJWarpSolverCfg(
njmax=70,
nconmax=70,
ls_iterations=40,
cone="elliptic",
impratio=100,
ls_parallel=False,
integrator="implicitfast",
),
num_substeps=2,
)
physics_cfg = newton_cfg or DEFAULT_NEWTON_CFG
else:
raise ValueError(f"Unsupported --physics value: {args.physics}")

# Resolve visualizer cfg
if not isinstance(args.visualizer, list) or len(args.visualizer) != 1:
raise ValueError("Demos support exactly one --visualizer value: kit or newton.")
viz_type = args.visualizer[0]
if viz_type == "kit":
from isaaclab_visualizers.kit import KitVisualizerCfg

visualizer_cfg = KitVisualizerCfg()
elif viz_type == "newton":
from isaaclab_visualizers.newton import NewtonVisualizerCfg

visualizer_cfg = NewtonVisualizerCfg()
else:
raise ValueError(f"Unsupported --visualizer value: {viz_type}")

# PhysX requires Isaac Sim Kit extensions even when rendering through a
# standalone visualizer such as Newton.
close_fn = None
if viz_type == "kit" or args.physics == "physx":
from isaaclab.app import AppLauncher

args.visualizer = [viz_type]
close_fn = AppLauncher(args).app.close

try:
yield physics_cfg, visualizer_cfg
finally:
# No-op for the newton visualizer; close Kit automatically upon exit
if close_fn is not None:
close_fn()


def has_no_alive_visualizer_window(sim) -> bool:
"""Check if there are no alive visualizer windows."""
visualizers = sim.visualizers or ()
return not any(v.is_running() and not v.is_closed for v in visualizers)


def get_usage_examples(doc: str) -> str:
"""Return usage examples from the module docstring."""
marker = ".. code-block:: bash"
if marker not in doc:
return ""

examples = textwrap.dedent(doc.split(marker, maxsplit=1)[1]).strip()
return "Examples:\n" + textwrap.indent(examples, " ")
76 changes: 46 additions & 30 deletions scripts/demos/quadrupeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,53 @@

.. code-block:: bash

# Usage
# Usage with the default PhysX backend (launches Isaac Sim Kit by default).
./isaaclab.sh -p scripts/demos/quadrupeds.py
./isaaclab.sh -p scripts/demos/quadrupeds.py --visualizer kit
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics physx
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics physx --visualizer kit

# Usage with the kit-less Newton (MJWarp) backend (launches Isaac Sim Kit by default).
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics newton_mjwarp
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics newton_mjwarp --visualizer kit

# Usage with the Newton (MJWarp) backend without Kit (launches Newton visualizer).
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics newton_mjwarp --visualizer newton

# Usage with the PhysX backend and Newton visualizer.
# PhysX still launches Isaac Sim Kit headless because it depends on Kit extensions.
./isaaclab.sh -p scripts/demos/quadrupeds.py --physics physx --visualizer newton

"""

"""Launch Isaac Sim Simulator first."""
"""Parse CLI first so we can decide whether to launch Isaac Sim Kit."""

import argparse

from demo_helper import get_usage_examples, has_no_alive_visualizer_window, resolve_backend_and_visualizer

from isaaclab.app import AppLauncher

# add argparse arguments
parser = argparse.ArgumentParser(description="This script demonstrates different legged robots.")
parser = argparse.ArgumentParser(
description="This script demonstrates different legged robots.",
epilog=get_usage_examples(str(__doc__)),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--physics", default="physx", choices=["physx", "newton_mjwarp"], help="Physics backend.")
# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
# demos should open Kit visualizer by default
parser.set_defaults(visualizer=["kit"])
# parse the arguments
args_cli = parser.parse_args()

# launch omniverse app
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app

"""Rest everything follows."""

import numpy as np
import torch

import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation

##
# Pre-defined configs
##
from isaaclab_assets.robots.anymal import ANYMAL_B_CFG, ANYMAL_C_CFG, ANYMAL_D_CFG # isort:skip
from isaaclab_assets.robots.spot import SPOT_CFG # isort:skip
from isaaclab_assets.robots.unitree import UNITREE_A1_CFG, UNITREE_GO1_CFG, UNITREE_GO2_CFG # isort:skip
Expand Down Expand Up @@ -124,14 +136,15 @@ def design_scene() -> tuple[dict, list[list[float]]]:
return scene_entities, origins


def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articulation], origins: torch.Tensor):
def run_simulator(sim, entities: dict, origins):
"""Runs the simulation loop."""
# Define simulation stepping
sim_dt = sim.get_physics_dt()
sim_time = 0.0
count = 0
# Simulate physics
while simulation_app.is_running():
# Exit when every visualizer window has been closed (works for kit and newton)
while not has_no_alive_visualizer_window(sim):
# reset
if count % 200 == 0:
# reset counters
Expand Down Expand Up @@ -175,24 +188,27 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula

def main():
"""Main function."""

# Initialize the simulation context
sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01))
# Set main camera
sim.set_camera_view(eye=[2.5, 2.5, 2.5], target=[0.0, 0.0, 0.0])
# design scene
scene_entities, scene_origins = design_scene()
scene_origins = torch.tensor(scene_origins, device=sim.device)
# Play the simulator
sim.reset()
# Now we are ready!
print("[INFO]: Setup complete...")
# Run the simulator
run_simulator(sim, scene_entities, scene_origins)
with resolve_backend_and_visualizer(args_cli) as (physics_cfg, visualizer_cfg):
# define simulation configuration
sim_cfg = sim_utils.SimulationCfg(
dt=0.005, device=args_cli.device, physics=physics_cfg, visualizer_cfgs=[visualizer_cfg]
)
# Initialize the simulation context
sim = sim_utils.SimulationContext(sim_cfg)
# Set main camera
sim.set_camera_view(eye=[2.5, 2.5, 2.5], target=[0.0, 0.0, 0.0])
# design scene
scene_entities, scene_origins = design_scene()
# convert origins to tensor
scene_origins = torch.tensor(scene_origins, device=sim.device)
# Play the simulator
sim.reset()
# Now we are ready!
print("[INFO]: Setup complete...")
# Run the simulator
run_simulator(sim, scene_entities, scene_origins)


if __name__ == "__main__":
# run the main function
main()
# close sim app
simulation_app.close()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fixed
^^^^^

* Fixed ``from isaaclab.assets import Articulation`` and ``from isaaclab.sim import SimulationContext``
loading ``pxr`` at module-import time, which forced :class:`~isaaclab.app.AppLauncher`
to run before any such import and blocked kit-less workflows that bind these
classes at module top.
22 changes: 10 additions & 12 deletions source/isaaclab/isaaclab/assets/asset_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
import torch
import warp as wp

from pxr import Usd

import isaaclab.sim as sim_utils
from isaaclab.physics import PhysicsEvent, PhysicsManager
from isaaclab.sim.simulation_context import SimulationContext
from isaaclab.sim.utils.stage import get_current_stage

if TYPE_CHECKING:
from pxr import Usd

from .asset_base_cfg import AssetBaseCfg


Expand Down Expand Up @@ -81,7 +79,7 @@ def __init__(self, cfg: AssetBaseCfg):
# flag for whether the asset is initialized
self._is_initialized = False
# get stage handle
self.stage: Usd.Stage = get_current_stage()
self.stage: Usd.Stage = sim_utils.get_current_stage()

# spawn the asset
# determine path where prims should exist after spawn
Expand Down Expand Up @@ -208,11 +206,11 @@ def set_debug_vis(self, debug_vis: bool) -> bool:
# toggle debug visualization handles (Kit/omni only for PhysX backend)
if debug_vis:
if self._debug_vis_handle is None:
sim_ctx = SimulationContext.instance()
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is not None:
self._debug_vis_handle = sim_ctx.vis_marker_registry.add_debug_vis_callback(self)
else:
sim_ctx = SimulationContext.instance()
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is not None:
sim_ctx.vis_marker_registry.clear_debug_vis_callback(self)
else:
Expand Down Expand Up @@ -353,7 +351,7 @@ def _debug_vis_callback(self, event):

def _register_callbacks(self):
"""Registers physics lifecycle callbacks via the current backend's physics manager."""
physics_mgr_cls = SimulationContext.instance().physics_manager
physics_mgr_cls = sim_utils.SimulationContext.instance().physics_manager

# note: use weakref on callbacks to ensure that this object can be deleted when its destructor is called.
obj_ref = weakref.proxy(self)
Expand Down Expand Up @@ -397,15 +395,15 @@ def _initialize_callback(self, event):
:attr:`PhysicsEvent.PHYSICS_READY` is dispatched by the current backend.
"""
if not self._is_initialized:
self._backend = SimulationContext.instance().physics_manager.get_backend()
self._device = SimulationContext.instance().physics_manager.get_device()
self._backend = sim_utils.SimulationContext.instance().physics_manager.get_backend()
self._device = sim_utils.SimulationContext.instance().physics_manager.get_device()
self._initialize_impl()
self._is_initialized = True

def _invalidate_initialize_callback(self, event):
"""Invalidates the scene elements."""
self._is_initialized = False
sim_ctx = SimulationContext.instance()
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is not None:
sim_ctx.vis_marker_registry.clear_debug_vis_callback(self)
else:
Expand Down Expand Up @@ -438,7 +436,7 @@ def _clear_callbacks(self) -> None:
if self._prim_deletion_handle is not None:
self._prim_deletion_handle.deregister()
self._prim_deletion_handle = None
sim_ctx = SimulationContext.instance()
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is not None:
sim_ctx.vis_marker_registry.clear_debug_vis_callback(self)
else:
Expand Down
18 changes: 11 additions & 7 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
import toml
import torch

from pxr import Gf, Usd, UsdGeom, UsdPhysics, UsdUtils

import isaaclab.sim as sim_utils
import isaaclab.sim.utils.stage as stage_utils
from isaaclab.app.settings_manager import SettingsManager
from isaaclab.envs.utils.recording_hooks import run_recording_hooks_after_visualizers
from isaaclab.markers.vis_marker_registry import VisMarkerRegistry
Expand All @@ -32,12 +29,13 @@
from isaaclab.renderers.render_context import RenderContext
from isaaclab.scene_data import SceneDataProvider
from isaaclab.sim.service_locator import ServiceLocator
from isaaclab.sim.utils import create_new_stage
from isaaclab.utils.string import clear_resolve_matching_names_cache
from isaaclab.utils.version import has_kit
from isaaclab.visualizers.base_visualizer import BaseVisualizer

if TYPE_CHECKING:
from pxr import Usd

from isaaclab.cloner.clone_plan import ClonePlan

from .simulation_cfg import SimulationCfg
Expand Down Expand Up @@ -111,13 +109,17 @@ def __init__(self, cfg: SimulationCfg | None = None):
if type(self)._instance is not None:
return # Already initialized

from pxr import UsdUtils # noqa: PLC0415

from isaaclab.sim.utils import stage as stage_utils # noqa: PLC0415

# Store config
self.cfg = SimulationCfg() if cfg is None else cfg

# Get or create stage based on config
stage_cache = UsdUtils.StageCache.Get()
if self.cfg.create_stage_in_memory:
self.stage = create_new_stage()
self.stage = sim_utils.create_new_stage()
else:
# Prefer the thread-local current stage (set by create_new_stage / test fixtures)
# over cache lookup, since the cache may contain stale stages from prior tests.
Expand All @@ -126,7 +128,7 @@ def __init__(self, cfg: SimulationCfg | None = None):
self.stage = current
else:
all_stages = stage_cache.GetAllStages() if stage_cache.Size() > 0 else [] # type: ignore[union-attr]
self.stage = all_stages[0] if all_stages else create_new_stage()
self.stage = all_stages[0] if all_stages else sim_utils.create_new_stage()

# Ensure stage is in the USD cache
stage_id = stage_cache.GetId(self.stage).ToLongInt() # type: ignore[union-attr]
Expand Down Expand Up @@ -322,6 +324,8 @@ def _apply_nested(data: dict[str, Any], path: str = "") -> None:

def _init_usd_physics_scene(self) -> None:
"""Create and configure the USD physics scene."""
from pxr import Gf, UsdGeom, UsdPhysics # noqa: PLC0415

cfg = self.cfg
with sim_utils.use_stage(self.stage):
# Set stage conventions for metric units
Expand Down Expand Up @@ -913,7 +917,7 @@ def clear_instance(cls) -> None:

# Tear down the stage. We skip clear_stage() (prim-by-prim deletion) since
# close_stage() + app shutdown destroy the entire stage at once.
stage_utils.close_stage()
sim_utils.close_stage()

# Discard cached name-resolution data from destroyed assets
clear_resolve_matching_names_cache()
Expand Down
Loading