Skip to content

feat: add BeiDou Open Service signals (B1I, B3I, B2b, B2a, B1C)#143

Open
giove-a wants to merge 1 commit into
masterfrom
sc/beidou
Open

feat: add BeiDou Open Service signals (B1I, B3I, B2b, B2a, B1C)#143
giove-a wants to merge 1 commit into
masterfrom
sc/beidou

Conversation

@giove-a

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

Copy link
Copy Markdown
Collaborator

Summary

Adds the BeiDou Open Service signals, generated from the official BeiDou
SIS‑ICDs (http://en.beidou.gov.cn/SYSTEMS/ICD/), following the existing
per‑constellation architecture (AbstractGPSSignal / AbstractGalileoSignal).

New signal types:

Type Band (carrier) Modulation Code Secondary
BeiDouB1I B1I (1561.098 MHz) BPSK(2) 2046‑chip Gold NH20 on D1 (MEO/IGSO)
BeiDouB3I B3I (1268.52 MHz) BPSK(10) 10230‑chip Gold NH20 on D1 (MEO/IGSO)
BeiDouB2bI B2b (1207.14 MHz) BPSK(10) 10230‑chip Gold
BeiDouB2aI / BeiDouB2aQ L5 (1176.45 MHz) BPSK(10) 10230‑chip Gold 5‑bit (data) / 100‑chip per‑SVID Weil (pilot)
BeiDouB1C_D / BeiDouB1C_P L1 (1575.42 MHz) BOC(1,1) 10230‑chip truncated‑Weil — / 1800‑chip per‑SVID Weil

Also adds:

  • AbstractBeiDouSignal supertype.
  • BDT (BeiDou Time) time system — epoch 2006‑01‑01T00:00:00, TAI − 33 s.
  • Bands B1I, B3I, B2b (B1C and B2a reuse the existing L1 / L5 carriers).
  • Exports, docstrings, docs/api.md, and README entries. The get_signal_id /
    get_band_id accessors work for the new types via their nameof defaults
    (:BeiDouB1I, :B1I, :L1 for B1C, :L5 for B2a, …); tests pin these.

Verification against the ICDs

Every ranging code and secondary code is generated directly from the ICD
LFSR/Weil definitions (no vendored code tables) and pinned in the test suite to
the ICDs' own published values:

  • B2a, B2b, B1C — the first‑ and last‑24‑chip octal values in each ICD's
    code table are reproduced exactly for all PRNs (primary + secondary).
  • B3I — the register‑shift table (ICD Table 4‑1) is reproduced for all 63 PRNs
    (the B3I ICD publishes no chip vectors).
  • B1I — its ICD publishes no chip vectors either, so the codes are checked
    structurally (maximal‑length G1/G2 m‑sequences, balanced 2046‑chip Gold codes).

Cross‑checks against other implementations

To close the gap for B1I/B3I (no ICD chip vectors), the generated codes were
additionally cross‑checked against two independent open‑source receivers:

  • PocketSDR (python/sdr_code.py): full chip‑for‑chip match on all
    signal components and every PRN (up to the global chip‑sign convention).
  • GNSS‑SDR (beidou_b1i/b3i_signal_replica.cc): B1I full‑code match, and the
    B1I phase‑select / B3I G2‑init parameter tables match 63/63.

The B3I G1 short‑cycle length (period 8190) follows the ICD's stated "period of
8190 chips", and is confirmed by the ICD's own B2a/B2b last‑24‑chip octal values,
which lie in the post‑8190 repeat region and match this implementation.

Modelling notes

  • NH20 is PRN‑dependent (B1I/B3I). Per the ICD satellite‑type tables, the
    NH20 overlay is applied only on the MEO/IGSO satellites (PRN 6‑58, D1 message);
    the GEO satellites (PRN 1‑5, 59‑63, D2 message) carry no secondary. This is
    exposed as a PerPRNSecondaryCode whose GEO columns are an all‑ones no‑op, so a
    GEO PRN's tiered code equals its primary code (matches PocketSDR's sec_code_B1I).
  • B1C pilot QMBOC(6,1,4/33). Unlike Galileo E1's real CBOC (baked faithfully
    by the LUT, with a _BOC11 approximation variant), QMBOC is a quadrature
    multiplex: its real in‑phase component simply is BOC(1,1). So BeiDouB1C_P
    generates a BOC(1,1) replica — the standard real tracking replica (same as
    PocketSDR) — and there is no separate full/_BOC11 split. The B1C data
    component is exactly BOC(1,1) per the ICD (no approximation).
  • B2b defines 53 codes (PRN 6‑58); other PRN indices generate an all‑zero code.

Compatibility

