use scirs2_core::ndarray::ArrayStatCompat;
use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, Data, Ix1, Ix2, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive, NumCast};
use std::fmt::{Debug, Display};
use crate::error::{Result, TimeSeriesError};
use statrs::statistics::Statistics;
#[allow(dead_code)]
pub fn autocovariance<S, F>(data: &ArrayBase<S, Ix1>, lag: usize) -> Result<F>
where
S: Data<Elem = F>,
F: Float + FromPrimitive,
{
if lag >= data.len() {
return Err(TimeSeriesError::InvalidInput(
"Lag exceeds data length".to_string(),
));
}
let n = data.len();
let mean = data.mean_or(F::zero());
let mut cov = F::zero();
for i in lag..n {
cov = cov + (data[i] - mean) * (data[i - lag] - mean);
}
Ok(cov / F::from(n - lag).expect("Failed to convert to float"))
}
#[allow(dead_code)]
pub fn is_stationary<F>(ts: &Array1<F>, lags: Option<usize>) -> Result<(F, F)>
where
F: Float + FromPrimitive + Debug,
{
if ts.len() < 3 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 3 points for stationarity test".to_string(),
));
}
let max_lags = match lags {
Some(l) => l,
None => {
let n = ts.len() as f64;
let max_lags_float = 12.0 * (n / 100.0).powf(0.25);
max_lags_float.min(n / 3.0).floor() as usize
}
};
let mut diff_ts = Vec::with_capacity(ts.len() - 1);
for i in 1..ts.len() {
diff_ts.push(ts[i] - ts[i - 1]);
}
let diff_ts = Array1::from(diff_ts);
let n_diff = diff_ts.len();
let n_obs = n_diff - max_lags;
let n_params = 2 + max_lags;
if n_obs <= n_params {
return Err(TimeSeriesError::InvalidInput(format!(
"Insufficient observations ({n_obs}) for ADF regression with {n_params} parameters; \
provide a longer series or fewer lags"
)));
}
let mut x = Array2::<F>::zeros((n_obs, n_params));
let mut y = Array1::<F>::zeros(n_obs);
for (row, k) in (max_lags..n_diff).enumerate() {
y[row] = diff_ts[k];
x[[row, 0]] = F::one(); x[[row, 1]] = ts[k]; for j in 1..=max_lags {
x[[row, 1 + j]] = diff_ts[k - j];
}
}
let mut xt_x = Array2::<F>::zeros((n_params, n_params));
for a in 0..n_params {
for b in a..n_params {
let mut acc = F::zero();
for row in 0..n_obs {
acc = acc + x[[row, a]] * x[[row, b]];
}
xt_x[[a, b]] = acc;
xt_x[[b, a]] = acc; }
}
let mut xt_y = Array1::<F>::zeros(n_params);
for a in 0..n_params {
let mut acc = F::zero();
for row in 0..n_obs {
acc = acc + x[[row, a]] * y[row];
}
xt_y[a] = acc;
}
let xt_x_inv = pseudo_inverse_spd(&xt_x);
let mut beta = Array1::<F>::zeros(n_params);
for a in 0..n_params {
let mut acc = F::zero();
for b in 0..n_params {
acc = acc + xt_x_inv[[a, b]] * xt_y[b];
}
beta[a] = acc;
}
let mut rss = F::zero();
for row in 0..n_obs {
let mut fitted = F::zero();
for a in 0..n_params {
fitted = fitted + x[[row, a]] * beta[a];
}
let resid = y[row] - fitted;
rss = rss + resid * resid;
}
let dof = F::from_usize(n_obs - n_params)
.ok_or_else(|| TimeSeriesError::InvalidInput("degrees of freedom overflow".to_string()))?;
let sigma2 = rss / dof;
let var_beta = sigma2 * xt_x_inv[[1, 1]];
if var_beta > F::zero() {
let se_beta = var_beta.sqrt();
let mut adf_stat = beta[1] / se_beta;
if !adf_stat.is_finite() {
adf_stat = F::zero();
}
let p_value = mackinnon_p_value(adf_stat);
return Ok((adf_stat, p_value));
}
let n_input = F::from_usize(ts.len()).unwrap_or_else(F::one);
let mut sum = F::zero();
for &val in ts.iter() {
sum = sum + val;
}
let mean = sum / n_input;
let mut sse = F::zero();
for &val in ts.iter() {
let centered = val - mean;
sse = sse + centered * centered;
}
let input_var = sse / n_input;
let var_tol = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
if input_var <= var_tol {
let stat = F::from_f64(-1e6).unwrap_or_else(F::min_value);
return Ok((stat, F::zero()));
}
let stat = F::zero();
Ok((stat, mackinnon_p_value(stat)))
}
#[allow(dead_code)]
fn jacobi_eigen_symmetric<F>(a: &Array2<F>) -> (Array1<F>, Array2<F>)
where
F: Float + FromPrimitive + Debug,
{
let n = a.nrows();
let mut v = Array2::<F>::zeros((n, n));
for i in 0..n {
v[[i, i]] = F::one();
}
if n == 0 {
return (Array1::<F>::zeros(0), v);
}
let mut m = a.clone();
if n == 1 {
let mut eig = Array1::<F>::zeros(1);
eig[0] = m[[0, 0]];
return (eig, v);
}
let half = F::from_f64(0.5).unwrap_or_else(|| F::one() / (F::one() + F::one()));
let hundred = F::from_f64(100.0).unwrap_or_else(F::one);
let frac = F::from_f64(0.2).unwrap_or_else(F::zero);
let eps = F::epsilon();
const MAX_SWEEPS: usize = 100;
for sweep in 0..MAX_SWEEPS {
let mut off = F::zero();
let mut diag = F::zero();
for p in 0..n {
diag = diag + m[[p, p]].abs();
for q in (p + 1)..n {
off = off + m[[p, q]].abs();
}
}
if off <= eps * diag {
break;
}
let n_sq = F::from_usize(n * n).unwrap_or_else(F::one);
let thresh = if sweep < 3 {
frac * off / n_sq
} else {
F::zero()
};
for p in 0..n {
for q in (p + 1)..n {
let apq = m[[p, q]];
let g = hundred * apq.abs();
let app = m[[p, p]];
let aqq = m[[q, q]];
if sweep > 4 && (app.abs() + g == app.abs()) && (aqq.abs() + g == aqq.abs()) {
m[[p, q]] = F::zero();
continue;
}
if apq.abs() <= thresh {
continue;
}
let h = aqq - app;
let t = if h.abs() + g == h.abs() {
apq / h
} else {
let theta = half * h / apq;
let denom = theta.abs() + (F::one() + theta * theta).sqrt();
let magnitude = if denom > F::zero() {
F::one() / denom
} else {
F::zero()
};
if theta < F::zero() {
-magnitude
} else {
magnitude
}
};
let c = F::one() / (F::one() + t * t).sqrt();
let s = t * c;
let tau = s / (F::one() + c);
let delta = t * apq;
m[[p, p]] = app - delta;
m[[q, q]] = aqq + delta;
m[[p, q]] = F::zero();
for j in 0..p {
let g1 = m[[j, p]];
let h1 = m[[j, q]];
m[[j, p]] = g1 - s * (h1 + g1 * tau);
m[[j, q]] = h1 + s * (g1 - h1 * tau);
}
for j in (p + 1)..q {
let g1 = m[[p, j]];
let h1 = m[[j, q]];
m[[p, j]] = g1 - s * (h1 + g1 * tau);
m[[j, q]] = h1 + s * (g1 - h1 * tau);
}
for j in (q + 1)..n {
let g1 = m[[p, j]];
let h1 = m[[q, j]];
m[[p, j]] = g1 - s * (h1 + g1 * tau);
m[[q, j]] = h1 + s * (g1 - h1 * tau);
}
for j in 0..n {
let g1 = v[[j, p]];
let h1 = v[[j, q]];
v[[j, p]] = g1 - s * (h1 + g1 * tau);
v[[j, q]] = h1 + s * (g1 - h1 * tau);
}
}
}
}
let mut eig = Array1::<F>::zeros(n);
for (i, value) in eig.iter_mut().enumerate() {
*value = m[[i, i]];
}
(eig, v)
}
#[allow(dead_code)]
fn pseudo_inverse_spd<F>(a: &Array2<F>) -> Array2<F>
where
F: Float + FromPrimitive + Debug,
{
let (eig, v) = jacobi_eigen_symmetric(a);
let n = eig.len();
let mut lambda_max = F::zero();
for &lam in eig.iter() {
let mag = lam.abs();
if mag > lambda_max {
lambda_max = mag;
}
}
let rcond = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
let cutoff = rcond * lambda_max;
let inv_eig = eig.mapv(|lam| {
if lam > cutoff {
F::one() / lam
} else {
F::zero()
}
});
let mut pinv = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in i..n {
let mut acc = F::zero();
for k in 0..n {
acc = acc + v[[i, k]] * inv_eig[k] * v[[j, k]];
}
pinv[[i, j]] = acc;
pinv[[j, i]] = acc;
}
}
pinv
}
#[allow(dead_code)]
fn invert_matrix<F>(a: &Array2<F>) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug,
{
let n = a.nrows();
if n != a.ncols() {
return Err(TimeSeriesError::InvalidInput(
"Matrix must be square to invert".to_string(),
));
}
let mut aug = Array2::<F>::zeros((n, 2 * n));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n + i]] = F::one();
}
let eps = F::from_f64(1e-12).unwrap_or_else(F::epsilon);
for col in 0..n {
let mut pivot_row = col;
let mut pivot_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let val = aug[[row, col]].abs();
if val > pivot_val {
pivot_val = val;
pivot_row = row;
}
}
if pivot_val <= eps {
return Err(TimeSeriesError::NumericalInstability(
"Singular matrix encountered while inverting ADF normal equations".to_string(),
));
}
if pivot_row != col {
for j in 0..(2 * n) {
let tmp = aug[[col, j]];
aug[[col, j]] = aug[[pivot_row, j]];
aug[[pivot_row, j]] = tmp;
}
}
let pivot = aug[[col, col]];
for j in 0..(2 * n) {
aug[[col, j]] = aug[[col, j]] / pivot;
}
for row in 0..n {
if row == col {
continue;
}
let factor = aug[[row, col]];
if factor != F::zero() {
for j in 0..(2 * n) {
aug[[row, j]] = aug[[row, j]] - factor * aug[[col, j]];
}
}
}
}
let mut inv = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[[i, n + j]];
}
}
Ok(inv)
}
#[allow(dead_code)]
fn mackinnon_p_value<F>(stat: F) -> F
where
F: Float + FromPrimitive + Debug,
{
let t = stat.to_f64().unwrap_or(0.0);
const TABLE: [(f64, f64); 7] = [
(0.01, -3.43),
(0.025, -3.12),
(0.05, -2.86),
(0.10, -2.57),
(0.50, -1.95),
(0.90, -1.14),
(0.99, -0.44),
];
let p = if t <= TABLE[0].1 {
let excess = TABLE[0].1 - t; (0.01 * (-1.2 * excess).exp()).max(1e-6)
} else if t >= TABLE[TABLE.len() - 1].1 {
let excess = t - TABLE[TABLE.len() - 1].1; (1.0 - 0.01 * (-1.0 * excess).exp()).min(1.0 - 1e-6)
} else {
let mut result = 0.5;
for w in TABLE.windows(2) {
let (p_lo, c_lo) = w[0];
let (p_hi, c_hi) = w[1];
if t >= c_lo && t <= c_hi {
let frac = (t - c_lo) / (c_hi - c_lo);
result = p_lo + frac * (p_hi - p_lo);
break;
}
}
result
};
F::from_f64(p).unwrap_or_else(|| F::from_f64(0.5).unwrap_or_else(F::zero))
}
#[allow(dead_code)]
pub fn transform_to_stationary<F>(
ts: &Array1<F>,
method: &str,
seasonal_period: Option<usize>,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for transformation".to_string(),
));
}
match method {
"diff" => {
let mut result = Vec::with_capacity(ts.len() - 1);
for i in 1..ts.len() {
result.push(ts[i] - ts[i - 1]);
}
Ok(Array1::from(result))
}
"log" => {
let mut result = Vec::with_capacity(ts.len());
for &val in ts.iter() {
if val <= F::zero() {
return Err(TimeSeriesError::InvalidInput(
"Cannot apply log transformation to non-positive values".to_string(),
));
}
result.push(val.ln());
}
Ok(Array1::from(result))
}
"seasonal_diff" => {
let _period = match seasonal_period {
Some(p) => p,
None => {
return Err(TimeSeriesError::InvalidInput(
"Seasonal _period must be provided for seasonal differencing".to_string(),
))
}
};
if _period >= ts.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Seasonal period ({}) must be less than time series length ({})",
_period,
ts.len()
)));
}
let mut result = Vec::with_capacity(ts.len() - _period);
for i in _period..ts.len() {
result.push(ts[i] - ts[i - _period]);
}
Ok(Array1::from(result))
}
_ => Err(TimeSeriesError::InvalidInput(format!(
"Unknown transformation method: {method}"
))),
}
}
#[allow(dead_code)]
pub fn moving_average<F>(_ts: &Array1<F>, windowsize: usize) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if windowsize < 1 {
return Err(TimeSeriesError::InvalidInput(
"Window size must be at least 1".to_string(),
));
}
if windowsize > _ts.len() {
return Err(TimeSeriesError::InvalidInput(format!(
"Window size ({}) cannot be larger than time series length ({})",
windowsize,
_ts.len()
)));
}
let half_window = windowsize / 2;
let mut result = Array1::zeros(_ts.len());
let is_even = windowsize.is_multiple_of(2);
for i in 0.._ts.len() {
let start = i.saturating_sub(half_window);
let end = if i + half_window >= _ts.len() {
_ts.len() - 1
} else {
i + half_window
};
let end = if is_even && (end + 1 < _ts.len()) {
end + 1
} else {
end
};
let mut sum = F::zero();
let mut count = F::zero();
for j in start..=end {
sum = sum + _ts[j];
count = count + F::one();
}
result[i] = sum / count;
}
Ok(result)
}
#[allow(dead_code)]
pub fn autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if _ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for autocorrelation".to_string(),
));
}
let max_lag = std::cmp::min(maxlag.unwrap_or(_ts.len() - 1), _ts.len() - 1);
let mean = _ts.iter().fold(F::zero(), |acc, &x| acc + x)
/ F::from_usize(_ts.len()).expect("Operation failed");
let denominator = _ts
.iter()
.fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean));
if denominator == F::zero() {
return Err(TimeSeriesError::InvalidInput(
"Cannot compute autocorrelation for constant time series".to_string(),
));
}
let mut result = Array1::zeros(max_lag + 1);
for _lag in 0..=max_lag {
let mut numerator = F::zero();
for i in 0..(_ts.len() - _lag) {
numerator = numerator + (_ts[i] - mean) * (_ts[i + _lag] - mean);
}
result[_lag] = numerator / denominator;
}
Ok(result)
}
#[allow(dead_code)]
pub fn cross_correlation<F>(
x: &Array1<F>,
y: &Array1<F>,
max_lag: Option<usize>,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
let min_len = x.len().min(y.len());
if min_len < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for cross-correlation".to_string(),
));
}
let default_max_lag = min_len / 4;
let max_lag = max_lag.unwrap_or(default_max_lag).min(min_len - 1);
let x_mean = x.sum() / F::from(x.len()).expect("Operation failed");
let y_mean = y.sum() / F::from(y.len()).expect("Operation failed");
let mut result = Array1::zeros(max_lag + 1);
for _lag in 0..=max_lag {
let mut numerator = F::zero();
let mut count = 0;
for i in 0..(min_len - _lag) {
numerator = numerator + (x[i] - x_mean) * (y[i + _lag] - y_mean);
count += 1;
}
if count > 0 {
result[_lag] = numerator / F::from(count).expect("Failed to convert to float");
}
}
Ok(result)
}
#[allow(dead_code)]
pub fn partial_autocorrelation<F>(_ts: &Array1<F>, maxlag: Option<usize>) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug,
{
if _ts.len() < 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series must have at least 2 points for partial autocorrelation".to_string(),
));
}
let default_max_lag = std::cmp::min(_ts.len() / 4, 10);
let max_lag = std::cmp::min(maxlag.unwrap_or(default_max_lag), _ts.len() - 1);
let acf = autocorrelation(_ts, Some(max_lag))?;
let mut pacf = Array1::zeros(max_lag + 1);
pacf[0] = F::one();
if max_lag >= 1 {
pacf[1] = acf[1];
}
if max_lag >= 2 {
let mut phi_old = Array1::zeros(max_lag + 1);
for j in 2..=max_lag {
let mut phi = Array1::zeros(j + 1);
for k in 1..j {
phi[k] = phi_old[k];
}
let mut numerator = acf[j];
for k in 1..j {
numerator = numerator - phi_old[k] * acf[j - k];
}
let mut denominator = F::one();
for k in 1..j {
denominator = denominator - phi_old[k] * acf[k];
}
phi[j] = numerator / denominator;
for k in 1..j {
phi[k] = phi_old[k] - phi[j] * phi_old[j - k];
}
pacf[j] = phi[j];
phi_old = phi;
}
}
Ok(pacf)
}
#[allow(dead_code)]
pub fn detrend<S, F>(
data: &ArrayBase<S, Ix1>,
axis: usize,
detrend_type: &str,
breakpoints: Option<&[usize]>,
) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
scirs2_core::validation::checkarray_finite(data, "data")?;
if axis != 0 {
return Err(TimeSeriesError::InvalidInput(
"Only axis=0 supported for 1D arrays".to_string(),
));
}
match detrend_type {
"constant" => {
let mean = data.mean().ok_or_else(|| {
TimeSeriesError::ComputationError("Failed to compute mean".to_string())
})?;
Ok(data.map(|&x| x - mean))
}
"linear" => {
let n = data.len();
if n < 2 {
return Err(TimeSeriesError::InvalidInput(
"Data must have at least 2 points for linear detrending".to_string(),
));
}
if let Some(bp) = breakpoints {
let mut result = data.to_owned();
let mut bp_indices = vec![0];
bp_indices.extend_from_slice(bp);
bp_indices.push(n);
for i in 0..bp_indices.len() - 1 {
let start = bp_indices[i];
let end = bp_indices[i + 1];
let segment = s![start..end];
let segment_data = data.slice(segment);
let trend = linear_trend(&segment_data, start)?;
for j in start..end {
result[j] = result[j] - trend[j - start];
}
}
Ok(result)
} else {
let trend = linear_trend(data, 0)?;
Ok(data.to_owned() - trend)
}
}
_ => Err(TimeSeriesError::InvalidInput(format!(
"Invalid detrend _type: {detrend_type}. Must be 'constant' or 'linear'"
))),
}
}
#[allow(dead_code)]
pub fn detrend_2d<S, F>(
data: &ArrayBase<S, Ix2>,
axis: usize,
detrend_type: &str,
breakpoints: Option<&[usize]>,
) -> Result<Array2<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
scirs2_core::validation::checkarray_finite(data, "data")?;
if axis > 1 {
return Err(TimeSeriesError::InvalidInput(
"Axis must be 0 or 1 for 2D arrays".to_string(),
));
}
let mut result = data.to_owned();
if axis == 0 {
for mut col in result.columns_mut() {
let detrended = detrend(&col.view(), 0, detrend_type, breakpoints)?;
col.assign(&detrended);
}
} else {
for mut row in result.rows_mut() {
let detrended = detrend(&row.view(), 0, detrend_type, breakpoints)?;
row.assign(&detrended);
}
}
Ok(result)
}
#[allow(dead_code)]
fn linear_trend<S, F>(data: &ArrayBase<S, Ix1>, offset: usize) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display + ScalarOperand,
{
let n = data.len();
let x = Array1::linspace(
F::from(offset).expect("Failed to convert to float"),
F::from(offset + n - 1).expect("Failed to convert to float"),
n,
);
let y = data.to_owned();
let x_mean = x
.mean()
.ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute x mean".to_string()))?;
let y_mean = y
.mean()
.ok_or_else(|| TimeSeriesError::ComputationError("Failed to compute y mean".to_string()))?;
let x_centered = &x - x_mean;
let y_centered = &y - y_mean;
let numerator = x_centered.dot(&y_centered);
let denominator = x_centered.dot(&x_centered);
if denominator.abs() < F::epsilon() {
return Err(TimeSeriesError::ComputationError(
"Singular matrix in linear regression".to_string(),
));
}
let slope = numerator / denominator;
let intercept = y_mean - slope * x_mean;
Ok(x.map(|&xi| slope * xi + intercept))
}
#[allow(dead_code)]
pub fn resample<S, F>(
x: &ArrayBase<S, Ix1>,
num: usize,
axis: usize,
window: Option<&Array1<F>>,
) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display,
{
scirs2_core::validation::checkarray_finite(x, "x")?;
scirs2_core::validation::check_positive(num as f64, "num")?;
if axis != 0 {
return Err(TimeSeriesError::InvalidInput(
"Only axis=0 supported for 1D arrays".to_string(),
));
}
let n = x.len();
if n == num {
return Ok(x.to_owned());
}
let x_f64: Vec<f64> = x
.iter()
.map(|v| v.to_f64().expect("Failed to convert to f64"))
.collect();
let mut spectrum = scirs2_fft::fft(&x_f64, Some(n))
.map_err(|e| TimeSeriesError::ComputationError(e.to_string()))?;
if let Some(win) = window {
if win.len() == n {
for (s_val, w_val) in spectrum.iter_mut().zip(win.iter()) {
let w_f64 = w_val.to_f64().expect("Failed to convert window to f64");
s_val.re *= w_f64;
s_val.im *= w_f64;
}
}
}
let mut new_spectrum: Vec<scirs2_core::numeric::Complex64> = Vec::with_capacity(num);
if num > n {
let pos_half = n / 2;
new_spectrum.extend_from_slice(&spectrum[..pos_half]);
if n % 2 == 0 {
let nyq = spectrum[pos_half];
let half_re = nyq.re * 0.5;
let half_im = nyq.im * 0.5;
new_spectrum.push(scirs2_core::numeric::Complex64::new(half_re, half_im));
let zeros = num - n - 1;
for _ in 0..zeros {
new_spectrum.push(scirs2_core::numeric::Complex64::new(0.0, 0.0));
}
new_spectrum.push(scirs2_core::numeric::Complex64::new(half_re, half_im));
new_spectrum.extend_from_slice(&spectrum[pos_half + 1..]);
} else {
let zeros = num - n;
for _ in 0..zeros {
new_spectrum.push(scirs2_core::numeric::Complex64::new(0.0, 0.0));
}
new_spectrum.extend_from_slice(&spectrum[pos_half..]);
}
} else {
let new_pos_half = num / 2;
let taper_start = (new_pos_half as f64 * 0.9) as usize;
for (i, s_val) in spectrum.iter_mut().enumerate().take(new_pos_half) {
if i >= taper_start && new_pos_half > taper_start {
let t = (i - taper_start) as f64 / (new_pos_half - taper_start) as f64;
let taper = 0.5 * (1.0 + (std::f64::consts::PI * t).cos());
s_val.re *= taper;
s_val.im *= taper;
}
}
new_spectrum.extend_from_slice(&spectrum[..new_pos_half]);
if num % 2 == 0 {
let nyq_pos = spectrum[new_pos_half];
let nyq_neg = spectrum[n - new_pos_half];
new_spectrum.push(scirs2_core::numeric::Complex64::new(
nyq_pos.re + nyq_neg.re,
nyq_pos.im + nyq_neg.im,
));
new_spectrum.extend_from_slice(&spectrum[n - new_pos_half + 1..]);
} else {
new_spectrum.extend_from_slice(&spectrum[n - new_pos_half..]);
}
}
debug_assert_eq!(
new_spectrum.len(),
num,
"BUG: new_spectrum has {} bins, expected {}",
new_spectrum.len(),
num
);
let scale_factor = num as f64 / n as f64;
let time_domain = scirs2_fft::ifft(&new_spectrum, Some(num))
.map_err(|e| TimeSeriesError::ComputationError(e.to_string()))?;
let result = Array1::from_vec(
time_domain
.iter()
.take(num)
.map(|c| {
F::from(c.re * scale_factor)
.expect("Failed to convert resampled value to output type")
})
.collect(),
);
Ok(result)
}
#[allow(dead_code)]
pub fn decimate<S, F>(
x: &ArrayBase<S, Ix1>,
q: usize,
n: Option<usize>,
ftype: Option<&str>,
axis: usize,
) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display,
{
scirs2_core::validation::checkarray_finite(x, "x")?;
scirs2_core::validation::check_positive(q as f64, "q")?;
if axis != 0 {
return Err(TimeSeriesError::InvalidInput(
"Only axis=0 supported for 1D arrays".to_string(),
));
}
if q == 1 {
return Ok(x.to_owned());
}
let filter_order = n.unwrap_or(8);
let filter_type = ftype.unwrap_or("iir");
let cutoff = F::from(0.5).expect("Failed to convert constant to float")
/ F::from(q).expect("Failed to convert to float");
let filtered = match filter_type {
"iir" => {
apply_chebyshev_filter(x, filter_order, cutoff)?
}
"fir" => {
apply_fir_filter(x, filter_order, cutoff)?
}
_ => {
return Err(TimeSeriesError::InvalidInput(format!(
"Invalid filter type: {filter_type}. Must be 'iir' or 'fir'"
)))
}
};
let mut result = Array1::zeros(x.len() / q);
for (i, j) in (0..x.len()).step_by(q).enumerate() {
if i < result.len() {
result[i] = filtered[j];
}
}
Ok(result)
}
#[allow(dead_code)]
fn apply_chebyshev_filter<S, F>(x: &ArrayBase<S, Ix1>, order: usize, cutoff: F) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display,
{
let n_samples = x.len();
if n_samples == 0 {
return Ok(Array1::zeros(0));
}
let order = order.max(1);
let cutoff_f64 = cutoff
.to_f64()
.expect("Failed to convert cutoff to f64")
.clamp(1e-6, 0.4999);
let ripple_db = 1.0_f64;
let eps = (10_f64.powf(ripple_db / 10.0) - 1.0).sqrt();
let omega_a = 2.0 * (std::f64::consts::PI * cutoff_f64).tan();
let sinh_part = (1.0 / eps).asinh() / order as f64;
let sinh_v = sinh_part.sinh();
let cosh_v = sinh_part.cosh();
let mut analog_poles: Vec<(f64, f64)> = Vec::with_capacity(order);
for k in 1..=order {
let phi = std::f64::consts::PI * (2 * k - 1) as f64 / (2 * order) as f64;
let re = -phi.sin() * sinh_v;
let im = phi.cos() * cosh_v;
analog_poles.push((re, im));
}
let poles_scaled: Vec<(f64, f64)> = analog_poles
.iter()
.map(|(re, im)| (re * omega_a, im * omega_a))
.collect();
let mut digital_poles: Vec<(f64, f64)> = Vec::with_capacity(order);
for (re, im) in &poles_scaled {
let n_re = 2.0 + re;
let n_im = *im;
let d_re = 2.0 - re;
let d_im = -im;
let denom = d_re * d_re + d_im * d_im;
let z_re = (n_re * d_re + n_im * d_im) / denom;
let z_im = (n_im * d_re - n_re * d_im) / denom;
digital_poles.push((z_re, z_im));
}
struct Biquad {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
}
let mut sections: Vec<Biquad> = Vec::new();
let mut i = 0;
while i < digital_poles.len() {
let (z_re, z_im) = digital_poles[i];
if z_im.abs() < 1e-10 {
let a1 = -z_re;
let dc_gain = 1.0 / (1.0 + a1); sections.push(Biquad {
b0: dc_gain,
b1: dc_gain,
b2: 0.0,
a1,
a2: 0.0,
});
i += 1;
} else if i + 1 < digital_poles.len() {
let a1 = -2.0 * z_re;
let a2 = z_re * z_re + z_im * z_im;
let dc_gain_denom = 1.0 + a1 + a2;
let scale = if dc_gain_denom.abs() < 1e-12 {
1.0
} else {
3.0 / dc_gain_denom
};
sections.push(Biquad {
b0: 1.0 * scale,
b1: 2.0 * scale,
b2: 1.0 * scale,
a1,
a2,
});
i += 2;
} else {
let a1 = -2.0 * z_re;
let a2 = z_re * z_re + z_im * z_im;
let dc_gain_denom = 1.0 + a1 + a2;
let scale = if dc_gain_denom.abs() < 1e-12 {
1.0
} else {
3.0 / dc_gain_denom
};
sections.push(Biquad {
b0: scale,
b1: 2.0 * scale,
b2: scale,
a1,
a2,
});
i += 1;
}
}
let x_f64: Vec<f64> = x
.iter()
.map(|v| v.to_f64().expect("Failed to convert signal to f64"))
.collect();
let mut y = x_f64.clone();
for sec in §ions {
let mut w1 = 0.0_f64;
let mut w2 = 0.0_f64;
for sample in y.iter_mut() {
let w0 = *sample - sec.a1 * w1 - sec.a2 * w2;
*sample = sec.b0 * w0 + sec.b1 * w1 + sec.b2 * w2;
w2 = w1;
w1 = w0;
}
}
let result = Array1::from_vec(
y.iter()
.map(|&v| F::from(v).expect("Failed to convert filtered value to output type"))
.collect(),
);
Ok(result)
}
#[allow(dead_code)]
fn apply_fir_filter<S, F>(x: &ArrayBase<S, Ix1>, order: usize, cutoff: F) -> Result<Array1<F>>
where
S: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display,
{
let mut coeffs = Array1::zeros(order + 1);
let fc = cutoff;
let half_order = order / 2;
for i in 0..=order {
let n = i as i32 - half_order as i32;
if n == 0 {
coeffs[i] = F::from(2.0).expect("Failed to convert constant to float") * fc;
} else {
let n_f = F::from(n).expect("Failed to convert to float");
let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
coeffs[i] =
(F::from(2.0).expect("Failed to convert constant to float") * fc * pi * n_f).sin()
/ (pi * n_f);
let window = F::from(0.54).expect("Failed to convert constant to float")
- F::from(0.46).expect("Failed to convert constant to float")
* (F::from(2.0).expect("Failed to convert constant to float")
* pi
* F::from(i).expect("Failed to convert to float")
/ F::from(order).expect("Failed to convert to float"))
.cos();
coeffs[i] = coeffs[i] * window;
}
}
let sum: F = coeffs.sum();
coeffs.map_inplace(|x| *x = *x / sum);
convolve_1d(x, &coeffs.view())
}
#[allow(dead_code)]
fn convolve_1d<S, T, F>(x: &ArrayBase<S, Ix1>, kernel: &ArrayBase<T, Ix1>) -> Result<Array1<F>>
where
S: Data<Elem = F>,
T: Data<Elem = F>,
F: Float + NumCast + FromPrimitive + Debug + Display,
{
let n = x.len();
let k = kernel.len();
let half_k = k / 2;
let mut result = Array1::zeros(n);
for i in 0..n {
let mut sum = F::zero();
for j in 0..k {
let idx = i as i32 + j as i32 - half_k as i32;
if idx >= 0 && idx < n as i32 {
sum = sum + x[idx as usize] * kernel[j];
}
}
result[i] = sum;
}
Ok(result)
}
#[allow(dead_code)]
pub fn create_time_series<F>(
start_date: &str,
end_date: &str,
values: &Array1<F>,
) -> Result<(Vec<String>, Array1<F>)>
where
F: Float + FromPrimitive + Debug,
{
fn parse_date(_datestr: &str) -> Result<(i32, u32, u32)> {
let parts: Vec<&str> = _datestr.split('-').collect();
if parts.len() != 3 {
return Err(TimeSeriesError::InvalidInput(format!(
"Invalid date format: {_datestr}, expected YYYY-MM-DD"
)));
}
let year = parts[0]
.parse::<i32>()
.map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid year: {}", parts[0])))?;
let month = parts[1]
.parse::<u32>()
.map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid month: {}", parts[1])))?;
let day = parts[2]
.parse::<u32>()
.map_err(|_| TimeSeriesError::InvalidInput(format!("Invalid day: {}", parts[2])))?;
if !(1..=12).contains(&month) {
return Err(TimeSeriesError::InvalidInput(format!(
"Month must be between 1 and 12, got {month}"
)));
}
if !(1..=31).contains(&day) {
return Err(TimeSeriesError::InvalidInput(format!(
"Day must be between 1 and 31, got {day}"
)));
}
Ok((year, month, day))
}
fn days_between(start: (i32, u32, u32), end: (i32, u32, u32)) -> i32 {
let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let start_days = start.0 * 365
+ (1..start.1).map(|m| days_in_month[m as usize]).sum::<u32>() as i32
+ start.2 as i32;
let end_days = end.0 * 365
+ (1..end.1).map(|m| days_in_month[m as usize]).sum::<u32>() as i32
+ end.2 as i32;
end_days - start_days + 1 }
fn generate_dates(start: (i32, u32, u32), n_days: usize) -> Vec<String> {
let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut dates = Vec::with_capacity(n_days);
let mut year = start.0;
let mut month = start.1;
let mut day = start.2;
for _ in 0..n_days {
dates.push(format!("{year:04}-{month:02}-{day:02}"));
day += 1;
if day > days_in_month[month as usize] {
day = 1;
month += 1;
if month > 12 {
month = 1;
year += 1;
}
}
}
dates
}
let start = parse_date(start_date)?;
let end = parse_date(end_date)?;
let days = days_between(start, end);
if days < 1 {
return Err(TimeSeriesError::InvalidInput(format!(
"End _date ({end_date}) must be after start _date ({start_date})"
)));
}
if values.len() != days as usize {
return Err(TimeSeriesError::InvalidInput(format!(
"Values length ({}) must match _date range length ({})",
values.len(),
days
)));
}
let dates = generate_dates(start, days as usize);
let time_series = values.clone();
Ok((dates, time_series))
}
pub fn calculate_basic_stats<F>(data: &Array1<F>) -> Result<std::collections::HashMap<String, f64>>
where
F: Float + FromPrimitive + Into<f64>,
{
let mut stats = std::collections::HashMap::new();
if data.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Data array is empty".to_string(),
));
}
let n = data.len() as f64;
let mean = data.mean_or(F::zero()).into();
let variance = data
.iter()
.map(|x| {
let diff = (*x).into() - mean;
diff * diff
})
.sum::<f64>()
/ n;
stats.insert("mean".to_string(), mean);
stats.insert("variance".to_string(), variance);
stats.insert("std".to_string(), variance.sqrt());
stats.insert(
"min".to_string(),
data.iter()
.map(|x| (*x).into())
.fold(f64::INFINITY, f64::min),
);
stats.insert(
"max".to_string(),
data.iter()
.map(|x| (*x).into())
.fold(f64::NEG_INFINITY, f64::max),
);
stats.insert("count".to_string(), n);
Ok(stats)
}
pub fn difference_series<F>(data: &Array1<F>, periods: usize) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Clone,
{
if periods == 0 {
return Err(TimeSeriesError::InvalidInput(
"Periods must be greater than 0".to_string(),
));
}
if data.len() <= periods {
return Err(TimeSeriesError::InvalidInput(
"Data length must be greater than periods".to_string(),
));
}
let mut result = Vec::new();
for i in periods..data.len() {
result.push(data[i] - data[i - periods]);
}
Ok(Array1::from_vec(result))
}
pub fn seasonal_difference_series<F>(data: &Array1<F>, periods: usize) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Clone,
{
difference_series(data, periods)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_invert_matrix_identity() {
let a = array![[4.0_f64, 3.0], [6.0, 3.0]];
let inv = invert_matrix(&a).expect("matrix should be invertible");
for i in 0..2 {
for j in 0..2 {
let mut acc = 0.0;
for k in 0..2 {
acc += a[[i, k]] * inv[[k, j]];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert_relative_eq!(acc, expected, epsilon = 1e-10);
}
}
}
#[test]
fn test_invert_matrix_singular_errors() {
let a = array![[1.0_f64, 2.0], [2.0, 4.0]];
assert!(invert_matrix(&a).is_err());
}
#[test]
fn test_is_stationary_returns_real_statistic() {
let stationary = array![
0.5_f64, -0.3, 0.2, -0.4, 0.1, -0.2, 0.3, -0.1, 0.25, -0.35, 0.15, -0.25, 0.05, -0.15,
0.2, -0.3, 0.1, -0.2, 0.3, -0.1, 0.2, -0.25, 0.15, -0.2
];
let (stat, p) = is_stationary(&stationary, Some(1)).expect("ADF should succeed");
assert!(stat.is_finite());
assert!((0.0..=1.0).contains(&p));
let trending = array![
1.0_f64, 2.3, 3.1, 4.6, 5.2, 6.9, 7.4, 8.8, 9.3, 10.7, 11.2, 12.9, 13.4, 14.1, 15.8,
16.2, 17.9, 18.3, 19.7, 20.4, 21.1, 22.8, 23.2, 24.9
];
let (stat_trend, _p_trend) = is_stationary(&trending, Some(1)).expect("ADF should succeed");
assert!((stat - stat_trend).abs() > 1e-9);
}
#[test]
fn test_is_stationary_too_short() {
let ts = array![1.0_f64, 2.0];
assert!(is_stationary(&ts, None).is_err());
}
#[test]
fn test_is_stationary_linear_ramp_no_panic() {
let ramp = Array1::from_vec((1..=20).map(|i| i as f64).collect());
let (stat, p) = is_stationary(&ramp, None).expect("linear ramp must not error");
assert!(stat.is_finite());
assert!((0.0..=1.0).contains(&p));
}
#[test]
fn test_is_stationary_constant_series_no_panic() {
let constant = Array1::from_elem(20, 5.0_f64);
let (stat, p) = is_stationary(&constant, None).expect("constant series must not error");
assert!(stat.is_finite());
assert!((0.0..=1.0).contains(&p));
}
#[test]
fn test_detrend_constant() {
let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
let detrended = detrend(&x.view(), 0, "constant", None).expect("Operation failed");
assert_relative_eq!(detrended.clone().mean(), 0.0, epsilon = 1e-10);
assert_relative_eq!(detrended[0], -2.0, epsilon = 1e-10);
assert_relative_eq!(detrended[2], 0.0, epsilon = 1e-10);
assert_relative_eq!(detrended[4], 2.0, epsilon = 1e-10);
}
#[test]
fn test_detrend_linear() {
let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
let detrended = detrend(&x.view(), 0, "linear", None).expect("Operation failed");
for i in 1..detrended.len() {
assert_relative_eq!(detrended[i] - detrended[i - 1], 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_detrend_linear_with_breakpoints() {
let x = array![1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0];
let breakpoints = vec![4];
let detrended =
detrend(&x.view(), 0, "linear", Some(&breakpoints)).expect("Operation failed");
assert_relative_eq!(detrended[0], 0.0, epsilon = 1e-10);
assert_relative_eq!(detrended[3], 0.0, epsilon = 1e-10);
assert_relative_eq!(detrended[4], 0.0, epsilon = 1e-10);
assert_relative_eq!(detrended[7], 0.0, epsilon = 1e-10);
}
#[test]
fn test_detrend_2d() {
let x = Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("Operation failed");
let detrended = detrend_2d(&x.view(), 0, "constant", None).expect("Operation failed");
for col in detrended.columns() {
assert_relative_eq!(col.mean(), 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_resample_upsample() {
let x = array![1.0, 2.0, 3.0, 4.0];
let resampled = resample(&x.view(), 8, 0, None).expect("Operation failed");
assert_eq!(resampled.len(), 8);
assert_relative_eq!(resampled[0], x[0], epsilon = 0.1);
assert_relative_eq!(resampled[resampled.len() - 1], 2.5_f64, epsilon = 0.1);
assert_relative_eq!(resampled[2], 2.0_f64, epsilon = 0.2);
}
#[test]
fn test_resample_downsample() {
let x = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let resampled = resample(&x.view(), 4, 0, None).expect("Operation failed");
assert_eq!(resampled.len(), 4);
}
#[test]
fn test_decimate() {
let x = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let decimated = decimate(&x.view(), 2, Some(4), Some("iir"), 0).expect("Operation failed");
assert_eq!(decimated.len(), 4);
}
#[test]
fn test_invalid_detrend_type() {
let x = array![1.0, 2.0, 3.0];
let result = detrend(&x.view(), 0, "invalid", None);
assert!(result.is_err());
}
#[test]
fn test_invalid_axis() {
let x = array![1.0, 2.0, 3.0];
let result = detrend(&x.view(), 1, "constant", None);
assert!(result.is_err());
}
}