use std::f64::consts::PI;
pub const F0_HZ: f64 = 1_023_000.0;
fn sinc(x: f64) -> f64 {
if x.abs() < 1e-12 {
1.0
} else {
x.sin() / x
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Modulation {
BpskR { n: f64 },
BocSin { m: f64, n: f64 },
}
impl Modulation {
pub fn chip_rate_hz(&self) -> f64 {
match *self {
Modulation::BpskR { n } => n * F0_HZ,
Modulation::BocSin { n, .. } => n * F0_HZ,
}
}
pub fn psd(&self, f_hz: f64) -> f64 {
match *self {
Modulation::BpskR { n } => {
let tc = 1.0 / (n * F0_HZ);
tc * sinc(PI * f_hz * tc).powi(2)
}
Modulation::BocSin { m, n } => {
let fc = n * F0_HZ; let fs = m * F0_HZ; if f_hz.abs() < 1e-9 {
return 0.0; }
let arg_sub = PI * f_hz / (2.0 * fs);
let cos_sub = arg_sub.cos();
if cos_sub.abs() < 1e-9 {
let eps = 1e-6 * fs;
return self.psd(f_hz + eps);
}
let n_half = 2.0 * fs / fc; let n_even = (n_half.round() as i64) % 2 == 0;
let num_code = if n_even {
(PI * f_hz / fc).sin()
} else {
(PI * f_hz / fc).cos()
};
let factor = num_code * arg_sub.sin() / (PI * f_hz * cos_sub);
fc * factor.powi(2)
}
}
}
}
fn integrate(half: f64, n: usize, g: impl Fn(f64) -> f64) -> f64 {
let n = if n % 2 == 0 { n.max(2) } else { n + 1 };
let h = 2.0 * half / n as f64;
let mut s = g(-half) + g(half);
for i in 1..n {
let f = -half + i as f64 * h;
s += if i % 2 == 1 { 4.0 } else { 2.0 } * g(f);
}
s * h / 3.0
}
pub fn rms_bandwidth_hz(m: &Modulation, band_hz: f64) -> f64 {
let half = band_hz / 2.0;
let n = 20_000;
let num = integrate(half, n, |f| f * f * m.psd(f));
let den = integrate(half, n, |f| m.psd(f));
(num / den.max(1e-300)).sqrt()
}
pub fn spectral_separation_coeff(sig: &Modulation, intf: &Modulation, band_hz: f64) -> f64 {
let half = band_hz / 2.0;
integrate(half, 40_000, |f| sig.psd(f) * intf.psd(f))
}
pub fn ssc_vs_white(sig: &Modulation, band_hz: f64) -> f64 {
let half = band_hz / 2.0;
integrate(half, 40_000, |f| sig.psd(f)) / band_hz
}
pub fn q_from_ssc(ssc_per_hz: f64, chip_rate_hz: f64) -> f64 {
1.0 / (chip_rate_hz * ssc_per_hz.max(1e-30))
}
pub fn dll_code_jitter_chips(
cn0_dbhz: f64,
loop_bw_hz: f64,
corr_spacing_chips: f64,
integ_time_s: f64,
) -> f64 {
let c = 10f64.powf(cn0_dbhz / 10.0);
let d = corr_spacing_chips.clamp(1e-3, 1.999);
let lead = loop_bw_hz * d / (2.0 * c);
let squaring = 1.0 + 2.0 / ((2.0 - d) * integ_time_s * c);
(lead * squaring).sqrt()
}
fn bpsk_acf(x: f64) -> f64 {
(1.0 - x.abs()).max(0.0)
}
pub fn multipath_error_envelope_chips(
spacing_chips: f64,
smr_db: f64,
delay_chips: f64,
) -> (f64, f64) {
let a = 10f64.powf(-smr_db.abs() / 20.0); let d = spacing_chips.max(1e-3);
let discrim = |eps: f64, cos_theta: f64| -> f64 {
let e = bpsk_acf(eps - d / 2.0) + cos_theta * a * bpsk_acf(eps - d / 2.0 - delay_chips);
let l = bpsk_acf(eps + d / 2.0) + cos_theta * a * bpsk_acf(eps + d / 2.0 - delay_chips);
e * e - l * l
};
let solve = |cos_theta: f64| -> f64 {
let w = ((1.0 - d / 2.0).max(0.05)) * 0.98;
let steps = 2000;
let mut prev_x = -w;
let mut prev_f = discrim(prev_x, cos_theta);
let mut best = 0.0;
let mut best_dist = f64::INFINITY;
for i in 1..=steps {
let x = -w + 2.0 * w * i as f64 / steps as f64;
let f = discrim(x, cos_theta);
if prev_f * f < 0.0 {
let (mut a_lo, mut a_hi) = (prev_x, x);
for _ in 0..60 {
let mid = 0.5 * (a_lo + a_hi);
if discrim(a_lo, cos_theta) * discrim(mid, cos_theta) <= 0.0 {
a_hi = mid;
} else {
a_lo = mid;
}
}
let root = 0.5 * (a_lo + a_hi);
if root.abs() < best_dist {
best_dist = root.abs();
best = root;
}
}
prev_x = x;
prev_f = f;
}
best
};
(solve(1.0), solve(-1.0))
}
pub const C_LIGHT_M_PER_S: f64 = 299_792_458.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CodeFamily {
MaximalLength { n: u32 },
Gold { n: u32 },
}
fn t_param(n: u32) -> u64 {
1 + (1u64 << ((n + 2) / 2))
}
fn euler_phi(mut m: u64) -> u64 {
let mut result = m;
let mut p = 2u64;
while p * p <= m {
if m % p == 0 {
while m % p == 0 {
m /= p;
}
result -= result / p;
}
p += 1;
}
if m > 1 {
result -= result / m;
}
result
}
impl CodeFamily {
fn n_in_range(n: u32) -> bool {
(2..=31).contains(&n)
}
pub fn register_length(&self) -> u32 {
match *self {
CodeFamily::MaximalLength { n } | CodeFamily::Gold { n } => n,
}
}
pub fn code_length(&self) -> u64 {
let n = self.register_length();
if n == 0 || n >= 63 {
return 0;
}
(1u64 << n) - 1
}
pub fn max_autocorr_sidelobe(&self) -> Option<f64> {
let n = self.register_length();
if !Self::n_in_range(n) {
return None;
}
let l = self.code_length() as f64;
match *self {
CodeFamily::MaximalLength { .. } => Some(1.0 / l),
CodeFamily::Gold { n } if n % 4 != 0 => Some(t_param(n) as f64 / l),
CodeFamily::Gold { .. } => None,
}
}
pub fn max_crosscorr(&self) -> Option<f64> {
match *self {
CodeFamily::MaximalLength { .. } => None,
CodeFamily::Gold { n } if Self::n_in_range(n) && n % 4 != 0 => {
Some(t_param(n) as f64 / self.code_length() as f64)
}
CodeFamily::Gold { .. } => None,
}
}
pub fn peak_to_sidelobe_db(&self) -> Option<f64> {
self.max_autocorr_sidelobe()
.map(|s| 20.0 * (1.0 / s).log10())
}
pub fn gold_codes_exist(&self) -> bool {
match *self {
CodeFamily::MaximalLength { n } => Self::n_in_range(n),
CodeFamily::Gold { n } => Self::n_in_range(n) && n % 4 != 0,
}
}
pub fn family_size(&self) -> Option<u64> {
let n = self.register_length();
if !Self::n_in_range(n) {
return None;
}
match *self {
CodeFamily::MaximalLength { n } => Some(euler_phi(self.code_length()) / n as u64),
CodeFamily::Gold { n } if n % 4 != 0 => Some(self.code_length() + 2),
CodeFamily::Gold { .. } => None,
}
}
}
pub fn range_ambiguity_m(chip_rate_hz: f64, code_length_chips: u64) -> f64 {
if !chip_rate_hz.is_finite() || chip_rate_hz <= 0.0 {
return f64::NAN;
}
C_LIGHT_M_PER_S * code_length_chips as f64 / (2.0 * chip_rate_hz)
}
pub fn code_length_for_ambiguity(chip_rate_hz: f64, required_range_m: f64) -> u64 {
if !chip_rate_hz.is_finite()
|| chip_rate_hz <= 0.0
|| !required_range_m.is_finite()
|| required_range_m < 0.0
{
return 0;
}
let l = 2.0 * chip_rate_hz * required_range_m / C_LIGHT_M_PER_S;
l.ceil().max(1.0) as u64
}
#[cfg(test)]
mod code_tests {
use super::*;
#[test]
fn msequence_period_is_two_pow_n_minus_one() {
assert_eq!(CodeFamily::MaximalLength { n: 10 }.code_length(), 1023);
assert_eq!(CodeFamily::Gold { n: 10 }.code_length(), 1023);
assert_eq!(CodeFamily::MaximalLength { n: 13 }.code_length(), 8191);
}
#[test]
fn msequence_sidelobe_is_inverse_length() {
let f = CodeFamily::MaximalLength { n: 10 };
assert!((f.max_autocorr_sidelobe().unwrap() - 1.0 / 1023.0).abs() < 1e-15);
assert!((f.peak_to_sidelobe_db().unwrap() - 60.197).abs() < 0.01);
}
#[test]
fn gps_ca_gold_crosscorr_matches_textbook() {
let f = CodeFamily::Gold { n: 10 };
let xc = f.max_crosscorr().unwrap();
assert!((xc - 65.0 / 1023.0).abs() < 1e-15, "xc {xc}");
let db = 20.0 * xc.log10();
assert!(
(db - (-23.94)).abs() < 0.1,
"GPS C/A Gold cross-corr {db:.2} dB, want ≈ −23.9 dB"
);
}
#[test]
fn gold_family_size_is_length_plus_two() {
assert_eq!(CodeFamily::Gold { n: 10 }.family_size(), Some(1025));
}
#[test]
fn msequence_count_degree_10() {
assert_eq!(CodeFamily::MaximalLength { n: 10 }.family_size(), Some(60));
assert_eq!(euler_phi(1023), 600);
}
#[test]
fn gold_existence_condition() {
assert!(CodeFamily::Gold { n: 10 }.gold_codes_exist()); assert!(CodeFamily::Gold { n: 11 }.gold_codes_exist()); assert!(!CodeFamily::Gold { n: 8 }.gold_codes_exist()); assert!(!CodeFamily::Gold { n: 12 }.gold_codes_exist());
}
#[test]
fn invalid_gold_reports_no_guarantee() {
for n in [8u32, 12, 16] {
let g = CodeFamily::Gold { n };
assert!(!g.gold_codes_exist(), "n={n} should have no Gold set");
assert_eq!(g.max_crosscorr(), None, "n={n} cross-corr must be None");
assert_eq!(
g.max_autocorr_sidelobe(),
None,
"n={n} sidelobe must be None"
);
assert_eq!(g.family_size(), None, "n={n} family size must be None");
assert_eq!(g.peak_to_sidelobe_db(), None);
}
}
#[test]
fn degenerate_inputs_do_not_panic() {
assert_eq!(CodeFamily::MaximalLength { n: 0 }.family_size(), None);
assert_eq!(CodeFamily::MaximalLength { n: 1 }.max_crosscorr(), None);
assert_eq!(CodeFamily::MaximalLength { n: 40 }.family_size(), None); assert!(range_ambiguity_m(0.0, 1023).is_nan());
assert!(range_ambiguity_m(-1.0, 1023).is_nan());
assert_eq!(code_length_for_ambiguity(1.023e6, f64::NAN), 0);
assert_eq!(code_length_for_ambiguity(0.0, 1.0e5), 0);
assert_eq!(code_length_for_ambiguity(1.023e6, -5.0), 0);
}
#[test]
fn longer_code_has_cleaner_peak() {
let short = CodeFamily::MaximalLength { n: 7 }
.peak_to_sidelobe_db()
.unwrap();
let long = CodeFamily::MaximalLength { n: 13 }
.peak_to_sidelobe_db()
.unwrap();
assert!(
long > short,
"longer code {long:.1} should beat {short:.1} dB"
);
let gold = CodeFamily::Gold { n: 10 }.max_autocorr_sidelobe().unwrap();
let mseq = CodeFamily::MaximalLength { n: 10 }
.max_autocorr_sidelobe()
.unwrap();
assert!(gold > mseq, "Gold sidelobe {gold:.4} > m-seq {mseq:.4}");
}
#[test]
fn ambiguity_forward_and_independent_inverse_anchors() {
let rc = 1.023e6; let du = range_ambiguity_m(rc, 1023);
assert!((du - 149_896.229).abs() < 1.0, "C/A ambiguity {du:.1} m");
let l = code_length_for_ambiguity(5.115e6, 3.0e5);
assert_eq!(l, 10_238, "hand-computed inverse anchor");
assert_eq!(code_length_for_ambiguity(rc, du), 1023);
assert!(
code_length_for_ambiguity(rc, 4.0e8) > 1023,
"deep-space needs longer code"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bpsk_psd_is_unit_area() {
let m = Modulation::BpskR { n: 1.0 };
let area = integrate(20.0 * m.chip_rate_hz(), 40_000, |f| m.psd(f));
assert!((area - 1.0).abs() < 0.02, "BPSK ∫G df = {area}, want ≈1");
}
#[test]
fn boc11_psd_is_unit_area() {
let m = Modulation::BocSin { m: 1.0, n: 1.0 };
let area = integrate(24.0 * m.chip_rate_hz(), 60_000, |f| m.psd(f));
assert!(
(area - 1.0).abs() < 0.03,
"BOC(1,1) ∫G df = {area}, want ≈1"
);
}
#[test]
fn bpsk_peaks_at_carrier_boc_splits() {
let bpsk = Modulation::BpskR { n: 1.0 };
let boc = Modulation::BocSin { m: 1.0, n: 1.0 };
assert!(bpsk.psd(0.0) > bpsk.psd(F0_HZ), "BPSK should peak at f=0");
assert!(
boc.psd(0.0) < boc.psd(F0_HZ),
"BOC should null at f=0, peak near ±f_s"
);
}
#[test]
fn bpsk_self_ssc_matches_closed_form() {
let m = Modulation::BpskR { n: 1.0 };
let rc = m.chip_rate_hz();
let kappa = spectral_separation_coeff(&m, &m, 24.0 * rc);
let closed = 2.0 / (3.0 * rc);
let rel = (kappa - closed).abs() / closed;
assert!(
rel < 0.03,
"BPSK self-SSC {kappa:.3e} vs 2/(3Rc) {closed:.3e}, rel {rel:.3}"
);
}
#[test]
fn boc_separates_from_bpsk() {
let bpsk = Modulation::BpskR { n: 1.0 };
let boc = Modulation::BocSin { m: 1.0, n: 1.0 };
let band = 24.0 * F0_HZ;
let self_ssc = spectral_separation_coeff(&bpsk, &bpsk, band);
let cross = spectral_separation_coeff(&bpsk, &boc, band);
assert!(
cross < self_ssc,
"BOC↔BPSK SSC {cross:.3e} should be < BPSK self {self_ssc:.3e}"
);
}
#[test]
fn boc_has_larger_gabor_bandwidth() {
let bpsk = Modulation::BpskR { n: 1.0 };
let boc = Modulation::BocSin { m: 1.0, n: 1.0 };
let band = 24.0 * F0_HZ;
assert!(rms_bandwidth_hz(&boc, band) > rms_bandwidth_hz(&bpsk, band));
}
#[test]
fn dll_jitter_ca_is_submetre_at_45dbhz() {
let sigma_chips = dll_code_jitter_chips(45.0, 1.0, 0.5, 0.02);
let metres = sigma_chips * 299_792_458.0 / F0_HZ;
assert!(
metres > 0.1 && metres < 2.0,
"C/A DLL jitter {metres:.2} m, want 0.1–2 m"
);
}
#[test]
fn dll_jitter_decreases_with_cn0() {
let lo = dll_code_jitter_chips(35.0, 1.0, 0.5, 0.02);
let hi = dll_code_jitter_chips(50.0, 1.0, 0.5, 0.02);
assert!(hi < lo, "higher C/N0 must track tighter ({hi} !< {lo})");
}
#[test]
fn q_from_white_noise_ssc_is_order_unity() {
let bpsk = Modulation::BpskR { n: 1.0 };
let rc = bpsk.chip_rate_hz();
let kappa = ssc_vs_white(&bpsk, 2.0 * rc);
let q = q_from_ssc(kappa, rc);
assert!(
q > 0.3 && q < 3.0,
"PSD-derived Q {q:.3} should be order unity"
);
}
#[test]
fn narrow_correlator_suppresses_multipath() {
let (max_wide, min_wide) = multipath_error_envelope_chips(1.0, 6.0, 0.3);
let (max_narrow, min_narrow) = multipath_error_envelope_chips(0.1, 6.0, 0.3);
let wide = max_wide.abs().max(min_wide.abs());
let narrow = max_narrow.abs().max(min_narrow.abs());
assert!(
narrow < wide,
"narrow correlator {narrow:.4} should beat wide {wide:.4}"
);
}
#[test]
fn multipath_vanishes_without_reflection() {
let (mx, mn) = multipath_error_envelope_chips(0.5, 60.0, 0.3);
assert!(
mx.abs() < 1e-3 && mn.abs() < 1e-3,
"no-multipath error should vanish"
);
}
#[test]
fn multipath_envelope_straddles_zero() {
let (mx, mn) = multipath_error_envelope_chips(1.0, 6.0, 0.4);
assert!(
mx.abs() > 1e-3,
"expected a non-trivial multipath bias, got {mx}"
);
assert!(
mx * mn < 0.0,
"in/anti-phase should straddle zero ({mx},{mn})"
);
}
}