1use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
16use scirs2_core::numeric::{Float, FromPrimitive};
17use std::collections::{BTreeSet, HashMap, HashSet};
18use std::fmt::Debug;
19
20use crate::error::{ClusteringError, Result};
21
22#[derive(Debug, Clone)]
28pub struct CliqueConfig {
29 pub n_intervals: usize,
31 pub density_threshold: f64,
33 pub max_subspace_dim: usize,
35}
36
37impl Default for CliqueConfig {
38 fn default() -> Self {
39 Self {
40 n_intervals: 10,
41 density_threshold: 0.1,
42 max_subspace_dim: 0,
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct DenseCell {
50 pub dimensions: Vec<usize>,
52 pub coords: Vec<usize>,
54}
55
56#[derive(Debug, Clone)]
58pub struct CliqueResult {
59 pub labels: Array1<i32>,
61 pub subspaces: Vec<Vec<usize>>,
63 pub dense_cells: Vec<DenseCell>,
65 pub n_clusters: usize,
67}
68
69pub fn clique<F: Float + FromPrimitive + Debug>(
75 data: ArrayView2<F>,
76 config: &CliqueConfig,
77) -> Result<CliqueResult> {
78 let (n_samples, n_features) = (data.shape()[0], data.shape()[1]);
79
80 if n_samples == 0 {
81 return Err(ClusteringError::InvalidInput("Empty input data".into()));
82 }
83 if config.n_intervals == 0 {
84 return Err(ClusteringError::InvalidInput(
85 "n_intervals must be > 0".into(),
86 ));
87 }
88 if config.density_threshold <= 0.0 || config.density_threshold > 1.0 {
89 return Err(ClusteringError::InvalidInput(
90 "density_threshold must be in (0, 1]".into(),
91 ));
92 }
93
94 let tau_count = (config.density_threshold * n_samples as f64).ceil() as usize;
95
96 let mut mins = vec![F::infinity(); n_features];
98 let mut maxs = vec![F::neg_infinity(); n_features];
99 for i in 0..n_samples {
100 for d in 0..n_features {
101 let v = data[[i, d]];
102 if v < mins[d] {
103 mins[d] = v;
104 }
105 if v > maxs[d] {
106 maxs[d] = v;
107 }
108 }
109 }
110
111 let xi = config.n_intervals;
112 let eps = F::from(1e-10).unwrap_or_else(|| F::epsilon());
113
114 let grid_index = |val: F, d: usize| -> usize {
116 let range = maxs[d] - mins[d] + eps;
117 let idx_f = ((val - mins[d]) / range) * F::from(xi).unwrap_or_else(|| F::one());
118 let idx = idx_f.to_usize().unwrap_or(0).min(xi - 1);
119 idx
120 };
121
122 let mut one_d_dense: HashMap<usize, HashSet<usize>> = HashMap::new(); for d in 0..n_features {
125 let mut counts = vec![0usize; xi];
126 for i in 0..n_samples {
127 let idx = grid_index(data[[i, d]], d);
128 counts[idx] += 1;
129 }
130 let dense_coords: HashSet<usize> = counts
131 .iter()
132 .enumerate()
133 .filter(|(_, &c)| c >= tau_count)
134 .map(|(idx, _)| idx)
135 .collect();
136 if !dense_coords.is_empty() {
137 one_d_dense.insert(d, dense_coords);
138 }
139 }
140
141 let mut all_dense_cells: Vec<DenseCell> = Vec::new();
143 for (&d, coords) in &one_d_dense {
144 for &c in coords {
145 all_dense_cells.push(DenseCell {
146 dimensions: vec![d],
147 coords: vec![c],
148 });
149 }
150 }
151
152 let mut current_level: HashSet<(Vec<usize>, Vec<usize>)> = all_dense_cells
155 .iter()
156 .map(|c| (c.dimensions.clone(), c.coords.clone()))
157 .collect();
158
159 let max_dim = if config.max_subspace_dim == 0 {
160 n_features
161 } else {
162 config.max_subspace_dim.min(n_features)
163 };
164
165 let mut sample_grid: Vec<Vec<usize>> = Vec::with_capacity(n_samples);
167 for i in 0..n_samples {
168 let mut row = Vec::with_capacity(n_features);
169 for d in 0..n_features {
170 row.push(grid_index(data[[i, d]], d));
171 }
172 sample_grid.push(row);
173 }
174
175 let mut dim_level = 1usize;
176
177 while dim_level < max_dim && !current_level.is_empty() {
178 let mut candidate_set: HashSet<(Vec<usize>, Vec<usize>)> = HashSet::new();
179
180 let cells_vec: Vec<(Vec<usize>, Vec<usize>)> = current_level.iter().cloned().collect();
181
182 for i in 0..cells_vec.len() {
184 for j in (i + 1)..cells_vec.len() {
185 let (ref dims_a, ref coords_a) = cells_vec[i];
186 let (ref dims_b, ref coords_b) = cells_vec[j];
187
188 if dims_a.len() != dim_level || dims_b.len() != dim_level {
189 continue;
190 }
191
192 let mut merged_dims: BTreeSet<usize> = BTreeSet::new();
194 for &d in dims_a {
195 merged_dims.insert(d);
196 }
197 for &d in dims_b {
198 merged_dims.insert(d);
199 }
200
201 if merged_dims.len() != dim_level + 1 {
202 continue;
203 }
204
205 let merged_dims_vec: Vec<usize> = merged_dims.iter().copied().collect();
206
207 let mut merged_coords: Vec<usize> = Vec::with_capacity(dim_level + 1);
209 for &md in &merged_dims_vec {
210 if let Some(pos) = dims_a.iter().position(|&x| x == md) {
212 merged_coords.push(coords_a[pos]);
213 } else if let Some(pos) = dims_b.iter().position(|&x| x == md) {
214 merged_coords.push(coords_b[pos]);
215 }
216 }
217
218 if merged_coords.len() != dim_level + 1 {
219 continue;
220 }
221
222 candidate_set.insert((merged_dims_vec, merged_coords));
223 }
224 }
225
226 let mut next_level: HashSet<(Vec<usize>, Vec<usize>)> = HashSet::new();
228
229 for (dims, coords) in &candidate_set {
230 let mut count = 0usize;
231 for sg in &sample_grid {
232 let mut matches = true;
233 for (pos, &d) in dims.iter().enumerate() {
234 if sg[d] != coords[pos] {
235 matches = false;
236 break;
237 }
238 }
239 if matches {
240 count += 1;
241 }
242 }
243
244 if count >= tau_count {
245 all_dense_cells.push(DenseCell {
246 dimensions: dims.clone(),
247 coords: coords.clone(),
248 });
249 next_level.insert((dims.clone(), coords.clone()));
250 }
251 }
252
253 current_level = next_level;
254 dim_level += 1;
255 }
256
257 let max_found_dim = all_dense_cells
260 .iter()
261 .map(|c| c.dimensions.len())
262 .max()
263 .unwrap_or(0);
264
265 let top_cells: Vec<&DenseCell> = all_dense_cells
266 .iter()
267 .filter(|c| c.dimensions.len() == max_found_dim)
268 .collect();
269
270 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); top_cells.len()];
272 for i in 0..top_cells.len() {
273 for j in (i + 1)..top_cells.len() {
274 if top_cells[i].dimensions == top_cells[j].dimensions {
275 let mut diff_count = 0usize;
276 for k in 0..top_cells[i].coords.len() {
277 let a = top_cells[i].coords[k] as i64;
278 let b = top_cells[j].coords[k] as i64;
279 if (a - b).unsigned_abs() == 1 {
280 diff_count += 1;
281 } else if a != b {
282 diff_count = 2; break;
284 }
285 }
286 if diff_count == 1 {
287 adj[i].push(j);
288 adj[j].push(i);
289 }
290 }
291 }
292 }
293
294 let mut cell_labels = vec![-1i32; top_cells.len()];
296 let mut cluster_id = 0i32;
297 for start in 0..top_cells.len() {
298 if cell_labels[start] >= 0 {
299 continue;
300 }
301 let mut queue = vec![start];
302 cell_labels[start] = cluster_id;
303 let mut head = 0usize;
304 while head < queue.len() {
305 let cur = queue[head];
306 head += 1;
307 for &nb in &adj[cur] {
308 if cell_labels[nb] < 0 {
309 cell_labels[nb] = cluster_id;
310 queue.push(nb);
311 }
312 }
313 }
314 cluster_id += 1;
315 }
316
317 let mut labels = Array1::from_elem(n_samples, -1i32);
319 for i in 0..n_samples {
320 for (ci, cell) in top_cells.iter().enumerate() {
321 let mut in_cell = true;
322 for (pos, &d) in cell.dimensions.iter().enumerate() {
323 if sample_grid[i][d] != cell.coords[pos] {
324 in_cell = false;
325 break;
326 }
327 }
328 if in_cell {
329 labels[i] = cell_labels[ci];
330 break;
331 }
332 }
333 }
334
335 let mut subspace_set: HashSet<Vec<usize>> = HashSet::new();
337 for cell in &all_dense_cells {
338 if cell.dimensions.len() > 1 {
339 subspace_set.insert(cell.dimensions.clone());
340 }
341 }
342 let subspaces: Vec<Vec<usize>> = subspace_set.into_iter().collect();
343
344 Ok(CliqueResult {
345 labels,
346 subspaces,
347 dense_cells: all_dense_cells,
348 n_clusters: cluster_id as usize,
349 })
350}
351
352#[derive(Debug, Clone)]
358pub struct ProclusConfig {
359 pub n_clusters: usize,
361 pub avg_dimensions: usize,
363 pub candidate_multiplier: usize,
365 pub max_iterations: usize,
367 pub min_improvement: f64,
369}
370
371impl Default for ProclusConfig {
372 fn default() -> Self {
373 Self {
374 n_clusters: 3,
375 avg_dimensions: 2,
376 candidate_multiplier: 10,
377 max_iterations: 30,
378 min_improvement: 1e-4,
379 }
380 }
381}
382
383#[derive(Debug, Clone)]
385pub struct ProclusResult<F: Float> {
386 pub labels: Array1<i32>,
388 pub medoids: Vec<usize>,
390 pub cluster_dimensions: Vec<Vec<usize>>,
392 pub medoid_coords: Array2<F>,
394 pub iterations: usize,
396}
397
398pub fn proclus<F: Float + FromPrimitive + Debug>(
404 data: ArrayView2<F>,
405 config: &ProclusConfig,
406) -> Result<ProclusResult<F>> {
407 let (n_samples, n_features) = (data.shape()[0], data.shape()[1]);
408
409 if n_samples == 0 {
410 return Err(ClusteringError::InvalidInput("Empty input data".into()));
411 }
412 if config.n_clusters == 0 || config.n_clusters > n_samples {
413 return Err(ClusteringError::InvalidInput(
414 "n_clusters must be in [1, n_samples]".into(),
415 ));
416 }
417 if config.avg_dimensions == 0 || config.avg_dimensions > n_features {
418 return Err(ClusteringError::InvalidInput(
419 "avg_dimensions must be in [1, n_features]".into(),
420 ));
421 }
422
423 let k = config.n_clusters;
424 let l = config.avg_dimensions;
425
426 let n_candidates = (k * config.candidate_multiplier).min(n_samples);
428 let mut medoids = greedy_initial_medoids(data, n_candidates, k);
429
430 let mut cluster_dims: Vec<Vec<usize>> = vec![Vec::new(); k];
432 let mut labels = Array1::from_elem(n_samples, -1i32);
433 let mut prev_objective = f64::MAX;
434 let mut iterations = 0usize;
435
436 for iter in 0..config.max_iterations {
437 iterations = iter + 1;
438
439 cluster_dims = find_cluster_dimensions(data, &medoids, l, n_features);
441
442 labels = assign_projected(data, &medoids, &cluster_dims);
444
445 let objective = compute_projected_objective(data, &medoids, &cluster_dims, &labels);
447
448 let improvement = if prev_objective < f64::MAX {
449 (prev_objective - objective) / prev_objective.abs().max(1e-15)
450 } else {
451 1.0
452 };
453
454 if improvement < config.min_improvement && iter > 0 {
455 break;
456 }
457 prev_objective = objective;
458
459 for ci in 0..k {
461 let mut best_idx = medoids[ci];
462 let mut best_cost = f64::MAX;
463 for i in 0..n_samples {
464 if labels[i] != ci as i32 {
465 continue;
466 }
467 let cost = cluster_projected_cost(data, i, &cluster_dims[ci], &labels, ci as i32);
468 if cost < best_cost {
469 best_cost = cost;
470 best_idx = i;
471 }
472 }
473 medoids[ci] = best_idx;
474 }
475 }
476
477 let mut medoid_coords = Array2::zeros((k, n_features));
478 for (ci, &mi) in medoids.iter().enumerate() {
479 medoid_coords.row_mut(ci).assign(&data.row(mi));
480 }
481
482 Ok(ProclusResult {
483 labels,
484 medoids,
485 cluster_dimensions: cluster_dims,
486 medoid_coords,
487 iterations,
488 })
489}
490
491fn greedy_initial_medoids<F: Float + FromPrimitive + Debug>(
493 data: ArrayView2<F>,
494 n_candidates: usize,
495 k: usize,
496) -> Vec<usize> {
497 let n = data.shape()[0];
498 let nc = n_candidates.min(n);
499
500 let step = (n as f64 / nc as f64).max(1.0);
502 let candidates: Vec<usize> = (0..nc)
503 .map(|i| ((i as f64 * step) as usize).min(n - 1))
504 .collect();
505
506 if candidates.len() <= k {
507 return candidates;
508 }
509
510 let mut selected = Vec::with_capacity(k);
512 selected.push(candidates[0]);
513 let mut min_dists = vec![f64::MAX; candidates.len()];
514
515 for _ in 1..k {
516 let last = *selected.last().unwrap_or(&0);
517 for (ci, &c) in candidates.iter().enumerate() {
518 let d = euclidean_sq_f64(data.row(c), data.row(last));
519 if d < min_dists[ci] {
520 min_dists[ci] = d;
521 }
522 }
523 let mut best_ci = 0;
525 let mut best_d = -1.0f64;
526 for (ci, &d) in min_dists.iter().enumerate() {
527 if d > best_d && !selected.contains(&candidates[ci]) {
528 best_d = d;
529 best_ci = ci;
530 }
531 }
532 selected.push(candidates[best_ci]);
533 }
534
535 selected
536}
537
538fn find_cluster_dimensions<F: Float + FromPrimitive + Debug>(
540 data: ArrayView2<F>,
541 medoids: &[usize],
542 avg_l: usize,
543 n_features: usize,
544) -> Vec<Vec<usize>> {
545 let n_samples = data.shape()[0];
546 let k = medoids.len();
547 let total_dims = avg_l * k;
548
549 let mut assignments = vec![0usize; n_samples];
551 for i in 0..n_samples {
552 let mut best_m = 0;
553 let mut best_d = f64::MAX;
554 for (mi, &m) in medoids.iter().enumerate() {
555 let d = euclidean_sq_f64(data.row(i), data.row(m));
556 if d < best_d {
557 best_d = d;
558 best_m = mi;
559 }
560 }
561 assignments[i] = best_m;
562 }
563
564 let mut spreads: Vec<Vec<f64>> = vec![vec![0.0; n_features]; k];
566 let mut counts = vec![0usize; k];
567
568 for i in 0..n_samples {
569 let ci = assignments[i];
570 counts[ci] += 1;
571 let mi = medoids[ci];
572 for d in 0..n_features {
573 let diff = (data[[i, d]] - data[[mi, d]]).abs().to_f64().unwrap_or(0.0);
574 spreads[ci][d] += diff;
575 }
576 }
577
578 for ci in 0..k {
579 if counts[ci] > 0 {
580 for d in 0..n_features {
581 spreads[ci][d] /= counts[ci] as f64;
582 }
583 }
584 }
585
586 let mut dim_spread: Vec<(f64, usize, usize)> = Vec::new();
588 for ci in 0..k {
589 for d in 0..n_features {
590 dim_spread.push((spreads[ci][d], ci, d));
591 }
592 }
593 dim_spread.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
594
595 let mut cluster_dims: Vec<Vec<usize>> = vec![Vec::new(); k];
597 let mut assigned = 0usize;
598 for &(_, ci, d) in &dim_spread {
599 if assigned >= total_dims {
600 break;
601 }
602 if !cluster_dims[ci].contains(&d) {
603 cluster_dims[ci].push(d);
604 assigned += 1;
605 }
606 }
607
608 for ci in 0..k {
610 if cluster_dims[ci].is_empty() {
611 cluster_dims[ci].push(0);
612 }
613 cluster_dims[ci].sort();
614 }
615
616 cluster_dims
617}
618
619fn assign_projected<F: Float + FromPrimitive + Debug>(
621 data: ArrayView2<F>,
622 medoids: &[usize],
623 cluster_dims: &[Vec<usize>],
624) -> Array1<i32> {
625 let n = data.shape()[0];
626 let k = medoids.len();
627 let mut labels = Array1::from_elem(n, -1i32);
628
629 for i in 0..n {
630 let mut best_m = 0i32;
631 let mut best_d = f64::MAX;
632 for ci in 0..k {
633 let mi = medoids[ci];
634 let dims = &cluster_dims[ci];
635 let mut dist = 0.0f64;
636 for &d in dims {
637 let diff = (data[[i, d]] - data[[mi, d]]).to_f64().unwrap_or(0.0);
638 dist += diff * diff;
639 }
640 if dist < best_d {
641 best_d = dist;
642 best_m = ci as i32;
643 }
644 }
645 labels[i] = best_m;
646 }
647
648 labels
649}
650
651fn compute_projected_objective<F: Float + FromPrimitive + Debug>(
653 data: ArrayView2<F>,
654 medoids: &[usize],
655 cluster_dims: &[Vec<usize>],
656 labels: &Array1<i32>,
657) -> f64 {
658 let n = data.shape()[0];
659 let mut total = 0.0f64;
660 for i in 0..n {
661 let ci = labels[i];
662 if ci < 0 {
663 continue;
664 }
665 let ci = ci as usize;
666 let mi = medoids[ci];
667 for &d in &cluster_dims[ci] {
668 let diff = (data[[i, d]] - data[[mi, d]]).to_f64().unwrap_or(0.0);
669 total += diff * diff;
670 }
671 }
672 total
673}
674
675fn cluster_projected_cost<F: Float + FromPrimitive + Debug>(
677 data: ArrayView2<F>,
678 idx: usize,
679 dims: &[usize],
680 labels: &Array1<i32>,
681 ci_label: i32,
682) -> f64 {
683 let n = data.shape()[0];
684 let mut cost = 0.0f64;
685 for i in 0..n {
686 if labels[i] != ci_label {
687 continue;
688 }
689 for &d in dims {
690 let diff = (data[[i, d]] - data[[idx, d]]).to_f64().unwrap_or(0.0);
691 cost += diff * diff;
692 }
693 }
694 cost
695}
696
697#[derive(Debug, Clone)]
703pub struct SscConfig {
704 pub lambda: f64,
706 pub max_iterations: usize,
708 pub tolerance: f64,
710 pub n_clusters: usize,
712 pub rho: f64,
714}
715
716impl Default for SscConfig {
717 fn default() -> Self {
718 Self {
719 lambda: 0.1,
720 max_iterations: 200,
721 tolerance: 1e-6,
722 n_clusters: 3,
723 rho: 1.0,
724 }
725 }
726}
727
728#[derive(Debug, Clone)]
730pub struct SscResult<F: Float> {
731 pub labels: Array1<i32>,
733 pub coefficients: Array2<F>,
735 pub affinity: Array2<F>,
737 pub n_clusters: usize,
739}
740
741pub fn ssc<F: Float + FromPrimitive + Debug>(
747 data: ArrayView2<F>,
748 config: &SscConfig,
749) -> Result<SscResult<F>> {
750 let (n_samples, n_features) = (data.shape()[0], data.shape()[1]);
751
752 if n_samples == 0 {
753 return Err(ClusteringError::InvalidInput("Empty input data".into()));
754 }
755 if config.n_clusters == 0 || config.n_clusters > n_samples {
756 return Err(ClusteringError::InvalidInput(
757 "n_clusters must be in [1, n_samples]".into(),
758 ));
759 }
760
761 let coefficients = ssc_admm(data, config)?;
764
765 let mut affinity = Array2::<F>::zeros((n_samples, n_samples));
767 for i in 0..n_samples {
768 for j in 0..n_samples {
769 affinity[[i, j]] = coefficients[[i, j]].abs() + coefficients[[j, i]].abs();
770 }
771 }
772
773 let labels = spectral_from_affinity(&affinity, config.n_clusters)?;
775 let n_clusters = config.n_clusters;
776
777 Ok(SscResult {
778 labels,
779 coefficients,
780 affinity,
781 n_clusters,
782 })
783}
784
785fn ssc_admm<F: Float + FromPrimitive + Debug>(
787 data: ArrayView2<F>,
788 config: &SscConfig,
789) -> Result<Array2<F>> {
790 let n = data.shape()[0];
791 let lambda_f = F::from(config.lambda).unwrap_or_else(|| F::one());
792 let rho_f = F::from(config.rho).unwrap_or_else(|| F::one());
793 let one = F::one();
794
795 let mut xtx = Array2::<F>::zeros((n, n));
797 for i in 0..n {
798 for j in 0..n {
799 let mut dot = F::zero();
800 for d in 0..data.shape()[1] {
801 dot = dot + data[[i, d]] * data[[j, d]];
802 }
803 xtx[[i, j]] = dot;
804 }
805 }
806
807 let mut c_mat = Array2::<F>::zeros((n, n));
813 let mut z_mat = Array2::<F>::zeros((n, n));
814 let mut u_mat = Array2::<F>::zeros((n, n));
815
816 let mut a_mat = xtx.clone();
818 for i in 0..n {
819 a_mat[[i, i]] = a_mat[[i, i]] + rho_f;
820 }
821
822 let tol_f = F::from(config.tolerance).unwrap_or_else(|| F::epsilon());
823
824 for _iter in 0..config.max_iterations {
825 for j in 0..n {
827 let mut rhs = Array1::<F>::zeros(n);
828 for i in 0..n {
829 rhs[i] = xtx[[i, j]] + rho_f * (z_mat[[i, j]] - u_mat[[i, j]]);
830 }
831 let mut x = Array1::<F>::zeros(n);
833 for _gs in 0..20 {
834 for i in 0..n {
835 let mut sum = rhs[i];
836 for k in 0..n {
837 if k != i {
838 sum = sum - a_mat[[i, k]] * x[k];
839 }
840 }
841 if a_mat[[i, i]].abs() > F::epsilon() {
842 x[i] = sum / a_mat[[i, i]];
843 }
844 }
845 }
846 x[j] = F::zero();
848 for i in 0..n {
849 c_mat[[i, j]] = x[i];
850 }
851 }
852
853 let thresh = lambda_f / rho_f;
855 for i in 0..n {
856 for j in 0..n {
857 let v = c_mat[[i, j]] + u_mat[[i, j]];
858 z_mat[[i, j]] = soft_threshold(v, thresh);
859 }
860 }
861
862 let mut primal_res = F::zero();
864 for i in 0..n {
865 for j in 0..n {
866 let r = c_mat[[i, j]] - z_mat[[i, j]];
867 u_mat[[i, j]] = u_mat[[i, j]] + r;
868 primal_res = primal_res + r * r;
869 }
870 }
871
872 if primal_res.sqrt() < tol_f {
873 break;
874 }
875 }
876
877 Ok(c_mat)
878}
879
880fn soft_threshold<F: Float>(x: F, t: F) -> F {
882 let abs_x = x.abs();
883 if abs_x <= t {
884 F::zero()
885 } else if x > F::zero() {
886 abs_x - t
887 } else {
888 t - abs_x
889 }
890}
891
892fn spectral_from_affinity<F: Float + FromPrimitive + Debug>(
894 affinity: &Array2<F>,
895 k: usize,
896) -> Result<Array1<i32>> {
897 let n = affinity.shape()[0];
898 if n <= k {
899 return Ok(Array1::from_vec((0..n as i32).collect()));
900 }
901
902 let mut degree = Array1::<F>::zeros(n);
904 for i in 0..n {
905 for j in 0..n {
906 degree[i] = degree[i] + affinity[[i, j]];
907 }
908 }
909
910 let mut l_sym = Array2::<F>::zeros((n, n));
912 for i in 0..n {
913 for j in 0..n {
914 let di = if degree[i] > F::epsilon() {
915 F::one() / degree[i].sqrt()
916 } else {
917 F::zero()
918 };
919 let dj = if degree[j] > F::epsilon() {
920 F::one() / degree[j].sqrt()
921 } else {
922 F::zero()
923 };
924 l_sym[[i, j]] = if i == j {
925 F::one() - di * affinity[[i, j]] * dj
926 } else {
927 F::zero() - di * affinity[[i, j]] * dj
928 };
929 }
930 }
931
932 let shifted = {
935 let mut m = Array2::<F>::zeros((n, n));
936 for i in 0..n {
937 for j in 0..n {
938 m[[i, j]] = if i == j {
939 F::one() - l_sym[[i, j]]
940 } else {
941 F::zero() - l_sym[[i, j]]
942 };
943 }
944 }
945 m
946 };
947
948 let mut eigvecs = Array2::<F>::zeros((n, k));
949 let mut deflated = shifted.clone();
950
951 for kk in 0..k {
952 let v = power_iteration(&deflated, 100)?;
953 for i in 0..n {
954 eigvecs[[i, kk]] = v[i];
955 }
956 let eigenval = rayleigh_quotient(&deflated, &v);
958 for i in 0..n {
959 for j in 0..n {
960 deflated[[i, j]] = deflated[[i, j]] - eigenval * v[i] * v[j];
961 }
962 }
963 }
964
965 for i in 0..n {
967 let mut norm = F::zero();
968 for kk in 0..k {
969 norm = norm + eigvecs[[i, kk]] * eigvecs[[i, kk]];
970 }
971 let norm = norm.sqrt();
972 if norm > F::epsilon() {
973 for kk in 0..k {
974 eigvecs[[i, kk]] = eigvecs[[i, kk]] / norm;
975 }
976 }
977 }
978
979 let labels = simple_kmeans(eigvecs.view(), k, 50);
981 Ok(labels)
982}
983
984fn power_iteration<F: Float + FromPrimitive + Debug>(
986 mat: &Array2<F>,
987 max_iter: usize,
988) -> Result<Array1<F>> {
989 let n = mat.shape()[0];
990 let mut v = Array1::<F>::zeros(n);
991 for i in 0..n {
993 v[i] = if i % 2 == 0 {
994 F::one()
995 } else {
996 F::zero() - F::one()
997 };
998 }
999 let mut norm = F::zero();
1001 for i in 0..n {
1002 norm = norm + v[i] * v[i];
1003 }
1004 let norm = norm.sqrt();
1005 if norm > F::epsilon() {
1006 for i in 0..n {
1007 v[i] = v[i] / norm;
1008 }
1009 }
1010
1011 for _ in 0..max_iter {
1012 let mut w = Array1::<F>::zeros(n);
1014 for i in 0..n {
1015 for j in 0..n {
1016 w[i] = w[i] + mat[[i, j]] * v[j];
1017 }
1018 }
1019 let mut norm = F::zero();
1021 for i in 0..n {
1022 norm = norm + w[i] * w[i];
1023 }
1024 let norm = norm.sqrt();
1025 if norm < F::epsilon() {
1026 break;
1027 }
1028 for i in 0..n {
1029 v[i] = w[i] / norm;
1030 }
1031 }
1032 Ok(v)
1033}
1034
1035fn rayleigh_quotient<F: Float>(mat: &Array2<F>, v: &Array1<F>) -> F {
1037 let n = v.len();
1038 let mut num = F::zero();
1039 let mut den = F::zero();
1040 for i in 0..n {
1041 den = den + v[i] * v[i];
1042 for j in 0..n {
1043 num = num + v[i] * mat[[i, j]] * v[j];
1044 }
1045 }
1046 if den > F::epsilon() {
1047 num / den
1048 } else {
1049 F::zero()
1050 }
1051}
1052
1053fn simple_kmeans<F: Float + FromPrimitive + Debug>(
1055 data: ArrayView2<F>,
1056 k: usize,
1057 max_iter: usize,
1058) -> Array1<i32> {
1059 let (n, d) = (data.shape()[0], data.shape()[1]);
1060 if n == 0 || k == 0 {
1061 return Array1::from_elem(n, -1i32);
1062 }
1063
1064 let mut centroids = Array2::<F>::zeros((k, d));
1066 let step = (n as f64 / k as f64).max(1.0);
1067 for ci in 0..k {
1068 let idx = ((ci as f64 * step) as usize).min(n - 1);
1069 centroids.row_mut(ci).assign(&data.row(idx));
1070 }
1071
1072 let mut labels = Array1::from_elem(n, 0i32);
1073
1074 for _ in 0..max_iter {
1075 let mut changed = false;
1077 for i in 0..n {
1078 let mut best = 0i32;
1079 let mut best_d = f64::MAX;
1080 for ci in 0..k {
1081 let dd = euclidean_sq_f64(data.row(i), centroids.row(ci));
1082 if dd < best_d {
1083 best_d = dd;
1084 best = ci as i32;
1085 }
1086 }
1087 if labels[i] != best {
1088 labels[i] = best;
1089 changed = true;
1090 }
1091 }
1092 if !changed {
1093 break;
1094 }
1095
1096 let mut counts = vec![0usize; k];
1098 let mut sums = Array2::<F>::zeros((k, d));
1099 for i in 0..n {
1100 let ci = labels[i] as usize;
1101 counts[ci] += 1;
1102 for dd in 0..d {
1103 sums[[ci, dd]] = sums[[ci, dd]] + data[[i, dd]];
1104 }
1105 }
1106 for ci in 0..k {
1107 if counts[ci] > 0 {
1108 let cnt = F::from(counts[ci]).unwrap_or_else(|| F::one());
1109 for dd in 0..d {
1110 centroids[[ci, dd]] = sums[[ci, dd]] / cnt;
1111 }
1112 }
1113 }
1114 }
1115
1116 labels
1117}
1118
1119fn euclidean_sq_f64<F: Float>(a: ArrayView1<F>, b: ArrayView1<F>) -> f64 {
1121 let mut s = 0.0f64;
1122 for i in 0..a.len() {
1123 let d = (a[i] - b[i]).to_f64().unwrap_or(0.0);
1124 s += d * d;
1125 }
1126 s
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1135pub enum SubspaceSelectionMethod {
1136 Variance,
1138 Entropy,
1140 CorrelationFiltering,
1142}
1143
1144pub fn select_subspace<F: Float + FromPrimitive + Debug>(
1148 data: ArrayView2<F>,
1149 k: usize,
1150 method: SubspaceSelectionMethod,
1151) -> Result<Vec<usize>> {
1152 let n_features = data.shape()[1];
1153 if k == 0 || k > n_features {
1154 return Err(ClusteringError::InvalidInput(
1155 "k must be in [1, n_features]".into(),
1156 ));
1157 }
1158
1159 match method {
1160 SubspaceSelectionMethod::Variance => select_by_variance(data, k),
1161 SubspaceSelectionMethod::Entropy => select_by_entropy(data, k),
1162 SubspaceSelectionMethod::CorrelationFiltering => select_by_correlation(data, k),
1163 }
1164}
1165
1166fn select_by_variance<F: Float + FromPrimitive + Debug>(
1167 data: ArrayView2<F>,
1168 k: usize,
1169) -> Result<Vec<usize>> {
1170 let (n, d) = (data.shape()[0], data.shape()[1]);
1171 if n < 2 {
1172 let mut r: Vec<usize> = (0..k.min(d)).collect();
1173 return Ok(r);
1174 }
1175
1176 let mut variances: Vec<(usize, f64)> = Vec::with_capacity(d);
1177 for dim in 0..d {
1178 let mean = (0..n)
1179 .map(|i| data[[i, dim]].to_f64().unwrap_or(0.0))
1180 .sum::<f64>()
1181 / n as f64;
1182 let var = (0..n)
1183 .map(|i| {
1184 let diff = data[[i, dim]].to_f64().unwrap_or(0.0) - mean;
1185 diff * diff
1186 })
1187 .sum::<f64>()
1188 / (n - 1) as f64;
1189 variances.push((dim, var));
1190 }
1191 variances.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1192 let mut result: Vec<usize> = variances.iter().take(k).map(|&(d, _)| d).collect();
1193 result.sort();
1194 Ok(result)
1195}
1196
1197fn select_by_entropy<F: Float + FromPrimitive + Debug>(
1198 data: ArrayView2<F>,
1199 k: usize,
1200) -> Result<Vec<usize>> {
1201 let (n, d) = (data.shape()[0], data.shape()[1]);
1202 let n_bins = 10usize;
1203
1204 let mut entropies: Vec<(usize, f64)> = Vec::with_capacity(d);
1205 for dim in 0..d {
1206 let mut min_v = f64::MAX;
1207 let mut max_v = f64::MIN;
1208 for i in 0..n {
1209 let v = data[[i, dim]].to_f64().unwrap_or(0.0);
1210 if v < min_v {
1211 min_v = v;
1212 }
1213 if v > max_v {
1214 max_v = v;
1215 }
1216 }
1217 let range = (max_v - min_v).max(1e-15);
1218 let mut counts = vec![0usize; n_bins];
1219 for i in 0..n {
1220 let v = data[[i, dim]].to_f64().unwrap_or(0.0);
1221 let bin = (((v - min_v) / range) * (n_bins as f64 - 1e-10)) as usize;
1222 counts[bin.min(n_bins - 1)] += 1;
1223 }
1224 let entropy: f64 = counts
1225 .iter()
1226 .filter(|&&c| c > 0)
1227 .map(|&c| {
1228 let p = c as f64 / n as f64;
1229 -p * p.ln()
1230 })
1231 .sum();
1232 entropies.push((dim, entropy));
1233 }
1234 entropies.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1235 let mut result: Vec<usize> = entropies.iter().take(k).map(|&(d, _)| d).collect();
1236 result.sort();
1237 Ok(result)
1238}
1239
1240fn select_by_correlation<F: Float + FromPrimitive + Debug>(
1241 data: ArrayView2<F>,
1242 k: usize,
1243) -> Result<Vec<usize>> {
1244 let (n, d) = (data.shape()[0], data.shape()[1]);
1245
1246 let mut means = vec![0.0f64; d];
1248 for i in 0..n {
1249 for dim in 0..d {
1250 means[dim] += data[[i, dim]].to_f64().unwrap_or(0.0);
1251 }
1252 }
1253 for dim in 0..d {
1254 means[dim] /= n as f64;
1255 }
1256 let mut stds = vec![0.0f64; d];
1257 for i in 0..n {
1258 for dim in 0..d {
1259 let diff = data[[i, dim]].to_f64().unwrap_or(0.0) - means[dim];
1260 stds[dim] += diff * diff;
1261 }
1262 }
1263 for dim in 0..d {
1264 stds[dim] = (stds[dim] / n.max(1) as f64).sqrt().max(1e-15);
1265 }
1266
1267 let mut variances: Vec<(usize, f64)> =
1269 stds.iter().enumerate().map(|(d, &s)| (d, s * s)).collect();
1270 variances.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1271
1272 let mut selected = vec![variances[0].0];
1273
1274 while selected.len() < k {
1275 let mut best_dim = None;
1276 let mut best_score = f64::MAX;
1277 for &(dim, _) in &variances {
1278 if selected.contains(&dim) {
1279 continue;
1280 }
1281 let mut max_corr = 0.0f64;
1283 for &s in &selected {
1284 let mut cov = 0.0f64;
1285 for i in 0..n {
1286 let a = data[[i, dim]].to_f64().unwrap_or(0.0) - means[dim];
1287 let b = data[[i, s]].to_f64().unwrap_or(0.0) - means[s];
1288 cov += a * b;
1289 }
1290 cov /= n.max(1) as f64;
1291 let corr = (cov / (stds[dim] * stds[s])).abs();
1292 if corr > max_corr {
1293 max_corr = corr;
1294 }
1295 }
1296 if max_corr < best_score {
1297 best_score = max_corr;
1298 best_dim = Some(dim);
1299 }
1300 }
1301 match best_dim {
1302 Some(d) => selected.push(d),
1303 None => break,
1304 }
1305 }
1306
1307 selected.sort();
1308 Ok(selected)
1309}
1310
1311#[derive(Debug, Clone)]
1317pub struct SubspaceQuality {
1318 pub coverage: f64,
1320 pub avg_density: f64,
1322 pub dimensionality_ratio: f64,
1324 pub separation: f64,
1326}
1327
1328pub fn subspace_quality<F: Float + FromPrimitive + Debug>(
1330 data: ArrayView2<F>,
1331 labels: &Array1<i32>,
1332 cluster_dims: &[Vec<usize>],
1333) -> Result<SubspaceQuality> {
1334 let (n, d) = (data.shape()[0], data.shape()[1]);
1335
1336 if labels.len() != n {
1337 return Err(ClusteringError::InvalidInput(
1338 "labels length must match n_samples".into(),
1339 ));
1340 }
1341
1342 let n_assigned = labels.iter().filter(|&&l| l >= 0).count();
1343 let coverage = n_assigned as f64 / n.max(1) as f64;
1344
1345 let k = cluster_dims.len();
1346 let avg_dims: f64 = if k > 0 {
1347 cluster_dims.iter().map(|d| d.len() as f64).sum::<f64>() / k as f64
1348 } else {
1349 0.0
1350 };
1351 let dimensionality_ratio = avg_dims / d.max(1) as f64;
1352
1353 let mut counts = vec![0usize; k];
1355 for &l in labels.iter() {
1356 if l >= 0 && (l as usize) < k {
1357 counts[l as usize] += 1;
1358 }
1359 }
1360
1361 let avg_density = if k > 0 && avg_dims > 0.0 {
1362 let total_assigned: usize = counts.iter().sum();
1363 total_assigned as f64 / (k as f64 * avg_dims)
1364 } else {
1365 0.0
1366 };
1367
1368 let mut centroids: Vec<Vec<f64>> = vec![vec![0.0; d]; k];
1370 for i in 0..n {
1371 let l = labels[i];
1372 if l >= 0 && (l as usize) < k {
1373 let ci = l as usize;
1374 for dd in 0..d {
1375 centroids[ci][dd] += data[[i, dd]].to_f64().unwrap_or(0.0);
1376 }
1377 }
1378 }
1379 for ci in 0..k {
1380 if counts[ci] > 0 {
1381 for dd in 0..d {
1382 centroids[ci][dd] /= counts[ci] as f64;
1383 }
1384 }
1385 }
1386
1387 let mut separation = 0.0f64;
1388 let mut sep_count = 0usize;
1389 for ci in 0..k {
1390 for cj in (ci + 1)..k {
1391 let mut dist = 0.0f64;
1392 let mut union_dims: HashSet<usize> = HashSet::new();
1394 for &d in &cluster_dims[ci] {
1395 union_dims.insert(d);
1396 }
1397 for &d in &cluster_dims[cj] {
1398 union_dims.insert(d);
1399 }
1400 for &dd in &union_dims {
1401 let diff = centroids[ci][dd] - centroids[cj][dd];
1402 dist += diff * diff;
1403 }
1404 separation += dist.sqrt();
1405 sep_count += 1;
1406 }
1407 }
1408 if sep_count > 0 {
1409 separation /= sep_count as f64;
1410 }
1411
1412 Ok(SubspaceQuality {
1413 coverage,
1414 avg_density,
1415 dimensionality_ratio,
1416 separation,
1417 })
1418}
1419
1420#[cfg(test)]
1425mod tests {
1426 use super::*;
1427 use scirs2_core::ndarray::Array2;
1428
1429 fn make_two_cluster_data() -> Array2<f64> {
1430 let mut data = Vec::new();
1432 for _ in 0..20 {
1433 data.extend_from_slice(&[1.0, 1.0, 0.5, 0.5]);
1434 }
1435 for _ in 0..20 {
1436 data.extend_from_slice(&[5.0, 5.0, 0.5, 0.5]);
1437 }
1438 let mut arr = Array2::from_shape_vec((40, 4), data).expect("shape construction failed");
1440 for i in 0..40 {
1441 let noise = (i as f64 * 0.037).sin() * 0.2;
1442 arr[[i, 0]] = arr[[i, 0]] + noise;
1443 arr[[i, 1]] = arr[[i, 1]] + noise * 0.5;
1444 }
1445 arr
1446 }
1447
1448 #[test]
1449 fn test_clique_basic() {
1450 let data = make_two_cluster_data();
1451 let config = CliqueConfig {
1452 n_intervals: 5,
1453 density_threshold: 0.1,
1454 max_subspace_dim: 2,
1455 };
1456 let result = clique(data.view(), &config).expect("clique failed");
1457 assert!(
1459 result.n_clusters >= 1,
1460 "Expected at least 1 cluster, got {}",
1461 result.n_clusters
1462 );
1463 let assigned = result.labels.iter().filter(|&&l| l >= 0).count();
1465 assert!(
1466 assigned > 20,
1467 "Expected most points assigned, got {}",
1468 assigned
1469 );
1470 }
1471
1472 #[test]
1473 fn test_clique_empty_data() {
1474 let data = Array2::<f64>::zeros((0, 4));
1475 let config = CliqueConfig::default();
1476 assert!(clique(data.view(), &config).is_err());
1477 }
1478
1479 #[test]
1480 fn test_clique_invalid_params() {
1481 let data = make_two_cluster_data();
1482 let config = CliqueConfig {
1483 n_intervals: 0,
1484 ..Default::default()
1485 };
1486 assert!(clique(data.view(), &config).is_err());
1487
1488 let config2 = CliqueConfig {
1489 density_threshold: 0.0,
1490 ..Default::default()
1491 };
1492 assert!(clique(data.view(), &config2).is_err());
1493 }
1494
1495 #[test]
1496 fn test_proclus_basic() {
1497 let data = make_two_cluster_data();
1498 let config = ProclusConfig {
1499 n_clusters: 2,
1500 avg_dimensions: 2,
1501 max_iterations: 20,
1502 ..Default::default()
1503 };
1504 let result = proclus(data.view(), &config).expect("proclus failed");
1505 assert_eq!(result.labels.len(), 40);
1506 assert_eq!(result.medoids.len(), 2);
1507 assert_eq!(result.cluster_dimensions.len(), 2);
1508 for dims in &result.cluster_dimensions {
1510 assert!(!dims.is_empty());
1511 }
1512 }
1513
1514 #[test]
1515 fn test_proclus_empty_data() {
1516 let data = Array2::<f64>::zeros((0, 3));
1517 let config = ProclusConfig::default();
1518 assert!(proclus(data.view(), &config).is_err());
1519 }
1520
1521 #[test]
1522 fn test_proclus_invalid_k() {
1523 let data = make_two_cluster_data();
1524 let config = ProclusConfig {
1525 n_clusters: 0,
1526 ..Default::default()
1527 };
1528 assert!(proclus(data.view(), &config).is_err());
1529 }
1530
1531 #[test]
1532 fn test_ssc_basic() {
1533 let data = Array2::from_shape_vec(
1535 (12, 3),
1536 vec![
1537 1.0, 0.0, 0.0, 1.1, 0.1, 0.0, 0.9, -0.1, 0.0, 1.2, 0.05, 0.0, 0.0, 1.0, 0.0, 0.1,
1538 1.1, 0.0, -0.1, 0.9, 0.0, 0.05, 1.2, 0.0, 0.0, 0.0, 1.0, 0.0, 0.1, 1.1, 0.0, -0.1,
1539 0.9, 0.0, 0.05, 1.2,
1540 ],
1541 )
1542 .expect("shape failed");
1543
1544 let config = SscConfig {
1545 n_clusters: 3,
1546 lambda: 0.5,
1547 max_iterations: 50,
1548 ..Default::default()
1549 };
1550 let result = ssc(data.view(), &config).expect("ssc failed");
1551 assert_eq!(result.labels.len(), 12);
1552 assert_eq!(result.n_clusters, 3);
1553 let n = result.affinity.shape()[0];
1555 for i in 0..n {
1556 for j in 0..n {
1557 let diff = (result.affinity[[i, j]] - result.affinity[[j, i]]).abs();
1558 assert!(diff < 1e-10);
1559 }
1560 }
1561 }
1562
1563 #[test]
1564 fn test_ssc_empty_data() {
1565 let data = Array2::<f64>::zeros((0, 3));
1566 let config = SscConfig::default();
1567 assert!(ssc(data.view(), &config).is_err());
1568 }
1569
1570 #[test]
1571 fn test_select_subspace_variance() {
1572 let data = make_two_cluster_data();
1573 let selected = select_subspace(data.view(), 2, SubspaceSelectionMethod::Variance)
1574 .expect("variance selection failed");
1575 assert_eq!(selected.len(), 2);
1576 assert!(selected.contains(&0));
1578 assert!(selected.contains(&1));
1579 }
1580
1581 #[test]
1582 fn test_select_subspace_entropy() {
1583 let data = make_two_cluster_data();
1584 let selected = select_subspace(data.view(), 2, SubspaceSelectionMethod::Entropy)
1585 .expect("entropy selection failed");
1586 assert_eq!(selected.len(), 2);
1587 }
1588
1589 #[test]
1590 fn test_select_subspace_correlation() {
1591 let data = make_two_cluster_data();
1592 let selected = select_subspace(
1593 data.view(),
1594 2,
1595 SubspaceSelectionMethod::CorrelationFiltering,
1596 )
1597 .expect("correlation selection failed");
1598 assert_eq!(selected.len(), 2);
1599 }
1600
1601 #[test]
1602 fn test_select_subspace_invalid_k() {
1603 let data = make_two_cluster_data();
1604 assert!(select_subspace(data.view(), 0, SubspaceSelectionMethod::Variance).is_err());
1605 assert!(select_subspace(data.view(), 100, SubspaceSelectionMethod::Variance).is_err());
1606 }
1607
1608 #[test]
1609 fn test_subspace_quality() {
1610 let data = make_two_cluster_data();
1611 let labels = Array1::from_vec((0..20).map(|_| 0i32).chain((0..20).map(|_| 1i32)).collect());
1612 let cluster_dims = vec![vec![0, 1], vec![0, 1]];
1613 let quality =
1614 subspace_quality(data.view(), &labels, &cluster_dims).expect("quality failed");
1615 assert!((quality.coverage - 1.0).abs() < 1e-10);
1616 assert!(quality.dimensionality_ratio > 0.0);
1617 assert!(quality.separation > 0.0);
1618 }
1619
1620 #[test]
1621 fn test_subspace_quality_with_noise() {
1622 let data = make_two_cluster_data();
1623 let mut labels_vec: Vec<i32> = (0..20).map(|_| 0).chain((0..20).map(|_| 1)).collect();
1624 labels_vec[0] = -1; let labels = Array1::from_vec(labels_vec);
1626 let cluster_dims = vec![vec![0, 1], vec![0, 1]];
1627 let quality =
1628 subspace_quality(data.view(), &labels, &cluster_dims).expect("quality failed");
1629 assert!(quality.coverage < 1.0);
1630 }
1631
1632 #[test]
1633 fn test_soft_threshold() {
1634 assert!((soft_threshold(1.5, 1.0) - 0.5).abs() < 1e-10);
1635 assert!((soft_threshold(-1.5, 1.0) - (-0.5)).abs() < 1e-10);
1636 assert!((soft_threshold(0.5, 1.0)).abs() < 1e-10);
1637 }
1638
1639 #[test]
1640 fn test_clique_single_point() {
1641 let data = Array2::from_shape_vec((1, 2), vec![1.0, 2.0]).expect("shape failed");
1642 let config = CliqueConfig {
1643 n_intervals: 5,
1644 density_threshold: 0.5,
1645 ..Default::default()
1646 };
1647 let result = clique(data.view(), &config);
1648 assert!(result.is_ok());
1649 }
1650
1651 #[test]
1652 fn test_proclus_single_cluster() {
1653 let data = Array2::from_shape_vec((10, 3), (0..30).map(|i| (i as f64) * 0.1).collect())
1654 .expect("shape failed");
1655 let config = ProclusConfig {
1656 n_clusters: 1,
1657 avg_dimensions: 2,
1658 ..Default::default()
1659 };
1660 let result = proclus(data.view(), &config).expect("proclus failed");
1661 for &l in result.labels.iter() {
1663 assert_eq!(l, 0);
1664 }
1665 }
1666
1667 #[test]
1668 fn test_euclidean_sq_f64() {
1669 let a = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1670 let b = Array1::from_vec(vec![4.0, 5.0, 6.0]);
1671 let d = euclidean_sq_f64(a.view(), b.view());
1672 assert!((d - 27.0).abs() < 1e-10);
1673 }
1674
1675 #[test]
1676 fn test_clique_f32() {
1677 let data = Array2::<f32>::from_shape_vec(
1678 (20, 2),
1679 (0..40)
1680 .map(|i| if i < 20 { 1.0f32 } else { 5.0f32 })
1681 .collect(),
1682 )
1683 .expect("shape failed");
1684 let config = CliqueConfig {
1685 n_intervals: 3,
1686 density_threshold: 0.2,
1687 ..Default::default()
1688 };
1689 let result = clique(data.view(), &config);
1690 assert!(result.is_ok());
1691 }
1692}