Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
bf7d614
Fix profiling benchmark
rjoomen Feb 25, 2026
e20fa92
Fix onCollisionMarginDataChanged updating wrong objects in dynamic ma…
claude Feb 13, 2026
4592c23
Remove early-out in two-pose setCollisionObjectsTransform
claude Feb 13, 2026
f2fbd25
Use unordered_set for O(1) lookup in collision object removal
claude Feb 13, 2026
86678c3
Cache base vertices to avoid re-tessellation on every cast transform …
claude Feb 13, 2026
c575538
Add fallback in computeBV for unhandled shape types
claude Feb 13, 2026
d7726e6
Add CMakeFiles/ to .gitignore
claude Feb 13, 2026
d670268
Populate cc_time, cc_type, cc_transform in collision callbacks
claude Feb 13, 2026
d85ef06
Align populateContinuousCollisionFields with Bullet's calculateContin…
claude Feb 13, 2026
3c42ac2
Simplify GetAverageSupport: delegate Box to coal::details::getSupport
claude Feb 13, 2026
aab8a5a
Drop ConvexBase32 special case from GetAverageSupport
claude Feb 13, 2026
96ed5a9
Fix CastHullShape AABB not updating after cast transform change
claude Feb 25, 2026
7f14f1c
Extend ConvexBase32 instead of ShapeBase for CastHullShape
claude Feb 25, 2026
585b134
Fix covariant return type for CastHullShape::clone()
claude Feb 25, 2026
a388f60
Derive CastHullShape from ConvexTpl<Triangle32> and compute proper co…
claude Feb 25, 2026
b12e400
Widen Z-axis tolerance in box-box cast collision test
claude Feb 25, 2026
e4f3308
Revert "Widen Z-axis tolerance in box-box cast collision test"
claude Feb 25, 2026
23cef07
Add custom GJK/EPA narrowphase for CastHullShape with Schulman suppor…
claude Feb 26, 2026
09c404c
Add build/ to .gitignore
claude Feb 26, 2026
9d5f290
Fix ContactTestDataWrapper to work with both old and new ContactTestD…
claude Feb 26, 2026
25c0cef
Fix castHullGetSupportFunc: negate direction for shape1 support
claude Feb 26, 2026
b888e46
Handle both shapes as CastHullShape in custom support function
claude Feb 26, 2026
bd84fc4
Fix GJK early stopping for CastHullShape: disable distance_upper_bound
claude Feb 27, 2026
29eebb0
Use ComputeCollision for CastHullShape instead of custom GJK path
claude Feb 27, 2026
57aa40e
Restore custom GJK path for CastHullShape with DefaultGJK variant
claude Feb 27, 2026
763a8c9
Fix Schulman support for continuous shapes: average tied support points
claude Feb 27, 2026
d59a0c1
Extend cast collision tests with detailed failure diagnostics
claude Feb 27, 2026
96a9204
Add GJK diagnostic logging and improve cc_time interpolation
claude Feb 27, 2026
0d155bc
Fix GJK status enum: NoCollision, not Valid
claude Feb 27, 2026
29353ca
Fix CastHullShape collision for primitive shapes and cc_time accuracy
claude Feb 28, 2026
1ec1f8f
Update convex sphere scenario 2 expected values for center-based cc_time
claude Feb 28, 2026
5e8a4f8
Revert convex sphere test expected values to Bullet's values
claude Feb 28, 2026
85696d4
Fix Coal CastHullShape to use standard GJK matching Bullet's approach
claude Feb 28, 2026
5719608
Restore Schulman GJK approach and fix support function tie-breaking
claude Feb 28, 2026
ed8274b
Remove convex hull building from CastHullShape
claude Mar 1, 2026
fc92089
Override getNodeType() to delegate to underlying shape
claude Mar 1, 2026
5e1ca4c
Fix per-child relative transform for multi-shape continuous collision
claude Mar 1, 2026
0995cac
Fix multi-shape cast test: move obstacle to sweep midpoint
claude Mar 1, 2026
a8ed4c9
Revert "Fix multi-shape cast test: move obstacle to sweep midpoint"
claude Mar 1, 2026
08ee392
Fix CastHullShape: always use EPA for penetration info
claude Mar 1, 2026
2b63ee5
Fix cc_type classification for per-shape cast transforms
claude Mar 2, 2026
abc9571
Remove unused pt_world0/pt_world1 variables
claude Mar 2, 2026
60335a1
Fix multi-shape cast test: place obstacle at sweep midpoint
claude Mar 2, 2026
277bedc
Fix swept sphere double-counting for primitive shapes in castHullCollide
claude Mar 2, 2026
c796152
Align cast BVH behavior with bullet and add octree cast support
rjoomen Mar 2, 2026
fa4b1e4
Fix CastHullShape UB: use separate collision request cache without Co…
claude Mar 3, 2026
181adb5
Fix octree broadphase bugs and add box shape reuse in cast manager
claude Mar 3, 2026
c8ff8f7
Support all octree subtypes in makeCastCollisionObject
claude Mar 3, 2026
507cfd1
Revert non-box octree subtypes from cast path
claude Mar 3, 2026
6b0e733
Initial plan
Copilot Mar 5, 2026
e2687c1
Replace castHullCollide with Coal native GEOM_CUSTOM shape dispatch
Copilot Mar 5, 2026
9249218
Remove coal_casthullshape_utility.h and coal_casthullshape_utility.cpp
Copilot Mar 5, 2026
9d8cefe
Update to latest Tesseract
rjoomen Mar 6, 2026
b7be92a
Fix CI test failures: set collision_distance_threshold to -gjk_tolera…
Copilot Mar 6, 2026
eb1882a
Fix warning about bypassing virtual dispatch
rjoomen Mar 6, 2026
996f94e
Fix benchmarks for jammy
rjoomen Mar 6, 2026
c359065
Review coal continuous collision detection and fix TransformMap itera…
claude Mar 11, 2026
655a399
Resolve TODOs and improve code clarity in collision managers
claude Mar 11, 2026
7a51bc5
Fix stale GJK cached guess for CastHullShape collision pairs
claude Mar 11, 2026
36e4120
Don't reuse ComputeCollision functor for CastHullShape pairs
claude Mar 11, 2026
ce8f065
Fix GJK initial guess for CastHullShape: use center-to-center vector
claude Mar 12, 2026
52b8997
Enable ComputeCollision functor caching for CastHullShape pairs
claude Mar 12, 2026
f765449
Don't reset support function guess for CastHullShape pairs
claude Mar 12, 2026
9b43363
Use separate support function hints for pose 0 and pose 1
claude Mar 12, 2026
4bda014
Cache GJK result guess for CastHullShape pairs
claude Mar 12, 2026
6ec3e73
Normalize collision cache pair ordering and clamp cc_time
claude Mar 13, 2026
da83855
Add unit tests for collision cache pair normalization
claude Mar 13, 2026
83af5de
Fix code quality issues from branch review
claude Mar 13, 2026
77a5b5c
Expand octree collision test coverage for Coal
claude Mar 13, 2026
5b09c45
Fix Coal octomap-octomap test failures for BOX subtype
claude Mar 13, 2026
277c609
Fix tesseract_collision paths
rjoomen Mar 13, 2026
dc36dec
Make octomap-octomap test expected distances explicit per subtype
claude Mar 13, 2026
d9dc7e3
Fix Coal cast shape_id mapping for expanded proxies; add generic cros…
rjoomen Mar 13, 2026
fd9f110
feat(collision): expand Coal coverage and promote octomap usage tests…
rjoomen Mar 13, 2026
dc32210
Fix Coal shape/subshape ids for expanded continuous proxies
rjoomen Mar 13, 2026
701a66c
Fix clang-tidy CI, clang-format CI
rjoomen Mar 16, 2026
2123e46
Fix review issues: include ordering, test helpers, and default params
claude Mar 17, 2026
ec64bd3
Keep active links updated when adding/removing collision objects
claude Mar 17, 2026
b135989
Remove auto-add-to-active logic from addCollisionObject
claude Mar 17, 2026
e051741
Add CLAUDE.md with handover instructions and architecture overview
claude Mar 18, 2026
25dafa0
Update REVIEW.md to reflect issues fixed since initial review
claude Mar 18, 2026
9531e5e
Clean up REVIEW.md: remove resolved and non-issues
claude Mar 18, 2026
8899c1c
Add CCD field assertions to octree cast unit tests
claude Mar 18, 2026
3ed4bd4
Fix octree cast test assertions to match Bullet reference
claude Mar 18, 2026
2ccb8e2
Accept both CCType_Time1 and CCType_Between in octree cast assertions
claude Mar 18, 2026
159be8c
Document CCType_Time0/Time1 gap for pure-translational sweeps in Coal
claude Mar 18, 2026
ceee7e3
Use link-frame world supports in populateContinuousCollisionFields
claude Mar 18, 2026
ede67a6
Fix cc_time projection to use per-shape centre, not link centre
claude Mar 18, 2026
2fc6959
Add missing cast test assertions: distance<0, nearest_points_local, n…
claude Mar 18, 2026
6ea7d4b
Close ContactResult test coverage gaps: CCType_Time0/Time1, type_id, …
claude Mar 18, 2026
ac95de6
Fix CCType_Time1 nearest_points_local and relax GJK noise tolerances
claude Mar 18, 2026
d057492
Relax nearest_points tolerances for Bullet GJK noise in cctype test
claude Mar 18, 2026
6ffc505
Fix octree cast collision detection and align cc_time with Trajopt paper
rjoomen Mar 18, 2026
65abfb1
Reduce duplication and fix packaging in collision coal
rjoomen Mar 18, 2026
2c81efa
Prune geometry cache on manager clone
rjoomen Mar 18, 2026
6512ed5
Remove collision_distance_threshold override for parity with other ba…
rjoomen Mar 19, 2026
93d24b4
Use raw OcTree for static octrees and default to StaticFilter
rjoomen Mar 20, 2026
5db6c25
Unify GJK settings and add Coal-vs-Bullet cast comparison test
rjoomen Mar 20, 2026
f506faa
Refactor managers for structural consistency and remove dead code
rjoomen Mar 20, 2026
1a27d74
Move coal_utils inline implementations to .cpp and simplify removeObj…
rjoomen Mar 20, 2026
7848151
Unify GJK settings, enable warm-start cache, and add cache invalidation
rjoomen Mar 23, 2026
decc11d
Clang-format
rjoomen Mar 23, 2026
414c3d0
Add explicit GEOM_CUSTOM opt-in and Nesterov heuristic delegation to …
rjoomen Mar 23, 2026
74bc9be
Update some unit tests with FCL exceptions, confirming Coal matches B…
rjoomen Mar 24, 2026
8f3c251
Preserve collision cache on GJK guess invalidation
rjoomen Mar 27, 2026
afeaf5f
Removed TESSERACT_COLLISION_COAL_ENABLE_COAL_CAST_TESTS build flag, a…
rjoomen Mar 27, 2026
afa585f
Replace GJK cache scanning with generation-counter invalidation
rjoomen Mar 31, 2026
8128c26
Optimize octree expansion
rjoomen Mar 31, 2026
5e3236f
Defer octree voxel expansion to promotion time
rjoomen Mar 31, 2026
777ced5
Add d-arc compensation and minor collision callback optimization
rjoomen Mar 31, 2026
2e75a35
Eliminate per-contactTest CollisionMarginData copy
rjoomen Apr 1, 2026
d7b6e5b
Reuse ShapeSupportData in CastHullShape to avoid per-call allocation
rjoomen Apr 1, 2026
1b01930
Use refit-based broadphase update when most dynamic objects moved
rjoomen Apr 1, 2026
e87b38a
Simplify cast clone and add full-refit path for static broadphase upd…
rjoomen Apr 8, 2026
cd165af
Preserve clean rotation matrices in setCollisionObjectsTransform
rjoomen Apr 8, 2026
105af88
Optimize collision manager initialization and margin updates
rjoomen Apr 8, 2026
f01f176
clang-format
rjoomen Apr 8, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
docs/build
CMakeFiles/
build/
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Coal-based collision checking plugin for the Tesseract motion planning framework. Implements discrete and continuous (swept/cast) collision detection using the Coal library, as an alternative to the existing Bullet and FCL backends. Architecture closely mirrors Tesseract's Bullet collision implementation.

