use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
use super::{
ConfidenceIntervalOptions, KnotPlacementStrategy, SplineTrendOptions, SplineType,
TrendWithConfidenceInterval,
};
use crate::error::{Result, TimeSeriesError};
#[allow(dead_code)]
pub fn estimate_spline_trend<F>(ts: &Array1<F>, options: &SplineTrendOptions) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if ts.len() < 4 {
return Err(TimeSeriesError::InsufficientData {
message: "Time series too short for spline trend estimation".to_string(),
required: 4,
actual: ts.len(),
});
}
let knots = match options.knot_placement {
KnotPlacementStrategy::Uniform => generate_uniform_knots(ts.len(), options.num_knots),
KnotPlacementStrategy::Quantile => generate_quantile_knots(ts.len(), options.num_knots),
KnotPlacementStrategy::Custom => {
if let Some(positions) = &options.knot_positions {
for &pos in positions {
if pos >= ts.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Custom knot position {} is out of bounds (time series length: {})",
pos,
ts.len()
)));
}
}
positions.clone()
} else {
return Err(TimeSeriesError::InvalidInput(
"Custom knot placement selected but no knot positions provided".to_string(),
));
}
}
};
if knots.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"At least 2 knots are required for spline trend estimation".to_string(),
));
}
match options.spline_type {
SplineType::Cubic => fit_cubic_spline(ts, &knots, options.extrapolate),
SplineType::NaturalCubic => fit_natural_cubic_spline(ts, &knots, options.extrapolate),
SplineType::BSpline => fit_bspline(ts, &knots, options.degree, options.extrapolate),
SplineType::PSpline => fit_pspline(
ts,
&knots,
options.degree,
F::from_f64(options.lambda).expect("Operation failed"),
options.extrapolate,
),
}
}
#[allow(dead_code)]
fn generate_uniform_knots(n: usize, numknots: usize) -> Vec<usize> {
let mut _knots = Vec::with_capacity(numknots);
_knots.push(0);
if numknots > 2 {
let step = (n - 1) as f64 / (numknots - 1) as f64;
for i in 1..(numknots - 1) {
let pos = (i as f64 * step).round() as usize;
_knots.push(pos);
}
}
_knots.push(n - 1);
_knots
}
#[allow(dead_code)]
fn generate_quantile_knots(n: usize, numknots: usize) -> Vec<usize> {
let mut _knots = Vec::with_capacity(numknots);
_knots.push(0);
if numknots > 2 {
for i in 1..(numknots - 1) {
let quantile = i as f64 / (numknots - 1) as f64;
let pos = (quantile * (n - 1) as f64).round() as usize;
_knots.push(pos);
}
}
_knots.push(n - 1);
_knots
}
#[allow(dead_code)]
fn fit_cubic_spline<F>(ts: &Array1<F>, knots: &[usize], extrapolate: bool) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if knots.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"At least 2 knots are required for cubic spline".to_string(),
));
}
let n = ts.len();
let mut result = Array1::<F>::zeros(n);
let num_knots = knots.len();
let mut a = Array2::<F>::zeros((num_knots, num_knots));
let mut b = Vec::with_capacity(num_knots);
for i in 1..(num_knots - 1) {
let h_i_minus_1 = F::from_usize(knots[i] - knots[i - 1]).expect("Operation failed");
let h_i = F::from_usize(knots[i + 1] - knots[i]).expect("Operation failed");
a[[i, i - 1]] = h_i_minus_1 / F::from_f64(6.0).expect("Operation failed");
a[[i, i]] = (h_i_minus_1 + h_i) / F::from_f64(3.0).expect("Operation failed");
a[[i, i + 1]] = h_i / F::from_f64(6.0).expect("Operation failed");
let f_i_minus_1 = ts[knots[i - 1]];
let f_i = ts[knots[i]];
let f_i_plus_1 = ts[knots[i + 1]];
let rhs = (f_i_plus_1 - f_i) / h_i - (f_i - f_i_minus_1) / h_i_minus_1;
b.push(rhs);
}
a[[0, 0]] = F::one();
b.insert(0, F::zero());
a[[num_knots - 1, num_knots - 1]] = F::one();
b.push(F::zero());
let second_derivs = solve_linear_system(a, b)?;
for i in 0..(num_knots - 1) {
let x_left = knots[i];
let x_right = knots[i + 1];
let h = F::from_usize(x_right - x_left).expect("Operation failed");
let y_left = ts[x_left];
let y_right = ts[x_right];
let d2_left = second_derivs[i];
let d2_right = second_derivs[i + 1];
for x in x_left..=x_right {
let t = F::from_usize(x - x_left).expect("Operation failed") / h;
let one_minus_t = F::one() - t;
result[x] = one_minus_t * y_left
+ t * y_right
+ ((one_minus_t.powi(3) - one_minus_t) * d2_left + (t.powi(3) - t) * d2_right)
* h
* h
/ F::from_f64(6.0).expect("Operation failed");
}
}
if extrapolate {
let x_0 = knots[0];
let x_1 = knots[1];
let h = F::from_usize(x_1 - x_0).expect("Operation failed");
let _slope_left = (ts[x_1] - ts[x_0]) / h
- h * (F::from_f64(2.0).expect("Operation failed") * second_derivs[0]
+ second_derivs[1])
/ F::from_f64(6.0).expect("Operation failed");
let x_n_minus_1 = knots[num_knots - 2];
let x_n = knots[num_knots - 1];
let h = F::from_usize(x_n - x_n_minus_1).expect("Operation failed");
let _slope_right = (ts[x_n] - ts[x_n_minus_1]) / h
+ h * (second_derivs[num_knots - 2]
+ F::from_f64(2.0).expect("Operation failed") * second_derivs[num_knots - 1])
/ F::from_f64(6.0).expect("Operation failed");
}
Ok(result)
}
#[allow(dead_code)]
fn fit_natural_cubic_spline<F>(
ts: &Array1<F>,
knots: &[usize],
_extrapolate: bool,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
fit_cubic_spline(ts, knots, _extrapolate)
}
#[allow(dead_code)]
fn fit_bspline<F>(
ts: &Array1<F>,
knots: &[usize],
degree: usize,
extrapolate: bool,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = ts.len();
let x_values: Vec<F> = (0..n)
.map(|i| F::from_usize(i).expect("Operation failed"))
.collect();
let basis = create_bspline_basis(x_values, knots, degree)?;
let coeffs = solve_spline_system(&basis, ts)?;
let mut trend = Array1::<F>::zeros(n);
for i in 0..n {
let mut val = F::zero();
for j in 0..coeffs.len() {
if j < basis.shape()[1] {
val = val + coeffs[j] * basis[[i, j]];
}
}
trend[i] = val;
}
Ok(trend)
}
#[allow(dead_code)]
fn fit_pspline<F>(
ts: &Array1<F>,
knots: &[usize],
degree: usize,
lambda: F,
extrapolate: bool,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = ts.len();
let x_values: Vec<F> = (0..n)
.map(|i| F::from_usize(i).expect("Operation failed"))
.collect();
let basis = create_bspline_basis(x_values, knots, degree)?;
let coeffs = solve_regularized_system(basis.clone(), ts, lambda)?;
let mut trend = Array1::<F>::zeros(n);
for i in 0..n {
let mut val = F::zero();
for j in 0..coeffs.len() {
if j < basis.shape()[1] {
val = val + coeffs[j] * basis[[i, j]];
}
}
trend[i] = val;
}
Ok(trend)
}
#[allow(dead_code)]
fn create_bspline_basis<F>(_xvalues: Vec<F>, knots: &[usize], degree: usize) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = _xvalues.len();
let num_basis = knots.len() + degree - 1;
let mut basis = Array2::<F>::zeros((n, num_basis));
let mut augmented_knots = Vec::with_capacity(knots.len() + 2 * degree);
for _ in 0..degree {
augmented_knots.push(F::from_usize(knots[0]).expect("Operation failed"));
}
for &k in knots {
augmented_knots.push(F::from_usize(k).expect("Operation failed"));
}
for _ in 0..degree {
augmented_knots.push(F::from_usize(knots[knots.len() - 1]).expect("Operation failed"));
}
for i in 0..n {
let x = _xvalues[i];
for j in 0..(augmented_knots.len() - 1) {
if j < num_basis {
if x >= augmented_knots[j] && x < augmented_knots[j + 1]
|| (x == augmented_knots[augmented_knots.len() - 1]
&& j == augmented_knots.len() - 2)
{
basis[[i, j]] = F::one();
} else {
basis[[i, j]] = F::zero();
}
}
}
}
let mut temp_basis = basis.clone();
for d in 1..=degree {
for i in 0..n {
let x = _xvalues[i];
for j in 0..(num_basis - d) {
let mut sum = F::zero();
let denom1 = augmented_knots[j + d] - augmented_knots[j];
if denom1 > F::zero() {
sum = sum + (x - augmented_knots[j]) / denom1 * temp_basis[[i, j]];
}
let denom2 = augmented_knots[j + d + 1] - augmented_knots[j + 1];
if denom2 > F::zero() {
sum = sum + (augmented_knots[j + d + 1] - x) / denom2 * temp_basis[[i, j + 1]];
}
basis[[i, j]] = sum;
}
}
temp_basis = basis.clone();
}
Ok(basis)
}
#[allow(dead_code)]
fn solve_spline_system<F>(basis: &Array2<F>, ts: &Array1<F>) -> Result<Vec<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = ts.len();
let p = basis.shape()[1];
let mut btb = Array2::<F>::zeros((p, p));
for i in 0..p {
for j in 0..p {
let mut sum = F::zero();
for k in 0..n {
sum = sum + basis[[k, i]] * basis[[k, j]];
}
btb[[i, j]] = sum;
}
}
let mut bty = Vec::with_capacity(p);
for i in 0..p {
let mut sum = F::zero();
for k in 0..n {
sum = sum + basis[[k, i]] * ts[k];
}
bty.push(sum);
}
solve_linear_system(btb, bty)
}
#[allow(dead_code)]
fn solve_regularized_system<F>(basis: Array2<F>, ts: &Array1<F>, lambda: F) -> Result<Vec<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = ts.len();
let p = basis.shape()[1];
let mut btb = Array2::<F>::zeros((p, p));
for i in 0..p {
for j in 0..p {
let mut sum = F::zero();
for k in 0..n {
sum = sum + basis[[k, i]] * basis[[k, j]];
}
btb[[i, j]] = sum;
}
}
let mut penalty = Array2::<F>::zeros((p, p));
for i in 0..(p - 2) {
penalty[[i, i]] = F::one();
penalty[[i, i + 1]] = F::from_f64(-2.0).expect("Operation failed");
penalty[[i, i + 2]] = F::one();
}
let mut dtd = Array2::<F>::zeros((p, p));
for i in 0..p {
for j in 0..p {
let mut sum = F::zero();
for k in 0..(p - 2) {
sum = sum + penalty[[k, i]] * penalty[[k, j]];
}
dtd[[i, j]] = sum;
}
}
for i in 0..p {
for j in 0..p {
btb[[i, j]] = btb[[i, j]] + lambda * dtd[[i, j]];
}
}
let mut bty = Vec::with_capacity(p);
for i in 0..p {
let mut sum = F::zero();
for k in 0..n {
sum = sum + basis[[k, i]] * ts[k];
}
bty.push(sum);
}
solve_linear_system(btb, bty)
}
#[allow(dead_code)]
fn solve_linear_system<F>(a: Array2<F>, b: Vec<F>) -> Result<Vec<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = a.shape()[0];
if n != b.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Matrix and vector dimensions do not match: A is {}x{}, b is {}",
n,
a.shape()[1],
b.len()
)));
}
let mut l = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut sum = F::zero();
if i == j {
for k in 0..j {
sum = sum + l[[j, k]] * l[[j, k]];
}
let val = (a[[j, j]] - sum).sqrt();
if !val.is_finite() {
return Err(TimeSeriesError::NumericalInstability(
"Cholesky decomposition failed due to non-positive definite matrix"
.to_string(),
));
}
l[[j, j]] = val;
} else {
for k in 0..j {
sum = sum + l[[i, k]] * l[[j, k]];
}
if l[[j, j]] > F::zero() {
l[[i, j]] = (a[[i, j]] - sum) / l[[j, j]];
} else {
return Err(TimeSeriesError::NumericalInstability(
"Cholesky decomposition failed due to zero pivot".to_string(),
));
}
}
}
}
let mut y = Vec::with_capacity(n);
for i in 0..n {
let mut sum = F::zero();
for j in 0..i {
sum = sum + l[[i, j]] * y[j];
}
y.push((b[i] - sum) / l[[i, i]]);
}
let mut x = vec![F::zero(); n];
for i in (0..n).rev() {
let mut sum = F::zero();
for j in (i + 1)..n {
sum = sum + l[[j, i]] * x[j];
}
x[i] = (y[i] - sum) / l[[i, i]];
}
Ok(x)
}
#[allow(dead_code)]
pub fn estimate_spline_trend_with_ci<F>(
ts: &Array1<F>,
options: &SplineTrendOptions,
ci_options: &ConfidenceIntervalOptions,
) -> Result<TrendWithConfidenceInterval<F>>
where
F: Float + FromPrimitive + Debug + 'static,
{
let trend = estimate_spline_trend(ts, options)?;
let (lower, upper) =
super::confidence::compute_trend_confidence_interval(ts, &trend, ci_options, |data| {
estimate_spline_trend(data, options)
})?;
Ok(TrendWithConfidenceInterval {
trend,
lower,
upper,
})
}