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.