feat(track): per-chunk Doppler update with collected correlator outputs#205
Conversation
b596735 to
428f038
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #205 +/- ##
=========================================
Coverage ? 98.24%
=========================================
Files ? 32
Lines ? 3353
Branches ? 0
=========================================
Hits ? 3294
Misses ? 59
Partials ? 0 ☔ 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 | 12.7 μs | 6.26 μs | 5.08 μs | 10.5 μs | 2.03 ✅ | 2.5 ✅ | 1.22 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 369.0 μs | 117.0 μs | 57.3 μs | 108.0 μs | 3.14 ✅ | 6.43 ✅ | 3.4 ✅ |
| GPS L1CA, 8 sats @ 5 MHz, 100 ms buffer | 5.07 ms | 1.87 ms | 1.2 ms | 1.95 ms | 2.71 ✅ | 4.23 ✅ | 2.6 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 56.1 μs | 21.9 μs | 16.2 μs | 24.4 μs | 2.56 ✅ | 3.45 ✅ | 2.3 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 142.0 μs | 59.8 μs | 2.37 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 6.82 μs | 2.45 μs | 1.57 μs | 2.61 μs | 2.78 ✅ | 4.34 ✅ | 2.62 ✅ |
| multi-signal N=3 @ 5 MHz | 11.2 μs | 5.33 μs | 4.48 μs | 7.71 μs | 2.1 ✅ | 2.5 ✅ | 1.45 ✅ |
Time benchmarks (base vs PR head)
Ratio = fb38c1b… / e0a2097…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| fb38c1b… | e0a2097… | fb38c1b… / e0a2097… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.69 μs | 2.59 μs | 1.04 |
| downconvert and correlate/CPU/Float32 4ant | 5.23 μs | 5.12 μs | 1.02 |
| downconvert and correlate/CPU/Float64 | 2.91 μs | 2.78 μs | 1.04 |
| downconvert and correlate/CPU/Int16 | 2.74 μs | 2.72 μs | 1.01 |
| downconvert and correlate/CPU/Int16 4ant | 4.83 μs | 4.88 μs | 0.99 |
| downconvert and correlate/CPU/Int32 | 2.58 μs | 2.64 μs | 0.977 |
| fused kernel/1-ant dynamic taps | 2.56 μs | 2.61 μs | 0.98 |
| fused kernel/1-ant static taps | 2.06 μs | 2.13 μs | 0.965 |
| fused kernel/4-ant dynamic taps | 7.81 μs | 8.05 μs | 0.97 |
| 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 | 0.998 |
| fused tuple kernel/1-ant N=3 | 3.4 μs | 3.41 μs | 0.998 |
| fused tuple kernel/2-ant N=2 | 6.31 μs | 6.31 μs | 1.0 |
| fused tuple kernel/2-ant N=3 | 5.61 μs | 5.62 μs | 0.998 |
| fused tuple kernel/4-ant N=2 | 11.5 μs | 12.0 μs | 0.965 |
| fused tuple kernel/4-ant N=3 | 10.1 μs | 10.1 μs | 0.996 |
| track/1. Float32/2K – track | 2.67 μs | 2.83 μs | 0.943 |
| track/1. Float32/2K – track! | 2.83 μs | 3.0 μs | 0.943 |
| track/2. L1 8sat/5K – track | 51.3 μs | 54.8 μs | 0.938 |
| track/2. L1 8sat/5K – track! | 49.4 μs | 54.5 μs | 0.907 |
| track/2. L1 8sat/5K – track! Int16 | 23.2 μs | 23.2 μs | 1.0 |
| track/2. L1 8sat/5K – track! OneBit | 16.2 μs | 17.8 μs | 0.911 |
| track/2. L1 8sat/5K – track!-threaded | 49.8 μs | 54.5 μs | 0.914 |
| track/2. L1 8sat/5K – track-threaded | 50.1 μs | 54.6 μs | 0.918 |
| track/3. E1B 4sat/25K – track | 140.0 μs | 141.0 μs | 0.994 |
| track/3. E1B 4sat/25K – track! | 140.0 μs | 141.0 μs | 0.994 |
| track/3. E1B 4sat/25K – track! Int16 | 58.2 μs | 59.5 μs | 0.978 |
| track/3. E1B 4sat/25K – track!-threaded | 135.0 μs | 146.0 μs | 0.927 |
| track/3. E1B 4sat/25K – track-threaded | 138.0 μs | 139.0 μs | 0.991 |
| track/4. 8L1+8E1B/25K – track | 507.0 μs | 510.0 μs | 0.994 |
| track/4. 8L1+8E1B/25K – track! | 489.0 μs | 490.0 μs | 0.997 |
| track/4. 8L1+8E1B/25K – track!-threaded | 489.0 μs | 526.0 μs | 0.928 |
| track/4. 8L1+8E1B/25K – track-threaded | 490.0 μs | 509.0 μs | 0.962 |
| track/5. multi-signal N=1/5K – track | 6.41 μs | 6.61 μs | 0.97 |
| track/5. multi-signal N=1/5K – track! | 6.39 μs | 6.82 μs | 0.936 |
| track/6. multi-signal N=2/5K – track | 10.5 μs | 10.7 μs | 0.98 |
| track/6. multi-signal N=2/5K – track! | 11.2 μs | 11.6 μs | 0.968 |
| track/7. L1 8sat/500K – track! Int16 | 1.96 ms | 2.06 ms | 0.949 |
| track/7. L1 8sat/500K – track! OneBit | 1.16 ms | 1.33 ms | 0.876 |
| track/7. L1 8sat/500K – track! TwoBit | 1.92 ms | 2.07 ms | 0.928 |
| track/7. multi-signal N=3/5K – track | 12.0 μs | 12.9 μs | 0.929 |
| track/7. multi-signal N=3/5K – track! | 12.0 μs | 12.6 μs | 0.954 |
| track/8. L1CA presync 2 blk – track! | 12.5 μs | 13.7 μs | 0.912 |
| track/8. L1CA presync 20 blk – track! | 121.0 μs | 132.0 μs | 0.918 |
| track/8. L1CA synced 2 blk – track! | 12.8 μs | 13.6 μs | 0.936 |
| track/8. L1CA synced 20 blk – track! | 119.0 μs | 131.0 μs | 0.911 |
| time_to_load | 3.73 ms | 156.0 μs | 24.0 ✅ |
Memory benchmarks (base vs PR head)
Ratio = fb38c1b… / e0a2097… (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).
| fb38c1b… | e0a2097… | fb38c1b… / e0a2097… | |
|---|---|---|---|
| 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. L1 8sat/500K – track! Int16 | 61 allocs: 28704 B | 61 allocs: 28704 B | 1.0 |
| track/7. L1 8sat/500K – track! OneBit | 1677 allocs: 119088 B | 1677 allocs: 119088 B | 1.0 |
| track/7. L1 8sat/500K – track! TwoBit | 1677 allocs: 119136 B | 1677 allocs: 119136 B | 1.0 |
| 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.0 μs | 7.34 μs | 5.91 μs | 10.8 μs | 1.64 ✅ | 2.04 ✅ | 1.11 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 338.0 μs | 136.0 μs | 63.8 μs | 116.0 μs | 2.49 ✅ | 5.3 ✅ | 2.92 ✅ |
| GPS L1CA, 8 sats @ 5 MHz, 100 ms buffer | 4.78 ms | 2.41 ms | 1.45 ms | 2.41 ms | 1.98 ✅ | 3.31 ✅ | 1.98 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 51.7 μs | 28.8 μs | 18.8 μs | 27.5 μs | 1.79 ✅ | 2.75 ✅ | 1.88 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 126.0 μs | 59.9 μs | 2.11 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 6.31 μs | 3.55 μs | 1.77 μs | 2.66 μs | 1.78 ✅ | 3.55 ✅ | 2.37 ✅ |
| multi-signal N=3 @ 5 MHz | 10.8 μs | 6.54 μs | 5.73 μs | 8.35 μs | 1.66 ✅ | 1.89 ✅ | 1.3 ✅ |
Time benchmarks (base vs PR head)
Ratio = fb38c1b… / e0a2097…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| fb38c1b… | e0a2097… | fb38c1b… / e0a2097… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.37 μs | 2.41 μs | 0.982 |
| downconvert and correlate/CPU/Float32 4ant | 4.41 μs | 4.43 μs | 0.996 |
| downconvert and correlate/CPU/Float64 | 2.77 μs | 2.8 μs | 0.991 |
| downconvert and correlate/CPU/Int16 | 2.47 μs | 2.5 μs | 0.987 |
| downconvert and correlate/CPU/Int16 4ant | 4.86 μs | 4.92 μs | 0.989 |
| downconvert and correlate/CPU/Int32 | 2.4 μs | 2.43 μs | 0.989 |
| fused kernel/1-ant dynamic taps | 2.29 μs | 2.3 μs | 0.997 |
| fused kernel/1-ant static taps | 1.91 μs | 1.91 μs | 0.999 |
| fused kernel/4-ant dynamic taps | 5.73 μs | 5.48 μs | 1.04 |
| fused kernel/4-ant static taps | 3.88 μs | 3.87 μs | 1.0 |
| fused tuple kernel/1-ant N=2 | 2.56 μs | 2.63 μs | 0.976 |
| fused tuple kernel/1-ant N=3 | 3.07 μs | 3.13 μs | 0.98 |
| fused tuple kernel/2-ant N=2 | 3.94 μs | 3.92 μs | 1.0 |
| fused tuple kernel/2-ant N=3 | 4.97 μs | 5.0 μs | 0.995 |
| fused tuple kernel/4-ant N=2 | 6.43 μs | 6.46 μs | 0.994 |
| fused tuple kernel/4-ant N=3 | 10.2 μs | 10.2 μs | 1.0 |
| track/1. Float32/2K – track | 2.57 μs | 2.81 μs | 0.916 |
| track/1. Float32/2K – track! | 2.68 μs | 2.88 μs | 0.933 |
| track/2. L1 8sat/5K – track | 48.2 μs | 49.8 μs | 0.967 |
| track/2. L1 8sat/5K – track! | 47.3 μs | 48.9 μs | 0.968 |
| track/2. L1 8sat/5K – track! Int16 | 26.6 μs | 28.4 μs | 0.938 |
| track/2. L1 8sat/5K – track! OneBit | 17.4 μs | 19.2 μs | 0.903 |
| track/2. L1 8sat/5K – track!-threaded | 47.5 μs | 49.1 μs | 0.967 |
| track/2. L1 8sat/5K – track-threaded | 48.5 μs | 49.7 μs | 0.976 |
| track/3. E1B 4sat/25K – track | 121.0 μs | 122.0 μs | 0.992 |
| track/3. E1B 4sat/25K – track! | 121.0 μs | 121.0 μs | 0.998 |
| track/3. E1B 4sat/25K – track! Int16 | 59.0 μs | 59.5 μs | 0.992 |
| track/3. E1B 4sat/25K – track!-threaded | 121.0 μs | 122.0 μs | 0.991 |
| track/3. E1B 4sat/25K – track-threaded | 121.0 μs | 122.0 μs | 0.992 |
| track/4. 8L1+8E1B/25K – track | 444.0 μs | 445.0 μs | 0.998 |
| track/4. 8L1+8E1B/25K – track! | 440.0 μs | 443.0 μs | 0.994 |
| track/4. 8L1+8E1B/25K – track!-threaded | 441.0 μs | 444.0 μs | 0.993 |
| track/4. 8L1+8E1B/25K – track-threaded | 443.0 μs | 445.0 μs | 0.995 |
| track/5. multi-signal N=1/5K – track | 6.16 μs | 6.38 μs | 0.966 |
| track/5. multi-signal N=1/5K – track! | 6.16 μs | 6.35 μs | 0.971 |
| track/6. multi-signal N=2/5K – track | 8.9 μs | 9.43 μs | 0.943 |
| track/6. multi-signal N=2/5K – track! | 9.06 μs | 9.51 μs | 0.953 |
| track/7. L1 8sat/500K – track! Int16 | 2.23 ms | 2.47 ms | 0.904 |
| track/7. L1 8sat/500K – track! OneBit | 1.33 ms | 1.46 ms | 0.905 |
| track/7. L1 8sat/500K – track! TwoBit | 2.09 ms | 2.38 ms | 0.879 |
| track/7. multi-signal N=3/5K – track | 11.3 μs | 12.2 μs | 0.925 |
| track/7. multi-signal N=3/5K – track! | 11.7 μs | 12.3 μs | 0.947 |
| track/8. L1CA presync 2 blk – track! | 11.6 μs | 12.0 μs | 0.968 |
| track/8. L1CA presync 20 blk – track! | 112.0 μs | 116.0 μs | 0.966 |
| track/8. L1CA synced 2 blk – track! | 11.5 μs | 11.9 μs | 0.965 |
| track/8. L1CA synced 20 blk – track! | 111.0 μs | 115.0 μs | 0.966 |
| time_to_load | 122.0 μs | 121.0 μs | 1.01 |
Memory benchmarks (base vs PR head)
Ratio = fb38c1b… / e0a2097… (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).
| fb38c1b… | e0a2097… | fb38c1b… / e0a2097… | |
|---|---|---|---|
| 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. L1 8sat/500K – track! Int16 | 61 allocs: 25696 B | 61 allocs: 25696 B | 1.0 |
| track/7. L1 8sat/500K – track! OneBit | 1677 allocs: 116080 B | 1677 allocs: 116080 B | 1.0 |
| track/7. L1 8sat/500K – track! TwoBit | 1677 allocs: 116128 B | 1677 allocs: 116128 B | 1.0 |
| 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 |
fb4875a to
f51856a
Compare
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>
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>
The one-/two-bit backends re-packed the WHOLE measurement buffer's
shared band planes on every downconvert_and_correlate! call (>1 sat),
i.e. twice per chunk under the two-pass loop — O(chunks x buffer)
work for an O(buffer) job. New samples_unchanged kwarg (default false)
promises the buffers hold the same content as the previous call with
this dc, letting backends reuse sample-derived caches; track! passes it
on every pass after the very first, so the pack now happens exactly
once per call. The float backends ignore it.
Benchmark (100 ms Complex{Int16} capture, 8 sats, GPS L1CA, 5 MHz,
default 1 ms chunks, single-threaded backends, best of 10):
one-bit: 3.65 ms -> 0.63 ms (~5.8x)
two-bit: 5.16 ms -> 0.84 ms (~6.1x)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15e3f0f to
6434c8f
Compare
The two-pass chunk correlated each code period in two kernel windows (completions up to the boundary, then the residue to the chunk end), doubling per-window fixed costs — the new 100 ms benchmark rows showed ~20-25 % regressions vs master across Int16/OneBit/TwoBit. But the residue and the next chunk's completions are contiguous samples at the SAME (just-updated) NCO Doppler, so the residue pass is unnecessary: run only the stop_before_partial pass each chunk and let the next chunk's pass start at the boundary — each integration is again one boundary-to-boundary kernel window, at the classic per-completion loop timing. The outer loop now walks the chunk grid itself (_chunks_left, replacing the per-sat _all_groups_reached_end), and a final pass after the last chunk drains the buffer's trailing partial into the live accumulator so it carries into the next track! call, followed by one trailing fold for a completion landing exactly on the buffer end. Semantics are unchanged — identical samples integrated at identical Dopplers: the FLL pull-in edge stays at 240/250 Hz and the strict 1800-block L1C-P overlay-sync assertions hold. Local 8-sat 100 ms capture: OneBit 2.0 -> 0.60 ms, TwoBit 2.8 -> 0.79 ms — back to within ~7 % of the (non-chunked) master loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…list rendering - Document the chunk_index / chunk_duration / stop_before_partial kwargs in the downconvert_and_correlate! docstring (they were added without docs and are exercised by tests and the custom-estimator guide). - Drop the stale 'boundary code phase' mention from the correlator_outputs field comment — the CorrelatorOutput.code_phase field was removed earlier in this PR. - Add the missing blank line after the doppler_update_interval bullet in docs/src/track.md so the coherent-integration paragraph no longer lazy-continues into the list item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ength The 1/N loop-bandwidth scaling exists to hold the loop's BL·Δt stability product at its single-code-period value, so N must be the number of code blocks a correlator-output record ACTUALLY covered — recovered from its sample count, like the bit-buffer credit (issue #125) — not the intended integration length from calc_num_code_blocks_to_integrate. Scaling by the intent under-gained the loop wherever the two disagree: - records folded after a mid-fold sync detection: correlated pre-sync as single blocks, but the live bit buffer already reports the post-sync length (an enlarged doppler_update_interval with preferred_num_code_blocks_to_integrate > 1 could divide the bandwidth by N for a one-block record); - the first post-sync integration, truncated to land on the data-bit boundary (bandwidth divided by the full N for a k < N record). The divisor is floored at 1 for the fractional-block record right after a sync phase-snap accumulator reset, whose rounded block count can be 0. For records whose actual length matches the intent — every record at the default preferred length of 1 — the update is bit-identical to before. The new test asserts the update depends only on the record itself: a synced one-block record with preferred = 20 matches the plain single-block baseline, and a 20-block record is scaled identically whatever the preferred length. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a45a677 to
e0a2097
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