Repository navigation
Chapter 4 prices the clock state against a beacon that answers back - #132
Merged
Merged
Conversation
…back
The inline comparison had no two-way row, so "what does estimating the receiver
clock actually cost?" was answerable only by intuition. `example_toa_positioning`
does run an RTT chain, but on different anchors, a different target and a
different error budget -- its 0.704 m and this table's numbers were never
comparable, and a deck built from the two would have compared them anyway.
**The design is one varied factor, and the discipline is the experiment.** The
new `RTT (two-way TOA)` series shares the anchors, the 50 test points, the
seed, the sigma ladder AND the realisation drawn from it -- `draw_range_noise`
is called once per level and handed to both range series. Only the state
vector differs: TOA solves `(x, y, c*dt)` per Eqs. (4.24)-(4.26) because a
one-way pseudorange carries an unknown receiver clock, RTT solves `(x, y)`
because Eq. (4.7)'s round trip is timed on one clock and cancels it. No bias is
injected into RTT.
Sharing the draw rather than redrawing is the point: `aoa_bearings` exists
because the weighted and unweighted AOA columns used to draw their own noise,
so their ratio carried a full realisation on each side. Same defect, avoided
before it was written.
**RTT is deliberately NOT noised through processing time and clock drift**,
though `simulate_rtt_measurement` offers exactly that. It would vary the error
budget as well as the state vector and the row could attribute the difference
to neither. The standalone section keeps teaching that cost and is untouched.
Measured (median over all 50 fixes, sigma 0 -> 0.5 m):
sigma 0.00 0.05 0.10 0.20 0.50
TOA 0.000 0.044 0.088 0.162 0.507
RTT 0.000 0.044 0.083 0.152 0.431
ratio -- 1.00x 1.06x 1.07x 1.18x
**That column is one number, not a trend, and reading it as a trend was the
trap.** The clock column inflates the position covariance by a factor set by
the geometry alone -- it does not grow with sigma. Analytically this array
gives 1.026 (median of per-point DOP ratios). The 1.00-1.18 spread is a
50-point median resolving 2.6% through about +/-7% of sampling noise: over 40
repeats per level the ratio of medians ranges 0.87 to 1.27, so a single level
cannot even establish the sign. The tests therefore pin the claim where it is
real -- exactly, at every one of the 50 points, via the Schur complement, and
over a 1200-point sample where 2.6% is resolvable -- and pin the per-level
ordering separately, labelled as a property of the shipped seed.
**The four pre-existing series are byte-identical, verified at full precision
rather than at three decimals.** Running main's copy of the module and this one
in a single process, every error value, `solved` mask and `n_failed` of TOA,
TDOA, AOA, AOA_unw and RSS agree with max|difference| = 0.0. The mechanism is
that `draw_range_noise` returns `None` at zero sigma instead of a zero array,
so the noiseless level still consumes nothing from the global stream, and the
new series consumes nothing at any level.
**Two defects were found and fixed in this change's own output.** The ratio
line first guarded on `median > 0`, and the noiseless medians are 3.5e-10 m and
9.9e-10 m -- both zero to a positioning system, both nonzero to float division
-- so it printed "0.35x" as the cost of the clock state. The floor is now
`STALL_M`, the solver's own 1e-6 m step tolerance. And a first test asserted the
recovered bias was 1.5 m on noisy data; it reads 1.4642, because the solve
absorbs the injected bias plus whatever common part the noise has. What is
exact there is the *difference* between the two clock estimates, and that is
what is asserted now.
**A log y-axis was rendered and rejected rather than argued about.** RSS at 8 m
does crush the range series near zero and the sibling geometry figure fixes
exactly that with `set_yscale("log")` -- but level 1 is noiseless, so those
medians are roundoff and a log axis spends ten of twelve decades on them,
compressing every informative level into the top fifth of the panel. The reason
is recorded in the panel's comment and in the README so the next reader does
not repeat the twenty minutes.
Colours and dash patterns are keyed by method rather than positional, so
inserting a series cannot silently recolour the four that were already there;
RTT is purple with its own dash pattern, needed because four of five methods
coincide at 100% in the solved-fixes panel.
Verified: 78 passed in `tests/ch4_rf_point_positioning` (11 of them new); 751
passed / 1 skipped across the docs guards, including the README transcript pin;
`test_repo_conventions` and the lint ratchet green; ruff on the two changed
files reports the same 3 pre-existing `typing.Dict` findings as main and
nothing new; `python -m ch4_rf_point_positioning.example_comparison` exits 0
and the regenerated figure was opened and read.
Three mutations were run to check the new guards can fail: giving RTT the clock
state too makes the ratio exactly 1.0000 (the position estimates become
bit-identical, since a common bias is absorbed whole into `c*dt`); letting RTT
redraw its own noise breaks the shared-draw test; injecting the bias into RTT
takes its median to ~1.2 m and its failures to 47-48 of 50.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`tests/test_black_is_actually_run.py` caught it on CI and nothing caught it here: black is in the `dev` extra and was simply not installed on this machine, so the guard could not run at all. CLAUDE.md's own line for this -- "a configured checker that is not run is indistinguishable from one that passes" -- with the twist that the checker was absent rather than unconfigured. black 26.5.1 installed to match the pin in `pyproject.toml`, which exists because versions disagree: black 26 reformats files black 25 accepts. Whitespace only: four wrapped expressions rejoined onto single lines. No assertion, tolerance or number moved, and `tests/ch4_rf_point_positioning` still reads 78 passed. `example_comparison.py` needed no change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chapter 4's inline comparison had no two-way row, so "what does estimating the receiver clock actually cost?" was answerable only by intuition.
example_toa_positioningdoes run an RTT chain, but on different anchors, a different target and a different error budget — its 0.704 m and this table's numbers were never comparable, and a deck built from the two would have compared them anyway.The design: one varied factor
The new RTT (two-way TOA) series shares the anchors, the 50 test points, the seed, the sigma ladder and the realisation drawn from it —
draw_range_noiseis called once per level and handed to both range series. Only the state vector differs:TOA(x, y, c·δt), Eqs. (4.24)-(4.26)RTT(x, y),TOAPositionerSharing the draw rather than redrawing is the point:
aoa_bearingsexists in this same file because the weighted and unweighted AOA columns used to draw their own noise, so their ratio carried a full realisation on each side. Same defect, avoided before it was written.RTT is deliberately not noised through processing time and clock drift, though
simulate_rtt_measurementoffers exactly that. Doing so would vary the error budget as well as the state vector, and the row could attribute the difference to neither. The standalone RTT section keeps teaching that cost and is untouched. Nothing here charges RTT for the transponder either, so the result is the best case for two-way ranging, not a net verdict on it.Measured
Median over all 50 fixes,
python -m ch4_rf_point_positioning.example_comparison:TOA/RTT ratio: — / 1.00× / 1.06× / 1.07× / 1.18×.
Read that ratio as one number, not a trend
Adding the clock column to
Hinflates the position covariance by a factor set by the geometry alone — it does not grow with σ. Analytically this array gives 1.026 (median of per-point DOP ratios; 1.033 as a ratio of medians). The 1.00→1.18 spread is a 50-point median resolving a 2.6% effect through roughly ±7% of sampling noise: over 40 repeats per level the ratio of medians ranges 0.87 to 1.27, so a single level cannot even establish the sign.So the tests pin the claim where it is real, and pin the shipped ordering separately as a property of seed 42:
The four pre-existing series are byte-identical
Verified at full precision, not at three decimals: running
main's copy of the module and this one in a single process, every error value,solvedmask andn_failedof TOA, TDOA, AOA, AOA_unw and RSS agree with max|difference| = 0.0. The mechanism is thatdraw_range_noisereturnsNoneat zero σ instead of a zero array, so the noiseless level still consumes nothing from the global stream, and the new series consumes nothing at any level.Two defects found in this change's own output
median > 0. The noiseless medians are 3.5e-10 m and 9.9e-10 m — both zero to a positioning system, both nonzero to float division — so it printed "0.35×" as the cost of the clock state. The floor is nowSTALL_M, the solver's own 1e-6 m step tolerance.A log y-axis was rendered and rejected, not argued about
RSS at 8 m does crush the range series near zero, and the sibling geometry figure fixes exactly that with
set_yscale("log"). It does not work here: level 1 is noiseless, so those medians are roundoff and a log axis spends ten of twelve decades on them, compressing every informative level into the top fifth of the panel. Recorded in the panel comment and the README so the next reader does not repeat it. Colours and dashes are now keyed by method rather than positional, so inserting a series cannot silently recolour the four already there; RTT is purple with its own dash pattern, needed because four of five methods coincide at 100 % in the solved-fixes panel.Verified
tests/ch4_rf_point_positioning— 78 passed (11 new).test_chapter_architecture_sections,test_readme_file_structure,test_chapter_readme_code_blocks,test_readme_example_output(the transcript pin),test_documented_flags_exist,test_documented_paths_exist,test_documented_commands_use_module_form.test_repo_conventions+test_lint_debt_only_shrinks+test_examples_answer_help— 522 passed.test_every_figure_has_a_demo_behind_it— 203 passed, 8 skipped.ruff checkon both changed files reports the same 3 pre-existingtyping.Dictfindings asmain, and nothing new.python -m tools.chapter_dependencies— "already up to date" (no newcoredependency).python -m ch4_rf_point_positioning.example_comparisonexits 0; the regenerated PNG was opened and read — legend legible, five series present, nothing clipped.compare_geometries, which this PR does not modify.Three mutations were run to check the new guards can fail: giving RTT the clock state too makes the ratio exactly
1.0000(the position estimates become bit-identical, since a common bias is absorbed whole intoc·δt); letting RTT redraw its own noise breaks the shared-draw test; injecting the bias into RTT takes its median to ~1.2 m and its failures to 47–48 of 50.🤖 Generated with Claude Code