Skip to content

Commit 6e0cbe2

Browse files
ooctipusmatthewtreptecamevorsylvesterkaczmarekAntoineRichard
authored
[Backport release/3.0.0] Newton startup, cloner, and rendering fixes (#7299)
## Summary Backports the following merged changes to `release/3.0.0` as separate provenance-preserving cherry-picks: - #7292 — scope Newton global imports with clone plans - #7285 — stabilize the sensor/PhysX video recording test - #7269 — streamline Newton contact and raycast sensor startup - #7119 — normalize non-finite depth display values safely - #7295 — avoid repeated Newton model and articulation startup work Each source squash commit was cherry-picked with `-x` and applied without conflicts. ## Validation - Stable patch IDs match all five source squash commits exactly. - File-by-file manifests match each source squash commit. - `git diff --check upstream/release/3.0.0..HEAD` - `uv run --frozen python tools/changelog/cli.py check backport-7285-7292-base` - `SKIP=check-changelog-fragments uv run --frozen isaaclab -f` - Cloner/Newton focused tests: 98 passed - Scene global-ownership tests: 2 passed - Simulator clone-plan tests: 4 passed - Video recording regression test: 1 passed - Newton BVH lifecycle tests: 2 passed - Newton contact-selector tests: 7 passed - Newton raycast BVH test: 4 passed - Non-finite depth display tests: 4 passed - #7295 physics lifecycle, cloner, manager, and coupling tests: 248 passed - #7295 Newton joint-wrench sensor tests: 11 passed - #7295 PhysX joint-wrench sensor tests: 16 passed PR #7121 remains open and is intentionally excluded; it will be backported from its final merge commit after merging. --------- Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Co-authored-by: matthewtrepte <mtrepte@nvidia.com> Co-authored-by: camevor <camevor@nvidia.com> Co-authored-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Co-authored-by: Antoine RICHARD <antoiner@nvidia.com>
1 parent 40f7415 commit 6e0cbe2

76 files changed

Lines changed: 657 additions & 280 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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ this page:
130130
- Long tensor of target env ids.
131131
* - ``positions``
132132
- Optional per-env world positions [m], shape ``[num_envs, 3]``.
133+
* - ``global_paths``
134+
- Unique prim paths for scene assets shared by every env and therefore not replicated.
133135

134136
The plan is stage-agnostic by design — the same instance can be replayed against a
135137
different stage, inspected by tooling, or serialized.
@@ -141,6 +143,7 @@ When every env is a copy of env_0:
141143
sources = ("/World/envs/env_0",)
142144
destinations = ("/World/envs/env_{}",)
143145
clone_mask = [[True, True, ..., True]]
146+
global_paths = ("/World/Ground", "/World/Light")
144147
145148
When envs differ — say a cartpole in every env plus a 2-variant obstacle (box into
146149
envs 0/1, sphere into envs 2/3):
@@ -215,8 +218,7 @@ and exiting the block drains every registration against the plan:
215218

216219
.. code-block:: python
217220
218-
with cloner.ReplicateSession(cfgs, num_clones=N, env_spacing=2.0,
219-
device=device, stage=stage):
221+
with cloner.ReplicateSession(cfgs, num_clones=N, env_spacing=2.0, device=device, stage=stage):
220222
for cfg in cfgs:
221223
cfg.class_type(cfg)
222224
@@ -264,7 +266,7 @@ Shortcut for the case where every env is just a copy of env_0.
264266
one line by pointing at the prototype, and :func:`~isaaclab.cloner.replicate`
265267
finishes the setup. This is the pattern most :class:`~isaaclab.envs.DirectRLEnv`
266268
subclasses use — they author the env-0 prototype prim by prim in
267-
``_setup_scene`` and end the method with these four lines:
269+
``_setup_scene`` and end the method with this sequence:
268270

269271
.. code-block:: python
270272
@@ -275,11 +277,13 @@ subclasses use — they author the env-0 prototype prim by prim in
275277
276278
src, dest = "/World/envs/env_0", "/World/envs/env_{}"
277279
pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0]
278-
plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos)
280+
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)
279282
cloner.replicate(plan, stage=self.scene.stage)
280283
281284
Every env receives the same prototype. When envs need to differ, use one of the
282-
other two.
285+
other two. Hand-built scenes must pass every shared asset root in ``global_paths``;
286+
use ``()`` when there are none.
283287

