Skip to content

feat(track): per-chunk Doppler update with collected correlator outputs#205

Open
zsoerenm wants to merge 10 commits into
masterfrom
estimate-doppler-timing
Open

feat(track): per-chunk Doppler update with collected correlator outputs#205
zsoerenm wants to merge 10 commits into
masterfrom
estimate-doppler-timing

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Decouples the Doppler-estimation (NCO-update) rate from the code period. track / track! now walk each measurement in fixed-size time chunks; every completed correlator output is collected — tagged with the sample index at which it was taken — into a per-signal buffer. After the chunk's completions the estimator folds over those outputs in order and updates every satellite's NCO once, at a common epoch; the chunk's trailing partial (the head of the next integration) is then integrated with the freshly updated Doppler.

Motivation:

  • Trade Doppler-estimation compute for update rate via a new doppler_update_interval kwarg (a time; default nothing ⇒ smallest code period across all signals).
  • Give vector tracking a batch of sample-indexed correlator outputs on a common time grid.
  • Synchronize NCO updates across satellites (previously staggered per code period).

What changed

  • Data model — new CorrelatorOutput{C} (correlator, integrated_samples, sample_index); TrackedSignal gains a preallocated, reused correlator_outputs buffer. Both exported. Removed the now-unused is_integration_completed field.
  • Correlate phase_update_tracked_sat_correlator inner-loops over the chunk; update/_build_new_signals snapshot a record and reset the accumulator on each completion. Chunk end is a per-band, drift-free time grid; replica sizing still uses the true buffer length. The per-sat chunk loop lives once in the generic AbstractDownconvertAndCorrelator method — the backend boundary is _correlate_signals (CPU-fused, Int16, one-bit, two-bit; single + threaded); the @generated kernels are untouched.
  • Two passes per chunktrack! first correlates only the completions (stop_before_partial = true, stopping at the last code-block boundary), runs the estimator, then integrates the residue with the updated NCO Doppler. Every completed integration is therefore produced by a single Doppler and each correction takes effect right at its completing boundary — the classic per-completion loop timing — while the NCO is still written once per chunk at a common epoch. A trailing fold after the last chunk catches the rare completion the updated code Doppler pulls back inside the final residue. For direct downconvert_and_correlate(!) callers stop_before_partial defaults to false (consume the whole window, as before).
  • Estimate phase — folds over correlator_outputs, threading loop-filter state + FLL previous_prompt per record (per-record integration_time / 1/N), writing the NCO once. Preserves the QPSK driver_carrier_phase de-rotation. The sync phase-snap also resets the in-flight accumulator (fixes a 0/0 → NaN at NH-chip sync), and records that follow a mid-fold sync detection are applied with skip_bit_buffer = true — they were correlated with pre-sync replicas (no secondary-code wipe-off), so they stay out of the bit buffer and re-enter cleanly next chunk (matters only for enlarged doppler_update_intervals).
  • track!/track — new doppler_update_interval kwarg, chunked two-pass loop, validated one-sample floor, and a clear ArgumentError when doppler_update_interval is passed without time units.
  • Docs — design plan under docs/plans/, plus track.md / custom_doppler_estimator.md / correlator.md; the track docstring documents that correlator_outputs (like filtered_prompts) is a shared scratch buffer between input and returned state.

Behavioral compatibility

With the two-pass chunk, edge behavior matches the previous per-code-period loop: the FLL-assisted pull-in edge sits at its historical 240 Hz (converges) / 250 Hz (fails), and the 1800-block L1C-P overlay sync locks after exactly one overlay cycle — the test suite keeps its original assertions. (An earlier single-pass design integrated the chunk residue at the pre-update Doppler, which tightened the FLL edge to ~210 Hz and could land the overlay sync one block late; the two-pass split removed both deltas.)

