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| arr[a].partial_cmp(&arr[b]).unwrap());
499        indices
500    }
501}
502
503/// Time series resampler for frequency conversion and aggregation
504#[derive(Debug, Clone)]
505#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
506pub struct TimeSeriesResampler {
507    method: ResamplingMethod,
508    target_frequency: Float,
509    fill_method: Option<InterpolationMethod>,
510}
511
512impl TimeSeriesResampler {
513    /// Create a new time series resampler
514    pub fn new(target_frequency: Float) -> Self {
515        Self {
516            method: ResamplingMethod::default(),
517            target_frequency,
518            fill_method: None,
519        }
520    }
521
522    /// Set the resampling method
523    pub fn with_method(mut self, method: ResamplingMethod) -> Self {
524        self.method = method;
525        self
526    }
527
528    /// Set interpolation method for missing values
529    pub fn with_fill_method(mut self, fill_method: InterpolationMethod) -> Self {
530        self.fill_method = Some(fill_method);
531        self
532    }
533
534    /// Resample time series to target frequency
535    pub fn resample(
536        &self,
537        times: &Array1<Float>,
538        values: &Array1<Float>,
539    ) -> Result<(Array1<Float>, Array1<Float>)> {
540        if times.len() != values.len() {
541            return Err(SklearsError::InvalidInput(
542                "Times and values must have the same length".to_string(),
543            ));
544        }
545
546        if times.is_empty() {
547            return Ok((Array1::zeros(0), Array1::zeros(0)));
548        }
549
550        let min_time = times[0];
551        let max_time = times[times.len() - 1];
552
553        // Create regular time grid
554        let num_points = ((max_time - min_time) / self.target_frequency).ceil() as usize + 1;
555        let mut target_times = Array1::zeros(num_points);
556        for i in 0..num_points {
557            target_times[i] = min_time + (i as Float) * self.target_frequency;
558        }
559
560        // Group data points by time windows
561        let resampled_values = self.aggregate_by_windows(times, values, &target_times)?;
562
563        Ok((target_times, resampled_values))
564    }
565
566    /// Aggregate values within time windows
567    fn aggregate_by_windows(
568        &self,
569        times: &Array1<Float>,
570        values: &Array1<Float>,
571        target_times: &Array1<Float>,
572    ) -> Result<Array1<Float>> {
573        let mut result = Array1::zeros(target_times.len());
574        let half_window = self.target_frequency / 2.0;
575
576        for (i, &target_time) in target_times.iter().enumerate() {
577            let window_start = target_time - half_window;
578            let window_end = target_time + half_window;
579
580            let window_values: Vec<Float> = times
581                .iter()
582                .zip(values.iter())
583                .filter_map(|(&t, &v)| {
584                    if t >= window_start && t < window_end {
585                        Some(v)
586                    } else {
587                        None
588                    }
589                })
590                .collect();
591
592            if window_values.is_empty() {
593                // Handle missing values with fill method if specified
594                result[i] = if let Some(fill_method) = &self.fill_method {
595                    self.interpolate_missing_value(times, values, target_time, fill_method)?
596                } else {
597                    Float::NAN
598                };
599            } else {
600                result[i] = match self.method {
601                    ResamplingMethod::Mean => {
602                        window_values.iter().sum::<Float>() / window_values.len() as Float
603                    }
604                    ResamplingMethod::Sum => window_values.iter().sum(),
605                    ResamplingMethod::Min => {
606                        window_values.iter().fold(Float::INFINITY, |a, &b| a.min(b))
607                    }
608                    ResamplingMethod::Max => window_values
609                        .iter()
610                        .fold(Float::NEG_INFINITY, |a, &b| a.max(b)),
611                    ResamplingMethod::First => window_values[0],
612                    ResamplingMethod::Last => window_values[window_values.len() - 1],
613                    ResamplingMethod::Median => self.calculate_median(&window_values),
614                    ResamplingMethod::Count => window_values.len() as Float,
615                };
616            }
617        }
618
619        Ok(result)
620    }
621
622    /// Interpolate missing value using specified method
623    fn interpolate_missing_value(
624        &self,
625        times: &Array1<Float>,
626        values: &Array1<Float>,
627        target_time: Float,
628        fill_method: &InterpolationMethod,
629    ) -> Result<Float> {
630        let interpolator = TimeSeriesInterpolator::new()
631            .with_method(*fill_method)
632            .with_extrapolation(true);
633
634        let target_times = Array1::from(vec![target_time]);
635        let interpolated = interpolator.interpolate(times, values, &target_times)?;
636        Ok(interpolated[0])
637    }
638
639    /// Calculate median of a vector
640    fn calculate_median(&self, values: &[Float]) -> Float {
641        if values.is_empty() {
642            return Float::NAN;
643        }
644
645        let mut sorted_values = values.to_vec();
646        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
647
648        let len = sorted_values.len();
649        if len % 2 == 0 {
650            (sorted_values[len / 2 - 1] + sorted_values[len / 2]) / 2.0
651        } else {
652            sorted_values[len / 2]
653        }
654    }
655}
656
657/// Multi-variate time series aligner
658#[derive(Debug, Clone)]
659#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
660pub struct MultiVariateTimeSeriesAligner {
661    interpolation_method: InterpolationMethod,
662    target_frequency: Option<Float>,
663    fill_method: InterpolationMethod,
664}
665
666impl Default for MultiVariateTimeSeriesAligner {
667    fn default() -> Self {
668        Self::new()
669    }
670}
671
672impl MultiVariateTimeSeriesAligner {
673    /// Create a new multi-variate time series aligner
674    pub fn new() -> Self {
675        Self {
676            interpolation_method: InterpolationMethod::Linear,
677            target_frequency: None,
678            fill_method: InterpolationMethod::Linear,
679        }
680    }
681
682    /// Set interpolation method for alignment
683    pub fn with_interpolation_method(mut self, method: InterpolationMethod) -> Self {
684        self.interpolation_method = method;
685        self
686    }
687
688    /// Set target frequency for resampling
689    pub fn with_target_frequency(mut self, frequency: Float) -> Self {
690        self.target_frequency = Some(frequency);
691        self
692    }
693
694    /// Set fill method for missing values
695    pub fn with_fill_method(mut self, method: InterpolationMethod) -> Self {
696        self.fill_method = method;
697        self
698    }
699
700    /// Align multiple time series to a common time index
701    pub fn align_series(
702        &self,
703        series_data: &[(Array1<Float>, Array1<Float>)], // (times, values) pairs
704    ) -> Result<(Array1<Float>, Array2<Float>)> {
705        if series_data.is_empty() {
706            return Ok((Array1::zeros(0), Array2::zeros((0, 0))));
707        }
708
709        // Find common time range
710        let mut min_time = Float::INFINITY;
711        let mut max_time = Float::NEG_INFINITY;
712
713        for (times, _) in series_data.iter() {
714            if !times.is_empty() {
715                min_time = min_time.min(times[0]);
716                max_time = max_time.max(times[times.len() - 1]);
717            }
718        }
719
720        if min_time == Float::INFINITY || max_time == Float::NEG_INFINITY {
721            return Err(SklearsError::InvalidInput(
722                "All time series are empty".to_string(),
723            ));
724        }
725
726        // Create common time index
727        let common_times = if let Some(freq) = self.target_frequency {
728            // Use specified frequency
729            let num_points = ((max_time - min_time) / freq).ceil() as usize + 1;
730            let mut times = Array1::zeros(num_points);
731            for i in 0..num_points {
732                times[i] = min_time + (i as Float) * freq;
733            }
734            times
735        } else {
736            // Use union of all time points
737            let mut all_times: Vec<Float> = Vec::new();
738            for (times, _) in series_data.iter() {
739                all_times.extend(times.iter());
740            }
741            all_times.sort_by(|a, b| a.partial_cmp(b).unwrap());
742            all_times.dedup_by(|a, b| (*a - *b).abs() < 1e-10);
743            Array1::from(all_times)
744        };
745
746        // Interpolate each series to common time index
747        let n_series = series_data.len();
748        let n_times = common_times.len();
749        let mut aligned_data = Array2::zeros((n_times, n_series));
750
751        let interpolator = TimeSeriesInterpolator::new()
752            .with_method(self.interpolation_method)
753            .with_extrapolation(true);
754
755        for (series_idx, (times, values)) in series_data.iter().enumerate() {
756            if !times.is_empty() && !values.is_empty() {
757                let interpolated = interpolator.interpolate(times, values, &common_times)?;
758                for (time_idx, &value) in interpolated.iter().enumerate() {
759                    aligned_data[[time_idx, series_idx]] = value;
760                }
761            }
762        }
763
764        Ok((common_times, aligned_data))
765    }
766}
767
768#[allow(non_snake_case)]
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use approx::assert_abs_diff_eq;
773    use scirs2_core::ndarray::Array1;
774
775    #[test]
776    fn test_linear_interpolation() -> Result<()> {
777        let times = Array1::from(vec![0.0, 1.0, 3.0, 4.0]);
778        let values = Array1::from(vec![0.0, 1.0, 3.0, 4.0]);
779        let target_times = Array1::from(vec![0.5, 2.0, 3.5]);
780
781        let interpolator = TimeSeriesInterpolator::new().with_method(InterpolationMethod::Linear);
782
783        let result = interpolator.interpolate(&times, &values, &target_times)?;
784
785        assert_abs_diff_eq!(result[0], 0.5, epsilon = 1e-10); // Linear between 0 and 1
786        assert_abs_diff_eq!(result[1], 2.0, epsilon = 1e-10); // Linear between 1 and 3
787        assert_abs_diff_eq!(result[2], 3.5, epsilon = 1e-10); // Linear between 3 and 4
788
789        Ok(())
790    }
791
792    #[test]
793    fn test_forward_fill_interpolation() -> Result<()> {
794        let times = Array1::from(vec![0.0, 2.0, 5.0]);
795        let values = Array1::from(vec![10.0, 20.0, 30.0]);
796        let target_times = Array1::from(vec![1.0, 3.0, 6.0]);
797
798        let interpolator = TimeSeriesInterpolator::new()
799            .with_method(InterpolationMethod::ForwardFill)
800            .with_extrapolation(true);
801
802        let result = interpolator.interpolate(&times, &values, &target_times)?;
803
804        assert_abs_diff_eq!(result[0], 10.0, epsilon = 1e-10); // Forward fill from 0.0
805        assert_abs_diff_eq!(result[1], 20.0, epsilon = 1e-10); // Forward fill from 2.0
806        assert_abs_diff_eq!(result[2], 30.0, epsilon = 1e-10); // Forward fill from 5.0
807
808        Ok(())
809    }
810
811    #[test]
812    fn test_nearest_neighbor_interpolation() -> Result<()> {
813        let times = Array1::from(vec![0.0, 2.0, 5.0]);
814        let values = Array1::from(vec![100.0, 200.0, 300.0]);
815        let target_times = Array1::from(vec![0.8, 2.1, 4.9]);
816
817        let interpolator =
818            TimeSeriesInterpolator::new().with_method(InterpolationMethod::NearestNeighbor);
819
820        let result = interpolator.interpolate(&times, &values, &target_times)?;
821
822        assert_abs_diff_eq!(result[0], 100.0, epsilon = 1e-10); // Nearest to 0.0
823        assert_abs_diff_eq!(result[1], 200.0, epsilon = 1e-10); // Nearest to 2.0
824        assert_abs_diff_eq!(result[2], 300.0, epsilon = 1e-10); // Nearest to 5.0
825
826        Ok(())
827    }
828
829    #[test]
830    fn test_time_series_resampling() -> Result<()> {
831        let times = Array1::from(vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]);
832        let values = Array1::from(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
833
834        let resampler = TimeSeriesResampler::new(1.0) // Resample to 1.0 time unit intervals
835            .with_method(ResamplingMethod::Mean);
836
837        let (resampled_times, resampled_values) = resampler.resample(&times, &values)?;
838
839        // Should create regular grid from 0.0 to 3.0 with step 1.0
840        assert_eq!(resampled_times.len(), 4);
841        assert_abs_diff_eq!(resampled_times[0], 0.0, epsilon = 1e-10);
842        assert_abs_diff_eq!(resampled_times[3], 3.0, epsilon = 1e-10);
843
844        // Values should be aggregated by mean within windows
845        assert_eq!(resampled_values.len(), 4);
846
847        Ok(())
848    }
849
850    #[test]
851    fn test_multivariate_alignment() -> Result<()> {
852        let series1_times = Array1::from(vec![0.0, 1.0, 2.0]);
853        let series1_values = Array1::from(vec![10.0, 20.0, 30.0]);
854
855        let series2_times = Array1::from(vec![0.5, 1.5, 2.5]);
856        let series2_values = Array1::from(vec![15.0, 25.0, 35.0]);
857
858        let series_data = vec![
859            (series1_times, series1_values),
860            (series2_times, series2_values),
861        ];
862
863        let aligner = MultiVariateTimeSeriesAligner::new()
864            .with_interpolation_method(InterpolationMethod::Linear)
865            .with_target_frequency(0.5);
866
867        let (aligned_times, aligned_data) = aligner.align_series(&series_data)?;
868
869        // Should have regular time grid with 0.5 step
870        assert!(aligned_times.len() > 3);
871        assert_eq!(aligned_data.dim().1, 2); // Two series
872
873        // Check first and last time points
874        assert_abs_diff_eq!(aligned_times[0], 0.0, epsilon = 1e-6);
875        assert_abs_diff_eq!(aligned_times[aligned_times.len() - 1], 2.5, epsilon = 1e-6);
876
877        Ok(())
878    }
879
880    #[test]
881    fn test_empty_input() -> Result<()> {
882        let times = Array1::zeros(0);
883        let values = Array1::zeros(0);
884        let target_times = Array1::from(vec![1.0, 2.0]);
885
886        let interpolator = TimeSeriesInterpolator::new();
887        let result = interpolator.interpolate(&times, &values, &target_times)?;
888
889        assert_eq!(result.len(), 2);
890        assert_eq!(result[0], 0.0);
891        assert_eq!(result[1], 0.0);
892
893        Ok(())
894    }
895
896    #[test]
897    fn test_resampling_methods() -> Result<()> {
898        let times = Array1::from(vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
899        let values = Array1::from(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
900
901        // Test different resampling methods
902        let methods = vec![
903            ResamplingMethod::Mean,
904            ResamplingMethod::Sum,
905            ResamplingMethod::Min,
906            ResamplingMethod::Max,
907            ResamplingMethod::First,
908            ResamplingMethod::Last,
909            ResamplingMethod::Count,
910        ];
911
912        for method in methods {
913            let resampler = TimeSeriesResampler::new(0.5).with_method(method);
914            let (resampled_times, resampled_values) = resampler.resample(&times, &values)?;
915
916            assert!(resampled_times.len() > 0);
917            assert_eq!(resampled_times.len(), resampled_values.len());
918
919            // All values should be finite (not NaN)
920            assert!(resampled_values.iter().all(|v| v.is_finite()));
921        }
922
923        Ok(())
924    }
925}