Skip to content

Decouple physics-engine event terms into per-backend modules#5915

Open
ooctipus wants to merge 3 commits into
isaac-sim:developfrom
ooctipus:zhengyuz/decouple-physics-events
Open

Decouple physics-engine event terms into per-backend modules#5915
ooctipus wants to merge 3 commits into
isaac-sim:developfrom
ooctipus:zhengyuz/decouple-physics-events

Conversation

@ooctipus
Copy link
Copy Markdown
Collaborator

@ooctipus ooctipus commented Jun 2, 2026

Summary

  • Splits the physics-property randomization event terms out of isaaclab.envs.mdp.events into a new isaaclab.envs.mdp.physics_events module: randomize_rigid_body_material, randomize_rigid_body_scale, randomize_rigid_body_mass, randomize_rigid_body_inertia, randomize_rigid_body_com, randomize_rigid_body_collider_offsets, randomize_physics_scene_gravity, randomize_joint_parameters, randomize_fixed_tendon_parameters, randomize_actuator_gains (plus the shared randomize_prop_by_op / validate_scale_range helpers). events.py now only holds backend-agnostic terms (resets, force/velocity, visual material/color).
  • Relocates the backend-specific implementations into isaaclab_<backend>.envs.mdp.physics_events for PhysX, Newton, and OVPhysX. The core public terms resolve the active backend's implementation at runtime via a small private resolver, so isaaclab.envs.mdp.events/physics_events no longer import pxr or any backend.
  • OVPhysX reuses the PhysX implementations for every term except material randomization, which has a dedicated no-op (the OVPhysX material tensor binding is not yet available).
  • Deprecates randomize_rigid_body_scale: it remains PhysX-only (Newton raises NotImplementedError) and emits a DeprecationWarning recommending multi-asset spawning (MultiAssetSpawnerCfg).
  • New extension envs/mdp packages follow the lazy __init__.py + .pyi stub convention.

Test plan

  • ./isaaclab.sh -p -m pytest source/isaaclab_physx/test/assets/test_newton_actuators_physx.py
  • ./isaaclab.sh -p -m pytest source/isaaclab_newton/test/assets/test_newton_actuators_newton.py
  • Run an env that uses randomize_rigid_body_material / randomize_joint_parameters on both the PhysX and Newton backends and confirm parity with the previous behavior.
  • Confirm randomize_rigid_body_scale raises on Newton and warns (deprecated) on PhysX.

Note: changelog fragments are not yet included; happy to add per-package fragments if desired.

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jun 2, 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.

Code Review: Decouple physics-engine event terms into per-backend modules

Thanks for the clean architecture to decouple physics-specific event terms into per-backend modules, @ooctipus. The runtime resolver pattern via _resolve_backend_event is a solid approach for keeping the core package backend-agnostic. Below are findings from reviewing the diff:


🔴 High Severity

1. _resolve_backend_event uses startswith which is less resilient than the original substring matching

File: source/isaaclab/isaaclab/envs/mdp/physics_events.py (lines ~56-63)

The resolver uses manager_name.startswith("newton"), startswith("ovphysx"), startswith("physx") to detect the backend. In the original events.py, detection used substring matching ("newton" in manager_name, "physx" in manager_name) and explicit equality for OVPhysX.

Current Newton subclass managers (e.g. NewtonMJWarpManagernewtonmjwarpmanager) start with "newton", so this works. But any future PhysX variant whose class name doesn't start with "physx" (e.g. FastPhysXManagerfastphysxmanager) would fail with ValueError. The original substring approach was more resilient.

Suggested fix:

if "newton" in manager_name:
    backend = "newton"
elif "ovphysx" in manager_name:
    backend = "physx" if ovphysx_uses_physx else "ovphysx"
elif "physx" in manager_name:
    backend = "physx"
else:
    raise ValueError(f"Unknown physics manager: {manager_name!r}")

Note: ordering matters — "ovphysx" contains "physx", so test "ovphysx" before "physx".


2. PhysX RandomizePhysicsSceneGravity hard-codes distribution="uniform", silently ignoring the user's distribution parameter

File: source/isaaclab_physx/isaaclab_physx/envs/mdp/physics_events.py (~line 247)

