use nalgebra::{Complex, DMatrix, DVector, RealField};
use num_traits::Float;
use rustfft::{FftNum, FftPlanner};
use thiserror::Error;
use crate::{Continuous, Discrete, Polynomial, TransferFunction};
#[derive(Clone, Debug, Error, PartialEq)]
pub enum SmlError {
#[error("input and output lengths differ: {input} vs {output}")]
LengthMismatch { input: usize, output: usize },
#[error("{samples} samples are not a whole number of periods of {period} samples")]
NotWholePeriods { samples: usize, period: usize },
#[error("{periods} periods, at least 4 needed")]
TooFewPeriods { periods: usize },
#[error("line {line} outside 0 ..= {} for the period of {period} samples", period / 2)]
InvalidLine { line: usize, period: usize },
#[error("spectral line {index}: non-finite values, negative variance or no noise")]
InvalidSpectralLine { index: usize },
#[error("{lines} lines for {parameters} parameters (2 F > nθ needed)")]
TooFewLines { lines: usize, parameters: usize },
#[error("invalid sampling period")]
InvalidSamplingPeriod,
#[error("singular least-squares problem (check orders and data)")]
Singular,
}
#[derive(Clone, Copy, Debug)]
pub struct SpectralLine<T> {
pub omega: T,
pub u: Complex<T>,
pub y: Complex<T>,
pub var_u: T,
pub var_y: T,
pub cov_yu: Complex<T>,
}
#[derive(Clone, Debug)]
pub struct SmlData<T> {
pub lines: Vec<SpectralLine<T>>,
pub periods: usize,
}
impl<T: Float> SmlData<T> {
pub fn new(lines: Vec<SpectralLine<T>>, periods: usize) -> Result<Self, SmlError> {
if periods < 4 {
return Err(SmlError::TooFewPeriods { periods });
}
for (index, l) in lines.iter().enumerate() {
let finite = [l.omega, l.u.re, l.u.im, l.y.re, l.y.im, l.var_u, l.var_y, l.cov_yu.re, l.cov_yu.im].iter().all(|v| v.is_finite());
if !finite || l.var_u < T::zero() || l.var_y < T::zero() || l.var_u + l.var_y == T::zero() {
return Err(SmlError::InvalidSpectralLine { index });
}
}
Ok(Self { lines, periods })
}
pub fn from_periods(u: &[T], y: &[T], ts: T, period: usize, lines: &[usize]) -> Result<Self, SmlError>
where
T: FftNum,
{
if u.len() != y.len() {
return Err(SmlError::LengthMismatch { input: u.len(), output: y.len() });
}
if !(ts > T::zero() && ts.is_finite()) {
return Err(SmlError::InvalidSamplingPeriod);
}
if period == 0 || !u.len().is_multiple_of(period) {
return Err(SmlError::NotWholePeriods { samples: u.len(), period });
}
let periods = u.len() / period;
if periods < 4 {
return Err(SmlError::TooFewPeriods { periods });
}
if let Some(&line) = lines.iter().find(|&&k| k > period / 2) {
return Err(SmlError::InvalidLine { line, period });
}
let fft = FftPlanner::new().plan_fft_forward(period);
let spectra = |x: &[T]| -> Vec<Vec<Complex<T>>> {
x.chunks(period)
.map(|chunk| {
let mut buffer: Vec<Complex<T>> = chunk.iter().map(|&v| Complex::new(v, T::zero())).collect();
fft.process(&mut buffer);
lines.iter().map(|&k| buffer[k]).collect()
})
.collect()
};
let (us, ys) = (spectra(u), spectra(y));
let m = T::from(periods).unwrap();
let scale = T::one() / (m * (m - T::one()));
let two_pi = T::from(2.0 * std::f64::consts::PI).unwrap();
let zero = Complex::new(T::zero(), T::zero());
let lines = lines
.iter()
.enumerate()
.map(|(i, &k)| {
let u_mean = us.iter().fold(zero, |acc, s| acc + s[i]) / m;
let y_mean = ys.iter().fold(zero, |acc, s| acc + s[i]) / m;
let (mut var_u, mut var_y, mut cov_yu) = (T::zero(), T::zero(), zero);
for (su, sy) in us.iter().zip(&ys) {
let (du, dy) = (su[i] - u_mean, sy[i] - y_mean);
var_u = var_u + du.norm_sqr();
var_y = var_y + dy.norm_sqr();
cov_yu = cov_yu + dy * du.conj();
}
SpectralLine {
omega: two_pi * T::from(k).unwrap() / (T::from(period).unwrap() * ts),
u: u_mean,
y: y_mean,
var_u: var_u * scale,
var_y: var_y * scale,
cov_yu: cov_yu * scale,
}
})
.collect();
Self::new(lines, periods)
}
}
#[derive(Clone, Debug)]
pub struct SmlOptions<T> {
pub initial_iterations: usize,
pub max_iterations: usize,
pub tolerance: T,
}
impl<T: Float> Default for SmlOptions<T> {
fn default() -> Self {
Self { initial_iterations: 20, max_iterations: 100, tolerance: T::from(1e-10).unwrap() }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Termination {
Converged,
Stalled,
MaxIterations,
}
#[derive(Clone, Debug)]
pub struct SmlResult<T, D> {
pub model: TransferFunction<T, D>,
pub std_numerator: Vec<T>,
pub std_denominator: Vec<T>,
pub cost: T,
pub expected_cost: T,
pub cost_std: T,
pub iterations: usize,
pub termination: Termination,
}
impl<T, D> SmlResult<T, D> {
pub fn converged(&self) -> bool {
self.termination == Termination::Converged
}
}
pub fn identify_continuous<T: Float + RealField>(
data: &SmlData<T>,
denom_order: usize,
numer_order: usize,
options: &SmlOptions<T>,
) -> Result<SmlResult<T, Continuous>, SmlError> {
let (n, m) = (denom_order, numer_order);
let positive = data.lines.iter().map(|l| Float::abs(l.omega)).filter(|&w| w > T::zero());
let (low, high) = positive.fold((T::infinity(), T::zero()), |(lo, hi), w| (Float::min(lo, w), Float::max(hi, w)));
let omega_s = if high > T::zero() { Float::sqrt(low * high) } else { T::one() };
let basis: Vec<LineBasis<T>> = data
.lines
.iter()
.map(|l| {
let s = Complex::new(T::zero(), l.omega / omega_s);
LineBasis { den: powers(s, n + 1), num: powers(s, m + 1) }
})
.collect();
let fit = fit(data, &basis, n, options)?;
let factor = |i: usize| Float::powi(omega_s, n as i32 - i as i32);
let (a, b) = (fit.denominator(n, n), &fit.theta[n..]);
let denominator: Vec<T> = (0..=n).rev().map(|i| a[i] * factor(i)).collect();
let numerator: Vec<T> = (0..=m).rev().map(|j| b[j] * factor(j)).collect();
let std_denominator: Vec<T> = (0..=n).rev().map(|i| if i == n { T::zero() } else { fit.std[i] * factor(i) }).collect();
let std_numerator: Vec<T> = (0..=m).rev().map(|j| fit.std[n + j] * factor(j)).collect();
Ok(fit.result(data, TransferFunction::from_polynomials(Polynomial(numerator), Polynomial(denominator)), std_numerator, std_denominator))
}
pub fn identify_discrete<T: Float + RealField>(
data: &SmlData<T>,
ts: T,
denom_order: usize,
numer_order: usize,
input_delay: usize,
options: &SmlOptions<T>,
) -> Result<SmlResult<T, Discrete>, SmlError> {
if !(ts > T::zero() && ts.is_finite()) {
return Err(SmlError::InvalidSamplingPeriod);
}
let (n, m, nk) = (denom_order, numer_order, input_delay);
let basis: Vec<LineBasis<T>> = data
.lines
.iter()
.map(|l| {
let z_inv = Complex::new(T::zero(), -l.omega * ts).exp();
let delay = Complex::new(T::zero(), -l.omega * ts * T::from(nk).unwrap()).exp();
LineBasis { den: powers(z_inv, n + 1), num: powers(z_inv, m + 1).into_iter().map(|p| p * delay).collect() }
})
.collect();
let fit = fit(data, &basis, 0, options)?;
let order = n.max(nk + m);
let a = fit.denominator(n, 0);
let mut denominator = a.clone();
denominator.resize(order + 1, T::zero());
let mut std_denominator: Vec<T> = std::iter::once(T::zero()).chain(fit.std[..n].iter().copied()).collect();
std_denominator.resize(order + 1, T::zero());
let mut numerator = vec![T::zero(); nk];
numerator.extend_from_slice(&fit.theta[n..]);
numerator.resize(order + 1, T::zero());
let mut std_numerator = vec![T::zero(); nk];
std_numerator.extend_from_slice(&fit.std[n..]);
std_numerator.resize(order + 1, T::zero());
Ok(fit.result(data, TransferFunction::from_polynomials(Polynomial(numerator), Polynomial(denominator)), std_numerator, std_denominator))
}
fn powers<T: Float>(x: Complex<T>, len: usize) -> Vec<Complex<T>> {
std::iter::successors(Some(Complex::new(T::one(), T::zero())), |&p| Some(p * x)).take(len).collect()
}
struct LineBasis<T> {
den: Vec<Complex<T>>,
num: Vec<Complex<T>>,
}
struct Fit<T> {
theta: Vec<T>,
std: Vec<T>,
cost: T,
parameters: usize,
iterations: usize,
termination: Termination,
}
impl<T: Float> Fit<T> {
fn denominator(&self, n: usize, fixed: usize) -> Vec<T> {
let mut free = self.theta[..n].iter();
(0..=n).map(|i| if i == fixed { T::one() } else { *free.next().unwrap() }).collect()
}
fn result<D>(&self, data: &SmlData<T>, model: TransferFunction<T, D>, std_numerator: Vec<T>, std_denominator: Vec<T>) -> SmlResult<T, D> {
let (f, m) = (T::from(data.lines.len()).unwrap(), T::from(data.periods).unwrap());
let one = T::one();
let two = one + one;
let ratio = (m - one) / (m - two);
SmlResult {
model,
std_numerator,
std_denominator,
cost: self.cost,
expected_cost: ratio * (f - T::from(self.parameters).unwrap() / two),
cost_std: Float::sqrt(f) * ratio * Float::sqrt((m - one) / (m - T::from(3).unwrap())),
iterations: self.iterations,
termination: self.termination,
}
}
}
fn polynomials<T: Float>(basis: &LineBasis<T>, theta: &[T], fixed: usize) -> (Complex<T>, Complex<T>) {
let n = basis.den.len() - 1;
let mut free = theta[..n].iter();
let a = basis.den.iter().enumerate().fold(Complex::new(T::zero(), T::zero()), |acc, (i, &d)| {
acc + d * if i == fixed { T::one() } else { *free.next().unwrap() }
});
let b = basis.num.iter().zip(&theta[n..]).fold(Complex::new(T::zero(), T::zero()), |acc, (&p, &c)| acc + p * c);
(a, b)
}
fn variance<T: Float>(line: &SpectralLine<T>, a: Complex<T>, b: Complex<T>) -> T {
let two = T::one() + T::one();
line.var_y * a.norm_sqr() + line.var_u * b.norm_sqr() - two * (line.cov_yu * a * b.conj()).re
}
fn residuals<T: Float + RealField>(
data: &SmlData<T>,
basis: &[LineBasis<T>],
theta: &[T],
fixed: usize,
jacobian: bool,
) -> Option<(DVector<T>, DMatrix<T>)> {
let p = theta.len();
let rows = 2 * data.lines.len();
let mut r = DVector::zeros(rows);
let mut j = if jacobian { DMatrix::zeros(rows, p) } else { DMatrix::zeros(0, 0) };
let two = T::one() + T::one();
for (k, (line, bk)) in data.lines.iter().zip(basis).enumerate() {
let (a, b) = polynomials(bk, theta, fixed);
let d = variance(line, a, b);
if !(d > T::zero() && d.is_finite()) {
return None;
}
let sd = Float::sqrt(d);
let num = b * line.u - a * line.y;
let e = num / sd;
r[2 * k] = e.re;
r[2 * k + 1] = e.im;
if jacobian {
let mut column = 0;
let mut put = |dn: Complex<T>, dd: T, column: usize| {
let de = dn / sd - num * (dd / (two * d * sd));
j[(2 * k, column)] = de.re;
j[(2 * k + 1, column)] = de.im;
};
for (i, &di) in bk.den.iter().enumerate() {
if i == fixed {
continue;
}
let dd = two * line.var_y * (a.conj() * di).re - two * (line.cov_yu * di * b.conj()).re;
put(-di * line.y, dd, column);
column += 1;
}
for &nj in &bk.num {
let dd = two * line.var_u * (b.conj() * nj).re - two * (line.cov_yu * a * nj.conj()).re;
put(nj * line.u, dd, column);
column += 1;
}
}
if !(e.re.is_finite() && e.im.is_finite()) {
return None;
}
}
Some((r, j))
}
fn cost<T: Float + RealField>(data: &SmlData<T>, basis: &[LineBasis<T>], theta: &[T], fixed: usize) -> T {
residuals(data, basis, theta, fixed, false).map_or(T::infinity(), |(r, _)| r.dot(&r))
}
fn scale_columns<T: Float + RealField>(j: &mut DMatrix<T>) -> Vec<T> {
(0..j.ncols())
.map(|c| {
let norm = j.column(c).norm();
let sc = if norm > T::zero() { norm } else { T::one() };
j.column_mut(c).unscale_mut(sc);
sc
})
.collect()
}
fn fit<T: Float + RealField>(data: &SmlData<T>, basis: &[LineBasis<T>], fixed: usize, options: &SmlOptions<T>) -> Result<Fit<T>, SmlError> {
let (n, m) = (basis.first().map_or(0, |b| b.den.len() - 1), basis.first().map_or(0, |b| b.num.len() - 1));
let parameters = n + m + 1;
if 2 * data.lines.len() <= parameters {
return Err(SmlError::TooFewLines { lines: data.lines.len(), parameters });
}
let rows = 2 * data.lines.len();
let mut weights: Vec<T> = data
.lines
.iter()
.map(|l| {
let d = variance(l, Complex::new(T::one(), T::zero()), l.y / l.u);
T::one() / d
})
.collect();
let mut best: Option<(Vec<T>, T)> = None;
let mut previous: Option<Vec<T>> = None;
for _ in 0..options.initial_iterations.max(1) {
if weights.iter().any(|w| !(*w > T::zero() && w.is_finite())) {
break;
}
let mut lhs = DMatrix::<T>::zeros(rows, parameters);
let mut rhs = DVector::<T>::zeros(rows);
for (k, (line, bk)) in data.lines.iter().zip(basis).enumerate() {
let w = Float::sqrt(weights[k]);
let mut column = 0;
for (i, &di) in bk.den.iter().enumerate() {
let v = di * line.y * w;
if i == fixed {
rhs[2 * k] = v.re;
rhs[2 * k + 1] = v.im;
} else {
lhs[(2 * k, column)] = -v.re;
lhs[(2 * k + 1, column)] = -v.im;
column += 1;
}
}
for &nj in &bk.num {
let v = nj * line.u * w;
lhs[(2 * k, column)] = v.re;
lhs[(2 * k + 1, column)] = v.im;
column += 1;
}
}
let scale = scale_columns(&mut lhs);
let Ok(x) = lhs.svd(true, true).solve(&rhs, T::from(1e-14).unwrap()) else {
break;
};
let theta: Vec<T> = x.iter().zip(&scale).map(|(&v, &sc)| v / sc).collect();
if theta.iter().any(|v| !v.is_finite()) {
break;
}
let c = cost(data, basis, &theta, fixed);
if best.as_ref().is_none_or(|(_, best_cost)| c < *best_cost) {
best = Some((theta.clone(), c));
}
let change = previous.as_ref().map(|p: &Vec<T>| {
let (dd, tt) = p.iter().zip(&theta).fold((T::zero(), T::zero()), |(dd, tt), (&a, &b)| (dd + (a - b) * (a - b), tt + b * b));
Float::sqrt(dd / tt)
});
if change.is_some_and(|c| c <= options.tolerance) {
break;
}
weights = data.lines.iter().zip(basis).map(|(l, bk)| {
let (a, b) = polynomials(bk, &theta, fixed);
T::one() / variance(l, a, b)
}).collect();
previous = Some(theta);
}
let Some((mut theta, mut current)) = best.filter(|(_, c)| c.is_finite()) else {
return Err(SmlError::Singular);
};
let ten = T::from(10).unwrap();
let (mu_min, mu_max) = (T::from(1e-12).unwrap(), T::from(1e10).unwrap());
let mut mu = T::from(1e-3).unwrap();
let mut iterations = 0;
let mut termination = Termination::MaxIterations;
'outer: while iterations < options.max_iterations {
let Some((r, mut j)) = residuals(data, basis, &theta, fixed, true) else {
return Err(SmlError::Singular);
};
let scale = scale_columns(&mut j);
let svd = j.svd(true, true);
let (u, v_t) = (svd.u.unwrap(), svd.v_t.unwrap());
let ur = u.transpose() * &r;
loop {
let filtered = DVector::from_iterator(ur.len(), ur.iter().zip(svd.singular_values.iter()).map(|(&x, &s)| x * s / (s * s + mu)));
let delta_scaled = v_t.transpose() * filtered;
let delta: Vec<T> = delta_scaled.iter().zip(&scale).map(|(&d, &sc)| -d / sc).collect();
let trial: Vec<T> = theta.iter().zip(&delta).map(|(&t, &d)| t + d).collect();
let trial_cost = cost(data, basis, &trial, fixed);
if trial_cost < current {
let (dd, tt) = delta.iter().zip(&trial).fold((T::zero(), T::zero()), |(dd, tt), (&d, &t)| (dd + d * d, tt + t * t));
theta = trial;
current = trial_cost;
mu = Float::max(mu / ten, mu_min);
iterations += 1;
if dd <= options.tolerance * options.tolerance * tt {
termination = Termination::Converged;
break 'outer;
}
break;
}
mu *= ten;
if mu > mu_max {
termination = Termination::Stalled;
break 'outer;
}
}
}
let Some((_, mut j)) = residuals(data, basis, &theta, fixed, true) else {
return Err(SmlError::Singular);
};
let scale = scale_columns(&mut j);
let svd = j.svd(false, true);
let v_t = svd.v_t.unwrap();
let mm = T::from(data.periods).unwrap();
let factor = (mm - T::from(2).unwrap()) / (mm - T::from(3).unwrap()) / T::from(2).unwrap();
let std: Vec<T> = (0..parameters)
.map(|c| {
let var = svd.singular_values.iter().enumerate().fold(T::zero(), |acc, (i, &s)| acc + v_t[(i, c)] * v_t[(i, c)] / (s * s));
Float::sqrt(factor * var) / scale[c]
})
.collect();
Ok(Fit { theta, std, cost: current, parameters, iterations, termination })
}