Skip to content

Use OrientedBoundingBox for collision checks - #1480

Open
zhx06 wants to merge 6 commits into
mainfrom
zxiao/feature/obb-collision
Open

zhx06 wants to merge 6 commits into
mainfrom
zxiao/feature/obb-collision

Conversation

@zhx06

@zhx06 zhx06 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Use oriented bounding boxes for placement collision loss and validation. Keep the existing interval calculation for axis-aligned boxes and batch collision checks across candidates. This builds on OrientedBoundingBox from #1477.

Why this change?

Enclosing AABBs can overlap even when rotated objects are separate. Placement previously penalized and rejected those layouts, although the oriented geometry was available.

What changes?

  • Preserve each candidate's rotation and assigned object geometry through solving, filtering, ranking and validation.
  • Penalize overlap_volume_upper_bound during optimization: the smaller of the interval-overlap volumes along either box's face axes. OverlapVolumeUpperBound caches the rotation-dependent terms once per solve.
  • Validate with the separating-axis test.
  • Keep the axis-aligned loss, directed gradients and existing mesh checks. Use matching collision geometry for meshless background obstacles.
  • In the oriented loss, used when any collision box in a solve is rotated, boxes thinner than 2 mm count as 2 mm thick. A flat object that overlaps an obstacle therefore still has an overlap volume to reduce.

collision_bboxes stores oriented geometry once per batch. Existing candidate AABBs remain available for relation constraints and mesh proxies; their enclosing extents cannot recover the original rotated shape.

CollisionMode.BBOX uses this automatically. Arbitrary-yaw results can change, including existing clutter examples. Support containment and clutter release initialization remain AABB-based; the existing rotated-support and On(overlap=True) containment issues need a separate correction.

Results

