use crate::error::{Result, TimeSeriesError};
use scirs2_core::ndarray::{Array1, Array2};
pub trait BasisSystem: Send + Sync {
fn n_basis(&self) -> usize;
fn evaluate(&self, t: f64) -> Result<Array1<f64>>;
fn evaluate_deriv(&self, t: f64, order: usize) -> Result<Array1<f64>>;
fn gram_matrix(&self) -> Result<Array2<f64>>;
fn penalty_matrix(&self, order: usize) -> Result<Array2<f64>>;
}
pub fn evaluate_basis_matrix<B: BasisSystem>(
basis: &B,
points: &Array1<f64>,
) -> Result<Array2<f64>> {
let n = points.len();
let k = basis.n_basis();
let mut mat = Array2::zeros((n, k));
for (i, &t) in points.iter().enumerate() {
let row = basis.evaluate(t)?;
for j in 0..k {
mat[[i, j]] = row[j];
}
}
Ok(mat)
}
pub fn evaluate_deriv_matrix<B: BasisSystem>(
basis: &B,
points: &Array1<f64>,
order: usize,
) -> Result<Array2<f64>> {
let n = points.len();
let k = basis.n_basis();
let mut mat = Array2::zeros((n, k));
for (i, &t) in points.iter().enumerate() {
let row = basis.evaluate_deriv(t, order)?;
for j in 0..k {
mat[[i, j]] = row[j];
}
}
Ok(mat)
}
#[derive(Debug, Clone)]
pub struct BSplineBasis {
pub knots: Vec<f64>,
pub order: usize,
n_basis: usize,
domain_min: f64,
domain_max: f64,
n_quad: usize,
}
impl BSplineBasis {
pub fn new(knots: Vec<f64>, order: usize) -> Result<Self> {
if order == 0 {
return Err(TimeSeriesError::InvalidInput(
"B-spline order must be >= 1".to_string(),
));
}
if knots.len() < 2 * order {
return Err(TimeSeriesError::InvalidInput(format!(
"Knot vector length {} must be >= 2*order = {}",
knots.len(),
2 * order
)));
}
for i in 1..knots.len() {
if knots[i] < knots[i - 1] {
return Err(TimeSeriesError::InvalidInput(
"Knot vector must be non-decreasing".to_string(),
));
}
}
let n_basis = knots.len() - order;
let domain_min = knots[order - 1];
let domain_max = knots[knots.len() - order];
Ok(Self {
knots,
order,
n_basis,
domain_min,
domain_max,
n_quad: 200,
})
}
pub fn uniform(n_interior: usize, order: usize) -> Result<Self> {
let n_knots = n_interior + 2 * order;
let mut knots = Vec::with_capacity(n_knots);
for _ in 0..order {
knots.push(0.0);
}
for i in 1..=(n_interior) {
knots.push(i as f64 / (n_interior + 1) as f64);
}
for _ in 0..order {
knots.push(1.0);
}
Self::new(knots, order)
}
fn bspline_value(&self, i: usize, k: usize, t: f64) -> f64 {
if k == 1 {
let left = self.knots[i];
let right = self.knots[i + 1];
if t >= left && t < right {
return 1.0;
}
if t >= right && (i + 1) == self.knots.len() - 1 {
return 1.0;
}
return 0.0;
}
let mut result = 0.0;
let denom1 = self.knots[i + k - 1] - self.knots[i];
if denom1 > 0.0 {
result += ((t - self.knots[i]) / denom1) * self.bspline_value(i, k - 1, t);
}
let denom2 = self.knots[i + k] - self.knots[i + 1];
if denom2 > 0.0 {
result +=
((self.knots[i + k] - t) / denom2) * self.bspline_value(i + 1, k - 1, t);
}
result
}
fn bspline_deriv(&self, i: usize, k: usize, t: f64, deriv_order: usize) -> f64 {
if deriv_order == 0 {
return self.bspline_value(i, k, t);
}
if k <= 1 {
return 0.0;
}
let mut result = 0.0;
let d1 = self.knots[i + k - 1] - self.knots[i];
if d1 > 0.0 {
result += (k as f64 - 1.0) / d1
* self.bspline_deriv(i, k - 1, t, deriv_order - 1);
}
let d2 = self.knots[i + k] - self.knots[i + 1];
if d2 > 0.0 {
result -= (k as f64 - 1.0) / d2
* self.bspline_deriv(i + 1, k - 1, t, deriv_order - 1);
}
result
}
fn gauss_legendre_points(&self, n: usize) -> (Vec<f64>, Vec<f64>) {
let (nodes_01, weights_01) = gauss_legendre_01(n);
let a = self.domain_min;
let b = self.domain_max;
let scale = b - a;
let nodes: Vec<f64> = nodes_01.iter().map(|&x| a + scale * x).collect();
let weights: Vec<f64> = weights_01.iter().map(|&w| scale * w).collect();
(nodes, weights)
}
}
impl BasisSystem for BSplineBasis {
fn n_basis(&self) -> usize {
self.n_basis
}
fn evaluate(&self, t: f64) -> Result<Array1<f64>> {
let mut vals = Array1::zeros(self.n_basis);
for i in 0..self.n_basis {
vals[i] = self.bspline_value(i, self.order, t);
}
Ok(vals)
}
fn evaluate_deriv(&self, t: f64, order: usize) -> Result<Array1<f64>> {
let mut vals = Array1::zeros(self.n_basis);
for i in 0..self.n_basis {
vals[i] = self.bspline_deriv(i, self.order, t, order);
}
Ok(vals)
}
fn gram_matrix(&self) -> Result<Array2<f64>> {
let (nodes, weights) = self.gauss_legendre_points(self.n_quad);
let k = self.n_basis;
let mut g = Array2::zeros((k, k));
for (&t, &w) in nodes.iter().zip(weights.iter()) {
let phi = self.evaluate(t)?;
for i in 0..k {
for j in 0..=i {
let val = w * phi[i] * phi[j];
g[[i, j]] += val;
if i != j {
g[[j, i]] += val;
}
}
}
}
Ok(g)
}
fn penalty_matrix(&self, order: usize) -> Result<Array2<f64>> {
let (nodes, weights) = self.gauss_legendre_points(self.n_quad);
let k = self.n_basis;
let mut d = Array2::zeros((k, k));
for (&t, &w) in nodes.iter().zip(weights.iter()) {
let dphi = self.evaluate_deriv(t, order)?;
for i in 0..k {
for j in 0..=i {
let val = w * dphi[i] * dphi[j];
d[[i, j]] += val;
if i != j {
d[[j, i]] += val;
}
}
}
}
Ok(d)
}
}
#[derive(Debug, Clone)]
pub struct FourierBasis {
pub n_harmonics: usize,
pub period: f64,
n_quad: usize,
}
impl FourierBasis {
pub fn new(n_harmonics: usize, period: f64) -> Result<Self> {
if period <= 0.0 {
return Err(TimeSeriesError::InvalidInput(
"Fourier basis period must be positive".to_string(),
));
}
Ok(Self {
n_harmonics,
period,
n_quad: 400,
})
}
fn eval_one(&self, j: usize, t: f64) -> f64 {
if j == 0 {
return 1.0;
}
let k = (j + 1) / 2;
let arg = 2.0 * std::f64::consts::PI * k as f64 * t / self.period;
if j % 2 == 1 {
arg.sin()
} else {
arg.cos()
}
}
fn eval_deriv_one(&self, j: usize, t: f64, deriv_order: usize) -> f64 {
if j == 0 {
if deriv_order == 0 {
return 1.0;
}
return 0.0;
}
let k = (j + 1) / 2;
let omega = 2.0 * std::f64::consts::PI * k as f64 / self.period;
let arg = omega * t;
let phase_shift = deriv_order as f64 * std::f64::consts::FRAC_PI_2;
let amplitude = omega.powi(deriv_order as i32);
if j % 2 == 1 {
amplitude * (arg + phase_shift).sin()
} else {
amplitude * (arg + phase_shift).cos()
}
}
}
impl BasisSystem for FourierBasis {
fn n_basis(&self) -> usize {
2 * self.n_harmonics + 1
}
fn evaluate(&self, t: f64) -> Result<Array1<f64>> {
let k = self.n_basis();
let mut vals = Array1::zeros(k);
for j in 0..k {
vals[j] = self.eval_one(j, t);
}
Ok(vals)
}
fn evaluate_deriv(&self, t: f64, order: usize) -> Result<Array1<f64>> {
let k = self.n_basis();
let mut vals = Array1::zeros(k);
for j in 0..k {
vals[j] = self.eval_deriv_one(j, t, order);
}
Ok(vals)
}
fn gram_matrix(&self) -> Result<Array2<f64>> {
let k = self.n_basis();
let mut g = Array2::zeros((k, k));
g[[0, 0]] = self.period;
for i in 1..k {
g[[i, i]] = self.period / 2.0;
}
Ok(g)
}
fn penalty_matrix(&self, order: usize) -> Result<Array2<f64>> {
let k = self.n_basis();
let mut d = Array2::zeros((k, k));
if order == 0 {
return self.gram_matrix();
}
for j in 1..k {
let freq = (j + 1) / 2;
let omega = 2.0 * std::f64::consts::PI * freq as f64 / self.period;
d[[j, j]] = omega.powi(2 * order as i32) * self.period / 2.0;
}
Ok(d)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WaveletType {
Haar,
Daubechies4,
}
#[derive(Debug, Clone)]
pub struct WaveletBasis {
pub wavelet_type: WaveletType,
pub n_levels: usize,
pub cascade_iters: usize,
}
impl WaveletBasis {
pub fn new(wavelet_type: WaveletType, n_levels: usize) -> Result<Self> {
if n_levels == 0 {
return Err(TimeSeriesError::InvalidInput(
"WaveletBasis requires at least 1 level".to_string(),
));
}
Ok(Self {
wavelet_type,
n_levels,
cascade_iters: 8,
})
}
fn haar_scaling(t: f64, j: usize, k: usize) -> f64 {
let scale = (1u64 << j) as f64;
let u = scale * t - k as f64;
if u >= 0.0 && u < 1.0 {
scale.sqrt()
} else {
0.0
}
}
fn haar_wavelet(t: f64, j: usize, k: usize) -> f64 {
let scale = (1u64 << j) as f64;
let u = scale * t - k as f64;
let amplitude = scale.sqrt();
if u >= 0.0 && u < 0.5 {
amplitude
} else if u >= 0.5 && u < 1.0 {
-amplitude
} else {
0.0
}
}
fn d4_lo() -> [f64; 4] {
let s3 = 3.0_f64.sqrt();
let denom = 4.0 * 2.0_f64.sqrt();
[
(1.0 + s3) / denom,
(3.0 + s3) / denom,
(3.0 - s3) / denom,
(1.0 - s3) / denom,
]
}
fn d4_hi() -> [f64; 4] {
let lo = Self::d4_lo();
[-lo[3], lo[2], -lo[1], lo[0]]
}
fn d4_scaling(&self, t: f64) -> f64 {
if t < 0.0 || t > 1.0 {
return 0.0;
}
let n_points = 1 << self.cascade_iters;
let idx = (t * n_points as f64) as usize;
let idx = idx.min(n_points - 1);
let values = self.cascade_scaling(self.cascade_iters);
if idx < values.len() {
values[idx]
} else {
0.0
}
}
fn cascade_scaling(&self, n: usize) -> Vec<f64> {
let lo = Self::d4_lo();
let support = 3; let n_out = support * (1 << n);
let mut vals = vec![0.0_f64; n_out + 1];
vals[0] = 1.0;
for _iter in 0..n {
let len = vals.len();
let mut new_vals = vec![0.0_f64; 2 * len];
for i in 0..len {
for (k, &h) in lo.iter().enumerate() {
let idx = 2 * i + k;
if idx < new_vals.len() {
new_vals[idx] += h * vals[i] * std::f64::consts::SQRT_2;
}
}
}
vals = new_vals;
}
vals
}
fn d4_wavelet(&self, t: f64) -> f64 {
if t < 0.0 || t > 1.0 {
return 0.0;
}
let hi = Self::d4_hi();
let n_points = 1 << self.cascade_iters;
let scaling = self.cascade_scaling(self.cascade_iters);
let support_n = scaling.len();
let mut result = 0.0;
for (k, &g) in hi.iter().enumerate() {
let u = 2.0 * t - k as f64;
if u >= 0.0 && u <= 3.0 {
let idx = (u * n_points as f64) as usize;
let idx = idx.min(support_n - 1);
result += std::f64::consts::SQRT_2 * g * scaling[idx];
}
}
result
}
fn eval_basis_one(&self, j_idx: usize, t: f64) -> f64 {
match self.wavelet_type {
WaveletType::Haar => {
if j_idx == 0 {
return if t >= 0.0 && t <= 1.0 { 1.0 } else { 0.0 };
}
let mut count = 1usize;
let mut level = 0usize;
loop {
let n_in_level = 1usize << level;
if j_idx < count + n_in_level {
let k = j_idx - count;
return Self::haar_wavelet(t, level, k);
}
count += n_in_level;
level += 1;
if level > self.n_levels + 2 {
return 0.0;
}
}
}
WaveletType::Daubechies4 => {
if j_idx == 0 {
return self.d4_scaling(t);
}
let mut count = 1usize;
let mut level = 0usize;
loop {
let n_in_level = 1usize << level;
if j_idx < count + n_in_level {
let k = j_idx - count;
let scale = (1u64 << level) as f64;
let u = scale * t - k as f64;
return scale.sqrt() * self.d4_wavelet(u);
}
count += n_in_level;
level += 1;
if level > self.n_levels + 2 {
return 0.0;
}
}
}
}
}
}
impl BasisSystem for WaveletBasis {
fn n_basis(&self) -> usize {
1 + (1usize << self.n_levels) - 1
}
fn evaluate(&self, t: f64) -> Result<Array1<f64>> {
let k = self.n_basis();
let mut vals = Array1::zeros(k);
for j in 0..k {
vals[j] = self.eval_basis_one(j, t);
}
Ok(vals)
}
fn evaluate_deriv(&self, t: f64, order: usize) -> Result<Array1<f64>> {
if order == 0 {
return self.evaluate(t);
}
let h = 1e-5;
let k = self.n_basis();
let mut vals = Array1::zeros(k);
if order == 1 {
let phi_plus = self.evaluate(t + h)?;
let phi_minus = self.evaluate(t - h)?;
for j in 0..k {
vals[j] = (phi_plus[j] - phi_minus[j]) / (2.0 * h);
}
} else if order == 2 {
let phi_plus = self.evaluate(t + h)?;
let phi_center = self.evaluate(t)?;
let phi_minus = self.evaluate(t - h)?;
for j in 0..k {
vals[j] = (phi_plus[j] - 2.0 * phi_center[j] + phi_minus[j]) / (h * h);
}
} else {
let phi1 = self.evaluate_deriv(t + h, order - 1)?;
let phi2 = self.evaluate_deriv(t - h, order - 1)?;
for j in 0..k {
vals[j] = (phi1[j] - phi2[j]) / (2.0 * h);
}
}
Ok(vals)
}
fn gram_matrix(&self) -> Result<Array2<f64>> {
let n = 400;
let (nodes, weights) = gauss_legendre_01_scaled(n, 0.0, 1.0);
let k = self.n_basis();
let mut g = Array2::zeros((k, k));
for (&t, &w) in nodes.iter().zip(weights.iter()) {
let phi = self.evaluate(t)?;
for i in 0..k {
for j in 0..=i {
let val = w * phi[i] * phi[j];
g[[i, j]] += val;
if i != j {
g[[j, i]] += val;
}
}
}
}
Ok(g)
}
fn penalty_matrix(&self, order: usize) -> Result<Array2<f64>> {
let n = 400;
let (nodes, weights) = gauss_legendre_01_scaled(n, 0.0, 1.0);
let k = self.n_basis();
let mut d = Array2::zeros((k, k));
for (&t, &w) in nodes.iter().zip(weights.iter()) {
let dphi = self.evaluate_deriv(t, order)?;
for i in 0..k {
for j in 0..=i {
let val = w * dphi[i] * dphi[j];
d[[i, j]] += val;
if i != j {
d[[j, i]] += val;
}
}
}
}
Ok(d)
}
}
#[derive(Debug, Clone)]
pub struct MonomialBasis {
pub degree: usize,
pub domain_min: f64,
pub domain_max: f64,
}
impl MonomialBasis {
pub fn new(degree: usize, domain_min: f64, domain_max: f64) -> Result<Self> {
if domain_min >= domain_max {
return Err(TimeSeriesError::InvalidInput(
"domain_min must be less than domain_max".to_string(),
));
}
Ok(Self {
degree,
domain_min,
domain_max,
})
}
fn normalize(&self, t: f64) -> f64 {
(t - self.domain_min) / (self.domain_max - self.domain_min)
}
fn poly_deriv_coeff(j: usize, order: usize) -> f64 {
if order > j {
return 0.0;
}
let mut coeff = 1.0_f64;
for m in 0..order {
coeff *= (j - m) as f64;
}
coeff
}
}
impl BasisSystem for MonomialBasis {
fn n_basis(&self) -> usize {
self.degree + 1
}
fn evaluate(&self, t: f64) -> Result<Array1<f64>> {
let u = self.normalize(t);
let k = self.n_basis();
let mut vals = Array1::zeros(k);
let mut pow = 1.0;
for j in 0..k {
vals[j] = pow;
pow *= u;
}
Ok(vals)
}
fn evaluate_deriv(&self, t: f64, order: usize) -> Result<Array1<f64>> {
if order == 0 {
return self.evaluate(t);
}
let u = self.normalize(t);
let scale = self.domain_max - self.domain_min;
let k = self.n_basis();
let mut vals = Array1::zeros(k);
for j in 0..k {
let coeff = Self::poly_deriv_coeff(j, order);
if coeff.abs() < 1e-15 {
vals[j] = 0.0;
} else {
let exp = if j >= order { j - order } else { 0 };
vals[j] = coeff * u.powi(exp as i32) / scale.powi(order as i32);
}
}
Ok(vals)
}
fn gram_matrix(&self) -> Result<Array2<f64>> {
let k = self.n_basis();
let mut g = Array2::zeros((k, k));
let scale = self.domain_max - self.domain_min;
for i in 0..k {
for j in 0..k {
g[[i, j]] = scale / (i + j + 1) as f64;
}
}
Ok(g)
}
fn penalty_matrix(&self, order: usize) -> Result<Array2<f64>> {
if order == 0 {
return self.gram_matrix();
}
let k = self.n_basis();
let mut d = Array2::zeros((k, k));
let scale = self.domain_max - self.domain_min;
for i in order..k {
for j in order..k {
let ci = Self::poly_deriv_coeff(i, order);
let cj = Self::poly_deriv_coeff(j, order);
let exp = (i + j) as i32 - 2 * order as i32;
if exp >= 0 {
d[[i, j]] = ci * cj / (exp as f64 + 1.0)
/ scale.powi(2 * order as i32 - 1);
}
}
}
Ok(d)
}
}
pub fn gauss_legendre_01(n: usize) -> (Vec<f64>, Vec<f64>) {
if n == 0 {
return (vec![], vec![]);
}
let (nodes_sym, weights_sym) = gauss_legendre_sym(n);
let nodes: Vec<f64> = nodes_sym.iter().map(|&x| (x + 1.0) / 2.0).collect();
let weights: Vec<f64> = weights_sym.iter().map(|&w| w / 2.0).collect();
(nodes, weights)
}
pub fn gauss_legendre_01_scaled(n: usize, a: f64, b: f64) -> (Vec<f64>, Vec<f64>) {
let (nodes_01, weights_01) = gauss_legendre_01(n);
let scale = b - a;
let nodes: Vec<f64> = nodes_01.iter().map(|&x| a + scale * x).collect();
let weights: Vec<f64> = weights_01.iter().map(|&w| scale * w).collect();
(nodes, weights)
}
fn gauss_legendre_sym(n: usize) -> (Vec<f64>, Vec<f64>) {
if n == 1 {
return (vec![0.0], vec![2.0]);
}
let mut beta = vec![0.0_f64; n - 1];
for i in 1..n {
beta[i - 1] = i as f64 / ((4 * i * i - 1) as f64).sqrt();
}
let (nodes, eigvecs) = tridiag_eig_sym(n, &beta);
let weights: Vec<f64> = eigvecs.iter().map(|v| 2.0 * v * v).collect();
(nodes, weights)
}
fn tridiag_eig_sym(n: usize, beta: &[f64]) -> (Vec<f64>, Vec<f64>) {
use std::f64::consts::PI;
let mut d = vec![0.0_f64; n]; let mut e: Vec<f64> = {
let mut v = beta.to_vec();
v.push(0.0);
v
};
let mut z = vec![1.0_f64; n]; let mut z_full: Vec<Vec<f64>> = (0..n).map(|i| {
let mut v = vec![0.0_f64; n];
v[i] = 1.0;
v
}).collect();
let max_iter = 100 * n;
let eps = f64::EPSILON;
for l in 0..n {
let mut iter = 0;
loop {
let mut m = l;
while m < n - 1 {
let dd = d[m].abs() + d[m + 1].abs();
if e[m].abs() <= eps * dd {
break;
}
m += 1;
}
if m == l {
break;
}
iter += 1;
if iter > max_iter {
break;
}
let g = (d[l + 1] - d[l]) / (2.0 * e[l]);
let r = (g * g + 1.0).sqrt();
let g = d[m] - d[l] + e[l] / (g + if g >= 0.0 { r } else { -r });
let (mut s, mut c, mut p) = (1.0_f64, 1.0_f64, 0.0_f64);
for i in (l..m).rev() {
let f = s * e[i];
let b = c * e[i];
let r = (f * f + g * g).sqrt();
e[i + 1] = r;
if r.abs() < 1e-300 {
d[i + 1] -= p;
e[m] = 0.0;
break;
}
s = f / r;
c = g / r;
let g_new = d[i + 1] - p;
let r2 = (d[i] - g_new) * s + 2.0 * c * b;
p = s * r2;
d[i + 1] = g_new + p;
let g = c * r2 - b;
for k in 0..n {
let fv = z_full[k][i + 1];
z_full[k][i + 1] = s * z_full[k][i] + c * fv;
z_full[k][i] = c * z_full[k][i] - s * fv;
}
let _ = g; let _ = b;
let g = g_new + p - r2 * s;
let _ = g;
}
d[l] -= p;
e[l] = g;
e[m] = 0.0;
}
}
let first_comps: Vec<f64> = (0..n).map(|i| z_full[0][i]).collect();
(d, first_comps)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_bspline_partition_of_unity() {
let basis = BSplineBasis::uniform(5, 4).expect("basis creation failed");
for &t in &[0.1, 0.3, 0.5, 0.7, 0.9] {
let vals = basis.evaluate(t).expect("evaluate failed");
let sum: f64 = vals.iter().sum();
assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-10);
}
}
#[test]
fn test_fourier_gram_matrix() {
let basis = FourierBasis::new(3, 1.0).expect("basis creation");
let g = basis.gram_matrix().expect("gram matrix");
assert_abs_diff_eq!(g[[0, 0]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(g[[1, 1]], 0.5, epsilon = 1e-10);
assert_abs_diff_eq!(g[[0, 1]], 0.0, epsilon = 1e-10);
}
#[test]
fn test_monomial_gram_matrix() {
let basis = MonomialBasis::new(3, 0.0, 1.0).expect("basis creation");
let g = basis.gram_matrix().expect("gram matrix");
assert_abs_diff_eq!(g[[0, 0]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(g[[0, 1]], 0.5, epsilon = 1e-10);
}
#[test]
fn test_evaluate_basis_matrix_shape() {
let basis = BSplineBasis::uniform(3, 4).expect("basis");
let pts = Array1::from_vec(vec![0.1, 0.2, 0.5, 0.8, 0.9]);
let mat = evaluate_basis_matrix(&basis, &pts).expect("eval matrix");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), basis.n_basis());
}
#[test]
fn test_wavelet_basis_haar() {
let basis = WaveletBasis::new(WaveletType::Haar, 3).expect("wavelet basis");
let vals = basis.evaluate(0.5).expect("evaluate");
assert_eq!(vals.len(), basis.n_basis());
}
}