Skip to content

Refactor sim launcher into a single config scan#5892

Open
ooctipus wants to merge 11 commits into
isaac-sim:developfrom
ooctipus:octi/sim-launcher-single-scan
Open

Refactor sim launcher into a single config scan#5892
ooctipus wants to merge 11 commits into
isaac-sim:developfrom
ooctipus:octi/sim-launcher-single-scan

Conversation

@ooctipus
Copy link
Copy Markdown
Collaborator

@ooctipus ooctipus commented Jun 1, 2026

Summary

  • Rework isaaclab.app.sim_launcher so launch decisions come from a single pass over the env config. One _scan traversal collects the physics backend, OVRTX renderer presence, Kit-camera requirements, and visualizer intent, and applies the --physics CLI override in place, replacing the previous multi-walk "patches over patches" flow.
  • launch_simulation now accepts a any cfg as env_cfg, resolves it against the launcher args, and yields the resolved config for SimulationContext. This makes partial cfg override and simulation launch convinient.
  • Repointed all benchmark/RL/demo script imports to isaaclab.app, and made scripts/demos/quadrupeds.py usable with both the Kit (PhysX) and kit-less Newton (MJWarp) backends.

Test plan

  • ./isaaclab.sh -p -m pytest source/isaaclab/test/app/test_sim_launcher_physics_override.py
  • ./isaaclab.sh -p -m pytest source/isaaclab_tasks/test/test_sim_launcher_visualizer_intent.py source/isaaclab_tasks/test/test_distributed_device_resolution.py source/isaaclab_tasks/test/test_preset_kit_decision.py source/isaaclab_tasks/test/test_runtime_compatibility.py
  • ./isaaclab.sh -p scripts/demos/quadrupeds.py --physics newton_mjwarp --headless
  • ./isaaclab.sh -f (pre-commit) passes

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jun 1, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR is a significant refactoring that consolidates the simulation launcher logic into a single-pass config scan. The core idea is sound: instead of multiple traversals patching over patches, one walk collects all needed signals (physics backend, renderer, camera requirements, visualizer intent) and applies --physics overrides in place.

What I Like

  1. Cleaner architecture: The _Scan dataclass approach is more maintainable than scattered helper functions
  2. Breaking change documented: The changelog entries clearly document the removal of the compatibility shim
  3. Import cleanup: Moving canonical exports to isaaclab.app makes the API surface cleaner
  4. make_physics_cfg factory: Nice addition for scripts that want to build configs programmatically
  5. quadrupeds.py demo: Great that it now demonstrates both PhysX and Newton backends

Concerns & Questions

1. Type annotation inconsistency (minor)

def run_simulator(sim, entities: dict[str, Articulation], origins: torch.Tensor):

The sim parameter lost its type annotation (sim_utils.SimulationContext). This is likely intentional since the import is deferred, but a TYPE_CHECKING import could preserve the annotation.

2. Potential edge case in _scan recursion

The visit function skips primitive types, but what about list or tuple containers? If a config has physics_cfgs: list[PhysicsCfg], those would be skipped since vars() would raise TypeError on the list. Is this intentional?

try:
    children = vars(node)
except TypeError:
    return

The previous _scan_config had the same limitation, so this is not a regression, but worth noting.

3. Hardcoded backend names (design question)

_KITLESS_PHYSICS_CFGS = ("NewtonCfg", "OvPhysxCfg")

Using string matching on class names is pragmatic but fragile. If someone subclasses NewtonCfg with a different name, they'd need to know about this list. The DeformableNewtonCfg comment acknowledges this, but it's a subtle contract.

Would a class attribute like is_kitless = True on the base configs be cleaner long-term?

4. Missing test for make_physics_cfg

The new make_physics_cfg function isn't directly tested. The existing tests exercise _scan with physics_str but not the standalone factory with invalid input (the ValueError path).

5. Visualizer loop termination in quadrupeds.py

while True:
    if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers):
        break

This assumes sim.visualizers is always defined. Is this guaranteed for all SimulationContext instances, or could this raise AttributeError when running headless without visualizers?

6. Changelog missing removed functions

