Skip to content
Draft
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
8 changes: 8 additions & 0 deletions isaaclab_arena/assets/object_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ class RedCube(LibraryObject):
name = "red_cube"
tags = ["object"]

# TODO(lanceli, 2026.02.04): There is a known bug where rigid body attributes can only bind to the root layer.
# As a workaround, the original assets from ISAAC_NUCLEUS_DIR have been adjusted and uploaded to ISAAC_NUCLEUS_STAGING_DIR.
# Once this bug is resolved, the original assets can be used instead.

# usd_path =f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/red_block.usd" # not support, rigid body attribute need to be bind to root xform.
usd_path = f"{ISAACLAB_STAGING_NUCLEUS_DIR}/Arena/assets/object_library/isaac_blocks/red_block_root_rigid.usd"

Expand All @@ -637,6 +641,10 @@ class GreenCube(LibraryObject):
name = "green_cube"
tags = ["object"]

# TODO(lanceli, 2026.02.04): There is a known bug where rigid body attributes can only bind to the root layer.
# As a workaround, the original assets from ISAAC_NUCLEUS_DIR have been adjusted and uploaded to ISAAC_NUCLEUS_STAGING_DIR.
# Once this bug is resolved, the original assets can be used instead.

# usd_path = f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/green_block.usd" # not support, rigid body attribute need to be bind to root xform.
usd_path = f"{ISAACLAB_STAGING_NUCLEUS_DIR}/Arena/assets/object_library/isaac_blocks/green_block_root_rigid.usd"
object_type = ObjectType.RIGID
Expand Down
100 changes: 62 additions & 38 deletions isaaclab_arena/examples/compile_env_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from isaaclab_arena.tasks.pick_and_place_task import PickAndPlaceTask
from isaaclab_arena.tasks.sequential_task_base import SequentialTaskBase
from isaaclab_arena.tasks.task_base import TaskBase
from isaaclab_arena.utils.pose import Pose, PoseRange
from isaaclab_arena.utils.pose import Pose

asset_registry = AssetRegistry()

Expand Down Expand Up @@ -71,48 +71,72 @@
)
)


