feat(track): per-chunk Doppler update with collected correlator outputs#205
feat(track): per-chunk Doppler update with collected correlator outputs#205zsoerenm wants to merge 10 commits into
Conversation
b596735 to
428f038
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
Benchmark Results (minimum time) — macos-14Reporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| 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,
| 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, ∞/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 |
Benchmark Results (minimum time) — ubuntu-latestReporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| 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,
| 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, ∞/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 |
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>
33e9820 to
3c84501
Compare
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>
fb4875a to
f51856a
Compare
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:
doppler_update_intervalkwarg (a time; defaultnothing⇒ smallest code period across all signals).What changed
CorrelatorOutput{C}(correlator, integrated_samples, sample_index);TrackedSignalgains a preallocated, reusedcorrelator_outputsbuffer. Both exported. Removed the now-unusedis_integration_completedfield._update_tracked_sat_correlatorinner-loops over the chunk;update/_build_new_signalssnapshot 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 genericAbstractDownconvertAndCorrelatormethod — the backend boundary is_correlate_signals(CPU-fused, Int16, one-bit, two-bit; single + threaded); the@generatedkernels are untouched.track!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 directdownconvert_and_correlate(!)callersstop_before_partialdefaults tofalse(consume the whole window, as before).correlator_outputs, threading loop-filter state + FLLprevious_promptper record (per-recordintegration_time/1/N), writing the NCO once. Preserves the QPSKdriver_carrier_phasede-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 withskip_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 enlargeddoppler_update_intervals).track!/track— newdoppler_update_intervalkwarg, chunked two-pass loop, validated one-sample floor, and a clearArgumentErrorwhendoppler_update_intervalis passed without time units.docs/plans/, plustrack.md/custom_doppler_estimator.md/correlator.md; thetrackdocstring documents thatcorrelator_outputs(likefiltered_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
Pkg.test()passes; docs build cleanly; JuliaFormatter 2.8.5 clean.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.track!allocation test (measure_track_alloc) passes at 0 bytes with chunking.🤖 Generated with Claude Code