Skip to content

Commit 5eb2b8e

Browse files
matthewtrepteAntoineRichardkellyguo11
authored
Adds cloner support to Newton Model builder in Physx SDP (isaac-sim#4865)
# Description <!-- Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html 💡 Please try to keep PRs small and focused. Large PRs are harder to review and merge. --> - Adds cloner support to Newton Model builder in Physx SDP - Adds a new scene data requirements file to improve & centralize newton model / usd stage req tracking for visualizers & renderers - Removesvisualizer cfg input from SDP - Adds newton model cloner unit tests - Adds docstrings to visualizers and SDP files (and files added in this change) <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task --> --------- Signed-off-by: Kelly Guo <kellyg@nvidia.com> Co-authored-by: Antoine RICHARD <antoiner@nvidia.com> Co-authored-by: Kelly Guo <kellyg@nvidia.com>
1 parent cee6ff5 commit 5eb2b8e

22 files changed

Lines changed: 1257 additions & 102 deletions

File tree

source/isaaclab/isaaclab/cloner/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ __all__ = [
1111
"filter_collisions",
1212
"grid_transforms",
1313
"make_clone_plan",
14+
"resolve_visualizer_clone_fn",
1415
"usd_replicate",
1516
]
1617

@@ -21,5 +22,6 @@ from .cloner_utils import (
2122
filter_collisions,
2223
grid_transforms,
2324
make_clone_plan,
25+
resolve_visualizer_clone_fn,
2426
usd_replicate,
2527
)

source/isaaclab/isaaclab/cloner/cloner_cfg.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class TemplateCloneCfg:
7373
physics_clone_fn: callable | None = None
7474
"""Function used to perform physics replication."""
7575

76+
visualizer_clone_fn: callable | None = None
77+
"""Optional function used to build precomputed visualizer artifacts from the clone plan."""
78+
7679
clone_strategy: callable = random
7780
"""Function used to build prototype-to-environment mapping. Default is :func:`random`."""
7881

source/isaaclab/isaaclab/cloner/cloner_utils.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
from __future__ import annotations
77

88
import itertools
9+
import logging
910
import math
11+
from collections.abc import Callable
1012
from typing import TYPE_CHECKING
1113

1214
import torch
1315

1416
from pxr import Gf, Sdf, Usd, UsdGeom, Vt
1517

1618
import isaaclab.sim as sim_utils
19+
from isaaclab.physics.scene_data_requirements import SceneDataRequirement, VisualizerPrebuiltArtifacts
1720

1821
if TYPE_CHECKING:
1922
from .cloner_cfg import TemplateCloneCfg
2023

24+
logger = logging.getLogger(__name__)
25+
2126

2227
def clone_from_template(stage: Usd.Stage, num_clones: int, template_clone_cfg: TemplateCloneCfg) -> None:
2328
"""Clone assets from a template root into per-environment destinations.
@@ -56,7 +61,7 @@ def clone_from_template(stage: Usd.Stage, num_clones: int, template_clone_cfg: T
5661
src_paths, dest_paths, clone_masking = make_clone_plan(src, dest, num_clones, cfg.clone_strategy, cfg.device)
5762

5863
# Spawn the first instance of clones from prototypes, then deactivate the prototypes, those first instances
59-
# will be served as sources for usd and physx replication.
64+
# will be served as sources for usd and physics replication.
6065
proto_idx = clone_masking.to(torch.int32).argmax(dim=1)
6166
proto_mask = torch.zeros_like(clone_masking)
6267
proto_mask.scatter_(1, proto_idx.view(-1, 1).to(torch.long), clone_masking.any(dim=1, keepdim=True))
@@ -70,6 +75,8 @@ def clone_from_template(stage: Usd.Stage, num_clones: int, template_clone_cfg: T
7075
replicate_args = [clone_path_fmt.format(0)], [clone_path_fmt], world_indices, mapping
7176
if cfg.clone_physics and cfg.physics_clone_fn is not None:
7277
cfg.physics_clone_fn(stage, *replicate_args, positions=positions, device=cfg.device)
78+
if cfg.visualizer_clone_fn is not None:
79+
cfg.visualizer_clone_fn(stage, *replicate_args, positions=positions, device=cfg.device)
7380
if cfg.clone_usd:
7481
# parse env_origins directly from clone_path
7582
usd_replicate(stage, *replicate_args, positions=positions)
@@ -79,6 +86,8 @@ def clone_from_template(stage: Usd.Stage, num_clones: int, template_clone_cfg: T
7986
replicate_args = selected_src, dest_paths, world_indices, clone_masking
8087
if cfg.clone_physics and cfg.physics_clone_fn is not None:
8188
cfg.physics_clone_fn(stage, *replicate_args, positions=positions, device=cfg.device)
89+
if cfg.visualizer_clone_fn is not None:
90+
cfg.visualizer_clone_fn(stage, *replicate_args, positions=positions, device=cfg.device)
8291
if cfg.clone_usd:
8392
usd_replicate(stage, *replicate_args)
8493

@@ -156,14 +165,13 @@ def usd_replicate(
156165
positions: Optional positions (``[E, 3]``) -> ``xformOp:translate``.
157166
quaternions: Optional orientations (``[E, 4]``) in ``xyzw`` -> ``xformOp:orient``.
158167
159-
Returns:
160-
None
161168
"""
162169
rl = stage.GetRootLayer()
163170

164171
# Group replication by destination path depth so ancestors land before deeper paths.
165172
# This avoids composition issues for nested or interdependent specs.
166173
def dp_depth(template: str) -> int:
174+
"""Return destination prim path depth for stable parent-first replication."""
167175
dp = template.format(0)
168176
return Sdf.Path(dp).pathElementCount
169177

@@ -240,8 +248,6 @@ def filter_collisions(
240248
prim_paths: Per-clone prim paths.
241249
global_paths: Optional global-collider paths.
242250
243-
Returns:
244-
None
245251
"""
246252

247253
scene_prim = stage.GetPrimAtPath(physicsscene_path)
@@ -375,3 +381,37 @@ def grid_transforms(N: int, spacing: float = 1.0, up_axis: str = "z", device="cp
375381
ori = torch.zeros((N, 4), device=device)
376382
ori[:, 3] = 1.0 # w=1 for identity quaternion
377383
return pos, ori
384+
385+
386+
def resolve_visualizer_clone_fn(
387+
physics_backend: str,
388+
requirements: SceneDataRequirement,
389+
stage,
390+
set_visualizer_artifact: Callable[[VisualizerPrebuiltArtifacts | None], None],
391+
):
392+
"""Return an optional visualizer prebuild hook for clone workflows.
393+
394+
Args:
395+
physics_backend: Active physics backend name.
396+
requirements: Aggregated scene-data requirements.
397+
stage: USD stage used by the clone callback.
398+
set_visualizer_artifact: Callback for storing prebuilt visualizer artifacts.
399+
400+
Returns:
401+
Clone callback when the prebuild path is supported; otherwise ``None``.
402+
"""
403+
if "physx" not in physics_backend or not requirements.requires_newton_model:
404+
return None
405+
try:
406+
from isaaclab_newton.cloner.newton_replicate import (
407+
create_newton_visualizer_prebuild_clone_fn,
408+
)
409+
except (ImportError, ModuleNotFoundError) as exc:
410+
logger.warning("Visualizer prebuild hook unavailable: failed to import backend helper.")
411+
logger.debug("Visualizer prebuild import failure details: %s", exc)
412+
return None
413+
414+
return create_newton_visualizer_prebuild_clone_fn(
415+
stage=stage,
416+
set_visualizer_artifact=set_visualizer_artifact,
417+
)

source/isaaclab/isaaclab/physics/scene_data_provider.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class SceneDataProvider(FactoryBase, BaseSceneDataProvider):
2323
_backend_class_names = {"physx": "PhysxSceneDataProvider", "newton": "NewtonSceneDataProvider"}
2424

2525
@classmethod
26-
def _get_backend(cls, visualizer_cfgs, stage, simulation_context: SimulationContext, *args, **kwargs) -> str:
26+
def _get_backend(cls, stage, simulation_context: SimulationContext, *args, **kwargs) -> str:
2727
manager_name = simulation_context.physics_manager.__name__.lower()
2828
if "newton" in manager_name:
2929
return "newton"
@@ -35,11 +35,9 @@ def _get_backend(cls, visualizer_cfgs, stage, simulation_context: SimulationCont
3535
def _get_module_name(cls, backend: str) -> str:
3636
return f"isaaclab_{backend}.scene_data_providers"
3737

38-
def __new__(
39-
cls, visualizer_cfgs, stage, simulation_context: SimulationContext, *args, **kwargs
40-
) -> BaseSceneDataProvider:
38+
def __new__(cls, stage, simulation_context: SimulationContext, *args, **kwargs) -> BaseSceneDataProvider:
4139
"""Create a new scene data provider based on the active physics backend."""
42-
result = super().__new__(cls, visualizer_cfgs, stage, simulation_context, *args, **kwargs)
40+
result = super().__new__(cls, stage, simulation_context, *args, **kwargs)
4341
if not isinstance(result, BaseSceneDataProvider):
4442
name = type(result).__name__
4543
raise TypeError(f"Backend scene data provider {name!r} must inherit from BaseSceneDataProvider.")
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Central requirement resolution for scene-data consumers.
7+
8+
This module is intentionally type-based (not config-import based) so requirement
9+
checks stay robust even when optional backend packages are not installed.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from collections.abc import Iterable
15+
from dataclasses import dataclass
16+
from typing import Any
17+
18+
19+
@dataclass(frozen=True)
20+
class SceneDataRequirement:
21+
"""Capabilities required from a scene data provider."""
22+
23+
requires_newton_model: bool = False
24+
requires_usd_stage: bool = False
25+
26+
27+
@dataclass(frozen=True)
28+
class VisualizerPrebuiltArtifacts:
29+
"""Prebuilt model/state payload shared from scene setup to providers.
30+
31+
This gets produced during clone-time visualizer prebuild and then read by
32+
scene data providers as a fast path (instead of rebuilding from USD).
33+
"""
34+
35+
model: Any
36+
state: Any
37+
rigid_body_paths: list[str]
38+
articulation_paths: list[str]
39+
num_envs: int
40+
41+
42+
_VISUALIZER_REQUIREMENTS: dict[str, SceneDataRequirement] = {
43+
"kit": SceneDataRequirement(requires_usd_stage=True),
44+
"newton": SceneDataRequirement(requires_newton_model=True),
45+
"rerun": SceneDataRequirement(requires_newton_model=True),
46+
"viser": SceneDataRequirement(requires_newton_model=True),
47+
}
48+
49+
_RENDERER_REQUIREMENTS: dict[str, SceneDataRequirement] = {
50+
"isaac_rtx": SceneDataRequirement(requires_usd_stage=True),
51+
"newton_warp": SceneDataRequirement(requires_newton_model=True),
52+
"ovrtx": SceneDataRequirement(requires_newton_model=True, requires_usd_stage=True),
53+
}
54+
55+
56+
def supported_visualizer_types() -> tuple[str, ...]:
57+
"""Return supported visualizer type names in sorted order.
58+
59+
Returns:
60+
Sorted tuple of supported visualizer type names.
61+
"""
62+
return tuple(sorted(_VISUALIZER_REQUIREMENTS))
63+
64+
65+
def supported_renderer_types() -> tuple[str, ...]:
66+
"""Return supported renderer type names in sorted order.
67+
68+
Returns:
69+
Sorted tuple of supported renderer type names.
70+
"""
71+
return tuple(sorted(_RENDERER_REQUIREMENTS))
72+
73+
74+
def requirement_for_visualizer_type(visualizer_type: str) -> SceneDataRequirement:
75+
"""Resolve scene-data requirements for one visualizer type.
76+
77+
Args:
78+
visualizer_type: Visualizer type name.
79+
80+
Returns:
81+
Requirement object for the given visualizer type.
82+
83+
Raises:
84+
ValueError: If ``visualizer_type`` is unknown.
85+
"""
86+
requirement = _VISUALIZER_REQUIREMENTS.get(visualizer_type)
87+
if requirement is None:
88+
supported = ", ".join(repr(v) for v in supported_visualizer_types())
89+
raise ValueError(f"Unknown visualizer type {visualizer_type!r}. Supported types: {supported}.")
90+
return requirement
91+
92+
93+
def requirement_for_renderer_type(renderer_type: str) -> SceneDataRequirement:
94+
"""Resolve scene-data requirements for one renderer type.
95+
96+
Args:
97+
renderer_type: Renderer type name.
98+
99+
Returns:
100+
Requirement object for the given renderer type.
101+
102+
Raises:
103+
ValueError: If ``renderer_type`` is unknown.
104+
"""
105+
requirement = _RENDERER_REQUIREMENTS.get(renderer_type)
106+
if requirement is None:
107+
supported = ", ".join(repr(v) for v in supported_renderer_types())
108+
raise ValueError(f"Unknown renderer type {renderer_type!r}. Supported types: {supported}.")
109+
return requirement
110+
111+
112+
def aggregate_requirements(requirements: Iterable[SceneDataRequirement]) -> SceneDataRequirement:
113+
"""Combine a sequence of requirements using logical OR.
114+
115+
Args:
116+
requirements: Requirement objects to combine.
117+
118+
Returns:
119+
Combined requirement object.
120+
"""
121+
requires_newton_model = False
122+
requires_usd_stage = False
123+
for requirement in requirements:
124+
requires_newton_model |= requirement.requires_newton_model
125+
requires_usd_stage |= requirement.requires_usd_stage
126+
return SceneDataRequirement(requires_newton_model=requires_newton_model, requires_usd_stage=requires_usd_stage)
127+
128+
129+
def resolve_scene_data_requirements(
130+
visualizer_types: Iterable[str],
131+
renderer_types: Iterable[str] = (),
132+
) -> SceneDataRequirement:
133+
"""Resolve combined scene-data requirements from visualizer and renderer types.
134+
135+
Args:
136+
visualizer_types: Visualizer type names to resolve.
137+
renderer_types: Renderer type names to resolve.
138+
139+
Returns:
140+
Combined requirement object.
141+
"""
142+
requirements = [requirement_for_visualizer_type(viz_type) for viz_type in visualizer_types]
143+
requirements.extend(requirement_for_renderer_type(renderer_type) for renderer_type in renderer_types)
144+
return aggregate_requirements(requirements)

0 commit comments

Comments
 (0)