The placement-success benchmark (#1497) ran every RoboLab and KitchenBench spec on main and on this branch, with 50 layouts × 3 seeds per spec.

Setting Suite Layout success, main → this PR Change (95% CI) Valid candidates, main → this PR Change (95% CI)
Random yaw RoboLab (38) 64.6% → 76.1% +10.6 to +12.0 pts 36.2% → 52.6% +16.0 to +16.8 pts
Random yaw KitchenBench (20) 31.9% → 32.5% −0.8 to +1.9 pts 8.2% → 8.3% −0.3 to +0.5 pts
As authored RoboLab (38) 94.2% → 94.2% −0.8 to +0.8 pts 67.0% → 67.4% +0.0 to +0.8 pts
As authored KitchenBench (20) 84.2% → 84.1% −1.7 to +1.6 pts 27.3% → 27.3% −0.6 to +0.6 pts
  • Random yaw: 21 RoboLab tasks improve significantly after a Holm adjustment, and none get worse. Bin and clutter tasks gain the most:
    • throw_away_snacks: layout success goes from 28% to 100%;
    • marker_in_mug: valid candidates go from 7% to 65%.
  • KitchenBench is unchanged: its failures come mostly from placing the robot by relations, which oriented boxes do not affect.
  • As authored: no suite changes. Two RoboLab tasks improve, food_packing_1_boxes and food_packing_1_cans (valid candidates 75% → 83%).
  • Why it improves: the separating-axis test is exact for boxes, so rotated layouts that enclosing AABBs falsely rejected now pass.
  • A crowded tabletop: on a reconstructed GR1 tabletop with random yaw, the dense scene's valid selected layouts rose from 171/192 to 192/192 on CPU and from 172/192 to 192/192 on GPU.
  • Exact quarter turns are unchanged: positions, losses and verdicts match main.

Limitations

  • Tilted boxes: the loss can penalize tilted boxes that the separating-axis test finds separate.

  • Stuck overlaps: when one box's projections sit inside the other's on every face axis, the loss is constant and its gradient is zero, so the solver can leave deep overlaps. With the same oriented validator, sparse scenes with long objects do slightly worse with the oriented loss than with the enclosing-AABB loss:

    Scene, random yaw Oriented loss Enclosing-AABB loss
    6 objects, valid without overlap 97.0–98.0% 98.75–99.4%
    Two 0.5 m rods, valid 75.0% 80.2%

    A follow-up adds an in-plane enclosing-box term to rotated pairs only. In evaluation it raised random-yaw RoboLab valid candidates from 52.6% to 57.9%, with no task worse.

  • Near-quarter yaws: robot yaws such as yaw_rad: 1.57 in KitchenBench are not exact quarter turns, so those scenes take the oriented path. Positions matched main in a probe; only the cost differs.

Performance

Compared with AABB placement at aaf71703d4, using the maintained GR1 six-object tabletop geometry and relations reconstructed as benchmark assets, with random yaw and a denser variant. Both versions use the same three seeds, 64 environments, ten candidates per environment, 600 iterations and eight CPU threads; Intel Core Ultra 9 285K and RTX 6000 Ada. These numbers were measured before the final review changes.

Times are means across the three seeds and cover candidate generation, geometry preparation, solving, validation and ranking. They exclude asset loading and physics.

Scene / device AABB OBB Change
Sparse / CPU 2.55 s 2.17 s −15%
Sparse / GPU 3.71 s 3.32 s −11%
Dense / CPU 2.67 s 2.62 s −2%
Dense / GPU 4.82 s 4.99 s +4%

Validation is now batched across candidates, which accounts for most of the total-time gain. Each solver iteration costs more, and the cached terms grow with directed pairs × candidates. These numbers are indicative, measured on a shared machine:

Scene (objects × candidates) CPU per iteration GPU per iteration Peak GPU memory
6 × 640 2.4 → 3.1 ms about equal 2.3 → 22.8 MiB
12 × 2560 10.2 → 17.2 ms 8.7 → 13.4 ms 38 → 127 MiB
25 × 1024 21.7 → 33.6 ms 20.8 → 23.2 ms 65 → 210 MiB

Full-suite wall time as authored went from 723 to 603 s on RoboLab and from 648 to 590 s on KitchenBench. Both arms ran in parallel on a shared machine.

These are placement measurements, not physics-acceptance rates.

Validation

  • Benchmark: the placement-success runs above cover main at 843d8fb656 and this branch at e695eeab3d. All 69 specs ran with no errors in both settings.
  • Tests at this revision, rebased on main:
    • 390 placement, geometry and collision tests pass;
    • 5 simulator tests pass, covering embodiment collision meshes, placement after build-time variations, and relation placement from YAML;
    • pre-commit passes.
  • Exact quarter turns: candidate positions, losses and verdicts match the AABB baseline exactly.
  • Zero thickness: a test checks that a zero-thickness panel overlapping a fixed block gets a loss and is pushed clear; it fails without the 2 mm minimum. Through ObjectPlacer, zero-thickness panels against a fixed block went from 14/64 to 36/64 valid candidates.
  • Loss vs validator: in 1.6M random pairs, yaw-only and tilted, at margins 0 and 0.01, the validator accepted every layout the loss treats as clear. Loss values match an independent float64 corner-projection oracle, and finite differences match autograd.
  • Bugs injected in review: the tests catch the bugs injected across the loss, validator, rotation and routing code. Not pinned: the obstacle side and exact size of the 2 mm minimum, and the pair cache's configuration key, which never changes within a solve.

@zhx06 zhx06 changed the title use obb for collision checks Use OrientedBoundingBox for collision checks Oct 19, 2026
@zhx06
zhx06 force-pushed the zxiao/feature/obb-geometry branch from aaf7170 to b1bebfc Compare October 20, 2026 17:06
@zhx06
zhx06 force-pushed the zxiao/feature/obb-collision branch from c43326d to 78c30b3 Compare October 21, 2026 06:43
@zhx06
zhx06 force-pushed the zxiao/feature/obb-geometry branch from b1bebfc to 1ff97e5 Compare October 21, 2026 17:52
@zhx06
zhx06 force-pushed the zxiao/feature/obb-collision branch from 78c30b3 to 3e2045c Compare October 22, 2026 15:22
@zhx06
zhx06 force-pushed the zxiao/feature/obb-geometry branch from 1ff97e5 to 69fb952 Compare October 22, 2026 15:52
@zhx06
zhx06 changed the base branch from zxiao/feature/obb-geometry to main October 22, 2026 19:30
@zhx06
zhx06 force-pushed the zxiao/feature/obb-collision branch from 3e2045c to e695eea Compare October 22, 2026 20:39
@zhx06
zhx06 marked this pull request as ready for review October 22, 2026 20:40
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Summary

The PR preserves rotated collision boxes through candidate solving and validation.

  • Placement collision checks use each candidate’s rotated box.

No actionable issues were found.

Author zhx06 explicitly acknowledges that tilted boxes can receive a penalty despite passing SAT, that support containment and clutter initialization remain AABB-based, and that mesh penalties can disagree with validation near contact. These are documented limitations or deferred work, not new findings.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Assigned object geometry and candidate rotations] --> B[Build enclosing and oriented bounds]
    B --> C[Optimize positions with cached box projections]
    C --> D[Apply FaceTo headings and rebuild bounds]
    D --> E[Batched box checks and existing mesh checks]
    E --> F[Rank validated candidates per environment]