The reference Bullet and FCL implementations live in `src/tesseract/tesseract_collision/`.

## Coal fork requirement

Continuous collision detection requires `GEOM_CUSTOM` support in Coal for the `CastHullShape` class. The workspace's `src/coal/` fork provides this. Without it, Coal's narrowphase dispatcher won't recognize custom shapes.

## Plugin configuration

Two factory classes (`CoalDiscreteBVHManagerFactory`, `CoalCastBVHManagerFactory`) registered via macros in `coal_factories.cpp`. Optional YAML config keys:
- `gjk_guess_threshold` (both factories, default 5mm)
- `d_arc_compensation` (cast factory only, default false) — arc-sagitta swept-sphere inflation for rotational motions

## Test organization

1. **Test suite headers** (`test_suite/include/`) — reusable `.hpp` headers shared across collision backends.
2. **Integration tests** (`test/*.cpp`) — instantiate Coal managers and call test suite `runTest()` functions.
3. **Coal-specific unit tests** (`test/coal/`) — internal component tests (CastHullShape, caching, shape conversion, etc.).

## Commit conventions

Follow the existing style: imperative mood, concise subject line explaining the "why", body for details when needed.
228 changes: 228 additions & 0 deletions D_ARC_COMPENSATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# d_arc compensation — correcting convex hull underestimation under rotation

