From ce4a49218695145d2781de56cc7a03712382721e Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Thu, 13 Aug 2026 03:39:39 -0700 Subject: [PATCH 1/7] Fix reorient prototype spawning Spawn the Shadow Hand, cube, and camera only in the source environment so the existing clone plan owns replication. Keep regex prim paths for runtime views while avoiding redundant USD authoring at large environment counts. --- .../changelog.d/fix-reorient-replication.rst | 5 +++++ .../config/shadow_hand/shadow_hand_common.py | 17 +++++++++++++---- .../shadow_hand_direct_camera_env_cfg.py | 6 +++++- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst diff --git a/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst new file mode 100644 index 000000000000..7c3a2e094e01 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed Shadow Hand reorientation scene setup authoring every environment before + backend replication. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index 74c4f0f3db77..02a75c7d7911 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -154,22 +154,30 @@ class ShadowHandManagerEventPresetCfg(PresetCfg): @configclass class ShadowHandRobotCfg(PresetCfg): - physx = SHADOW_HAND_CFG.replace(prim_path="/World/envs/env_.*/Robot").replace( + physx = SHADOW_HAND_CFG.replace( + prim_path="/World/envs/env_.*/Robot", + spawn=SHADOW_HAND_CFG.spawn.replace(spawn_path="/World/envs/env_0/Robot"), init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.5), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={".*": 0.0}, - ) + ), ) isaacsim_physx = physx # Newton robot lives in the asset (see isaaclab_assets.robots.shadow_hand); reorient # uses its default gains. The handover task consumes the same asset cfg and overrides # only the finger gains. - newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace(prim_path="/World/envs/env_.*/Robot") + newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace( + prim_path="/World/envs/env_.*/Robot", + spawn=SHADOW_HAND_NEWTON_CFG.spawn.replace(spawn_path="/World/envs/env_0/Robot"), + ) ovphysx = SHADOW_HAND_CFG.replace( prim_path="/World/envs/env_.*/Robot", # OVPhysX does not expose the fixed-tendon runtime API, so spawn without tendon overrides. - spawn=SHADOW_HAND_CFG.spawn.replace(fixed_tendons_props=None), + spawn=SHADOW_HAND_CFG.spawn.replace( + spawn_path="/World/envs/env_0/Robot", + fixed_tendons_props=None, + ), init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.5), rot=(0.0, 0.0, 0.0, 1.0), @@ -182,6 +190,7 @@ class ShadowHandRobotCfg(PresetCfg): CUBE_CFG = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( + spawn_path="/World/envs/env_0/object", usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg( kinematic_enabled=False, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index ceeaa6a5d8c7..02137ce8d73b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -75,7 +75,11 @@ class _ShadowHandBaseTiledCameraCfg(CameraCfg): ) data_types: list[str] = [] spawn: sim_utils.PinholeCameraCfg = sim_utils.PinholeCameraCfg( - focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + spawn_path="/World/envs/env_0/Camera", + focal_length=24.0, + focus_distance=400.0, + horizontal_aperture=20.955, + clipping_range=(0.1, 20.0), ) width: int = 120 height: int = 120 From 68ba63207147daee358945b5f8a14076e5749f7e Mon Sep 17 00:00:00 2001 From: Richard Lei Date: Tue, 18 Aug 2026 10:22:54 +1200 Subject: [PATCH 2/7] Resolve Newton segmentation labels through the prototype environment NewtonSegmentationMapper resolves each shape's prim path against the USD stage, which assumes every env is authored there. Scenes that spawn only the prototype and replicate in the physics backend author labels for one env, so every cloned shape resolved to UNLABELLED. Fall back to the prototype via cloner.query.path_to_source, rebasing the matched ancestor onto the clone so instance ids stay distinct per env. --- ...x-newton-segmentation-prototype-labels.rst | 9 ++ .../isaaclab_newton/renderers/segmentation.py | 72 +++++++++++++++ .../test/renderers/test_segmentation.py | 92 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 source/isaaclab_newton/changelog.d/fix-newton-segmentation-prototype-labels.rst diff --git a/source/isaaclab_newton/changelog.d/fix-newton-segmentation-prototype-labels.rst b/source/isaaclab_newton/changelog.d/fix-newton-segmentation-prototype-labels.rst new file mode 100644 index 000000000000..6619b5b9fd1a --- /dev/null +++ b/source/isaaclab_newton/changelog.d/fix-newton-segmentation-prototype-labels.rst @@ -0,0 +1,9 @@ +Fixed +^^^^^ + +* Fixed the Newton Warp renderer reporting UNLABELLED semantic and instance segmentation for every + environment but the prototype when the scene spawns only the prototype + (:attr:`~isaaclab.sim.spawners.SpawnerCfg.spawn_path`) and relies on backend replication. Shapes + cloned by the physics backend now resolve their labels through the prototype environment recorded + in the clone plan, with the matched ancestor rebased into the cloned environment so instance ids + stay distinct per environment. diff --git a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py index 04e701cb01b6..67d772a2a076 100644 --- a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py +++ b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py @@ -28,7 +28,10 @@ # Colorization (host ``random_color_from_id`` / ``pack_rgba``) and the reserved BACKGROUND / UNLABELLED # ids are shared with the RTX and OVRTX renderers to keep colorized segmentation visually consistent. +from isaaclab.cloner import path as cloner_path +from isaaclab.cloner import query as cloner_query from isaaclab.renderers.segmentation_colors import BACKGROUND_ID, UNLABELLED_ID, pack_rgba, random_color_from_id +from isaaclab.sim import SimulationContext from isaaclab.utils.timer import Timer if TYPE_CHECKING: @@ -276,6 +279,11 @@ def __init__(self, model: newton.Model, stage: Usd.Stage | None, cfg) -> None: # Cache of prim path -> (matched_labels or None); labels resolved with ancestor inheritance. self._matched_cache: dict[str, tuple[dict[SemanticType, SemanticLabels], SemanticPrimPath] | None] = {} self._mappings: dict[tuple[str, bool], NewtonSegmentationMapping] = {} + # Clone plan used to fall back to the prototype env when a replicated shape has no prim on + # the stage (backend-only replication). ``None`` when the scene was not cloned from a plan, + # or when there is no simulation context at all (the mapper is usable standalone). + sim = SimulationContext.instance() + self._clone_plan = sim.get_clone_plan() if sim is not None else None def build_mapping(self, kind: _SegKind, colorize: bool) -> None: """Build and cache the :class:`NewtonSegmentationMapping` for ``kind`` at the requested colorization.""" @@ -311,10 +319,28 @@ def _resolve_semantic_match( short-circuits the traversal immediately. All newly-visited paths are back-filled with ``result`` at the end, so sibling shapes that share an ancestry prefix resolve in O(1) on subsequent calls without re-walking the hierarchy or re-querying USD. + + When the walk finds nothing, :meth:`_resolve_via_prototype` retries against the prototype + environment, covering scenes replicated only in the physics backend. """ if prim_path in self._matched_cache: return self._matched_cache[prim_path] + result = self._walk_for_labels(prim_path) + if result is None: + result = self._resolve_via_prototype(prim_path) + # Overwrite the ``None`` the walk back-filled for this path; ancestors keep theirs so + # a sibling shape re-enters the prototype fallback rather than reusing a stale miss. + self._matched_cache[prim_path] = result + return result + + def _walk_for_labels(self, prim_path: str) -> tuple[dict[SemanticType, SemanticLabels], SemanticPrimPath] | None: + """Walk ``prim_path`` up to the stage root for the nearest ancestor passing the filter. + + The stage-only half of :meth:`_resolve_semantic_match`: it performs the traversal and the + cache back-fill described there, and returns ``None`` when no ancestor carries a matching + label — including when ``prim_path`` names no prim at all. + """ result: tuple[dict[SemanticType, SemanticLabels], SemanticPrimPath] | None = None # Paths visited this traversal that were not already in the cache; back-filled at the end. traversed: list[str] = [] @@ -347,6 +373,52 @@ def _resolve_semantic_match( self._matched_cache[prim_path] = result return result + def _resolve_via_prototype( + self, prim_path: str + ) -> tuple[dict[SemanticType, SemanticLabels], SemanticPrimPath] | None: + """Resolve a replicated shape's labels through the prototype env it was cloned from. + + Newton replicates the *model*, rewriting each cloned shape's ``shape_label`` to its per-env + path (see ``isaaclab_newton.cloner.rename_builder_labels``), but a scene that spawns only + the prototype (:attr:`~isaaclab.sim.spawners.SpawnerCfg.spawn_path`) authors USD prims — + and therefore :class:`UsdSemantics.LabelsAPI` labels — for that one env only. Those clones + would otherwise resolve to UNLABELLED, so their labels are read off the prototype instead. + + The matched ancestor is rebased back onto the clone before being returned, because + ``instance_segmentation`` groups by that path: leaving it on the prototype side would + collapse every environment into a single instance id. + + Returns: + The prototype's ``(filtered_labels, matched_ancestor_path)`` with the ancestor rebased + into ``prim_path``'s environment, or ``None`` when there is no clone plan, ``prim_path`` + is not a clone, or the prototype itself is unlabelled. + """ + if self._clone_plan is None: + return None + try: + resolved = cloner_query.path_to_source(self._clone_plan, prim_path) + except ValueError: + # Ambiguous ownership (several equally-near destination templates) — treat as unlabelled + # rather than guessing which prototype the shape came from. + return None + if resolved is None: + return None + source_root, _, asset_suffix = resolved + prototype_path = source_root + asset_suffix + if prototype_path == prim_path: + # Already the prototype: the stage walk was authoritative and found nothing. + return None + match = self._resolve_semantic_match(prototype_path) + if match is None: + return None + matched, ancestor_path = match + # ``asset_suffix`` is the part of ``prim_path`` below the destination template, so trimming + # it yields this clone's counterpart of ``source_root``. An ancestor above ``source_root`` + # (a label authored outside the cloned subtree) is genuinely shared and ``rebase`` leaves + # it alone, keeping such shapes in one instance group across envs. + clone_root = prim_path[: len(prim_path) - len(asset_suffix)] if asset_suffix else prim_path + return matched, cloner_path.rebase(ancestor_path, source_root, clone_root) + def _apply_filter(self, labels: dict[SemanticType, SemanticLabels]) -> dict[SemanticType, SemanticLabels]: """Restrict ``labels`` (``{type: [labels]}``) to the types/labels passing the semantic filter. diff --git a/source/isaaclab_newton/test/renderers/test_segmentation.py b/source/isaaclab_newton/test/renderers/test_segmentation.py index f8296cbc5d03..0ae1af7dbd76 100644 --- a/source/isaaclab_newton/test/renderers/test_segmentation.py +++ b/source/isaaclab_newton/test/renderers/test_segmentation.py @@ -174,6 +174,98 @@ def counting_get_labels(prim): assert not duplicates, f"Prims queried more than once (ancestor cache missed): {duplicates}" +def _prototype_only_scene(): + """A scene replicated by the physics backend only: USD prims exist for env_0 alone. + + Mirrors a cfg that sets :attr:`~isaaclab.sim.spawners.SpawnerCfg.spawn_path` to the prototype + env, so the clone plan spreads the asset to env_1 while the stage never gains env_1 prims. + Returns the stage, the per-shape prim-path list, and the matching clone plan. + """ + import torch + + from isaaclab.cloner import ClonePlan + + stage = Usd.Stage.CreateInMemory() + # Only the prototype env is authored; env_1 exists in the Newton model alone. + for path in ("/World/envs/env_0/Robot/pole/geom", "/World/envs/env_0/Robot/cart/geom", "/World/ground/geom"): + stage.DefinePrim(path, "Mesh") + add_labels(stage.GetPrimAtPath("/World/envs/env_0/Robot"), labels=["cartpole"], instance_name="class") + + # ``shape_label`` still names every env: Newton's replication renames the cloned shapes. + shape_paths = [ + "/World/envs/env_0/Robot/pole/geom", + "/World/envs/env_0/Robot/cart/geom", + "/World/envs/env_1/Robot/pole/geom", + "/World/envs/env_1/Robot/cart/geom", + "/World/ground/geom", + ] + plan = ClonePlan( + sources=("/World/envs/env_0/Robot",), + destinations=("/World/envs/env_{}/Robot",), + clone_mask=torch.ones(1, 2, dtype=torch.bool), + env_ids=torch.tensor([0, 1]), + ) + return stage, shape_paths, plan + + +def _mapper_with_plan(shape_paths, stage, plan, **cfg_overrides): + """Build a mapper that sees ``plan``, standing in for a live :class:`SimulationContext`.""" + from unittest.mock import patch + + import isaaclab_newton.renderers.segmentation as _segmentation_mod + + sim = SimpleNamespace(get_clone_plan=lambda: plan) + with patch.object(_segmentation_mod.SimulationContext, "instance", staticmethod(lambda: sim)): + return NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(**cfg_overrides)) + + +def test_semantic_labels_resolve_through_prototype_env(): + """Backend-replicated shapes inherit the prototype env's class id instead of going UNLABELLED. + + When only the prototype env is authored on the stage, env_1's shapes have no prim to walk, so + before the fix they all resolved to UNLABELLED and rendered as background. + """ + stage, shape_paths, plan = _prototype_only_scene() + mapper = _mapper_with_plan(shape_paths, stage, plan) + mapper.build_mapping("semantic_segmentation", colorize=False) + mapping = mapper.get_mapping("semantic_segmentation", colorize=False) + + ids = mapping.shape_to_id.numpy().tolist() + # Every robot shape, prototype or clone, shares the one cartpole class id. + assert ids[0] == ids[1] == ids[2] == ids[3] + assert ids[0] >= 2 + assert mapping.info["idToLabels"][ids[0]] == {"class": "cartpole"} + # The un-cloned ground plane is unaffected by the fallback. + assert ids[4] == UNLABELLED_ID + + +def test_instance_ids_stay_distinct_per_env_through_prototype(): + """The prototype fallback rebases the matched ancestor, so each env keeps its own instance id.""" + stage, shape_paths, plan = _prototype_only_scene() + mapper = _mapper_with_plan(shape_paths, stage, plan) + mapper.build_mapping("instance_segmentation", colorize=False) + mapping = mapper.get_mapping("instance_segmentation", colorize=False) + + ids = mapping.shape_to_id.numpy().tolist() + assert ids[0] == ids[1], "prototype pole and cart are one instance" + assert ids[2] == ids[3], "cloned pole and cart are one instance" + assert ids[2] != ids[0], "the clone must not collapse into the prototype's instance" + assert mapping.info["idToLabels"][ids[0]] == "/World/envs/env_0/Robot" + assert mapping.info["idToLabels"][ids[2]] == "/World/envs/env_1/Robot" + assert mapping.info["idToSemantics"][ids[2]] == {"class": "cartpole"} + assert ids[4] == UNLABELLED_ID + + +def test_prototype_fallback_respects_semantic_filter(): + """A clone is UNLABELLED when the prototype's labels are filtered out, not silently labelled.""" + stage, shape_paths, plan = _prototype_only_scene() + mapper = _mapper_with_plan(shape_paths, stage, plan, semantic_filter=["shape"]) + mapper.build_mapping("semantic_segmentation", colorize=False) + mapping = mapper.get_mapping("semantic_segmentation", colorize=False) + + assert mapping.shape_to_id.numpy().tolist() == [UNLABELLED_ID] * len(shape_paths) + + def test_semantic_segmentation_mapping_overrides_color(): """``semantic_segmentation_mapping`` forces the class color and its info key.""" stage, shape_paths = _scene() From 25d90cf4b00b2e89f4644e71baa0408c7a94867a Mon Sep 17 00:00:00 2001 From: Richard Lei Date: Wed, 19 Aug 2026 14:03:51 +1200 Subject: [PATCH 3/7] Clean-up NewtonSegmentationMapper._resolve_via_prototype logic --- .../renderers/newton_warp_renderer.py | 3 +- .../isaaclab_newton/renderers/segmentation.py | 43 +++++++++---------- .../test/renderers/test_segmentation.py | 38 +++++++++------- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py b/source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py index f79795207019..d88b6eca6c9f 100644 --- a/source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py +++ b/source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py @@ -508,7 +508,8 @@ def create_render_data(self, spec: CameraRenderSpec) -> RenderData: or RenderBufferKind.INSTANCE_SEGMENTATION in spec.cfg.data_types ): if self._seg_mapper is None: - self._seg_mapper = NewtonSegmentationMapper(self._newton_model, self._stage, self.cfg) + clone_plan = SimulationContext.instance().get_clone_plan() + self._seg_mapper = NewtonSegmentationMapper(self._newton_model, self._stage, self.cfg, clone_plan) if RenderBufferKind.SEMANTIC_SEGMENTATION in spec.cfg.data_types: self._seg_mapper.build_mapping( RenderBufferKind.SEMANTIC_SEGMENTATION, bool(self.cfg.colorize_semantic_segmentation) diff --git a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py index 67d772a2a076..d24a189c0410 100644 --- a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py +++ b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py @@ -31,12 +31,12 @@ from isaaclab.cloner import path as cloner_path from isaaclab.cloner import query as cloner_query from isaaclab.renderers.segmentation_colors import BACKGROUND_ID, UNLABELLED_ID, pack_rgba, random_color_from_id -from isaaclab.sim import SimulationContext from isaaclab.utils.timer import Timer if TYPE_CHECKING: import newton + from isaaclab.cloner import ClonePlan from pxr import Usd _UNLABELLED_COLOR: int = 0xFF000000 @@ -257,8 +257,8 @@ def convert_shape_index_to_output(self, shape_index: wp.array, out_view: wp.arra class NewtonSegmentationMapper: """Builds per-shape segmentation lookup tables from a Newton model and its USD stage.""" - def __init__(self, model: newton.Model, stage: Usd.Stage | None, cfg) -> None: - """Initialize the mapper from the Newton model, USD stage, and renderer config. + def __init__(self, model: newton.Model, stage: Usd.Stage | None, cfg, clone_plan: ClonePlan) -> None: + """Initialize the mapper from the Newton model, USD stage, renderer config, and clone plan. Construction is cheap — it only captures references and snapshots ``model.shape_label``. Call :meth:`build_mapping` to do the actual per-shape USD resolution and id assignment. @@ -268,6 +268,9 @@ def __init__(self, model: newton.Model, stage: Usd.Stage | None, cfg) -> None: stage: The live USD stage used to read :class:`UsdSemantics.LabelsAPI` labels. May be ``None`` in stageless setups, in which case every shape is treated as unlabelled. cfg: Renderer config exposing ``semantic_filter`` and ``semantic_segmentation_mapping``. + clone_plan: The scene's published :class:`~isaaclab.cloner.ClonePlan`, used to fall back + to the prototype env when a replicated shape has no prim on the stage (backend-only + replication). See :meth:`_resolve_via_prototype`. """ self._model = model self._stage = stage @@ -279,11 +282,7 @@ def __init__(self, model: newton.Model, stage: Usd.Stage | None, cfg) -> None: # Cache of prim path -> (matched_labels or None); labels resolved with ancestor inheritance. self._matched_cache: dict[str, tuple[dict[SemanticType, SemanticLabels], SemanticPrimPath] | None] = {} self._mappings: dict[tuple[str, bool], NewtonSegmentationMapping] = {} - # Clone plan used to fall back to the prototype env when a replicated shape has no prim on - # the stage (backend-only replication). ``None`` when the scene was not cloned from a plan, - # or when there is no simulation context at all (the mapper is usable standalone). - sim = SimulationContext.instance() - self._clone_plan = sim.get_clone_plan() if sim is not None else None + self._clone_plan = clone_plan def build_mapping(self, kind: _SegKind, colorize: bool) -> None: """Build and cache the :class:`NewtonSegmentationMapping` for ``kind`` at the requested colorization.""" @@ -390,25 +389,25 @@ def _resolve_via_prototype( Returns: The prototype's ``(filtered_labels, matched_ancestor_path)`` with the ancestor rebased - into ``prim_path``'s environment, or ``None`` when there is no clone plan, ``prim_path`` - is not a clone, or the prototype itself is unlabelled. + into ``prim_path``'s environment, or ``None`` when ``prim_path`` is not a clone, or the + prototype itself is unlabelled. + + Raises: + ValueError: When ``prim_path`` is owned by multiple distinct, equally near destination + templates — a malformed clone plan, not a state to resolve around. """ - if self._clone_plan is None: - return None - try: - resolved = cloner_query.path_to_source(self._clone_plan, prim_path) - except ValueError: - # Ambiguous ownership (several equally-near destination templates) — treat as unlabelled - # rather than guessing which prototype the shape came from. - return None + resolved = cloner_query.path_to_source(self._clone_plan, prim_path) if resolved is None: + # ``prim_path`` is not owned by the clone plan at all (e.g. an un-cloned ground plane or + # other static prim) — nothing to fall back to, so it stays unlabelled. return None source_root, _, asset_suffix = resolved prototype_path = source_root + asset_suffix - if prototype_path == prim_path: - # Already the prototype: the stage walk was authoritative and found nothing. - return None - match = self._resolve_semantic_match(prototype_path) + # The prototype path is where the labels actually live, so a plain stage walk — not another + # round of clone-plan resolution — is all that's needed here. When ``prim_path`` names the + # prototype's own env, this re-walks the same path the caller already walked; the cache it + # left behind makes that a cheap no-op rather than a correctness concern. + match = self._walk_for_labels(prototype_path) if match is None: return None matched, ancestor_path = match diff --git a/source/isaaclab_newton/test/renderers/test_segmentation.py b/source/isaaclab_newton/test/renderers/test_segmentation.py index 0ae1af7dbd76..1fda16ec3a5a 100644 --- a/source/isaaclab_newton/test/renderers/test_segmentation.py +++ b/source/isaaclab_newton/test/renderers/test_segmentation.py @@ -12,9 +12,12 @@ import pytest pytest.importorskip("numpy") +pytest.importorskip("torch") pytest.importorskip("warp") pytest.importorskip("pxr") +import torch + from isaaclab_newton.renderers.segmentation import NewtonSegmentationMapper from pxr import Usd @@ -22,10 +25,16 @@ # The color palette / reserved ids live in core and are unit-tested there # (``isaaclab/test/renderers/test_segmentation_colors.py``); here they are only an oracle for the # mapper's info-dict keys. +from isaaclab.cloner import ClonePlan from isaaclab.renderers.segmentation_colors import BACKGROUND_ID, UNLABELLED_ID, pack_rgba, random_color_from_id from isaaclab.sim.utils.semantics import add_labels +def _empty_clone_plan() -> ClonePlan: + """A clone plan owning nothing, standing in for scenes with no replicated shapes to fall back to.""" + return ClonePlan(sources=(), destinations=(), clone_mask=torch.zeros(0, 0, dtype=torch.bool)) + + def _cfg(**overrides): """Minimal renderer-cfg stand-in exposing only the fields the mapper reads.""" base = { @@ -63,7 +72,7 @@ def _model(shape_paths): def test_semantic_segmentation_shares_class_id_across_envs(): """All cartpole shapes across envs share one class id; the unlabelled ground is UNLABELLED.""" stage, shape_paths = _scene() - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg()) + mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(), _empty_clone_plan()) mapper.build_mapping("semantic_segmentation", colorize=False) mapping = mapper.get_mapping("semantic_segmentation", colorize=False) @@ -81,7 +90,7 @@ def test_semantic_segmentation_shares_class_id_across_envs(): def test_instance_segmentation_groups_by_labelled_ancestor(): """Shapes group by their nearest labelled ancestor; idToSemantics carries the class label.""" stage, shape_paths = _scene() - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg()) + mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(), _empty_clone_plan()) mapper.build_mapping("instance_segmentation", colorize=False) mapping = mapper.get_mapping("instance_segmentation", colorize=False) @@ -97,7 +106,7 @@ def test_instance_segmentation_groups_by_labelled_ancestor(): def test_colorize_info_keys_are_color_tuples(): """With colorization, info keys are ``(r, g, b, a)`` color tuples and a color palette is built.""" stage, shape_paths = _scene() - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg()) + mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(), _empty_clone_plan()) mapper.build_mapping("semantic_segmentation", colorize=True) mapping = mapper.get_mapping("semantic_segmentation", colorize=True) @@ -109,7 +118,7 @@ def test_colorize_info_keys_are_color_tuples(): def test_semantic_filter_excludes_non_matching_types(): """A filter restricted to an absent type marks every shape UNLABELLED.""" stage, shape_paths = _scene() - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(semantic_filter=["shape"])) + mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(semantic_filter=["shape"]), _empty_clone_plan()) mapper.build_mapping("semantic_segmentation", colorize=False) mapping = mapper.get_mapping("semantic_segmentation", colorize=False) @@ -135,7 +144,9 @@ def test_semantic_filter_comma_separated_type_clauses(): add_labels(stage.GetPrimAtPath("/World/robot"), labels=["cartpole"], instance_name="class") add_labels(stage.GetPrimAtPath("/World/shelf"), labels=["wood"], instance_name="material") - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(semantic_filter="class:cartpole, material:wood")) + mapper = NewtonSegmentationMapper( + _model(shape_paths), stage, _cfg(semantic_filter="class:cartpole, material:wood"), _empty_clone_plan() + ) mapper.build_mapping("semantic_segmentation", colorize=False) mapping = mapper.get_mapping("semantic_segmentation", colorize=False) @@ -158,7 +169,7 @@ def test_ancestor_cache_prevents_redundant_get_labels_calls(): import isaaclab.sim.utils.semantics as _semantics_mod stage, shape_paths = _scene() - mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg()) + mapper = NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(), _empty_clone_plan()) original_get_labels = _semantics_mod.get_labels queried_paths: list[str] = [] @@ -209,14 +220,8 @@ def _prototype_only_scene(): def _mapper_with_plan(shape_paths, stage, plan, **cfg_overrides): - """Build a mapper that sees ``plan``, standing in for a live :class:`SimulationContext`.""" - from unittest.mock import patch - - import isaaclab_newton.renderers.segmentation as _segmentation_mod - - sim = SimpleNamespace(get_clone_plan=lambda: plan) - with patch.object(_segmentation_mod.SimulationContext, "instance", staticmethod(lambda: sim)): - return NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(**cfg_overrides)) + """Build a mapper that sees ``plan``.""" + return NewtonSegmentationMapper(_model(shape_paths), stage, _cfg(**cfg_overrides), plan) def test_semantic_labels_resolve_through_prototype_env(): @@ -271,7 +276,10 @@ def test_semantic_segmentation_mapping_overrides_color(): stage, shape_paths = _scene() override = (255, 36, 66, 255) mapper = NewtonSegmentationMapper( - _model(shape_paths), stage, _cfg(semantic_segmentation_mapping={"class:cartpole": override}) + _model(shape_paths), + stage, + _cfg(semantic_segmentation_mapping={"class:cartpole": override}), + _empty_clone_plan(), ) mapper.build_mapping("semantic_segmentation", colorize=True) mapping = mapper.get_mapping("semantic_segmentation", colorize=True) From ec4af6cae45891bf3aac4281804eb5d1b5c29854 Mon Sep 17 00:00:00 2001 From: Mustafa Haiderbhai Date: Wed, 19 Aug 2026 12:22:45 -0700 Subject: [PATCH 4/7] Fix pre-commit formatting --- .../isaaclab_newton/isaaclab_newton/renderers/segmentation.py | 3 ++- source/isaaclab_newton/test/renderers/test_segmentation.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py index d24a189c0410..efd8a1aa3b6f 100644 --- a/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py +++ b/source/isaaclab_newton/isaaclab_newton/renderers/segmentation.py @@ -36,9 +36,10 @@ if TYPE_CHECKING: import newton - from isaaclab.cloner import ClonePlan from pxr import Usd + from isaaclab.cloner import ClonePlan + _UNLABELLED_COLOR: int = 0xFF000000 """Packed RGBA color for UNLABELLED pixels: ``(0, 0, 0, 255)`` opaque black.""" diff --git a/source/isaaclab_newton/test/renderers/test_segmentation.py b/source/isaaclab_newton/test/renderers/test_segmentation.py index 1fda16ec3a5a..929e03c252e6 100644 --- a/source/isaaclab_newton/test/renderers/test_segmentation.py +++ b/source/isaaclab_newton/test/renderers/test_segmentation.py @@ -17,7 +17,6 @@ pytest.importorskip("pxr") import torch - from isaaclab_newton.renderers.segmentation import NewtonSegmentationMapper from pxr import Usd From b9c9468808e966ec1183ad0ba348b164be18ade1 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Wed, 19 Aug 2026 20:30:20 -0700 Subject: [PATCH 5/7] Fix Shadow Hand demo config ownership --- scripts/demos/hands.py | 5 ++--- .../isaaclab/test/app/test_standalone_scripts.py | 15 +++++++++++++++ .../config/shadow_hand/shadow_hand_common.py | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts/demos/hands.py b/scripts/demos/hands.py index 2ac56b46c848..756585864b4a 100644 --- a/scripts/demos/hands.py +++ b/scripts/demos/hands.py @@ -51,8 +51,7 @@ from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg # isort:skip from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG # isort:skip -from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG # isort:skip -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ShadowHandRobotCfg # isort:skip +from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG, SHADOW_HAND_NEWTON_CFG # isort:skip if TYPE_CHECKING: from isaaclab.assets import Articulation @@ -95,7 +94,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: # Origin 2 with Shadow Hand sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # -- Robot - shadow_hand_cfg = ShadowHandRobotCfg().newton_mjwarp if args_cli.physics == "newton_mjwarp" else SHADOW_HAND_CFG + shadow_hand_cfg = SHADOW_HAND_NEWTON_CFG if args_cli.physics == "newton_mjwarp" else SHADOW_HAND_CFG shadow_hand_cfg = shadow_hand_cfg.replace(prim_path="/World/Origin2/Robot") shadow_hand = shadow_hand_cfg.class_type(shadow_hand_cfg) diff --git a/source/isaaclab/test/app/test_standalone_scripts.py b/source/isaaclab/test/app/test_standalone_scripts.py index 99dd7ab14936..5e5fa312bd7b 100644 --- a/source/isaaclab/test/app/test_standalone_scripts.py +++ b/source/isaaclab/test/app/test_standalone_scripts.py @@ -242,6 +242,21 @@ def test_commands_respect_script_launcher_capabilities(): assert "--enable_cameras" not in usd_camera_case.command() +def test_hands_demo_uses_asset_owned_shadow_hand_configs(): + """The generic hands demo must not inherit task-specific spawn policy.""" + path = script_cases.ROOT / "scripts/demos/hands.py" + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + imports = { + (node.module, alias.name) for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) for alias in node.names + } + + assert not {module for module, _ in imports if module and module.startswith("isaaclab_tasks")} + assert { + ("isaaclab_assets.robots.shadow_hand", "SHADOW_HAND_CFG"), + ("isaaclab_assets.robots.shadow_hand", "SHADOW_HAND_NEWTON_CFG"), + } <= imports + + @pytest.mark.parametrize( "relative_path", [ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index d4fd904ea3e5..c117c99bb10e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -155,7 +155,7 @@ class ShadowHandManagerEventPresetCfg(PresetCfg): @configclass class ShadowHandRobotCfg(PresetCfg): physx = SHADOW_HAND_CFG.replace( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=SHADOW_HAND_CFG.spawn.replace(spawn_path="/World/envs/env_0/Robot"), init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.5), @@ -168,7 +168,7 @@ class ShadowHandRobotCfg(PresetCfg): # uses its default gains. The handover task consumes the same asset cfg and overrides # only the finger gains. newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace( - prim_path="/World/envs/env_.*/Robot", + prim_path="{ENV_REGEX_NS}/Robot", spawn=SHADOW_HAND_NEWTON_CFG.spawn.replace(spawn_path="/World/envs/env_0/Robot"), ) ovphysx = SHADOW_HAND_CFG.replace( From 081372813dc649fd0eda3f841974c812410ff52c Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Wed, 19 Aug 2026 20:50:04 -0700 Subject: [PATCH 6/7] Update Shadow Hand replication changelog --- .../isaaclab_tasks/changelog.d/fix-reorient-replication.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst index 7c3a2e094e01..cf253e799b92 100644 --- a/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst +++ b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst @@ -1,5 +1,7 @@ Fixed ^^^^^ -* Fixed Shadow Hand reorientation scene setup authoring every environment before - backend replication. +* Fixed Shadow Hand reorientation scene setup to author only the prototype environment before + backend replication while preserving ``{ENV_REGEX_NS}`` for runtime views. +* Fixed the Newton Shadow Hand demo failing at startup after inheriting the reorientation task's + prototype spawn path. From 765543d7ffea79fbc54524a9f491fd94c1f3c66f Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Wed, 19 Aug 2026 20:58:44 -0700 Subject: [PATCH 7/7] Add Isaac Lab demo changelog fragment --- source/isaaclab/changelog.d/fix-shadow-hand-demo-config.rst | 5 +++++ .../isaaclab_tasks/changelog.d/fix-reorient-replication.rst | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 source/isaaclab/changelog.d/fix-shadow-hand-demo-config.rst diff --git a/source/isaaclab/changelog.d/fix-shadow-hand-demo-config.rst b/source/isaaclab/changelog.d/fix-shadow-hand-demo-config.rst new file mode 100644 index 000000000000..b59387adba8d --- /dev/null +++ b/source/isaaclab/changelog.d/fix-shadow-hand-demo-config.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed the Newton Shadow Hand demo failing at startup after inheriting the reorientation task's + prototype spawn path. diff --git a/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst index cf253e799b92..97321fbc24ac 100644 --- a/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst +++ b/source/isaaclab_tasks/changelog.d/fix-reorient-replication.rst @@ -3,5 +3,3 @@ Fixed * Fixed Shadow Hand reorientation scene setup to author only the prototype environment before backend replication while preserving ``{ENV_REGEX_NS}`` for runtime views. -* Fixed the Newton Shadow Hand demo failing at startup after inheriting the reorientation task's - prototype spawn path.