use core::f32::consts::PI;
#[must_use]
pub fn fit_polynomial(
values: &[f32],
max_degree: usize,
error_threshold: f64,
) -> Option<(Vec<f64>, usize, f64)> {
if values.len() < 2 {
return None;
}
let variance = variance_f32(values);
if variance == 0.0 {
return Some((vec![f64::from(values[0])], 0, 0.0));
}
let n = values.len();
let ys: Vec<f64> = values.iter().map(|v| f64::from(*v)).collect();
for degree in 1..=max_degree {
let Some(coeffs) = solve_polynomial_ls(n, &ys, degree) else {
continue;
};
let mse = ys
.iter()
.enumerate()
.map(|(i, &y)| {
let yhat = eval_poly(&coeffs, i as f64);
(y - yhat).powi(2)
})
.sum::<f64>()
/ (n as f64);
let rel = mse / variance;
if rel < error_threshold {
return Some((coeffs, degree, rel));
}
}
None
}
#[must_use]
pub fn generate_polynomial(n: usize, coefficients: &[f64]) -> Vec<f32> {
(0..n)
.map(|i| eval_poly(coefficients, i as f64) as f32)
.collect()
}
#[must_use]
pub fn analyze_signal(
values: &[f32],
max_coefficients: usize,
energy_threshold: f32,
) -> (Vec<(usize, f32, f32)>, f32) {
if values.is_empty() || max_coefficients == 0 {
return (Vec::new(), 0.0);
}
let n = values.len();
let dc_offset = values.iter().sum::<f32>() / (n as f32);
let half = n / 2;
let mut spectrum: Vec<(usize, f32, f32, f32)> = Vec::with_capacity(half); for k in 1..=half {
let (re, im) = dft_bin(values, k);
let mag = (re * re + im * im).sqrt();
let phase = im.atan2(re);
spectrum.push((k, mag, phase, mag * mag));
}
spectrum.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
spectrum.truncate(max_coefficients);
let total_energy: f32 = spectrum.iter().map(|(_, _, _, e)| *e).sum();
if total_energy > 0.0 && energy_threshold > 0.0 && energy_threshold < 1.0 {
let cutoff = total_energy * energy_threshold;
let mut cum = 0.0;
let mut keep = 0;
for (i, &(_, _, _, e)) in spectrum.iter().enumerate() {
cum += e;
keep = i + 1;
if cum >= cutoff {
break;
}
}
spectrum.truncate(keep);
}
let coefs = spectrum.into_iter().map(|(k, m, p, _)| (k, m, p)).collect();
(coefs, dc_offset)
}
#[must_use]
pub fn generate_from_coefficients(
n: usize,
coefficients: &[(usize, f32, f32)],
dc_offset: f32,
) -> Vec<f32> {
if n == 0 {
return Vec::new();
}
let inv_n = 1.0 / n as f32;
(0..n)
.map(|i| {
let x = i as f32;
let sum: f32 = coefficients
.iter()
.map(|&(k, mag, phase)| {
let amp = 2.0 * mag * inv_n;
let theta = 2.0 * PI * k as f32 * x * inv_n + phase;
amp * theta.cos()
})
.sum();
sum + dc_offset
})
.collect()
}
#[must_use]
pub fn generate_sine_wave(
n: usize,
frequency: f32,
amplitude: f32,
phase: f32,
dc_offset: f32,
) -> Vec<f32> {
if n == 0 {
return Vec::new();
}
let inv_n = 1.0 / n as f32;
(0..n)
.map(|i| {
let theta = 2.0 * PI * frequency * i as f32 * inv_n + phase;
amplitude * theta.sin() + dc_offset
})
.collect()
}
#[must_use]
pub fn generate_multi_sine(
n: usize,
components: &[(f32, f32, f32)],
dc_offset: f32,
) -> Vec<f32> {
if n == 0 {
return Vec::new();
}
let inv_n = 1.0 / n as f32;
(0..n)
.map(|i| {
let x = i as f32;
let sum: f32 = components
.iter()
.map(|&(freq, amp, phase)| {
let theta = 2.0 * PI * freq * x * inv_n + phase;
amp * theta.sin()
})
.sum();
sum + dc_offset
})
.collect()
}
#[must_use]
pub fn generate_perlin_advanced(
n: usize,
_dimension: usize,
seed: u64,
scale: f32,
octaves: u32,
persistence: f32,
lacunarity: f32,
) -> Vec<f32> {
if n == 0 {
return Vec::new();
}
let octaves = octaves.max(1);
let inv_n = 1.0 / n as f32;
(0..n)
.map(|i| {
let mut sum = 0.0_f32;
let mut amplitude = 1.0_f32;
let mut frequency = scale.max(1e-6);
let mut max_amp = 0.0_f32;
let x = i as f32 * inv_n;
for _ in 0..octaves {
let px = x * frequency;
sum += amplitude * value_noise_1d(px, seed);
max_amp += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
if max_amp > 0.0 {
sum / max_amp
} else {
sum
}
})
.collect()
}
fn variance_f32(values: &[f32]) -> f64 {
let n = values.len() as f64;
if n == 0.0 {
return 0.0;
}
let mean = values.iter().map(|v| f64::from(*v)).sum::<f64>() / n;
values
.iter()
.map(|v| (f64::from(*v) - mean).powi(2))
.sum::<f64>()
/ n
}
fn eval_poly(coeffs: &[f64], x: f64) -> f64 {
coeffs.iter().rev().fold(0.0_f64, |acc, &c| acc * x + c)
}
fn solve_polynomial_ls(n: usize, ys: &[f64], degree: usize) -> Option<Vec<f64>> {
let k = degree + 1; if n < k {
return None;
}
let mut s = vec![0.0_f64; 2 * degree + 1];
for i in 0..n {
let xi = i as f64;
let mut pow = 1.0_f64;
for sj in s.iter_mut() {
*sj += pow;
pow *= xi;
}
}
let mut t = vec![0.0_f64; k];
for (i, &y) in ys.iter().enumerate() {
let xi = i as f64;
let mut pow = 1.0_f64;
for tj in t.iter_mut() {
*tj += y * pow;
pow *= xi;
}
}
let mut a: Vec<Vec<f64>> = (0..k)
.map(|r| {
let mut row = Vec::with_capacity(k + 1);
for c in 0..k {
row.push(s[r + c]);
}
row.push(t[r]);
row
})
.collect();
#[allow(clippy::needless_range_loop)]
for i in 0..k {
let mut pivot = i;
let mut best = a[i][i].abs();
for r in (i + 1)..k {
let v = a[r][i].abs();
if v > best {
best = v;
pivot = r;
}
}
if best < 1e-12 {
return None;
}
a.swap(i, pivot);
for r in (i + 1)..k {
let factor = a[r][i] / a[i][i];
for c in i..=k {
a[r][c] -= factor * a[i][c];
}
}
}
let mut coeffs = vec![0.0_f64; k];
for i in (0..k).rev() {
let mut sum = a[i][k];
for c in (i + 1)..k {
sum -= a[i][c] * coeffs[c];
}
coeffs[i] = sum / a[i][i];
}
Some(coeffs)
}
fn value_noise_1d(x: f32, seed: u64) -> f32 {
let x0 = x.floor();
let x1 = x0 + 1.0;
let t = x - x0;
let s = t * t * (3.0 - 2.0 * t);
let g0 = hash_to_unit(x0 as i32, seed);
let g1 = hash_to_unit(x1 as i32, seed);
g0 * (1.0 - s) + g1 * s
}
fn hash_to_unit(x: i32, seed: u64) -> f32 {
let mut h = (x as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
h ^= seed.wrapping_mul(0x85eb_ca6b_c2b2_ae35);
h ^= h >> 33;
h = h.wrapping_mul(0xc2b2_ae35_9e37_79b9);
h ^= h >> 33;
let f = (h & 0x00ff_ffff) as f32 / 8_388_608.0; f - 1.0
}
fn dft_bin(values: &[f32], k: usize) -> (f32, f32) {
let n = values.len() as f32;
let inv_n = 1.0 / n;
let mut re = 0.0_f32;
let mut im = 0.0_f32;
for (i, &v) in values.iter().enumerate() {
let theta = 2.0 * PI * k as f32 * i as f32 * inv_n;
re += v * theta.cos();
im -= v * theta.sin();
}
(re, im)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_polynomial_linear() {
let out = generate_polynomial(5, &[3.0, 2.0]);
assert_eq!(out, vec![3.0, 5.0, 7.0, 9.0, 11.0]);
}
#[test]
fn generate_polynomial_quadratic() {
let out = generate_polynomial(4, &[1.0, 1.0, 1.0]);
assert_eq!(out, vec![1.0, 3.0, 7.0, 13.0]);
}
#[test]
fn generate_polynomial_empty() {
assert!(generate_polynomial(0, &[1.0, 2.0]).is_empty());
}
#[test]
fn fit_polynomial_linear_series() {
let values: Vec<f32> = (0..20).map(|i| 3.0 + 2.0 * i as f32).collect();
let (coefs, deg, err) = fit_polynomial(&values, 3, 1e-6).expect("linear fit");
assert_eq!(deg, 1);
assert!((coefs[0] - 3.0).abs() < 1e-4);
assert!((coefs[1] - 2.0).abs() < 1e-4);
assert!(err < 1e-6);
}
#[test]
fn fit_polynomial_quadratic_series() {
let values: Vec<f32> = (0..20)
.map(|i| 1.0 + i as f32 + (i as f32).powi(2))
.collect();
let (coefs, deg, _) = fit_polynomial(&values, 4, 1e-4).expect("quadratic fit");
assert_eq!(deg, 2);
assert!((coefs[0] - 1.0).abs() < 1e-3);
assert!((coefs[1] - 1.0).abs() < 1e-3);
assert!((coefs[2] - 1.0).abs() < 1e-3);
}
#[test]
fn fit_polynomial_noise_returns_none() {
let values: Vec<f32> = (0..40)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
assert!(fit_polynomial(&values, 3, 0.0001).is_none());
}
#[test]
fn fit_polynomial_too_short() {
assert!(fit_polynomial(&[1.0], 2, 0.01).is_none());
assert!(fit_polynomial(&[], 2, 0.01).is_none());
}
#[test]
fn generate_sine_wave_shape() {
let out = generate_sine_wave(4, 1.0, 1.0, 0.0, 0.0);
assert!(out[0].abs() < 1e-5);
assert!((out[1] - 1.0).abs() < 1e-5);
assert!(out[2].abs() < 1e-5);
assert!((out[3] + 1.0).abs() < 1e-5);
}
#[test]
fn analyze_signal_pure_tone() {
let n = 32usize;
let samples: Vec<f32> = (0..n)
.map(|i| 2.0 * (2.0 * PI * i as f32 / n as f32).sin())
.collect();
let (coefs, dc) = analyze_signal(&samples, 4, 0.99);
assert!(dc.abs() < 1e-4, "DC offset should be ~0");
assert!(!coefs.is_empty());
assert_eq!(coefs[0].0, 1);
}
#[test]
fn round_trip_pure_tone() {
let n = 32usize;
let samples: Vec<f32> = (0..n)
.map(|i| 3.0 + 2.0 * (2.0 * PI * i as f32 / n as f32).sin())
.collect();
let (coefs, dc) = analyze_signal(&samples, 4, 0.99);
let recon = generate_from_coefficients(n, &coefs, dc);
assert_eq!(recon.len(), n);
let mse: f32 = samples
.iter()
.zip(recon.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
/ n as f32;
assert!(mse < 0.05, "reconstruction MSE = {mse} too high");
}
#[test]
fn analyze_signal_empty_and_zero_max() {
let (c, dc) = analyze_signal(&[], 3, 0.9);
assert!(c.is_empty() && dc == 0.0);
let (c, _) = analyze_signal(&[1.0, 2.0, 3.0], 0, 0.9);
assert!(c.is_empty());
}
#[test]
fn generate_from_coefficients_empty() {
assert!(generate_from_coefficients(0, &[], 0.0).is_empty());
}
}