## Overview

When CastHullShape checks continuous collision, it computes the signed distance between an obstacle and the convex hull of a shape at two consecutive poses. This is exact for pure translation, but when the shape also rotates, points trace **circular arcs** while the convex hull connects them with **straight chords**. The gap — the arc-chord sagitta, d_arc — can exceed the collision safety margin, allowing the true swept arc to penetrate obstacles that the convex hull reports as safe.

The Coal cast manager implements **d_arc compensation**: on every transform update, it computes d_arc per shape and applies it as the CastHullShape's swept sphere radius. Coal's GJK automatically subtracts this from reported distances, and the broadphase AABB inflates accordingly. This is transparent to all callers. Disabled by default; enable via:

```yaml
plugins:
CoalCastBVHManager:
class: CoalCastBVHManagerFactory
config:
d_arc_compensation: true
```

## Background

Schulman et al. (IJRR 2014, Section IV-B) quantify the gap as `d_arc = r·φ²/8`, where r is the maximum distance from any point on the shape to the rotation axis and φ is the rotation angle. The paper dismisses this as "well under 1 cm" for their applications. However, with a 1 mm collision margin, d_arc can easily exceed the safety margin (see Practical impact section below).

## Mathematical proof

### Setup

Consider shape A undergoing a rigid-body motion from time t to t+1. By Chasles' theorem, any rigid motion can be decomposed into a screw motion: rotation by angle φ around a unique axis k̂ (the screw axis) plus translation along k̂. Each point on A traces a **helix** around this screw axis.

