[OUTDATED] Make no-overlap a built-in solver behavior#573
[OUTDATED] Make no-overlap a built-in solver behavior#573cvolkcvolk wants to merge 18 commits intomainfrom
Conversation
Change compute_loss signature to take clearance_m as a parameter instead of reading it from a NoCollision relation instance. This prepares the strategy for use as an internal solver utility. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
…tegies Single parameter controls both solver optimization and post-solve validation. NoCollision is no longer a pluggable strategy. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
The solver now computes no-overlap loss for all (non-anchor, non-anchor) and (non-anchor, anchor) pairs using clearance_m from RelationSolverParams. Both directions are computed for non-anchor pairs so each object receives gradient. No explicit NoCollision relations needed. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
No-overlap is now handled internally by RelationSolver. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
The solver now handles no-overlap internally. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Remove min_separation_m from ObjectPlacerParams. Post-solve validation now uses the same clearance_m as the solver optimization. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Examples no longer need explicit NoCollision relations. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Objects placed On(anchor) are intentionally adjacent to the anchor in Z, so applying clearance_m to anchor pairs caused false rejection. Validation now only checks clearance between non-anchor pairs. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Greptile SummaryThis PR refactors no-overlap handling from an explicit user-facing
Confidence Score: 4/5Safe to merge only after the _validate_on_relations upper-bound regression is addressed; default-clearance users are unaffected but sub-5mm On.clearance_m silently fails. One P1 regression: On.clearance_m values smaller than (solver.clearance_m - on_relation_z_tolerance_m) = 5 mm will cause all placement attempts to fail silently because the built-in no-overlap loss pushes the child past the On validation upper bound. All tests use matching default clearances (10 mm) and therefore miss this path. The core refactor logic, gradient coupling, and margin-based no-overlap validation are otherwise sound. isaaclab_arena/relations/object_placer.py (_validate_on_relations upper-bound) and isaaclab_arena/tests/test_no_collision_loss.py (missing coverage for On.clearance_m < solver.clearance_m) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ObjectPlacer.place] --> B[_generate_initial_positions]
B --> C[RelationSolver.solve]
C --> D[_compute_total_loss per iteration]
D --> E[Relation losses\nOn / NextTo / AtPosition]
D --> F[_compute_no_overlap_loss\nbuilt-in, slope=10000]
F --> G{child in\noptimizable?}
G -->|yes| H[compute overlap vs ALL objects\nanchor + non-anchor]
H --> I[volume loss = slope * overlap_x * overlap_y * overlap_z]
C --> J[_validate_placement]
J --> K[_validate_no_overlap\nmargin = solver.clearance_m - 1e-6]
J --> L[_validate_on_relations\nupper = On.clearance_m + 5mm]
K --> M{gap ≥ margin?}
L --> N{child_bottom ≤\nOn.clearance_m + 5mm?}
M -->|yes| O[pass]
N -->|yes| O
M -->|no| P[retry]
N -->|no - regression when\nOn.clearance_m < 5mm| P
O --> Q[_apply_positions to objects]
|
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Instead of skipping On-parent anchor pairs entirely, check them with margin=0.0 to catch actual 3D penetration. Only non-anchor vs non-anchor pairs use the clearance_m margin. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
kellyguo11
left a comment
There was a problem hiding this comment.
Review: Make no-overlap a built-in solver behavior
Nice refactor. Moving no-overlap from a user-managed NoCollision relation to a built-in solver behavior is the right call — reduces API surface and eliminates a common footgun (forgetting to add pairwise constraints). The commit stack is clean and logical.
What works well
- Single parameter (
clearance_m) onRelationSolverParams— clean, discoverable, with a sensible 1cm default and__post_init__validation. - Bidirectional loss for non-anchor pairs — computing loss in both directions (
i→jandj→i) ensures both objects receive gradient. Good detail. - Smart On-parent skipping in validation —
_has_on_relation_with()correctly lets child-on-parent overlap pass while still checking non-On anchor pairs. The asymmetric margin (clearance for sibling pairs, 0.0 for non-On anchor pairs) is a reasonable choice. - Test coverage — 9 tests cover the strategy API change, solver separation, clearance_m respect, negative validation, On-parent overlap acceptance, non-anchor overlap rejection, and reproducibility. Solid.
- Clean removal —
NoCollisionclass,_add_pairwise_no_collision(),min_separation_m, and all example/env usages are fully removed. No stale references found in docs or other source files.
Issues / Suggestions
1. Solver applies clearance to non-anchor↔anchor pairs, but validation doesn't (intentional asymmetry?)
In _compute_no_overlap_loss, non-anchor vs anchor pairs use clearance_m. But in _validate_no_overlap, those same pairs use margin=0.0. The docstring says "clearance is meant for sibling objects" — but the solver loss pushes non-anchors away from anchors by clearance_m during optimization, then validation only checks hard overlap.
This is probably fine in practice (the solver already enforced the gap), but it means a scenario that barely passes optimization could technically fail validation if the solver residual ate into the margin. Consider whether the solver should also use margin=0.0 for non-anchor↔anchor pairs to keep them consistent, or document the rationale more explicitly.
2. O(n²) complexity for large object counts
The pairwise loop is O(n²) per iteration for n non-anchor objects. With the current use cases (handful of objects on a table) this is fine, but worth a brief comment noting that scaling to 50+ objects may want spatial hashing or a grid-based approach.
3. NoCollisionLossStrategy slope is hardcoded at 10000.0
self._no_collision_strategy = NoCollisionLossStrategy(slope=10000.0)Previously this was configurable via the strategy dict. Now it's hardcoded. For most users this won't matter, but power users may want to tune it (e.g., if clearance loss dominates their On/NextTo losses). Consider exposing no_overlap_slope on RelationSolverParams for completeness.
4. get_world_bounding_box() assumption for anchors
In _compute_no_overlap_loss, anchor bboxes are retrieved via anchor.get_world_bounding_box(), while in _compute_total_loss for regular relations, the same is done with parent.get_world_bounding_box() for anchor parents. This is consistent, but it means the solver assumes anchors always have their world bbox precomputed. If that's guaranteed by RelationSolverState initialization, great — just worth noting.
5. Spec doc not included
The PR body references docs/superpowers/specs/2026-04-10-no-collision-solver-builtin-design.md but this file is not in the branch. Not blocking, but consider including it or removing the reference.
6. Minor: relation_loss_weight no longer applies to no-overlap
Previously users could set relation_loss_weight on NoCollision to increase/decrease its priority relative to other relations. The removed test test_no_collision_loss_scales_with_relation_weight confirms this is intentionally dropped. Worth a migration note if any downstream code relied on per-pair weight tuning.
Verdict
Clean, well-tested, good API simplification. The items above are mostly minor / future-proofing. 👍
| # i as child, j as parent (gradient flows through pos_i) | ||
| world_bbox_j = obj_j.get_bounding_box().translated((pos_j[0].item(), pos_j[1].item(), pos_j[2].item())) | ||
| loss_ij = self._no_collision_strategy.compute_loss( | ||
| clearance_m=clearance, |
There was a problem hiding this comment.
The .item() calls to build the translation tuple detach from the computation graph, which is correct here — parent_world_bbox is used as a fixed target for the loss, and gradients flow only through child_pos. Good.
One thing to keep in mind: if you ever wanted gradients to flow through both positions simultaneously (e.g., for a different optimizer), you'd need to restructure this. Current approach (compute in both directions with one detached) is fine for the Adam optimizer loop.
No special-casing for anchor pairs. All non-anchor-anchor pairs are checked with the same clearance_m margin. Small epsilon tolerance (1e-6) avoids false positives at floating-point boundaries where On.clearance_m equals solver.clearance_m. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
For each non-anchor, compute overlap loss against every other object. No separate sections for anchor vs non-anchor pairs. Gradient flows to the non-anchor in each pair naturally. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Remove deleted min_separation_m from docs code example, fix max_placement_attempts default (10 not 5), update NoCollision references in example notebooks and test docstrings. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Signed-off-by: Clemens Volk <cvolk@nvidia.com>
| """Check that no two objects overlap in 3D (axis-aligned bbox with margin).""" | ||
| """Check that no two objects overlap in 3D (axis-aligned bbox with clearance margin). | ||
|
|
||
| All pairs must be separated by at least ``clearance_m``. |
There was a problem hiding this comment.
A minor question: Do we plan to have the same clearance_m for all objects? Will we expect that different object sizes sometimes benefit from different clearance values in the future?
There was a problem hiding this comment.
Yes that's a good point, we might want to add this in the future. For now I think we should go with simplicity.
| def _add_pairwise_no_collision(self, objects_with_relations: list[Object | ObjectReference]) -> None: | ||
| """Add NoCollision between every pair of non-anchor objects (if not already present).""" | ||
| non_anchors = [ | ||
| obj for obj in objects_with_relations if not any(isinstance(r, IsAnchor) for r in obj.get_relations()) | ||
| ] | ||
| for i, obj_a in enumerate(non_anchors): | ||
| for obj_b in non_anchors[i + 1 :]: | ||
| has_no_collision = any(isinstance(r, NoCollision) and r.parent is obj_b for r in obj_a.get_relations()) | ||
| if not has_no_collision: | ||
| obj_a.add_relation(NoCollision(obj_b)) |
There was a problem hiding this comment.
Yea good move to remove this from the EnvBuilder.
|
Superseded by #589 (re-implemented on develop branch) |
- Remove min_separation_m from docs code example (parameter deleted) - Fix max_placement_attempts default in docs (5 -> 10) - Add test_solver_respects_clearance_m (non-default clearance_m) - Add test_negative_clearance_m_raises (ValueError validation) - Add test_validation_accepts_on_parent_overlap (On pairs skipped) - Add test_validation_rejects_non_anchor_overlap (rejection path) - Update stale NoCollision docstrings in example smoke tests Signed-off-by: Clemens Volk <cvolk@nvidia.com>
Supersedes #573 (rebased onto `develop` due to batch solver divergence). ## Motivation No-overlap between objects was managed via explicit `NoCollision` relations that `ArenaEnvBuilder` auto-injected. This was an implementation detail leaking into the user API, and the solver had no built-in collision avoidance. ## Summary - No-overlap is now a built-in behavior of `RelationSolver`, controlled by a single `clearance_m` parameter on `RelationSolverParams` (default 1cm). - The solver computes pairwise no-overlap loss for all object pairs automatically (except anchor-anchor). - `NoCollision` class removed from the public API. - `ArenaEnvBuilder._add_pairwise_no_collision()` workaround removed. - `ObjectPlacerParams.min_separation_m` removed; validation unified to use `clearance_m`. - Stale docs and comments updated. ## Test plan - [ ] All 62 relation/placer tests pass - [x] Pre-commit checks pass --------- Signed-off-by: Clemens Volk <cvolk@nvidia.com> Signed-off-by: Lionel Gulich <lgulich@nvidia.com> Co-authored-by: Vikram Ramasamy <158473438+viiik-inside@users.noreply.github.com> Co-authored-by: qianlin <53278415+qianl-nv@users.noreply.github.com> Co-authored-by: Xinjie Yao <xyao@nvidia.com> Co-authored-by: Alex Millane <amillane@nvidia.com> Co-authored-by: lgulich <22480644+lgulich@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: isaaclab-review-bot[bot] <270793704+isaaclab-review-bot[bot]@users.noreply.github.com>
Summary
RelationSolver, controlled by a singleclearance_mparameter and exposed throughRelationSolverParams(default 1cm).ArenaEnvBuilder._add_pairwise_no_collision()removed from ArenaEnvBuilder -> This is solver specific and now handled within the solver (Eg just using the RelationSolver without the ArenaEnvBuilder would have not gotten NoCollisionConstraints)NoCollisionclass removed from the public API : Users should not need to manage collision avoidance manually. This is now a default behaviour.Test plan