Skip to content

mapping pipeline: map-build orchestrator - #113

Open
MJohnson459 wants to merge 1 commit into
mainfrom
mapping-map-build
Open

mapping pipeline: map-build orchestrator#113
MJohnson459 wants to merge 1 commit into
mainfrom
mapping-map-build

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

Stage 2 of the mapping pipeline (docs/design/mapping-pipeline.md, work item 4)
as one command. pixi run map-build -- --bag <bag> takes a recorded mapping bag
and emits a validated candidate map revision, the gzipped bundle the registry
accepts, and a build report — with no robot, no live SLAM session and no map
needing to exist first.

What it does

mote_simulation/tools/map_build/ (runbook in its README), beside bag_replay
for that harness's reason: workstation-only, excluded from the robot sync. The
chain is solve → assemble → declutter → segment → validate + score → package.

  • solve calls bag_replay.replay.run_one in-process rather than
    re-implementing it, so the DDS isolation, stack launch, lockstep
    acceptance-chain feed and teardown stay in one place.
  • assemble writes the grid and posegraph in the layout save-map writes,
    since there is exactly one shape of revision and the registry knows it.
  • declutter is sites.promote_cleaned itself, not a copy — which is why
    that helper and clean_map_png stopped being private.
  • segment proposes one polygon zone per room; merge_into_zones gained
    site/floor/platform_id kwargs so a build can stamp a revision for a
    floor it was told about on the command line.
  • validate is hard: bundle.validate errors mean nothing is emitted, as
    does a solve that serialized no posegraph (such a revision navigates and
    cannot be extended — the frame, and every zone taught in it, is gone).
  • score is soft: truth-free metrics diffed against --baseline, printed as
    review evidence.

Verification

Run against the bag the acceptance names, 20260802_142539, with the promoted
home/ground/20260802T203339 as baseline. Report, maps and build.json are
banked in docs/tuning/2026-09-01-map-build/; the write-up is
docs/tuning/2026-09-01-map-build.md.

  • Loop drift 0.0992 m against the hand build's recorded 0.098 — within a
    millimetre, and it is the one metric here that measures the solve.
  • Wall thickness 0.0602 vs 0.0607, explored area 63.3 vs 62.8 m², 4 wall
    directions each, and the flat's angled wing reproduced as a second orthogonal
    frame 18.5° off the dominant one carrying 0.219 of the energy — the figure
    the 2026-08-25 report found.
  • Speckle 0.00364 vs 0.00277 is the only flagged difference, and it is the
    declutter thresholds rather than the solve: the hand build stripped more
    short structure at peak_rel_threshold 0.55 than task 337's prominence-picked
    default does. Reported, not gated.
  • 21 s of solve for a 21-minute bag, unattended, --bag to candidate.
  • 16 new unit tests; mote_bringup + mote_tasks suites 468 passed / 12
    skipped; bag_replay 32 passed; pre-commit clean.

Two defects the acceptance run found in this tool

Both would have told a reviewer to reject a good map.

The candidate was scored from the raw solve, 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 run
reported the candidate's speckle as five times the baseline's when the two
served maps agree to a thousandth. Both sides now read back a revision's own
map.png at the thresholds its own map.yaml declares.

unknown_frac moves with the bounding box. A candidate 4 px wider than the
baseline read 2.2% worse on it while covering 0.4 m² more floor. It left the
diff table; explored_area_m2 carries the same signal in metres.
angular_support_deg was already out, confounded by coverage. The rule both are
instances of: a diff row is a claim that the metric ranks candidates.

Two smaller decisions