gravity = randomize_prop_by_op(
    gravity,
    (self._dist_param_0.cpu(), self._dist_param_1.cpu()),
    None,
    slice(None),
    operation=operation,
    distribution="uniform",  # ← hard-coded, ignores user's `distribution` param
)

The distribution parameter is accepted in __call__ but never propagated. The Newton implementation correctly caches self._dist_fn at init and uses it. This is a silent correctness bug: users requesting "log_uniform" or "gaussian" gravity randomization on PhysX will get uniform sampling without any warning.

Suggested fix: Cache the distribution name/function at init and use it in the call, or at minimum pass the distribution parameter through.


🟡 Medium Severity

3. Renamed helper functions may break downstream code importing the private API

File: source/isaaclab/isaaclab/envs/mdp/physics_events.py

The helpers _randomize_prop_by_oprandomize_prop_by_op and _validate_scale_rangevalidate_scale_range have been renamed (underscore prefix removed) for their new role as shared utilities. Any code that imported these from isaaclab.envs.mdp.events will break.

While these were private APIs (underscore-prefixed), downstream packages sometimes import them for custom event terms.

Consider adding backward-compatible aliases in events.py:

from .physics_events import randomize_prop_by_op as _randomize_prop_by_op  # noqa: F401
from .physics_events import validate_scale_range as _validate_scale_range  # noqa: F401

4. PhysX RandomizePhysicsSceneGravity.__init__ does not validate the distribution parameter

File: source/isaaclab_physx/isaaclab_physx/envs/mdp/physics_events.py (~line 216)

The Newton implementation validates distribution at init and raises NotImplementedError for unknown values. The PhysX implementation skips this entirely. If finding #2 is fixed, add validation here as well.


5. randomize_rigid_body_scale instantiates a new backend object on every call

File: source/isaaclab/isaaclab/envs/mdp/physics_events.py (~line 118)

_resolve_backend_event("RandomizeRigidBodyScale")(env, env_ids, scale_range, asset_cfg, relative_child_path)

Unlike other terms (which cache self._impl at init), this function creates a new RandomizeRigidBodyScale instance per call via importlib.import_module + getattr. Since the term is deprecated and should only run at USD-event time (once), this is acceptable but noted.


🟢 Low Severity / Nits

6. Top-level imports of carb and pxr in PhysX backend module

File: source/isaaclab_physx/isaaclab_physx/envs/mdp/physics_events.py (lines 13-14)

import carb
from pxr import Gf, Sdf, UsdGeom, Vt

Only RandomizeRigidBodyScale uses pxr and RandomizePhysicsSceneGravity uses carb. Consider lazy imports inside the classes to reduce module load time when only a subset of terms is used.

7. Missing __all__ in core physics_events.py

The new module exports public symbols but doesn't define __all__. Adding it would make from .physics_events import * behavior explicit.

8. CI status: pre-commit and Check changelog fragments are currently failing