Testing

  • Full Pkg.test() passes; docs build cleanly; JuliaFormatter 2.8.5 clean.
  • New test/doppler_update_interval.jl: chunk-grid arithmetic, default resolution, validation (including the units error message), multi-record collection with sample indices, output clearing, end-to-end convergence with an enlarged interval.
  • The strict full-track! allocation test (measure_track_alloc) passes at 0 bytes with chunking.

🤖 Generated with Claude Code

@zsoerenm
zsoerenm force-pushed the estimate-doppler-timing branch from b596735 to 428f038 Compare July 21, 2026 13:48
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.14%. Comparing base (5f03cd2) to head (4d5b2fa).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #205      +/-   ##
==========================================
+ Coverage   97.87%   98.14%   +0.27%     
==========================================
  Files          32       32              
  Lines        3299     3348      +49     
==========================================
+ Hits         3229     3286      +57     
+ Misses         70       62       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 11.8 μs 6.28 μs 4.73 μs 9.71 μs 1.89 ✅ 2.5 ✅ 1.22 ✅
GPS L1CA, 8 sats @ 40 MHz 368.0 μs 118.0 μs 60.5 μs 114.0 μs 3.12 ✅ 6.1 ✅ 3.23 ✅
GPS L1CA, 8 sats @ 5 MHz 53.8 μs 21.8 μs 16.0 μs 24.7 μs 2.47 ✅ 3.36 ✅ 2.18 ✅
Galileo E1B, 4 sats @ 25 MHz 142.0 μs 59.4 μs 2.39 ✅
dynamic taps @ 5 MHz (kernel) 6.68 μs 2.29 μs 1.49 μs 2.55 μs 2.92 ✅ 4.48 ✅ 2.62 ✅
multi-signal N=3 @ 5 MHz 11.2 μs 5.33 μs 4.39 μs 7.2 μs 2.1 ✅ 2.55 ✅ 1.56 ✅
Time benchmarks (base vs PR head)

Ratio = 5f03cd2… / f51856a…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

5f03cd2 f51856a 5f03cd2… / f51856a
downconvert and correlate/CPU/Float32 2.47 μs 2.6 μs 0.95
downconvert and correlate/CPU/Float32 4ant 4.88 μs 4.92 μs 0.99
downconvert and correlate/CPU/Float64 2.68 μs 2.73 μs 0.98
downconvert and correlate/CPU/Int16 2.61 μs 2.65 μs 0.986
downconvert and correlate/CPU/Int16 4ant 4.83 μs 4.87 μs 0.991
downconvert and correlate/CPU/Int32 2.55 μs 2.57 μs 0.991
fused kernel/1-ant dynamic taps 2.56 μs 2.56 μs 0.998
fused kernel/1-ant static taps 2.06 μs 2.13 μs 0.965
fused kernel/4-ant dynamic taps 7.76 μs 7.76 μs 1.0
fused kernel/4-ant static taps 4.43 μs 4.43 μs 1.0
fused tuple kernel/1-ant N=2 3.27 μs 3.27 μs 1.0
fused tuple kernel/1-ant N=3 3.4 μs 3.4 μs 1.0
fused tuple kernel/2-ant N=2 6.3 μs 6.3 μs 1.0
fused tuple kernel/2-ant N=3 5.61 μs 5.61 μs 1.0
fused tuple kernel/4-ant N=2 11.5 μs 11.5 μs 1.0
fused tuple kernel/4-ant N=3 10.1 μs 10.1 μs 1.0
track/1. Float32/2K – track 2.66 μs 2.81 μs 0.947 ⚠️
track/1. Float32/2K – track! 2.74 μs 2.93 μs 0.934 ⚠️
track/2. L1 8sat/5K – track 49.6 μs 51.0 μs 0.973
track/2. L1 8sat/5K – track! 49.4 μs 50.8 μs 0.973
track/2. L1 8sat/5K – track! Int16 21.5 μs 21.6 μs 0.998
track/2. L1 8sat/5K – track! OneBit 15.9 μs 15.8 μs 1.0
track/2. L1 8sat/5K – track!-threaded 49.6 μs 50.9 μs 0.975
track/2. L1 8sat/5K – track-threaded 49.8 μs 51.0 μs 0.977
track/3. E1B 4sat/25K – track 136.0 μs 136.0 μs 0.994
track/3. E1B 4sat/25K – track! 135.0 μs 136.0 μs 0.994
track/3. E1B 4sat/25K – track! Int16 58.6 μs 58.8 μs 0.996
track/3. E1B 4sat/25K – track!-threaded 135.0 μs 136.0 μs 0.994
track/3. E1B 4sat/25K – track-threaded 136.0 μs 137.0 μs 0.995
track/4. 8L1+8E1B/25K – track 490.0 μs 491.0 μs 0.996
track/4. 8L1+8E1B/25K – track! 488.0 μs 490.0 μs 0.996
track/4. 8L1+8E1B/25K – track!-threaded 488.0 μs 490.0 μs 0.996
track/4. 8L1+8E1B/25K – track-threaded 490.0 μs 492.0 μs 0.996
track/5. multi-signal N=1/5K – track 6.39 μs 6.6 μs 0.968
track/5. multi-signal N=1/5K – track! 6.39 μs 6.6 μs 0.968
track/6. multi-signal N=2/5K – track 10.3 μs 10.7 μs 0.965
track/6. multi-signal N=2/5K – track! 10.4 μs 10.8 μs 0.965
track/7. multi-signal N=3/5K – track 11.6 μs 12.1 μs 0.959
track/7. multi-signal N=3/5K – track! 11.6 μs 12.1 μs 0.955
track/8. L1CA presync 2 blk – track! 12.4 μs 13.0 μs 0.952
track/8. L1CA presync 20 blk – track! 120.0 μs 128.0 μs 0.939 ⚠️
track/8. L1CA synced 2 blk – track! 12.2 μs 12.9 μs 0.945 ⚠️
track/8. L1CA synced 20 blk – track! 118.0 μs 127.0 μs 0.928 ⚠️
time_to_load 201.0 μs 151.0 μs 1.33 ✅
Memory benchmarks (base vs PR head)

