1#![allow(clippy::needless_range_loop)]
13#![warn(missing_docs)]
14
15pub mod clustering;
16pub mod errors;
17pub mod graph;
18pub mod nearest_neighbours;
19pub mod prelude;
20pub mod utils;
21
22use ann_search_rs::cpu::hnsw::{HnswIndex, HnswState};
23use ann_search_rs::cpu::nndescent::{NNDescent, NNDescentQuery};
24use ann_search_rs::prelude::AnnSearchFloat;
25use ann_search_rs::utils::nndescent_utils::ApplySortedUpdates;
26use faer::MatRef;
27use std::time::Instant;
28
29#[cfg(feature = "gpu")]
30use cubecl_utils_rs::CubeclFloat;
31
32#[cfg(feature = "gpu")]
33use cubecl::prelude::*;
34
35use crate::clustering::condensed_tree::*;
36use crate::clustering::linkage::mst_to_linkage_tree;
37use crate::clustering::mst::build_mst;
38use crate::clustering::persistence::build_cluster_layers;
39use crate::graph::embedding::*;
40use crate::graph::fuzzy_graph::*;
41use crate::graph::label_prop::label_propagation_init;
42use crate::nearest_neighbours::nearest_neighbour_cpu::*;
43use crate::prelude::*;
44
45#[cfg(feature = "gpu")]
46use crate::nearest_neighbours::nearest_neighbour_gpu::*;
47
48#[derive(Clone, Debug)]
54pub struct EvocParams<T> {
55 pub n_neighbours: usize,
57 pub noise_level: T,
60 pub n_epochs: usize,
62 pub embedding_dim: Option<usize>,
65 pub neighbour_scale: T,
67 pub symmetrise: bool,
69 pub min_samples: usize,
71 pub base_min_cluster_size: usize,
73 pub approx_n_clusters: Option<usize>,
76 pub min_similarity_threshold: f64,
78 pub max_layers: usize,
80}
81
82impl<T: EvocFloat> Default for EvocParams<T> {
84 fn default() -> Self {
85 Self {
86 n_neighbours: 15,
87 noise_level: T::from(0.5).unwrap(),
88 n_epochs: 50,
89 embedding_dim: None,
90 neighbour_scale: T::one(),
91 symmetrise: true,
92 min_samples: 5,
93 base_min_cluster_size: 5,
94 approx_n_clusters: None,
95 min_similarity_threshold: 0.2,
96 max_layers: 10,
97 }
98 }
99}
100
101pub struct EvocResult<T> {
107 pub cluster_layers: Vec<Vec<i64>>,
110 pub membership_strengths: Vec<Vec<T>>,
112 pub persistence_scores: Vec<f64>,
114 pub nn_indices: Vec<Vec<usize>>,
116 pub nn_distances: Vec<Vec<T>>,
118}
119
120impl<T: EvocFloat> EvocResult<T> {
121 pub fn best_labels(&self) -> &[i64] {
125 if self.cluster_layers.len() <= 1 {
126 &self.cluster_layers[0]
127 } else {
128 let best = self
129 .persistence_scores
130 .iter()
131 .enumerate()
132 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
133 .map(|(i, _)| i)
134 .unwrap_or(0);
135 &self.cluster_layers[best]
136 }
137 }
138
139 pub fn best_strengths(&self) -> &[T] {
142 if self.membership_strengths.len() <= 1 {
143 &self.membership_strengths[0]
144 } else {
145 let best = self
146 .persistence_scores
147 .iter()
148 .enumerate()
149 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
150 .map(|(i, _)| i)
151 .unwrap_or(0);
152 &self.membership_strengths[best]
153 }
154 }
155
156 pub fn n_clusters(&self) -> usize {
158 let labels = self.best_labels();
159 (labels.iter().copied().reduce(i64::max).unwrap_or(-1) + 1).max(0) as usize
160 }
161}
162
163pub fn evoc<T>(
200 data: MatRef<T>,
201 ann_type: String,
202 precomputed_knn: PreComputedKnn<T>,
203 evoc_params: &EvocParams<T>,
204 nn_params: &NearestNeighbourParamsEvoc<T>,
205 seed: usize,
206 verbose: usize,
207) -> Result<EvocResult<T>, EvocErrors>
208where
209 T: EvocFloat + AnnSearchFloat,
210 NNDescent<T>: ApplySortedUpdates<T> + NNDescentQuery<T>,
211 HnswIndex<T>: HnswState<T>,
212{
213 let verbosity = parse_verbosity_level(verbose);
214
215 let start_all = Instant::now();
216
217 let (knn_indices, knn_dist) = match precomputed_knn {
219 Some((indices, distances)) => {
220 if verbosity.normal_verbosity() {
221 println!("Using precomputed kNN graph...");
222 }
223 (indices, distances)
224 }
225 None => {
226 if verbosity.normal_verbosity() {
227 println!(
228 "Running approximate nearest neighbour search using {}...",
229 ann_type
230 );
231 }
232 let start_knn = Instant::now();
233 let result = run_ann_search(
234 data,
235 evoc_params.n_neighbours,
236 ann_type,
237 nn_params,
238 seed,
239 verbose,
240 )?;
241 if verbosity.normal_verbosity() {
242 println!("kNN search done in {:.2?}.", start_knn.elapsed());
243 }
244 result
245 }
246 };
247
248 if verbosity.normal_verbosity() {
250 println!("Constructing fuzzy simplicial set...");
251 }
252 let start_graph = Instant::now();
253 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
254 let graph =
255 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
256 let adj = coo_to_adjacency_list(&graph);
257 if verbosity.normal_verbosity() {
258 println!(
259 "... fuzzy simplicial set done in {:.2?}.",
260 start_graph.elapsed()
261 );
262 }
263
264 let dim = evoc_params
267 .embedding_dim
268 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
269
270 let start_init = Instant::now();
272 let n = data.nrows();
273 let d = data.ncols();
274 let data_vecs: Vec<Vec<T>> = (0..n)
275 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
276 .collect();
277
278 if verbosity.normal_verbosity() {
279 println!("Computing label propagation initialisation...");
280 }
281 let initial_embedding =
282 label_propagation_init(&graph, dim, Some(&data_vecs), seed as u64, verbose);
283 if verbosity.normal_verbosity() {
284 println!("Label prop init done in {:.2?}.", start_init.elapsed());
285 }
286
287 if verbosity.normal_verbosity() {
289 println!(
290 "Computing {}-d node embedding ({} epochs)...",
291 dim, evoc_params.n_epochs
292 );
293 }
294 let start_embed = Instant::now();
295 let embed_params = EvocEmbeddingParams {
296 n_epochs: evoc_params.n_epochs,
297 noise_level: evoc_params.noise_level,
298 initial_alpha: T::from(0.1).unwrap(),
299 ..EvocEmbeddingParams::default()
300 };
301
302 let embedding = evoc_embedding(
303 &adj,
304 dim,
305 &embed_params,
306 Some(&initial_embedding),
307 seed as u64,
308 verbose,
309 );
310 if verbosity.normal_verbosity() {
311 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
312 }
313
314 if verbosity.normal_verbosity() {
316 println!("Running density-based clustering...");
317 }
318 let start_cluster = Instant::now();
319
320 let (cluster_layers, membership_strengths, persistence_scores) =
321 if let Some(target_k) = evoc_params.approx_n_clusters {
322 let (labels, strengths) =
323 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
324 (vec![labels], vec![strengths], vec![0.0])
325 } else {
326 build_cluster_layers(
327 &embedding,
328 evoc_params.min_samples,
329 evoc_params.base_min_cluster_size,
330 evoc_params.min_similarity_threshold,
331 evoc_params.max_layers,
332 )
333 };
334
335 if verbosity.normal_verbosity() {
336 let n_layers = cluster_layers.len();
337 println!(
338 "Clustering done in {:.2?}: {} layer(s).",
339 start_cluster.elapsed(),
340 n_layers,
341 );
342 println!("EVoC total: {:.2?}.", start_all.elapsed());
343 }
344
345 Ok(EvocResult {
346 cluster_layers,
347 membership_strengths,
348 persistence_scores,
349 nn_indices: knn_indices,
350 nn_distances: knn_dist,
351 })
352}
353
354pub fn search_for_n_clusters<T>(
378 embedding: &[Vec<T>],
379 min_samples: usize,
380 target_k: usize,
381) -> (Vec<i64>, Vec<T>)
382where
383 T: EvocFloat,
384{
385 let n = embedding.len();
386 if n == 0 {
387 return (Vec::new(), Vec::new());
388 }
389
390 let mut mst = build_mst(embedding, min_samples);
391 let linkage = mst_to_linkage_tree(&mut mst, n);
392
393 let mut lo = 2usize;
394 let mut hi = n / 2;
395
396 while hi - lo > 1 {
397 let mid = (lo + hi) / 2;
398 if mid == lo || mid == hi {
399 break;
400 }
401
402 let ct_mid = condense_tree(&linkage, n, mid);
403 let leaves_mid = extract_leaves(&ct_mid);
404 let mid_k = leaves_mid.len();
405
406 if mid_k < target_k {
407 hi = mid;
409 } else {
410 lo = mid;
412 }
413 }
414
415 let ct_lo = condense_tree(&linkage, n, lo);
417 let leaves_lo = extract_leaves(&ct_lo);
418 let labels_lo = get_cluster_label_vector(&ct_lo, &leaves_lo, n);
419 let lo_k = leaves_lo.len();
420
421 let ct_hi = condense_tree(&linkage, n, hi);
422 let leaves_hi = extract_leaves(&ct_hi);
423 let labels_hi = get_cluster_label_vector(&ct_hi, &leaves_hi, n);
424 let hi_k = leaves_hi.len();
425
426 let lo_diff = (lo_k as isize - target_k as isize).unsigned_abs();
427 let hi_diff = (hi_k as isize - target_k as isize).unsigned_abs();
428
429 if lo_diff < hi_diff {
430 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
431 (labels_lo, strengths)
432 } else if hi_diff < lo_diff {
433 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
434 (labels_hi, strengths)
435 } else {
436 let lo_assigned = labels_lo.iter().filter(|&&l| l >= 0).count();
438 let hi_assigned = labels_hi.iter().filter(|&&l| l >= 0).count();
439 if lo_assigned >= hi_assigned {
440 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
441 (labels_lo, strengths)
442 } else {
443 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
444 (labels_hi, strengths)
445 }
446 }
447}
448
449#[allow(clippy::too_many_arguments)]
479#[cfg(feature = "gpu")]
480pub fn evoc_gpu<T, R>(
481 data: MatRef<T>,
482 ann_type: String,
483 precomputed_knn: PreComputedKnn<T>,
484 evoc_params: &EvocParams<T>,
485 nn_params: &NearestNeighbourParamsGpuEvoc<T>,
486 device: R::Device,
487 seed: usize,
488 verbose: usize,
489) -> Result<EvocResult<T>, EvocErrors>
490where
491 T: EvocFloat + AnnSearchFloat + CubeclFloat,
492 R: Runtime,
493{
494 let start_all = Instant::now();
495 let verbosity = parse_verbosity_level(verbose);
496
497 let (knn_indices, knn_dist) = match precomputed_knn {
499 Some((indices, distances)) => {
500 if verbosity.normal_verbosity() {
501 println!("Using precomputed kNN graph...");
502 }
503 (indices, distances)
504 }
505 None => {
506 let k = evoc_params.n_neighbours;
513 let scaled_params: NearestNeighbourParamsGpuEvoc<T>;
514 let nn_params = if nn_params.k.is_none() || nn_params.k_build.is_none() {
515 scaled_params = NearestNeighbourParamsGpuEvoc {
516 k: nn_params.k.or(Some(k)),
517 k_build: nn_params.k_build.or(Some(2 * k)),
518 ..nn_params.clone()
519 };
520 &scaled_params
521 } else {
522 nn_params
523 };
524
525 if verbosity.normal_verbosity() {
526 println!("Running GPU nearest neighbour search using {}...", ann_type);
527 }
528 let start_knn = Instant::now();
529 let result = run_ann_search_gpu::<T, R>(
530 data,
531 evoc_params.n_neighbours,
532 ann_type,
533 nn_params,
534 device,
535 seed,
536 verbose,
537 )?;
538 if verbosity.normal_verbosity() {
539 println!("GPU kNN search done in {:.2?}.", start_knn.elapsed());
540 }
541 result
542 }
543 };
544
545 if verbosity.normal_verbosity() {
547 println!("Constructing fuzzy simplicial set...");
548 }
549 let start_graph = Instant::now();
550 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
551 let graph =
552 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
553 let adj = coo_to_adjacency_list(&graph);
554 if verbosity.normal_verbosity() {
555 println!(
556 "... fuzzy simplicial set done in {:.2?}.",
557 start_graph.elapsed()
558 );
559 }
560
561 let dim = evoc_params
563 .embedding_dim
564 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
565
566 let start_init = Instant::now();
568 let n = data.nrows();
569 let d = data.ncols();
570 let data_vecs: Vec<Vec<T>> = (0..n)
571 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
572 .collect();
573
574 if verbosity.normal_verbosity() {
575 println!("Computing label propagation initialisation...");
576 }
577 let initial_embedding = crate::graph::label_prop::label_propagation_init(
578 &graph,
579 dim,
580 Some(&data_vecs),
581 seed as u64,
582 verbose,
583 );
584 if verbosity.normal_verbosity() {
585 println!(" ... label prop init done in {:.2?}.", start_init.elapsed());
586 }
587
588 if verbosity.normal_verbosity() {
590 println!(
591 "Computing {}-d node embedding ({} epochs)...",
592 dim, evoc_params.n_epochs
593 );
594 }
595 let start_embed = Instant::now();
596 let embed_params = EvocEmbeddingParams {
597 n_epochs: evoc_params.n_epochs,
598 noise_level: evoc_params.noise_level,
599 initial_alpha: T::from(0.1).unwrap(),
600 ..EvocEmbeddingParams::default()
601 };
602
603 let embedding = evoc_embedding(
604 &adj,
605 dim,
606 &embed_params,
607 Some(&initial_embedding),
608 seed as u64,
609 verbose,
610 );
611 if verbosity.normal_verbosity() {
612 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
613 }
614
615 if verbosity.normal_verbosity() {
617 println!("Running density-based clustering...");
618 }
619 let start_cluster = Instant::now();
620
621 let (cluster_layers, membership_strengths, persistence_scores) =
622 if let Some(target_k) = evoc_params.approx_n_clusters {
623 let (labels, strengths) =
624 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
625 (vec![labels], vec![strengths], vec![0.0])
626 } else {
627 build_cluster_layers(
628 &embedding,
629 evoc_params.min_samples,
630 evoc_params.base_min_cluster_size,
631 evoc_params.min_similarity_threshold,
632 evoc_params.max_layers,
633 )
634 };
635
636 if verbosity.normal_verbosity() {
637 let n_layers = cluster_layers.len();
638 println!(
639 "Clustering done in {:.2?}: {} layer(s).",
640 start_cluster.elapsed(),
641 n_layers,
642 );
643 println!("EVoC (GPU) total: {:.2?}.", start_all.elapsed());
644 }
645
646 Ok(EvocResult {
647 cluster_layers,
648 membership_strengths,
649 persistence_scores,
650 nn_indices: knn_indices,
651 nn_distances: knn_dist,
652 })
653}