Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Guidelines for modifications:
* Qingyang Jiang
* Qinxi Yu
* Rafael Wiltz
* Rebecca Zhang
* Rebecca (Rui) Zhang
* Renaud Poncelet
* René Zurbrügg
* Richard Lei
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions docs/source/overview/showroom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,35 @@ A few quick showroom scripts to run and checkout:
:alt: Multiple assets managed through the same simulation handles


- Compose registered task scenes into one heterogeneous simulation using clone combinations:

.. tab-set::
:sync-group: os

.. tab-item:: :icon:`fa-brands fa-linux` Linux
:sync: linux

.. code:: bash

./isaaclab.sh -p scripts/demos/heterogeneous_scene.py

.. tab-item:: :icon:`fa-brands fa-windows` Windows
:sync: windows

.. code:: batch

isaaclab.bat -p scripts\demos\heterogeneous_scene.py

.. image:: ../_static/demos/heterogeneous_scene.jpg
:width: 100%
:alt: Registered task scenes composed into one heterogeneous cloned simulation

The demo gathers every registered flat-floor PhysX task scene, folds them into a
single scene with :func:`~isaaclab.scene.add`, and clones the combined scene so
each environment hosts one task's assets. Use ``--num_task`` and ``--num_envs``
to run a smaller composition.


- Use the RigidObjectCollection spawn and view manipulation to demonstrate bin-packing example:

.. tab-set::
Expand Down
181 changes: 181 additions & 0 deletions scripts/demos/heterogeneous_scene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Compose a multi-robot scene from task configs and step physics only.

The pipeline is: gather registered task scenes, filter scenes whose floor is
not at level 0, fold them together with :func:`~isaaclab.scene.add`
while skipping every task light and floor, then add one Dome light and one
shared ground plane. No task environments or MDP managers are constructed;
the demo owns generic PhysX simulation settings. ``-Play`` task variants are
excluded up front; Newton scenes and scenes without a declarative level-0
floor are reported and skipped.

.. code-block:: bash

# Usage with every supported registered task scene.
./isaaclab.sh -p scripts/demos/heterogeneous_scene.py

# Usage with a smaller composition.
./isaaclab.sh -p scripts/demos/heterogeneous_scene.py --num_task 3 --num_envs 3

"""

from __future__ import annotations

"""Parse CLI first so we can decide whether to launch Isaac Sim Kit."""

import argparse
import sys

from isaaclab.app import add_launcher_args, launch_simulation

parser = argparse.ArgumentParser(
description="Demo: clone-only multi-robot multi-task scene.",
conflict_handler="resolve",
)
parser.add_argument("--num_envs", type=int, default=64, help="Number of environments.")
parser.add_argument("--env_spacing", type=float, default=2.5, help="Distance between environment origins [m].")
parser.add_argument("--sim_dt", type=float, default=1.0 / 60.0, help="Physics timestep [s].")
parser.add_argument(
"--num_task",
type=int,
default=None,
help="Number of tasks to use from the default order. Omit to use all tasks.",
)
parser.add_argument("--physics", default="physx", choices=["physx"], help="Physics backend.")
add_launcher_args(parser)
parser.set_defaults(visualizer=["kit"])
args_cli, hydra_args = parser.parse_known_args()
# strip consumed args so hydra-based task-config resolution does not re-parse them
sys.argv = [sys.argv[0], *hydra_args]

import gymnasium as gym

import isaaclab.sim as sim_utils
from isaaclab.assets import AssetBaseCfg
from isaaclab.physics import PhysicsCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.scene import add as scene_add
from isaaclab.terrains import TerrainImporterCfg

from isaaclab_tasks.utils import resolve_task_config


def _registered_task_ids() -> list[str]:
"""Return registered task IDs owned by isaaclab_tasks, skipping ``-Play`` variants."""
task_ids = []
for task_spec in gym.registry.values():
if task_spec.id.endswith("-Play"):
continue
entry_point = task_spec.kwargs.get("env_cfg_entry_point")
if isinstance(entry_point, str):
module_name = entry_point.split(":", maxsplit=1)[0]
else:
module_name = getattr(entry_point, "__module__", type(entry_point).__module__)
if module_name.startswith("isaaclab_tasks."):
task_ids.append(task_spec.id)
return sorted(task_ids)


def reject_scene(env_cfg: object) -> str | None:
"""Return why one resolved task config is outside this demo's scope."""
# inspect cfg metadata only: importing a backend package to type-check would
# defeat the resolvable class_type pattern (backends load via cfg resolution).
physics_cfg = env_cfg.sim.physics
if physics_cfg is not None and any(
cls.__module__.startswith("isaaclab_newton.") for cls in type(physics_cfg).__mro__
):
return f"Newton physics config {type(physics_cfg).__name__}"