Loading

Reviews (1) · Last reviewed commit: "fix docs and tests" · Reviewed by Greptile

centers_W[obj] = geometry_center
subject_centers_W = torch.stack([centers_W[p.subject] for p in pairs])
obstacle_centers_W = torch.stack([centers_W[p.obstacle].detach() for p in pairs])
return strategy.slope * overlap_bound.compute(subject_centers_W, obstacle_centers_W, margin=clearance_m)

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.

🟡 Loss can stop moving boxes that still overlap

OverlapVolumeUpperBound.compute returns torch.minimum(volume_A, volume_B), so only the smaller volume passes gradient. If that side is saturated on every axis, the loss stays positive but the subject gets exactly zero gradient, even when the other box's frame would still push it out.

Example: a 0.4 × 0.01 × 0.1 m bar, yawed 45°, with its center 5 cm from the center of a fixed 0.2 m cube. The loss is positive and the gradient is exactly zero. The old enclosing-AABB loss had a nonzero gradient here. I re-implemented the formula outside the repo and tried random yawed boxes overlapping a fixed box. In about two thirds of the zero-gradient cases, the other frame still had a gradient.

The validator rejects these layouts, so the cost is fewer valid candidates, not bad layouts.

Could the loss combine both volumes? For example, 2 * volume_A * volume_B / (volume_A + volume_B).clamp_min(eps):

  • It is zero exactly when either volume is zero.
  • It equals the exact volume for axis-aligned pairs, so the quarter-turn match with the old loss still holds.
  • It passes gradient from both frames.

OverlapVolumeUpperBound.compute can stay a true upper bound if the loss reads the two volumes separately.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@arena-review-bot Could you make a diagram explaining this?

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.

Sure, here's the example from the comment

Top view, to scale: 1 character = 1 cm across, 1 line = 2 cm up. + is the cube's center and o is the bar's center, 5 cm to the right. The bar is 40 cm long, 1 cm thick and 10 cm tall.

                             /
                           /
+-------------------+    /
|                   |  /
|                   |/
|                  /|
|                /  |
|         +    o    |
|            /      |
|          /        |
|        /          |
|      /            |
+----/--------------+
   /
 /

compute projects both boxes onto each box's own three axes, multiplies the three interval overlaps, and keeps the smaller of the two products. On one axis, moving the bar changes the overlap only if the two intervals partly overlap. If one interval sits fully inside the other, the overlap is just the shorter length, so a small move doesn't change it.