284288

285289
Under the Hood

docs/source/migration/migrating_from_isaacgymenvs.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ adding any other optional objects into the scene, such as lights.
231231
| | spawn_ground_plane( |
232232
| | prim_path="/World/ground", cfg=GroundPlaneCfg()) |
233233
| self.sim = super().create_sim(self.device_id, self.graphics_device_id, | # create and apply a clone plan |
234-
| self.physics_engine, self.sim_params) | plan = cloner.clone_plan_from_env_0(...) |
234+
| self.physics_engine, self.sim_params) | plan = cloner.clone_plan_from_env_0(..., global_paths=...) |
235235
| self._create_ground_plane() | cloner.replicate(plan, stage=self.scene.stage) |
236236
| self._create_envs(self.num_envs, self.cfg["env"]['envSpacing'], | # add articulation to scene |
237237
| int(np.sqrt(self.num_envs))) | self.scene.articulations["cartpole"] = self.cartpole |
@@ -680,8 +680,10 @@ the need to set simulation parameters for actors in the task implementation.
680680
| self.sim_params) | positions = cloner.grid_transforms( |
681681
| self._create_ground_plane() | self.scene.num_envs, self.scene.cfg.env_spacing, |
682682
| self._create_envs(self.num_envs, | device=self.device)[0] |
683-
| self.cfg["env"]['envSpacing'], | plan = cloner.clone_plan_from_env_0( |
684-
| int(np.sqrt(self.num_envs))) | src, dest, self.scene.num_envs, self.device, positions) |
683+
| self.cfg["env"]['envSpacing'], | global_paths = ("/World/ground",) |
684+
| int(np.sqrt(self.num_envs))) | plan = cloner.clone_plan_from_env_0( |
685+
| | src, dest, self.scene.num_envs, self.device, positions, |
686+
| | global_paths=global_paths) |
685687
| | cloner.replicate(plan, stage=self.scene.stage) |
686688
| def _create_ground_plane(self): | if "physx" in self.scene.physics_backend: |
687689
| plane_params = gymapi.PlaneParams() | self.scene.filter_collisions(global_prim_paths=[]) |

scripts/demos/pick_and_place.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ def _setup_scene(self):
228228
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
229229
src, dest = "/World/envs/env_0", "/World/envs/env_{}"
230230
pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0]
231-
plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos)
231+
global_paths = ("/World/ground",)
232+
plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos, global_paths=global_paths)
232233
cloner.replicate(plan, stage=self.scene.stage)
233234
# PhysX replication requires explicit collision filtering between environments.
234235
if "physx" in self.scene.physics_backend:

scripts/tutorials/06_deploy/anymal_c_env.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def _setup_scene(self):
6666
self._terrain = self.cfg.terrain.class_type(self.cfg.terrain)
6767
src, dest = "/World/envs/env_0", "/World/envs/env_{}"
6868
pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0]
69-
plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos)
69+
global_paths = (self.cfg.terrain.prim_path,)
70+
plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos, global_paths=global_paths)
7071
cloner.replicate(plan, stage=self.scene.stage)
7172
# PhysX replication requires explicit collision filtering between environments.
7273
if "physx" in self.scene.physics_backend:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Docstring-only clarification of the shape-expression convention; behaviour change lives in isaaclab_newton.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Added
2+
^^^^^
3+
4+
* Added :attr:`~isaaclab.cloner.ClonePlan.global_paths` to identify scene assets shared by every environment
5+
without representing them as replication rows.
6+
7+
Changed
8+
^^^^^^^
9+
10+
* Changed :func:`~isaaclab.cloner.make_clone_plan`, :func:`~isaaclab.cloner.clone_plan_from_env_0`, and
11+
:class:`~isaaclab.cloner.ReplicateSession` to accept explicit ``global_paths`` tuples.

