#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::many_single_char_names,
clippy::needless_range_loop,
clippy::ptr_arg,
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::unnecessary_wraps
)]
use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphError, IgraphResult};
const MAX_LEVELS_PER_ITERATION: usize = 64;
pub const LEIDEN_DEFAULT_BETA: f64 = 0.01;
pub const LEIDEN_DEFAULT_ITERATIONS: i32 = 2;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum LeidenObjective {
Modularity,
Cpm,
Er,
}
#[derive(Debug, Clone)]
pub struct LeidenOptions {
pub objective: LeidenObjective,
pub resolution: f64,
pub beta: f64,
pub n_iterations: i32,
pub seed: u64,
pub start: Option<Vec<u32>>,
}
impl Default for LeidenOptions {
fn default() -> Self {
Self {
objective: LeidenObjective::Modularity,
resolution: 1.0,
beta: LEIDEN_DEFAULT_BETA,
n_iterations: LEIDEN_DEFAULT_ITERATIONS,
seed: 0,
start: None,
}
}
}
#[derive(Debug, Clone)]
pub struct LeidenResult {
pub membership: Vec<u32>,
pub quality: f64,
pub nb_clusters: u32,
pub n_iterations_run: u32,
pub qualities: Vec<f64>,
}
pub fn leiden(graph: &Graph) -> IgraphResult<LeidenResult> {
leiden_with_options(graph, None, &LeidenOptions::default())
}
pub fn leiden_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<LeidenResult> {
leiden_with_options(graph, Some(weights), &LeidenOptions::default())
}
pub fn leiden_with_options(
graph: &Graph,
weights: Option<&[f64]>,
opts: &LeidenOptions,
) -> IgraphResult<LeidenResult> {
if graph.is_directed() {
return Err(IgraphError::Unsupported(
"leiden currently supports undirected graphs only",
));
}
if !opts.resolution.is_finite() || opts.resolution < 0.0 {
return Err(IgraphError::InvalidArgument(
"resolution parameter must be non-negative and finite".to_string(),
));
}
if !opts.beta.is_finite() || opts.beta < 0.0 {
return Err(IgraphError::InvalidArgument(
"beta parameter must be non-negative and finite".to_string(),
));
}
validate_weights(graph, weights, opts.objective)?;
let n = graph.vcount() as usize;
if n == 0 {
return Ok(LeidenResult {
membership: Vec::new(),
quality: 0.0,
nb_clusters: 0,
n_iterations_run: 0,
qualities: Vec::new(),
});
}
let mut membership: Vec<u32> = if let Some(start) = opts.start.as_deref() {
if start.len() != n {
return Err(IgraphError::InvalidArgument(format!(
"start membership length ({}) differs from vcount ({n})",
start.len()
)));
}
let mut m = start.to_vec();
reindex_membership(&mut m);
m
} else {
(0..n as u32).collect()
};
let edge_weights: Vec<f64> = match weights {
Some(w) => w.to_vec(),
None => vec![1.0; graph.ecount()],
};
let (eff_resolution, vertex_weights) =
derive_objective_params(graph, &edge_weights, opts.objective, opts.resolution)?;
let mut rng = SplitMix64::new(opts.seed);
let mut qualities: Vec<f64> = Vec::new();
let mut n_iterations_run: u32 = 0;
let mut changed = true;
let max_iter = if opts.n_iterations < 0 {
u32::MAX
} else {
u32::try_from(opts.n_iterations).unwrap_or(u32::MAX)
};
for _itr in 0..max_iter {
if opts.n_iterations < 0 && !changed {
break;
}
changed = false;
community_leiden_pass(
graph,
&edge_weights,
&vertex_weights,
eff_resolution,
opts.beta,
&mut membership,
&mut rng,
&mut changed,
)?;
n_iterations_run += 1;
let q = compute_quality(
graph,
&edge_weights,
&vertex_weights,
&membership,
eff_resolution,
);
qualities.push(q);
}
let nb_clusters = reindex_membership(&mut membership);
let quality = compute_quality(
graph,
&edge_weights,
&vertex_weights,
&membership,
eff_resolution,
);
Ok(LeidenResult {
membership,
quality,
nb_clusters,
n_iterations_run,
qualities,
})
}
fn derive_objective_params(
graph: &Graph,
edge_weights: &[f64],
objective: LeidenObjective,
user_resolution: f64,
) -> IgraphResult<(f64, Vec<f64>)> {
let n = graph.vcount() as usize;
let m = graph.ecount();
match objective {
LeidenObjective::Modularity => {
let mut strength = vec![0.0_f64; n];
for e in 0..m {
let e_id = u32::try_from(e).map_err(|_| {
IgraphError::InvalidArgument("edge count exceeds u32::MAX".into())
})?;
let (u, v) = graph.edge(e_id as EdgeId)?;
let w = edge_weights[e];
strength[u as usize] += w;
if u != v {
strength[v as usize] += w;
}
}
let total: f64 = strength.iter().sum();
if total <= 0.0 {
return Ok((user_resolution, strength));
}
Ok((user_resolution / total, strength))
}
LeidenObjective::Cpm => Ok((user_resolution, vec![1.0; n])),
LeidenObjective::Er => {
let total: f64 = edge_weights.iter().sum();
if n == 0 {
return Ok((user_resolution, Vec::new()));
}
let denom = (n as f64) * (n as f64 + 1.0) * 0.5;
let p = if denom > 0.0 { total / denom } else { 0.0 };
Ok((user_resolution * p, vec![1.0; n]))
}
}
}
struct LeidenLevel {
n: usize,
adj: Vec<Vec<(u32, f64)>>,
loop_weight: Vec<f64>,
vertex_weight: Vec<f64>,
}
fn build_level_from_graph(
graph: &Graph,
edge_weights: &[f64],
vertex_weights: &[f64],
) -> LeidenLevel {
let n = graph.vcount() as usize;
let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
let mut loop_weight = vec![0.0_f64; n];
let m = graph.ecount();
for e in 0..m {
let Ok((u, v)) = graph.edge(e as EdgeId) else {
continue;
};
let w = edge_weights[e];
if u == v {
loop_weight[u as usize] += w;
} else {
adj[u as usize].push((v, w));
adj[v as usize].push((u, w));
}
}
LeidenLevel {
n,
adj,
loop_weight,
vertex_weight: vertex_weights.to_vec(),
}
}
fn build_level_from_edges(
n: usize,
edges: &[(u32, u32, f64)],
vertex_weights: Vec<f64>,
) -> LeidenLevel {
let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
let mut loop_weight = vec![0.0_f64; n];
for &(u, v, w) in edges {
if u == v {
loop_weight[u as usize] += w;
} else {
adj[u as usize].push((v, w));
adj[v as usize].push((u, w));
}
}
LeidenLevel {
n,
adj,
loop_weight,
vertex_weight: vertex_weights,
}
}
fn community_leiden_pass(
graph: &Graph,
edge_weights: &[f64],
vertex_weights: &[f64],
resolution: f64,
beta: f64,
membership: &mut Vec<u32>,
rng: &mut SplitMix64,
changed: &mut bool,
) -> IgraphResult<()> {
let n = graph.vcount() as usize;
if n == 0 {
return Ok(());
}
let mut level = build_level_from_graph(graph, edge_weights, vertex_weights);
let mut aggregate_vertex: Vec<u32> = (0..n as u32).collect();
let mut work_membership: Vec<u32> = membership.clone();
reindex_membership(&mut work_membership);
for level_idx in 0..MAX_LEVELS_PER_ITERATION {
let (nb_clusters, level_changed) =
leiden_fastmove_vertices(&level, resolution, &mut work_membership, rng);
if level_changed {
*changed = true;
}
let continue_clustering = (nb_clusters as usize) < level.n;
if !continue_clustering {
break;
}
if level_idx > 0 {
for i in 0..n {
let v_agg = aggregate_vertex[i] as usize;
membership[i] = work_membership[v_agg];
}
} else {
membership.copy_from_slice(&work_membership);
}
let mut refined_membership: Vec<u32> = vec![0; level.n];
let mut nb_refined_clusters: usize = 0;
let mut clusters: Vec<Vec<u32>> = vec![Vec::new(); nb_clusters as usize];
for v in 0..level.n {
let c = work_membership[v] as usize;
clusters[c].push(v as u32);
}
for (c, vertex_subset) in clusters.iter().enumerate() {
leiden_merge_vertices(
&level,
vertex_subset,
&work_membership,
c as u32,
resolution,
beta,
&mut nb_refined_clusters,
&mut refined_membership,
rng,
);
}
if nb_refined_clusters >= level.n {
refined_membership.copy_from_slice(&work_membership);
nb_refined_clusters = nb_clusters as usize;
}
for i in 0..n {
let v_agg = aggregate_vertex[i] as usize;
aggregate_vertex[i] = refined_membership[v_agg];
}
let (next_level, next_membership) = leiden_aggregate(
&level,
&work_membership,
&refined_membership,
nb_refined_clusters,
);
level = next_level;
work_membership = next_membership;
reindex_membership(&mut work_membership);
}
if level.n == n {
membership.copy_from_slice(&work_membership);
} else {
for i in 0..n {
let v_agg = aggregate_vertex[i] as usize;
membership[i] = work_membership[v_agg];
}
}
reindex_membership(membership);
Ok(())
}
fn leiden_fastmove_vertices(
level: &LeidenLevel,
resolution: f64,
membership: &mut [u32],
rng: &mut SplitMix64,
) -> (u32, bool) {
let n = level.n;
if n == 0 {
return (0, false);
}
let mut cluster_weight = vec![0.0_f64; n];
let mut cluster_size: Vec<u32> = vec![0; n];
for v in 0..n {
let c = membership[v] as usize;
cluster_weight[c] += level.vertex_weight[v];
cluster_size[c] += 1;
}
let mut empty_clusters: Vec<u32> = Vec::new();
for c in 0..n {
if cluster_size[c] == 0 {
empty_clusters.push(c as u32);
}
}
let mut order: Vec<u32> = (0..n as u32).collect();
if n > 1 {
shuffle_in_place(&mut order, rng);
}
let mut queue: std::collections::VecDeque<u32> = order.into_iter().collect();
let mut stable: Vec<bool> = vec![false; n];
let mut edge_weight_per_cluster = vec![0.0_f64; n];
let mut neighbor_clusters: Vec<u32> = Vec::new();
let mut cluster_touched: Vec<bool> = vec![false; n];
let mut changed = false;
while let Some(v) = queue.pop_front() {
let v_idx = v as usize;
let weight_v = level.vertex_weight[v_idx];
let current_cluster = membership[v_idx] as usize;
cluster_weight[current_cluster] -= weight_v;
cluster_size[current_cluster] -= 1;
if cluster_size[current_cluster] == 0 {
empty_clusters.push(current_cluster as u32);
}
neighbor_clusters.clear();
let empty_top = empty_clusters.last().copied();
if let Some(top) = empty_top {
let top_us = top as usize;
if !cluster_touched[top_us] {
cluster_touched[top_us] = true;
neighbor_clusters.push(top);
}
}
for &(u, w) in &level.adj[v_idx] {
let u_us = u as usize;
if u_us == v_idx {
continue;
}
let c = membership[u_us];
let c_us = c as usize;
if !cluster_touched[c_us] {
cluster_touched[c_us] = true;
neighbor_clusters.push(c);
}
edge_weight_per_cluster[c_us] += w;
}
let mut best_cluster = current_cluster as u32;
let mut max_diff = edge_weight_per_cluster[current_cluster]
- weight_v * cluster_weight[current_cluster] * resolution;
for &c in &neighbor_clusters {
let c_us = c as usize;
let diff = edge_weight_per_cluster[c_us] - weight_v * cluster_weight[c_us] * resolution;
if diff > max_diff {
best_cluster = c;
max_diff = diff;
}
}
for &c in &neighbor_clusters {
let c_us = c as usize;
edge_weight_per_cluster[c_us] = 0.0;
cluster_touched[c_us] = false;
}
let best_us = best_cluster as usize;
cluster_weight[best_us] += weight_v;
cluster_size[best_us] += 1;
if empty_top == Some(best_cluster) && cluster_size[best_us] == 1 {
empty_clusters.pop();
}
stable[v_idx] = true;
if best_us != current_cluster {
changed = true;
membership[v_idx] = best_cluster;
for &(u, _) in &level.adj[v_idx] {
let u_us = u as usize;
if stable[u_us] && membership[u_us] as usize != best_us {
queue.push_back(u);
stable[u_us] = false;
}
}
}
}
let nb_clusters = reindex_membership(membership);
(nb_clusters, changed)
}
fn leiden_merge_vertices(
level: &LeidenLevel,
vertex_subset: &[u32],
parent_membership: &[u32],
parent_cluster: u32,
resolution: f64,
beta: f64,
nb_refined_clusters: &mut usize,
refined_membership: &mut [u32],
rng: &mut SplitMix64,
) {
let n_sub = vertex_subset.len();
if n_sub == 0 {
return;
}
if n_sub == 1 {
refined_membership[vertex_subset[0] as usize] = (*nb_refined_clusters) as u32;
*nb_refined_clusters += 1;
return;
}
let mut sub_cluster_weight = vec![0.0_f64; n_sub];
let mut sub_nb_vertices: Vec<u32> = vec![0; n_sub];
let mut external_edge_weight = vec![0.0_f64; n_sub];
let mut total_subset_weight = 0.0_f64;
for (i, &v) in vertex_subset.iter().enumerate() {
let v_us = v as usize;
refined_membership[v_us] = i as u32;
sub_cluster_weight[i] += level.vertex_weight[v_us];
total_subset_weight += level.vertex_weight[v_us];
sub_nb_vertices[i] += 1;
for &(u, w) in &level.adj[v_us] {
let u_us = u as usize;
if u_us != v_us && parent_membership[u_us] == parent_cluster {
external_edge_weight[i] += w;
}
}
}
let mut order: Vec<u32> = vertex_subset.to_vec();
if n_sub > 1 {
shuffle_in_place(&mut order, rng);
}
let mut non_singleton: Vec<bool> = vec![false; n_sub];
let mut edge_weight_per_local = vec![0.0_f64; n_sub];
let mut neighbor_locals: Vec<u32> = Vec::new();
let mut local_touched: Vec<bool> = vec![false; n_sub];
let mut cum_trans_diff = vec![0.0_f64; n_sub];
for &v in &order {
let v_us = v as usize;
let current = refined_membership[v_us] as usize;
if non_singleton[current] {
continue;
}
let vw_prod =
sub_cluster_weight[current] * (total_subset_weight - sub_cluster_weight[current]);
if external_edge_weight[current] < vw_prod * resolution {
continue;
}
sub_cluster_weight[current] = 0.0;
sub_nb_vertices[current] = 0;
neighbor_locals.clear();
if !local_touched[current] {
local_touched[current] = true;
neighbor_locals.push(current as u32);
}
for &(u, w) in &level.adj[v_us] {
let u_us = u as usize;
if u_us != v_us && parent_membership[u_us] == parent_cluster {
let c = refined_membership[u_us];
let c_us = c as usize;
if !local_touched[c_us] {
local_touched[c_us] = true;
neighbor_locals.push(c);
}
edge_weight_per_local[c_us] += w;
}
}
let weight_v = level.vertex_weight[v_us];
let mut best_cluster = current as u32;
let mut max_diff = 0.0_f64;
let mut total_cum = 0.0_f64;
for (j, &c) in neighbor_locals.iter().enumerate() {
let c_us = c as usize;
let vw_prod_c =
sub_cluster_weight[c_us] * (total_subset_weight - sub_cluster_weight[c_us]);
if external_edge_weight[c_us] >= vw_prod_c * resolution {
let diff =
edge_weight_per_local[c_us] - weight_v * sub_cluster_weight[c_us] * resolution;
if diff > max_diff {
best_cluster = c;
max_diff = diff;
}
if diff >= 0.0 {
if beta > 0.0 {
total_cum += (diff / beta).exp();
} else if diff > 0.0 {
total_cum = f64::INFINITY;
}
}
}
cum_trans_diff[j] = total_cum;
}
let chosen_cluster = if total_cum.is_finite() && total_cum > 0.0 {
let r = rng.next_f64() * total_cum;
let mut idx = neighbor_locals.len() - 1;
for (j, &cum) in cum_trans_diff
.iter()
.enumerate()
.take(neighbor_locals.len())
{
if cum >= r {
idx = j;
break;
}
}
neighbor_locals[idx]
} else if total_cum == 0.0 {
current as u32
} else {
best_cluster
};
let chosen_us = chosen_cluster as usize;
sub_cluster_weight[chosen_us] += weight_v;
sub_nb_vertices[chosen_us] += 1;
for &(u, w) in &level.adj[v_us] {
let u_us = u as usize;
if parent_membership[u_us] == parent_cluster {
if refined_membership[u_us] as usize == chosen_us {
external_edge_weight[chosen_us] -= w;
} else {
external_edge_weight[chosen_us] += w;
}
}
}
if chosen_us != current {
refined_membership[v_us] = chosen_cluster;
non_singleton[chosen_us] = true;
}
for &c in &neighbor_locals {
let c_us = c as usize;
edge_weight_per_local[c_us] = 0.0;
local_touched[c_us] = false;
}
}
let base = *nb_refined_clusters;
let mut local_to_global: Vec<u32> = vec![u32::MAX; n_sub];
let mut next_global = base;
for &v in vertex_subset {
let v_us = v as usize;
let local = refined_membership[v_us] as usize;
if local_to_global[local] == u32::MAX {
local_to_global[local] = next_global as u32;
next_global += 1;
}
refined_membership[v_us] = local_to_global[local];
}
*nb_refined_clusters = next_global;
}
fn leiden_aggregate(
level: &LeidenLevel,
parent_membership: &[u32],
refined_membership: &[u32],
nb_refined: usize,
) -> (LeidenLevel, Vec<u32>) {
let k = nb_refined;
let mut inter: Vec<Vec<(u32, f64)>> = vec![Vec::new(); k];
let mut loops: Vec<f64> = vec![0.0_f64; k];
let mut agg_vertex_weight: Vec<f64> = vec![0.0_f64; k];
let mut new_membership: Vec<u32> = vec![0; k];
for v in 0..level.n {
let c = refined_membership[v] as usize;
agg_vertex_weight[c] += level.vertex_weight[v];
new_membership[c] = parent_membership[v];
let lw = level.loop_weight[v];
if lw > 0.0 {
loops[c] += lw;
}
for &(u, w) in &level.adj[v] {
let u_us = u as usize;
if u_us <= v {
continue;
}
let c2 = refined_membership[u_us] as usize;
if c == c2 {
loops[c] += w;
} else {
let (a, b) = if c < c2 { (c, c2) } else { (c2, c) };
push_or_merge(&mut inter[a], b as u32, w);
}
}
}
let mut edges: Vec<(u32, u32, f64)> = Vec::new();
for c in 0..k {
if loops[c] > 0.0 {
edges.push((c as u32, c as u32, loops[c]));
}
for &(c2, w) in &inter[c] {
edges.push((c as u32, c2, w));
}
}
let next_level = build_level_from_edges(k, &edges, agg_vertex_weight);
(next_level, new_membership)
}
fn push_or_merge(list: &mut Vec<(u32, f64)>, neighbour: u32, weight: f64) {
for slot in list.iter_mut() {
if slot.0 == neighbour {
slot.1 += weight;
return;
}
}
list.push((neighbour, weight));
}
fn compute_quality(
graph: &Graph,
edge_weights: &[f64],
vertex_weights: &[f64],
membership: &[u32],
resolution: f64,
) -> f64 {
let n = graph.vcount() as usize;
let m = graph.ecount();
if n == 0 {
return 0.0;
}
let mut q = 0.0_f64;
let mut total_edge_weight = 0.0_f64;
for e in 0..m {
let Ok((u, v)) = graph.edge(e as EdgeId) else {
continue;
};
let w = edge_weights[e];
total_edge_weight += w;
if membership[u as usize] == membership[v as usize] {
q += 2.0 * w;
}
}
let k = membership.iter().copied().max().map_or(0, |m| m + 1) as usize;
let mut cluster_weight = vec![0.0_f64; k];
for v in 0..n {
cluster_weight[membership[v] as usize] += vertex_weights[v];
}
for c in 0..k {
q -= resolution * cluster_weight[c] * cluster_weight[c];
}
if total_edge_weight <= 0.0 {
return 0.0;
}
q / (2.0 * total_edge_weight)
}
fn reindex_membership(membership: &mut [u32]) -> u32 {
if membership.is_empty() {
return 0;
}
let max = *membership.iter().max().unwrap_or(&0);
let mut remap: Vec<i64> = vec![-1; max as usize + 1];
let mut next: u32 = 0;
for slot in membership.iter_mut() {
let old = *slot as usize;
if remap[old] < 0 {
remap[old] = i64::from(next);
next += 1;
}
*slot = remap[old] as u32;
}
next
}
fn validate_weights(
graph: &Graph,
weights: Option<&[f64]>,
objective: LeidenObjective,
) -> IgraphResult<()> {
let Some(w) = weights else {
return Ok(());
};
let m = graph.ecount();
if w.len() != m {
return Err(IgraphError::InvalidArgument(format!(
"weights vector size ({}) differs from edge count ({m})",
w.len(),
)));
}
let allow_negative = matches!(objective, LeidenObjective::Cpm);
for (e, &v) in w.iter().enumerate() {
if !v.is_finite() {
return Err(IgraphError::InvalidArgument(format!(
"weight at edge {e} is not finite ({v})"
)));
}
if !allow_negative && v < 0.0 {
return Err(IgraphError::InvalidArgument(format!(
"weight at edge {e} is negative ({v}); leiden with this objective requires non-negative weights"
)));
}
}
Ok(())
}
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self(seed.wrapping_add(0x9E37_79B9_7F4A_7C15))
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn gen_index(&mut self, bound: usize) -> usize {
debug_assert!(bound > 0);
let r = self.next_u64() % (bound as u64);
usize::try_from(r).unwrap_or(0)
}
fn next_f64(&mut self) -> f64 {
((self.next_u64() >> 11) as f64) * (1.0 / ((1u64 << 53) as f64))
}
}
fn shuffle_in_place<T>(slice: &mut [T], rng: &mut SplitMix64) {
let len = slice.len();
for i in (1..len).rev() {
let j = rng.gen_index(i + 1);
slice.swap(i, j);
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
fn add_edges(g: &mut Graph, edges: &[(u32, u32)]) {
for &(u, v) in edges {
g.add_edge(u, v).unwrap();
}
}
#[test]
fn empty_graph_returns_empty_membership() {
let g = Graph::with_vertices(0);
let r = leiden(&g).unwrap();
assert_eq!(r.membership.len(), 0);
assert_eq!(r.quality, 0.0);
assert_eq!(r.nb_clusters, 0);
}
#[test]
fn isolated_vertices_keep_singletons_after_modularity_reduction() {
let g = Graph::with_vertices(4);
let r = leiden(&g).unwrap();
assert_eq!(r.membership.len(), 4);
let k = r.nb_clusters;
assert!((1..=4).contains(&k));
}
#[test]
fn two_triangles_bridge_split() {
let mut g = Graph::with_vertices(6);
add_edges(
&mut g,
&[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
);
let r = leiden(&g).unwrap();
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[0], r.membership[2]);
assert_eq!(r.membership[3], r.membership[4]);
assert_eq!(r.membership[3], r.membership[5]);
assert_ne!(r.membership[0], r.membership[3]);
}
#[test]
fn directed_input_rejected() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
assert!(leiden(&g).is_err());
assert!(leiden_weighted(&g, &[1.0]).is_err());
assert!(leiden_with_options(&g, None, &LeidenOptions::default()).is_err());
}
#[test]
fn weight_validation() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(leiden_weighted(&g, &[1.0]).is_err());
assert!(leiden_weighted(&g, &[1.0, -0.1]).is_err());
assert!(leiden_weighted(&g, &[1.0, f64::NAN]).is_err());
assert!(leiden_weighted(&g, &[1.0, f64::INFINITY]).is_err());
let opts = LeidenOptions {
objective: LeidenObjective::Cpm,
..LeidenOptions::default()
};
assert!(leiden_with_options(&g, Some(&[1.0, -0.1]), &opts).is_ok());
}
#[test]
fn deterministic_under_seed() {
let mut g = Graph::with_vertices(6);
add_edges(
&mut g,
&[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
);
let opts = LeidenOptions {
seed: 12345,
..LeidenOptions::default()
};
let a = leiden_with_options(&g, None, &opts).unwrap();
let b = leiden_with_options(&g, None, &opts).unwrap();
assert_eq!(a.membership, b.membership);
assert!((a.quality - b.quality).abs() < 1e-12);
}
#[test]
fn cpm_objective_works() {
let mut g = Graph::with_vertices(6);
add_edges(
&mut g,
&[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
);
let opts = LeidenOptions {
objective: LeidenObjective::Cpm,
resolution: 0.5,
..LeidenOptions::default()
};
let r = leiden_with_options(&g, None, &opts).unwrap();
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[0], r.membership[2]);
assert_eq!(r.membership[3], r.membership[4]);
assert_eq!(r.membership[3], r.membership[5]);
assert_ne!(r.membership[0], r.membership[3]);
}
#[test]
fn negative_resolution_rejected() {
let g = Graph::with_vertices(3);
let opts = LeidenOptions {
resolution: -0.1,
..LeidenOptions::default()
};
assert!(leiden_with_options(&g, None, &opts).is_err());
}
#[test]
fn negative_beta_rejected() {
let g = Graph::with_vertices(3);
let opts = LeidenOptions {
beta: -0.1,
..LeidenOptions::default()
};
assert!(leiden_with_options(&g, None, &opts).is_err());
}
}