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