source/isaaclab/changelog.d/ooctipus-newton-active-solver-attributes.skip

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed camera depth display normalization producing ``NaN`` values and suppressing finite depth contrast when
5+
no-hit pixels contain ``inf`` or ``NaN`` values.

source/isaaclab/isaaclab/cloner/clone_plan.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class ClonePlan:
6161
cfg_rows: dict[int, tuple[int, ...]] = field(default_factory=dict)
6262
"""``id(cfg)`` to the row indices the cfg owns."""
6363

64+
global_paths: tuple[str, ...] = ()
65+
"""Unique prim paths for scene assets shared by every environment."""
66+
6467

6568
def grid_transforms(N: int, spacing: float = 1.0, up_axis: str = "z", device="cpu"):
6669
"""Create a centered grid of transforms for ``N`` instances.
@@ -221,7 +224,7 @@ def make_clone_plan(
221224
num_clones: int,
222225
env_spacing: float,
223226
device: str,
224-
*,
227+
global_paths: tuple[str, ...] = (),
225228
clone_strategy: Callable = sequential,
226229
valid_set: torch.Tensor | None = None,
227230
env_template: str = DEFAULT_ENV_TEMPLATE,
@@ -234,15 +237,16 @@ def make_clone_plan(
234237
envs, and returns a self-contained :class:`ClonePlan` with ``cfg_rows`` populated.
235238
236239
Each input cfg's ``spawn_path`` / ``spawn_paths`` is mutated so the subsequent
237-
asset constructor spawns the prototype into its first active environment. Cfgs
238-
whose ``prim_path`` is global (not under the env root ``/World/envs/``) or that
239-
lack a spawn are skipped — they do not appear in the plan and are not replicated.
240+
asset constructor spawns the prototype into its first active environment. Every cfg
241+
is an env-scoped entity with a spawner. Shared assets are declared explicitly through
242+
``global_paths`` and are never replicated.
240243
241244
Args:
242-
cfgs: Asset cfgs with resolved ``prim_path`` (no ``{ENV_REGEX_NS}`` macros).
245+
cfgs: Cloneable asset cfgs with resolved env-scoped ``prim_path`` and ``spawn``.
243246
num_clones: Number of target envs.
244247
env_spacing: Distance between neighboring grid env origins [m].
245248
device: Torch device for plan tensors.
249+
global_paths: Complete shared-asset roots declared by the scene composition root. Defaults to none.
246250
clone_strategy: Function that assigns prototype combinations to envs. Defaults
247251
to :func:`~isaaclab.cloner.sequential`.
248252
valid_set: Optional ``[num_combos, num_groups]`` long tensor of valid prototype
@@ -251,8 +255,8 @@ def make_clone_plan(
251255
252256
Returns:
253257
A :class:`ClonePlan` whose ``sources``/``destinations``/``clone_mask`` describe
254-
the flat prototype-to-env mapping and whose ``cfg_rows`` maps each cfg to the
255-
rows it owns.
258+
the flat prototype-to-env mapping, whose ``cfg_rows`` maps each replicated cfg
259+
to the rows it owns, and whose ``global_paths`` names shared scene assets.
256260
"""
257261

258262
def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
@@ -270,16 +274,11 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
270274
# 1) Build per-group records: (cfg, spawn_cfg, destination_template, num_variants).
271275
groups: list[tuple[Any, Any, str, int]] = []
272276
for cfg in cfgs:
273-
if not hasattr(cfg, "prim_path") or not hasattr(cfg, "spawn") or cfg.spawn is None:
274-
continue
275-
prim_path = cfg.prim_path
276-
if (matched := match(prim_path, env_template)) is None:
277-
continue
277+
matched = match(cfg.prim_path, env_template)
278278
count = num_spawn_variants(cfg.spawn)
279279
if count <= 0:
280-
raise ValueError(f"Spawner at '{prim_path}' must have at least one variant.")
280+
raise ValueError(f"Spawner at '{cfg.prim_path}' must have at least one variant.")
281281
groups.append((cfg, cfg.spawn, env_template + matched.suffix, count))
282-
283282
env_ids = torch.arange(num_clones, dtype=torch.long, device=device)
284283
positions, _ = grid_transforms(num_clones, env_spacing, device=device)
285284