Bar's axes -> volume_A (cm, relative to the bar's center)
  along    bar   [-20 ======================== 20]
           cube     [-17.7 -------------- 10.6]      cube inside bar  -> 28.3, fixed
  across   bar             [-0.5 = 0.5]
           cube     [-10.6 -------------- 17.7]      bar inside cube  -> 1, fixed
  up       bar [-5, 5] inside cube [-10, 10]                          -> 10, fixed
  volume_A = 28.3 x 1 x 10 = 283 cm^3, gradient 0

Cube's axes -> volume_B (cm, relative to the cube's center)
  x        cube  [-10 ============ 10]
           bar    [-9.5 -------------------- 19.5]   partial overlap  -> 19.5, left end
                                                                         moves with the bar
  y        cube [-10, 10] inside bar [-14.5, 14.5]                    -> 20, fixed
  up       bar [-5, 5] inside cube [-10, 10]                          -> 10, fixed
  volume_B = 19.5 x 20 x 10 = 3900 cm^3, gradient pushes the bar toward +x

torch.minimum picks the 283 cm³ from volume_A, so the bar gets zero gradient even though volume_B would push it out. With 2 * volume_A * volume_B / (volume_A + volume_B), the combined volume here is about 527 cm³ and the gradient from volume_B reaches the bar.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zhx06 If the agent is correct, this seems like it could be an issue?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the issue is real. We adopt the harmonic mean suggestion, and the valid rate increases a lot.

Comment thread isaaclab_arena/relations/relation_solver_state.py Outdated
Comment thread isaaclab_arena/relations/relation_solver_state.py Outdated
Comment thread isaaclab_arena/relations/bounding_box_helpers.py
Comment thread isaaclab_arena/tests/test_obb_collision.py Outdated
@arena-review-bot

Copy link
Copy Markdown
Contributor

🤖 Isaac Lab-Arena Review Bot

Summary

This PR switches CollisionMode.BBOX from enclosing AABBs to oriented boxes. The solver now penalizes a face-axis overlap-volume bound, and the no-overlap validator runs a batched SAT check across candidates. The approach is sound and well covered by tests. The main concern is that the new loss can stop giving any gradient while two boxes still overlap. The other comments suggest ways to keep the new box state and caching smaller.

Design, Boundaries & Scope

  • Default results change for many users. BBOX is the default mode, so seeded layouts will change for anyone who uses random_yaw_init, ClutterOn (its random_yaw defaults to True), FaceTo, or a non-quarter-turn RotateAroundSolution. Keeping it on by default seems reasonable: validation now only accepts extra layouts whose boxes really don't overlap. Still, downstream users should hear about it in a release note.
  • Box state is spread across several places. RelationSolverState gains three box maps, a geometry cache and a keyed pair cache. PlacementCandidateBatch.collision_bboxes holds the same geometry as each candidate.bboxes, in a second form that update_candidate_bounds has to keep in sync. The inline comments suggest three changes:
    • Prepare the pair cache in RelationSolver.solve, next to _mesh_cache.
    • Drop the duplicate maps.
    • Derive the enclosing boxes from the collision boxes.

Findings

🟡 Warning: isaaclab_arena/relations/no_overlap_bbox.py:235: The loss takes torch.minimum of the two face-axis volumes. When the smaller volume is saturated on every axis, the loss stays positive but the subject gets exactly zero gradient, even when the other box's frame would still push it out. The validator still rejects these layouts, so the cost is fewer valid candidates, not bad layouts.

🔵 Improvement: isaaclab_arena/relations/relation_solver_state.py:155: The pair cache's key can't change during a solve. The cache could be prepared once in RelationSolver.solve the same way _mesh_cache is. That removes the key, two fields and the circular import of no_overlap_bbox.