These are expected (PR description notes changelog fragments aren't included yet), but pre-commit failure may indicate formatting issues that should be resolved before merge.


✅ What looks good

  • Clean separation of concerns: Backend-specific code now lives in the appropriate extension packages (isaaclab_physx, isaaclab_newton, isaaclab_ovphysx).
  • Runtime resolver pattern: _resolve_backend_event is a simple, understandable mechanism for late-binding backend implementations.
  • OVPhysX strategy: Reusing PhysX implementations by default (via ovphysx_uses_physx=True) with explicit no-op for material randomization is pragmatic.
  • Deprecation handling: randomize_rigid_body_scale properly emits DeprecationWarning and the Newton backend raises NotImplementedError.
  • Import hygiene: The core module no longer imports pxr, carb, or warp — making it truly backend-agnostic.
  • Test updates: Both test files correctly update their imports to the new module path.

Summary

# Severity Issue
1 🔴 High startswith in resolver less resilient than original substring matching
2 🔴 High PhysX gravity randomization silently ignores user-specified distribution
3 🟡 Medium Renamed private helpers break downstream private-API consumers
4 🟡 Medium PhysX gravity missing distribution validation
5 🟡 Medium Scale randomization instantiates backend on every call
6 🟢 Low Eager heavy imports in PhysX backend module
7 🟢 Low Missing __all__ in core physics_events.py
8 🟢 Low CI pre-commit check failing

Overall this is a well-structured refactor with clean separation of concerns. The main actionable items are #1 (resolver resilience) and #2 (distribution parameter silently ignored). The architecture is sound and scales well for future backends.


Update (37b83f6): Changelog fragments added for all affected packages and minor whitespace cleanup in physics_events.py (removed extra blank lines). ✅ Addresses concern #8 (CI changelog check). Remaining concerns #1#7 still apply to unchanged code.


Update (3942c86): Reviewed incremental changes since 37b83f6.

New feature: resample_interval_on_reset added to EventTermCfg — allows interval-mode event terms to preserve their per-environment timer across resets. Implementation looks correct in both the standard and experimental event managers, with a solid test covering both paths. ✅

Refactoring: isaaclab_experimental/envs/mdp/__init__.py now uses lazy_export() instead of explicit star-imports — cleaner and more maintainable. ✅

Previous concerns status:

  • #2 (PhysX gravity distribution hardcoded) — still present at line 274 of PhysX physics_events.py
  • #1 (resolver startswith) — still present (unchanged code)
  • #3#7 — unchanged

No new issues found in this increment.


Update (527813b): Lint-only change — reordered imports in isaaclab_newton/envs/mdp/physics_events.py to follow Python import conventions (third-party imports before local imports, TYPE_CHECKING block reorganized). ✅ No functional changes, no new concerns.

Previous concerns status: #1#7 unchanged.


Update (1f45c56): Major increment — includes backend resolver fix + cartpole task consolidation.

✅ Fixed:

  • #1 (resolver startswith) — Now uses substring matching ("in") with correct ordering (ovphysx tested before physx). Exactly matches the recommendation.
  • #4 (PhysX gravity missing distribution validation) — Validation is now present at __init__ time.

Still present (unchanged code):

  • #2 — PhysX RandomizePhysicsSceneGravity still hardcodes distribution="uniform" at line ~283
  • #3, #5, #6, #7 — Unchanged

New changes (no issues): Cartpole task restructuring (merged direct_cartpole + manager_cartpolecartpole, dropped -v0 suffixes, removed deprecated alias registrations). All test imports updated consistently. isaaclab_experimental switched to lazy_export(). Clean refactor, no new concerns.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR decouples physics-engine-specific event terms from the generic isaaclab.envs.mdp.events module by introducing a new physics_events.py in the core package plus per-backend implementations under isaaclab_physx, isaaclab_newton, and isaaclab_ovphysx. A small runtime resolver (_resolve_backend_event) dispatches to the correct backend at construction time, eliminating pxr/warp imports from the public surface.

  • Backend-agnostic terms (resets, external forces, visual material) remain in events.py; all physics-property randomization terms move to physics_events.py with delegation to per-backend classes.
  • randomize_rigid_body_scale is deprecated and raises NotImplementedError on Newton; OVPhysX reuses PhysX implementations except for material randomization (documented no-op).
  • Backend modules adopt a lazy __init__.py + .pyi stub convention consistent with the rest of the codebase.

Confidence Score: 3/5

Two real behavioral defects need fixing: gravity randomization on PhysX silently falls back to uniform regardless of configuration, and the Newton collider-offset implementation permanently overwrites stored defaults on each call.

The PhysX gravity implementation hardcodes distribution='uniform' inside randomize_prop_by_op, discarding any configured distribution. Newton's RandomizeRigidBodyColliderOffsets mutates self.default_margin in-place, making repeated resets over disjoint env_ids subsets accumulate drift.

source/isaaclab_physx/isaaclab_physx/envs/mdp/physics_events.py (gravity distribution) and source/isaaclab_newton/isaaclab_newton/envs/mdp/physics_events.py (collider default margin mutation)

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/envs/mdp/physics_events.py New public module with backend-agnostic wrappers delegating via _resolve_backend_event; confusing deprecation+error ordering for randomize_rigid_body_scale on Newton.
source/isaaclab_physx/isaaclab_physx/envs/mdp/physics_events.py PhysX backend implementations; RandomizePhysicsSceneGravity hardcodes distribution="uniform", silently ignoring any user-configured distribution.
source/isaaclab_newton/isaaclab_newton/envs/mdp/physics_events.py Newton backend implementations; RandomizeRigidBodyColliderOffsets mutates self.default_margin on every call, diverging from original defaults across resets.
source/isaaclab_ovphysx/isaaclab_ovphysx/envs/mdp/physics_events.py OVPhysX no-op for material randomization with a clear warning; all other terms fall through to PhysX backend.
source/isaaclab/isaaclab/envs/mdp/events.py Physics-property randomization moved to physics_events.py; now holds only backend-agnostic resets and visual-material terms.
source/isaaclab/isaaclab/envs/mdp/init.pyi Stub updated to re-export physics terms from physics_events and visual/reset terms from events; split looks correct.
source/isaaclab_ovphysx/isaaclab_ovphysx/envs/mdp/init.py Stale _EventTermFactory reference in docstring; the actual router is _resolve_backend_event.

Reviews (1): Last reviewed commit: "Decouple physics-engine event terms into..." | Re-trigger Greptile

Comment on lines +265 to +274
gravity = randomize_prop_by_op(
gravity,
(self._dist_param_0.cpu(), self._dist_param_1.cpu()),
None,
slice(None),
operation=operation,
distribution="uniform",
)
gravity = gravity[0].tolist()
self._physics_sim_view.set_gravity(self._carb.Float3(*gravity))
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 Distribution parameter silently ignored on PhysX

randomize_prop_by_op is called with a hardcoded distribution="uniform", so any value configured in cfg.params["distribution"] (e.g. "log_uniform" or "gaussian") is silently discarded. Newton's RandomizePhysicsSceneGravity.__init__ correctly caches the configured distribution function in self._dist_fn, but the PhysX implementation never stores it. A user who sets distribution="gaussian" on PhysX will silently get uniform sampling with no error or warning.

Comment on lines +158 to +160
)
self.default_margin[env_ids] = margin[env_ids]
margin_view[env_ids] = margin[env_ids]
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 default_margin mutated across calls, diverging from original defaults