The isaaclab_tasks changelog mentions removing add_launcher_args, launch_simulation, but the diff shows compute_kit_requirements, validate_runtime_compatibility, and apply_launcher_physics_override were also removed from the public API. Consider enumerating all removed symbols.

Test Plan Verification

The test plan items look reasonable. I'd suggest also running:

  • A kitless Newton test without visualizer (--visualizer none)
  • The distributed device resolution test with mocked multi-GPU environment

Nits

  1. Line 431 in sim_launcher.py has a bare except Exception: followed by traceback.print_exc(). This is fine for debugging but might swallow context in production. Consider logger.exception() instead.

  2. The _get_arg / _set_arg helpers are clean, but the pattern of checking isinstance(launcher_args, ...) twice (once in _get_arg, once in the caller) could be simplified with a small wrapper class.


Overall, this is a well-structured refactor that improves code organization. The breaking changes are clearly documented. Requesting changes mainly for the edge cases and test coverage mentioned above.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR replaces the multi-pass sim_launcher with a single _scan traversal that collects physics backend, OVRTX renderer, Kit-camera, and visualizer signals in one walk, then derives all launch decisions from the resulting _Scan dataclass. It also moves ownership of launch_simulation, add_launcher_args, and the new make_physics_cfg factory from isaaclab_tasks.utils into the core isaaclab.app package, removing the compatibility shim and repointing all benchmark, RL, and demo script imports.

  • sim_launcher.py relocated to isaaclab/app/ with a single-pass _scan replacing the previous multi-walk approach; launch_simulation now yields the resolved PhysicsCfg so callers can pass a bare placeholder and select the backend from the CLI.
  • scripts/demos/quadrupeds.py extended to support both PhysX and Newton (MJWarp) backends; the run loop replaces simulation_app.is_running() with a visualizer-window check.
  • All RL, benchmark, and export scripts updated to import launch_simulation / add_launcher_args directly from isaaclab.app; test monkeypatches updated from compute_kit_requirements to _Scan.needs_kit.

Confidence Score: 3/5

The demo script has a NameError that fires at import time, an infinite loop in headless mode, and the core package now unconditionally imports all three physics backends — meaning a minimal installation missing any one of them breaks the entire isaaclab.app import chain.

The core scan-and-launch logic is clean and the RL/benchmark import migrations are correct. However, the Articulation annotation in quadrupeds.py raises NameError at import time (no from future import annotations, TYPE_CHECKING-only import), the while True loop has no exit in headless mode (directly contradicting the --headless test plan item), and moving unconditional physics-backend imports to the core package could silently break environments missing any one of the three optional backends.

scripts/demos/quadrupeds.py (NameError, infinite loop, is_closed inconsistency) and source/isaaclab/isaaclab/app/sim_launcher.py (unconditional top-level backend imports)

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/app/sim_launcher.py New core module implementing single-pass config scan; top-level imports of all three physics backends make them hard deps of the core package
scripts/demos/quadrupeds.py Refactored to dual-backend demo; NameError from TYPE_CHECKING-only Articulation annotation, infinite loop in headless mode, and is_closed attribute/method inconsistency
source/isaaclab_tasks/isaaclab_tasks/utils/sim_launcher.py Deleted compatibility shim — all callers correctly migrated to isaaclab.app imports
source/isaaclab/isaaclab/app/init.pyi Type stub updated to re-export add_launcher_args, launch_simulation, make_physics_cfg from the new sim_launcher module
source/isaaclab_tasks/isaaclab_tasks/utils/init.pyi Removed deprecated exports (add_launcher_args, compute_kit_requirements, validate_runtime_compatibility) from isaaclab_tasks.utils type stub
source/isaaclab_tasks/test/test_preset_kit_decision.py Tests updated to use _scan().needs_kit instead of compute_kit_requirements; logically equivalent
source/isaaclab_tasks/test/test_distributed_device_resolution.py Monkeypatches updated from compute_kit_requirements to _Scan.needs_kit property; coverage maintained
source/isaaclab_tasks/test/test_runtime_compatibility.py Test adapter wraps _scan + _validate_runtime to preserve existing test interface; no coverage gaps

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["launch_simulation(env_cfg, launcher_args)"] --> B["_scan(env_cfg, --physics override)"]
    B --> C{_Scan}
    C --> D["physics_cfgs\nhas_ovrtx\nhas_kit_camera\nvisualizer_intent"]
    A --> E["_validate_runtime(scan, launcher_args)"]
    E --> F{needs_kit?}
    F -- "has_kit_camera OR no kitless physics\nOR --visualizer kit" --> G["_ensure_isaac_sim_available()\nAppLauncher(launcher_args)"]
    F -- "kitless path" --> H["sync_visualizer_cli_settings_to_carb\n(if visualizer flags present)"]
    G --> I["yield physics_cfg"]
    H --> I
    I --> J["SimulationContext(SimulationCfg(physics=physics_cfg))"]
    J --> K["run loop"]
    K --> L["finally: app.close()"]
