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
22 changes: 18 additions & 4 deletions isaaclab_arena/assets/dummy_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,37 @@
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import torch
from typing import TYPE_CHECKING

from isaaclab_arena.relations.relations import Relation, RelationBase, UnaryRelation
from isaaclab_arena.relations.relations import IsAnchor, Relation, RelationBase, UnaryRelation
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox, quaternion_to_90_deg_z_quarters
from isaaclab_arena.utils.pose import Pose

if TYPE_CHECKING:
import trimesh


class DummyObject:
"""
Dummy object for testing purposes without Isaac Sim dependencies.
"""
"""Dummy object for testing purposes without Isaac Sim dependencies."""

def __init__(
self,
name: str,
bounding_box: AxisAlignedBoundingBox,
initial_pose: Pose | None = None,
relations: list[RelationBase] = [],
collision_mesh: trimesh.Trimesh | None = None,
**kwargs,
):
self.name = name
self.initial_pose = initial_pose
self.bounding_box = bounding_box
assert self.bounding_box is not None
self.relations = list(relations)
self._collision_mesh = collision_mesh

def add_relation(self, relation: RelationBase) -> None:
self.relations.append(relation)
Expand Down Expand Up @@ -63,3 +69,11 @@ def get_initial_pose(self) -> Pose | None:

def is_initial_pose_set(self) -> bool:
return self.initial_pose is not None

@property
def is_anchor(self) -> bool:
return any(isinstance(r, IsAnchor) for r in self.relations)

def get_collision_mesh(self) -> trimesh.Trimesh | None:
"""Return the collision mesh, or None to fall back to AABB."""
return self._collision_mesh
14 changes: 10 additions & 4 deletions isaaclab_arena/assets/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import torch
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
import trimesh

from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg
from isaaclab.sensors.contact_sensor.contact_sensor_cfg import ContactSensorCfg
Expand All @@ -20,9 +25,7 @@


class Object(ObjectBase):
"""
Encapsulates the pick-up object config for a pick-and-place environment.
"""
"""Pick-up object config for a pick-and-place environment."""

def __init__(
self,
Expand Down Expand Up @@ -74,6 +77,9 @@ def get_bounding_box(self) -> AxisAlignedBoundingBox:
self.bounding_box = compute_local_bounding_box_from_usd(self.usd_path, self.scale)
return self.bounding_box

def get_collision_mesh(self) -> trimesh.Trimesh | None:
"""Collision mesh is unavailable for USD-backed objects; subclasses with preloaded geometry may override."""

def get_world_bounding_box(self) -> AxisAlignedBoundingBox:
"""Get bounding box in world coordinates (local bbox rotated and translated).

Expand Down
14 changes: 14 additions & 0 deletions isaaclab_arena/assets/object_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@

import torch
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

import warp as wp
from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg

if TYPE_CHECKING:
import trimesh
from isaaclab.envs import ManagerBasedEnv
from isaaclab.managers import EventTermCfg, SceneEntityCfg
from isaaclab.sensors.contact_sensor.contact_sensor_cfg import ContactSensorCfg
Expand Down Expand Up @@ -73,6 +77,9 @@ def get_world_bounding_box(self) -> AxisAlignedBoundingBox:
"""Get bounding box in world coordinates (local bbox rotated and translated)."""
...

def get_collision_mesh(self) -> trimesh.Trimesh | None:
Comment thread
zhx06 marked this conversation as resolved.
"""Return collision mesh, or None to fall back to AABB overlap."""

def _get_initial_pose_as_pose(self) -> Pose | None:
"""Return a single ``Pose`` suitable for *init_state* and bounding-box calculations.

Expand Down Expand Up @@ -170,6 +177,13 @@ def get_relations(self) -> list[RelationBase]:
"""Get all relations for this object."""
return self.relations

@property
def is_anchor(self) -> bool:
"""True if this object has an IsAnchor relation."""
from isaaclab_arena.relations.relations import IsAnchor

return any(isinstance(r, IsAnchor) for r in self.relations)

def get_spatial_relations(self) -> list[RelationBase]:
"""Get only spatial relations (On, NextTo, AtPosition, etc.), excluding markers like IsAnchor."""
return [r for r in self.relations if isinstance(r, (Relation, UnaryRelation))]
Expand Down
7 changes: 7 additions & 0 deletions isaaclab_arena/cli/isaaclab_arena_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ def add_isaaclab_arena_cli_args(parser: argparse.ArgumentParser) -> None:
default=False,
help="Print Hydra-configurable variations for the selected environment and exit.",
)
arena_group.add_argument(
"--collision_mode",
Comment thread
zhx06 marked this conversation as resolved.
type=str,
choices=["bbox", "mesh"],
default="bbox",
help="Collision detection mode: 'bbox' (AABB, default) or 'mesh' (sphere-to-SDF, requires Warp).",
)
Comment thread
zhx06 marked this conversation as resolved.


