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
1 change: 1 addition & 0 deletions .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jobs:
--exclude 'docs\.ray\.io'
--exclude 'docs\.conda\.io'
--exclude 'stackoverflow\.com'
--exclude 'threedworld\.org'
--max-retries 5
--retry-wait-time 10
--timeout 20
Expand Down
569 changes: 274 additions & 295 deletions docs/source/how-to/cloning.rst

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions docs/source/migration/migrating_from_isaacgymenvs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ adding any other optional objects into the scene, such as lights.
| self.up_axis = self.cfg["sim"]["up_axis"] | # add ground plane |
| | spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg() |
| self.sim = super().create_sim(self.device_id, self.graphics_device_id, | # clone, filter, and replicate |
| self.physics_engine, self.sim_params) | self.scene.clone_environments(copy_from_source=False) |
| self.physics_engine, self.sim_params) | # assets are built inside ReplicateSession |
| self._create_ground_plane() | self.scene.filter_collisions(global_prim_paths=[]) |
| self._create_envs(self.num_envs, self.cfg["env"]['envSpacing'], | # add articulation to scene |
| int(np.sqrt(self.num_envs))) | self.scene.articulations["cartpole"] = self.cartpole |
Expand Down Expand Up @@ -369,16 +369,15 @@ Isaac Lab eliminates the need for looping through the environments by using the
The scene creation process is as follow:

#. Construct a single environment (what the scene would look like if number of environments = 1)
#. Call ``clone_environments()`` to replicate the single environment
#. Use ``cloner.ReplicateSession`` to replicate the single environment
#. Call ``filter_collisions()`` to filter out collision between environments (if required)


.. code-block:: python

# construct a single environment with the Cartpole robot
self.cartpole = Articulation(self.cfg.robot_cfg)
# clone the environment
self.scene.clone_environments(copy_from_source=False)
# construct and replicate a single environment with the Cartpole robot
with cloner.ReplicateSession():
self.cartpole = Articulation(self.cfg.robot_cfg)
# filter collisions
self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path])