Loading

Reviews (1): Last reviewed commit: "add string guard" | Re-trigger Greptile



def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articulation], origins: torch.Tensor):
def run_simulator(sim, entities: dict[str, "Articulation"], origins: torch.Tensor):
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.

P1 Articulation annotation causes NameError at runtime

Articulation is imported only under TYPE_CHECKING (line 48–49), which is False at runtime. Without from __future__ import annotations, Python evaluates function annotations eagerly when the def statement executes, so dict[str, Articulation] immediately raises NameError: name 'Articulation' is not defined the moment this module is imported. Either add from __future__ import annotations at the top of the file, or quote the annotation as "dict[str, Articulation]".

Comment on lines +24 to +26
from isaaclab_newton.physics import NewtonCfg
from isaaclab_ovphysx.physics import OvPhysxCfg
from isaaclab_physx.physics import PhysxCfg
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.

P1 Unconditional top-level imports of all three physics backends

NewtonCfg, OvPhysxCfg, and PhysxCfg are imported at module load time from isaaclab_newton, isaaclab_ovphysx, and isaaclab_physx. This means any from isaaclab.app import launch_simulation call fails immediately if any backend package is not installed — even in a PhysX-only environment where Newton was never needed. The rest of the module uses string-name matching (_KITLESS_PHYSICS_CFGS) precisely to avoid this hard dependency; only make_physics_cfg actually needs the class objects. Moving these three imports inside make_physics_cfg would restore the lazy-loading behaviour and eliminate the hard dependency on optional backends for the core package.

Comment thread scripts/demos/quadrupeds.py Outdated
Comment on lines +143 to +146
while True:
# exit when every visualizer window has been closed (works for kit and newton)
if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers):
break
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.

P1 Headless mode never terminates

The only exit point from while True: is if sim.visualizers and not any(...). When --headless is passed (as in the test plan), sim.visualizers is empty/None, so the outer and short-circuits and the loop runs forever. The old while simulation_app.is_running(): guard handled termination via Kit's lifecycle. The new code needs an equivalent: a step counter, a SIGINT handler, or checking sim.is_running() if that exists.

Comment thread scripts/demos/quadrupeds.py Outdated
while simulation_app.is_running():
while True:
# exit when every visualizer window has been closed (works for kit and newton)
if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers):
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 v.is_closed is accessed as an attribute while v.is_running() is called as a method. If is_closed is a callable rather than a property, the expression not v.is_closed always evaluates to True (a bound method object is truthy), so the break condition never fires even when every window is closed. Calling it consistently with () guards against this.

Suggested change
if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers):
if sim.visualizers and not any(v.is_running() and not v.is_closed() for v in sim.visualizers):

Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

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

🔄 Updated Review (commit 1704a47)

Latest commit moves import isaaclab.sim as sim_utils from local function scope to module level in quadrupeds.py and removes the unused --visualizer CLI argument.


What changed since c29c2da

Change Assessment
import isaaclab.sim as sim_utils moved to module level ✅ Good practice — eliminates repeated import overhead per function call. Import was previously duplicated inside design_scene() and main().
Removed --visualizer argument from argparse ✅ Correct — this argument was defined but never consumed by the script. Likely dead code from an earlier iteration.

