extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
#[derive(Clone, Copy, Debug)]
pub struct BaselineParams {
pub nseg: usize,
pub npct: usize,
pub nterms: usize,
pub offset_db: f32,
}
impl BaselineParams {
pub const FT8_FT4: Self = Self {
nseg: 10,
npct: 10,
nterms: 5,
offset_db: 0.65,
};
pub const FST4: Self = Self {
nseg: 8,
npct: 30,
nterms: 3,
offset_db: 0.2,
};
}
pub fn fit_baseline(
avg_spectrum_power: &[f32],
freq_min_bin: usize,
freq_max_bin: usize,
) -> Vec<f32> {
fit_baseline_with(
avg_spectrum_power,
freq_min_bin,
freq_max_bin,
BaselineParams::FT8_FT4,
)
}
pub fn fit_baseline_with(
avg_spectrum_power: &[f32],
freq_min_bin: usize,
freq_max_bin: usize,
params: BaselineParams,
) -> Vec<f32> {
let BaselineParams {
nseg,
npct,
nterms,
offset_db,
} = params;
let ia = freq_min_bin.min(avg_spectrum_power.len().saturating_sub(1));
let ib = freq_max_bin.min(avg_spectrum_power.len().saturating_sub(1));
if ib <= ia {
return Vec::new();
}
let n = ib - ia + 1;
let s_db: Vec<f32> = avg_spectrum_power[ia..=ib]
.iter()
.map(|&p| 10.0 * p.max(1e-30).log10())
.collect();
let nlen = n / nseg;
if nlen == 0 {
return s_db;
}
let i0 = (n / 2) as i32;
let mut xs: Vec<f64> = Vec::with_capacity(n);
let mut ys: Vec<f64> = Vec::with_capacity(n);
for seg in 0..nseg {
let ja = seg * nlen;
let jb = (ja + nlen).min(n);
if jb <= ja {
continue;
}
let mut sorted: Vec<f32> = s_db[ja..jb].to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let pct_idx = (npct * sorted.len()) / 100;
let base = sorted[pct_idx.min(sorted.len() - 1)];
for j in ja..jb {
if s_db[j] <= base {
xs.push(j as f64 - i0 as f64);
ys.push(s_db[j] as f64);
}
}
}
if xs.len() < nterms {
let mut flat = s_db.clone();
flat.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = flat[flat.len() / 2];
return vec![med + offset_db; n];
}
let coeffs = polyfit_nterm(&xs, &ys, nterms);
(0..n)
.map(|i| {
let t = i as f64 - i0 as f64;
let mut p = coeffs[nterms - 1];
for &c in coeffs[..nterms - 1].iter().rev() {
p = p * t + c;
}
p as f32 + offset_db
})
.collect()
}
fn polyfit_nterm(xs: &[f64], ys: &[f64], nterms: usize) -> Vec<f64> {
debug_assert_eq!(xs.len(), ys.len());
debug_assert!(xs.len() >= nterms);
let mut mom = vec![0.0f64; 2 * nterms - 1];
let mut rhs = vec![0.0f64; nterms];
for (i, &x) in xs.iter().enumerate() {
let mut xp = 1.0f64;
for k in 0..nterms {
mom[k] += xp;
rhs[k] += xp * ys[i];
xp *= x;
}
for k in nterms..2 * nterms - 1 {
mom[k] += xp;
xp *= x;
}
}
let mut aug = vec![vec![0.0f64; nterms + 1]; nterms];
for (i, row) in aug.iter_mut().enumerate() {
row[..nterms].copy_from_slice(&mom[i..i + nterms]);
row[nterms] = rhs[i];
}
for i in 0..nterms {
let mut max_row = i;
let mut max_abs = aug[i][i].abs();
for r in (i + 1)..nterms {
if aug[r][i].abs() > max_abs {
max_abs = aug[r][i].abs();
max_row = r;
}
}
if max_row != i {
aug.swap(i, max_row);
}
if aug[i][i].abs() < 1e-30 {
return vec![0.0; nterms];
}
for r in (i + 1)..nterms {
let factor = aug[r][i] / aug[i][i];
for c in i..=nterms {
aug[r][c] -= factor * aug[i][c];
}
}
}
let mut a = vec![0.0f64; nterms];
for i in (0..nterms).rev() {
let mut s = aug[i][nterms];
for j in (i + 1)..nterms {
s -= aug[i][j] * a[j];
}
a[i] = s / aug[i][i];
}
a
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_spectrum_gives_flat_baseline() {
let n = 1000;
let avg = vec![100.0f32; n]; let base = fit_baseline(&avg, 0, n - 1);
assert_eq!(base.len(), n);
for &b in base.iter().take(50).chain(base.iter().skip(950)) {
assert!(
(b - 20.65).abs() < 0.5,
"expected ~20.65 dB for flat power, got {b:.2}"
);
}
}
#[test]
fn baseline_below_signal_peak() {
let n = 1000;
let mut avg = vec![100.0f32; n];
avg[500] = 10000.0;
let base = fit_baseline(&avg, 0, n - 1);
assert!(
base[500] < 30.0,
"baseline at signal bin {:.2} dB; expected near 20 dB noise floor",
base[500]
);
}
}