Skip to content

Commit 924e407

Browse files
author
NeoZng
committed
Fix MuJoCo USD resolver selection
Select SchemaResolverMjc from the SolverMuJoCo attributes registered by the active manager, including fixed and runtime-configured coupled managers. Reuse the policy for standalone and clone imports while keeping visualization and ordering on the fixed resolver pair. Add production-path, coupled-manager, and conflicting three-schema regression coverage.
1 parent 7bf6959 commit 924e407

10 files changed

Lines changed: 266 additions & 6 deletions

File tree

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Guidelines for modifications:
153153
* Muhong Guo
154154
* Narendra Dahile
155155
* Neel Anand Jawale
156+
* NeoZng
156157
* Nicola Loi
157158
* Nicholas Blauch
158159
* Nicolas Moenne-Loccoz
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed couplers with nested MJWarp solvers dropping ``mjc:frictionloss`` during
5+
USD stage imports.

source/isaaclab_contrib/isaaclab_contrib/coupling/coupler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from isaaclab_newton.physics.newton_manager import NewtonManager
2424
from isaaclab_newton.physics.vbd_manager import NewtonVBDManager
2525
from newton import CollisionPipeline, Model, ModelBuilder, ShapeFlags
26+
from newton.solvers import SolverBase
2627
from newton.solvers.experimental.coupled import SolverCoupled, SolverCoupledADMM, SolverCoupledProxy
2728

2829
from isaaclab.physics import PhysicsManager
@@ -191,6 +192,14 @@ def _register_builder_attributes(cls, builder: ModelBuilder) -> None:
191192
for entry in PhysicsManager._cfg.solver_cfg.entries:
192193
entry.solver_cfg.class_type._register_builder_attributes(builder)
193194

195+
@classmethod
196+
def _registers_builder_attributes_from_solver(cls, solver_cls: type[SolverBase]) -> bool:
197+
"""Return whether the manager or a configured nested entry registers ``solver_cls`` attributes."""
198+
return super()._registers_builder_attributes_from_solver(solver_cls) or any(
199+
entry.solver_cfg.class_type._registers_builder_attributes_from_solver(solver_cls)
200+
for entry in PhysicsManager._cfg.solver_cfg.entries
201+
)
202+
194203
@classmethod
195204
def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None:
196205
"""Normalize kinematic colliders when a coupled entry uses implicit MPM."""

source/isaaclab_contrib/test/coupling/test_coupler.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
from dataclasses import dataclass, field
1818
from types import SimpleNamespace
1919