Previous findings — status update

# Finding Status
🟡 1 needs_kit=True when no physics config exists Addressed (earlier commit)
🟡 2 scan does not traverse list/tuple/dict containers Still applicable
🟡 3 Test plan references non-existent test file Still applicable
🔵 4 _is_kit_camera imports from isaaclab_tasks Still applicable
✅ 5 Headless infinite loop in quadrupeds.py Fixed (commit 767747d)
🔵 6 launch_simulation yield type change Still applicable

Summary of all reviewed commits (since 767e53a)

  1. 68ad5cf — Rename env_cfgcfg parameter across scan/launch APIs
  2. 57333b1 — Migrate imports from isaaclab_tasks.utils to isaaclab.app
  3. 767747d — Fix headless infinite loop in quadrupeds.py
  4. c29c2da — Convert section comments to triple-quoted banners
  5. 1704a47 — Remove local imports, delete unused --visualizer arg

Verdict: Clean incremental cleanup. Module-level import is appropriate given the unconditional use of sim_utils throughout the script, and removing dead CLI arguments improves maintainability. No new issues introduced.


Update (bb9ae16): Large restructuring commit — tasks refactored into flat core/contrib layout, sim_launcher.py moved from isaaclab_tasks.utils to isaaclab.app, deferred pxr imports, PhysX tensor import path fixes (omni.physics.tensors.apiomni.physics.tensors), new resample_interval_on_reset feature, and removal of deprecated environments.

Inline comment status:

  • Articulation NameError (P1, quadrupeds.py L135): Fixed — annotation is now quoted as "Articulation"
  • ⏸️ Unconditional top-level backend imports in sim_launcher.py (P1): Not addressed in this commit
  • ⏸️ Headless infinite loop in quadrupeds.py (P1): Not addressed — loop condition still has no fallback for empty sim.visualizers
  • ⏸️ v.is_closed attribute vs method call (P2): Not addressed

No new issues found in this commit. The refactoring is mechanical and correct — all renamed import paths are consistent, and the deferred pxr imports properly use TYPE_CHECKING guards with runtime-local imports where needed.


Update (bb9ae16): Large restructuring commit — tasks refactored into flat core/contrib layout, sim_launcher.py moved from isaaclab_tasks.utils to isaaclab.app, deferred pxr imports, PhysX tensor import path fixes (omni.physics.tensors.apiomni.physics.tensors), new resample_interval_on_reset feature, and removal of deprecated environments.

Inline comment status:

  • Articulation NameError (P1, quadrupeds.py L135): Fixed — annotation is now quoted as "Articulation"
  • ⏸️ Unconditional top-level backend imports in sim_launcher.py (P1): Not addressed in this commit
  • ⏸️ Headless infinite loop in quadrupeds.py (P1): Not addressed — loop condition still has no fallback for empty sim.visualizers
  • ⏸️ v.is_closed attribute vs method call (P2): Not addressed

No new issues found in this commit. The refactoring is mechanical and correct — all renamed import paths are consistent, and the deferred pxr imports properly use TYPE_CHECKING guards with runtime-local imports where needed.

Comment thread scripts/demos/quadrupeds.py Outdated
count = 0
# Simulate physics
while simulation_app.is_running():
while True:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we put sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers) here?

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.

yes we can!

Copy link
Copy Markdown
Collaborator

@AntoineRichard AntoineRichard left a comment

Choose a reason for hiding this comment

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

It looks ok, should this also cover the leapp export script?

raise ValueError(f"Invalid physics config: {physics_cfg_str!r} (expected 'physx', 'newton_mjwarp', or 'ovphysx').")


# -- node predicates ---------------------------------------------------------
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In general this type of comment is not homogeneous across our code base. Sometimes we use:

# -- node predicates ---------------------------------------------------------

Sometimes:

"""
Node Predicates
"""

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.

Done

return self.has_kit_camera or not self.has_kitless_physics


def _scan(env_cfg, physics_str: str | None = None) -> _Scan:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be public?

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.

Done



