1use scirs2_core::ndarray::{s, Array1};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use super::config::TurningPointsConfig;
12use super::utils::detect_turning_points;
13use crate::error::{Result, TimeSeriesError};
14
15#[derive(Debug, Clone)]
17pub struct TurningPointsFeatures<F> {
18 pub total_turning_points: usize,
21 pub local_minima_count: usize,
23 pub local_maxima_count: usize,
25 pub peak_valley_ratio: F,
27 pub average_turning_point_distance: F,
29
30 pub upward_changes: usize,
33 pub downward_changes: usize,
35 pub directional_change_ratio: F,
37 pub average_upward_magnitude: F,
39 pub average_downward_magnitude: F,
41 pub directional_change_std: F,
43
44 pub longest_upward_sequence: usize,
47 pub longest_downward_sequence: usize,
49 pub average_upward_sequence_length: F,
51 pub average_downward_sequence_length: F,
53 pub momentum_persistence_ratio: F,
55
56 pub average_peak_amplitude: F,
59 pub average_valley_amplitude: F,
61 pub peak_amplitude_std: F,
63 pub valley_amplitude_std: F,
65 pub peak_valley_amplitude_ratio: F,
67 pub extrema_asymmetry: F,
69
70 pub major_trend_reversals: usize,
73 pub minor_trend_reversals: usize,
75 pub average_major_reversal_magnitude: F,
77 pub average_minor_reversal_magnitude: F,
79 pub trend_reversal_frequency: F,
81 pub reversal_strength_index: F,
83
84 pub turning_point_regularity: F,
87 pub turning_point_clustering: F,
89 pub turning_point_periodicity: F,
91 pub turning_point_autocorrelation: F,
93
94 pub turning_point_volatility: F,
97 pub stability_index: F,
99 pub noise_signal_ratio: F,
101 pub trend_consistency: F,
103
104 pub double_peak_count: usize,
107 pub double_bottom_count: usize,
109 pub head_shoulders_count: usize,
111 pub triangular_pattern_count: usize,
113
114 pub upper_half_turning_points: F,
117 pub lower_half_turning_points: F,
119 pub turning_point_position_skewness: F,
121 pub turning_point_position_kurtosis: F,
123
124 pub multiscale_turning_points: Vec<usize>,
127 pub scale_turning_point_ratio: F,
129 pub cross_scale_consistency: F,
131 pub hierarchical_structure_index: F,
133}
134
135impl<F> Default for TurningPointsFeatures<F>
136where
137 F: Float + FromPrimitive,
138{
139 fn default() -> Self {
140 Self {
141 total_turning_points: 0,
143 local_minima_count: 0,
144 local_maxima_count: 0,
145 peak_valley_ratio: F::one(),
146 average_turning_point_distance: F::zero(),
147
148 upward_changes: 0,
150 downward_changes: 0,
151 directional_change_ratio: F::one(),
152 average_upward_magnitude: F::zero(),
153 average_downward_magnitude: F::zero(),
154 directional_change_std: F::zero(),
155
156 longest_upward_sequence: 0,
158 longest_downward_sequence: 0,
159 average_upward_sequence_length: F::zero(),
160 average_downward_sequence_length: F::zero(),
161 momentum_persistence_ratio: F::zero(),
162
163 average_peak_amplitude: F::zero(),
165 average_valley_amplitude: F::zero(),
166 peak_amplitude_std: F::zero(),
167 valley_amplitude_std: F::zero(),
168 peak_valley_amplitude_ratio: F::one(),
169 extrema_asymmetry: F::zero(),
170
171 major_trend_reversals: 0,
173 minor_trend_reversals: 0,
174 average_major_reversal_magnitude: F::zero(),
175 average_minor_reversal_magnitude: F::zero(),
176 trend_reversal_frequency: F::zero(),
177 reversal_strength_index: F::zero(),
178
179 turning_point_regularity: F::zero(),
181 turning_point_clustering: F::zero(),
182 turning_point_periodicity: F::zero(),
183 turning_point_autocorrelation: F::zero(),
184
185 turning_point_volatility: F::zero(),
187 stability_index: F::zero(),
188 noise_signal_ratio: F::zero(),
189 trend_consistency: F::zero(),
190
191 double_peak_count: 0,
193 double_bottom_count: 0,
194 head_shoulders_count: 0,
195 triangular_pattern_count: 0,
196
197 upper_half_turning_points: F::from(0.5).unwrap(),
199 lower_half_turning_points: F::from(0.5).unwrap(),
200 turning_point_position_skewness: F::zero(),
201 turning_point_position_kurtosis: F::zero(),
202
203 multiscale_turning_points: Vec::new(),
205 scale_turning_point_ratio: F::zero(),
206 cross_scale_consistency: F::zero(),
207 hierarchical_structure_index: F::zero(),
208 }
209 }
210}
211
212#[derive(Debug, Clone)]
218struct DirectionalChangeStats<F> {
219 directional_change_ratio: F,
220 average_upward_magnitude: F,
221 average_downward_magnitude: F,
222 directional_change_std: F,
223}
224
225#[derive(Debug, Clone)]
227struct MomentumFeatures<F> {
228 longest_upward_sequence: usize,
229 longest_downward_sequence: usize,
230 average_upward_sequence_length: F,
231 average_downward_sequence_length: F,
232 momentum_persistence_ratio: F,
233}
234
235#[derive(Debug, Clone)]
237struct ExtremaFeatures<F> {
238 average_peak_amplitude: F,
239 average_valley_amplitude: F,
240 peak_amplitude_std: F,
241 valley_amplitude_std: F,
242 peak_valley_amplitude_ratio: F,
243 extrema_asymmetry: F,
244}
245
246#[derive(Debug, Clone)]
248struct TrendReversalFeatures<F> {
249 major_trend_reversals: usize,
250 minor_trend_reversals: usize,
251 average_major_reversal_magnitude: F,
252 average_minor_reversal_magnitude: F,
253 trend_reversal_frequency: F,
254 reversal_strength_index: F,
255}
256
257#[derive(Debug, Clone)]
259struct TurningPointTemporalFeatures<F> {
260 turning_point_regularity: F,
261 turning_point_clustering: F,
262 turning_point_periodicity: F,
263 turning_point_autocorrelation: F,
264}
265
266impl<F> Default for TurningPointTemporalFeatures<F>
267where
268 F: Float + FromPrimitive,
269{
270 fn default() -> Self {
271 Self {
272 turning_point_regularity: F::zero(),
273 turning_point_clustering: F::zero(),
274 turning_point_periodicity: F::zero(),
275 turning_point_autocorrelation: F::zero(),
276 }
277 }
278}
279
280#[derive(Debug, Clone)]
282struct StabilityFeatures<F> {
283 turning_point_volatility: F,
284 stability_index: F,
285 noise_signal_ratio: F,
286 trend_consistency: F,
287}
288
289#[derive(Debug, Clone, Default)]
291struct AdvancedPatternFeatures {
292 double_peak_count: usize,
293 double_bottom_count: usize,
294 head_shoulders_count: usize,
295 triangular_pattern_count: usize,
296}
297
298#[derive(Debug, Clone)]
300struct PositionFeatures<F> {
301 upper_half_turning_points: F,
302 lower_half_turning_points: F,
303 turning_point_position_skewness: F,
304 turning_point_position_kurtosis: F,
305}
306
307#[derive(Debug, Clone)]
309struct MultiscaleTurningPointFeatures<F> {
310 multiscale_turning_points: Vec<usize>,
311 scale_turning_point_ratio: F,
312 cross_scale_consistency: F,
313 hierarchical_structure_index: F,
314}
315
316impl<F> Default for MultiscaleTurningPointFeatures<F>
317where
318 F: Float + FromPrimitive,
319{
320 fn default() -> Self {
321 Self {
322 multiscale_turning_points: Vec::new(),
323 scale_turning_point_ratio: F::zero(),
324 cross_scale_consistency: F::zero(),
325 hierarchical_structure_index: F::zero(),
326 }
327 }
328}
329
330#[allow(dead_code)]
350pub fn calculate_turning_points_features<F>(
351 ts: &Array1<F>,
352 config: &TurningPointsConfig,
353) -> Result<TurningPointsFeatures<F>>
354where
355 F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
356 for<'a> F: std::iter::Sum<&'a F>,
357{
358 let n = ts.len();
359 if n < config.extrema_window_size * 2 {
360 return Err(TimeSeriesError::InsufficientData {
361 message: "Insufficient data for turning points analysis".to_string(),
362 required: config.extrema_window_size * 2,
363 actual: n,
364 });
365 }
366
367 let (turning_points, local_maxima, local_minima) = detect_turning_points(ts, config)?;
369
370 let total_turning_points = turning_points.len();
372 let local_maxima_count = local_maxima.len();
373 let local_minima_count = local_minima.len();
374 let peak_valley_ratio = if local_minima_count > 0 {
375 F::from(local_maxima_count).unwrap() / F::from(local_minima_count).unwrap()
376 } else {
377 F::zero()
378 };
379
380 let average_turning_point_distance = if total_turning_points > 1 {
382 let total_distance: usize = turning_points.windows(2).map(|w| w[1] - w[0]).sum();
383 F::from(total_distance).unwrap() / F::from(total_turning_points - 1).unwrap()
384 } else {
385 F::zero()
386 };
387
388 let (upward_changes, downward_changes, directional_stats) =
390 analyze_directional_changes(ts, &turning_points, config)?;
391
392 let momentum_features = analyze_momentum_persistence(ts, config)?;
394
395 let extrema_features = characterize_local_extrema(ts, &local_maxima, &local_minima)?;
397
398 let reversal_features = detect_trend_reversals(ts, &turning_points, config)?;
400
401 let temporal_features = if config.calculate_temporal_patterns {
403 analyze_temporal_patterns(&turning_points, config)?
404 } else {
405 TurningPointTemporalFeatures::default()
406 };
407
408 let stability_features = calculate_stability_measures(ts, &turning_points)?;
410
411 let pattern_features = if config.detect_advanced_patterns {
413 detect_advanced_patterns(ts, &local_maxima, &local_minima, config)?
414 } else {
415 AdvancedPatternFeatures::default()
416 };
417
418 let position_features = analyze_turning_point_positions(ts, &turning_points)?;
420
421 let multiscale_features = if config.multiscale_analysis {
423 analyze_multiscale_turning_points(ts, config)?
424 } else {
425 MultiscaleTurningPointFeatures::default()
426 };
427
428 Ok(TurningPointsFeatures {
429 total_turning_points,
431 local_minima_count,
432 local_maxima_count,
433 peak_valley_ratio,
434 average_turning_point_distance,
435
436 upward_changes,
438 downward_changes,
439 directional_change_ratio: directional_stats.directional_change_ratio,
440 average_upward_magnitude: directional_stats.average_upward_magnitude,
441 average_downward_magnitude: directional_stats.average_downward_magnitude,
442 directional_change_std: directional_stats.directional_change_std,
443
444 longest_upward_sequence: momentum_features.longest_upward_sequence,
446 longest_downward_sequence: momentum_features.longest_downward_sequence,
447 average_upward_sequence_length: momentum_features.average_upward_sequence_length,
448 average_downward_sequence_length: momentum_features.average_downward_sequence_length,
449 momentum_persistence_ratio: momentum_features.momentum_persistence_ratio,
450
451 average_peak_amplitude: extrema_features.average_peak_amplitude,
453 average_valley_amplitude: extrema_features.average_valley_amplitude,
454 peak_amplitude_std: extrema_features.peak_amplitude_std,
455 valley_amplitude_std: extrema_features.valley_amplitude_std,
456 peak_valley_amplitude_ratio: extrema_features.peak_valley_amplitude_ratio,
457 extrema_asymmetry: extrema_features.extrema_asymmetry,
458
459 major_trend_reversals: reversal_features.major_trend_reversals,
461 minor_trend_reversals: reversal_features.minor_trend_reversals,
462 average_major_reversal_magnitude: reversal_features.average_major_reversal_magnitude,
463 average_minor_reversal_magnitude: reversal_features.average_minor_reversal_magnitude,
464 trend_reversal_frequency: reversal_features.trend_reversal_frequency,
465 reversal_strength_index: reversal_features.reversal_strength_index,
466
467 turning_point_regularity: temporal_features.turning_point_regularity,
469 turning_point_clustering: temporal_features.turning_point_clustering,
470 turning_point_periodicity: temporal_features.turning_point_periodicity,
471 turning_point_autocorrelation: temporal_features.turning_point_autocorrelation,
472
473 turning_point_volatility: stability_features.turning_point_volatility,
475 stability_index: stability_features.stability_index,
476 noise_signal_ratio: stability_features.noise_signal_ratio,
477 trend_consistency: stability_features.trend_consistency,
478
479 double_peak_count: pattern_features.double_peak_count,
481 double_bottom_count: pattern_features.double_bottom_count,
482 head_shoulders_count: pattern_features.head_shoulders_count,
483 triangular_pattern_count: pattern_features.triangular_pattern_count,
484
485 upper_half_turning_points: position_features.upper_half_turning_points,
487 lower_half_turning_points: position_features.lower_half_turning_points,
488 turning_point_position_skewness: position_features.turning_point_position_skewness,
489 turning_point_position_kurtosis: position_features.turning_point_position_kurtosis,
490
491 multiscale_turning_points: multiscale_features.multiscale_turning_points,
493 scale_turning_point_ratio: multiscale_features.scale_turning_point_ratio,
494 cross_scale_consistency: multiscale_features.cross_scale_consistency,
495 hierarchical_structure_index: multiscale_features.hierarchical_structure_index,
496 })
497}
498
499#[allow(dead_code)]
505fn analyze_directional_changes<F>(
506 ts: &Array1<F>,
507 turning_points: &[usize],
508 _config: &TurningPointsConfig,
509) -> Result<(usize, usize, DirectionalChangeStats<F>)>
510where
511 F: Float + FromPrimitive + Debug + Clone,
512{
513 let mut upward_changes = 0;
514 let mut downward_changes = 0;
515 let mut upward_magnitudes = Vec::new();
516 let mut downward_magnitudes = Vec::new();
517
518 for window in turning_points.windows(2) {
520 let start_idx = window[0];
521 let end_idx = window[1];
522
523 if start_idx < ts.len() && end_idx < ts.len() {
524 let change = ts[end_idx] - ts[start_idx];
525 let magnitude = change.abs();
526
527 if change > F::zero() {
528 upward_changes += 1;
529 upward_magnitudes.push(magnitude);
530 } else if change < F::zero() {
531 downward_changes += 1;
532 downward_magnitudes.push(magnitude);
533 }
534 }
535 }
536
537 let directional_change_ratio = if downward_changes > 0 {
539 F::from(upward_changes).unwrap() / F::from(downward_changes).unwrap()
540 } else {
541 F::from(upward_changes).unwrap()
542 };
543
544 let average_upward_magnitude = if !upward_magnitudes.is_empty() {
545 upward_magnitudes.iter().fold(F::zero(), |acc, &x| acc + x)
546 / F::from(upward_magnitudes.len()).unwrap()
547 } else {
548 F::zero()
549 };
550
551 let average_downward_magnitude = if !downward_magnitudes.is_empty() {
552 downward_magnitudes
553 .iter()
554 .fold(F::zero(), |acc, &x| acc + x)
555 / F::from(downward_magnitudes.len()).unwrap()
556 } else {
557 F::zero()
558 };
559
560 let all_magnitudes: Vec<F> = upward_magnitudes
562 .into_iter()
563 .chain(downward_magnitudes)
564 .collect();
565
566 let directional_change_std = if all_magnitudes.len() > 1 {
567 let mean = all_magnitudes.iter().fold(F::zero(), |acc, &x| acc + x)
568 / F::from(all_magnitudes.len()).unwrap();
569 let variance = all_magnitudes
570 .iter()
571 .fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean))
572 / F::from(all_magnitudes.len() - 1).unwrap();
573 variance.sqrt()
574 } else {
575 F::zero()
576 };
577
578 let stats = DirectionalChangeStats {
579 directional_change_ratio,
580 average_upward_magnitude,
581 average_downward_magnitude,
582 directional_change_std,
583 };
584
585 Ok((upward_changes, downward_changes, stats))
586}
587
588#[allow(dead_code)]
590fn analyze_momentum_persistence<F>(
591 ts: &Array1<F>,
592 config: &TurningPointsConfig,
593) -> Result<MomentumFeatures<F>>
594where
595 F: Float + FromPrimitive + Debug,
596{
597 let n = ts.len();
598 let mut current_up_sequence = 0;
599 let mut current_down_sequence = 0;
600 let mut longest_upward_sequence = 0;
601 let mut longest_downward_sequence = 0;
602 let mut upward_sequences = Vec::new();
603 let mut downward_sequences = Vec::new();
604
605 for i in 1..n {
607 let change = ts[i] - ts[i - 1];
608
609 if change > F::zero() {
610 current_up_sequence += 1;
612 if current_down_sequence >= config.min_sequence_length {
613 downward_sequences.push(current_down_sequence);
614 }
615 current_down_sequence = 0;
616 } else if change < F::zero() {
617 current_down_sequence += 1;
619 if current_up_sequence >= config.min_sequence_length {
620 upward_sequences.push(current_up_sequence);
621 }
622 current_up_sequence = 0;
623 }
624
625 longest_upward_sequence = longest_upward_sequence.max(current_up_sequence);
626 longest_downward_sequence = longest_downward_sequence.max(current_down_sequence);
627 }
628
629 if current_up_sequence >= config.min_sequence_length {
631 upward_sequences.push(current_up_sequence);
632 }
633 if current_down_sequence >= config.min_sequence_length {
634 downward_sequences.push(current_down_sequence);
635 }
636
637 let average_upward_sequence_length = if !upward_sequences.is_empty() {
639 F::from(upward_sequences.iter().sum::<usize>()).unwrap()
640 / F::from(upward_sequences.len()).unwrap()
641 } else {
642 F::zero()
643 };
644
645 let average_downward_sequence_length = if !downward_sequences.is_empty() {
646 F::from(downward_sequences.iter().sum::<usize>()).unwrap()
647 / F::from(downward_sequences.len()).unwrap()
648 } else {
649 F::zero()
650 };
651
652 let long_sequences = upward_sequences
654 .iter()
655 .filter(|&&len| len >= config.min_sequence_length * 2)
656 .count()
657 + downward_sequences
658 .iter()
659 .filter(|&&len| len >= config.min_sequence_length * 2)
660 .count();
661 let total_sequences = upward_sequences.len() + downward_sequences.len();
662
663 let momentum_persistence_ratio = if total_sequences > 0 {
664 F::from(long_sequences).unwrap() / F::from(total_sequences).unwrap()
665 } else {
666 F::zero()
667 };
668
669 Ok(MomentumFeatures {
670 longest_upward_sequence,
671 longest_downward_sequence,
672 average_upward_sequence_length,
673 average_downward_sequence_length,
674 momentum_persistence_ratio,
675 })
676}
677
678#[allow(dead_code)]
680fn characterize_local_extrema<F>(
681 ts: &Array1<F>,
682 local_maxima: &[usize],
683 local_minima: &[usize],
684) -> Result<ExtremaFeatures<F>>
685where
686 F: Float + FromPrimitive + Debug,
687{
688 let peak_amplitudes: Vec<F> = local_maxima
690 .iter()
691 .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
692 .collect();
693
694 let valley_amplitudes: Vec<F> = local_minima
696 .iter()
697 .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
698 .collect();
699
700 let average_peak_amplitude = if !peak_amplitudes.is_empty() {
702 peak_amplitudes.iter().fold(F::zero(), |acc, &x| acc + x)
703 / F::from(peak_amplitudes.len()).unwrap()
704 } else {
705 F::zero()
706 };
707
708 let average_valley_amplitude = if !valley_amplitudes.is_empty() {
710 valley_amplitudes.iter().fold(F::zero(), |acc, &x| acc + x)
711 / F::from(valley_amplitudes.len()).unwrap()
712 } else {
713 F::zero()
714 };
715
716 let peak_amplitude_std = if peak_amplitudes.len() > 1 {
718 let variance = peak_amplitudes.iter().fold(F::zero(), |acc, &x| {
719 acc + (x - average_peak_amplitude) * (x - average_peak_amplitude)
720 }) / F::from(peak_amplitudes.len() - 1).unwrap();
721 variance.sqrt()
722 } else {
723 F::zero()
724 };
725
726 let valley_amplitude_std = if valley_amplitudes.len() > 1 {
728 let variance = valley_amplitudes.iter().fold(F::zero(), |acc, &x| {
729 acc + (x - average_valley_amplitude) * (x - average_valley_amplitude)
730 }) / F::from(valley_amplitudes.len() - 1).unwrap();
731 variance.sqrt()
732 } else {
733 F::zero()
734 };
735
736 let peak_valley_amplitude_ratio = if average_valley_amplitude != F::zero() {
738 average_peak_amplitude / average_valley_amplitude
739 } else {
740 F::one()
741 };
742
743 let all_extrema: Vec<F> = peak_amplitudes
745 .into_iter()
746 .chain(valley_amplitudes)
747 .collect();
748
749 let extrema_asymmetry = if all_extrema.len() > 2 {
750 let mean = all_extrema.iter().fold(F::zero(), |acc, &x| acc + x)
751 / F::from(all_extrema.len()).unwrap();
752 let variance = all_extrema
753 .iter()
754 .fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean))
755 / F::from(all_extrema.len()).unwrap();
756
757 if variance > F::zero() {
758 let std_dev = variance.sqrt();
759 let skewness = all_extrema.iter().fold(F::zero(), |acc, &x| {
760 let normalized = (x - mean) / std_dev;
761 acc + normalized * normalized * normalized
762 }) / F::from(all_extrema.len()).unwrap();
763 skewness
764 } else {
765 F::zero()
766 }
767 } else {
768 F::zero()
769 };
770
771 Ok(ExtremaFeatures {
772 average_peak_amplitude,
773 average_valley_amplitude,
774 peak_amplitude_std,
775 valley_amplitude_std,
776 peak_valley_amplitude_ratio,
777 extrema_asymmetry,
778 })
779}
780
781#[allow(dead_code)]
783fn detect_trend_reversals<F>(
784 ts: &Array1<F>,
785 turning_points: &[usize],
786 config: &TurningPointsConfig,
787) -> Result<TrendReversalFeatures<F>>
788where
789 F: Float + FromPrimitive + Debug,
790{
791 let major_threshold = F::from(config.major_reversal_threshold).unwrap();
792 let mut major_reversals = Vec::new();
793 let mut minor_reversals = Vec::new();
794
795 let min_val = ts.iter().fold(F::infinity(), |a, &b| a.min(b));
797 let max_val = ts.iter().fold(F::neg_infinity(), |a, &b| a.max(b));
798 let range = max_val - min_val;
799 let major_abs_threshold = major_threshold * range;
800
801 for window in turning_points.windows(2) {
803 let start_idx = window[0];
804 let end_idx = window[1];
805
806 if start_idx < ts.len() && end_idx < ts.len() {
807 let change_magnitude = (ts[end_idx] - ts[start_idx]).abs();
808
809 if change_magnitude >= major_abs_threshold {
810 major_reversals.push(change_magnitude);
811 } else {
812 minor_reversals.push(change_magnitude);
813 }
814 }
815 }
816
817 let major_trend_reversals = major_reversals.len();
818 let minor_trend_reversals = minor_reversals.len();
819
820 let average_major_reversal_magnitude = if !major_reversals.is_empty() {
821 major_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
822 / F::from(major_reversals.len()).unwrap()
823 } else {
824 F::zero()
825 };
826
827 let average_minor_reversal_magnitude = if !minor_reversals.is_empty() {
828 minor_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
829 / F::from(minor_reversals.len()).unwrap()
830 } else {
831 F::zero()
832 };
833
834 let trend_reversal_frequency = F::from(major_trend_reversals + minor_trend_reversals).unwrap()
835 / F::from(ts.len()).unwrap();
836
837 let reversal_strength_index = major_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
838 + minor_reversals.iter().fold(F::zero(), |acc, &x| acc + x);
839
840 Ok(TrendReversalFeatures {
841 major_trend_reversals,
842 minor_trend_reversals,
843 average_major_reversal_magnitude,
844 average_minor_reversal_magnitude,
845 trend_reversal_frequency,
846 reversal_strength_index,
847 })
848}
849
850#[allow(dead_code)]
852fn analyze_temporal_patterns<F>(
853 turning_points: &[usize],
854 config: &TurningPointsConfig,
855) -> Result<TurningPointTemporalFeatures<F>>
856where
857 F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
858{
859 if turning_points.len() < 3 {
860 return Ok(TurningPointTemporalFeatures::default());
861 }
862
863 let intervals: Vec<F> = turning_points
865 .windows(2)
866 .map(|w| F::from(w[1] - w[0]).unwrap())
867 .collect();
868
869 let mean_interval =
871 intervals.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(intervals.len()).unwrap();
872
873 let interval_variance = if intervals.len() > 1 {
874 intervals.iter().fold(F::zero(), |acc, &x| {
875 acc + (x - mean_interval) * (x - mean_interval)
876 }) / F::from(intervals.len() - 1).unwrap()
877 } else {
878 F::zero()
879 };
880
881 let turning_point_regularity = if mean_interval > F::zero() {
882 interval_variance.sqrt() / mean_interval
883 } else {
884 F::zero()
885 };
886
887 let turning_point_clustering = calculate_clustering_coefficient(&intervals)?;
889
890 let turning_point_periodicity = calculate_periodicity_strength(&intervals)?;
892
893 let max_lag = config.max_autocorr_lag.min(intervals.len() / 2);
895 let turning_point_autocorrelation = if max_lag > 0 {
896 calculate_autocorrelation_at_lag(&intervals, 1)?
897 } else {
898 F::zero()
899 };
900
901 Ok(TurningPointTemporalFeatures {
902 turning_point_regularity,
903 turning_point_clustering,
904 turning_point_periodicity,
905 turning_point_autocorrelation,
906 })
907}
908
909#[allow(dead_code)]
911fn calculate_stability_measures<F>(
912 ts: &Array1<F>,
913 turning_points: &[usize],
914) -> Result<StabilityFeatures<F>>
915where
916 F: Float + FromPrimitive + Debug,
917{
918 let n = ts.len();
919
920 let mut local_variances = Vec::new();
922 let window_size = 5; for &tp_idx in turning_points {
925 let start = tp_idx.saturating_sub(window_size / 2);
926 let end = (tp_idx + window_size / 2 + 1).min(n);
927
928 if end > start + 1 {
929 let local_slice = ts.slice(s![start..end]);
930 let local_mean = local_slice.sum() / F::from(local_slice.len()).unwrap();
931 let local_variance = local_slice
932 .mapv(|x| (x - local_mean) * (x - local_mean))
933 .sum()
934 / F::from(local_slice.len()).unwrap();
935 local_variances.push(local_variance);
936 }
937 }
938
939 let turning_point_volatility = if !local_variances.is_empty() {
940 local_variances.iter().fold(F::zero(), |acc, &x| acc + x)
941 / F::from(local_variances.len()).unwrap()
942 } else {
943 F::zero()
944 };
945
946 let turning_point_frequency = F::from(turning_points.len()).unwrap() / F::from(n).unwrap();
948 let stability_index = if turning_point_frequency > F::zero() {
949 F::one() / turning_point_frequency
950 } else {
951 F::from(n).unwrap()
952 };
953
954 let signal_mean = ts.sum() / F::from(n).unwrap();
956 let signal_variance =
957 ts.mapv(|x| (x - signal_mean) * (x - signal_mean)).sum() / F::from(n).unwrap();
958 let noise_signal_ratio = if signal_variance > F::zero() {
959 turning_point_volatility / signal_variance
960 } else {
961 F::zero()
962 };
963
964 let mut directional_changes = 0;
966 for i in 1..n {
967 if i >= 2 {
968 let prev_change = ts[i - 1] - ts[i - 2];
969 let curr_change = ts[i] - ts[i - 1];
970 if (prev_change > F::zero()) != (curr_change > F::zero()) {
971 directional_changes += 1;
972 }
973 }
974 }
975
976 let trend_consistency =
977 F::one() - F::from(directional_changes).unwrap() / F::from(n - 2).unwrap();
978
979 Ok(StabilityFeatures {
980 turning_point_volatility,
981 stability_index,
982 noise_signal_ratio,
983 trend_consistency,
984 })
985}
986
987#[allow(dead_code)]
989fn detect_advanced_patterns<F>(
990 ts: &Array1<F>,
991 local_maxima: &[usize],
992 local_minima: &[usize],
993 _config: &TurningPointsConfig,
994) -> Result<AdvancedPatternFeatures>
995where
996 F: Float + FromPrimitive + Debug + PartialOrd,
997{
998 let double_peak_count = detect_double_peaks(ts, local_maxima)?;
1000
1001 let double_bottom_count = detect_double_bottoms(ts, local_minima)?;
1003
1004 let head_shoulders_count = detect_head_and_shoulders(ts, local_maxima, local_minima)?;
1006
1007 let triangular_pattern_count = detect_triangular_patterns(ts, local_maxima, local_minima)?;
1009
1010 Ok(AdvancedPatternFeatures {
1011 double_peak_count,
1012 double_bottom_count,
1013 head_shoulders_count,
1014 triangular_pattern_count,
1015 })
1016}
1017
1018#[allow(dead_code)]
1020fn analyze_turning_point_positions<F>(
1021 ts: &Array1<F>,
1022 turning_points: &[usize],
1023) -> Result<PositionFeatures<F>>
1024where
1025 F: Float + FromPrimitive + Debug,
1026{
1027 if turning_points.is_empty() {
1028 return Ok(PositionFeatures {
1029 upper_half_turning_points: F::from(0.5).unwrap(),
1030 lower_half_turning_points: F::from(0.5).unwrap(),
1031 turning_point_position_skewness: F::zero(),
1032 turning_point_position_kurtosis: F::zero(),
1033 });
1034 }
1035
1036 let min_val = ts.iter().fold(F::infinity(), |a, &b| a.min(b));
1038 let max_val = ts.iter().fold(F::neg_infinity(), |a, &b| a.max(b));
1039 let range = max_val - min_val;
1040 let midpoint = min_val + range / F::from(2.0).unwrap();
1041
1042 let tp_values: Vec<F> = turning_points
1044 .iter()
1045 .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
1046 .collect();
1047
1048 let upper_half_count = tp_values.iter().filter(|&&x| x > midpoint).count();
1049 let total_count = tp_values.len();
1050
1051 let upper_half_turning_points =
1052 F::from(upper_half_count).unwrap() / F::from(total_count).unwrap();
1053 let lower_half_turning_points = F::one() - upper_half_turning_points;
1054
1055 let mean_position =
1057 tp_values.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(total_count).unwrap();
1058
1059 let variance = tp_values.iter().fold(F::zero(), |acc, &x| {
1060 acc + (x - mean_position) * (x - mean_position)
1061 }) / F::from(total_count).unwrap();
1062
1063 let (turning_point_position_skewness, turning_point_position_kurtosis) = if variance > F::zero()
1064 {
1065 let std_dev = variance.sqrt();
1066
1067 let skewness = tp_values.iter().fold(F::zero(), |acc, &x| {
1068 let normalized = (x - mean_position) / std_dev;
1069 acc + normalized * normalized * normalized
1070 }) / F::from(total_count).unwrap();
1071
1072 let kurtosis = tp_values.iter().fold(F::zero(), |acc, &x| {
1073 let normalized = (x - mean_position) / std_dev;
1074 let normalized_sq = normalized * normalized;
1075 acc + normalized_sq * normalized_sq
1076 }) / F::from(total_count).unwrap()
1077 - F::from(3.0).unwrap();
1078
1079 (skewness, kurtosis)
1080 } else {
1081 (F::zero(), F::zero())
1082 };
1083
1084 Ok(PositionFeatures {
1085 upper_half_turning_points,
1086 lower_half_turning_points,
1087 turning_point_position_skewness,
1088 turning_point_position_kurtosis,
1089 })
1090}
1091
1092#[allow(dead_code)]
1094fn analyze_multiscale_turning_points<F>(
1095 ts: &Array1<F>,
1096 config: &TurningPointsConfig,
1097) -> Result<MultiscaleTurningPointFeatures<F>>
1098where
1099 F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
1100{
1101 let mut multiscale_turning_points = Vec::new();
1102 let mut scale_consistencies = Vec::new();
1103
1104 for &window_size in &config.smoothing_windows {
1106 let smoothed = apply_moving_average(ts, window_size)?;
1108
1109 let smoothed_config = TurningPointsConfig {
1111 min_turning_point_threshold: config.min_turning_point_threshold,
1112 extrema_window_size: config.extrema_window_size,
1113 major_reversal_threshold: config.major_reversal_threshold,
1114 detect_advanced_patterns: false,
1115 smoothing_windows: vec![],
1116 calculate_temporal_patterns: false,
1117 max_autocorr_lag: 0,
1118 analyze_clustering: false,
1119 min_sequence_length: config.min_sequence_length,
1120 multiscale_analysis: false,
1121 };
1122
1123 let (tp__, _, _) = detect_turning_points(&smoothed, &smoothed_config)?;
1125 multiscale_turning_points.push(tp__.len());
1126
1127 if !multiscale_turning_points.is_empty() {
1129 let original_count = multiscale_turning_points[0] as f64;
1130 let current_count = tp__.len() as f64;
1131 let consistency =
1132 1.0 - (original_count - current_count).abs() / original_count.max(current_count);
1133 scale_consistencies.push(F::from(consistency).unwrap());
1134 }
1135 }
1136
1137 let scale_turning_point_ratio = if multiscale_turning_points.len() > 1 {
1139 let first_scale = F::from(multiscale_turning_points[0]).unwrap();
1140 let last_scale = F::from(*multiscale_turning_points.last().unwrap()).unwrap();
1141 if last_scale > F::zero() {
1142 first_scale / last_scale
1143 } else {
1144 F::one()
1145 }
1146 } else {
1147 F::one()
1148 };
1149
1150 let cross_scale_consistency = if !scale_consistencies.is_empty() {
1152 scale_consistencies
1153 .iter()
1154 .fold(F::zero(), |acc, &x| acc + x)
1155 / F::from(scale_consistencies.len()).unwrap()
1156 } else {
1157 F::zero()
1158 };
1159
1160 let hierarchical_structure_index = if multiscale_turning_points.len() > 2 {
1162 let mut structure_measure = F::zero();
1163 for i in 1..multiscale_turning_points.len() {
1164 let ratio = F::from(multiscale_turning_points[i - 1]).unwrap()
1165 / F::from(multiscale_turning_points[i])
1166 .unwrap()
1167 .max(F::from(1.0).unwrap());
1168 structure_measure = structure_measure + ratio;
1169 }
1170 structure_measure / F::from(multiscale_turning_points.len() - 1).unwrap()
1171 } else {
1172 F::one()
1173 };
1174
1175 Ok(MultiscaleTurningPointFeatures {
1176 multiscale_turning_points,
1177 scale_turning_point_ratio,
1178 cross_scale_consistency,
1179 hierarchical_structure_index,
1180 })
1181}
1182
1183#[allow(dead_code)]
1189fn apply_moving_average<F>(_ts: &Array1<F>, windowsize: usize) -> Result<Array1<F>>
1190where
1191 F: Float + FromPrimitive + Clone,
1192{
1193 let n = _ts.len();
1194 if windowsize >= n {
1195 return Ok(_ts.clone());
1196 }
1197
1198 let mut smoothed = Array1::zeros(n);
1199 let half_window = windowsize / 2;
1200
1201 for i in 0..n {
1202 let start = i.saturating_sub(half_window);
1203 let end = (i + half_window + 1).min(n);
1204
1205 let window_sum = _ts.slice(s![start..end]).sum();
1206 let window_len = F::from(end - start).unwrap();
1207 smoothed[i] = window_sum / window_len;
1208 }
1209
1210 Ok(smoothed)
1211}
1212
1213#[allow(dead_code)]
1215fn calculate_clustering_coefficient<F>(intervals: &[F]) -> Result<F>
1216where
1217 F: Float + FromPrimitive,
1218{
1219 if intervals.len() < 3 {
1220 return Ok(F::zero());
1221 }
1222
1223 let mut ratios = Vec::new();
1225 for i in 1..intervals.len() {
1226 if intervals[i] > F::zero() && intervals[i - 1] > F::zero() {
1227 ratios.push(intervals[i] / intervals[i - 1]);
1228 }
1229 }
1230
1231 if ratios.len() < 2 {
1232 return Ok(F::zero());
1233 }
1234
1235 let mean_ratio =
1236 ratios.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(ratios.len()).unwrap();
1237 let variance = ratios.iter().fold(F::zero(), |acc, &x| {
1238 acc + (x - mean_ratio) * (x - mean_ratio)
1239 }) / F::from(ratios.len()).unwrap();
1240
1241 Ok(F::one() / (F::one() + variance))
1243}
1244
1245#[allow(dead_code)]
1247fn calculate_periodicity_strength<F>(intervals: &[F]) -> Result<F>
1248where
1249 F: Float + FromPrimitive,
1250{
1251 if intervals.len() < 4 {
1252 return Ok(F::zero());
1253 }
1254
1255 calculate_autocorrelation_at_lag(intervals, 1)
1257}
1258
1259#[allow(dead_code)]
1261fn calculate_autocorrelation_at_lag<F>(data: &[F], lag: usize) -> Result<F>
1262where
1263 F: Float + FromPrimitive,
1264{
1265 if data.len() <= lag {
1266 return Ok(F::zero());
1267 }
1268
1269 let n = data.len() - lag;
1270 if n < 2 {
1271 return Ok(F::zero());
1272 }
1273
1274 let mean = data.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(data.len()).unwrap();
1275
1276 let mut numerator = F::zero();
1277 let mut denominator = F::zero();
1278
1279 for i in 0..n {
1280 let x_centered = data[i] - mean;
1281 let y_centered = data[i + lag] - mean;
1282 numerator = numerator + x_centered * y_centered;
1283 denominator = denominator + x_centered * x_centered;
1284 }
1285
1286 if denominator > F::zero() {
1287 Ok(numerator / denominator)
1288 } else {
1289 Ok(F::zero())
1290 }
1291}
1292
1293#[allow(dead_code)]
1295fn detect_double_peaks<F>(_ts: &Array1<F>, localmaxima: &[usize]) -> Result<usize>
1296where
1297 F: Float + FromPrimitive + PartialOrd,
1298{
1299 let mut count = 0;
1301 for window in localmaxima.windows(3) {
1302 let spacing1 = window[1] - window[0];
1303 let spacing2 = window[2] - window[1];
1304
1305 if spacing1 > 0 && spacing2 > 0 {
1307 let ratio = spacing1 as f64 / spacing2 as f64;
1308 if (0.5..=2.0).contains(&ratio) {
1309 count += 1;
1310 }
1311 }
1312 }
1313 Ok(count)
1314}
1315
1316#[allow(dead_code)]
1317fn detect_double_bottoms<F>(_ts: &Array1<F>, localminima: &[usize]) -> Result<usize>
1318where
1319 F: Float + FromPrimitive + PartialOrd,
1320{
1321 let mut count = 0;
1323 for window in localminima.windows(3) {
1324 let spacing1 = window[1] - window[0];
1325 let spacing2 = window[2] - window[1];
1326
1327 if spacing1 > 0 && spacing2 > 0 {
1329 let ratio = spacing1 as f64 / spacing2 as f64;
1330 if (0.5..=2.0).contains(&ratio) {
1331 count += 1;
1332 }
1333 }
1334 }
1335 Ok(count)
1336}
1337
1338#[allow(dead_code)]
1339fn detect_head_and_shoulders<F>(
1340 _ts: &Array1<F>,
1341 local_maxima: &[usize],
1342 _local_minima: &[usize],
1343) -> Result<usize>
1344where
1345 F: Float + FromPrimitive + PartialOrd,
1346{
1347 let mut count = 0;
1349 if local_maxima.len() >= 3 {
1350 for window in local_maxima.windows(3) {
1351 let spacing1 = window[1] - window[0];
1353 let spacing2 = window[2] - window[1];
1354
1355 if spacing1 > 0 && spacing2 > 0 && spacing1 <= spacing2 * 2 && spacing2 <= spacing1 * 2
1356 {
1357 count += 1;
1358 }
1359 }
1360 }
1361 Ok(count)
1362}
1363
1364#[allow(dead_code)]
1365fn detect_triangular_patterns<F>(
1366 _ts: &Array1<F>,
1367 local_maxima: &[usize],
1368 local_minima: &[usize],
1369) -> Result<usize>
1370where
1371 F: Float + FromPrimitive + PartialOrd,
1372{
1373 let min_pattern_length = 4;
1375 let mut count = 0;
1376
1377 if local_maxima.len() >= 2 && local_minima.len() >= 2 {
1378 for i in 0..(local_maxima.len().saturating_sub(min_pattern_length)) {
1380 let peak_range_start = local_maxima[i + 1] - local_maxima[i];
1381 let peak_range_end = if i + 3 < local_maxima.len() {
1382 local_maxima[i + 3] - local_maxima[i + 2]
1383 } else {
1384 continue;
1385 };
1386
1387 if peak_range_end > 0 && peak_range_start > peak_range_end {
1389 count += 1;
1390 }
1391 }
1392 }
1393
1394 Ok(count)
1395}