Focus on a single point **p** on A at perpendicular distance r from the screw axis. The translation along k̂ is captured exactly by the convex hull (linear interpolation between start and end), so only the rotational component contributes to d_arc. Project onto the plane perpendicular to k̂. In this plane, p traces a circular arc of radius r from angle 0 to angle φ.

### Arc vs chord gap (the sagitta)

The chord connects the start position p₀ = (r, 0) to the end position p₁ = (r cos φ, r sin φ).

The midpoint of the arc is at angle φ/2: p_mid = (r cos(φ/2), r sin(φ/2)), at distance r from the origin.

The midpoint of the chord is:
```
M = (p₀ + p₁)/2 = (r(1 + cos φ)/2, r sin φ/2)
```

Distance of M from the origin:
```
|M|² = r²(1 + cos φ)²/4 + r² sin²φ/4
= r²/4 · ((1 + cos φ)² + sin²φ)
= r²/4 · (2 + 2 cos φ)
= r²/2 · (1 + cos φ)
```

Using the half-angle identity `1 + cos φ = 2 cos²(φ/2)`:
```
|M| = r · cos(φ/2)
```

The **sagitta** (maximum perpendicular distance from chord to arc) is:
```
s = r - r·cos(φ/2) = r·(1 - cos(φ/2))
```

