[PresetCLI] Refactor preset into reusable class#5535
[PresetCLI] Refactor preset into reusable class#5535hujc7 wants to merge 16 commits intoisaac-sim:developfrom
Conversation
Preset selection currently happens through bare ``presets=foo`` Hydra overrides, which are not discoverable from ``--help`` and provide a single error message regardless of whether the user typed a physics, renderer, or domain preset name. This change introduces ``isaaclab_tasks.utils.PresetCli``, a reusable argparse helper that exposes preset selection as three typed flags: --physics NAME (PhysicsCfg subclasses) --renderer NAME (RendererCfg subclasses) --presets NAME[,NAME...] (everything else) Both ``--flag value`` and ``--flag=value`` forms are accepted. The kind of each preset is inferred from the type of its alternatives, so envs do not need any per-task wiring beyond the existing PresetCfg declarations. Internally the flags are translated into the same ``presets=<csv>`` global broadcast that ``register_task`` already consumes, so behavior matches the legacy form. When the user passes ``--task=<task> --help`` the argparse output is augmented with a per-task listing of valid preset names grouped by flag, and unknown names raise a kind-specific error that lists only the relevant alternatives (a wrong-kind name suggests the right flag to use). For the non-Hydra path, ``parse_env_cfg`` now accepts a ``presets`` keyword that is forwarded to ``resolve_presets``. Wired into ``scripts/reinforcement_learning/rsl_rl/train.py`` and ``scripts/environments/zero_agent.py`` as reference integrations.
Replace the type-only kind classification with a decorator-based binding
that pins each canonical preset name to the cfg class it represents.
@preset_name("physx")
@configclass
class PhysxCfg(PhysicsCfg):
...
The CLI layer (PresetCli) reads the registry populated by the decorators
to:
* validate that user input matches the canonical vocabulary across all
IsaacLab envs (no per-env drift on names like physx, newton_mjwarp,
newton_kamino, ovphysx, isaacsim_rtx_renderer, newton_renderer,
ovrtx_renderer);
* surface that vocabulary in argparse --help, with per-task subsets
appearing when --task=<task> --help is used;
* run a CI lint (validate_preset_cfg) over every registered task config
asserting that any PresetCfg containing canonical-typed alternatives
has the canonical name as one of its field names. Variants beyond the
canonical entry (e.g. physx_high_fidelity = PhysxCfg(...)) remain
free-form.
Wire-up details:
* The decorator lives in isaaclab.utils.preset_meta -- dependency-light,
no Isaac Sim imports.
* preset_cli eagerly imports the seven decorated backend cfg modules at
startup so the registry is fully populated before --help reads it
(gym registry alone is lazy and would not trigger them).
* setup_cli(parser) collapses the standard preset+launcher argparse
wiring into a single line; opting out into commit=False mode returns
(args, remaining, preset_cli) for scripts with mid-flow argv intercepts
like rsl_rl/train.py's --external_callback.
* parse_env_cfg gains a presets= kwarg so non-Hydra entry points can
apply preset selection from PresetCli.collect_selected().
Listing format: each kind on its own line in --help and error messages,
comma-separated names within a line; bullet lists used only for long
domain vocabularies. Replaces the {a, b, c} group syntax that crowded
unrelated names together.
Drift fix bundled with the lint: g1/go1/spot flat-env PhysicsCfg now
exposes the canonical 'physx = default' alias alongside 'newton_mjwarp',
matching the convention used everywhere else in the repo.
Replace the standalone module-level _REGISTRY dict + decorator + lookup
helpers with a PresetRegistry class that owns the registry as a class
variable and exposes the operations as classmethods. Free-function aliases
(preset_name, canonical_name_for, all_canonical_names, class_for_canonical_name)
remain at module level so decorator sites stay terse:
@preset_name("physx")
@configclass
class PhysxCfg(PhysicsCfg):
...
Both forms hit the same single source of truth. Tab-completing
PresetRegistry. now surfaces every operation in one discoverable home,
addressing the "hard to track separate functions" concern.
PresetCfg, the preset() factory, the generic walkers (_preset_fields,
_walk_cfg, collect_presets), and the entire preset_cli helper are
generic primitives that don't depend on Hydra or gym. They previously
lived in isaaclab_tasks.utils because that's where Hydra integration is,
but the layering is wrong: cfg classes in isaaclab_physx / isaaclab_newton
are decorated with @preset_name (which lives in core) and produce values
of type PresetCfg (which lived in tasks), forcing a cross-layer reference
to read the canonical-name validator.
This change splits the file into:
isaaclab.utils.preset_cfg
PresetCfg, preset(), _preset_fields, _walk_cfg, collect_presets,
_LEGACY_PRESET_ALIASES (the deprecation map shared by PresetCfg's
__getattr__ alias channel and the CLI normalize hook).
isaaclab.utils.preset_cli
PresetCli, setup_cli, validate_preset_cfg, kind classification,
error formatting. _enumerate lazy-imports load_cfg_from_registry
(the only gym-coupled call); setup_cli calls
AppLauncher.add_app_launcher_args directly instead of going through
the isaaclab_tasks wrapper.
isaaclab_tasks.utils.hydra
Slimmed to Hydra + gym integration only: parse_overrides,
apply_overrides, register_task, _run_hydra, hydra_task_config,
resolve_task_config, plus _normalize_preset_name (which needs its
own stacklevel walker keyed to hydra.py's __file__). Re-exports
PresetCfg, preset, collect_presets etc. so existing
`from isaaclab_tasks.utils.hydra import PresetCfg` keeps working.
isaaclab_tasks.utils.preset_cli
Becomes a backward-compat shim that re-exports from core, so
existing `from isaaclab_tasks.utils.preset_cli import PresetCli`
keeps working.
`from isaaclab_tasks.utils import PresetCfg`, `... import PresetCli`,
`... import setup_cli` continue to resolve to the same identical objects
as the new core paths -- env code touches nothing.
Fixes the BUG and CONCERNs raised by the codex review:
* Layer hygiene (BUG): isaaclab.utils.preset_cli no longer reaches into
isaaclab_tasks. Replaced the lazy import inside _enumerate with an
injectable task loader (register_task_loader). isaaclab_tasks/__init__.py
registers the gym-backed loader at import time, so the dependency now
flows the right direction (tasks -> core, never core -> tasks).
* Backward-compat re-export: hydra.py now also re-imports configclass so
existing 'from isaaclab_tasks.utils.hydra import configclass' keeps
working after the preset primitives moved to core.
* Decorator double-binding: PresetRegistry.name now rejects re-decorating
the same class with a different canonical name (checks the class's own
__dict__ so subclasses can still legitimately override an inherited
parent name with their own decoration).
* Test coverage:
- Tightened the cross-env drift lint to surface unexpected task-load
failures via _DRIFT_LINT_EXPECTED_SKIPS allowlist instead of silently
swallowing them.
- Added test_decorator_rejects_same_class_under_different_name.
- Added test_setup_cli_commit_true_rewrites_sys_argv +
test_setup_cli_commit_false_returns_pieces_and_leaves_sys_argv,
stubbing isaaclab.app to avoid Isaac Sim's stdin-reading kit_app init
in pytest's captured-output mode.
- Documented why the "no cross-kind name overlap" lint codex suggested
is not enforced: the repo's existing convention reuses backend labels
(newton_mjwarp, physx) on backend-tagged PresetCfgs across kinds
(events, sensors, articulation), and the global broadcast propagating
to all of them is the intended behavior.
120 tests passing.
Pulls every standalone helper into its associated class so the public
surface is one identifier per concern instead of a fan-out of free
functions:
* PresetRegistry (preset_meta) -- already absorbed all registry/decorator/
lookup helpers in an earlier commit. Drop the module-level lookup aliases
(canonical_name_for, all_canonical_names, class_for_canonical_name); the
decorator alias preset_name stays because @preset_name("...") is the
natural decorator spelling.
* PresetCfg (preset_cfg) -- adds classmethods fields, walk, collect,
factory. Drop module-level _preset_fields, _walk_cfg, collect_presets;
callers reach them via PresetCfg.fields/.walk/.collect. The DSL alias
preset = PresetCfg.factory stays because preset(default=..., foo=...)
reads naturally as a one-liner inside env code. Helpers must be
classmethods (not staticmethods) because @configclass strips static-method
descriptors during dataclass conversion -- noted in a comment so future
contributors don't trip on it.
* PresetCli (preset_cli) -- absorbs register_task_loader,
validate_preset_cfg, _kind_of_class, _kind_of_value, _vocab_for_kind,
_format_inline_or_bullet, _ensure_backends_imported,
_extract_task_from_argv, _enumerate, _format_unknown_canonical,
_format_not_in_task, _validate_one. Module-level setup_cli =
PresetCli.setup alias kept because the canonical script wiring is
setup_cli(parser).
isaaclab_tasks/__init__.py uses PresetCli.register_task_loader. The
isaaclab_tasks/utils/preset_cli.py shim still re-exports the public
API surface (PresetCli, setup_cli, validate_preset_cfg, kind constants).
120 tests passing. Pre-commit clean.
Rebuilds the canonical-name registry around two ideas:
1. PresetKind is an Enum. Each member carries (label, legacy_aliases):
the lowercase label drives the --{label} CLI flag and error text;
the per-kind legacy_aliases dict deprecates names that mapped to
canonical replacements of this kind. Adding a new CLI flag = adding
one enum member -- the CLI layer auto-discovers it via
PresetRegistry.all_kinds() and PresetRegistry.names_for_kind().
2. The decorator becomes @register(kind, name). Both bindings live at
the same site, no inheritance-based or isinstance-based kind
inference. The connection from "physx" to PresetKind.PHYSICS is now
explicit at the decorator call, not deduced via class hierarchy.
PresetCli's argparse setup now iterates PresetCli._ordered_kinds()
(physics, renderer, then any other registered kinds, domain last) and
builds one parser flag per kind. Adding a new PresetKind member adds
its --{kind} flag automatically; PresetCli has no kind-specific code
paths or hardcoded constants.
The legacy alias map (newton -> newton_mjwarp, kamino -> newton_kamino)
moved from a private module-level dict in preset_cfg.py to
PresetKind.PHYSICS.legacy_aliases. Both deprecation channels --
hydra._normalize_preset_name (CLI side) and PresetCfg.__getattr__
(attribute side) -- now go through PresetKind.find_legacy() instead of
imported dict references.
7 backend cfg files updated: @register(PresetKind.PHYSICS, "physx") /
@register(PresetKind.RENDERER, "newton_renderer") / etc. The
isaaclab.utils package re-exports PresetKind and `register`; the old
preset_name decorator is gone (only 7 internal call sites used it,
all updated in this commit).
120 tests passing. Pre-commit clean.
There was a problem hiding this comment.
Review Summary
Well-architected refactor that elevates preset selection from a Hydra implementation detail to a first-class CLI concept. The three-layer design (registry → cfg → CLI) provides good separation of concerns.
Strengths
preset_namedecorator keeps canonical names co-located with their cfg classes — prevents vocabulary drift- Kind taxonomy via class inheritance rather than name-matching is robust and extensible
- Backwards compatibility preserved: legacy
presets=...override andfrom isaaclab_tasks.utils.hydra import PresetCfgstill work - Comprehensive test coverage (616 lines) with edge cases for legacy aliases, unknown presets, and multi-kind validation
- Clean API surface:
setup_cli(parser)one-liner for script authors
Concerns
See inline comments. High-level:
- The
_BACKEND_MODULESforce-import list is a maintenance liability — adding a new backend requires editing this list PresetCli._task_loaderas a mutable class variable is a testing footgun (global state shared across test cases)- The
_user_stacklevel()function is duplicated across modules
| "isaaclab_newton.physics.kamino_manager_cfg", | ||
| "isaaclab_physx.renderers.isaac_rtx_renderer_cfg", | ||
| "isaaclab_newton.renderers.newton_warp_renderer_cfg", | ||
| "isaaclab_ov.renderers.ovrtx_renderer_cfg", |
There was a problem hiding this comment.
🟡 Hardcoded backend module list: _BACKEND_MODULES requires manual updates when new backends are added. Consider using entry points or a plugin registry pattern so backends self-register:
# In each backend's __init__.py or cfg module:
from isaaclab.utils.preset_meta import PresetRegistry
PresetRegistry.register(PresetKind.PHYSICS, "physx")(PhysxCfg)Then _ensure_backends_imported could discover registered entry points instead of maintaining a hardcoded list. This is a longer-term suggestion — the current approach is fine for now given the small number of backends.
| """ | ||
| cls._task_loader = loader | ||
|
|
||
| # -- public API: per-script wiring ------------------------------------ |
There was a problem hiding this comment.
🟡 Mutable class variable for task loader: _task_loader: ClassVar[Any] = None is mutable global state that persists across test cases. If tests register different loaders or forget to reset, they can leak state.
Consider using a contextmanager or explicit reset for testing:
@classmethod
@contextlib.contextmanager
def override_task_loader(cls, loader):
prev = cls._task_loader
cls._task_loader = loader
try:
yield
finally:
cls._task_loader = prev| the helper that emitted the warning. | ||
|
|
||
| Kept module-private because it inspects ``__file__`` of *this* module; | ||
| the call chain on the CLI side has its own copy in ``hydra.py``. |
There was a problem hiding this comment.
💡 _user_stacklevel duplication: This helper appears in both preset_cfg.py and hydra.py. Consider extracting it to a shared utility (e.g., isaaclab.utils._warnings):
# isaaclab/utils/_warnings.py
def stacklevel_outside(module_file: str, max_walk: int = 16) -> int: ...Minor, but reduces the chance of the two implementations drifting.
| obj.legacy_aliases = dict(legacy_aliases) if legacy_aliases else {} | ||
| return obj | ||
|
|
||
| PHYSICS = ("physics", {"newton": "newton_mjwarp", "kamino": "newton_kamino"}) |
There was a problem hiding this comment.
💡 PresetKind enum with dual-value constructor: The __new__ override with (value, legacy_aliases) tuple is clever but non-obvious. A first-time reader seeing PHYSICS = ("physics", {"newton": "newton_mjwarp", ...}) might not immediately understand the structure.
Consider adding a brief code comment above the first member:
# Members: (label_for_CLI_flag, {deprecated_alias: canonical_replacement})
PHYSICS = ("physics", {"newton": "newton_mjwarp", "kamino": "newton_kamino"})| from isaaclab.envs.utils.spaces import replace_env_cfg_spaces_with_strings, replace_strings_with_env_cfg_spaces | ||
|
|
||
| # ``configclass`` is imported here (and re-exported via __all__) so existing code that does | ||
| # ``from isaaclab_tasks.utils.hydra import configclass`` keeps working after the preset |
There was a problem hiding this comment.
💡 Good backwards compat: Re-exporting PresetCfg, preset, and collect_presets from their new home while keeping the old import path working is the right call. Consider adding a deprecation note in the module docstring or via __deprecated__ so IDEs can flag the old imports:
# Deprecated re-exports — use isaaclab.utils.preset_cfg directly in new code.…stants * preset_cli.py top docstring referenced preset_name (gone) and described kind classification as "derived from class inheritance" (no longer true after the @register(kind, name) refactor). Rewrote to describe the enum-driven kind taxonomy and PresetRegistry-backed vocabulary. * preset_cli.py module-level KIND_PHYSICS/KIND_RENDERER/KIND_DOMAIN string constants were unused. Dropped them and the corresponding re-exports in the isaaclab_tasks shim. * PresetRegistry: factored the MRO walk into a single ``_attr_via_mro`` static helper. ``name_of``, ``kind_of``, and ``kind_of_class`` all share it -- one place to change the lookup contract. 120 tests passing. Pre-commit clean.
* BUG: register() accepted re-decorating the same class with the same
name but a *different* PresetKind, silently mutating _preset_kind. Now
rejected with a "cannot re-register" error. Regression test added.
* CONCERN: PresetRegistry.all_kinds() now returns the full PresetKind
enum, not just kinds with at least one registered class. Adding a new
PresetKind member is sufficient to surface its --{label} flag in
--help (with an empty vocabulary until a class is decorated). Matches
the documented contract.
* NIT: PresetCli._vocab_for_kind() used to scan all_names() and re-classify
each entry by walking MRO. Replaced with a direct call to
PresetRegistry.names_for_kind(kind), which already partitions by kind.
* Docs/comments cleanup:
- changelog fragment: switched the example from @preset_name("physx") to
@register(PresetKind.PHYSICS, "physx") and added the "new kind = one
enum member" line.
- docs/source/features/hydra.rst: stopped describing kind as inferred
from class inheritance; describes the @register decorator instead.
- PresetCfg docstring + __getattr__ comment: reference
isaaclab.utils.preset_meta.register and PresetKind.legacy_aliases.
- PresetCli.apply() docstring: reference @register, not @preset_name.
- test_preset_cli.py module docstring + drift-lint failure message:
same.
- PresetKind class: inline comment above PHYSICS explains that members
are (label, legacy_aliases) tuples.
121 tests passing (added test_decorator_rejects_same_class_same_name_different_kind).
Pre-commit clean.
Deferred (lower-priority bot/codex notes):
* _BACKEND_MODULES is a hardcoded fallback for scripts that don't import
isaaclab_tasks. Self-discovery via per-package __init__ imports is a
follow-up.
* PresetCli._task_loader as a class variable is mutable global state; a
test contextmanager (override_task_loader) could replace direct setattr.
* _user_stacklevel exists in both preset_cfg.py and hydra.py (each
inspects its own module's __file__). Sharing a stacklevel-outside helper
is a minor cleanup.
PresetCli now lives in source/isaaclab_tasks/isaaclab_tasks/utils/preset_cli.py. The CLI surface is task-aware end-to-end (gym task lookup, backend cfg force-import, AppLauncher wiring), so hosting it in core required a mutable class-var task-loader injection plus a hardcoded backend module list -- both flagged in review. Tasks is its native habitat; the loader injection goes away (parse_cfg is now a direct local import) and the backend module list is in-layer rather than a layer violation. Generic value-type primitives (PresetCfg, PresetKind, PresetRegistry) remain in core so backend cfg classes can decorate themselves without pulling in tasks. Restored bare-string KIND_PHYSICS / KIND_RENDERER / KIND_DOMAIN aliases on the new module so legacy `from isaaclab_tasks.utils.preset_cli import KIND_PHYSICS` callers keep getting the string they did pre-refactor. Other fixes: - add_args is now idempotent on the same parser (second call no-ops); guards the notebook / multi-step setup_cli path. - Typed flag values (--physics newton) now route through PresetKind.normalize_name before validation, so legacy aliases yield the same FutureWarning + canonical replacement that the override path already produced. Tests: 125 passed (added shim-import, idempotency, and typed-flag legacy-alias regressions).
Three small cleanups, no behavior change: - Drop unused private re-export aliases from hydra.py: _setattr, _parse_val, _known_preset_names, _format_unknown_presets_error, _normalize_preset_name, _pick_alternative, _preset_fields, _walk_cfg. None had external callers; they were leftover scaffolding from the PresetCli core/tasks split. register_task now calls PresetCli/PresetKind directly. The two test sites that did import these privates now go through PresetCli/PresetCfg. - Delete dead _LITERAL_MAP from hydra.py (the live copy lives on PresetCli where _parse_val moved). - Nest _PresetAwareHelpAction inside PresetCli as PresetCli._HelpAction. Per AGENTS.md "prefer nested classes when self-contained" -- the class only matters as the replacement installed by _install_help_extension and has no public identity. hydra.py drops from ~252 lines to ~199; net diff -36 lines across the preset surface. Tests: 125 passed.
Address adversarial review on preset_cli, four changes in one pass: 1. BUG fix: parser-level idempotency. The previous guard (``self._added_args_for is parser``) was instance-local; ``setup_cli`` builds a fresh PresetCli per call, so calling it twice on the same parser used to add ``--physics``/``--renderer`` twice and crash. Stamp the parser object (``_isaaclab_preset_args_added`` and ``_isaaclab_preset_help_installed``) so any instance respects what was already wired. Tests cover same-instance and cross-instance. 2. PresetOverrides dataclass replaces the 4-tuple handshake between ``parse_overrides``, ``_run_hydra``, and ``apply_overrides``. The public signature is now ``apply_overrides(env_cfg, agent_cfg, hydra_cfg, overrides, presets)`` instead of six positional list arguments. Test sites and ``env_test_utils`` / ``rendering_test_utils`` updated to construct PresetOverrides explicitly. 3. ``_requests_from_args`` helper consolidates the duplicated "walk every kind, pluck off the namespace, normalize aliases" loop that lived in both ``collect_selected`` and ``apply``. Both now call the shared method and reduce to a one-liner each. 4. Centralize legacy alias normalization. Aliases were normalized in four places (typed flag apply path, ``parse_overrides`` global, ``parse_overrides`` path-selection, ``apply_overrides`` re-pass), so a single ``presets=newton`` token emitted two FutureWarning events. ``parse_overrides`` is now the single normalization point; ``apply_overrides`` trusts its input. Added a regression test that asserts exactly one deprecation warning per legacy token. Tests: 125 passed.
The PR was carrying re-export shims that pretended to be back-compat with develop, but none of these symbols ever shipped: KIND_PHYSICS string aliases, validate_preset_cfg module alias, hydra.py re-exports of PresetCfg / preset / configclass / collect_presets / resolve_presets / parse_overrides / apply_overrides, and the matching shim test that codex flagged as a BUG. There is nothing to be backward-compatible with. What stays: - isaaclab_tasks.utils still re-exports PresetCfg + preset for env-cfg authors (real public API: 30+ env configs import via this path). - setup_cli, hydra_task_config, resolve_task_config: actual public API. - PresetCli, PresetOverrides: the CLI surface. What goes: - KIND_PHYSICS / KIND_RENDERER / KIND_DOMAIN module-level string aliases. Use PresetKind.PHYSICS.value if you really want the string. - validate_preset_cfg module-level alias. Use PresetCli.validate_preset_cfg. - hydra.py re-exports of PresetCfg / preset / collect_presets / resolve_presets / parse_overrides / apply_overrides. Reach those through their canonical class methods (PresetCfg.collect, PresetCli.parse_overrides). - parse_cfg.py "resolve_presets = PresetCfg.resolve" alias. - test files' module-level aliases of the same. - The two shim-import tests added in the previous commit -- they were guarding nothing real. Backend force-load also collapsed: the _BACKEND_MODULES tuple and the PresetCli._ensure_backends_imported helper are gone. The seven contextlib.suppress(ImportError) imports run unconditionally at the top of preset_cli.py. Every script that uses setup_cli reaches add_args anyway, so lazy import bought nothing. Tests: 123 passed.
The two-file split was a historical artifact of staged refactors -- preset_cfg.PresetCfg.__getattr__ already calls preset_meta.PresetKind.normalize_name, and preset_meta's docstrings already reference PresetCfg. With the public surface stable they should sit next to each other so a reader looking for "the preset stuff" lands in one place. Module is named ``presets.py`` (plural) to avoid colliding with the ``preset()`` factory function the package re-exports -- ``preset.py`` would shadow the function name in ``isaaclab.utils.preset`` lookups. Files removed: source/isaaclab/isaaclab/utils/preset_meta.py source/isaaclab/isaaclab/utils/preset_cfg.py File added: source/isaaclab/isaaclab/utils/presets.py (570 lines, both halves) All call sites and Sphinx cross-refs updated to point at the new module. Tests: 123 passed.
The @register decorator on each backend cfg class is the only place canonical names are declared. Anything else listing those names was a duplicate. Removed: - source/isaaclab_tasks/.../preset_cli.py: the seven-entry force-import loop at module-top, and the inline canonical-vocabulary in --physics / --renderer help text. How the registry stays populated without the loop: - ``apply()`` calls ``_enumerate(task_name)`` which loads the task's env config; the env config imports the backends it uses; @register fires. - ``--task=X --help`` follows the same path via ``describe_task``. Tradeoff: bare ``--help`` (no --task) used to inline the global canonical vocabulary, which required the registry pre-populated. It now prints "use --task=<task> --help to list valid names". Honest: without an env config there is no source for that list other than a duplicate one, which is what we removed. Tests stub ``_enumerate`` to bypass real env loading, so the test file now imports the backend cfg modules explicitly at the top so its assertions still see the canonical names. Production code does not need the explicit imports. Tests: 123 passed.
Goal
Make preset selection a first-class, discoverable, and drift-resistant CLI surface — replacing the bare
presets=...Hydra-style override with typed flags that work uniformly across every IsaacLab script.Key design choices
Canonical names live next to the cfg class via a decorator. Each backend config class declares its preset name once, in the same file as the class. The CLI layer reads the registry; preset_cli has no hardcoded vocabulary list.
Closed vocabulary for
--physicsand--renderer; free-form for--presets. Physics and renderer names are project-wide concepts (small set, shared meaning across envs). Domain presets are intentionally env-specific. Drift on the canonical names is enforced by a CI lint; domain names are author-discretion.Single validation surface. Both "name not in canonical vocabulary" and "name not defined for this task" errors come from one code path with consistent formatting. argparse stays open (no
choices=) so the friendly error never collides with the terse argparse default.Loose canonical-name rule, not strict. Inside a
PresetCfg, every canonical type that appears must have at least one field named after its canonical name. Variants beyond that (e.g.physx_high_fidelity = PhysxCfg(...)) stay free-form. So envs can expose multiple PhysX tunings as long as the canonicalphysxentry is also present.One-line script wiring.
setup_cli(parser)collapses argparse + launcher + parse + apply + sys.argv mutation into a single line. Scripts with mid-flow argv intercepts (e.g.rsl_rl/train.py's--external_callback) opt intocommit=Falseand finish withpreset_cli.commit(args, remaining)— never touchingsys.argvdirectly.Kind detection is structural, not name-based. Whether a preset is "physics", "renderer", or "domain" comes from class inheritance (
PhysicsCfg/RendererCfg/ else). A class can be canonical-named without being one of those kinds (e.g. solver-dispatch types likeMJWarpSolverCfg), and the CLI handles the dispatch viavalue.solver_cfg.Before / after
CLI surface
python train.py --task=... presets=newton_mjwarp,newton_renderer(Hydra-style, no--, undiscoverable in--help)python train.py --task=... --physics newton_mjwarp --renderer newton_renderer(typed flags shown in--help). Legacypresets=...form still works.--flag valueand--flag=valueboth work natively.--presetsaccepts CSV.--helplisting--help. With--task=<task> --helpthe output also lists per-task valid names, grouped by flag, one kind per line.Error messages
Before — generic, mixed-kind:
After — kind-specific, single-place, with consistent formatting:
A wrong-kind name (e.g.
--physics albedo) gets a flag suggestion: "'albedo' is defined as a domain preset on this task; did you mean '--presets albedo'?"Env author experience
Before — every PresetCfg name was free-form Python; nothing prevented one env from naming the Newton MJWarp variant
mjwarpwhile another called itnewton_mjwarp. Drift on canonical names was undetectable.After — same
PresetCfgsyntax, but a CI lint walks the gym registry and asserts every PresetCfg containing a canonical-typed alternative has the canonical name as one of its fields. A future PR addingsuper_solver_v2 = NewtonCfg(MJWarpSolverCfg(...))to some env config fails CI with:This PR also lands the small drift fix for G1/Go1/Spot velocity-flat envs that the lint surfaced — adds a
physx = defaultalias so--physics physxworks on those tasks too.Backend author experience
Adding a new physics solver "FastNewton":
class FastNewtonSolverCfg(NewtonSolverCfg): ...@preset_name("newton_fastsolver")above the classclass MyPhysicsCfg(PresetCfg): newton_fastsolver = NewtonCfg(...)--helpautomaticallyScript wiring
Before (every script copies 5+ lines of boilerplate):
After (one line):
For scripts with mid-flow argv intercepts (e.g.
rsl_rl/train.py):What's not changed
env.sim.dt=0.001,agent.seed=42, etc.) are untouched.presets=newton_mjwarp,...global broadcast still works exactly as before — typed flags translate into it internally, so nothing the existing test suite or workflow assumes is broken.Files
source/isaaclab/isaaclab/utils/preset_meta.pysource/isaaclab_tasks/isaaclab_tasks/utils/preset_cli.pyphysx_manager_cfg.py,ovphysx_manager_cfg.py,mjwarp_manager_cfg.py,kamino_manager_cfg.py,isaac_rtx_renderer_cfg.py,newton_warp_renderer_cfg.py,ovrtx_renderer_cfg.pyg1/flat_env_cfg.py,go1/flat_env_cfg.py,spot/flat_env_cfg.pyscripts/reinforcement_learning/rsl_rl/train.py,scripts/environments/zero_agent.pyparse_cfg.py(addedpresets=kwarg)source/isaaclab_tasks/test/test_preset_cli.py(41 tests, including registry-walk drift lint)docs/source/features/hydra.rstsource/isaaclab/changelog.d/,source/isaaclab_tasks/changelog.d/Test plan
./isaaclab.sh -p -m pytest source/isaaclab_tasks/test/test_preset_cli.py— 41/41 passed locally./isaaclab.sh -f) passesdescribe_task('Isaac-Cartpole-Camera-Presets-Direct-v0')smoke produces the expected per-task listing--physics newton_mjwarpagainst an env without a physics preset emits the targeted errorpython scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-Camera-Presets-Direct-v0 --physics newton_mjwarp --renderer newton_renderer --headlessruns to completion (full Kit launch)python scripts/environments/zero_agent.py --task=Isaac-Cartpole-Camera-Presets-Direct-v0 --physics=physx --headlessworks (the=form)