Skip to content

Commit d9819f7

Browse files
committed
pr
1 parent 6f991d4 commit d9819f7

36 files changed

Lines changed: 811 additions & 174 deletions

CONTRIBUTORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Guidelines for modifications:
165165
* Qingyang Jiang
166166
* Qinxi Yu
167167
* Rafael Wiltz
168-
* Rebecca Zhang
168+
* Rebecca (Rui) Zhang
169169
* Renaud Poncelet
170170
* René Zurbrügg
171171
* Richard Schmitt
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
"""Compose a multi-robot scene from task configs and step physics only.
7+
8+
The pipeline is: gather registered task scenes, filter scenes whose floor is
9+
not at level 0, fold them together with :func:`~isaaclab.scene.scene_add`
10+
while skipping every task light and floor, then add one Dome light and one
11+
shared ground plane. No task environments or MDP managers are constructed;
12+
the demo owns generic PhysX simulation settings. ``-Play`` task variants are
13+
excluded up front; Newton scenes and scenes without a declarative level-0
14+
floor are reported and skipped.
15+
16+
.. code-block:: bash
17+
18+
# Usage with every supported registered task scene.
19+
./isaaclab.sh -p scripts/demos/multitask_clone_scene.py
20+
21+
# Usage with a smaller composition.
22+
./isaaclab.sh -p scripts/demos/multitask_clone_scene.py --num_task 3 --num_envs 3
23+
24+
"""
25+
26+
from __future__ import annotations
27+
28+
"""Parse CLI first so we can decide whether to launch Isaac Sim Kit."""
29+
30+
import argparse
31+
import sys
32+
33+
from isaaclab.app import add_launcher_args, launch_simulation
34+
35+
parser = argparse.ArgumentParser(
36+
description="Demo: clone-only multi-robot multi-task scene.",
37+
conflict_handler="resolve",
38+
)
39+
parser.add_argument("--num_envs", type=int, default=64, help="Number of environments.")
40+
parser.add_argument("--env_spacing", type=float, default=2.5, help="Distance between environment origins [m].")
41+
parser.add_argument("--sim_dt", type=float, default=1.0 / 60.0, help="Physics timestep [s].")
42+
parser.add_argument(
43+
"--num_task",
44+
type=int,
45+
default=None,
46+
help="Number of tasks to use from the default order. Omit to use all tasks.",
47+
)
48+
parser.add_argument("--physics", default="physx", choices=["physx"], help="Physics backend.")
49+
add_launcher_args(parser)
50+
parser.set_defaults(visualizer=["kit"])
51+
args_cli, hydra_args = parser.parse_known_args()
52+
# strip consumed args so hydra-based task-config resolution does not re-parse them
53+
sys.argv = [sys.argv[0], *hydra_args]
54+
55+
import gymnasium as gym
56+
57+
import isaaclab.sim as sim_utils
58+
from isaaclab.assets import AssetBaseCfg
59+
from isaaclab.cloner import sequential
60+
from isaaclab.physics import PhysicsCfg
61+
from isaaclab.scene import InteractiveSceneCfg, scene_add
62+
from isaaclab.terrains import TerrainImporterCfg
63+
64+
from isaaclab_tasks.utils import resolve_task_config
65+
66+
67+
def _registered_task_ids() -> list[str]:
68+
"""Return registered task IDs owned by isaaclab_tasks, skipping ``-Play`` variants."""
69+
task_ids = []
70+
for task_spec in gym.registry.values():
71+
if task_spec.id.endswith("-Play"):
72+
continue
73+
entry_point = task_spec.kwargs.get("env_cfg_entry_point")
74+
if isinstance(entry_point, str):
75+
module_name = entry_point.split(":", maxsplit=1)[0]
76+
else:
77+
module_name = getattr(entry_point, "__module__", type(entry_point).__module__)
78+
if module_name.startswith("isaaclab_tasks."):
79+
task_ids.append(task_spec.id)
80+
return sorted(task_ids)
81+
82+
83+
def reject_scene(env_cfg: object) -> str | None:
84+
"""Return why one resolved task config is outside this demo's scope."""
85+
# inspect cfg metadata only: importing a backend package to type-check would
86+
# defeat the resolvable class_type pattern (backends load via cfg resolution).
87+
physics_cfg = env_cfg.sim.physics
88+
if physics_cfg is not None and any(
89+
cls.__module__.startswith("isaaclab_newton.") for cls in type(physics_cfg).__mro__
90+
):
91+
return f"Newton physics config {type(physics_cfg).__name__}"
92+
93+
scene_cfg = env_cfg.scene
94+
fields = [value for name, value in vars(scene_cfg).items() if name not in InteractiveSceneCfg.__dataclass_fields__]
95+
# flat terrain importers always place their plane at the stage origin
96+
floor_levels = [0.0 for value in fields if isinstance(value, TerrainImporterCfg) and value.terrain_type == "plane"]
97+
floor_levels += [
98+
value.init_state.pos[2]
99+
for value in fields
100+
if isinstance(value, AssetBaseCfg) and isinstance(value.spawn, sim_utils.GroundPlaneCfg)
101+
]
102+
if not floor_levels:
103+
return "no declarative flat floor"
104+
mismatched = [level for level in floor_levels if abs(level) > 1e-6]
105+
if mismatched:
106+
return f"floor level {mismatched[0]} is not 0"
107+
return None
108+
109+
110+
def _load_task_scenes() -> tuple[list[str], list[InteractiveSceneCfg]]:
111+
"""Gather every registered task scene and filter unsupported ones."""
112+
accepted_ids, accepted_scenes, skipped = [], [], []
113+
for task_id in _registered_task_ids():
114+
env_cfg, _ = resolve_task_config(task_id, "")
115+
reason = reject_scene(env_cfg)
116+
if reason is not None:
117+
skipped.append((task_id, reason))
118+
continue
119+
accepted_ids.append(task_id)
120+
accepted_scenes.append(env_cfg.scene)
121+
if args_cli.num_task is not None and len(accepted_ids) == args_cli.num_task:
122+
break
123+
124+
if skipped:
125+
print("\n[INFO] Skipped task scenes outside the composition scope:")
126+
for task_id, reason in skipped:
127+
print(f" {task_id}: {reason}")
128+
if len(accepted_ids) < 2:
129+
raise ValueError("Select at least two supported task scenes.")
130+
return accepted_ids, accepted_scenes
131+
132+
133+
def main() -> None:
134+
"""Gather task scenes, filter by floor level, compose, add light and floor, simulate."""
135+
# Resolve and compose every task scene before Kit launches: config resolution is
136+
# simulator-free, and the launch swaps module state that must not interleave with it.
137+
task_ids, task_scene_cfgs = _load_task_scenes()
138+
print(f"\n[INFO] Composing task scenes: {task_ids}")
139+
for task_scene_cfg in task_scene_cfgs:
140+
task_scene_cfg.env_spacing = args_cli.env_spacing
141+
142+
scene_cfg = task_scene_cfgs[0]
143+
144+
def is_global_asset(a: AssetBaseCfg) -> bool:
145+
return isinstance(a.spawn, (sim_utils.LightCfg, sim_utils.GroundPlaneCfg))
146+
147+
for task_scene_cfg in task_scene_cfgs[1:]:
148+
scene_cfg = scene_add(scene_cfg, task_scene_cfg, asset_skip=is_global_asset)
149+
scene_cfg.light = AssetBaseCfg(
150+
prim_path="/World/light",
151+
spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0),
152+
)
153+
scene_cfg.ground = AssetBaseCfg(prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg())
154+
155+
scene_cfg.num_envs = args_cli.num_envs
156+
scene_cfg.replicate_physics = True
157+
scene_cfg.clone_cfg.clone_strategy = sequential
158+
159+
with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
160+
sim = sim_utils.SimulationContext(
161+
sim_utils.SimulationCfg(dt=args_cli.sim_dt, device=args_cli.device, physics=physics_cfg)
162+
)
163+
sim.set_camera_view(eye=[6.0, 6.0, 4.0], target=[0.0, 0.0, 0.5])
164+
scene = scene_cfg.class_type(scene_cfg)
165+
sim.reset()
166+
scene.reset()
167+
scene.write_data_to_sim()
168+
print(f"[INFO] Composed {len(task_ids)} task scenes into {args_cli.num_envs} environments. Stepping physics.")
169+
170+
sim_dt = sim.get_physics_dt()
171+
# Step while a visualizer window is still open (or none exist, e.g. headless).
172+
while sim.is_headless_or_exist_active_visualizer():
173+
if not sim.is_playing():
174+
sim.step()
175+
continue
176+
scene.write_data_to_sim()
177+
sim.step()
178+
scene.update(sim_dt)
179+
180+
181+
if __name__ == "__main__":
182+
main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Added
2+
^^^^^
3+
4+
* Added :attr:`~isaaclab.cloner.CloneCfg.clone_combinations` support for
5+
heterogeneous scenes backed by direct clone plans.
6+
* Added :func:`~isaaclab.scene.scene_add` to compose environment-scoped scene
7+
assets into heterogeneous clone combinations while deduplicating equivalent
8+
environment definitions.
9+
* Added a direct clone-only demo that composes registered flat PhysX task
10+
scenes, excludes task scenes whose floor is not at level 0, and replaces
11+
task lights and floors with one Dome light and one shared ground plane,
12+
without constructing task environments.
13+
14+
Fixed
15+
^^^^^
16+
17+
* Fixed :func:`~isaaclab.cloner.resolve_clone_plan_source` raising
18+
``NotImplementedError`` for assets cloned into only a subset of envs, which
19+
blocked heterogeneous scenes where a robot type is present in just one task
20+
group. Partial-env coverage now resolves to a destination glob spanning only
21+
the envs that received the asset.
22+
* Fixed static :class:`~isaaclab.assets.AssetBaseCfg` assets (e.g. tables) being
23+
spawned only into their source env in heterogeneous scenes. They are now queued
24+
for USD replication so the clone plan spreads them to every env of their group.
25+
* Fixed USD replication double-applying per-env grid origins to assets cloned into
26+
nested (sub-env) destinations, which offset static assets to the wrong place.
27+
Env-origin transforms are now authored only on env-root prims; nested assets keep
28+
their intra-env transform and inherit the origin from their env parent.

