Skip to content

Commit c792488

Browse files
committed
Honor replicate_physics as cloner-side policy directed by asset cfgs
Since the replication-session refactor, InteractiveSceneCfg.replicate_physics was ignored: scenes configured with replicate_physics=False invoked native physics replication anyway, silently discarding per-environment USD differences (identified in #6541). The flag now lives where the policy executes: - CloneCfg.replicate_physics is the policy's home; InteractiveScene pipes its cfg flag into it and forwards it through ReplicateSession to replicate(). - REPLICATION_QUEUE holds bare asset cfgs: construction only registers which cfgs participate (required by ClonePlan.from_env_0 in direct workflows and to distinguish clonable assets from lights/terrain). - replicate() resolves how each cfg clones at dispatch: the cfg's cloning_contexts field when set, otherwise the active backend's default stack exported as isaaclab_<backend>.cloner.REPLICATION. PhysX pairs physics replication with USD clones (collision groups are authored on the per-env prims; PhysX has no kitless mode), Newton includes USD only under Kit, and OvPhysX replicates alone since its clone replay authors USD. - With replicate_physics=False, cloning is USD-only: the physics engine parses the per-env USD prims directly; an asset whose contexts are all physics-based is simply not cloned. Asset implementations know nothing about cloning beyond registering their cfg; per-asset overrides are a cfg assignment. The per-backend queue_*_replication helpers are removed in favor of this single path.
1 parent d099fe3 commit c792488

42 files changed

Lines changed: 385 additions & 179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/source/how-to/cloning.rst

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -283,27 +283,35 @@ Every backend supplies its own :class:`~isaaclab.cloner.UsdReplicateContext` /
283283
``PhysxReplicateContext`` / ``NewtonReplicateContext``, a class that hides the
284284
timing and granularity differences above behind a single uniform interface. A
285285
shared :data:`~isaaclab.cloner.REPLICATION_QUEUE` then remembers which asset
286-
belongs to which backend's context until it is time to run. The three
286+
cfgs participate until it is time to run. The three
287287
subsections below explain the queue, the contexts, and the function that joins
288288
them against a plan.
289289

290290
The registration queue
291291
~~~~~~~~~~~~~~~~~~~~~~
292292

293-
Asset constructors do not replicate inline. They register their intent with
294-
:data:`~isaaclab.cloner.REPLICATION_QUEUE` and the framework defers the actual
295-
work to the drain. The queue ends up holding one entry per ``(asset, backend)``
296-
pair:
293+
Assets do not replicate inline. Construction only registers the asset's cfg
294+
through :func:`~isaaclab.cloner.queue_replication`; the queue records *which*
295+
cfgs participate:
297296

298297
.. code-block:: text
299298
300299
REPLICATION_QUEUE
301-
(cartpole_cfg, PhysxReplicateContext)
302-
(cartpole_cfg, UsdReplicateContext)
303-
(cube_cfg, UsdReplicateContext)
304-
(light_cfg, UsdReplicateContext)
300+
cartpole_cfg
301+
cube_cfg
302+
camera_cfg
305303
...
306304
305+
*How* each cfg is cloned is resolved by :func:`~isaaclab.cloner.replicate` at
306+
dispatch: the cfg's :attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts`
307+
when set, otherwise the active backend's default stack. Each backend cloner
308+
exports that stack under the conventional name ``REPLICATION`` — PhysX pairs
309+
its native replication with USD clones, Newton includes USD only under Kit,
310+
and OvPhysX replicates alone because its clone replay authors USD itself.
311+
With :attr:`~isaaclab.cloner.CloneCfg.replicate_physics` disabled, cloning is
312+
USD-only: every context except ``UsdReplicateContext`` is dropped and the
313+
physics engine parses the per-env USD prims directly.
314+
307315
Deferring the work like this buys three things at once:
308316

309317
* Replication can wait until the plan is fully built, so the final layout is
@@ -313,6 +321,11 @@ Deferring the work like this buys three things at once:
313321
* Asset code stays free of any branching on which backend is active — it just
314322
registers and lets the framework take it from there.
315323

324+
:attr:`~isaaclab.scene.InteractiveSceneCfg.replicate_physics` is piped into
325+
:attr:`~isaaclab.cloner.CloneCfg.replicate_physics` and applied at dispatch;
326+
an asset whose only cloning mechanism is physics replication is then simply
327+
not cloned.
328+
316329
Backend contexts
317330
~~~~~~~~~~~~~~~~
318331

@@ -326,12 +339,12 @@ runtime:
326339
PhysxReplicateContext # replicates PhysX rigid bodies and articulations
327340
NewtonReplicateContext # replicates Newton bodies in its parallel pipeline
328341
329-
A single asset can register more than one context — a PhysX articulation
330-
registers a PhysX context and a USD context so physics and visuals both follow,
331-
a Newton articulation registers a Newton context plus a USD context only if it
332-
owns visual prims. This is where backend differences are absorbed: swapping a
333-
scene from PhysX to Newton swaps which context an asset registers with, while
334-
the cfgs and the rest of the user code stay unchanged.
342+
A single asset usually resolves to more than one context — PhysX pairs its
343+
context with USD so physics and visuals both follow; Newton's default stack
344+
includes USD only under Kit, so kitless runs skip the authoring cost. This is
345+
where backend differences are absorbed: swapping a scene from PhysX to Newton
346+
swaps which default stack resolves at dispatch, while the cfgs and the rest of
347+
the user code stay unchanged.
335348

336349
Running replication
337350
~~~~~~~~~~~~~~~~~~~
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed :attr:`~isaaclab.scene.InteractiveSceneCfg.replicate_physics` being ignored since the
5+
replication-session refactor: scenes configured with ``replicate_physics=False`` invoked
6+
native physics replication anyway, silently discarding per-environment USD differences
7+
(e.g. prestartup scale randomization). Cloning is now USD-only in that case, so the physics
8+
engine parses each environment's USD prims directly; assets whose only cloning mechanism is
9+
physics replication are not cloned.
10+
11+
Added
12+
^^^^^
13+
14+
* Added :attr:`~isaaclab.cloner.CloneCfg.replicate_physics` as the cloner-side home of the
15+
policy; :class:`~isaaclab.scene.InteractiveScene` pipes the scene flag into it and
16+
:func:`~isaaclab.cloner.replicate` applies it.
17+
* Added :attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts` to override, per asset, the
18+
cloning contexts resolved at replication time; ``None`` uses the active backend's default
19+
stack (``isaaclab_<backend>.cloner.REPLICATION``).
20+
21+
Changed
22+
^^^^^^^
23+
24+
* **Breaking:** Changed :data:`~isaaclab.cloner.REPLICATION_QUEUE` to hold asset cfgs instead
25+
of ``(cfg, BackendCtxCls)`` pairs: construction registers *which* cfgs participate via
26+
:func:`~isaaclab.cloner.queue_replication`, and *how* each is cloned resolves inside
27+
:func:`~isaaclab.cloner.replicate`. Code appending pairs should register the cfg and direct
28+
contexts through :attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts`.
29+
30+
Removed
31+
^^^^^^^
32+
33+
* Removed ``queue_usd_replication``: register the cfg with
34+
:func:`~isaaclab.cloner.queue_replication` and direct contexts through the cfg's
35+
``cloning_contexts`` field instead.

source/isaaclab/isaaclab/assets/asset_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import warp as wp
1616

1717
import isaaclab.sim as sim_utils
18+
from isaaclab.cloner import queue_replication
1819
from isaaclab.physics import PhysicsEvent, PhysicsManager
1920
from isaaclab.sim.simulation_context import SimulationContext
2021
from isaaclab.sim.utils.stage import get_current_stage
@@ -68,6 +69,9 @@ def __init__(self, cfg: AssetBaseCfg):
6869
"""
6970
# check that the config is valid
7071
cfg.validate()
72+
# register the original cfg object for cloning: the clone plan keys rows by the
73+
# cfg identity the scene collected; contexts and policy resolve at replication time
74+
queue_replication(cfg)
7175
# store inputs
7276
self.cfg = cfg.copy()
7377
# Resolve shape-check flag once: True means checks are active.

source/isaaclab/isaaclab/assets/asset_base_cfg.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ class InitialStateCfg:
4646
The class should inherit from :class:`isaaclab.assets.asset_base.AssetBase`.
4747
"""
4848

49+
cloning_contexts: tuple[str | type, ...] | None = None
50+
"""Cloning contexts for this asset. Defaults to None.
51+
52+
Entries are ``"module:ContextClass"`` references (or classes) that clone this asset
53+
across environments. If None, :func:`~isaaclab.cloner.replicate` uses the active
54+
physics backend's default stack (``isaaclab_<backend>.cloner.REPLICATION``); an empty
55+
tuple means the asset is not cloned.
56+
"""
57+
4958
prim_path: str = MISSING
5059
"""Prim path (or expression) to the asset.
5160

source/isaaclab/isaaclab/cloner/__init__.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ __all__ = [
1616
"ReplicateSession",
1717
"REPLICATION_QUEUE",
1818
"replicate",
19+
"queue_replication",
1920
"resolve_clone_plan_source",
2021
"split_clone_template",
21-
"queue_usd_replication",
2222
"sequential",
2323
"UsdReplicateContext",
2424
"usd_replicate",
@@ -40,10 +40,10 @@ from .cloner_utils import (
4040
from .replicate_session import (
4141
REPLICATION_QUEUE,
4242
ReplicateSession,
43+
queue_replication,
4344
replicate,
4445
)
4546
from .usd import (
4647
UsdReplicateContext,
47-
queue_usd_replication,
4848
usd_replicate,
4949
)

source/isaaclab/isaaclab/cloner/clone_plan.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def from_env_0(
5050
Auto-populates :attr:`cfg_rows` from
5151
:data:`~isaaclab.cloner.REPLICATION_QUEUE`, including only cfgs whose
5252
``prim_path`` falls under the env-root prefix of ``destination``. Must be
53-
called *after* all asset constructors have run, so their replication entries
54-
are already in the queue; otherwise those assets will be skipped by the
53+
called *after* all asset constructors have run, so their cfgs are already
54+
registered in the queue; otherwise those assets will be skipped by the
5555
subsequent :func:`~isaaclab.cloner.replicate` call.
5656
5757
Args:
@@ -66,7 +66,7 @@ def from_env_0(
6666

6767
prefix, _ = split_clone_template(destination)
6868
cfg_rows: dict[int, tuple[int, ...]] = {
69-
id(cfg): (0,) for cfg, _ in REPLICATION_QUEUE if cfg.prim_path.startswith(prefix)
69+
id(cfg): (0,) for cfg in REPLICATION_QUEUE if cfg.prim_path.startswith(prefix)
7070
}
7171
return cls(
7272
sources=(source,),

source/isaaclab/isaaclab/cloner/cloner_cfg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@ class CloneCfg:
2828

2929
clone_regex: str = "/World/envs/env_.*"
3030
"""Regex matching every replicated env prim. Used to expand ``{ENV_REGEX_NS}`` cfg macros."""
31+
32+
replicate_physics: bool = True
33+
"""Whether physics replication clones each environment. Default is True.
34+
35+
If False, cloning is USD-only: the physics engine parses the per-env USD prims directly
36+
instead of replicating env_0's parsed structure. Applied by :func:`~isaaclab.cloner.replicate`.
37+
"""

source/isaaclab/isaaclab/cloner/replicate_session.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77

88
from __future__ import annotations
99

10+
import importlib
1011
from collections.abc import Callable, Iterable
1112
from typing import TYPE_CHECKING, Any
1213

14+
from isaaclab.utils.backend_utils import FactoryBase
15+
from isaaclab.utils.string import string_to_callable
16+
1317
from .cloner_strategies import sequential
18+
from .usd import UsdReplicateContext
1419

1520
if TYPE_CHECKING:
1621
import torch
@@ -20,32 +25,67 @@
2025
from .clone_plan import ClonePlan
2126

2227

23-
REPLICATION_QUEUE: list[tuple[Any, type]] = []
24-
"""``(cfg, BackendCtxCls)`` pairs appended by ``queue_<backend>_replication`` and drained by :func:`replicate`."""
28+
REPLICATION_QUEUE: list[Any] = []
29+
"""Asset cfgs registered by :func:`queue_replication` and drained by :func:`replicate`.
30+
31+
The queue only records *which* cfgs participate in cloning; how each cfg is cloned is
32+
resolved at dispatch from :attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts` or the
33+
active backend's default stack.
34+
"""
35+
36+
37+
def queue_replication(cfg: Any) -> None:
38+
"""Register ``cfg`` for cloning when :func:`replicate` next runs.
39+
40+
Args:
41+
cfg: Asset cfg with resolved ``prim_path``.
42+
"""
43+
REPLICATION_QUEUE.append(cfg)
2544

2645

27-
def replicate(plan: ClonePlan, *, stage: Usd.Stage) -> None:
46+
def replicate(plan: ClonePlan, *, stage: Usd.Stage, replicate_physics: bool = True) -> None:
2847
"""Drain :data:`REPLICATION_QUEUE` against ``plan``, dispatch each backend, publish the plan.
2948
49+
Each cfg's cloning contexts come from
50+
:attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts` when set, otherwise from the
51+
active backend's default stack (``isaaclab_<backend>.cloner.REPLICATION``); entries may
52+
be ``"module:ContextClass"`` references or classes. With ``replicate_physics=False``
53+
cloning is USD-only: contexts other than
54+
:class:`~isaaclab.cloner.UsdReplicateContext` are dropped, and the physics engine
55+
parses the per-env USD prims directly.
56+
3057
Cfgs absent from ``plan.cfg_rows`` are silently skipped. Backend contexts run in
3158
ascending ``replicate_priority`` order. The queue is cleared up front, so a backend
3259
failure cannot leak stale entries into the next call.
60+
61+
Args:
62+
plan: Replication layout to dispatch.
63+
stage: USD stage to author replicated prim specs into.
64+
replicate_physics: Whether physics replication clones each environment. If False,
65+
cloning is USD-only; an asset whose contexts are all physics-based is not cloned.
3366
"""
3467
from isaaclab.sim import SimulationContext # noqa: PLC0415
3568

3669
queued = list(REPLICATION_QUEUE)
3770
REPLICATION_QUEUE.clear()
3871

3972
# Group queued cfgs by backend, taking the union of row indices each backend owns.
40-
# In the homogeneous plan every cfg maps to row 0, so multiple queue_<backend>_replication
73+
# In the homogeneous plan every cfg maps to row 0, so multiple queue_replication
4174
# calls (e.g. one per body type in RigidObjectCollection) all contribute {0} and the set
4275
# union keeps it as a single row — no redundant copy specs are authored.
4376
backend_rows: dict[type, set[int]] = {}
44-
for cfg, BackendCtxCls in queued:
77+
for cfg in queued:
4578
rows = plan.cfg_rows.get(id(cfg))
4679
if rows is None:
4780
continue
48-
backend_rows.setdefault(BackendCtxCls, set()).update(rows)
81+
contexts = cfg.cloning_contexts
82+
if contexts is None:
83+
contexts = getattr(importlib.import_module(f"isaaclab_{FactoryBase._get_backend()}.cloner"), "REPLICATION")
84+
contexts = [string_to_callable(c) if isinstance(c, str) else c for c in contexts]
85+
if not replicate_physics:
86+
contexts = [c for c in contexts if c is UsdReplicateContext]
87+
for BackendCtxCls in contexts:
88+
backend_rows.setdefault(BackendCtxCls, set()).update(rows)
4989

5090
backend_ctxs: dict[type, Any] = {}
5191
for BackendCtxCls, row_set in backend_rows.items():
@@ -70,7 +110,7 @@ class ReplicateSession:
70110
"""Folds :func:`make_clone_plan` and :func:`replicate` into a ``with`` block.
71111
72112
``__enter__`` builds the plan (and mutates each cfg's ``spawn_path``); asset
73-
constructors inside the block register backend replication into
113+
constructors inside the block register their cfgs into
74114
:data:`REPLICATION_QUEUE`; ``__exit__`` drains and dispatches.
75115
76116
Example:
@@ -92,6 +132,7 @@ def __init__(
92132
stage: Usd.Stage,
93133
clone_strategy: Callable = sequential,
94134
valid_set: torch.Tensor | None = None,
135+
replicate_physics: bool = True,
95136
):
96137
"""Capture arguments for :func:`make_clone_plan` and :func:`replicate`.
97138
@@ -104,9 +145,12 @@ def __init__(
104145
clone_strategy: Prototype-to-env assignment function.
105146
valid_set: Optional ``[num_combos, num_groups]`` long tensor of valid
106147
prototype combinations; ``None`` uses the full cartesian product.
148+
replicate_physics: Whether physics replication clones each environment;
149+
forwarded to :func:`replicate`.
107150
"""
108151
self._cfgs = cfgs
109152
self._stage = stage
153+
self._replicate_physics = replicate_physics
110154
self._kwargs = dict(
111155
num_clones=num_clones,
112156
env_spacing=env_spacing,
@@ -125,7 +169,7 @@ def __enter__(self) -> ReplicateSession:
125169
def __exit__(self, exc_type, exc_value, traceback) -> None:
126170
if exc_type is None:
127171
assert self._plan is not None
128-
replicate(self._plan, stage=self._stage)
172+
replicate(self._plan, stage=self._stage, replicate_physics=self._replicate_physics)
129173
else:
130174
# Drop cfgs registered before the failure so the next session is clean.
131175
REPLICATION_QUEUE.clear()

source/isaaclab/isaaclab/cloner/usd.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66
from __future__ import annotations
77

88
from collections.abc import Sequence
9-
from typing import Any
109

1110
import torch
1211

1312
from pxr import Gf, Sdf, Usd, UsdGeom, Vt
1413

1514
from ._fabric_notices import disabled_fabric_change_notifies
1615
from .cloner_utils import split_clone_template
17-
from .replicate_session import REPLICATION_QUEUE
1816

1917

2018
def _select_env_ids(env_ids: torch.Tensor, mask: torch.Tensor | None, row: int) -> torch.Tensor:
@@ -155,16 +153,6 @@ def dp_depth(template: str) -> int:
155153
op_order.default = Vt.TokenArray(op_names)
156154

157155

158-
def queue_usd_replication(cfg: Any) -> None:
159-
"""Register ``cfg`` for USD replication when :func:`~isaaclab.cloner.replicate` next runs.
160-
161-
Appends ``(cfg, UsdReplicateContext)`` to :data:`~isaaclab.cloner.REPLICATION_QUEUE`.
162-
The actual row resolution and dispatch happen inside :func:`~isaaclab.cloner.replicate`,
163-
so this helper is safe to call from any asset constructor — no active session is required.
164-
"""
165-
REPLICATION_QUEUE.append((cfg, UsdReplicateContext))
166-
167-
168156
def usd_replicate(
169157
stage: Usd.Stage,
170158
sources: Sequence[str],

source/isaaclab/isaaclab/scene/interactive_scene.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def __init__(self, cfg: InteractiveSceneCfg):
156156
# physics scene path
157157
self._physics_scene_path = None
158158
# prepare cloner for environment replication
159-
self.cloner_cfg = cloner.CloneCfg(device=self.device)
159+
self.cloner_cfg = cloner.CloneCfg(device=self.device, replicate_physics=self.cfg.replicate_physics)
160160
env_root = self.cloner_cfg.clone_regex.rsplit("/", 1)[0]
161161
self.env_prim_paths = [f"{env_root}/env_{i}" for i in range(self.cfg.num_envs)]
162162

@@ -184,6 +184,7 @@ def __init__(self, cfg: InteractiveSceneCfg):
184184
device=self.device,
185185
stage=self.stage,
186186
clone_strategy=self.cloner_cfg.clone_strategy,
187+
replicate_physics=self.cloner_cfg.replicate_physics,
187188
):
188189
if self._is_scene_setup_from_cfg():
189190
self._add_entities_from_cfg()

0 commit comments

Comments
 (0)