Source code for isaaclab_physx.cloner.replicate

# 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

from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING

import numpy as np

from omni.physx import get_physx_replicator_interface
from pxr import Sdf, Usd, UsdUtils

from isaaclab import cloner

if TYPE_CHECKING:
    from isaaclab.cloner import ClonePlan


[docs] class PhysxReplicateContext: """Apply one clone plan through the PhysX replicator.""" replicate_priority = 0
[docs] def __init__(self, stage: Usd.Stage): """Initialize the context. Args: stage: USD stage to register with the PhysX replicator. """ self.stage = stage cache = UsdUtils.StageCache.Get() cached_id = cache.GetId(stage) self._stage_id = cached_id.ToLongInt() if cached_id.IsValid() else cache.Insert(stage).ToLongInt()
def replicate(self, plan: ClonePlan) -> None: """Register the PhysX replicator for this context's plan rows. Args: plan: Replication layout shared by every clone backend. """ if plan.env_ids is None: raise ValueError("ClonePlan.env_ids is required for replication.") rows = plan.context_rows[type(self)] native_rows = set(rows) other_rows = { row for context, routed in plan.context_rows.items() if context is not type(self) for row in routed } self._replicate_mapping( sources=tuple(plan.sources[row] for row in rows), destinations=tuple(plan.destinations[row] for row in rows), env_ids=plan.env_ids, mapping=plan.clone_mask[list(rows)], has_usd_only_rows=bool(other_rows - native_rows), exclude_self_replication=True, ) def _replicate_mapping( self, sources: Sequence[str], destinations: Sequence[str], env_ids: np.ndarray, mapping: np.ndarray, has_usd_only_rows: bool, exclude_self_replication: bool, ) -> None: """Register one raw source-to-environment mapping with PhysX.""" physx_queue: list[tuple[str, str, tuple[int, ...]]] = [] expected_shape = (len(sources), len(env_ids)) if mapping.shape != expected_shape: raise ValueError(f"mapping must have shape {expected_shape}, got {mapping.shape}.") if mapping.shape[1] <= 1: return native_paths: list[str] = [] for i, src in enumerate(sources): worlds = tuple(map(int, env_ids[np.flatnonzero(mapping[i])])) if has_usd_only_rows: native_paths.append(src) native_paths.extend(destinations[i].format(world) for world in worlds) if exclude_self_replication: matched = cloner.path.match(src, destinations[i]) if matched is not None and matched.instance.isdigit(): filtered = tuple(world for world in worlds if world != int(matched.instance)) worlds = filtered if filtered else worlds physx_queue.append((src, destinations[i], worlds)) # Fully-heterogeneous 1:1 layouts have every source mapped only to its own # environment (no cross-env replication needed). Calling rep.replicate() once # per source with a single self-target is known to trigger intermittent native # heap corruption (double-free / SIGABRT) under mGPU, likely due to per-call # PhysX-internal allocations summing to a problematic total across processes. # For these layouts the source prims are already in their correct env positions # and PhysX can parse them from the stage without any replicator registration. if all(len(envs) == 1 and src == destination.format(envs[0]) for src, destination, envs in physx_queue): return physics_scene_prim = self.stage.GetPrimAtPath("/physicsScene") if physics_scene_prim.IsValid(): physics_scene_prim.CreateAttribute("physxScene:envIdInBoundsBitCount", Sdf.ValueTypeNames.Int).Set(4) current_worlds: list[int] = [] current_template: str = "" prefixes = [cloner.path.split(destination)[0] for destination in destinations] env_namespaces = [ prefix.rstrip("/") if prefix.endswith("/") else prefix.rsplit("/", 1)[0] for prefix in prefixes ] excluded_paths = ( list(dict.fromkeys(native_paths)) if has_usd_only_rows else list(dict.fromkeys(("/World/template", *env_namespaces))) ) def attach_fn(_stage_id: int): return excluded_paths def rename_fn(_replicate_path: str, i: int): return current_template.format(current_worlds[i]) def attach_end_fn(_stage_id: int): nonlocal current_template replicator = get_physx_replicator_interface() for src, destination, target_envs in physx_queue: current_template = destination current_worlds[:] = target_envs if not current_worlds: continue replicator.replicate( _stage_id, src, len(current_worlds), useEnvIds=False, useFabricForReplication=False, ) replicator.unregister_replicator(_stage_id) get_physx_replicator_interface().register_replicator(self._stage_id, attach_fn, attach_end_fn, rename_fn)
def physx_replicate( stage: Usd.Stage, sources: Sequence[str], destinations: Sequence[str], env_ids: np.ndarray, mapping: np.ndarray, positions: np.ndarray | None = None, quaternions: np.ndarray | None = None, exclude_self_replication: bool = True, ) -> None: """Replicate a raw source-to-environment mapping through PhysX. Args: stage: USD stage containing the source prims. sources: Source prim paths, one per mapping row. destinations: Destination templates containing ``"{}"``, one per mapping row. env_ids: Integer environment identifiers, shape ``[num_envs]``. mapping: Boolean source-to-environment selection, shape ``[len(sources), num_envs]``. positions: Optional environment positions [m], shape ``[num_envs, 3]``. Unused by PhysX. quaternions: Optional environment orientations in xyzw order, shape ``[num_envs, 4]``. Unused by PhysX. exclude_self_replication: Whether to omit a source environment from its own targets. """ del positions, quaternions context = PhysxReplicateContext(stage) context._replicate_mapping( sources=sources, destinations=destinations, env_ids=env_ids, mapping=mapping, has_usd_only_rows=False, exclude_self_replication=exclude_self_replication, )