From 573dae8290ff2d5028d229801f0eae2a3f7d8fab Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Wed, 29 Jul 2026 21:26:54 +0800 Subject: [PATCH 1/2] Make collision mode init param and set it true for the kitchen yaml --- isaaclab_arena/assets/background_library.py | 9 +++++++-- isaaclab_arena/relations/placement_asset.py | 8 ++++++-- .../droid_pick_and_place_lightwheel_kitchen.yaml | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/isaaclab_arena/assets/background_library.py b/isaaclab_arena/assets/background_library.py index 4e5639bcfb..7fcaf01c9a 100644 --- a/isaaclab_arena/assets/background_library.py +++ b/isaaclab_arena/assets/background_library.py @@ -173,7 +173,12 @@ class LightwheelKitchenBackground(LibraryBackground): initial_pose = Pose.identity() object_min_z = -0.2 - def __init__(self, layout_id: int = 1, style_id: int = 1): + def __init__( + self, + layout_id: int = 1, + style_id: int = 1, + **kwargs, + ): from lightwheel_sdk.loader import floorplan_loader # Lazily download the USD @@ -188,7 +193,7 @@ def __init__(self, layout_id: int = 1, style_id: int = 1): backend="robocasa", )[0] ) - super().__init__() + super().__init__(**kwargs) def get_viewer_cfg(self) -> ViewerCfg: # Looking in through the open front. diff --git a/isaaclab_arena/relations/placement_asset.py b/isaaclab_arena/relations/placement_asset.py index c659f54979..e1ad2625fd 100644 --- a/isaaclab_arena/relations/placement_asset.py +++ b/isaaclab_arena/relations/placement_asset.py @@ -28,15 +28,19 @@ class PlaceableAsset(Asset, ABC): """Asset whose root pose can be constrained by spatial relations.""" def __init__(self, name: str, tags: list[str] | None = None, **kwargs) -> None: + collision_mode = kwargs.pop("collision_mode", None) + repair_collision_mesh_non_watertight = kwargs.pop("repair_collision_mesh_non_watertight", True) super().__init__(name=name, tags=tags, **kwargs) self.initial_pose: Pose | PoseRange | PosePerEnv | None = None self._pose_event_cfg: EventTermCfg | None = None """Reset event restoring this asset's root pose; ``None`` until a pose with a reset event is set.""" self.relations: list[RelationBase] = [] # None delegates collision-mode selection to the solver. - self.collision_mode: CollisionMode | None = None + if collision_mode is not None: + collision_mode = CollisionMode(collision_mode) + self.collision_mode: CollisionMode | None = collision_mode # Whether to replace a non-watertight collision mesh with its convex hull. - self.repair_collision_mesh_non_watertight = True + self.repair_collision_mesh_non_watertight = repair_collision_mesh_non_watertight def add_relation(self, relation: RelationBase) -> None: """Attach a relation to the asset.""" diff --git a/isaaclab_arena_environments/kitchen_bench/droid_pick_and_place_lightwheel_kitchen.yaml b/isaaclab_arena_environments/kitchen_bench/droid_pick_and_place_lightwheel_kitchen.yaml index 831f2d51f9..151c41e0ed 100644 --- a/isaaclab_arena_environments/kitchen_bench/droid_pick_and_place_lightwheel_kitchen.yaml +++ b/isaaclab_arena_environments/kitchen_bench/droid_pick_and_place_lightwheel_kitchen.yaml @@ -13,7 +13,9 @@ embodiment: background: id: kitchen registry_name: lightwheel_robocasa_kitchen - params: {} + params: + collision_mode: mesh + repair_collision_mesh_non_watertight: false objects: - id: mustard_bottle registry_name: mustard_bottle_hope_robolab From ae128185f12938fe9b3cdf77f839ecd2647708bc Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Wed, 29 Jul 2026 21:26:56 +0800 Subject: [PATCH 2/2] Bind placement pool on env at runtime Pass the live PooledObjectPlacer through env_kwargs (like variation_recorder) instead of EventTermCfg params. Mesh-mode placement builds Warp BVHs on the pool; storing it in config deepcopy fails with "ctypes objects containing pointers cannot be pickled" and configclass validation recurses into cyclic asset graphs. The reset event now has empty params and reads the pool from the env. Leaving Warp mesh caches on the pool through Fabric startup also hides the Droid stand_instanceable in Kit viz (physics and placement stay correct). Use --disable_fabric for viewport runs until that is resolved; a 4-env / 2000-step benchmark showed no rollout cost from the flag (87.0 vs 86.5 ms/step). Signed-off-by: Qian Lin --- .../environments/arena_env_builder.py | 6 +- .../isaaclab_arena_manager_based_env.py | 11 ++++ .../environments/relation_solver_interface.py | 21 ++++--- isaaclab_arena/relations/placement_events.py | 25 ++++---- isaaclab_arena/tests/test_placement_events.py | 63 ++++++++++--------- ...st_relation_solver_background_collision.py | 4 +- .../tests/test_relation_solver_interface.py | 62 +++++++++++++++--- 7 files changed, 127 insertions(+), 65 deletions(-) diff --git a/isaaclab_arena/environments/arena_env_builder.py b/isaaclab_arena/environments/arena_env_builder.py index a5d7b7fefe..533403204a 100644 --- a/isaaclab_arena/environments/arena_env_builder.py +++ b/isaaclab_arena/environments/arena_env_builder.py @@ -40,6 +40,7 @@ from isaaclab_arena.recording.progress_terms import ProgressEpisodeRecorderTermCfg from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams from isaaclab_arena.relations.placement_events import PLACEMENT_RESET_EVENT_NAME +from isaaclab_arena.relations.pooled_object_placer import PooledObjectPlacer from isaaclab_arena.relations.relation_solver_params import RelationSolverParams from isaaclab_arena.tasks.no_task import NoTask from isaaclab_arena.utils.configclass import combine_configclass_instances, make_configclass @@ -66,6 +67,7 @@ def __init__( num_envs=cfg.num_envs, env_spacing=cfg.env_spacing, replicate_physics=False ) self._placement_event_cfg: EventTermCfg | None = None + self._placement_pool: PooledObjectPlacer | None = None def _solve_relations(self) -> None: """Solve spatial relations for scene objects and the embodiment. @@ -107,7 +109,7 @@ def _solve_relations(self) -> None: # Delists itself unless the embodiment has a registered cuRobo config and the solver deps are importable. # TODO(xinjieyao, 2026-07-22): updated once robot-object co-placement is merged. placer_params.reachability_config.embodiment = self.arena_env.embodiment - self._placement_event_cfg = solve_and_apply_relation_placement( + self._placement_event_cfg, self._placement_pool = solve_and_apply_relation_placement( placement_assets, num_envs=self.cfg.num_envs, placer_params=placer_params, @@ -402,6 +404,8 @@ def compose_manager_cfg(self) -> tuple[IsaacLabArenaManagerBasedRLEnvCfg, dict[s env_cfg.scene.replicate_physics = True env_kwargs: dict[str, Any] = {"variation_recorder": variation_recorder} + if self._placement_pool is not None: + env_kwargs["placement_pool"] = self._placement_pool return env_cfg, env_kwargs def get_entry_point(self) -> str | type[ManagerBasedRLMimicEnv]: diff --git a/isaaclab_arena/environments/isaaclab_arena_manager_based_env.py b/isaaclab_arena/environments/isaaclab_arena_manager_based_env.py index 5616e3b7a6..69e3709b4e 100644 --- a/isaaclab_arena/environments/isaaclab_arena_manager_based_env.py +++ b/isaaclab_arena/environments/isaaclab_arena_manager_based_env.py @@ -6,6 +6,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import TYPE_CHECKING from isaaclab.envs import ManagerBasedRLEnv @@ -16,6 +17,9 @@ from isaaclab_arena.tasks.predicates.object_settling import ObjectInitialRestPoseRecorder from isaaclab_arena.variations.variation_recorder import VariationRecorder +if TYPE_CHECKING: + from isaaclab_arena.relations.pooled_object_placer import PooledObjectPlacer + class IsaacLabArenaManagerBasedRLEnv(ManagerBasedRLEnv): """Arena extension to ManagerBasedRLEnv that adds additional Arena-specific functionality.""" @@ -27,12 +31,14 @@ def __init__( cfg: IsaacLabArenaManagerBasedRLEnvCfg, render_mode: str | None = None, variation_recorder: VariationRecorder | None = None, + placement_pool: PooledObjectPlacer | None = None, **kwargs, ): self._object_initial_rest_pose_recorder = ObjectInitialRestPoseRecorder( num_envs=cfg.scene.num_envs, device=cfg.sim.device ) self._variation_recorder = variation_recorder + self._placement_pool = placement_pool if variation_recorder is not None: # Bind so run-time variation draws can be attributed to the current episode index. variation_recorder.bind_env(self) @@ -42,6 +48,11 @@ def __init__( self._first_reset = True super().__init__(cfg=cfg, render_mode=render_mode, **kwargs) + @property + def placement_pool(self) -> PooledObjectPlacer | None: + """The runtime placement pool, or ``None`` when the env has no pooled reset placement.""" + return self._placement_pool + @property def variation_recorder(self) -> VariationRecorder | None: """The recorder of variation samples, or ``None`` if the env was not built with one.""" diff --git a/isaaclab_arena/environments/relation_solver_interface.py b/isaaclab_arena/environments/relation_solver_interface.py index c8730b60ce..9caf4ef15c 100644 --- a/isaaclab_arena/environments/relation_solver_interface.py +++ b/isaaclab_arena/environments/relation_solver_interface.py @@ -42,7 +42,7 @@ def solve_and_apply_relation_placement( placer_params: ObjectPlacerParams | None = None, collision_objects: list[CollisionObject] | None = None, scene_assets: Iterable[Asset | RigidObjectSet] | None = None, -) -> EventTermCfg | None: +) -> tuple[EventTermCfg | None, PooledObjectPlacer | None]: """Solve relation placement and apply the result to asset reset/static state. Args: @@ -56,12 +56,13 @@ def solve_and_apply_relation_placement( when collision_objects is not supplied. Returns: - Reset event config to attach to the environment when placement should be - resolved on reset. Returns ``None`` when no reset event is needed. + A ``(reset_event_cfg, placement_pool)`` pair. When ``resolve_on_reset`` is + enabled, ``placement_pool`` is the live runtime pool to pass through + ``env_kwargs``; otherwise both entries are ``None``. """ if not assets: print("No assets with relations found in scene. Skipping relation solving.") - return None + return None, None asset_names = {asset.name for asset in assets} assert len(asset_names) == len(assets), "Placement asset names must be unique" scene_keys = [asset.get_scene_key() for asset in assets] @@ -92,7 +93,7 @@ def solve_and_apply_relation_placement( collision_objects=collision_objects, ) # Validators are built once above and reused for every refill, so the embodiment is done being read; drop - # it before the reset-event params below capture (and deep-copy/validate) the pool. + # it before the pool is handed to the env at runtime. placer_params.reachability_config.embodiment = None if placement_pool.had_fallbacks: @@ -101,12 +102,15 @@ def solve_and_apply_relation_placement( "that failed strict placement validation." ) - return _apply_relation_placement_result( + event_cfg = _apply_relation_placement_result( assets=assets, placer_params=placer_params, placement_pool=placement_pool, num_envs=num_envs, ) + if event_cfg is not None: + return event_cfg, placement_pool + return None, None def _should_include_background_mesh( @@ -183,10 +187,7 @@ def _apply_dynamic_spawn_pose( return EventTermCfg( func=solve_and_place_objects, mode="reset", - params={ - "assets": assets, - "placement_pool": placement_pool, - }, + params={}, ) diff --git a/isaaclab_arena/relations/placement_events.py b/isaaclab_arena/relations/placement_events.py index bed1ca3d7e..801ced1c27 100644 --- a/isaaclab_arena/relations/placement_events.py +++ b/isaaclab_arena/relations/placement_events.py @@ -22,24 +22,21 @@ IDENTITY_ROTATION_XYZW = (0.0, 0.0, 0.0, 1.0) -# Name of the reset event term that owns the pooled object placer. +# Name of the reset event term registered when ``resolve_on_reset`` is enabled. PLACEMENT_RESET_EVENT_NAME = "placement_reset" def get_placement_pool(env) -> PooledObjectPlacer | None: - """Return the pooled placer stored on the env reset event, or ``None`` when absent. + """Return the runtime placement pool bound on the env, or ``None`` when absent. - Lets a runtime caller reach the pool (e.g. to run the post-reset settle check) from the env alone, - without holding the builder. The pool is reached through the env's event manager. + The pool is build-time state passed through ``ArenaEnvBuilder`` ``env_kwargs`` and + stored on :class:`~isaaclab_arena.environments.isaaclab_arena_manager_based_env.IsaacLabArenaManagerBasedRLEnv`, + not in ``EventTermCfg`` params. Args: env: The gym-wrapped Isaac Lab env; the base env is reached via ``env.unwrapped``. """ - try: - term_cfg = env.unwrapped.event_manager.get_term_cfg(PLACEMENT_RESET_EVENT_NAME) - except ValueError: - return None - return term_cfg.params.get("placement_pool") + return getattr(env.unwrapped, "placement_pool", None) def get_rotation_xyzw(asset: PlaceableAsset) -> tuple[float, float, float, float]: @@ -113,23 +110,23 @@ def write_layout_to_sim( def solve_and_place_objects( env: ManagerBasedEnv, env_ids: torch.Tensor | None, - assets: list[PlaceableAsset], - placement_pool: PooledObjectPlacer, ) -> None: """Coordinated reset event that draws layouts from the pool and writes poses. Registered as a single EventTermCfg(mode="reset"). Layouts are env-indexed: one layout is consumed for each requested absolute env id, so partial resets - only advance the pools of the resetting envs. + only advance the pools of the resetting envs. The pool is read from the env + via :func:`get_placement_pool`. Args: env: The Isaac Lab environment. env_ids: 1-D tensor of environment indices being reset. - assets: Assets participating in relation solving. - placement_pool: Runtime pool of solved placement layouts. """ + placement_pool = get_placement_pool(env) + assert placement_pool is not None, "Env has no placement pool; build through ArenaEnvBuilder." if env_ids is None or len(env_ids) == 0: return + assets = placement_pool.objects reset_env_ids = env_ids.tolist() num_scene_envs = env.scene.env_origins.shape[0] assert ( diff --git a/isaaclab_arena/tests/test_placement_events.py b/isaaclab_arena/tests/test_placement_events.py index 29b7dfbeda..cf98029b05 100644 --- a/isaaclab_arena/tests/test_placement_events.py +++ b/isaaclab_arena/tests/test_placement_events.py @@ -135,6 +135,7 @@ def _make_mock_env(num_envs: int, device: str = "cpu") -> MagicMock: env = MagicMock() env.device = device + env.unwrapped = env env.scene.env_origins = torch.zeros(num_envs, 3, device=device) assets: dict[str, MagicMock] = {} @@ -149,16 +150,12 @@ def scene_getitem(self, name: str) -> MagicMock: return env -def _solve_and_place_with_pool(env, env_ids, objects, pool): - """Call the reset event with the same runtime params EventTermCfg stores.""" +def _solve_and_place_with_pool(env, env_ids, pool): + """Call the reset event with the pool bound on the env.""" from isaaclab_arena.relations.placement_events import solve_and_place_objects - return solve_and_place_objects( - env, - env_ids, - assets=objects, - placement_pool=pool, - ) + env.unwrapped.placement_pool = pool + return solve_and_place_objects(env, env_ids) def test_solve_and_place_objects_writes_poses_to_sim(): @@ -176,7 +173,7 @@ def test_solve_and_place_objects_writes_poses_to_sim(): placer_params = ObjectPlacerParams(solver_params=solver_params) pool = PooledObjectPlacer(objects=objects, placer_params=placer_params, pool_size=10) - _solve_and_place_with_pool(env, env_ids, objects, pool) + _solve_and_place_with_pool(env, env_ids, pool) # Anchor (desk) should NOT have been written. assert "desk" not in env._assets, "Anchor pose should not be written to sim" @@ -206,6 +203,7 @@ def test_solve_and_place_objects_uses_runtime_pool(): class Pool: num_envs = 1 + objects = [desk, robot] def sample_for_envs(self, env_ids: list[int]) -> dict[int, PlacementResult]: assert env_ids == [0] @@ -218,11 +216,10 @@ def sample_for_envs(self, env_ids: list[int]) -> dict[int, PlacementResult]: ) } + env.unwrapped.placement_pool = Pool() solve_and_place_objects( env, torch.tensor([0]), - assets=[desk, robot], - placement_pool=Pool(), ) assert "desk" not in env._assets @@ -303,7 +300,7 @@ class Pool: pool = Pool() env = MagicMock() - env.unwrapped.event_manager.get_term_cfg.return_value.params = {"placement_pool": pool} + env.unwrapped.placement_pool = pool assert get_placement_pool(env) is pool @@ -328,7 +325,7 @@ def test_solve_and_place_objects_applies_random_yaw(): ) pool = PooledObjectPlacer(objects=objects, placer_params=placer_params, pool_size=10) - _solve_and_place_with_pool(env, env_ids, objects, pool) + _solve_and_place_with_pool(env, env_ids, pool) # Anchor (desk) is never rotated or written, even with random yaw enabled. assert "desk" not in env._assets, "Anchor pose should not be written to sim" @@ -356,7 +353,7 @@ def test_solve_and_place_objects_skips_empty_env_ids(): placer_params = ObjectPlacerParams(solver_params=solver_params) pool = PooledObjectPlacer(objects=[desk, box1, box2], placer_params=placer_params, pool_size=10) - _solve_and_place_with_pool(env, torch.tensor([], dtype=torch.int64), [desk, box1, box2], pool) + _solve_and_place_with_pool(env, torch.tensor([], dtype=torch.int64), pool) assert len(env._assets) == 0, "No writes should occur for empty env_ids" @@ -373,7 +370,7 @@ def test_solve_and_place_objects_skips_none_env_ids(): placer_params = ObjectPlacerParams(solver_params=solver_params) pool = PooledObjectPlacer(objects=[desk, box1, box2], placer_params=placer_params, pool_size=10) - _solve_and_place_with_pool(env, None, [desk, box1, box2], pool) + _solve_and_place_with_pool(env, None, pool) assert len(env._assets) == 0, "No writes should occur for None env_ids" @@ -394,7 +391,7 @@ def test_solve_and_place_objects_handles_multiple_env_ids(): placer_params = ObjectPlacerParams(solver_params=solver_params) pool = PooledObjectPlacer(objects=objects, placer_params=placer_params, pool_size=12, num_envs=num_envs) - _solve_and_place_with_pool(env, env_ids, objects, pool) + _solve_and_place_with_pool(env, env_ids, pool) assert "desk" not in env._assets, "Anchor pose should not be written to sim" @@ -424,7 +421,7 @@ def test_solve_and_place_objects_partial_reset_homogeneous_pool_consumes_only_re pool = PooledObjectPlacer(objects=objects, placer_params=placer_params, pool_size=12, num_envs=num_envs) available_before = pool.total_remaining - _solve_and_place_with_pool(env, env_ids, objects, pool) + _solve_and_place_with_pool(env, env_ids, pool) available_after = pool.total_remaining assert available_before - available_after == len(env_ids) @@ -436,11 +433,11 @@ def test_solve_and_place_objects_writes_invalid_fallback_layout(capsys): from isaaclab_arena.relations.placement_result import PlacementResult desk, box1, box2 = _create_test_objects() - objects = [desk, box1, box2] env = _make_mock_env(num_envs=1) class InvalidPool: num_envs = 1 + objects = [desk, box1, box2] def sample_for_envs(self, env_ids: list[int]) -> dict[int, PlacementResult]: assert env_ids == [0] @@ -453,7 +450,7 @@ def sample_for_envs(self, env_ids: list[int]) -> dict[int, PlacementResult]: ) } - _solve_and_place_with_pool(env, torch.tensor([0]), objects, InvalidPool()) + _solve_and_place_with_pool(env, torch.tensor([0]), InvalidPool()) captured = capsys.readouterr() assert set(env._assets) == {box1.name, box2.name} @@ -466,12 +463,12 @@ def test_solve_and_place_objects_partial_reset_applies_absolute_env_origin(): from isaaclab_arena.relations.placement_result import PlacementResult desk, box1, box2 = _create_test_objects() - objects = [desk, box1, box2] env = _make_mock_env(num_envs=4) env.scene.env_origins[2] = torch.tensor([10.0, 0.0, 0.0]) class EnvIndexedPool: num_envs = 4 + objects = [desk, box1, box2] requested_env_ids = None def sample_without_replacement(self, count: int) -> list[PlacementResult]: @@ -493,7 +490,7 @@ def sample_for_envs(self, env_ids: list[int]) -> dict[int, PlacementResult]: } pool = EnvIndexedPool() - _solve_and_place_with_pool(env, torch.tensor([2]), objects, pool) + _solve_and_place_with_pool(env, torch.tensor([2]), pool) box1_pose = env._assets[box1.name].write_root_pose_to_sim.call_args[0][0] box2_pose = env._assets[box2.name].write_root_pose_to_sim.call_args[0][0] @@ -510,14 +507,14 @@ def test_solve_and_place_objects_asserts_env_indexed_pool_size_matches_scene(): """Env-indexed pool slots must line up with absolute Isaac Lab env ids.""" desk, box1, box2 = _create_test_objects() - objects = [desk, box1, box2] env = _make_mock_env(num_envs=2) class MismatchedEnvIndexedPool: num_envs = 1 + objects = [desk, box1, box2] with pytest.raises(AssertionError, match="scene has 2 env origins"): - _solve_and_place_with_pool(env, torch.tensor([0]), objects, MismatchedEnvIndexedPool()) + _solve_and_place_with_pool(env, torch.tensor([0]), MismatchedEnvIndexedPool()) def test_pooled_placer_sample_without_replacement_returns_different_layouts(): @@ -950,11 +947,11 @@ def test_reachability_validator_reject_all_raises_without_fallback(): ) -def test_solve_and_apply_relation_placement_drops_embodiment_from_event_params(): - """The build-time-only reachability embodiment must not survive into the reset-event params. +def test_solve_and_apply_relation_placement_drops_embodiment_from_runtime_pool(): + """The build-time-only reachability embodiment must not survive on the runtime pool. - Isaac Lab deep-copies and validates those params, and a live embodiment's cyclic ``mimic_env``/scene - config graph overflows both passes (deepcopy on un-picklable handles, ``_validate`` on the cycle). + Validators are built once during pool construction; the embodiment is dropped before + the pool is handed to the env at runtime. """ from types import SimpleNamespace @@ -976,12 +973,16 @@ def test_solve_and_apply_relation_placement_drops_embodiment_from_event_params() ) params.reachability_config.embodiment = embodiment - event = solve_and_apply_relation_placement([desk, box1, box2], num_envs=1, placer_params=params) + event, pool = solve_and_apply_relation_placement([desk, box1, box2], num_envs=1, placer_params=params) # The caller's own config is copied before severing, so its embodiment is left intact... assert params.reachability_config.embodiment is embodiment - # ...while the pool the reset event captured no longer references the embodiment -- on the placer params - # and on every built validator alike -- so configclass never deep-copies or recurses into it. - pool = event.params["placement_pool"] + # ...while the runtime pool no longer references the embodiment on the placer params + # and on every built validator alike. + from isaaclab.utils.configclass import _validate + + assert event is not None + assert pool is not None + _validate(event, prefix="") assert pool._placer.params.reachability_config.embodiment is None assert all(v._params.reachability_config.embodiment is None for v in pool._placer._validators) diff --git a/isaaclab_arena/tests/test_relation_solver_background_collision.py b/isaaclab_arena/tests/test_relation_solver_background_collision.py index 83dfb443ac..9e3dae9e27 100644 --- a/isaaclab_arena/tests/test_relation_solver_background_collision.py +++ b/isaaclab_arena/tests/test_relation_solver_background_collision.py @@ -555,7 +555,7 @@ def fake_solve_and_apply_relation_placement( calls["placer_params"] = placer_params calls["scene_assets"] = list(scene_assets) calls["collision_objects"] = collision_objects - return "placement_event" + return "placement_event", None monkeypatch.setattr(builder_module, "solve_and_apply_relation_placement", fake_solve_and_apply_relation_placement) placer_params = ObjectPlacerParams(solver_params=RelationSolverParams(collision_mode=CollisionMode.MESH)) @@ -598,6 +598,7 @@ def fake_solve_and_apply_relation_placement( calls["objects"] = objects calls["scene_assets"] = list(scene_assets) calls["collision_objects"] = collision_objects + return None, None monkeypatch.setattr(builder_module, "solve_and_apply_relation_placement", fake_solve_and_apply_relation_placement) arena_env = SimpleNamespace(scene=Scene(), placer_params=None, embodiment=None, task=None) @@ -634,6 +635,7 @@ def get_relations(self): def fake_solve_and_apply_relation_placement(*args, **kwargs): calls.update(kwargs) calls["objects"] = args[0] + return None, None monkeypatch.setattr(builder_module, "solve_and_apply_relation_placement", fake_solve_and_apply_relation_placement) embodiment = Embodiment() diff --git a/isaaclab_arena/tests/test_relation_solver_interface.py b/isaaclab_arena/tests/test_relation_solver_interface.py index 561a446770..e2a792dab8 100644 --- a/isaaclab_arena/tests/test_relation_solver_interface.py +++ b/isaaclab_arena/tests/test_relation_solver_interface.py @@ -34,8 +34,13 @@ def _make_box(name: str = "box"): class _FakePlacementPool: - def __init__(self, layouts) -> None: + def __init__(self, layouts, objects=None) -> None: self._layouts = layouts + self._objects = objects or [] + + @property + def objects(self): + return self._objects def sample_with_replacement(self, count: int): return self._layouts[:count] @@ -57,9 +62,10 @@ def _fallback_layout(positions): def test_solve_and_apply_relation_placement_with_no_objects_returns_empty_result(): from isaaclab_arena.environments.relation_solver_interface import solve_and_apply_relation_placement - placement_event_cfg = solve_and_apply_relation_placement([], num_envs=1, scene_assets=[]) + placement_event_cfg, placement_pool = solve_and_apply_relation_placement([], num_envs=1, scene_assets=[]) assert placement_event_cfg is None + assert placement_pool is None def test_solve_and_apply_relation_placement_requires_unique_asset_names(): @@ -89,13 +95,14 @@ def test_solve_and_apply_relation_placement_with_only_anchors_returns_no_reset_e from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams params = ObjectPlacerParams(placement_seed=11, resolve_on_reset=False) - placement_event_cfg = solve_and_apply_relation_placement( + placement_event_cfg, placement_pool = solve_and_apply_relation_placement( [_make_desk()], num_envs=3, placer_params=params, ) assert placement_event_cfg is None + assert placement_pool is None def test_static_solve_and_apply_relation_placement_reuses_object_only_placement(): @@ -109,13 +116,14 @@ def test_static_solve_and_apply_relation_placement_reuses_object_only_placement( box.add_relation(On(desk, clearance_m=0.01)) params = ObjectPlacerParams(placement_seed=7, resolve_on_reset=False) - placement_event_cfg = solve_and_apply_relation_placement( + placement_event_cfg, placement_pool = solve_and_apply_relation_placement( [desk, box], num_envs=2, placer_params=params, ) assert placement_event_cfg is None + assert placement_pool is None initial_pose = box.get_initial_pose() assert isinstance(initial_pose, PosePerEnv) @@ -137,12 +145,15 @@ def test_dynamic_spawn_pose_rejects_layout_missing_non_anchor(): ) -def test_dynamic_spawn_pose_event_params_use_runtime_assets(): +def test_dynamic_spawn_pose_event_has_no_runtime_params(): from isaaclab_arena.environments.relation_solver_interface import _apply_dynamic_spawn_pose desk = _make_desk() box = _make_box() - placement_pool = _FakePlacementPool([_fallback_layout(positions={box: (0.1, 0.2, 0.3)})]) + placement_pool = _FakePlacementPool( + [_fallback_layout(positions={box: (0.1, 0.2, 0.3)})], + objects=[desk, box], + ) event_cfg = _apply_dynamic_spawn_pose( assets=[desk, box], @@ -150,8 +161,43 @@ def test_dynamic_spawn_pose_event_params_use_runtime_assets(): anchor_assets={desk}, ) - assert [asset.name for asset in event_cfg.params["assets"]] == ["desk", "box"] - assert "placement_pool" in event_cfg.params + assert event_cfg.params == {} + + +def test_dynamic_spawn_pose_event_cfg_deepcopy_after_mesh_solve(): + """EventTermCfg deep-copies params; runtime pool lives on the env, not in params.""" + import copy + import trimesh + + from isaaclab.managers import EventTermCfg + + from isaaclab_arena.environments.relation_solver_interface import solve_and_apply_relation_placement + from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams + from isaaclab_arena.relations.relation_solver_params import CollisionMode, RelationSolverParams + from isaaclab_arena.relations.relations import On + + desk = _make_desk() + box = _make_box() + box.add_relation(On(desk, clearance_m=0.01)) + box.collision_mode = CollisionMode.MESH + box._collision_mesh = trimesh.creation.box(extents=(0.2, 0.2, 0.2)) + + params = ObjectPlacerParams( + placement_seed=17, + resolve_on_reset=True, + min_unique_layouts_per_env=1, + solver_params=RelationSolverParams(collision_mode=CollisionMode.MESH, max_iters=50), + ) + event_cfg, placement_pool = solve_and_apply_relation_placement([desk, box], num_envs=1, placer_params=params) + + assert event_cfg is not None + assert isinstance(event_cfg, EventTermCfg) + assert placement_pool is not None + assert event_cfg.params == {} + copy.deepcopy(event_cfg) + from isaaclab.utils.configclass import _validate + + _validate(event_cfg, prefix="") def test_static_embodiment_placement_stores_per_env_poses():