feat(signals): move to GNSSSignals v4 and track its Galileo E5b/E6 and BeiDou signals - #232
Conversation
…d BeiDou signals
Raises `[compat] GNSSSignals` from "3.7" to "4" and adds the per-signal
support layer for the eleven signals GNSSSignals defines but Tracking had
no methods for — every one of them previously died on `MethodError: no
method matching get_default_correlator`:
Galileo E5b-I, E5b-Q, E6-B, E6-C
BeiDou B1I, B3I, B2b-I, B2a data, B2a pilot, B1C data, B1C pilot
v4's one breaking change, the band rename `B2b` -> `E5b`, needed no code
change here: Tracking never named `B2b`. The Galileo E5b and E6 types
exist only in v4, though, so the floor has to move with them.
Each signal needs only a correlator default, a sync-search buffer width
and a sync detector; the loop-bandwidth defaults, the packed
secondary-code reference and both soft CFAR detectors are already
signal-agnostic and derive from the GNSSSignals accessors, so no generic
detector changed shape — the one generic edit is the packer consolidation
below.
Detector shapes, all reusing the existing shared bodies:
* overlay-period-is-one-symbol (`_detect_secondary_code_sync`) — E5b-I
(CS4, 250 sym/s), B2a data (5-chip, 200 sym/s), B1I / B3I (NH20,
50 sym/s); the E5a-I / CS20 shape at other rates.
* dataless pilot with a 100-chip overlay — E5b-Q, E6-C, B2a pilot;
the E5a-Q shape.
* one symbol per primary period (`_detect_symbol_is_code_block_sync`)
— E6-B, B2b-I (both 1000 sym/s) and B1C data (100 sym/s); the E1B /
L1C-D shape.
* 1800-chip overlay pilot on the hard rotation sweep — B1C pilot,
reusing the `UInt1800` that GPS L1C-P's identically sized overlay
introduced. Like L1C-P it reaches the sweep through the generic
`_packed_secondary_code`: GNSSSignals exposes the overlay as a
`PerPRNSecondaryCode`, which that packer already reads.
Correlator defaults follow the modulation: EarlyPromptLate for the BPSK
(`LOC`) signals, VeryEarlyPromptLate for BeiDou B1C, whose components
are BOC(1,1)-class — the QMBOC(6,1,4/33) pilot's principal 29/33-power
axis is BOC(1,1) — and so need the VEML discriminator against the
split-spectrum side peaks, as GPS L1C and Galileo E1 do.
`_packed_secondary_code` drops to a single generic method. Both per-signal
overrides have been redundant since the `SecondaryCode` hierarchy landed in
GNSSSignals 2.0.0 — nothing about v4 or BeiDou enabled this; adding a second
1800-chip overlay pilot is what surfaced it. `get_secondary_code(GPSL1C_P())`
is a `PerPRNSecondaryCode` whose columns are the very `overlay_codes` matrix
the bespoke L1C-P packer walked, and it produces bit-identical references for
every PRN. GPS L5I's hard-coded `0x035` was worse than redundant — it is
the complement of what the generic packer derives from the NH10 chips, so
L5I's hard path reported the opposite polarity from the convention its own
soft detector, the post-sync replica and every other signal use. With one
packer left, `polarity = +1` means the same thing everywhere: the prompt
signs follow `get_secondary_code`.
BeiDou B1I/B3I keep one documented rough edge. Their NH20 overlay is
carried only by the MEO/IGSO satellites (PRN 6-58, D1 at 50 sym/s); the
GEO ones (PRN 1-5, 59-63) broadcast D2 at 500 sym/s with no overlay,
which GNSSSignals models as an all-ones column. Those two facts cancel the
sync lock together: an all-ones reference is rotation-invariant, so the 20
rotation bins are separated only by the data transitions they straddle,
and at the D2 rate a 20-block bin averages ~10 random symbols, so no
rotation stands out and the CFAR test never accepts. A GEO satellite
therefore tracks and can be ranged on, but stays in the pre-sync one-block
integration regime and decodes no bits — decoding one would need a 2-block
bit-edge search at a per-PRN data rate, where `get_data_frequency` is per
signal type and reports the D1 value (50 Hz) for every PRN. (Were the
overlay absent at the *D1* rate, the same bins would reduce to exactly the
bit-edge search GPS L1 C/A uses and lock on the symbol boundary; no
satellite broadcasts that combination.) `test/beidou_b1i.jl` drives the
live soft path and pins all three cases; `src/beidou/b1i.jl` spells the
reasoning out.
Galileo E5a-QP stays out on purpose, and not as a to-do: it is an
acquisition aid rather than a tracking signal — a dataless BPSK(5)
quasi-pilot whose 330-chip code repeats every 64.5 us, there to let a
receiver find E5a cheaply and hand the satellite over to E5a-I / E5a-Q
("designed to enable low complexity acquisition capability", OS SIS ICD
v2.2 §2.3.1.4). It carries neither data nor a secondary code, so it has no
sync feature at all, and one primary code period — Tracking's integration
and loop-bandwidth unit — is 64.5 us, which makes both meaningless
(`default_carrier_loop_filter_bandwidth` ~279 Hz, and the one-code-block
minimum integration would run the loop at ~15.5 kHz). Tracking it is not a
matter of adding a dispatch method, so it is listed rather than stubbed.
`test/signal_coverage.jl` is the new guard for exactly that judgement: it
discovers the concrete `AbstractGNSSSignal` types from the type tree
rather than a hard-coded list, and asserts each is either fully
supported — correlator, buffer width wide enough for one secondary-code
period, loop bandwidths inside absolute sanity bounds (< 100 Hz, and a
primary-code rate under 2 kHz, both of which E5a-QP fails), and a clean
synthetic signal run end to end through `track` — or listed with a reason.
The exception lists have to stay honest too: a signal cannot be both
listed and supported, and neither list may name a type GNSSSignals no
longer defines. A GNSSSignals release that adds a signal now fails there,
in one place, instead of at the first `get_default_correlator` call in
user code.
Resolving the Acquisition extension alongside GNSSSignals v4 needs
Acquisition >= 2.6.4, which its own compat enforces, so the
`Acquisition = "..., 2"` bound here is left as it is. Verified on top of
8.0.0 with GNSSSignals 4.0.0 + Acquisition 2.6.4: full suite green, 376
test sets, 59 of them added here, formatted with the pinned JuliaFormatter
2.8.5.
Requiring GNSSSignals v4 is a floor bump, not a break of Tracking's own API:
no exported type, function, field or kwarg changes here, so code that compiles
against 8.0 keeps compiling, and a project pinned to GNSSSignals 3.x simply
does not resolve this version and stays on 8.0.x — the same mechanism as the
2.x -> 3 move in `build(deps): migrate to released GNSSSignals 3 + SinCosLUT
3`. Two consequences are worth naming for anyone who does upgrade:
* GNSSSignals v4 drops the `B2b` band in favour of `E5b`, so
`get_band_id(BeiDouB2bI())` is now `:E5b`. Tracking only passes that id
through, but a per-band table keyed off it — including the
`BandMeasurement` NamedTuple keys — has to remap the key. No released
Tracking version could track a signal on that band, so this reaches only
code that spelled the key out by hand.
* A receiver that forces GPS L5I onto the hard sync path with
`Tracking.uses_soft_secondary_code_detection(::GPSL5I) = false` now gets
the opposite `SyncResult.polarity`, since L5I's bespoke NH10 reference —
complementary to every other signal's — is gone. Decoded soft bits flip
sign with it, which the navigation preamble resolves. The default soft
path, and every signal's post-sync replica, are untouched.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #232 +/- ##
==========================================
+ Coverage 98.16% 98.28% +0.11%
==========================================
Files 39 46 +7
Lines 3923 3959 +36
==========================================
+ Hits 3851 3891 +40
+ Misses 72 68 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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 |
|---|---|---|---|---|---|---|---|
| GPS L1CA, 1 sat, 3 signals @ 5 MHz | 7.37 μs | 4.76 μs | 4.34 μs | 6.43 μs | 1.55 ✅ | 1.7 ✅ | 1.15 ✅ |
| GPS L1CA, 1 sat, 4 antennas @ 5 MHz | 7.6 μs | 4.69 μs | 3.56 μs | 5.28 μs | 1.62 ✅ | 2.13 ✅ | 1.44 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 115.0 μs | 41.2 μs | 27.3 μs | 34.6 μs | 2.8 ✅ | 4.23 ✅ | 3.33 ✅ |
| GPS L1CA, 8 sats @ 5 MHz, 100 ms buffer | 1.89 ms | 935.0 μs | 757.0 μs | 869.0 μs | 2.02 ✅ | 2.49 ✅ | 2.17 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 21.3 μs | 12.0 μs | 10.9 μs | 12.8 μs | 1.77 ✅ | 1.95 ✅ | 1.66 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 49.6 μs | 21.6 μs | 2.3 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 3.63 μs | 1.97 μs | 930.0 ns | 1.47 μs | 1.85 ✅ | 3.9 ✅ | 2.47 ✅ |
Time benchmarks (base vs PR head)
Ratio = 8ad24ba… / e420765…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 8ad24ba… | e420765… | 8ad24ba… / e420765… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 1.54 μs | 1.52 μs | 1.01 |
| downconvert and correlate/CPU/Float32 4ant | 2.87 μs | 2.88 μs | 0.994 |
| downconvert and correlate/CPU/Float64 | 1.78 μs | 1.78 μs | 1.0 |
| downconvert and correlate/CPU/Int16 | 1.59 μs | 1.58 μs | 1.01 |
| downconvert and correlate/CPU/Int16 4ant | 2.97 μs | 3.0 μs | 0.991 |
| downconvert and correlate/CPU/Int32 | 1.54 μs | 1.53 μs | 1.0 |
| fused kernel/1-ant dynamic taps | 1.27 μs | 1.25 μs | 1.01 |
| fused kernel/1-ant static taps | 1.12 μs | 1.11 μs | 1.0 |
| fused kernel/4-ant dynamic taps | 3.39 μs | 3.4 μs | 0.998 |
| fused kernel/4-ant static taps | 2.38 μs | 2.38 μs | 0.998 |
| fused tuple kernel/1-ant N=2 | 1.49 μs | 1.49 μs | 1.0 |
| fused tuple kernel/1-ant N=3 | 1.78 μs | 1.78 μs | 0.999 |
| fused tuple kernel/2-ant N=2 | 2.39 μs | 2.39 μs | 1.0 |
| fused tuple kernel/2-ant N=3 | 2.96 μs | 2.96 μs | 0.998 |
| fused tuple kernel/4-ant N=2 | 3.93 μs | 3.95 μs | 0.994 |
| fused tuple kernel/4-ant N=3 | 5.08 μs | 5.09 μs | 0.998 |
| noise estimation/append_noise_observation! – steady state | 27.0 ns | 26.8 ns | 1.0 |
| noise estimation/track! – GPS L1CA, 1 sat, 2K @ 5 MHz with a noise reference | 3.55 μs | 3.5 μs | 1.01 |
| noise estimation/update_noise! – 1 ms @ 20 MHz | 11.2 μs | 11.2 μs | 1.0 |
| noise estimation/update_noise! – 1 ms @ 4 MHz | 2.56 μs | 2.56 μs | 1.0 |
| noise estimation/update_noise! – 1 ms @ 4 MHz, 4 antennas | 5.06 μs | 5.04 μs | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32 | 281.0 μs | 283.0 μs | 0.994 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32, threaded | 136.0 μs | 146.0 μs | 0.929 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32 | 283.0 μs | 284.0 μs | 0.998 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 148.0 μs | 138.0 μs | 1.07 ✅ |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – in-place, Float32 | 7.55 μs | 7.49 μs | 1.01 |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – out-of-place, Float32 | 7.41 μs | 7.38 μs | 1.0 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – in-place, Float32 | 9.92 μs | 9.58 μs | 1.04 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – out-of-place, Float32 | 9.51 μs | 9.53 μs | 0.998 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – in-place, Float32 | 3.55 μs | 3.55 μs | 0.999 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – out-of-place, Float32 | 3.31 μs | 3.3 μs | 1.0 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – in-place, Float32 | 11.9 μs | 11.9 μs | 0.999 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – out-of-place, Float32 | 11.4 μs | 11.4 μs | 1.01 |
| track/GPS L1CA, 1 sat, bit sync found, 2 blk @ 5 MHz – in-place, Float32 | 13.9 μs | 13.9 μs | 1.0 |
| track/GPS L1CA, 1 sat, bit sync found, 20 blk @ 5 MHz – in-place, Float32 | 134.0 μs | 134.0 μs | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 2 blk @ 5 MHz – in-place, Float32 | 14.0 μs | 14.0 μs | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 20 blk @ 5 MHz – in-place, Float32 | 135.0 μs | 135.0 μs | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 200 blk @ 5 MHz – in-place, Float32 | 1.4 ms | 1.4 ms | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, Int16 | 1.39 ms | 1.43 ms | 0.972 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, OneBit | 972.0 μs | 1.01 ms | 0.958 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, TwoBit | 1.3 ms | 1.33 ms | 0.978 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32 | 34.3 μs | 34.5 μs | 0.994 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32, threaded | 20.3 μs | 20.9 μs | 0.972 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Int16 | 18.3 μs | 18.8 μs | 0.977 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, OneBit | 14.2 μs | 14.4 μs | 0.986 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32 | 35.2 μs | 35.4 μs | 0.995 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32, threaded | 21.0 μs | 25.8 μs | 0.811 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32 | 81.7 μs | 81.6 μs | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32, threaded | 48.0 μs | 48.0 μs | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Int16 | 36.3 μs | 36.3 μs | 0.999 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32 | 82.1 μs | 82.3 μs | 0.998 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 48.4 μs | 50.7 μs | 0.955 |
| time_to_load | 108.0 μs | 107.0 μs | 1.01 |
Memory benchmarks (base vs PR head)
Ratio = 8ad24ba… / e420765… (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).
| 8ad24ba… | e420765… | 8ad24ba… / e420765… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 528 B | 2 allocs: 528 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: 528 B | 2 allocs: 528 B | 1.0 |
| downconvert and correlate/CPU/Int16 | 2 allocs: 528 B | 2 allocs: 528 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: 528 B | 2 allocs: 528 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 | — |
| noise estimation/append_noise_observation! – steady state | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/track! – GPS L1CA, 1 sat, 2K @ 5 MHz with a noise reference | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 20 MHz | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 4 MHz | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 4 MHz, 4 antennas | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32, threaded | 4 allocs: 384 B | 4 allocs: 384 B | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32 | 26 allocs: 9840 B | 26 allocs: 9840 B | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 30 allocs: 10224 B | 30 allocs: 10224 B | 1.0 |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 896 B | 9 allocs: 896 B | 1.0 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 1296 B | 9 allocs: 1296 B | 1.0 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – out-of-place, Float32 | 9 allocs: 896 B | 9 allocs: 896 B | 1.0 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 1648 B | 9 allocs: 1648 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync found, 2 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync found, 20 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 2 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 20 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 200 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, Int16 | 62 allocs: 25920 B | 62 allocs: 25920 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, OneBit | 62 allocs: 25808 B | 62 allocs: 25808 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, TwoBit | 62 allocs: 25856 B | 62 allocs: 25856 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32, threaded | 2 allocs: 192 B | 2 allocs: 192 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Int16 | 14 allocs: 1280 B | 14 allocs: 1280 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, OneBit | 14 allocs: 1168 B | 14 allocs: 1168 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32 | 10 allocs: 4280 B | 10 allocs: 4280 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32, threaded | 12 allocs: 4472 B | 12 allocs: 4472 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32, threaded | 2 allocs: 192 B | 2 allocs: 192 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Int16 | 6 allocs: 640 B | 6 allocs: 640 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32 | 10 allocs: 2680 B | 10 allocs: 2680 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 12 allocs: 2872 B | 12 allocs: 2872 B | 1.0 |
| time_to_load | 149 allocs: 11408 B | 149 allocs: 11408 B | 1.0 |
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 |
|---|---|---|---|---|---|---|---|
| GPS L1CA, 1 sat, 3 signals @ 5 MHz | 12.2 μs | 5.97 μs | 5.06 μs | 7.82 μs | 2.04 ✅ | 2.41 ✅ | 1.56 ✅ |
| GPS L1CA, 1 sat, 4 antennas @ 5 MHz | 13.0 μs | 6.8 μs | 5.18 μs | 10.6 μs | 1.91 ✅ | 2.5 ✅ | 1.22 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 151.0 μs | 50.0 μs | 33.2 μs | 56.6 μs | 3.02 ✅ | 4.55 ✅ | 2.66 ✅ |
| GPS L1CA, 8 sats @ 5 MHz, 100 ms buffer | 2.15 ms | 889.0 μs | 675.0 μs | 997.0 μs | 2.42 ✅ | 3.19 ✅ | 2.16 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 24.0 μs | 11.2 μs | 10.0 μs | 14.2 μs | 2.14 ✅ | 2.4 ✅ | 1.69 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 110.0 μs | 44.5 μs | 2.46 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 7.16 μs | 2.49 μs | 1.6 μs | 2.55 μs | 2.87 ✅ | 4.46 ✅ | 2.81 ✅ |
Time benchmarks (base vs PR head)
Ratio = 8ad24ba… / e420765…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 8ad24ba… | e420765… | 8ad24ba… / e420765… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.52 μs | 2.65 μs | 0.951 |
| downconvert and correlate/CPU/Float32 4ant | 4.93 μs | 5.26 μs | 0.938 |
| downconvert and correlate/CPU/Float64 | 2.72 μs | 2.87 μs | 0.947 |
| downconvert and correlate/CPU/Int16 | 2.62 μs | 2.78 μs | 0.945 |
| downconvert and correlate/CPU/Int16 4ant | 4.91 μs | 5.21 μs | 0.943 |
| downconvert and correlate/CPU/Int32 | 2.56 μs | 2.71 μs | 0.947 |
| fused kernel/1-ant dynamic taps | 2.61 μs | 2.62 μs | 0.996 |
| fused kernel/1-ant static taps | 2.1 μs | 2.06 μs | 1.02 |
| fused kernel/4-ant dynamic taps | 8.32 μs | 7.81 μs | 1.06 ✅ |
| fused kernel/4-ant static taps | 4.51 μs | 4.59 μs | 0.983 |
| fused tuple kernel/1-ant N=2 | 3.51 μs | 3.51 μs | 1.0 |
| fused tuple kernel/1-ant N=3 | 3.4 μs | 3.65 μs | 0.933 |
| fused tuple kernel/2-ant N=2 | 6.31 μs | 6.52 μs | 0.967 |
| fused tuple kernel/2-ant N=3 | 5.62 μs | 5.63 μs | 0.999 |
| fused tuple kernel/4-ant N=2 | 11.8 μs | 12.0 μs | 0.986 |
| fused tuple kernel/4-ant N=3 | 10.3 μs | 10.5 μs | 0.98 |
| noise estimation/append_noise_observation! – steady state | 17.5 ns | 16.0 ns | 1.1 ✅ |
| noise estimation/track! – GPS L1CA, 1 sat, 2K @ 5 MHz with a noise reference | 5.44 μs | 5.62 μs | 0.969 |
| noise estimation/update_noise! – 1 ms @ 20 MHz | 21.3 μs | 22.8 μs | 0.938 |
| noise estimation/update_noise! – 1 ms @ 4 MHz | 4.64 μs | 4.95 μs | 0.937 |
| noise estimation/update_noise! – 1 ms @ 4 MHz, 4 antennas | 9.79 μs | 10.1 μs | 0.971 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32 | 546.0 μs | 584.0 μs | 0.935 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32, threaded | 229.0 μs | 228.0 μs | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32 | 547.0 μs | 586.0 μs | 0.934 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 231.0 μs | 230.0 μs | 1.0 |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – in-place, Float32 | 12.4 μs | 12.8 μs | 0.967 |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – out-of-place, Float32 | 12.2 μs | 13.0 μs | 0.942 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – in-place, Float32 | 16.9 μs | 17.8 μs | 0.951 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – out-of-place, Float32 | 16.5 μs | 17.6 μs | 0.939 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – in-place, Float32 | 5.42 μs | 5.62 μs | 0.963 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – out-of-place, Float32 | 5.31 μs | 5.51 μs | 0.963 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – in-place, Float32 | 18.4 μs | 19.0 μs | 0.969 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – out-of-place, Float32 | 17.9 μs | 19.2 μs | 0.935 |
| track/GPS L1CA, 1 sat, bit sync found, 2 blk @ 5 MHz – in-place, Float32 | 24.1 μs | 25.6 μs | 0.943 |
| track/GPS L1CA, 1 sat, bit sync found, 20 blk @ 5 MHz – in-place, Float32 | 237.0 μs | 251.0 μs | 0.941 |
| track/GPS L1CA, 1 sat, bit sync pending, 2 blk @ 5 MHz – in-place, Float32 | 24.2 μs | 25.7 μs | 0.942 |
| track/GPS L1CA, 1 sat, bit sync pending, 20 blk @ 5 MHz – in-place, Float32 | 238.0 μs | 253.0 μs | 0.94 |
| track/GPS L1CA, 1 sat, bit sync pending, 200 blk @ 5 MHz – in-place, Float32 | 2.42 ms | 2.58 ms | 0.94 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, Int16 | 2.16 ms | 2.27 ms | 0.952 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, OneBit | 1.42 ms | 1.54 ms | 0.925 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, TwoBit | 2.24 ms | 2.46 ms | 0.91 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32 | 55.9 μs | 59.1 μs | 0.945 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32, threaded | 22.2 μs | 22.0 μs | 1.01 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Int16 | 25.4 μs | 25.5 μs | 0.995 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, OneBit | 18.6 μs | 19.7 μs | 0.947 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32 | 56.0 μs | 59.5 μs | 0.941 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32, threaded | 22.5 μs | 22.3 μs | 1.01 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32 | 165.0 μs | 176.0 μs | 0.934 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32, threaded | 76.2 μs | 75.8 μs | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Int16 | 70.4 μs | 75.7 μs | 0.931 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32 | 165.0 μs | 177.0 μs | 0.934 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 105.0 μs | 104.0 μs | 1.0 |
| time_to_load | 174.0 μs | 134.0 μs | 1.3 ✅ |
Memory benchmarks (base vs PR head)
Ratio = 8ad24ba… / e420765… (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).
| 8ad24ba… | e420765… | 8ad24ba… / e420765… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 528 B | 2 allocs: 528 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: 528 B | 2 allocs: 528 B | 1.0 |
| downconvert and correlate/CPU/Int16 | 2 allocs: 528 B | 2 allocs: 528 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: 528 B | 2 allocs: 528 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 | — |
| noise estimation/append_noise_observation! – steady state | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/track! – GPS L1CA, 1 sat, 2K @ 5 MHz with a noise reference | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 20 MHz | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 4 MHz | 0 allocs: 0 B | 0 allocs: 0 B | — |
| noise estimation/update_noise! – 1 ms @ 4 MHz, 4 antennas | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – in-place, Float32, threaded | 4 allocs: 384 B | 4 allocs: 384 B | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32 | 26 allocs: 10464 B | 26 allocs: 10464 B | 1.0 |
| track/GPS L1CA + Galileo E1B, 8+8 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 30 allocs: 10848 B | 30 allocs: 10848 B | 1.0 |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 1 signal, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 896 B | 9 allocs: 896 B | 1.0 |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 2 signals, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 1296 B | 9 allocs: 1296 B | 1.0 |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 2K @ 5 MHz – out-of-place, Float32 | 9 allocs: 896 B | 9 allocs: 896 B | 1.0 |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 1 sat, 3 signals, 5K @ 5 MHz – out-of-place, Float32 | 9 allocs: 1648 B | 9 allocs: 1648 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync found, 2 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync found, 20 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 2 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 20 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 1 sat, bit sync pending, 200 blk @ 5 MHz – in-place, Float32 | 7 allocs: 384 B | 7 allocs: 384 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, Int16 | 62 allocs: 28928 B | 62 allocs: 28928 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, OneBit | 62 allocs: 28816 B | 62 allocs: 28816 B | 1.0 |
| track/GPS L1CA, 8 sats, 500K @ 5 MHz – in-place, TwoBit | 62 allocs: 28864 B | 62 allocs: 28864 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Float32, threaded | 2 allocs: 192 B | 2 allocs: 192 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, Int16 | 14 allocs: 1280 B | 14 allocs: 1280 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – in-place, OneBit | 14 allocs: 1168 B | 14 allocs: 1168 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32 | 10 allocs: 4656 B | 10 allocs: 4656 B | 1.0 |
| track/GPS L1CA, 8 sats, 5K @ 5 MHz – out-of-place, Float32, threaded | 12 allocs: 4848 B | 12 allocs: 4848 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32 | 0 allocs: 0 B | 0 allocs: 0 B | — |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Float32, threaded | 2 allocs: 192 B | 2 allocs: 192 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – in-place, Int16 | 6 allocs: 640 B | 6 allocs: 640 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32 | 10 allocs: 3056 B | 10 allocs: 3056 B | 1.0 |
| track/Galileo E1B, 4 sats, 25K @ 25 MHz – out-of-place, Float32, threaded | 12 allocs: 3248 B | 12 allocs: 3248 B | 1.0 |
| time_to_load | 200 allocs: 14176 B | 200 allocs: 14176 B | 1.0 |
Adds the per-signal support layer for the eleven signals GNSSSignals defines but Tracking had no methods for — each previously died on
MethodError: no method matching get_default_correlator:[compat] GNSSSignalsmoves"3.7"→"4": BeiDou itself shipped in GNSSSignals 3.9, but the Galileo E5b and E6 types exist only in v4, so the floor has to move with them.How each signal is wired
No generic detector changed shape — every signal delegates to an existing shared body, and the loop-bandwidth defaults, packed secondary-code reference and both soft CFAR detectors already derive from the GNSSSignals accessors:
UInt1800GPS L1C-P introduced.Correlator defaults follow the modulation:
EarlyPromptLatefor the BPSK (LOC) signals,VeryEarlyPromptLatefor BeiDou B1C, whose components are BOC(1,1)-class.Packer consolidation
_packed_secondary_codedrops to a single generic method. Both per-signal overrides had been redundant since theSecondaryCodehierarchy landed in GNSSSignals 2.0.0 — nothing about v4 or BeiDou enabled this; adding a second 1800-chip overlay pilot is what surfaced it.get_secondary_code(GPSL1C_P())is aPerPRNSecondaryCodewrapping the veryoverlay_codesmatrix the bespoke packer walked; bit-identical references for every PRN.0x035was the complement of what the generic packer derives from the NH10 chips, so L5I's hard path reported the opposite polarity from its own soft detector, its post-sync replica, and every other signal. With one packer left,polarity = +1means the same thing everywhere.Not a breaking change
A compat floor bump does not break Tracking's API — no exported type, function, field or kwarg changes, and a project pinned to GNSSSignals 3.x simply does not resolve this version and stays on 8.0.x. Same mechanism as
89f1c93 build(deps): migrate to released GNSSSignals 3 + SinCosLUT 3, which moved the floor across a major boundary and shipped with no release at all. Two consequences for anyone who does upgrade:B2bband forE5b, soget_band_id(BeiDouB2bI())is now:E5band a per-band table keyed off it — includingBandMeasurementkeys — must remap. No released Tracking version could track a signal on that band, so this reaches only hand-written key literals.Tracking.uses_soft_secondary_code_detection(::GPSL5I) = falsenow yields the oppositeSyncResult.polarity. Non-exported, opt-in, and the navigation preamble resolves the sign; the default soft path is untouched.BeiDou GEO satellites — a documented limitation
B1I/B3I carry NH20 only on the MEO/IGSO satellites (PRN 6-58, D1 at 50 sym/s). The GEO ones (PRN 1-5, 59-63) broadcast D2 at 500 sym/s with no overlay, which GNSSSignals models as an all-ones column. Those two facts cancel the sync lock together: an all-ones reference is rotation-invariant, and at the D2 rate a 20-block bin averages ~10 random symbols, so no rotation stands out and the CFAR test never accepts. A GEO satellite tracks and can be ranged on, but stays pre-sync and decodes no bits — that would need a 2-block bit-edge search at a per-PRN data rate, while
get_data_frequencyis per signal type.test/beidou_b1i.jldrives the live soft path and pins all three cases (MEO locks; GEO-at-D1-rate locks; GEO at the real D2 rate never does).Galileo E5a-QP stays out, by design
It is an acquisition aid, not a tracking signal — a dataless BPSK(5) quasi-pilot whose 330-chip code repeats every 64.5 µs so a receiver can find E5a cheaply and hand over to E5a-I/Q. No data, no secondary code, hence no sync feature, and a 64.5 µs primary period makes the period-derived defaults meaningless (~279 Hz carrier bandwidth, ~15.5 kHz loop rate). Listed with a reason rather than stubbed.
Test plan
test/signal_coverage.jlis a new cross-cutting guard: it discovers concreteAbstractGNSSSignaltypes from the type tree rather than a hard-coded list, and asserts each is either fully supported — correlator, buffer width wide enough for one secondary-code period, loop bandwidths inside absolute sanity bounds, and a clean synthetic signal run end to end throughtrack— or listed with a reason. The exception lists have to stay honest too: a signal cannot be both listed and supported, and neither list may name a type GNSSSignals no longer defines. A GNSSSignals release that adds a signal now fails there, in one place, instead of at the firstget_default_correlatorcall in user code.🤖 Generated with Claude Code