def add_env_graph_spec_cli_args(parser: argparse.ArgumentParser) -> None:
Expand Down
24 changes: 21 additions & 3 deletions isaaclab_arena/environments/arena_env_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
make_progress_tracking_events_cfg,
make_progress_tracking_recorder_cfg,
)
from isaaclab_arena.relations.collision_mode import CollisionMode
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
from isaaclab_arena.relations.placement_events import PLACEMENT_RESET_EVENT_NAME
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
from isaaclab_arena.utils.isaaclab_utils.simulation_app import reapply_viewer_cfg
Expand Down Expand Up @@ -80,12 +83,27 @@ def _solve_relations(self) -> None:
events restore the same layout every time.
"""
objects_with_relations = self.arena_env.scene.get_objects_with_relations()

# Prefer env-level placer_params; fall back to CLI-constructed defaults.
placer_params = self.arena_env.placer_params
if placer_params is None:
collision_mode_str = getattr(self.args, "collision_mode", "bbox")
mode = CollisionMode.MESH if collision_mode_str == "mesh" else CollisionMode.BBOX
placer_params = ObjectPlacerParams(
placement_seed=self.args.placement_seed,
random_yaw_init=self.args.random_yaw_init,
solver_params=RelationSolverParams(
collision_mode=mode,
save_position_history=False,
verbose=False,
),
)
if self.args.resolve_on_reset is not None:
placer_params.resolve_on_reset = self.args.resolve_on_reset
self._placement_event_cfg = solve_and_apply_relation_placement(
objects_with_relations,
num_envs=self.args.num_envs,
placement_seed=self.args.placement_seed,
resolve_on_reset=self.args.resolve_on_reset,
random_yaw_init=self.args.random_yaw_init,
placer_params=placer_params,
)

def get_all_variations(self) -> dict[str, list[VariationBase]]:
Expand Down
5 changes: 5 additions & 0 deletions isaaclab_arena/environments/isaaclab_arena_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from isaaclab_arena.assets.teleop_device_base import TeleopDeviceBase
from isaaclab_arena.embodiments.embodiment_base import EmbodimentBase
from isaaclab_arena.environments.isaaclab_arena_manager_based_env_cfg import IsaacLabArenaManagerBasedRLEnvCfg
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
from isaaclab_arena.scene.scene import Scene
from isaaclab_arena.tasks.task_base import TaskBase

Expand All @@ -29,6 +30,7 @@ def __init__(
env_cfg_callback: Callable[IsaacLabArenaManagerBasedRLEnvCfg] | None = None,
rl_framework_entry_point: str | None = None,
rl_policy_cfg: str | None = None,
placer_params: ObjectPlacerParams | None = None,
):
"""
Args:
Expand All @@ -46,6 +48,8 @@ def __init__(
``rl_policy_cfg`` is set.
rl_policy_cfg: Import path to the RL policy config class, e.g.
``"my_module:RLPolicyCfg"``.
placer_params: Object placement configuration. When set, used as-is
(CLI flags are ignored). When None, params are built from CLI flags.
"""
self.name = name
self.scene = scene
Expand All @@ -57,3 +61,4 @@ def __init__(
raise ValueError("rl_framework_entry_point and rl_policy_cfg must both be set or both be None.")
self.rl_framework_entry_point = rl_framework_entry_point
self.rl_policy_cfg = rl_policy_cfg
self.placer_params = placer_params
36 changes: 8 additions & 28 deletions isaaclab_arena/environments/relation_solver_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from __future__ import annotations

import copy
from typing import TYPE_CHECKING

from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
from isaaclab_arena.relations.placement_events import get_rotation_xyzw, solve_and_place_objects
from isaaclab_arena.relations.pooled_object_placer import PooledObjectPlacer
from isaaclab_arena.relations.relation_solver_params import RelationSolverParams
from isaaclab_arena.relations.relations import get_anchor_objects
from isaaclab_arena.utils.pose import Pose, PosePerEnv, rotate_quat_by_yaw

Expand All @@ -23,39 +23,19 @@
def solve_and_apply_relation_placement(
objects: list[ObjectBase],
num_envs: int,
placement_seed: int | None = None,
resolve_on_reset: bool | None = None,
random_yaw_init: bool = False,
placer_params: ObjectPlacerParams | None = None,
) -> EventTermCfg | None:
"""Solve relation placement and apply the result to object reset/static state.

Args:
objects: Objects with spatial predicates that should be relation-solved.
num_envs: Number of environments to prepare placements for.
placement_seed: Optional random seed for reproducible object placement.
resolve_on_reset: Optional override for whether to draw fresh layouts from
the placement pool on reset. When ``False``, fixed per-environment
initial poses are applied immediately.
random_yaw_init: If True, randomly rotates non-anchor objects around the vertical (Z)
axis at startup to add visual variety to the scene.

Returns:
Reset event config to attach to the environment when placement should be
resolved on reset. Returns ``None`` when no reset event is needed.
"""
"""Solve relation placement and return a reset EventTermCfg (or None if no objects)."""
objects = list(objects)
if not objects:
print("No objects with relations found in scene. Skipping relation solving.")
return None

placer_params = ObjectPlacerParams(
placement_seed=placement_seed,
apply_positions_to_objects=False,
solver_params=RelationSolverParams(save_position_history=False, verbose=False),
random_yaw_init=random_yaw_init,
)
if resolve_on_reset is not None:
placer_params.resolve_on_reset = resolve_on_reset
if placer_params is None:
placer_params = ObjectPlacerParams()
else:
placer_params = copy.copy(placer_params)
placer_params.apply_positions_to_objects = False

# TODO(xinjieyao, 2026-05-22): Add joint object/embodiment placement once task-dependent
# reachability constraints are available. For now this always uses the object-only placer.
Expand Down
16 changes: 16 additions & 0 deletions isaaclab_arena/relations/collision_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

from enum import Enum


class CollisionMode(Enum):
"""Selects which collision detection method the solver uses for no-overlap constraints."""

BBOX = "bbox"
"""Axis-aligned bounding box overlap volume (fast, conservative)."""

MESH = "mesh"
"""Sphere-to-SDF queries against actual mesh geometry (accurate, slower)."""
88 changes: 88 additions & 0 deletions isaaclab_arena/relations/mesh_pair_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

"""Typed container for precomputed mesh-collision pair data."""

from __future__ import annotations

import torch
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
import warp as wp

from isaaclab_arena.assets.object_base import ObjectBase


@dataclass(slots=True)
class MeshPairCache:
"""Precomputed per-pair collision data for the vectorized multi-mesh kernel."""

all_centers_local: torch.Tensor
"""(S, 3) sphere centers in each subject's local frame, concatenated across pairs."""

all_radii: torch.Tensor
"""(S,) sphere radii, concatenated across pairs."""

pair_subject_objs: list[ObjectBase]
"""Per-pair subject (sphere source) object reference."""

pair_obstacle_objs: list[ObjectBase]
"""Per-pair obstacle (mesh target) object reference."""

pair_is_anchor: list[bool]
"""Per-pair flag: True if the obstacle is a static anchor."""

pair_anchor_pos: list[torch.Tensor | None]
"""Per-pair world position for anchor obstacles (None for non-anchor obstacles)."""

pair_anchor_yaw: list[float]
"""Per-pair anchor yaw in radians (0.0 for non-anchor obstacles)."""

pair_subject_bbox_min: torch.Tensor
"""(P, B, 3) subject bbox min corners for broadphase."""

pair_subject_bbox_max: torch.Tensor
"""(P, B, 3) subject bbox max corners for broadphase."""

pair_obstacle_bbox_min: torch.Tensor
"""(P, B, 3) obstacle bbox min corners for broadphase."""

pair_obstacle_bbox_max: torch.Tensor
"""(P, B, 3) obstacle bbox max corners for broadphase."""

pair_max_radius: torch.Tensor
"""(P,) max sphere radius per pair (broadphase margin)."""

sphere_pair_id: torch.Tensor
"""(S,) maps each sphere to its pair index for segment reduction."""

sphere_mesh_idx: torch.Tensor
"""(S,) per-sphere index into mesh_id_array."""

pair_sphere_count: torch.Tensor
"""(P,) number of spheres per pair (for mean reduction)."""

mesh_id_array: wp.array
"""Warp uint64 array of mesh IDs for the multi-mesh kernel."""

num_pairs: int
"""Total number of active object pairs."""

total_spheres: int
"""Total number of sphere queries across all pairs."""

def __post_init__(self) -> None:
assert len(self.pair_subject_objs) == self.num_pairs, "pair_subject_objs length mismatch"
assert len(self.pair_obstacle_objs) == self.num_pairs, "pair_obstacle_objs length mismatch"
assert len(self.pair_is_anchor) == self.num_pairs, "pair_is_anchor length mismatch"
assert self.all_centers_local.shape[0] == self.total_spheres, "all_centers_local size mismatch"
assert self.all_radii.shape[0] == self.total_spheres, "all_radii size mismatch"
assert self.sphere_pair_id.shape[0] == self.total_spheres, "sphere_pair_id size mismatch"
assert self.sphere_mesh_idx.shape[0] == self.total_spheres, "sphere_mesh_idx size mismatch"
assert int(self.pair_sphere_count.sum().item()) == self.total_spheres, "pair_sphere_count sum mismatch"
for i, (is_anchor, pos) in enumerate(zip(self.pair_is_anchor, self.pair_anchor_pos)):
assert not is_anchor or pos is not None, f"pair {i}: is_anchor=True but anchor_pos is None"
Loading
Loading