Ratio = 5f03cd2… / f51856a… (bytes allocated): >1 means the PR allocates less. ✅ ≥ 5 % less, ⚠️ ≥ 5 % more. /0 mark a benchmark that drops to / picks up allocations, means both revisions allocate nothing. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

5f03cd2 f51856a 5f03cd2… / f51856a
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 928 B 0.914 ⚠️
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 928 B 0.914 ⚠️
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B 1.0
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B 1.0
track/1. Float32/2K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4656 B 10 allocs: 4656 B 1.0
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track! Int16 13 allocs: 1056 B 13 allocs: 1056 B 1.0
track/2. L1 8sat/5K – track! OneBit 45 allocs: 2736 B 45 allocs: 2736 B 1.0
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4656 B 10 allocs: 4656 B 1.0
track/3. E1B 4sat/25K – track 10 allocs: 3056 B 10 allocs: 3056 B 1.0
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track! Int16 5 allocs: 416 B 5 allocs: 416 B 1.0
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 3056 B 10 allocs: 3056 B 1.0
track/4. 8L1+8E1B/25K – track 26 allocs: 10464 B 26 allocs: 10976 B 0.953
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10464 B 26 allocs: 10976 B 0.953
track/5. multi-signal N=1/5K – track 9 allocs: 944 B 9 allocs: 944 B 1.0
track/5. multi-signal N=1/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 9 allocs: 1408 B 9 allocs: 1408 B 1.0
track/6. multi-signal N=2/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 9 allocs: 1760 B 9 allocs: 1760 B 1.0
track/7. multi-signal N=3/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA presync 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B 1.0

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 12.8 μs 6.85 μs 6.24 μs 11.6 μs 1.87 ✅ 2.05 ✅ 1.1 ✅
GPS L1CA, 8 sats @ 40 MHz 333.0 μs 133.0 μs 73.8 μs 120.0 μs 2.5 ✅ 4.51 ✅ 2.77 ✅
GPS L1CA, 8 sats @ 5 MHz 50.6 μs 29.1 μs 21.2 μs 29.4 μs 1.74 ✅ 2.39 ✅ 1.72 ✅
Galileo E1B, 4 sats @ 25 MHz 123.0 μs 56.4 μs 2.18 ✅
dynamic taps @ 5 MHz (kernel) 5.77 μs 3.41 μs 2.01 μs 2.9 μs 1.7 ✅ 2.88 ✅ 1.99 ✅
multi-signal N=3 @ 5 MHz 10.3 μs 7.05 μs 6.16 μs 8.71 μs 1.46 ✅ 1.67 ✅ 1.18 ✅
Time benchmarks (base vs PR head)

