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 std::time::Instant;
27
28#[cfg(feature = "gpu")]
29use cubecl_utils_rs::CubeclFloat;
30
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
47pub const VERSION: &str = env!("CARGO_PKG_VERSION");
54
55#[derive(Clone, Debug)]
61pub struct EvocParams<T> {
62 pub n_neighbours: usize,
64 pub noise_level: T,
67 pub n_epochs: usize,
69 pub embedding_dim: Option<usize>,
72 pub neighbour_scale: T,
74 pub symmetrise: bool,
76 pub min_samples: usize,
78 pub base_min_cluster_size: usize,
80 pub approx_n_clusters: Option<usize>,
83 pub min_similarity_threshold: f64,
85 pub max_layers: usize,
87}
88
89impl<T: EvocFloat> Default for EvocParams<T> {
91 fn default() -> Self {
92 Self {
93 n_neighbours: 15,
94 noise_level: T::from(0.5).unwrap(),
95 n_epochs: 50,
96 embedding_dim: None,
97 neighbour_scale: T::one(),
98 symmetrise: true,
99 min_samples: 5,
100 base_min_cluster_size: 5,
101 approx_n_clusters: None,
102 min_similarity_threshold: 0.2,
103 max_layers: 10,
104 }
105 }
106}
107
108pub struct EvocResult<T> {
114 pub cluster_layers: Vec<Vec<i64>>,
117 pub membership_strengths: Vec<Vec<T>>,
119 pub persistence_scores: Vec<f64>,
121 pub nn_indices: Vec<Vec<usize>>,
123 pub nn_distances: Vec<Vec<T>>,
125}
126
127impl<T: EvocFloat> EvocResult<T> {
128 pub fn best_labels(&self) -> &[i64] {
132 if self.cluster_layers.len() <= 1 {
133 &self.cluster_layers[0]
134 } else {
135 let best = self
136 .persistence_scores
137 .iter()
138 .enumerate()
139 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
140 .map(|(i, _)| i)
141 .unwrap_or(0);
142 &self.cluster_layers[best]
143 }
144 }
145
146 pub fn best_strengths(&self) -> &[T] {
149 if self.membership_strengths.len() <= 1 {
150 &self.membership_strengths[0]
151 } else {
152 let best = self
153 .persistence_scores
154 .iter()
155 .enumerate()
156 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
157 .map(|(i, _)| i)
158 .unwrap_or(0);
159 &self.membership_strengths[best]
160 }
161 }
162
163 pub fn n_clusters(&self) -> usize {
165 let labels = self.best_labels();
166 (labels.iter().copied().reduce(i64::max).unwrap_or(-1) + 1).max(0) as usize
167 }
168}
169
170pub fn evoc<T>(
209 data: impl EvocMatrix<T>,
210 ann_type: String,
211 precomputed_knn: PreComputedKnn<T>,
212 evoc_params: &EvocParams<T>,
213 nn_params: &NearestNeighbourParamsEvoc<T>,
214 seed: usize,
215 verbose: usize,
216) -> Result<EvocResult<T>, EvocErrors>
217where
218 T: EvocFloat + AnnSearchFloat,
219 NNDescent<T>: ApplySortedUpdates<T> + NNDescentQuery<T>,
220 HnswIndex<T>: HnswState<T>,
221{
222 let data_input = data.to_mat_input();
223 let data = data_input.as_mat_ref();
224 let verbosity = parse_verbosity_level(verbose);
225
226 let start_all = Instant::now();
227
228 let (knn_indices, knn_dist) = match precomputed_knn {
230 Some((indices, distances)) => {
231 if verbosity.normal_verbosity() {
232 println!("Using precomputed kNN graph...");
233 }
234 (indices, distances)
235 }
236 None => {
237 if verbosity.normal_verbosity() {
238 println!(
239 "Running approximate nearest neighbour search using {}...",
240 ann_type
241 );
242 }
243 let start_knn = Instant::now();
244 let result = run_ann_search(
245 data,
246 evoc_params.n_neighbours,
247 ann_type,
248 nn_params,
249 seed,
250 verbose,
251 )?;
252 if verbosity.normal_verbosity() {
253 println!("kNN search done in {:.2?}.", start_knn.elapsed());
254 }
255 result
256 }
257 };
258
259 if verbosity.normal_verbosity() {
261 println!("Constructing fuzzy simplicial set...");
262 }
263 let start_graph = Instant::now();
264 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
265 let graph =
266 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
267 let adj = coo_to_adjacency_list(&graph);
268 if verbosity.normal_verbosity() {
269 println!(
270 "... fuzzy simplicial set done in {:.2?}.",
271 start_graph.elapsed()
272 );
273 }
274
275 let dim = evoc_params
278 .embedding_dim
279 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
280
281 let start_init = Instant::now();
283 let n = data.nrows();
284 let d = data.ncols();
285 let data_vecs: Vec<Vec<T>> = (0..n)
286 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
287 .collect();
288
289 if verbosity.normal_verbosity() {
290 println!("Computing label propagation initialisation...");
291 }
292 let initial_embedding =
293 label_propagation_init(&graph, dim, Some(&data_vecs), seed as u64, verbose);
294 if verbosity.normal_verbosity() {
295 println!("Label prop init done in {:.2?}.", start_init.elapsed());
296 }
297
298 if verbosity.normal_verbosity() {
300 println!(
301 "Computing {}-d node embedding ({} epochs)...",
302 dim, evoc_params.n_epochs
303 );
304 }
305 let start_embed = Instant::now();
306 let embed_params = EvocEmbeddingParams {
307 n_epochs: evoc_params.n_epochs,
308 noise_level: evoc_params.noise_level,
309 initial_alpha: T::from(0.1).unwrap(),
310 ..EvocEmbeddingParams::default()
311 };
312
313 let embedding = evoc_embedding(
314 &adj,
315 dim,
316 &embed_params,
317 Some(&initial_embedding),
318 seed as u64,
319 verbose,
320 );
321 if verbosity.normal_verbosity() {
322 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
323 }
324
325 if verbosity.normal_verbosity() {
327 println!("Running density-based clustering...");
328 }
329 let start_cluster = Instant::now();
330
331 let (cluster_layers, membership_strengths, persistence_scores) =
332 if let Some(target_k) = evoc_params.approx_n_clusters {
333 let (labels, strengths) =
334 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
335 (vec![labels], vec![strengths], vec![0.0])
336 } else {
337 build_cluster_layers(
338 &embedding,
339 evoc_params.min_samples,
340 evoc_params.base_min_cluster_size,
341 evoc_params.min_similarity_threshold,
342 evoc_params.max_layers,
343 )
344 };
345
346 if verbosity.normal_verbosity() {
347 let n_layers = cluster_layers.len();
348 println!(
349 "Clustering done in {:.2?}: {} layer(s).",
350 start_cluster.elapsed(),
351 n_layers,
352 );
353 println!("EVoC total: {:.2?}.", start_all.elapsed());
354 }
355
356 Ok(EvocResult {
357 cluster_layers,
358 membership_strengths,
359 persistence_scores,
360 nn_indices: knn_indices,
361 nn_distances: knn_dist,
362 })
363}
364
365pub fn search_for_n_clusters<T>(
389 embedding: &[Vec<T>],
390 min_samples: usize,
391 target_k: usize,
392) -> (Vec<i64>, Vec<T>)
393where
394 T: EvocFloat,
395{
396 let n = embedding.len();
397 if n == 0 {
398 return (Vec::new(), Vec::new());
399 }
400
401 let mut mst = build_mst(embedding, min_samples);
402 let linkage = mst_to_linkage_tree(&mut mst, n);
403
404 let mut lo = 2usize;
405 let mut hi = n / 2;
406
407 while hi - lo > 1 {
408 let mid = (lo + hi) / 2;
409 if mid == lo || mid == hi {
410 break;
411 }
412
413 let ct_mid = condense_tree(&linkage, n, mid);
414 let leaves_mid = extract_leaves(&ct_mid);
415 let mid_k = leaves_mid.len();
416
417 if mid_k < target_k {
418 hi = mid;
420 } else {
421 lo = mid;
423 }
424 }
425
426 let ct_lo = condense_tree(&linkage, n, lo);
428 let leaves_lo = extract_leaves(&ct_lo);
429 let labels_lo = get_cluster_label_vector(&ct_lo, &leaves_lo, n);
430 let lo_k = leaves_lo.len();
431
432 let ct_hi = condense_tree(&linkage, n, hi);
433 let leaves_hi = extract_leaves(&ct_hi);
434 let labels_hi = get_cluster_label_vector(&ct_hi, &leaves_hi, n);
435 let hi_k = leaves_hi.len();
436
437 let lo_diff = (lo_k as isize - target_k as isize).unsigned_abs();
438 let hi_diff = (hi_k as isize - target_k as isize).unsigned_abs();
439
440 if lo_diff < hi_diff {
441 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
442 (labels_lo, strengths)
443 } else if hi_diff < lo_diff {
444 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
445 (labels_hi, strengths)
446 } else {
447 let lo_assigned = labels_lo.iter().filter(|&&l| l >= 0).count();
449 let hi_assigned = labels_hi.iter().filter(|&&l| l >= 0).count();
450 if lo_assigned >= hi_assigned {
451 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
452 (labels_lo, strengths)
453 } else {
454 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
455 (labels_hi, strengths)
456 }
457 }
458}
459
460#[allow(clippy::too_many_arguments)]
492#[cfg(feature = "gpu")]
493pub fn evoc_gpu<T, R>(
494 data: impl EvocMatrix<T>,
495 ann_type: String,
496 precomputed_knn: PreComputedKnn<T>,
497 evoc_params: &EvocParams<T>,
498 nn_params: &NearestNeighbourParamsGpuEvoc<T>,
499 device: R::Device,
500 seed: usize,
501 verbose: usize,
502) -> Result<EvocResult<T>, EvocErrors>
503where
504 T: EvocFloat + AnnSearchFloat + CubeclFloat,
505 R: Runtime,
506{
507 let data_input = data.to_mat_input();
508 let data = data_input.as_mat_ref();
509 let start_all = Instant::now();
510 let verbosity = parse_verbosity_level(verbose);
511
512 let (knn_indices, knn_dist) = match precomputed_knn {
514 Some((indices, distances)) => {
515 if verbosity.normal_verbosity() {
516 println!("Using precomputed kNN graph...");
517 }
518 (indices, distances)
519 }
520 None => {
521 let k = evoc_params.n_neighbours;
528 let scaled_params: NearestNeighbourParamsGpuEvoc<T>;
529 let nn_params = if nn_params.k.is_none() || nn_params.k_build.is_none() {
530 scaled_params = NearestNeighbourParamsGpuEvoc {
531 k: nn_params.k.or(Some(k)),
532 k_build: nn_params.k_build.or(Some(2 * k)),
533 ..nn_params.clone()
534 };
535 &scaled_params
536 } else {
537 nn_params
538 };
539
540 if verbosity.normal_verbosity() {
541 println!("Running GPU nearest neighbour search using {}...", ann_type);
542 }
543 let start_knn = Instant::now();
544 let result = run_ann_search_gpu::<T, R>(
545 data,
546 evoc_params.n_neighbours,
547 ann_type,
548 nn_params,
549 device,
550 seed,
551 verbose,
552 )?;
553 if verbosity.normal_verbosity() {
554 println!("GPU kNN search done in {:.2?}.", start_knn.elapsed());
555 }
556 result
557 }
558 };
559
560 if verbosity.normal_verbosity() {
562 println!("Constructing fuzzy simplicial set...");
563 }
564 let start_graph = Instant::now();
565 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
566 let graph =
567 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
568 let adj = coo_to_adjacency_list(&graph);
569 if verbosity.normal_verbosity() {
570 println!(
571 "... fuzzy simplicial set done in {:.2?}.",
572 start_graph.elapsed()
573 );
574 }
575
576 let dim = evoc_params
578 .embedding_dim
579 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
580
581 let start_init = Instant::now();
583 let n = data.nrows();
584 let d = data.ncols();
585 let data_vecs: Vec<Vec<T>> = (0..n)
586 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
587 .collect();
588
589 if verbosity.normal_verbosity() {
590 println!("Computing label propagation initialisation...");
591 }
592 let initial_embedding = crate::graph::label_prop::label_propagation_init(
593 &graph,
594 dim,
595 Some(&data_vecs),
596 seed as u64,
597 verbose,
598 );
599 if verbosity.normal_verbosity() {
600 println!(" ... label prop init done in {:.2?}.", start_init.elapsed());
601 }
602
603 if verbosity.normal_verbosity() {
605 println!(
606 "Computing {}-d node embedding ({} epochs)...",
607 dim, evoc_params.n_epochs
608 );
609 }
610 let start_embed = Instant::now();
611 let embed_params = EvocEmbeddingParams {
612 n_epochs: evoc_params.n_epochs,
613 noise_level: evoc_params.noise_level,
614 initial_alpha: T::from(0.1).unwrap(),
615 ..EvocEmbeddingParams::default()
616 };
617
618 let embedding = evoc_embedding(
619 &adj,
620 dim,
621 &embed_params,
622 Some(&initial_embedding),
623 seed as u64,
624 verbose,
625 );
626 if verbosity.normal_verbosity() {
627 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
628 }
629
630 if verbosity.normal_verbosity() {
632 println!("Running density-based clustering...");
633 }
634 let start_cluster = Instant::now();
635
636 let (cluster_layers, membership_strengths, persistence_scores) =
637 if let Some(target_k) = evoc_params.approx_n_clusters {
638 let (labels, strengths) =
639 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
640 (vec![labels], vec![strengths], vec![0.0])
641 } else {
642 build_cluster_layers(
643 &embedding,
644 evoc_params.min_samples,
645 evoc_params.base_min_cluster_size,
646 evoc_params.min_similarity_threshold,
647 evoc_params.max_layers,
648 )
649 };
650
651 if verbosity.normal_verbosity() {
652 let n_layers = cluster_layers.len();
653 println!(
654 "Clustering done in {:.2?}: {} layer(s).",
655 start_cluster.elapsed(),
656 n_layers,
657 );
658 println!("EVoC (GPU) total: {:.2?}.", start_all.elapsed());
659 }
660
661 Ok(EvocResult {
662 cluster_layers,
663 membership_strengths,
664 persistence_scores,
665 nn_indices: knn_indices,
666 nn_distances: knn_dist,
667 })
668}