Skip to content

Commit 2b8adcd

Browse files
committed
improve docstring
Signed-off-by: zhx06 <zihaox@nvidia.com>
1 parent f5ba4ae commit 2b8adcd

7 files changed

Lines changed: 43 additions & 112 deletions

File tree

isaaclab_arena/assets/object.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525

2626

2727
class Object(ObjectBase):
28-
"""
29-
Encapsulates the pick-up object config for a pick-and-place environment.
30-
"""
28+
"""Pick-up object config for a pick-and-place environment."""
3129

3230
def __init__(
3331
self,

isaaclab_arena/relations/mesh_pair_cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
class MeshPairCache:
2222
"""Precomputed per-pair collision data for the vectorized multi-mesh kernel.
2323
24-
Built once per solve by _build_vectorized_cache; consumed each iteration by
25-
_compute_no_overlap_loss_mesh. Contains a wp.array handle (mesh_id_array) that
26-
is not deepcopy-safe — RelationSolver.__deepcopy__ nulls this cache to avoid
24+
Built once per solve, consumed each iteration until the solver is
25+
re-initialized. Contains a wp.array handle (mesh_id_array) that is not
26+
deepcopy-safe — RelationSolver.__deepcopy__ nulls this cache to avoid
2727
copying it.
2828
"""
2929

isaaclab_arena/relations/relation_loss_strategies.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
single_boundary_linear_loss,
2222
single_point_linear_loss,
2323
)
24+
from isaaclab_arena.relations.relations import Side
25+
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
26+
from isaaclab_arena.relations.warp_sdf_kernels import clamp_sdf_sentinel, mesh_sdf
2427
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
2528
from isaaclab_arena.utils.pose import Pose, yaw_from_quat_xyzw
2629

@@ -29,9 +32,6 @@
2932

3033
from isaaclab_arena.assets.object_base import ObjectBase
3134
from isaaclab_arena.relations.relations import AtPosition, NextTo, NotNextTo, On, PositionLimits, Relation
32-
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
33-
34-
from isaaclab_arena.relations.relations import Side
3535

3636