source/isaaclab/isaaclab/cloner/__init__.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
__all__ = [
77
"CloneCfg",
88
"ClonePlan",
9+
"InclusionSet",
910
"disabled_fabric_change_notifies",
1011
"filter_collisions",
1112
"get_suffix",
1213
"grid_transforms",
1314
"iter_clone_plan_matches",
1415
"make_clone_plan",
16+
"make_valid_clone_combinations",
1517
"random",
1618
"ReplicateSession",
1719
"REPLICATION_QUEUE",
@@ -25,7 +27,7 @@ __all__ = [
2527
]
2628

2729
from .clone_plan import ClonePlan
28-
from .cloner_cfg import CloneCfg
30+
from .cloner_cfg import CloneCfg, InclusionSet
2931
from .cloner_strategies import random, sequential
3032
from ._fabric_notices import disabled_fabric_change_notifies
3133
from .cloner_utils import (
@@ -34,6 +36,7 @@ from .cloner_utils import (
3436
grid_transforms,
3537
iter_clone_plan_matches,
3638
make_clone_plan,
39+
make_valid_clone_combinations,
3740
resolve_clone_plan_source,
3841
split_clone_template,
3942
)

source/isaaclab/isaaclab/cloner/cloner_cfg.py

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

88
from collections.abc import Callable
9+
from dataclasses import MISSING
910

1011
from isaaclab.utils.configclass import configclass
1112

1213
from .cloner_strategies import random
1314

1415

16+
@configclass
17+
class InclusionSet:
18+
"""Legal clone combination defined by explicitly listing active assets."""
19+
20+
assets: list[str] = MISSING
21+
"""Scene asset names active in this clone combination."""
22+
23+
weight: int = 1
24+
"""Relative sampling weight for this clone combination."""
25+
26+
1527
@configclass
1628
class CloneCfg:
1729
"""Configuration for environment replication.
@@ -23,6 +35,14 @@ class CloneCfg:
2335
clone_strategy: Callable[..., object] = random
2436
"""Function used to build prototype-to-environment mapping. Default is :func:`random`."""
2537

38+
clone_combinations: list[InclusionSet] = []
39+
"""Legal scene-asset combinations for heterogeneous clone planning.
40+
41+
Each entry names the assets that are active in one legal combination.
42+
Assets not referenced by any entry are active in every combination. An
43+
empty list keeps the homogeneous/default behavior.
44+
"""
45+
2646
device: str = "cpu"
2747
"""Torch device on which mapping buffers are allocated."""
2848

0 commit comments

Comments
 (0)