Ratio = 5f03cd2… / f51856a…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

5f03cd2 f51856a 5f03cd2… / f51856a
downconvert and correlate/CPU/Float32 2.34 μs 2.38 μs 0.986
downconvert and correlate/CPU/Float32 4ant 4.72 μs 4.78 μs 0.986
downconvert and correlate/CPU/Float64 2.76 μs 2.8 μs 0.985
downconvert and correlate/CPU/Int16 2.43 μs 2.46 μs 0.989
downconvert and correlate/CPU/Int16 4ant 5.17 μs 5.31 μs 0.974
downconvert and correlate/CPU/Int32 2.41 μs 2.46 μs 0.981
fused kernel/1-ant dynamic taps 2.16 μs 2.15 μs 1.01
fused kernel/1-ant static taps 1.88 μs 1.88 μs 0.997
fused kernel/4-ant dynamic taps 5.31 μs 5.32 μs 0.998
fused kernel/4-ant static taps 4.14 μs 4.14 μs 1.0
fused tuple kernel/1-ant N=2 2.35 μs 2.35 μs 1.0
fused tuple kernel/1-ant N=3 2.82 μs 2.82 μs 0.999
fused tuple kernel/2-ant N=2 3.59 μs 3.6 μs 0.998
fused tuple kernel/2-ant N=3 4.53 μs 4.53 μs 1.0
fused tuple kernel/4-ant N=2 6.04 μs 6.09 μs 0.992
fused tuple kernel/4-ant N=3 9.85 μs 9.86 μs 0.999
track/1. Float32/2K – track 2.56 μs 2.78 μs 0.921 ⚠️
track/1. Float32/2K – track! 2.66 μs 2.85 μs 0.932 ⚠️
track/2. L1 8sat/5K – track 47.8 μs 49.0 μs 0.977
track/2. L1 8sat/5K – track! 47.6 μs 48.4 μs 0.984
track/2. L1 8sat/5K – track! Int16 29.3 μs 30.7 μs 0.956
track/2. L1 8sat/5K – track! OneBit 20.1 μs 21.4 μs 0.938 ⚠️
track/2. L1 8sat/5K – track!-threaded 47.1 μs 48.4 μs 0.973
track/2. L1 8sat/5K – track-threaded 48.1 μs 49.3 μs 0.976
track/3. E1B 4sat/25K – track 119.0 μs 119.0 μs 1.0
track/3. E1B 4sat/25K – track! 119.0 μs 118.0 μs 1.01
track/3. E1B 4sat/25K – track! Int16 57.0 μs 55.9 μs 1.02
track/3. E1B 4sat/25K – track!-threaded 119.0 μs 118.0 μs 1.01
track/3. E1B 4sat/25K – track-threaded 119.0 μs 118.0 μs 1.01
track/4. 8L1+8E1B/25K – track 437.0 μs 436.0 μs 1.0
track/4. 8L1+8E1B/25K – track! 435.0 μs 433.0 μs 1.01
track/4. 8L1+8E1B/25K – track!-threaded 434.0 μs 433.0 μs 1.0
track/4. 8L1+8E1B/25K – track-threaded 438.0 μs 435.0 μs 1.01
track/5. multi-signal N=1/5K – track 6.12 μs 6.29 μs 0.972
track/5. multi-signal N=1/5K – track! 6.19 μs 6.29 μs 0.983
track/6. multi-signal N=2/5K – track 8.36 μs 8.86 μs 0.943 ⚠️
track/6. multi-signal N=2/5K – track! 8.5 μs 8.95 μs 0.95 ⚠️
track/7. multi-signal N=3/5K – track 10.7 μs 11.4 μs 0.937 ⚠️
track/7. multi-signal N=3/5K – track! 11.0 μs 11.7 μs 0.94 ⚠️
track/8. L1CA presync 2 blk – track! 11.6 μs 12.2 μs 0.948 ⚠️
track/8. L1CA presync 20 blk – track! 113.0 μs 123.0 μs 0.919 ⚠️
track/8. L1CA synced 2 blk – track! 11.5 μs 12.1 μs 0.95 ⚠️
track/8. L1CA synced 20 blk – track! 111.0 μs 121.0 μs 0.916 ⚠️
time_to_load 100.0 μs 103.0 μs 0.972
Memory benchmarks (base vs PR head)

