use super::discrim::DllDiscriminator;
use crate::iq::Cf64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BocKind {
SineBoc11,
}
impl BocKind {
pub fn half_period_chips(self) -> f64 {
match self {
BocKind::SineBoc11 => 0.5,
}
}
pub fn autocorrelation(self, tau_chips: f64) -> f64 {
match self {
BocKind::SineBoc11 => {
let t = tau_chips.abs();
if t <= 0.5 {
1.0 - 3.0 * t
} else if t <= 1.0 {
t - 1.0
} else {
0.0
}
}
}
}
pub fn max_spacing_chips(self) -> f64 {
match self {
BocKind::SineBoc11 => 2.0 / 3.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BumpJumpConfig {
pub threshold: u32,
}
impl Default for BumpJumpConfig {
fn default() -> Self {
Self { threshold: 8 }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BocTracking {
pub kind: BocKind,
pub bump_jump: Option<BumpJumpConfig>,
}
impl BocTracking {
pub fn boc11() -> Self {
Self {
kind: BocKind::SineBoc11,
bump_jump: Some(BumpJumpConfig::default()),
}
}
pub fn validate(&self, spacing_chips: f64) -> Result<(), String> {
let max = self.kind.max_spacing_chips();
if !(spacing_chips > 0.0 && spacing_chips < max) {
return Err(format!(
"BOC tracking needs an early-late spacing in (0, {max:.4}) chips so that E and L \
stay on the main peak (got {spacing_chips})"
));
}
if let Some(b) = self.bump_jump {
if b.threshold == 0 {
return Err("bump-jump threshold must be at least 1".into());
}
}
Ok(())
}
}
pub fn boc11_dll_scale(dll: DllDiscriminator, d: f64) -> f64 {
match dll {
DllDiscriminator::EarlyMinusLatePower | DllDiscriminator::EarlyMinusLateEnvelope => {
(2.0 - 3.0 * d) / (3.0 * (2.0 - d))
}
DllDiscriminator::DotProduct => 1.0 / 3.0,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BumpJumpEvent {
pub epoch: u64,
pub code_epoch_s: f64,
pub jump_chips: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BumpJump {
cfg: BumpJumpConfig,
ve: u32,
vl: u32,
}
impl BumpJump {
pub fn new(cfg: BumpJumpConfig) -> Self {
Self { cfg, ve: 0, vl: 0 }
}
pub fn counters(&self) -> (u32, u32) {
(self.ve, self.vl)
}
pub fn update(&mut self, ve: Cf64, p: Cf64, vl: Cf64) -> i8 {
let (a_ve, a_p, a_vl) = (ve.abs(), p.abs(), vl.abs());
let step = |c: &mut u32, up: bool| {
if up {
*c += 1;
} else {
*c = c.saturating_sub(1);
}
};
step(&mut self.ve, a_ve > a_p);
step(&mut self.vl, a_vl > a_p);
let n = self.cfg.threshold;
let dir = match (self.ve >= n, self.vl >= n) {
(false, false) => 0,
(true, false) => 1,
(false, true) => -1,
(true, true) => {
if a_ve >= a_vl {
1
} else {
-1
}
}
};
if dir != 0 {
self.ve = 0;
self.vl = 0;
}
dir
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scaled(dll: DllDiscriminator, d: f64, eps: f64) -> f64 {
let k = BocKind::SineBoc11;
let rot = Cf64::new(0.6, -0.8);
let e = rot * k.autocorrelation(eps - d / 2.0);
let p = rot * k.autocorrelation(eps);
let l = rot * k.autocorrelation(eps + d / 2.0);
dll.discriminate(e, p, l, d) * boc11_dll_scale(dll, d)
}
const ALL: [DllDiscriminator; 3] = [
DllDiscriminator::EarlyMinusLatePower,
DllDiscriminator::DotProduct,
DllDiscriminator::EarlyMinusLateEnvelope,
];
#[test]
fn boc11_scaled_discriminators_have_unit_slope_at_zero_error() {
for d in [0.1, 0.2, 0.5] {
for eps in [-1e-4_f64, 1e-4] {
for dll in ALL {
let got = scaled(dll, d, eps);
assert!(
(got / eps - 1.0).abs() < 1e-2,
"{dll:?} d={d} eps={eps} got={got}"
);
}
}
}
}
#[test]
fn b4_unit_grid_misses_are_the_recorded_ones() {
const RECORDED: [(f64, f64, [f64; 3]); 9] = [
(0.1, -0.05, [-0.0302, 0.1765, 0.0]),
(0.1, 0.02, [-0.0050, 0.0638, 0.0]),
(0.1, 0.08, [-0.3272, -0.1776, -0.30099]),
(0.2, -0.05, [-0.0439, 0.1765, 0.0]),
(0.2, 0.02, [-0.0073, 0.0638, 0.0]),
(0.2, 0.08, [-0.1052, 0.3158, 0.0]),
(0.5, -0.05, [-0.2647, 0.1765, 0.0]),
(0.5, 0.02, [-0.0545, 0.0638, 0.0]),
(0.5, 0.08, [-0.4796, 0.3158, 0.0]),
];
let mut misses = 0;
for (d, eps, want) in RECORDED {
assert!(
eps.abs() + d / 2.0 < 1.0 / 3.0,
"grid point inside the registered range"
);
for (dll, w) in ALL.into_iter().zip(want) {
let rel = scaled(dll, d, eps) / eps - 1.0;
assert!(
(rel - w).abs() < 2e-3,
"{dll:?} d={d} eps={eps}: {rel:.4} vs {w}"
);
if rel.abs() > 0.1 {
misses += 1;
}
}
}
assert_eq!(
misses, 11,
"the B4 unit-test bar is recorded NOT MET with 11 misses"
);
}
#[test]
fn autocorrelation_has_the_boc11_shape() {
let k = BocKind::SineBoc11;
assert_eq!(k.autocorrelation(0.0), 1.0);
assert!((k.autocorrelation(1.0 / 3.0)).abs() < 1e-15);
assert_eq!(k.autocorrelation(-0.5), -0.5);
assert_eq!(k.autocorrelation(1.0), 0.0);
assert_eq!(k.autocorrelation(1.5), 0.0);
}
#[test]
fn counters_jump_towards_the_stronger_tap_and_reset() {
let mut b = BumpJump::new(BumpJumpConfig { threshold: 3 });
let one = Cf64::new(1.0, 0.0);
assert_eq!(b.update(one * 2.0, one, one * 0.1), 0);
assert_eq!(b.update(one * 2.0, one, one * 0.1), 0);
assert_eq!(b.counters(), (2, 0));
assert_eq!(b.update(one * 2.0, one, one * 0.1), 1);
assert_eq!(b.counters(), (0, 0));
assert_eq!(b.update(one * 0.5, one, one * 0.5), 0);
assert_eq!(b.counters(), (0, 0));
for _ in 0..2 {
assert_eq!(b.update(one * 0.1, one, one * 3.0), 0);
}
assert_eq!(b.update(one * 0.1, one, one * 3.0), -1);
}
#[test]
fn validation_rejects_wide_spacing_and_zero_threshold() {
let b = BocTracking::boc11();
assert!(b.validate(0.2).is_ok());
assert!(b.validate(0.7).is_err());
assert!(b.validate(0.0).is_err());
let z = BocTracking {
bump_jump: Some(BumpJumpConfig { threshold: 0 }),
..b
};
assert!(z.validate(0.2).is_err());
}
}