This is the maximum because the circular arc is symmetric about the perpendicular bisector of the chord. The gap is zero at both endpoints and increases monotonically toward the midpoint, where the arc point (r cos(φ/2), r sin(φ/2)) lies on the perpendicular bisector at distance r from the origin, while the chord midpoint M lies at distance r·cos(φ/2).

Taylor-expanding `cos(φ/2) = 1 - φ²/8 + φ⁴/384 - ...`:
```
s = r·φ²/8 - r·φ⁴/384 + ...
```

Therefore **d_arc = r·φ²/8** is a second-order approximation of the exact gap r·(1 - cos(φ/2)).

### Why this is a tight upper bound

For any point p on shape A at perpendicular distance r_p ≤ r from the screw axis, the gap for that point is r_p·(1 - cos(φ/2)) ≤ r·(1 - cos(φ/2)). So expanding the convex hull by d_arc = r·(1 - cos(φ/2)) via Minkowski sum with a ball of that radius guarantees the swept volume is contained: every point p(t) on the swept arc is within d_arc of its chord, and every chord is a subset of convhull(A(0), A(1)), so p(t) is within d_arc of convhull(A(0), A(1)).

The bound is tight for a single point at distance exactly r from the screw axis (achieved at the arc midpoint). For the full shape, the Minkowski ball expansion inflates uniformly in all directions while the arc bulge is directional. This non-directionality is inherent in the signed-distance formulation: signed distance is a scalar, so d_arc can only be applied uniformly. The paper uses the same approach (Eq. 25e: `sd ≥ d_safe + d_arc`).

In a multi-timestep trajectory, the over-expansion at sweep endpoints is typically covered by the adjacent interval's convex hull. However, at the trajectory start and end, the over-expansion is not covered — if the task requires approaching an obstacle up to exactly the safety margin at the final pose, d_arc inflation at the endpoint might prevent this.

### Exact vs approximate formula

| φ (deg) | φ (rad) | Exact: r·(1 - cos(φ/2)) | Approx: r·φ²/8 | Relative error |
|---------|---------|--------------------------|-----------------|----------------|
| 5 | 0.0873 | 0.000952r | 0.000952r | 0.02% |
| 10 | 0.1745 | 0.00381r | 0.00381r | 0.06% |
| 20 | 0.3491 | 0.01519r | 0.01524r | 0.3% |
| 45 | 0.7854 | 0.07612r | 0.07711r | 1.3% |
| 90 | 1.5708 | 0.2929r | 0.3084r | 5.3% |

The paper uses the r·φ²/8 approximation for analytical clarity (to show d_arc is O(φ²)). For implementation, **use the exact formula r·(1 - cos(φ/2))**:
- Always correct, valid for any φ
- Tight (equals the maximum gap exactly, no overestimation)
- Negligible cost per shape per timestep (see trig-free evaluation below)

The approximation is only useful for symbolic/gradient analysis where φ² is easier to differentiate than 1 - cos(φ/2).

### Trig-free evaluation via half-angle identities

The exact formula can be evaluated without any trigonometric function calls by exploiting the rotation matrix trace and half-angle identities:

1. **Rotation angle from matrix trace** (no `acos`):
```
cos(φ) = (tr(R) - 1) / 2
```
This is just three additions on the matrix diagonal.

2. **Half-angle values from cos(φ)** (no `cos`/`sin`, just `sqrt`):
```
cos(φ/2) = √((1 + cos(φ)) / 2)
sin(φ/2) = √((1 - cos(φ)) / 2)
```
These follow directly from the half-angle identities cos²(x) = (1 + cos(2x))/2.

3. **Sagitta factor** is then simply `1 - cos(φ/2)`, already computed.

4. **Rotation axis from skew-symmetric part of R** (no `atan2`):
```
k = [R(2,1)-R(1,2), R(0,2)-R(2,0), R(1,0)-R(0,1)] / (2·sin(φ))
```
where `sin(φ) = 2·sin(φ/2)·cos(φ/2)`, both already computed.

5. **Screw axis position** (closest point to origin in shape-local frame):
```
t_perp = t - (t·k)k
c = t_perp/2 + (k × t_perp)·cos(φ/2) / (2·sin(φ/2))
```
The `cos(φ/2)/sin(φ/2)` ratio reuses the values from step 2.

