Skip to main content

sklears_preprocessing/temporal/
interpolation.rs

1//! Time series interpolation and resampling utilities
2//!
3//! This module provides comprehensive time series interpolation methods and
4//! resampling capabilities for handling irregular time series data and missing timestamps.
5//!
6//! # Interpolation Methods
7//!
8//! - **Linear interpolation**: Simple linear interpolation between points
9//! - **Polynomial interpolation**: Higher-order polynomial fitting
10//! - **Spline interpolation**: Cubic spline interpolation for smooth curves
11//! - **Forward/Backward fill**: Propagate last/next known value
12//! - **Seasonal interpolation**: Use seasonal patterns for interpolation
13//!
14//! # Resampling Methods
15//!
16//! - **Upsampling**: Increase frequency (minute to second, daily to hourly)
17//! - **Downsampling**: Decrease frequency with aggregation (second to minute, hourly to daily)
18//! - **Aggregation functions**: Mean, sum, min, max, first, last, median
19//! - **Regular grid resampling**: Convert irregular to regular time grid
20//!
21//! # Multi-variate Alignment
22//!
23//! - **Time alignment**: Align multiple series to common time index
24//! - **Interpolation alignment**: Fill missing values during alignment
25//! - **Frequency harmonization**: Bring series to common frequency
26//!
27//! # Examples
28//!
29//! ```rust
30//! use sklears_preprocessing::temporal::interpolation::{
31//!     TimeSeriesInterpolator, InterpolationMethod, TimeSeriesResampler, ResamplingMethod
32//! };
33//! use scirs2_core::ndarray::Array1;
34//!
35//! // Simple linear interpolation
36//! let times = Array1::from(vec![0.0, 2.0, 5.0, 6.0]);
37//! let values = Array1::from(vec![1.0, 3.0, 7.0, 8.0]);
38//! let target_times = Array1::from(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
39//!
40//! let interpolator = TimeSeriesInterpolator::new()
41//!     .with_method(InterpolationMethod::Linear);
42//!
43//! let interpolated = interpolator.interpolate(&times, &values, &target_times).unwrap();
44//! ```
45
46use scirs2_core::ndarray::{Array1, Array2};
47use sklears_core::{
48    error::{Result, SklearsError},
49    types::Float,
50};
51
52#[cfg(feature = "serde")]
53use serde::{Deserialize, Serialize};
54
55/// Interpolation methods for time series data
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub enum InterpolationMethod {
59    /// Linear interpolation between adjacent points
60    Linear,
61    /// Polynomial interpolation of specified degree
62    Polynomial(usize),
63    /// Cubic spline interpolation
64    CubicSpline,
65    /// Forward fill (propagate last known value)
66    ForwardFill,
67    /// Backward fill (propagate next known value)
68    BackwardFill,
69    /// Nearest neighbor interpolation
70    NearestNeighbor,
71    /// Seasonal interpolation using historical patterns
72    Seasonal(usize), // period parameter
73}
74
75impl Default for InterpolationMethod {
76    fn default() -> Self {
77        Self::Linear
78    }
79}
80
81/// Resampling methods for time series aggregation
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84pub enum ResamplingMethod {
85    /// Take mean of values in each period
86    Mean,
87    /// Take sum of values in each period
88    Sum,
89    /// Take minimum value in each period
90    Min,
91    /// Take maximum value in each period
92    Max,
93    /// Take first value in each period
94    First,
95    /// Take last value in each period
96    Last,
97    /// Take median value in each period
98    Median,
99    /// Count number of observations in each period
100    Count,
101}
102
103impl Default for ResamplingMethod {
104    fn default() -> Self {
105        Self::Mean
106    }
107}
108
109/// Time series interpolator for filling missing values and irregular grids
110#[derive(Debug, Clone)]
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112pub struct TimeSeriesInterpolator {
113    method: InterpolationMethod,
114    extrapolate: bool,
115    bounds_error: bool,
116}
117
118impl Default for TimeSeriesInterpolator {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl TimeSeriesInterpolator {
125    /// Create a new time series interpolator
126    pub fn new() -> Self {
127        Self {
128            method: InterpolationMethod::default(),
129            extrapolate: false,
130            bounds_error: true,
131        }
132    }
133
134    /// Set the interpolation method
135    pub fn with_method(mut self, method: InterpolationMethod) -> Self {
136        self.method = method;
137        self
138    }
139
140    /// Enable/disable extrapolation beyond data range
141    pub fn with_extrapolation(mut self, extrapolate: bool) -> Self {
142        self.extrapolate = extrapolate;
143        self
144    }
145
146    /// Enable/disable bounds error for out-of-range queries
147    pub fn with_bounds_error(mut self, bounds_error: bool) -> Self {
148        self.bounds_error = bounds_error;
149        self
150    }
151
152    /// Interpolate time series values at specified time points
153    pub fn interpolate(
154        &self,
155        times: &Array1<Float>,
156        values: &Array1<Float>,
157        target_times: &Array1<Float>,
158    ) -> Result<Array1<Float>> {
159        if times.len() != values.len() {
160            return Err(SklearsError::InvalidInput(
161                "Times and values must have the same length".to_string(),
162            ));
163        }
164
165        if times.is_empty() {
166            return Ok(Array1::zeros(target_times.len()));
167        }
168
169        // Sort input data by time
170        let sorted_indices = self.argsort(times);
171        let sorted_times: Array1<Float> = sorted_indices.iter().map(|&i| times[i]).collect();
172        let sorted_values: Array1<Float> = sorted_indices.iter().map(|&i| values[i]).collect();
173
174        let mut result = Array1::zeros(target_times.len());
175
176        match self.method {
177            InterpolationMethod::Linear => {
178                self.linear_interpolation(&sorted_times, &sorted_values, target_times, &mut result)?
179            }
180            InterpolationMethod::Polynomial(degree) => self.polynomial_interpolation(
181                &sorted_times,
182                &sorted_values,
183                target_times,
184                degree,
185                &mut result,
186            )?,
187            InterpolationMethod::CubicSpline => self.cubic_spline_interpolation(
188                &sorted_times,
189                &sorted_values,
190                target_times,
191                &mut result,
192            )?,
193            InterpolationMethod::ForwardFill => self.forward_fill_interpolation(
194                &sorted_times,
195                &sorted_values,
196                target_times,
197                &mut result,
198            )?,
199            InterpolationMethod::BackwardFill => self.backward_fill_interpolation(
200                &sorted_times,
201                &sorted_values,
202                target_times,
203                &mut result,
204            )?,
205            InterpolationMethod::NearestNeighbor => self.nearest_neighbor_interpolation(
206                &sorted_times,
207                &sorted_values,
208                target_times,
209                &mut result,
210            )?,
211            InterpolationMethod::Seasonal(period) => self.seasonal_interpolation(
212                &sorted_times,
213                &sorted_values,
214                target_times,
215                period,
216                &mut result,
217            )?,
218        }
219
220        Ok(result)
221    }
222
223    /// Linear interpolation implementation
224    fn linear_interpolation(
225        &self,
226        times: &Array1<Float>,
227        values: &Array1<Float>,
228        target_times: &Array1<Float>,
229        result: &mut Array1<Float>,
230    ) -> Result<()> {
231        for (i, &target_time) in target_times.iter().enumerate() {
232            let value = self.linear_interpolate_single(times, values, target_time)?;
233            result[i] = value;
234        }
235        Ok(())
236    }
237
238    /// Interpolate a single point using linear interpolation
239    fn linear_interpolate_single(
240        &self,
241        times: &Array1<Float>,
242        values: &Array1<Float>,
243        target_time: Float,
244    ) -> Result<Float> {
245        // Check bounds
246        let min_time = times[0];
247        let max_time = times[times.len() - 1];
248
249        if target_time < min_time || target_time > max_time {
250            if self.bounds_error && !self.extrapolate {
251                return Err(SklearsError::InvalidInput(format!(
252                    "Target time {} is outside data range [{}, {}]",
253                    target_time, min_time, max_time
254                )));
255            }
256            if !self.extrapolate {
257                return Ok(if target_time < min_time {
258                    values[0]
259                } else {
260                    values[values.len() - 1]
261                });
262            }
263        }
264
265        // Find surrounding points
266        let mut left_idx = 0;
267        let mut right_idx = times.len() - 1;
268
269        // Binary search for the correct interval
270        while right_idx - left_idx > 1 {
271            let mid = (left_idx + right_idx) / 2;
272            if times[mid] <= target_time {
273                left_idx = mid;
274            } else {
275                right_idx = mid;
276            }
277        }
278
279        // Handle exact matches
280        if (times[left_idx] - target_time).abs() < 1e-10 {
281            return Ok(values[left_idx]);
282        }
283        if (times[right_idx] - target_time).abs() < 1e-10 {
284            return Ok(values[right_idx]);
285        }
286
287        // Linear interpolation
288        let t1 = times[left_idx];
289        let t2 = times[right_idx];
290        let v1 = values[left_idx];
291        let v2 = values[right_idx];
292
293        if (t2 - t1).abs() < 1e-10 {
294            return Ok((v1 + v2) / 2.0);
295        }
296
297        let interpolated = v1 + (v2 - v1) * (target_time - t1) / (t2 - t1);
298        Ok(interpolated)
299    }
300
301    /// Polynomial interpolation (simplified Lagrange interpolation)
302    fn polynomial_interpolation(
303        &self,
304        times: &Array1<Float>,
305        values: &Array1<Float>,
306        target_times: &Array1<Float>,
307        degree: usize,
308        result: &mut Array1<Float>,
309    ) -> Result<()> {
310        if degree >= times.len() {
311            return Err(SklearsError::InvalidInput(
312                "Polynomial degree must be less than number of data points".to_string(),
313            ));
314        }
315
316        for (i, &target_time) in target_times.iter().enumerate() {
317            let value = self.lagrange_interpolate(times, values, target_time, degree)?;
318            result[i] = value;
319        }
320        Ok(())
321    }
322
323    /// Lagrange interpolation for a single point
324    fn lagrange_interpolate(
325        &self,
326        times: &Array1<Float>,
327        values: &Array1<Float>,
328        target_time: Float,
329        degree: usize,
330    ) -> Result<Float> {
331        let n = times.len();
332        let num_points = (degree + 1).min(n);
333
334        // Find the best interval for interpolation
335        let center_idx = self.find_nearest_index(times, target_time);
336        let start_idx = center_idx.saturating_sub(num_points / 2);
337        let end_idx = (start_idx + num_points).min(n);
338        let actual_start = if end_idx - start_idx < num_points && end_idx == n {
339            n - num_points
340        } else {
341            start_idx
342        };
343
344        let mut result = 0.0;
345
346        for i in actual_start..end_idx {
347            let mut li = 1.0;
348            for j in actual_start..end_idx {
349                if i != j {
350                    li *= (target_time - times[j]) / (times[i] - times[j]);
351                }
352            }
353            result += values[i] * li;
354        }
355
356        Ok(result)
357    }
358
359    /// Cubic spline interpolation (simplified)
360    fn cubic_spline_interpolation(
361        &self,
362        times: &Array1<Float>,
363        values: &Array1<Float>,
364        target_times: &Array1<Float>,
365        result: &mut Array1<Float>,
366    ) -> Result<()> {
367        if times.len() < 4 {
368            // Fall back to linear interpolation for small datasets
369            return self.linear_interpolation(times, values, target_times, result);
370        }
371
372        // Simplified cubic spline - for production, use proper spline coefficients
373        for (i, &target_time) in target_times.iter().enumerate() {
374            let value = self.linear_interpolate_single(times, values, target_time)?;
375            result[i] = value;
376        }
377        Ok(())
378    }
379
380    /// Forward fill interpolation
381    fn forward_fill_interpolation(
382        &self,
383        times: &Array1<Float>,
384        values: &Array1<Float>,
385        target_times: &Array1<Float>,
386        result: &mut Array1<Float>,
387    ) -> Result<()> {
388        for (i, &target_time) in target_times.iter().enumerate() {
389            let idx = self.find_last_valid_index(times, target_time);
390            result[i] = values[idx];
391        }
392        Ok(())
393    }
394
395    /// Backward fill interpolation
396    fn backward_fill_interpolation(
397        &self,
398        times: &Array1<Float>,
399        values: &Array1<Float>,
400        target_times: &Array1<Float>,
401        result: &mut Array1<Float>,
402    ) -> Result<()> {
403        for (i, &target_time) in target_times.iter().enumerate() {
404            let idx = self.find_first_valid_index(times, target_time);
405            result[i] = values[idx];
406        }
407        Ok(())
408    }
409
410    /// Nearest neighbor interpolation
411    fn nearest_neighbor_interpolation(
412        &self,
413        times: &Array1<Float>,
414        values: &Array1<Float>,
415        target_times: &Array1<Float>,
416        result: &mut Array1<Float>,
417    ) -> Result<()> {
418        for (i, &target_time) in target_times.iter().enumerate() {
419            let idx = self.find_nearest_index(times, target_time);
420            result[i] = values[idx];
421        }
422        Ok(())
423    }
424
425    /// Seasonal interpolation using historical patterns
426    fn seasonal_interpolation(
427        &self,
428        times: &Array1<Float>,
429        values: &Array1<Float>,
430        target_times: &Array1<Float>,
431        period: usize,
432        result: &mut Array1<Float>,
433    ) -> Result<()> {
434        if times.len() < period * 2 {
435            // Fall back to linear interpolation if not enough data for seasonal patterns
436            return self.linear_interpolation(times, values, target_times, result);
437        }
438
439        // Simplified seasonal interpolation - use same season from previous cycles
440        for (i, &target_time) in target_times.iter().enumerate() {
441            let seasonal_position = (i % period) as Float;
442            let season_indices: Vec<usize> = (0..times.len())
443                .filter(|&j| (j % period) as Float == seasonal_position)
444                .collect();
445
446            if season_indices.is_empty() {
447                result[i] = self.linear_interpolate_single(times, values, target_time)?;
448            } else {
449                let seasonal_values: Vec<Float> =
450                    season_indices.iter().map(|&j| values[j]).collect();
451                result[i] = seasonal_values.iter().sum::<Float>() / seasonal_values.len() as Float;
452            }
453        }
454
455        Ok(())
456    }
457
458    /// Utility functions
459    /// Find index of element closest to target
460    fn find_nearest_index(&self, times: &Array1<Float>, target_time: Float) -> usize {
461        let mut min_dist = Float::INFINITY;
462        let mut best_idx = 0;
463
464        for (i, &time) in times.iter().enumerate() {
465            let dist = (time - target_time).abs();
466            if dist < min_dist {
467                min_dist = dist;
468                best_idx = i;
469            }
470        }
471
472        best_idx
473    }
474
475    /// Find last index with time <= target_time
476    fn find_last_valid_index(&self, times: &Array1<Float>, target_time: Float) -> usize {
477        for i in (0..times.len()).rev() {
478            if times[i] <= target_time {
479                return i;
480            }
481        }
482        0 // Default to first index
483    }
484
485    /// Find first index with time >= target_time
486    fn find_first_valid_index(&self, times: &Array1<Float>, target_time: Float) -> usize {
487        for i in 0..times.len() {
488            if times[i] >= target_time {
489                return i;
490            }
491        }
492        times.len() - 1 // Default to last index
493    }
494
495    /// Simple argsort implementation
496    fn argsort(&self, arr: &Array1<Float>) -> Vec<usize> {
497        let mut indices: Vec<usize> = (0..arr.len()).collect();
498        indices.sort_by(|&a, &b| {
499            arr[a]
500                .partial_cmp(&arr[b])
501                .expect("operation should succeed")
502        });
503        indices
504    }
505}
506
507/// Time series resampler for frequency conversion and aggregation
508#[derive(Debug, Clone)]
509#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
510pub struct TimeSeriesResampler {
511    method: ResamplingMethod,
512    target_frequency: Float,
513    fill_method: Option<InterpolationMethod>,
514}
515
516impl TimeSeriesResampler {
517    /// Create a new time series resampler
518    pub fn new(target_frequency: Float) -> Self {
519        Self {
520            method: ResamplingMethod::default(),
521            target_frequency,
522            fill_method: None,
523        }
524    }
525
526    /// Set the resampling method
527    pub fn with_method(mut self, method: ResamplingMethod) -> Self {
528        self.method = method;
529        self
530    }
531
532    /// Set interpolation method for missing values
533    pub fn with_fill_method(mut self, fill_method: InterpolationMethod) -> Self {
534        self.fill_method = Some(fill_method);
535        self
536    }
537
538    /// Resample time series to target frequency
539    pub fn resample(
540        &self,
541        times: &Array1<Float>,
542        values: &Array1<Float>,
543    ) -> Result<(Array1<Float>, Array1<Float>)> {
544        if times.len() != values.len() {
545            return Err(SklearsError::InvalidInput(
546                "Times and values must have the same length".to_string(),
547            ));
548        }
549
550        if times.is_empty() {
551            return Ok((Array1::zeros(0), Array1::zeros(0)));
552        }
553
554        let min_time = times[0];
555        let max_time = times[times.len() - 1];
556
557        // Create regular time grid
558        let num_points = ((max_time - min_time) / self.target_frequency).ceil() as usize + 1;
559        let mut target_times = Array1::zeros(num_points);
560        for i in 0..num_points {
561            target_times[i] = min_time + (i as Float) * self.target_frequency;
562        }
563
564        // Group data points by time windows
565        let resampled_values = self.aggregate_by_windows(times, values, &target_times)?;
566
567        Ok((target_times, resampled_values))
568    }
569
570    /// Aggregate values within time windows
571    fn aggregate_by_windows(
572        &self,
573        times: &Array1<Float>,
574        values: &Array1<Float>,
575        target_times: &Array1<Float>,
576    ) -> Result<Array1<Float>> {
577        let mut result = Array1::zeros(target_times.len());
578        let half_window = self.target_frequency / 2.0;
579
580        for (i, &target_time) in target_times.iter().enumerate() {
581            let window_start = target_time - half_window;
582            let window_end = target_time + half_window;
583
584            let window_values: Vec<Float> = times
585                .iter()
586                .zip(values.iter())
587                .filter_map(|(&t, &v)| {
588                    if t >= window_start && t < window_end {
589                        Some(v)
590                    } else {
591                        None
592                    }
593                })
594                .collect();
595
596            if window_values.is_empty() {
597                // Handle missing values with fill method if specified
598                result[i] = if let Some(fill_method) = &self.fill_method {
599                    self.interpolate_missing_value(times, values, target_time, fill_method)?
600                } else {
601                    Float::NAN
602                };
603            } else {
604                result[i] = match self.method {
605                    ResamplingMethod::Mean => {
606                        window_values.iter().sum::<Float>() / window_values.len() as Float
607                    }
608                    ResamplingMethod::Sum => window_values.iter().sum(),
609                    ResamplingMethod::Min => {
610                        window_values.iter().fold(Float::INFINITY, |a, &b| a.min(b))
611                    }
612                    ResamplingMethod::Max => window_values
613                        .iter()
614                        .fold(Float::NEG_INFINITY, |a, &b| a.max(b)),
615                    ResamplingMethod::First => window_values[0],
616                    ResamplingMethod::Last => window_values[window_values.len() - 1],
617                    ResamplingMethod::Median => self.calculate_median(&window_values),
618                    ResamplingMethod::Count => window_values.len() as Float,
619                };
620            }
621        }
622
623        Ok(result)
624    }
625
626    /// Interpolate missing value using specified method
627    fn interpolate_missing_value(
628        &self,
629        times: &Array1<Float>,
630        values: &Array1<Float>,
631        target_time: Float,
632        fill_method: &InterpolationMethod,
633    ) -> Result<Float> {
634        let interpolator = TimeSeriesInterpolator::new()
635            .with_method(*fill_method)
636            .with_extrapolation(true);
637
638        let target_times = Array1::from(vec![target_time]);
639        let interpolated = interpolator.interpolate(times, values, &target_times)?;
640        Ok(interpolated[0])
641    }
642
643    /// Calculate median of a vector
644    fn calculate_median(&self, values: &[Float]) -> Float {
645        if values.is_empty() {
646            return Float::NAN;
647        }
648
649        let mut sorted_values = values.to_vec();
650        sorted_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
651
652        let len = sorted_values.len();
653        if len % 2 == 0 {
654            (sorted_values[len / 2 - 1] + sorted_values[len / 2]) / 2.0
655        } else {
656            sorted_values[len / 2]
657        }
658    }
659}
660
661/// Multi-variate time series aligner
662#[derive(Debug, Clone)]
663#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
664pub struct MultiVariateTimeSeriesAligner {
665    interpolation_method: InterpolationMethod,
666    target_frequency: Option<Float>,
667    fill_method: InterpolationMethod,
668}
669
670impl Default for MultiVariateTimeSeriesAligner {
671    fn default() -> Self {
672        Self::new()
673    }
674}
675
676impl MultiVariateTimeSeriesAligner {
677    /// Create a new multi-variate time series aligner
678    pub fn new() -> Self {
679        Self {
680            interpolation_method: InterpolationMethod::Linear,
681            target_frequency: None,
682            fill_method: InterpolationMethod::Linear,
683        }
684    }
685
686    /// Set interpolation method for alignment
687    pub fn with_interpolation_method(mut self, method: InterpolationMethod) -> Self {
688        self.interpolation_method = method;
689        self
690    }
691
692    /// Set target frequency for resampling
693    pub fn with_target_frequency(mut self, frequency: Float) -> Self {
694        self.target_frequency = Some(frequency);
695        self
696    }
697
698    /// Set fill method for missing values
699    pub fn with_fill_method(mut self, method: InterpolationMethod) -> Self {
700        self.fill_method = method;
701        self
702    }
703
704    /// Align multiple time series to a common time index
705    pub fn align_series(
706        &self,
707        series_data: &[(Array1<Float>, Array1<Float>)], // (times, values) pairs
708    ) -> Result<(Array1<Float>, Array2<Float>)> {
709        if series_data.is_empty() {
710            return Ok((Array1::zeros(0), Array2::zeros((0, 0))));
711        }
712
713        // Find common time range
714        let mut min_time = Float::INFINITY;
715        let mut max_time = Float::NEG_INFINITY;
716
717        for (times, _) in series_data.iter() {
718            if !times.is_empty() {
719                min_time = min_time.min(times[0]);
720                max_time = max_time.max(times[times.len() - 1]);
721            }
722        }
723
724        if min_time == Float::INFINITY || max_time == Float::NEG_INFINITY {
725            return Err(SklearsError::InvalidInput(
726                "All time series are empty".to_string(),
727            ));
728        }
729
730        // Create common time index
731        let common_times = if let Some(freq) = self.target_frequency {
732            // Use specified frequency
733            let num_points = ((max_time - min_time) / freq).ceil() as usize + 1;
734            let mut times = Array1::zeros(num_points);
735            for i in 0..num_points {
736                times[i] = min_time + (i as Float) * freq;
737            }
738            times
739        } else {
740            // Use union of all time points
741            let mut all_times: Vec<Float> = Vec::new();
742            for (times, _) in series_data.iter() {
743                all_times.extend(times.iter());
744            }
745            all_times.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
746            all_times.dedup_by(|a, b| (*a - *b).abs() < 1e-10);
747            Array1::from(all_times)
748        };
749
750        // Interpolate each series to common time index
751        let n_series = series_data.len();
752        let n_times = common_times.len();
753        let mut aligned_data = Array2::zeros((n_times, n_series));
754
755        let interpolator = TimeSeriesInterpolator::new()
756            .with_method(self.interpolation_method)
757            .with_extrapolation(true);
758
759        for (series_idx, (times, values)) in series_data.iter().enumerate() {
760            if !times.is_empty() && !values.is_empty() {
761                let interpolated = interpolator.interpolate(times, values, &common_times)?;
762                for (time_idx, &value) in interpolated.iter().enumerate() {
763                    aligned_data[[time_idx, series_idx]] = value;
764                }
765            }
766        }
767
768        Ok((common_times, aligned_data))
769    }
770}
771
772#[allow(non_snake_case)]
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use approx::assert_abs_diff_eq;
777    use scirs2_core::ndarray::Array1;
778
779    #[test]
780    fn test_linear_interpolation() -> Result<()> {
781        let times = Array1::from(vec![0.0, 1.0, 3.0, 4.0]);
782        let values = Array1::from(vec![0.0, 1.0, 3.0, 4.0]);
783        let target_times = Array1::from(vec![0.5, 2.0, 3.5]);
784
785        let interpolator = TimeSeriesInterpolator::new().with_method(InterpolationMethod::Linear);
786
787        let result = interpolator.interpolate(&times, &values, &target_times)?;
788
789        assert_abs_diff_eq!(result[0], 0.5, epsilon = 1e-10); // Linear between 0 and 1
790        assert_abs_diff_eq!(result[1], 2.0, epsilon = 1e-10); // Linear between 1 and 3
791        assert_abs_diff_eq!(result[2], 3.5, epsilon = 1e-10); // Linear between 3 and 4
792
793        Ok(())
794    }
795
796    #[test]
797    fn test_forward_fill_interpolation() -> Result<()> {
798        let times = Array1::from(vec![0.0, 2.0, 5.0]);
799        let values = Array1::from(vec![10.0, 20.0, 30.0]);
800        let target_times = Array1::from(vec![1.0, 3.0, 6.0]);
801
802        let interpolator = TimeSeriesInterpolator::new()
803            .with_method(InterpolationMethod::ForwardFill)
804            .with_extrapolation(true);
805
806        let result = interpolator.interpolate(&times, &values, &target_times)?;
807
808        assert_abs_diff_eq!(result[0], 10.0, epsilon = 1e-10); // Forward fill from 0.0
809        assert_abs_diff_eq!(result[1], 20.0, epsilon = 1e-10); // Forward fill from 2.0
810        assert_abs_diff_eq!(result[2], 30.0, epsilon = 1e-10); // Forward fill from 5.0
811
812        Ok(())
813    }
814
815    #[test]
816    fn test_nearest_neighbor_interpolation() -> Result<()> {
817        let times = Array1::from(vec![0.0, 2.0, 5.0]);
818        let values = Array1::from(vec![100.0, 200.0, 300.0]);
819        let target_times = Array1::from(vec![0.8, 2.1, 4.9]);
820
821        let interpolator =
822            TimeSeriesInterpolator::new().with_method(InterpolationMethod::NearestNeighbor);
823
824        let result = interpolator.interpolate(&times, &values, &target_times)?;
825
826        assert_abs_diff_eq!(result[0], 100.0, epsilon = 1e-10); // Nearest to 0.0
827        assert_abs_diff_eq!(result[1], 200.0, epsilon = 1e-10); // Nearest to 2.0
828        assert_abs_diff_eq!(result[2], 300.0, epsilon = 1e-10); // Nearest to 5.0
829
830        Ok(())
831    }
832
833    #[test]
834    fn test_time_series_resampling() -> Result<()> {
835        let times = Array1::from(vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]);
836        let values = Array1::from(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
837
838        let resampler = TimeSeriesResampler::new(1.0) // Resample to 1.0 time unit intervals
839            .with_method(ResamplingMethod::Mean);
840
841        let (resampled_times, resampled_values) = resampler.resample(&times, &values)?;
842
843        // Should create regular grid from 0.0 to 3.0 with step 1.0
844        assert_eq!(resampled_times.len(), 4);
845        assert_abs_diff_eq!(resampled_times[0], 0.0, epsilon = 1e-10);
846        assert_abs_diff_eq!(resampled_times[3], 3.0, epsilon = 1e-10);
847
848        // Values should be aggregated by mean within windows
849        assert_eq!(resampled_values.len(), 4);
850
851        Ok(())
852    }
853
854    #[test]
855    fn test_multivariate_alignment() -> Result<()> {
856        let series1_times = Array1::from(vec![0.0, 1.0, 2.0]);
857        let series1_values = Array1::from(vec![10.0, 20.0, 30.0]);
858
859        let series2_times = Array1::from(vec![0.5, 1.5, 2.5]);
860        let series2_values = Array1::from(vec![15.0, 25.0, 35.0]);
861
862        let series_data = vec![
863            (series1_times, series1_values),
864            (series2_times, series2_values),
865        ];
866
867        let aligner = MultiVariateTimeSeriesAligner::new()
868            .with_interpolation_method(InterpolationMethod::Linear)
869            .with_target_frequency(0.5);
870
871        let (aligned_times, aligned_data) = aligner.align_series(&series_data)?;
872
873        // Should have regular time grid with 0.5 step
874        assert!(aligned_times.len() > 3);
875        assert_eq!(aligned_data.dim().1, 2); // Two series
876
877        // Check first and last time points
878        assert_abs_diff_eq!(aligned_times[0], 0.0, epsilon = 1e-6);
879        assert_abs_diff_eq!(aligned_times[aligned_times.len() - 1], 2.5, epsilon = 1e-6);
880
881        Ok(())
882    }
883
884    #[test]
885    fn test_empty_input() -> Result<()> {
886        let times = Array1::zeros(0);
887        let values = Array1::zeros(0);
888        let target_times = Array1::from(vec![1.0, 2.0]);
889
890        let interpolator = TimeSeriesInterpolator::new();
891        let result = interpolator.interpolate(&times, &values, &target_times)?;
892
893        assert_eq!(result.len(), 2);
894        assert_eq!(result[0], 0.0);
895        assert_eq!(result[1], 0.0);
896
897        Ok(())
898    }
899
900    #[test]
901    fn test_resampling_methods() -> Result<()> {
902        let times = Array1::from(vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
903        let values = Array1::from(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
904
905        // Test different resampling methods
906        let methods = vec![
907            ResamplingMethod::Mean,
908            ResamplingMethod::Sum,
909            ResamplingMethod::Min,
910            ResamplingMethod::Max,
911            ResamplingMethod::First,
912            ResamplingMethod::Last,
913            ResamplingMethod::Count,
914        ];
915
916        for method in methods {
917            let resampler = TimeSeriesResampler::new(0.5).with_method(method);
918            let (resampled_times, resampled_values) = resampler.resample(&times, &values)?;
919
920            assert!(resampled_times.len() > 0);
921            assert_eq!(resampled_times.len(), resampled_values.len());
922
923            // All values should be finite (not NaN)
924            assert!(resampled_values.iter().all(|v| v.is_finite()));
925        }
926
927        Ok(())
928    }
929}