@@ -11,10 +11,9 @@ by hand would be hopelessly slow. Cloning is Isaac Lab's answer: you author a
1111small representative scene under ``/World/envs/env_n `` and the cloner expands it
1212across the rest of the env population for you, optionally with per-env variation.
1313
14- The expansion itself is performed by each physics backend's native replicator —
15- USD, PhysX, or Newton — wrapped by Isaac Lab's core :mod: `isaaclab.cloner ` module
16- behind a single uniform surface so the same user code works regardless of which
17- backend is active.
14+ The expansion itself is performed by USD and the active physics backend's native
15+ replicator, wrapped by Isaac Lab's core :mod: `isaaclab.cloner ` module behind a
16+ single uniform surface.
1817
1918.. contents :: On this page
2019 :local:
@@ -24,24 +23,29 @@ backend is active.
2423The Backend Layer
2524-----------------
2625
27- At the bottom of the stack, each backend exposes a single function that takes a
28- flat description of the world layout and materializes it on its runtime. The
29- signatures are deliberately parallel so the layers above can target every backend
30- through one interface:
26+ At the bottom of the stack, each backend exposes a raw function that takes a flat
27+ description of the world layout. These functions are useful for standalone tools
28+ and tests and deliberately have parallel signatures:
3129
3230.. code-block :: text
3331
34- backend_replicate(stage, sources, destinations, env_ids, mask , positions=None, quaternions=None, ...)
32+ backend_replicate(stage, sources, destinations, env_ids, selection , positions=None, quaternions=None, ...)
3533
3634 The arguments are parallel arrays describing the layout:
3735
3836* ``sources `` — source prim paths already authored on the stage.
3937* ``destinations `` — destination templates containing ``"{}" ``, formatted with each env id.
40- * ``env_ids `` — long tensor of target env indices.
41- * ``mask `` — bool tensor of shape ``[len(sources), num_envs] ``; ``mask[i, j] `` is
42- ``True `` when env ``j `` should be populated from source ``i ``.
38+ * ``env_ids `` — NumPy integer array of target env indices.
39+ * ``selection `` — NumPy boolean array of shape ``[len(sources), num_envs] ``;
40+ ``selection[i, j] `` is ``True `` when env ``j `` should be populated from source ``i ``.
41+ The raw USD function names this argument ``mask ``; physics functions name it ``mapping ``.
4342* ``positions `` / ``quaternions `` — optional per-env world transforms.
4443
44+ Production scene construction stores those arrays once in a
45+ :class: `~isaaclab.cloner.ClonePlan `. Simulation-owned backend contexts consume the
46+ same value through ``context.replicate(plan) ``; no backend rebuilds the mapping
47+ from a second queue of array arguments.
48+
4549
4650Standalone Examples
4751~~~~~~~~~~~~~~~~~~~
@@ -54,7 +58,7 @@ control. Production code reaches for one of the ways in
5458
5559.. code-block :: python
5660
57- import torch
61+ import numpy as np
5862 import isaaclab.sim as sim_utils
5963 from isaaclab.cloner import usd_replicate
6064
@@ -65,10 +69,10 @@ control. Production code reaches for one of the ways in
6569
6670 usd_replicate(
6771 stage,
68- sources = [ " /World/envs/env_0/Cube" ] ,
69- destinations = [ " /World/envs/env_{} /Cube" ] ,
70- env_ids = torch .arange(num_envs, device = " cuda:0 " ),
71- mask = torch .ones((1 , num_envs), dtype = torch.bool, device = " cuda:0 " ),
72+ sources = ( " /World/envs/env_0/Cube" ,) ,
73+ destinations = ( " /World/envs/env_{} /Cube" ,) ,
74+ env_ids = np .arange(num_envs),
75+ mask = np .ones((1 , num_envs), dtype = np.bool_ ),
7276 )
7377
7478 **PhysX ** — call PhysX and USD on the same sources and destinations (either order):
@@ -77,16 +81,28 @@ control. Production code reaches for one of the ways in
7781
7882 from isaaclab_physx.cloner import physx_replicate
7983
80- physx_replicate(stage, sources, destinations, env_ids, mask)
81- usd_replicate(stage, sources, destinations, env_ids, mask)
84+ sources = (" /World/envs/env_0/Cube" ,)
85+ destinations = (" /World/envs/env_{} /Cube" ,)
86+ env_ids = np.arange(num_envs)
87+ mapping = np.ones((1 , num_envs), dtype = np.bool_)
88+ physx_replicate(stage, sources, destinations, env_ids, mapping = mapping)
89+ usd_replicate(stage, sources, destinations, env_ids, mask = mapping)
8290
8391 **Newton **:
8492
8593.. code-block :: python
8694
8795 from isaaclab_newton.cloner import newton_physics_replicate
8896
89- newton_physics_replicate(stage, sources, destinations, env_ids, mapping = mask)
97+ newton_physics_replicate(stage, sources, destinations, env_ids, mapping = mapping)
98+
99+ **OvPhysX **:
100+
101+ .. code-block :: python
102+
103+ from isaaclab_ov.cloner import ovphysx_replicate
104+
105+ ovphysx_replicate(stage, sources, destinations, env_ids, mapping = mapping)
90106
91107
92108 Cloning in a Backend-Agnostic Way
@@ -106,13 +122,10 @@ code never branches on the backend.
106122ClonePlan
107123~~~~~~~~~
108124
109- A plan holds the parallel arrays a backend replicate consumes — sources,
125+ A plan holds the parallel arrays used by production clone contexts — sources,
110126destinations, mask, env ids — in one place. Conceptually it is a small table
111127where each row describes one distinct prototype-to-destination mapping; the
112- fields listed below are that table's columns. Every entry point in
113- :mod: `isaaclab.cloner ` either produces a plan, consumes a plan, or both, so a
114- quick look at the fields is the fastest way to build intuition for the rest of
115- this page:
128+ fields listed below are that table's columns:
116129
117130.. list-table ::
118131 :header-rows: 1
@@ -125,16 +138,18 @@ this page:
125138 * - ``destinations ``
126139 - Destination templates with ``"{}" `` for the env id, one per row.
127140 * - ``clone_mask ``
128- - Bool tensor ``[len(sources), num_envs] ``; ``True `` when env ``j `` comes from row ``i ``.
141+ - NumPy boolean array ``[len(sources), num_envs] ``; ``True `` when env ``j `` comes from row ``i ``.
129142 * - ``env_ids ``
130- - Long tensor of target env ids.
143+ - Optional NumPy integer array of target env ids; execution requires it .
131144 * - ``positions ``
132145 - Optional per-env world positions [m], shape ``[num_envs, 3] ``.
133146 * - ``global_paths ``
134147 - Unique prim paths for scene assets shared by every env and therefore not replicated.
148+ * - ``context_rows ``
149+ - Clone-context types mapped to the rows they consume.
135150
136- The plan is stage-agnostic by design — the same instance can be replayed against a
137- different stage, inspected by tooling, or serialized .
151+ The plan does not own a stage. Simulation-owned contexts supply their own runtime
152+ when they consume it .
138153
139154When every env is a copy of env_0:
140155
@@ -193,7 +208,7 @@ column ``j`` stands for ``env_ids[j]``, and the queries speak ids throughout.
193208
194209A plan is the *what *. Putting one together and handing it to the backends is
195210the *how *, and Isaac Lab exposes three idiomatic ways to do that. All three end
196- in the same ``cloner.replicate(plan, stage=... ) `` call, so the choice between
211+ in the same ``cloner.replicate(plan) `` call, so the choice between
197212them is purely about ergonomics:
198213
199214* The first wraps both phases in a context manager and is what
@@ -214,11 +229,11 @@ them is purely about ergonomics:
214229:class: `~isaaclab.cloner.ReplicateSession ` is a context manager that brackets the
215230whole cloning lifecycle. Entering the block builds the plan, the body is where
216231you construct your assets (each one registers itself as part of its constructor),
217- and exiting the block drains every registration against the plan:
232+ and exiting the block clears those constructor registrations and dispatches the plan:
218233
219234.. code-block :: python
220235
221- with cloner.ReplicateSession(cfgs, num_clones = N, env_spacing = 2.0 , device = device, stage = stage ):
236+ with cloner.ReplicateSession(cfgs, num_clones = N, env_spacing = 2.0 ):
222237 for cfg in cfgs:
223238 cfg.class_type(cfg)
224239
@@ -253,10 +268,10 @@ intervene before replication actually happens:
253268
254269.. code-block :: python
255270
256- plan = cloner.make_clone_plan(cfgs, num_clones = N, env_spacing = 2.0 , device = device )
271+ plan = cloner.make_clone_plan(cfgs, num_clones = N, env_spacing = 2.0 )
257272 for cfg in cfgs:
258273 cfg.class_type(cfg)
259- cloner.replicate(plan, stage = stage )
274+ cloner.replicate(plan)
260275
261276 ``clone_plan_from_env_0 `` + ``replicate ``
262277~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -276,10 +291,10 @@ subclasses use — they author the env-0 prototype prim by prim in
276291 # ... any other assets ...
277292
278293 src, dest = " /World/envs/env_0" , " /World/envs/env_{} "
279- pos = cloner.grid_transforms(self .scene.num_envs, self .scene.cfg.env_spacing, device = self .device )[0 ]
294+ pos = cloner.grid_transforms(self .scene.num_envs, self .scene.cfg.env_spacing)[0 ]
280295 global_paths = (" /World/ground" ,)
281- plan = cloner.clone_plan_from_env_0(src, dest, self .scene.num_envs, self .device, pos, global_paths = global_paths)
282- cloner.replicate(plan, stage = self .scene.stage )
296+ plan = cloner.clone_plan_from_env_0(src, dest, self .scene.num_envs, pos, global_paths = global_paths)
297+ cloner.replicate(plan)
283298
284299 Every env receives the same prototype. When envs need to differ, use one of the
285300other two. Hand-built scenes must pass every shared asset root in ``global_paths ``;
@@ -289,118 +304,37 @@ use ``()`` when there are none.
289304Under the Hood
290305--------------
291306
292- To see how the backend-agnostic surface works, follow one asset through the
293- system. Suppose you write ``Articulation(cfg) `` for a PhysX articulation
294- somewhere inside a :class: `~isaaclab.cloner.ReplicateSession `. The constructor
295- does not actually clone anything yet — at that moment the plan describing how
296- the full env population should be laid out may not even exist. Instead the
297- constructor *registers * the asset with the cloner, the cloner files the
298- registration into a queue, and later — when the session exits and the cloner
299- runs replication — that registration is handed to the backend code that knows
300- how to replicate a PhysX articulation, with the plan telling it where each
301- clone goes.
302-
303- The story has to look like this because the engines underneath disagree about
304- *when * and *how * replication actually happens:
305-
306- * **PhysX ** defers the real work to physics runtime. At construction time the
307- only thing user code can do is register intent; PhysX replays those
308- registrations entity by entity when the simulation comes up.
309- * **USD ** is declarative and immediate — calling :func: `~isaaclab.cloner.usd_replicate `
310- materializes the clones in place, right then and there.
311- * **Newton ** is also declarative and immediate, but it insists on replicating
312- the whole world in one shot rather than asset by asset, so the framework
313- cannot just hand it one cfg at a time — everything Newton-related has to be
314- assembled first.
315-
316- Isaac Lab reconciles these into one surface with two small pieces of plumbing.
317- Every backend supplies its own :class: `~isaaclab.cloner.UsdReplicateContext ` /
318- ``PhysxReplicateContext `` / ``NewtonReplicateContext ``, a class that hides the
319- timing and granularity differences above behind a single uniform interface. A
320- shared :data: `~isaaclab.cloner.REPLICATION_QUEUE ` then remembers which asset
321- cfgs participate until it is time to run. The three
322- subsections below explain the queue, the contexts, and the function that joins
323- them against a plan.
324-
325- The registration queue
326- ~~~~~~~~~~~~~~~~~~~~~~
327-
328- Assets do not replicate inline. Construction only registers the asset's cfg
329- through :func: `~isaaclab.cloner.queue_replication `; the queue records *which *
330- cfgs participate:
307+ Planning maps each cfg to rows in ``cfg_rows `` and each participating backend to
308+ its subset in ``context_rows ``. The active physics manager registers its clone
309+ context during simulation initialization. Assets use that context by default;
310+ :attr: `~isaaclab.assets.AssetBaseCfg.cloning_contexts ` can select an explicitly
311+ registered context instead. Planning also registers
312+ :class: `~isaaclab.cloner.UsdReplicateContext ` for spawned assets when Kit is
313+ available.
331314
332- .. code-block :: text
333-
334- REPLICATION_QUEUE
335- cartpole_cfg
336- cube_cfg
337- camera_cfg
338- ...
339-
340- *How * each cfg is cloned is resolved by :func: `~isaaclab.cloner.replicate ` at
341- dispatch. The physics side comes from the cfg's
342- :attr: `~isaaclab.assets.AssetBaseCfg.cloning_contexts ` when set, otherwise the
343- active backend's default physics context, which each backend cloner exports as
344- ``PHYSICS_CONTEXT `` (PhysX and Newton replicate natively; OvPhysX replays its
345- own clones). :func: `~isaaclab.cloner.replicate ` adds
346- :class: `~isaaclab.cloner.UsdReplicateContext ` automatically whenever a cfg has a
347- spawner and Kit is available, so USD clones accompany physics replication under
348- Kit and are skipped by default in headless runs. An explicit cfg override may
349- still request USD replication without Kit. With
350- :attr: `~isaaclab.cloner.CloneCfg.replicate_physics ` disabled, cloning is
351- USD-only: every physics context is dropped and the physics engine parses the
352- per-env USD prims directly.
353-
354- Deferring the work like this buys three things at once:
355-
356- * Replication can wait until the plan is fully built, so the final layout is
357- known before any prims are spawned.
358- * Every asset's request is batched into a single backend call instead of one
359- call per asset.
360- * Asset code stays free of any branching on which backend is active — it just
361- registers and lets the framework take it from there.
362-
363- :attr: `~isaaclab.scene.InteractiveSceneCfg.replicate_physics ` is piped into
364- :attr: `~isaaclab.cloner.CloneCfg.replicate_physics ` and applied at dispatch;
365- an asset whose only cloning mechanism is physics replication is then simply
366- not cloned.
367-
368- Backend contexts
369- ~~~~~~~~~~~~~~~~
370-
371- Each backend ships a small adapter class — its *replicate context * — that
372- knows how to take a registered cfg and replicate it on the backend's specific
373- runtime:
315+ The backend packages expose different context implementations behind one
316+ execution contract:
374317
375318.. code-block :: text
376319
377320 UsdReplicateContext # replicates USD prim subtrees
378321 PhysxReplicateContext # replicates PhysX rigid bodies and articulations
379322 NewtonReplicateContext # replicates Newton bodies in its parallel pipeline
380323
381- A single asset usually resolves to more than one context — PhysX pairs its
382- context with USD so physics and visuals both follow; Newton's default stack
383- includes USD only under Kit, so kitless runs skip the authoring cost. This is
384- where backend differences are absorbed: swapping a scene from PhysX to Newton
385- swaps which default stack resolves at dispatch, while the cfgs and the rest of
386- the user code stay unchanged.
387-
388- Running replication
389- ~~~~~~~~~~~~~~~~~~~
390-
391- :func: `~isaaclab.cloner.replicate ` is what actually runs the registered work.
392- The dispatch shape is roughly:
324+ :func: `~isaaclab.cloner.replicate ` resolves these types through the
325+ :class: `~isaaclab.sim.SimulationContext ` backend registry, orders them by
326+ ``replicate_priority ``, and passes the same plan to each one:
393327
394328.. code-block :: python
395329
396- def replicate (plan , stage ):
397- for context_cls, rows in group_queue_by_context( plan) :
398- context_cls() .replicate(rows = rows, stage = stage )
330+ def replicate (plan ):
331+ for context_type in plan.context_rows :
332+ simulation_backends[context_type] .replicate(plan )
399333 publish(plan)
400334
401- Contexts run in a priority order that puts physics ahead of visuals, and the
402- plan is published to :class: ` ~isaaclab.sim.SimulationContext ` so the rest of the
403- framework can read the per-env layout back .
335+ USD runs before native physics contexts so the destination topology exists when
336+ they consume it. No fallback context is constructed during dispatch. The plan is
337+ then published to the simulation context for downstream consumers .
404338
405339Collision Filtering
406340-------------------
0 commit comments