🔵 Improvement: isaaclab_arena/relations/relation_solver_state.py:124: _enclosing_collision_bboxes repeats get_bbox and _fixed_obstacle_world_bboxes. _collision_bboxes copies the fixed world boxes again, next to boxes stored relative to each movable object's origin.

🔵 Improvement: isaaclab_arena/relations/bounding_box_helpers.py:138: update_candidate_bounds rotates every object's bounds twice. The enclosing boxes could be derived from the collision boxes with to_aabb().

🔵 Improvement: isaaclab_arena/relations/object_placer_params.py:27-29 (not in the diff): The random_yaw_init docstring still says collisions use "the conservative box enclosing the rotated object". That is no longer true in BBOX mode.

Test Coverage and CI Cost

  • The new tests use plain torch and don't start a SimulationApp, so they add little CI time.
  • test_validator_oriented_boxes_clear_fixed_obstacles_and_support_enclosing_bounds repeats cases that other tests in this PR already check (see the inline comment).
  • When this review ran, "Run tests" and "Run tests with subprocess" were still in progress, so I couldn't confirm that the test suite passes. Pre-commit and the docs build had passed.

Verdict

Minor fixes needed

@alexmillane alexmillane left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM.

The only functional issue is the one that the bot commented on about the loss being zero even when there's overlap in some cases.

My additional small comments are about breaking up some functions, moving some code to different places.

state: RelationSolverState,
no_collision_strategy: NoCollisionLossStrategy,
clearance_m: float,
mesh_manager: WarpMeshAndSphereCache | None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Design: compute_no_overlap_loss_bbox does mesh routing

compute_no_overlap_loss_bbox only computes box losses, but it takes mesh_manager, default_collision_mode and skip_mesh_pairs so that prepare_no_overlap_pair_cache can decide which pairs it handles:

  • _fixed_pair_is_covered_by_mesh_collision and _dynamic_pair_is_covered_by_mesh_collision drop pairs that the mesh loss covers.
  • The mesh_objects set sends pairs that involve a mesh object to mesh_fallback_pairs.

In BBOX-only mode, RelationSolver._compute_no_overlap_loss still passes self._mesh_manager, but without skip_mesh_pairs the argument is never used.

This makes the bbox module depend on mesh concerns. It also repeats the "is this pair covered by the mesh loss" rule from _collect_mesh_pairs in no_overlap_mesh.py. If the two copies drift apart, a pair is either penalized by both losses or by neither.

This builds on the bot's comment on relation_solver_state.py:155. Could RelationSolver.solve decide the routing once, next to prepare_mesh_collision_cache, and pass only the resulting pair lists to compute_no_overlap_loss_bbox? Ideally both losses would get their pairs from that one place, so the bbox module no longer needs mesh_manager.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed. Routing is in solve now. We leave shared router in a follow-up

Comment thread isaaclab_arena/utils/bounding_box.py Outdated
return AxisAlignedBoundingBox(min_point=rotated.amin(dim=1), max_point=rotated.amax(dim=1))


class OverlapVolumeUpperBound:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Question: does caching in OverlapVolumeUpperBound pay off?

OverlapVolumeUpperBound.__init__ caches _R_A_W, _R_B_A and the projected radii once per solve, and compute() reuses them every iteration. On main, _oriented_projected_overlap_volume recomputed these terms on each call.

Does the cache measurably speed up a solver iteration? The PR's performance numbers compare AABB with OBB placement, not cached with uncached, and they show per-iteration time and peak memory going up. The cached tensors scale with pairs × candidates × 9, so they may explain much of the memory increase.

If the gain is small, keeping the plain function would be simpler. BoundingBox.overlap_volume, which builds an instance and calls compute() once, wouldn't need the class either.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I measured cached against uncached, recomputing the rotations and projected radii every call:

