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"]
391pub fn sbd_kmedoids(data: &FdMatrix, config: &KMedoidsConfig) -> Result<KMedoidsResult, FdarError> {
392 let dist = sbd_distance_matrix(data)?;
393 kmedoids_from_distances(&dist, config)
394}
395
396struct RestartOutcome {
398 cluster: Vec<usize>,
399 centroids: Vec<Vec<f64>>,
400 inertia: f64,
401 iter: usize,
402 converged: bool,
403 restart_idx: usize,
404}
405
406#[allow(clippy::too_many_arguments)]
408fn run_restart(
409 series: &[Vec<f64>],
410 n: usize,
411 m: usize,
412 k: usize,
413 max_iter: usize,
414 tol: f64,
415 rng: &mut StdRng,
416 restart_idx: usize,
417) -> RestartOutcome {
418 let mut cluster: Vec<usize> = (0..n).map(|_| rng.gen_range(0..k)).collect();
420 ensure_no_empty_random(&mut cluster, n, k, rng);
421
422 let mut centroids: Vec<Vec<f64>> = vec![vec![0.0f64; m]; k];
425 refine_centroids(series, &cluster, k, m, &mut centroids);
426
427 let mut prev_inertia = f64::INFINITY;
428 let mut iter = 0usize;
429 let mut converged = false;
430 let mut inertia = f64::INFINITY;
431
432 while iter < max_iter {
433 iter += 1;
434
435 let mut new_cluster = vec![0usize; n];
437 let mut dist_to_own = vec![0.0f64; n];
438 for i in 0..n {
439 let mut best_c = 0usize;
440 let mut best_d = f64::INFINITY;
441 for (c, cent) in centroids.iter().enumerate() {
442 let d = sbd(&series[i], cent).map(|r| r.distance).unwrap_or(1.0);
443 if d < best_d {
444 best_d = d;
445 best_c = c;
446 }
447 }
448 new_cluster[i] = best_c;
449 dist_to_own[i] = best_d;
450 }
451
452 recover_empty_clusters(&mut new_cluster, &dist_to_own, n, k);
455
456 refine_centroids(series, &new_cluster, k, m, &mut centroids);
458
459 inertia = 0.0;
461 for i in 0..n {
462 let d = sbd(&series[i], ¢roids[new_cluster[i]])
463 .map(|r| r.distance)
464 .unwrap_or(1.0);
465 inertia += d;
466 }
467
468 let changed = new_cluster != cluster;
469 cluster = new_cluster;
470
471 if !changed || (prev_inertia - inertia).abs() < tol {
472 converged = true;
473 break;
474 }
475 prev_inertia = inertia;
476 }
477
478 RestartOutcome {
479 cluster,
480 centroids,
481 inertia,
482 iter,
483 converged,
484 restart_idx,
485 }
486}
487
488fn refine_centroids(
493 series: &[Vec<f64>],
494 cluster: &[usize],
495 k: usize,
496 m: usize,
497 centroids: &mut [Vec<f64>],
498) {
499 for c in 0..k {
500 let members: Vec<usize> = (0..series.len()).filter(|&i| cluster[i] == c).collect();
501 if members.is_empty() {
502 continue;
503 }
504 centroids[c] = shape_extraction(series, &members, ¢roids[c], m);
505 }
506}
507
508fn shape_extraction(
517 series: &[Vec<f64>],
518 members: &[usize],
519 centroid: &[f64],
520 m: usize,
521) -> Vec<f64> {
522 let n_k = members.len();
523
524 let mut x_aligned: Vec<Vec<f64>> = Vec::with_capacity(n_k);
530 for &i in members {
531 let shift = sbd(centroid, &series[i]).map(|r| r.shift).unwrap_or(0);
532 let shifted = circular_shift(&series[i], shift);
533 x_aligned.push(z_normalize_window(&shifted));
534 }
535
536 let mut s = DMatrix::<f64>::zeros(m, m);
538 for row_vec in &x_aligned {
539 for a in 0..m {
540 let va = row_vec[a];
541 if va == 0.0 {
542 continue;
543 }
544 for b in 0..m {
545 s[(a, b)] += va * row_vec[b];
546 }
547 }
548 }
549
550 let inv_m = 1.0 / m as f64;
554 let mut qs = s.clone();
556 for b in 0..m {
557 let mut col_mean = 0.0;
558 for a in 0..m {
559 col_mean += s[(a, b)];
560 }
561 col_mean *= inv_m;
562 for a in 0..m {
563 qs[(a, b)] -= col_mean;
564 }
565 }
566 let mut mmat = qs.clone();
568 for a in 0..m {
569 let mut row_mean = 0.0;
570 for b in 0..m {
571 row_mean += qs[(a, b)];
572 }
573 row_mean *= inv_m;
574 for b in 0..m {
575 mmat[(a, b)] -= row_mean;
576 }
577 }
578 for a in 0..m {
580 for b in (a + 1)..m {
581 let avg = 0.5 * (mmat[(a, b)] + mmat[(b, a)]);
582 mmat[(a, b)] = avg;
583 mmat[(b, a)] = avg;
584 }
585 }
586
587 let eig = SymmetricEigen::new(mmat);
589 let mut arg = 0usize;
590 let mut best_eval = f64::NEG_INFINITY;
591 for (i, &ev) in eig.eigenvalues.iter().enumerate() {
592 if ev > best_eval {
593 best_eval = ev;
594 arg = i;
595 }
596 }
597 let mut v: Vec<f64> = eig.eigenvectors.column(arg).iter().copied().collect();
598
599 let neg: Vec<f64> = v.iter().map(|x| -x).collect();
601 let mut sum_pos = 0.0;
602 let mut sum_neg = 0.0;
603 for row_vec in &x_aligned {
604 sum_pos += sbd(&v, row_vec).map(|r| r.distance).unwrap_or(1.0);
605 sum_neg += sbd(&neg, row_vec).map(|r| r.distance).unwrap_or(1.0);
606 }
607 if sum_neg < sum_pos {
608 v = neg;
609 }
610
611 z_normalize_window(&v)
613}
614
615fn circular_shift(x: &[f64], shift: isize) -> Vec<f64> {
617 let n = x.len();
618 if n == 0 {
619 return Vec::new();
620 }
621 let n_i = n as isize;
622 let s = ((shift % n_i) + n_i) % n_i; let mut out = vec![0.0f64; n];
624 for (i, &v) in x.iter().enumerate() {
625 let j = ((i as isize + s) % n_i) as usize;
626 out[j] = v;
627 }
628 out
629}
630
631fn ensure_no_empty_random(cluster: &mut [usize], n: usize, k: usize, rng: &mut StdRng) {
634 loop {
635 let mut sizes = vec![0usize; k];
636 for &c in cluster.iter() {
637 sizes[c] += 1;
638 }
639 let empty: Vec<usize> = (0..k).filter(|&c| sizes[c] == 0).collect();
640 if empty.is_empty() {
641 return;
642 }
643 for c in empty {
644 let donors: Vec<usize> = (0..n).filter(|&i| sizes[cluster[i]] > 1).collect();
645 if donors.is_empty() {
646 return; }
648 let pick = donors[rng.gen_range(0..donors.len())];
649 sizes[cluster[pick]] -= 1;
650 cluster[pick] = c;
651 sizes[c] += 1;
652 }
653 }
654}
655
656fn recover_empty_clusters(cluster: &mut [usize], dist_to_own: &[f64], n: usize, k: usize) {
659 loop {
660 let mut sizes = vec![0usize; k];
661 for &c in cluster.iter() {
662 sizes[c] += 1;
663 }
664 let Some(empty) = (0..k).find(|&c| sizes[c] == 0) else {
665 return;
666 };
667 let mut best_i = None;
668 let mut best_d = f64::NEG_INFINITY;
669 for i in 0..n {
670 if sizes[cluster[i]] <= 1 {
671 continue;
672 }
673 let d = dist_to_own[i];
674 if d > best_d {
675 best_d = d;
676 best_i = Some(i);
677 }
678 }
679 match best_i {
680 Some(i) => cluster[i] = empty,
681 None => return, }
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use std::f64::consts::PI;
690
691 fn matrix_from_rows(rows: &[Vec<f64>]) -> FdMatrix {
693 let n = rows.len();
694 let m = rows[0].len();
695 let mut data = vec![0.0; n * m];
696 for (i, r) in rows.iter().enumerate() {
697 for (j, &v) in r.iter().enumerate() {
698 data[i + j * n] = v; }
700 }
701 FdMatrix::from_slice(&data, n, m).unwrap()
702 }
703
704 fn purity(labels: &[usize], truth: &[usize], k: usize) -> f64 {
706 let n = labels.len();
707 let n_truth = truth.iter().copied().max().unwrap_or(0) + 1;
708 let mut correct = 0usize;
709 for c in 0..k {
710 let mut counts = vec![0usize; n_truth];
711 for i in 0..n {
712 if labels[i] == c {
713 counts[truth[i]] += 1;
714 }
715 }
716 correct += counts.iter().copied().max().unwrap_or(0);
717 }
718 correct as f64 / n as f64
719 }
720
721 fn shifted_groups(seed: u64) -> (FdMatrix, Vec<usize>) {
725 let m = 40usize;
726 let mut rng = StdRng::seed_from_u64(seed);
727 let mut rows = Vec::new();
728 let mut truth = Vec::new();
729 let base_a: Vec<f64> = (0..m)
730 .map(|j| (2.0 * PI * j as f64 / m as f64).sin())
731 .collect();
732 let base_b: Vec<f64> = (0..m)
733 .map(|j| (4.0 * PI * j as f64 / m as f64).sin())
734 .collect();
735 for (label, base) in [(0usize, &base_a), (1usize, &base_b)] {
736 for _ in 0..8 {
737 let shift = rng.gen_range(0..m) as isize;
738 let shifted = circular_shift(base, shift);
739 let noisy: Vec<f64> = shifted
740 .iter()
741 .map(|&v| v + (rng.gen::<f64>() - 0.5) * 0.05)
742 .collect();
743 rows.push(noisy);
744 truth.push(label);
745 }
746 }
747 (matrix_from_rows(&rows), truth)
748 }
749
750 #[test]
751 fn test_kshape_recovers_shifted_groups() {
752 let (data, truth) = shifted_groups(7);
753 let cfg = KShapeConfig {
754 n_clusters: 2,
755 n_init: 10,
756 seed: 3,
757 ..Default::default()
758 };
759 let res = kshape_fd(&data, &cfg).unwrap();
760 assert_eq!(res.cluster.len(), 16);
761 let p = purity(&res.cluster, &truth, 2);
762 assert!((p - 1.0).abs() < 1e-12, "purity {p} != 1.0");
763 assert_eq!(res.n_clusters(), 2);
764 for c in 0..2 {
766 let row = res.centroids.row(c);
767 let mean: f64 = row.iter().sum::<f64>() / row.len() as f64;
768 assert!(mean.abs() < 1e-8, "centroid {c} not zero-mean: {mean}");
769 }
770 }
771
772 #[test]
773 fn test_kshape_centroid_sign() {
774 let m = 32usize;
777 let base: Vec<f64> = (0..m)
778 .map(|j| (2.0 * PI * j as f64 / m as f64).sin())
779 .collect();
780 let mut rng = StdRng::seed_from_u64(1);
781 let rows: Vec<Vec<f64>> = (0..6)
782 .map(|_| {
783 base.iter()
784 .map(|&v| v + (rng.gen::<f64>() - 0.5) * 0.01)
785 .collect()
786 })
787 .collect();
788 let data = matrix_from_rows(&rows);
789 let cfg = KShapeConfig::new(1);
790 let res = kshape_fd(&data, &cfg).unwrap();
791 let cent = res.centroids.row(0);
792 let base_z = z_normalize_window(&base);
793 let cent_z = z_normalize_window(¢);
794 let corr: f64 = cent_z
796 .iter()
797 .zip(base_z.iter())
798 .map(|(a, b)| a * b)
799 .sum::<f64>()
800 / m as f64;
801 assert!(
802 corr > 0.99,
803 "centroid must correlate positively, corr={corr}"
804 );
805 }
806
807 #[test]
808 fn test_kshape_empty_cluster_recovery() {
809 let (data, _) = shifted_groups(11);
811 let cfg = KShapeConfig {
812 n_clusters: 5,
813 n_init: 3,
814 seed: 2,
815 ..Default::default()
816 };
817 let res = kshape_fd(&data, &cfg).unwrap();
818 assert_eq!(res.cluster.len(), 16);
819 assert!(res.cluster.iter().all(|&c| c < 5));
820 let mut sizes = vec![0usize; 5];
821 for &c in &res.cluster {
822 sizes[c] += 1;
823 }
824 assert!(
825 sizes.iter().all(|&s| s >= 1),
826 "an empty cluster survived: {sizes:?}"
827 );
828 }
829
830 #[test]
831 fn test_kshape_deterministic() {
832 let (data, _) = shifted_groups(5);
833 let cfg = KShapeConfig {
834 n_clusters: 2,
835 n_init: 5,
836 seed: 42,
837 ..Default::default()
838 };
839 let a = kshape_fd(&data, &cfg).unwrap();
840 let b = kshape_fd(&data, &cfg).unwrap();
841 assert_eq!(a.cluster, b.cluster, "same seed must give identical labels");
842 assert_eq!(a.inertia.to_bits(), b.inertia.to_bits());
843 assert_eq!(a.n_init_best, b.n_init_best);
844 let n = a.centroids.nrows();
846 let m = a.centroids.ncols();
847 for i in 0..n {
848 for j in 0..m {
849 assert_eq!(a.centroids[(i, j)].to_bits(), b.centroids[(i, j)].to_bits());
850 }
851 }
852 }
853
854 #[test]
855 fn test_kshape_best_of_n_init() {
856 let (data, _) = shifted_groups(9);
859 let multi = KShapeConfig {
860 n_clusters: 2,
861 n_init: 10,
862 seed: 4,
863 ..Default::default()
864 };
865 let single = KShapeConfig {
866 n_init: 1,
867 ..multi.clone()
868 };
869 let rm = kshape_fd(&data, &multi).unwrap();
870 let rs = kshape_fd(&data, &single).unwrap();
871 assert!(
872 rm.inertia <= rs.inertia + 1e-12,
873 "multi-init inertia {} worse than single-init {}",
874 rm.inertia,
875 rs.inertia
876 );
877 }
878
879 #[test]
880 fn test_kshape_predict() {
881 let (data, _) = shifted_groups(13);
882 let cfg = KShapeConfig {
883 n_clusters: 2,
884 n_init: 10,
885 seed: 6,
886 ..Default::default()
887 };
888 let res = kshape_fd(&data, &cfg).unwrap();
889
890 let preds = res.predict(&data).unwrap();
892 assert_eq!(preds, res.cluster, "predict(train) must reproduce cluster");
893
894 let m = data.ncols();
896 let src = data.row(0);
897 let novel = circular_shift(&src, 7);
898 let test = matrix_from_rows(&[novel]);
899 let p = res.predict(&test).unwrap();
900 assert_eq!(p.len(), 1);
901 assert_eq!(
902 p[0], res.cluster[0],
903 "shifted copy of series 0 should route to its cluster"
904 );
905 let _ = m;
906 }
907
908 #[test]
909 fn test_kshape_validation() {
910 let (data, _) = shifted_groups(1);
911 let cfg0 = KShapeConfig::new(0);
913 assert!(matches!(
914 kshape_fd(&data, &cfg0),
915 Err(FdarError::InvalidParameter { .. })
916 ));
917 let cfg_big = KShapeConfig::new(999);
919 assert!(matches!(
920 kshape_fd(&data, &cfg_big),
921 Err(FdarError::InvalidParameter { .. })
922 ));
923 let cfg_ni = KShapeConfig {
925 n_init: 0,
926 ..KShapeConfig::new(2)
927 };
928 assert!(matches!(
929 kshape_fd(&data, &cfg_ni),
930 Err(FdarError::InvalidParameter { .. })
931 ));
932 let empty = FdMatrix::zeros(0, 0);
934 assert!(matches!(
935 kshape_fd(&empty, &KShapeConfig::new(2)),
936 Err(FdarError::InvalidDimension { .. })
937 ));
938 let res = kshape_fd(&data, &KShapeConfig::new(2)).unwrap();
940 let wrong = matrix_from_rows(&[vec![1.0, 2.0, 3.0]]);
941 assert!(matches!(
942 res.predict(&wrong),
943 Err(FdarError::InvalidDimension { .. })
944 ));
945 }
946
947 #[test]
948 fn test_sbd_kmedoids_recovers_groups() {
949 let (data, truth) = shifted_groups(7);
953 let cfg = KMedoidsConfig {
954 k: 2,
955 max_iter: 100,
956 seed: 3,
957 };
958 let res = sbd_kmedoids(&data, &cfg).unwrap();
959 assert_eq!(res.labels.len(), 16);
960 assert_eq!(res.medoid_indices.len(), 2);
961 let p = purity(&res.labels, &truth, 2);
962 assert!(p >= 0.9, "SBD k-medoids purity {p} too low (< 0.9)");
963 }
964
965 #[test]
966 fn test_sbd_kmedoids_uses_sbd_matrix() {
967 let (data, _) = shifted_groups(5);
970 let cfg = KMedoidsConfig {
971 k: 2,
972 max_iter: 100,
973 seed: 42,
974 };
975 let res = sbd_kmedoids(&data, &cfg).unwrap();
976 let dist = sbd_distance_matrix(&data).unwrap();
977 let manual = kmedoids_from_distances(&dist, &cfg).unwrap();
978 assert_eq!(
979 res.labels, manual.labels,
980 "labels must match manual composition"
981 );
982 assert_eq!(
983 res.medoid_indices, manual.medoid_indices,
984 "medoids must match manual composition"
985 );
986 assert_eq!(
987 res.total_within_distance.to_bits(),
988 manual.total_within_distance.to_bits()
989 );
990 }
991
992 #[test]
993 fn test_sbd_kmedoids_validation() {
994 let (data, _) = shifted_groups(1);
995 let cfg0 = KMedoidsConfig {
997 k: 0,
998 ..Default::default()
999 };
1000 assert!(matches!(
1001 sbd_kmedoids(&data, &cfg0),
1002 Err(FdarError::InvalidParameter { .. })
1003 ));
1004 let cfg_big = KMedoidsConfig {
1006 k: 999,
1007 ..Default::default()
1008 };
1009 assert!(matches!(
1010 sbd_kmedoids(&data, &cfg_big),
1011 Err(FdarError::InvalidParameter { .. })
1012 ));
1013 }
1014
1015 #[test]
1021 fn test_kshape_reexports() {
1022 use crate::{
1023 kshape_fd, sbd, sbd_distance_matrix, sbd_kmedoids, KMedoidsConfig, KMedoidsResult,
1024 KShapeConfig, KShapeResult, SbdResult,
1025 };
1026 let _f: fn(&FdMatrix, &KShapeConfig) -> Result<KShapeResult, FdarError> = kshape_fd;
1028 let _k: fn(&FdMatrix, &KMedoidsConfig) -> Result<KMedoidsResult, FdarError> = sbd_kmedoids;
1029 let _s: fn(&[f64], &[f64]) -> Result<SbdResult, FdarError> = sbd;
1030 let _m: fn(&FdMatrix) -> Result<FdMatrix, FdarError> = sbd_distance_matrix;
1031 }
1032}