Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Changed
^^^^^^^

* Changed :func:`~isaaclab.app.scan` to request the Kit visualizer for livestreamed runs itself,
instead of relying on :func:`~isaaclab.app.launch_simulation` to do so beforehand. Callers that
scan a config directly now resolve the ``rtx`` renderer preset the same way the launcher does.
14 changes: 9 additions & 5 deletions source/isaaclab/isaaclab/app/sim_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,14 @@ def scan(cfg, launcher_args: argparse.Namespace | dict | None = None) -> Scan:
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)

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 · Api — scan mutates launcher_args without documenting it

scan is public and now forces the Kit visualizer into the caller-owned launcher_args for livestream runs, a side effect previously confined to launch_simulation. The docstring updated in this same hunk only states that cfg is mutated in place, yet show_run_summary reads args_cli afterwards for the visualizer row, so a direct caller can observe its namespace changed silently. Add one docstring line stating that livestream runs get the Kit visualizer written back into launcher_args.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Livestreaming requires a Kit viewport, but nothing in the config says so. scan needs to be self sufficient so that when it is called from the show_summary this information is already known


physics_str = _get_arg(launcher_args, "physics", None)
physics_cfgs: list[PhysicsCfg] = []
concrete_physics_cfgs: list[PhysicsCfg] = []
Expand Down Expand Up @@ -509,11 +516,8 @@ def launch_simulation(
if launcher_args is None:
launcher_args = {}

# Livestreaming implies a Kit visualizer; make that visible to auto RTX
# resolution during the single scan.
_ensure_livestream_kit_visualizer(launcher_args)

# The single walk: collect every signal and apply the --physics override.
# 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Fixed
^^^^^

* Fixed the train and play startup summary reporting ``physics=physx`` and ``renderer=rtx`` by the
name the command line asked for. Both name a backend family that is resolved at launch, so the
summary now names the backend the run will actually use next to the family it was asked for --
``rtx (ovrtx)`` for a kitless ``physics=ovphysx renderer=rtx`` run and ``rtx (isaacsim_rtx)``
when the run needs Kit.
* Fixed the summary of a ``--video`` training run reporting no visualizer, which happened because
the recording visualizer was injected after the summary was printed.
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def run(argv: list[str]) -> None:
args_cli = _parse_args(argv)
with startup_screen(args_cli, num_stages=3) as screen:
env_cfg, agent_cfg = resolve_task_config(args_cli.task, args_cli.agent)
show_run_summary(screen, args_cli, env_cfg, library="rl_games", action="train")
pre_launch_video_config(env_cfg, args_cli=args_cli)
show_run_summary(screen, args_cli, env_cfg, library="rl_games", action="train")
screen.stage("Launching simulation")
with launch_simulation(env_cfg, args_cli):
apply_env_overrides(args_cli, env_cfg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ def _run(args_cli: argparse.Namespace) -> None:

with startup_screen(args_cli, num_stages=3) as screen:
env_cfg, agent_cfg = resolve_task_config(args_cli.task, args_cli.agent)
show_run_summary(screen, args_cli, env_cfg, library="rsl_rl", action="train")
pre_launch_video_config(env_cfg, args_cli=args_cli)
show_run_summary(screen, args_cli, env_cfg, library="rsl_rl", action="train")
screen.stage("Launching simulation")
with launch_simulation(env_cfg, args_cli):
agent_cfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ def run(argv: list[str]) -> None:
args_cli = _parse_args(argv)
with startup_screen(args_cli, num_stages=3) as screen:
env_cfg, agent_cfg = resolve_task_config(args_cli.task, args_cli.agent)
show_run_summary(screen, args_cli, env_cfg, library="sb3", action="train")
pre_launch_video_config(env_cfg, args_cli=args_cli)
show_run_summary(screen, args_cli, env_cfg, library="sb3", action="train")
screen.stage("Launching simulation")
with launch_simulation(env_cfg, args_cli):
if args_cli.seed == -1:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ def _run(args_cli: argparse.Namespace) -> None:
agent_cfg_entry_point, algorithm = _resolve_agent_entry_point(args_cli)
with startup_screen(args_cli, num_stages=3) as screen:
env_cfg, agent_cfg = resolve_task_config(args_cli.task, agent_cfg_entry_point)
show_run_summary(screen, args_cli, env_cfg, library="skrl", action="train")
pre_launch_video_config(env_cfg, args_cli=args_cli)
show_run_summary(screen, args_cli, env_cfg, library="skrl", action="train")
screen.stage("Launching simulation")
with launch_simulation(env_cfg, args_cli):
if args_cli.ml_framework.startswith("torch"):
Expand Down
63 changes: 49 additions & 14 deletions source/isaaclab_rl/isaaclab_rl/entrypoints/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import torch
from PIL import Image

from isaaclab.app import AppLauncher, LoadingScreen
from isaaclab.app import AppLauncher, LoadingScreen, scan
from isaaclab.envs import DirectMARLEnvCfg, ManagerBasedRLEnvCfg
from isaaclab.renderers.renderer_cfg import RendererCfg
from isaaclab.utils.dict import print_dict
Expand Down Expand Up @@ -545,9 +545,15 @@ def show_run_summary(
) -> None:
"""Print a summary of the backends and scale a run is about to use.

Values the run did not pick itself are shown as ``default (<resolved>)``, so
it is clear which backends came from the command line and which are the
task's own defaults.
Every row names the backend that will run, alongside the choice the run stopped at.
A backend reached through a family the command line named -- ``physics=physx`` and
``renderer=rtx`` name a family that launch resolves -- is shown as
``<family> (<resolved>)``, and a backend the run named neither directly nor by
family is shown as ``default (<resolved>)``.

Resolving those selectors mutates *env_cfg* in place, exactly as the following
:func:`~isaaclab.app.launch_simulation` call would; call this after every other
pre-launch config change, in particular :func:`pre_launch_video_config`.

Args:
screen: Loading screen that owns the console.
Expand All @@ -559,27 +565,54 @@ def show_run_summary(
selected = _selected_preset_names()
device = getattr(args_cli, "device", None) or env_cfg.sim.device
num_envs = getattr(args_cli, "num_envs", None) or env_cfg.scene.num_envs

# Names read before the scan resolves the automatic selectors, so a row can report
# the family the run asked for next to the backend that family resolved to
requested_physics = _physics_name(env_cfg.sim.physics)
requested_renderer = _renderer_name(env_cfg)
scan(env_cfg, args_cli)
physics = _physics_name(env_cfg.sim.physics)
renderer = _renderer_name(env_cfg)

screen.summary(
f"Isaac Lab · {action}",
{
"Task": args_cli.task,
"Workflow": _workflow_name(env_cfg),
"RL library": library,
"Physics": _label(physics, selected=selected),
"Renderer": "n/a (no camera sensors)" if renderer is None else _label(renderer, selected=selected),
"Presets": _additional_preset_names({physics, renderer}),
"Physics": _label(requested_physics, physics, selected=selected),
"Renderer": (
"n/a (no camera sensors)"
if renderer is None
else _label(requested_renderer or renderer, renderer, selected=selected)
),
"Presets": _additional_preset_names({requested_physics, physics, requested_renderer, renderer}),
"Visualizer": _visualizer_name(args_cli, env_cfg),
"Device": str(device),
"Environments": str(num_envs),
},
)


def _label(name: str, *, selected: set[str] = frozenset()) -> str:
"""Mark *name* as a default unless the run asked for it by name."""
return name if name in selected else f"default ({name})"
def _label(requested: str, resolved: str, *, selected: set[str] = frozenset()) -> str:
"""Name the backend a row reports, and where the run stopped choosing it.

Args:
requested: Preset name the config carried before launch resolved its automatic
selectors, which names a backend family when it differs from *resolved*.
resolved: Preset name of the backend that will run.
selected: Preset names the command line asked for.

Returns:
The resolved name when the run asked for that backend, ``<family> (<resolved>)``
when it asked for the family the backend was picked from, and
``default (<resolved>)`` when it asked for neither.
"""
if resolved in selected:
return resolved

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

if the name was resolved by auto selection output the name that was resolved

if requested in selected:
return f"{requested} ({resolved})"
return f"default ({resolved})"


def _selected_preset_names() -> set[str]:
Expand All @@ -599,14 +632,16 @@ def _selected_preset_names() -> set[str]:


def _additional_preset_names(shown: Container[str | None]) -> str:
"""Return the presets the command line asked for beyond those with a row of their own.
"""Return the presets the command line asked for that no other row names.

Domain presets such as ``presets=cube`` do not surface anywhere else in the
summary, so they are listed here; the physics and renderer presets are left
out because their own rows already name them.
summary, so they are listed here. A preset a row already reports is left out,
whether it names the backend that will run or the family the row resolved it
from -- ``renderer=rtx`` is reported by an ``rtx (ovrtx)`` renderer row.

Args:
shown: Preset names already reported by the physics and renderer rows.
shown: Preset names reported by the physics and renderer rows, including
the families those rows resolved from.

Returns:
The remaining preset names in command-line order, comma separated, or
Expand Down
52 changes: 52 additions & 0 deletions source/isaaclab_rl/test/test_entrypoints_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,55 @@ def test_additional_preset_names_lists_presets_without_a_row_of_their_own(
"""The presets row names the chosen presets that physics and renderer do not already report."""
monkeypatch.setattr(_rl_common.sys, "argv", ["train.py"] + argv)
assert _rl_common._additional_preset_names({"newton_mjwarp", "rtx"}) == expected


class _RecordingScreen:
"""Loading screen stand-in that keeps the summary fields instead of drawing them."""

def __init__(self) -> None:
self.fields: dict[str, str] = {}

def summary(self, title: str, fields: dict[str, str]) -> None:
self.fields = fields


@pytest.mark.parametrize(
"selectors, expected_physics, expected_renderer, expected_presets",

@mataylor-nvidia mataylor-nvidia Aug 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

marking which selectors were used should be more descriptive for this test

[
(["physics=ovphysx", "renderer=rtx"], "ovphysx", "rtx (ovrtx)", "none"),
(["physics=isaacsim_physx", "renderer=rtx"], "isaacsim_physx", "rtx (isaacsim_rtx)", "none"),
# ``physx`` reaches the physics backend the same way ``rtx`` reaches the renderer
(["physics=physx", "renderer=rtx"], "physx (ovphysx)", "rtx (ovrtx)", "none"),
# a run that names no backend reports the ones the task pinned as defaults
([], "default (isaacsim_physx)", "default (isaacsim_rtx)", "none"),
# a domain preset has no row of its own
(["physics=physx", "presets=depth"], "physx (isaacsim_physx)", "default (isaacsim_rtx)", "depth"),
],
)
def test_run_summary_reports_the_backends_the_run_resolves_to(
selectors: list[str],
expected_physics: str,
expected_renderer: str,
expected_presets: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``physics=physx`` and ``renderer=rtx`` name a family; the summary names what will run.

A row reached through such a selector names the resolved backend on its own, since
the run never asked for a different one; the selector is listed under presets. Only
a backend the task pinned and the run never named is marked a default.
"""
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils import resolve_task_config

task = "Isaac-Cartpole-Camera-Direct"
monkeypatch.setattr(_rl_common.sys, "argv", ["train.py", *selectors])
env_cfg, _ = resolve_task_config(task, "rsl_rl_cfg_entry_point")
screen = _RecordingScreen()
args_cli = argparse.Namespace(task=task, device=None, num_envs=None, visualizer=None)

_rl_common.show_run_summary(screen, args_cli, env_cfg, library="rsl_rl", action="train")

assert screen.fields["Physics"] == expected_physics
assert screen.fields["Renderer"] == expected_renderer
assert screen.fields["Presets"] == expected_presets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Internal: added a test covering that scanning a config twice reaches the same launch decision, which
the run summary relies on now that it resolves the launch placeholders before ``launch_simulation``
scans them again. No source change in this package.
24 changes: 24 additions & 0 deletions source/isaaclab_tasks/test/core/test_runtime_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,30 @@ def test_rtx_with_ovphysx_is_valid_and_resolves_to_ovrtx():
assert config_scan.needs_kit is False


@pytest.mark.parametrize(
"presets, expected_renderer",
[("physx,rtx", OVRTXRendererCfg), ("isaacsim_physx,rtx", IsaacRtxRendererCfg)],
)
def test_scanning_twice_reaches_the_same_launch_decision(presets, expected_renderer):
"""Resolving a placeholder consumes it, so a second scan sees the same signals.

Callers scan ahead of :func:`launch_simulation` to report the backends a run
will use; the launch then scans the same config again.
"""
env_cfg = _resolve_with_presets(presets)

first = scan(env_cfg, argparse.Namespace())
second = scan(env_cfg, argparse.Namespace())

assert isinstance(env_cfg.tiled_camera.renderer_cfg, expected_renderer)
assert (second.needs_kit, second.has_ovrtx, second.has_kit_camera) == (
first.needs_kit,
first.has_ovrtx,
first.has_kit_camera,
)
assert type(second.resolved_physics_cfg) is type(first.resolved_physics_cfg)


def test_rtx_with_kit_visualizer_is_valid_and_resolves_to_isaac_rtx():
"""The RTX preset chooses Isaac RTX when the Kit visualizer is requested."""
env_cfg = _resolve_with_presets("newton_mjwarp,rtx")
Expand Down
Loading