Skip to content

Commit 7c46283

Browse files
committed
supoort for rotation, verification of algorithm
Signed-off-by: zhx06 <zihaox@nvidia.com>
1 parent ca1fb49 commit 7c46283

12 files changed

Lines changed: 177 additions & 304 deletions

isaaclab_arena/environments/relation_solver_interface.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def solve_and_apply_relation_placement(
5252
print("No objects with relations found in scene. Skipping relation solving.")
5353
return None
5454

55+
assert collision_mode in ("bbox", "mesh"), f"Invalid collision_mode '{collision_mode}', expected 'bbox' or 'mesh'"
5556
mode = CollisionMode.MESH if collision_mode == "mesh" else CollisionMode.BBOX
5657
placer_params = ObjectPlacerParams(
5758
placement_seed=placement_seed,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
from enum import Enum
7+
8+
9+
class CollisionMode(Enum):
10+
"""Selects which collision detection method the solver uses for no-overlap constraints."""
11+
12+
BBOX = "bbox"
13+
"""Axis-aligned bounding box overlap volume (fast, conservative)."""
14+
15+
MESH = "mesh"
16+
"""Sphere-to-SDF queries against actual mesh geometry (accurate, slower)."""

isaaclab_arena/relations/object_placer.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from typing import TYPE_CHECKING
1212

1313
from isaaclab_arena.relations.bounding_box_helpers import assign_variants_for_envs, build_per_env_bounding_boxes
14+
from isaaclab_arena.relations.collision_mode import CollisionMode
1415
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
1516
from isaaclab_arena.relations.placement_result import MultiEnvPlacementResult, PlacementResult
1617
from isaaclab_arena.relations.relation_solver import RelationSolver
17-
from isaaclab_arena.relations.relation_solver_params import CollisionMode
1818
from isaaclab_arena.relations.relations import (
1919
IsAnchor,
2020
On,
@@ -23,7 +23,7 @@
2323
get_anchor_objects,
2424
)
2525
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
26-
from isaaclab_arena.utils.pose import Pose, PosePerEnv, rotate_quat_by_yaw, wrap_angle_to_pi
26+
from isaaclab_arena.utils.pose import Pose, PosePerEnv, rotate_quat_by_yaw, wrap_angle_to_pi, yaw_from_quat_xyzw
2727
from isaaclab_arena.utils.random import get_random_rotation
2828

2929
if TYPE_CHECKING:
@@ -673,9 +673,35 @@ def _validate_no_overlap_mesh(
673673
if mesh is None and obj.name not in warned_no_mesh:
674674
warned_no_mesh.add(obj.name)
675675
print(
676-
f" [NoCollision] MESH mode: '{obj.name}' has no collision mesh, skipping mesh"
677-
" validation"
676+
f" [NoCollision] MESH mode: '{obj.name}' has no collision mesh,"
677+
" falling back to AABB validation for this pair"
678678
)
679+
# Fall back to AABB overlap check (matching the solver AABB fallback).
680+
for obj in (a, b):
681+
if id(obj) in anchor_ids:
682+
pose = obj.get_initial_pose()
683+
if pose is not None and hasattr(pose, "rotation_xyzw"):
684+
qx, qy = pose.rotation_xyzw[0], pose.rotation_xyzw[1]
685+
assert abs(qx) < 1e-6 and abs(qy) < 1e-6, (
686+
f"AABB fallback requires anchor '{obj.name}' to have pure-Z rotation, "
687+
f"got rotation_xyzw={pose.rotation_xyzw}"
688+
)
689+
a_pos = torch.tensor(positions[a], dtype=torch.float32)
690+
b_pos = torch.tensor(positions[b], dtype=torch.float32)
691+
a_bbox = a.get_bounding_box()
692+
b_bbox = b.get_bounding_box()
693+
a_yaw = ObjectPlacer._effective_yaw(a, orientations)
694+
b_yaw = ObjectPlacer._effective_yaw(b, orientations)
695+
if a_yaw != 0.0:
696+
a_bbox = a_bbox.rotated_around_z(a_yaw)
697+
if b_yaw != 0.0:
698+
b_bbox = b_bbox.rotated_around_z(b_yaw)
699+
a_world = a_bbox.translated(a_pos)
700+
b_world = b_bbox.translated(b_pos)
701+
if a_world.overlaps(b_world, margin=tolerance).item():
702+
if self.params.verbose:
703+
print(f" AABB overlap between '{a.name}' and '{b.name}' (mesh unavailable)")
704+
return False
679705
continue
680706

681707
a_pos = torch.tensor(positions[a], dtype=torch.float32)
@@ -718,10 +744,7 @@ def _effective_yaw(obj: ObjectBase, orientations: dict[ObjectBase, float] | None
718744
pose = obj.get_initial_pose()
719745
if pose is None or not hasattr(pose, "rotation_xyzw"):
720746
return 0.0
721-
qx, qy, qz, qw = pose.rotation_xyzw
722-
if abs(qx) > 1e-6 or abs(qy) > 1e-6:
723-
return 0.0
724-
return 2.0 * math.atan2(qz, qw)
747+
return yaw_from_quat_xyzw(pose.rotation_xyzw)
725748

726749
@staticmethod
727750
def _centers_in_target_frame(

isaaclab_arena/relations/relation_loss_strategies.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
from __future__ import annotations
77

8+
import math
89
import torch
910
from abc import ABC, abstractmethod
1011
from dataclasses import dataclass
1112
from enum import IntEnum
1213
from typing import TYPE_CHECKING
1314

15+
from isaaclab_arena.relations.collision_mode import CollisionMode
1416
from isaaclab_arena.relations.loss_primitives import (
1517
interval_overlap_axis_loss,
1618
linear_band_loss,
@@ -20,7 +22,11 @@
2022
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
2123

2224
if TYPE_CHECKING:
25+
import trimesh
26+
27+
from isaaclab_arena.assets.object_base import ObjectBase
2328
from isaaclab_arena.relations.relations import AtPosition, NextTo, NotNextTo, On, PositionLimits, Relation
29+
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshManager
2430

2531
from isaaclab_arena.relations.relations import Side
2632

@@ -427,7 +433,7 @@ def __init__(
427433
self,
428434
slope: float = 10.0,
429435
debug: bool = False,
430-
collision_mode=None,
436+
collision_mode: CollisionMode | None = None,
431437
num_spheres: int = 30,
432438
):
433439
"""
@@ -437,15 +443,12 @@ def __init__(
437443
collision_mode: CollisionMode enum value (BBOX or MESH). Defaults to BBOX.
438444
num_spheres: Number of spheres for mesh decomposition (MESH mode only).
439445
"""
440-
from isaaclab_arena.relations.relation_solver_params import CollisionMode
441-
442446
self.slope = slope
443447
self.debug = debug
444448
self._mode = collision_mode if collision_mode is not None else CollisionMode.BBOX
445449
self._num_spheres = num_spheres
446-
self._CollisionMode = CollisionMode
447450
self._warned_no_mesh: set[str] = set()
448-
self._mesh_managers: dict[str, object] = {} # keyed by device string
451+
self._mesh_managers: dict[str, WarpMeshManager] = {}
449452

450453
if self._mode == CollisionMode.MESH:
451454
try:
@@ -461,9 +464,11 @@ def compute_loss(
461464
child_pos: torch.Tensor,
462465
child_bbox: AxisAlignedBoundingBox,
463466
parent_world_bbox: AxisAlignedBoundingBox,
464-
child_obj=None,
465-
parent_obj=None,
467+
child_obj: ObjectBase | None = None,
468+
parent_obj: ObjectBase | None = None,
466469
parent_pos: torch.Tensor | None = None,
470+
child_yaw: float = 0.0,
471+
parent_yaw: float = 0.0,
467472
) -> torch.Tensor:
468473
"""Compute collision loss, dispatching based on mode and mesh availability.
469474
@@ -475,16 +480,26 @@ def compute_loss(
475480
child_obj: Object with get_collision_mesh() (MESH mode).
476481
parent_obj: Object with get_collision_mesh() (MESH mode).
477482
parent_pos: Parent position tensor, or None for anchors (MESH mode).
483+
child_yaw: Z-yaw (radians) of the child object (MESH mode).
484+
parent_yaw: Z-yaw (radians) of the parent object (MESH mode).
478485
479486
Returns:
480487
Loss tensor of shape (N,).
481488
"""
482-
if self._mode == self._CollisionMode.MESH and child_obj is not None and parent_obj is not None:
489+
if self._mode == CollisionMode.MESH and child_obj is not None and parent_obj is not None:
483490
child_mesh = child_obj.get_collision_mesh()
484491
parent_mesh = parent_obj.get_collision_mesh()
485492
if child_mesh is not None and parent_mesh is not None:
486493
return self._compute_mesh_loss(
487-
clearance_m, child_pos, child_obj, child_mesh, parent_pos, parent_obj, parent_mesh
494+
clearance_m,
495+
child_pos,
496+
child_obj,
497+
child_mesh,
498+
parent_pos,
499+
parent_obj,
500+
parent_mesh,
501+
child_yaw=child_yaw,
502+
parent_yaw=parent_yaw,
488503
)
489504
for obj, mesh in [(child_obj, child_mesh), (parent_obj, parent_mesh)]:
490505
name = getattr(obj, "name", "?")
@@ -544,7 +559,7 @@ def _compute_aabb_loss(
544559

545560
return total_loss.squeeze(0) if single_input else total_loss
546561

547-
def _get_mesh_manager(self, device: str = "cuda:0"):
562+
def _get_mesh_manager(self, device: str = "cuda:0") -> WarpMeshManager:
548563
"""Return a cached WarpMeshManager for the given device."""
549564
if device not in self._mesh_managers:
550565
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshManager
@@ -556,13 +571,15 @@ def _compute_mesh_loss(
556571
self,
557572
clearance_m: float,
558573
child_pos: torch.Tensor,
559-
child_obj,
560-
child_mesh,
574+
child_obj: ObjectBase,
575+
child_mesh: trimesh.Trimesh,
561576
parent_pos: torch.Tensor | None,
562-
parent_obj,
563-
parent_mesh,
577+
parent_obj: ObjectBase,
578+
parent_mesh: trimesh.Trimesh,
579+
child_yaw: float = 0.0,
580+
parent_yaw: float = 0.0,
564581
) -> torch.Tensor:
565-
"""Sphere-to-SDF penetration loss using mesh geometry."""
582+
"""Per-pair sphere-to-SDF penetration loss."""
566583
from isaaclab_arena.relations.warp_sdf_kernels import sphere_penetration_loss
567584

568585
single_input = child_pos.dim() == 1
@@ -577,12 +594,11 @@ def _compute_mesh_loss(
577594
assert isinstance(
578595
pose, Pose
579596
), f"Anchor '{getattr(parent_obj, 'name', '?')}' must have a fixed Pose for mesh collision"
580-
identity = (0.0, 0.0, 0.0, 1.0)
581-
assert pose.rotation_xyzw == identity, (
582-
f"Mesh collision with rotated anchor '{getattr(parent_obj, 'name', '?')}' "
583-
f"is not yet supported (rotation={pose.rotation_xyzw})"
584-
)
585597
parent_pos = torch.tensor(pose.position_xyz, dtype=child_pos.dtype, device=child_pos.device)
598+
if parent_yaw == 0.0:
599+
from isaaclab_arena.utils.pose import yaw_from_quat_xyzw
600+
601+
parent_yaw = yaw_from_quat_xyzw(pose.rotation_xyzw)
586602

587603
assert parent_pos is not None
588604
parent_pos_resolved: torch.Tensor = parent_pos
@@ -596,14 +612,31 @@ def _compute_mesh_loss(
596612
radii = spheres[:, 3]
597613
warp_mesh = manager.get_warp_mesh(parent_mesh, obj=parent_obj)
598614

615+
# Rotate child sphere centers by net_yaw = child_yaw - parent_yaw.
616+
net_yaw = child_yaw - parent_yaw
617+
if net_yaw != 0.0:
618+
cos_n = math.cos(net_yaw)
619+
sin_n = math.sin(net_yaw)
620+
rx = centers_local[:, 0] * cos_n - centers_local[:, 1] * sin_n
621+
ry = centers_local[:, 0] * sin_n + centers_local[:, 1] * cos_n
622+
centers_local = torch.stack([rx, ry, centers_local[:, 2]], dim=-1)
623+
599624
batch_size = child_pos.shape[0]
600625
parent_pos_resolved = parent_pos_resolved.expand(batch_size, -1)
601626
total_loss = torch.zeros(batch_size, device=device, dtype=child_pos.dtype)
602627

603628
for b in range(batch_size):
604-
centers_world = centers_local + child_pos[b] - parent_pos_resolved[b]
629+
offset = child_pos[b] - parent_pos_resolved[b]
630+
# Rotate offset into the parent's local frame.
631+
if parent_yaw != 0.0:
632+
cos_p = math.cos(-parent_yaw)
633+
sin_p = math.sin(-parent_yaw)
634+
ox = offset[0] * cos_p - offset[1] * sin_p
635+
oy = offset[0] * sin_p + offset[1] * cos_p
636+
offset = torch.stack([ox, oy, offset[2]])
637+
centers_in_parent = centers_local + offset
605638
total_loss[b] = self.slope * sphere_penetration_loss(
606-
centers_world, radii, warp_mesh, clearance_m=clearance_m
639+
centers_in_parent, radii, warp_mesh, clearance_m=clearance_m
607640
)
608641

609642
return total_loss.squeeze(0) if single_input else total_loss

isaaclab_arena/relations/relation_solver.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
from __future__ import annotations
77

8-
import math
98
import torch
109
from typing import TYPE_CHECKING, cast
1110

11+
from isaaclab_arena.relations.collision_mode import CollisionMode
1212
from isaaclab_arena.relations.relation_loss_strategies import (
1313
NoCollisionLossStrategy,
1414
RelationLossStrategy,
1515
UnaryRelationLossStrategy,
1616
)
17-
from isaaclab_arena.relations.relation_solver_params import CollisionMode, RelationSolverParams
17+
from isaaclab_arena.relations.relation_solver_params import RelationSolverParams
1818
from isaaclab_arena.relations.relation_solver_state import RelationSolverState
1919
from isaaclab_arena.relations.relations import On, Relation, RelationBase, UnaryRelation
2020
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
@@ -182,25 +182,29 @@ def _compute_no_overlap_loss(
182182
Per-environment loss tensor of shape (batch_size,).
183183
"""
184184
if self.params.collision_mode == CollisionMode.MESH:
185-
return self._compute_no_overlap_loss_mesh(state, debug)
185+
mesh_loss = self._compute_no_overlap_loss_mesh(state, debug)
186+
aabb_loss = self._compute_no_overlap_loss_aabb(state, debug, skip_mesh_pairs=True)
187+
return mesh_loss + aabb_loss
186188
else:
187189
return self._compute_no_overlap_loss_aabb(state, debug)
188190

189191
def _compute_no_overlap_loss_aabb(
190192
self,
191193
state: RelationSolverState,
192194
debug: bool,
195+
skip_mesh_pairs: bool = False,
193196
) -> torch.Tensor:
194-
"""Per-pair AABB collision loss (used when collision_mode != MESH)."""
197+
"""Per-pair AABB collision loss.
198+
199+
When skip_mesh_pairs=True (used as AABB fallback in MESH mode), only
200+
processes pairs where at least one object lacks a collision mesh.
201+
"""
195202
device = state.device
196203
total_loss = torch.zeros(state.batch_size, device=device, dtype=torch.float32)
197204

198205
non_anchor_objects = state.optimizable_objects
199206
anchor_objects = list(state.anchor_objects)
200207

201-
# Skip no-overlap for On-linked pairs: the On constraint already forces
202-
# vertical contact, and penalizing overlap here would fight it, causing
203-
# oscillation between "push apart" and "keep on surface" gradients.
204208
on_pairs: set[tuple[int, int]] = set()
205209
for obj in [*non_anchor_objects, *anchor_objects]:
206210
for rel in obj.get_relations():
@@ -215,6 +219,12 @@ def _compute_no_overlap_loss_aabb(
215219
for anchor in anchor_objects:
216220
if (id(child), id(anchor)) in on_pairs:
217221
continue
222+
if (
223+
skip_mesh_pairs
224+
and child.get_collision_mesh() is not None
225+
and anchor.get_collision_mesh() is not None
226+
):
227+
continue
218228
anchor_world_bbox = anchor.get_world_bounding_box().to(device)
219229
loss = self._no_collision_strategy.compute_loss(
220230
clearance_m=self.params.clearance_m,
@@ -233,6 +243,12 @@ def _compute_no_overlap_loss_aabb(
233243
other = non_anchor_objects[j]
234244
if (id(child), id(other)) in on_pairs:
235245
continue
246+
if (
247+
skip_mesh_pairs
248+
and child.get_collision_mesh() is not None
249+
and other.get_collision_mesh() is not None
250+
):
251+
continue
236252
other_pos = state.get_position(other)
237253
other_bbox = state.get_bbox(other)
238254

@@ -302,7 +318,7 @@ def _build_vectorized_cache(
302318

303319
import warp as wp
304320

305-
from isaaclab_arena.utils.pose import Pose
321+
from isaaclab_arena.utils.pose import Pose, yaw_from_quat_xyzw
306322

307323
centers_list: list[torch.Tensor] = []
308324
radii_list: list[torch.Tensor] = []
@@ -352,14 +368,13 @@ def _build_vectorized_cache(
352368
p_bbox_max = parent_bbox.max_point.to(device)
353369
pose = anchor.get_initial_pose()
354370
assert pose is not None and isinstance(pose, Pose)
355-
qx, qy, qz, qw = pose.rotation_xyzw
356-
assert abs(qx) < 1e-6 and abs(qy) < 1e-6, (
371+
assert abs(pose.rotation_xyzw[0]) < 1e-6 and abs(pose.rotation_xyzw[1]) < 1e-6, (
357372
f"MESH collision requires anchor '{anchor.name}' to have identity or "
358373
f"pure-Z rotation, got rotation_xyzw={pose.rotation_xyzw}. "
359374
"Roll/pitch anchors are not supported in MESH mode."
360375
)
361376
anchor_pos = torch.tensor(pose.position_xyz, dtype=torch.float32, device=device)
362-
anchor_yaw = 2.0 * math.atan2(qz, qw)
377+
anchor_yaw = yaw_from_quat_xyzw(pose.rotation_xyzw)
363378

364379
n_spheres = child_centers_local.shape[0]
365380
mesh_key = id(warp_mesh)

0 commit comments

Comments
 (0)