Expand Down Expand Up @@ -660,8 +659,8 @@ the need to set simulation parameters for actors in the task implementation.
| | spawn_ground_plane(prim_path="/World/ground", |
| self.sim = super().create_sim(self.device_id, | cfg=GroundPlaneCfg()) |
| self.graphics_device_id, self.physics_engine, | # clone, filter, and replicate |
| self.sim_params) | self.scene.clone_environments( |
| self._create_ground_plane() | copy_from_source=False) |
| self.sim_params) | # assets are built inside ReplicateSession |
| self._create_ground_plane() | |
| self._create_envs(self.num_envs, | self.scene.filter_collisions( |
| self.cfg["env"]['envSpacing'], | global_prim_paths=[]) |
| int(np.sqrt(self.num_envs))) | # add articulation to scene |
Expand Down
6 changes: 3 additions & 3 deletions docs/source/migration/migrating_from_omniisaacgymenvs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ will automatically be created for the actor. This avoids the need to separately
| super().set_up_scene(scene) | # add ground plane |
| | spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg() |
| self._cartpoles = ArticulationView( | # clone, filter, and replicate |
| prim_paths_expr="/World/envs/.*/Cartpole", | self.scene.clone_environments(copy_from_source=False) |
| prim_paths_expr="/World/envs/.*/Cartpole", | # assets are built inside ReplicateSession |
| name="cartpole_view", reset_xform_properties=False | self.scene.filter_collisions(global_prim_paths=[]) |
| ) | # add articulation to scene |
| scene.add(self._cartpoles) | self.scene.articulations["cartpole"] = self.cartpole |
Expand Down Expand Up @@ -633,8 +633,8 @@ Adding actors to the scene has been replaced by ``self.scene.articulations["cart
| super().set_up_scene(scene) | spawn_ground_plane(prim_path="/World/ground", |
| self._cartpoles = ArticulationView( | cfg=GroundPlaneCfg()) |
| prim_paths_expr="/World/envs/.*/Cartpole", | # clone, filter, and replicate |
| name="cartpole_view", | self.scene.clone_environments( |
| reset_xform_properties=False | copy_from_source=False) |
| name="cartpole_view", | # assets are built inside ReplicateSession |
| reset_xform_properties=False | |
| ) | self.scene.filter_collisions( |
| scene.add(self._cartpoles) | global_prim_paths=[]) |
| return | # add articulation to scene |
Expand Down
11 changes: 5 additions & 6 deletions docs/source/setup/walkthrough/api_env_design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,12 @@ Next, let's take a look at the contents of the other python file in our task dir
. . .

def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
with cloner.ReplicateSession():
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
# add lights
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
light_cfg.func("/World/Light", light_cfg)
Expand Down Expand Up @@ -143,7 +142,7 @@ When the environment is initialized, it receives its own config as an argument,
to initialize the ``DirectRLEnv``. This super call also calls ``_setup_scene``, which actually constructs the scene and clones
it appropriately. Notably is how the robot is created and registered to the scene in ``_setup_scene``. First, the robot articulation
is created by using the ``robot_config`` we defined in ``IsaacLabTutorialEnvCfg``: it doesn't exist before this point! When the
articulation is created, the robot exists on the stage at ``/World/envs/env_0/Robot``. The call to ``scene.clone_environments`` then
articulation is created, the robot exists on the stage at ``/World/envs/env_0/Robot``. The call to ``cloner.ReplicateSession`` then
copies ``env_0`` appropriately. At this point the robot exists as many copies on the stage, so all that's left is to notify the ``scene``
object of the existence of this articulation to be tracked. The articulations of the scene are kept as a dictionary, so ``scene.articulations["robot"] = self.robot``
creates a new ``robot`` element of the ``articulations`` dictionary and sets the value to be ``self.robot``.
Expand Down
9 changes: 4 additions & 5 deletions docs/source/setup/walkthrough/technical_env_design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,10 @@ replace the contents of the ``__init__`` and ``_setup_scene`` methods with the f
self.dof_idx, _ = self.robot.find_joints(self.cfg.dof_names)

def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
with cloner.ReplicateSession():
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# add lights
Expand Down
9 changes: 4 additions & 5 deletions docs/source/setup/walkthrough/training_jetbot_gt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,10 @@ Next, we need to expand the initialization and setup steps to construct the data
.. code-block:: python

def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
with cloner.ReplicateSession():
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# add lights
Expand Down
7 changes: 5 additions & 2 deletions scripts/demos/pick_and_place.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import omni

import isaaclab.sim as sim_utils
from isaaclab import cloner
from isaaclab.assets import (
Articulation,
ArticulationCfg,
Expand Down Expand Up @@ -221,8 +222,10 @@ def _setup_scene(self):
self.gripper = SurfaceGripper(self.cfg.gripper)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
src, dest = "/World/envs/env_0", "/World/envs/env_{}"
pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0]
plan = cloner.ClonePlan.from_env_0(src, dest, self.scene.num_envs, self.device, pos)
cloner.replicate(plan, stage=self.scene.stage)
# add articulation to scene
self.scene.articulations["pick_and_place"] = self.pick_and_place
self.scene.rigid_objects["cube"] = self.cube
Expand Down
6 changes: 5 additions & 1 deletion scripts/tutorials/06_deploy/anymal_c_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import warp as wp

import isaaclab.sim as sim_utils
from isaaclab import cloner
from isaaclab.assets import Articulation
from isaaclab.envs import DirectRLEnv
from isaaclab.sensors import ContactSensor, RayCaster
Expand Down Expand Up @@ -63,7 +64,10 @@ def _setup_scene(self):
self.cfg.terrain.num_envs = self.scene.cfg.num_envs
self.cfg.terrain.env_spacing = self.scene.cfg.env_spacing
self._terrain = self.cfg.terrain.class_type(self.cfg.terrain)
self.scene.clone_environments(copy_from_source=False)
src, dest = "/World/envs/env_0", "/World/envs/env_{}"
pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0]
plan = cloner.ClonePlan.from_env_0(src, dest, self.scene.num_envs, self.device, pos)
cloner.replicate(plan, stage=self.scene.stage)
if self.device == "cpu":
self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path])
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
Expand Down
49 changes: 49 additions & 0 deletions source/isaaclab/changelog.d/replication-session-redesign.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Added
^^^^^

* Added :data:`~isaaclab.cloner.REPLICATION_QUEUE` and the free function
:func:`~isaaclab.cloner.replicate`, the explicit registry-and-drain pair
that backends now hook into for replication.
* Added :meth:`~isaaclab.cloner.ClonePlan.from_env_0` for direct envs that
clone a single env-0 prototype across every env.
* Added :attr:`~isaaclab.cloner.CloneCfg.clone_regex` as the single source
of truth for the env-namespace convention (default ``"/World/envs/env_.*"``).

Fixed
^^^^^

* Fixed :data:`~isaaclab.cloner.REPLICATION_QUEUE` leaking stale entries
when a backend or asset construction raised mid-session.

Changed
^^^^^^^

* **Breaking:** Rewrote :class:`~isaaclab.cloner.ReplicateSession` as a thin
context manager around :func:`~isaaclab.cloner.make_clone_plan` and
:func:`~isaaclab.cloner.replicate`. The no-arg form and the cached
``plan`` / ``cfg_rows`` / ``replicate_on_exit`` fields are gone. Direct
envs migrate to ``cloner.replicate(cloner.ClonePlan.from_env_0(...))``.
* **Breaking:** Changed :func:`~isaaclab.cloner.make_clone_plan` to take
``cfgs`` and absorb the cfg-driven planning logic previously inside
:class:`~isaaclab.scene.InteractiveScene`, returning a self-contained
:class:`~isaaclab.cloner.ClonePlan`.
* **Breaking:** :func:`~isaaclab.cloner.replicate` and
:class:`~isaaclab.cloner.ReplicateSession` now require an explicit
``stage=`` keyword; the :class:`~isaaclab.cloner.ClonePlan` is
stage-agnostic.
* Changed :attr:`~isaaclab.scene.InteractiveScene.env_origins` to read from
the published :class:`~isaaclab.cloner.ClonePlan`, making the plan the
single source of truth for env placement.

