|
| 1 | +# ADR-0015: Collision checking in capsules, with an allowed-collision set that is not optional |
| 2 | + |
| 3 | +- **Status**: Accepted |
| 4 | +- **Date**: 2026-09-10 |
| 5 | +- **Deciders**: Onur Can Urhan |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +WP-12d bundled three things: full time-optimal path parameterisation, CUDA |
| 10 | +batch inverse kinematics, and collision checking. Only the third is here, and |
| 11 | +the other two are open for reasons worth writing down rather than leaving to be |
| 12 | +inferred from an empty directory. |
| 13 | + |
| 14 | +**CUDA cannot be verified.** GitHub's runners have no GPU, and every claim this |
| 15 | +library makes is currently gated by something that runs on every pull request. |
| 16 | +A component whose correctness rested on a machine under a desk would be the |
| 17 | +first exception, and the first exception is how a project stops being able to |
| 18 | +say its tests mean anything. |
| 19 | + |
| 20 | +**Full TOPP conflicts with a decision already made.** Classical TOPP optimises |
| 21 | +in the phase plane of the path parameter and its rate, subject to velocity and |
| 22 | +acceleration. It has **no jerk limit** -- and this library's README calls jerk |
| 23 | +"the limit that distinguishes an S-curve from a trapezoid, and the one that |
| 24 | +decides whether the mechanics ring". Shipping a planner that quietly dropped it |
| 25 | +in exchange for a shorter cycle time would contradict |
| 26 | +[ADR-0006](0006-jerk-limited-profiles-and-a-single-path-parameter.md). Adding |
| 27 | +jerk to TOPP is a substantially harder problem than TOPP, not a refinement of |
| 28 | +it, and pretending otherwise in a commit message would be the worst of the |
| 29 | +options. |
| 30 | + |
| 31 | +Collision checking has none of those problems. It is exactly testable, needs no |
| 32 | +hardware, and answers a question the Cartesian planner already half-answers: |
| 33 | +`least_manipulability` reports how close a move came to a singularity, and |
| 34 | +nothing reported how close it came to the fence. |
| 35 | + |
| 36 | +## Decision 1: everything is a capsule |
| 37 | + |
| 38 | +A link, a fence post, a fixture, a cable: all of them a segment with a radius. |
| 39 | + |
| 40 | +The distance between two capsules is the distance between two segments minus |
| 41 | +their radii. That is **one function with no special cases and no orientation to |
| 42 | +track**, and it is the entire geometry in the file. Boxes need separating-axis |
| 43 | +tests and an orientation each; meshes need a broad phase before they are |
| 44 | +affordable at all. |
| 45 | + |
| 46 | +The cost is fidelity, and it is a real cost. A link that is not very |
| 47 | +capsule-shaped is covered generously, so the model refuses moves that would |
| 48 | +have been fine. That is the right direction to be wrong in, and it is cheaper to |
| 49 | +fix by adding a second capsule than by adopting a mesh. |
| 50 | + |
| 51 | +Capsules are given **in the base frame with every joint at zero**, the same |
| 52 | +frame joints and inertias use, so all three can be read off one CAD model in one |
| 53 | +pose. [ADR-0008](0008-kinematics-by-screws-and-a-damped-inverse.md) explains why |
| 54 | +this library keeps asking for that frame and no other. |
| 55 | + |
| 56 | +## Decision 2: skipping adjacent links is not enough, and believing it is breaks the model |
| 57 | + |
| 58 | +Two links sharing a joint touch at every configuration, so a self-collision |
| 59 | +check that includes them reports a collision always. Skipping adjacent pairs is |
| 60 | +obvious and universal. |
| 61 | + |
| 62 | +It is also insufficient, and the insufficiency is not obvious. A spherical wrist |
| 63 | +turns its last three links about nearly one point, so links **two** apart |
| 64 | +overlap there at every configuration as well. A model with only the adjacent |
| 65 | +rule reports a collision while the arm is parked. |
| 66 | + |
| 67 | +That failure has a predictable ending. Nobody debugs a collision checker that |
| 68 | +fires on a stationary machine; they switch collision checking off, and then the |
| 69 | +cell has none. So `build` takes the pairs to ignore -- the same |
| 70 | +allowed-collision set a real cell keeps beside its geometry -- and the example |
| 71 | +model ships with the three wrist pairs already excluded. The test that matters |
| 72 | +most in the file is the dull one asserting the example arm reports positive |
| 73 | +clearance while standing still. |
| 74 | + |
| 75 | +An exclusion naming a link that does not exist is **refused**, not dropped. |
| 76 | +Somebody wrote it meaning something, and a model that silently ignores it |
| 77 | +believes it is safer than it is. |
| 78 | + |
| 79 | +## Decision 3: self-clearance and obstacle clearance are reported apart |
| 80 | + |
| 81 | +`clearance()` returns the closest pair overall, and beside it `to_self` and |
| 82 | +`to_obstacles`. |
| 83 | + |
| 84 | +Returning only the overall minimum was the first design, and the tests killed |
| 85 | +it. A straight arm's own links sit a fixed distance apart -- **0.22 m** on the |
| 86 | +example, between the upper arm and the wrist -- so the overall minimum reports |
| 87 | +0.22 no matter where an obstacle is, until the obstacle comes nearer than the |
| 88 | +arm is to itself. An obstacle 30 cm away is then completely invisible to |
| 89 | +somebody asking about obstacles. |
| 90 | + |
| 91 | +That is not a bug in the arithmetic; the overall minimum was exactly the overall |
| 92 | +minimum. It is a question answered so literally that it stopped being useful. |
| 93 | +The two numbers cost one extra comparison each and answer the two questions |
| 94 | +people actually ask. |
| 95 | + |
| 96 | +## Decision 4: distances are signed |
| 97 | + |
| 98 | +Negative means overlapping, by the depth of the overlap, rather than clamped at |
| 99 | +zero. "Touching" and "driven 40 mm through" call for different responses, and a |
| 100 | +caller who only wants a yes or no compares against zero. |
| 101 | + |
| 102 | +When there is genuinely nothing to measure -- no obstacles, every link pair |
| 103 | +adjacent or excluded -- the answer is `kNothingNear`, a large finite number. |
| 104 | +Infinity is the honest answer and a terrible one to hand back, because it |
| 105 | +survives arithmetic that should have failed and reappears as a NaN much later, |
| 106 | +somewhere else. |
| 107 | + |
| 108 | +## Consequences |
| 109 | + |
| 110 | +- A clearance query costs **432 ns** for six links, three obstacles and nine |
| 111 | + self pairs -- twenty-seven capsule distances, 0.043% of a millisecond cycle, |
| 112 | + allocating nothing. It can therefore run as a gate on every setpoint rather |
| 113 | + than as a planning-time check, which is the difference between catching a bad |
| 114 | + move and catching a bad *command*. |
| 115 | +- The model is per-configuration. Nothing here checks the swept volume between |
| 116 | + two configurations, so a step large enough to pass through a thin obstacle |
| 117 | + passes through it undetected. Following a plan means sampling it densely |
| 118 | + enough, and a test does exactly that along a Cartesian move. |
| 119 | +- Six links of capsules is a coarse robot. It will refuse poses a real machine |
| 120 | + could hold. That is stated rather than hidden, and the fix is more capsules. |
| 121 | +- Nothing is wired into `CartesianPlan` automatically. Planning does not take |
| 122 | + obstacles and does not report clearance, because the obstacle set changes far |
| 123 | + more often than the arm does and baking it into the planner would mean |
| 124 | + replanning to answer a question about a fence that moved. |
| 125 | + |
| 126 | +## Alternatives considered |
| 127 | + |
| 128 | +**Spheres only.** Simpler still -- distance between two spheres is one |
| 129 | +subtraction -- and a link needs five or six of them to be covered as well as one |
| 130 | +capsule covers it. The segment-segment distance is thirty lines and pays for |
| 131 | +itself immediately. |
| 132 | + |
| 133 | +**A full mesh with a broad phase.** What a production cell uses, and correct in |
| 134 | +a way capsules never are. It is also a bounding-volume hierarchy, a mesh loader |
| 135 | +and a format decision, none of which this library has any business owning. |
| 136 | + |
| 137 | +**Reporting every colliding pair rather than the closest.** A caller deciding |
| 138 | +whether to move needs one number. A caller diagnosing why gets the pair that |
| 139 | +produced it, which in practice is the one being argued about anyway. |
| 140 | + |
| 141 | +**Checking swept volumes between configurations.** The honest fix for the gap |
| 142 | +named above, and it needs a continuous-collision formulation -- conservative |
| 143 | +advancement, or a bound on how far the geometry moves per unit of path. Both are |
| 144 | +larger than this whole file. |
0 commit comments