Objects × candidates CPU ms per iteration (main / cached / uncached) Peak GPU memory, MiB (main / cached / uncached)
6 × 640 3.2 / 3.5 / 3.7 2.4 / 6.6 / 7.9
12 × 1024 7.3 / 9.8 / 12.4 15.2 / 44.4 / 53.2
25 × 1024 26.5 / 37.0 / 43.8 65.8 / 194.6 / 236.5

Dropping the cache makes iterations 6–26% slower and raises peak memory by 16–22%, because autograd keeps the recomputed terms for the backward pass. The increase over main comes from storing a rotation per pair and candidate, which the uncached version needs too.

So I kept the cache and renamed it CachedOverlapVolumeUpperBound.



class OverlapVolumeUpperBound:
"""Overlap-volume upper bound for box pairs whose half-extents and rotations stay fixed while centers move.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Suggestion (non-blocking): if the cache stays, name it as one and move it next to the solver

This follows from the question above. If caching does measurably speed up the solver, the class is solver acceleration rather than bounding-box geometry, and its name and location should say so.

  • Name: OverlapVolumeUpperBound reads like a geometric quantity, but the class holds precomputed per-pair terms for repeated evaluation. A name such as CachedOverlapVolumeUpperBound or OverlapVolumeUpperBoundCache would make that clear to callers.
  • Location: its only real user is the solver loss in no_overlap_bbox.py (prepare_no_overlap_pair_cache and _compute_oriented_pair_losses). It could live there, or in another module under relations/, alongside NoOverlapPairCache.

BoundingBox.overlap_volume (line 125) would then go back to calling a plain function, as on main, since utils/ shouldn't import from relations/. The moved class would need that function, or _interval_overlap_volume, exposed as a public helper in bounding_box.py so the geometry is still defined once.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed. kept in utils/ to avoid a second copy

Comment on lines +96 to +186
def prepare_no_overlap_pair_cache(
state: RelationSolverState,
mesh_manager: WarpMeshAndSphereCache | None,
default_collision_mode: CollisionMode,
skip_mesh_pairs: bool,
) -> NoOverlapPairCache:
"""Prepare fixed pair routing and the overlap-volume upper bound after mesh geometry is available.

Args:
state: Solver state with fixed collision bounds and object relations.
mesh_manager: Cache supplying available collision meshes, or None.
default_collision_mode: Mode for objects without an explicit override.
skip_mesh_pairs: Exclude pairs represented by the mesh collision loss.