@@ -293,6 +292,7 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
293292
env_ids=env_ids,
294293
positions=positions,
295294
cfg_rows={},
295+
global_paths=global_paths,
296296
)
297297

298298
# 3) Homogeneous (every cfg is single-variant): emit the simpler env-root plan.
@@ -307,6 +307,7 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
307307
env_ids=env_ids,
308308
positions=positions,
309309
cfg_rows=cfg_rows,
310+
global_paths=global_paths,
310311
)
311312

312313
# 4) Heterogeneous: enumerate prototype combos, build per-row mask, mutate spawn paths.
@@ -375,6 +376,7 @@ def validate_combo_tensor(combos: torch.Tensor, name: str, expected_rows: int |
375376
env_ids=env_ids,
376377
positions=positions,
377378
cfg_rows=cfg_rows,
379+
global_paths=global_paths,
378380
)
379381

380382

@@ -384,35 +386,37 @@ def clone_plan_from_env_0(
384386
num_clones: int,
385387
device: str,
386388
positions: torch.Tensor | None = None,
389+
global_paths: tuple[str, ...] = (),
387390
) -> ClonePlan:
388391
"""Build a single-source clone plan that targets every env from one source row.
389392
390393
Auto-populates :attr:`ClonePlan.cfg_rows` from :data:`~isaaclab.cloner.REPLICATION_QUEUE`,
391394
including only cfgs whose ``prim_path`` falls under the env-root prefix of
392-
``destination``. Must be called *after* all asset constructors have run, so their cfgs
393-
are already registered in the queue; otherwise those assets will be skipped by the
394-
subsequent :func:`~isaaclab.cloner.replicate` call.
395+
``destination``. ``global_paths`` is the complete declaration of shared assets; it is
396+
never inferred from the stage or replication queue. Must be called *after* all asset
397+
constructors have run, so their cfgs are already registered in the queue; otherwise
398+
those assets will be skipped by the subsequent :func:`~isaaclab.cloner.replicate` call.
395399
396400
Args:
397401
source: Source prim path (typically ``/World/envs/env_0``).
398402
destination: Destination template with ``"{}"`` for the env id.
399403
num_clones: Number of target envs.
400404
device: Torch device for the mask and env id buffers.
401405
positions: Optional per-env world positions [m], shape ``[num_clones, 3]``.
406+
global_paths: Complete shared-asset roots for the hand-built scene. Defaults to none.
402407
403408
Returns:
404409
A :class:`ClonePlan` with a single source row covering every env.
405410
"""
406411
from .replicate_session import REPLICATION_QUEUE # noqa: PLC0415
407412

408-
cfg_rows: dict[int, tuple[int, ...]] = {
409-
id(cfg): (0,) for cfg in REPLICATION_QUEUE if match(cfg.prim_path, destination) is not None
410-
}
413+
cfg_rows = {id(cfg): (0,) for cfg in REPLICATION_QUEUE if match(cfg.prim_path, destination) is not None}
411414
return ClonePlan(
412415
sources=(source,),
413416
destinations=(destination,),
414417
clone_mask=torch.ones((1, num_clones), dtype=torch.bool, device=device),
415418
env_ids=torch.arange(num_clones, dtype=torch.long, device=device),
416419
positions=positions,
417420
cfg_rows=cfg_rows,
421+
global_paths=global_paths,
418422
)