@dataclass
class _Scan:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be public?

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.

Done

Comment on lines +142 to +175
@dataclass
class _Scan:
"""Signals gathered from one walk of the config tree (see :func:`_scan`)."""

physics_cfgs: list[PhysicsCfg] # physics configs in walk order (post --physics override)
has_ovrtx: bool
has_kit_camera: bool
visualizer_intent: dict[str, bool]
effective_cfg: Any # env_cfg, or the new physics config when env_cfg was an overridden root
physics_cfg: PhysicsCfg | None # config produced by the --physics override, else None

@property
def resolved_physics_cfg(self) -> PhysicsCfg | None:
return self.physics_cfgs[0] if self.physics_cfgs else None

@property
def has_kit_physics(self) -> bool:
return any(type(cfg).__name__ == "PhysxCfg" for cfg in self.physics_cfgs)

@property
def has_kitless_physics(self) -> bool:
return any(type(cfg).__name__ in _KITLESS_PHYSICS_CFGS for cfg in self.physics_cfgs)

@property
def has_ovphysx_physics(self) -> bool:
return any(type(cfg).__name__ == "OvPhysxCfg" for cfg in self.physics_cfgs)

@property
def needs_kit(self) -> bool:
"""Whether the config requires Kit: a Kit-renderer camera or non-kitless physics.

The launcher additionally forces Kit when ``--visualizer kit`` is requested.
"""
return self.has_kit_camera or not self.has_kitless_physics
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We're doing a dataclass already, does it make sense to add properties? Are those dynamic? Everything looks static to me I'm not sure we need to define these as properties vs variables.

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.

Done

# Origin 1 with Anymal B
sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0])
# -- Robot
anymal_b = Articulation(ANYMAL_B_CFG.replace(prim_path="/World/Origin1/Robot"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we move back to Articulation by importing the class in this function?

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.

We can, tho generally the recommended practice is to not import anything within function. That rule was strict prior IsaacLab3.0, and we relaxed it a bit due to all the dependency issues. But I think it will be good to slowly resolve all the dependency issue instead of accepting local import is ok.



def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articulation], origins: torch.Tensor):
def run_simulator(sim, entities: dict[str, "Articulation"], origins: torch.Tensor):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we keep the hints by TYPE_CHECKING?

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.

I think its already type checked

@ooctipus ooctipus requested a review from jtigue-bdai as a code owner June 1, 2026 19:30
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 1, 2026
ooctipus added 10 commits June 1, 2026 21:49
Address review feedback: the derived _Scan values are static once the
single walk completes, so compute them as plain fields in _scan instead
of properties. Also drop the unused physics_cfg field, which was set but
never read.

Update the two tests that forced the kitless path by patching the old
needs_kit property to instead wrap _scan and set the now-plain field.
Promote the config-tree walk (_scan) and its result (_Scan) to public
symbols scan and Scan, exported from isaaclab.app, so callers can inspect
launch signals without launching. Rename the launch_simulation local to
config_scan to avoid shadowing the new scan function, repoint tests to the
public names, and document the public sim_launcher API.
The scan and launch_simulation entry points accept any config object (a
bare PhysicsCfg, an env cfg, etc.), so the env_cfg parameter name was
misleadingly narrow. Rename it to the generic cfg throughout sim_launcher
and update the one keyword caller in the quadrupeds demo.
The play_<lib>, play, train, and random/zero agent entry points still
imported add_launcher_args and launch_simulation from isaaclab_tasks.utils,
which broke after the sim_launcher shim was removed (ImportError at startup,
caught by the typed-preset CLI test). Import them from isaaclab.app instead
and fix the stale cross-reference in preset_cli.
Replace the while True loop and if/break with a loop condition that steps
while a visualizer window is open (or none exist, e.g. headless), per
review feedback. Behavior is unchanged.
Switch the in-file section dividers from the ad-hoc # -- ... --- comments
to the triple-quoted banner style used across the rest of the codebase,
per review feedback.
@ooctipus ooctipus force-pushed the octi/sim-launcher-single-scan branch from 1704a47 to f0417fb Compare June 2, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants