Source code for isaaclab.cloner.usd

# 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 ._fabric_notices import disabled_fabric_change_notifies
from .path import split

if TYPE_CHECKING:
    from pxr import Usd

    from .clone_plan import ClonePlan


def _select_columns(env_ids: np.ndarray, mask: np.ndarray | None, row: int) -> np.ndarray:
    """Return the mask columns selected by a replication row."""
    if mask is None:
        return np.arange(len(env_ids))
    row_mask = mask if mask.ndim == 1 else mask[row]
    return np.flatnonzero(row_mask)


[docs] class UsdReplicateContext: """Apply routed clone-plan rows to one USD stage.""" # USD destinations must exist before native physics contexts consume them. replicate_priority = -100
[docs] def __init__(self, stage: Usd.Stage): """Initialize the context. Args: stage: USD stage to author replicated prim specs into. """ self.stage = stage
def replicate(self, plan: ClonePlan) -> None: """Apply this context's routed rows from a clone plan. 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)] replication_rows = [] for row in rows: columns = _select_columns(plan.env_ids, plan.clone_mask, row) target_envs = plan.env_ids[columns] positions = None if plan.positions is None else plan.positions[columns] replication_rows.append((plan.sources[row], plan.destinations[row], target_envs, positions, None)) if not replication_rows: return # Suspend Fabric's per-Sdf.CopySpec notice listener for the duration of the copy work; # no-op outside a live Kit application. with disabled_fabric_change_notifies(self.stage): self._apply(replication_rows) def _apply( self, replication_rows: list[tuple[str, str, np.ndarray, np.ndarray | None, np.ndarray | None]], ) -> None: """Author the supplied copy specs into the stage's root layer.""" # pxr must be imported after Kit starts; importing it with this module can bind # a different USD runtime before Kit initializes its plugins. from pxr import Gf, Sdf, UsdGeom, Vt # noqa: PLC0415 rl = self.stage.GetRootLayer() def dp_depth(template: str) -> int: """Return destination prim path depth for stable parent-first replication.""" dp = template.format(0) return Sdf.Path(dp).pathElementCount rows_by_depth: dict[int, list[tuple[str, str, np.ndarray, np.ndarray | None, np.ndarray | None]]] = {} for row in replication_rows: rows_by_depth.setdefault(dp_depth(row[1]), []).append(row) for depth in sorted(rows_by_depth): with Sdf.ChangeBlock(): for src, tmpl, target_envs, positions, quaternions in rows_by_depth[depth]: _, clone_suffix = split(tmpl) is_instance_root = clone_suffix == "" for column, wid in enumerate(target_envs): wid = int(wid) dp = tmpl.format(wid) Sdf.CreatePrimInLayer(rl, dp) # ``CreatePrimInLayer`` authors missing intermediate ancestors (e.g. the # ``Groceries`` scope in ``env_{}/Groceries/Object``) as ``over`` specs. A # ``def`` copied below an ``over`` ancestor never composes as defined, so # Hydra skips it and its references stay unexpanded. Promote such ancestors # to ``def``; for ancestors already defined elsewhere this is a no-op. ancestor = Sdf.Path(dp).GetParentPath() while ancestor != Sdf.Path.absoluteRootPath: ancestor_spec = rl.GetPrimAtPath(ancestor) if ancestor_spec is None or ancestor_spec.specifier != Sdf.SpecifierOver: break ancestor_spec.specifier = Sdf.SpecifierDef ancestor = ancestor.GetParentPath() if src != dp: Sdf.CopySpec(rl, Sdf.Path(src), rl, Sdf.Path(dp)) # Author positions/quaternions for instance roots only. if is_instance_root and (positions is not None or quaternions is not None): ps = rl.GetPrimAtPath(dp) op_names = [] if positions is not None: p = positions[column] t_attr = ps.GetAttributeAtPath(dp + ".xformOp:translate") if t_attr is None: t_attr = Sdf.AttributeSpec(ps, "xformOp:translate", Sdf.ValueTypeNames.Double3) t_attr.default = Gf.Vec3d(float(p[0]), float(p[1]), float(p[2])) op_names.append("xformOp:translate") if quaternions is not None: q = quaternions[column] o_attr = ps.GetAttributeAtPath(dp + ".xformOp:orient") if o_attr is None: o_attr = Sdf.AttributeSpec(ps, "xformOp:orient", Sdf.ValueTypeNames.Quatd) o_attr.default = Gf.Quatd(float(q[3]), Gf.Vec3d(float(q[0]), float(q[1]), float(q[2]))) op_names.append("xformOp:orient") if op_names: op_order = ps.GetAttributeAtPath(dp + ".xformOpOrder") or Sdf.AttributeSpec( ps, UsdGeom.Tokens.xformOpOrder, Sdf.ValueTypeNames.TokenArray ) op_order.default = Vt.TokenArray(op_names)
def usd_replicate( stage: Usd.Stage, sources: Sequence[str], destinations: Sequence[str], env_ids: np.ndarray, mask: np.ndarray | None = None, positions: np.ndarray | None = None, quaternions: np.ndarray | None = None, ) -> None: """Replicate USD prims directly for standalone tooling and tests. Production clone lifecycles route a :class:`~isaaclab.cloner.ClonePlan` through :meth:`UsdReplicateContext.replicate`; this wrapper retains direct control over raw mappings for tools that do not own a clone plan. Args: stage: USD stage. sources: Source prim paths. destinations: Destination formattable templates with ``"{}"`` for env index. env_ids: Environment indices. mask: Optional per-source or shared mask. ``None`` selects all. positions: Optional positions [m], shape ``[E, 3]``. Authored as ``xformOp:translate`` only for env-instance root destinations (``.../env_{}``). quaternions: Optional orientations in xyzw order, shape ``[E, 4]``. Authored as ``xformOp:orient`` only for env-instance root destinations (``.../env_{}``). """ replication_rows = [] for row, source in enumerate(sources): columns = _select_columns(env_ids, mask, row) target_envs = env_ids[columns] row_positions = None if positions is None else positions[columns] row_quaternions = None if quaternions is None else quaternions[columns] replication_rows.append((source, destinations[row], target_envs, row_positions, row_quaternions)) context = UsdReplicateContext(stage) if replication_rows: with disabled_fabric_change_notifies(stage): context._apply(replication_rows)