Returns:
Ordered pair groups and the prepared upper bound for this solve.
"""
movable = state.optimizable_objects
anchors = list(state.anchor_objects)
fixed = anchors + state.collision_objects
on_pairs = set()
for obj in [*movable, *anchors]:
for relation in obj.get_relations():
if isinstance(relation, On):
on_pairs.add((id(obj), id(relation.parent)))
on_pairs.add((id(relation.parent), id(obj)))

pairs = []
for child in movable:
for obstacle in fixed:
if (id(child), id(obstacle)) in on_pairs:
continue
if (
skip_mesh_pairs
and mesh_manager is not None
and _fixed_pair_is_covered_by_mesh_collision(
state, child, obstacle, mesh_manager, default_collision_mode
)
):
continue
pairs.append(NoOverlapPair(child, obstacle))
for index, child in enumerate(movable):
for other in movable[index + 1 :]:
if (id(child), id(other)) in on_pairs:
continue
if (
skip_mesh_pairs
and mesh_manager is not None
and _dynamic_pair_is_covered_by_mesh_collision(
state, child, other, mesh_manager, default_collision_mode
)
):
continue
pairs.extend((NoOverlapPair(child, other), NoOverlapPair(other, child)))
if not pairs:
return NoOverlapPairCache([], [], None)

mesh_objects = set()
if skip_mesh_pairs and mesh_manager is not None:
for obj in [*movable, *fixed]:
if (
object_uses_mesh_collision(obj, default_collision_mode)
and mesh_manager.get_collision_mesh(obj) is not None
):
mesh_objects.add(obj)
box_pairs, mesh_fallback_pairs = [], []
background_objects = set(state.collision_objects)
for pair in pairs:
uses_mesh_validation = pair.obstacle in mesh_objects or (
pair.subject in mesh_objects and pair.obstacle not in background_objects
)
if uses_mesh_validation:
# Mesh validation uses enclosing-box sphere proxies for these unsupported pairs.
mesh_fallback_pairs.append(pair)
else:
box_pairs.append(pair)

overlap_bound = None
if box_pairs and not state.collision_boxes_axis_aligned:
geometry = {}
for pair in box_pairs:
for obj in (pair.subject, pair.obstacle):
if obj not in geometry:
geometry[obj] = state.get_collision_geometry(obj)
overlap_bound = OverlapVolumeUpperBound(
torch.stack([geometry[p.subject][1] for p in box_pairs]).clamp_min(_MIN_LOSS_HALF_EXTENT_M),
torch.stack([geometry[p.subject][2] for p in box_pairs]),
torch.stack([geometry[p.obstacle][1].detach() for p in box_pairs]).clamp_min(_MIN_LOSS_HALF_EXTENT_M),
torch.stack([geometry[p.obstacle][2].detach() for p in box_pairs]),
)
return NoOverlapPairCache(box_pairs, mesh_fallback_pairs, overlap_bound)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Suggestion (non-blocking): split prepare_no_overlap_pair_cache into its steps

prepare_no_overlap_pair_cache is 74 lines and runs four separate steps one after another:

  1. Build the On-linked on_pairs set.
  2. List directed pairs (movable vs fixed, then movable vs movable), skipping pairs the mesh loss covers.
  3. Split them into box_pairs and mesh_fallback_pairs using the mesh_objects set.
  4. Gather, stack and clamp geometry to build OverlapVolumeUpperBound.

Could each step be a small helper, for example _get_on_linked_pairs, _list_no_overlap_pairs, _split_mesh_fallback_pairs and _build_overlap_bound? This function would then be about eight lines that read as the outline above.

  • _get_on_linked_pairs could also replace the identical on_pairs loop in RelationSolver.solve, as the bot's pair-cache comment on relation_solver_state.py:155 suggests.
  • Steps 1–3 are the routing from the mesh_manager comment, so having them as separate helpers would make them easy to move into solve.

@zhx06 zhx06 Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: split into three helpers, and get_on_linked_pairs is now shared with the mesh cache in solve

Comment on lines +251 to +276
collision_bboxes_with_defaults = dict(collision_bboxes or {})
base_bounds = {}
for obj in objects:
# Anchors use their fixed world bounds, so they need no candidate collision bounds.
if obj.is_anchor or obj in collision_bboxes_with_defaults:
continue
if env_bboxes is not None and obj in env_bboxes:
if env_bboxes_include_yaw:
collision_bboxes_with_defaults[obj] = env_bboxes[obj]
else:
base_bounds[obj] = env_bboxes[obj]
else:
base_bounds[obj] = obj.get_bounding_box()
if base_bounds:
collision_bboxes_with_defaults.update(
get_candidate_collision_bboxes(
list(base_bounds), base_bounds, orientations or [{} for _ in initial_positions]
)
)
state = RelationSolverState(
objects, initial_positions, device=device, env_bboxes=env_bboxes, collision_objects=collision_objects
objects,
initial_positions,
device=device,
env_bboxes=env_bboxes,
collision_objects=collision_objects,
collision_bboxes=collision_bboxes_with_defaults,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Suggestion (non-blocking): extract the collision-bounds defaulting from solve

RelationSolver.solve is already about 145 lines, and this block adds 20 more. It does one thing: fill in collision bounds for movable objects that have no collision_bboxes entry, either from env_bboxes (unchanged when env_bboxes_include_yaw, otherwise rotated by orientations) or from get_bounding_box().

Could it be a helper such as _get_collision_bboxes_with_defaults(objects, env_bboxes, env_bboxes_include_yaw, orientations, collision_bboxes, batch_size)? solve would then read as: get collision bounds, build state, run the loop.

Related: RelationSolverState.__init__ falls back to enclosing bounds for objects missing from collision_bboxes, so the defaulting logic is currently split between the two. A helper would be a natural single home for it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: extracted _get_collision_bboxes_with_defaults, so solve now reads as get bounds, build state, run the loop. The state keeps a small fallback only for states built outside solve, as in tests and debug_losses.

Comment on lines +124 to +147
self._enclosing_collision_bboxes = dict(self._fixed_obstacle_world_bboxes)
for obj in self._optimizable_objects:
self._enclosing_collision_bboxes[obj] = self.get_bbox(obj)
self._collision_bboxes: dict[PlaceableAsset | CollisionObject, BoundingBox] = dict(
self._fixed_obstacle_world_bboxes
)
for obj in self._optimizable_objects:
bounds = (
collision_bboxes[obj]
if collision_bboxes is not None and obj in collision_bboxes
else self._enclosing_collision_bboxes[obj]
)
self._collision_bboxes[obj] = bounds.to(self._device)
self._collision_boxes_axis_aligned = all(
isinstance(box, AxisAlignedBoundingBox) for box in self._collision_bboxes.values()
)
self._collision_geometry = {}
"""Fixed center offsets (B, 3), half-lengths (B, 3), and axes (B, 3, 3), with B = batch size."""
if not self.collision_boxes_axis_aligned:
for obj in self._collision_bboxes:
self.get_collision_geometry(obj)
self._no_overlap_pair_cache: NoOverlapPairCache | None = None
"""Pair routing and the prepared upper bound for the current collision configuration."""
self._no_overlap_pair_cache_key: tuple[WarpMeshAndSphereCache | None, CollisionMode, bool] | None = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-drafted (Claude Code), reviewed by me.

Suggestion (non-blocking): move collision-bounds setup out of RelationSolverState.__init__

These lines make __init__ about 93 lines long. They do one self-contained job: build _enclosing_collision_bboxes and _collision_bboxes, set _collision_boxes_axis_aligned, precompute geometry, and initialize the pair-cache fields.

Could they be one method, for example _build_collision_bboxes(collision_bboxes), called from __init__?

This pairs with the bot's comment on line 124 about the boxes being stored three times. With the setup in one method, that cleanup touches a single place. If the pair cache moves into RelationSolver.solve, as the bot's line-155 comment suggests, the cache fields leave this block too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: the setup is now _build_collision_bboxes, called from __init__. With the bot's two suggestions also applied, it only stores collision boxes for movable objects: the enclosing copy and the pair-cache fields are gone, and the pair cache now lives in RelationSolver.

centers_W[obj] = geometry_center
subject_centers_W = torch.stack([centers_W[p.subject] for p in pairs])
obstacle_centers_W = torch.stack([centers_W[p.obstacle].detach() for p in pairs])
return strategy.slope * overlap_bound.compute(subject_centers_W, obstacle_centers_W, margin=clearance_m)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@zhx06 If the agent is correct, this seems like it could be an issue?

zhx06 added 5 commits October 23, 2026 14:05
Signed-off-by: zhx06 <zihaox@nvidia.com>
Signed-off-by: zhx06 <zihaox@nvidia.com>
Signed-off-by: zhx06 <zihaox@nvidia.com>
Signed-off-by: zhx06 <zihaox@nvidia.com>
Signed-off-by: zhx06 <zihaox@nvidia.com>
@zhx06
zhx06 force-pushed the zxiao/feature/obb-collision branch from e695eea to f2249a6 Compare October 23, 2026 21:06
Signed-off-by: zhx06 <zihaox@nvidia.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants