Skip to content

Commit 8087ffe

Browse files
committed
make cloning respect .* wild card
1 parent 3e51f84 commit 8087ffe

File tree

10 files changed

+122
-156
lines changed

10 files changed

+122
-156
lines changed

source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
from typing import TYPE_CHECKING
99

1010
from pxr import Usd
11-
11+
import logging
1212
import isaaclab.sim.utils.prims as prim_utils
1313
from isaaclab.sim.spawners.from_files import UsdFileCfg
1414

1515
if TYPE_CHECKING:
1616
from . import wrappers_cfg
1717

18+
logger = logging.getLogger(__name__)
19+
1820

1921
def spawn_multi_asset(
2022
prim_path: str,
@@ -24,11 +26,14 @@ def spawn_multi_asset(
2426
clone_in_fabric: bool = False,
2527
replicate_physics: bool = False,
2628
) -> Usd.Prim:
27-
"""Spawn multiple assets based on the provided configurations.
29+
"""Spawn multiple assets into numbered prim paths derived from the provided configuration.
2830
29-
This function spawns multiple assets based on the provided configurations. The assets are spawned
30-
in the order they are provided in the list. If the :attr:`~MultiAssetSpawnerCfg.random_choice` parameter is
31-
set to True, a random asset configuration is selected for each spawn.
31+
Assets are created in the order they appear in ``cfg.assets_cfg`` using the base name in ``prim_path``,
32+
which must contain ``.*`` (for example, ``/World/Env_0/asset_.*`` spawns ``asset_0``, ``asset_1``, ...).
33+
The prefix portion of ``prim_path`` may also include ``.*`` (for example, ``/World/env_.*/asset_.*``);
34+
this is only allowed when :attr:`~isaaclab.sim.spawners.wrappers.wrappers_cfg.MultiAssetSpawnerCfg.enable_clone`
35+
is True, in which case assets are spawned under the first match (``env_0``) and that structure is cloned to
36+
other matching environments.
3237
3338
Args:
3439
prim_path: The prim path to spawn the assets.
@@ -41,7 +46,27 @@ def spawn_multi_asset(
4146
Returns:
4247
The created prim at the first prim path.
4348
"""
44-
# spawn everything first in a "Dataset" prim
49+
split_path = prim_path.split("/")
50+
prefix_path, base_name = "/".join(split_path[:-1]), split_path[-1]
51+
if ".*" in prefix_path and not cfg.enable_clone:
52+
raise ValueError(
53+
f" Found '.*' in prefix path '{prefix_path}' but enable_clone is False. Set enable_clone=True to allow"
54+
f" this pattern, which would replicate all {len(cfg.assets_cfg)} assets into every environment that"
55+
" matches the prefix. If you want heterogeneous assets across envs, instead of set enable_clone to True"
56+
"spawn them under a single template (e.g., /World/Template/Robot) and use the cloner to place"
57+
"them at their final paths."
58+
)
59+
if ".*" not in base_name:
60+
raise ValueError(
61+
f" The base name '{base_name}' in the prim path '{prim_path}' must contain '.*' to indicate"
62+
" the path each individual multiple-asset to be spawned."
63+
)
64+
if cfg.random_choice:
65+
logger.warning(
66+
"`random_choice` parameter in `spawn_multi_asset` is deprecated, and nothing will happen. "
67+
"Use `isaaclab.scene.interactive_scene_cfg.InteractiveSceneCfg.random_heterogeneous_cloning` instead."
68+
)
69+
4570
proto_prim_paths = list()
4671
for index, asset_cfg in enumerate(cfg.assets_cfg):
4772
# append semantic tags if specified
@@ -56,8 +81,8 @@ def spawn_multi_asset(
5681
attr_value = getattr(cfg, attr_name)
5782
if hasattr(asset_cfg, attr_name) and attr_value is not None:
5883
setattr(asset_cfg, attr_name, attr_value)
59-
# spawn single instance
60-
proto_prim_path = prim_path.replace(".*", f"{index}")
84+
85+
proto_prim_path = f"{prefix_path}/{base_name.replace('.*', str(index))}"
6186
asset_cfg.func(
6287
proto_prim_path,
6388
asset_cfg,
@@ -69,7 +94,7 @@ def spawn_multi_asset(
6994
# append to proto prim paths
7095
proto_prim_paths.append(proto_prim_path)
7196

72-
return prim_utils.get_prim_at_path(proto_prim_paths[0])
97+
return prim_utils.find_first_matching_prim(proto_prim_paths[0])
7398

7499

75100
def spawn_multi_usd_file(

source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers_cfg.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ class MultiAssetSpawnerCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
3434
assets_cfg: list[SpawnerCfg] = MISSING
3535
"""List of asset configurations to spawn."""
3636

37-
random_choice: bool = True
38-
"""Whether to randomly select an asset configuration. Default is True.
37+
enable_clone: bool = False
38+
"""This is enables the cloning of spawned assets from first instance to all other instances that has the
39+
same prefix path pattern, for example, given /World/env_.*/asset_.*, multi-asset spawner will spawn asset_0,
40+
asset_1, asset_n under /World/env_0. Then if enable_clone is set to True, env_0 is cloned to env_1, env_2, etc.
41+
If False, .* is not allowed in the prefix path of the prim_path, an safty check error will be raised."""
3942

40-
If False, the asset configurations are spawned in the order they are provided in the list.
41-
If True, a random asset configuration is selected for each spawn.
43+
random_choice: bool = True
44+
""" This parameter is ignored.
45+
See :attr:`isaaclab.scene.interactive_scene_cfg.InteractiveSceneCfg.random_heterogeneous_cloning` for details.
4246
4347
.. warning::
4448

source/isaaclab/isaaclab/sim/utils/prims.py

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import inspect
1010
import logging
1111
import numpy as np
12+
import torch
1213
import re
1314
from collections.abc import Callable, Sequence
1415
from typing import TYPE_CHECKING, Any
@@ -17,13 +18,10 @@
1718
import omni.kit.commands
1819
import omni.usd
1920
import usdrt
20-
from isaacsim.core.cloner import Cloner
21-
from isaacsim.core.version import get_version
2221
from omni.usd.commands import DeletePrimsCommand, MovePrimCommand
2322
from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, UsdShade
2423

2524
from isaaclab.utils.string import to_camel_case
26-
2725
from .semantics import add_labels
2826
from .stage import add_reference_to_stage, attach_stage_to_usd_context, get_current_stage
2927

@@ -1489,10 +1487,10 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs):
14891487
else:
14901488
source_prim_paths = [root_path]
14911489

1492-
# resolve prim paths for spawning and cloning
1493-
prim_paths = [f"{source_prim_path}/{asset_path}" for source_prim_path in source_prim_paths]
1490+
# resolve prim paths for spawning
1491+
prim_spawn_path = prim_path.replace(".*", "0")
14941492
# spawn single instance
1495-
prim = func(prim_paths[0], cfg, *args, **kwargs)
1493+
prim = func(prim_spawn_path, cfg, *args, **kwargs)
14961494
# set the prim visibility
14971495
if hasattr(cfg, "visible"):
14981496
imageable = UsdGeom.Imageable(prim)
@@ -1519,28 +1517,18 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs):
15191517
if hasattr(cfg, "activate_contact_sensors") and cfg.activate_contact_sensors:
15201518
from ..schemas import schemas as _schemas
15211519

1522-
_schemas.activate_contact_sensors(prim_paths[0])
1520+
_schemas.activate_contact_sensors(prim_spawn_path)
15231521
# clone asset using cloner API
1524-
if len(prim_paths) > 1:
1525-
cloner = Cloner(stage=stage)
1526-
# check version of Isaac Sim to determine whether clone_in_fabric is valid
1527-
isaac_sim_version = float(".".join(get_version()[2]))
1528-
if isaac_sim_version < 5:
1529-
# clone the prim
1530-
cloner.clone(
1531-
prim_paths[0], prim_paths[1:], replicate_physics=False, copy_from_source=cfg.copy_from_source
1532-
)
1533-
else:
1534-
# clone the prim
1535-
clone_in_fabric = kwargs.get("clone_in_fabric", False)
1536-
replicate_physics = kwargs.get("replicate_physics", False)
1537-
cloner.clone(
1538-
prim_paths[0],
1539-
prim_paths[1:],
1540-
replicate_physics=replicate_physics,
1541-
copy_from_source=cfg.copy_from_source,
1542-
clone_in_fabric=clone_in_fabric,
1543-
)
1522+
if len(source_prim_paths) > 1:
1523+
# lazy import to avoid circular import
1524+
from isaaclab.scene.cloner import usd_replicate
1525+
formattable_path = f"{root_path.replace('.*', '{}')}/{asset_path}"
1526+
usd_replicate(
1527+
stage=stage,
1528+
sources=[formattable_path.format(0)],
1529+
destinations=[formattable_path],
1530+
env_ids=torch.arange(len(source_prim_paths)),
1531+
)
15441532
# return the source prim
15451533
return prim
15461534

source/isaaclab/test/assets/test_articulation.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from isaaclab.assets import Articulation, ArticulationCfg
3232
from isaaclab.envs.mdp.terminations import joint_effort_out_of_limit
3333
from isaaclab.managers import SceneEntityCfg
34-
from isaaclab.scene import cloner
3534
from isaaclab.sim import build_simulation_context
3635
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
3736

@@ -179,14 +178,6 @@ def generate_articulation(
179178
prim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3])
180179
articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot"))
181180

182-
cloner.usd_replicate(
183-
stage=prim_utils.get_current_stage(),
184-
sources=["/World/Env_0"],
185-
destinations=["/World/Env_{}"],
186-
env_ids=torch.arange(num_articulations),
187-
positions=translations,
188-
)
189-
190181
return articulation, translations
191182

192183

source/isaaclab/test/assets/test_deformable_object.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import isaaclab.sim.utils.prims as prim_utils
2828
import isaaclab.utils.math as math_utils
2929
from isaaclab.assets import DeformableObject, DeformableObjectCfg
30-
from isaaclab.scene import cloner
3130
from isaaclab.sim import build_simulation_context
3231

3332

@@ -86,13 +85,6 @@ def generate_cubes_scene(
8685
init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height), rot=initial_rot),
8786
)
8887
cube_object = DeformableObject(cfg=cube_object_cfg)
89-
cloner.usd_replicate(
90-
stage=prim_utils.get_current_stage(),
91-
sources=["/World/Table_0"],
92-
destinations=["/World/Table_{}"],
93-
env_ids=torch.arange(num_cubes),
94-
positions=origins,
95-
)
9688
return cube_object
9789

9890

source/isaaclab/test/assets/test_rigid_object.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import isaaclab.sim as sim_utils
2727
import isaaclab.sim.utils.prims as prim_utils
2828
from isaaclab.assets import RigidObject, RigidObjectCfg
29-
from isaaclab.scene import cloner
3029
from isaaclab.sim import build_simulation_context
3130
from isaaclab.sim.spawners import materials
3231
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR
@@ -94,14 +93,6 @@ def generate_cubes_scene(
9493
)
9594
cube_object = RigidObject(cfg=cube_object_cfg)
9695

97-
cloner.usd_replicate(
98-
stage=prim_utils.get_current_stage(),
99-
sources=["/World/Table_0"],
100-
destinations=["/World/Table_{}"],
101-
env_ids=torch.arange(num_cubes),
102-
positions=origins,
103-
)
104-
10596
return cube_object, origins
10697

10798

source/isaaclab/test/assets/test_rigid_object_collection.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import isaaclab.sim as sim_utils
2525
import isaaclab.sim.utils.prims as prim_utils
2626
from isaaclab.assets import RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg
27-
from isaaclab.scene import cloner
2827
from isaaclab.sim import build_simulation_context
2928
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
3029
from isaaclab.utils.math import (
@@ -91,13 +90,6 @@ def generate_cubes_scene(
9190
# create the rigid object collection
9291
cube_object_collection_cfg = RigidObjectCollectionCfg(rigid_objects=cube_config_dict)
9392
cube_object_colection = RigidObjectCollection(cfg=cube_object_collection_cfg)
94-
cloner.usd_replicate(
95-
stage=prim_utils.get_current_stage(),
96-
sources=["/World/Table_0"],
97-
destinations=["/World/Table_{}"],
98-
env_ids=torch.arange(num_envs),
99-
positions=origins,
100-
)
10193
return cube_object_colection, origins
10294

10395

source/isaaclab/test/sensors/test_multi_tiled_camera.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,6 @@
3030
import isaaclab.sim.utils.prims as prim_utils
3131
import isaaclab.sim.utils.stage as stage_utils
3232
from isaaclab.sensors.camera import TiledCamera, TiledCameraCfg
33-
from isaaclab.scene import cloner
34-
35-
36-
def _replicate_group_origins(group_index: int, num_cameras: int) -> None:
37-
"""Replicate `/World/Origin_{i}_0` subtree to `/World/Origin_{i}_{1..N-1}` with preserved translations."""
38-
stage = prim_utils.get_current_stage()
39-
fmt = f"/World/Origin_{group_index}_{{}}"
40-
get_translate = (
41-
lambda prim_path: stage.GetPrimAtPath(prim_path).GetAttribute("xformOp:translate").Get()
42-
) # noqa: E731
43-
positions = torch.tensor([get_translate(fmt.format(j)) for j in range(num_cameras)])
44-
cloner.usd_replicate(
45-
stage=stage,
46-
sources=[fmt.format(0)],
47-
destinations=[fmt],
48-
env_ids=torch.arange(num_cameras),
49-
positions=positions,
50-
)
5133

5234

5335
@pytest.fixture()
@@ -102,8 +84,6 @@ def test_multi_tiled_camera_init(setup_camera):
10284
camera_cfg = copy.deepcopy(camera_cfg)
10385
camera_cfg.prim_path = f"/World/Origin_{i}_.*/CameraSensor"
10486
camera = TiledCamera(camera_cfg)
105-
# Ensure each origin in this group gets a camera
106-
_replicate_group_origins(i, num_cameras_per_tiled_camera)
10787
tiled_cameras.append(camera)
10888

10989
# Check simulation parameter is set correctly
@@ -200,8 +180,6 @@ def test_all_annotators_multi_tiled_camera(setup_camera):
200180
camera_cfg.data_types = all_annotator_types
201181
camera_cfg.prim_path = f"/World/Origin_{i}_.*/CameraSensor"
202182
camera = TiledCamera(camera_cfg)
203-
# Ensure each origin in this group gets a camera
204-
_replicate_group_origins(i, num_cameras_per_tiled_camera)
205183
tiled_cameras.append(camera)
206184

207185
# Check simulation parameter is set correctly
@@ -303,8 +281,6 @@ def test_different_resolution_multi_tiled_camera(setup_camera):
303281
camera_cfg.prim_path = f"/World/Origin_{i}_.*/CameraSensor"
304282
camera_cfg.height, camera_cfg.width = resolutions[i]
305283
camera = TiledCamera(camera_cfg)
306-
# Ensure each origin in this group gets a camera
307-
_replicate_group_origins(i, num_cameras_per_tiled_camera)
308284
tiled_cameras.append(camera)
309285

310286
# Check simulation parameter is set correctly
@@ -375,8 +351,6 @@ def test_frame_offset_multi_tiled_camera(setup_camera):
375351
camera_cfg = copy.deepcopy(camera_cfg)
376352
camera_cfg.prim_path = f"/World/Origin_{i}_.*/CameraSensor"
377353
camera = TiledCamera(camera_cfg)
378-
# Ensure each origin in this group gets a camera
379-
_replicate_group_origins(i, num_cameras_per_tiled_camera)
380354
tiled_cameras.append(camera)
381355

382356
# modify scene to be less stochastic
@@ -446,7 +420,6 @@ def test_frame_different_poses_multi_tiled_camera(setup_camera):
446420
camera_cfg.prim_path = f"/World/Origin_{i}_.*/CameraSensor"
447421
camera_cfg.offset = TiledCameraCfg.OffsetCfg(pos=positions[i], rot=rotations[i], convention="ros")
448422
camera = TiledCamera(camera_cfg)
449-
_replicate_group_origins(i, num_cameras_per_tiled_camera)
450423
tiled_cameras.append(camera)
451424

452425
# Play sim

0 commit comments

Comments
 (0)