use super::{Cf64, IqError, IqSource, SampleSpec, SpreadingCode};
use crate::acquisition::{pd_square_law, threshold_for_pfa};
use crate::portable_math::{FftPlan, PortableFloat};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AcqConfig {
pub coherent_periods: usize,
pub noncoherent: usize,
pub doppler_max_hz: f64,
pub doppler_step_hz: f64,
pub pfa: f64,
}
pub const AUTO_COHERENT_S: f64 = 4.0e-3;
pub fn auto_coherent_periods(code_period_s: f64) -> usize {
if !(code_period_s.is_finite() && code_period_s > 0.0) {
return 1;
}
((AUTO_COHERENT_S / code_period_s - 1e-9).ceil() as usize).max(1)
}
impl AcqConfig {
pub fn doppler_bins(&self) -> Vec<f64> {
let n = (self.doppler_max_hz / self.doppler_step_hz + 1e-9).floor() as i64;
(-n..=n).map(|j| j as f64 * self.doppler_step_hz).collect()
}
}
pub fn samples_per_period(spec: &SampleSpec, code: &dyn SpreadingCode) -> Result<usize, String> {
let spc = spec.fs_hz * code.period_s();
if !(spc.is_finite() && spc >= 2.0) || (spc - spc.round()).abs() > 1e-6 {
return Err(format!(
"{} Hz does not hold a whole number of samples per {} period ({spc} samples)",
spec.fs_hz,
code.name()
));
}
Ok(spc.round() as usize)
}
pub fn samples_needed(
spec: &SampleSpec,
code: &dyn SpreadingCode,
cfg: &AcqConfig,
) -> Result<usize, String> {
Ok(samples_per_period(spec, code)? * cfg.coherent_periods * cfg.noncoherent)
}
#[derive(Clone, Debug, PartialEq)]
pub struct AcqResult {
pub code_name: String,
pub doppler_hz: f64,
pub doppler_index: usize,
pub delay_samples: usize,
pub code_phase_chips: f64,
pub statistic: f64,
pub second_peak: f64,
pub peak_ratio: f64,
pub pfa_cell: f64,
pub threshold: f64,
pub acquired: bool,
pub samples_per_period: usize,
pub n_doppler_bins: usize,
pub sample_power: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AcqGrid {
pub result: AcqResult,
pub grid: Vec<Vec<f64>>,
}
pub fn acquire(
samples: &[Cf64],
spec: &SampleSpec,
code: &dyn SpreadingCode,
cfg: &AcqConfig,
) -> Result<AcqGrid, String> {
if cfg.coherent_periods == 0 || cfg.noncoherent == 0 {
return Err("coherent_periods and noncoherent must be positive".into());
}
if !(cfg.doppler_step_hz > 0.0 && cfg.doppler_max_hz >= 0.0) {
return Err("the Doppler grid needs a positive step and a non-negative maximum".into());
}
if !(cfg.pfa > 0.0 && cfg.pfa < 1.0) {
return Err("pfa must lie in (0, 1)".into());
}
let spc = samples_per_period(spec, code)?;
let block = spc * cfg.coherent_periods;
let needed = block * cfg.noncoherent;
if samples.len() < needed {
return Err(format!(
"{} samples supplied, {needed} needed",
samples.len()
));
}
let bins = cfg.doppler_bins();
let fs = spec.fs_hz;
let base_hz = spec.baseband_hz(code.carrier_hz());
let chips_per_sample = code.chip_rate_hz() / fs;
let plan = FftPlan::new(spc);
let rep: Vec<(f64, f64)> = (0..spc)
.map(|i| (code.value_at(i as f64 * chips_per_sample), 0.0))
.collect();
let code_fft: Vec<(f64, f64)> = plan
.forward(&rep)
.into_iter()
.map(|(r, i)| (r, -i))
.collect();
let power: f64 = samples[..needed]
.iter()
.map(|x| x.re * x.re + x.im * x.im)
.sum();
let sigma2 = power / needed as f64;
if sigma2.is_nan() || sigma2 <= 0.0 {
return Err("the samples carry no power".into());
}
let mut grid = vec![vec![0.0_f64; spc]; bins.len()];
for (row, &d) in grid.iter_mut().zip(&bins) {
let f = base_hz + d;
for m in 0..cfg.noncoherent {
let start = m * block;
let mut fold = vec![(0.0_f64, 0.0_f64); spc];
for i in 0..block {
let n = start + i;
let cyc = (f * n as f64 / fs).fract();
let (sn, cs) = (-core::f64::consts::TAU * cyc).psin_cos();
let s = samples[n];
let r = &mut fold[i % spc];
r.0 += s.re * cs - s.im * sn;
r.1 += s.re * sn + s.im * cs;
}
let y = plan.forward(&fold);
let prod: Vec<(f64, f64)> = y
.iter()
.zip(&code_fft)
.map(|(&(a, b), &(c, e))| (a * c - b * e, a * e + b * c))
.collect();
for (cell, (re, im)) in row.iter_mut().zip(plan.inverse(&prod)) {
*cell += re * re + im * im;
}
}
}
let norm = 2.0 / (block as f64 * sigma2);
let mut best = (f64::NEG_INFINITY, 0usize, 0usize);
for (j, row) in grid.iter_mut().enumerate() {
for (t, cell) in row.iter_mut().enumerate() {
*cell *= norm;
if *cell > best.0 {
best = (*cell, j, t);
}
}
}
let (peak, jb, tb) = best;
let guard = (1.0 / chips_per_sample).ceil() as usize + 1;
let second_peak = grid[jb]
.iter()
.enumerate()
.filter(|&(t, _)| {
let d = t.abs_diff(tb);
d.min(spc - d) > guard
})
.map(|(_, &v)| v)
.fold(0.0_f64, f64::max);
let n_cells = (spc * bins.len()) as f64;
let pfa_cell = -((-cfg.pfa).ln_1p() / n_cells).exp_m1();
let threshold = threshold_for_pfa(pfa_cell, cfg.noncoherent as f64);
let len = code.len_chips() as f64;
let code_phase_chips = (-(tb as f64) * chips_per_sample).rem_euclid(len);
Ok(AcqGrid {
result: AcqResult {
code_name: code.name(),
doppler_hz: bins[jb],
doppler_index: jb,
delay_samples: tb,
code_phase_chips,
statistic: peak,
second_peak,
peak_ratio: if second_peak > 0.0 {
peak / second_peak
} else {
f64::INFINITY
},
pfa_cell,
threshold,
acquired: peak > threshold,
samples_per_period: spc,
n_doppler_bins: bins.len(),
sample_power: sigma2,
},
grid,
})
}
pub fn acquire_source(
src: &mut dyn IqSource,
code: &dyn SpreadingCode,
cfg: &AcqConfig,
) -> Result<AcqGrid, IqError> {
let spec = src.spec();
let needed = samples_needed(&spec, code, cfg).map_err(IqError::Format)?;
let mut buf = vec![Cf64::default(); needed];
let mut got = 0;
while got < needed {
let n = src.read(&mut buf[got..])?;
if n == 0 {
break;
}
got += n;
}
if got < needed {
return Err(IqError::Format(format!(
"stream ended after {got} of {needed} samples"
)));
}
acquire(&buf, &spec, code, cfg).map_err(IqError::Format)
}
pub fn predicted_pd(cfg: &AcqConfig, period_s: f64, cn0_dbhz: f64, pfa_cell: f64) -> f64 {
let m = cfg.noncoherent as f64;
let rho = 10f64.powf(cn0_dbhz / 10.0) * cfg.coherent_periods as f64 * period_s;
pd_square_law(threshold_for_pfa(pfa_cell, m), m, rho)
}
#[cfg(test)]
mod tests {
use super::auto_coherent_periods;
#[test]
fn the_auto_coherent_length_is_about_four_milliseconds() {
assert_eq!(auto_coherent_periods(1.0e-3), 4); assert_eq!(auto_coherent_periods(4.0e-3), 1); assert_eq!(auto_coherent_periods(10.0e-3), 1); assert_eq!(auto_coherent_periods(100.0e-3), 1); assert_eq!(auto_coherent_periods(1.5e-3), 3);
assert_eq!(auto_coherent_periods(0.0), 1);
}
}