source/isaaclab/isaaclab/cloner/replicate_session.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ def replicate(plan: ClonePlan, *, stage: Usd.Stage, replicate_physics: bool = Tr
5858
5959
Cfgs absent from ``plan.cfg_rows`` are silently skipped. Backend contexts run in
6060
ascending ``replicate_priority`` order. The queue is cleared up front, so a backend
61-
failure cannot leak stale entries into the next call.
61+
failure cannot leak stale entries into the next call. Every context receives the plan's
62+
explicitly declared shared assets when it is constructed.
6263
6364
Args:
6465
plan: Replication layout to dispatch.
@@ -72,7 +73,7 @@ def replicate(plan: ClonePlan, *, stage: Usd.Stage, replicate_physics: bool = Tr
7273
REPLICATION_QUEUE.clear()
7374

7475
backend_package = FactoryBase._get_package_name(FactoryBase._get_backend())
75-
backend_physics_ctx = getattr(importlib.import_module(f"{backend_package}.cloner"), "PHYSICS_CONTEXT", None)
76+
backend_physics_ctx = importlib.import_module(f"{backend_package}.cloner").PHYSICS_CONTEXT
7677

7778
# Group queued cfgs by backend, taking the union of row indices each backend owns.
7879
# In the homogeneous plan every cfg maps to row 0, so multiple queue_replication
@@ -85,20 +86,20 @@ def replicate(plan: ClonePlan, *, stage: Usd.Stage, replicate_physics: bool = Tr
8586
if rows is None:
8687
continue
8788
if cfg.cloning_contexts is None:
88-
contexts = [backend_physics_ctx] if backend_physics_ctx else []
89+
contexts = [backend_physics_ctx]
8990
else:
9091
contexts = [string_to_callable(c) if isinstance(c, str) else c for c in cfg.cloning_contexts]
9192
if not replicate_physics:
9293
contexts = [c for c in contexts if c is UsdReplicateContext]
9394
ctx_set = dict.fromkeys(contexts)
94-
if getattr(cfg, "spawn", None) is not None and kit_available:
95+
if cfg.spawn is not None and kit_available:
9596
ctx_set.setdefault(UsdReplicateContext, None)
9697
for BackendCtxCls in ctx_set:
9798
backend_rows.setdefault(BackendCtxCls, set()).update(rows)
9899

99100
backend_ctxs: dict[type, Any] = {}
100101
for BackendCtxCls, row_set in backend_rows.items():
101-
ctx = BackendCtxCls(stage)
102+
ctx = BackendCtxCls(stage, global_paths=plan.global_paths)
102103
backend_ctxs[BackendCtxCls] = ctx
103104
row_list = sorted(row_set)
104105
ctx.queue_mapping(
@@ -109,7 +110,7 @@ def replicate(plan: ClonePlan, *, stage: Usd.Stage, replicate_physics: bool = Tr
109110
positions=plan.positions,
110111
)
111112

112-
for ctx in sorted(backend_ctxs.values(), key=lambda c: getattr(c, "replicate_priority", 0)):
113+
for ctx in sorted(backend_ctxs.values(), key=lambda ctx: ctx.replicate_priority):
113114
ctx.replicate()
114115

115116
SimulationContext.instance().set_clone_plan(plan)
@@ -139,6 +140,7 @@ def __init__(
139140
device: str,
140141
*,
141142
stage: Usd.Stage,
143+
global_paths: tuple[str, ...] = (),
142144
clone_strategy: Callable = sequential,
143145
valid_set: torch.Tensor | None = None,
144146
replicate_physics: bool = True,
@@ -152,6 +154,7 @@ def __init__(
152154
env_spacing: Grid spacing between env origins [m].
153155
device: Torch device for plan tensors.
154156
stage: USD stage to author replicated prim specs into.
157+
global_paths: Complete shared-asset roots declared by the composition root. Defaults to none.
155158
clone_strategy: Prototype-to-env assignment function.
156159
valid_set: Optional ``[num_combos, num_groups]`` long tensor of valid
157160
prototype combinations; ``None`` uses the full cartesian product.
@@ -166,6 +169,7 @@ def __init__(
166169
num_clones=num_clones,
167170
env_spacing=env_spacing,
168171
device=device,
172+
global_paths=global_paths,
169173
clone_strategy=clone_strategy,
170174
valid_set=valid_set,
171175
env_template=env_template,

0 commit comments

Comments
 (0)