Skip to content

fix: combined pilot+data decode for quadrature signals (GPS L5, Galileo E5a)#201

Merged
siebc merged 1 commit into
masterfrom
sc/quadrature-decode
Jul 15, 2026
Merged

fix: combined pilot+data decode for quadrature signals (GPS L5, Galileo E5a)#201
siebc merged 1 commit into
masterfrom
sc/quadrature-decode

Conversation

@giove-a

@giove-a giove-a commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

In a combined tracking group the loops lock the driver (signals[1], the pilot) onto the real axis. A QPSK data component sitting on the orthogonal carrier (GPS L5, Galileo E5a) therefore lands on the imaginary axis, so its real(prompt) bit decision collapses to noise and the navigation message never syncs.

This PR de-rotates each component's bit-buffer prompt onto the driver's phase frame before the secondary/bit-sync search and coherent symbol accumulation:

prompt *= cis(get_carrier_phase_offset(driver) - get_carrier_phase_offset(signal))
  • Driver, co-phased pairs (E1B/E1C), and data-only satscis(0) === 1 + 0im, a bit-identical no-op. Pre-fix behaviour is preserved exactly.
  • Quadrature component (GPS L5 I/Q, Galileo E5a I/Q) → rotates ±90° back onto the real axis; the residual sign is resolved downstream by the nav decoder's preamble.

Changes

  • src/conventional_pll_and_dll.jl — add _carrier_phase_derotation, thread the driver's carrier phase through the driver and passenger signal paths, and apply the de-rotation to the bit-buffer prompt in the shared per-signal advance.
  • Project.toml — bump GNSSSignals compat floor to 3.6 (introduces get_carrier_phase_offset).
  • test/carrier_phase.jl (+ test/runtests.jl) — new tests.

Tests

test/carrier_phase.jl covers:

  • GPS L5 and Galileo E5a quadrature pairs rotate imaginary-axis data back onto the real axis with magnitude preserved.
  • No-op cases: driver-against-itself, co-phased CBOC pair (E1C/E1B), and single-signal data-only sats (L5I, L1CA) — verified bit-for-bit.
  • Symmetry: the de-rotation is relative to whichever signal drives the loop (signals[1]), not hard-coded to "the pilot" — reversing the driver/passenger roles stays correct.

Notes

Requires GNSSSignals 3.6.

🤖 Generated with Claude Code

@giove-a
giove-a force-pushed the sc/quadrature-decode branch from c325777 to c35eec6 Compare July 14, 2026 06:30
@siebc
siebc requested a review from zsoerenm July 14, 2026 06:35
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.63%. Comparing base (ce2e56e) to head (6f589e2).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #201      +/-   ##
==========================================
- Coverage   97.87%   97.63%   -0.25%     
==========================================
  Files          32       32              
  Lines        3296     3299       +3     
==========================================
- Hits         3226     3221       -5     
- Misses         70       78       +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 14, 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.5 μs 5.88 μs 4.01 μs 8.44 μs 1.95 ✅ 2.86 ✅ 1.36 ✅
GPS L1CA, 8 sats @ 40 MHz 367.0 μs 117.0 μs 59.9 μs 113.0 μs 3.14 ✅ 6.13 ✅ 3.24 ✅
GPS L1CA, 8 sats @ 5 MHz 52.7 μs 20.4 μs 15.0 μs 23.9 μs 2.58 ✅ 3.5 ✅ 2.2 ✅
Galileo E1B, 4 sats @ 25 MHz 141.0 μs 58.3 μs 2.42 ✅
dynamic taps @ 5 MHz (kernel) 6.68 μs 2.31 μs 1.49 μs 2.56 μs 2.9 ✅ 4.48 ✅ 2.61 ✅
multi-signal N=3 @ 5 MHz 10.4 μs 4.91 μs 3.64 μs 5.98 μs 2.12 ✅ 2.87 ✅ 1.74 ✅
Time benchmarks (base vs PR head)

Ratio = ce2e56e… / 6f589e2…: >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).

