1use crate::alignment::{kmedoids_from_distances, KMedoidsConfig, KMedoidsResult};
55use crate::error::FdarError;
56use crate::matrix::FdMatrix;
57use crate::metric::sbd::{sbd, sbd_distance_matrix};
58use crate::shapelet::z_normalize_window;
59use nalgebra::{DMatrix, SymmetricEigen};
60use rand::prelude::*;
61
62#[derive(Debug, Clone, PartialEq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69#[non_exhaustive]
70pub struct KShapeConfig {
71 pub n_clusters: usize,
73 pub n_init: usize,
75 pub max_iter: usize,
77 pub tol: f64,
79 pub seed: u64,
81}
82
83impl Default for KShapeConfig {
84 fn default() -> Self {
85 Self {
86 n_clusters: 2,
87 n_init: 10,
88 max_iter: 100,
89 tol: 1e-6,
90 seed: 0,
91 }
92 }
93}
94
95impl KShapeConfig {
96 #[must_use]
98 pub fn new(n_clusters: usize) -> Self {
99 Self {
100 n_clusters,
101 ..Self::default()
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113#[non_exhaustive]
114pub struct KShapeResult {
115 pub centroids: FdMatrix,
117 pub cluster: Vec<usize>,
119 pub inertia: f64,
121 pub iter: usize,
123 pub converged: bool,
125 pub n_init_best: usize,
127}
128
129impl KShapeResult {
130 #[must_use]
132 pub fn centroids(&self) -> &FdMatrix {
133 &self.centroids
134 }
135
136 #[must_use]
138 pub fn cluster(&self) -> &[usize] {
139 &self.cluster
140 }
141
142 #[must_use]
144 pub fn inertia(&self) -> f64 {
145 self.inertia
146 }
147
148 #[must_use]
150 pub fn n_clusters(&self) -> usize {
151 self.centroids.nrows()
152 }
153
154 pub fn predict(&self, new_data: &FdMatrix) -> Result<Vec<usize>, FdarError> {
165 let p = new_data.nrows();
166 let m = new_data.ncols();
167 let k = self.centroids.nrows();
168 let cm = self.centroids.ncols();
169 if p == 0 || m == 0 {
170 return Err(FdarError::InvalidDimension {
171 parameter: "new_data",
172 expected: "non-empty matrix (nrows > 0, ncols > 0)".to_string(),
173 actual: format!("{p}x{m}"),
174 });
175 }
176 if m != cm {
177 return Err(FdarError::InvalidDimension {
178 parameter: "new_data",
179 expected: format!("series length m={cm} matching fitted centroids"),
180 actual: format!("m={m}"),
181 });
182 }
183
184 let mut centroid_rows: Vec<Vec<f64>> = Vec::with_capacity(k);
186 for c in 0..k {
187 centroid_rows.push(self.centroids.row(c));
188 }
189
190 let mut labels = vec![0usize; p];
191 let mut row = vec![0.0f64; m];
192 for t in 0..p {
193 new_data.row_to_buf(t, &mut row);
194 let z = z_normalize_window(&row);
195 let mut best = 0usize;
196 let mut best_d = f64::INFINITY;
197 for (c, cent) in centroid_rows.iter().enumerate() {
198 let d = sbd(&z, cent).map(|r| r.distance).unwrap_or(1.0);
199 if d < best_d {
200 best_d = d;
201 best = c;
202 }
203 }
204 labels[t] = best;
205 }
206 Ok(labels)
207 }
208}
209
210#[must_use = "expensive computation whose result should not be discarded"]
253pub fn kshape_fd(data: &FdMatrix, config: &KShapeConfig) -> Result<KShapeResult, FdarError> {
254 let n = data.nrows();
255 let m = data.ncols();
256 if n == 0 || m == 0 {
257 return Err(FdarError::InvalidDimension {
258 parameter: "data",
259 expected: "non-empty matrix (nrows > 0, ncols > 0)".to_string(),
260 actual: format!("{n}x{m}"),
261 });
262 }
263 let k = config.n_clusters;
264 if k < 1 {
265 return Err(FdarError::InvalidParameter {
266 parameter: "n_clusters",
267 message: "number of clusters must be >= 1".to_string(),
268 });
269 }
270 if k > n {
271 return Err(FdarError::InvalidParameter {
272 parameter: "n_clusters",
273 message: format!("n_clusters={k} exceeds number of series n={n}"),
274 });
275 }
276 if config.n_init < 1 {
277 return Err(FdarError::InvalidParameter {
278 parameter: "n_init",
279 message: "n_init must be >= 1".to_string(),
280 });
281 }
282
283 let mut series: Vec<Vec<f64>> = Vec::with_capacity(n);
286 let mut row = vec![0.0f64; m];
287 for i in 0..n {
288 data.row_to_buf(i, &mut row);
289 series.push(z_normalize_window(&row));
290 }
291
292 let mut best: Option<RestartOutcome> = None;
293 for restart in 0..config.n_init {
294 let mut rng = StdRng::seed_from_u64(config.seed.wrapping_add(restart as u64));
295 let outcome = run_restart(
296 &series,
297 n,
298 m,
299 k,
300 config.max_iter,
301 config.tol,
302 &mut rng,
303 restart,
304 );
305 let take = match &best {
306 None => true,
307 Some(b) => outcome.inertia < b.inertia,
308 };
309 if take {
310 best = Some(outcome);
311 }
312 }
313
314 let RestartOutcome {
316 cluster,
317 centroids,
318 inertia,
319 iter,
320 converged,
321 restart_idx,
322 } = best.expect("n_init >= 1 guarantees a restart outcome");
323
324 let mut cmat = FdMatrix::zeros(k, m);
326 for (c, cent) in centroids.iter().enumerate() {
327 for (j, &v) in cent.iter().enumerate() {
328 cmat[(c, j)] = v;
329 }
330 }
331
332 Ok(KShapeResult {
333 centroids: cmat,
334 cluster,
335 inertia,
336 iter,
337 converged,
338 n_init_best: restart_idx,
339 })
340}
341
342#[must_use = "expensive computation whose result should not be discarded"]
392pub fn sbd_kmedoids(data: &FdMatrix, config: &KMedoidsConfig) -> Result<KMedoidsResult, FdarError> {
393 let dist = sbd_distance_matrix(data)?;
394 kmedoids_from_distances(&dist, config)
395}
396
397struct RestartOutcome {
399 cluster: Vec<usize>,
400 centroids: Vec<Vec<f64>>,
401 inertia: f64,
402 iter: usize,
403 converged: bool,
404 restart_idx: usize,
405}
406
407#[allow(clippy::too_many_arguments)]
409fn run_restart(
410 series: &[Vec<f64>],
411 n: usize,
412 m: usize,
413 k: usize,
414 max_iter: usize,
415 tol: f64,
416 rng: &mut StdRng,
417 restart_idx: usize,
418) -> RestartOutcome {
419 let mut cluster: Vec<usize> = (0..n).map(|_| rng.gen_range(0..k)).collect();
421 ensure_no_empty_random(&mut cluster, n, k, rng);
422
423 let mut centroids: Vec<Vec<f64>> = vec![vec![0.0f64; m]; k];
426 refine_centroids(series, &cluster, k, m, &mut centroids);
427
428 let mut prev_inertia = f64::INFINITY;
429 let mut iter = 0usize;
430 let mut converged = false;
431 let mut inertia = f64::INFINITY;
432
433 while iter < max_iter {
434 iter += 1;
435
436 let mut new_cluster = vec![0usize; n];
438 let mut dist_to_own = vec![0.0f64; n];
439 for i in 0..n {
440 let mut best_c = 0usize;
441 let mut best_d = f64::INFINITY;
442 for (c, cent) in centroids.iter().enumerate() {
443 let d = sbd(&series[i], cent).map(|r| r.distance).unwrap_or(1.0);
444 if d < best_d {
445 best_d = d;
446 best_c = c;
447 }
448 }
449 new_cluster[i] = best_c;
450 dist_to_own[i] = best_d;
451 }
452
453 recover_empty_clusters(&mut new_cluster, &dist_to_own, n, k);
456
457 refine_centroids(series, &new_cluster, k, m, &mut centroids);
459
460 inertia = 0.0;
462 for i in 0..n {
463 let d = sbd(&series[i], ¢roids[new_cluster[i]])
464 .map(|r| r.distance)
465 .unwrap_or(1.0);
466 inertia += d;
467 }
468
469 let changed = new_cluster != cluster;
470 cluster = new_cluster;
471
472 if !changed || (prev_inertia - inertia).abs() < tol {
473 converged = true;
474 break;
475 }
476 prev_inertia = inertia;
477 }
478
479 RestartOutcome {
480 cluster,
481 centroids,
482 inertia,
483 iter,
484 converged,
485 restart_idx,
486 }
487}
488
489fn refine_centroids(
494 series: &[Vec<f64>],
495 cluster: &[usize],
496 k: usize,
497 m: usize,
498 centroids: &mut [Vec<f64>],
499) {
500 for c in 0..k {
501 let members: Vec<usize> = (0..series.len()).filter(|&i| cluster[i] == c).collect();
502 if members.is_empty() {
503 continue;
504 }
505 centroids[c] = shape_extraction(series, &members, ¢roids[c], m);
506 }
507}
508
509fn shape_extraction(
518 series: &[Vec<f64>],
519 members: &[usize],
520 centroid: &[f64],
521 m: usize,
522) -> Vec<f64> {
523 let n_k = members.len();
524
525 let mut x_aligned: Vec<Vec<f64>> = Vec::with_capacity(n_k);
531 for &i in members {
532 let shift = sbd(centroid, &series[i]).map(|r| r.shift).unwrap_or(0);
533 let shifted = circular_shift(&series[i], shift);
534 x_aligned.push(z_normalize_window(&shifted));
535 }
536
537 let mut s = DMatrix::<f64>::zeros(m, m);
539 for row_vec in &x_aligned {
540 for a in 0..m {
541 let va = row_vec[a];
542 if va == 0.0 {
543 continue;
544 }
545 for b in 0..m {
546 s[(a, b)] += va * row_vec[b];
547 }
548 }
549 }
550
551 let inv_m = 1.0 / m as f64;
555 let mut qs = s.clone();
557 for b in 0..m {
558 let mut col_mean = 0.0;
559 for a in 0..m {
560 col_mean += s[(a, b)];
561 }
562 col_mean *= inv_m;
563 for a in 0..m {
564 qs[(a, b)] -= col_mean;
565 }
566 }
567 let mut mmat = qs.clone();
569 for a in 0..m {
570 let mut row_mean = 0.0;
571 for b in 0..m {
572 row_mean += qs[(a, b)];
573 }
574 row_mean *= inv_m;
575 for b in 0..m {
576 mmat[(a, b)] -= row_mean;
577 }
578 }
579 for a in 0..m {
581 for b in (a + 1)..m {
582 let avg = 0.5 * (mmat[(a, b)] + mmat[(b, a)]);
583 mmat[(a, b)] = avg;
584 mmat[(b, a)] = avg;
585 }
586 }
587
588 let eig = SymmetricEigen::new(mmat);
590 let mut arg = 0usize;
591 let mut best_eval = f64::NEG_INFINITY;
592 for (i, &ev) in eig.eigenvalues.iter().enumerate() {
593 if ev > best_eval {
594 best_eval = ev;
595 arg = i;
596 }
597 }
598 let mut v: Vec<f64> = eig.eigenvectors.column(arg).iter().copied().collect();
599
600 let neg: Vec<f64> = v.iter().map(|x| -x).collect();
602 let mut sum_pos = 0.0;
603 let mut sum_neg = 0.0;
604 for row_vec in &x_aligned {
605 sum_pos += sbd(&v, row_vec).map(|r| r.distance).unwrap_or(1.0);
606 sum_neg += sbd(&neg, row_vec).map(|r| r.distance).unwrap_or(1.0);
607 }
608 if sum_neg < sum_pos {
609 v = neg;
610 }
611
612 z_normalize_window(&v)
614}
615
616fn circular_shift(x: &[f64], shift: isize) -> Vec<f64> {
618 let n = x.len();
619 if n == 0 {
620 return Vec::new();
621 }
622 let n_i = n as isize;
623 let s = ((shift % n_i) + n_i) % n_i; let mut out = vec![0.0f64; n];
625 for (i, &v) in x.iter().enumerate() {
626 let j = ((i as isize + s) % n_i) as usize;
627 out[j] = v;
628 }
629 out
630}
631
632fn ensure_no_empty_random(cluster: &mut [usize], n: usize, k: usize, rng: &mut StdRng) {
635 loop {
636 let mut sizes = vec![0usize; k];
637 for &c in cluster.iter() {
638 sizes[c] += 1;
639 }
640 let empty: Vec<usize> = (0..k).filter(|&c| sizes[c] == 0).collect();
641 if empty.is_empty() {
642 return;
643 }
644 for c in empty {
645 let donors: Vec<usize> = (0..n).filter(|&i| sizes[cluster[i]] > 1).collect();
646 if donors.is_empty() {
647 return; }
649 let pick = donors[rng.gen_range(0..donors.len())];
650 sizes[cluster[pick]] -= 1;
651 cluster[pick] = c;
652 sizes[c] += 1;
653 }
654 }
655}
656
657fn recover_empty_clusters(cluster: &mut [usize], dist_to_own: &[f64], n: usize, k: usize) {
660 loop {
661 let mut sizes = vec![0usize; k];
662 for &c in cluster.iter() {
663 sizes[c] += 1;
664 }
665 let Some(empty) = (0..k).find(|&c| sizes[c] == 0) else {
666 return;
667 };
668 let mut best_i = None;
669 let mut best_d = f64::NEG_INFINITY;
670 for i in 0..n {
671 if sizes[cluster[i]] <= 1 {
672 continue;
673 }
674 let d = dist_to_own[i];
675 if d > best_d {
676 best_d = d;
677 best_i = Some(i);
678 }
679 }
680 match best_i {
681 Some(i) => cluster[i] = empty,
682 None => return, }
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use std::f64::consts::PI;
691
692 fn matrix_from_rows(rows: &[Vec<f64>]) -> FdMatrix {
694 let n = rows.len();
695 let m = rows[0].len();
696 let mut data = vec![0.0; n * m];
697 for (i, r) in rows.iter().enumerate() {
698 for (j, &v) in r.iter().enumerate() {
699 data[i + j * n] = v; }
701 }
702 FdMatrix::from_slice(&data, n, m).unwrap()
703 }
704
705 fn purity(labels: &[usize], truth: &[usize], k: usize) -> f64 {
707 let n = labels.len();
708 let n_truth = truth.iter().copied().max().unwrap_or(0) + 1;
709 let mut correct = 0usize;
710 for c in 0..k {
711 let mut counts = vec![0usize; n_truth];
712 for i in 0..n {
713 if labels[i] == c {
714 counts[truth[i]] += 1;
715 }
716 }
717 correct += counts.iter().copied().max().unwrap_or(0);
718 }
719 correct as f64 / n as f64
720 }
721
722 fn shifted_groups(seed: u64) -> (FdMatrix, Vec<usize>) {
726 let m = 40usize;
727 let mut rng = StdRng::seed_from_u64(seed);
728 let mut rows = Vec::new();
729 let mut truth = Vec::new();
730 let base_a: Vec<f64> = (0..m)
731 .map(|j| (2.0 * PI * j as f64 / m as f64).sin())
732 .collect();
733 let base_b: Vec<f64> = (0..m)
734 .map(|j| (4.0 * PI * j as f64 / m as f64).sin())
735 .collect();
736 for (label, base) in [(0usize, &base_a), (1usize, &base_b)] {
737 for _ in 0..8 {
738 let shift = rng.gen_range(0..m) as isize;
739 let shifted = circular_shift(base, shift);
740 let noisy: Vec<f64> = shifted
741 .iter()
742 .map(|&v| v + (rng.gen::<f64>() - 0.5) * 0.05)
743 .collect();
744 rows.push(noisy);
745 truth.push(label);
746 }
747 }
748 (matrix_from_rows(&rows), truth)
749 }
750
751 #[test]
752 fn test_kshape_recovers_shifted_groups() {
753 let (data, truth) = shifted_groups(7);
754 let cfg = KShapeConfig {
755 n_clusters: 2,
756 n_init: 10,
757 seed: 3,
758 ..Default::default()
759 };
760 let res = kshape_fd(&data, &cfg).unwrap();
761 assert_eq!(res.cluster.len(), 16);
762 let p = purity(&res.cluster, &truth, 2);
763 assert!((p - 1.0).abs() < 1e-12, "purity {p} != 1.0");
764 assert_eq!(res.n_clusters(), 2);
765 for c in 0..2 {
767 let row = res.centroids.row(c);
768 let mean: f64 = row.iter().sum::<f64>() / row.len() as f64;
769 assert!(mean.abs() < 1e-8, "centroid {c} not zero-mean: {mean}");
770 }
771 }
772
773 #[test]
774 fn test_kshape_centroid_sign() {
775 let m = 32usize;
778 let base: Vec<f64> = (0..m)
779 .map(|j| (2.0 * PI * j as f64 / m as f64).sin())
780 .collect();
781 let mut rng = StdRng::seed_from_u64(1);
782 let rows: Vec<Vec<f64>> = (0..6)
783 .map(|_| {
784 base.iter()
785 .map(|&v| v + (rng.gen::<f64>() - 0.5) * 0.01)
786 .collect()
787 })
788 .collect();
789 let data = matrix_from_rows(&rows);
790 let cfg = KShapeConfig::new(1);
791 let res = kshape_fd(&data, &cfg).unwrap();
792 let cent = res.centroids.row(0);
793 let base_z = z_normalize_window(&base);
794 let cent_z = z_normalize_window(¢);
795 let corr: f64 = cent_z
797 .iter()
798 .zip(base_z.iter())
799 .map(|(a, b)| a * b)
800 .sum::<f64>()
801 / m as f64;
802 assert!(
803 corr > 0.99,
804 "centroid must correlate positively, corr={corr}"
805 );
806 }
807
808 #[test]
809 fn test_kshape_empty_cluster_recovery() {
810 let (data, _) = shifted_groups(11);
812 let cfg = KShapeConfig {
813 n_clusters: 5,
814 n_init: 3,
815 seed: 2,
816 ..Default::default()
817 };
818 let res = kshape_fd(&data, &cfg).unwrap();
819 assert_eq!(res.cluster.len(), 16);
820 assert!(res.cluster.iter().all(|&c| c < 5));
821 let mut sizes = vec![0usize; 5];
822 for &c in &res.cluster {
823 sizes[c] += 1;
824 }
825 assert!(
826 sizes.iter().all(|&s| s >= 1),
827 "an empty cluster survived: {sizes:?}"
828 );
829 }
830
831 #[test]
832 fn test_kshape_deterministic() {
833 let (data, _) = shifted_groups(5);
834 let cfg = KShapeConfig {
835 n_clusters: 2,
836 n_init: 5,
837 seed: 42,
838 ..Default::default()
839 };
840 let a = kshape_fd(&data, &cfg).unwrap();
841 let b = kshape_fd(&data, &cfg).unwrap();
842 assert_eq!(a.cluster, b.cluster, "same seed must give identical labels");
843 assert_eq!(a.inertia.to_bits(), b.inertia.to_bits());
844 assert_eq!(a.n_init_best, b.n_init_best);
845 let n = a.centroids.nrows();
847 let m = a.centroids.ncols();
848 for i in 0..n {
849 for j in 0..m {
850 assert_eq!(a.centroids[(i, j)].to_bits(), b.centroids[(i, j)].to_bits());
851 }
852 }
853 }
854
855 #[test]
856 fn test_kshape_best_of_n_init() {
857 let (data, _) = shifted_groups(9);
860 let multi = KShapeConfig {
861 n_clusters: 2,
862 n_init: 10,
863 seed: 4,
864 ..Default::default()
865 };
866 let single = KShapeConfig {
867 n_init: 1,
868 ..multi.clone()
869 };
870 let rm = kshape_fd(&data, &multi).unwrap();
871 let rs = kshape_fd(&data, &single).unwrap();
872 assert!(
873 rm.inertia <= rs.inertia + 1e-12,
874 "multi-init inertia {} worse than single-init {}",
875 rm.inertia,
876 rs.inertia
877 );
878 }
879
880 #[test]
881 fn test_kshape_predict() {
882 let (data, _) = shifted_groups(13);
883 let cfg = KShapeConfig {
884 n_clusters: 2,
885 n_init: 10,
886 seed: 6,
887 ..Default::default()
888 };
889 let res = kshape_fd(&data, &cfg).unwrap();
890
891 let preds = res.predict(&data).unwrap();
893 assert_eq!(preds, res.cluster, "predict(train) must reproduce cluster");
894
895 let m = data.ncols();
897 let src = data.row(0);
898 let novel = circular_shift(&src, 7);
899 let test = matrix_from_rows(&[novel]);
900 let p = res.predict(&test).unwrap();
901 assert_eq!(p.len(), 1);
902 assert_eq!(
903 p[0], res.cluster[0],
904 "shifted copy of series 0 should route to its cluster"
905 );
906 let _ = m;
907 }
908
909 #[test]
910 fn test_kshape_validation() {
911 let (data, _) = shifted_groups(1);
912 let cfg0 = KShapeConfig::new(0);
914 assert!(matches!(
915 kshape_fd(&data, &cfg0),
916 Err(FdarError::InvalidParameter { .. })
917 ));
918 let cfg_big = KShapeConfig::new(999);
920 assert!(matches!(
921 kshape_fd(&data, &cfg_big),
922 Err(FdarError::InvalidParameter { .. })
923 ));
924 let cfg_ni = KShapeConfig {
926 n_init: 0,
927 ..KShapeConfig::new(2)
928 };
929 assert!(matches!(
930 kshape_fd(&data, &cfg_ni),
931 Err(FdarError::InvalidParameter { .. })
932 ));
933 let empty = FdMatrix::zeros(0, 0);
935 assert!(matches!(
936 kshape_fd(&empty, &KShapeConfig::new(2)),
937 Err(FdarError::InvalidDimension { .. })
938 ));
939 let res = kshape_fd(&data, &KShapeConfig::new(2)).unwrap();
941 let wrong = matrix_from_rows(&[vec![1.0, 2.0, 3.0]]);
942 assert!(matches!(
943 res.predict(&wrong),
944 Err(FdarError::InvalidDimension { .. })
945 ));
946 }
947
948 #[test]
949 fn test_sbd_kmedoids_recovers_groups() {
950 let (data, truth) = shifted_groups(7);
954 let cfg = KMedoidsConfig {
955 k: 2,
956 max_iter: 100,
957 seed: 3,
958 };
959 let res = sbd_kmedoids(&data, &cfg).unwrap();
960 assert_eq!(res.labels.len(), 16);
961 assert_eq!(res.medoid_indices.len(), 2);
962 let p = purity(&res.labels, &truth, 2);
963 assert!(p >= 0.9, "SBD k-medoids purity {p} too low (< 0.9)");
964 }
965
966 #[test]
967 fn test_sbd_kmedoids_uses_sbd_matrix() {
968 let (data, _) = shifted_groups(5);
971 let cfg = KMedoidsConfig {
972 k: 2,
973 max_iter: 100,
974 seed: 42,
975 };
976 let res = sbd_kmedoids(&data, &cfg).unwrap();
977 let dist = sbd_distance_matrix(&data).unwrap();
978 let manual = kmedoids_from_distances(&dist, &cfg).unwrap();
979 assert_eq!(
980 res.labels, manual.labels,
981 "labels must match manual composition"
982 );
983 assert_eq!(
984 res.medoid_indices, manual.medoid_indices,
985 "medoids must match manual composition"
986 );
987 assert_eq!(
988 res.total_within_distance.to_bits(),
989 manual.total_within_distance.to_bits()
990 );
991 }
992
993 #[test]
994 fn test_sbd_kmedoids_validation() {
995 let (data, _) = shifted_groups(1);
996 let cfg0 = KMedoidsConfig {
998 k: 0,
999 ..Default::default()
1000 };
1001 assert!(matches!(
1002 sbd_kmedoids(&data, &cfg0),
1003 Err(FdarError::InvalidParameter { .. })
1004 ));
1005 let cfg_big = KMedoidsConfig {
1007 k: 999,
1008 ..Default::default()
1009 };
1010 assert!(matches!(
1011 sbd_kmedoids(&data, &cfg_big),
1012 Err(FdarError::InvalidParameter { .. })
1013 ));
1014 }
1015
1016 #[test]
1022 fn test_kshape_reexports() {
1023 use crate::{
1024 kshape_fd, sbd, sbd_distance_matrix, sbd_kmedoids, KMedoidsConfig, KMedoidsResult,
1025 KShapeConfig, KShapeResult, SbdResult,
1026 };
1027 let _f: fn(&FdMatrix, &KShapeConfig) -> Result<KShapeResult, FdarError> = kshape_fd;
1029 let _k: fn(&FdMatrix, &KMedoidsConfig) -> Result<KMedoidsResult, FdarError> = sbd_kmedoids;
1030 let _s: fn(&[f64], &[f64]) -> Result<SbdResult, FdarError> = sbd;
1031 let _m: fn(&FdMatrix) -> Result<FdMatrix, FdarError> = sbd_distance_matrix;
1032 }
1033}