Repository navigation
Two-way TOA earns its place in Chapter 4 - #129
Merged
Merged
Conversation
`example_toa_positioning` argued the opposite of what the chapter teaches, by
juxtaposition. Examples 1-2 solve one-way ranges at 0.1 m of measurement noise
and printed 0.000 / 0.055 m; Example 5 measures RTT on an honest nanosecond
timing budget and printed 0.718 m RMSE, 0.704 m of position error. A reader
flipping through saw two-way as 7x worse -- a comparison between one
protocol's assumptions and another's physics, not between protocols. Grepped
before believing it: the word "sync" appeared nowhere in the file.
Three changes.
**The assumption is stated where it is made.** Examples 1, 2 and the module
docstring now say that one-way TOA as solved here needs the clocks already
synchronised, and that 1 ns of unmodelled offset is 0.300 m at c. Example 2's
range-noise line carries its own size in nanoseconds (0.33), which is the
comparison that matters: the whole error budget of Examples 1-2 is smaller
than the offset a free-running consumer oscillator reaches in a fraction of a
second.
**Example 7 measures it.** One 20 m array, one agent position, one number --
sigma_t, the agent clock's per-timestamp jitter -- and four solves over 2000
draws each, every draw scored:
case median RMSE failed predicted
A one-way, clocks synchronised 0.2518 0.3018 0 0.3000
B(i) one-way, offset, position only 1.7978 1.8416 1 1.7239
B(ii) one-way, offset, clock solved 0.2545 0.3039 0 0.3014
C two-way RTT, no sync needed 0.2148 0.2613 0 0.2598
A, B(ii) and C land on DOP x sigma_range. Twenty nanoseconds of unmodelled
offset costs 1.8 m; estimating it instead (Eqs. 4.24-4.26) recovers almost all
of it at 0.45% of position DOP; two-way needs neither.
**The bookkeeping is derived, not chosen per case.** One-way is a single
receive timestamp against a scheduled epoch, sigma_range = c*sigma_t. Two-way
is two timestamps on the agent's own clock plus the turnaround calibration
residual, sigma_RTT = sqrt(2*st^2 + sp^2), halved by Eq. (4.7). It is the
bookkeeping that flatters two-way least -- giving the transmit epoch its own
sigma_t would double the gap. Verified against the simulation rather than
asserted: measured range std matches `two_way_range_sigma` to within 0.5% over
sigma_proc from 0 to 3 ns, so both knobs demonstrably reach the measurement.
**Reported disagreement.** C comes out 13% ahead of A, where A <= C was
expected. That is arithmetic, not tuning: a round trip puts twice the distance
into the interval being timed while the jitter grows only as sqrt(2). The
example prints the crossover -- the two protocols range equally well at
sigma_proc = sqrt(2)*sigma_t = 1.414 ns -- rather than moving a parameter to
recover the expected order.
Checks: zero-noise sanity is 0.000000 m for all four arms. B(i)'s bias is
predicted by b*||(H'H)^-1 sum(u_i)|| = 1.1323 m against 1.6976 m measured, and
the example shows why -- at b = 0.01 m the ratio is 1.001, so the gap is
nonlinearity and 6 m of bias on 11-17 m ranges is not a small perturbation.
The agent is off-centre deliberately: at the centre of a symmetric array
sum(u_i) is zero and a common bias is invisible.
Also, in passing: Example 5's single-draw positioning number is labelled as one
draw with the scale to expect beside it, and its Monte-Carlo RMSE gets the
prediction it should match (0.8072 m against 0.7180 m over 100 draws).
READMEs: the chapter's TOA section gains a clock-requirement table over the
four approaches, the RTT section gains the derivation and an explicit "this
does not beat TDOA", and the square dataset README names what `toa_ranges.txt`
assumes. The transcript block is re-pasted from a real run and was
mutation-checked -- perturbing one new digit turns it red.
Output verified reproducible across processes; the committed figure is
byte-identical; no dataset moved.
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.
The defect
ch4_rf_point_positioning/example_toa_positioning.pyargued the opposite of what the chapter teaches, by juxtaposition:grep -i syncreturned nothing.A reader flipping through sees two-way as 7x worse — a comparison between one protocol's assumptions and another's physics. The RTT math itself was correct and is unchanged.
The three-case comparison (Example 7)
One 20 m array, agent at (8, 12), one number —
sigma_t, the agent clock's per-timestamp jitter — and 2000 draws per case, every draw scored:Position DOP 1.0007 (position only) against 1.0053 with the clock as a third unknown — the +0.45% B(ii) pays.
The bookkeeping, derived rather than chosen per case
sigma_range = c*sigma_t= 0.2998 m.sigma_RTT = sqrt(2*st^2 + sp^2), halved by Eq. (4.7) — 0.2596 m.This is the bookkeeping that flatters two-way least: giving the transmit epoch its own
sigma_twould make one-wayc*sqrt(2)*sigma_tand double the gap. Verified against the simulation rather than asserted — measured range std matchestwo_way_range_sigmato within 0.5% forsigma_procfrom 0 to 3 ns, so both knobs demonstrably reach the measurement.Reported disagreement with the expected ordering
A <= Cwas expected; C comes out 13% ahead of A. That is arithmetic, not a thumb on the scale — a round trip puts twice the distance into the interval being timed while the jitter grows only as sqrt(2). The example prints the crossover (sigma_proc = sqrt(2)*sigma_t= 1.414 ns, where the two range equally well) rather than moving a parameter to recover the expected order.Checks
sigma_t = 0,b = 0,sigma_proc = 0.b*||(H'H)^-1 sum(u_i)||= 1.1323 m, measured noiseless 1.6976 m. The example shows why: atb = 0.01 mthe ratio is 1.001, so the gap is nonlinearity — 6 m of bias on 11-17 m ranges is not a small perturbation. The agent is off-centre deliberately; at a symmetric array's centresum(u_i)is zero and a common bias is invisible.test_readme_example_output.pyred.Also, in passing
Example 5's single-draw positioning number is labelled as one draw with the scale to expect beside it, and its Monte-Carlo RMSE gets the prediction it should match (0.8072 m predicted against 0.7180 m measured over 100 draws).
Documentation
data/sim/ch4_rf_2d_square/README.md: names whattoa_ranges.txtassumes, and adds two-way TOA to the equation list.Test plan
tests/ch4_rf_point_positioning tests/core/rf— 243 passedtests/docs/test_readme_example_output.py -k ch4alone — 3 passedtests/docs/test_readme_code_blocks.py+test_chapter_readme_code_blocks.py— 57 passed, 1 skippedtest_repo_validators.py test_semantic_api_debt_only_shrinks.py+ lint/mypy/black/doctest trio — 34 passedtest_datasets_reproduce_from_their_recipe.py test_examples_answer_help.py— 82 passed🤖 Generated with Claude Code