1use crate::iter_maybe_parallel;
13use crate::matrix::FdMatrix;
14use crate::smoothing::local_polynomial;
15use nalgebra::{DMatrix, DVector, Dyn, SVD};
16#[cfg(feature = "parallel")]
17use rayon::iter::ParallelIterator;
18use std::borrow::Cow;
19
20#[derive(Debug, Clone)]
22pub struct TrendResult {
23 pub trend: FdMatrix,
25 pub detrended: FdMatrix,
27 pub method: Cow<'static, str>,
29 pub coefficients: Option<FdMatrix>,
32 pub rss: Vec<f64>,
34 pub n_params: usize,
36}
37
38impl TrendResult {
39 fn empty(
41 data: &FdMatrix,
42 n: usize,
43 m: usize,
44 method: Cow<'static, str>,
45 n_params: usize,
46 ) -> Self {
47 TrendResult {
48 trend: FdMatrix::zeros(n, m),
49 detrended: FdMatrix::from_slice(data.as_slice(), n, m)
50 .unwrap_or_else(|| FdMatrix::zeros(n, m)),
51 method,
52 coefficients: None,
53 rss: vec![0.0; n],
54 n_params,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct DecomposeResult {
62 pub trend: FdMatrix,
64 pub seasonal: FdMatrix,
66 pub remainder: FdMatrix,
68 pub period: f64,
70 pub method: Cow<'static, str>,
72}
73
74pub fn detrend_linear(data: &FdMatrix, argvals: &[f64]) -> TrendResult {
83 let (n, m) = data.shape();
84 if n == 0 || m < 2 || argvals.len() != m {
85 return TrendResult::empty(data, n, m, Cow::Borrowed("linear"), 2);
86 }
87
88 let mean_t: f64 = argvals.iter().sum::<f64>() / m as f64;
89 let ss_t: f64 = argvals.iter().map(|&t| (t - mean_t).powi(2)).sum();
90
91 let scalars: Vec<(f64, f64, f64)> = iter_maybe_parallel!(0..n)
93 .map(|i| {
94 let mut sum_y = 0.0;
95 for j in 0..m {
96 sum_y += data[(i, j)];
97 }
98 let mean_y = sum_y / m as f64;
99 let mut sp = 0.0;
100 for j in 0..m {
101 sp += (argvals[j] - mean_t) * (data[(i, j)] - mean_y);
102 }
103 let slope = if ss_t.abs() > 1e-15 { sp / ss_t } else { 0.0 };
104 let intercept = mean_y - slope * mean_t;
105 let mut rss = 0.0;
106 for j in 0..m {
107 let residual = data[(i, j)] - (intercept + slope * argvals[j]);
108 rss += residual * residual;
109 }
110 (intercept, slope, rss)
111 })
112 .collect();
113
114 let mut trend = FdMatrix::zeros(n, m);
116 let mut detrended = FdMatrix::zeros(n, m);
117 let mut coefficients = FdMatrix::zeros(n, 2);
118 let mut rss = vec![0.0; n];
119
120 for (i, &(intercept, slope, r)) in scalars.iter().enumerate() {
121 coefficients[(i, 0)] = intercept;
122 coefficients[(i, 1)] = slope;
123 rss[i] = r;
124 for j in 0..m {
125 let trend_val = intercept + slope * argvals[j];
126 trend[(i, j)] = trend_val;
127 detrended[(i, j)] = data[(i, j)] - trend_val;
128 }
129 }
130
131 TrendResult {
132 trend,
133 detrended,
134 method: Cow::Borrowed("linear"),
135 coefficients: Some(coefficients),
136 rss,
137 n_params: 2,
138 }
139}
140
141fn build_vandermonde_matrix(t_norm: &[f64], m: usize, n_coef: usize) -> DMatrix<f64> {
142 let mut design = DMatrix::zeros(m, n_coef);
143 for j in 0..m {
144 let t = t_norm[j];
145 let mut power = 1.0;
146 for k in 0..n_coef {
147 design[(j, k)] = power;
148 power *= t;
149 }
150 }
151 design
152}
153
154fn fit_polynomial_single_curve(
155 curve: &[f64],
156 svd: &SVD<f64, Dyn, Dyn>,
157 design: &DMatrix<f64>,
158 n_coef: usize,
159 m: usize,
160) -> (Vec<f64>, Vec<f64>, Vec<f64>, f64) {
161 let y = DVector::from_row_slice(curve);
162 let beta = svd
163 .solve(&y, 1e-10)
164 .unwrap_or_else(|_| DVector::zeros(n_coef));
165 let fitted = design * β
166 let mut trend = vec![0.0; m];
167 let mut detrended = vec![0.0; m];
168 let mut rss = 0.0;
169 for j in 0..m {
170 trend[j] = fitted[j];
171 detrended[j] = curve[j] - fitted[j];
172 rss += detrended[j].powi(2);
173 }
174 let coefs: Vec<f64> = beta.iter().cloned().collect();
175 (trend, detrended, coefs, rss)
176}
177
178fn diff_single_curve(curve: &[f64], m: usize, order: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, f64) {
179 let diff1: Vec<f64> = (0..m - 1).map(|j| curve[j + 1] - curve[j]).collect();
180 let detrended = if order == 2 {
181 (0..diff1.len() - 1)
182 .map(|j| diff1[j + 1] - diff1[j])
183 .collect()
184 } else {
185 diff1.clone()
186 };
187 let initial_values = if order == 2 {
188 vec![curve[0], curve[1]]
189 } else {
190 vec![curve[0]]
191 };
192 let rss: f64 = detrended.iter().map(|&x| x.powi(2)).sum();
193 let new_m = m - order;
194 let mut trend = vec![0.0; m];
195 trend[0] = curve[0];
196 if order == 1 {
197 for j in 1..m {
198 trend[j] = curve[j] - if j <= new_m { detrended[j - 1] } else { 0.0 };
199 }
200 } else {
201 trend = curve.to_vec();
202 }
203 let mut det_full = vec![0.0; m];
204 det_full[..new_m].copy_from_slice(&detrended[..new_m]);
205 (trend, det_full, initial_values, rss)
206}
207
208fn reassemble_polynomial_results(
209 results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>, f64)>,
210 n: usize,
211 m: usize,
212 n_coef: usize,
213) -> (FdMatrix, FdMatrix, FdMatrix, Vec<f64>) {
214 let mut trend = FdMatrix::zeros(n, m);
215 let mut detrended = FdMatrix::zeros(n, m);
216 let mut coefficients = FdMatrix::zeros(n, n_coef);
217 let mut rss = vec![0.0; n];
218 for (i, (t, d, coefs, r)) in results.into_iter().enumerate() {
219 for j in 0..m {
220 trend[(i, j)] = t[j];
221 detrended[(i, j)] = d[j];
222 }
223 for k in 0..n_coef {
224 coefficients[(i, k)] = coefs[k];
225 }
226 rss[i] = r;
227 }
228 (trend, detrended, coefficients, rss)
229}
230
231fn reassemble_trend_results(
233 results: Vec<(Vec<f64>, Vec<f64>, f64)>,
234 n: usize,
235 m: usize,
236) -> (FdMatrix, FdMatrix, Vec<f64>) {
237 let mut trend = FdMatrix::zeros(n, m);
238 let mut detrended = FdMatrix::zeros(n, m);
239 let mut rss = vec![0.0; n];
240 for (i, (t, d, r)) in results.into_iter().enumerate() {
241 for j in 0..m {
242 trend[(i, j)] = t[j];
243 detrended[(i, j)] = d[j];
244 }
245 rss[i] = r;
246 }
247 (trend, detrended, rss)
248}
249
250pub fn detrend_polynomial(data: &FdMatrix, argvals: &[f64], degree: usize) -> TrendResult {
252 let (n, m) = data.shape();
253 if n == 0 || m < degree + 1 || argvals.len() != m || degree == 0 {
254 return TrendResult::empty(
255 data,
256 n,
257 m,
258 Cow::Owned(format!("polynomial({})", degree)),
259 degree + 1,
260 );
261 }
262 if degree == 1 {
263 let mut result = detrend_linear(data, argvals);
264 result.method = Cow::Borrowed("polynomial(1)");
265 return result;
266 }
267 let n_coef = degree + 1;
268 let t_min = argvals.iter().cloned().fold(f64::INFINITY, f64::min);
269 let t_max = argvals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
270 let t_range = if (t_max - t_min).abs() > 1e-15 {
271 t_max - t_min
272 } else {
273 1.0
274 };
275 let t_norm: Vec<f64> = argvals.iter().map(|&t| (t - t_min) / t_range).collect();
276 let design = build_vandermonde_matrix(&t_norm, m, n_coef);
277 let svd = design.clone().svd(true, true);
278 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>, f64)> = iter_maybe_parallel!(0..n)
279 .map(|i| {
280 let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
281 fit_polynomial_single_curve(&curve, &svd, &design, n_coef, m)
282 })
283 .collect();
284 let (trend, detrended, coefficients, rss) =
285 reassemble_polynomial_results(results, n, m, n_coef);
286 TrendResult {
287 trend,
288 detrended,
289 method: Cow::Owned(format!("polynomial({})", degree)),
290 coefficients: Some(coefficients),
291 rss,
292 n_params: n_coef,
293 }
294}
295
296pub fn detrend_diff(data: &FdMatrix, order: usize) -> TrendResult {
298 let (n, m) = data.shape();
299 if n == 0 || m <= order || order == 0 || order > 2 {
300 return TrendResult::empty(data, n, m, Cow::Owned(format!("diff{}", order)), order);
301 }
302 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>, f64)> = iter_maybe_parallel!(0..n)
303 .map(|i| {
304 let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
305 diff_single_curve(&curve, m, order)
306 })
307 .collect();
308 let (trend, detrended, coefficients, rss) = reassemble_polynomial_results(results, n, m, order);
309 TrendResult {
310 trend,
311 detrended,
312 method: Cow::Owned(format!("diff{}", order)),
313 coefficients: Some(coefficients),
314 rss,
315 n_params: order,
316 }
317}
318
319pub fn detrend_loess(
321 data: &FdMatrix,
322 argvals: &[f64],
323 bandwidth: f64,
324 degree: usize,
325) -> TrendResult {
326 let (n, m) = data.shape();
327 if n == 0 || m < 3 || argvals.len() != m || bandwidth <= 0.0 {
328 return TrendResult::empty(
329 data,
330 n,
331 m,
332 Cow::Borrowed("loess"),
333 (m as f64 * bandwidth.max(0.0)).ceil() as usize,
334 );
335 }
336 let t_min = argvals.iter().cloned().fold(f64::INFINITY, f64::min);
337 let t_max = argvals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
338 let abs_bandwidth = (t_max - t_min) * bandwidth;
339 let results: Vec<(Vec<f64>, Vec<f64>, f64)> = iter_maybe_parallel!(0..n)
340 .map(|i| {
341 let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
342 let trend =
343 local_polynomial(argvals, &curve, argvals, abs_bandwidth, degree, "tricube");
344 let mut detrended = vec![0.0; m];
345 let mut rss = 0.0;
346 for j in 0..m {
347 detrended[j] = curve[j] - trend[j];
348 rss += detrended[j].powi(2);
349 }
350 (trend, detrended, rss)
351 })
352 .collect();
353 let (trend, detrended, rss) = reassemble_trend_results(results, n, m);
354 let n_params = (m as f64 * bandwidth).ceil() as usize;
355 TrendResult {
356 trend,
357 detrended,
358 method: Cow::Borrowed("loess"),
359 coefficients: None,
360 rss,
361 n_params,
362 }
363}
364
365pub fn auto_detrend(data: &FdMatrix, argvals: &[f64]) -> TrendResult {
367 let (n, m) = data.shape();
368 if n == 0 || m < 4 || argvals.len() != m {
369 return TrendResult::empty(data, n, m, Cow::Borrowed("auto(none)"), 0);
370 }
371 let compute_aic = |result: &TrendResult| -> f64 {
372 let mut total_aic = 0.0;
373 for i in 0..n {
374 let rss = result.rss[i];
375 let k = result.n_params as f64;
376 let aic = if rss > 1e-15 {
377 m as f64 * (rss / m as f64).ln() + 2.0 * k
378 } else {
379 f64::NEG_INFINITY
380 };
381 total_aic += aic;
382 }
383 total_aic / n as f64
384 };
385 let linear = detrend_linear(data, argvals);
386 let poly2 = detrend_polynomial(data, argvals, 2);
387 let poly3 = detrend_polynomial(data, argvals, 3);
388 let loess = detrend_loess(data, argvals, 0.3, 2);
389 let aic_linear = compute_aic(&linear);
390 let aic_poly2 = compute_aic(&poly2);
391 let aic_poly3 = compute_aic(&poly3);
392 let aic_loess = compute_aic(&loess);
393 let methods = [
394 (aic_linear, "linear", linear),
395 (aic_poly2, "polynomial(2)", poly2),
396 (aic_poly3, "polynomial(3)", poly3),
397 (aic_loess, "loess", loess),
398 ];
399 let (_, best_name, mut best_result) = methods
400 .into_iter()
401 .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
402 .unwrap();
403 best_result.method = Cow::Owned(format!("auto({})", best_name));
404 best_result
405}
406
407fn fit_fourier_seasonal(
408 detrended_i: &[f64],
409 argvals: &[f64],
410 omega: f64,
411 n_harm: usize,
412 m: usize,
413) -> (Vec<f64>, Vec<f64>) {
414 let n_coef = 2 * n_harm;
415 let mut design = DMatrix::zeros(m, n_coef);
416 for j in 0..m {
417 let t = argvals[j];
418 for k in 0..n_harm {
419 let freq = (k + 1) as f64 * omega;
420 design[(j, 2 * k)] = (freq * t).cos();
421 design[(j, 2 * k + 1)] = (freq * t).sin();
422 }
423 }
424 let y = DVector::from_row_slice(detrended_i);
425 let svd = design.clone().svd(true, true);
426 let coef = svd
427 .solve(&y, 1e-10)
428 .unwrap_or_else(|_| DVector::zeros(n_coef));
429 let fitted = &design * &coef;
430 let seasonal: Vec<f64> = fitted.iter().cloned().collect();
431 let remainder: Vec<f64> = (0..m).map(|j| detrended_i[j] - seasonal[j]).collect();
432 (seasonal, remainder)
433}
434
435pub fn decompose_additive(
437 data: &FdMatrix,
438 argvals: &[f64],
439 period: f64,
440 trend_method: &str,
441 bandwidth: f64,
442 n_harmonics: usize,
443) -> DecomposeResult {
444 let (n, m) = data.shape();
445 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
446 return DecomposeResult {
447 trend: FdMatrix::zeros(n, m),
448 seasonal: FdMatrix::zeros(n, m),
449 remainder: FdMatrix::from_slice(data.as_slice(), n, m)
450 .unwrap_or_else(|| FdMatrix::zeros(n, m)),
451 period,
452 method: Cow::Borrowed("additive"),
453 };
454 }
455 let _ = trend_method;
456 let trend_result = detrend_loess(data, argvals, bandwidth.max(0.3), 2);
457 let n_harm = n_harmonics.max(1).min(m / 4);
458 let omega = 2.0 * std::f64::consts::PI / period;
459 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
460 .map(|i| {
461 let trend_i: Vec<f64> = (0..m).map(|j| trend_result.trend[(i, j)]).collect();
462 let detrended_i: Vec<f64> = (0..m).map(|j| trend_result.detrended[(i, j)]).collect();
463 let (seasonal, remainder) =
464 fit_fourier_seasonal(&detrended_i, argvals, omega, n_harm, m);
465 (trend_i, seasonal, remainder)
466 })
467 .collect();
468 let mut trend = FdMatrix::zeros(n, m);
469 let mut seasonal = FdMatrix::zeros(n, m);
470 let mut remainder = FdMatrix::zeros(n, m);
471 for (i, (t, s, r)) in results.into_iter().enumerate() {
472 for j in 0..m {
473 trend[(i, j)] = t[j];
474 seasonal[(i, j)] = s[j];
475 remainder[(i, j)] = r[j];
476 }
477 }
478 DecomposeResult {
479 trend,
480 seasonal,
481 remainder,
482 period,
483 method: Cow::Borrowed("additive"),
484 }
485}
486
487pub fn decompose_multiplicative(
489 data: &FdMatrix,
490 argvals: &[f64],
491 period: f64,
492 trend_method: &str,
493 bandwidth: f64,
494 n_harmonics: usize,
495) -> DecomposeResult {
496 let (n, m) = data.shape();
497 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
498 return DecomposeResult {
499 trend: FdMatrix::zeros(n, m),
500 seasonal: FdMatrix::zeros(n, m),
501 remainder: FdMatrix::from_slice(data.as_slice(), n, m)
502 .unwrap_or_else(|| FdMatrix::zeros(n, m)),
503 period,
504 method: Cow::Borrowed("multiplicative"),
505 };
506 }
507 let min_val = data
508 .as_slice()
509 .iter()
510 .cloned()
511 .fold(f64::INFINITY, f64::min);
512 let shift = if min_val <= 0.0 { -min_val + 1.0 } else { 0.0 };
513 let log_data_vec: Vec<f64> = data.as_slice().iter().map(|&x| (x + shift).ln()).collect();
514 let log_data = FdMatrix::from_column_major(log_data_vec, n, m).unwrap();
515 let additive_result = decompose_additive(
516 &log_data,
517 argvals,
518 period,
519 trend_method,
520 bandwidth,
521 n_harmonics,
522 );
523 let mut trend = FdMatrix::zeros(n, m);
524 let mut seasonal = FdMatrix::zeros(n, m);
525 let mut remainder = FdMatrix::zeros(n, m);
526 for i in 0..n {
527 for j in 0..m {
528 trend[(i, j)] = additive_result.trend[(i, j)].exp() - shift;
529 seasonal[(i, j)] = additive_result.seasonal[(i, j)].exp();
530 remainder[(i, j)] = additive_result.remainder[(i, j)].exp();
531 }
532 }
533 DecomposeResult {
534 trend,
535 seasonal,
536 remainder,
537 period,
538 method: Cow::Borrowed("multiplicative"),
539 }
540}
541
542#[derive(Debug, Clone)]
548pub struct StlResult {
549 pub trend: FdMatrix,
551 pub seasonal: FdMatrix,
553 pub remainder: FdMatrix,
555 pub weights: FdMatrix,
557 pub period: usize,
559 pub s_window: usize,
561 pub t_window: usize,
563 pub inner_iterations: usize,
565 pub outer_iterations: usize,
567}
568
569pub fn stl_decompose(
571 data: &FdMatrix,
572 period: usize,
573 s_window: Option<usize>,
574 t_window: Option<usize>,
575 l_window: Option<usize>,
576 robust: bool,
577 inner_iterations: Option<usize>,
578 outer_iterations: Option<usize>,
579) -> StlResult {
580 let (n, m) = data.shape();
581 if n == 0 || m < 2 * period || period < 2 {
582 return StlResult {
583 trend: FdMatrix::zeros(n, m),
584 seasonal: FdMatrix::zeros(n, m),
585 remainder: FdMatrix::from_slice(data.as_slice(), n, m)
586 .unwrap_or_else(|| FdMatrix::zeros(n, m)),
587 weights: FdMatrix::from_column_major(vec![1.0; n * m], n, m)
588 .unwrap_or_else(|| FdMatrix::zeros(n, m)),
589 period,
590 s_window: 0,
591 t_window: 0,
592 inner_iterations: 0,
593 outer_iterations: 0,
594 };
595 }
596 let s_win = s_window.unwrap_or(7).max(3) | 1;
597 let t_win = t_window.unwrap_or_else(|| {
598 let ratio = 1.5 * period as f64 / (1.0 - 1.5 / s_win as f64);
599 let val = ratio.ceil() as usize;
600 val.max(3) | 1
601 });
602 let l_win = l_window.unwrap_or(period) | 1;
603 let n_inner = inner_iterations.unwrap_or(2);
604 let n_outer = outer_iterations.unwrap_or(if robust { 15 } else { 1 });
605 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
606 .map(|i| {
607 let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
608 stl_single_series(
609 &curve, period, s_win, t_win, l_win, robust, n_inner, n_outer,
610 )
611 })
612 .collect();
613 let mut trend = FdMatrix::zeros(n, m);
614 let mut seasonal = FdMatrix::zeros(n, m);
615 let mut remainder = FdMatrix::zeros(n, m);
616 let mut weights = FdMatrix::from_column_major(vec![1.0; n * m], n, m).unwrap();
617 for (i, (t, s, r, w)) in results.into_iter().enumerate() {
618 for j in 0..m {
619 trend[(i, j)] = t[j];
620 seasonal[(i, j)] = s[j];
621 remainder[(i, j)] = r[j];
622 weights[(i, j)] = w[j];
623 }
624 }
625 StlResult {
626 trend,
627 seasonal,
628 remainder,
629 weights,
630 period,
631 s_window: s_win,
632 t_window: t_win,
633 inner_iterations: n_inner,
634 outer_iterations: n_outer,
635 }
636}
637
638fn stl_single_series(
639 data: &[f64],
640 period: usize,
641 s_window: usize,
642 t_window: usize,
643 l_window: usize,
644 robust: bool,
645 n_inner: usize,
646 n_outer: usize,
647) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
648 let m = data.len();
649 let mut trend = vec![0.0; m];
650 let mut seasonal = vec![0.0; m];
651 let mut weights = vec![1.0; m];
652 for _outer in 0..n_outer {
653 for _inner in 0..n_inner {
654 let detrended: Vec<f64> = data
655 .iter()
656 .zip(trend.iter())
657 .map(|(&y, &t)| y - t)
658 .collect();
659 let cycle_smoothed = smooth_cycle_subseries(&detrended, period, s_window, &weights);
660 let low_pass = stl_lowpass_filter(&cycle_smoothed, period, l_window);
661 seasonal = cycle_smoothed
662 .iter()
663 .zip(low_pass.iter())
664 .map(|(&c, &l)| c - l)
665 .collect();
666 let deseasonalized: Vec<f64> = data
667 .iter()
668 .zip(seasonal.iter())
669 .map(|(&y, &s)| y - s)
670 .collect();
671 trend = weighted_loess(&deseasonalized, t_window, &weights);
672 }
673 if robust && _outer < n_outer - 1 {
674 let remainder: Vec<f64> = data
675 .iter()
676 .zip(trend.iter())
677 .zip(seasonal.iter())
678 .map(|((&y, &t), &s)| y - t - s)
679 .collect();
680 weights = compute_robustness_weights(&remainder);
681 }
682 }
683 let remainder: Vec<f64> = data
684 .iter()
685 .zip(trend.iter())
686 .zip(seasonal.iter())
687 .map(|((&y, &t), &s)| y - t - s)
688 .collect();
689 (trend, seasonal, remainder, weights)
690}
691
692fn smooth_cycle_subseries(
693 data: &[f64],
694 period: usize,
695 s_window: usize,
696 weights: &[f64],
697) -> Vec<f64> {
698 let m = data.len();
699 let n_cycles = m.div_ceil(period);
700 let mut result = vec![0.0; m];
701 for pos in 0..period {
702 let mut subseries_idx: Vec<usize> = Vec::new();
703 let mut subseries_vals: Vec<f64> = Vec::new();
704 let mut subseries_weights: Vec<f64> = Vec::new();
705 for cycle in 0..n_cycles {
706 let idx = cycle * period + pos;
707 if idx < m {
708 subseries_idx.push(idx);
709 subseries_vals.push(data[idx]);
710 subseries_weights.push(weights[idx]);
711 }
712 }
713 if subseries_vals.is_empty() {
714 continue;
715 }
716 let smoothed = weighted_loess(&subseries_vals, s_window, &subseries_weights);
717 for (i, &idx) in subseries_idx.iter().enumerate() {
718 result[idx] = smoothed[i];
719 }
720 }
721 result
722}
723
724fn stl_lowpass_filter(data: &[f64], period: usize, _l_window: usize) -> Vec<f64> {
725 let ma1 = moving_average(data, period);
726 let ma2 = moving_average(&ma1, period);
727 moving_average(&ma2, 3)
728}
729
730fn moving_average(data: &[f64], window: usize) -> Vec<f64> {
731 let m = data.len();
732 if m == 0 || window == 0 {
733 return data.to_vec();
734 }
735 let half = window / 2;
736 let mut result = vec![0.0; m];
737 for i in 0..m {
738 let start = i.saturating_sub(half);
739 let end = (i + half + 1).min(m);
740 let sum: f64 = data[start..end].iter().sum();
741 let count = (end - start) as f64;
742 result[i] = sum / count;
743 }
744 result
745}
746
747fn weighted_loess(data: &[f64], window: usize, weights: &[f64]) -> Vec<f64> {
748 let m = data.len();
749 if m == 0 {
750 return vec![];
751 }
752 let half = window / 2;
753 let mut result = vec![0.0; m];
754 for i in 0..m {
755 let start = i.saturating_sub(half);
756 let end = (i + half + 1).min(m);
757 let mut sum_w = 0.0;
758 let mut sum_wx = 0.0;
759 let mut sum_wy = 0.0;
760 let mut sum_wxx = 0.0;
761 let mut sum_wxy = 0.0;
762 for j in start..end {
763 let dist = (j as f64 - i as f64).abs() / (half.max(1) as f64);
764 let tricube = if dist < 1.0 {
765 (1.0 - dist.powi(3)).powi(3)
766 } else {
767 0.0
768 };
769 let w = tricube * weights[j];
770 let x = j as f64;
771 let y = data[j];
772 sum_w += w;
773 sum_wx += w * x;
774 sum_wy += w * y;
775 sum_wxx += w * x * x;
776 sum_wxy += w * x * y;
777 }
778 if sum_w > 1e-10 {
779 let denom = sum_w * sum_wxx - sum_wx * sum_wx;
780 if denom.abs() > 1e-10 {
781 let intercept = (sum_wxx * sum_wy - sum_wx * sum_wxy) / denom;
782 let slope = (sum_w * sum_wxy - sum_wx * sum_wy) / denom;
783 result[i] = intercept + slope * i as f64;
784 } else {
785 result[i] = sum_wy / sum_w;
786 }
787 } else {
788 result[i] = data[i];
789 }
790 }
791 result
792}
793
794fn compute_robustness_weights(residuals: &[f64]) -> Vec<f64> {
795 let m = residuals.len();
796 if m == 0 {
797 return vec![];
798 }
799 let mut abs_residuals: Vec<f64> = residuals.iter().map(|&r| r.abs()).collect();
800 abs_residuals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
801 let median_idx = m / 2;
802 let mad = if m % 2 == 0 {
803 (abs_residuals[median_idx - 1] + abs_residuals[median_idx]) / 2.0
804 } else {
805 abs_residuals[median_idx]
806 };
807 let h = 6.0 * mad.max(1e-10);
808 residuals
809 .iter()
810 .map(|&r| {
811 let u = r.abs() / h;
812 if u < 1.0 {
813 (1.0 - u * u).powi(2)
814 } else {
815 0.0
816 }
817 })
818 .collect()
819}
820
821pub fn stl_fdata(
823 data: &FdMatrix,
824 _argvals: &[f64],
825 period: usize,
826 s_window: Option<usize>,
827 t_window: Option<usize>,
828 robust: bool,
829) -> StlResult {
830 stl_decompose(data, period, s_window, t_window, None, robust, None, None)
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836 use std::f64::consts::PI;
837
838 #[test]
839 fn test_detrend_linear_removes_linear_trend() {
840 let m = 100;
841 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
842 let data_vec: Vec<f64> = argvals
843 .iter()
844 .map(|&t| 2.0 + 0.5 * t + (2.0 * PI * t / 2.0).sin())
845 .collect();
846 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
847 let result = detrend_linear(&data, &argvals);
848 let expected: Vec<f64> = argvals
849 .iter()
850 .map(|&t| (2.0 * PI * t / 2.0).sin())
851 .collect();
852 let mut max_diff = 0.0f64;
853 for j in 0..m {
854 let diff = (result.detrended[(0, j)] - expected[j]).abs();
855 max_diff = max_diff.max(diff);
856 }
857 assert!(max_diff < 0.2, "Max difference: {}", max_diff);
858 }
859
860 #[test]
861 fn test_detrend_polynomial_removes_quadratic_trend() {
862 let m = 100;
863 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
864 let data_vec: Vec<f64> = argvals
865 .iter()
866 .map(|&t| 1.0 + 0.5 * t - 0.1 * t * t + (2.0 * PI * t / 2.0).sin())
867 .collect();
868 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
869 let result = detrend_polynomial(&data, &argvals, 2);
870 let expected: Vec<f64> = argvals
871 .iter()
872 .map(|&t| (2.0 * PI * t / 2.0).sin())
873 .collect();
874 let detrended_vec: Vec<f64> = (0..m).map(|j| result.detrended[(0, j)]).collect();
875 let mean_det: f64 = detrended_vec.iter().sum::<f64>() / m as f64;
876 let mean_exp: f64 = expected.iter().sum::<f64>() / m as f64;
877 let mut num = 0.0;
878 let mut den_det = 0.0;
879 let mut den_exp = 0.0;
880 for j in 0..m {
881 num += (detrended_vec[j] - mean_det) * (expected[j] - mean_exp);
882 den_det += (detrended_vec[j] - mean_det).powi(2);
883 den_exp += (expected[j] - mean_exp).powi(2);
884 }
885 let corr = num / (den_det.sqrt() * den_exp.sqrt());
886 assert!(corr > 0.95, "Correlation: {}", corr);
887 }
888
889 #[test]
890 fn test_detrend_diff1() {
891 let m = 100;
892 let data_vec: Vec<f64> = {
893 let mut v = vec![0.0; m];
894 v[0] = 1.0;
895 for i in 1..m {
896 v[i] = v[i - 1] + 0.1 * (i as f64).sin();
897 }
898 v
899 };
900 let data = FdMatrix::from_column_major(data_vec.clone(), 1, m).unwrap();
901 let result = detrend_diff(&data, 1);
902 for j in 0..m - 1 {
903 let expected = data_vec[j + 1] - data_vec[j];
904 assert!(
905 (result.detrended[(0, j)] - expected).abs() < 1e-10,
906 "Mismatch at {}: {} vs {}",
907 j,
908 result.detrended[(0, j)],
909 expected
910 );
911 }
912 }
913
914 #[test]
915 fn test_auto_detrend_selects_linear_for_linear_data() {
916 let m = 100;
917 let argvals: Vec<f64> = (0..m).map(|i| i as f64).collect();
918 let data_vec: Vec<f64> = argvals.iter().map(|&t| 2.0 + 0.5 * t).collect();
919 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
920 let result = auto_detrend(&data, &argvals);
921 assert!(
922 result.method.contains("linear") || result.method.contains("polynomial"),
923 "Method: {}",
924 result.method
925 );
926 }
927
928 #[test]
929 fn test_detrend_loess_removes_linear_trend() {
930 let m = 100;
931 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
932 let data_vec: Vec<f64> = argvals
933 .iter()
934 .map(|&t| 2.0 + 0.5 * t + (2.0 * PI * t / 2.0).sin())
935 .collect();
936 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
937 let result = detrend_loess(&data, &argvals, 0.3, 1);
938 let expected: Vec<f64> = argvals
939 .iter()
940 .map(|&t| (2.0 * PI * t / 2.0).sin())
941 .collect();
942 let detrended_vec: Vec<f64> = (0..m).map(|j| result.detrended[(0, j)]).collect();
943 let mean_det: f64 = detrended_vec.iter().sum::<f64>() / m as f64;
944 let mean_exp: f64 = expected.iter().sum::<f64>() / m as f64;
945 let mut num = 0.0;
946 let mut den_det = 0.0;
947 let mut den_exp = 0.0;
948 for j in 0..m {
949 num += (detrended_vec[j] - mean_det) * (expected[j] - mean_exp);
950 den_det += (detrended_vec[j] - mean_det).powi(2);
951 den_exp += (expected[j] - mean_exp).powi(2);
952 }
953 let corr = num / (den_det.sqrt() * den_exp.sqrt());
954 assert!(corr > 0.9, "Correlation: {}", corr);
955 assert_eq!(result.method, "loess");
956 }
957
958 #[test]
959 fn test_detrend_loess_removes_quadratic_trend() {
960 let m = 100;
961 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
962 let data_vec: Vec<f64> = argvals
963 .iter()
964 .map(|&t| 1.0 + 0.3 * t - 0.05 * t * t + (2.0 * PI * t / 2.0).sin())
965 .collect();
966 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
967 let result = detrend_loess(&data, &argvals, 0.3, 2);
968 assert_eq!(result.trend.ncols(), m);
969 assert_eq!(result.detrended.ncols(), m);
970 assert!(result.rss[0] > 0.0);
971 }
972
973 #[test]
974 fn test_detrend_loess_different_bandwidths() {
975 let m = 100;
976 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
977 let data_vec: Vec<f64> = argvals
978 .iter()
979 .enumerate()
980 .map(|(i, &t)| (2.0 * PI * t / 2.0).sin() + 0.1 * ((i * 17) % 100) as f64 / 100.0)
981 .collect();
982 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
983 let result_small = detrend_loess(&data, &argvals, 0.1, 1);
984 let result_large = detrend_loess(&data, &argvals, 0.5, 1);
985 assert_eq!(result_small.trend.ncols(), m);
986 assert_eq!(result_large.trend.ncols(), m);
987 assert!(result_large.n_params >= result_small.n_params);
988 }
989
990 #[test]
991 fn test_detrend_loess_short_series() {
992 let m = 10;
993 let argvals: Vec<f64> = (0..m).map(|i| i as f64).collect();
994 let data_vec: Vec<f64> = argvals.iter().map(|&t| t * 2.0).collect();
995 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
996 let result = detrend_loess(&data, &argvals, 0.3, 1);
997 assert_eq!(result.trend.ncols(), m);
998 assert_eq!(result.detrended.ncols(), m);
999 }
1000
1001 #[test]
1002 fn test_decompose_additive_separates_components() {
1003 let m = 200;
1004 let period = 2.0;
1005 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1006 let data_vec: Vec<f64> = argvals
1007 .iter()
1008 .map(|&t| 2.0 + 0.5 * t + (2.0 * PI * t / period).sin())
1009 .collect();
1010 let data = FdMatrix::from_column_major(data_vec.clone(), 1, m).unwrap();
1011 let result = decompose_additive(&data, &argvals, period, "loess", 0.3, 3);
1012 assert_eq!(result.trend.ncols(), m);
1013 assert_eq!(result.seasonal.ncols(), m);
1014 assert_eq!(result.remainder.ncols(), m);
1015 assert_eq!(result.method, "additive");
1016 assert_eq!(result.period, period);
1017 for j in 0..m {
1018 let reconstructed =
1019 result.trend[(0, j)] + result.seasonal[(0, j)] + result.remainder[(0, j)];
1020 assert!(
1021 (reconstructed - data_vec[j]).abs() < 0.5,
1022 "Reconstruction error at {}: {} vs {}",
1023 j,
1024 reconstructed,
1025 data_vec[j]
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn test_decompose_additive_different_harmonics() {
1032 let m = 200;
1033 let period = 2.0;
1034 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1035 let data_vec: Vec<f64> = argvals
1036 .iter()
1037 .map(|&t| 1.0 + (2.0 * PI * t / period).sin())
1038 .collect();
1039 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1040 let result1 = decompose_additive(&data, &argvals, period, "loess", 0.3, 1);
1041 let result5 = decompose_additive(&data, &argvals, period, "loess", 0.3, 5);
1042 assert_eq!(result1.seasonal.ncols(), m);
1043 assert_eq!(result5.seasonal.ncols(), m);
1044 }
1045
1046 #[test]
1047 fn test_decompose_additive_residual_properties() {
1048 let m = 200;
1049 let period = 2.0;
1050 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1051 let data_vec: Vec<f64> = argvals
1052 .iter()
1053 .map(|&t| 2.0 + 0.3 * t + (2.0 * PI * t / period).sin())
1054 .collect();
1055 let data = FdMatrix::from_column_major(data_vec.clone(), 1, m).unwrap();
1056 let result = decompose_additive(&data, &argvals, period, "loess", 0.3, 3);
1057 let remainder_vec: Vec<f64> = (0..m).map(|j| result.remainder[(0, j)]).collect();
1058 let mean_rem: f64 = remainder_vec.iter().sum::<f64>() / m as f64;
1059 assert!(mean_rem.abs() < 0.5, "Remainder mean: {}", mean_rem);
1060 let data_mean: f64 = data_vec.iter().sum::<f64>() / m as f64;
1061 let var_data: f64 = data_vec
1062 .iter()
1063 .map(|&x| (x - data_mean).powi(2))
1064 .sum::<f64>()
1065 / m as f64;
1066 let var_rem: f64 = remainder_vec
1067 .iter()
1068 .map(|&x| (x - mean_rem).powi(2))
1069 .sum::<f64>()
1070 / m as f64;
1071 assert!(
1072 var_rem < var_data,
1073 "Remainder variance {} should be < data variance {}",
1074 var_rem,
1075 var_data
1076 );
1077 }
1078
1079 #[test]
1080 fn test_decompose_additive_multi_sample() {
1081 let n = 3;
1082 let m = 100;
1083 let period = 2.0;
1084 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1085 let mut data = FdMatrix::zeros(n, m);
1086 for i in 0..n {
1087 let amp = (i + 1) as f64;
1088 for j in 0..m {
1089 data[(i, j)] =
1090 1.0 + 0.1 * argvals[j] + amp * (2.0 * PI * argvals[j] / period).sin();
1091 }
1092 }
1093 let result = decompose_additive(&data, &argvals, period, "loess", 0.3, 2);
1094 assert_eq!(result.trend.shape(), (n, m));
1095 assert_eq!(result.seasonal.shape(), (n, m));
1096 assert_eq!(result.remainder.shape(), (n, m));
1097 }
1098
1099 #[test]
1100 fn test_decompose_multiplicative_basic() {
1101 let m = 200;
1102 let period = 2.0;
1103 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1104 let data_vec: Vec<f64> = argvals
1105 .iter()
1106 .map(|&t| (2.0 + 0.1 * t) * (1.0 + 0.3 * (2.0 * PI * t / period).sin()))
1107 .collect();
1108 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1109 let result = decompose_multiplicative(&data, &argvals, period, "loess", 0.3, 3);
1110 assert_eq!(result.trend.ncols(), m);
1111 assert_eq!(result.seasonal.ncols(), m);
1112 assert_eq!(result.remainder.ncols(), m);
1113 assert_eq!(result.method, "multiplicative");
1114 let seasonal_vec: Vec<f64> = (0..m).map(|j| result.seasonal[(0, j)]).collect();
1115 let mean_seasonal: f64 = seasonal_vec.iter().sum::<f64>() / m as f64;
1116 assert!(
1117 (mean_seasonal - 1.0).abs() < 0.5,
1118 "Mean seasonal factor: {}",
1119 mean_seasonal
1120 );
1121 }
1122
1123 #[test]
1124 fn test_decompose_multiplicative_non_positive_data() {
1125 let m = 100;
1126 let period = 2.0;
1127 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1128 let data_vec: Vec<f64> = argvals
1129 .iter()
1130 .map(|&t| -1.0 + (2.0 * PI * t / period).sin())
1131 .collect();
1132 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1133 let result = decompose_multiplicative(&data, &argvals, period, "loess", 0.3, 2);
1134 assert_eq!(result.trend.ncols(), m);
1135 assert_eq!(result.seasonal.ncols(), m);
1136 for j in 0..m {
1137 let s = result.seasonal[(0, j)];
1138 assert!(s.is_finite(), "Seasonal should be finite");
1139 }
1140 }
1141
1142 #[test]
1143 fn test_decompose_multiplicative_vs_additive() {
1144 let m = 200;
1145 let period = 2.0;
1146 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 10.0).collect();
1147 let data_vec: Vec<f64> = argvals
1148 .iter()
1149 .map(|&t| 5.0 + (2.0 * PI * t / period).sin())
1150 .collect();
1151 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1152 let add_result = decompose_additive(&data, &argvals, period, "loess", 0.3, 3);
1153 let mult_result = decompose_multiplicative(&data, &argvals, period, "loess", 0.3, 3);
1154 assert_eq!(add_result.seasonal.ncols(), m);
1155 assert_eq!(mult_result.seasonal.ncols(), m);
1156 let add_seasonal_vec: Vec<f64> = (0..m).map(|j| add_result.seasonal[(0, j)]).collect();
1157 let add_mean: f64 = add_seasonal_vec.iter().sum::<f64>() / m as f64;
1158 let mult_seasonal_vec: Vec<f64> = (0..m).map(|j| mult_result.seasonal[(0, j)]).collect();
1159 let mult_mean: f64 = mult_seasonal_vec.iter().sum::<f64>() / m as f64;
1160 assert!(
1161 add_mean.abs() < mult_mean,
1162 "Additive mean {} vs mult mean {}",
1163 add_mean,
1164 mult_mean
1165 );
1166 }
1167
1168 #[test]
1169 fn test_decompose_multiplicative_edge_cases() {
1170 let empty = FdMatrix::zeros(0, 0);
1171 let result = decompose_multiplicative(&empty, &[], 2.0, "loess", 0.3, 2);
1172 assert_eq!(result.trend.len(), 0);
1173 let m = 5;
1174 let argvals: Vec<f64> = (0..m).map(|i| i as f64).collect();
1175 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 2.0, 1.0], 1, m).unwrap();
1176 let result = decompose_multiplicative(&data, &argvals, 2.0, "loess", 0.3, 1);
1177 assert_eq!(result.remainder.ncols(), m);
1178 }
1179
1180 #[test]
1181 fn test_stl_decompose_basic() {
1182 let period = 12;
1183 let n_cycles = 10;
1184 let m = period * n_cycles;
1185 let data_vec: Vec<f64> = (0..m)
1186 .map(|i| {
1187 let t = i as f64;
1188 0.01 * t + (2.0 * PI * t / period as f64).sin()
1189 })
1190 .collect();
1191 let data = FdMatrix::from_column_major(data_vec.clone(), 1, m).unwrap();
1192 let result = stl_decompose(&data, period, None, None, None, false, None, None);
1193 assert_eq!(result.trend.ncols(), m);
1194 assert_eq!(result.seasonal.ncols(), m);
1195 assert_eq!(result.remainder.ncols(), m);
1196 assert_eq!(result.period, period);
1197 for j in 0..m {
1198 let reconstructed =
1199 result.trend[(0, j)] + result.seasonal[(0, j)] + result.remainder[(0, j)];
1200 assert!(
1201 (reconstructed - data_vec[j]).abs() < 1e-8,
1202 "Reconstruction error at {}: {} vs {}",
1203 j,
1204 reconstructed,
1205 data_vec[j]
1206 );
1207 }
1208 }
1209
1210 #[test]
1211 fn test_stl_decompose_robust() {
1212 let period = 12;
1213 let n_cycles = 10;
1214 let m = period * n_cycles;
1215 let mut data_vec: Vec<f64> = (0..m)
1216 .map(|i| {
1217 let t = i as f64;
1218 0.01 * t + (2.0 * PI * t / period as f64).sin()
1219 })
1220 .collect();
1221 data_vec[30] += 10.0;
1222 data_vec[60] += 10.0;
1223 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1224 let result = stl_decompose(&data, period, None, None, None, true, None, Some(5));
1225 assert!(
1226 result.weights[(0, 30)] < 1.0,
1227 "Weight at outlier should be < 1.0: {}",
1228 result.weights[(0, 30)]
1229 );
1230 assert!(
1231 result.weights[(0, 60)] < 1.0,
1232 "Weight at outlier should be < 1.0: {}",
1233 result.weights[(0, 60)]
1234 );
1235 let non_outlier_weight = result.weights[(0, 15)];
1236 assert!(
1237 non_outlier_weight > result.weights[(0, 30)],
1238 "Non-outlier weight {} should be > outlier weight {}",
1239 non_outlier_weight,
1240 result.weights[(0, 30)]
1241 );
1242 }
1243
1244 #[test]
1245 fn test_stl_decompose_default_params() {
1246 let period = 10;
1247 let m = period * 8;
1248 let data_vec: Vec<f64> = (0..m)
1249 .map(|i| (2.0 * PI * i as f64 / period as f64).sin())
1250 .collect();
1251 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1252 let result = stl_decompose(&data, period, None, None, None, false, None, None);
1253 assert_eq!(result.trend.ncols(), m);
1254 assert_eq!(result.seasonal.ncols(), m);
1255 assert!(result.s_window >= 3);
1256 assert!(result.t_window >= 3);
1257 assert_eq!(result.inner_iterations, 2);
1258 assert_eq!(result.outer_iterations, 1);
1259 }
1260
1261 #[test]
1262 fn test_stl_decompose_invalid() {
1263 let data = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
1264 let result = stl_decompose(&data, 1, None, None, None, false, None, None);
1265 assert_eq!(result.s_window, 0);
1266 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 1, 3).unwrap();
1267 let result = stl_decompose(&data, 5, None, None, None, false, None, None);
1268 assert_eq!(result.s_window, 0);
1269 let data = FdMatrix::zeros(0, 0);
1270 let result = stl_decompose(&data, 10, None, None, None, false, None, None);
1271 assert_eq!(result.trend.len(), 0);
1272 }
1273
1274 #[test]
1275 fn test_stl_fdata() {
1276 let n = 3;
1277 let period = 10;
1278 let m = period * 5;
1279 let argvals: Vec<f64> = (0..m).map(|i| i as f64).collect();
1280 let mut data = FdMatrix::zeros(n, m);
1281 for i in 0..n {
1282 let amp = (i + 1) as f64;
1283 for j in 0..m {
1284 data[(i, j)] = amp * (2.0 * PI * argvals[j] / period as f64).sin();
1285 }
1286 }
1287 let result = stl_fdata(&data, &argvals, period, None, None, false);
1288 assert_eq!(result.trend.shape(), (n, m));
1289 assert_eq!(result.seasonal.shape(), (n, m));
1290 assert_eq!(result.remainder.shape(), (n, m));
1291 for i in 0..n {
1292 for j in 0..m {
1293 let reconstructed =
1294 result.trend[(i, j)] + result.seasonal[(i, j)] + result.remainder[(i, j)];
1295 assert!(
1296 (reconstructed - data[(i, j)]).abs() < 1e-8,
1297 "Reconstruction error for sample {} at {}: {} vs {}",
1298 i,
1299 j,
1300 reconstructed,
1301 data[(i, j)]
1302 );
1303 }
1304 }
1305 }
1306
1307 #[test]
1308 fn test_stl_decompose_multi_sample() {
1309 let n = 5;
1310 let period = 10;
1311 let m = period * 6;
1312 let mut data = FdMatrix::zeros(n, m);
1313 for i in 0..n {
1314 let offset = i as f64 * 0.5;
1315 for j in 0..m {
1316 data[(i, j)] =
1317 offset + 0.01 * j as f64 + (2.0 * PI * j as f64 / period as f64).sin();
1318 }
1319 }
1320 let result = stl_decompose(&data, period, None, None, None, false, None, None);
1321 assert_eq!(result.trend.shape(), (n, m));
1322 assert_eq!(result.seasonal.shape(), (n, m));
1323 assert_eq!(result.remainder.shape(), (n, m));
1324 assert_eq!(result.weights.shape(), (n, m));
1325 }
1326
1327 #[test]
1328 fn test_detrend_diff_order2() {
1329 let m = 50;
1330 let data_vec: Vec<f64> = (0..m).map(|i| (i as f64).powi(2)).collect();
1331 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1332 let result = detrend_diff(&data, 2);
1333 for j in 0..m - 2 {
1334 assert!(
1335 (result.detrended[(0, j)] - 2.0).abs() < 1e-10,
1336 "Second diff at {}: expected 2.0, got {}",
1337 j,
1338 result.detrended[(0, j)]
1339 );
1340 }
1341 }
1342
1343 #[test]
1344 fn test_detrend_polynomial_degree3() {
1345 let m = 100;
1346 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64 * 5.0).collect();
1347 let data_vec: Vec<f64> = argvals
1348 .iter()
1349 .map(|&t| 1.0 + 2.0 * t - 0.5 * t * t + 0.1 * t * t * t)
1350 .collect();
1351 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1352 let result = detrend_polynomial(&data, &argvals, 3);
1353 assert_eq!(result.method, "polynomial(3)");
1354 assert!(result.coefficients.is_some());
1355 let max_detrend: f64 = (0..m)
1356 .map(|j| result.detrended[(0, j)].abs())
1357 .fold(0.0, f64::max);
1358 assert!(
1359 max_detrend < 0.1,
1360 "Pure cubic should be nearly zero after degree-3 detrend: {}",
1361 max_detrend
1362 );
1363 }
1364
1365 #[test]
1366 fn test_detrend_loess_invalid() {
1367 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0, 5.0], 1, 5).unwrap();
1368 let argvals = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1369 let result = detrend_loess(&data, &argvals, -0.1, 1);
1370 assert_eq!(result.detrended.as_slice(), data.as_slice());
1371 let data2 = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
1372 let result = detrend_loess(&data2, &[0.0, 1.0], 0.3, 1);
1373 assert_eq!(result.detrended.as_slice(), &[1.0, 2.0]);
1374 }
1375
1376 #[test]
1377 fn test_nan_linear_detrend() {
1378 let m = 20;
1379 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1380 let mut data_vec = vec![1.0; m];
1381 data_vec[5] = f64::NAN;
1382 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1383 let result = detrend_linear(&data, &argvals);
1384 assert_eq!(result.detrended.nrows(), 1);
1386 }
1387
1388 #[test]
1389 fn test_n1_detrend() {
1390 let m = 50;
1391 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1392 let data_vec: Vec<f64> = argvals.iter().map(|&t| 2.0 * t + 1.0).collect();
1393 let data = FdMatrix::from_column_major(data_vec, 1, m).unwrap();
1394 let result = detrend_linear(&data, &argvals);
1395 assert_eq!(result.detrended.ncols(), m);
1396 for j in 0..m {
1398 assert!(result.detrended[(0, j)].abs() < 0.1);
1399 }
1400 }
1401
1402 #[test]
1403 fn test_constant_signal_detrend() {
1404 let m = 30;
1405 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1406 let data = FdMatrix::from_column_major(vec![5.0; m], 1, m).unwrap();
1407 let result = detrend_linear(&data, &argvals);
1408 assert_eq!(result.detrended.ncols(), m);
1410 }
1411
1412 #[test]
1413 fn test_m2_minimal_detrend() {
1414 let argvals = vec![0.0, 1.0];
1416 let data = FdMatrix::from_column_major(vec![0.0, 1.0], 1, 2).unwrap();
1417 let result = detrend_linear(&data, &argvals);
1418 assert_eq!(result.detrended.ncols(), 2);
1419 }
1420}