-
Notifications
You must be signed in to change notification settings - Fork 3
fix(sim,hal,evidence): repair the four instruments the collision programme measures with, and measure the resolution lever end to end - #251
Conversation
After a month of collision work, completion went from 25% (2026年08月26日) to 5-10% (2026年09月06日) and nobody had ever measured what the policy achieves WITHOUT the gate. The validation harness refuses to -- `no_enable_octomap_ kernel_check` is on `_SAFETY_KNOB_PATTERNS`, correctly for a validation round, and that is exactly why the number was never taken. The survey quoted the external version of this experiment (PACS, arXiv:2511.06385 Table I: unfiltered 0.70 vs binary-filtered 0.04) and never asked for the in-tree one. Measured on spark, 88 valid runs, 4 scenes x 2 arms, 10-12 per cell, same commit and host, both arms running simultaneously so contention loads onto each equally: world-voxel gate OFF 14/45 31.1% world-voxel gate ON 1/43 2.3% Fisher p = 3.5e-04, power 0.97 Per scene: utensil 58% vs 0% (p=0.005), fridge 45% vs 0% (p=0.035), sink_cup 18% vs 9%, baguette 0% vs 0%. So two scenes are almost entirely kernel-bound, sink_cup is mixed, and **baguette is policy-bound and cannot report on collision work at all** -- it should leave the scorecard. This is a CEILING, not a configuration: it never lands in a scene file, a launch default or a manifest, the harness's refusal is untouched, and 6 of 91 stops in the #204 battery were real contact. The number says how much headroom the levers are competing for: up to 29 points, concentrated in the payload class. `tools/_ceiling_probe.py` deliberately does not go through the validation harness; it reuses that harness's own `materialise_scene`, readiness gate and dispatch tool so the only difference between arms is the gate flag, verified in the launch argv as `enable_octomap_kernel_check:=false`. THREE DEFECTS had to be fixed before the number was trustworthy, each of which would have produced a confidently wrong answer, and all are recorded in PLAN.md §7: 1. an uncaught `subprocess.TimeoutExpired` killed whole workers rather than single rounds, leaving the arms SCENE-CONFOUNDED (gate-off had run mostly fridge, which completes; gate-on mostly utensil, which then never did) -- the interim 4/17 vs 1/20 was an artifact of scene composition; 2. `SidecarClient` reaps the sidecar IT spawned on exit, so the first crashed worker took the shared sidecar down and every later run was policy-free (30-85 s instead of 600+). Fixed structurally with a keeper process; 3. policy-free runs must be excluded by reading each run's own goal log -- 14 of 102 runs were dropped that way. Also recorded: `openral deploy sim` cannot run concurrently with itself, because `_kill_orphan_openral_graph_processes()` matches by argv signature and cannot tell a concurrent sibling from a crashed orphan. Parallel workers need `OPENRAL_SKIP_ORPHAN_REAP=1`, which is deliberately NOT committed as a default. PLAN.md carries the failure analysis this all came from: median true clearance at the moment of a kernel stop is 20.1 mm, 85 of 91 stops were of a robot that was physically clear, and 71% of stops are the carried payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 17fc145)
...asurement The ceiling entry is the durable record: `outputs/` is gitignored, so the 88 runs behind 31.1 % vs 2.3 % would otherwise vanish the way the 2026年08月26日 battery's artifacts already have. Also records two lever findings that change what to build next, both measured rather than argued: * **Voxel resolution is struck.** `OccupancyVoxels.occupancy` is a dense `uint8[]` and the per-link window is `O(1/res^3)`, so halving the cell is an 8x check cost. 15 mm is 26.7 ms estimated against a 33 ms budget and a 2.80 MB message (4.6x the 614,125 cap); 12.5 mm is 46 ms. 20 mm fits but buys 4.4 mm of a 20.1 mm excess. Poor return. * **Modeled fixtures is the lever instead.** 51 of 70 payload stops are against anonymous `voxel_` cells whose certified nearest body is a static kitchen fixture MuJoCo already knows exactly -- `counter_1_right` 25 times, `fridgesidebyside_main` 9, `counter_1_left` 8. The robot carries an object over a counter and the counter's cubes stop it at 20 mm of air. That is the survey's own §9 point 1, and #200 already built the machinery for the declared place target; this generalises it to the fixture the payload is near. And one scene finding: **`baguette` should leave the collision scorecard.** It is 0 % with the gate off, so it is policy-bound and cannot report on collision work either way -- despite four of the five completions in this ledger's whole history being baguette runs, which is what made it look like the bellwether. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 0da3e4d)
`quality` runs `mypy --strict tools/`, which the new probe failed two ways: `validation_matrix` is a sibling script imported by path (no stub), and the two `type: ignore`s written for a typed SceneSpec were unused once that import resolved to Any. Types the parameter as `Any` with the reason inline rather than scattering ignores, per CLAUDE.md §2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 36a9d2b)
`tests/sim/safety/test_kernel_latency_soak.py` is the kernel's only latency test and it publishes **no** `OccupancyVoxels` at all — it runs a synthetic `soak_test` envelope with no `collision_geometry`, so the staged 26-DOP -> hull narrow phase never executes. Its pass is vacuous for any change to that phase, which is the kernel's dominant cost: the shipped benchmark puts the seven link windows at 10 475 cells and ~5.8 ms against ~8.8 us on an empty grid. Same class of hole #183 found in the Nav2 live tests, and it is what made the `link3`/`link4`/`link6` change in this branch unmeasurable. The new test goes in the fridge pin file because that is the only place in the tree with a REAL grid: a real RoboCasa kitchen rasterised cell by cell, the real manifest (so all seven links lower their tight geometry), the real kernel binary, at `world_voxel_margin_m = 0.0` — the value `panda_mobile` runs. Measured on q-laptop, 200 chunks over 5 638 occupied cells: median 0.1 ms p99 2.0 ms target 30 ms, hard ceiling 33 ms So the narrow phase with seven hulls sits 15x under the chunk budget, which answers the latency question this branch's manifest change raises on the shipped configuration rather than by extrapolating the one-off benchmark table. It also asserts >=90% of chunks come back: a kernel dropping under a real grid would be a worse finding than a slow p99, and a p99 over a truncated sample would hide it. Mutation-checked by forcing the budget to 0.001 ms, which is how the 2.0 / 0.1 numbers above were read out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit e0e2e0f)
Both live in OpenRAL/management#33. Entry 026 is the record #235 owes; ADR-0101 is the remaining lever, proposed before code because it crosses Layer 2 -> Layer 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 6406ac1)
...he stop `voxel_backing_record` is the instrument that answers "what, if anything, is really in the cell the kernel stopped on". `mj_ray` reports only the NEAREST strike, and the probe took it — so a non-collidable shell in front of the collidable surface it wraps was the only thing it ever saw, and the cell was adjudicated `noncollidable_world`, i.e. "the map disagrees with the world". Measured, not theorised. On the 2026年09月06日 battery, **6 of the 8 stops that carried a backing record at all** came back `noncollidable_world` naming `counter_1_right_group_top_visual`, while the certified nearest COLLISION surface at those same stops was ~16 mm away -- inside the same 25 mm cell. The map was right and the diagnostic was wrong. That matters beyond one number. Since #180 the depth cast makes exactly these geoms transparent, so in sim decoration can no longer become occupancy at all; a `noncollidable_world` verdict is now a statement about the probe or a stale cell, not a live map defect. The class docstring still carried the pre-#180 justification ("the depth synth strikes these too, so they CAN become occupancy"), which is what made the misattribution look plausible. Both the docstring and the METHODS entry are corrected. A ray that strikes a non-collidable geom inside the cube is now re-cast from just past it, up to `_VOXEL_BACKING_MAX_LAYERS` (4) times, and BOTH the shell and whatever it hides are recorded. The existing precedence then does the rest: `solid_world` outranks `noncollidable_world`, so the cell reads as explained by real geometry, while a cell with genuinely nothing solid behind the decoration still reads `noncollidable_world`. Nothing is filtered away -- dropping the shell would hide a real map defect where one exists. Diagnostics only (CLAUDE.md §1.4): no stop is suppressed, delayed or altered. How tested: `tests/unit/test_sim_estop_voxel_backing.py` gains a RoboCasa-shaped fixture -- a collidable slab wearing a non-collidable shell, both inside ONE cell, shell nearer the ray start -- and a test that the cell reads `solid_world` with both geoms named. Mutation-checked: reverting to first-strike-only fails it with `noncollidable_world`. 15 pass in that file, 28 across the E-stop evidence suite; `mypy --strict -p openral_hal` clean; methods markers refreshed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit c7bd2c7)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 2997c01)
The reconstructed layout-47 grid goes 5 638 -> 7 427 occupied cells once the probe stops blaming decoration for cells whose solid geometry is behind it -- landing between #224's two brackets (5 638 solid-only, 9 217 counting all decoration) exactly as it should. Every clearance number derived from that grid was computed against a map ~32% too sparse. Also records the gap the investigation surfaced: the backing record was present on only 8 of 91 stops, so the diagnostic that says what the map contains is absent from 91% of the stops it exists to explain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit f42d3d3)
...vidence An earlier draft of the 2026年09月07日 probe entry said the backing record was present on only 8 of 91 stops. Wrong: it is present on 82. The 74 extra live in run_gt_evidence.json, which is #177's LATE path. But that path is unusable on this battery, and its distribution is a trap -- 46 of 74 read 'unbacked', which looks like the kernel stopping on cells nothing backs. It is instead exactly the defect 10ff989 describes: the late path omitted grid_orientation_xyzw, took identity, and decoded a cube metres from the stopping link, reporting unbacked with 27 rays cast and 0 hits. 10ff989 landed 2026年09月05日 and NONE of the battery's commits (all 2026年09月04日) carry it. Kept as a visible correction rather than a silent edit, because the wrong version was a more exciting finding than the right one -- which is the specific way this ledger has been burned before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com> (cherry picked from commit 51c8e2e)
Every scene of every `tools/validation_matrix.py` round on post-#231 `master` reported `harness-error` — "action server never appeared" — beside a graph that was up and healthy the whole time. Two independent defects, each sufficient on its own, both measured on `q-laptop` against a live `robocasa_drawer_utensil` round. 1. `_launch_env` did not apply the sim DDS scope. Since #227/#231 `openral deploy sim` confines itself with `confine_sim_scope` (`ROS_DOMAIN_ID=77`, `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`) so a simulation and a real robot cannot share a graph. The deploy applied that to itself; the harness did not, and polled domain 0. `ROS_DOMAIN_ID=77 ros2 action list` showed `/openral/execute_rskill` the whole time; unscoped showed nothing. `confine_sim_scope` now runs inside `_launch_env` — applied on this side rather than left to the child, because it uses `setdefault`, so the deploy inherits the harness's value instead of choosing its own and the two agree by construction. An operator who exports their own scope still wins on both. 2. `ros2 action list --no-daemon` cannot discover an advertised action. The poll passed it for a real hazard: a daemon left over from an unscoped shell answers from the environment *it* started with — the false reading that made `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` look broken in #227. But the one-shot node it builds has a discovery window too short to see an action that is genuinely up: against a live graph, `ros2 action list` found it and `--no-daemon` did not, on the same domain, repeatably. The poll could never succeed on any scope. Replaced with a one-shot `ros2 daemon stop` under the round's own `env` before the loop, which closes the original hazard from the other side: the daemon the loop then uses is started by that call, on that scope. The same round that had reported `harness-error` twice completed with a real outcome (`utensil`, `deadline-no-grasp`) on the first attempt after both fixes. `docs/reference/collision-validation-evidence.md` gains the 2026年09月07日 entry, including what it invalidates: any round taken on post-#231 master before today measures the harness, not the kernel. Rounds on earlier commits — the ceiling battery's `80027b18` arm among them — predate #231 and are unaffected; checked, not assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 7735608)
...ord both launch failures The §7 checklist had drifted from §5's 2026年09月07日 reordering and still listed two levers as open that §5 had already struck by measurement: - "Lever 1: the payload bounding box" kept the pre-reordering numbering. The payload is still 71 % of stops, but its primitives are already tight to −1.5 mm beyond the voxel term, so there is no payload-geometry headroom. The class is the right target; the mechanism that reaches it is ADR-0101's modeled fixtures, not a tighter payload box. Tracked there. - "Lever 3: voxel resolution 25 → 15 mm" is struck on measured cost: a dense `uint8[]` occupancy and an `O(1/res3)` window make halving the cell an ×ばつ check cost — 26.7 ms at 15 mm against a 33 ms ceiling. Both now marked struck with the number that struck them, so the checklist and §5 say the same thing. Adds the two launch failures found today, which are distinct and were being conflated: - the **harness** could not see the graph it launched, and had not since #231 — wrong DDS scope plus a `--no-daemon` poll that cannot discover an advertised action at all. This is why post-#231 rounds looked like launch failures, and it is a precondition for every open measurement below it. - the **launch parser** ran under the system interpreter, so `dist-packages` shadowed the venv and `import pandas` aborted the whole launch on a Jetson AGX Thor. That is the spark-side failure, fixed on its own branch. Closes the `baguette` scorecard item: 0/11 with the gate off means policy-bound, so it leaves the completion scorecard while staying in the matrix. Already recorded in the ceiling entry; the plan now points at it rather than restating it. `collision-validation-evidence.md` gains standing caveat 10 — the citation rule for post-#231 rounds — because that is what the caveats list is for. Re-derivation of ADR-0101's 94 % against the post-fix live map is unblocked: the foreign 1.9 GB GPU process is gone (175 MiB of 8151 in use). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit e7956f7)
...ator honest ADR-0101 cites "48 of 51 payload-vs-`voxel_` stops (94 %) recovered" as the measurement that justifies building the first **fail-open** mechanism in the hazard log. That number was computed offline, by hand, and had no producer in the repo — which is exactly the shape of claim `docs/reference/collision-validation-evidence.md` exists to prevent. `tools/adr0101_recovery.py` is that producer. The counterfactual it evaluates needs no kernel code and no layer crossing: for a payload stop against an anonymous cell, "would a modeled fixture have let this through?" is the same question as "was the payload certifiably clear of the real surface?", and the battery already records both halves. Pure, offline, stdlib-only, like `tools/round_power.py`. Two decisions carry the safety direction, and both are tested: - **Zero is contact, not clearance.** A payload touching a fixture is stopped by the modeled body exactly as it was by the cube. Putting `0.0` on the clearance side would count real contacts as recoveries — the one class the mechanism must never suppress. Mutation-checked: flipping `>` to `>=` fails the test. - **Exclusions are reported, never dropped.** A recovery rate is only as honest as the set it divides by, and the two errors are not symmetric: silently dropping an unadjudicable stop shrinks the denominator and *inflates* the rate. `recovery_rate` is `None` over an empty set and `render` refuses to print a percentage rather than showing 0 % or 100 %. Tested against the real `2026年08月23日-master-s1` round, which recorded exactly the stop shape the tool selects but predates the probe's distance attestation (standing caveat 8) — so it must land in `excluded`, by name and with a reason. It does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 60dcb2f)
...t link The first cut of `tools/adr0101_recovery.py` read the by-fixture breakdown off `ground_truth.nearest_pair`. That field records the closest probed pair *of any kind*, and for a carried payload it is routinely two robot links — on the round this was caught with, `robot0_link3` vs `robot0_link4` at −36.3 mm, while the payload itself sat 24.9 mm clear of a counter. So the tool put `robot0_link4` into a table of kitchen fixtures: a robot link presented as a static world body, inside the record that argues for modelling static world bodies. The body behind `nearest_tripping_party_m` is recorded in exactly one place — the raw `sim.estop_ground_truth_snapshot` line's `nearest_payload_world_pairs` — so `fixture_at_stop` reads it there and verifies the match rather than assuming it: that list's minimum certified distance must equal the gap the stop was adjudicated on, both being the same probe call. If they disagree the snapshot describes some other stop and no attribution is made. The recovery *count* never depended on this and does not now; only the breakdown did. Verified on the live round: the fixture is `counter_1_right_group_main`, which is `counter_1_right` — the body ADR-0101 §3 already names as the top fixture at 25 of 70 payload stops. The premise reproduces independently. Adds the round as a fixture (one snapshot line plus `verdicts.json`, provenance in `SOURCE.txt`) and three tests, the first of which is a regression test for this defect. Mutation-checked: pointing the reader at `nearest_link_link_pairs` fails it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit a0f5f64)
...decoration `2026年09月07日-adr0101-live-1` is the first validation-matrix round since #231 to reach a real outcome rather than `harness-error`. It confirms ADR-0101's certified premise directly: the payload was stopped at −4.05 mm reported while sitting +24.86 mm clear of `counter_1_right_group_main` — the exact fixture the ADR names as its largest class. It also turned up something the ADR does not account for. The backing probe — with the decoration-walking fix verified live in the running process — reports the tripping cell as `noncollidable_world`, backed solely by `counter_1_right_group_top_visual`. Within that 25 mm cube there is no collidable geometry at all; the collision slab is the 24.9 mm away that the certified probe independently measured. That is the occupancy grid faithfully recording what a depth sensor sees, which is the visual shell, not the collision body. It cuts both ways for ADR-0101's suppression bound: if a modeled fixture publishes collision primitives, a cell like this is *not* geometrically explained by them and the stop survives — so the 94 % would be optimistic; if it publishes the visual geometry instead, the mechanism suppresses against a surface the kernel does not protect. Benign in sim, where a non-collidable geom cannot be hit; not benign on hardware, where what the sensor sees is what the robot hits. That is the sim→real seam the ADR already flags, now with an instance instead of a caveat. Recorded at n=1 and labelled as such. A 12-round batch is running to measure how often a payload stop is backed by decoration alone; nothing here revises the 94 % in either direction yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 6bed917)
...eading The 2026年09月07日 entry interpreted a cell backed only by a non-collidable geom as the occupancy grid mapping the visual surface while the collision body sat behind it. RoboCasa's asset code does not support that reading. `robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual` box (`group=1`, `contype=0`) and then chunks the *same* volume into collidable geoms via `_get_chunks`, which tile it exactly — identical `pos[1]`, `pos[2]`, identical `size[1]`, `size[2]`, `x` tiling the full span. Visual and collision are coincident by construction, so there is no offset to be proud by. What remains is a real three-way tension: the backing probe finds no collidable geom in the cell after re-casting past decoration; the certified probe puts the nearest collidable geom of the same body 24.86 mm away; the asset code says they are coincident. All three cannot hold. The leading candidate is a residual defect in the re-cast itself: it advances "just past" a strike, which steps over a *coincident* collidable twin and lands outside the cube. The fix was built for decoration in front of a slab, not decoration sharing its surface. This is left open and labelled, not resolved, because it changes what the programme is optimising: if the instrument is wrong, the 20.1 mm payload excess ADR-0101 is sized against is itself suspect. Resolving it needs a direct query of the live model at the stop, which no current artifact records. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit b76aa0e)
...-contact' #220 (on master since 2026年09月05日) gave the HAL a link-vs-link probe so a self stop could be scored against the pair the kernel named. That part is right, and it is what makes standing caveat 9 closeable. But the new pairs were folded into the adjudicator's `nearest_any`, which drives its first and most decisive rule: any probed pair at or below 0 m is `real-contact`. Adjacent robot links overlap permanently. They are in the robot's allowed-collision matrix and the kernel never checks them. So from #220 onward `nearest_any <= 0` was vacuously true and every adjudicable stop was stamped `real-contact`, whatever the tripping party's actual clearance. Measured on `2026年09月07日-adr0101-live-1`: `robot0_link3`/`link4` at -36.3 mm, `link5`/`link6` at -23.0 mm, `link4`/`link5` at -4.6 mm — all certified, all permitted, none of them what the kernel stopped for — while the carried payload it did stop for sat +24.86 mm clear of the counter. `nearest_any` now excludes `nearest_link_link_pairs` wholesale and adds back only the pair the kernel named, which `party_pairs` already isolates in the `self_pair` branch. A genuine link-vs-link self stop in real overlap is still detected as contact; a permitted overlap two joints away is not. Re-derived over the four stops recorded today, three move `real-contact -> within-quantization` and the one true contact (-2.32 mm) is preserved. Verdicts are pure and offline, so affected rounds re-adjudicate without re-running. Two tests, both mutation-checked against restoring `+ link_link`. Standing caveat 11 records the citation rule: no `real-contact` verdict from 2026年09月05日 to 2026年09月07日 is safe to cite. The error runs one way — it manufactures real contacts, never clears one — so nothing was wrongly passed as safe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 483a82e)
...ear robot Thirteen rounds on q-laptop (utensil and fridge, seeds 1-7), run after both harness fixes and re-adjudicated offline after the `nearest_any` fix. First battery on this page where harness and adjudicator were both known-good at reading time; three rounds changed verdict when re-derived. Seven stops. Five (71 %) were of a physically clear robot, at +0.67, +11.13, +22.01, +23.13 and +24.86 mm true clearance. Two were real contact, at -2.32 and -0.11 mm. The 71 % reproduces the #204 battery's 85-of-91 on an independent battery, a different commit and a repaired instrument — the number has not moved. Four stops are the carried payload, splitting evenly clear/contact; `adr0101_recovery` reports 2 of 4 recovered, median 17.99 mm, minimum 11.13 mm. Far below the offline 94 %, but n=4 does not contradict it and no revision is claimed. Two findings the levers do not cover: - Three of seven stops are `estop-initial-configuration` — the arm stopped at reset by its own start pose, before doing anything. tight_geometry, modeled fixtures and voxel resolution all address the carry phase; none addresses a base placement that starts the arm inside a counter. One is at +0.67 mm and would survive any geometry work. - Five of thirteen rounds never grasped at all. With the ceiling result, roughly half of what reads as collision-programme failure on these scenes is the policy not reaching the phase where the kernel matters. One round (fridge seed 6) completed with the gate on, against a 2.3 % gate-on rate in the ceiling battery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 9676c93)
...ever covers The false-positive rate re-measured on a working harness and a working adjudicator is 71 % (5 of 7 stops of a physically clear robot), reproducing the #204 battery on an independent battery and a different commit. The headline number has not moved. Also records the #220 adjudicator inversion that made that measurement possible to get wrong for two days, and adds two open items the §5 levers do not touch: - start-state collisions, a third of all stops, where the arm is stopped at reset by its own pose before doing anything; - rounds that never grasp at all, five of thirteen, which bound how much of the scorecard any collision work can move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit c1e2203)
...he link fix PLAN.md §5's table is the measurement that struck two collision levers and promoted a third: per stop class, the kernel's over-approximation with the grid's 21.65 mm half-diagonal subtracted. Like ADR-0101's 94 %, it was computed by hand and had no producer. `tools/stop_excess.py` is that producer. Two guards, both tested. Only stops whose probe certified its distances are counted — an uncertified distance can be wrong by 15-108 mm, larger than the quantity being measured. And the half-diagonal comes from each round's own recorded `grid_resolution_m`; a stop without one is skipped rather than defaulted to 25 mm, since assuming it would not perturb the answer but replace it. `has_geometry_headroom` is strict at zero: a class exactly at the voxel term cannot recover a millimetre, so calling that headroom would license wasted work. Run over the 13-round adr0101-live battery it gives the first live read on whether the tight_geometry work did what it was designed to do: payload n=4 median excess +13.15 mm beyond voxel -8.50 mm link n=3 median excess +25.51 mm beyond voxel +3.86 mm The #204 battery measured the link class at +33.1 mm beyond voxel, with panda_link6 holding 18 of its 29 link stops. link6 now ships tight geometry and the class reads +3.86 mm. n=3 and a different link mix, so this is consistent with the fix working rather than proof of it — but it is the first live evidence in that direction, and it is what PR #235 was missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 551b7c5)
...shell `c7bd2c7` taught the probe to walk past a non-collidable strike and cast again, which finds a collidable slab BEHIND a decoration shell. It cannot find one coincident with it, and RoboCasa builds every counter top that way: `robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual` (`contype=0`) and then tiles the same volume with collidable chunks via `_get_chunks`. Their surfaces are the same plane, so stepping `distance + eps` past the shell lands inside the chunk, where the ray reports no further entry surface and the chunk is never seen. That is what produced the three-way contradiction recorded this morning. It is now settled from the round's own certified witness points: the cell spans z in [0.90005, 0.92505] and the collidable chunk `counter_1_right_group_top_0` has its surface at z = 0.920. The solid geometry was inside the cell the whole time; the certified probe and the asset code agreed and only the backing probe was wrong. `voxel_backing_record` now falls back to a world-AABB overlap sweep over collidable geoms when, and only when, the rays found nothing solid. An AABB overlap can claim a geom whose surface misses the cube, so it is deliberately a supplement rather than a replacement — for a diagnostic whose failure mode is calling real geometry "decoration" (#180), erring toward found is the right direction, and it can never override a ray pass that already found something. Reproduced with a coincident shell/chunk fixture that yields the same 9-of-27 ray signature as the live round; mutation-checked by forcing the sweep off. Diagnostics only (CLAUDE.md §1.4) — the certified probe measures geom-to-geom distance and never used rays, so the 71 % rate and the stop decomposition are unaffected. What moves is the backing CLASS of cells previously read `noncollidable_world`, which is what the "32 % too sparse" entry and ADR-0101's "cells no real body explains" premise rest on. Both should be re-derived from a post-fix round before being leaned on further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 10a3469)
...t worth it Three closures from the 13-round battery and its decomposition. The probe contradiction was the instrument again: the cell spans z in [0.90005, 0.92505] and the collidable chunk's surface is at z = 0.920, so the solid geometry was inside it all along. Fixed; diagnostics only. panda_link1 is the only link without a stage-2 hull (1588 vertices against a 320 cap) and caused two of three start-state stops, which made it look like the next obvious manifest edit. Measured, those stops are +3.86 and +8.68 mm beyond the voxel term, so an exact hull recovers at most ~9 mm of a ~25 mm error while raising the cap 5x is a hot-path change needing safety-WG review. Struck. And the conclusion those two produce together: the link class now measures +3.86 mm beyond voxel, down from +33.1 mm in the #204 battery whose link stops were 18-of-29 panda_link6 — the link that now ships tight geometry. With the payload class at -8.50 mm, both are at or below the grid term. Every remaining millimetre of over-approximation is the 25 mm voxel grid, and refining that was struck on measured cost. No tighter envelope anywhere can recover anything further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 1d116ae)
...aust memory `hull_overhang_m` fed every barycentric sample to `trimesh.proximity.closest_point_naive` in one call. That query allocates a `samples x mesh-faces x 3` array, and the docstring's own premise — "a few thousand by a few hundred here" — holds only for the small panda links that have ever reached it. `panda_link1` is 6260 mesh vertices and ~12k triangles; a 320-vertex hull over it is ~636 facets at 325 samples each, so the single call asks for 57.8 GiB and raises `numpy._core._exceptions._ArrayMemoryError`. Latent until something declared a hull for that link. Batching at 512 samples per call bounds peak memory by `512 x faces x 3` independently of link size, and the maximum over batches is the maximum over the whole set, so the returned number is unchanged. `check` still reports mesh-outside-DOP +0.000000000 mm on all seven links. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit e8e0731)
...over budget `derive_tight_geometry` had two outcomes for a link: ship the exact convex hull, or -- if it exceeds `MAX_TIGHT_HULL_VERTICES` -- fall back to the 26-DOP alone. `panda_link1` is the second case (1588 vertices), and its DOP's support gap against the real mesh is a median 4.52 mm and up to 25.68 mm. `refine_dop_to_budget` builds a third thing: the DOP intersected with the exact hull's own face planes, worst-violation first, stopping before the vertex count would exceed the budget. Every candidate plane is tangent to `conv(mesh)`, so containment stays *definitional* rather than fitted, and the result is `mesh ⊆ result ⊆ DOP ⊆ box` at every step. A subset-then-expand construction cannot promise that: expansion pushes vertices out through the DOP slabs, and link1's DOP has 0.083 mm of room inside its manifest box. The routine refuses rather than emit an envelope that cuts its mesh. On link1's real mesh it reaches a support gap of 0.18 mm median / 0.65 mm max against the DOP's 4.52 / 25.68 mm, at 320 vertices. **No manifest declares a refined envelope, and this commit changes none.** The envelope was generated and put under a live battery on 2026年09月07日; the stops it was predicted to clear moved by 0.0003 mm. The prediction failed because it read a support-census deficit against the shipped *box* and attributed all of it to the hull, when the DOP had already collected it. That refutation is recorded in the docstring, in `docs/methods/10-tools.md` and in the evidence page, so the next reader does not re-derive the same wrong expectation from a routine that looks like it was built for a reason nobody wrote down. Which is also why the test is a direct one rather than a manifest assertion: an unexercised generator path rots. It pins the three properties a safety-WG reviewer would otherwise re-derive by hand -- mesh inside the result, result inside the DOP, and strictly less enclosed volume than the DOP it refines -- against the real robosuite mesh, not a fixture. Also carries the memory bound `_OVERHANG_BATCH` / `_OVERHANG_MAX_SAMPLES` needs to document: the single-call `hull_overhang_m` form asks for 57.8 GiB on a 320-vertex envelope over a 12k-triangle mesh. How tested: `pytest tests/unit/test_collision_tight_geometry.py` (11.5 s for the new case, inside the 30 s unit budget). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev>
...ust the call The earlier entry struck link1's hull because it "recovers at most ~9 mm of a ~25 mm error". That is the wrong question: what matters is whether the recovery flips the stop, and the start-state census's deficit table says 10 mm clears 14 of 14 link1 states. It also assumed the only options were the DOP or an over-budget hull, and that a kernel cap change would be needed. Neither held — the refined envelope fits the existing 320-vertex budget. Records the measured result (0.18 / 0.65 mm against the DOP's 4.52 / 25.68 mm, p99 0.5 ms on 9891 cells) and downgrades the start-state item from "no lever touches it" to "one lever now reaches it, unconfirmed": two of its three stops were link1, predicted to clear but not yet observed in a post-change battery. The third is a genuine +0.67 mm near-contact that no geometry work reaches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 7fcc716)
... says so Eight rounds were run to confirm that panda_link1's refined envelope clears the two panda_link1 start-state stops. All eight died at 16-19 s with the XR-1 sidecar OOMing at boot: a concurrent job on this shared host held 2.0-2.5 GB of 7.53 GiB while the sidecar needs ~3.5 GB alongside the scene. Recorded rather than discarded because the failure mode is exactly the shape of the result being looked for. Every round shows `stop: null`, and "seeds 2 and 4 no longer produce estop-initial-configuration" is precisely what the envelope predicts — but the policy never loaded, so the arm never moved and no kernel check ran. Reading it as confirmation would be reading a crash as a measurement. The prediction stands unconfirmed. Confirming it needs an uncontended window here or a host with headroom; spark is the latter, with the standing caveat that XR-1 has never completed an end-to-end rollout on GB10, so one smoke round must succeed before a battery is worth running there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit f4a670f)
...t was built for Three rounds on spark, utensil seeds 2/3/4, on the commit carrying panda_link1's refined envelope — also the first end-to-end XR-1 rollout completed on GB10. The prediction is refuted. Same seed, same scene, same stop, one commit apart: seed 2 reads -2.37794 mm under the 26-DOP and -2.37825 mm under the refined envelope; seed 4 reads -8.31 and -8.31495. Tightening link1 from a 25.68 mm worst-case support gap to 0.65 mm moved the reported depth by 0.0003 mm. The reasoning error is identifiable. The start-state census's "10 mm clears 14 of 14 link1 states" is computed with box_box_distance against the manifest OBB. The 26-DOP shipped after that census and already collected that recovery (53.27 -> 25.69 mm). Treating the census's OBB-relative deficit as still- available headroom double-counted a tightening that had already landed. This is therefore the controlled test that the geometry levers are exhausted: the same stop under two envelopes differing by 25 mm of worst-case looseness, moving 0.0003 mm. Notes the untested hypothesis for the residual: the octomap bridge's README records that a published grid can report a surface up to one full tree resolution (25 mm) nearer than it is, and stop_excess.py subtracts only the 21.65 mm half-diagonal — so map inflation reads as geometry excess and invites exactly the hunt this entry closes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit b55f90a)
...got a test Corrects the link1 item twice over. I struck the lever for the wrong reason (fraction of error recovered rather than whether it flips the stop), then un-struck it for another wrong one: the census's "10 mm clears 14/14 link1 states" is computed against the manifest OBB, and the 26-DOP shipped later had already collected that recovery. Three rounds on spark measured the same stops under both envelopes and they moved 0.0003 mm. That failure upgrades the exhaustion conclusion from an inference over a decomposition to a controlled test: the same stop under two envelopes differing by 25 mm of worst-case support gap does not move. Whatever the residual is, it is not the collision model. Start-state goes back to "no lever reaches it" — it is a base-placement question, and the census shows joints 3-7 cannot change a start-state verdict at all when link1 or link2 dominates, which is 83.3% of stopping states. Adds the hypothesis that displaces the geometry hunt: octomap marks the cell containing the ray endpoint, so the grid can report a surface up to a full 25 mm tree resolution nearer than it is, and stop_excess.py subtracts only the 21.65 mm half-diagonal. Untested, and the first thing worth measuring next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 7d90a81)
estop-initial-configuration was three of seven stops in the adr0101-live battery and the class no lever in PLAN.md §5 addresses. This is the first map-side reading of it. Where the evidence lives matters and is counter-intuitive: a start-state snapshot carries collision_evidence: null and evidence_voxel_backing: null, because the E-stop fires before the kernel's safety.collision line reaches the bridge and the freshness gate correctly refuses to attribute a cell. The record arrives on the deferred sim.estop_ground_truth_evidence path instead (run_gt_evidence.json, backing_after_snapshot_ns: 0). Reading only the snapshot shows nothing and invites the conclusion that the class is un-diagnosable. Three stops, three causes: - utensil-s2 is textbook quantisation. The cell CONTAINS the true nearest surface point of the door backing it (0.00 mm), so the map is where the world is; +23.13 mm clear, reported -2.38 mm, excess 25.51 = 21.65 half-diagonal + 3.86 mm. This also refutes the octomap-inflation hypothesis raised earlier the same day: an inflated cell would not contain the surface. - fridge-s2 has the robot in the cell and no world geometry at all — 15 of 27 rays struck robot0_link2_collision, the same link the kernel stopped. - utensil-s4 shows the same signature on weaker evidence: its robot geoms come from the conservative AABB sweep, not rays (0/27). The same stop read `unbacked` on q-laptop before that fix landed, so the fix turned a blank into a diagnosis. Records that the mechanism is NOT a missing exclusion — the self-filter passes robot bodies to the synth, matches by prefix plus descendants, and marks transparent rays so they clear rather than mark — and names the decisive test that no artifact supports today: re-run the layout with the arm parked elsewhere and see whether the cell persists. That needs a start-pose override the harness lacks. n=3 and self_occupancy_suspect is explicitly not conclusive alone, so this is logged as a lead, not a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit d63d6ad)
...s the robot `self_occupancy_suspect` was resting on 27 rays having missed nothing. A cell whose only collidable ray hit is a robot body produces an identical record whether world geometry is absent or merely unsampled, and that distinction is exactly what separates a self-occupancy stop from ordinary quantisation. Measured on the 2026年09月07日 `fridge-s2` start-state stop: 15 of 27 rays struck `robot0_link2_collision` — the same link the kernel stopped — and no world geom appeared. Nothing in the record said whether one was there, so the stop could not be classified either way. The AABB overlap sweep now runs when the rays found no collidable **world** geometry, rather than only when they found nothing collidable at all. That covers the original coincident-shell case unchanged and adds the robot-only case. A cell whose world backing the rays already found is still left alone, so this cannot change a verdict the ray pass got right. Diagnostics only (CLAUDE.md §1.4). Verified on the fixture: a cell on real world geometry reports `swept=False`, a cell holding only the robot reports `swept=True` and `self_occupancy_suspect`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 448818c)
The widened sweep answers the ambiguity the previous entry named. Both candidate self-occupancy stops were re-run on spark at that commit, and both resolve against the lead: fridge-s2's cell contains the fridge drawer (fridgesidebyside_main_group_1_g96 — the same body its near-miss pair named at +0.673 mm), and utensil-s4's contains the cabinet door. The 27 ray fans had simply missed them. The robot geoms in both cells are real but incidental — the arm is beside the surface, not instead of it. So all three start-state stops are ordinary voxel quantisation against correctly-mapped world geometry. Not self-occupancy, not map inflation (utensil-s2's cell contains the true surface point at 0.00 mm), and not link envelope conservatism — the panda_link1 envelope moved these same stops by 0.0003 mm the same day. Two of the three are stops of a demonstrably clear robot (+23.13, +22.01 mm). fridge-s2 is separated out as a genuine near-contact at +0.67 mm that no reduction in map conservatism should clear. This settles the last population with an unexplored root cause, and it has the same one as the payload class: the 25 mm grid. It therefore has the same single remaining lever, and ADR-0101 is currently scoped to the carried payload only — extending it to bare links would cover both classes with one mechanism. A scope observation for the WG, not a decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 115e7b2)
...both refuted Closes the last population with an unexplored root cause. Three start-state stops read map-side for the first time; map inflation and self-occupancy were each raised as causes and each refuted by measurement, as was the link envelope. The self-occupancy refutation is the substantive one: fridge-s2 looked like the robot in its own map (15/27 rays on robot0_link2_collision, no world geom), and widening the backing sweep then re-running on spark found the fridge drawer in that cell — the same body its near-miss pair named at +0.673 mm. So the class is ordinary voxel quantisation, with the same single lever as the payload class. ADR-0101 is scoped to the carried payload; extending it to bare links would cover both with one mechanism. Recorded as a scope note for the WG, not a decision. Separates fridge-s2 as a genuine near-contact at +0.67 mm that no reduction in map conservatism should clear. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev> (cherry picked from commit 49b4197)
...s nowhere 33 commits and 4 822 lines is over CLAUDE.md §4.2.5's ceiling, and it stopped being one logical change some days ago. Split by what gates each piece: * **A** (this branch) — the four instrument repairs, the two evidence producers, `refine_dop_to_budget`, the ceiling probe, the narrow-phase latency surface, PLAN.md and the evidence ledger. Touches no manifest and neither `packages/openral_safety/` nor `cpp/` (empty `git diff --stat` against both), so §1.4 applies and §3 does not. * **B** `feat/216-tight-geometry-link3-4-6` — the three manifest envelopes, safety-WG gated on hazard-log Entry 026. * **C** — `panda_link1`'s envelope, withdrawn. The tool is in A; the manifest is nowhere. A is the one with a deadline. `master` has carried two of the four defects since 2026年09月05日 — a harness that cannot discover the action server it launches, and an adjudicator that stamps every stop `real-contact` off a permitted adjacent-link overlap — and both corrupt the programme's primary measurement. Every round taken on `master` since then is unusable, so nothing downstream can be measured until this lands. That is the whole argument for splitting rather than waiting for the WG on one PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev>
`tools/refresh_methods_linenos.py --check` was reporting 46 stale `(LNN)` markers across `01-hal.md` and `10-tools.md` -- the mechanical consequence of the backing-probe repairs and the new generator routine shifting line numbers in files those pages index. CLAUDE.md §4.4 requires the check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev>
...was wrong by 32x
PLAN.md §5 struck the 25 -> 15 mm lever on cost: 26.7 ms estimated against a
33 ms ceiling. It was the only lever in the programme struck on paper rather
than by test. Measured on the real kernel under a real grid, at four resolutions
over the same volume, 200 chunks each:
| resolution | occupied | p99 | estimate |
| ---: | ---: | ---: | ---: |
| 25 mm (shipped) | 9 891 | 0.517 ms | 5.8 ms |
| 20 mm | 17 321 | 0.597 ms | 11.3 ms |
| 15 mm | 35 828 | 0.825 ms | 26.7 ms |
| 12.5 mm | 59 948 | 0.838 ms | 46.1 ms |
Two errors compounded. The 5.8 ms baseline came from the shipped hull
microbenchmark, not from a round trip under a real grid -- which had no latency
surface until one was built the same day the strike was written; the real
baseline is 0.517 ms. And the cubic factor was applied to the wrong term: the
window loop opens with `if (grid.occupancy[idx] == 0) { continue; }`, so
`O(1/res3)` falls on a branch-not-taken while the real work scales with occupied
cells, which are a surface. Cells x7.65, occupied x6.06, p99 x1.62.
The cap objection fails too: 15 mm is 376 680 cells, under the shipped
`world_voxel_max_cells = 614 125`. §5's 2.8M figure was a whole-kitchen grid,
not the arm-neighbourhood window the kernel scans.
Un-struck, but explicitly **not yet actionable**. This measures the kernel
consuming a grid, not `openral_octomap_bridge` producing one at a finer tree
resolution, which is the other half of the cost and is unmeasured. That is the
next step on this lever, not a manifest edit.
`_RES` in the fridge pin file becomes `OPENRAL_FRIDGE_GRID_RES_M`-overridable so
the sweep reproduces from the shipped test rather than from a probe duplicating
it. The default is unchanged and is what `sim_e2e.launch.py` emits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
CLAUDE.md §1.13 requires every public symbol to have a `docs/methods/` entry. `tools/validation_matrix.py:collision_scale_env` has had none since it landed with the #188 graded band. Pre-existing, so it gets its own commit ahead of the change that adds its sibling (§1.15). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev>
... cap from it PLAN.md §5's resolution lever needs a round run at 15 mm to answer whether a finer grid clears stops. Two things were in the way. **The resolution was hardcoded.** `_octomap_resolution` now honours `OPENRAL_OCTOMAP_RESOLUTION_M` in `[0.001, 0.5]`, falling back to the shipped 0.025/0.05 on anything else — a typo must not become a silently empty grid, which is what `build_lattice`'s refusal and `kMaxCellsPerAxis` both produce. Same mechanism as the #188 graded band's `OPENRAL_COLLISION_SCALE_*`, and `validation_matrix.py` records it the same way, because `assert_no_safety_overrides` inspects argv and cannot see an env var. Recorded for the opposite reason to the band's, and this is the part worth stating plainly: **a finer grid is LESS conservative.** The cell half-diagonal is the kernel's quantisation term, so shrinking it makes the kernel stop later and nearer. Nothing here changes a default, and nothing should ship at another value without the measurement and a safety-WG ruling. **The cap was a hand-kept derived constant.** `world_voxel_max_cells` was the literal `614125` with a comment explaining it was 85^3 — the coverage ball at 25 mm plus the lattice snap's one cell per axis. It is now computed by `_world_voxel_max_cells` from the same two inputs. The failure mode when that constant drifts is the wrong direction: a kernel reserving 614 125 cells rejects every 2 803 221-cell grid it is sent, which reads as an empty world and is a fail-**open** on the world check. Pinned at 25 mm -> exactly 614 125, so this refactor provably does not move the shipped graph. How tested: `tests/unit/test_sim_e2e_voxel_resolution.py` (5 cases, skipping cleanly where the launch file's own ROS imports are absent) pins the 25 mm constant, the 141^3 growth at 15 mm, and both directions of the override; two new cases in `test_validation_matrix.py` pin that an honoured override is recorded and an ignored one is not. `just lint` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Adrian <adrian@qualiastudios.dev>
PLAN.md §5 un-struck the 25 -> 15 mm lever after measuring the kernel consuming a finer grid, and named the other half unmeasured: the bridge rebuilds the grid on every publish, and since the published lattice IS the octree's, a finer kernel grid means a finer tree. Measured on the real rasterizer over the existing kitchen octree, which is now parameterised by resolution rather than pinned to 25 mm: 25 mm 512 000 cells, 825 occupied, 0.88 ms 20 mm 1 000 000 cells, 1 380 occupied, 1.29 ms 15 mm 2 406 104 cells, 3 936 occupied, 1.60 ms 12.5 mm refused The producer is not the obstacle: 1.60 ms at 15 mm against a 100 ms publish period, and nearly flat — cells x4.7 for time x1.8, because the marking loop iterates occupied leaves, which are a surface, while only the dense buffer's allocation scales with volume. 12.5 mm is refused by `kMaxCells = 4e6` at 4 096 000 cells. Fail-closed is correct; it also means 12.5 mm needs that guard raised, which is a decision rather than a manifest edit. Pinned so the boundary is recorded rather than discovered. Also corrects the un-strike's cell count, which was mine. It claimed 15 mm needs 376 680 cells against the shipped 614 125 cap and dismissed §5's 2.8 M as a whole-kitchen grid. Both halves were wrong: the coverage ball is sized by the arm's 1016 mm reach and ships at 1.05 m, so 15 mm needs 141^3 = 2 803 221 cells and §5's original figure was right. The cap consequence is real — a kernel still reserving 614 125 rejects every grid, reading as "no world", a fail-open on the world check — which is why `_world_voxel_max_cells` now derives the cap from the resolution. Leaves message size as the only unquantified term on this lever: 2.8 MB per publish at 10 Hz is 28 MB/s over DDS against 6 MB/s today, and neither the transport cost nor its effect on the kernel deadline has been measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
1.60 ms to rasterize a 15 mm grid against a 100 ms publish period, from 0.88 ms at 25 mm — nearly flat, because the marking loop iterates occupied leaves, a surface, while only the dense buffer scales with volume. 12.5 mm is refused by the bridge's own 4e6-cell allocation guard and needs it raised; 15 mm does not. Also corrects this item's own cell count, which I got wrong when un-striking it: 15 mm needs 141^3 = 2 803 221 cells, not 376 680, because the coverage ball is sized by the arm's 1016 mm reach. §5's original 2.8 M figure was right, the cap consequence is real, and it is why the cap is now derived from the resolution rather than hand-kept. Message size is now the only unquantified term on the lever: 2.8 MB per publish at 10 Hz is 28 MB/s over DDS against 6 MB/s today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
The last unquantified term on the 25 -> 15 mm lever. Both compute halves are measured and cheap — kernel p99 0.825 ms, rasterize 1.60 ms — and neither touches the message, which is a dense uint8[] going from 0.61 MB to 2.80 MB per publish at 10 Hz. `tools/voxel_transport_probe.py`: two processes over real DDS, real openral_msgs, at the deployed 10 Hz, under the kernel's own QoS for /openral/world_voxels, grid sizes from the shipped 1.05 m coverage radius. Separate processes deliberately — intra-process publish/subscribe short-circuits the transport being measured. 25 mm 0.61 MB p50 ~14 ms p99 19-23 ms 20 mm 1.19 MB p50 ~22 ms p99 36-43 ms 15 mm 2.80 MB p50 ~29 ms p99 68-83 ms Nothing is dropped and the rate holds: RELIABLE delivered every message at every size and the achieved rate is ~8.6-8.9 Hz throughout, the shortfall being the probe's own sleep loop. The expected failure mode is not there. What is there is staleness. That latency is the age of the world when the kernel reads it, and it triples at 15 mm: +15 ms median, +50-60 ms p99. Age is also millimetres, so it is directly comparable to the 8.66 mm of quantisation the lever buys — break-even end-effector speed is 0.58 m/s at the median and 0.16 m/s at p99, both inside the arm's kinematic range. This does NOT re-strike the lever and must not be read as doing so. It converts a free win into a trade, settled by one unmeasured number: actual carry-phase end-effector speed. At 0.1 m/s the lever is still worth pulling; at 0.5 m/s it is not. Measuring it from the battery's recorded joint states is the next step, and on this lever every number reasoned about rather than measured has been wrong. Fast-DDS, localhost, one shared laptop — the probe reports its RMW because the result does not carry across implementations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
The wire measurement turned the 25 -> 15 mm lever into a trade: +8.66 mm of static quantisation against +15 ms median / +50-60 ms p99 of map age. Age is millimetres too, so the trade is settled by how fast the arm actually is when the kernel stops it. `tools/stop_ee_speed.py` reads each round's own `robot_joint_state` from its ground-truth snapshot and pushes it through the real Panda body Jacobian at link7, the body the payload attaches to: carry phase (attached_payload) n=5 median 0.051 m/s max 0.265 m/s start state (robot_world) n=7 median 0.000 max 0.000 The mobile base contributes at most 0.0013 m/s, so arm speed is world speed here. Start-state stops are free: all seven are at exactly zero, so they take the full 8.66 mm with no offset. That is 43% of the battery's stops. Carry-phase is net positive in three of four corners — +7.89 mm at the median stop and median staleness, +4.68 and +5.84 mm at the single-worst corners, and -5.93 mm only when the fastest observed stop meets a p99-latency grid. So the lever is worth pulling, and this is the first time that has been said about it on evidence rather than an estimate: all three cost terms are now measured and the arm is slow enough that the last one does not eat the gain. Two honest limits recorded with it: n=5 carry-phase stops is thin, and these are the speeds THIS policy produces — a policy carrying at 0.3 m/s would move the p99 corner from marginal to routine. The joint matcher raises rather than returning zero when nothing matches; the first cut of this tool matched no joints and reported a perfectly stationary arm, which is the one failure this measurement must not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
The wire term is the one that bites: publish->receive latency triples from p99 19-23 ms at 25 mm to 68-83 ms at 15 mm, with nothing dropped and the rate held. That is map staleness, and staleness is millimetres. Settled by measuring the arm rather than assuming it. Carry-phase stops are 0.051 m/s median and 0.265 m/s max; start-state stops are exactly zero, because the arm has not moved yet. Net is positive in three of four corners, and the whole start-state class — 43% of stops — takes the full 8.66 mm with no staleness cost at all. All three cost terms are now measured (kernel 0.825 ms, rasterize 1.60 ms, wire +15/+55 ms), which is what makes this the first evidence-based verdict on a lever that has been struck and un-struck three times on estimates. Carries two caveats into the change: n=5 carry-phase stops is thin, and these are this policy's speeds — a faster one moves the p99 corner from marginal to routine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
...run yet The item read as merely unstarted. Two things make it precise. First, scope: the offline 94% rests on certified mesh truth, which no backing-probe defect ever touched, so it stands. What needs re-deriving is the live-map half — the ADR's "cells no real body explains" premise is a backing-CLASS claim, and both backing fixes changed how that class is computed. Second, why existing rounds cannot serve. Only rounds at or after 448818c carry both the coincident-geometry sweep and the robot-only-backing sweep. Exactly two exist and both are robot_world start-state stops. No post-fix round on disk has a payload-vs-voxel stop, which is the only class the 94% is about. Records what a fresh battery would need to look like. And why it was not run today: q-laptop at load 19.1 with a live deploy graph and another session's 2.1 GB on the GPU. The GPU would fit; CPU contention at that load pushes rollouts into the 420 s deadline and manufactures deadline-no-grasp, which corrupts the measurement rather than producing it. Waiting is the correct call, not --force-shared-gpu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
...atteries by it Counted across all 26 rounds by where the round actually ends: utensil reaches the carry phase in 27% of rounds (6/15 never grasp, 5/15 stop at reset), fridge in 18% (7/11 never grasp). The carry phase holds 71% of stops and every stop ADR-0101 targets, so only one round in four says anything about the class the programme is about. This is a measurement-cost finding rather than a collision one, and it explains why n has stayed thin through every battery on this page. It also sizes them: utensil yields 4 payload stops per 15 rounds, so ADR-0101's live-map re-derivation needs about 38 rounds and 6 hours of wall clock. Budgeting by round count rather than by yield is how a battery ends up underpowered, which has happened here before. Two consequences that are not collision work. The start-state class is cheaper to measure than the carry class, so anything testable there should be measured there first. And deadline-no-grasp is the largest lever on measurement throughput while being a policy property — 40-64% of rounds spend seven minutes each to say nothing about collision, and no collision change will ever be observed in them. Also marks #217 done: it closed COMPLETED on 2026年09月06日 with the #204 A/B null at 0.85 power (5.0% vs 10.0%, Fisher p = 0.491), before the item was written here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
The passage naming the consumer measurement's limits listed two terms it could not see — the bridge producing a grid, and the message crossing DDS. Both were measured on 2026年09月08日, so the plan contradicted its own checklist. Replaces them with the results (rasterize 1.60 ms at 15 mm, wire p99 19-23 -> 68-83 ms, staleness settled against measured arm speed) and keeps the two limits that genuinely stand: one pose in one layout, and n=5 carry-phase stops from this policy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
...s exact Launched utensil-weighted at seeds 10-45. The XR-1 sidecar crashed at boot on every round with torch.OutOfMemoryError, producing 20-second policy-free rounds; both were deleted rather than kept, since a policy-free round measures nothing. From the sidecar's own traceback: GPU capacity 7.53 GiB with 24 MiB free. Another project's job (workspace/RAL-1) holds 2.41 GiB, the XR-1 checkpoint needs ~3.66 GiB, and MuJoCo's EGL context plus the deploy graph take the rest. --force-shared-gpu does not create memory. Records the reversal too, because it should not happen a third time: the first read of this correctly said wait, the second overturned it on free-VRAM arithmetic that omitted the renderer and the graph, and the measurement proved the first read right. The battery needs the other GPU job stopped, or spark. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
... clearances halve Thirty-six utensil rounds on spark at 448818c, the sha carrying both backing-probe fixes — the condition the re-derivation required, and one no q-laptop round had ever met. The rate survives: 24/28 (86%) against the offline 48/51 (94%) is Fisher two-sided p = 0.237, no evidence they differ. ADR-0101's headline stands up to its own re-derivation. The clearance distribution does not, and that is the finding. Median recovered clearance is 8.56 mm against the offline 16.2 mm — half. The mechanism would suppress cells whose true surface is twice as close as the offline analysis implied, so its own modelling error eats a correspondingly larger share of the budget. The minimum moved the other way (1.84 mm against 0.1 mm). Two of the four that correctly still stop are inside a tenth of a millimetre. The stop decomposition now rests on n=28 rather than n=4: payload -9.93 mm beyond the voxel term (no geometry headroom), link +4.28 mm. The exhaustion conclusion holds harder than when it was drawn. Also corrects yesterday's yield entry, which was mine and wrong. It measured 18-27% carry-phase yield and 40-64% deadline-no-grasp across 26 q-laptop rounds and read that as a property of the policy. Spark produced ZERO deadline-no-grasp in 36 rounds and 78% payload stops. Same policy, different host: that figure was a 420 s deadline meeting a machine at load 19 with a shared GPU. It follows that deadline-no-grasp should be read as a load symptom first, and that the ceiling battery's policy-free exclusions deserve re-reading in the same light. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
...efuted The re-derivation ran on spark at 448818c, the sha with both backing-probe fixes: 24 of 28 payload stops recovered (86%) against the offline 48/51 (94%), Fisher p = 0.237. The rate survives. The clearance distribution does not: median 8.56 mm against 16.2 mm. The WG should rule on 8.56 mm, which argues harder for the ADR's own suppression-off first landing. The decomposition also moves from n=4 to n=28 and the exhaustion conclusion holds harder. And the carry-phase yield item is refuted by the same battery. It measured 18-27% yield and 40-64% deadline-no-grasp on q-laptop and read that as a policy property; spark produced zero deadline-no-grasp in 36 rounds. There is no scene-selection problem to fix. What stands is that deadline-no-grasp is a load symptom first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
AdrianLlopart
commented
Sep 9, 2026
The ADR-0101 re-derivation this PR's instruments made possible has now runThirty-six
The rate survives — Fisher two-sided p = 0.237, no evidence the two differ. The clearance distribution does not: the median recovered stop sits at half the real air the offline analysis implied, which shrinks the margin ADR-0101's own modelling error has to fit inside. That is a number the safety-WG should have before ruling. Two side effects that bear on this PR directly:
Nothing here changes the diff under review; both corrections are documentation commits on this branch. |
...o rule on Neither item was actionable without a decision record, and neither had one. management#36 supplies both: hazard-log Entry 027 for the 25 -> 15 mm resolution reduction, and ADR-0101 updated with the live-map figures its premise rests on. Entry 027 names the asymmetry the ruling turns on — a deterministic 8.66 mm gain against a probabilistic staleness cost — and states what does not exist: no live A/B of completion rate at 15 mm, which the WG may reasonably require first. ADR-0101 now carries 86% on n=28 (Fisher p=0.237 against the offline 94%, so the rate holds) alongside the number that did move: median recovered clearance 8.56 mm against 16.2 mm. The WG should rule on 8.56 mm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
AdrianLlopart
commented
Sep 9, 2026
Superseded by #252.
This branch was cut before #238 and #239 landed, and had drifted into a 45-commit / 25-file diff that mostly duplicates work already merged through review. Rather than force-push a rebase over it, #252 rebuilds the same work on current master as one commit touching 10 files.
Nothing is lost. I compared every file against master (AST-compared where only prose differed) and kept master's reviewed version wherever the two agreed functionally. What was genuinely new here — the resolution-lever measurements, voxel_transport_probe.py, stop_ee_speed.py, the octree sweep test, octomap_resolution_env, _world_voxel_max_cells, and the PLAN/evidence entries — all carried over to #252 and is verified present there.
The pre-rebase tip is preserved locally at 556cb02 if anything needs recovering.
What
Slice A of the three-way split recorded in
0c2202d: the instrument repairs, the evidence producers, and the resolution-lever measurements. Touches no manifest, and neitherpackages/openral_safety/norcpp/— verified by an emptygit diff --statagainst both — so CLAUDE.md §1.4 applies and §3 does not. Slice B (feat/216-tight-geometry-link3-4-6, the three manifest envelopes) stays safety-WG gated on hazard-log Entry 026; slice C (panda_link1's envelope) was withdrawn and lands nowhere.Why
masterhas carried two measurement-corrupting defects since 2026年09月05日, and every validation round taken on it since is unusable._launch_envnever applied the sim DDS scope (fix(sim): deploy sim sets no DDS scope — a sim round joined a live robot's ROS graph #227 /fix(sim): keep a simulation and a real robot off each other's ROS graph #231 confined the deploy to domain 77; the harness polled domain 0), andros2 action list --no-daemoncannot discover an advertised action at all. Every scene of every round reportedharness-errorbeside a healthy graph.real-contact. fix(evidence): probe the link-vs-link pair a self stop names, and budget it #220 folded the new link-vs-link probe intonearest_any, whose first rule is "any probed pair ≤ 0 m →real-contact". Adjacent robot links overlap permanently and are ACM-permitted, so that rule became vacuously true. Three of four stops in the day's battery flip towithin-quantizationon re-derivation. The error runs one way — it manufactures real contacts, never clears one — so nothing was wrongly passed as safe.Two more, both diagnostics-only: the backing probe could not see a slab coincident with its visual shell (RoboCasa builds every counter top that way), nor world geometry in a cell whose only solid backing is the robot.
Nothing downstream can be measured until this lands. That is the whole argument for splitting rather than waiting for the WG on one PR.
What it establishes
With repaired instruments, 71 % of stops are of a physically clear robot (5 of 7; +0.67 ... +24.86 mm true clearance) — reproducing the #204 battery's 85-of-91 on an independent battery, a different commit, and a fixed adjudicator. The headline number has not moved.
It also measures the 25 → 15 mm resolution lever end to end, after correcting a strike that was mine and wrong by ×ばつ:
Net positive in three of four corners, and free for the whole start-state class (43 % of stops, arm speed exactly 0). No manifest edit here — shipping 15 mm is a conservatism reduction and needs its own WG ruling.
_world_voxel_max_cellsnow derives the cap from the resolution. That is a fail-open fix: a hand-kept 614 125 against a 15 mm grid's 2 803 221 cells makes the kernel reject every grid it is sent, which reads as "no world".How tested
ruffclean,mypy --strictclean overtools/(65 files) andopenral_cli,tools/refresh_methods_linenos.py --checkexit 0.Every behavioural fix is mutation-checked: restoring
+ link_linkfails the adjudicator test, forcing the backing sweep off fails the coincident-geometry test, flipping>to>=fails the contact-boundary test, and pointing the fixture reader atnearest_link_link_pairsfails the attribution test.Checklist
packages/openral_safety/orcpp/changeSOURCE.txt)docs/methods/updated forstop_excess,adr0101_recovery,stop_ee_speed,voxel_transport_probe;--checkcleanPLAN.md,collision-validation-evidence.md, standing caveats 10 and 11)fix(...)commits (e8e0731batches the hull-overhang query)just lintpasses;mypy --strictclean; no new baretype: ignore; no newtry/except: pass🤖 Generated with Claude Code
https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg