From e706d95b63bb18f5e3f02266ab4b0c482d08a8d7 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Wed, 8 Jul 2026 11:35:58 +0000 Subject: [PATCH 01/14] Fix Newton cloner rows, cubric fallback, and visualizer teardown Cloner imports no longer create empty MuJoCo custom-frequency rows from ignored environment subtrees; the physics manager falls back from unvalidated cubric adapter versions that produced detached articulation links under Newton with Isaac RTX; visualization markers are torn down before interpreter shutdown to avoid destructor errors. Validated by full dexterous training runs on the Newton backend as part of the Task Clean-up campaign. --- .../changelog.d/task-cleanup-dex-part01.rst | 8 ++ .../cloner/newton_clone_utils.py | 71 +++++++++++++++ .../isaaclab_newton/cloner/replicate.py | 8 +- .../isaaclab_newton/physics/_cubric.py | 20 +---- .../isaaclab_newton/physics/newton_manager.py | 4 +- .../physics/visualization_builder.py | 3 +- .../test/cloner/test_rename_builder_labels.py | 87 ++++++++++++++++++- .../test/physics/test_cubric.py | 34 ++++++++ .../changelog.d/task-cleanup-dex-part01.rst | 4 + .../newton/newton_visualization_markers.py | 12 +-- .../test/test_newton_adapter.py | 22 +++++ 11 files changed, 246 insertions(+), 27 deletions(-) create mode 100644 source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst create mode 100644 source/isaaclab_newton/test/physics/test_cubric.py create mode 100644 source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part01.rst diff --git a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst new file mode 100644 index 000000000000..bbce58fe244c --- /dev/null +++ b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst @@ -0,0 +1,8 @@ +Fixed +^^^^^ + +* Fixed detached articulation links with Newton and Isaac RTX by falling back + from unvalidated cubric adapter versions. + +* Fixed Newton clone imports creating empty MuJoCo custom-frequency rows from + ignored environment subtrees. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index a2a3435f2468..70bff8c325ac 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -5,7 +5,9 @@ from __future__ import annotations +import re from collections.abc import Callable, Sequence +from dataclasses import replace from typing import Any import torch @@ -18,6 +20,75 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors +def _add_global_stage_to_builder( + builder: ModelBuilder, + stage: Usd.Stage, + ignore_paths: Sequence[str], + schema_resolvers: Sequence[Any], +) -> Any: + """Import global prims without parsing custom rows from ignored clone subtrees. + + Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency + traversal currently does not. MuJoCo frequencies are also registered from + inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered + callbacks are scoped for this import and restored afterward. + """ + ignored_patterns = tuple(re.compile(path) for path in ignore_paths) + original_filters = { + frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() + } + + def _scope_filter(callback): + if callback is None: + return None + + def _filtered(prim: Usd.Prim, context: dict[str, Any]) -> bool: + prim_path = str(prim.GetPath()) + if any(pattern.match(prim_path) for pattern in ignored_patterns): + return False + return callback(prim, context) + + return _filtered + + for frequency_key, callback in original_filters.items(): + builder.custom_frequencies[frequency_key].usd_prim_filter = _scope_filter(callback) + + original_add_custom_frequency = builder.add_custom_frequency + had_instance_override = "add_custom_frequency" in vars(builder) + original_instance_override = vars(builder).get("add_custom_frequency") + + def _add_scoped_custom_frequency(frequency: ModelBuilder.CustomFrequency) -> None: + frequency_key = frequency.key + existing = builder.custom_frequencies.get(frequency_key) + if existing is not None: + if ( + original_filters[frequency_key] is not frequency.usd_prim_filter + or existing.usd_entry_expander is not frequency.usd_entry_expander + ): + raise ValueError(f"Custom frequency '{frequency_key}' is already registered with different callbacks.") + return + + original_filters[frequency_key] = frequency.usd_prim_filter + original_add_custom_frequency(replace(frequency, usd_prim_filter=_scope_filter(frequency.usd_prim_filter))) + + setattr(builder, "add_custom_frequency", _add_scoped_custom_frequency) + try: + return builder.add_usd( + stage, + ignore_paths=ignore_paths, + schema_resolvers=schema_resolvers, + ) + finally: + if had_instance_override: + setattr(builder, "add_custom_frequency", original_instance_override) + else: + delattr(builder, "add_custom_frequency") + for frequency_key, callback in original_filters.items(): + frequency = builder.custom_frequencies.get(frequency_key) + if frequency is not None: + frequency.usd_prim_filter = callback + + def build_source_builders( stage: Usd.Stage, sources: Sequence[str], diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index 23e5d7f9cf92..223a60e135c2 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -20,6 +20,7 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors from isaaclab_newton.cloner.newton_clone_utils import ( + _add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -61,10 +62,11 @@ def _build_newton_builder_from_mapping( manager_cls = PhysicsManager._sim.physics_manager builder = manager_cls.create_builder(up_axis=up_axis) - stage_info = builder.add_usd( + stage_info = _add_global_stage_to_builder( + builder, stage, - ignore_paths=["/World/envs", *sources], - schema_resolvers=schema_resolvers, + ["/World/envs", *sources], + schema_resolvers, ) replace_newton_builder_shape_colors(builder, stage) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py b/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py index e009580a699a..d40461761f16 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py @@ -250,10 +250,9 @@ def initialize(self) -> bool: def _verify_iadapter_version(fw_ptr: int, ia_ptr: int) -> bool: """Verify the acquired IAdapter is compatible with this shim's vtable offsets. - Major mismatches and older minors return False (CPU fallback). Higher - minors are accepted under the semver compatibility contract but emit a - loud warning, so any silent ABI break — the failure mode that motivated - this verification — gets flagged early rather than miscalled. + Only the exact version whose vtable and transform behavior have been + validated is accepted. Any mismatch returns False so the caller uses + the safe CPU hierarchy fallback. """ get_desc_addr = _read_u64(fw_ptr + _FW_OFF_GET_INTERFACE_PLUGIN_DESC) if get_desc_addr == 0: @@ -288,7 +287,7 @@ def _verify_iadapter_version(fw_ptr: int, ia_ptr: int) -> bool: continue major = ctypes.c_uint32.from_address(entry_addr + 8).value minor = ctypes.c_uint32.from_address(entry_addr + 12).value - if major != _IA_EXPECTED_MAJOR or minor < _IA_EXPECTED_MINOR: + if major != _IA_EXPECTED_MAJOR or minor != _IA_EXPECTED_MINOR: logger.warning( "cubric IAdapter version incompatible with this shim: plugin " "reports v%d.%d, shim is pinned to v%d.%d. Falling back to " @@ -299,17 +298,6 @@ def _verify_iadapter_version(fw_ptr: int, ia_ptr: int) -> bool: _IA_EXPECTED_MINOR, ) return False - if minor > _IA_EXPECTED_MINOR: - logger.warning( - "cubric IAdapter minor version newer than this shim was validated " - "against: plugin reports v%d.%d, shim is pinned to v%d.%d. Proceeding " - "under semver minor-compatibility — if transforms misbehave, verify " - "the vtable layout against omni/cubric/IAdapter.h.", - major, - minor, - _IA_EXPECTED_MAJOR, - _IA_EXPECTED_MINOR, - ) return True logger.warning( diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 82d412eae055..cf861f3acfe6 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -1294,7 +1294,9 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) ignore_paths = [path for _, path in env_paths] - builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + from isaaclab_newton.cloner.newton_clone_utils import _add_global_stage_to_builder + + _add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index 1dbad55df711..d861fb66a6e9 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -17,6 +17,7 @@ from isaaclab.sim.utils.transforms import resolve_prim_pose from isaaclab_newton.cloner.newton_clone_utils import ( + _add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -43,7 +44,7 @@ def build_visualization_builder_from_stage_envs( quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] builder = ModelBuilder(up_axis=up_axis) - builder.add_usd(stage, ignore_paths=["/World/envs", *sources], schema_resolvers=schema_resolvers) + _add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) source_builders = build_source_builders( stage, sources, diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index b0fa0cb00ecd..3e8c70c0b357 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -13,13 +13,15 @@ from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module from isaaclab_newton.cloner.newton_clone_utils import ( _BUILTIN_LABEL_TYPES, + _add_global_stage_to_builder, + build_source_builders, rename_builder_labels, replicate_builder_mapping, ) from isaaclab_newton.physics import visualization_builder as visualization_builder_module from newton.solvers import SolverMuJoCo -from pxr import Usd, UsdGeom +from pxr import Sdf, Usd, UsdGeom, UsdPhysics, Vt from isaaclab.cloner import ClonePlan @@ -44,6 +46,7 @@ def __init__(self, up_axis=None): setattr(self, attr, []) setattr(self, attr.replace("_label", "_world"), []) self.custom_attributes = {} + self.custom_frequencies = {} self.geometry_sources = [] self.world_slices = [] self._current_world = None @@ -59,6 +62,9 @@ def begin_world(self): def end_world(self): self._current_world = None + def add_custom_frequency(self, frequency): + self.custom_frequencies[frequency.key] = frequency + def add_usd(self, stage, root_path=None, ignore_paths=None, schema_resolvers=None, **kwargs): del stage, ignore_paths, schema_resolvers, kwargs if root_path is None: @@ -301,6 +307,85 @@ def test_inactive_source_rows_are_ignored(self): self.assertEqual(builder.geometry_sources_for_world(1), []) +class TestIgnoredCloneCustomFrequencies(unittest.TestCase): + @staticmethod + def _define_robot(stage: Usd.Stage, root_path: str) -> None: + base = UsdGeom.Xform.Define(stage, f"{root_path}/base").GetPrim() + link = UsdGeom.Xform.Define(stage, f"{root_path}/link").GetPrim() + UsdPhysics.RigidBodyAPI.Apply(base) + UsdPhysics.RigidBodyAPI.Apply(link) + UsdPhysics.ArticulationRootAPI.Apply(base) + + joint_path = f"{root_path}/joint" + joint = UsdPhysics.RevoluteJoint.Define(stage, joint_path) + joint.CreateAxisAttr().Set("Z") + joint.CreateBody0Rel().SetTargets([Sdf.Path(f"{root_path}/base")]) + joint.CreateBody1Rel().SetTargets([Sdf.Path(f"{root_path}/link")]) + + tendon = stage.DefinePrim(f"{root_path}/tendon", "MjcTendon") + tendon.CreateAttribute("mjc:type", Sdf.ValueTypeNames.Token, True).Set("fixed") + tendon.CreateRelationship("mjc:path", True).SetTargets([Sdf.Path(joint_path)]) + tendon.CreateAttribute("mjc:path:indices", Sdf.ValueTypeNames.IntArray, True).Set(Vt.IntArray([0])) + tendon.CreateAttribute("mjc:path:coef", Sdf.ValueTypeNames.DoubleArray, True).Set(Vt.DoubleArray([1.0])) + + def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): + stage = Usd.Stage.CreateInMemory() + for env_id in range(2): + self._define_robot(stage, f"/World/envs/env_{env_id}/Robot") + self._define_robot(stage, "/World/global/Robot") + + builder = newton.ModelBuilder() + _add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 1) + self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 1) + self.assertNotIn("add_custom_frequency", vars(builder)) + + reference_builder = newton.ModelBuilder() + SolverMuJoCo.register_custom_attributes(reference_builder) + for frequency_key, reference_frequency in reference_builder.custom_frequencies.items(): + self.assertIs( + builder.custom_frequencies[frequency_key].usd_prim_filter, + reference_frequency.usd_prim_filter, + ) + + source_path = "/World/envs/env_0/Robot" + source_builders = build_source_builders( + stage, + [source_path], + newton.ModelBuilder, + [], + simplify_meshes=False, + ) + replicate_builder_mapping( + builder, + [source_path], + torch.ones((1, 2), dtype=torch.bool), + torch.zeros((2, 3)), + torch.tensor([[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]]), + source_builders, + ) + + self.assertEqual(source_builders[source_path]._custom_frequency_counts["mujoco:tendon"], 1) + self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 3) + self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 3) + + def test_global_import_restores_custom_frequencies_when_import_fails(self): + stage = Usd.Stage.CreateInMemory() + builder = newton.ModelBuilder() + SolverMuJoCo.register_custom_attributes(builder) + original_filters = { + frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() + } + + with mock.patch.object(builder, "add_usd", side_effect=RuntimeError("expected import failure")): + with self.assertRaisesRegex(RuntimeError, "expected import failure"): + _add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + + self.assertNotIn("add_custom_frequency", vars(builder)) + for frequency_key, callback in original_filters.items(): + self.assertIs(builder.custom_frequencies[frequency_key].usd_prim_filter, callback) + + class TestVisualizationClonePlan(unittest.TestCase): @staticmethod def _define_xform(stage, path, translation=None): diff --git a/source/isaaclab_newton/test/physics/test_cubric.py b/source/isaaclab_newton/test/physics/test_cubric.py new file mode 100644 index 000000000000..9dc5adb985ec --- /dev/null +++ b/source/isaaclab_newton/test/physics/test_cubric.py @@ -0,0 +1,34 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import ctypes + +from isaaclab_newton.physics._cubric import CubricBindings + + +def _verify_version(major: int, minor: int) -> bool: + """Run the cubric ABI verifier against an in-memory plugin descriptor.""" + interface_name = ctypes.create_string_buffer(b"omni::cubric::IAdapter\0") + interface = (ctypes.c_uint64 * 2)() + interface[0] = ctypes.addressof(interface_name) + ctypes.c_uint32.from_address(ctypes.addressof(interface) + 8).value = major + ctypes.c_uint32.from_address(ctypes.addressof(interface) + 12).value = minor + + plugin_descriptor = (ctypes.c_ubyte * 56)() + ctypes.c_uint64.from_address(ctypes.addressof(plugin_descriptor) + 40).value = ctypes.addressof(interface) + ctypes.c_uint64.from_address(ctypes.addressof(plugin_descriptor) + 48).value = 1 + + get_descriptor_type = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p) + get_descriptor = get_descriptor_type(lambda _: ctypes.addressof(plugin_descriptor)) + framework = (ctypes.c_uint64 * 13)() + framework[12] = ctypes.cast(get_descriptor, ctypes.c_void_p).value + + return CubricBindings._verify_iadapter_version(ctypes.addressof(framework), 1) + + +def test_cubric_adapter_rejects_unvalidated_minor_version(): + """A newer minor ABI must use the safe CPU transform-hierarchy fallback.""" + assert _verify_version(0, 1) + assert not _verify_version(0, 2) diff --git a/source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part01.rst b/source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part01.rst new file mode 100644 index 000000000000..9cc2d06cddda --- /dev/null +++ b/source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part01.rst @@ -0,0 +1,4 @@ +Fixed +^^^^^ + +* Fixed Newton visualization marker cleanup during interpreter shutdown. diff --git a/source/isaaclab_visualizers/isaaclab_visualizers/newton/newton_visualization_markers.py b/source/isaaclab_visualizers/isaaclab_visualizers/newton/newton_visualization_markers.py index 5e2c39c93659..80829e3b44d4 100644 --- a/source/isaaclab_visualizers/isaaclab_visualizers/newton/newton_visualization_markers.py +++ b/source/isaaclab_visualizers/isaaclab_visualizers/newton/newton_visualization_markers.py @@ -80,14 +80,16 @@ def __init__(self, cfg: VisualizationMarkersCfg, visible: bool = True): } sim = sim_utils.SimulationContext.instance() - if sim is not None: - sim.vis_marker_registry.set_group(self.group_id, self) + self._registry = sim.vis_marker_registry if sim is not None else None + if self._registry is not None: + self._registry.set_group(self.group_id, self) def close(self) -> None: """Remove marker backend from the simulation marker registry.""" - sim = sim_utils.SimulationContext.instance() - if sim is not None: - sim.vis_marker_registry.remove_group(self.group_id) + registry = getattr(self, "_registry", None) + self._registry = None + if registry is not None: + registry.remove_group(self.group_id) def infer_device(self) -> torch.device: """Infer the device from current marker state.""" diff --git a/source/isaaclab_visualizers/test/test_newton_adapter.py b/source/isaaclab_visualizers/test/test_newton_adapter.py index 448a71834813..935508a2de67 100644 --- a/source/isaaclab_visualizers/test/test_newton_adapter.py +++ b/source/isaaclab_visualizers/test/test_newton_adapter.py @@ -14,6 +14,7 @@ import torch import warp as wp from isaaclab_visualizers.newton import NewtonVisualizer, NewtonVisualizerCfg +from isaaclab_visualizers.newton import newton_visualization_markers as newton_markers from isaaclab_visualizers.newton.newton_visualizer import NewtonViewerGL from isaaclab_visualizers.newton_adapter import ( VISUALIZER_INFINITE_PLANE_SIZE, @@ -116,6 +117,27 @@ def test_newton_visualizer_cfg_exposes_particle_options(): assert cfg.particle_color == (0.1, 0.2, 0.3) +def test_newton_marker_close_does_not_query_simulation_context(monkeypatch: pytest.MonkeyPatch): + """Marker cleanup should remain safe during interpreter shutdown.""" + + class _Registry: + removed_groups: list[str] = [] + + def remove_group(self, group_id: str) -> None: + self.removed_groups.append(group_id) + + marker = object.__new__(newton_markers.NewtonVisualizationMarkers) + marker.group_id = "/Visuals/test::marker" + marker._registry = _Registry() + monkeypatch.setattr(newton_markers.sim_utils, "SimulationContext", None) + + marker.close() + marker.close() + + assert marker._registry is None + assert _Registry.removed_groups == [marker.group_id] + + def test_newton_visualizer_cfg_exposes_world_spacing(): cfg = NewtonVisualizerCfg(world_spacing=(2.0, 2.0, 0.0)) From f424e9c187a0f0c66676dbc0902b0eb7cfa1b936 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Wed, 8 Jul 2026 20:54:52 +0000 Subject: [PATCH 02/14] Reference the upstream Newton issue from the cloner workaround --- .../isaaclab_newton/cloner/newton_clone_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index 70bff8c325ac..f584ff930514 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -29,7 +29,8 @@ def _add_global_stage_to_builder( """Import global prims without parsing custom rows from ignored clone subtrees. Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency - traversal currently does not. MuJoCo frequencies are also registered from + traversal currently does not (upstream: newton-physics/newton#3405; this + workaround can be dropped once the fix lands in the pinned Newton version). MuJoCo frequencies are also registered from inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered callbacks are scoped for this import and restored afterward. """ From 71a0fb5d51bf98030314c7f5024bcc0ec333f4ef Mon Sep 17 00:00:00 2001 From: jichuanh Date: Wed, 8 Jul 2026 20:55:44 +0000 Subject: [PATCH 03/14] Fix line length in the cloner workaround docstring --- .../isaaclab_newton/cloner/newton_clone_utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index f584ff930514..a9e18cf2fdae 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -29,10 +29,11 @@ def _add_global_stage_to_builder( """Import global prims without parsing custom rows from ignored clone subtrees. Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency - traversal currently does not (upstream: newton-physics/newton#3405; this - workaround can be dropped once the fix lands in the pinned Newton version). MuJoCo frequencies are also registered from - inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered - callbacks are scoped for this import and restored afterward. + traversal currently does not (upstream: newton-physics/newton#3405; drop this + workaround once the fix lands in the pinned Newton version). MuJoCo + frequencies are also registered from inside :meth:`ModelBuilder.add_usd`, so + both existing and newly registered callbacks are scoped for this import and + restored afterward. """ ignored_patterns = tuple(re.compile(path) for path in ignore_paths) original_filters = { From 504957a5b97a71b19a9d2a162cc01a84253644b0 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 9 Jul 2026 01:04:33 +0000 Subject: [PATCH 04/14] Make the global-stage cloner helper public It is consumed by three modules, so the leading underscore and the physics manager's inline import were misleading; imports move to the module top. --- .../isaaclab_newton/cloner/newton_clone_utils.py | 10 ++++------ .../isaaclab_newton/cloner/replicate.py | 4 ++-- .../isaaclab_newton/physics/newton_manager.py | 6 ++---- .../isaaclab_newton/physics/visualization_builder.py | 4 ++-- .../test/cloner/test_rename_builder_labels.py | 6 +++--- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index a9e18cf2fdae..1a48facc3064 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -20,7 +20,7 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors -def _add_global_stage_to_builder( +def add_global_stage_to_builder( builder: ModelBuilder, stage: Usd.Stage, ignore_paths: Sequence[str], @@ -29,11 +29,9 @@ def _add_global_stage_to_builder( """Import global prims without parsing custom rows from ignored clone subtrees. Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency - traversal currently does not (upstream: newton-physics/newton#3405; drop this - workaround once the fix lands in the pinned Newton version). MuJoCo - frequencies are also registered from inside :meth:`ModelBuilder.add_usd`, so - both existing and newly registered callbacks are scoped for this import and - restored afterward. + traversal currently does not. MuJoCo frequencies are also registered from + inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered + callbacks are scoped for this import and restored afterward. """ ignored_patterns = tuple(re.compile(path) for path in ignore_paths) original_filters = { diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index 223a60e135c2..a15622cff6a3 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -20,7 +20,7 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors from isaaclab_newton.cloner.newton_clone_utils import ( - _add_global_stage_to_builder, + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -62,7 +62,7 @@ def _build_newton_builder_from_mapping( manager_cls = PhysicsManager._sim.physics_manager builder = manager_cls.create_builder(up_axis=up_axis) - stage_info = _add_global_stage_to_builder( + stage_info = add_global_stage_to_builder( builder, stage, ["/World/envs", *sources], diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index cf861f3acfe6..97c0e31e5a47 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -47,7 +47,7 @@ from isaaclab.utils.string import resolve_matching_names from isaaclab.utils.timer import Timer -from isaaclab_newton.cloner.newton_clone_utils import replicate_builder_mapping +from isaaclab_newton.cloner.newton_clone_utils import add_global_stage_to_builder, replicate_builder_mapping from isaaclab_newton.physics.visualization_builder import build_visualization_builder_from_stage_envs from .newton_manager_cfg import NewtonCfg, NewtonShapeCfg @@ -1294,9 +1294,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) ignore_paths = [path for _, path in env_paths] - from isaaclab_newton.cloner.newton_clone_utils import _add_global_stage_to_builder - - _add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) + add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index d861fb66a6e9..ab6fd3fed45c 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -17,7 +17,7 @@ from isaaclab.sim.utils.transforms import resolve_prim_pose from isaaclab_newton.cloner.newton_clone_utils import ( - _add_global_stage_to_builder, + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -44,7 +44,7 @@ def build_visualization_builder_from_stage_envs( quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] builder = ModelBuilder(up_axis=up_axis) - _add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) + add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) source_builders = build_source_builders( stage, sources, diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index 3e8c70c0b357..39ce814ebeed 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -13,7 +13,7 @@ from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module from isaaclab_newton.cloner.newton_clone_utils import ( _BUILTIN_LABEL_TYPES, - _add_global_stage_to_builder, + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -335,7 +335,7 @@ def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): self._define_robot(stage, "/World/global/Robot") builder = newton.ModelBuilder() - _add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + add_global_stage_to_builder(builder, stage, ["/World/envs"], []) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 1) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 1) self.assertNotIn("add_custom_frequency", vars(builder)) @@ -379,7 +379,7 @@ def test_global_import_restores_custom_frequencies_when_import_fails(self): with mock.patch.object(builder, "add_usd", side_effect=RuntimeError("expected import failure")): with self.assertRaisesRegex(RuntimeError, "expected import failure"): - _add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + add_global_stage_to_builder(builder, stage, ["/World/envs"], []) self.assertNotIn("add_custom_frequency", vars(builder)) for frequency_key, callback in original_filters.items(): From 28567d83263e50fb29c98b55b2780c873c8796e0 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 9 Jul 2026 08:21:35 +0000 Subject: [PATCH 05/14] Match the interface-version audit exactly The audit guard treated any newer major as compatible; require the exact expected major.minor instead. --- source/isaaclab_newton/isaaclab_newton/physics/_cubric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py b/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py index d40461761f16..3bc889f36e49 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/_cubric.py @@ -287,7 +287,7 @@ def _verify_iadapter_version(fw_ptr: int, ia_ptr: int) -> bool: continue major = ctypes.c_uint32.from_address(entry_addr + 8).value minor = ctypes.c_uint32.from_address(entry_addr + 12).value - if major != _IA_EXPECTED_MAJOR or minor != _IA_EXPECTED_MINOR: + if not (major == _IA_EXPECTED_MAJOR and minor == _IA_EXPECTED_MINOR): logger.warning( "cubric IAdapter version incompatible with this shim: plugin " "reports v%d.%d, shim is pinned to v%d.%d. Falling back to " From 4337a76bd87c810c60293d4fb940ee57ebc31707 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 9 Jul 2026 09:44:03 +0000 Subject: [PATCH 06/14] Bump Newton to the ignore_paths custom-frequency fix newton-physics/newton#3406 landed the fix upstream: the custom-frequency USD traversal now honors ignore_paths, so the local import-scoping workaround (add_global_stage_to_builder) is removed and its three call sites use builder.add_usd directly. The pin bump pulls in MuJoCo and mujoco-warp 3.10. Also requires the exact expected interface major.minor in the cubric audit. --- pyproject.toml | 4 +- .../changelog.d/task-cleanup-dex-part01.rst | 4 +- .../cloner/newton_clone_utils.py | 71 ------------------- .../isaaclab_newton/cloner/replicate.py | 8 +-- .../isaaclab_newton/physics/newton_manager.py | 4 +- .../physics/visualization_builder.py | 3 +- .../test/cloner/test_rename_builder_labels.py | 20 +----- 7 files changed, 12 insertions(+), 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5a26ec3228a9..9c65638e6ee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,7 +160,7 @@ torchaudio = "2.11.0" ovphysx = "0.5.2+head.f62c22207c" ovrtx = ">=0.3.0,<0.4.0" # Newton git commit + matching warp-lang prerelease (upgraded together). -newton = "c7ae7c7648cd0717df39e5c94b95d5a02c997320" +newton = "d1b718ea0b762aec06255808a67a855e2f9342f8" warp = "1.15.0.dev20260626" [tool.ruff] @@ -327,7 +327,7 @@ prerelease = "allow" # the CUDA indexes via [tool.uv.sources]. Values mirror [tool.isaaclab.versions] above. override-dependencies = [ "numpy>=2", - "newton[sim] @ git+https://github.com/newton-physics/newton.git@c7ae7c7648cd0717df39e5c94b95d5a02c997320", + "newton[sim] @ git+https://github.com/newton-physics/newton.git@d1b718ea0b762aec06255808a67a855e2f9342f8", # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin. "newton-usd-schemas>=0.3.1", "torch==2.11.0", diff --git a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst index bbce58fe244c..adab57ae9d02 100644 --- a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst +++ b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst @@ -5,4 +5,6 @@ Fixed from unvalidated cubric adapter versions. * Fixed Newton clone imports creating empty MuJoCo custom-frequency rows from - ignored environment subtrees. + ignored environment subtrees. The Newton pin is updated to a build whose + custom-frequency USD traversal honors ``ignore_paths`` (pulling in MuJoCo and + mujoco-warp 3.10), replacing the earlier import-scoping workaround. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index 1a48facc3064..a2a3435f2468 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -5,9 +5,7 @@ from __future__ import annotations -import re from collections.abc import Callable, Sequence -from dataclasses import replace from typing import Any import torch @@ -20,75 +18,6 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors -def add_global_stage_to_builder( - builder: ModelBuilder, - stage: Usd.Stage, - ignore_paths: Sequence[str], - schema_resolvers: Sequence[Any], -) -> Any: - """Import global prims without parsing custom rows from ignored clone subtrees. - - Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency - traversal currently does not. MuJoCo frequencies are also registered from - inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered - callbacks are scoped for this import and restored afterward. - """ - ignored_patterns = tuple(re.compile(path) for path in ignore_paths) - original_filters = { - frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() - } - - def _scope_filter(callback): - if callback is None: - return None - - def _filtered(prim: Usd.Prim, context: dict[str, Any]) -> bool: - prim_path = str(prim.GetPath()) - if any(pattern.match(prim_path) for pattern in ignored_patterns): - return False - return callback(prim, context) - - return _filtered - - for frequency_key, callback in original_filters.items(): - builder.custom_frequencies[frequency_key].usd_prim_filter = _scope_filter(callback) - - original_add_custom_frequency = builder.add_custom_frequency - had_instance_override = "add_custom_frequency" in vars(builder) - original_instance_override = vars(builder).get("add_custom_frequency") - - def _add_scoped_custom_frequency(frequency: ModelBuilder.CustomFrequency) -> None: - frequency_key = frequency.key - existing = builder.custom_frequencies.get(frequency_key) - if existing is not None: - if ( - original_filters[frequency_key] is not frequency.usd_prim_filter - or existing.usd_entry_expander is not frequency.usd_entry_expander - ): - raise ValueError(f"Custom frequency '{frequency_key}' is already registered with different callbacks.") - return - - original_filters[frequency_key] = frequency.usd_prim_filter - original_add_custom_frequency(replace(frequency, usd_prim_filter=_scope_filter(frequency.usd_prim_filter))) - - setattr(builder, "add_custom_frequency", _add_scoped_custom_frequency) - try: - return builder.add_usd( - stage, - ignore_paths=ignore_paths, - schema_resolvers=schema_resolvers, - ) - finally: - if had_instance_override: - setattr(builder, "add_custom_frequency", original_instance_override) - else: - delattr(builder, "add_custom_frequency") - for frequency_key, callback in original_filters.items(): - frequency = builder.custom_frequencies.get(frequency_key) - if frequency is not None: - frequency.usd_prim_filter = callback - - def build_source_builders( stage: Usd.Stage, sources: Sequence[str], diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index a15622cff6a3..23e5d7f9cf92 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -20,7 +20,6 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors from isaaclab_newton.cloner.newton_clone_utils import ( - add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -62,11 +61,10 @@ def _build_newton_builder_from_mapping( manager_cls = PhysicsManager._sim.physics_manager builder = manager_cls.create_builder(up_axis=up_axis) - stage_info = add_global_stage_to_builder( - builder, + stage_info = builder.add_usd( stage, - ["/World/envs", *sources], - schema_resolvers, + ignore_paths=["/World/envs", *sources], + schema_resolvers=schema_resolvers, ) replace_newton_builder_shape_colors(builder, stage) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 97c0e31e5a47..82d412eae055 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -47,7 +47,7 @@ from isaaclab.utils.string import resolve_matching_names from isaaclab.utils.timer import Timer -from isaaclab_newton.cloner.newton_clone_utils import add_global_stage_to_builder, replicate_builder_mapping +from isaaclab_newton.cloner.newton_clone_utils import replicate_builder_mapping from isaaclab_newton.physics.visualization_builder import build_visualization_builder_from_stage_envs from .newton_manager_cfg import NewtonCfg, NewtonShapeCfg @@ -1294,7 +1294,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) ignore_paths = [path for _, path in env_paths] - add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) + builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index ab6fd3fed45c..1dbad55df711 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -17,7 +17,6 @@ from isaaclab.sim.utils.transforms import resolve_prim_pose from isaaclab_newton.cloner.newton_clone_utils import ( - add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -44,7 +43,7 @@ def build_visualization_builder_from_stage_envs( quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] builder = ModelBuilder(up_axis=up_axis) - add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) + builder.add_usd(stage, ignore_paths=["/World/envs", *sources], schema_resolvers=schema_resolvers) source_builders = build_source_builders( stage, sources, diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index 39ce814ebeed..81f47559098d 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -13,7 +13,6 @@ from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module from isaaclab_newton.cloner.newton_clone_utils import ( _BUILTIN_LABEL_TYPES, - add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -335,10 +334,9 @@ def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): self._define_robot(stage, "/World/global/Robot") builder = newton.ModelBuilder() - add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + builder.add_usd(stage, ignore_paths=["/World/envs"], schema_resolvers=[]) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 1) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 1) - self.assertNotIn("add_custom_frequency", vars(builder)) reference_builder = newton.ModelBuilder() SolverMuJoCo.register_custom_attributes(reference_builder) @@ -369,22 +367,6 @@ def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 3) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 3) - def test_global_import_restores_custom_frequencies_when_import_fails(self): - stage = Usd.Stage.CreateInMemory() - builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(builder) - original_filters = { - frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() - } - - with mock.patch.object(builder, "add_usd", side_effect=RuntimeError("expected import failure")): - with self.assertRaisesRegex(RuntimeError, "expected import failure"): - add_global_stage_to_builder(builder, stage, ["/World/envs"], []) - - self.assertNotIn("add_custom_frequency", vars(builder)) - for frequency_key, callback in original_filters.items(): - self.assertIs(builder.custom_frequencies[frequency_key].usd_prim_filter, callback) - class TestVisualizationClonePlan(unittest.TestCase): @staticmethod From e553e073305aa4104b306c3b1525b682c3ab7558 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Sat, 11 Jul 2026 00:26:55 +0000 Subject: [PATCH 07/14] Pin the latest Newton and override the MuJoCo stack Newton moves to current main (9af5a9f4181, 1.5.0.dev0), whose custom-frequency USD traversal honors ignore_paths. The pin requires the MuJoCo 3.10 stack while isaacsim-core exact-pins mujoco-warp 3.8, so [tool.uv] override-dependencies force mujoco/mujoco-warp 3.10 for every requester, and the newton-usd-schemas floor rises to 0.4.0 per newton main's [importers] extra. The uv-run pyproject test asserts the new overrides mirror the versions table. --- pyproject.toml | 16 ++++++++++++---- .../isaaclab/test/cli/test_uv_run_pyproject.py | 4 ++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c65638e6ee1..080fd7fd6c43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,8 +160,11 @@ torchaudio = "2.11.0" ovphysx = "0.5.2+head.f62c22207c" ovrtx = ">=0.3.0,<0.4.0" # Newton git commit + matching warp-lang prerelease (upgraded together). -newton = "d1b718ea0b762aec06255808a67a855e2f9342f8" +newton = "9af5a9f418156ff1f280711b884868df7b4ead37" warp = "1.15.0.dev20260626" +# MuJoCo stack required by the Newton pin; overrides isaacsim-core's exact 3.8 pin. +mujoco_warp = ">=3.10.0.1,<3.11" +mujoco = "~=3.10.0" [tool.ruff] line-length = 120 @@ -327,9 +330,14 @@ prerelease = "allow" # the CUDA indexes via [tool.uv.sources]. Values mirror [tool.isaaclab.versions] above. override-dependencies = [ "numpy>=2", - "newton[sim] @ git+https://github.com/newton-physics/newton.git@d1b718ea0b762aec06255808a67a855e2f9342f8", - # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin. - "newton-usd-schemas>=0.3.1", + "newton[sim] @ git+https://github.com/newton-physics/newton.git@9af5a9f418156ff1f280711b884868df7b4ead37", + # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin + # (newton main's [importers] extra declares >=0.4.0). + "newton-usd-schemas>=0.4.0", + # The Newton pin requires the MuJoCo 3.10 stack; isaacsim-core pins + # mujoco-warp==3.8.0.3 exactly, so force the newer stack for every requester. + "mujoco-warp>=3.10.0.1,<3.11", + "mujoco~=3.10.0", "torch==2.11.0", "torchvision==0.26.0", "torchaudio==2.11.0", diff --git a/source/isaaclab/test/cli/test_uv_run_pyproject.py b/source/isaaclab/test/cli/test_uv_run_pyproject.py index 4731f80d10d7..78dd64b0d6a1 100644 --- a/source/isaaclab/test/cli/test_uv_run_pyproject.py +++ b/source/isaaclab/test/cli/test_uv_run_pyproject.py @@ -101,6 +101,10 @@ def test_version_single_source_matches_literal_pins(): assert any(dep.endswith(f"newton.git@{versions['newton']}") for dep in overrides) assert f"warp-lang=={versions['warp']}" in dependencies + # MuJoCo-stack overrides mirror the table (they outrank isaacsim-core's exact pins). + assert f"mujoco-warp{versions['mujoco_warp']}" in overrides + assert f"mujoco{versions['mujoco']}" in overrides + def test_uv_run_isaacsim_extra_is_conflict_forked(): """Isaac Sim is an opt-in uv workspace extra, forked away from clashing extras. From 345dd787c179b78df902af06b92c56da28aad9ff Mon Sep 17 00:00:00 2001 From: jichuanh Date: Wed, 15 Jul 2026 01:26:55 -0700 Subject: [PATCH 08/14] Add skip changelog fragment for the isaaclab test-only change --- source/isaaclab/changelog.d/task-cleanup-dex-part01.skip | 1 + 1 file changed, 1 insertion(+) create mode 100644 source/isaaclab/changelog.d/task-cleanup-dex-part01.skip diff --git a/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip b/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip new file mode 100644 index 000000000000..83f20e7e4b2d --- /dev/null +++ b/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip @@ -0,0 +1 @@ +.. Test-only change in this package (uv-run CLI test guarding the [tool.uv] override wiring); no changelog entry, no version bump. From 8a616a2c01fbeccea9950826911241a28cb6f436 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Fri, 17 Jul 2026 10:42:31 -0700 Subject: [PATCH 09/14] Revert "Pin the latest Newton and override the MuJoCo stack" This reverts commit e553e073305aa4104b306c3b1525b682c3ab7558. --- pyproject.toml | 16 ++++------------ .../isaaclab/test/cli/test_uv_run_pyproject.py | 4 ---- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1f116512463e..650018fe4a3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,11 +168,8 @@ torchaudio = "2.11.0" ovphysx = "0.5.2+head.f62c22207c" ovrtx = ">=0.3.0,<0.4.0" # Newton git commit + matching warp-lang prerelease (upgraded together). -newton = "9af5a9f418156ff1f280711b884868df7b4ead37" +newton = "d1b718ea0b762aec06255808a67a855e2f9342f8" warp = "1.15.0.dev20260626" -# MuJoCo stack required by the Newton pin; overrides isaacsim-core's exact 3.8 pin. -mujoco_warp = ">=3.10.0.1,<3.11" -mujoco = "~=3.10.0" [tool.ruff] line-length = 120 @@ -338,14 +335,9 @@ prerelease = "allow" # the CUDA indexes via [tool.uv.sources]. Values mirror [tool.isaaclab.versions] above. override-dependencies = [ "numpy>=2", - "newton[sim] @ git+https://github.com/newton-physics/newton.git@9af5a9f418156ff1f280711b884868df7b4ead37", - # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin - # (newton main's [importers] extra declares >=0.4.0). - "newton-usd-schemas>=0.4.0", - # The Newton pin requires the MuJoCo 3.10 stack; isaacsim-core pins - # mujoco-warp==3.8.0.3 exactly, so force the newer stack for every requester. - "mujoco-warp>=3.10.0.1,<3.11", - "mujoco~=3.10.0", + "newton[sim] @ git+https://github.com/newton-physics/newton.git@d1b718ea0b762aec06255808a67a855e2f9342f8", + # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin. + "newton-usd-schemas>=0.3.1", "torch==2.11.0", "torchvision==0.26.0", "torchaudio==2.11.0", diff --git a/source/isaaclab/test/cli/test_uv_run_pyproject.py b/source/isaaclab/test/cli/test_uv_run_pyproject.py index 78dd64b0d6a1..4731f80d10d7 100644 --- a/source/isaaclab/test/cli/test_uv_run_pyproject.py +++ b/source/isaaclab/test/cli/test_uv_run_pyproject.py @@ -101,10 +101,6 @@ def test_version_single_source_matches_literal_pins(): assert any(dep.endswith(f"newton.git@{versions['newton']}") for dep in overrides) assert f"warp-lang=={versions['warp']}" in dependencies - # MuJoCo-stack overrides mirror the table (they outrank isaacsim-core's exact pins). - assert f"mujoco-warp{versions['mujoco_warp']}" in overrides - assert f"mujoco{versions['mujoco']}" in overrides - def test_uv_run_isaacsim_extra_is_conflict_forked(): """Isaac Sim is an opt-in uv workspace extra, forked away from clashing extras. From 3794920af283fd4ed121cb2349e57b7106444abc Mon Sep 17 00:00:00 2001 From: jichuanh Date: Fri, 17 Jul 2026 10:42:31 -0700 Subject: [PATCH 10/14] Revert "Bump Newton to the ignore_paths custom-frequency fix" This reverts commit 4337a76bd87c810c60293d4fb940ee57ebc31707. --- pyproject.toml | 4 +- .../changelog.d/task-cleanup-dex-part01.rst | 4 +- .../cloner/newton_clone_utils.py | 71 +++++++++++++++++++ .../isaaclab_newton/cloner/replicate.py | 8 ++- .../isaaclab_newton/physics/newton_manager.py | 4 +- .../physics/visualization_builder.py | 3 +- .../test/cloner/test_rename_builder_labels.py | 20 +++++- 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 650018fe4a3f..d17b40da9784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ torchaudio = "2.11.0" ovphysx = "0.5.2+head.f62c22207c" ovrtx = ">=0.3.0,<0.4.0" # Newton git commit + matching warp-lang prerelease (upgraded together). -newton = "d1b718ea0b762aec06255808a67a855e2f9342f8" +newton = "c7ae7c7648cd0717df39e5c94b95d5a02c997320" warp = "1.15.0.dev20260626" [tool.ruff] @@ -335,7 +335,7 @@ prerelease = "allow" # the CUDA indexes via [tool.uv.sources]. Values mirror [tool.isaaclab.versions] above. override-dependencies = [ "numpy>=2", - "newton[sim] @ git+https://github.com/newton-physics/newton.git@d1b718ea0b762aec06255808a67a855e2f9342f8", + "newton[sim] @ git+https://github.com/newton-physics/newton.git@c7ae7c7648cd0717df39e5c94b95d5a02c997320", # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin. "newton-usd-schemas>=0.3.1", "torch==2.11.0", diff --git a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst index adab57ae9d02..bbce58fe244c 100644 --- a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst +++ b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst @@ -5,6 +5,4 @@ Fixed from unvalidated cubric adapter versions. * Fixed Newton clone imports creating empty MuJoCo custom-frequency rows from - ignored environment subtrees. The Newton pin is updated to a build whose - custom-frequency USD traversal honors ``ignore_paths`` (pulling in MuJoCo and - mujoco-warp 3.10), replacing the earlier import-scoping workaround. + ignored environment subtrees. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index a2a3435f2468..1a48facc3064 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -5,7 +5,9 @@ from __future__ import annotations +import re from collections.abc import Callable, Sequence +from dataclasses import replace from typing import Any import torch @@ -18,6 +20,75 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors +def add_global_stage_to_builder( + builder: ModelBuilder, + stage: Usd.Stage, + ignore_paths: Sequence[str], + schema_resolvers: Sequence[Any], +) -> Any: + """Import global prims without parsing custom rows from ignored clone subtrees. + + Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency + traversal currently does not. MuJoCo frequencies are also registered from + inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered + callbacks are scoped for this import and restored afterward. + """ + ignored_patterns = tuple(re.compile(path) for path in ignore_paths) + original_filters = { + frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() + } + + def _scope_filter(callback): + if callback is None: + return None + + def _filtered(prim: Usd.Prim, context: dict[str, Any]) -> bool: + prim_path = str(prim.GetPath()) + if any(pattern.match(prim_path) for pattern in ignored_patterns): + return False + return callback(prim, context) + + return _filtered + + for frequency_key, callback in original_filters.items(): + builder.custom_frequencies[frequency_key].usd_prim_filter = _scope_filter(callback) + + original_add_custom_frequency = builder.add_custom_frequency + had_instance_override = "add_custom_frequency" in vars(builder) + original_instance_override = vars(builder).get("add_custom_frequency") + + def _add_scoped_custom_frequency(frequency: ModelBuilder.CustomFrequency) -> None: + frequency_key = frequency.key + existing = builder.custom_frequencies.get(frequency_key) + if existing is not None: + if ( + original_filters[frequency_key] is not frequency.usd_prim_filter + or existing.usd_entry_expander is not frequency.usd_entry_expander + ): + raise ValueError(f"Custom frequency '{frequency_key}' is already registered with different callbacks.") + return + + original_filters[frequency_key] = frequency.usd_prim_filter + original_add_custom_frequency(replace(frequency, usd_prim_filter=_scope_filter(frequency.usd_prim_filter))) + + setattr(builder, "add_custom_frequency", _add_scoped_custom_frequency) + try: + return builder.add_usd( + stage, + ignore_paths=ignore_paths, + schema_resolvers=schema_resolvers, + ) + finally: + if had_instance_override: + setattr(builder, "add_custom_frequency", original_instance_override) + else: + delattr(builder, "add_custom_frequency") + for frequency_key, callback in original_filters.items(): + frequency = builder.custom_frequencies.get(frequency_key) + if frequency is not None: + frequency.usd_prim_filter = callback + + def build_source_builders( stage: Usd.Stage, sources: Sequence[str], diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index 23e5d7f9cf92..a15622cff6a3 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -20,6 +20,7 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors from isaaclab_newton.cloner.newton_clone_utils import ( + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -61,10 +62,11 @@ def _build_newton_builder_from_mapping( manager_cls = PhysicsManager._sim.physics_manager builder = manager_cls.create_builder(up_axis=up_axis) - stage_info = builder.add_usd( + stage_info = add_global_stage_to_builder( + builder, stage, - ignore_paths=["/World/envs", *sources], - schema_resolvers=schema_resolvers, + ["/World/envs", *sources], + schema_resolvers, ) replace_newton_builder_shape_colors(builder, stage) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 3929677b25c1..8e3435d29384 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -76,7 +76,7 @@ def _paused_gc(): from isaaclab.utils.timer import Timer from isaaclab.utils.version import has_kit -from isaaclab_newton.cloner.newton_clone_utils import replicate_builder_mapping +from isaaclab_newton.cloner.newton_clone_utils import add_global_stage_to_builder, replicate_builder_mapping from isaaclab_newton.physics.visualization_builder import build_visualization_builder_from_stage_envs from .newton_manager_cfg import NewtonCfg, NewtonShapeCfg @@ -1407,7 +1407,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) ignore_paths = [path for _, path in env_paths] - builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) + add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index 1dbad55df711..ab6fd3fed45c 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -17,6 +17,7 @@ from isaaclab.sim.utils.transforms import resolve_prim_pose from isaaclab_newton.cloner.newton_clone_utils import ( + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -43,7 +44,7 @@ def build_visualization_builder_from_stage_envs( quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] builder = ModelBuilder(up_axis=up_axis) - builder.add_usd(stage, ignore_paths=["/World/envs", *sources], schema_resolvers=schema_resolvers) + add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) source_builders = build_source_builders( stage, sources, diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index 965ce44735bb..d75d2fd3df3c 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -13,6 +13,7 @@ from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module from isaaclab_newton.cloner.newton_clone_utils import ( _BUILTIN_LABEL_TYPES, + add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -359,9 +360,10 @@ def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): self._define_robot(stage, "/World/global/Robot") builder = newton.ModelBuilder() - builder.add_usd(stage, ignore_paths=["/World/envs"], schema_resolvers=[]) + add_global_stage_to_builder(builder, stage, ["/World/envs"], []) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 1) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 1) + self.assertNotIn("add_custom_frequency", vars(builder)) reference_builder = newton.ModelBuilder() SolverMuJoCo.register_custom_attributes(reference_builder) @@ -392,6 +394,22 @@ def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 3) self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 3) + def test_global_import_restores_custom_frequencies_when_import_fails(self): + stage = Usd.Stage.CreateInMemory() + builder = newton.ModelBuilder() + SolverMuJoCo.register_custom_attributes(builder) + original_filters = { + frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() + } + + with mock.patch.object(builder, "add_usd", side_effect=RuntimeError("expected import failure")): + with self.assertRaisesRegex(RuntimeError, "expected import failure"): + add_global_stage_to_builder(builder, stage, ["/World/envs"], []) + + self.assertNotIn("add_custom_frequency", vars(builder)) + for frequency_key, callback in original_filters.items(): + self.assertIs(builder.custom_frequencies[frequency_key].usd_prim_filter, callback) + class TestVisualizationClonePlan(unittest.TestCase): @staticmethod From adb8998dccf9c10c8057ecef3a351054491648fb Mon Sep 17 00:00:00 2001 From: jichuanh Date: Fri, 17 Jul 2026 10:42:31 -0700 Subject: [PATCH 11/14] Revert "Add skip changelog fragment for the isaaclab test-only change" This reverts commit 345dd787c179b78df902af06b92c56da28aad9ff. --- source/isaaclab/changelog.d/task-cleanup-dex-part01.skip | 1 - 1 file changed, 1 deletion(-) delete mode 100644 source/isaaclab/changelog.d/task-cleanup-dex-part01.skip diff --git a/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip b/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip deleted file mode 100644 index 83f20e7e4b2d..000000000000 --- a/source/isaaclab/changelog.d/task-cleanup-dex-part01.skip +++ /dev/null @@ -1 +0,0 @@ -.. Test-only change in this package (uv-run CLI test guarding the [tool.uv] override wiring); no changelog entry, no version bump. From 8eac687b3b04b60fa5cfa1ef0b5780859b555c8d Mon Sep 17 00:00:00 2001 From: jichuanh Date: Tue, 21 Jul 2026 12:02:35 -0700 Subject: [PATCH 12/14] Drop the ignore_paths cloner shim superseded by the Newton bump Newton commit 81cdcfc (merged via the develop pin bump) contains the upstream fix that makes ModelBuilder.add_usd honor ignore_paths in the custom-frequency traversal (newton#3406), so the scoped-import shim and its tests are no longer needed: the three call sites go back to direct add_usd calls. The behavior stays regression-tested inside Newton. --- .../changelog.d/task-cleanup-dex-part01.rst | 3 - .../cloner/newton_clone_utils.py | 71 ---------------- .../isaaclab_newton/cloner/replicate.py | 8 +- .../isaaclab_newton/physics/newton_manager.py | 4 +- .../physics/visualization_builder.py | 3 +- .../test/cloner/test_rename_builder_labels.py | 83 +------------------ 6 files changed, 7 insertions(+), 165 deletions(-) diff --git a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst index bbce58fe244c..af5a8a106c46 100644 --- a/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst +++ b/source/isaaclab_newton/changelog.d/task-cleanup-dex-part01.rst @@ -3,6 +3,3 @@ Fixed * Fixed detached articulation links with Newton and Isaac RTX by falling back from unvalidated cubric adapter versions. - -* Fixed Newton clone imports creating empty MuJoCo custom-frequency rows from - ignored environment subtrees. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index 882d300d9c50..586746744398 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -5,10 +5,8 @@ from __future__ import annotations -import re import warnings from collections.abc import Callable, Sequence -from dataclasses import replace from typing import Any import torch @@ -70,75 +68,6 @@ def _unauthored_collision_mesh_shapes(builder: ModelBuilder, authored_shape_indi ] -def add_global_stage_to_builder( - builder: ModelBuilder, - stage: Usd.Stage, - ignore_paths: Sequence[str], - schema_resolvers: Sequence[Any], -) -> Any: - """Import global prims without parsing custom rows from ignored clone subtrees. - - Newton's built-in USD import honors ``ignore_paths``, but its custom-frequency - traversal currently does not. MuJoCo frequencies are also registered from - inside :meth:`ModelBuilder.add_usd`, so both existing and newly registered - callbacks are scoped for this import and restored afterward. - """ - ignored_patterns = tuple(re.compile(path) for path in ignore_paths) - original_filters = { - frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() - } - - def _scope_filter(callback): - if callback is None: - return None - - def _filtered(prim: Usd.Prim, context: dict[str, Any]) -> bool: - prim_path = str(prim.GetPath()) - if any(pattern.match(prim_path) for pattern in ignored_patterns): - return False - return callback(prim, context) - - return _filtered - - for frequency_key, callback in original_filters.items(): - builder.custom_frequencies[frequency_key].usd_prim_filter = _scope_filter(callback) - - original_add_custom_frequency = builder.add_custom_frequency - had_instance_override = "add_custom_frequency" in vars(builder) - original_instance_override = vars(builder).get("add_custom_frequency") - - def _add_scoped_custom_frequency(frequency: ModelBuilder.CustomFrequency) -> None: - frequency_key = frequency.key - existing = builder.custom_frequencies.get(frequency_key) - if existing is not None: - if ( - original_filters[frequency_key] is not frequency.usd_prim_filter - or existing.usd_entry_expander is not frequency.usd_entry_expander - ): - raise ValueError(f"Custom frequency '{frequency_key}' is already registered with different callbacks.") - return - - original_filters[frequency_key] = frequency.usd_prim_filter - original_add_custom_frequency(replace(frequency, usd_prim_filter=_scope_filter(frequency.usd_prim_filter))) - - setattr(builder, "add_custom_frequency", _add_scoped_custom_frequency) - try: - return builder.add_usd( - stage, - ignore_paths=ignore_paths, - schema_resolvers=schema_resolvers, - ) - finally: - if had_instance_override: - setattr(builder, "add_custom_frequency", original_instance_override) - else: - delattr(builder, "add_custom_frequency") - for frequency_key, callback in original_filters.items(): - frequency = builder.custom_frequencies.get(frequency_key) - if frequency is not None: - frequency.usd_prim_filter = callback - - def build_source_builders( stage: Usd.Stage, sources: Sequence[str], diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index a15622cff6a3..23e5d7f9cf92 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -20,7 +20,6 @@ from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors from isaaclab_newton.cloner.newton_clone_utils import ( - add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -62,11 +61,10 @@ def _build_newton_builder_from_mapping( manager_cls = PhysicsManager._sim.physics_manager builder = manager_cls.create_builder(up_axis=up_axis) - stage_info = add_global_stage_to_builder( - builder, + stage_info = builder.add_usd( stage, - ["/World/envs", *sources], - schema_resolvers, + ignore_paths=["/World/envs", *sources], + schema_resolvers=schema_resolvers, ) replace_newton_builder_shape_colors(builder, stage) diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index d678b31c37eb..1822fa8acad6 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -76,7 +76,7 @@ def _paused_gc(): from isaaclab.utils.timer import Timer from isaaclab.utils.version import has_kit -from isaaclab_newton.cloner.newton_clone_utils import add_global_stage_to_builder, replicate_builder_mapping +from isaaclab_newton.cloner.newton_clone_utils import replicate_builder_mapping from isaaclab_newton.physics.visualization_builder import build_visualization_builder_from_stage_envs from .newton_manager_cfg import NewtonCfg, NewtonShapeCfg @@ -1424,7 +1424,7 @@ def instantiate_builder_from_stage(cls): else: # Load everything except the env subtrees (ground plane, lights, etc.) ignore_paths = [path for _, path in env_paths] - add_global_stage_to_builder(builder, stage, ignore_paths, schema_resolvers) + builder.add_usd(stage, ignore_paths=ignore_paths, schema_resolvers=schema_resolvers) replace_newton_builder_shape_colors(builder, stage) _, proto_path = env_paths[0] diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index ab6fd3fed45c..1dbad55df711 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -17,7 +17,6 @@ from isaaclab.sim.utils.transforms import resolve_prim_pose from isaaclab_newton.cloner.newton_clone_utils import ( - add_global_stage_to_builder, build_source_builders, rename_builder_labels, replicate_builder_mapping, @@ -44,7 +43,7 @@ def build_visualization_builder_from_stage_envs( quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) schema_resolvers = [SchemaResolverNewton(), SchemaResolverPhysx()] builder = ModelBuilder(up_axis=up_axis) - add_global_stage_to_builder(builder, stage, ["/World/envs", *sources], schema_resolvers) + builder.add_usd(stage, ignore_paths=["/World/envs", *sources], schema_resolvers=schema_resolvers) source_builders = build_source_builders( stage, sources, diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index d75d2fd3df3c..99e88fe18943 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -13,15 +13,13 @@ from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module from isaaclab_newton.cloner.newton_clone_utils import ( _BUILTIN_LABEL_TYPES, - add_global_stage_to_builder, - build_source_builders, rename_builder_labels, replicate_builder_mapping, ) from isaaclab_newton.physics import visualization_builder as visualization_builder_module from newton.solvers import SolverMuJoCo -from pxr import Sdf, Usd, UsdGeom, UsdPhysics, Vt +from pxr import Usd, UsdGeom from isaaclab.cloner import ClonePlan @@ -332,85 +330,6 @@ def test_inactive_source_rows_are_ignored(self): self.assertEqual(builder.geometry_sources_for_world(1), []) -class TestIgnoredCloneCustomFrequencies(unittest.TestCase): - @staticmethod - def _define_robot(stage: Usd.Stage, root_path: str) -> None: - base = UsdGeom.Xform.Define(stage, f"{root_path}/base").GetPrim() - link = UsdGeom.Xform.Define(stage, f"{root_path}/link").GetPrim() - UsdPhysics.RigidBodyAPI.Apply(base) - UsdPhysics.RigidBodyAPI.Apply(link) - UsdPhysics.ArticulationRootAPI.Apply(base) - - joint_path = f"{root_path}/joint" - joint = UsdPhysics.RevoluteJoint.Define(stage, joint_path) - joint.CreateAxisAttr().Set("Z") - joint.CreateBody0Rel().SetTargets([Sdf.Path(f"{root_path}/base")]) - joint.CreateBody1Rel().SetTargets([Sdf.Path(f"{root_path}/link")]) - - tendon = stage.DefinePrim(f"{root_path}/tendon", "MjcTendon") - tendon.CreateAttribute("mjc:type", Sdf.ValueTypeNames.Token, True).Set("fixed") - tendon.CreateRelationship("mjc:path", True).SetTargets([Sdf.Path(joint_path)]) - tendon.CreateAttribute("mjc:path:indices", Sdf.ValueTypeNames.IntArray, True).Set(Vt.IntArray([0])) - tendon.CreateAttribute("mjc:path:coef", Sdf.ValueTypeNames.DoubleArray, True).Set(Vt.DoubleArray([1.0])) - - def test_global_import_does_not_create_tendon_rows_from_ignored_envs(self): - stage = Usd.Stage.CreateInMemory() - for env_id in range(2): - self._define_robot(stage, f"/World/envs/env_{env_id}/Robot") - self._define_robot(stage, "/World/global/Robot") - - builder = newton.ModelBuilder() - add_global_stage_to_builder(builder, stage, ["/World/envs"], []) - self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 1) - self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 1) - self.assertNotIn("add_custom_frequency", vars(builder)) - - reference_builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(reference_builder) - for frequency_key, reference_frequency in reference_builder.custom_frequencies.items(): - self.assertIs( - builder.custom_frequencies[frequency_key].usd_prim_filter, - reference_frequency.usd_prim_filter, - ) - - source_path = "/World/envs/env_0/Robot" - source_builders = build_source_builders( - stage, - [source_path], - newton.ModelBuilder, - [], - simplify_meshes=False, - ) - replicate_builder_mapping( - builder, - [source_path], - torch.ones((1, 2), dtype=torch.bool), - torch.zeros((2, 3)), - torch.tensor([[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]]), - source_builders, - ) - - self.assertEqual(source_builders[source_path]._custom_frequency_counts["mujoco:tendon"], 1) - self.assertEqual(builder._custom_frequency_counts["mujoco:tendon"], 3) - self.assertEqual(builder._custom_frequency_counts["mujoco:tendon_joint"], 3) - - def test_global_import_restores_custom_frequencies_when_import_fails(self): - stage = Usd.Stage.CreateInMemory() - builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(builder) - original_filters = { - frequency_key: frequency.usd_prim_filter for frequency_key, frequency in builder.custom_frequencies.items() - } - - with mock.patch.object(builder, "add_usd", side_effect=RuntimeError("expected import failure")): - with self.assertRaisesRegex(RuntimeError, "expected import failure"): - add_global_stage_to_builder(builder, stage, ["/World/envs"], []) - - self.assertNotIn("add_custom_frequency", vars(builder)) - for frequency_key, callback in original_filters.items(): - self.assertIs(builder.custom_frequencies[frequency_key].usd_prim_filter, callback) - - class TestVisualizationClonePlan(unittest.TestCase): @staticmethod def _define_xform(stage, path, translation=None): From 1dbd53cbb83cf9943124c35ad9e5ef90a25302b7 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Tue, 21 Jul 2026 12:15:44 -0700 Subject: [PATCH 13/14] Drop the fake-builder scaffolding left by the removed shim tests --- .../isaaclab_newton/test/cloner/test_rename_builder_labels.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index 99e88fe18943..70801dbd099c 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -59,7 +59,6 @@ def __init__(self, up_axis=None): references="world", ), } - self.custom_frequencies = {} self.geometry_sources = [] self.world_slices = [] self._current_world = None @@ -75,9 +74,6 @@ def begin_world(self): def end_world(self): self._current_world = None - def add_custom_frequency(self, frequency): - self.custom_frequencies[frequency.key] = frequency - def add_usd(self, stage, root_path=None, ignore_paths=None, schema_resolvers=None, **kwargs): del stage, ignore_paths, schema_resolvers, kwargs if root_path is None: From 34430045ef451d89d382fe0164efdfb307a1d37a Mon Sep 17 00:00:00 2001 From: jichuanh Date: Tue, 21 Jul 2026 12:22:37 -0700 Subject: [PATCH 14/14] Exercise the marker registry lifecycle through the real constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the review finding that the teardown test bypassed the constructor's registry caching: construct NewtonVisualizationMarkers normally against a monkeypatched SimulationContext, tear the context down, and close twice — covering registration, cached ownership, shutdown cleanup, and idempotency in one test. --- .../test/test_newton_adapter.py | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/source/isaaclab_visualizers/test/test_newton_adapter.py b/source/isaaclab_visualizers/test/test_newton_adapter.py index 935508a2de67..d006acc5f3be 100644 --- a/source/isaaclab_visualizers/test/test_newton_adapter.py +++ b/source/isaaclab_visualizers/test/test_newton_adapter.py @@ -117,25 +117,47 @@ def test_newton_visualizer_cfg_exposes_particle_options(): assert cfg.particle_color == (0.1, 0.2, 0.3) -def test_newton_marker_close_does_not_query_simulation_context(monkeypatch: pytest.MonkeyPatch): - """Marker cleanup should remain safe during interpreter shutdown.""" +def test_newton_marker_registry_lifecycle(monkeypatch: pytest.MonkeyPatch): + """Construction caches the registry; close survives context teardown and is idempotent.""" class _Registry: - removed_groups: list[str] = [] + def __init__(self) -> None: + self.groups: dict[str, object] = {} + + def set_group(self, group_id: str, marker) -> None: + self.groups[group_id] = marker def remove_group(self, group_id: str) -> None: - self.removed_groups.append(group_id) + self.groups.pop(group_id) + + class _FakeContext: + def __init__(self, registry: _Registry) -> None: + self.vis_marker_registry = registry + + class _FakeSimulationContext: + current: object | None = None + + @classmethod + def instance(cls): + return cls.current + + registry = _Registry() + monkeypatch.setattr(newton_markers.sim_utils, "SimulationContext", _FakeSimulationContext) + _FakeSimulationContext.current = _FakeContext(registry) + + marker = newton_markers.NewtonVisualizationMarkers( + newton_markers.VisualizationMarkersCfg(prim_path="/Visuals/test", markers={}), visible=False + ) + assert registry.groups == {marker.group_id: marker} - marker = object.__new__(newton_markers.NewtonVisualizationMarkers) - marker.group_id = "/Visuals/test::marker" - marker._registry = _Registry() - monkeypatch.setattr(newton_markers.sim_utils, "SimulationContext", None) + # the context is torn down before markers close during interpreter shutdown + _FakeSimulationContext.current = None marker.close() marker.close() assert marker._registry is None - assert _Registry.removed_groups == [marker.group_id] + assert registry.groups == {} def test_newton_visualizer_cfg_exposes_world_spacing():