RANDOMIZATION_HALF_RANGE_X_M = 0.04
RANDOMIZATION_HALF_RANGE_Y_M = 0.01
RANDOMIZATION_HALF_RANGE_Z_M = 0.0
z_position = {
"sweet_potato": 1.0,
"jug": 1.1,
}[pickup_object_name]
yaw = {
"sweet_potato": 0.0,
"jug": -90.0,
}[pickup_object_name]
pickup_object.set_initial_pose(
# Bench (no randomization)
# Pose(position_xyz=(3.922, -0.565, 1.019), rotation_wxyz=(0.7071068, 0.0, 0.0, 0.7071068))
# Bench (with randomization)
PoseRange(
position_xyz_min=(
4.1 - RANDOMIZATION_HALF_RANGE_X_M,
-0.6 - RANDOMIZATION_HALF_RANGE_Y_M,
z_position - RANDOMIZATION_HALF_RANGE_Z_M,
),
position_xyz_max=(
4.1 + RANDOMIZATION_HALF_RANGE_X_M,
-0.6 + RANDOMIZATION_HALF_RANGE_Y_M,
z_position + RANDOMIZATION_HALF_RANGE_Z_M,
),
# position_xyz_max=(
# 3.922 + RANDOMIZATION_HALF_RANGE_X_M,
# -0.565 + RANDOMIZATION_HALF_RANGE_Y_M,
# 1.019 + RANDOMIZATION_HALF_RANGE_Z_M
# ),
rpy_min=(0.0, 0.0, yaw),
rpy_max=(0.0, 0.0, yaw),

from isaaclab_arena.relations.relations import AtPosition, IsAnchor, On, RandomAroundSolution

kitchen_counter_top = ObjectReference(
name="kitchen_counter_top",
prim_path="{ENV_REGEX_NS}/lightwheel_robocasa_kitchen/counter_right_main_group/top_geometry",
parent_asset=kitchen_background,
)
kitchen_counter_top.add_relation(IsAnchor())
pickup_object.add_relation(On(kitchen_counter_top))
pickup_object.add_relation(AtPosition(x=4.1, y=-0.6))
pickup_object.add_relation(
RandomAroundSolution(
x_half_m=RANDOMIZATION_HALF_RANGE_X_M,
y_half_m=RANDOMIZATION_HALF_RANGE_Y_M,
z_half_m=RANDOMIZATION_HALF_RANGE_Z_M,
yaw_base_rad=-90.0,
)
# Above shelf
# Pose(
# position_xyz=(4.625, -0.395, 1.224),
# rotation_wxyz=(0.7071068, 0.0, 0.0, 0.7071068)
# )
)

# Add reference to the scene.
# scene = Scene(assets=[kitchen_background, kitchen_counter_top, refrigerator, refrigerator_shelf, pickup_object, light])


# z_position = {
# "sweet_potato": 1.0,
# "jug": 1.1,
# }[pickup_object_name]
# yaw = {
# "sweet_potato": 0.0,
# "jug": -90.0,
# }[pickup_object_name]

# pickup_object.set_initial_pose(
# # Bench (no randomization)
# # Pose(position_xyz=(3.922, -0.565, 1.019), rotation_wxyz=(0.7071068, 0.0, 0.0, 0.7071068))
# # Bench (with randomization)
# PoseRange(
# position_xyz_min=(
# 4.1 - RANDOMIZATION_HALF_RANGE_X_M,
# -0.6 - RANDOMIZATION_HALF_RANGE_Y_M,
# z_position - RANDOMIZATION_HALF_RANGE_Z_M,
# ),
# position_xyz_max=(
# 4.1 + RANDOMIZATION_HALF_RANGE_X_M,
# -0.6 + RANDOMIZATION_HALF_RANGE_Y_M,
# z_position + RANDOMIZATION_HALF_RANGE_Z_M,
# ),
# # position_xyz_max=(
# # 3.922 + RANDOMIZATION_HALF_RANGE_X_M,
# # -0.565 + RANDOMIZATION_HALF_RANGE_Y_M,
# # 1.019 + RANDOMIZATION_HALF_RANGE_Z_M
# # ),
# rpy_min=(0.0, 0.0, yaw),
# rpy_max=(0.0, 0.0, yaw),
# )
# # Above shelf
# # Pose(
# # position_xyz=(4.625, -0.395, 1.224),
# # rotation_wxyz=(0.7071068, 0.0, 0.0, 0.7071068)
# # )
# )


class PutAndCloseDoorTask(SequentialTaskBase):

Expand All @@ -138,7 +162,7 @@ def get_mimic_env_cfg(self, arm_mode):

task = PutAndCloseDoorTask(subtasks=[pick_and_place_task, close_door_task])

scene = Scene(assets=[kitchen_background, refrigerator, refrigerator_shelf, pickup_object, light])
scene = Scene(assets=[kitchen_background, kitchen_counter_top, refrigerator, refrigerator_shelf, pickup_object, light])
isaaclab_arena_environment = IsaacLabArenaEnvironment(
name="reference_object_test",
embodiment=embodiment,
Expand Down
30 changes: 25 additions & 5 deletions isaaclab_arena/relations/object_placer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
from isaaclab_arena.relations.placement_result import PlacementResult
from isaaclab_arena.relations.relation_solver import RelationSolver
from isaaclab_arena.relations.relations import RandomAroundSolution, get_anchor_objects
from isaaclab_arena.relations.relations import RandomAroundSolution, RotateAroundSolution, get_anchor_objects
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox, get_random_pose_within_bounding_box
from isaaclab_arena.utils.pose import Pose

Expand Down Expand Up @@ -208,18 +208,24 @@ def _apply_positions(
) -> None:
"""Apply solved positions to objects (skipping anchors).

If an object has a RandomAroundSolution marker, a PoseRange is created
centered on the solved position. Otherwise, a fixed Pose is set.
If RandomAroundSolution marker is present, sets a PoseRange (for reset-time randomization).
Rotation is taken from RotateAroundSolution marker if present, otherwise keep the identity rotation.
"""
for obj, pos in positions.items():
if obj in anchor_objects:
continue

random_marker = self._get_random_around_solution(obj)
rotate_marker = self._get_rotate_around_solution(obj)
rotation_wxyz = rotate_marker.get_rotation_wxyz() if rotate_marker else (1.0, 0.0, 0.0, 0.0)

if random_marker is not None:
obj.set_initial_pose(random_marker.to_pose_range(pos))
# We need to set a PoseRange for the randomization to be picked up on reset.
# Set a PoseRange with the explicit rotation from RotateAroundSolution if present
obj.set_initial_pose(random_marker.to_pose_range_centered_at(pos, rotation_wxyz=rotation_wxyz))
else:
obj.set_initial_pose(Pose(position_xyz=pos, rotation_wxyz=(1.0, 0.0, 0.0, 0.0)))
# Without randomization, we can set a fixed Pose.
obj.set_initial_pose(Pose(position_xyz=pos, rotation_wxyz=rotation_wxyz))

def _get_random_around_solution(self, obj: Object | ObjectReference) -> RandomAroundSolution | None:
"""Get RandomAroundSolution marker from object if present.
Expand All @@ -235,6 +241,20 @@ def _get_random_around_solution(self, obj: Object | ObjectReference) -> RandomAr
return rel
return None

def _get_rotate_around_solution(self, obj: Object | ObjectReference) -> RotateAroundSolution | None:
"""Get RotateAroundSolution marker from object if present.

Args:
obj: Object to check for the marker.

Returns:
The RotateAroundSolution marker if found, None otherwise.
"""
for rel in obj.get_relations():
if isinstance(rel, RotateAroundSolution):
return rel
return None

@property
def last_loss_history(self) -> list[float]:
"""Loss values from the most recent place() call."""
Expand Down
90 changes: 81 additions & 9 deletions isaaclab_arena/relations/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

from __future__ import annotations

import torch
from enum import Enum
from typing import TYPE_CHECKING

from isaaclab.utils.math import euler_xyz_from_quat

from isaaclab_arena.utils.pose import PoseRange

if TYPE_CHECKING:
Expand Down Expand Up @@ -151,6 +154,9 @@ def __init__(
roll_half_rad: float = 0.0,
pitch_half_rad: float = 0.0,
yaw_half_rad: float = 0.0,
roll_base_rad: float = 0.0,
pitch_base_rad: float = 0.0,
yaw_base_rad: float = 0.0,
):
"""
Args:
Expand All @@ -160,23 +166,42 @@ def __init__(
roll_half_rad: Half-extent for roll (radians). Rotation will be randomized ±roll_half_rad.
pitch_half_rad: Half-extent for pitch (radians). Rotation will be randomized ±pitch_half_rad.
yaw_half_rad: Half-extent for yaw (radians). Rotation will be randomized ±yaw_half_rad.
roll_base_rad: Base roll angle (radians). Center of the roll randomization range.
pitch_base_rad: Base pitch angle (radians). Center of the pitch randomization range.
yaw_base_rad: Base yaw angle (radians). Center of the yaw randomization range.
"""
self.x_half_m = x_half_m
self.y_half_m = y_half_m
self.z_half_m = z_half_m
self.roll_half_rad = roll_half_rad
self.pitch_half_rad = pitch_half_rad
self.yaw_half_rad = yaw_half_rad
self.roll_base_rad = roll_base_rad
self.pitch_base_rad = pitch_base_rad
self.yaw_base_rad = yaw_base_rad

def to_pose_range(self, position: tuple[float, float, float]) -> PoseRange:
"""Create a PoseRange centered on the given position.
def to_pose_range_centered_at(
self,
position: tuple[float, float, float],
rotation_wxyz: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0),
) -> PoseRange:
"""Create a PoseRange centered on the given position and rotation.

Args:
position: Center position (x, y, z) for the range.
rotation_wxyz: Center rotation as quaternion (w, x, y, z) for the range.
Defaults to identity quaternion.

Returns:
PoseRange spanning ± half-extents around the position.
PoseRange spanning ± half-extents around the position and rotation.
"""
# Convert quaternion to euler angles (roll, pitch, yaw)
quat_tensor = torch.tensor([rotation_wxyz])
roll, pitch, yaw = euler_xyz_from_quat(quat_tensor)
center_roll = float(roll[0])
center_pitch = float(pitch[0])
center_yaw = float(yaw[0])

return PoseRange(
position_xyz_min=(
position[0] - self.x_half_m,
Expand All @@ -189,18 +214,65 @@ def to_pose_range(self, position: tuple[float, float, float]) -> PoseRange:
position[2] + self.z_half_m,
),
rpy_min=(
-self.roll_half_rad,
-self.pitch_half_rad,
-self.yaw_half_rad,
center_roll - self.roll_half_rad,
center_pitch - self.pitch_half_rad,
center_yaw - self.yaw_half_rad,
),
rpy_max=(
self.roll_half_rad,
self.pitch_half_rad,
self.yaw_half_rad,
center_roll + self.roll_half_rad,
center_pitch + self.pitch_half_rad,
center_yaw + self.yaw_half_rad,
),
)


class RotateAroundSolution(RelationBase):
"""Marker specifying an explicit rotation to apply on top of the solver solution.

When ObjectPlacer applies positions, objects with this marker will have the
specified rotation applied on top of the solved position to create a fixed Pose.

Note: This is NOT a spatial relation - the RelationSolver ignores it. It only
affects how ObjectPlacer applies the solved position to the object.

Usage:
import math
box.add_relation(On(desk))
box.add_relation(RotateAroundSolution(yaw_rad=math.pi / 4))
# -> ObjectPlacer sets a Pose with solved position and 45° yaw rotation
"""

def __init__(
self,
roll_rad: float = 0.0,
pitch_rad: float = 0.0,
yaw_rad: float = 0.0,
):
"""
Args:
roll_rad: Roll rotation in radians.
pitch_rad: Pitch rotation in radians.
yaw_rad: Yaw rotation in radians.
"""
self.roll_rad = roll_rad
self.pitch_rad = pitch_rad
self.yaw_rad = yaw_rad

def get_rotation_wxyz(self) -> tuple[float, float, float, float]:
"""Get the rotation as a quaternion (w, x, y, z).

Returns:
Quaternion rotation converted from roll/pitch/yaw.
"""
from isaaclab.utils.math import quat_from_euler_xyz

roll = torch.tensor(self.roll_rad)
pitch = torch.tensor(self.pitch_rad)
yaw = torch.tensor(self.yaw_rad)
quat = quat_from_euler_xyz(roll, pitch, yaw)
return tuple(quat.tolist())


class AtPosition(RelationBase):
"""Constrains object to specific world coordinates.

Expand Down
Loading
Loading