1use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::{ClusteringError, Result};
14use crate::hierarchy::{fcluster, ClusterCriterion};
15
16#[allow(dead_code)]
44pub fn dtw_distance<F>(
45 series1: ArrayView1<F>,
46 series2: ArrayView1<F>,
47 window: Option<usize>,
48) -> Result<F>
49where
50 F: Float + FromPrimitive + Debug + 'static,
51{
52 let n = series1.len();
53 let m = series2.len();
54
55 if n == 0 || m == 0 {
56 return Err(ClusteringError::InvalidInput(
57 "Time series cannot be empty".to_string(),
58 ));
59 }
60
61 let mut dtw = Array2::from_elem((n + 1, m + 1), F::infinity());
63 dtw[[0, 0]] = F::zero();
64
65 let effective_window = window.unwrap_or(m.max(n));
67
68 for i in 1..=n {
69 let start_j = if effective_window < i {
70 i - effective_window
71 } else {
72 1
73 };
74 let end_j = (i + effective_window).min(m + 1);
75
76 for j in start_j..end_j {
77 if j <= m {
78 let cost = (series1[i - 1] - series2[j - 1]).abs();
79
80 let candidates = [
81 dtw[[i - 1, j]], dtw[[i, j - 1]], dtw[[i - 1, j - 1]], ];
85
86 let min_prev = candidates.iter().fold(F::infinity(), |acc, &x| acc.min(x));
87 dtw[[i, j]] = cost + min_prev;
88 }
89 }
90 }
91
92 Ok(dtw[[n, m]])
93}
94
95#[allow(dead_code)]
110pub fn dtw_distance_custom<F, D>(
111 series1: ArrayView1<F>,
112 series2: ArrayView1<F>,
113 local_distance: D,
114 window: Option<usize>,
115) -> Result<F>
116where
117 F: Float + FromPrimitive + Debug + 'static,
118 D: Fn(F, F) -> F,
119{
120 let n = series1.len();
121 let m = series2.len();
122
123 if n == 0 || m == 0 {
124 return Err(ClusteringError::InvalidInput(
125 "Time series cannot be empty".to_string(),
126 ));
127 }
128
129 let mut dtw = Array2::from_elem((n + 1, m + 1), F::infinity());
130 dtw[[0, 0]] = F::zero();
131
132 let effective_window = window.unwrap_or(m.max(n));
133
134 for i in 1..=n {
135 let start_j = if effective_window < i {
136 i - effective_window
137 } else {
138 1
139 };
140 let end_j = (i + effective_window).min(m + 1);
141
142 for j in start_j..end_j {
143 if j <= m {
144 let cost = local_distance(series1[i - 1], series2[j - 1]);
145
146 let candidates = [dtw[[i - 1, j]], dtw[[i, j - 1]], dtw[[i - 1, j - 1]]];
147
148 let min_prev = candidates.iter().fold(F::infinity(), |acc, &x| acc.min(x));
149 dtw[[i, j]] = cost + min_prev;
150 }
151 }
152 }
153
154 Ok(dtw[[n, m]])
155}
156
157#[allow(dead_code)]
173pub fn soft_dtw_distance<F>(series1: ArrayView1<F>, series2: ArrayView1<F>, gamma: F) -> Result<F>
174where
175 F: Float + FromPrimitive + Debug + 'static,
176{
177 let n = series1.len();
178 let m = series2.len();
179
180 if n == 0 || m == 0 {
181 return Err(ClusteringError::InvalidInput(
182 "Time series cannot be empty".to_string(),
183 ));
184 }
185
186 if gamma <= F::zero() {
187 return Err(ClusteringError::InvalidInput(
188 "Gamma must be positive".to_string(),
189 ));
190 }
191
192 let mut dtw = Array2::from_elem((n + 1, m + 1), F::infinity());
193 dtw[[0, 0]] = F::zero();
194
195 for i in 1..=n {
196 for j in 1..=m {
197 let cost = (series1[i - 1] - series2[j - 1]).powi(2);
198
199 let candidates = [dtw[[i - 1, j]], dtw[[i, j - 1]], dtw[[i - 1, j - 1]]];
200
201 let min_val = candidates.iter().fold(F::infinity(), |acc, &x| acc.min(x));
204 let sum_exp = candidates
205 .iter()
206 .map(|&x| (-(x - min_val) / gamma).exp())
207 .fold(F::zero(), |acc, x| acc + x);
208
209 let soft_min = min_val - gamma * sum_exp.ln();
210 dtw[[i, j]] = cost + soft_min;
211 }
212 }
213
214 Ok(dtw[[n, m]])
215}
216
217#[allow(dead_code)]
234pub fn dtw_k_medoids<F>(
235 time_series: ArrayView2<F>,
236 k: usize,
237 max_iterations: usize,
238 window: Option<usize>,
239) -> Result<(Array1<usize>, Array1<usize>)>
240where
241 F: Float + FromPrimitive + Debug + 'static,
242{
243 let n_series = time_series.nrows();
244
245 if k > n_series {
246 return Err(ClusteringError::InvalidInput(
247 "Number of clusters cannot exceed number of time _series".to_string(),
248 ));
249 }
250
251 if n_series == 0 {
252 return Err(ClusteringError::InvalidInput(
253 "No time _series provided".to_string(),
254 ));
255 }
256
257 let mut medoids: Array1<usize> = Array1::from_iter(0..k);
259 let mut assignments = Array1::zeros(n_series);
260
261 for _iteration in 0..max_iterations {
262 let mut changed = false;
263
264 for i in 0..n_series {
266 let mut min_distance = F::infinity();
267 let mut best_cluster = 0;
268
269 for (cluster_id, &medoid_idx) in medoids.iter().enumerate() {
270 let distance =
271 dtw_distance(time_series.row(i), time_series.row(medoid_idx), window)?;
272
273 if distance < min_distance {
274 min_distance = distance;
275 best_cluster = cluster_id;
276 }
277 }
278
279 if assignments[i] != best_cluster {
280 assignments[i] = best_cluster;
281 changed = true;
282 }
283 }
284
285 for cluster_id in 0..k {
287 let cluster_members: Vec<usize> = assignments
288 .iter()
289 .enumerate()
290 .filter(|(_, &assignment)| assignment == cluster_id)
291 .map(|(idx, _)| idx)
292 .collect();
293
294 if !cluster_members.is_empty() {
295 let mut best_medoid = medoids[cluster_id];
296 let mut min_total_distance = F::infinity();
297
298 for &candidate in &cluster_members {
300 let mut total_distance = F::zero();
301
302 for &member in &cluster_members {
303 if candidate != member {
304 let distance = dtw_distance(
305 time_series.row(candidate),
306 time_series.row(member),
307 window,
308 )?;
309 total_distance = total_distance + distance;
310 }
311 }
312
313 if total_distance < min_total_distance {
314 min_total_distance = total_distance;
315 best_medoid = candidate;
316 }
317 }
318
319 if medoids[cluster_id] != best_medoid {
320 medoids[cluster_id] = best_medoid;
321 changed = true;
322 }
323 }
324 }
325
326 if !changed {
327 break;
328 }
329 }
330
331 Ok((medoids, assignments))
332}
333
334#[allow(dead_code)]
348pub fn dtw_hierarchical_clustering<F>(
349 time_series: ArrayView2<F>,
350 window: Option<usize>,
351) -> Result<Array2<F>>
352where
353 F: Float + FromPrimitive + Debug + 'static,
354{
355 let n_series = time_series.nrows();
356
357 if n_series < 2 {
358 return Err(ClusteringError::InvalidInput(
359 "Need at least 2 time _series for clustering".to_string(),
360 ));
361 }
362
363 let mut distances = Array2::zeros((n_series, n_series));
365 for i in 0..n_series {
366 for j in (i + 1)..n_series {
367 let distance = dtw_distance(time_series.row(i), time_series.row(j), window)?;
368 distances[[i, j]] = distance;
369 distances[[j, i]] = distance;
370 }
371 }
372
373 let mut clusters: Vec<Vec<usize>> = (0..n_series).map(|i| vec![i]).collect();
377 let mut active_ids: Vec<usize> = (0..n_series).collect();
378 let mut linkage = Vec::new();
379 let mut next_cluster_id = n_series;
380
381 while clusters.len() > 1 {
382 let mut min_distance = F::infinity();
384 let mut merge_i = 0;
385 let mut merge_j = 1;
386
387 for i in 0..clusters.len() {
388 for j in (i + 1)..clusters.len() {
389 let mut max_dist = F::zero();
391 for &point_i in &clusters[i] {
392 for &point_j in &clusters[j] {
393 max_dist = max_dist.max(distances[[point_i, point_j]]);
394 }
395 }
396
397 if max_dist < min_distance {
398 min_distance = max_dist;
399 merge_i = i;
400 merge_j = j;
401 }
402 }
403 }
404
405 let cluster_i_size = clusters[merge_i].len();
407 let cluster_j_size = clusters[merge_j].len();
408 let id_i = active_ids[merge_i];
409 let id_j = active_ids[merge_j];
410 let (lo, hi) = if id_i <= id_j {
412 (id_i, id_j)
413 } else {
414 (id_j, id_i)
415 };
416
417 linkage.push([
418 F::from(lo).expect("Failed to convert cluster id to float"),
419 F::from(hi).expect("Failed to convert cluster id to float"),
420 min_distance,
421 F::from(cluster_i_size + cluster_j_size).expect("Failed to convert size to float"),
422 ]);
423
424 let mut new_cluster = clusters[merge_i].clone();
426 new_cluster.extend(&clusters[merge_j]);
427
428 let (first, second) = if merge_i > merge_j {
430 (merge_i, merge_j)
431 } else {
432 (merge_j, merge_i)
433 };
434
435 clusters.remove(first);
436 clusters.remove(second);
437 active_ids.remove(first);
438 active_ids.remove(second);
439
440 clusters.push(new_cluster);
441 active_ids.push(next_cluster_id);
442 next_cluster_id += 1;
443 }
444
445 let linkage_array =
447 Array2::from_shape_vec((linkage.len(), 4), linkage.into_iter().flatten().collect())
448 .map_err(|_| {
449 ClusteringError::ComputationError("Failed to create linkage matrix".to_string())
450 })?;
451
452 Ok(linkage_array)
453}
454
455#[allow(dead_code)]
471pub fn dtw_k_means<F>(
472 time_series: ArrayView2<F>,
473 k: usize,
474 max_iterations: usize,
475 tolerance: F,
476) -> Result<(Array2<F>, Array1<usize>)>
477where
478 F: Float + FromPrimitive + Debug + 'static,
479{
480 let n_series = time_series.nrows();
481 let series_length = time_series.ncols();
482
483 if k > n_series {
484 return Err(ClusteringError::InvalidInput(
485 "Number of clusters cannot exceed number of time _series".to_string(),
486 ));
487 }
488
489 let mut centers = Array2::zeros((k, series_length));
491 for i in 0..k {
492 centers.row_mut(i).assign(&time_series.row(i));
493 }
494
495 let mut assignments = Array1::zeros(n_series);
496
497 for _iteration in 0..max_iterations {
498 let mut changed = false;
499
500 for i in 0..n_series {
502 let mut min_distance = F::infinity();
503 let mut best_cluster = 0;
504
505 for j in 0..k {
506 let distance = dtw_distance(time_series.row(i), centers.row(j), None)?;
507
508 if distance < min_distance {
509 min_distance = distance;
510 best_cluster = j;
511 }
512 }
513
514 if assignments[i] != best_cluster {
515 assignments[i] = best_cluster;
516 changed = true;
517 }
518 }
519
520 if !changed {
521 break;
522 }
523
524 let mut center_changed = false;
526 for cluster_id in 0..k {
527 let cluster_members: Vec<usize> = assignments
528 .iter()
529 .enumerate()
530 .filter(|(_, &assignment)| assignment == cluster_id)
531 .map(|(idx, _)| idx)
532 .collect();
533
534 if !cluster_members.is_empty() {
535 let new_center = dtw_barycenter_averaging(
536 &time_series.select(Axis(0), &cluster_members),
537 10,
538 tolerance,
539 )?;
540
541 let center_distance =
542 dtw_distance(centers.row(cluster_id), new_center.view(), None)?;
543
544 if center_distance > tolerance {
545 center_changed = true;
546 }
547
548 centers.row_mut(cluster_id).assign(&new_center);
549 }
550 }
551
552 if !center_changed {
553 break;
554 }
555 }
556
557 Ok((centers, assignments))
558}
559
560#[allow(dead_code)]
575pub fn dtw_barycenter_averaging<F>(
576 time_series: &Array2<F>,
577 max_iterations: usize,
578 tolerance: F,
579) -> Result<Array1<F>>
580where
581 F: Float + FromPrimitive + Debug + 'static,
582{
583 let n_series = time_series.nrows();
584 let series_length = time_series.ncols();
585
586 if n_series == 0 {
587 return Err(ClusteringError::InvalidInput(
588 "No time _series provided".to_string(),
589 ));
590 }
591
592 if n_series == 1 {
593 return Ok(time_series.row(0).to_owned());
594 }
595
596 let mut barycenter = time_series.mean_axis(Axis(0)).expect("Operation failed");
598
599 for _iteration in 0..max_iterations {
600 let mut new_barycenter = Array1::zeros(series_length);
601 let mut weights = Array1::zeros(series_length);
602
603 for i in 0..n_series {
605 let (aligned_series, alignment_weights) =
606 dtw_align_series(time_series.row(i), barycenter.view())?;
607
608 new_barycenter = new_barycenter + aligned_series;
609 weights = weights + alignment_weights;
610 }
611
612 for i in 0..series_length {
614 if weights[i] > F::zero() {
615 new_barycenter[i] = new_barycenter[i] / weights[i];
616 }
617 }
618
619 let change = dtw_distance(barycenter.view(), new_barycenter.view(), None)?;
621 if change < tolerance {
622 break;
623 }
624
625 barycenter = new_barycenter;
626 }
627
628 Ok(barycenter)
629}
630
631#[allow(dead_code)]
633fn dtw_align_series<F>(
634 series: ArrayView1<F>,
635 reference: ArrayView1<F>,
636) -> Result<(Array1<F>, Array1<F>)>
637where
638 F: Float + FromPrimitive + Debug + 'static,
639{
640 let n = series.len();
641 let m = reference.len();
642
643 let mut dtw = Array2::from_elem((n + 1, m + 1), F::infinity());
645 dtw[[0, 0]] = F::zero();
646
647 for i in 1..=n {
648 for j in 1..=m {
649 let cost = (series[i - 1] - reference[j - 1]).abs();
650 let min_prev = [dtw[[i - 1, j]], dtw[[i, j - 1]], dtw[[i - 1, j - 1]]]
651 .iter()
652 .fold(F::infinity(), |acc, &x| acc.min(x));
653
654 dtw[[i, j]] = cost + min_prev;
655 }
656 }
657
658 let mut i = n;
660 let mut j = m;
661 let mut aligned_series = Array1::zeros(m);
662 let mut weights = Array1::zeros(m);
663
664 while i > 0 && j > 0 {
665 aligned_series[j - 1] = aligned_series[j - 1] + series[i - 1];
667 weights[j - 1] = weights[j - 1] + F::one();
668
669 let candidates = [
671 (dtw[[i - 1, j - 1]], (i - 1, j - 1)), (dtw[[i - 1, j]], (i - 1, j)), (dtw[[i, j - 1]], (i, j - 1)), ];
675
676 let (_, (next_i, next_j)) = candidates
677 .iter()
678 .min_by(|a, b| a.0.partial_cmp(&b.0).expect("Operation failed"))
679 .expect("Operation failed");
680
681 i = *next_i;
682 j = *next_j;
683 }
684
685 Ok((aligned_series, weights))
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct TimeSeriesClusteringConfig {
691 pub algorithm: TimeSeriesAlgorithm,
693 pub n_clusters: usize,
695 pub max_iterations: usize,
697 pub tolerance: f64,
699 pub dtw_window: Option<usize>,
701 pub soft_dtw_gamma: Option<f64>,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
707pub enum TimeSeriesAlgorithm {
708 DTWKMedoids,
710 DTWKMeans,
712 DTWHierarchical,
714}
715
716impl Default for TimeSeriesClusteringConfig {
717 fn default() -> Self {
718 Self {
719 algorithm: TimeSeriesAlgorithm::DTWKMedoids,
720 n_clusters: 3,
721 max_iterations: 100,
722 tolerance: 1e-4,
723 dtw_window: None,
724 soft_dtw_gamma: None,
725 }
726 }
727}
728
729#[allow(dead_code)]
740pub fn time_series_clustering<F>(
741 time_series: ArrayView2<F>,
742 config: &TimeSeriesClusteringConfig,
743) -> Result<Array1<usize>>
744where
745 F: Float + FromPrimitive + Debug + 'static,
746{
747 match config.algorithm {
748 TimeSeriesAlgorithm::DTWKMedoids => {
749 let (_, assignments) = dtw_k_medoids(
750 time_series,
751 config.n_clusters,
752 config.max_iterations,
753 config.dtw_window,
754 )?;
755 Ok(assignments)
756 }
757 TimeSeriesAlgorithm::DTWKMeans => {
758 let tolerance = F::from(config.tolerance).expect("Failed to convert to float");
759 let (_, assignments) = dtw_k_means(
760 time_series,
761 config.n_clusters,
762 config.max_iterations,
763 tolerance,
764 )?;
765 Ok(assignments)
766 }
767 TimeSeriesAlgorithm::DTWHierarchical => {
768 let linkage = dtw_hierarchical_clustering(time_series, config.dtw_window)?;
773
774 let n_series = time_series.nrows();
775 let n_clusters = config.n_clusters.clamp(1, n_series.max(1));
776
777 if linkage.nrows() == 0 {
779 return Ok(Array1::zeros(n_series));
780 }
781
782 fcluster(&linkage, n_clusters, Some(ClusterCriterion::MaxClust))
783 }
784 }
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790 use scirs2_core::ndarray::Array2;
791
792 #[test]
793 fn test_dtw_distance() {
794 let series1 = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
795 let series2 = Array1::from_vec(vec![1.0, 2.0, 2.0, 3.0, 2.0, 1.0]);
796
797 let distance =
798 dtw_distance(series1.view(), series2.view(), None).expect("Operation failed");
799 assert!(distance >= 0.0);
800 }
801
802 #[test]
803 fn test_dtw_identical_series() {
804 let series = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
805 let distance = dtw_distance(series.view(), series.view(), None).expect("Operation failed");
806 assert_eq!(distance, 0.0);
807 }
808
809 #[test]
810 fn test_dtw_hierarchical_real_dendrogram_cut() {
811 let time_series = Array2::from_shape_vec(
815 (4, 5),
816 vec![
817 1.0, 2.0, 3.0, 2.0, 1.0, 1.1, 2.1, 3.1, 2.1, 1.1, 8.0, 9.0, 10.0, 9.0, 8.0, 8.1, 9.1, 10.1, 9.1, 8.1, ],
822 )
823 .expect("Operation failed");
824
825 let config = TimeSeriesClusteringConfig {
826 algorithm: TimeSeriesAlgorithm::DTWHierarchical,
827 n_clusters: 2,
828 ..TimeSeriesClusteringConfig::default()
829 };
830
831 let assignments =
832 time_series_clustering(time_series.view(), &config).expect("Operation failed");
833
834 assert_eq!(assignments.len(), 4);
835 assert_eq!(assignments[0], assignments[1]);
837 assert_eq!(assignments[2], assignments[3]);
838 assert_ne!(assignments[0], assignments[2]);
839 let mut distinct: Vec<usize> = assignments.to_vec();
841 distinct.sort_unstable();
842 distinct.dedup();
843 assert_eq!(distinct.len(), 2);
844 }
845
846 #[test]
847 fn test_dtw_k_medoids() {
848 let time_series = Array2::from_shape_vec(
849 (4, 5),
850 vec![
851 1.0, 2.0, 3.0, 2.0, 1.0, 1.1, 2.1, 3.1, 2.1, 1.1, 5.0, 6.0, 7.0, 6.0, 5.0, 5.1,
852 6.1, 7.1, 6.1, 5.1,
853 ],
854 )
855 .expect("Operation failed");
856
857 let (medoids, assignments) =
858 dtw_k_medoids(time_series.view(), 2, 10, None).expect("Operation failed");
859
860 assert_eq!(medoids.len(), 2);
861 assert_eq!(assignments.len(), 4);
862
863 assert_eq!(assignments[0], assignments[1]);
865 assert_eq!(assignments[2], assignments[3]);
866 assert_ne!(assignments[0], assignments[2]);
867 }
868
869 #[test]
870 fn test_soft_dtw_distance() {
871 let series1 = Array1::from_vec(vec![1.0, 2.0, 3.0]);
872 let series2 = Array1::from_vec(vec![1.0, 2.5, 3.0]);
873
874 let distance =
875 soft_dtw_distance(series1.view(), series2.view(), 0.1).expect("Operation failed");
876 assert!(distance >= 0.0);
877 }
878
879 #[test]
880 fn test_dtw_barycenter_averaging() {
881 let time_series = Array2::from_shape_vec(
882 (3, 4),
883 vec![1.0, 2.0, 3.0, 2.0, 1.1, 2.1, 3.1, 2.1, 0.9, 1.9, 2.9, 1.9],
884 )
885 .expect("Operation failed");
886
887 let barycenter =
888 dtw_barycenter_averaging(&time_series, 10, 1e-3).expect("Operation failed");
889 assert_eq!(barycenter.len(), 4);
890
891 let mean_series = time_series.mean_axis(Axis(0)).expect("Operation failed");
893 for i in 0..4 {
894 assert!((barycenter[i] - mean_series[i]).abs() < 0.5);
895 }
896 }
897
898 #[test]
899 fn test_time_series_clustering_config() {
900 let config = TimeSeriesClusteringConfig::default();
901 assert_eq!(config.n_clusters, 3);
902 assert_eq!(config.max_iterations, 100);
903
904 let time_series = Array2::from_shape_vec(
905 (4, 5),
906 vec![
907 1.0, 2.0, 3.0, 2.0, 1.0, 1.1, 2.1, 3.1, 2.1, 1.1, 5.0, 6.0, 7.0, 6.0, 5.0, 5.1,
908 6.1, 7.1, 6.1, 5.1,
909 ],
910 )
911 .expect("Operation failed");
912
913 let assignments =
914 time_series_clustering(time_series.view(), &config).expect("Operation failed");
915 assert_eq!(assignments.len(), 4);
916 }
917}