1#![allow(clippy::needless_range_loop)]
13#![warn(missing_docs)]
14
15pub mod clustering;
16pub mod errors;
17pub mod graph;
18pub mod prelude;
19pub mod utils;
20
21use ann_search_rs::cpu::hnsw::{HnswIndex, HnswState};
22use ann_search_rs::cpu::nndescent::{ApplySortedUpdates, NNDescent, NNDescentQuery};
23use ann_search_rs::prelude::AnnSearchFloat;
24use faer::MatRef;
25use manifolds_rs::PreComputedKnn;
26use manifolds_rs::data::nearest_neighbours::*;
27use std::time::Instant;
28
29#[cfg(feature = "gpu")]
30use ann_search_rs::gpu::nndescent_gpu::NNDescentGpu;
31#[cfg(feature = "gpu")]
32use ann_search_rs::gpu::traits_gpu::AnnSearchGpuFloat;
33#[cfg(feature = "gpu")]
34use cubecl::prelude::*;
35#[cfg(feature = "gpu")]
36use manifolds_rs::data::nearest_neighbours_gpu::*;
37
38use crate::clustering::condensed_tree::*;
39use crate::clustering::linkage::mst_to_linkage_tree;
40use crate::clustering::mst::build_mst;
41use crate::clustering::persistence::build_cluster_layers;
42use crate::graph::embedding::*;
43use crate::graph::fuzzy_graph::*;
44use crate::graph::label_prop::label_propagation_init;
45use crate::prelude::*;
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>(
198 data: MatRef<T>,
199 ann_type: String,
200 precomputed_knn: PreComputedKnn<T>,
201 evoc_params: &EvocParams<T>,
202 nn_params: &NearestNeighbourParams<T>,
203 seed: usize,
204 verbose: usize,
205) -> Result<EvocResult<T>, EvocErrors>
206where
207 T: EvocFloat + AnnSearchFloat,
208 NNDescent<T>: ApplySortedUpdates<T> + NNDescentQuery<T>,
209 HnswIndex<T>: HnswState<T>,
210{
211 let verbosity = parse_verbosity_level(verbose);
212
213 let start_all = Instant::now();
214
215 let (knn_indices, knn_dist) = match precomputed_knn {
217 Some((indices, distances)) => {
218 if verbosity.normal_verbosity() {
219 println!("Using precomputed kNN graph...");
220 }
221 (indices, distances)
222 }
223 None => {
224 if verbosity.normal_verbosity() {
225 println!(
226 "Running approximate nearest neighbour search using {}...",
227 ann_type
228 );
229 }
230 let start_knn = Instant::now();
231 let result = run_ann_search(
232 data,
233 evoc_params.n_neighbours,
234 ann_type,
235 nn_params,
236 seed,
237 verbose,
238 )?;
239 if verbosity.normal_verbosity() {
240 println!("kNN search done in {:.2?}.", start_knn.elapsed());
241 }
242 result
243 }
244 };
245
246 if verbosity.normal_verbosity() {
248 println!("Constructing fuzzy simplicial set...");
249 }
250 let start_graph = Instant::now();
251 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
252 let graph =
253 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
254 let adj = coo_to_adjacency_list(&graph);
255 if verbosity.normal_verbosity() {
256 println!(
257 "... fuzzy simplicial set done in {:.2?}.",
258 start_graph.elapsed()
259 );
260 }
261
262 let dim = evoc_params
265 .embedding_dim
266 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
267
268 let start_init = Instant::now();
270 let n = data.nrows();
271 let d = data.ncols();
272 let data_vecs: Vec<Vec<T>> = (0..n)
273 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
274 .collect();
275
276 if verbosity.normal_verbosity() {
277 println!("Computing label propagation initialisation...");
278 }
279 let initial_embedding =
280 label_propagation_init(&graph, dim, Some(&data_vecs), seed as u64, verbose);
281 if verbosity.normal_verbosity() {
282 println!("Label prop init done in {:.2?}.", start_init.elapsed());
283 }
284
285 if verbosity.normal_verbosity() {
287 println!(
288 "Computing {}-d node embedding ({} epochs)...",
289 dim, evoc_params.n_epochs
290 );
291 }
292 let start_embed = Instant::now();
293 let embed_params = EvocEmbeddingParams {
294 n_epochs: evoc_params.n_epochs,
295 noise_level: evoc_params.noise_level,
296 initial_alpha: T::from(0.1).unwrap(),
297 ..EvocEmbeddingParams::default()
298 };
299
300 let embedding = evoc_embedding(
301 &adj,
302 dim,
303 &embed_params,
304 Some(&initial_embedding),
305 seed as u64,
306 verbose,
307 );
308 if verbosity.normal_verbosity() {
309 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
310 }
311
312 if verbosity.normal_verbosity() {
314 println!("Running density-based clustering...");
315 }
316 let start_cluster = Instant::now();
317
318 let (cluster_layers, membership_strengths, persistence_scores) =
319 if let Some(target_k) = evoc_params.approx_n_clusters {
320 let (labels, strengths) =
321 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
322 (vec![labels], vec![strengths], vec![0.0])
323 } else {
324 build_cluster_layers(
325 &embedding,
326 evoc_params.min_samples,
327 evoc_params.base_min_cluster_size,
328 evoc_params.min_similarity_threshold,
329 evoc_params.max_layers,
330 )
331 };
332
333 if verbosity.normal_verbosity() {
334 let n_layers = cluster_layers.len();
335 println!(
336 "Clustering done in {:.2?}: {} layer(s).",
337 start_cluster.elapsed(),
338 n_layers,
339 );
340 println!("EVoC total: {:.2?}.", start_all.elapsed());
341 }
342
343 Ok(EvocResult {
344 cluster_layers,
345 membership_strengths,
346 persistence_scores,
347 nn_indices: knn_indices,
348 nn_distances: knn_dist,
349 })
350}
351
352pub fn search_for_n_clusters<T>(
376 embedding: &[Vec<T>],
377 min_samples: usize,
378 target_k: usize,
379) -> (Vec<i64>, Vec<T>)
380where
381 T: EvocFloat,
382{
383 let n = embedding.len();
384 if n == 0 {
385 return (Vec::new(), Vec::new());
386 }
387
388 let mut mst = build_mst(embedding, min_samples);
389 let linkage = mst_to_linkage_tree(&mut mst, n);
390
391 let mut lo = 2usize;
392 let mut hi = n / 2;
393
394 while hi - lo > 1 {
395 let mid = (lo + hi) / 2;
396 if mid == lo || mid == hi {
397 break;
398 }
399
400 let ct_mid = condense_tree(&linkage, n, mid);
401 let leaves_mid = extract_leaves(&ct_mid);
402 let mid_k = leaves_mid.len();
403
404 if mid_k < target_k {
405 hi = mid;
407 } else {
408 lo = mid;
410 }
411 }
412
413 let ct_lo = condense_tree(&linkage, n, lo);
415 let leaves_lo = extract_leaves(&ct_lo);
416 let labels_lo = get_cluster_label_vector(&ct_lo, &leaves_lo, n);
417 let lo_k = leaves_lo.len();
418
419 let ct_hi = condense_tree(&linkage, n, hi);
420 let leaves_hi = extract_leaves(&ct_hi);
421 let labels_hi = get_cluster_label_vector(&ct_hi, &leaves_hi, n);
422 let hi_k = leaves_hi.len();
423
424 let lo_diff = (lo_k as isize - target_k as isize).unsigned_abs();
425 let hi_diff = (hi_k as isize - target_k as isize).unsigned_abs();
426
427 if lo_diff < hi_diff {
428 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
429 (labels_lo, strengths)
430 } else if hi_diff < lo_diff {
431 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
432 (labels_hi, strengths)
433 } else {
434 let lo_assigned = labels_lo.iter().filter(|&&l| l >= 0).count();
436 let hi_assigned = labels_hi.iter().filter(|&&l| l >= 0).count();
437 if lo_assigned >= hi_assigned {
438 let strengths = get_point_membership_strengths(&ct_lo, &leaves_lo, &labels_lo);
439 (labels_lo, strengths)
440 } else {
441 let strengths = get_point_membership_strengths(&ct_hi, &leaves_hi, &labels_hi);
442 (labels_hi, strengths)
443 }
444 }
445}
446
447#[allow(clippy::too_many_arguments)]
476#[cfg(feature = "gpu")]
477pub fn evoc_gpu<T, R>(
478 data: MatRef<T>,
479 ann_type: String,
480 precomputed_knn: PreComputedKnn<T>,
481 evoc_params: &EvocParams<T>,
482 nn_params: &NearestNeighbourParamsGpu<T>,
483 device: R::Device,
484 seed: usize,
485 verbose: usize,
486) -> Result<EvocResult<T>, EvocErrors>
487where
488 T: EvocFloat + AnnSearchFloat + AnnSearchGpuFloat,
489 R: Runtime,
490 NNDescentGpu<T, R>: NNDescentQuery<T>,
491{
492 let start_all = Instant::now();
493 let verbosity = parse_verbosity_level(verbose);
494
495 let (knn_indices, knn_dist) = match precomputed_knn {
497 Some((indices, distances)) => {
498 if verbosity.normal_verbosity() {
499 println!("Using precomputed kNN graph...");
500 }
501 (indices, distances)
502 }
503 None => {
504 if verbosity.normal_verbosity() {
505 println!("Running GPU nearest neighbour search using {}...", ann_type);
506 }
507 let start_knn = Instant::now();
508 let result = run_ann_search_gpu::<T, R>(
509 data,
510 evoc_params.n_neighbours,
511 ann_type,
512 nn_params,
513 device,
514 seed,
515 verbose,
516 )?;
517 if verbosity.normal_verbosity() {
518 println!("GPU kNN search done in {:.2?}.", start_knn.elapsed());
519 }
520 result
521 }
522 };
523
524 if verbosity.normal_verbosity() {
526 println!("Constructing fuzzy simplicial set...");
527 }
528 let start_graph = Instant::now();
529 let effective_k = evoc_params.neighbour_scale * T::from(evoc_params.n_neighbours).unwrap();
530 let graph =
531 build_fuzzy_simplicial_set(&knn_indices, &knn_dist, effective_k, evoc_params.symmetrise);
532 let adj = coo_to_adjacency_list(&graph);
533 if verbosity.normal_verbosity() {
534 println!(
535 "... fuzzy simplicial set done in {:.2?}.",
536 start_graph.elapsed()
537 );
538 }
539
540 let dim = evoc_params
542 .embedding_dim
543 .unwrap_or_else(|| (evoc_params.n_neighbours / 4).clamp(4, 16));
544
545 let start_init = Instant::now();
547 let n = data.nrows();
548 let d = data.ncols();
549 let data_vecs: Vec<Vec<T>> = (0..n)
550 .map(|i| (0..d).map(|j| data[(i, j)]).collect())
551 .collect();
552
553 if verbosity.normal_verbosity() {
554 println!("Computing label propagation initialisation...");
555 }
556 let initial_embedding = crate::graph::label_prop::label_propagation_init(
557 &graph,
558 dim,
559 Some(&data_vecs),
560 seed as u64,
561 verbose,
562 );
563 if verbosity.normal_verbosity() {
564 println!(" ... label prop init done in {:.2?}.", start_init.elapsed());
565 }
566
567 if verbosity.normal_verbosity() {
569 println!(
570 "Computing {}-d node embedding ({} epochs)...",
571 dim, evoc_params.n_epochs
572 );
573 }
574 let start_embed = Instant::now();
575 let embed_params = EvocEmbeddingParams {
576 n_epochs: evoc_params.n_epochs,
577 noise_level: evoc_params.noise_level,
578 initial_alpha: T::from(0.1).unwrap(),
579 ..EvocEmbeddingParams::default()
580 };
581
582 let embedding = evoc_embedding(
583 &adj,
584 dim,
585 &embed_params,
586 Some(&initial_embedding),
587 seed as u64,
588 verbose,
589 );
590 if verbosity.normal_verbosity() {
591 println!(" ... embedding done in {:.2?}.", start_embed.elapsed());
592 }
593
594 if verbosity.normal_verbosity() {
596 println!("Running density-based clustering...");
597 }
598 let start_cluster = Instant::now();
599
600 let (cluster_layers, membership_strengths, persistence_scores) =
601 if let Some(target_k) = evoc_params.approx_n_clusters {
602 let (labels, strengths) =
603 search_for_n_clusters(&embedding, evoc_params.min_samples, target_k);
604 (vec![labels], vec![strengths], vec![0.0])
605 } else {
606 build_cluster_layers(
607 &embedding,
608 evoc_params.min_samples,
609 evoc_params.base_min_cluster_size,
610 evoc_params.min_similarity_threshold,
611 evoc_params.max_layers,
612 )
613 };
614
615 if verbosity.normal_verbosity() {
616 let n_layers = cluster_layers.len();
617 println!(
618 "Clustering done in {:.2?}: {} layer(s).",
619 start_cluster.elapsed(),
620 n_layers,
621 );
622 println!("EVoC (GPU) total: {:.2?}.", start_all.elapsed());
623 }
624
625 Ok(EvocResult {
626 cluster_layers,
627 membership_strengths,
628 persistence_scores,
629 nn_indices: knn_indices,
630 nn_distances: knn_dist,
631 })
632}