use super::super::Cf64;
use crate::portable_math::PortableFloat;
use std::f64::consts::TAU;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PllDiscriminator {
Atan2,
CostasAtan,
CostasDecisionDirected,
}
impl PllDiscriminator {
pub fn discriminate(self, p: Cf64) -> f64 {
match self {
PllDiscriminator::Atan2 => p.im.patan2(p.re),
PllDiscriminator::CostasAtan => {
let s = if p.re < 0.0 { -1.0 } else { 1.0 };
(p.im * s).patan2(p.re.abs())
}
PllDiscriminator::CostasDecisionDirected => {
let m = p.abs();
if m == 0.0 {
0.0
} else {
p.im * p.re.signum() / m
}
}
}
}
pub fn tolerates_data(self) -> bool {
!matches!(self, PllDiscriminator::Atan2)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FllDiscriminator {
CrossProduct,
Atan2,
Atan2Pilot,
}
impl FllDiscriminator {
pub fn discriminate(self, p1: Cf64, p2: Cf64, t: f64) -> f64 {
let dot = p1.re * p2.re + p1.im * p2.im;
let cross = p1.re * p2.im - p2.re * p1.im;
let sgn = if dot < 0.0 { -1.0 } else { 1.0 };
let dphi = match self {
FllDiscriminator::CrossProduct => {
let m = p1.abs() * p2.abs();
if m == 0.0 {
0.0
} else {
cross * sgn / m
}
}
FllDiscriminator::Atan2 => (cross * sgn).patan2(dot.abs()),
FllDiscriminator::Atan2Pilot => cross.patan2(dot),
};
dphi / (TAU * t)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DllDiscriminator {
EarlyMinusLatePower,
DotProduct,
EarlyMinusLateEnvelope,
}
impl DllDiscriminator {
pub fn discriminate(self, e: Cf64, p: Cf64, l: Cf64, d: f64) -> f64 {
match self {
DllDiscriminator::EarlyMinusLatePower => {
let pe = e.re * e.re + e.im * e.im;
let pl = l.re * l.re + l.im * l.im;
if pe + pl == 0.0 {
0.0
} else {
0.25 * (2.0 - d) * (pe - pl) / (pe + pl)
}
}
DllDiscriminator::DotProduct => {
let pp = p.re * p.re + p.im * p.im;
if pp == 0.0 {
0.0
} else {
((e.re - l.re) * p.re + (e.im - l.im) * p.im) / (2.0 * pp)
}
}
DllDiscriminator::EarlyMinusLateEnvelope => {
let (ae, al) = (e.abs(), l.abs());
if ae + al == 0.0 {
0.0
} else {
0.5 * (2.0 - d) * (ae - al) / (ae + al)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tri(x: f64) -> f64 {
(1.0 - x.abs()).max(0.0)
}
#[test]
fn dll_discriminators_are_unit_slope_on_the_ideal_triangle() {
for d in [0.2, 0.5, 1.0] {
for eps in [-0.05, 0.02, 0.08] {
let rot = Cf64::new(0.6, 0.8);
let e = rot * tri(eps - d / 2.0);
let p = rot * tri(eps);
let l = rot * tri(eps + d / 2.0);
for k in [
DllDiscriminator::EarlyMinusLatePower,
DllDiscriminator::DotProduct,
DllDiscriminator::EarlyMinusLateEnvelope,
] {
let got = k.discriminate(e, p, l, d);
let tol = 0.1 * eps.abs() + 1e-12;
assert!((got - eps).abs() <= tol, "{k:?} d={d} eps={eps} got={got}");
}
}
}
}
#[test]
fn costas_ignores_a_bit_flip_and_atan2_does_not() {
let p = Cf64::new(0.2_f64.cos(), 0.2_f64.sin()) * 5.0;
let flipped = p * -1.0;
for k in [
PllDiscriminator::CostasAtan,
PllDiscriminator::CostasDecisionDirected,
] {
assert!((k.discriminate(p) - k.discriminate(flipped)).abs() < 1e-12);
}
let a = PllDiscriminator::Atan2;
assert!((a.discriminate(p) - a.discriminate(flipped)).abs() > 3.0);
}
#[test]
fn fll_reads_a_known_rotation() {
let t = 1e-3;
let f = 37.0;
let p1 = Cf64::new(1.0, 0.0);
let ang = TAU * f * t;
let p2 = Cf64::new(ang.cos(), ang.sin());
let a = FllDiscriminator::Atan2.discriminate(p1, p2, t);
assert!((a - f).abs() < 1e-9);
let a = FllDiscriminator::Atan2.discriminate(p1, p2 * -1.0, t);
assert!((a - f).abs() < 1e-9, "data-tolerant across a flip");
let c = FllDiscriminator::CrossProduct.discriminate(p1, p2, t);
assert!((c - ang.sin() / (TAU * t)).abs() < 1e-9);
let pl = FllDiscriminator::Atan2Pilot.discriminate(p1, p2, t);
assert!((pl - f).abs() < 1e-9);
}
}