Removed
^^^^^^^

* **Breaking:** Removed ``isaaclab.cloner.replicate_session_defaults`` and
``isaaclab.cloner.replicate_session``. Use
:data:`~isaaclab.cloner.REPLICATION_QUEUE` and
:func:`~isaaclab.cloner.replicate` instead.
* **Breaking:** Removed :meth:`InteractiveScene.clone_environments`; direct
envs should use ``cloner.replicate(cloner.ClonePlan.from_env_0(...))``.
* **Breaking:** Removed :attr:`InteractiveScene.env_ns` and
:attr:`InteractiveScene.env_regex_ns`; read
:attr:`~isaaclab.cloner.CloneCfg.clone_regex` instead.
16 changes: 15 additions & 1 deletion source/isaaclab/isaaclab/cloner/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,30 @@ __all__ = [
"filter_collisions",
"grid_transforms",
"make_clone_plan",
"ReplicateSession",
"REPLICATION_QUEUE",
"replicate",
"queue_usd_replication",
"UsdReplicateContext",
"usd_replicate",
]

from .clone_plan import ClonePlan
from .cloner_cfg import CloneCfg
from .cloner_strategies import random, sequential
from ._fabric_notices import disabled_fabric_change_notifies
from .cloner_utils import (
disabled_fabric_change_notifies,
filter_collisions,
grid_transforms,
make_clone_plan,
)
from .replicate_session import (
REPLICATION_QUEUE,
ReplicateSession,
replicate,
)
from .usd import (
UsdReplicateContext,
queue_usd_replication,
usd_replicate,
)
58 changes: 58 additions & 0 deletions source/isaaclab/isaaclab/cloner/_fabric_notices.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@

from __future__ import annotations

import contextlib
import ctypes
import logging
import threading
from collections.abc import Iterator

from pxr import Usd, UsdUtils

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -149,3 +153,57 @@ def get_bindings() -> FabricNoticeBindings | None:
return None
_BINDINGS = b
return _BINDINGS


@contextlib.contextmanager
def disabled_fabric_change_notifies(stage: Usd.Stage, *, restore: bool = True) -> Iterator[None]:
"""Suspend Fabric's USD notice listener for the body of the ``with`` block.

The listener is a global ``TfNotice`` that fires on every ``Sdf.CopySpec`` during
cloning; toggling it off via ``IFabricUsd::setEnableChangeNotifies`` skips the
per-spec Fabric sync that dominates cloning time for large PhysX rigid-body scenes.
Same toggle that :meth:`isaacsim.core.cloner.Cloner.disable_change_listener` flips,
called directly through Carbonite so we don't take an
``isaacsim.core.simulation_manager`` dependency.

Falls through to a no-op if the Carbonite interface can't be acquired (e.g. outside
a live Kit application).

Args:
stage: USD stage whose Fabric notice handler should be suspended.
restore: When ``True`` (default), re-enable the handler on exit. Set to
``False`` if a downstream Fabric resync is about to happen anyway (e.g.
right before ``sim.reset()``), to avoid a redundant re-enable.

Yields:
None.
"""
bindings = get_bindings()
if bindings is None:
yield
return

# usdrt only works with a live Kit app — defer import so module load stays cheap.
import usdrt

# Avoid leaking a strong reference into the global ``StageCache`` for stages we did not
# author into the cache: ``Insert`` keeps the stage alive for the rest of the process.
cache = UsdUtils.StageCache.Get()
cached_id = cache.GetId(stage)
stage_id = cached_id.ToLongInt() if cached_id.IsValid() else cache.Insert(stage).ToLongInt()
# ``FabricId`` wraps a uint64; the C ABI needs the raw integer.
fabric_id = usdrt.Usd.Stage.Attach(stage_id).GetFabricId().id
# First-call ABI sanity check — if the toggle doesn't actually round-trip the flag
# (e.g. Kit's vtable shifted), fall through to a no-op rather than corrupting state.
if not bindings.validate_with(fabric_id):
logger.warning("Fabric notice toggle failed round-trip check — suspension disabled")
yield
return
was_enabled = bindings.is_enabled(fabric_id)
if was_enabled:
bindings.set_enable(fabric_id, False)
try:
yield
finally:
if restore and was_enabled:
bindings.set_enable(fabric_id, True)
Loading
Loading