use std::f64::consts::TAU;
use std::collections::BTreeMap;
use sva_formula::spectral_sum::atom::{Exp, Singular, SpectralAtom, SpectralAtomKey};
use sva_formula::{
C64, Lane, Line, Origin, Run, commensurate, lines as series_lines, modal, spacing,
};
use crate::fft::idft;
use crate::label::Dropped;
pub struct Found {
pub lines: Vec<Line>,
pub grids: Vec<f64>,
pub tail_db: Option<f64>,
}
pub fn of_lane(lane: &Lane, ceiling: f64, floor_db: f64, precision: f64) -> Option<Found> {
let mut out = Vec::new();
let mut grids = Vec::new();
let mut tail: Option<f64> = None;
for atom in &lane.atoms {
out.push(as_line(atom)?);
}
for bank in &lane.modal {
for atom in modal::atoms(bank, Origin::UNKNOWN) {
out.push(as_line(&atom)?);
}
}
for series in &lane.series {
let enumerated = series_lines(series, ceiling, floor_db, precision);
if enumerated.taken.is_empty() && enumerated.dropped.is_empty() {
return None;
}
if enumerated.tail_db.is_finite() {
tail = Some(tail.map_or(enumerated.tail_db, |held: f64| held.max(enumerated.tail_db)));
}
grids.extend(spacing(series));
out.extend(enumerated.taken);
out.extend(enumerated.dropped);
}
Some(Found {
lines: out,
grids,
tail_db: tail,
})
}
const MAX_TRANSFORM: usize = 1 << 21;
pub fn grid(kept: &[Vec<Line>], grids: &[f64], rate: u32, span: f64, len: usize) -> usize {
let mut best = bins(span, rate);
let mut placed = on_grid(kept, best, rate);
for step in grids {
let Some(n) = closing(*step, rate, len) else {
continue;
};
let found = on_grid(kept, n, rate);
if found > placed || (found == placed && n < best) {
best = n;
placed = found;
}
}
best
}
fn closing(step: f64, rate: u32, len: usize) -> Option<usize> {
let period = f64::from(rate) / step;
if !period.is_finite() || period <= 0.0 {
return None;
}
let bound = len.max(MAX_TRANSFORM) as f64;
let base = whole_periods(period, bound)?;
let n = base * (len as f64 / base).ceil().max(1.0);
(n <= bound).then_some(n.round() as usize)
}
fn whole_periods(period: f64, bound: f64) -> Option<f64> {
let (mut held, mut before) = (0.0f64, 1.0f64);
let mut x = period;
for _ in 0..MAX_CONVERGENTS {
let whole = x.floor();
let turns = whole * held + before;
let samples = period * turns;
if (samples.round() - samples).abs() <= TURN_EPSILON * samples.max(1.0) {
return (samples <= bound).then_some(samples.round());
}
let fraction = x - whole;
if samples > bound || fraction <= 0.0 {
return None;
}
x = 1.0 / fraction;
before = held;
held = turns;
}
None
}
const MAX_CONVERGENTS: usize = 64;
const TURN_EPSILON: f64 = 1e-9;
fn on_grid(kept: &[Vec<Line>], n: usize, rate: u32) -> usize {
let span = n as f64 / f64::from(rate);
kept.iter()
.flat_map(|lane| lane.iter())
.filter(|l| commensurate(l.hz, span))
.count()
}
fn as_line(a: &SpectralAtom) -> Option<Line> {
if !a.poly.is_one()
|| a.gauss.is_some()
|| a.ind.is_some()
|| a.pole.is_some()
|| !matches!(a.sing, Singular::Regular)
{
return None;
}
let omega = a
.exp
.map_or(0.0, |e| if e.sigma == 0.0 { e.omega } else { f64::NAN });
omega.is_finite().then(|| Line::bare(omega / TAU, a.c))
}
pub fn grouped(lane: &Lane) -> Option<Vec<(SpectralAtom, Vec<Line>)>> {
if !lane.is_finite_sum() || lane.atoms.is_empty() {
return None;
}
let mut at: BTreeMap<SpectralAtomKey, usize> = BTreeMap::new();
let mut out: Vec<(SpectralAtom, Vec<Line>)> = Vec::new();
for a in &lane.atoms {
let exp = a.exp.unwrap_or(Exp::at(0.0, 0.0));
if exp.sigma != 0.0 || a.pole.is_some() || !matches!(a.sing, Singular::Regular) {
return None;
}
let held = *at.entry(a.key_without_line()).or_insert_with(|| {
out.push((
SpectralAtom {
c: C64::ONE,
exp: None,
..*a
},
Vec::new(),
));
out.len() - 1
});
out[held].1.push(Line::bare(exp.omega / TAU, a.c));
}
Some(out)
}
pub struct Direct {
level: f64,
runs: Vec<Run>,
lines: usize,
}
impl Direct {
pub fn of(kept: &[Line]) -> Option<Direct> {
if kept.is_empty() {
return None;
}
let (dc, moving): (Vec<Line>, Vec<Line>) = kept.iter().partition(|l| l.hz == 0.0);
Some(Direct {
level: dc.iter().map(|l| l.amp.re).sum(),
runs: Run::of(&moving),
lines: kept.len(),
})
}
pub fn hz(&self) -> Vec<f64> {
self.runs
.iter()
.flat_map(Run::lines)
.map(|l| l.hz)
.collect()
}
pub fn at(&self, t: f64) -> f64 {
let moving: f64 = self.runs.iter().map(|r| super::run::at(r, t).re).sum();
self.level + moving
}
pub fn lines_priced_and_turned(&self) -> (usize, usize) {
(self.lines, self.runs.iter().map(Run::len).sum())
}
pub fn bound(&self) -> f64 {
let runs: f64 = self.runs.iter().map(super::run::bound).sum();
let reach: f64 = self.runs.iter().map(super::run::reach).sum::<f64>() + self.level.abs();
let adds = (self.runs.len() + 2) as f64 * f64::EPSILON;
(runs + adds * reach) * (1.0 + adds)
}
}
pub fn add_direct(plane: &mut [f64], kept: &[Line], extent: super::Extent, rate: u32) {
let Some(direct) = Direct::of(kept) else {
return;
};
for (i, held) in plane.iter_mut().enumerate() {
*held += direct.at((extent.start + i as i64) as f64 / f64::from(rate));
}
}
pub fn transformed(
kept: &[Line],
extent: super::Extent,
n: usize,
rate: u32,
len: usize,
) -> Vec<f64> {
let start_secs = extent.start_secs(rate);
if kept.is_empty() {
return vec![0.0; len];
}
let span = n as f64 / f64::from(rate);
let mut re = vec![0.0; n];
let mut im = vec![0.0; n];
for l in kept {
let bin = (l.hz * span).round() as i64;
let k = bin.rem_euclid(n as i64) as usize;
let shifted = l.amp * C64::new(0.0, TAU * l.hz * start_secs).exp();
re[k] += shifted.re * n as f64;
im[k] += shifted.im * n as f64;
}
idft(&mut re, &mut im);
re.truncate(len);
re.resize(len, 0.0);
re
}
pub fn bins(span: f64, rate: u32) -> usize {
(span * f64::from(rate)).round().max(1.0) as usize
}
pub fn split(kept: &[Line], n: usize, rate: u32) -> (Vec<Line>, Vec<Line>) {
let span = n as f64 / f64::from(rate);
kept.iter()
.partition(|l| l.hz != 0.0 && commensurate(l.hz, span))
}
pub fn dropped_list(per_lane: &[Vec<Line>]) -> (Vec<Dropped>, usize) {
let mut folded = merge(per_lane, f64::max);
let total = folded.len();
if total > 64 {
folded.sort_by(|a, b| b.1.total_cmp(&a.1));
folded.truncate(64);
folded.sort_by(|a, b| a.0.total_cmp(&b.0));
}
let list = folded
.into_iter()
.map(|(hz, amp)| Dropped {
hz,
db: 20.0 * amp.log10(),
})
.collect();
(list, total.saturating_sub(64))
}
pub fn distinct(per_lane: &[Vec<Line>]) -> usize {
merge(per_lane, f64::max).len()
}
fn merge(per_lane: &[Vec<Line>], across: fn(f64, f64) -> f64) -> Vec<(f64, f64)> {
let mut out: Vec<(f64, f64)> = Vec::new();
for lane in per_lane {
out.extend(fold_lane(lane));
}
out.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut merged: Vec<(f64, f64)> = Vec::with_capacity(out.len());
for (hz, amp) in out {
match merged.last_mut() {
Some((at, held)) if *at == hz => *held = across(*held, amp),
_ => merged.push((hz, amp)),
}
}
merged
}
fn fold_lane(lane: &[Line]) -> Vec<(f64, f64)> {
let mut out: Vec<(f64, f64)> = lane.iter().map(|l| (l.hz.abs(), l.amp.abs())).collect();
out.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut folded: Vec<(f64, f64)> = Vec::with_capacity(out.len());
for (hz, amp) in out {
match folded.last_mut() {
Some((at, held)) if *at == hz => *held += amp,
_ => folded.push((hz, amp)),
}
}
folded
}