Skip to content

Give the TOA comparison the clock state its own bias needs - #88

Merged
qmohsu merged 1 commit into
mainfrom
claude/ch4-toa-clock-bias
Aug 23, 2026
Merged

Give the TOA comparison the clock state its own bias needs#88
qmohsu merged 1 commit into
mainfrom
claude/ch4-toa-clock-bias

Conversation

@qmohsu

@qmohsu qmohsu commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Closes #85.

run_inline_comparison injects a shared 1.5 m receiver clock bias into the TOA pseudoranges — correctly, to show TDOA differencing it away — then solved them with a position-only (x, y) state. A common bias is unobservable to that state, so the residual never reached tol and the solve was discarded.

Confirmed, and one correction to the issue

Measured convergence, holding everything else fixed:

injected bias converged median error at zero noise
1.5 m (before) 2–5 / 100 0.153 m
0 m 99–100 / 100 0.000 m

The bias is the entire cause.

The issue says "the 2–4 % that do converge are accurate". They are not. TOA printed 0.153 m of median error at zero measurement noise, where TDOA and AOA both printed 0.000. A method solving perfect data is exact, so a nonzero number there is never noise. The survivors are the geometries where the bias could be partly absorbed into the position — the least-inaccurate rather than the accurate ones.

That matters for the choice of fix: it rules out option 3 (relabel the panel), because the median column is contaminated too, not just the convergence panel.

Option 1, cheaper than the issue estimates

toa_solve_with_clock_bias is Eqs. (4.24)-(4.26), already in core/rf/positioning.py and already in core.rf's __all__. The example injected a clock bias and then declined to use the solver written for it, one import away.

noise converged median error recovered bias
0.00 100/100 0.000 m +1.500 m
0.05 100/100 0.044 m +1.497 m
0.10 100/100 0.091 m +1.506 m
0.20 100/100 0.158 m +1.528 m
0.50 100/100 0.464 m +1.539 m

Summary-table medians go 0.153 → 0.000, 0.177 → 0.044, 0.211 → 0.152. TOA now tracks TDOA at the cost of the extra unknown, which is the comparison Chapter 4 exists to make: TOA carries the clock, TDOA differences it away.

The RSS branch keeps iterative_ls — it reaches the same class through rss_to_distance and carries no clock bias, so it has nothing to estimate. (The issue lists :482 as a position-only site; it is, but correctly so.)

The fix introduced a mirrored defect, also fixed

Three methods then sit on 100 %, and four solid lines at one value show only the last one drawn — the panel that used to say "TOA barely converges" would have said "TOA is missing", which is the same failure this file warns about at :81. Each method now has its own dash pattern. No value is nudged.

Guard behaviour

The chapter README pins this example's stdout, so test_readme_example_output.py went red on the old numbers exactly as designed. The transcript is updated from a real run and passes.

Verification

44 passed across tests/ch4_rf_point_positioning and the lint ratchet; README transcript guard passes; pyflakes clean. Figure regenerated and opened.

🤖 Generated with Claude Code

`run_inline_comparison` injects a shared 1.5 m receiver clock bias into the TOA
pseudoranges -- correctly, to show TDOA differencing it away -- and then solved
them with a position-only (x, y) state. A common bias is unobservable to that
state: no position makes four uniformly inflated ranges consistent, so the
residual never reached `tol` and the solve was discarded. The figure's
"Convergence Success Rate" panel therefore read 2-5 of 100 for TOA at every
noise level, beside a median-error table where TOA was among the best. Both
cannot be true of a method, and the panel was the one lying -- it reported the
survival rate of a model mismatch.

The tell was the noiseless row. TOA printed 0.153 m of median error at zero
measurement noise where TDOA and AOA both printed 0.000. A method solving
perfect data is exact, so a nonzero number there is never noise. That also
corrects the issue's reading that "the 2-4 % that do converge are accurate":
they are not. The survivors are the geometries where the bias could be partly
absorbed *into the position*, which makes them the least-inaccurate rather than
the accurate ones -- and it rules out relabelling the panel as a fix, because
the median column is wrong too.

The fix was already exported. `toa_solve_with_clock_bias` is Eqs. (4.24)-(4.26),
the chapter's own joint position-and-clock estimate, in `core/rf/positioning.py`
and in `core.rf`'s `__all__`. The example injected a clock bias and then
declined to use the solver written for it, one import away. With the state as
(x, y, c*dt): 100/100 converge at every level, the bias returns as +1.500 m on
noiseless data, and the medians go 0.153 -> 0.000, 0.177 -> 0.044, 0.211 ->
0.152, so TOA now tracks TDOA at the cost of the extra unknown. That is the
comparison Chapter 4 exists to make.

The RSS branch keeps `iterative_ls`. It reaches the same class through
`rss_to_distance` and carries no clock bias, so it has nothing to estimate.

Fixing it made TOA invisible, which is the same defect mirrored: three methods
then sit on 100 % and four solid lines at one value show only the last drawn, so
a panel that used to say "TOA barely converges" would have said "TOA is
missing". Each method now has its own dash pattern; no value is nudged.

The chapter README pins this example's stdout, so that guard went red on the
old numbers exactly as intended and the transcript is updated from a real run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qmohsu
qmohsu merged commit b952f94 into main Aug 23, 2026
1 check passed
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.

ch4 comparison: TOA's 2-4% convergence rate is a model mismatch in the harness, not a property of TOA

1 participant