use std::f64::consts::TAU;
pub const CA_CHIP_RATE_HZ: f64 = 1_023_000.0;
pub const CA_CODE_LEN: usize = 1023;
pub const L1_HZ: f64 = 1_575_420_000.0;
const G2_TAPS: [(usize, usize); 32] = [
(2, 6), (3, 7), (4, 8), (5, 9), (1, 9), (2, 10), (1, 8), (2, 9), (3, 10), (2, 3), (3, 4), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9), (1, 3), (4, 6), (5, 7), (6, 8), (7, 9), (8, 10), (1, 6), (2, 7), (3, 8), (4, 9), ];
#[derive(Clone, Debug)]
pub struct CaCode {
pub prn: u8,
pub chips: Vec<u8>,
pub bipolar: Vec<f64>,
}
impl CaCode {
pub fn new(prn: u8) -> Option<Self> {
if prn < 1 || prn as usize > G2_TAPS.len() {
return None;
}
let (s1, s2) = G2_TAPS[(prn - 1) as usize];
let mut g1 = [1u8; 11];
let mut g2 = [1u8; 11];
let mut chips = Vec::with_capacity(CA_CODE_LEN);
for _ in 0..CA_CODE_LEN {
let g1_out = g1[10];
let g2_out = g2[s1] ^ g2[s2];
chips.push(g1_out ^ g2_out);
let fb1 = g1[3] ^ g1[10];
let fb2 = g2[2] ^ g2[3] ^ g2[6] ^ g2[8] ^ g2[9] ^ g2[10];
for i in (2..=10).rev() {
g1[i] = g1[i - 1];
g2[i] = g2[i - 1];
}
g1[1] = fb1;
g2[1] = fb2;
}
let bipolar = chips
.iter()
.map(|&c| if c == 0 { 1.0 } else { -1.0 })
.collect();
Some(Self {
prn,
chips,
bipolar,
})
}
pub fn first_chips_as_int(&self, n: usize) -> u32 {
self.chips
.iter()
.take(n)
.fold(0u32, |acc, &c| (acc << 1) | c as u32)
}
pub fn ones(&self) -> usize {
self.chips.iter().filter(|&&c| c == 1).count()
}
pub fn periodic_autocorr(&self, lag: usize) -> i32 {
let b = &self.bipolar;
let n = b.len();
let l = lag % n;
let mut acc = 0i32;
for i in 0..n {
acc += (b[i] * b[(i + l) % n]) as i32;
}
acc
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Cf64 {
pub re: f64,
pub im: f64,
}
impl Cf64 {
pub fn new(re: f64, im: f64) -> Self {
Self { re, im }
}
pub fn abs(self) -> f64 {
self.re.hypot(self.im)
}
}
impl std::ops::Add for Cf64 {
type Output = Cf64;
fn add(self, o: Cf64) -> Cf64 {
Cf64::new(self.re + o.re, self.im + o.im)
}
}
impl std::ops::Mul for Cf64 {
type Output = Cf64;
fn mul(self, o: Cf64) -> Cf64 {
Cf64::new(
self.re * o.re - self.im * o.im,
self.re * o.im + self.im * o.re,
)
}
}
impl std::ops::Mul<f64> for Cf64 {
type Output = Cf64;
fn mul(self, s: f64) -> Cf64 {
Cf64::new(self.re * s, self.im * s)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Correlation {
pub early: Cf64,
pub prompt: Cf64,
pub late: Cf64,
}
#[derive(Clone, Copy, Debug)]
pub struct CorrParams {
pub fs_hz: f64,
pub carrier_freq_hz: f64,
pub carrier_phase_rad: f64,
pub code_rate_hz: f64,
pub code_phase_chips: f64,
pub corr_spacing_chips: f64,
}
#[inline]
fn chip_at(code: &CaCode, phase_chips: f64) -> f64 {
let idx = phase_chips.floor().rem_euclid(CA_CODE_LEN as f64) as usize;
code.bipolar[idx]
}
pub fn correlate(iq: &[Cf64], code: &CaCode, p: &CorrParams) -> Correlation {
let mut out = Correlation::default();
let half = p.corr_spacing_chips / 2.0;
for (k, &s) in iq.iter().enumerate() {
let t = k as f64 / p.fs_hz;
let theta = TAU * p.carrier_freq_hz * t + p.carrier_phase_rad;
let (sin, cos) = theta.sin_cos();
let wiped = s * Cf64::new(cos, -sin);
let cp = p.code_phase_chips + p.code_rate_hz * t;
let prompt = chip_at(code, cp);
let early = chip_at(code, cp + half);
let late = chip_at(code, cp - half);
out.early = out.early + wiped * early;
out.prompt = out.prompt + wiped * prompt;
out.late = out.late + wiped * late;
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn synth_if(
code: &CaCode,
fs_hz: f64,
carrier_freq_hz: f64,
code_rate_hz: f64,
code_phase0_chips: f64,
amp: f64,
n_samples: usize,
noise: f64,
seed: u64,
) -> Vec<Cf64> {
let mut rng = seed.max(1);
let mut next = || {
let mut g = 0.0;
for _ in 0..2 {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
g += (rng as f64 / u64::MAX as f64) * 2.0 - 1.0;
}
g
};
(0..n_samples)
.map(|k| {
let t = k as f64 / fs_hz;
let cp = code_phase0_chips + code_rate_hz * t;
let chip = chip_at(code, cp);
let theta = TAU * carrier_freq_hz * t;
let (sin, cos) = theta.sin_cos();
let sig = Cf64::new(amp * chip * cos, amp * chip * sin);
if noise > 0.0 {
Cf64::new(sig.re + noise * next(), sig.im + noise * next())
} else {
sig
}
})
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct Acquisition {
pub prn: u8,
pub code_phase_chips: f64,
pub doppler_hz: f64,
pub peak_ratio: f64,
pub acquired: bool,
}
pub fn acquire(
iq: &[Cf64],
code: &CaCode,
fs_hz: f64,
if_hz: f64,
doppler_max_hz: f64,
doppler_step_hz: f64,
threshold: f64,
) -> Acquisition {
let n_bins = (2.0 * doppler_max_hz / doppler_step_hz).round() as i64;
let mut best = (f64::NEG_INFINITY, 0.0_f64, 0.0_f64); let mut best_grid: Vec<(f64, f64)> = Vec::new();
for b in 0..=n_bins {
let doppler = -doppler_max_hz + b as f64 * doppler_step_hz;
let fc = if_hz + doppler;
let wiped: Vec<Cf64> = iq
.iter()
.enumerate()
.map(|(k, &s)| {
let theta = TAU * fc * (k as f64 / fs_hz);
let (sin, cos) = theta.sin_cos();
s * Cf64::new(cos, -sin)
})
.collect();
let mut grid: Vec<(f64, f64)> = Vec::with_capacity(CA_CODE_LEN);
for p0 in 0..CA_CODE_LEN {
let mut acc = Cf64::default();
let phase0 = p0 as f64;
for (k, &w) in wiped.iter().enumerate() {
let cp = phase0 + CA_CHIP_RATE_HZ * (k as f64 / fs_hz);
acc = acc + w * chip_at(code, cp);
}
let power = acc.re * acc.re + acc.im * acc.im;
grid.push((power, phase0));
if power > best.0 {
best = (power, phase0, doppler);
}
}
if (best.2 - doppler).abs() < doppler_step_hz / 2.0 {
best_grid = grid;
}
}
let peak_phase = best.1;
let mut second = 0.0_f64;
for &(power, phase) in &best_grid {
let dchip = (phase - peak_phase)
.abs()
.min(CA_CODE_LEN as f64 - (phase - peak_phase).abs());
if dchip > 1.0 && power > second {
second = power;
}
}
let peak_ratio = if second > 0.0 {
best.0 / second
} else {
f64::INFINITY
};
Acquisition {
prn: code.prn,
code_phase_chips: best.1,
doppler_hz: best.2,
peak_ratio,
acquired: peak_ratio >= threshold,
}
}
#[derive(Clone, Copy, Debug)]
pub struct CorrelatorDump {
pub epoch_s: f64,
pub prn: u8,
pub early: Cf64,
pub prompt: Cf64,
pub late: Cf64,
}
impl CorrelatorDump {
pub fn el_imbalance(&self) -> f64 {
let (e, l) = (self.early.abs(), self.late.abs());
if e + l > 0.0 {
((e - l) / (e + l)).abs()
} else {
0.0
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TrackConfig {
pub dll_spacing_chips: f64,
pub sqm_spacing_chips: f64,
pub dll_gain: f64,
pub pll_phase_gain: f64,
pub pll_freq_gain: f64,
}
impl Default for TrackConfig {
fn default() -> Self {
Self {
dll_spacing_chips: 0.2,
sqm_spacing_chips: 1.0,
dll_gain: 0.5,
pll_phase_gain: 0.5,
pll_freq_gain: 0.1,
}
}
}
pub fn track(
iq: &[Cf64],
code: &CaCode,
acq: &Acquisition,
fs_hz: f64,
if_hz: f64,
cfg: &TrackConfig,
n_epochs: usize,
) -> Vec<CorrelatorDump> {
let spe = (fs_hz / 1000.0).round() as usize; if spe == 0 {
return Vec::new();
}
let epoch_dur = spe as f64 / fs_hz;
let mut code_phase = acq.code_phase_chips;
let mut carrier_freq = if_hz + acq.doppler_hz;
let mut carrier_phase = 0.0_f64;
let mut dumps = Vec::with_capacity(n_epochs);
for e in 0..n_epochs {
let start = e * spe;
let end = start + spe;
if end > iq.len() {
break;
}
let block = &iq[start..end];
let base = CorrParams {
fs_hz,
carrier_freq_hz: carrier_freq,
carrier_phase_rad: carrier_phase,
code_rate_hz: CA_CHIP_RATE_HZ,
code_phase_chips: code_phase,
corr_spacing_chips: cfg.dll_spacing_chips,
};
let loop_corr = correlate(block, code, &base);
let mon = correlate(
block,
code,
&CorrParams {
corr_spacing_chips: cfg.sqm_spacing_chips,
..base
},
);
dumps.push(CorrelatorDump {
epoch_s: e as f64 * epoch_dur,
prn: code.prn,
early: mon.early,
prompt: mon.prompt,
late: mon.late,
});
let (em, lm) = (loop_corr.early.abs(), loop_corr.late.abs());
let dll = if em + lm > 0.0 {
0.5 * (em - lm) / (em + lm)
} else {
0.0
};
let pll = if loop_corr.prompt.re != 0.0 {
(loop_corr.prompt.im / loop_corr.prompt.re).atan()
} else {
0.0
};
let freq_err = pll / (TAU * epoch_dur);
carrier_freq += cfg.pll_freq_gain * freq_err;
code_phase += CA_CHIP_RATE_HZ * epoch_dur + cfg.dll_gain * dll;
code_phase = code_phase.rem_euclid(CA_CODE_LEN as f64);
carrier_phase += TAU * carrier_freq * epoch_dur + cfg.pll_phase_gain * pll;
carrier_phase = carrier_phase.rem_euclid(TAU);
}
dumps
}
#[cfg(test)]
mod track_tests {
use super::*;
fn acquire_and_track(
iq: &[Cf64],
code: &CaCode,
fs: f64,
if_hz: f64,
n: usize,
) -> Vec<CorrelatorDump> {
let acq = acquire(
&iq[..(fs / 1000.0) as usize],
code,
fs,
if_hz,
5000.0,
250.0,
2.0,
);
assert!(
acq.acquired,
"setup: must acquire first (ratio {})",
acq.peak_ratio
);
track(iq, code, &acq, fs, if_hz, &TrackConfig::default(), n)
}
#[test]
fn tracks_clean_signal_with_stable_prompt_and_low_sqm() {
let code = CaCode::new(10).unwrap();
let fs = 5_000_000.0;
let if_hz = 50_000.0;
let doppler = 800.0;
let n_epochs = 20;
let n = (fs / 1000.0) as usize * n_epochs;
let iq = synth_if(
&code,
fs,
if_hz + doppler,
CA_CHIP_RATE_HZ,
123.0,
1.0,
n,
0.0,
7,
);
let dumps = acquire_and_track(&iq, &code, fs, if_hz, n_epochs);
assert_eq!(dumps.len(), n_epochs);
let settled = &dumps[5..];
let mean_sqm: f64 =
settled.iter().map(|d| d.el_imbalance()).sum::<f64>() / settled.len() as f64;
assert!(
mean_sqm < 0.1,
"clean-signal mean SQM {mean_sqm:.3} should be < 0.1"
);
let first_p = dumps[5].prompt.abs();
let last_p = dumps[n_epochs - 1].prompt.abs();
assert!(
last_p > 0.5 * first_p,
"prompt collapsed: {last_p} vs {first_p}"
);
}
#[test]
fn multipath_distortion_raises_sqm_above_clean() {
let code = CaCode::new(10).unwrap();
let fs = 5_000_000.0;
let if_hz = 50_000.0;
let doppler = 800.0;
let n_epochs = 20;
let n = (fs / 1000.0) as usize * n_epochs;
let direct = synth_if(
&code,
fs,
if_hz + doppler,
CA_CHIP_RATE_HZ,
123.0,
1.0,
n,
0.0,
7,
);
let echo = synth_if(
&code,
fs,
if_hz + doppler,
CA_CHIP_RATE_HZ,
123.5,
0.7,
n,
0.0,
7,
);
let mixed: Vec<Cf64> = direct
.iter()
.zip(&echo)
.map(|(a, b)| Cf64::new(a.re + b.re, a.im + b.im))
.collect();
let clean_dumps = acquire_and_track(&direct, &code, fs, if_hz, n_epochs);
let mp_dumps = acquire_and_track(&mixed, &code, fs, if_hz, n_epochs);
let mean = |d: &[CorrelatorDump]| {
let s = &d[5..];
s.iter().map(|x| x.el_imbalance()).sum::<f64>() / s.len() as f64
};
let clean_sqm = mean(&clean_dumps);
let mp_sqm = mean(&mp_dumps);
assert!(
mp_sqm > clean_sqm + 0.05,
"multipath SQM {mp_sqm:.3} should exceed clean {clean_sqm:.3} clearly"
);
}
}
#[cfg(test)]
mod acq_tests {
use super::*;
#[test]
fn acquires_known_code_phase_and_doppler() {
let code = CaCode::new(6).unwrap();
let fs = 5_000_000.0;
let if_hz = 50_000.0;
let doppler_true = 1500.0;
let code_phase_true = 300.0;
let n = (fs / 1000.0) as usize; let iq = synth_if(
&code,
fs,
if_hz + doppler_true,
CA_CHIP_RATE_HZ,
code_phase_true,
1.0,
n,
0.0,
1,
);
let acq = acquire(&iq, &code, fs, if_hz, 5000.0, 500.0, 2.0);
assert!(acq.acquired, "should acquire, ratio {}", acq.peak_ratio);
let dphase = (acq.code_phase_chips - code_phase_true).abs();
assert!(dphase <= 1.0, "code phase {} vs 300", acq.code_phase_chips);
assert!(
(acq.doppler_hz - doppler_true).abs() <= 500.0,
"doppler {} vs 1500",
acq.doppler_hz
);
}
#[test]
fn does_not_acquire_pure_noise() {
let code = CaCode::new(6).unwrap();
let fs = 5_000_000.0;
let n = (fs / 1000.0) as usize;
let empty = CaCode::new(6).unwrap();
let iq = synth_if(&empty, fs, 50_000.0, CA_CHIP_RATE_HZ, 0.0, 0.0, n, 1.0, 42);
let acq = acquire(&iq, &code, fs, 50_000.0, 5000.0, 500.0, 2.5);
assert!(
!acq.acquired,
"noise should not acquire, ratio {}",
acq.peak_ratio
);
}
}
#[cfg(test)]
mod corr_tests {
use super::*;
fn params(fs: f64, fc: f64, phase: f64) -> CorrParams {
CorrParams {
fs_hz: fs,
carrier_freq_hz: fc,
carrier_phase_rad: 0.0,
code_rate_hz: CA_CHIP_RATE_HZ,
code_phase_chips: phase,
corr_spacing_chips: 0.5,
}
}
#[test]
fn aligned_prompt_dominates_and_early_late_are_symmetric() {
let code = CaCode::new(1).unwrap();
let fs = 5_000_000.0; let fc = 50_000.0; let n = (fs / 1000.0) as usize; let iq = synth_if(&code, fs, fc, CA_CHIP_RATE_HZ, 0.0, 1.0, n, 0.0, 1);
let c = correlate(&iq, &code, ¶ms(fs, fc, 0.0));
assert!(c.prompt.abs() > c.early.abs(), "prompt must beat early");
assert!(c.prompt.abs() > c.late.abs(), "prompt must beat late");
let asym = (c.early.abs() - c.late.abs()).abs() / c.prompt.abs();
assert!(asym < 0.05, "aligned E/L asymmetry {asym:.3} should be ~0");
}
#[test]
fn prompt_correlation_falls_off_with_code_phase_error() {
let code = CaCode::new(11).unwrap();
let fs = 5_000_000.0;
let fc = 0.0;
let n = (fs / 1000.0) as usize;
let iq = synth_if(&code, fs, fc, CA_CHIP_RATE_HZ, 0.0, 1.0, n, 0.0, 1);
let aligned = correlate(&iq, &code, ¶ms(fs, fc, 0.0)).prompt.abs();
let off = correlate(&iq, &code, ¶ms(fs, fc, 0.5)).prompt.abs();
assert!(off < 0.7 * aligned, "0.5-chip error {off} vs {aligned}");
}
#[test]
fn wrong_prn_does_not_correlate() {
let tx = CaCode::new(7).unwrap();
let rx = CaCode::new(8).unwrap();
let fs = 5_000_000.0;
let fc = 0.0;
let n = (fs / 1000.0) as usize;
let iq = synth_if(&tx, fs, fc, CA_CHIP_RATE_HZ, 0.0, 1.0, n, 0.0, 1);
let matched = correlate(&iq, &tx, ¶ms(fs, fc, 0.0)).prompt.abs();
let mismatched = correlate(&iq, &rx, ¶ms(fs, fc, 0.0)).prompt.abs();
assert!(mismatched < 0.1 * matched, "{mismatched} vs {matched}");
}
}
#[cfg(test)]
mod code_tests {
use super::*;
#[test]
fn ca_first_ten_chips_match_is_gps_200_octal() {
assert_eq!(CaCode::new(1).unwrap().first_chips_as_int(10), 0o1440);
assert_eq!(CaCode::new(2).unwrap().first_chips_as_int(10), 0o1620);
assert_eq!(CaCode::new(3).unwrap().first_chips_as_int(10), 0o1710);
assert_eq!(CaCode::new(9).unwrap().first_chips_as_int(10), 0o1626);
}
#[test]
fn ca_code_has_1023_chips_and_is_balanced() {
let c = CaCode::new(5).unwrap();
assert_eq!(c.chips.len(), CA_CODE_LEN);
assert_eq!(c.ones(), 512);
}
#[test]
fn ca_periodic_autocorrelation_is_three_valued() {
let c = CaCode::new(7).unwrap();
assert_eq!(c.periodic_autocorr(0), CA_CODE_LEN as i32);
for lag in 1..CA_CODE_LEN {
let v = c.periodic_autocorr(lag);
assert!(
v == -1 || v == 63 || v == -65,
"PRN7 autocorr at lag {lag} = {v}, not three-valued"
);
}
}
#[test]
fn distinct_prns_have_low_bounded_cross_correlation() {
let a = CaCode::new(1).unwrap();
let b = CaCode::new(2).unwrap();
let n = CA_CODE_LEN;
for lag in 0..n {
let mut acc = 0i32;
for i in 0..n {
acc += (a.bipolar[i] * b.bipolar[(i + lag) % n]) as i32;
}
assert!(
acc == -1 || acc == 63 || acc == -65,
"PRN1xPRN2 xcorr at lag {lag} = {acc}"
);
}
}
#[test]
fn out_of_range_prn_is_none() {
assert!(CaCode::new(0).is_none());
assert!(CaCode::new(33).is_none());
}
}