ce2e56e 6f589e2 ce2e56e… / 6f589e2
downconvert and correlate/CPU/Float32 2.66 μs 2.48 μs 1.07 ✅
downconvert and correlate/CPU/Float32 4ant 5.06 μs 4.86 μs 1.04
downconvert and correlate/CPU/Float64 2.9 μs 2.68 μs 1.08 ✅
downconvert and correlate/CPU/Int16 2.63 μs 2.62 μs 1.01
downconvert and correlate/CPU/Int16 4ant 5.18 μs 4.81 μs 1.08 ✅
downconvert and correlate/CPU/Int32 2.73 μs 2.54 μs 1.08 ✅
fused kernel/1-ant dynamic taps 2.74 μs 2.65 μs 1.03
fused kernel/1-ant static taps 2.06 μs 2.06 μs 1.0
fused kernel/4-ant dynamic taps 7.84 μs 7.77 μs 1.01
fused kernel/4-ant static taps 4.6 μs 4.43 μs 1.04
fused tuple kernel/1-ant N=2 3.51 μs 3.51 μs 1.0
fused tuple kernel/1-ant N=3 3.53 μs 3.52 μs 1.0
fused tuple kernel/2-ant N=2 6.52 μs 6.31 μs 1.03
fused tuple kernel/2-ant N=3 5.63 μs 5.61 μs 1.0
fused tuple kernel/4-ant N=2 12.0 μs 11.5 μs 1.04
fused tuple kernel/4-ant N=3 10.8 μs 10.4 μs 1.04
track/1. Float32/2K – track 2.78 μs 2.63 μs 1.05 ✅
track/1. Float32/2K – track! 2.93 μs 2.73 μs 1.07 ✅
track/2. L1 8sat/5K – track 53.2 μs 49.2 μs 1.08 ✅
track/2. L1 8sat/5K – track! 52.9 μs 49.0 μs 1.08 ✅
track/2. L1 8sat/5K – track! Int16 20.4 μs 20.5 μs 0.998
track/2. L1 8sat/5K – track! OneBit 14.9 μs 15.0 μs 0.994
track/2. L1 8sat/5K – track!-threaded 49.7 μs 49.5 μs 1.01
track/2. L1 8sat/5K – track-threaded 49.9 μs 49.8 μs 1.0
track/3. E1B 4sat/25K – track 136.0 μs 136.0 μs 1.0
track/3. E1B 4sat/25K – track! 135.0 μs 135.0 μs 1.0
track/3. E1B 4sat/25K – track! Int16 58.6 μs 58.0 μs 1.01
track/3. E1B 4sat/25K – track!-threaded 135.0 μs 135.0 μs 0.999
track/3. E1B 4sat/25K – track-threaded 136.0 μs 136.0 μs 1.0
track/4. 8L1+8E1B/25K – track 490.0 μs 489.0 μs 1.0
track/4. 8L1+8E1B/25K – track! 489.0 μs 488.0 μs 1.0
track/4. 8L1+8E1B/25K – track!-threaded 506.0 μs 488.0 μs 1.04
track/4. 8L1+8E1B/25K – track-threaded 490.0 μs 489.0 μs 1.0
track/5. multi-signal N=1/5K – track 6.9 μs 6.37 μs 1.08 ✅
track/5. multi-signal N=1/5K – track! 6.38 μs 6.39 μs 0.999
track/6. multi-signal N=2/5K – track 10.4 μs 10.3 μs 1.01
track/6. multi-signal N=2/5K – track! 10.5 μs 10.4 μs 1.01
track/7. multi-signal N=3/5K – track 12.4 μs 11.6 μs 1.07 ✅
track/7. multi-signal N=3/5K – track! 12.0 μs 11.5 μs 1.04
track/8. L1CA presync 2 blk – track! 12.4 μs 12.3 μs 1.0
track/8. L1CA presync 20 blk – track! 120.0 μs 120.0 μs 1.0
track/8. L1CA synced 2 blk – track! 12.2 μs 12.2 μs 1.0
track/8. L1CA synced 20 blk – track! 119.0 μs 118.0 μs 1.0
time_to_load 141.0 μs 171.0 μs 0.828 ⚠️
Memory benchmarks (base vs PR head)

Ratio = ce2e56e… / 6f589e2… (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).

ce2e56e 6f589e2 ce2e56e… / 6f589e2
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: 848 B 1.0
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: 848 B 1.0
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: 10464 B 1.0
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: 10464 B 1.0
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 14, 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.2 μs 6.16 μs 5.22 μs 9.73 μs 1.98 ✅ 2.34 ✅ 1.26 ✅
GPS L1CA, 8 sats @ 40 MHz 331.0 μs 130.0 μs 71.3 μs 120.0 μs 2.54 ✅ 4.64 ✅ 2.77 ✅
GPS L1CA, 8 sats @ 5 MHz 49.3 μs 28.2 μs 20.9 μs 27.9 μs 1.75 ✅ 2.36 ✅ 1.77 ✅
Galileo E1B, 4 sats @ 25 MHz 122.0 μs 54.7 μs 2.23 ✅
dynamic taps @ 5 MHz (kernel) 5.79 μs 3.43 μs 2.03 μs 2.82 μs 1.69 ✅ 2.85 ✅ 2.05 ✅
multi-signal N=3 @ 5 MHz 9.54 μs 5.87 μs 5.06 μs 7.13 μs 1.62 ✅ 1.88 ✅ 1.34 ✅
Time benchmarks (base vs PR head)

Ratio = ce2e56e… / 6f589e2…: >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).