20+
import isaaclab_newton.physics.newton_manager as newton_manager_module
2021
import numpy as np
2122
import pytest
2223
from isaaclab_newton.physics import (
2324
FeatherstoneSolverCfg,
2425
KaminoPADMMSolverCfg,
2526
MJWarpSolverCfg,
2627
MPMSolverCfg,
28+
NewtonCfg,
2729
NewtonCollisionPipelineCfg,
2830
NewtonVBDManager,
2931
VBDSolverCfg,
@@ -33,6 +35,8 @@
3335
from newton import ModelBuilder, ShapeFlags
3436
from newton.solvers.experimental.coupled import SolverCoupledADMM, SolverCoupledProxy
3537

38+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
39+
3640
from isaaclab_contrib.coupling import (
3741
CouplerAdmmCfg,
3842
CouplerCfg,
@@ -596,6 +600,52 @@ def test_nested_solvers_register_their_builder_attributes(monkeypatch):
596600
assert builder.has_custom_attribute("mpm:young_modulus")
597601

598602

603+
@pytest.mark.parametrize(
604+
("entry_solver_cfg", "expected_friction", "expected_damping"),
605+
[
606+
pytest.param(MJWarpSolverCfg(), 0.11, 0.23, id="mjwarp"),
607+
pytest.param(XPBDSolverCfg(), 0.0, 0.0, id="xpbd"),
608+
],
609+
)
610+
def test_nested_solver_scopes_mujoco_joint_properties(
611+
monkeypatch, entry_solver_cfg, expected_friction, expected_damping
612+
):
613+
"""A coupler imports MuJoCo properties only when a nested solver consumes them."""
614+
solver_cfg = CouplerProxyCfg(entries=[CouplerEntryCfg(name="rigid", solver_cfg=entry_solver_cfg)])
615+
monkeypatch.setattr(coupler.PhysicsManager, "_cfg", NewtonCfg(solver_cfg=solver_cfg))
616+
617+
stage = Usd.Stage.CreateInMemory()
618+
UsdGeom.Xform.Define(stage, "/World")
619+
root_path = "/World/robot"
620+
root = UsdGeom.Cube.Define(stage, root_path).GetPrim()
621+
UsdPhysics.RigidBodyAPI.Apply(root)
622+
UsdPhysics.ArticulationRootAPI.Apply(root)
623+
child_path = f"{root_path}/child"
624+
child = UsdGeom.Cube.Define(stage, child_path).GetPrim()
625+
UsdPhysics.RigidBodyAPI.Apply(child)
626+
joint = UsdPhysics.RevoluteJoint.Define(stage, f"{child_path}/joint")
627+
joint.CreateAxisAttr().Set("Z")
628+
joint.CreateBody0Rel().SetTargets([root_path])
629+
joint.CreateBody1Rel().SetTargets([child_path])
630+
joint.GetPrim().CreateAttribute("mjc:frictionloss", Sdf.ValueTypeNames.Double, True).Set(0.11)
631+
joint.GetPrim().CreateAttribute("mjc:damping", Sdf.ValueTypeNames.Double, True).Set(0.23)
632+
633+
monkeypatch.setattr(newton_manager_module, "get_current_stage", lambda: stage)
634+
monkeypatch.setattr(newton_manager_module, "_restore_visible_colliders_without_visual_shapes", lambda *args: None)
635+
monkeypatch.setattr(newton_manager_module, "replace_newton_builder_shape_colors", lambda *args: None)
636+
monkeypatch.setattr(newton_manager_module, "import_builder_visual_material_paths", lambda *args: None)
637+
monkeypatch.setattr(NewtonManager, "_builder", None)
638+
monkeypatch.setattr(NewtonManager, "_deformable_registry", [])
639+
monkeypatch.setattr(NewtonManager, "_per_world_builder_hooks", [])
640+
641+
NewtonCouplerManager.instantiate_builder_from_stage()
642+
builder = NewtonManager._builder
643+
model = builder.finalize(device="cpu")
644+
645+
assert model.joint_friction.numpy()[-1] == pytest.approx(expected_friction)
646+
assert model.joint_damping.numpy()[-1] == pytest.approx(expected_damping)
647+
648+
599649
def test_contact_initialization_prepares_coupled_solver_buffers(monkeypatch):
600650
"""Entry-local contact buffers are allocated before graph capture."""
601651
events: list[tuple[str, object | None]] = []

source/isaaclab_contrib/test/custom_coupling/test_manager.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from isaaclab_newton.physics import MJWarpSolverCfg, VBDSolverCfg
1313
from newton import ModelBuilder
1414

15+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
16+
1517
import isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager as manager_module
1618
from isaaclab_contrib.custom_coupling.coupled_mjwarp_vbd_manager import NewtonCoupledMJWarpVBDManager
1719
from isaaclab_contrib.custom_coupling.newton_manager_cfg import CoupledMJWarpVBDSolverCfg
@@ -28,6 +30,35 @@ def test_register_builder_attributes_includes_nested_solvers(monkeypatch: pytest
2830
assert builder.has_custom_attribute("mujoco:condim")
2931

3032

33+
def test_registered_mujoco_solver_imports_mujoco_joint_properties(monkeypatch: pytest.MonkeyPatch) -> None:
34+
"""The coupled manager imports joint properties consumed by its MuJoCo solver."""
35+
cfg = CoupledMJWarpVBDSolverCfg()
36+
monkeypatch.setattr(manager_module.PhysicsManager, "_cfg", SimpleNamespace(solver_cfg=cfg))
37+
38+
stage = Usd.Stage.CreateInMemory()
39+
root_path = "/World/robot"
40+
root = UsdGeom.Cube.Define(stage, root_path).GetPrim()
41+
UsdPhysics.RigidBodyAPI.Apply(root)
42+
UsdPhysics.ArticulationRootAPI.Apply(root)
43+
child_path = f"{root_path}/child"
44+
child = UsdGeom.Cube.Define(stage, child_path).GetPrim()
45+
UsdPhysics.RigidBodyAPI.Apply(child)
46+
joint = UsdPhysics.RevoluteJoint.Define(stage, f"{child_path}/joint")
47+
joint.CreateAxisAttr().Set("Z")
48+
joint.CreateBody0Rel().SetTargets([root_path])
49+
joint.CreateBody1Rel().SetTargets([child_path])
50+
joint.GetPrim().CreateAttribute("mjc:frictionloss", Sdf.ValueTypeNames.Double, True).Set(0.11)
51+
joint.GetPrim().CreateAttribute("mjc:damping", Sdf.ValueTypeNames.Double, True).Set(0.23)
52+
53+
builder = ModelBuilder()
54+
NewtonCoupledMJWarpVBDManager._register_builder_attributes(builder)
55+
builder.add_usd(stage, schema_resolvers=NewtonCoupledMJWarpVBDManager._get_usd_import_schema_resolvers())
56+
model = builder.finalize(device="cpu")
57+
58+
assert model.joint_friction.numpy()[-1] == pytest.approx(0.11)
59+
assert model.joint_damping.numpy()[-1] == pytest.approx(0.23)
60+
61+
3162
def test_reset_forwards_to_both_subsolvers(monkeypatch: pytest.MonkeyPatch) -> None:
3263
"""Reset the real sub-solvers instead of the dummy solver slot."""
3364
rigid_solver = MagicMock()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed MuJoCo-based solver managers dropping ``mjc:frictionloss`` during USD
5+
stage imports.

source/isaaclab_newton/isaaclab_newton/cloner/replicate.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import numpy as np
1515
import warp as wp
1616
from newton import ModelBuilder
17-
from newton._src.usd.schemas import SchemaResolverNewton, SchemaResolverPhysx
1817

1918
from pxr import Usd
2019

@@ -110,8 +109,8 @@ def _build_newton_builder_from_mapping(
110109
quaternions = np.zeros((mapping.shape[1], 4), dtype=np.float32)
111110
quaternions[:, 3] = 1.0
112111

113-
schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()]
114112
manager_cls = PhysicsManager._sim.physics_manager
113+
schema_resolvers = manager_cls._get_usd_import_schema_resolvers()
115114

116115
builder = manager_cls.create_builder(up_axis=up_axis)
117116
import_paths = (PhysicsManager._sim.cfg.physics_prim_path, *global_paths)

source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def _paused_gc():
7575
from newton.sensors import SensorContact as NewtonContactSensor
7676
from newton.sensors import SensorFrameTransform
7777
from newton.sensors import SensorIMU as NewtonSensorIMU
78-
from newton.solvers import SolverBase, SolverKamino
79-
from newton.usd import SchemaResolverNewton, SchemaResolverPhysx
78+
from newton.solvers import SolverBase, SolverKamino, SolverMuJoCo
79+
from newton.usd import SchemaResolver, SchemaResolverMjc, SchemaResolverNewton, SchemaResolverPhysx
8080

8181
from pxr import Usd, UsdGeom
8282

@@ -1257,6 +1257,11 @@ def _register_builder_attributes(cls, builder: ModelBuilder) -> None:
12571257
for solver_cls in cls._builder_attribute_solvers:
12581258
solver_cls.register_custom_attributes(builder)
12591259

1260+
@classmethod
1261+
def _registers_builder_attributes_from_solver(cls, solver_cls: type[SolverBase]) -> bool:
1262+
"""Return whether this manager registers custom attributes from ``solver_cls``."""
1263+
return solver_cls in cls._builder_attribute_solvers
1264+
12601265
@classmethod
12611266
def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None:
12621267
"""Subclass hook to normalize *builder* before model finalization.
@@ -1914,6 +1919,24 @@ def _get_usd_import_ignore_paths(cls) -> list[str]:
19141919
"""Return solver-specific prim paths excluded from USD import."""
19151920
return []
19161921

1922+
@classmethod
1923+
def _get_usd_import_schema_resolvers(cls) -> list[SchemaResolver]:
1924+
"""Return the schema resolvers used to import the stage into Newton.
1925+
1926+
A manager that registers MuJoCo solver attributes also imports the MJC
1927+
schema that authors them. Resolver order defines precedence when
1928+
multiple schemas author the same Newton model property.
1929+
1930+
This policy is limited to physics-model imports. Visualization builders
1931+
and articulation-ordering probes intentionally keep the fixed Newton and
1932+
PhysX resolver pair because solver-specific attributes cannot affect
1933+
their rendering or ordering products.
1934+
"""
1935+
resolvers: list[SchemaResolver] = [SchemaResolverNewton(), SchemaResolverPhysx()]
1936+
if cls._registers_builder_attributes_from_solver(SolverMuJoCo):
1937+
resolvers.append(SchemaResolverMjc())
1938+
return resolvers
1939+
19171940
@classmethod
19181941
def instantiate_builder_from_stage(cls):
19191942
"""Create builder from USD stage.
@@ -1943,7 +1966,7 @@ def instantiate_builder_from_stage(cls):
19431966

19441967
builder = cls.create_builder(up_axis=up_axis)
19451968

1946-
schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()]
1969+
schema_resolvers = cls._get_usd_import_schema_resolvers()
19471970

19481971
# NOTE: None of the add_usd calls below pass joint_ordering or
19491972
# bodies_follow_joint_ordering, so the live articulation's native

source/isaaclab_newton/test/cloner/test_newton_builder_world_hook.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def test_explicit_global_import_uses_global_world(monkeypatch):
8080
add_usd = mock.Mock(wraps=builder.add_usd)
8181
monkeypatch.setattr(builder, "add_usd", add_usd)
8282
manager = SimpleNamespace(
83-
create_builder=mock.Mock(return_value=builder), _inject_terrain_heightfields=mock.Mock(return_value=[])
83+
create_builder=mock.Mock(return_value=builder),
84+
_get_usd_import_schema_resolvers=NewtonManager._get_usd_import_schema_resolvers,
85+
_inject_terrain_heightfields=mock.Mock(return_value=[]),
8486
)
8587
monkeypatch.setattr(
8688
replicate_module.PhysicsManager,

source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,141 @@ def test_active_manager_create_builder_registers_mpm_attributes():
714714
assert builder.has_custom_attribute("mpm:young_modulus")
715715

716716

717+
@pytest.mark.parametrize("import_path", ["clone", "standalone"])
718+
@pytest.mark.parametrize(
719+
("manager_cls", "solver_cfg", "expected_friction", "expected_damping"),
720+
[
721+
pytest.param(NewtonMJWarpManager, MJWarpSolverCfg(), 0.11, 0.23, id="mjwarp"),
722+
pytest.param(NewtonFeatherstoneManager, FeatherstoneSolverCfg(), 0.0, 0.0, id="featherstone"),
723+
],
724+
)
725+
def test_production_imports_scope_mujoco_joint_properties(
726+
monkeypatch, import_path, manager_cls, solver_cfg, expected_friction, expected_damping
727+
):
728+
"""Only MJWarp imports MuJoCo joint properties through either production path."""
729+
from isaaclab_newton.cloner.replicate import _build_newton_builder_from_mapping
730+
731+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
732+
733+
stage = Usd.Stage.CreateInMemory()
734+
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z)
735+
UsdGeom.SetStageMetersPerUnit(stage, 1.0)
736+
physics_prim_path = "/physicsScene"
737+
UsdPhysics.Scene.Define(stage, physics_prim_path)
738+
739+
root_path = "/Sources/robot" if import_path == "clone" else "/World/robot"
740+
root = UsdGeom.Cube.Define(stage, root_path).GetPrim()
741+
UsdPhysics.RigidBodyAPI.Apply(root)
742+
UsdPhysics.ArticulationRootAPI.Apply(root)
743+
744+
child_path = f"{root_path}/child"
745+
child = UsdGeom.Cube.Define(stage, child_path).GetPrim()
746+
UsdPhysics.RigidBodyAPI.Apply(child)
747+
748+
joint = UsdPhysics.RevoluteJoint.Define(stage, f"{child_path}/joint")
749+
joint.CreateAxisAttr().Set("Z")
750+
joint.CreateBody0Rel().SetTargets([root_path])
751+
joint.CreateBody1Rel().SetTargets([child_path])
752+
joint.GetPrim().CreateAttribute("mjc:frictionloss", Sdf.ValueTypeNames.Double, True).Set(0.11)
753+
joint.GetPrim().CreateAttribute("mjc:damping", Sdf.ValueTypeNames.Double, True).Set(0.23)
754+
755+
monkeypatch.setattr(
756+
PhysicsManager,
757+
"_sim",
758+
SimpleNamespace(physics_manager=manager_cls, cfg=SimpleNamespace(physics_prim_path=physics_prim_path)),
759+
)
760+
monkeypatch.setattr(PhysicsManager, "_cfg", NewtonCfg(solver_cfg=solver_cfg))
761+
monkeypatch.setattr(PhysicsManager, "_device", "cpu")
762+
monkeypatch.setattr(NewtonManager, "_builder", None)
763+
monkeypatch.setattr(NewtonManager, "_deformable_registry", [])
764+
monkeypatch.setattr(NewtonManager, "_cl_pending_sites", {})
765+
monkeypatch.setattr(NewtonManager, "_per_world_builder_hooks", [])
766+
monkeypatch.setattr(NewtonManager, "_world_xforms", None)
767+
768+
if import_path == "clone":
769+
builder, *_ = _build_newton_builder_from_mapping(
770+
stage=stage,
771+
sources=(root_path,),
772+
destinations=("/World/envs/env_{}/robot",),
773+
env_ids=np.array([0], dtype=np.int64),
774+
mapping=np.ones((1, 1), dtype=np.bool_),
775+
load_visual_shapes=False,
776+
)
777+
else:
778+
monkeypatch.setattr(newton_manager_module, "get_current_stage", lambda: stage)
779+
monkeypatch.setattr(
780+
newton_manager_module, "_restore_visible_colliders_without_visual_shapes", lambda *args: None
781+
)
782+
monkeypatch.setattr(newton_manager_module, "replace_newton_builder_shape_colors", lambda *args: None)
783+
monkeypatch.setattr(newton_manager_module, "import_builder_visual_material_paths", lambda *args: None)
784+
manager_cls.instantiate_builder_from_stage()
785+
builder = NewtonManager._builder
786+
787+
model = builder.finalize(device="cpu")
788+
789+
assert model.joint_friction.numpy()[-1] == pytest.approx(expected_friction)
790+
assert model.joint_damping.numpy()[-1] == pytest.approx(expected_damping)
791+
792+
793+
@pytest.mark.parametrize(
794+
("manager_cls", "imports_mujoco"),
795+
[
796+
pytest.param(NewtonMJWarpManager, True, id="mjwarp"),
797+
pytest.param(NewtonFeatherstoneManager, False, id="featherstone"),
798+
],
799+
)
800+
@pytest.mark.parametrize(
801+
"author_newton_values",
802+
[
803+
pytest.param(False, id="physx-over-mjc"),
804+
pytest.param(True, id="newton-over-physx-over-mjc"),
805+
],
806+
)
807+
def test_schema_resolver_policy_and_precedence(manager_cls, imports_mujoco, author_newton_values):
808+
"""Resolver precedence and MuJoCo fallback selection follow active solver needs."""
809+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
810+
811+
stage = Usd.Stage.CreateInMemory()
812+
root_path = "/World/robot"
813+
root = UsdGeom.Cube.Define(stage, root_path).GetPrim()
814+
UsdPhysics.RigidBodyAPI.Apply(root)
815+
UsdPhysics.ArticulationRootAPI.Apply(root)
816+
child_path = f"{root_path}/child"
817+
child = UsdGeom.Cube.Define(stage, child_path).GetPrim()
818+
UsdPhysics.RigidBodyAPI.Apply(child)
819+
joint = UsdPhysics.RevoluteJoint.Define(stage, f"{child_path}/joint")
820+
joint.CreateAxisAttr().Set("Z")
821+
joint.CreateBody0Rel().SetTargets([root_path])
822+
joint.CreateBody1Rel().SetTargets([child_path])
823+
joint_prim = joint.GetPrim()
824+
joint_prim.CreateAttribute("mjc:frictionloss", Sdf.ValueTypeNames.Double, True).Set(0.11)
825+
joint_prim.CreateAttribute("mjc:damping", Sdf.ValueTypeNames.Double, True).Set(0.23)
826+
joint_prim.CreateAttribute("mjc:armature", Sdf.ValueTypeNames.Double, True).Set(0.12)
827+
joint_prim.CreateAttribute("physxJoint:armature", Sdf.ValueTypeNames.Float, True).Set(0.21)
828+
if author_newton_values:
829+
joint_prim.CreateAttribute("newton:friction", Sdf.ValueTypeNames.Double, True).Set(0.31)
830+
joint_prim.CreateAttribute("newton:armature", Sdf.ValueTypeNames.Double, True).Set(0.41)
831+
832+
schema_resolvers = manager_cls._get_usd_import_schema_resolvers()
833+
assert [type(resolver).__name__ for resolver in schema_resolvers] == [
834+
"SchemaResolverNewton",
835+
"SchemaResolverPhysx",
836+
*(["SchemaResolverMjc"] if imports_mujoco else []),
837+
]
838+
839+
builder = ModelBuilder()
840+
manager_cls._register_builder_attributes(builder)
841+
builder.add_usd(stage, schema_resolvers=schema_resolvers)
842+
model = builder.finalize(device="cpu")
843+
844+
expected_friction = 0.31 if author_newton_values else (0.11 if imports_mujoco else 0.0)
845+
expected_damping = 0.23 if imports_mujoco else 0.0
846+
expected_armature = 0.41 if author_newton_values else 0.21
847+
assert model.joint_friction.numpy()[-1] == pytest.approx(expected_friction)
848+
assert model.joint_damping.numpy()[-1] == pytest.approx(expected_damping)
849+
assert model.joint_armature.numpy()[-1] == pytest.approx(expected_armature)
850+
851+
717852
def test_mpm_end_to_end_with_particle_custom_attributes():
718853
"""End-to-end MPM step using ``add_particles(custom_attributes=...)`` — the production path."""
719854
sim_cfg = SimulationCfg(

0 commit comments

Comments
 (0)