Skip to content

Add WarpFrontend bridge: run any stable manager-based task on the warp runtime via --manager=warp#5504

Draft
hujc7 wants to merge 9 commits intoisaac-sim:developfrom
hujc7:jichuanh/warp-manager-bridge
Draft

Add WarpFrontend bridge: run any stable manager-based task on the warp runtime via --manager=warp#5504
hujc7 wants to merge 9 commits intoisaac-sim:developfrom
hujc7:jichuanh/warp-manager-bridge

Conversation

@hujc7
Copy link
Copy Markdown
Collaborator

@hujc7 hujc7 commented May 5, 2026

Summary

Adds a small adapter (WarpFrontend) that lets a stable manager-based RL
task config run on the experimental warp runtime
(ManagerBasedRLEnvWarp) without a parallel -Warp-v0 registration. For
direct envs, where the env class itself encodes the runtime, the same
frontend dispatches by verifying the registered entry-point lives under
isaaclab_experimental / isaaclab_tasks_experimental and forwarding to
gym.make unchanged. Selection is a single CLI flag: --frontend=warp
on rsl_rl/train.py. The warp implementations stay where they are; the
bridge just wires the existing pieces together at runtime.

The motivation is to stop forking task registrations — every warp env
that exists today is a near-duplicate of a stable env with
presets=newton and a swap of mdp callables. With this bridge, a stable
cfg + --frontend=warp is sufficient to drive the warp runtime, and the
duplicate *-Warp-v0 registries can be deprecated in a follow-up.

Pluggable compatibility framework

Adaptation runs as a sequence of CompatRule objects. Each rule
declares which workflow it applies to (manager-based / direct / both),
mutates the cfg in place, and appends to a shared CompatReport. New
incompatibilities are added by writing a small subclass instead of
editing the dispatcher:

class MyCompatRule(CompatRule):
    name = "drop_my_thing"
    applies_to = frozenset({CfgKind.MANAGER_BASED})
    def apply(self, cfg, ctx, report): ...

Default rules cover physics-preset resolution (PresetCfg → newton),
dropping unsupported sensors, in-place SceneEntityCfg promotion to the
warp variant, mdp term.func swaps, and ActionTermCfg.class_type
swaps. Twins are matched by name with a __module__ prefix check, so a
plain from isaaclab.envs.mdp import * re-export in a warp mdp
package is rejected as not-a-twin.

What --frontend=warp does

Cfg kind Behaviour
Stable ManagerBasedRLEnvCfg Auto-injects presets=newton; runs the rule pipeline; constructs ManagerBasedRLEnvWarp.
Stable DirectRLEnvCfg IncompatibleEnvError with the offending entry-point and a hint to use *-Direct-Warp-v0.
Warp-direct DirectRLEnvCfg _verify_direct_warp confirms entry-point lives under the warp prefixes; then plain gym.make. No mutation.
Manager-based warp cfg _classify recognises it; rule pipeline is largely a no-op (preset already resolved); env built normally.

The CompatReport is attached on env.unwrapped.warp_compat_report so
tests and callers can inspect what was changed and what was missing.

Other small fixes

  • manager_based_env_warp.py: aligned the visualizer probe with the
    stable env after the recent SimulationContext.get_setting API change.
    The old warp path tried to .split a dict and crashed on init.
  • isaaclab_experimental.managers.manager_term_cfg: re-exports the
    stable term-cfg classes verbatim instead of defining sibling classes
    that broke isinstance(stable_term, warp.TermCfg). The warp managers
    already accept the stable cfg shape — only func differs at runtime.
  • train.py: render_mode is forwarded so --video keeps working
    under the warp frontend; warns when the user passes presets=<other>
    alongside --frontend=warp.

Validation

All regression runs use 4096 envs, 300 iterations, fixed seed 42, on a
single L40.

Cartpole — manager-based parity

path task extra reward ep_len iter_time
stable_torch Isaac-Cartpole-v0 (default) 4.96 300.00 0.22 s
warp_direct Isaac-Cartpole-Warp-v0 (default) 4.95 300.00 0.13 s
warp_bridge Isaac-Cartpole-v0 --frontend=warp 4.95 300.00 0.13 s

Bridge matches direct registration exactly at 300 iter.

Anymal-D Flat — manager-based parity