3737
class Axis(IntEnum):
@@ -458,14 +458,6 @@ def __init__(
458458
self._warned_no_mesh: set[str] = set()
459459
self._mesh_managers: dict[str, WarpMeshAndSphereCache] = {}
460460

461-
if self._mode == CollisionMode.MESH:
462-
try:
463-
import warp # noqa: F401
464-
except ImportError as e:
465-
raise ImportError(
466-
"CollisionMode.MESH requires the 'warp' package. Install it with: pip install warp-lang"
467-
) from e
468-
469461
def compute_loss(
470462
self,
471463
clearance_m: float,
@@ -559,8 +551,6 @@ def _compute_aabb_loss(
559551
def _get_mesh_manager(self, device: str = "cuda:0") -> WarpMeshAndSphereCache:
560552
"""Return a cached WarpMeshAndSphereCache for the given device."""
561553
if device not in self._mesh_managers:
562-
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
563-
564554
self._mesh_managers[device] = WarpMeshAndSphereCache(num_spheres=self._num_spheres, device=device)
565555
return self._mesh_managers[device]
566556

@@ -577,8 +567,6 @@ def _compute_mesh_loss(
577567
parent_yaw: float = 0.0,
578568
) -> torch.Tensor:
579569
"""Per-pair sphere-to-SDF penetration loss."""
580-
from isaaclab_arena.relations.warp_sdf_kernels import clamp_sdf_sentinel, mesh_sdf
581-
582570
single_input = child_pos.dim() == 1
583571
if single_input:
584572
child_pos = child_pos.unsqueeze(0)
@@ -676,17 +664,14 @@ def compute_loss(
676664

677665
total_loss = torch.zeros(child_pos.shape[0], dtype=child_pos.dtype, device=child_pos.device)
678666

679-
# X position constraint
680667
if relation.x is not None:
681668
x_loss = single_point_linear_loss(child_pos[:, 0], relation.x, slope=self.slope)
682669
total_loss = total_loss + x_loss
683670

684-
# Y position constraint
685671
if relation.y is not None:
686672
y_loss = single_point_linear_loss(child_pos[:, 1], relation.y, slope=self.slope)
687673
total_loss = total_loss + y_loss
688674

689-
# Z position constraint
690675
if relation.z is not None:
691676
z_loss = single_point_linear_loss(child_pos[:, 2], relation.z, slope=self.slope)
692677
total_loss = total_loss + z_loss
@@ -732,7 +717,6 @@ def compute_loss(
732717

733718
total_loss = torch.zeros(child_pos.shape[0], dtype=child_pos.dtype, device=child_pos.device)
734719

735-
# Iterate over X (0), Y (1), Z (2) with their optional bounds
736720
axis_bounds = [
737721
(relation.x_min, relation.x_max),
738722
(relation.y_min, relation.y_max),

isaaclab_arena/relations/relation_solver.py

Lines changed: 20 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55

66
from __future__ import annotations
77

8+
import copy
9+
import numpy as np
810
import torch
911
from typing import TYPE_CHECKING, cast
1012

13+
import warp as wp
1114
from isaaclab.utils.math import quat_apply, quat_apply_inverse
1215

1316
from isaaclab_arena.relations.collision_mode import CollisionMode
17+
from isaaclab_arena.relations.mesh_pair_cache import MeshPairCache
1418
from isaaclab_arena.relations.relation_loss_strategies import (
1519
NoCollisionLossStrategy,
1620
RelationLossStrategy,
@@ -19,16 +23,17 @@
1923
from isaaclab_arena.relations.relation_solver_params import RelationSolverParams
2024
from isaaclab_arena.relations.relation_solver_state import RelationSolverState
2125
from isaaclab_arena.relations.relations import On, Relation, RelationBase, UnaryRelation
26+
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
27+
from isaaclab_arena.relations.warp_sdf_kernels import clamp_sdf_sentinel, multi_mesh_sdf
2228
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
29+
from isaaclab_arena.utils.pose import Pose, yaw_from_quat_xyzw
2330

2431
if TYPE_CHECKING:
2532
from isaaclab_arena.assets.object_base import ObjectBase
26-
from isaaclab_arena.relations.mesh_pair_cache import MeshPairCache
27-
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
2833

2934

3035
class RelationSolver:
31-
"""Differentiable solver for 3D spatial relations of IsaacLab Arena Objects
36+
"""Differentiable solver for 3D spatial relations of IsaacLab Arena Objects.
3237
3338
Uses the Strategy pattern for loss computation: each Relation type has a
3439
corresponding RelationLossStrategy that handles the actual loss calculation.
@@ -41,7 +46,8 @@ def __init__(
4146
self,
4247
params: RelationSolverParams | None = None,
4348
):
44-
"""
49+
"""Initialize the solver with the given parameters.
50+
4551
Args:
4652
params: Solver configuration parameters. If None, uses defaults.
4753
"""
@@ -62,8 +68,7 @@ def __init__(
6268
self._mesh_cache_rev: MeshPairCache | None = None
6369

6470
def __deepcopy__(self, memo):
65-
"""Deep-copy resets ephemeral Warp caches (rebuilt on next solve)."""
66-
import copy
71+
"""Reset Warp GPU caches on copy; wp.Mesh handles are not copy-safe."""
6772

6873
cls = self.__class__
6974
result = cls.__new__(cls)
@@ -76,16 +81,10 @@ def __deepcopy__(self, memo):
7681
return result
7782

7883
def _get_strategy(self, relation: RelationBase) -> RelationLossStrategy | UnaryRelationLossStrategy:
79-
"""Look up the appropriate strategy for a relation type.
84+
"""Look up the loss strategy for a relation type; raises ValueError if none registered.
8085
8186
Args:
8287
relation: The relation to find a strategy for.
83-
84-
Returns:
85-
The RelationLossStrategy or UnaryRelationLossStrategy for this relation type.
86-
87-
Raises:
88-
ValueError: If no strategy is registered for this relation type.
8988
"""
9089
strategy = self.params.strategies.get(type(relation))
9190
if strategy is None:
@@ -114,14 +113,12 @@ def _compute_total_loss(
114113
device = state.device
115114
total_loss = torch.zeros(batch_size, device=device, dtype=torch.float32)
116115

117-
# Compute loss from all spatial relations using strategies
118116
for obj in state.optimizable_objects:
119117
for relation in obj.get_spatial_relations():
120118
child_pos = state.get_position(obj)
121119
strategy = self._get_strategy(relation)
122120
child_bbox = state.get_bbox(obj)
123121

124-
# Handle unary relations (no parent)
125122
if isinstance(relation, UnaryRelation):
126123
unary_strategy = cast(UnaryRelationLossStrategy, strategy)
127124
loss = unary_strategy.compute_loss(
@@ -131,7 +128,7 @@ def _compute_total_loss(
131128
)
132129
if debug:
133130
_print_unary_relation_debug(obj, relation, child_pos[0], loss.mean())
134-
# Handle binary relations (with parent) like On, NextTo
131+
# Binary relation (On, NextTo, etc.)
135132
elif isinstance(relation, Relation):
136133
relation_strategy = cast(RelationLossStrategy, strategy)
137134
parent = relation.parent
@@ -166,24 +163,7 @@ def _compute_no_overlap_loss(
166163
state: RelationSolverState,
167164
debug: bool = False,
168165
) -> torch.Tensor:
169-
"""Compute pairwise no-overlap loss, skipping On-linked pairs.
170-
171-
Each unique non-On pair is evaluated twice (once per direction):
172-
- Non-anchor vs anchor: gradient flows to the non-anchor only.
173-
- Non-anchor vs non-anchor: both objects receive gradient by computing
174-
the loss in both directions with the other's position detached.
175-
176-
In MESH mode, a precomputed cache feeds a single multi-mesh SDF kernel
177-
per batch element per direction (fwd/rev).
178-
179-
Args:
180-
state: Current optimization state with object positions and
181-
optional per-env bounding boxes.
182-
debug: If True, print detailed loss breakdown.
183-
184-
Returns:
185-
Per-environment loss tensor of shape (batch_size,).
186-
"""
166+
"""Compute pairwise no-overlap loss, skipping On-linked pairs."""
187167
if self.params.collision_mode == CollisionMode.MESH:
188168
mesh_loss = self._compute_no_overlap_loss_mesh(state, debug)
189169
aabb_loss = self._compute_no_overlap_loss_aabb(state, debug, skip_mesh_pairs=True)
@@ -292,8 +272,6 @@ def _prepare_mesh_collision_cache(
292272
on_pairs: set[tuple[int, int]],
293273
) -> None:
294274
"""Precompute static per-pair mesh collision data (called once per solve)."""
295-
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
296-
297275
device = state.device
298276
device_str = str(device)
299277
if self._mesh_manager is None or self._mesh_manager.device != device_str:
@@ -317,12 +295,6 @@ def _build_vectorized_cache(
317295
318296
Returns None if no valid pairs exist for this direction.
319297
"""
320-
import numpy as np
321-
322-
import warp as wp
323-
324-
from isaaclab_arena.utils.pose import Pose, yaw_from_quat_xyzw
325-
326298
centers_list: list[torch.Tensor] = []
327299
radii_list: list[torch.Tensor] = []
328300
pair_child_objs: list = []
@@ -346,7 +318,7 @@ def _build_vectorized_cache(
346318
if child_mesh is None:
347319
if child.name not in self._warned_no_mesh:
348320
self._warned_no_mesh.add(child.name)
349-
print(f"[NoCollision] MESH mode: '{child.name}' has no collision mesh, skipping.")
321+
print(f"[NoCollision] '{child.name}' has no collision mesh; pair will use AABB fallback.")
350322
continue
351323
child_spheres = manager.get_query_spheres(child_mesh, obj=child).to(device)
352324
child_centers_local = child_spheres[:, :3]
@@ -363,7 +335,7 @@ def _build_vectorized_cache(
363335
if parent_mesh is None:
364336
if anchor.name not in self._warned_no_mesh:
365337
self._warned_no_mesh.add(anchor.name)
366-
print(f"[NoCollision] MESH mode: '{anchor.name}' has no collision mesh, skipping.")
338+
print(f"[NoCollision] '{anchor.name}' has no collision mesh; pair will use AABB fallback.")
367339
continue
368340
warp_mesh = manager.get_warp_mesh(parent_mesh, obj=anchor)
369341
parent_bbox = state.get_bbox(anchor)
@@ -412,7 +384,7 @@ def _build_vectorized_cache(
412384
if other_mesh is None:
413385
if other.name not in self._warned_no_mesh:
414386
self._warned_no_mesh.add(other.name)
415-
print(f"[NoCollision] MESH mode: '{other.name}' has no collision mesh, skipping.")
387+
print(f"[NoCollision] '{other.name}' has no collision mesh; pair will use AABB fallback.")
416388
continue
417389
warp_mesh = manager.get_warp_mesh(other_mesh, obj=other)
418390
other_bbox = state.get_bbox(other)
@@ -451,7 +423,7 @@ def _build_vectorized_cache(
451423
if other_mesh is None:
452424
if other.name not in self._warned_no_mesh:
453425
self._warned_no_mesh.add(other.name)
454-
print(f"[NoCollision] MESH mode: '{other.name}' has no collision mesh, skipping.")
426+
print(f"[NoCollision] '{other.name}' has no collision mesh; pair will use AABB fallback.")
455427
continue
456428
other_spheres = manager.get_query_spheres(other_mesh, obj=other).to(device)
457429
other_centers_local = other_spheres[:, :3]
@@ -487,8 +459,6 @@ def _build_vectorized_cache(
487459
if not centers_list:
488460
return None
489461

490-
from isaaclab_arena.relations.mesh_pair_cache import MeshPairCache
491-
492462
wp_device = str(device)
493463
pair_sphere_count = torch.tensor([e - s for s, e in pair_slices], dtype=torch.float32, device=device)
494464
sphere_pair_id = torch.repeat_interleave(
@@ -526,10 +496,6 @@ def _compute_no_overlap_loss_mesh(
526496
Uses precomputed pair cache (centers, radii, mesh indices) to batch all
527497
sphere queries into a single Warp kernel call per iteration.
528498
"""
529-
import warp as wp
530-
531-
from isaaclab_arena.relations.warp_sdf_kernels import clamp_sdf_sentinel, multi_mesh_sdf
532-
533499
device = state.device
534500
total_loss = torch.zeros(state.batch_size, device=device, dtype=torch.float32)
535501
clearance_m = self.params.clearance_m
@@ -714,7 +680,7 @@ def solve(
714680
if self.params.collision_mode == CollisionMode.MESH:
715681
self._mesh_manager.reset_sentinel_warning()
716682

717-
# Setup optimizer (only for optimizable positions)
683+
# Only optimizable_positions participates in Adam — anchors are fixed.
718684
optimizer = torch.optim.Adam([state.optimizable_positions], lr=self.params.lr)
719685

720686
# Compute initial loss so _last_loss_per_env is always populated
@@ -732,12 +698,10 @@ def solve(
732698
if self.params.save_position_history and iter % self.POSITION_HISTORY_SAVE_INTERVAL == 0:
733699
position_history.append(state.get_all_positions_snapshot())
734700

735-
# Compute total loss
736701
loss = self._compute_total_loss(state)
737702
loss_history.append(loss.item())
738703

739-
# Backprop and update (only optimizable positions will update).
740-
# Constant-zero loss (all pairs broadphase-culled, no relations) has no grad_fn; nothing to step.
704+
# Constant-zero loss has no grad_fn — skip backward when broadphase culls all pairs.
741705
if loss.grad_fn is not None:
742706
loss.backward()
743707
optimizer.step()
@@ -758,7 +722,6 @@ def solve(
758722
print(f"\nFinal loss: {loss_history[-1]:.6f}")
759723
print(f"Total iterations: {len(loss_history)}")
760724

761-
# Store metadata for optional access
762725
self._last_loss_history = loss_history
763726
self._last_position_history = position_history
764727

@@ -796,7 +759,6 @@ def debug_losses(self, objects: list[ObjectBase]) -> None:
796759
print("No position history available. Run solve() first.")
797760
return
798761

799-
# Build positions dict from final position history
800762
final_positions = {obj: (pos[0], pos[1], pos[2]) for obj, pos in zip(objects, final_positions_list)}
801763

802764
state = RelationSolverState(objects, [final_positions])

0 commit comments

Comments
 (0)