Skip to content

Chapter 4: sweep the initial guess, and find the residual behind it - #92

Merged
qmohsu merged 2 commits into
mainfrom
claude/ch4-initial-guess-basin
Aug 24, 2026
Merged

Chapter 4: sweep the initial guess, and find the residual behind it#92
qmohsu merged 2 commits into
mainfrom
claude/ch4-initial-guess-basin

Conversation

@qmohsu

@qmohsu qmohsu commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

When an iterative positioning solve fails, the reflex is to blame the starting point. This adds ch4_rf_point_positioning/example_initial_guess_basin.py, which holds the geometry at the well-behaved square array, fixes one target, sets the measurement noise to zero, and sweeps the initial guess over 1681 seeds — twice, changing nothing but the space the residual is formed in.

residual="tan" residual="angle"
seeds that fail 785 / 1681 341 / 1681
quiet: stalled at the seed, or stopped somewhere plausible 263 0
loud: walked off past 100 m 522 341
failures that still reported converged=True 305 196

The example was written expecting the wrapped-angle form to remove the basin outright, and the sweep said otherwise. The docstring, the README section and the tests all state the narrower claim it can actually support: 2.3× fewer failures, and underneath that, the quiet class goes to zero — the failures that look like answers. A seed far outside the room still walks off under either parameterisation, and 196 of those still set the convergence flag.

So fixing the residual makes the solver honest, not safe, which is why core.rf.solve_batch's four conditions are not optional either way. One traced seed at (−4.5, −2.0) walked to 9.4 × 10¹¹ m in 18 iterations with converged=True; that run is the fourth panel of the figure.

Two details worth keeping

  • The target sits off the seed lattice, at (3.2, 6.8). On a lattice point one of the 1681 seeds is the answer, so the solver cannot move and solve_batch scores it — correctly, by its own definition — as a stall. That is a classification artifact, and it is also the rule against seeding a solver with the ground truth, arrived at by accident. An assert in seed_grid() now makes the collision impossible rather than subtracting it afterwards.
  • Classification goes through core.rf.solve_batch rather than being re-derived, and the failure modes are drawn as their own bar panel, per 030-figures-and-claims.mdc.

Also in this PR

  • tests/ch4_rf_point_positioning/test_initial_guess_basin.py — pins both halves of the claim, so the caption cannot drift to "fixed", plus a guard that fails if the two parameterisations ever perform the same, at which point the example has stopped demonstrating anything. Companion to the existing test_aoa_initialisation_basin.py, which pins the same behaviour from a single cold start; this one sweeps the floor.
  • Notebook: new Part 7, "When the solver lies." Parts 1–6 all report the accuracy of the solves that worked. Part 7 is the other half — first the collinear geometry from compare_geometries(), where TOA and TDOA fail 100/100 while GDOP calls the array fine at 1.43 against 1.02 for the square; then this sweep. Three "Try it" edits at the end.
  • README: a new section, the Quick Start line, the File Structure entry, and a regenerated Architecture block (python -m tools.chapter_dependencies).

Checks

  • tests/ch4_rf_point_positioning and the example-level meta tests: 368 passed (one genuine failure found and fixed — the example imported core without putting the repo root on sys.path first, per issue Examples do not put the repo root on sys.path, so a fresh clone fails on 'import core' #86).
  • tests/docs: 507 passed, after regenerating the Architecture section and listing the new example in File Structure.
  • ruff check: clean.
  • The rendered PNG was opened and read before committing; the two panel titles and the y-label were re-laid-out after the first look, and the bar series were given their own palette because they had inherited the outcome colours.

qmohsu added 2 commits August 24, 2026 15:20
When an iterative positioning solve fails, the reflex is to blame the starting
point. example_initial_guess_basin.py holds the geometry at the well-behaved
square, fixes one target, sets the measurement noise to zero, and sweeps the
initial guess over 1681 seeds twice - changing nothing but the space the
residual is formed in.

The example was written expecting the wrapped-angle form to remove the basin
outright and the sweep said otherwise, so the docstring, the README and the
tests all state the narrower claim it can actually support. Over 1681 seeds:

                                    tan(psi)   wrap(angle)
  seeds that fail                        785           341
    quiet: stalled or plausible          263             0
    loud: walked off past 100 m          522           341
  failures claiming converged            305           196

So it is 2.3x fewer failures, and underneath that a sharper statement: the
wrapped-angle form removes the quiet class completely - the failures that look
like answers - while a seed far outside the room still walks off under either
parameterisation, and 196 of those still set converged=True. Fixing the
residual makes the solver honest, not safe, which is why solve_batch's four
conditions are not optional either way. One traced seed at (-4.5, -2.0) walked
to 9.4e11 m in 18 iterations with the flag set; that run is the fourth panel.

Two details worth keeping:

  - The target sits off the seed lattice at (3.2, 6.8). On a lattice point one
    of the 1681 seeds IS the answer, so the solver cannot move and scores as a
    stall - a classification artifact, and the rule against seeding a solver
    with the ground truth arrived at by accident. An assert now makes the
    collision impossible rather than subtracting it afterwards.
  - Classification goes through core.rf.solve_batch rather than being
    re-derived, and the failure modes are drawn as their own bar panel.

Also adds tests/ch4_rf_point_positioning/test_initial_guess_basin.py, which
pins both halves of the claim so the caption cannot drift to "fixed", and a
guard that fails if the two parameterisations ever perform the same - at which
point the example has stopped demonstrating anything.

Notebook: new Part 7, "When the solver lies" - the collinear geometry from
compare_geometries() where TOA and TDOA fail 100/100 with a GDOP of 1.43, then
this sweep. Parts 1-6 all report the accuracy of the solves that worked.
The cell appended an error only `if info['converged']`, wrapped each solve in
`except: pass`, and reported RMSE, mean and a "Success %" over whatever
survived. That is the shape 030-figures-and-claims.mdc exists to stop: the
solves that silently returned nonsense are averaged in, the ones that honestly
refused disappear from the denominator, and a solve that converges at 1e11 m
counts as a success.

Now on core.rf.solve_batch - the four conditions applied once - reporting the
median beside the failure count, with the failure rate drawn as its own panel
because an accuracy plot cannot say "this did not work". The seed stays the
beacon centroid, which was already right.

Every method reports 0/100 on this geometry, which is the honest result and the
right setup for Part 7: the same code and the same noise, with the beacons
moved into a line, fails 100/100.
@qmohsu
qmohsu merged commit e8d7051 into main Aug 24, 2026
1 check passed
@qmohsu
qmohsu deleted the claude/ch4-initial-guess-basin branch August 24, 2026 08:51
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