6. **r_max**: distance from shape bounding sphere center to screw axis, plus bounding radius.

Total cost: 2 `sqrt` calls, 1 division, a few dot/cross products, and 1 `norm`. No `acos`, `cos`, `sin`, or `atan2`. This is ~60 flops, comparable to the `inverseTimes` matrix operation already performed per shape in the transform update path.

**Numerical stability:** When φ < ~10⁻⁷ rad (cos(φ) ≈ 1), d_arc is below double-precision noise and the function returns 0 immediately. When φ ≈ π, the axis extraction from the skew-symmetric part becomes noisy (sin(φ) → 0), but d_arc is dominated by the sagitta factor ≈ 1.0 and r_max, both stable. Per-segment rotations near π should not occur in practice.

## Practical impact with 1 mm collision margin

Typical industrial robot parameters:

| Scenario | r (m) | φ (deg) | d_arc (mm) | vs 1mm margin |
|----------|-------|---------|------------|---------------|
| End-effector, slow motion | 0.5 | 5° | 0.48 | 48% of margin |
| End-effector, moderate | 0.5 | 10° | 1.90 | **190%** |
| End-effector, fast | 0.5 | 20° | 7.60 | **760%** |
| Long reach, moderate | 1.0 | 10° | 3.81 | **381%** |
| Wrist link, fast | 0.1 | 30° | 3.41 | **341%** |

All values computed with exact formula: d_arc = r·(1 - cos(φ/2)).

**At 10° rotation per timestep with r = 0.5 m, d_arc is already nearly 2 mm — twice a 1 mm safety margin.** The LVS thresholds section below shows that staying under 1 mm d_arc at this reach requires ≤ 7.2° per segment. Since `collision_margin_buffer` is d_check (broadphase query range), not d_safe, there is **zero implicit buffer** covering d_arc. The convex hull check can report `distance ≥ margin` (constraint satisfied) while the true swept arc penetrates the obstacle.

## Implementation

### How d_arc compensation works

`collectCastTransformUpdate()` in `coal_cast_managers.cpp` computes d_arc per shape using the trig-free method (see below), then calls `CastHullShape::setSweptSphereRadius(d_arc)` before `updateCastTransform()`. This has two effects:

1. **Broadphase**: `computeLocalAABB()` inflates the swept-volume AABB by d_arc in all directions, ensuring the broadphase tree covers the arc, not just the chord.
2. **Narrowphase**: Coal's GJK stores the swept sphere radius in `MinkowskiDiff::swept_sphere_radius` and subtracts it from the reported distance post-convergence. The collision constraint effectively becomes `distance ≥ margin + d_arc` without any change to callers.

**Swept sphere interaction**: CastHullShape's `computeShapeSupport()` uses `WithSweptSphere` mode for the underlying shape's intrinsic radius (e.g., sphere, capsule). CastHullShape's own swept sphere radius (d_arc) is a second, independent inflation layer — no double-counting. In a self-collision pair (two CastHullShapes), Coal sums both radii: d_arc₁ + d_arc₂, which is correct since each link's arc gap is independent.

### CastHullShape support function (unchanged)

`coal_casthullshape.cpp` — `computeShapeSupport()` implements the Schulman convex-hull support function: it evaluates the underlying shape's support at both pose 0 and pose 1, and returns whichever has a larger dot product with the query direction. This computes `sd(convhull(A(t), A(t+1)), B)`. The d_arc compensation is applied externally via the swept sphere radius, not by modifying the support function.

### Bullet's btCastHullShape

Same Schulman support function. d_arc is not compensated in Bullet.

### TrajOpt's collision_margin_buffer — d_check, NOT d_safe

The code comment says "Additional collision margin that is added for the collision check but is not used when calculating the error" (`collision_types.h:153-154`). Tracing the code confirms this:

1. **d_safe = margin** — the constraint enforces `distance ≥ margin` (`continuous_collision_constraint.cpp:120`, `collision_utils.cpp:182`)
2. **d_check = margin + margin_buffer** — controls broadphase AABB inflation and GJK `distance_upper_bound` (`continuous_collision_evaluators.cpp:96`)
3. **Gradient weighting**: `error_with_buffer = margin + margin_buffer - distance` is used only for weighting which contacts contribute most to the gradient (`weighted_average_methods.cpp:50`)

**The buffer does NOT increase the safety margin.** Without d_arc compensation enabled, the collision constraint has no rotational correction term.

## LVS thresholds required for d_arc ≤ 1 mm

Translation does not contribute to d_arc — the convex hull captures translational motion exactly (see Setup above). Only the per-segment rotation φ matters. From the exact formula, the maximum rotation per segment that keeps d_arc ≤ 1 mm is:

```
φ_max = 2 · arccos(1 - 0.001 / r)
```

| r (m) | Typical scenario | φ_max (deg) | φ_max (rad) |
|-------|----------------------|-------------|-------------|
| 0.1 | Wrist / small link | 16.2° | 0.283 |
| 0.2 | Forearm link | 11.5° | 0.200 |
| 0.3 | Mid-arm | 9.3° | 0.163 |
| 0.5 | End-effector reach | 7.2° | 0.126 |
| 0.8 | Long arm | 5.7° | 0.100 |
| 1.0 | Full reach | 5.1° | 0.089 |

For a typical 6-DOF industrial arm with ~1 m reach, **~5° per segment** keeps d_arc under 1 mm for all links. A 90° joint motion would require ~18 subdivisions.

Note that r is per-shape, not per-joint: it is the maximum distance from any point on a collision shape to the screw axis of the motion, which depends on the shape's bounding radius and its offset from the screw axis. For multi-DOF motion the screw axis of the composed rotation is configuration-dependent, so the worst case uses the robot's maximum reach.

### TrajOpt does not implement rotation-aware LVS

TrajOpt's collision evaluators (both SCO and IFOPT) subdivide segments using only the **joint-space L2 norm**, not Cartesian translation or rotation:

```cpp
// continuous_collision_evaluators.cpp:174, collision_terms.cpp:840
const double dist = (dof_vals1 - dof_vals0).norm();
long cnt = std::ceil(dist / longest_valid_segment_length) + 1;
```

This norm is blind to Cartesian geometry: a small joint-space step on a long link can produce large rotation (and large d_arc), while a large joint-space step on a short wrist link may produce negligible Cartesian motion.

Tesseract's **SimplePlanner LVS profiles** do evaluate translation, rotation, and joint-space distance separately, taking the maximum:

```cpp
// interpolation.cpp:781
int trans_steps = int(trans_dist / translation_lvs_length) + 1; // default 0.1 m
int rot_steps = int(rot_dist / rotation_lvs_length) + 1; // default 5°
int joint_steps = int(joint_dist / state_lvs_length) + 1; // default 5°
steps = std::max({trans_steps, rot_steps, joint_steps});
```

However, this only generates the **seed trajectory**. Once TrajOpt starts optimizing, it moves waypoints freely and uses its own joint-space LVS for collision sub-stepping. The seed's fine subdivision has no effect after the first optimization iteration reshapes the trajectory.

**Without d_arc compensation**, the gap is doubly unguarded: no correction in the collision check, and no rotation-aware subdivision in the optimizer. Enabling `d_arc_compensation: true` on the cast manager addresses the collision check side, which works regardless of the planner or optimizer's subdivision strategy.

## Alternative approach (not implemented)

- **More timesteps**: d_arc ∝ φ², so doubling timesteps (halving φ) reduces d_arc by 4×. However, TrajOpt's `longest_valid_segment_length` uses joint-space norm, not Cartesian rotation, so it cannot automatically enforce the rotation thresholds from the table above. A rotation-aware LVS in the optimizer would be needed. Also increases optimization cost linearly.

## References

