use alloc::vec::Vec;
use core::f32::consts::PI;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::engine::Protocol;
use crate::engine::dsp::dotprod::dot_f32;
use crate::engine::sync::{SyncCandidate, SyncDims};
#[derive(Clone, Debug)]
pub struct Sync2dResult {
pub freq_hz: f32,
pub i0: i32,
pub score: f32,
}
fn make_costas_ref_continuous(pattern: &[u8], ds_spb: usize) -> Vec<Complex<f32>> {
let mut out = Vec::with_capacity(pattern.len() * ds_spb);
let mut phi = 0.0f64;
for &tone in pattern {
let dphi = core::f64::consts::TAU * (tone as f64) / (ds_spb as f64);
for _ in 0..ds_spb {
out.push(Complex::new(phi.cos() as f32, phi.sin() as f32));
phi += dphi;
}
}
out
}
#[cfg(feature = "std")]
type CostasRefContinuousCacheEntry = (&'static [u8], usize, Vec<Complex<f32>>);
#[cfg(feature = "std")]
std::thread_local! {
static COSTAS_REF_CONTINUOUS_CACHE: core::cell::RefCell<Vec<CostasRefContinuousCacheEntry>> =
const { core::cell::RefCell::new(Vec::new()) };
}
#[cfg(feature = "std")]
fn cached_costas_ref_continuous(pattern: &'static [u8], ds_spb: usize) -> Vec<Complex<f32>> {
COSTAS_REF_CONTINUOUS_CACHE.with_borrow_mut(|cache| {
if let Some((_, _, flat)) = cache.iter().find(|(p, d, _)| *p == pattern && *d == ds_spb) {
return flat.clone();
}
let flat = make_costas_ref_continuous(pattern, ds_spb);
if cache.len() >= 2 {
cache.remove(0);
}
cache.push((pattern, ds_spb, flat.clone()));
flat
})
}
#[cfg(not(feature = "std"))]
fn cached_costas_ref_continuous(pattern: &[u8], ds_spb: usize) -> Vec<Complex<f32>> {
make_costas_ref_continuous(pattern, ds_spb)
}
struct FlatRef {
plain: Vec<f32>,
swapped: Vec<f32>,
}
impl FlatRef {
fn with_len(n: usize) -> Self {
Self {
plain: alloc::vec![0.0; n * 2],
swapped: alloc::vec![0.0; n * 2],
}
}
fn fill(&mut self, flat_ref: &[Complex<f32>], df_hz: f32, ds_rate: f32) {
debug_assert_eq!(self.plain.len(), flat_ref.len() * 2);
let omega = 2.0 * PI * df_hz / ds_rate;
let shift = df_hz.abs() >= f32::EPSILON;
for (n, &r) in flat_ref.iter().enumerate() {
let r = if shift {
let p = omega * n as f32;
r * Complex::new(p.cos(), p.sin())
} else {
r
};
self.plain[2 * n] = r.re;
self.plain[2 * n + 1] = r.im;
self.swapped[2 * n] = -r.im;
self.swapped[2 * n + 1] = r.re;
}
}
fn len(&self) -> usize {
self.plain.len() / 2
}
}
fn score_flat_coherent(cd0: &[Complex<f32>], flat_ref: &FlatRef, cd0_start: i32) -> f32 {
let np = cd0.len() as i32;
let len = flat_ref.len() as i32;
if cd0_start < 0 || cd0_start + len > np {
return 0.0;
}
let s0 = cd0_start as usize;
let c: &[f32] = unsafe {
core::slice::from_raw_parts(cd0[s0..].as_ptr() as *const f32, flat_ref.plain.len())
};
let zr = dot_f32(c, &flat_ref.plain);
let zi = dot_f32(c, &flat_ref.swapped);
(zr * zr + zi * zi).sqrt()
}
pub fn fst4_sync_search<P: Protocol>(
cd0: &[Complex<f32>],
candidate: &SyncCandidate,
) -> Sync2dResult {
let d = SyncDims::of::<P>(12_000.0);
let ds_spb = d.ds_spb;
let ds_rate = d.ds_rate;
let baud = P::TONE_SPACING_HZ;
let init_i0 = ((candidate.dt_sec + P::TX_START_OFFSET_S) * ds_rate).round() as i32;
let ishw = (1.5 * ds_rate as f64).floor() as i32;
let flat_blocks: Vec<(i32, Vec<Complex<f32>>)> = P::SYNC_MODE
.blocks()
.iter()
.map(|b| {
let off = b.start_symbol as i32 * ds_spb as i32;
let flat = make_costas_ref_continuous(b.pattern, ds_spb);
(off, flat)
})
.collect();
let mut twiddled: Vec<(i32, FlatRef)> = flat_blocks
.iter()
.map(|(off, flat)| (*off, FlatRef::with_len(flat.len())))
.collect();
let score_flat = |twiddled: &Vec<(i32, FlatRef)>, i0: i32| -> f32 {
twiddled
.iter()
.map(|(off, flat)| score_flat_coherent(cd0, flat, i0 + off))
.sum::<f32>()
};
let retwiddle = |twiddled: &mut Vec<(i32, FlatRef)>, df: f32| {
for ((_, dst), (_, src)) in twiddled.iter_mut().zip(flat_blocks.iter()) {
dst.fill(src, df, ds_rate);
}
};
let mut best_df = 0.0f32;
let mut best_i0 = init_i0;
let mut best_score = f32::NEG_INFINITY;
for si in -12i32..=12 {
let df = si as f32 * 0.1 * baud;
retwiddle(&mut twiddled, df);
let mut di = -ishw;
while di <= ishw {
let i0 = init_i0 + di;
let s = score_flat(&twiddled, i0);
if s > best_score {
best_score = s;
best_df = df;
best_i0 = i0;
}
di += 4;
}
}
let coarse_winner_df = best_df;
let coarse_winner_i0 = best_i0;
best_score = 0.0;
for si in -7i32..=7 {
let df = coarse_winner_df + si as f32 * 0.02 * baud;
retwiddle(&mut twiddled, df);
for di in -4i32..=4 {
let i0 = coarse_winner_i0 + di;
let s = score_flat(&twiddled, i0);
if s > best_score {
best_score = s;
best_df = df;
best_i0 = i0;
}
}
}
Sync2dResult {
freq_hz: candidate.freq_hz + best_df,
i0: best_i0,
score: best_score,
}
}
pub fn ft4_sync_search<P: Protocol>(
cd0: &[Complex<f32>],
candidate: &SyncCandidate,
) -> Sync2dResult {
ft4_sync_search_window::<P>(cd0, candidate, -344, 1012)
}
pub fn ft4_sync_search_window<P: Protocol>(
cd0: &[Complex<f32>],
candidate: &SyncCandidate,
ib_min: i32,
ib_max: i32,
) -> Sync2dResult {
let d = SyncDims::of::<P>(12_000.0);
let ds_spb = d.ds_spb;
let ds_rate = d.ds_rate;
const COARSE_DT_STEP: i32 = 4;
let blocks_ref: Vec<(i32, Vec<Complex<f32>>)> = P::SYNC_MODE
.blocks()
.iter()
.map(|b| {
let off = b.start_symbol as i32 * ds_spb as i32;
(off, cached_costas_ref_continuous(b.pattern, ds_spb))
})
.collect();
let score_at = |i0: i32, step: Complex<f32>, has_df: bool| -> f32 {
blocks_ref
.iter()
.map(|(off, flat)| {
let cd0_start = i0 + off;
let np = cd0.len() as i32;
let len = flat.len() as i32;
if cd0_start < 0 || cd0_start + len > np {
return 0.0;
}
let s0 = cd0_start as usize;
if !has_df {
let z: Complex<f32> = cd0[s0..s0 + len as usize]
.iter()
.zip(flat.iter())
.map(|(&c, &r)| c * r.conj())
.sum();
z.norm()
} else {
let mut twid = Complex::new(1.0f32, 0.0f32);
let z: Complex<f32> = cd0[s0..s0 + len as usize]
.iter()
.zip(flat.iter())
.map(|(&c, &r)| {
let val = c * r.conj() * twid;
twid *= step;
val
})
.sum();
z.norm()
}
})
.sum::<f32>()
};
let mut best_df = 0.0f32;
let mut best_i0 = ((candidate.dt_sec + P::TX_START_OFFSET_S) * ds_rate).round() as i32;
let mut best_score = f32::NEG_INFINITY;
let phasor_for = |df: f32| -> (Complex<f32>, bool) {
let has_df = df.abs() >= f32::EPSILON;
let step = if has_df {
let omega = -2.0 * PI * df / ds_rate;
Complex::new(omega.cos(), omega.sin())
} else {
Complex::new(1.0f32, 0.0f32)
};
(step, has_df)
};
let mut idf = -12i32;
while idf <= 12 {
let df = idf as f32;
let (step, has_df) = phasor_for(df);
let mut i0 = ib_min;
while i0 <= ib_max {
let s = score_at(i0, step, has_df);
if s > best_score {
best_score = s;
best_df = df;
best_i0 = i0;
}
i0 += COARSE_DT_STEP;
}
idf += 3;
}
let coarse_winner_df = best_df;
let coarse_winner_i0 = best_i0;
best_score = f32::NEG_INFINITY;
for si in -4i32..=4 {
let df = coarse_winner_df + si as f32;
let (step, has_df) = phasor_for(df);
for di in -5i32..=5 {
let i0 = coarse_winner_i0 + di;
let s = score_at(i0, step, has_df);
if s > best_score {
best_score = s;
best_df = df;
best_i0 = i0;
}
}
}
Sync2dResult {
freq_hz: candidate.freq_hz + best_df,
i0: best_i0,
score: best_score,
}
}
pub fn freq_shift_cd0(cd0: &[Complex<f32>], df_hz: f32, ds_rate: f32) -> Vec<Complex<f32>> {
if df_hz.abs() < f32::EPSILON {
return cd0.to_vec();
}
let omega = -2.0 * PI * df_hz / ds_rate;
cd0.iter()
.enumerate()
.map(|(n, &c)| {
let p = omega * n as f32;
c * Complex::new(p.cos(), p.sin())
})
.collect()
}