Ratio = 5f03cd2… / f51856a… (bytes allocated): >1 means the PR allocates less. ✅ ≥ 5 % less, ⚠️ ≥ 5 % more. /0 mark a benchmark that drops to / picks up allocations, means both revisions allocate nothing. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

5f03cd2 f51856a 5f03cd2… / f51856a
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 928 B 0.914 ⚠️
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B 1.0
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 928 B 0.914 ⚠️
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B 1.0
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B 1.0
track/1. Float32/2K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4536 B 10 allocs: 4664 B 0.973
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track! Int16 13 allocs: 1056 B 13 allocs: 1056 B 1.0
track/2. L1 8sat/5K – track! OneBit 45 allocs: 2736 B 45 allocs: 2736 B 1.0
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4536 B 10 allocs: 4664 B 0.973
track/3. E1B 4sat/25K – track 10 allocs: 2808 B 10 allocs: 2872 B 0.978
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track! Int16 5 allocs: 416 B 5 allocs: 416 B 1.0
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 2808 B 10 allocs: 2872 B 0.978
track/4. 8L1+8E1B/25K – track 26 allocs: 10352 B 26 allocs: 10608 B 0.976
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10352 B 26 allocs: 10608 B 0.976
track/5. multi-signal N=1/5K – track 9 allocs: 944 B 9 allocs: 944 B 1.0
track/5. multi-signal N=1/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 9 allocs: 1408 B 9 allocs: 1408 B 1.0
track/6. multi-signal N=2/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 9 allocs: 1760 B 9 allocs: 1760 B 1.0
track/7. multi-signal N=3/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA presync 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B 1.0
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B 1.0

zsoerenm and others added 7 commits July 22, 2026 08:50
Process each measurement in fixed-size time chunks. Within a chunk the NCO
Doppler is held fixed and every completed correlator output is collected into a
per-signal buffer, each tagged with the sample index at which it was taken; the
estimator then folds over them in order and updates every satellite's NCO once,
at a common epoch.

- New `CorrelatorOutput{C}` and a reused `TrackedSignal.correlator_outputs`
  buffer; drop the now-unused `is_integration_completed` field.
- New `update_interval` kwarg on track/track! (time; default = smallest code
  period across all signals). Chunk boundaries lie on a drift-free per-band time
  grid.
- Correlate phase inner-loops over the chunk and snapshots/records completions;
  estimator fold threads loop-filter state + FLL previous_prompt per record and
  writes the NCO once. Sync phase-snap resets the in-flight accumulator.
- Applied across all four correlate backends (cpu/int16/onebit/twobit); the
  @generated kernels are untouched.

Default behavior is numerically very close but not bit-identical to the old
per-code-period update; the FLL pull-in edge and a long overlay-sync test are
recalibrated. (Both are restored to their historical values by the follow-up
two-pass-chunk commit in this PR.) New test/update_interval.jl covers the
feature. Applied JuliaFormatter 2.8.5.