scene_cfg = env_cfg.scene
fields = [value for name, value in vars(scene_cfg).items() if name not in InteractiveSceneCfg.__dataclass_fields__]
# flat terrain importers always place their plane at the stage origin
floor_levels = [0.0 for value in fields if isinstance(value, TerrainImporterCfg) and value.terrain_type == "plane"]
floor_levels += [
value.init_state.pos[2]
for value in fields
if isinstance(value, AssetBaseCfg) and isinstance(value.spawn, sim_utils.GroundPlaneCfg)
]
if not floor_levels:
return "no declarative flat floor"
mismatched = [level for level in floor_levels if abs(level) > 1e-6]
if mismatched:
return f"floor level {mismatched[0]} is not 0"
return None


def _load_task_scenes() -> tuple[list[str], list[InteractiveSceneCfg]]:
"""Gather every registered task scene and filter unsupported ones."""
accepted_ids, accepted_scenes, skipped = [], [], []
for task_id in _registered_task_ids():
env_cfg, _ = resolve_task_config(task_id, "")
reason = reject_scene(env_cfg)
if reason is not None:
skipped.append((task_id, reason))
continue
accepted_ids.append(task_id)
accepted_scenes.append(env_cfg.scene)
if args_cli.num_task is not None and len(accepted_ids) == args_cli.num_task:
break

if skipped:
print("\n[INFO] Skipped task scenes outside the composition scope:")
for task_id, reason in skipped:
print(f" {task_id}: {reason}")
if len(accepted_ids) < 2:
raise ValueError("Select at least two supported task scenes.")
return accepted_ids, accepted_scenes


def main() -> None:
"""Gather task scenes, filter by floor level, compose, add light and floor, simulate."""
# Resolve and compose every task scene before Kit launches: config resolution is
# simulator-free, and the launch swaps module state that must not interleave with it.
task_ids, task_scene_cfgs = _load_task_scenes()
print(f"\n[INFO] Composing task scenes: {task_ids}")
for task_scene_cfg in task_scene_cfgs:
task_scene_cfg.env_spacing = args_cli.env_spacing

scene_cfg = task_scene_cfgs[0]

def is_global_asset(a: AssetBaseCfg) -> bool:
return isinstance(a.spawn, (sim_utils.LightCfg, sim_utils.GroundPlaneCfg))

for task_scene_cfg in task_scene_cfgs[1:]:
scene_cfg = scene_add(scene_cfg, task_scene_cfg, asset_skip=is_global_asset)
scene_cfg.light = AssetBaseCfg(
prim_path="/World/light",
spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0),
)
scene_cfg.ground = AssetBaseCfg(prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg())

scene_cfg.num_envs = args_cli.num_envs
scene_cfg.replicate_physics = True

with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
sim = sim_utils.SimulationContext(
sim_utils.SimulationCfg(dt=args_cli.sim_dt, device=args_cli.device, physics=physics_cfg)
)
sim.set_camera_view(eye=[6.0, 6.0, 4.0], target=[0.0, 0.0, 0.5])
scene = scene_cfg.class_type(scene_cfg)
sim.reset()
scene.reset()
scene.write_data_to_sim()
print(f"[INFO] Composed {len(task_ids)} task scenes into {args_cli.num_envs} environments. Stepping physics.")

sim_dt = sim.get_physics_dt()
# Step while a visualizer window is still open (or none exist, e.g. headless).
while True:
if not sim.is_playing():
sim.step()
continue
scene.write_data_to_sim()
sim.step()
scene.update(sim_dt)


if __name__ == "__main__":
main()
38 changes: 38 additions & 0 deletions source/isaaclab/changelog.d/reb-multitask-demo-migrate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Added
^^^^^

* Added :attr:`~isaaclab.cloner.CloneCfg.clone_combinations` support for
heterogeneous scenes backed by direct clone plans.
* Added :func:`~isaaclab.scene.add` to fold environment-scoped scene
assets into heterogeneous clone combinations while deduplicating equivalent
environment definitions.
* Added a direct clone-only demo that composes registered flat PhysX task
scenes, excludes task scenes whose floor is not at level 0, and replaces
task lights and floors with one Dome light and one shared ground plane,
without constructing task environments.