ce2e56e 6f589e2 ce2e56e… / 6f589e2
downconvert and correlate/CPU/Float32 2.38 μs 2.35 μs 1.01
downconvert and correlate/CPU/Float32 4ant 4.71 μs 4.75 μs 0.991
downconvert and correlate/CPU/Float64 2.77 μs 2.76 μs 1.01
downconvert and correlate/CPU/Int16 2.43 μs 2.43 μs 1.0
downconvert and correlate/CPU/Int16 4ant 5.18 μs 5.21 μs 0.995
downconvert and correlate/CPU/Int32 2.42 μs 2.42 μs 0.998
fused kernel/1-ant dynamic taps 2.17 μs 2.2 μs 0.984
fused kernel/1-ant static taps 1.88 μs 1.88 μs 0.999
fused kernel/4-ant dynamic taps 5.42 μs 5.3 μs 1.02
fused kernel/4-ant static taps 4.18 μs 4.16 μs 1.0
fused tuple kernel/1-ant N=2 2.35 μs 2.41 μs 0.973
fused tuple kernel/1-ant N=3 2.81 μs 2.81 μs 1.0
fused tuple kernel/2-ant N=2 3.6 μs 3.6 μs 1.0
fused tuple kernel/2-ant N=3 4.67 μs 4.54 μs 1.03
fused tuple kernel/4-ant N=2 6.03 μs 6.07 μs 0.994
fused tuple kernel/4-ant N=3 9.77 μs 9.85 μs 0.992
track/1. Float32/2K – track 2.58 μs 2.68 μs 0.965
track/1. Float32/2K – track! 2.69 μs 2.6 μs 1.04
track/2. L1 8sat/5K – track 47.9 μs 47.8 μs 1.0
track/2. L1 8sat/5K – track! 47.6 μs 47.1 μs 1.01
track/2. L1 8sat/5K – track! Int16 28.5 μs 27.7 μs 1.03
track/2. L1 8sat/5K – track! OneBit 20.3 μs 20.0 μs 1.02
track/2. L1 8sat/5K – track!-threaded 47.7 μs 47.4 μs 1.0
track/2. L1 8sat/5K – track-threaded 48.1 μs 48.1 μs 0.999
track/3. E1B 4sat/25K – track 119.0 μs 119.0 μs 1.0
track/3. E1B 4sat/25K – track! 118.0 μs 118.0 μs 1.0
track/3. E1B 4sat/25K – track! Int16 54.6 μs 54.7 μs 0.998
track/3. E1B 4sat/25K – track!-threaded 118.0 μs 118.0 μs 0.998
track/3. E1B 4sat/25K – track-threaded 119.0 μs 119.0 μs 1.0
track/4. 8L1+8E1B/25K – track 436.0 μs 437.0 μs 0.998
track/4. 8L1+8E1B/25K – track! 433.0 μs 433.0 μs 1.0
track/4. 8L1+8E1B/25K – track!-threaded 434.0 μs 434.0 μs 0.999
track/4. 8L1+8E1B/25K – track-threaded 436.0 μs 436.0 μs 1.0
track/5. multi-signal N=1/5K – track 6.16 μs 6.14 μs 1.0
track/5. multi-signal N=1/5K – track! 6.19 μs 6.14 μs 1.01
track/6. multi-signal N=2/5K – track 8.4 μs 8.41 μs 0.999
track/6. multi-signal N=2/5K – track! 8.6 μs 8.53 μs 1.01
track/7. multi-signal N=3/5K – track 10.8 μs 10.7 μs 1.01
track/7. multi-signal N=3/5K – track! 10.9 μs 10.9 μs 1.0
track/8. L1CA presync 2 blk – track! 11.6 μs 11.7 μs 0.996
track/8. L1CA presync 20 blk – track! 114.0 μs 112.0 μs 1.01
track/8. L1CA synced 2 blk – track! 11.5 μs 11.6 μs 0.996
track/8. L1CA synced 20 blk – track! 112.0 μs 111.0 μs 1.02
time_to_load 102.0 μs 113.0 μs 0.905 ⚠️
Memory benchmarks (base vs PR head)

Ratio = ce2e56e… / 6f589e2… (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).

ce2e56e 6f589e2 ce2e56e… / 6f589e2
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: 848 B 1.0
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: 848 B 1.0
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: 4536 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: 4536 B 10 allocs: 4536 B 1.0
track/3. E1B 4sat/25K – track 10 allocs: 2808 B 10 allocs: 2808 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: 2808 B 10 allocs: 2808 B 1.0
track/4. 8L1+8E1B/25K – track 26 allocs: 10352 B 26 allocs: 10352 B 1.0
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: 10352 B 1.0
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

…eo E5a)

In a combined group the loops lock the driver (signals[1], the pilot) onto
the real axis, so a QPSK data component on the orthogonal carrier (GPS L5,
Galileo E5a) lands on the imaginary axis and its real(prompt) bit decision
collapses to noise — the nav message never syncs.

De-rotate each component's bit-buffer prompt onto the driver's frame before
the sync search and symbol accumulation:

    prompt *= cis(get_carrier_phase_offset(driver) - get_carrier_phase_offset(signal))

A no-op (cis(0)) for the driver, co-phased pairs (E1B/E1C) and data-only
sats; a quadrature component rotates ±90° onto the real axis, sign resolved
downstream by the decoder. Requires GNSSSignals 3.6 (introduces
get_carrier_phase_offset); compat floor pinned. Adds test/carrier_phase.jl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@giove-a
giove-a force-pushed the sc/quadrature-decode branch from c35eec6 to 6f589e2 Compare July 15, 2026 07:35
@siebc
siebc merged commit 32f7178 into master Jul 15, 2026
12 checks passed
@siebc
siebc deleted the sc/quadrature-decode branch July 15, 2026 07:53
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.

2 participants