BREAKING CHANGE: Custom `AbstractDopplerEstimator` implementations must now
fold over each signal's `correlator_outputs` (a `Vector{CorrelatorOutput}`,
to be `empty!`-ed when consumed) instead of checking the removed
`TrackedSignal.is_integration_completed` flag and reading the live
correlator. `TrackedSignal` lost the `is_integration_completed` field and
gained `correlator_outputs`, so its positional constructor arity and
copy-constructor kwargs changed. A bare `downconvert_and_correlate(!)` call
now consumes the whole sample buffer, moving each completed accumulator into
`correlator_outputs` and leaving only the trailing partial integration in the
live correlator — previously one call advanced a single integration step
whose completed accumulator remained readable via `get_correlator`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The field was documented as anchoring the sync phase-snap, but the snap
reads the end-of-chunk sat.code_phase and never consumed it. Remove it
until an actual consumer (e.g. vector tracking) needs it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-bit and two-bit _update_tracked_sat_correlator overloads were
textually identical to the generic AbstractDownconvertAndCorrelator
method — the backend boundary is _correlate_signals, which already
dispatches on the dc type. Delete the copies so the chunk loop exists
once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ent buffer sharing

- _validate_update_interval now throws an ArgumentError explaining that
  update_interval must be a Unitful time (e.g. 1u"ms") instead of a
  cryptic Unitful conversion error on plain numbers.
- The track docstring's shared-scratch list now includes each signal's
  correlator_outputs buffer (aliased between input and returned state,
  also for bare downconvert_and_correlate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records that follow the syncing record within one chunk's fold were
correlated with pre-sync replicas (no secondary-code wipe-off);
accumulating them as if wiped feeds sign-corrupted prompts into the
first post-sync bits. Detect the found-transition inside the driver and
passenger folds and apply such records with skip_bit_buffer = true:
prompt filter, CN0 and loop-filter processing still run, only the
bit-buffer update is skipped. Irrelevant at the default interval; it
protects enlarged update_intervals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Split each chunk into two correlate passes around the estimate:

1. stop_before_partial = true — integrate every completion inside the
   chunk, stopping at the last code-block boundary instead of the
   chunk-clamped trailing partial;
2. after the NCO write — integrate the residue (last boundary to chunk
   end), the head of the NEXT integration, with the updated Doppler.

Every completed integration is thus produced by a single NCO Doppler
and each correction takes effect right at its completing boundary — the
classic per-completion loop timing — while the estimator still runs
once per chunk at a common epoch. A trailing fold after the last chunk
catches the rare completion the updated code Doppler pulls back inside
the final residue.

This removes both behavioral deltas of the single-pass design: the FLL
pull-in edge is back at its historical 240 Hz (fails at 250 Hz) and the
1800-block L1C-P overlay sync locks after exactly one overlay cycle —
both test recalibrations are reverted. Also de-links the
downconvert_and_correlate mention in the track docstring (no @ref
target; fixes the docs CI build).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the estimate-doppler-timing branch from 33e9820 to 3c84501 Compare July 22, 2026 08:50
zsoerenm and others added 3 commits July 22, 2026 08:53
Documents that the repo follows Conventional Commits with rebase merge,
that breaking changes must carry a ! marker plus an explanatory
BREAKING CHANGE footer (it drives the major bump and the release
notes), and that CHANGELOG.md and Project.toml's version are generated
by CI (semantic-release) and must not be edited manually. Also notes
the pinned JuliaFormatter check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'update' alone could mean anything; the kwarg paces the Doppler
estimation / NCO update, so name it that. Internal helpers, tests, and
docs follow suit. Not a breaking change: the kwarg was introduced
earlier in this PR and has never been released.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renaming or removing API introduced in the same unmerged PR is an
ordinary refactor/fix commit; reserve ! + BREAKING CHANGE for changes
affecting users of a published version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the estimate-doppler-timing branch from fb4875a to f51856a Compare July 22, 2026 09:11
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