path task extra reward ep_len iter_time
stable_newton Isaac-Velocity-Flat-Anymal-D-v0 presets=newton 20.74 990.46 0.58 s
warp_direct Isaac-Velocity-Flat-Anymal-D-Warp-v0 (default) 21.35 982.63 0.45 s
warp_bridge Isaac-Velocity-Flat-Anymal-D-v0 --frontend=warp 21.36 996.47 0.46 s

Bridge tracks warp_direct within run-to-run variance on locomotion
(reward 21.36 vs 21.35, ep_len in the same bucket) and recovers the warp
runtime's per-iter time (0.46 vs 0.45 s, vs 0.58 s for stable Newton).

Cartpole — direct-env dispatch

path task extra reward ep_len iter_time notes
direct_stable Isaac-Cartpole-Direct-v0 (default) 8.81 36.91 0.21 s PhysX direct, separate impl
direct_warp_native Isaac-Cartpole-Direct-Warp-v0 (default) 296.53 299.00 0.14 s warp direct baseline
direct_warp_via_frontend Isaac-Cartpole-Direct-Warp-v0 --frontend=warp 296.53 299.00 0.14 s bridge dispatch ↔ baseline match
stable_via_frontend_xfail Isaac-Cartpole-Direct-v0 --frontend=warp raises IncompatibleEnvError

direct_warp_via_frontend matches direct_warp_native exactly — the
frontend's direct-env path verifies the entry-point and forwards to
gym.make, no cfg mutation, identical numerics.

direct_stable and direct_warp_native are different env classes
(separate kernel implementations with their own reward shaping), so
their absolute numbers aren't expected to match — only the
bridge-vs-native pair is a parity check.

stable_via_frontend_xfail correctly raises IncompatibleEnvError
naming the offending entry-point and pointing at *-Direct-Warp-v0.

Test plan

  • Isaac-Cartpole-v0 --frontend=warp matches Isaac-Cartpole-Warp-v0
    at 300 iter / 4096 envs.
  • Isaac-Velocity-Flat-Anymal-D-v0 --frontend=warp matches
    Isaac-Velocity-Flat-Anymal-D-Warp-v0 within run-to-run variance.
  • Isaac-Cartpole-Direct-Warp-v0 --frontend=warp is a numeric-exact
    pass-through to the native direct-warp path.
  • Isaac-Cartpole-Direct-v0 --frontend=warp raises
    IncompatibleEnvError with the offending entry-point.
  • Stable path (--frontend=stable, default) is unchanged on
    manager-based and direct tasks.

Out of scope (follow-up)

  • Removing the duplicate *-Warp-v0 manager-based registrations once the
    bridge is the recommended way to opt into warp.
  • Extending the adapter to non-RSL trainers (skrl, rl_games) — the
    adapter itself is trainer-agnostic; only the --frontend CLI flag
    lives in the RSL train script today.

hujc7 added 4 commits May 5, 2026 09:37
PR isaac-sim#5297 (Decouple Renderer from Camera) replaced
`sim.get_setting('/isaaclab/visualizer')` (returned a comma-separated
string) with `sim.has_active_visualizers()` plus per-type queries; the
warp env path was missed, so it crashes with
"AttributeError: 'dict' object has no attribute 'split'" during env
construction.

Mirror the stable env's pattern:
- `has_active_visualizers()` for the gating predicate
- `has_kit()` so kitless Newton-only runs (`--viz rerun`) skip
  ViewportCameraController
Warp's ObservationTermCfg / RewardTermCfg / TerminationTermCfg had no
behavioural difference from their stable counterparts — same fields,
same base class. The override only carried Warp-first docstrings while
keeping the classes as siblings of stable's, which broke
`isinstance(stable_term, warp.TermCfg)` checks inside the experimental
managers when a stable cfg is fed through the warp runtime.

Re-export stable's term cfg classes directly. Term funcs still follow
the warp-first `func(env, out, **params)` signature at runtime; the
type annotation just lives on stable's class instead of warp's.
Lets a stable manager-based RL env cfg run on the experimental warp
runtime without a separate warp task registration. Replaces the
duplicate per-robot warp env cfgs by adapting the stable cfg in place.