self.default_margin[env_ids] = margin[env_ids] permanently overwrites the stored defaults. On the next reset for a different env_ids subset, those environments' default margin is the previously randomized value rather than the original scene default. The PhysX counterpart always clones from self.default_rest_offsets (never mutated). Because the operation is currently hardcoded to "abs" the mutation does not affect output values today, but would silently break if "add" or "scale" support is added.

Comment on lines +11 to +13
OVPhysX reuses the PhysX implementations for most event terms (see the ``ovphysx -> physx``
fallback in :class:`~isaaclab.envs.mdp.physics_events._EventTermFactory`); only terms that need a
different OVPhysX behavior are defined here.
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 docstring references _EventTermFactory, a class that does not exist in the codebase. The runtime routing is the module-level function _resolve_backend_event in isaaclab.envs.mdp.physics_events.

Suggested change
OVPhysX reuses the PhysX implementations for most event terms (see the ``ovphysx -> physx``
fallback in :class:`~isaaclab.envs.mdp.physics_events._EventTermFactory`); only terms that need a
different OVPhysX behavior are defined here.
OVPhysX reuses the PhysX implementations for most event terms (see the ``ovphysx -> physx``
fallback in :func:`~isaaclab.envs.mdp.physics_events._resolve_backend_event`); only terms that need a
different OVPhysX behavior are defined here.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +116 to +124
warnings.warn(
"'randomize_rigid_body_scale' is deprecated and only supported on the PhysX backend. Prefer"
" multi-asset spawning (see :class:`~isaaclab.sim.MultiAssetSpawnerCfg`) with per-scale USD"
" variants instead.",
DeprecationWarning,
stacklevel=2,
)
# the USD scale authoring is backend-specific; resolve the active backend's implementation
_resolve_backend_event("RandomizeRigidBodyScale")(env, env_ids, scale_range, asset_cfg, relative_child_path)
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 Confusing warning/error ordering for Newton users

On Newton, randomize_rigid_body_scale first emits a DeprecationWarning saying the function is "only supported on the PhysX backend", then immediately raises NotImplementedError. Users on Newton see the deprecation first, implying it could work if they switch backends, before hitting an uncaught error. The Newton path should skip the PhysX-flavored deprecation warning and raise NotImplementedError directly.

@ooctipus ooctipus force-pushed the zhengyuz/decouple-physics-events branch 3 times, most recently from 9a97962 to 527813b Compare June 2, 2026 06:48
@ooctipus ooctipus force-pushed the zhengyuz/decouple-physics-events branch from 527813b to 1f45c56 Compare June 2, 2026 10:02
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