Changed
^^^^^^^

* Changed the :attr:`~isaaclab.cloner.CloneCfg.clone_strategy` default from
:func:`~isaaclab.cloner.random` to :func:`~isaaclab.cloner.sequential`,
matching :func:`~isaaclab.cloner.make_clone_plan` and
:class:`~isaaclab.cloner.ReplicateSession`. Set
``clone_cfg.clone_strategy = random`` explicitly to keep random
prototype-to-environment assignment.

Fixed
^^^^^

* Fixed :func:`~isaaclab.cloner.resolve_clone_plan_source` raising
``NotImplementedError`` for assets cloned into only a subset of envs, which
blocked heterogeneous scenes where a robot type is present in just one task
group. Partial-env coverage now resolves to a destination glob spanning only
the envs that received the asset.
* Fixed static :class:`~isaaclab.assets.AssetBaseCfg` assets (e.g. tables) being
spawned only into their source env in heterogeneous scenes. They are now queued
for USD replication so the clone plan spreads them to every env of their group.
* Fixed USD replication double-applying per-env grid origins to assets cloned into
nested (sub-env) destinations, which offset static assets to the wrong place.
Env-origin transforms are now authored only on env-root prims; nested assets keep
their intra-env transform and inherit the origin from their env parent.
6 changes: 5 additions & 1 deletion source/isaaclab/isaaclab/cloner/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
__all__ = [
"CloneCfg",
"ClonePlan",
"InclusionSet",
"add",
"disabled_fabric_change_notifies",
"filter_collisions",
"get_suffix",
"grid_transforms",
"iter_clone_plan_matches",
"make_clone_plan",
"make_valid_clone_combinations",
"random",
"ReplicateSession",
"REPLICATION_QUEUE",
Expand All @@ -25,7 +28,7 @@ __all__ = [
]

from .clone_plan import ClonePlan
from .cloner_cfg import CloneCfg
from .cloner_cfg import CloneCfg, InclusionSet, add
from .cloner_strategies import random, sequential
from ._fabric_notices import disabled_fabric_change_notifies
from .cloner_utils import (
Expand All @@ -34,6 +37,7 @@ from .cloner_utils import (
grid_transforms,
iter_clone_plan_matches,
make_clone_plan,
make_valid_clone_combinations,
resolve_clone_plan_source,
split_clone_template,
)
Expand Down
40 changes: 37 additions & 3 deletions source/isaaclab/isaaclab/cloner/cloner_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@
from __future__ import annotations

from collections.abc import Callable
from dataclasses import MISSING

from isaaclab.utils.configclass import configclass

from .cloner_strategies import random
from .cloner_strategies import sequential


@configclass
class InclusionSet:
"""Legal clone combination defined by explicitly listing active assets."""

assets: list[str] = MISSING
"""Scene asset names active in this clone combination."""

weight: int = 1
"""Relative sampling weight for this clone combination."""


@configclass
Expand All @@ -20,11 +32,33 @@ class CloneCfg:
:func:`~isaaclab.cloner.make_clone_plan` when building per-env layouts.
"""

clone_strategy: Callable[..., object] = random
"""Function used to build prototype-to-environment mapping. Default is :func:`random`."""
clone_strategy: Callable[..., object] = sequential
"""Function used to build prototype-to-environment mapping. Default is :func:`sequential`."""

clone_combinations: list[InclusionSet] = []
"""Legal scene-asset combinations for heterogeneous clone planning.

Each entry names the assets that are active in one legal combination.
Assets not referenced by any entry are active in every combination. An
empty list keeps the homogeneous/default behavior.
"""

device: str = "cpu"
"""Torch device on which mapping buffers are allocated."""

clone_regex: str = "/World/envs/env_.*"
"""Regex matching every replicated env prim. Used to expand ``{ENV_REGEX_NS}`` cfg macros."""


def add(this: CloneCfg, other: InclusionSet) -> CloneCfg:
"""Append one clone combination to ``this`` and return it.

Args:
this: Configuration that accumulates the combination.
other: Clone combination to append.

Returns:
``this``, with the combination appended.
"""
this.clone_combinations = [*this.clone_combinations, other]
return this
Loading
Loading