Components:
- isaaclab_experimental/envs/warp_frontend.py: 'WarpFrontend' walks the
  stable cfg, swaps each `term.func` to its same-named warp twin (only
  accepting candidates whose `__module__` lives under the warp packages
  — the warp mdp module re-exports stable terms via `from … import *`,
  so a naive `getattr` would silently keep the stable function), swaps
  each action `class_type`, picks the `newton` field of any PresetCfg,
  drops sensors with no warp counterpart (`height_scanner`), and
  in-place class-promotes `SceneEntityCfg` instances so warp kernels
  see the `joint_mask` / *\_ids_wp` cached fields. A
  `WarpAdaptReport` records every term that had no warp twin and
  surfaces them via the logger; `strict=True` makes the same condition
  raise `LookupError` instead.
- scripts/reinforcement_learning/rsl_rl/train.py: '--manager=warp' flag
  routes the constructed env through `WarpFrontend.build` instead of
  `gym.make`. The flag also auto-injects `presets=newton` into Hydra's
  argv so that PresetCfg wrappers resolve to the newton preset (Hydra's
  preset resolution runs *before* the adapter).

Validated: cartpole and Anymal-D Flat both pass a 3-way comparison —
- 'Isaac-Cartpole-v0' (stable manager): trains.
- 'Isaac-Cartpole-Warp-v0' (existing direct path): reward 0.06 / ep 76.
- 'Isaac-Cartpole-v0 --manager=warp': reward 0.06 / ep 76 (matches
  direct exactly).
- 'Isaac-Velocity-Flat-Anymal-D-v0 presets=newton': -8.45 / 191.
- 'Isaac-Velocity-Flat-Anymal-D-Warp-v0': -7.47 / 168.
- 'Isaac-Velocity-Flat-Anymal-D-v0 --manager=warp': -7.69 / 174 (within
  run-to-run variance of the direct warp path).

Both stable and warp frontends remain functional for every task; this
PR adds a flag-based selector without removing the existing direct
warp registrations.
Adds the isaaclab_experimental changelog fragment for the WarpFrontend
adapter and the --manager flag, and applies the ruff-format pass that
the pre-commit hook produced on warp_frontend.py.
@hujc7 hujc7 requested a review from ooctipus as a code owner May 5, 2026 16:42
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label May 5, 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.

🤖 Isaac Lab Review Bot

Summary

This PR introduces a WarpFrontend adapter that allows stable manager-based RL task configs to run on the experimental warp runtime without requiring parallel -Warp-v0 task registrations. The implementation mutates configs in-place to swap stable MDP functions with warp twins, upgrades SceneEntityCfg instances, and drops unsupported sensors. The approach is architecturally sound for reducing task duplication, but there are several correctness issues that need attention before shipping.

Architecture Impact

The changes are relatively self-contained within isaaclab_experimental, with the main integration point being rsl_rl/train.py. The adapter operates at config mutation time before env construction, so it doesn't change the warp runtime itself. However:

  • The manager_term_cfg.py change from custom classes to re-exports affects any code doing isinstance checks against the warp term cfg types
  • The --manager=warp flag is only added to rsl_rl/train.py, not other trainers (skrl, rl_games) — documented as out of scope but creates API inconsistency
  • The in-place __class__ mutation pattern in _upgrade_scene_entity_cfgs is fragile if the warp SceneEntityCfg ever diverges structurally from stable

Implementation Verdict

Minor fixes needed — The core design is solid, but there are edge cases and correctness issues that should be addressed.

Test Coverage

Insufficient. The PR adds no automated tests. The validation is entirely manual (described in PR description). Given the complexity of the adapter logic (module resolution, class swapping, term iteration), this needs at minimum:

  • Unit tests for WarpFrontend.adapt() with mock configs
  • Regression test for the visualizer fix in manager_based_env_warp.py
  • Integration test that the --manager=warp flag correctly invokes the adapter

CI Status

No CI checks available yet — cannot assess whether existing tests pass.

Findings

🔴 Critical: warp_frontend.py:278-287 — In-place __class__ mutation is unsafe without verifying inheritance

The code does obj.__class__ = _WarpSE after checking isinstance(obj, _StableSE), but if _WarpSE doesn't actually inherit from _StableSE (which the code assumes but doesn't verify), this will corrupt the object. The comment claims "warp inherits stable" but there's no runtime assertion. If someone refactors the warp SceneEntityCfg to not inherit from stable, this silently corrupts configs.

# Should verify at import time:
if not issubclass(_WarpSE, _StableSE):
    raise TypeError("WarpFrontend requires warp SceneEntityCfg to inherit from stable")

🔴 Critical: warp_frontend.py:95-97 — Iterating over dir(group) while mutating attributes

The apply_to method iterates over dir(group) and calls setattr(group, name, None) to drop terms. While this doesn't crash because the iteration is over a snapshot from dir(), it means subsequent code that relies on attribute presence (like the warp manager's term resolution) will see None values rather than missing attributes. The warp managers need to handle None term entries, or this should use delattr.

🟡 Warning: warp_frontend.py:316-329 — Module resolution swallows all exceptions

try:
    import gymnasium as gym
    entry = gym.spec(task_id).kwargs.get("env_cfg_entry_point")
except Exception:
    entry = None

This catches all exceptions including KeyboardInterrupt and SystemExit. Should be except (ImportError, KeyError, AttributeError): or similar to avoid masking real errors.

🟡 Warning: train.py:100-101 — Preset injection doesn't check for conflicting explicit presets

if args_cli.manager == "warp" and not any(a.startswith("presets=") for a in remaining_args):
    remaining_args.append("presets=newton")

If user passes presets=physx, this silently does nothing (doesn't append newton), but the warp adapter may still try to run and fail confusingly later. Should warn when --manager=warp is used with a non-newton preset.

🟡 Warning: warp_frontend.py:215-217 — Physics preset extraction logic is redundant

The comment says "by the time we run, Hydra's resolve_presets has already collapsed any PresetCfg wrapper" — but then the code checks hasattr(physics, "newton") anyway. If the preset is already resolved, this condition will always be False. This is dead code that adds confusion.

🔵 Improvement: warp_frontend.py:170-177 — DEFAULT_TERM_GROUPS misses commands group

The default term groups don't include ("commands",), which is a manager group in the stable env. If a task has command terms with SceneEntityCfg params, they won't be upgraded. Either add it or document why it's excluded.

🔵 Improvement: manager_term_cfg.py — Re-export change may break type annotations

The change from explicit @configclass definitions to from ... import * means type checkers can no longer see what's exported from this module. Code doing from isaaclab_experimental.managers.manager_term_cfg import RewardTermCfg will work at runtime but may fail type checking. Consider adding explicit __all__ or type stub.

🔵 Improvement: warp_frontend.py:256-261 — build() ignores the adapt report

The build() method calls adapt() but doesn't return or expose the report. If the caller wants to inspect what was dropped/missing, they have to call adapt() separately first. Consider returning a tuple (env, report) or attaching the report to the env.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR introduces WarpFrontend, a runtime adapter that mutates a stable manager-based RL task config in-place so it can drive the experimental ManagerBasedRLEnvWarp backend, selected via a new --manager=warp CLI flag in rsl_rl/train.py. It also fixes a visualizer-probe crash in the warp env and collapses the warp manager term-cfg classes into simple re-exports of the stable counterparts.

  • WarpFrontend.adapt() resolves physics presets, drops unsupported sensors, upgrades SceneEntityCfg instances in-place to the warp subclass, and swaps term.func / ActionTermCfg.class_type to same-named warp twins discovered by __module__ inspection.
  • train.py gains --manager={stable,warp} and auto-injects presets=newton into Hydra args when the warp backend is chosen; the warp path dispatches through WarpFrontend().build() instead of gym.make().
  • manager_term_cfg.py is simplified to a single from isaaclab.managers.manager_term_cfg import * so isinstance checks against stable term-cfg types pass on the warp managers without parallel class definitions.

Confidence Score: 3/5

The bridge logic and the visualizer fix are correct, but the warp path in train.py does not forward render_mode, so combining --video with --manager=warp will fail at the RecordVideo wrapper step.

Two related files (warp_frontend.py and train.py) both omit render_mode from the warp env construction, breaking the --video code path that the stable branch handles correctly. The missing-term report is also silently discarded by build(), making it harder to detect incomplete adaptations programmatically.

warp_frontend.py (build signature, exception handling in _mdp_modules_for) and train.py (render_mode omission in the warp branch)

Important Files Changed

Filename Overview
source/isaaclab_experimental/isaaclab_experimental/envs/warp_frontend.py New adapter class; build() drops render_mode (breaks --video) and discards the WarpAdaptReport; _mdp_modules_for uses a bare except Exception that silently falls back if gym spec lookup fails
scripts/reinforcement_learning/rsl_rl/train.py Adds --manager flag and warp dispatch path; warp branch omits render_mode when --video is set, causing RecordVideo wrapper failure
source/isaaclab_experimental/isaaclab_experimental/envs/manager_based_env_warp.py Fixes visualizer probe to use has_active_visualizers() + has_kit() instead of the removed get_setting string-split approach
source/isaaclab_experimental/isaaclab_experimental/managers/manager_term_cfg.py Replaces duplicate sibling class definitions with a single star-import; fixes isinstance compatibility without any new logic
source/isaaclab_experimental/changelog.d/warp-manager-bridge.rst Changelog entry for the new bridge and the visualizer fix; documentation only

Sequence Diagram

sequenceDiagram
    participant CLI as rsl_rl/train.py
    participant WF as WarpFrontend
    participant NS as _NameSwap
    participant Env as ManagerBasedRLEnvWarp

    CLI->>CLI: parse --manager=warp
    CLI->>CLI: inject presets=newton into remaining_args
    CLI->>WF: WarpFrontend().build(env_cfg, task_id)
    WF->>WF: adapt(cfg, task_id)
    WF->>WF: resolve PresetCfg to newton
    WF->>WF: drop unsupported sensors
    WF->>WF: _upgrade_scene_entity_cfgs(cfg)
    WF->>WF: _mdp_modules_for(task_id)
    WF->>NS: apply_to(obs/reward/term/event groups)
    NS-->>NS: _resolve_twin(name) via __module__ check
    NS-->>WF: swapped funcs or drop/raise
    WF->>NS: apply_to(actions) class_type swap always strict
    NS-->>WF: swapped class_type or LookupError
    WF->>WF: log WarpAdaptReport report discarded
    WF->>Env: ManagerBasedRLEnvWarp(cfg=cfg) render_mode missing
    Env-->>CLI: env
    CLI->>CLI: RecordVideo(env) if --video fails without render_mode
Loading

Reviews (1): Last reviewed commit: "Add changelog and apply ruff formatting ..." | Re-trigger Greptile

Comment on lines +256 to +267
def build(self, cfg: Any, task_id: str):
"""Adapt ``cfg`` and return a :class:`ManagerBasedRLEnvWarp` instance.

Always logs the missing-twin report (if any). With ``strict=True``,
any missing twin raises before the env is constructed.
"""
# Lazy: this is the first warp-lib load. Caller must already be inside
# the SimulationApp context, i.e. inside ``launch_simulation``.
from isaaclab_experimental.envs import ManagerBasedRLEnvWarp

self.adapt(cfg, task_id)
return ManagerBasedRLEnvWarp(cfg=cfg)
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 WarpFrontend.build always constructs ManagerBasedRLEnvWarp with render_mode=None, but ManagerBasedRLEnvWarp.__init__ accepts render_mode and stores it. When --video is combined with --manager=warp, gym.wrappers.RecordVideo is applied to the returned env, but because render_mode was never set to "rgb_array", gymnasium will raise an error. The warp path silently drops the flag that the stable path (gym.make(..., render_mode="rgb_array" if args_cli.video else None)) correctly sets.

Suggested change
def build(self, cfg: Any, task_id: str):
"""Adapt ``cfg`` and return a :class:`ManagerBasedRLEnvWarp` instance.
Always logs the missing-twin report (if any). With ``strict=True``,
any missing twin raises before the env is constructed.
"""
# Lazy: this is the first warp-lib load. Caller must already be inside
# the SimulationApp context, i.e. inside ``launch_simulation``.
from isaaclab_experimental.envs import ManagerBasedRLEnvWarp
self.adapt(cfg, task_id)
return ManagerBasedRLEnvWarp(cfg=cfg)
def build(self, cfg: Any, task_id: str, render_mode: str | None = None):
"""Adapt ``cfg`` and return a :class:`ManagerBasedRLEnvWarp` instance.
Always logs the missing-twin report (if any). With ``strict=True``,
any missing twin raises before the env is constructed.
"""
# Lazy: this is the first warp-lib load. Caller must already be inside
# the SimulationApp context, i.e. inside ``launch_simulation``.
from isaaclab_experimental.envs import ManagerBasedRLEnvWarp
self.adapt(cfg, task_id)
return ManagerBasedRLEnvWarp(cfg=cfg, render_mode=render_mode)

# SimulationApp is already alive avoids racing pxr extension init.
from isaaclab_experimental.envs.warp_frontend import WarpFrontend

env = WarpFrontend().build(env_cfg, task_id=args_cli.task)
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 The warp env is built without render_mode, so gym.wrappers.RecordVideo applied on line 222 will fail when --video --manager=warp is used. Pass the same render_mode the stable path uses.

Suggested change
env = WarpFrontend().build(env_cfg, task_id=args_cli.task)
env = WarpFrontend().build(env_cfg, task_id=args_cli.task, render_mode="rgb_array" if args_cli.video else None)

Comment on lines +256 to +267
def build(self, cfg: Any, task_id: str):
"""Adapt ``cfg`` and return a :class:`ManagerBasedRLEnvWarp` instance.

Always logs the missing-twin report (if any). With ``strict=True``,
any missing twin raises before the env is constructed.
"""
# Lazy: this is the first warp-lib load. Caller must already be inside
# the SimulationApp context, i.e. inside ``launch_simulation``.
from isaaclab_experimental.envs import ManagerBasedRLEnvWarp

self.adapt(cfg, task_id)
return ManagerBasedRLEnvWarp(cfg=cfg)
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 build() discards the WarpAdaptReport returned by adapt(), so callers have no programmatic access to what was dropped. In the default non-strict mode, silently-dropped terms cause the env to train with a smaller effective term set than expected. A caller who wants to enforce a "fail if anything was dropped" policy must call adapt() separately, which then re-mutates the already-mutated cfg when build() calls it again internally.

Comment on lines +323 to +329
modules: list[ModuleType] = []
try:
import gymnasium as gym

entry = gym.spec(task_id).kwargs.get("env_cfg_entry_point")
except Exception:
entry = None
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 The bare except Exception swallows any error from gymnasium.spec(task_id) — including gymnasium.error.NameNotFound if the task ID is misspelled and real programming errors. When this fires, entry is set to None and only the fallback mdp module is used, so no task-specific warp twins are discovered, causing every term to go missing without any indication of why.

@hujc7 hujc7 marked this pull request as draft May 5, 2026 17:05
hujc7 added 5 commits May 5, 2026 17:24
The adapter is now a sequence of CompatRule objects (resolve preset, drop
sensors, promote SceneEntityCfg, swap mdp funcs, swap action class). New
incompatibilities are added by writing a small rule subclass instead of
editing the dispatcher.

The CLI flag is renamed --manager → --frontend because the dispatch also
covers direct envs: a stable manager-based cfg is adapted onto
ManagerBasedRLEnvWarp; a direct task is verified to point at a warp env
class and dispatched via gym.make. A stable direct cfg + --frontend=warp
raises IncompatibleEnvError with the offending entry_point and a hint at
the *-Direct-Warp-v0 alternative.

Other fixes:

- Forward render_mode through build() so --video keeps working.
- Attach the CompatReport on env.unwrapped.warp_compat_report so callers
  can inspect what was dropped or left unresolved.
- Assert the warp SceneEntityCfg subclasses the stable one before doing
  the in-place __class__ promotion; the rule fails loudly if the
  hierarchy is ever broken.
- Narrow the bare except in mdp-module discovery so real ImportErrors
  from broken cfgs propagate.
- presets=newton is now only auto-injected for stable manager-based
  tasks; direct warp tasks (which don't carry presets) are left alone.
- Warn when the user passes presets=<other> with --frontend=warp.
- Add the commands group to the rule that promotes SceneEntityCfg.
The earlier check inspected gym.spec(task).entry_point, which for stable
manager-based tasks is "isaaclab.envs:ManagerBasedRLEnv" — the env class
path, not the cfg path. So the startswith("isaaclab_tasks.manager_based")
test always failed and presets=newton was never injected. Hydra then
resolved every PresetCfg in the cfg tree (physics, contact_forces, etc.)
to its default field, leaving the warp runtime with PhysX class_types it
can't load.

Switch to spec.kwargs["env_cfg_entry_point"], which actually points at the
task cfg module (e.g. "isaaclab_tasks.manager_based.locomotion.velocity.
config.anymal_d.flat_env_cfg:AnymalDFlatEnvCfg"), and the prefix check
selects the right tasks.
The single-file warp_frontend.py grew into a real subsystem worth
splitting out, so move it into a frontend/ package with explicit
abstractions:

- frontend/base.py: Frontend ABC, CompatRule (check + transform via a
  unified run() method), TaskResolver (centralised gym.spec
  introspection -> TaskMeta), Workflow / Runtime / Severity enums,
  Issue / Change / Report record types, register_frontend / get_frontend
  registry. Helpers walk_attrs / resolve_warp_twin / iter_term_attrs are
  shared utilities used by rules.
- frontend/torch.py: TorchFrontend, the default. Pass-through to gym.make
  with one rule (WarnIfTaskIsWarpRegistered) for the contradiction case.
- frontend/warp.py: WarpFrontend with the full rule pipeline. Includes a
  new CheckPhysicsIsNewton blocking rule that surfaces the PhysX-with-warp
  incompatibility (asset class_type strings resolve to isaaclab_physx.*
  classes that depend on omni.physics.tensors.api, which the warp runtime
  does not initialise).

CLI: rename --frontend stable -> torch since the axis is *runtime*, not
*stability tier*. The frontend selector now reads cleanly:

  --frontend torch  ->  default gym.make path
  --frontend warp   ->  experimental warp runtime via WarpFrontend

train.py becomes a thin dispatcher: get_frontend(name) gives a Frontend
instance, frontend.preprocess_hydra_args(...) handles preset injection,
frontend.build(cfg, task) returns the env. No more inline conditional
imports; no more inline preset-injection logic.

env.unwrapped.frontend_report is the inspection point - callers and tests
can read what changed and what was missing without re-running adapt().

To add a new compatibility check, write a CompatRule subclass and append
it to the relevant frontend's `rules` tuple. To add a new runtime,
subclass Frontend and call register_frontend(name, cls).
- SwapMdpFunctions: skip terms whose ``func.__module__`` is already under
  the warp prefixes. Without this, running the bridge against a task
  already registered under ``isaaclab_tasks_experimental`` (e.g.
  ``Isaac-Cartpole-Warp-v0 --frontend=warp``) would silently drop terms
  whose warp twin happens not to live in the resolved fallback module.
  Also tighten ``_mdp_modules`` to require the trailing dot when matching
  ``isaaclab_tasks`` so we don't double-replace the prefix and end up
  importing ``isaaclab_tasks_experimental_experimental.*``.

- ResolvePhysicsPreset: scope to MANAGER_BASED via ``applies_to``. Direct
  cfgs aren't expected to carry ``PresetCfg`` wrappers; running this rule
  on them was a no-op but the scoping makes the contract explicit.

- CheckPhysicsIsNewton: positively accept ``isaaclab_newton.*`` modules,
  block on ``isaaclab_physx.*``, warn on anything else. The previous
  rule only rejected PhysX, so a custom or third-party physics cfg in an
  unrelated module would slip through silently.

- PromoteSceneEntityCfg: catch ``TypeError`` from the in-place
  ``__class__`` reassignment and surface it as a blocking issue.
  ``issubclass`` does not guarantee Python permits the layout change
  (slots, layout flags); failing loud at this seam is better than a
  cryptic crash mid-pipeline.

- WarpFrontend.preprocess_hydra_args: normalise leading dashes when
  inspecting ``presets=``, so ``--presets=foo`` is treated the same as
  ``presets=foo`` (Hydra accepts both forms).

- Frontend.resolve: hard-block when ``gym.spec`` returned no spec.
  Previously ``meta.runtime`` was ``UNKNOWN`` and ``construct`` would
  fail later with a less specific error; now the block fires before any
  rule runs.

- train.py: import the frontend lazily and tolerate ``ImportError``
  when ``--frontend=torch`` (the default). The experimental package is
  optional, so a missing install used to break the default path; now it
  falls back to ``gym.make`` for torch and only fails for ``warp``.

- frontend/__init__.py: trim ``__all__`` and the wildcard re-export so
  helpers (``walk_attrs``, ``iter_term_attrs``, ``resolve_warp_twin``,
  ``WARP_ROOT_PREFIXES``) are no longer advertised as the public framework
  surface. They remain importable from ``frontend.base`` for users
  writing their own rules.
- TaskResolver._classify_runtime: also accept class/callable entry points
  by inspecting __module__. gym.register accepts both ``"module:Class"``
  strings and class objects; the old check only handled strings, so a
  warp-registered task using the class form classified as
  Runtime.UNKNOWN and the warn / verify rules silently disengaged.

- SwapMdpFunctions._mdp_modules: narrow the exception match from
  ImportError to ModuleNotFoundError where ``exc.name`` matches the
  module being looked up. Previously a real ImportError raised inside
  an existing mdp module (broken import inside the package) would be
  silently swallowed and the rule would fall through to the fallback
  module, producing misleading "no warp twin" reports.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant