@@ -9,38 +9,79 @@ def usd_replicate(
99 sources : list [str ],
1010 destinations : list [str ],
1111 env_ids : torch .Tensor ,
12- mapping : torch .Tensor ,
12+ mask : torch .Tensor | None = None ,
1313 positions : torch .Tensor | None = None ,
1414 quaternions : torch .Tensor | None = None ,
1515) -> None :
16+ """Replicate USD prims to per-environment destinations.
17+
18+ Copies each source prim spec to destination templates for selected environments
19+ (``mask``). Optionally authors translate/orient from position/quaternion buffers.
20+ Replication runs in path-depth order (parents before children) for robust composition.
21+
22+ Args:
23+ stage: USD stage.
24+ sources: Source prim paths.
25+ destinations: Destination formattable templates with ``"{}"`` for env index.
26+ env_ids: Environment indices.
27+ mask: Optional per-source or shared mask. ``None`` selects all.
28+ positions: Optional positions (``[E, 3]``) -> ``xformOp:translate``.
29+ quaternions: Optional orientations (``[E, 4]``) in ``wxyz`` -> ``xformOp:orient``.
30+
31+ Returns:
32+ None
33+ """
1634 rl = stage .GetRootLayer ()
17- with Sdf .ChangeBlock ():
18- for i , src in enumerate (sources ):
19- tmpl = destinations [i ]
20- for wid in env_ids [mapping [i ]].tolist ():
21- dp = tmpl .format (wid )
22- Sdf .CreatePrimInLayer (rl , dp )
23- Sdf .CopySpec (rl , Sdf .Path (src ), rl , Sdf .Path (dp ))
24-
25- if positions is not None or quaternions is not None :
26- ps = rl .GetPrimAtPath (dp )
27- # translate
28- if positions is not None :
29- p = positions [wid ]
30- t_attr = ps .GetAttributeAtPath (dp + ".xformOp:translate" )
31- if t_attr is None :
32- t_attr = Sdf .AttributeSpec (ps , "xformOp:translate" , Sdf .ValueTypeNames .Double3 )
33- t_attr .default = Gf .Vec3d (float (p [0 ]), float (p [1 ]), float (p [2 ]))
34- # orient (always set as Quatd)
35- if quaternions is not None :
36- q = quaternions [wid ]
37- o_attr = ps .GetAttributeAtPath (dp + ".xformOp:orient" )
38- if o_attr is None :
39- o_attr = Sdf .AttributeSpec (ps , "xformOp:orient" , Sdf .ValueTypeNames .Quatd )
40- o_attr .default = Gf .Quatd (float (q [0 ]), Gf .Vec3d (float (q [1 ]), float (q [2 ]), float (q [3 ])))
41- # op order
42- order = ps .GetAttributeAtPath (dp + ".xformOpOrder" ) or Sdf .AttributeSpec (ps , UsdGeom .Tokens .xformOpOrder , Sdf .ValueTypeNames .TokenArray )
43- order .default = Vt .TokenArray (["xformOp:translate" , "xformOp:orient" , "xformOp:scale" ])
35+
36+ # Group replication by destination path depth so ancestors land before deeper paths.
37+ # This avoids composition issues for nested or interdependent specs.
38+ def dp_depth (template : str ) -> int :
39+ dp = template .format (0 )
40+ return Sdf .Path (dp ).pathElementCount
41+
42+ order = sorted (range (len (sources )), key = lambda i : dp_depth (destinations [i ]))
43+
44+ # Process in layers of equal depth, committing at each depth to stabilize composition
45+ depth_to_indices : dict [int , list [int ]] = {}
46+ for i in order :
47+ d = dp_depth (destinations [i ])
48+ depth_to_indices .setdefault (d , []).append (i )
49+
50+ for depth in sorted (depth_to_indices .keys ()):
51+ with Sdf .ChangeBlock ():
52+ for i in depth_to_indices [depth ]:
53+ src = sources [i ]
54+ tmpl = destinations [i ]
55+ # Select target environments for this source (supports None, [E], or [S, E])
56+ target_envs = env_ids if mask is None else env_ids [mask [i ]]
57+ for wid in target_envs .tolist ():
58+ dp = tmpl .format (wid )
59+ Sdf .CreatePrimInLayer (rl , dp )
60+ Sdf .CopySpec (rl , Sdf .Path (src ), rl , Sdf .Path (dp ))
61+
62+ if positions is not None or quaternions is not None :
63+ ps = rl .GetPrimAtPath (dp )
64+ op_names = []
65+ if positions is not None :
66+ p = positions [wid ]
67+ t_attr = ps .GetAttributeAtPath (dp + ".xformOp:translate" )
68+ if t_attr is None :
69+ t_attr = Sdf .AttributeSpec (ps , "xformOp:translate" , Sdf .ValueTypeNames .Double3 )
70+ t_attr .default = Gf .Vec3d (float (p [0 ]), float (p [1 ]), float (p [2 ]))
71+ op_names .append ("xformOp:translate" )
72+ if quaternions is not None :
73+ q = quaternions [wid ]
74+ o_attr = ps .GetAttributeAtPath (dp + ".xformOp:orient" )
75+ if o_attr is None :
76+ o_attr = Sdf .AttributeSpec (ps , "xformOp:orient" , Sdf .ValueTypeNames .Quatd )
77+ o_attr .default = Gf .Quatd (float (q [0 ]), Gf .Vec3d (float (q [1 ]), float (q [2 ]), float (q [3 ])))
78+ op_names .append ("xformOp:orient" )
79+ # Only author xformOpOrder for the ops we actually authored
80+ if op_names :
81+ op_order = ps .GetAttributeAtPath (dp + ".xformOpOrder" ) or Sdf .AttributeSpec (
82+ ps , UsdGeom .Tokens .xformOpOrder , Sdf .ValueTypeNames .TokenArray
83+ )
84+ op_order .default = Vt .TokenArray (op_names )
4485
4586
4687def physx_replicate (
@@ -49,16 +90,24 @@ def physx_replicate(
4990 destinations : list [str ], # e.g. ["/World/envs/env_{}/Robot", "/World/envs/env_{}/Object"]
5091 env_ids : torch .Tensor , # env_ids
5192 mapping : torch .Tensor , # (num_sources, num_envs) bool; True -> place sources[i] into world=j
52- use_env_ids : bool = True , # hint: only used when a row covers all worlds
5393 use_fabric : bool = False ,
5494) -> None :
55- """
56- PhysX replication that matches the (sources, destinations, mapping) convention.
57- For each source i, we build the per-world destination list:
58- dests_i = [ destinations[i].format(w) for w in worlds where mapping[i, w] is True ]
95+ """Replicate prims via PhysX replicator with per-row mapping.
96+
97+ Builds per-source destination lists from ``mapping`` and calls PhysX ``replicate``.
98+ Rows covering all environments use ``useEnvIds=True``; partial rows use ``False``.
99+ The replicator is registered for the call and then unregistered.
59100
60- If a row covers ALL worlds, we pass useEnvIds=True to PhysX so per-env filtering is automatic.
61- If a row is partial, we force useEnvIds=False so cross-row collisions within the same env remain possible.
101+ Args:
102+ stage: USD stage.
103+ sources: Source prim paths (``S``).
104+ destinations: Destination templates (``S``) with ``"{}"`` for env index.
105+ env_ids: Environment indices (``[E]``).
106+ mapping: Bool/int mask (``[S, E]``) selecting envs per source.
107+ use_fabric: Use Fabric for replication.
108+
109+ Returns:
110+ None
62111 """
63112
64113 stage_id = UsdUtils .StageCache .Get ().Insert (stage ).ToLongInt ()
@@ -86,16 +135,27 @@ def attach_end_fn(_stage_id: int):
86135
87136
88137def filter_collisions (
89- stage : Usd .Stage , physicsscene_path : str , collision_root_path : str , prim_paths : list [str ], global_paths : list [str ] = []
90- ):
91- """Filters collisions between clones. Clones will not collide with each other, but can collide with objects specified in global_paths.
138+ stage : Usd .Stage ,
139+ physicsscene_path : str ,
140+ collision_root_path : str ,
141+ prim_paths : list [str ],
142+ global_paths : list [str ] = []
143+ ) -> None :
144+ """Create inverted collision groups for clones.
92145
93- Args:
94- physicsscene_path (str): Path to PhysicsScene object in stage.
95- collision_root_path (str): Path to place collision groups under.
96- prim_paths (List[str]): Paths of objects to filter out collision.
97- global_paths (List[str]): Paths of objects to generate collision (e.g. ground plane).
146+ Creates one PhysicsCollisionGroup per prim under ``collision_root_path``, enabling
147+ inverted filtering so clones don't collide across groups. Optionally adds a global
148+ group that collides with all.
98149
150+ Args:
151+ stage: USD stage.
152+ physicsscene_path: Path to PhysicsScene prim.
153+ collision_root_path: Root scope for collision groups.
154+ prim_paths: Per-clone prim paths.
155+ global_paths: Optional global-collider paths.
156+
157+ Returns:
158+ None
99159 """
100160
101161 physx_scene = PhysxSchema .PhysxSceneAPI (stage .GetPrimAtPath (physicsscene_path ))
@@ -181,6 +241,23 @@ def filter_collisions(
181241
182242
183243def grid_transforms (N : int , spacing : float = 1.0 , up_axis : str = "z" , device = "cpu" ):
244+ """Create a centered grid of transforms for ``N`` instances.
245+
246+ Computes ``(x, y)`` coordinates in a roughly square grid centered at the origin
247+ with the provided spacing, places the third coordinate according to ``up_axis``,
248+ and returns identity orientations (``wxyz``) for each instance.
249+
250+ Args:
251+ N: Number of instances.
252+ spacing: Distance between neighboring grid positions.
253+ up_axis: Up axis for positions ("z", "y", or "x").
254+ device: Torch device for returned tensors.
255+
256+ Returns:
257+ A tuple ``(pos, ori)`` where:
258+ - ``pos`` is a tensor of shape ``(N, 3)`` with positions.
259+ - ``ori`` is a tensor of shape ``(N, 4)`` with identity quaternions in ``(w, x, y, z)``.
260+ """
184261 # rows/cols
185262 rows = int (math .ceil (math .sqrt (N )))
186263 cols = int (math .ceil (N / rows ))
0 commit comments