use ann_search_rs::utils::dist::{Dist, SimdDistance, parse_ann_dist};
use ann_search_rs::utils::k_means_utils::assign_all_parallel_top_m;
use ann_search_rs::utils::matrix_to_flat;
use either::Either;
use faer::{Mat, MatRef};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::time::Instant;
use crate::core::math::vector_helpers::{mean_nan, median};
use crate::graph::community_detections::*;
use crate::graph::graph_metrics::conductance_undirected;
use crate::graph::graph_structures::*;
use crate::ml::clustering::clustering_metrics::adjusted_rand_index;
use crate::ml::clustering::k_means::*;
use crate::prelude::*;
use crate::single_cell::sc_processing::snn::*;
use crate::single_cell::sc_processing::utils_doublets::dispatch_knn;
pub enum FastLouvainResults {
Single {
assignments: Vec<Vec<usize>>,
clusters: Option<Vec<usize>>,
centroids: Option<Mat<f32>>,
},
GridRes {
assignments: Vec<FastLouvainGridResult>,
clusters: Option<Vec<usize>>,
centroids: Option<Mat<f32>>,
},
}
impl FastLouvainResults {
pub fn single(
assignments: Vec<Vec<usize>>,
centroids: Mat<f32>,
clusters: Vec<usize>,
keep_kmeans: bool,
) -> Self {
let (centroids, clusters) = if keep_kmeans {
(Some(centroids), Some(clusters))
} else {
(None, None)
};
Self::Single {
assignments,
clusters,
centroids,
}
}
pub fn grid(
assignments: Vec<FastLouvainGridResult>,
centroids: Mat<f32>,
clusters: Vec<usize>,
keep_kmeans: bool,
) -> Self {
let (centroids, clusters) = if keep_kmeans {
(Some(centroids), Some(clusters))
} else {
(None, None)
};
Self::GridRes {
assignments,
clusters,
centroids,
}
}
#[inline]
pub fn get_assignments(&self) -> Either<Vec<Vec<usize>>, Vec<FastLouvainGridResult>> {
match self {
Self::Single { assignments, .. } => Either::Left(assignments.clone()),
Self::GridRes { assignments, .. } => Either::Right(assignments.clone()),
}
}
#[inline]
pub fn get_centroids(&self) -> Result<Mat<f32>, BixverseErrors> {
let centroids = match self {
Self::Single { centroids, .. } | Self::GridRes { centroids, .. } => centroids,
};
centroids
.to_owned()
.ok_or(BixverseErrors::FastClusterNoCentroids)
}
#[inline]
pub fn get_k_mean_clusters(&self) -> Result<Vec<usize>, BixverseErrors> {
let clusters = match self {
Self::Single { clusters, .. } | Self::GridRes { clusters, .. } => clusters,
};
clusters
.clone()
.ok_or(BixverseErrors::FastClusterNoKmeansAssignments)
}
}
#[derive(Clone, Debug)]
pub struct CentroidGraphParams {
pub knn_params: KnnParams,
pub same_weight: bool,
pub full_snn: bool,
pub pruning: Option<f32>,
pub snn_similarity: String,
pub cell_mediated: bool,
pub n_anchors: usize,
}
#[derive(Clone, Debug)]
pub struct FastLouvainParams<T> {
pub n_centroids: usize,
pub batch_size: usize,
pub drift_threshold: T,
pub lr_alpha: T,
pub kmeans_params: KMeansParamsWrappers,
pub knn_params: KnnParams,
pub same_weight: bool,
pub full_snn: bool,
pub pruning: Option<T>,
pub snn_similarity: String,
pub cell_mediated: bool,
pub n_anchors: usize,
pub louvain_iters: usize,
pub multi_level_louvain: bool,
}
impl<T> FastLouvainParams<T>
where
T: BixverseFloat,
{
pub fn new() -> Self {
Self {
n_centroids: 1000,
batch_size: 4096,
drift_threshold: T::from_f64(1e-4).unwrap(),
lr_alpha: T::from_f64(1.0).unwrap(),
kmeans_params: KMeansParamsWrappers::default(),
full_snn: false,
pruning: None,
snn_similarity: "jaccard".to_string(),
knn_params: KnnParams::default(),
same_weight: false,
cell_mediated: false,
n_anchors: 5,
louvain_iters: 10,
multi_level_louvain: true,
}
}
pub fn get_kmeans_params(&self) -> KMeansParamsWrappers {
self.kmeans_params
}
}
impl FastLouvainParams<f32> {
pub fn graph_params(&self) -> CentroidGraphParams {
CentroidGraphParams {
knn_params: self.knn_params.clone(),
same_weight: self.same_weight,
full_snn: self.full_snn,
pruning: self.pruning,
snn_similarity: self.snn_similarity.clone(),
cell_mediated: self.cell_mediated,
n_anchors: self.n_anchors,
}
}
}
impl<T> Default for FastLouvainParams<T>
where
T: BixverseFloat,
{
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct FastLouvainGridResult {
pub resolution: f32,
pub mean_ari: f32,
pub median_ari: f32,
pub mean_conductance: f32,
pub median_conductance: f32,
pub mean_n_communities: f32,
pub best_labels: Vec<usize>,
}
fn flatten_knn_column_major<T: Copy>(knn: &[Vec<T>], k: usize) -> Vec<T> {
let n = knn.len();
let mut out = Vec::with_capacity(n * k);
out.resize(n * k, knn[0][0]);
for (i, row) in knn.iter().enumerate() {
debug_assert_eq!(row.len(), k);
for (j, &v) in row.iter().enumerate() {
out[j * n + i] = v;
}
}
out
}
const CELL_MEDIATED_BLOCK: usize = 16_384;
fn cell_mediated_graph(
data: MatRef<f32>,
centroids: MatRef<f32>,
n_anchors: usize,
ann_dist: &str,
) -> SparseGraph<f32> {
let (data_flat, n, dim) = matrix_to_flat(data);
let (cent_flat, k, _) = matrix_to_flat(centroids);
let dist = parse_ann_dist(ann_dist).unwrap_or_default();
let (data_norms, cent_norms) = if dist == Dist::Cosine {
(
compute_data_norms(&data_flat, dim, n, &dist),
recompute_centroid_norms(¢_flat, dim, k, &dist),
)
} else {
(Vec::new(), Vec::new())
};
let m = n_anchors.min(k).max(1);
let anchors = assign_all_parallel_top_m(
&data_flat,
&data_norms,
dim,
n,
¢_flat,
¢_norms,
k,
m,
&dist,
);
let blocks: Vec<FxHashMap<(u32, u32), f32>> = anchors
.par_chunks(m * CELL_MEDIATED_BLOCK)
.enumerate()
.map(|(b, block)| {
let mut acc: FxHashMap<(u32, u32), f32> = FxHashMap::default();
let mut w = vec![0.0f32; m];
for (r, row) in block.chunks_exact(m).enumerate() {
let i = b * CELL_MEDIATED_BLOCK + r;
let x = &data_flat[i * dim..(i + 1) * dim];
for (wj, &c) in w.iter_mut().zip(row) {
let c = c as usize;
let cent = ¢_flat[c * dim..(c + 1) * dim];
*wj = match dist {
Dist::Cosine => {
let denom = data_norms[i] * cent_norms[c];
if denom > 0.0 {
(1.0 - f32::dot_simd(x, cent) / denom).max(0.0)
} else {
1.0
}
}
_ => f32::euclidean_simd(x, cent).sqrt(),
};
}
let sigma = w.iter().sum::<f32>() / m as f32;
for wj in w.iter_mut() {
*wj = if sigma > 0.0 {
(-(*wj * *wj) / (2.0 * sigma * sigma)).exp()
} else {
1.0
};
}
let dx: f32 = w.iter().sum();
for a in 0..m {
for c in (a + 1)..m {
let key = (row[a].min(row[c]), row[a].max(row[c]));
*acc.entry(key).or_insert(0.0) += w[a] * w[c] / dx;
}
}
}
acc
})
.collect();
let mut total: FxHashMap<(u32, u32), f32> = FxHashMap::default();
for block in blocks {
for (key, v) in block {
*total.entry(key).or_insert(0.0) += v;
}
}
let mut pairs: Vec<((u32, u32), f32)> = total.into_iter().collect();
pairs.sort_unstable_by_key(|p| p.0);
let mut edges = Vec::with_capacity(2 * pairs.len());
let mut weights = Vec::with_capacity(pairs.len());
for ((u, v), wt) in pairs {
edges.push(u as usize);
edges.push(v as usize);
weights.push(wt);
}
edge_pairs_to_sparse_graph(&edges, &weights, k)
}
pub fn fast_cluster_kmeans(
data: MatRef<f32>,
km_type: &str,
params: &FastLouvainParams<f32>,
seed: usize,
verbose: usize,
) -> Result<(Mat<f32>, Vec<usize>), BixverseErrors> {
let verbosity = parse_verbosity_level(verbose);
let km_type = parse_k_means(km_type).unwrap_or_default();
let n_centroids = params.n_centroids.min(data.nrows() - 1);
let res = match km_type {
KMeansType::StandardKMeans => k_means_clusters(
data,
¶ms.knn_params.ann_dist,
n_centroids,
Some(params.get_kmeans_params()),
seed,
verbosity.detailed_verbosity(),
)?,
KMeansType::MiniBatchKMeans => train_centroids_minibatch(
data,
¶ms.knn_params.ann_dist,
n_centroids,
params.kmeans_params.get_data().iters,
params.batch_size,
params.drift_threshold,
params.lr_alpha,
seed,
verbosity.detailed_verbosity(),
),
};
Ok(res)
}
pub fn build_centroid_graph(
data: MatRef<f32>,
centroids: MatRef<f32>,
params: &CentroidGraphParams,
run_snn: bool,
seed: usize,
verbose: usize,
) -> Result<SparseGraph<f32>, BixverseErrors> {
let verbosity = parse_verbosity_level(verbose);
if params.cell_mediated {
let start = Instant::now();
let graph = cell_mediated_graph(
data,
centroids,
params.n_anchors,
¶ms.knn_params.ann_dist,
);
if verbosity.normal_verbosity() {
println!(
" Built the cell-mediated centroid graph in {:.2?}...",
start.elapsed()
);
}
return Ok(graph);
}
let knn = dispatch_knn(
centroids,
params.knn_params.k,
¶ms.knn_params,
seed,
verbosity.detailed_verbosity(),
)?;
if !run_snn {
return Ok(knn_to_sparse_graph(&knn, params.same_weight));
}
let pruning = params
.pruning
.unwrap_or(1.0 / (params.knn_params.k as f32 * 0.8).ceil());
let start = Instant::now();
if verbosity.normal_verbosity() {
println!(" Running the sNN graph as {}...", ¶ms.snn_similarity)
}
let knn_flat = flatten_knn_column_major(&knn, params.knn_params.k);
let snn_method = parse_snn_similiarity_method(¶ms.snn_similarity).unwrap_or_default();
let (snn_edges, snn_weights) = if params.full_snn {
generate_snn_full(
&knn_flat,
params.knn_params.k,
knn.len(),
pruning,
snn_method,
verbose,
)
} else {
generate_snn_limited(
&knn_flat,
params.knn_params.k,
knn.len(),
pruning,
snn_method,
verbose,
)
};
if verbosity.normal_verbosity() {
println!(" Finished the sNN generation in {:.2?}...", start.elapsed())
}
Ok(snn_edges_to_sparse_graph(
&snn_edges,
&snn_weights,
knn.len(),
))
}
pub fn louvain_over_resolutions(
graph: &SparseGraph<f32>,
assignments: &[usize],
resolutions: &[f32],
louvain_iters: usize,
multi_level: bool,
seed: usize,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
resolutions
.iter()
.map(|&res| {
let centroid_communities =
louvain_sparse_graph(graph, res, louvain_iters, multi_level, seed)?;
Ok(assignments
.iter()
.map(|&c| centroid_communities[c])
.collect())
})
.collect()
}
pub fn louvain_grid_over_resolutions(
graph: &SparseGraph<f32>,
assignments: &[usize],
resolutions: &[f32],
louvain_iters: usize,
multi_level: bool,
seed: usize,
n_louvain_seeds: usize,
) -> Result<Vec<FastLouvainGridResult>, BixverseErrors> {
if n_louvain_seeds < 2 {
return Err(BixverseErrors::InvalidArgument(
"n_louvain_seeds must be at least 2".to_string(),
));
}
let mut seed_rng = StdRng::seed_from_u64(seed as u64);
let louvain_seeds: Vec<usize> = (0..n_louvain_seeds)
.map(|_| seed_rng.random::<u64>() as usize)
.collect();
let mut results = Vec::with_capacity(resolutions.len());
for &res in resolutions {
let centroid_partitions: Vec<Vec<usize>> = louvain_seeds
.iter()
.map(|&s| louvain_sparse_graph(graph, res, louvain_iters, multi_level, s))
.collect::<Result<_, _>>()?;
let s = louvain_seeds.len();
let mut aris: Vec<f64> = Vec::with_capacity(s * (s - 1) / 2);
for i in 0..s {
for j in (i + 1)..s {
aris.push(adjusted_rand_index(
¢roid_partitions[i],
¢roid_partitions[j],
));
}
}
let mean_ari = aris.iter().sum::<f64>() / aris.len() as f64;
let median_ari = median(&aris).unwrap();
let mut per_seed_mean_cond = Vec::with_capacity(s);
let mut per_seed_median_cond = Vec::with_capacity(s);
for cp in ¢roid_partitions {
let cond = conductance_undirected(graph, cp)?;
let finite: Vec<f32> = cond.into_iter().filter(|x| x.is_finite()).collect();
per_seed_mean_cond.push(mean_nan(&finite));
per_seed_median_cond.push(median(&finite).unwrap_or(f32::NAN));
}
let mean_conductance = mean_nan(&per_seed_mean_cond);
let median_conductance = mean_nan(&per_seed_median_cond);
let mean_n_communities = centroid_partitions
.iter()
.map(|cp| cp.iter().max().copied().unwrap_or(0) + 1)
.sum::<usize>() as f32
/ s as f32;
let best_idx = per_seed_mean_cond
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let a = if a.is_finite() { **a } else { f32::INFINITY };
let b = if b.is_finite() { **b } else { f32::INFINITY };
a.partial_cmp(&b).unwrap()
})
.map(|(i, _)| i)
.unwrap_or(0);
let best_labels = assignments
.iter()
.map(|&c| centroid_partitions[best_idx][c])
.collect();
results.push(FastLouvainGridResult {
resolution: res,
mean_ari: mean_ari as f32,
median_ari: median_ari as f32,
mean_conductance,
median_conductance,
mean_n_communities,
best_labels,
});
}
Ok(results)
}
#[allow(clippy::too_many_arguments)]
pub fn fast_louvain_clusters(
data: MatRef<f32>,
km_type: &str,
resolutions: &[f32],
params: &FastLouvainParams<f32>,
run_snn: bool,
return_k_mean: bool,
seed: usize,
verbose: usize,
) -> Result<FastLouvainResults, BixverseErrors> {
let (centroids, assignments) = fast_cluster_kmeans(data, km_type, params, seed, verbose)?;
let graph = build_centroid_graph(
data,
centroids.as_ref(),
¶ms.graph_params(),
run_snn,
seed,
verbose,
)?;
let results = louvain_over_resolutions(
&graph,
&assignments,
resolutions,
params.louvain_iters,
params.multi_level_louvain,
seed,
)?;
Ok(FastLouvainResults::single(
results,
centroids,
assignments,
return_k_mean,
))
}
#[allow(clippy::too_many_arguments)]
pub fn fast_louvain_clusters_grid(
data: MatRef<f32>,
km_type: &str,
resolutions: &[f32],
params: &FastLouvainParams<f32>,
run_snn: bool,
return_k_mean: bool,
seed: usize,
n_louvain_seeds: usize,
verbose: usize,
) -> Result<FastLouvainResults, BixverseErrors> {
let (centroids, assignments) = fast_cluster_kmeans(data, km_type, params, seed, verbose)?;
let graph = build_centroid_graph(
data,
centroids.as_ref(),
¶ms.graph_params(),
run_snn,
seed,
verbose,
)?;
let results = louvain_grid_over_resolutions(
&graph,
&assignments,
resolutions,
params.louvain_iters,
params.multi_level_louvain,
seed,
n_louvain_seeds,
)?;
Ok(FastLouvainResults::grid(
results,
centroids,
assignments,
return_k_mean,
))
}
#[cfg(test)]
mod tests {
use super::*;
use either::Either;
use faer::Mat;
use std::collections::HashMap;
fn make_two_blobs(n_per: usize, dim: usize, separation: f32, seed: u64) -> Mat<f32> {
let mut rng = StdRng::seed_from_u64(seed);
let mut buf = vec![0.0f32; 2 * n_per * dim];
for i in 0..(2 * n_per) {
let centre = if i < n_per { 0.0 } else { separation };
for j in 0..dim {
buf[i * dim + j] = centre + rng.random_range(-0.3..0.3);
}
}
Mat::from_fn(2 * n_per, dim, |i, j| buf[i * dim + j])
}
fn default_params() -> FastLouvainParams<f32> {
let mut knn = KnnParams::new();
knn.knn_method = "exhaustive".to_string();
knn.ann_dist = "euclidean".to_string();
knn.k = 5;
FastLouvainParams {
batch_size: 100,
drift_threshold: 1e-4,
lr_alpha: 1.0,
n_centroids: 30,
kmeans_params: KMeansParamsWrappers::default(),
knn_params: knn,
louvain_iters: 10,
full_snn: false,
pruning: None,
same_weight: false,
multi_level_louvain: true,
snn_similarity: "jaccard".to_string(),
cell_mediated: false,
n_anchors: 5,
}
}
fn majority(xs: &[usize]) -> usize {
let mut counts: HashMap<usize, usize> = HashMap::new();
for &x in xs {
*counts.entry(x).or_insert(0) += 1;
}
*counts.iter().max_by_key(|(_, c)| **c).unwrap().0
}
fn unwrap_single(r: FastLouvainResults) -> Vec<Vec<usize>> {
match r.get_assignments() {
Either::Left(v) => v,
Either::Right(_) => panic!("expected Single variant"),
}
}
fn unwrap_grid(r: FastLouvainResults) -> Vec<FastLouvainGridResult> {
match r.get_assignments() {
Either::Right(v) => v,
Either::Left(_) => panic!("expected GridRes variant"),
}
}
#[test]
fn deterministic_with_same_seed() {
let data = make_two_blobs(100, 5, 5.0, 42);
let params = default_params();
let resolutions: Vec<f32> = vec![1.0];
let a = unwrap_single(
fast_louvain_clusters(
data.as_ref(),
"standard",
&resolutions,
¶ms,
false,
false,
7,
0,
)
.unwrap(),
);
let b = unwrap_single(
fast_louvain_clusters(
data.as_ref(),
"standard",
&resolutions,
¶ms,
false,
false,
7,
0,
)
.unwrap(),
);
assert_eq!(a, b);
}
#[test]
fn produces_multiple_communities_on_separated_blobs() {
let n_per = 200;
let data = make_two_blobs(n_per, 8, 15.0, 42);
let resolutions: Vec<f32> = vec![1.0];
for (run_snn, cell_mediated) in [(false, false), (true, false), (false, true)] {
let mut params = default_params();
params.cell_mediated = cell_mediated;
let labels = unwrap_single(
fast_louvain_clusters(
data.as_ref(),
"standard",
&resolutions,
¶ms,
run_snn,
false,
0,
0,
)
.unwrap(),
);
assert_eq!(labels.len(), resolutions.len());
assert_eq!(labels[0].len(), 2 * n_per);
let (blob_a, blob_b) = labels[0].split_at(n_per);
assert_ne!(
majority(blob_a),
majority(blob_b),
"run_snn {} cell_mediated {}",
run_snn,
cell_mediated
);
let n_labels = labels[0].iter().max().unwrap() + 1;
for l in 0..n_labels {
let in_a = blob_a.iter().filter(|&&x| x == l).count();
let in_b = blob_b.iter().filter(|&&x| x == l).count();
let purity = in_a.max(in_b) as f32 / (in_a + in_b).max(1) as f32;
assert!(
purity > 0.9,
"community {} purity {} (snn {}, cell_mediated {})",
l,
purity,
run_snn,
cell_mediated
);
}
}
}
#[test]
fn grid_rejects_too_few_seeds() {
let data = make_two_blobs(100, 5, 5.0, 42);
let resolutions: Vec<f32> = vec![1.0];
let result = fast_louvain_clusters_grid(
data.as_ref(),
"standard",
&resolutions,
&default_params(),
false,
false,
0,
1,
0,
);
assert!(matches!(result, Err(BixverseErrors::InvalidArgument(_))));
}
#[test]
fn grid_deterministic_with_same_seed() {
let data = make_two_blobs(100, 5, 5.0, 42);
let resolutions: Vec<f32> = vec![1.0];
let a = unwrap_grid(
fast_louvain_clusters_grid(
data.as_ref(),
"standard",
&resolutions,
&default_params(),
false,
false,
7,
4,
0,
)
.unwrap(),
);
let b = unwrap_grid(
fast_louvain_clusters_grid(
data.as_ref(),
"standard",
&resolutions,
&default_params(),
false,
false,
7,
4,
0,
)
.unwrap(),
);
assert_eq!(a.len(), b.len());
for (ra, rb) in a.iter().zip(b.iter()) {
assert!((ra.mean_ari - rb.mean_ari).abs() < 1e-6);
assert!((ra.median_ari - rb.median_ari).abs() < 1e-6);
assert!((ra.mean_conductance - rb.mean_conductance).abs() < 1e-6);
assert!((ra.median_conductance - rb.median_conductance).abs() < 1e-6);
assert_eq!(ra.best_labels, rb.best_labels);
}
}
#[test]
fn grid_well_separated_blobs_are_stable() {
let n_per = 250;
let data = make_two_blobs(n_per, 8, 15.0, 42);
let resolutions: Vec<f32> = vec![1.0, 0.5, 0.25];
let results = unwrap_grid(
fast_louvain_clusters_grid(
data.as_ref(),
"standard",
&resolutions,
&default_params(),
false,
false,
42,
5,
0,
)
.unwrap(),
);
for (i, r) in results.iter().enumerate() {
assert!(
r.mean_ari > 0.9,
"mean_ari {} for seed no {}",
r.mean_ari,
i
);
let (blob_a, blob_b) = r.best_labels.split_at(n_per);
let maj_a = majority(blob_a);
let maj_b = majority(blob_b);
assert_ne!(maj_a, maj_b, "not the same majority for seed no {}", i);
}
}
fn edge_list(g: &SparseGraph<f32>) -> Vec<(usize, usize, f32)> {
let mut out = Vec::new();
for u in 0..g.get_node_number() {
let (nbrs, w) = g.get_neighbours(u);
out.extend(nbrs.into_iter().zip(w).map(|(v, &x)| (u, v, x)));
}
out.sort_by_key(|e| (e.0, e.1));
out
}
#[test]
fn cell_mediated_edge_weight_matches_hand_computation() {
let data = Mat::from_fn(1, 1, |_, _| 0.0f32);
let centroids = Mat::from_fn(3, 1, |i, _| [1.0f32, 3.0, 10.0][i]);
let g = cell_mediated_graph(data.as_ref(), centroids.as_ref(), 2, "euclidean");
let w1 = (-1.0f32 / 8.0).exp();
let w2 = (-9.0f32 / 8.0).exp();
let expected = w1 * w2 / (w1 + w2);
let edges = edge_list(&g);
assert_eq!(edges.len(), 2, "one undirected edge, stored both ways");
assert_eq!((edges[0].0, edges[0].1), (0, 1));
assert_eq!((edges[1].0, edges[1].1), (1, 0));
approx::assert_relative_eq!(edges[0].2, expected, max_relative = 1e-6);
assert_eq!(g.get_node_degree(2), 0);
}
#[test]
fn cell_mediated_graph_has_no_edges_across_an_empty_gap() {
let n_per = 200;
let dim = 4;
let data = make_two_blobs(n_per, dim, 15.0, 42);
let centroids = Mat::from_fn(4, dim, |i, j| {
let centre = if i < 2 { 0.0 } else { 15.0 };
let nudge = if j == 0 {
[-0.1, 0.1, -0.1, 0.1][i]
} else {
0.0
};
centre + nudge
});
let g = cell_mediated_graph(data.as_ref(), centroids.as_ref(), 2, "euclidean");
for (u, v, w) in edge_list(&g) {
assert_eq!(u < 2, v < 2, "edge {u}-{v} crosses the gap");
assert!(w > 0.0);
}
assert_eq!(g.get_neighbours(0).0, vec![1]);
assert_eq!(g.get_neighbours(2).0, vec![3]);
}
#[test]
fn cell_mediated_graph_independent_of_thread_count() {
let n = 2 * CELL_MEDIATED_BLOCK + 1000;
let dim = 4;
let mut rng = StdRng::seed_from_u64(7);
let data = Mat::from_fn(n, dim, |_, _| rng.random_range(-1.0f32..1.0));
let centroids = Mat::from_fn(20, dim, |i, j| data[(i * 997, j)]);
let build = |threads: usize| {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
.install(|| cell_mediated_graph(data.as_ref(), centroids.as_ref(), 5, "euclidean"))
};
let one = edge_list(&build(1));
let four = edge_list(&build(4));
assert!(!one.is_empty());
assert_eq!(one, four);
}
}