- Schulman, J., Duan, Y., Ho, J., Lee, A., Awwal, I., Bradlow, H., Pan, J., Patil, S., Goldberg, K., & Abbeel, P. (2014). Motion Planning with Sequential Convex Optimization and Convex Collision Checking. *International Journal of Robotics Research*, 33(9), 1251-1270.
4 changes: 2 additions & 2 deletions dependencies.repos
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- git:
local-name: coal
uri: https://github.com/coal-library/coal.git
version: devel
uri: https://github.com/rjoomen/coal.git
version: copilot/review-custom-cast-shape-support
4 changes: 2 additions & 2 deletions dependencies_focal.repos
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
version: catkin
- git:
local-name: coal
uri: https://github.com/coal-library/coal.git
version: devel
uri: https://github.com/rjoomen/coal.git
version: copilot/review-custom-cast-shape-support
4 changes: 2 additions & 2 deletions dependencies_with_ext.repos
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
version: catkin
- git:
local-name: coal
uri: https://github.com/coal-library/coal.git
version: devel
uri: https://github.com/rjoomen/coal.git
version: copilot/review-custom-cast-shape-support
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ find_package(Eigen3 REQUIRED)
find_package(Boost REQUIRED COMPONENTS system program_options)
find_package(octomap REQUIRED)
find_package(console_bridge REQUIRED)
find_package(tesseract_geometry REQUIRED)
find_package(tesseract_common REQUIRED)
find_package(tesseract REQUIRED COMPONENTS common geometry)
find_package(boost_plugin_loader REQUIRED)
find_package(yaml-cpp REQUIRED)
find_package(cereal REQUIRED)
Expand Down Expand Up @@ -61,15 +60,6 @@ if(TESSERACT_BUILD_COAL)
add_subdirectory(coal)
endif()

# Unit test options
option(TESSERACT_COLLISION_COAL_ENABLE_COAL_CAST_TESTS "Enable CoalCastBVHManager unit tests" OFF)

# Add compiler definition to core so it can find all plugins produced string( REPLACE ";" ":"
# CONTACT_MANAGERS_PLUGINS_STRING
# "${CONTACT_MANAGERS_PLUGINS}")
# target_compile_definitions(${PROJECT_NAME}_core PRIVATE
# TESSERACT_CONTACT_MANAGERS_PLUGINS="${CONTACT_MANAGERS_PLUGINS_STRING}")

# Add compiler definition to coal so it can find all plugins produced
string(
REPLACE ";"
Expand All @@ -82,7 +72,6 @@ target_compile_definitions(${PROJECT_NAME}_coal
# Testing
if((TESSERACT_ENABLE_TESTING OR TESSERACT_COLLISION_ENABLE_TESTING) AND TESSERACT_BUILD_COAL)
enable_testing()
add_run_tests_target(ENABLE ${TESSERACT_ENABLE_RUN_TESTING})
add_subdirectory(test)
endif()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ find_package(octomap REQUIRED)
find_package(coal REQUIRED)

find_package(boost_plugin_loader REQUIRED)
find_package(tesseract_collision)
find_package(tesseract_geometry)
find_package(tesseract REQUIRED COMPONENTS collision geometry)

# Create target for Coal implementation
add_library(
${PROJECT_NAME}_coal
src/coal_cast_managers.cpp
src/coal_casthullshape_utility.cpp
src/coal_casthullshape.cpp
src/coal_collision_geometry_cache.cpp
src/coal_collision_object_wrapper.cpp
Expand All @@ -23,8 +21,8 @@ target_link_libraries(
console_bridge::console_bridge
octomap
octomath
tesseract::tesseract_collision_core
tesseract::tesseract_geometry)
tesseract::collision
tesseract::geometry)
target_compile_options(${PROJECT_NAME}_coal PRIVATE ${TESSERACT_COMPILE_OPTIONS_PRIVATE})
target_compile_options(${PROJECT_NAME}_coal PUBLIC ${TESSERACT_COMPILE_OPTIONS_PUBLIC})
target_compile_definitions(${PROJECT_NAME}_coal PUBLIC ${TESSERACT_COMPILE_DEFINITIONS})
Expand Down Expand Up @@ -82,7 +80,7 @@ configure_component(
COMPONENT coal
NAMESPACE tesseract
TARGETS ${PROJECT_NAME}_coal
DEPENDENCIES "tesseract_collision COMPONENTS core" "coal")
DEPENDENCIES "tesseract COMPONENTS collision" "coal")

if(TESSERACT_PACKAGE)
cpack_component(
Expand Down
Loading
Loading