The map pair declares free_thresh 0.100 rather than map_saver's 0.196,
which is the unknown shade's own occupancy value to five decimals and so decides
"unknown" against "free space the planner may drive through" on a rounding
(filed as #621 for the revisions already saved that way). And replayer.py now
records the grid's origin yaw — always zero in practice, recorded because a
build assembling a map.yaml must not assume it: a dropped origin yaw moves
every zone on the floor and leaves the map looking perfectly good.

Three of the design's steps are deliberately absent

Each is reported on the candidate rather than left silent.

Alignment. The step measures the wall rotation, re-solves with it injected
and keeps the better map — which needs an estimator that can say which is
better. The one in the tree called four of the seven banked 2026-08-02 solves
square when they were 3.5–5.6° off, and a re-solve is not a rigid rotation (the
same −3.0° injection moved three solves of one bag by +0.1°, −4.3° and −5.8°),
so the step is undecidable rather than merely ungated. That is task 615, and
the design (#111, landed after this task was written) already says the step is
blocked on it. Birth-alignment stays an operator's --frame X Y YAW, recorded
in the revision's meta, which is how the 2026-08-02 map is reproduced above.

The acceptance's "walls ≤ 0.5° off axis" therefore cannot be evaluated and
was not: docs/tuning/2026-09-01-alignment-residual.md established that this
map has no single wall grid to half a degree. The build measures wall structure
and prints it. Everything else in the acceptance is met.

Vocabulary carry-forward is task 345: the build emits the segmenter's
room_NN placeholders and reports what the baseline floor's places were called.

Upload needs a build identity (task 344) — the registry accepts candidate
uploads only from enrolled robots — so the bundle is emitted locally and the
report says what will send it.

Every revision's meta.yaml names its exact inputs: bag sha256 over bytes and
file names, params sha256, frame, feed, harness commit.

Also confirmed in passing, and not this tool's: the nine room proposals are
visibly tilted ~2° off their walls (task 349, on the building it was reported
against), and the Pi's home/ground floor has the stray maps/map directory of
task 300.

Branch mapping-map-build, commit c310d81. Not pushed.

Stage 2 of the mapping pipeline as one command. `pixi run map-build --bag <bag>`
solves the bag in lockstep under the committed build params, assembles the grid
and posegraph into the layout `save-map` writes, declutters, segments rooms,
validates, scores against a baseline revision and packs the registry's bundle,
with a build report beside it. No robot, no live SLAM session.

On the 2026-08-02 flat bag it reproduces the hand-built map's loop drift —
0.0992 m against 0.098 — in 21 s of solve for a 21-minute bag, unattended.
`docs/tuning/2026-09-01-map-build.md` and the banked artifacts beside it.

Four things are load-bearing. The declutter step is `sites.promote_cleaned`
itself rather than a copy, so a built map and a saved one are the same pass over
the same pixels (which is why those two helpers stopped being private). A solve
that serialized no posegraph fails the build: a revision without one navigates
and cannot be extended, and the frame — with every zone taught in it — is gone.
`bundle.validate` errors stop the build; a metric regression rides on the
candidate as review evidence. And both sides of that diff are read back from a
revision's own served `map.png` at the thresholds its own `map.yaml` declares —
the replay leg's metrics describe the *raw* solve, so scoring the candidate from
the leg and the baseline from disk compared two different artifacts and reported
the candidate's speckle as five times the baseline's when the served maps agree
to a thousandth. `unknown_frac` left the diff table for the same class of reason
(a 4-px-wider canvas read 2.2% worse on it while covering 0.4 m² more floor);
`angular_support_deg` was already out, being confounded by coverage.

Two smaller decisions. The map pair declares `free_thresh` 0.100, not
`map_saver`'s 0.196 — which is the unknown shade's own value to five decimals,
so it decides "unknown" against "free space the planner may drive through" on a
rounding. And the replay harness now records the grid's origin yaw: it has
always been zero, but a build assembling a `map.yaml` must not assume it, since
a dropped origin yaw moves every zone on the floor and leaves the map looking
perfectly good.

Three of the design's steps are deliberately absent, each reported on the
candidate rather than left silent. Alignment needs an estimator that can say
which of two solves is squarer; the one in the tree called four of the seven
banked 2026-08-02 solves square when they were 3.5-5.6 deg off, and a re-solve
is not a rigid rotation, so the step is undecidable rather than merely ungated
(task 615). Birth-alignment stays an operator's `--frame X Y YAW`, recorded in
the revision's meta. Vocabulary carry-forward is task 345. Upload needs a build
identity (task 344), so the bundle is emitted locally.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant