Refactors the doc on Schema for Lab 3.0, and adds MuJoCo gravcomp#5276
Conversation
Greptile SummaryAdds MuJoCo gravity-compensation and Newton-native schema cfg classes (
Confidence Score: 4/5The cfg classes and forwarding shims are well-structured and safe. One block in the core spawner unconditionally imports from isaaclab_newton, which will break any non-Newton spawn that sets joint drive properties when isaaclab_newton is not installed. The cfg definitions, shims, and tests are solid. The spawner change in from_files.py introduces a hard dependency on isaaclab_newton for all joint-drive users: the import fires unconditionally inside the if-block, raising ImportError for PhysX and base configs alike when isaaclab_newton is absent. Additionally, the isinstance guard that decides whether to auto-apply body-level gravcomp does not check whether gravcomp is actually set on an existing MujocoRigidBodyPropertiesCfg, so the auto-apply can be silently skipped even when body-level compensation is absent. source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py — both the unconditional import and the isinstance guard logic need to be fixed before merging. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[_spawn_from_usd_file] --> B{joint_drive_props\nis not None?}
B -- No --> Z[skip gravcomp block]
B -- Yes --> C["from isaaclab_newton import\nMujocoJointDrivePropertiesCfg\nMujocoRigidBodyPropertiesCfg\n⚠️ UNCONDITIONAL — fails if\nisaaclab_newton not installed"]
C --> D{isinstance\njoint_drive_props,\nMujocoJointDrivePropertiesCfg?}
D -- No --> G[modify_joint_drive_properties]
D -- Yes --> E{actuatorgravcomp\nis truthy?}
E -- No --> G
E -- Yes --> F{isinstance\nrigid_props,\nMujocoRigidBodyPropertiesCfg?}
F -- "Yes, but gravcomp=None" --> H["⚠️ No auto-apply fires\nBody gravcomp not set\nBut no warning logged"]
F -- "No / different type" --> I["Auto-apply\nMujocoRigidBodyPropertiesCfg\ngravcomp=1.0"]
I --> G
H --> G
Reviews (3): Last reviewed commit: "Merge branch 'develop' into vidur/featur..." | Re-trigger Greptile |
| Args: | ||
| prim_path: The prim path to the rigid body. | ||
| cfg: The configuration for the rigid body. | ||
| cfg: The configuration for the rigid body. Can be | ||
| :class:`~schemas_cfg.RigidBodyPropertiesCfg` for solver-common properties or | ||
| :class:`~schemas_cfg.PhysxRigidBodyPropertiesCfg` for PhysX-specific properties. | ||
| stage: The stage where to find the prim. Defaults to None, in which case the |
There was a problem hiding this comment.
cfg arg docstring doesn't mention the new MuJoCo type
The Args section for cfg lists RigidBodyPropertiesCfg and PhysxRigidBodyPropertiesCfg but not MujocoRigidBodyPropertiesCfg. The same omission exists in modify_joint_drive_properties (around line 650). Since users discover supported types from the docstring, both should be updated to include the new classes.
| Args: | |
| prim_path: The prim path to the rigid body. | |
| cfg: The configuration for the rigid body. | |
| cfg: The configuration for the rigid body. Can be | |
| :class:`~schemas_cfg.RigidBodyPropertiesCfg` for solver-common properties or | |
| :class:`~schemas_cfg.PhysxRigidBodyPropertiesCfg` for PhysX-specific properties. | |
| stage: The stage where to find the prim. Defaults to None, in which case the | |
| cfg: The configuration for the rigid body. Can be | |
| :class:`~schemas_cfg.RigidBodyPropertiesCfg` for solver-common properties, | |
| :class:`~schemas_cfg.PhysxRigidBodyPropertiesCfg` for PhysX-specific properties, or | |
| :class:`~schemas_cfg.MujocoRigidBodyPropertiesCfg` for Newton (MuJoCo)-specific properties. |
| gravity_compensation_scale: float | None = None | ||
| """Scale factor for gravity compensation for the body [dimensionless]. Defaults to None | ||
| (attribute not written to USD). | ||
|
|
||
| In MuJoCo, ``0.0`` means no compensation and ``1.0`` means full compensation. | ||
| """ |
There was a problem hiding this comment.
No range validation for
gravity_compensation_scale
MuJoCo's gravcomp is documented as a scale in [0.0, 1.0] where 0 means no compensation and 1 means full compensation. Values outside this range are silently accepted and passed through to USD, which may produce unexpected simulation behaviour. Consider adding a __post_init__ validator or at least a more explicit range annotation in the docstring (e.g. [0.0, 1.0, dimensionless]) to prevent silently misconfigured assets.
| if applied_schema: | ||
| if applied_schema not in rigid_body_prim.GetAppliedSchemas(): | ||
| rigid_body_prim.AddAppliedSchema(applied_schema) | ||
| for attr_name, value in cfg_dict.items(): | ||
| usd_attr_name = attr_name_map.get(attr_name, to_camel_case(attr_name, "cC")) | ||
| safe_set_attribute_on_usd_prim(rigid_body_prim, f"{namespace}:{usd_attr_name}", value, camel_case=False) |
There was a problem hiding this comment.
Missing guard when
namespace is None but cfg_dict is non-empty
If a future subclass of RigidBodyPropertiesCfg adds fields without setting _usd_namespace, the f-string f"{namespace}:{usd_attr_name}" evaluates to "None:someAttr" and silently writes a garbage attribute name. The existing subclasses are safe today, but a guard would be defensive:
if namespace is not None:
for attr_name, value in cfg_dict.items():
usd_attr_name = attr_name_map.get(attr_name, to_camel_case(attr_name, "cC"))
safe_set_attribute_on_usd_prim(rigid_body_prim, f"{namespace}:{usd_attr_name}", value, camel_case=False)e59303f to
a1593d5
Compare
4cb53ee to
9ecd5e4
Compare
|
My main concern here is usability. Users will have to call the right schema for the right backend which will mean backend specific assets? @vidurv-nvidia |
9ecd5e4 to
34adc18
Compare
…ields (#5275) # Description Splits IsaacLab's USD-physics cfg classes into solver-common base classes and backend-specific subclasses, and refactors the writers (`modify_*_properties`, `spawn_rigid_body_material`) so that schema application is data-driven rather than hard-coded per-class. Prepares the schema layer for multi-backend support (PhysX today, Newton/Mjc next) without polluting base classes with silently-ignored fields or stamping backend-specific schemas onto prims that didn't opt in. ## Architecture Two layered concepts: 1. **Per-declaring-class routing.** Each cfg field's USD namespace is determined by the class that declares it (walking the MRO). Base-class fields write under `physics:*`; subclass fields write under their own namespace (`physxRigidBody:*`, etc.). When a `PhysxRigidBodyPropertiesCfg` instance is written, base fields still go under `physics:*` because `_usd_namespace` is read from the declaring class via `__dict__`, not via `getattr` (which would hit the subclass override). 2. **Per-field exceptions.** Some "universal physics" fields have no USD path except through a backend-namespaced attribute today (e.g., `disable_gravity` only exists at `physxRigidBody:disableGravity`). These are declared as `_usd_field_exceptions = {applied_schema: (namespace, [fields...])}` on the base class; the writer applies the exception schema only when one of the listed fields is non-None. The single helper `_apply_namespaced_schemas(prim, cfg, cfg_dict)` in `schemas.py` does both passes for every writer (rigid body, collision, articulation root, joint drive, mesh collision, rigid-body material). ## Design constraints **One cfg class per spawner slot.** Spawners (`UsdFileCfg`, `MeshCuboidCfg`, etc.) carry a single field for each property group: `rigid_props: RigidBodyBaseCfg | None`, `collision_props: CollisionBaseCfg | None`, `joint_drive_props: JointDriveBaseCfg | None`, etc. The user cannot pass two cfgs into the same slot, so the cfg class hierarchy must be **single-rooted per spawner field** — one base class per group, with backend-specific subclasses below. This rules out a "PhysX cfg sits next to a Newton cfg as siblings" design and drives several placement decisions: | Constraint | Consequence | |---|---| | Universal-physics fields must be reachable from any backend's cfg | Goes on the **base** class, not a sibling backend cfg. Users on Newton-only deployments can use `RigidBodyBaseCfg(disable_gravity=True)` without importing `isaaclab_physx`. | | A PhysX-namespaced field whose semantics are universal (e.g., `disable_gravity`) | Lives on the base but routes to the PhysX namespace via `_usd_field_exceptions`. The base stays backend-clean; the writer dispatches the PhysX write only when the field is non-None. | | Writer logic must not branch on cfg subclass | Every writer is the same code path regardless of subclass. The cfg metadata (`_usd_namespace`, `_usd_applied_schema`, `_usd_field_exceptions`) drives behavior; the writer is a pure data interpreter. | | Adding a new backend (Newton, Mjc) | Requires a new subclass with its own `_usd_namespace` / `_usd_applied_schema`. No spawner-side changes, no writer-side changes, no base-cfg-side changes. | | A field has multiple USD paths today (one PhysX-namespaced, one Newton-namespaced) | Belongs on the **PhysX subclass**, not the base. A future `NewtonArticulationRootPropertiesCfg` will own the same conceptual field on the Newton side. ("Rule 2" — e.g., `enabled_self_collisions`.) | | A field has only one USD path today, namespaced under PhysX, but the conceptual quantity is universal | Belongs on the **base** with an `_usd_field_exceptions` entry. ("Rule 1" — e.g., `disable_gravity`, `articulation_enabled`, `contact_offset`, `rest_offset`, `max_joint_velocity`.) When Newton ships its own native attribute, the exception namespace switches transparently with no API change. | ## Field placement ### Base (solver-common) classes — `physics:*` namespace via `UsdPhysics.*API` | Cfg class | Field | USD attribute | |---|---|---| | `RigidBodyBaseCfg` | `rigid_body_enabled` | `physics:rigidBodyEnabled` | | `RigidBodyBaseCfg` | `kinematic_enabled` | `physics:kinematicEnabled` | | `CollisionBaseCfg` | `collision_enabled` | `physics:collisionEnabled` | | `MassPropertiesCfg` | `mass` | `physics:mass` | | `MassPropertiesCfg` | `density` | `physics:density` | | `RigidBodyMaterialBaseCfg` | `static_friction` | `physics:staticFriction` | | `RigidBodyMaterialBaseCfg` | `dynamic_friction` | `physics:dynamicFriction` | | `RigidBodyMaterialBaseCfg` | `restitution` | `physics:restitution` | | `JointDriveBaseCfg` | `drive_type` | `drive:<axis>:physics:type` | | `JointDriveBaseCfg` | `max_force` | `drive:<axis>:physics:maxForce` | | `JointDriveBaseCfg` | `stiffness` | `drive:<axis>:physics:stiffness` | | `JointDriveBaseCfg` | `damping` | `drive:<axis>:physics:damping` | | `MeshCollisionBaseCfg` | `mesh_approximation_name` | `physics:approximation` (token) | | `ArticulationRootBaseCfg` | `fix_root_link` | (synthesizes `UsdPhysics.FixedJoint`) | `JointDriveBaseCfg` and `MeshCollisionBaseCfg` use the typed `UsdPhysics.DriveAPI` / `UsdPhysics.MeshCollisionAPI` accessors at the writer level (multi-instance namespace and `TfToken` with `allowedTokens`, respectively); all other base fields flow through the helper's per-class routing. ### PhysX subclasses — `physx*:*` namespaces, `Physx*API` schemas | Cfg class | `_usd_namespace` | `_usd_applied_schema` | Adds fields | |---|---|---|---| | `PhysxRigidBodyPropertiesCfg` | `physxRigidBody` | `PhysxRigidBodyAPI` | `linear_damping`, `angular_damping`, `max_linear_velocity`, `max_angular_velocity`, `max_depenetration_velocity`, `max_contact_impulse`, `enable_gyroscopic_forces`, `retain_accelerations`, solver iter counts, sleep / stabilization thresholds | | `PhysxCollisionPropertiesCfg` | `physxCollision` | `PhysxCollisionAPI` | `torsional_patch_radius`, `min_torsional_patch_radius` | | `PhysxArticulationRootPropertiesCfg` | `physxArticulation` | `PhysxArticulationAPI` | `enabled_self_collisions`, solver iter counts, sleep / stabilization thresholds | | `PhysxJointDrivePropertiesCfg` | `physxJoint` | `PhysxJointAPI` | (currently empty; reserved for future PhysX-only knobs) | | `PhysxRigidBodyMaterialCfg` | `physxMaterial` | `PhysxMaterialAPI` | `compliant_contact_stiffness`, `compliant_contact_damping`, `friction_combine_mode`, `restitution_combine_mode` | | `PhysxConvexHullPropertiesCfg` | `physxConvexHullCollision` | `PhysxConvexHullCollisionAPI` | `hull_vertex_limit`, `min_thickness` | | `PhysxConvexDecompositionPropertiesCfg` | `physxConvexDecompositionCollision` | `PhysxConvexDecompositionCollisionAPI` | hull / voxel / shrink-wrap tunables | | `PhysxTriangleMeshPropertiesCfg` | `physxTriangleMeshCollision` | `PhysxTriangleMeshCollisionAPI` | `weld_tolerance` | | `PhysxTriangleMeshSimplificationPropertiesCfg` | `physxTriangleMeshSimplificationCollision` | `PhysxTriangleMeshSimplificationCollisionAPI` | `simplification_metric`, `weld_tolerance` | | `PhysxSDFMeshPropertiesCfg` | `physxSDFMeshCollision` | `PhysxSDFMeshCollisionAPI` | `sdf_margin`, `sdf_narrow_band_thickness`, `sdf_resolution`, etc. | ### `_usd_field_exceptions` table These fields are declared on a *base* class but the only USD path today goes through a non-base namespace. Each entry says: "if any listed field on this cfg is non-None, apply the exception schema and write that one attribute under the exception namespace." All other fields on the cfg follow the per-declaring-class routing rule. | Base cfg class | Exception schema | Namespace | Field(s) | Why on the base | |---|---|---|---|---| | `RigidBodyBaseCfg` | `PhysxRigidBodyAPI` | `physxRigidBody` | `disable_gravity` | Per-body gravity exclusion is universal physics; PhysX honors per-body, Newton consumes the same attribute via the bridge resolver (scene-level today; per-body fix is a Newton-side kernel change, not a cfg-API change) | | `CollisionBaseCfg` | `PhysxCollisionAPI` | `physxCollision` | `contact_offset`, `rest_offset` | Collision-pair generation distance and rest gap are universal physics; Newton importer consumes both via PhysX bridge to populate `Model.shape_collision_radius` / `_thickness` (`import_usd.py:2104, 2111`) | | `ArticulationRootBaseCfg` | `PhysxArticulationAPI` | `physxArticulation` | `articulation_enabled` | PhysX honors at sim time; IsaacLab Newton wrapper reads it as a spawn-time guard at `rigid_object.py:1035`. Universal user-facing intent | | `JointDriveBaseCfg` | `PhysxJointAPI` | `physxJoint` | `max_joint_velocity` | Sole USD path to `Model.joint_velocity_limit` in Newton (no `newton:*` equivalent today). The exception namespace switches transparently when Newton ships `newton:maxJointVelocity` as a registered applied API | When any exception field is non-None, the corresponding `Physx*API` schema is applied to the prim. When all exception fields are None, no PhysX schema is stamped — Newton-targeted prims authored from `*BaseCfg` stay free of PhysX schemas they didn't opt in to. ## Field renames (with deprecation aliases) To enforce the convention that python `snake_case` cfg field names map identity-style to USD `camelCase` attribute names, two legacy fields were renamed. Both keep the old name as a deprecation alias forwarded via `__post_init__` (emits `DeprecationWarning`, scheduled for removal in 5.0). | Old name | New name | USD attribute | |---|---|---| | `JointDriveBaseCfg.max_velocity` | `max_joint_velocity` | `physxJoint:maxJointVelocity` | | `JointDriveBaseCfg.max_effort` | `max_force` | `drive:<axis>:physics:maxForce` | ## Type of change - New feature (non-breaking change which adds functionality) - Breaking change (existing functionality will not work without user modification) The split is non-breaking at the spawner-cfg level — every base-class type accepts any subclass via polymorphism, and every legacy `RigidBodyPropertiesCfg` / `JointDrivePropertiesCfg` / `CollisionPropertiesCfg` / `ArticulationRootPropertiesCfg` / `MeshCollisionPropertiesCfg` / `RigidBodyMaterialCfg` / `FixedTendonPropertiesCfg` / `SpatialTendonPropertiesCfg` import path continues to work via deprecation-alias subclasses and `__getattr__` shims on `isaaclab.sim`, `isaaclab.sim.schemas`, and `isaaclab.sim.schemas.schemas_cfg`. Direct attribute access to the renamed fields still works through deprecation aliases. Removal scheduled for 5.0. The breaking aspect: cfg classes in `isaaclab_physx.sim.schemas` and `isaaclab_physx.sim.spawners.materials` are physically relocated. Anyone importing from internal paths (rather than `isaaclab.sim`) needs to update. ## Migration ```python # Before import isaaclab.sim as sim_utils rigid_props = sim_utils.RigidBodyPropertiesCfg(disable_gravity=True, linear_damping=0.1) joint_props = sim_utils.JointDrivePropertiesCfg(max_effort=80.0, max_velocity=5.0) collision_props = sim_utils.CollisionPropertiesCfg(contact_offset=0.02, torsional_patch_radius=1.0) material = sim_utils.RigidBodyMaterialCfg(static_friction=0.7, compliant_contact_stiffness=1000.0) # After (PhysX-targeted) import isaaclab.sim as sim_utils from isaaclab_physx.sim.schemas import ( PhysxRigidBodyPropertiesCfg, PhysxJointDrivePropertiesCfg, PhysxCollisionPropertiesCfg, ) from isaaclab_physx.sim.spawners.materials import PhysxRigidBodyMaterialCfg rigid_props = PhysxRigidBodyPropertiesCfg(disable_gravity=True, linear_damping=0.1) joint_props = PhysxJointDrivePropertiesCfg(max_force=80.0, max_joint_velocity=5.0) collision_props = PhysxCollisionPropertiesCfg(contact_offset=0.02, torsional_patch_radius=1.0) material = PhysxRigidBodyMaterialCfg(static_friction=0.7, compliant_contact_stiffness=1000.0) # After (Newton-targeted — base classes only, no PhysX schemas applied) from isaaclab.sim.schemas import RigidBodyBaseCfg, JointDriveBaseCfg, CollisionBaseCfg from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg rigid_props = RigidBodyBaseCfg(disable_gravity=True) # only base + exception fields available joint_props = JointDriveBaseCfg(max_force=80.0, max_joint_velocity=5.0) material = RigidBodyMaterialBaseCfg(static_friction=0.7) ``` Spawner type annotations remain unchanged — they accept any subclass via polymorphism. ## Internal helper ```python def _apply_namespaced_schemas(prim, cfg, cfg_dict): # 1. Per-field exceptions: pop listed fields, apply exception schema if any non-None, # write under exception namespace. # 2. Per-declaring-class routing: walk MRO to find each remaining field's owner class; # write under that class's _usd_namespace; apply that class's _usd_applied_schema. ``` Used by all five `modify_*_properties` writers and `spawn_rigid_body_material`. Replaced ~125 lines of duplicated gating logic with a single ~30-line helper. ## Side change: configclass `source/isaaclab/isaaclab/utils/configclass.py:_process_mutable_types` now detects string-form `ClassVar` annotations under PEP 563 (`from __future__ import annotations`) so it doesn't wrap `ClassVar[dict]` defaults in `field(default_factory=...)`. Matches Python stdlib `dataclasses` semantics. No pre-existing IsaacLab class used `ClassVar` inside a `@configclass` block, so the change has no effect on existing code; it enables the `ClassVar` metadata pattern this PR introduces. ## Test plan - [x] `test_schemas.py` (38 → 40 tests): all schema-cfg classes write correct attributes under the right namespace; PhysX schemas are NOT applied when only base/UsdPhysics fields are set; deprecation aliases (`max_velocity` → `max_joint_velocity`, `max_effort` → `max_force`) forward correctly and emit `DeprecationWarning`. **40 passed.** - [x] `test_schemas_shim.py`: legacy import paths (`isaaclab.sim.schemas.RigidBodyPropertiesCfg` etc.) resolve via `__getattr__` shims. **All passing.** - [x] `test_articulation.py`, `test_rigid_object_iface.py`, `test_valid_configs.py`, `test_spawn_*` — no regressions. - [x] Full suite (`./isaaclab.sh -t`): 8768/9205 pass, 437 unrelated baseline failures (rendering, `omni.physics.tensors.api` missing, OSC controller, `install_ci`, `pyglet`, Newton env-path, Anymal-C determinism). Zero new regressions; +123 passing tests vs. earlier state. - [x] `./isaaclab.sh -f` (pre-commit) clean. ## Supersedes Together with #5276, supersedes #4847 and #5203 with a cleaner schema-layer design. ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation (changelog fragments under `source/isaaclab/changelog.d/`) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog (fragment-based system) and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: ooctipus <zhengyuz@nvidia.com>
Adds MuJoCo (Newton MuJoCo kernel) and Newton-native schema cfg classes to isaaclab_newton.sim.schemas using the framework from isaac-sim#5275: * MujocoRigidBodyPropertiesCfg (gravcomp -> mjc:gravcomp) * MujocoJointDrivePropertiesCfg (actuatorgravcomp -> mjc:actuatorgravcomp via MjcJointAPI) * NewtonCollisionPropertiesCfg (contact_margin, contact_gap via NewtonCollisionAPI) * NewtonMeshCollisionPropertiesCfg (max_hull_vertices via NewtonMeshCollisionAPI) * NewtonMaterialPropertiesCfg (torsional_friction, rolling_friction via NewtonMaterialAPI) * NewtonArticulationRootPropertiesCfg (self_collision_enabled via NewtonArticulationRootAPI) All cfgs follow the per-declaring-class MRO routing from isaac-sim#5275 with ClassVar metadata. Field names follow the snake_case = camelCase convention; old names (gravity_compensation_scale, gravity_compensation) kept as deprecation aliases forwarded via __post_init__. Forwarding shims on isaaclab.sim.schemas preserve existing imports. Spawner auto-enables body-level gravcomp when joint-level actuatorgravcomp is requested without it.
34adc18 to
4c9a192
Compare
Mujoco is one of Newton's solver options (alongside MJWarp, XPBD, Featherstone, etc.), so the cfg class hierarchy now reflects that: * NewtonRigidBodyPropertiesCfg(RigidBodyBaseCfg) — empty Newton-targeted base * NewtonJointDrivePropertiesCfg(JointDriveBaseCfg) — empty Newton-targeted base * MujocoRigidBodyPropertiesCfg(NewtonRigidBodyPropertiesCfg) — gravcomp * MujocoJointDrivePropertiesCfg(NewtonJointDrivePropertiesCfg) — actuatorgravcomp The empty Newton bases reserve the newton:* namespace for future Newton-native fields (Newton has no rigid-body or joint-drive applied schemas today, so the classes are placeholders). Per-declaring-class MRO routing in _apply_namespaced_schemas keeps each field in its own USD namespace regardless of which subclass is instantiated: * gravcomp (declared on Mujoco subclass) -> mjc:gravcomp * future Newton-native field (declared on Newton parent) -> newton:* * inherited base fields -> physics:* isinstance(cfg, NewtonRigidBodyPropertiesCfg) is now true for both Newton-native and Mujoco-targeted cfgs, useful for spawner-side polymorphic dispatch.
These field names only existed on the unmerged 5276 PR — they were never released on develop, so no deprecation cycle is needed. Renaming to gravcomp and actuatorgravcomp directly without aliases. Removed the aliases, the __post_init__ forwarding, and the alias tests in the core test file.
| if cfg.joint_drive_props is not None: | ||
| # auto-enable body-level gravcomp if joint-level actuator gravcomp is requested | ||
| # without it — actuatorgravcomp has no effect since there are no forces to route | ||
| from isaaclab_newton.sim.schemas.schemas_cfg import MujocoJointDrivePropertiesCfg, MujocoRigidBodyPropertiesCfg |
There was a problem hiding this comment.
Unconditional
isaaclab_newton import breaks non-MuJoCo spawning without the extension
The import on this line is placed unconditionally inside if cfg.joint_drive_props is not None:, so it executes for every call that sets any joint_drive_props — including plain PhysxJointDrivePropertiesCfg. If isaaclab_newton is not installed, every such spawn will raise an ImportError, silently regressing the core PhysX path for anyone who doesn't have the Newton extension.
The rest of the PR goes to great lengths to keep all Newton imports lazy; this one location undoes that guarantee. Wrapping the import in try/except ImportError (or moving it inside the inner if isinstance(...) condition) restores the intended isolation.
| if cfg.joint_drive_props is not None: | ||
| # auto-enable body-level gravcomp if joint-level actuator gravcomp is requested | ||
| # without it — actuatorgravcomp has no effect since there are no forces to route | ||
| from isaaclab_newton.sim.schemas.schemas_cfg import MujocoJointDrivePropertiesCfg, MujocoRigidBodyPropertiesCfg |
There was a problem hiding this comment.
Unconditional
isaaclab_newton import breaks non-Newton joint-drive users
The import on this line executes unconditionally whenever cfg.joint_drive_props is not None, regardless of whether the cfg is a MuJoCo type. Any user of isaaclab (without isaaclab_newton) who sets joint_drive_props to a PhysX or base config will receive an ImportError at spawn time — a regression from the pre-PR behaviour. The import must be guarded with try/except ImportError so it's a no-op when isaaclab_newton is absent.
| if ( | ||
| isinstance(cfg.joint_drive_props, MujocoJointDrivePropertiesCfg) | ||
| and cfg.joint_drive_props.actuatorgravcomp | ||
| and not isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg) | ||
| ): | ||
| logger.info( | ||
| "Joint-level actuator gravity compensation requires body-level gravcomp." | ||
| " Auto-setting rigid_props to MujocoRigidBodyPropertiesCfg(gravcomp=1.0)." | ||
| ) | ||
| schemas.modify_rigid_body_properties(prim_path, MujocoRigidBodyPropertiesCfg(gravcomp=1.0)) |
There was a problem hiding this comment.
Auto-enable guard misses
MujocoRigidBodyPropertiesCfg with gravcomp=None
The condition not isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg) evaluates to False (skipping the auto-apply) even when cfg.rigid_props = MujocoRigidBodyPropertiesCfg() — i.e. gravcomp is still None. Body-level compensation won't be active, but no warning is logged and no auto-apply fires. The check should be not (isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg) and cfg.rigid_props.gravcomp is not None) to match the intent stated in the inline comment.
* Remove four MuJoCo gravcomp tests from source/isaaclab/test/sim/test_schemas.py
that had stale docstrings (referencing the dropped gravity_compensation_scale/
gravity_compensation kwargs) and duplicated coverage already in
test_newton_schemas.py.
* Add four new tests in source/isaaclab_newton/test/sim/test_newton_schemas.py:
- test_newton_mesh_collision_max_hull_vertices_written: covers
NewtonMeshCollisionPropertiesCfg writing newton:maxHullVertices and
applying NewtonMeshCollisionAPI.
- test_newton_mesh_collision_no_schema_when_none: confirms gating works.
- test_mujoco_isinstance_newton: locks in the class-hierarchy contract that
spawner auto-enable depends on (MujocoXxxCfg IS-A NewtonXxxCfg).
- test_newton_mesh_collision_mixed_namespace_write: verifies per-declaring-
class MRO routing on a single instance with fields owned by both
NewtonCollisionPropertiesCfg (contact_margin) and
NewtonMeshCollisionPropertiesCfg (max_hull_vertices).
* Add new core-concepts page `schema_cfgs.rst` explaining the base/subclass hierarchy, when to use each tier (base / Physx / Newton / Mujoco), what parameters live where, mixed-namespace authoring, and the spawner pattern. * Add migration entry to `migrating_to_isaaclab_3-0.rst` covering the class moves (RigidBodyPropertiesCfg -> RigidBodyBaseCfg + PhysxRigidBodyPropertiesCfg etc.), the field renames (max_velocity -> max_joint_velocity, max_effort -> max_force), and the new Newton/Mujoco cfg classes. * Update `isaaclab.sim.schemas` API doc to expose the new base classes (RigidBodyBaseCfg, JointDriveBaseCfg, CollisionBaseCfg, ArticulationRootBaseCfg, MeshCollisionBaseCfg) with cross-references to the PhysX and Newton subclasses. * Update `isaaclab_physx.sim.schemas` API doc with the full PhysX subclass inventory (rigid body, joint drive, collision, articulation root, mesh-cooking family, tendon, deformable body). * Add new `isaaclab_newton.sim.schemas` API doc listing the Newton family roots (Newton*PropertiesCfg) and MuJoCo solver-specific subclasses (Mujoco*Cfg). * Wire the new Newton schemas API doc into `api/index.rst`. Sphinx build clean (no new warnings introduced).
* Add 'Quick example' section near top of schema_cfgs.rst so the most common
use case (MuJoCo gravity compensation) appears within the first screen.
* Fix hierarchy diagram: NewtonMeshCollisionPropertiesCfg uses multiple
inheritance (NewtonCollisionPropertiesCfg + MeshCollisionBaseCfg); annotate
in the tree and add a paragraph framing it as the textbook MRO routing case.
* Recategorize the 'Universal physics' table heading (was misleading because
some base-class fields like disable_gravity route to physx*: namespaces via
_usd_field_exceptions). Now titled 'Universal physics (declared on the base
class)' with a clarifying lead paragraph.
* Add ensure_drives_exist row to the base-class table.
* Add :ref: anchor on Mixed-namespace section (schema-cfgs-mixed) for in-page
links from earlier sections.
* Add dedicated 'Gravity compensation (MuJoCo solver)' subsection
(schema-cfgs-gravcomp anchor) explaining body- vs joint-level halves and
documenting the spawner's auto-enable behavior.
* Naming convention section now correctly describes the rename mechanism: old
names remain as real dataclass fields (visible to dataclasses.fields()) and
forward via __post_init__; documents the both-set collision behavior
(canonical wins, alias value discarded).
* Migration doc: add :ref: anchor (schemas-cfg-refactor) so other pages can
link in. Fix the rename-section's misdescription of the alias mechanism.
Fix the migration code example: the '3.0' line was using the deprecated
JointDrivePropertiesCfg alias as if it were the recommended name; now uses
JointDriveBaseCfg or PhysxJointDrivePropertiesCfg.
* Migration doc: add the auto-enable foot-gun note with ref into the explainer.
* Drop the abstract RigidObjectSpawnerCfg from the spawner-list ('used by
abstract base, not instantiated by users').
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| from .schemas_cfg import ( | ||
| MujocoJointDrivePropertiesCfg as MujocoJointDrivePropertiesCfg, |
There was a problem hiding this comment.
is the rhs of as aliasing exactly the same as lhs?
| "Joint-level actuator gravity compensation requires body-level gravcomp." | ||
| " Auto-setting rigid_props to MujocoRigidBodyPropertiesCfg(gravcomp=1.0)." | ||
| ) | ||
| schemas.modify_rigid_body_properties(prim_path, MujocoRigidBodyPropertiesCfg(gravcomp=1.0)) |
There was a problem hiding this comment.
hmmmm maybe only override to 1.0 if the default is None?, what if I want gravcomp=0.5?
There was a problem hiding this comment.
body_gravcomp_not_exist_in_rigid_object = (
not isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg)
or cfg.rigid_props.gravcomp is None
)
if (
isinstance(cfg.joint_drive_props, MujocoJointDrivePropertiesCfg)
and cfg.joint_drive_props.actuatorgravcomp
and body_gravcomp_not_exist_in_rigid_object
):
schemas.modify_rigid_body_properties(
prim_path,
MujocoRigidBodyPropertiesCfg(gravcomp=1.0),
)
|
Base schema cfg already has drive properties, but some are missing: Missing MuJoCo also provides more. From newton/newton/_src/usd/schemas.py:284, MuJoCo resolver has: Are you planning to include them ? if yes is it in follow up pr? |
There was a problem hiding this comment.
Review Summary
Excellent addition that fills an important gap in the multi-backend schema story. The per-declaring-class MRO routing design from #5275 pays off here — each Newton/MuJoCo cfg cleanly declares its namespace and schema without any backend branching in the writer code.
Strengths
- Clean class hierarchy:
MujocoRigidBodyPropertiesCfg → NewtonRigidBodyPropertiesCfg → RigidBodyBaseCfgis exactly right for the solver-within-backend relationship - Lazy forwarding shims:
isaaclab.sim.schemasdoesn't hard-depend onisaaclab_newton— great for users who only have PhysX installed - Auto-enable gravcomp: The spawner auto-enabling body-level
gravcompwhen joint-levelactuatorgravcompis set prevents a footgun (joint-level alone is a no-op) - Documentation: The
schema_cfgs.rstconcept page is outstanding — clear hierarchy diagram, when-to-use-which guidance, mixed-namespace explanation, and the gravity compensation section anticipates exactly the question users will have - Test coverage: Every cfg tested for both "written when set" and "not written when None" — correct approach for optional schema authoring
- Migration guide: Comprehensive tables in
migrating_to_isaaclab_3-0.rstwith before/after code examples
Concerns
See inline comments.
| # note: these are only for setting low-level simulation properties. all others should be set or are | ||
| # and overridden by the articulation/actuator properties. | ||
| if cfg.joint_drive_props is not None: | ||
| # auto-enable body-level gravcomp if joint-level actuator gravcomp is requested |
There was a problem hiding this comment.
🔴 Hard import of isaaclab_newton inside core isaaclab: This imports MujocoJointDrivePropertiesCfg and MujocoRigidBodyPropertiesCfg directly from isaaclab_newton inside a spawner function in core isaaclab. This creates a runtime dependency of isaaclab on isaaclab_newton — if someone uses a MuJoCo joint drive cfg, this code path will fail if isaaclab_newton isn't installed.
The lazy __getattr__ shims in __init__.py were specifically designed to avoid this. This should use the same pattern or guard with a try/except:
| # auto-enable body-level gravcomp if joint-level actuator gravcomp is requested | |
| # auto-enable body-level gravcomp if joint-level actuator gravcomp is requested | |
| # without it — actuatorgravcomp has no effect since there are no forces to route | |
| try: | |
| from isaaclab_newton.sim.schemas.schemas_cfg import MujocoJointDrivePropertiesCfg, MujocoRigidBodyPropertiesCfg | |
| except ImportError: | |
| MujocoJointDrivePropertiesCfg = type(None) # type: ignore[misc,assignment] | |
| MujocoRigidBodyPropertiesCfg = type(None) # type: ignore[misc,assignment] |
Alternatively, this logic could use duck-typing: check for hasattr(cfg.joint_drive_props, 'actuatorgravcomp') and hasattr(cfg.rigid_props, 'gravcomp') instead of isinstance checks.
| and not isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg) | ||
| ): | ||
| logger.info( | ||
| "Joint-level actuator gravity compensation requires body-level gravcomp." |
There was a problem hiding this comment.
🟡 modify_rigid_body_properties called before the cfg's own rigid_props: The auto-enable calls modify_rigid_body_properties(prim_path, MujocoRigidBodyPropertiesCfg(gravcomp=1.0)) here, but the existing rigid_props from the cfg was already applied earlier in the function (around line 342). This means:
- If the user passed
rigid_props=RigidBodyBaseCfg(disable_gravity=True), those properties are already on the prim. - The auto-enable then calls
modify_rigid_body_propertiesagain with a freshMujocoRigidBodyPropertiesCfg(gravcomp=1.0)— this will writemjc:gravcompbut might also re-apply base-class fields with their defaults (allNone, so probably fine since the writer skipsNonevalues).
Is this ordering intentional? The auto-enable won't clobber existing base fields (since they're None on the auto-generated cfg), but it's worth adding a comment confirming this expectation.
| """ | ||
|
|
||
| _usd_namespace: ClassVar[str | None] = "newton" | ||
| _usd_applied_schema: ClassVar[str | None] = "NewtonCollisionAPI" |
There was a problem hiding this comment.
🟡 NewtonMeshCollisionPropertiesCfg multiple inheritance: This class inherits from both NewtonCollisionPropertiesCfg and MeshCollisionBaseCfg. While the MRO routing handles field-to-namespace mapping correctly, Python's dataclass (and configclass) MRO can be tricky with diamond inheritance if both parents share a common grandparent.
Does NewtonCollisionPropertiesCfg → CollisionBaseCfg share any fields with MeshCollisionBaseCfg? If MeshCollisionBaseCfg doesn't inherit from CollisionBaseCfg, this is fine. But if it does (even transitionally), configclass field resolution order could produce surprising behavior.
A quick MujocoJointDrivePropertiesCfg.__mro__ printout in tests would be a good sanity check to document the resolution order.
|
|
||
| See :meth:`~isaaclab.sim.schemas.modify_rigid_body_properties` for more information. | ||
| """ | ||
|
|
There was a problem hiding this comment.
💡 Empty base classes as namespace reservations: NewtonRigidBodyPropertiesCfg and NewtonJointDrivePropertiesCfg are currently empty — they exist to reserve the newton: namespace and provide a type for isinstance checks. This is a good pattern, but it means users who isinstance(cfg, NewtonRigidBodyPropertiesCfg) are testing "this cfg targets Newton" rather than "this cfg has Newton-specific fields".
Consider adding a brief class-level note about this intended usage:
# Use isinstance(cfg, NewtonRigidBodyPropertiesCfg) to check whether a cfg
# targets the Newton backend (including MuJoCo solver cfgs).| @@ -44,6 +44,19 @@ | |||
| } | |||
There was a problem hiding this comment.
💡 DRY: _NEWTON_FORWARDS defined in three places: The same frozenset of Newton class names appears in isaaclab.sim.__init__.py, isaaclab.sim.schemas.__init__.py, and isaaclab.sim.schemas.schemas_cfg.py. If a new Newton cfg is added, all three must be updated.
Consider defining it once (e.g., in isaaclab.sim.schemas.schemas_cfg) and importing from there:
# In isaaclab.sim.schemas.__init__.py:
from .schemas_cfg import _NEWTON_FORWARDSThis would centralize the vocabulary. Minor suggestion — the pattern is already established by the PhysX forwards, so consistency with existing code is also valid.
| @@ -0,0 +1,371 @@ | |||
| .. _schema-cfgs: | |||
There was a problem hiding this comment.
💡 Outstanding documentation: This concept page is one of the best-structured docs in the project. The hierarchy diagram, when-to-use decision matrix, mixed-namespace explanation, and gravity compensation deep-dive are all exactly what a user needs. The tables mapping fields → USD attributes → namespaces are particularly valuable for debugging.
One small addition that would help: a "Quick Reference" at the top showing which import to use for the most common scenarios:
.. code-block:: python
# Backend-portable (any solver):
from isaaclab.sim.schemas import RigidBodyBaseCfg
# PhysX-targeted:
from isaaclab_physx.sim.schemas import PhysxRigidBodyPropertiesCfg
# Newton MuJoCo-targeted:
from isaaclab_newton.sim.schemas import MujocoRigidBodyPropertiesCfg
AntoineRichard
left a comment
There was a problem hiding this comment.
In general I think it's pretty good considering our timeline I would be in favor of getting this in in its current state with a couple of small changes. (see the imports) But we'd need to severly rework the doc to avoid maintenance issues in the future.
There was a problem hiding this comment.
Shouldn't that be in a separate PR?
There was a problem hiding this comment.
Same here, could be in another PR?
There was a problem hiding this comment.
Could be in another PR?
There was a problem hiding this comment.
Could be in another PR?
| spawner layer for solver-specific knobs (PhysX TGS iterations, MuJoCo gravity | ||
| compensation, etc.). | ||
|
|
There was a problem hiding this comment.
I think what's ambiguous, and that's not on you @vidurv-nvidia, is that PhysX TGS iterations, is really not in the same class as MuJoCo grav comp. PhysX TGS iterations should very likely live inside a solver CFG similar to the newton solvers.
There was a problem hiding this comment.
Could you not mention PhysX TGS iterations here?
| - ``actuatorgravcomp`` | ||
| - ``mjc:actuatorgravcomp`` via ``MjcJointAPI`` |
There was a problem hiding this comment.
What's the difference?
| Gravity compensation (MuJoCo solver) | ||
| """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | ||
|
|
||
| Gravity compensation has two halves and you typically need both: | ||
|
|
||
| * **Body-level**: | ||
| :attr:`~isaaclab_newton.sim.schemas.MujocoRigidBodyPropertiesCfg.gravcomp` | ||
| on each rigid body (writes ``mjc:gravcomp``). This is what *computes* the | ||
| compensation force. | ||
| * **Joint-level**: | ||
| :attr:`~isaaclab_newton.sim.schemas.MujocoJointDrivePropertiesCfg.actuatorgravcomp` | ||
| on each joint (writes ``mjc:actuatorgravcomp``). This routes the compensation | ||
| force through the actuator channel (``qfrc_actuator``) so it counts against | ||
| ``actuatorfrcrange``; otherwise it goes to ``qfrc_passive``. | ||
|
|
||
| ``actuatorgravcomp=True`` alone is a no-op — without body-level ``gravcomp`` | ||
| there are no forces to route. To prevent this footgun, the spawner | ||
| **auto-enables** ``MujocoRigidBodyPropertiesCfg(gravcomp=1.0)`` whenever | ||
| ``joint_drive_props`` is a Mujoco cfg with ``actuatorgravcomp=True`` and | ||
| ``rigid_props`` is not already a Mujoco cfg. If you want a different | ||
| ``gravcomp`` value (or want to disable the auto-enable), pass an explicit | ||
| ``MujocoRigidBodyPropertiesCfg`` in ``rigid_props``. |
There was a problem hiding this comment.
So we need this here? Can this be in the code and directly collected using auto-doc?
| @@ -79,8 +93,18 @@ def __getattr__(name): | |||
| " shim is scheduled for removal in 5.0." | |||
| raise ImportError( | ||
| f"'isaaclab.sim.schemas.{name}' has moved to 'isaaclab_newton.sim.schemas'." | ||
| " Install the isaaclab_newton extension or update your import. This forwarding" | ||
| " shim is scheduled for removal in 5.0." |
| from .schemas_cfg import ( | ||
| MujocoJointDrivePropertiesCfg as MujocoJointDrivePropertiesCfg, | ||
| MujocoRigidBodyPropertiesCfg as MujocoRigidBodyPropertiesCfg, | ||
| NewtonArticulationRootPropertiesCfg as NewtonArticulationRootPropertiesCfg, | ||
| NewtonCollisionPropertiesCfg as NewtonCollisionPropertiesCfg, | ||
| NewtonJointDrivePropertiesCfg as NewtonJointDrivePropertiesCfg, | ||
| NewtonMaterialPropertiesCfg as NewtonMaterialPropertiesCfg, | ||
| NewtonMeshCollisionPropertiesCfg as NewtonMeshCollisionPropertiesCfg, | ||
| NewtonRigidBodyPropertiesCfg as NewtonRigidBodyPropertiesCfg, |
There was a problem hiding this comment.
YEah that weird why not do:
| from .schemas_cfg import ( | |
| MujocoJointDrivePropertiesCfg as MujocoJointDrivePropertiesCfg, | |
| MujocoRigidBodyPropertiesCfg as MujocoRigidBodyPropertiesCfg, | |
| NewtonArticulationRootPropertiesCfg as NewtonArticulationRootPropertiesCfg, | |
| NewtonCollisionPropertiesCfg as NewtonCollisionPropertiesCfg, | |
| NewtonJointDrivePropertiesCfg as NewtonJointDrivePropertiesCfg, | |
| NewtonMaterialPropertiesCfg as NewtonMaterialPropertiesCfg, | |
| NewtonMeshCollisionPropertiesCfg as NewtonMeshCollisionPropertiesCfg, | |
| NewtonRigidBodyPropertiesCfg as NewtonRigidBodyPropertiesCfg, | |
| from .schemas_cfg import ( | |
| MujocoJointDrivePropertiesCfg, | |
| MujocoRigidBodyPropertiesCfg, | |
| NewtonArticulationRootPropertiesCfg, | |
| NewtonCollisionPropertiesCfg, | |
| NewtonJointDrivePropertiesCfg, | |
| NewtonMaterialPropertiesCfg, | |
| NewtonMeshCollisionPropertiesCfg, | |
| NewtonRigidBodyPropertiesCfg, |
Code: * Fix spawner gravcomp auto-enable to only fire when user did not set their own gravcomp value (Octi). Previously stomped explicit MujocoRigidBodyPropertiesCfg(gravcomp=0.5) with gravcomp=1.0. * Drop X as X aliasing pattern from isaaclab_newton schemas pyi stub to match isaaclab_physx convention (Octi + Antoine). * Bump deprecation removal target from 5.0 to 4.0 across all shim ImportError messages, _deprecate_field_alias DeprecationWarning, and cfg-field deprecation docstrings (Antoine). Docs: * Drop the 'PhysX TGS iterations' example from the migration intro — TGS iterations belong in a solver cfg, not the same conceptual bucket as MuJoCo gravcomp (Antoine). * Split combined code blocks in the migration doc into separate per-use-case code blocks (2.x style / 3.0 backend-portable / 3.0 PhysX-targeted) for readability (Antoine). * Add admonition to schema_cfgs.rst pointing to the auto-generated API docs as the canonical source for field listings — the explainer tables remain manual but are now positioned as a navigation aid rather than the source of truth (Antoine). * Clarify the 'raw attribute vs MjcJointAPI' difference between the two MuJoCo cfg rows with a note explaining that mjc:gravcomp has no registered applied schema while mjc:actuatorgravcomp is part of MjcJointAPI (Antoine).
Description
Adds Newton-native and MuJoCo-specific schema cfg classes to
isaaclab_newton.sim.schemas,following the base/subclass framework from #5275. All new cfgs use the per-declaring-class
MRO routing in
_apply_namespaced_schemas— no backend-specific branching in any writer.Depends on #5275.
New cfgs
MuJoCo (Newton MuJoCo kernel,
mjc:*namespace)MujocoRigidBodyPropertiesCfggravcompmjc:gravcompMujocoJointDrivePropertiesCfgactuatorgravcompmjc:actuatorgravcompMjcJointAPIBody-level
gravcompmust be set for joint-levelactuatorgravcompto have any effect.The spawner auto-enables
MujocoRigidBodyPropertiesCfg(gravcomp=1.0)when joint-levelactuator gravcomp is requested without body-level gravcomp.
Newton-native (
newton:*namespace)NewtonCollisionPropertiesCfgcontact_margin,contact_gapnewton:contactMargin,newton:contactGapNewtonCollisionAPINewtonMeshCollisionPropertiesCfgmax_hull_verticesnewton:maxHullVerticesNewtonMeshCollisionAPINewtonMaterialPropertiesCfgtorsional_friction,rolling_frictionnewton:torsionalFriction,newton:rollingFrictionNewtonMaterialAPINewtonArticulationRootPropertiesCfgself_collision_enablednewton:selfCollisionEnabledNewtonArticulationRootAPIDesign constraints
Same single-cfg-per-spawner-slot rule as #5275. Newton cfgs subclass the same base classes
as PhysX cfgs; each declares
_usd_namespace/_usd_applied_schema(ClassVar) and fieldsthat auto-camelCase to their USD attr names. Per-declaring-class MRO routing handles mixed
PhysX+Newton cfg hierarchies correctly.
Field renames (with deprecation aliases through 5.0)
gravity_compensation_scalegravcompgravcomp→mjc:gravcompgravity_compensationactuatorgravcompactuatorgravcomp→mjc:actuatorgravcompType of change
Forwarding shims on
isaaclab.sim.schemaskeep existing imports working.Deprecation aliases keep old field names working through 5.0.
Test plan
mjc:gravcomp/mjc:actuatorgravcompwritten when set, not written when Nonetest_schemas.py46/46 pass — no regressionsSupersedes
Together with #5275, supersedes #4847 and #5203.