1use scirs2_core::ndarray::{s, Array1, Array2};
7use scirs2_core::numeric::Float;
8use std::fmt::Debug;
9
10use super::{utils, GpuConfig, GpuDeviceManager};
11use crate::error::{Result, TimeSeriesError};
12
13#[derive(Debug)]
15pub struct GpuTimeSeriesProcessor<F: Float + Debug> {
16 #[allow(dead_code)]
17 config: GpuConfig,
18 device_manager: GpuDeviceManager,
19 #[allow(dead_code)]
20 stream_handles: Vec<usize>, _phantom: std::marker::PhantomData<F>,
22}
23
24impl<
25 F: Float
26 + Debug
27 + Clone
28 + scirs2_core::numeric::Zero
29 + scirs2_core::numeric::One
30 + std::iter::Sum
31 + PartialOrd
32 + Copy,
33 > GpuTimeSeriesProcessor<F>
34{
35 pub fn new(config: GpuConfig) -> Result<Self> {
37 let device_manager = GpuDeviceManager::new()?;
38 Ok(Self {
39 config,
40 device_manager,
41 stream_handles: Vec::new(),
42 _phantom: std::marker::PhantomData,
43 })
44 }
45
46 pub fn batch_forecast(
48 &self,
49 series_batch: &[Array1<F>],
50 forecast_steps: usize,
51 method: ForecastMethod,
52 ) -> Result<Vec<Array1<F>>> {
53 if !self.device_manager.is_gpu_available() {
54 return self.cpu_fallback_batch_forecast(series_batch, forecast_steps, method);
55 }
56
57 self.gpu_optimized_batch_forecast(series_batch, forecast_steps, method)
59 }
60
61 fn cpu_fallback_batch_forecast(
63 &self,
64 series_batch: &[Array1<F>],
65 forecast_steps: usize,
66 method: ForecastMethod,
67 ) -> Result<Vec<Array1<F>>> {
68 let forecasts: Result<Vec<_>> = series_batch
70 .iter()
71 .map(|series| self.single_series_forecast(series, forecast_steps, &method))
72 .collect();
73 forecasts
74 }
75
76 fn gpu_optimized_batch_forecast(
78 &self,
79 series_batch: &[Array1<F>],
80 forecast_steps: usize,
81 method: ForecastMethod,
82 ) -> Result<Vec<Array1<F>>> {
83 let gpu_memory_limit = 256 * 1024 * 1024; let optimal_batch_size =
86 utils::get_recommended_batch_size(series_batch.len(), gpu_memory_limit);
87
88 let mut all_forecasts = Vec::with_capacity(series_batch.len());
89
90 for (batch_idx, batch) in series_batch.chunks(optimal_batch_size).enumerate() {
92 let stream_id = batch_idx % 4; let batch_forecasts =
97 self.gpu_parallel_forecast(batch, forecast_steps, &method, stream_id)?;
98 all_forecasts.extend(batch_forecasts);
99 }
100
101 Ok(all_forecasts)
102 }
103
104 fn gpu_parallel_forecast(
106 &self,
107 batch: &[Array1<F>],
108 forecast_steps: usize,
109 method: &ForecastMethod,
110 _stream_id: usize,
111 ) -> Result<Vec<Array1<F>>> {
112 match method {
114 ForecastMethod::ExponentialSmoothing { alpha } => self.gpu_batch_exponential_smoothing(
115 batch,
116 F::from(*alpha).unwrap_or_else(|| F::from(0.3).unwrap()),
117 forecast_steps,
118 ),
119 ForecastMethod::LinearTrend => self.gpu_batch_linear_trend(batch, forecast_steps),
120 ForecastMethod::MovingAverage { window } => {
121 self.gpu_batch_moving_average(batch, *window, forecast_steps)
122 }
123 ForecastMethod::AutoRegressive { order } => {
124 self.gpu_batch_autoregressive(batch, *order, forecast_steps)
125 }
126 }
127 }
128
129 fn gpu_batch_exponential_smoothing(
131 &self,
132 batch: &[Array1<F>],
133 alpha: F,
134 steps: usize,
135 ) -> Result<Vec<Array1<F>>> {
136 let mut results = Vec::with_capacity(batch.len());
137
138 for series in batch {
140 if series.is_empty() {
141 return Err(TimeSeriesError::InvalidInput("Empty series".to_string()));
142 }
143
144 let mut smoothed = series[0];
146 let alpha_complement = F::one() - alpha;
147
148 let data_len = series.len() - 1; let chunks = data_len / 4;
152 let remainder = data_len % 4;
153
154 for chunk_idx in 0..chunks {
156 let base_idx = chunk_idx * 4 + 1; for i in 0..4 {
158 if base_idx + i < series.len() {
159 let value = series[base_idx + i];
160 smoothed = alpha * value + alpha_complement * smoothed;
161 }
162 }
163 }
164
165 let remainder_start = chunks * 4 + 1;
167 for i in 0..remainder {
168 if remainder_start + i < series.len() {
169 let value = series[remainder_start + i];
170 smoothed = alpha * value + alpha_complement * smoothed;
171 }
172 }
173
174 let forecast = Array1::from_elem(steps, smoothed);
176 results.push(forecast);
177 }
178
179 Ok(results)
180 }
181
182 fn gpu_batch_linear_trend(&self, batch: &[Array1<F>], steps: usize) -> Result<Vec<Array1<F>>> {
184 let mut results = Vec::with_capacity(batch.len());
185
186 for series in batch {
188 if series.len() < 2 {
189 return Err(TimeSeriesError::InsufficientData {
190 message: "Need at least 2 points for trend".to_string(),
191 required: 2,
192 actual: series.len(),
193 });
194 }
195
196 let n = F::from(series.len()).unwrap();
198 let x_mean = (n - F::one()) / F::from(2).unwrap();
199
200 let y_sum = series.sum();
202 let y_mean = y_sum / n;
203
204 let mut numerator = F::zero();
206 let mut denominator = F::zero();
207
208 let chunk_size = 8; let chunks = series.len() / chunk_size;
211
212 for chunk_idx in 0..chunks {
213 let mut chunk_num = F::zero();
214 let mut chunk_den = F::zero();
215
216 for i in 0..chunk_size {
217 let idx = chunk_idx * chunk_size + i;
218 let x = F::from(idx).unwrap();
219 let y = series[idx];
220 let x_diff = x - x_mean;
221
222 chunk_num = chunk_num + x_diff * (y - y_mean);
223 chunk_den = chunk_den + x_diff * x_diff;
224 }
225
226 numerator = numerator + chunk_num;
227 denominator = denominator + chunk_den;
228 }
229
230 for idx in (chunks * chunk_size)..series.len() {
232 let x = F::from(idx).unwrap();
233 let y = series[idx];
234 let x_diff = x - x_mean;
235
236 numerator = numerator + x_diff * (y - y_mean);
237 denominator = denominator + x_diff * x_diff;
238 }
239
240 let slope = if denominator > F::zero() {
241 numerator / denominator
242 } else {
243 F::zero()
244 };
245
246 let intercept = y_mean - slope * x_mean;
247 let last_x = F::from(series.len() - 1).unwrap();
248
249 let mut forecasts = Array1::zeros(steps);
251 for i in 0..steps {
252 let future_x = last_x + F::from(i + 1).unwrap();
253 forecasts[i] = slope * future_x + intercept;
254 }
255
256 results.push(forecasts);
257 }
258
259 Ok(results)
260 }
261
262 fn gpu_batch_moving_average(
264 &self,
265 batch: &[Array1<F>],
266 window: usize,
267 steps: usize,
268 ) -> Result<Vec<Array1<F>>> {
269 let mut results = Vec::with_capacity(batch.len());
270
271 for series in batch {
272 if series.len() < window {
273 return Err(TimeSeriesError::InsufficientData {
274 message: "Series shorter than window".to_string(),
275 required: window,
276 actual: series.len(),
277 });
278 }
279
280 let last_window_start = series.len() - window;
282 let mut sum = F::zero();
283
284 for i in 0..window {
286 sum = sum + series[last_window_start + i];
287 }
288
289 let avg = sum / F::from(window).unwrap();
290 let forecast = Array1::from_elem(steps, avg);
291 results.push(forecast);
292 }
293
294 Ok(results)
295 }
296
297 fn gpu_batch_autoregressive(
299 &self,
300 batch: &[Array1<F>],
301 order: usize,
302 steps: usize,
303 ) -> Result<Vec<Array1<F>>> {
304 let mut results = Vec::with_capacity(batch.len());
305
306 for series in batch {
307 if series.len() < order + 1 {
308 return Err(TimeSeriesError::InsufficientData {
309 message: "Insufficient data for AR model".to_string(),
310 required: order + 1,
311 actual: series.len(),
312 });
313 }
314
315 let coefficients = self.gpu_estimate_ar_coefficients(series, order)?;
317
318 let mut forecasts = Array1::zeros(steps);
320 let mut extended_series = series.to_vec();
321
322 for i in 0..steps {
323 let mut forecast = F::zero();
324
325 for (j, &coeff) in coefficients.iter().enumerate() {
327 let lag_index = extended_series.len() - 1 - j;
328 forecast = forecast + coeff * extended_series[lag_index];
329 }
330
331 forecasts[i] = forecast;
332 extended_series.push(forecast);
333 }
334
335 results.push(forecasts);
336 }
337
338 Ok(results)
339 }
340
341 fn gpu_estimate_ar_coefficients(&self, series: &Array1<F>, order: usize) -> Result<Vec<F>> {
343 let n = series.len();
344 if n < order + 1 {
345 return Err(TimeSeriesError::InsufficientData {
346 message: "Insufficient data for coefficient estimation".to_string(),
347 required: order + 1,
348 actual: n,
349 });
350 }
351
352 let _num_equations = n - order;
354 let mut autocorrelations = vec![F::zero(); order + 1];
355
356 for lag in 0..=order {
358 let mut sum = F::zero();
359 let count = n - lag;
360
361 for i in 0..count {
363 sum = sum + series[i] * series[i + lag];
364 }
365
366 autocorrelations[lag] = sum / F::from(count).unwrap();
367 }
368
369 self.gpu_levinson_durbin(&autocorrelations[1..], autocorrelations[0])
371 }
372
373 fn gpu_levinson_durbin(&self, autocorr: &[F], variance: F) -> Result<Vec<F>> {
375 let order = autocorr.len();
376 let mut coefficients = vec![F::zero(); order];
377 let mut reflection_coeffs = vec![F::zero(); order];
378 let mut prediction_error = variance;
379
380 for k in 0..order {
381 let mut sum = F::zero();
383 for j in 0..k {
384 sum = sum + coefficients[j] * autocorr[k - 1 - j];
385 }
386
387 reflection_coeffs[k] = (autocorr[k] - sum) / prediction_error;
388
389 let new_coeff = reflection_coeffs[k];
391
392 let old_coeffs: Vec<F> = coefficients[..k].to_vec();
394
395 for j in 0..k {
397 coefficients[j] = old_coeffs[j] - new_coeff * old_coeffs[k - 1 - j];
398 }
399
400 coefficients[k] = new_coeff;
401
402 prediction_error = prediction_error * (F::one() - new_coeff * new_coeff);
404 }
405
406 Ok(coefficients)
407 }
408
409 #[allow(dead_code)]
411 fn optimized_batch_forecast(
412 &self,
413 series_batch: &[Array1<F>],
414 forecast_steps: usize,
415 method: ForecastMethod,
416 ) -> Result<Vec<Array1<F>>> {
417 let optimal_batch_size = utils::get_recommended_batch_size(
418 series_batch.len(),
419 8 * 1024 * 1024, );
421
422 let mut all_forecasts = Vec::with_capacity(series_batch.len());
423
424 for _batch in series_batch.chunks(optimal_batch_size) {
426 let _batch_forecasts: Result<Vec<_>> = _batch
427 .iter()
428 .map(|series| self.single_series_forecast(series, forecast_steps, &method))
429 .collect();
430 all_forecasts.extend(_batch_forecasts?);
431 }
432
433 Ok(all_forecasts)
434 }
435
436 fn single_series_forecast(
438 &self,
439 series: &Array1<F>,
440 forecast_steps: usize,
441 method: &ForecastMethod,
442 ) -> Result<Array1<F>> {
443 match method {
444 ForecastMethod::ExponentialSmoothing { alpha } => self
445 .gpu_exponential_smoothing_forecast(
446 series,
447 F::from(*alpha).unwrap_or_else(|| F::from(0.3).unwrap()),
448 forecast_steps,
449 ),
450 ForecastMethod::LinearTrend => self.gpu_linear_trend_forecast(series, forecast_steps),
451 ForecastMethod::MovingAverage { window } => {
452 self.gpu_moving_average_forecast(series, *window, forecast_steps)
453 }
454 ForecastMethod::AutoRegressive { order } => {
455 self.gpu_ar_forecast(series, *order, forecast_steps)
456 }
457 }
458 }
459
460 fn gpu_exponential_smoothing_forecast(
462 &self,
463 series: &Array1<F>,
464 alpha: F,
465 steps: usize,
466 ) -> Result<Array1<F>> {
467 if series.is_empty() {
468 return Err(TimeSeriesError::InvalidInput("Empty series".to_string()));
469 }
470
471 let mut smoothed = series[0];
473 for &value in series.iter().skip(1) {
474 smoothed = alpha * value + (F::one() - alpha) * smoothed;
475 }
476
477 Ok(Array1::from_elem(steps, smoothed))
479 }
480
481 fn gpu_linear_trend_forecast(&self, series: &Array1<F>, steps: usize) -> Result<Array1<F>> {
483 if series.len() < 2 {
484 return Err(TimeSeriesError::InsufficientData {
485 message: "Need at least 2 points for trend".to_string(),
486 required: 2,
487 actual: series.len(),
488 });
489 }
490
491 let n = F::from(series.len()).unwrap();
492 let x_mean = (n - F::one()) / F::from(2).unwrap();
493 let y_mean = series.sum() / n;
494
495 let mut numerator = F::zero();
497 let mut denominator = F::zero();
498
499 for (i, &y) in series.iter().enumerate() {
500 let x = F::from(i).unwrap();
501 let x_diff = x - x_mean;
502 numerator = numerator + x_diff * (y - y_mean);
503 denominator = denominator + x_diff * x_diff;
504 }
505
506 let slope = if denominator > F::zero() {
507 numerator / denominator
508 } else {
509 F::zero()
510 };
511
512 let intercept = y_mean - slope * x_mean;
513 let last_x = F::from(series.len() - 1).unwrap();
514
515 let mut forecasts = Array1::zeros(steps);
517 for i in 0..steps {
518 let future_x = last_x + F::from(i + 1).unwrap();
519 forecasts[i] = slope * future_x + intercept;
520 }
521
522 Ok(forecasts)
523 }
524
525 fn gpu_moving_average_forecast(
527 &self,
528 series: &Array1<F>,
529 window: usize,
530 steps: usize,
531 ) -> Result<Array1<F>> {
532 if series.len() < window {
533 return Err(TimeSeriesError::InsufficientData {
534 message: "Series shorter than window".to_string(),
535 required: window,
536 actual: series.len(),
537 });
538 }
539
540 let last_window = series.slice(s![series.len() - window..]);
542 let avg = last_window.sum() / F::from(window).unwrap();
543
544 Ok(Array1::from_elem(steps, avg))
546 }
547
548 fn gpu_ar_forecast(&self, series: &Array1<F>, order: usize, steps: usize) -> Result<Array1<F>> {
550 if series.len() < order + 1 {
551 return Err(TimeSeriesError::InsufficientData {
552 message: "Insufficient data for AR model".to_string(),
553 required: order + 1,
554 actual: series.len(),
555 });
556 }
557
558 let coefficients = self.estimate_ar_coefficients(series, order)?;
560
561 let mut forecasts = Array1::zeros(steps);
563 let mut extended_series = series.to_vec();
564
565 for i in 0..steps {
566 let mut forecast = F::zero();
567 for (j, &coeff) in coefficients.iter().enumerate() {
568 let lag_index = extended_series.len() - 1 - j;
569 forecast = forecast + coeff * extended_series[lag_index];
570 }
571 forecasts[i] = forecast;
572 extended_series.push(forecast);
573 }
574
575 Ok(forecasts)
576 }
577
578 fn estimate_ar_coefficients(&self, series: &Array1<F>, order: usize) -> Result<Vec<F>> {
580 let n = series.len();
581 if n < order + 1 {
582 return Err(TimeSeriesError::InsufficientData {
583 message: "Insufficient data for coefficient estimation".to_string(),
584 required: order + 1,
585 actual: n,
586 });
587 }
588
589 let num_equations = n - order;
591 let mut x = Array2::zeros((num_equations, order));
592 let mut y = Array1::zeros(num_equations);
593
594 for i in 0..num_equations {
595 y[i] = series[i + order];
596 for j in 0..order {
597 x[[i, j]] = series[i + order - 1 - j];
598 }
599 }
600
601 self.solve_normal_equations(&x, &y)
603 }
604
605 fn solve_normal_equations(&self, x: &Array2<F>, y: &Array1<F>) -> Result<Vec<F>> {
607 let p = x.ncols();
608
609 let mut coefficients = vec![F::zero(); p];
612
613 for j in 0..p {
614 let mut num = F::zero();
615 let mut den = F::zero();
616
617 for i in 0..x.nrows() {
618 num = num + x[[i, j]] * y[i];
619 den = den + x[[i, j]] * x[[i, j]];
620 }
621
622 coefficients[j] = if den > F::zero() {
623 num / den
624 } else {
625 F::zero()
626 };
627 }
628
629 Ok(coefficients)
630 }
631
632 pub fn batch_correlation_matrix(&self, seriesbatch: &[Array1<F>]) -> Result<Array2<F>> {
634 let n = seriesbatch.len();
635 let mut correlation_matrix = Array2::zeros((n, n));
636
637 for i in 0..n {
639 for j in i..n {
640 let corr = if i == j {
641 F::one()
642 } else {
643 self.gpu_correlation(&seriesbatch[i], &seriesbatch[j])?
644 };
645 correlation_matrix[[i, j]] = corr;
646 correlation_matrix[[j, i]] = corr;
647 }
648 }
649
650 Ok(correlation_matrix)
651 }
652
653 fn gpu_correlation(&self, series1: &Array1<F>, series2: &Array1<F>) -> Result<F> {
655 if series1.len() != series2.len() || series1.is_empty() {
656 return Err(TimeSeriesError::DimensionMismatch {
657 expected: series1.len(),
658 actual: series2.len(),
659 });
660 }
661
662 let n = F::from(series1.len()).unwrap();
663 let mean1 = series1.sum() / n;
664 let mean2 = series2.sum() / n;
665
666 let mut num = F::zero();
667 let mut den1 = F::zero();
668 let mut den2 = F::zero();
669
670 for (&x1, &x2) in series1.iter().zip(series2.iter()) {
671 let diff1 = x1 - mean1;
672 let diff2 = x2 - mean2;
673 num = num + diff1 * diff2;
674 den1 = den1 + diff1 * diff1;
675 den2 = den2 + diff2 * diff2;
676 }
677
678 let denominator = (den1 * den2).sqrt();
679 if denominator > F::zero() {
680 Ok(num / denominator)
681 } else {
682 Ok(F::zero())
683 }
684 }
685
686 pub fn sliding_window_statistics(
688 &self,
689 series: &Array1<F>,
690 window_size: usize,
691 statistics: &[WindowStatistic],
692 ) -> Result<Vec<Array1<F>>> {
693 if series.len() < window_size {
694 return Err(TimeSeriesError::InsufficientData {
695 message: "Series shorter than window".to_string(),
696 required: window_size,
697 actual: series.len(),
698 });
699 }
700
701 let num_windows = series.len() - window_size + 1;
702 let mut results = Vec::with_capacity(statistics.len());
703
704 for stat in statistics {
705 let mut stat_values = Array1::zeros(num_windows);
706
707 for i in 0..num_windows {
708 let window = series.slice(s![i..i + window_size]);
709 stat_values[i] = match stat {
710 WindowStatistic::Mean => window.sum() / F::from(window_size).unwrap(),
711 WindowStatistic::Variance => {
712 let mean = window.sum() / F::from(window_size).unwrap();
713 window
714 .iter()
715 .map(|&x| (x - mean) * (x - mean))
716 .fold(F::zero(), |acc, x| acc + x)
717 / F::from(window_size).unwrap()
718 }
719 WindowStatistic::Min => window.iter().fold(F::infinity(), |acc, &x| acc.min(x)),
720 WindowStatistic::Max => {
721 window.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x))
722 }
723 WindowStatistic::Range => {
724 let min_val = window.iter().fold(F::infinity(), |acc, &x| acc.min(x));
725 let max_val = window.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x));
726 max_val - min_val
727 }
728 };
729 }
730
731 results.push(stat_values);
732 }
733
734 Ok(results)
735 }
736}
737
738#[derive(Debug, Clone)]
740pub enum ForecastMethod {
741 ExponentialSmoothing {
743 alpha: f64,
745 },
746 LinearTrend,
748 MovingAverage {
750 window: usize,
752 },
753 AutoRegressive {
755 order: usize,
757 },
758}
759
760#[derive(Debug, Clone)]
762pub enum WindowStatistic {
763 Mean,
765 Variance,
767 Min,
769 Max,
771 Range,
773}
774
775#[derive(Debug)]
777pub struct GpuFeatureExtractor<F: Float + Debug> {
778 #[allow(dead_code)]
779 processor: GpuTimeSeriesProcessor<F>,
780 feature_config: FeatureConfig,
781}
782
783#[derive(Debug, Clone)]
785pub struct FeatureConfig {
786 pub extract_statistical: bool,
788 pub extract_frequency: bool,
790 pub extract_complexity: bool,
792 pub window_sizes: Vec<usize>,
794}
795
796impl Default for FeatureConfig {
797 fn default() -> Self {
798 Self {
799 extract_statistical: true,
800 extract_frequency: true,
801 extract_complexity: false,
802 window_sizes: vec![5, 10, 20],
803 }
804 }
805}
806
807impl<F: Float + Debug + Clone + std::iter::Sum> GpuFeatureExtractor<F> {
808 pub fn new(_config: GpuConfig, featureconfig: FeatureConfig) -> Result<Self> {
810 let processor = GpuTimeSeriesProcessor::new(_config)?;
811 Ok(Self {
812 processor,
813 feature_config: featureconfig,
814 })
815 }
816
817 pub fn batch_extract_features(&self, seriesbatch: &[Array1<F>]) -> Result<Array2<F>> {
819 let mut all_features = Vec::new();
820
821 for series in seriesbatch {
822 let features = self.extract_features(series)?;
823 all_features.push(features);
824 }
825
826 if all_features.is_empty() {
828 return Ok(Array2::zeros((0, 0)));
829 }
830
831 let n_series = all_features.len();
832 let n_features = all_features[0].len();
833 let mut feature_matrix = Array2::zeros((n_series, n_features));
834
835 for (i, features) in all_features.iter().enumerate() {
836 for (j, &feature) in features.iter().enumerate() {
837 feature_matrix[[i, j]] = feature;
838 }
839 }
840
841 Ok(feature_matrix)
842 }
843
844 fn extract_features(&self, series: &Array1<F>) -> Result<Vec<F>> {
846 let mut features = Vec::new();
847
848 if self.feature_config.extract_statistical {
849 features.extend(self.extract_statistical_features(series)?);
850 }
851
852 if self.feature_config.extract_frequency {
853 features.extend(self.extract_frequency_features(series)?);
854 }
855
856 if self.feature_config.extract_complexity {
857 features.extend(self.extract_complexity_features(series)?);
858 }
859
860 Ok(features)
861 }
862
863 fn extract_statistical_features(&self, series: &Array1<F>) -> Result<Vec<F>> {
865 if series.is_empty() {
866 return Ok(vec![F::zero(); 8]); }
868
869 let n = F::from(series.len()).unwrap();
870 let mean = series.sum() / n;
871
872 let variance = series
874 .iter()
875 .map(|&x| (x - mean) * (x - mean))
876 .fold(F::zero(), |acc, x| acc + x)
877 / n;
878
879 let min_val = series.iter().fold(F::infinity(), |acc, &x| acc.min(x));
881 let max_val = series.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x));
882
883 let std_dev = variance.sqrt();
885 let skewness = if std_dev > F::zero() {
886 series
887 .iter()
888 .map(|&x| {
889 let normalized = (x - mean) / std_dev;
890 normalized * normalized * normalized
891 })
892 .fold(F::zero(), |acc, x| acc + x)
893 / n
894 } else {
895 F::zero()
896 };
897
898 let kurtosis = if std_dev > F::zero() {
900 series
901 .iter()
902 .map(|&x| {
903 let normalized = (x - mean) / std_dev;
904 let squared = normalized * normalized;
905 squared * squared
906 })
907 .fold(F::zero(), |acc, x| acc + x)
908 / n
909 } else {
910 F::zero()
911 };
912
913 let range = max_val - min_val;
915
916 let trend = if series.len() > 1 {
918 let x_mean = F::from(series.len() - 1).unwrap() / F::from(2).unwrap();
919 let mut num = F::zero();
920 let mut den = F::zero();
921
922 for (i, &y) in series.iter().enumerate() {
923 let x = F::from(i).unwrap();
924 num = num + (x - x_mean) * (y - mean);
925 den = den + (x - x_mean) * (x - x_mean);
926 }
927
928 if den > F::zero() {
929 num / den
930 } else {
931 F::zero()
932 }
933 } else {
934 F::zero()
935 };
936
937 Ok(vec![
938 mean,
939 variance.sqrt(),
940 min_val,
941 max_val,
942 skewness,
943 kurtosis,
944 range,
945 trend,
946 ])
947 }
948
949 fn extract_frequency_features(&self, series: &Array1<F>) -> Result<Vec<F>> {
951 let n = series.len();
953 if n < 4 {
954 return Ok(vec![F::zero(); 3]);
955 }
956
957 let mut max_autocorr = F::zero();
959 let mut dominant_period = 1;
960
961 for lag in 1..(n / 2).min(20) {
962 let mut autocorr = F::zero();
963 let mut count = 0;
964
965 for i in lag..n {
966 autocorr = autocorr + series[i] * series[i - lag];
967 count += 1;
968 }
969
970 if count > 0 {
971 autocorr = autocorr / F::from(count).unwrap();
972 if autocorr > max_autocorr {
973 max_autocorr = autocorr;
974 dominant_period = lag;
975 }
976 }
977 }
978
979 let dominant_frequency = F::one() / F::from(dominant_period).unwrap();
980
981 let spectral_energy = series
983 .iter()
984 .map(|&x| x * x)
985 .fold(F::zero(), |acc, x| acc + x)
986 / F::from(n).unwrap();
987
988 Ok(vec![dominant_frequency, max_autocorr, spectral_energy])
989 }
990
991 fn extract_complexity_features(&self, series: &Array1<F>) -> Result<Vec<F>> {
993 if series.len() < 3 {
994 return Ok(vec![F::zero(); 2]);
995 }
996
997 let mut changes = 0;
999 for i in 1..series.len() {
1000 if (series[i] - series[i - 1]).abs() > F::zero() {
1001 changes += 1;
1002 }
1003 }
1004 let entropy = F::from(changes).unwrap() / F::from(series.len() - 1).unwrap();
1005
1006 let mut matches = 0;
1008 let tolerance = series
1009 .iter()
1010 .map(|&x| x * x)
1011 .fold(F::zero(), |acc, x| acc + x)
1012 .sqrt()
1013 / F::from(series.len()).unwrap()
1014 * F::from(0.1).unwrap();
1015
1016 for i in 0..series.len() - 2 {
1017 for j in i + 1..series.len() - 1 {
1018 if (series[i] - series[j]).abs() <= tolerance
1019 && (series[i + 1] - series[j + 1]).abs() <= tolerance
1020 {
1021 matches += 1;
1022 }
1023 }
1024 }
1025
1026 let sample_entropy = if matches > 0 {
1027 -F::from(matches).unwrap().ln()
1028 } else {
1029 F::from(10).unwrap() };
1031
1032 Ok(vec![entropy, sample_entropy])
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039
1040 #[test]
1041 fn test_gpu_processor_creation() {
1042 let config = GpuConfig::default();
1043 let processor = GpuTimeSeriesProcessor::<f64>::new(config);
1044 assert!(processor.is_ok());
1045 }
1046
1047 #[test]
1048 fn test_batch_exponential_smoothing() {
1049 let config = GpuConfig::default();
1050 let processor = GpuTimeSeriesProcessor::<f64>::new(config).unwrap();
1051
1052 let series1 = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1053 let series2 = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
1054 let batch = vec![series1, series2];
1055
1056 let method = ForecastMethod::ExponentialSmoothing { alpha: 0.3 };
1057 let results = processor.batch_forecast(&batch, 3, method);
1058
1059 assert!(results.is_ok());
1060 let forecasts = results.unwrap();
1061 assert_eq!(forecasts.len(), 2);
1062 assert_eq!(forecasts[0].len(), 3);
1063 assert_eq!(forecasts[1].len(), 3);
1064 }
1065
1066 #[test]
1067 fn test_correlation_matrix() {
1068 let config = GpuConfig::default();
1069 let processor = GpuTimeSeriesProcessor::<f64>::new(config).unwrap();
1070
1071 let series1 = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1072 let series2 = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
1073 let batch = vec![series1, series2];
1074
1075 let correlation_matrix = processor.batch_correlation_matrix(&batch).unwrap();
1076
1077 assert_eq!(correlation_matrix.dim(), (2, 2));
1078 assert!((correlation_matrix[[0, 0]] - 1.0).abs() < 1e-10);
1079 assert!((correlation_matrix[[1, 1]] - 1.0).abs() < 1e-10);
1080 assert!(correlation_matrix[[0, 1]] > 0.99); }
1082
1083 #[test]
1084 fn test_feature_extraction() {
1085 let config = GpuConfig::default();
1086 let feature_config = FeatureConfig::default();
1087 let extractor = GpuFeatureExtractor::new(config, feature_config).unwrap();
1088
1089 let series = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0]);
1090 let batch = vec![series];
1091
1092 let features = extractor.batch_extract_features(&batch).unwrap();
1093 assert_eq!(features.nrows(), 1);
1094 assert!(features.ncols() > 0); }
1096
1097 #[test]
1098 fn test_sliding_window_statistics() {
1099 let config = GpuConfig::default();
1100 let processor = GpuTimeSeriesProcessor::<f64>::new(config).unwrap();
1101
1102 let series = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
1103 let statistics = vec![WindowStatistic::Mean, WindowStatistic::Variance];
1104
1105 let results = processor
1106 .sliding_window_statistics(&series, 3, &statistics)
1107 .unwrap();
1108
1109 assert_eq!(results.len(), 2); assert_eq!(results[0].len(), 6); assert!((results[0][0] - 2.0).abs() < 1e-10);
1114 }
1115}