Skip to content

feat(signals): move to GNSSSignals v4 and track its Galileo E5b/E6 and BeiDou signals - #232

Merged
siebc merged 1 commit into
masterfrom
sc/gnss-signals-4
Aug 21, 2026
Merged

feat(signals): move to GNSSSignals v4 and track its Galileo E5b/E6 and BeiDou signals#232
siebc merged 1 commit into
masterfrom
sc/gnss-signals-4

Conversation

@giove-a

@giove-a giove-a commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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:

Constellation Signals
Galileo E5b-I, E5b-Q, E6-B, E6-C
BeiDou B1I, B3I, B2b-I, B2a data, B2a pilot, B1C data, B1C pilot

[compat] GNSSSignals moves "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:

  • overlay period = one symbol — E5b-I (CS4, 250 sym/s), B2a data (5-chip, 200 sym/s), B1I / B3I (NH20, 50 sym/s); the E5a-I shape at other rates.
  • dataless pilot, 100-chip overlay — E5b-Q, E6-C, B2a pilot; the E5a-Q shape.
  • one symbol per primary period — E6-B, B2b-I (1000 sym/s), B1C data (100 sym/s); the E1B / L1C-D shape.
  • 1800-chip overlay pilot on the hard rotation sweep — B1C pilot, reusing the UInt1800 GPS L1C-P introduced.

Correlator defaults follow the modulation: EarlyPromptLate for the BPSK (LOC) signals, VeryEarlyPromptLate for BeiDou B1C, whose components are BOC(1,1)-class.

Packer consolidation

_packed_secondary_code drops to a single generic method. Both per-signal overrides had 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.

  • GPS L1C-Pget_secondary_code(GPSL1C_P()) is a PerPRNSecondaryCode wrapping the very overlay_codes matrix the bespoke packer walked; bit-identical references for every PRN.
  • GPS L5I — the hard-coded 0x035 was 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 = +1 means 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:

  • GNSSSignals v4 drops the B2b band for E5b, so get_band_id(BeiDouB2bI()) is now :E5b and a per-band table keyed off it — including BandMeasurement keys — must remap. No released Tracking version could track a signal on that band, so this reaches only hand-written key literals.
  • Forcing L5I onto the hard path with Tracking.uses_soft_secondary_code_detection(::GPSL5I) = false now yields the opposite SyncResult.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_frequency is per signal type. test/beidou_b1i.jl drives 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.jl is a new cross-cutting guard: it discovers 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, 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.

  • Full suite green on top of 8.0.0 with GNSSSignals 4.0.0 + Acquisition 2.6.4: 376 test sets, 59 added here.
  • Formatted with the pinned JuliaFormatter 2.8.5.

🤖 Generated with Claude Code

…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

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.28%. Comparing base (8ad24ba) to head (e420765).
⚠️ Report is 2 commits behind head on master.

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.
📢 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

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
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, ⚠️ ≥ 5 % slower. 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 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, ⚠️ ≥ 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).

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

@github-actions

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
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, ⚠️ ≥ 5 % slower. 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.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, ⚠️ ≥ 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).

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

@siebc
siebc merged commit d4d3e96 into master Aug 21, 2026
12 checks passed
@siebc
siebc deleted the sc/gnss-signals-4 branch August 21, 2026 13:43
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