Skip to content
Closed
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
9 changes: 7 additions & 2 deletions isaaclab_arena/assets/background_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion isaaclab_arena/environments/arena_env_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Comment on lines 406 to +408

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Placement pool lost in callback

When a relation-based environment with reset-time placement is launched through the IsaacLab external callback, the callback discards the env_kwargs containing placement_pool. The environment is then constructed with a null pool, causing its initial placement reset to fail with AssertionError: Env has no placement pool; build through ArenaEnvBuilder.

Knowledge Base Used: Environment builder flow

return env_cfg, env_kwargs

def get_entry_point(self) -> str | type[ManagerBasedRLMimicEnv]:
Expand Down
11 changes: 11 additions & 0 deletions isaaclab_arena/environments/isaaclab_arena_manager_based_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING

from isaaclab.envs import ManagerBasedRLEnv

Expand All @@ -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."""
Expand All @@ -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)
Expand All @@ -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."""
Expand Down
21 changes: 11 additions & 10 deletions isaaclab_arena/environments/relation_solver_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]
Expand Down Expand Up @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -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={},
)


Expand Down
8 changes: 6 additions & 2 deletions isaaclab_arena/relations/placement_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
25 changes: 11 additions & 14 deletions isaaclab_arena/relations/placement_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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 (
Expand Down
Loading
Loading