Purely additive — no existing type, signature, or behaviour changes; every
GPS/Galileo signal produces identical output. Suggested release: minor bump to
3.4.0
(the one caveat is the usual SemVer note that adding exports — here the
short band/time‑system names B1I/B3I/B2b/BDT — can theoretically collide
with a downstream user's own bindings under using GNSSSignals).

Tests

New test/beidou/ suite (per‑signal files + shared ICD verification vectors)
covering code lengths, chip rates, bands, time system, secondary codes, the
ICD‑octal pins above, the B3I shift table, and gen_code! sampling. get_band_id
/ get_signal_id tests extended with BeiDou coverage. Full suite green locally,
including Aqua and the AVX‑512 SDE SIMD‑only path.

🤖 Generated with Claude Code

Add the BeiDou Open Service signals from the official BDS-SIS-ICDs
(http://en.beidou.gov.cn/SYSTEMS/ICD/):

  * BeiDouB1I           (B1, 1561.098 MHz, 2046-chip Gold code, BPSK(2))
  * BeiDouB3I           (B3, 1268.52 MHz, 10230-chip Gold code, BPSK(10))
  * BeiDouB2bI          (B2b, 1207.14 MHz, 10230-chip Gold code, BPSK(10))
  * BeiDouB2aI/B2aQ     (L5, 10230-chip Gold codes, BPSK(10), data+pilot)
  * BeiDouB1C_D/B1C_P   (L1, 10230-chip truncated-Weil codes, BOC(1,1),
                         data+pilot)

Also add the BDT (BeiDou Time) time system, the B1I/B3I/B2b RF bands, and
the AbstractBeiDouSignal supertype.

Every ranging code and secondary code is generated from the ICD LFSR/Weil
definitions and verified against the ICDs' own first/last-24-chip octal
values (B2a, B2b, B1C) and the B3I register-shift table; all codes were
additionally cross-checked chip-for-chip against PocketSDR (all signals)
and GNSS-SDR (B1I, B3I). B1I/B3I apply the NH20 secondary only on the
MEO/IGSO (D1) PRNs 6-58, with no overlay on the GEO (D2) PRNs, matching
the ICD satellite-type tables. The B1C pilot QMBOC(6,1,4/33) is modelled
as its standard BOC(1,1) tracking approximation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.90%. Comparing base (bd4fdcc) to head (447fbbe).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #143      +/-   ##
==========================================
+ Coverage   96.38%   96.90%   +0.52%     
==========================================
  Files          21       27       +6     
  Lines        1272     1488     +216     
==========================================
+ Hits         1226     1442     +216     
  Misses         46       46              

☔ 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 5, 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. Ratio = bd4fdcc… / 447fbbe…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means that benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

Time benchmarks
bd4fdcc 447fbbe bd4fdcc… / 447fbbe
1 ms code generation/GPSL1CA @ 5 MHz 450.0 ns 442.0 ns 1.02
1 ms code generation/GPSL1C_D @ 5 MHz 550.0 ns 550.0 ns 1.0
1 ms code generation/GPSL1C_P @ 15 MHz 1.82 μs 1.82 μs 1.0
1 ms code generation/GPSL2CL @ 2.5 MHz 250.0 ns 258.0 ns 0.968
1 ms code generation/GPSL2CM @ 2.5 MHz 258.0 ns 250.0 ns 1.03
1 ms code generation/GPSL5I @ 40 MHz 4.08 μs 4.08 μs 0.999
1 ms code generation/GPSL5Q @ 40 MHz 4.08 μs 4.07 μs 1.0
1 ms code generation/GalileoE1B @ 15 MHz 1.59 μs 1.6 μs 0.997
1 ms code generation/GalileoE1B_BOC11 @ 5 MHz 550.0 ns 546.0 ns 1.01
1 ms code generation/GalileoE1C @ 15 MHz 1.82 μs 1.82 μs 1.0
1 ms code generation/GalileoE1C_BOC11 @ 5 MHz 633.0 ns 633.0 ns 1.0
1 ms code generation/GalileoE5aI @ 40 MHz 4.07 μs 4.07 μs 0.999
1 ms code generation/GalileoE5aQ @ 40 MHz 4.08 μs 4.09 μs 0.998
2000 sample code generation/GPSL1 @ 2 MHz 225.0 ns 225.0 ns 1.0
2000 sample code generation/GPSL5 @ 20 MHz 238.0 ns 238.0 ns 1.0
2000 sample code generation/GalileoE1B @ 15 MHz 225.0 ns 225.0 ns 1.0
oversampling sweep/02x/4k 442.0 ns 442.0 ns 1.0
oversampling sweep/02x/64k 6.87 μs 6.87 μs 1.0
oversampling sweep/08x/4k 304.0 ns 288.0 ns 1.06 ✅
oversampling sweep/08x/64k 4.05 μs 4.03 μs 1.01
oversampling sweep/128x/4k 79.1 ns 79.1 ns 1.0
oversampling sweep/128x/64k 842.0 ns 808.0 ns 1.04
oversampling sweep/17x/4k 175.0 ns 154.0 ns 1.14 ✅
oversampling sweep/17x/64k 2.13 μs 2.14 μs 0.998
oversampling sweep/24x/4k 121.0 ns 133.0 ns 0.906 ⚠️
oversampling sweep/24x/64k 1.53 μs 1.53 μs 1.0
oversampling sweep/32x/4k 133.0 ns 133.0 ns 1.0
oversampling sweep/32x/64k 1.5 μs 1.49 μs 1.01
oversampling sweep/64x/4k 83.4 ns 87.5 ns 0.953
oversampling sweep/64x/64k 900.0 ns 896.0 ns 1.0
time_to_load 284.0 μs 192.0 μs 1.48 ✅
Memory benchmarks
bd4fdcc 447fbbe
1 ms code generation/GPSL1CA @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL1C_D @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL1C_P @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL2CL @ 2.5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL2CM @ 2.5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL5I @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL5Q @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1B @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1B_BOC11 @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1C @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1C_BOC11 @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE5aI @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE5aQ @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GPSL1 @ 2 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GPSL5 @ 20 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GalileoE1B @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/02x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/02x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/08x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/08x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/128x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/128x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/17x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/17x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/24x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/24x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/32x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/32x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/64x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/64x/64k 0 allocs: 0 B 0 allocs: 0 B
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B

@github-actions

github-actions Bot commented Jul 5, 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. Ratio = bd4fdcc… / 447fbbe…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means that benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

Time benchmarks
bd4fdcc 447fbbe bd4fdcc… / 447fbbe
1 ms code generation/GPSL1CA @ 5 MHz 477.0 ns 472.0 ns 1.01
1 ms code generation/GPSL1C_D @ 5 MHz 631.0 ns 644.0 ns 0.98
1 ms code generation/GPSL1C_P @ 15 MHz 1.94 μs 1.98 μs 0.983
1 ms code generation/GPSL2CL @ 2.5 MHz 274.0 ns 266.0 ns 1.03
1 ms code generation/GPSL2CM @ 2.5 MHz 268.0 ns 265.0 ns 1.01
1 ms code generation/GPSL5I @ 40 MHz 4.52 μs 4.46 μs 1.01
1 ms code generation/GPSL5Q @ 40 MHz 4.41 μs 4.51 μs 0.976
1 ms code generation/GalileoE1B @ 15 MHz 1.76 μs 1.8 μs 0.977
1 ms code generation/GalileoE1B_BOC11 @ 5 MHz 630.0 ns 644.0 ns 0.978
1 ms code generation/GalileoE1C @ 15 MHz 1.94 μs 1.97 μs 0.985
1 ms code generation/GalileoE1C_BOC11 @ 5 MHz 723.0 ns 719.0 ns 1.01
1 ms code generation/GalileoE5aI @ 40 MHz 4.51 μs 4.51 μs 1.0
1 ms code generation/GalileoE5aQ @ 40 MHz 4.42 μs 4.58 μs 0.965
2000 sample code generation/GPSL1 @ 2 MHz 318.0 ns 322.0 ns 0.988
2000 sample code generation/GPSL5 @ 20 MHz 350.0 ns 351.0 ns 0.997
2000 sample code generation/GalileoE1B @ 15 MHz 318.0 ns 331.0 ns 0.961
oversampling sweep/02x/4k 515.0 ns 524.0 ns 0.983
oversampling sweep/02x/64k 7.25 μs 7.48 μs 0.968
oversampling sweep/08x/4k 229.0 ns 213.0 ns 1.08 ✅
oversampling sweep/08x/64k 2.7 μs 2.66 μs 1.02
oversampling sweep/128x/4k 102.0 ns 89.1 ns 1.15 ✅
oversampling sweep/128x/64k 914.0 ns 886.0 ns 1.03
oversampling sweep/17x/4k 263.0 ns 236.0 ns 1.11 ✅
oversampling sweep/17x/64k 3.25 μs 3.27 μs 0.993
oversampling sweep/24x/4k 134.0 ns 127.0 ns 1.06 ✅
oversampling sweep/24x/64k 1.61 μs 1.55 μs 1.04
oversampling sweep/32x/4k 146.0 ns 142.0 ns 1.03
oversampling sweep/32x/64k 1.44 μs 1.43 μs 1.01
oversampling sweep/64x/4k 123.0 ns 97.2 ns 1.27 ✅
oversampling sweep/64x/64k 1.13 μs 1.02 μs 1.11 ✅
time_to_load 101.0 μs 109.0 μs 0.931 ⚠️
Memory benchmarks
bd4fdcc 447fbbe
1 ms code generation/GPSL1CA @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL1C_D @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL1C_P @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL2CL @ 2.5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL2CM @ 2.5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL5I @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GPSL5Q @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1B @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1B_BOC11 @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1C @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE1C_BOC11 @ 5 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE5aI @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
1 ms code generation/GalileoE5aQ @ 40 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GPSL1 @ 2 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GPSL5 @ 20 MHz 0 allocs: 0 B 0 allocs: 0 B
2000 sample code generation/GalileoE1B @ 15 MHz 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/02x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/02x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/08x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/08x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/128x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/128x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/17x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/17x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/24x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/24x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/32x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/32x/64k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/64x/4k 0 allocs: 0 B 0 allocs: 0 B
oversampling sweep/64x/64k 0 allocs: 0 B 0 allocs: 0 B
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B

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