Skip to content

map-build against the map it was written to reproduce

The 2026-08-02 flat map was built by hand: a lockstep re-solve of the session's bag under corrected parameters, a measured-then-injected frame rotation, a declutter pass with a hand-tuned threshold, room segmentation, and a hand-assembled revision side-loaded onto the robot. pixi run map-build is that chain as one command. This is what it produces on the same bag.

Everything below is in 2026-09-01-map-build/: the build's own report, the map it emitted, the map it is compared against, and build.json.

0. The two artifacts

promoted (by hand, 2026-08-02) built (map-build, 2026-09-01)
revision home/ground/20260802T203339 20260901T180343
bag 20260802_142539 the same, sha256 099f9d06…
params live file + chain 10 + coarse_angle_resolution 0.0175 committed slam_toolbox_build_params.yaml (chain 10, coarse_angle_resolution 0.0349)
frame --frame 0 0 -3.0 the same, passed on the command line
declutter peak_rel_threshold 0.55, hand-tuned the committed default, prominence-picked (task 337)
size 236x181 @ 0.05 m/px 240x182 @ 0.05 m/px

The two parameter differences are not slips. coarse_angle_resolution 0.0175 was adopted in the hand build on the reasoning that the live 2.0° value snapped solutions to an orientation lattice; there is no such lattice, and the finer sweep beat the live value on nothing (2026-08-25-slam-build-params.md §2), so the committed build file keeps the live value. The hand-tuned declutter threshold is what task 337 replaced.

1. What the build did

solve        ok — 340 pose-graph nodes from 542 scans in 21 s
assemble     ok — 240x182 @ 0.050 m/px, origin (-5.316, -5.211, 0.000)
declutter    ok — -1244 cells, +277
segment      ok — 9 room zone(s) proposed
validate     ok — valid
score        ok — 1 metric worse than the baseline: map.speckle_frac
package      ok — 7101101 bytes

21 seconds of solve for a 21-minute bag, unattended, from --bag to a validated candidate and a build report. That is the economy the whole design rests on, and it is the lockstep harness's (task 295), not this tool's.

2. It reproduces the map

metric built promoted verdict
loop drift (start↔end, m) 0.0992 0.098 (recorded in the hand build's meta) reproduced
drift ratio 0.00071 —
mean wall thickness (m) 0.0602 0.0607 same
speckle fraction 0.00364 0.00277 worse
explored area (m²) 63.3 62.8 same
wall directions 4 4 same
second wall frame 18.5° off dominant, 0.219 of energy — the flat's angled wing

Loop drift lands within a millimetre of the hand build's, which is the number that matters: it is the only one of these that measures the solve. The second orthogonal frame at 18.5° carrying about a fifth of the energy is the flat's angled wing, exactly as the 2026-08-25 report found it.

The speckle difference — 0.0009 of occupied cells — is the declutter thresholds, not the solve: the hand build stripped more short structure at peak_rel_threshold 0.55 than the committed prominence-picked default does. It is reported as review evidence and gates nothing, which is what the design asks for.

Side by side, map.png and promoted-map.png: the same flat, the same layout, the built one a degree or two rotated and holding slightly more short-wall detail.

3. Two ways the report was wrong before it was right

Both were found by running the acceptance and reading the numbers, and both would have told a reviewer to reject a good map.

The candidate was scored from the raw solve and the baseline from disk. The replay leg carries map metrics, but they describe the image before the declutter pass, while a stored revision only ever keeps the cleaned one. The first acceptance run reported the candidate's speckle as 0.0137 against the baseline's 0.0028 — five times worse — when the two served maps are 0.0036 and 0.0028. Both sides now go through one function that reads a revision's own map.png at the thresholds its own map.yaml declares.

unknown_frac moves with the bounding box. It is a fraction of the grid, so the candidate — 4 px wider than the baseline — read 2.2% worse on it while covering 0.4 m² more floor. It is out of the diff table; explored_area_m2 carries the same signal in metres and does not depend on the canvas. It is still in build.json, like everything map_quality measures.

The general rule both cases are instances of: a diff row is a claim that the metric ranks one candidate against another. angular_support_deg was already excluded on the same grounds (it is confounded by coverage).

4. What the acceptance asked for and could not be checked

The task's acceptance included "walls ≤ 0.5° off axis". That assertion cannot be made: 2026-09-01-alignment-residual.md established that the map has no single wall grid to half a degree — thirds of this building disagree by 8°, and there is a second family 18° off — and that the estimator in the tree reports maps 3.5–5.6° out as square. The design was corrected accordingly (#111) and the build measures wall structure and prints it rather than asserting a residual. An orientation estimator the alignment step can be gated on is task 615.

5. What the run also confirmed, and is not this tool's

The nine room proposals are visibly tilted about 2° off the walls they enclose (rooms.png) — task 349, segment-map's wall-alignment estimator on a multi-family building, reproduced here on the building it was reported against.

Reproducing this

pixi run map-build -- \
  --bag ~/.mote/bags/mapping/20260802_142539 \
  --site home --floor ground --frame 0 0 -3.0 \
  --baseline <a copy of home/ground/20260802T203339>

The bag is on the robot under ~/.mote/bags/mapping/; the baseline revision is that floor's current map. build.json records both digests.