use std::collections::VecDeque;
use crate::graph::GraphData;
use crate::graph::GraphDataBuilder;
use crate::partition::Partition;
use crate::quality::{MoveComponents, QualityFunction};
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rustc_hash::FxHashMap;
pub(crate) struct LocalMovingBuffers {
pub comm_total_degree: Vec<Vec<f64>>,
pub comm_in_degree: Vec<Vec<f64>>,
pub comm_size: Vec<f64>,
pub layer_out_weights: Vec<Vec<f64>>,
pub layer_in_weights: Vec<Vec<f64>>,
pub touched_list: Vec<usize>,
pub touched_mark: Vec<bool>,
pub in_queue: Vec<bool>,
pub two_m_values: Vec<f64>,
}
impl LocalMovingBuffers {
pub fn new(capacity: usize, num_layers: usize) -> Self {
Self {
comm_total_degree: vec![vec![0.0; capacity]; num_layers],
comm_in_degree: vec![vec![0.0; capacity]; num_layers],
comm_size: vec![0.0; capacity],
layer_out_weights: vec![vec![0.0; capacity]; num_layers],
layer_in_weights: vec![vec![0.0; capacity]; num_layers],
touched_list: Vec::with_capacity(64),
touched_mark: vec![false; capacity],
in_queue: vec![false; capacity],
two_m_values: vec![0.0; num_layers],
}
}
pub fn resize_and_clear(&mut self, n: usize, num_layers: usize) {
if num_layers > self.comm_total_degree.len() {
self.comm_total_degree
.resize_with(num_layers, || vec![0.0; n]);
self.comm_in_degree
.resize_with(num_layers, || vec![0.0; n]);
}
for arr in &mut self.comm_total_degree[..num_layers] {
if n > arr.len() {
arr.resize(n, 0.0);
}
arr[..n].fill(0.0);
}
for arr in &mut self.comm_in_degree[..num_layers] {
if n > arr.len() {
arr.resize(n, 0.0);
}
arr[..n].fill(0.0);
}
if n > self.comm_size.len() {
self.comm_size.resize(n, 0.0);
self.touched_mark.resize(n, false);
self.in_queue.resize(n, false);
}
self.comm_size[..n].fill(0.0);
self.touched_mark[..n].fill(false);
self.in_queue[..n].fill(false);
if num_layers > self.layer_out_weights.len() {
self.layer_out_weights
.resize_with(num_layers, || vec![0.0; n]);
self.layer_in_weights
.resize_with(num_layers, || vec![0.0; n]);
}
for lw in &mut self.layer_out_weights[..num_layers] {
if n > lw.len() {
lw.resize(n, 0.0);
}
lw[..n].fill(0.0);
}
for lw in &mut self.layer_in_weights[..num_layers] {
if n > lw.len() {
lw.resize(n, 0.0);
}
lw[..n].fill(0.0);
}
if num_layers > self.two_m_values.len() {
self.two_m_values.resize(num_layers, 0.0);
}
for &idx in &self.touched_list {
if idx < self.touched_mark.len() {
self.touched_mark[idx] = false;
}
}
self.touched_list.clear();
}
}
pub(crate) struct RefinementBuffers {
comm_total_degree: Vec<Vec<f64>>,
comm_in_degree: Vec<Vec<f64>>,
comm_size: Vec<f64>,
layer_out_weights: Vec<Vec<f64>>,
layer_in_weights: Vec<Vec<f64>>,
touched_list: Vec<usize>,
touched_mark: Vec<bool>,
two_m_values: Vec<f64>,
}
impl RefinementBuffers {
pub fn new(capacity: usize, num_layers: usize) -> Self {
Self {
comm_total_degree: vec![vec![0.0; capacity]; num_layers],
comm_in_degree: vec![vec![0.0; capacity]; num_layers],
comm_size: vec![0.0; capacity],
layer_out_weights: vec![vec![0.0; capacity]; num_layers],
layer_in_weights: vec![vec![0.0; capacity]; num_layers],
touched_list: Vec::with_capacity(64),
touched_mark: vec![false; capacity],
two_m_values: vec![0.0; num_layers],
}
}
pub fn resize_and_clear(&mut self, new_capacity: usize, layers: &[GraphData]) {
let num_layers = layers.len();
if num_layers > self.comm_total_degree.len() {
self.comm_total_degree
.resize_with(num_layers, || vec![0.0; new_capacity]);
self.comm_in_degree
.resize_with(num_layers, || vec![0.0; new_capacity]);
}
for arr in &mut self.comm_total_degree[..num_layers] {
if new_capacity > arr.len() {
arr.resize(new_capacity, 0.0);
}
arr[..new_capacity].fill(0.0);
}
for arr in &mut self.comm_in_degree[..num_layers] {
if new_capacity > arr.len() {
arr.resize(new_capacity, 0.0);
}
arr[..new_capacity].fill(0.0);
}
if new_capacity > self.comm_size.len() {
self.comm_size.resize(new_capacity, 0.0);
for layer_weights in &mut self.layer_out_weights {
layer_weights.resize(new_capacity, 0.0);
}
for layer_weights in &mut self.layer_in_weights {
layer_weights.resize(new_capacity, 0.0);
}
self.touched_mark.resize(new_capacity, false);
}
self.comm_size[..new_capacity].fill(0.0);
for layer_weights in &mut self.layer_out_weights[..num_layers] {
if new_capacity > layer_weights.len() {
layer_weights.resize(new_capacity, 0.0);
}
layer_weights[..new_capacity].fill(0.0);
}
for layer_weights in &mut self.layer_in_weights[..num_layers] {
if new_capacity > layer_weights.len() {
layer_weights.resize(new_capacity, 0.0);
}
layer_weights[..new_capacity].fill(0.0);
}
for &idx in &self.touched_list {
self.touched_mark[idx] = false;
}
self.touched_list.clear();
if num_layers > self.two_m_values.len() {
self.two_m_values.resize(num_layers, 0.0);
}
for (l, layer) in layers.iter().enumerate() {
self.two_m_values[l] = 2.0 * layer.total_weight();
}
}
}
#[inline]
pub(crate) fn find_best_community(
candidates: impl Iterator<Item = usize>,
current_community: usize,
epsilon: f64,
max_comm_size: usize,
comm_size: &[f64],
node_weight: f64,
delta_fn: impl Fn(usize) -> f64,
) -> (usize, f64) {
let mut best_community = current_community;
let mut best_delta = epsilon;
for target_comm in candidates {
if max_comm_size > 0 && comm_size[target_comm] + node_weight > max_comm_size as f64 {
continue;
}
let delta = delta_fn(target_comm);
if delta > best_delta {
best_delta = delta;
best_community = target_comm;
}
}
(best_community, best_delta)
}
pub(crate) enum MoveTarget<'a> {
Partition(&'a mut Partition),
RefinedMap(&'a mut [usize]),
}
pub(crate) struct NodeContribution<'a> {
pub k_v_out: &'a [f64],
pub k_v_in: &'a [f64],
pub weight: f64,
}
pub(crate) struct CommunityStats<'a> {
pub total_degree_out: &'a mut [Vec<f64>],
pub in_degree_out: &'a mut [Vec<f64>],
pub size: &'a mut [f64],
}
pub(crate) struct MovingConfig {
pub max_comm_size: usize,
pub epsilon: f64,
}
pub(crate) struct CommunitySubset<'a> {
pub community: usize,
pub nodes: &'a [usize],
}
#[inline]
pub(crate) fn apply_move(
target: MoveTarget<'_>,
node: usize,
old_comm: usize,
new_comm: usize,
contribution: NodeContribution<'_>,
stats: &mut CommunityStats<'_>,
) {
match target {
MoveTarget::RefinedMap(map) => map[node] = new_comm,
MoveTarget::Partition(partition) => partition.move_node(node, new_comm),
}
for l in 0..contribution.k_v_out.len() {
stats.total_degree_out[l][old_comm] -= contribution.k_v_out[l];
stats.total_degree_out[l][new_comm] += contribution.k_v_out[l];
if !stats.in_degree_out[l].is_empty() {
stats.in_degree_out[l][old_comm] -= contribution.k_v_in[l];
stats.in_degree_out[l][new_comm] += contribution.k_v_in[l];
}
}
stats.size[old_comm] -= contribution.weight;
stats.size[new_comm] += contribution.weight;
}
pub(crate) fn init_community_stats_into(
layers: &[GraphData],
community_of_fn: impl Fn(usize) -> usize,
comm_total_degree_out: &mut [Vec<f64>],
comm_in_degree_out: &mut [Vec<f64>],
comm_size_out: &mut [f64],
) {
let n = comm_size_out.len();
for node in 0..n {
let comm = community_of_fn(node);
comm_size_out[comm] += layers[0].node_weight(node);
}
for (l, layer) in layers.iter().enumerate() {
for node in 0..n {
let comm = community_of_fn(node);
comm_total_degree_out[l][comm] += layer.out_degree_of(node);
comm_in_degree_out[l][comm] += layer.in_degree_of(node);
}
}
}
#[inline]
pub(crate) fn aggregate_node_edges_into(
source: &GraphData,
u: usize,
orig_to_agg: &[usize],
directed: bool,
map: &mut FxHashMap<(usize, usize), f64>,
) {
let ru = orig_to_agg[u];
if directed {
for (v, w) in source.neighbors(u) {
let rv = orig_to_agg[v];
*map.entry((ru, rv)).or_default() += w;
}
} else {
for (v, w) in source.neighbors(u) {
if u == v {
*map.entry((ru, ru)).or_default() += w;
} else if v > u {
let rv = orig_to_agg[v];
let key = if ru <= rv { (ru, rv) } else { (rv, ru) };
*map.entry(key).or_default() += w;
}
}
}
}
pub(crate) fn build_orig_to_agg_mapping(
n: usize,
refined_partition: &Partition,
) -> (Vec<usize>, usize) {
let mut orig_to_agg: Vec<usize> = vec![0; n];
let mut comm_to_agg: FxHashMap<usize, usize> = FxHashMap::default();
let mut next_id = 0usize;
for (node, entry) in orig_to_agg.iter_mut().enumerate() {
let c = refined_partition.community_of(node);
let agg_id = *comm_to_agg.entry(c).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
*entry = agg_id;
}
(orig_to_agg, next_id)
}
pub(crate) fn build_aggregated_graph(
orig_to_agg: Vec<usize>,
agg_n: usize,
directed: bool,
agg_edges_map: FxHashMap<(usize, usize), f64>,
coarse_partition: &Partition,
node_weight_fn: impl Fn(usize) -> f64,
) -> crate::error::Result<(crate::graph::GraphData, Vec<usize>, Partition)> {
let mut agg_edges: Vec<((usize, usize), f64)> = agg_edges_map.into_iter().collect();
agg_edges.sort_by_key(|&((u, v), _)| (u, v));
let mut agg_node_weight: Vec<f64> = vec![0.0; agg_n];
for (orig, &agg_node) in orig_to_agg.iter().enumerate() {
agg_node_weight[agg_node] += node_weight_fn(orig);
}
let mut builder = GraphDataBuilder::new(agg_n);
if directed {
builder = builder.directed();
}
for &((u, v), w) in &agg_edges {
builder.add_edge(u, v, w)?;
}
for (node, &nw) in agg_node_weight.iter().enumerate() {
if nw != 1.0 {
builder.set_node_weight(node, nw)?;
}
}
let agg_data = builder.build()?;
let mut agg_initial = Partition::new(agg_n);
for (orig, &agg_node) in orig_to_agg.iter().enumerate() {
let coarse_comm = coarse_partition.community_of(orig);
agg_initial.move_node(agg_node, coarse_comm);
}
agg_initial.renumber();
Ok((agg_data, orig_to_agg, agg_initial))
}
pub(crate) fn refinement_generic(
n: usize,
_num_layers: usize,
partition: &Partition,
rng: &mut StdRng,
buffers: &mut RefinementBuffers,
refine_fn: impl Fn(usize, &[usize], &mut RefinementBuffers) -> Vec<(usize, usize)> + Send + Sync,
) -> Partition {
let mut refined = Partition::new(n);
let num_comms = partition.num_communities();
let mut community_nodes: Vec<Vec<usize>> = vec![Vec::new(); num_comms];
for node in 0..n {
community_nodes[partition.community_of(node)].push(node);
}
for nodes in &mut community_nodes {
nodes.shuffle(rng);
}
let results: Vec<Vec<(usize, usize)>> = {
#[cfg(feature = "rayon")]
{
let par_threshold = std::cmp::max(4, rayon::current_num_threads() * 2);
if num_comms > par_threshold {
use rayon::prelude::*;
let num_layers = buffers.comm_total_degree.len();
community_nodes
.par_iter()
.enumerate()
.map_init(
|| RefinementBuffers::new(n, num_layers),
|thread_buffers, (community, nodes)| refine_fn(community, nodes, thread_buffers),
)
.collect()
} else {
community_nodes
.iter()
.enumerate()
.map(|(community, nodes)| refine_fn(community, nodes, buffers))
.collect()
}
}
#[cfg(not(feature = "rayon"))]
{
community_nodes
.iter()
.enumerate()
.map(|(community, nodes)| refine_fn(community, nodes, buffers))
.collect()
}
};
for moves in &results {
for &(node, new_comm) in moves {
refined.move_node(node, new_comm);
}
}
refined.renumber();
refined
}
pub(crate) fn local_moving_generic(
layers: &[GraphData],
layer_weights: &[f64],
partition: &mut Partition,
quality: &(dyn QualityFunction + Sync),
rng: &mut StdRng,
cfg: &MovingConfig,
buffers: &mut LocalMovingBuffers,
) -> bool {
let n = layers[0].node_count();
if n == 0 {
return false;
}
let num_layers = layers.len();
let total_weight: f64 = layers.iter().map(|l| l.total_weight()).sum();
if total_weight <= 0.0 {
return false;
}
buffers.resize_and_clear(n, num_layers);
for (l, layer) in layers.iter().enumerate() {
buffers.two_m_values[l] = 2.0 * layer.total_weight();
}
let total_node_weight: f64 = layers[0].total_node_weight();
init_community_stats_into(
layers,
|node| partition.community_of(node),
&mut buffers.comm_total_degree,
&mut buffers.comm_in_degree,
&mut buffers.comm_size[..n],
);
let comm_total_degree = &mut buffers.comm_total_degree;
let comm_in_degree = &mut buffers.comm_in_degree;
let comm_size = &mut buffers.comm_size;
let mut order: Vec<usize> = (0..n)
.filter(|&node| layers.iter().any(|l| l.degree_of(node) > 0.0))
.collect();
order.shuffle(rng);
let mut queue: VecDeque<usize> = order.into_iter().collect();
let in_queue = &mut buffers.in_queue;
for &node in &queue {
in_queue[node] = true;
}
let layer_out_weights = &mut buffers.layer_out_weights;
let layer_in_weights = &mut buffers.layer_in_weights;
let two_m_values = &buffers.two_m_values;
let mut changed = false;
let touched_list = &mut buffers.touched_list;
let touched_mark = &mut buffers.touched_mark;
let mut k_v_out_buf: Vec<f64> = vec![0.0; num_layers];
let mut k_v_in_buf: Vec<f64> = vec![0.0; num_layers];
while let Some(node) = queue.pop_front() {
in_queue[node] = false;
let current_community = partition.community_of(node);
for l in 0..num_layers {
layer_out_weights[l][current_community] = 0.0;
layer_in_weights[l][current_community] = 0.0;
}
let mut current_touched = false;
for (l, layer) in layers.iter().enumerate() {
let (targets, weights) = layer.neighbor_slices(node);
for i in 0..targets.len() {
let neighbor = targets[i];
let weight = weights[i];
if neighbor == node {
continue;
}
let comm = partition.community_of(neighbor);
if layer_out_weights[l][comm] == 0.0 && layer_in_weights[l][comm] == 0.0 {
if comm == current_community {
current_touched = true;
} else if !touched_mark[comm] {
touched_mark[comm] = true;
touched_list.push(comm);
}
}
layer_out_weights[l][comm] += weight;
}
let (in_targets, in_weights) = layer.in_neighbor_slices(node);
for i in 0..in_targets.len() {
let neighbor = in_targets[i];
let weight = in_weights[i];
if neighbor == node {
continue;
}
let comm = partition.community_of(neighbor);
if layer_out_weights[l][comm] == 0.0 && layer_in_weights[l][comm] == 0.0 {
if comm == current_community {
current_touched = true;
} else if !touched_mark[comm] {
touched_mark[comm] = true;
touched_list.push(comm);
}
}
layer_in_weights[l][comm] += weight;
}
}
for (l, layer) in layers.iter().enumerate() {
k_v_out_buf[l] = layer.out_degree_of(node);
k_v_in_buf[l] = layer.in_degree_of(node);
}
let node_weight = layers[0].node_weight(node);
let (best_community, _) = find_best_community(
touched_list.iter().copied(),
current_community,
cfg.epsilon,
cfg.max_comm_size,
&comm_size[..n],
node_weight,
|target_comm| {
let mut total_delta = 0.0f64;
for (l, layer) in layers.iter().enumerate() {
let delta = quality.delta_move_from_components(&MoveComponents {
two_m: two_m_values[l],
node_weight,
total_node_weight,
k_v_out: k_v_out_buf[l],
k_v_to_target_out: layer_out_weights[l][target_comm],
k_v_to_current_out: layer_out_weights[l][current_community],
sigma_tot_target_out: comm_total_degree[l][target_comm],
sigma_tot_current_out: comm_total_degree[l][current_community],
k_v_in: k_v_in_buf[l],
k_v_to_target_in: layer_in_weights[l][target_comm],
k_v_to_current_in: layer_in_weights[l][current_community],
sigma_tot_target_in: comm_in_degree[l][target_comm],
sigma_tot_current_in: comm_in_degree[l][current_community],
n_target: comm_size[target_comm],
n_current: comm_size[current_community],
directed: layer.is_directed(),
});
total_delta += layer_weights[l] * delta;
}
total_delta
},
);
for l in 0..num_layers {
if current_touched {
layer_out_weights[l][current_community] = 0.0;
layer_in_weights[l][current_community] = 0.0;
}
for &comm in &*touched_list {
layer_out_weights[l][comm] = 0.0;
layer_in_weights[l][comm] = 0.0;
}
}
for &idx in &*touched_list {
touched_mark[idx] = false;
}
touched_list.clear();
if best_community != current_community {
apply_move(
MoveTarget::Partition(&mut *partition),
node,
current_community,
best_community,
NodeContribution {
k_v_out: &k_v_out_buf,
k_v_in: &k_v_in_buf,
weight: node_weight,
},
&mut CommunityStats {
total_degree_out: comm_total_degree,
in_degree_out: comm_in_degree,
size: &mut comm_size[..n],
},
);
changed = true;
for layer in layers {
let (targets, _) = layer.neighbor_slices(node);
for &neighbor in targets {
if !in_queue[neighbor] {
queue.push_back(neighbor);
in_queue[neighbor] = true;
}
}
let (in_targets, _) = layer.in_neighbor_slices(node);
for &neighbor in in_targets {
if !in_queue[neighbor] {
queue.push_back(neighbor);
in_queue[neighbor] = true;
}
}
}
}
}
changed
}
pub(crate) fn refine_community_generic(
layers: &[GraphData],
layer_weights: &[f64],
partition: &Partition,
quality: &(dyn QualityFunction + Sync),
subset: &CommunitySubset,
cfg: &MovingConfig,
buffers: &mut RefinementBuffers,
) -> Vec<(usize, usize)> {
if subset.nodes.len() <= 1 {
return Vec::new();
}
let num_layers = layers.len();
let total_node_weight: f64 = layers[0].total_node_weight();
let max_node_id = subset.nodes.iter().copied().max().unwrap_or(0);
let mut refined_map: Vec<usize> = (0..=max_node_id).collect();
buffers.resize_and_clear(max_node_id + 1, layers);
let two_m_values = &buffers.two_m_values;
let comm_total_degree = &mut buffers.comm_total_degree;
let comm_in_degree = &mut buffers.comm_in_degree;
let comm_size = &mut buffers.comm_size;
let layer_out_weights = &mut buffers.layer_out_weights;
let layer_in_weights = &mut buffers.layer_in_weights;
let touched_list = &mut buffers.touched_list;
let touched_mark = &mut buffers.touched_mark;
for &node in subset.nodes {
for (l, layer) in layers.iter().enumerate() {
comm_total_degree[l][node] += layer.out_degree_of(node);
comm_in_degree[l][node] += layer.in_degree_of(node);
}
comm_size[node] += layers[0].node_weight(node);
}
let mut k_v_out_buf: Vec<f64> = vec![0.0; num_layers];
let mut k_v_in_buf: Vec<f64> = vec![0.0; num_layers];
for &node in subset.nodes {
let current_refined = refined_map[node];
for l in 0..num_layers {
layer_out_weights[l][current_refined] = 0.0;
layer_in_weights[l][current_refined] = 0.0;
}
for (l, layer) in layers.iter().enumerate() {
let (targets, weights) = layer.neighbor_slices(node);
for i in 0..targets.len() {
let neighbor = targets[i];
let weight = weights[i];
if partition.community_of(neighbor) != subset.community {
continue;
}
if neighbor == node {
continue;
}
let rc = refined_map[neighbor];
if layer_out_weights[l][rc] == 0.0
&& layer_in_weights[l][rc] == 0.0
&& rc != current_refined
&& !touched_mark[rc]
{
touched_mark[rc] = true;
touched_list.push(rc);
}
layer_out_weights[l][rc] += weight;
}
let (in_targets, in_weights) = layer.in_neighbor_slices(node);
for i in 0..in_targets.len() {
let neighbor = in_targets[i];
let weight = in_weights[i];
if partition.community_of(neighbor) != subset.community {
continue;
}
if neighbor == node {
continue;
}
let rc = refined_map[neighbor];
if layer_out_weights[l][rc] == 0.0
&& layer_in_weights[l][rc] == 0.0
&& rc != current_refined
&& !touched_mark[rc]
{
touched_mark[rc] = true;
touched_list.push(rc);
}
layer_in_weights[l][rc] += weight;
}
}
for (l, layer) in layers.iter().enumerate() {
k_v_out_buf[l] = layer.out_degree_of(node);
k_v_in_buf[l] = layer.in_degree_of(node);
}
let node_weight = layers[0].node_weight(node);
let (best_refined, _) = find_best_community(
touched_list.iter().copied(),
current_refined,
cfg.epsilon,
0,
&comm_size[..],
node_weight,
|target_comm| {
let mut total_delta = 0.0f64;
for (l, layer) in layers.iter().enumerate() {
let delta = quality.delta_move_from_components(&MoveComponents {
two_m: two_m_values[l],
node_weight,
total_node_weight,
k_v_out: k_v_out_buf[l],
k_v_to_target_out: layer_out_weights[l][target_comm],
k_v_to_current_out: layer_out_weights[l][current_refined],
sigma_tot_target_out: comm_total_degree[l][target_comm],
sigma_tot_current_out: comm_total_degree[l][current_refined],
k_v_in: k_v_in_buf[l],
k_v_to_target_in: layer_in_weights[l][target_comm],
k_v_to_current_in: layer_in_weights[l][current_refined],
sigma_tot_target_in: comm_in_degree[l][target_comm],
sigma_tot_current_in: comm_in_degree[l][current_refined],
n_target: comm_size[target_comm],
n_current: comm_size[current_refined],
directed: layer.is_directed(),
});
total_delta += layer_weights[l] * delta;
}
total_delta
},
);
for l in 0..num_layers {
for &rc in &*touched_list {
layer_out_weights[l][rc] = 0.0;
layer_in_weights[l][rc] = 0.0;
}
layer_out_weights[l][current_refined] = 0.0;
layer_in_weights[l][current_refined] = 0.0;
}
for &idx in &*touched_list {
touched_mark[idx] = false;
}
touched_list.clear();
if best_refined != current_refined {
apply_move(
MoveTarget::RefinedMap(&mut refined_map),
node,
current_refined,
best_refined,
NodeContribution {
k_v_out: &k_v_out_buf,
k_v_in: &k_v_in_buf,
weight: node_weight,
},
&mut CommunityStats {
total_degree_out: comm_total_degree,
in_degree_out: comm_in_degree,
size: &mut comm_size[..],
},
);
}
}
subset.nodes
.iter()
.filter_map(|&node| {
let rc = refined_map[node];
if rc != node {
Some((node, rc))
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod per_layer_tests {
use super::*;
use crate::graph::GraphDataBuilder;
#[test]
fn test_init_community_stats_per_layer_not_accumulated() {
let mut b1 = GraphDataBuilder::new(4);
b1.add_edge(0, 1, 1.0).unwrap();
b1.add_edge(1, 2, 1.0).unwrap();
b1.add_edge(0, 2, 1.0).unwrap();
let layer1 = b1.build().unwrap();
let mut b2 = GraphDataBuilder::new(4);
b2.add_edge(0, 1, 2.0).unwrap();
b2.add_edge(1, 2, 2.0).unwrap();
b2.add_edge(2, 3, 2.0).unwrap();
let layer2 = b2.build().unwrap();
let layers = [layer1, layer2];
let n = 4;
let community_of = |_node: usize| -> usize { 0 };
let mut comm_total_degree: Vec<Vec<f64>> = vec![vec![0.0; n]; 2];
let mut comm_in_degree: Vec<Vec<f64>> = vec![vec![0.0; n]; 2];
let mut comm_size: Vec<f64> = vec![0.0; n];
init_community_stats_into(
&layers,
community_of,
&mut comm_total_degree,
&mut comm_in_degree,
&mut comm_size,
);
assert!(
(comm_total_degree[0][0] - 6.0).abs() < 1e-10,
"Layer 0 comm_total_degree should be 6.0 (triangle degrees), got {}",
comm_total_degree[0][0],
);
assert!(
(comm_total_degree[1][0] - 12.0).abs() < 1e-10,
"Layer 1 comm_total_degree should be 12.0 (chain degrees), got {}",
comm_total_degree[1][0],
);
assert!(
(comm_size[0] - 4.0).abs() < 1e-10,
"comm_size should be 4.0 (counted once, not accumulated), got {}",
comm_size[0],
);
}
#[test]
fn test_find_best_community_picks_highest_delta() {
let comm_size: Vec<f64> = vec![1.0, 1.0, 1.0];
let node_weight = 1.0;
let candidates = [0usize, 2].into_iter();
let delta_fn = |target: usize| -> f64 {
match target {
0 => 0.1,
2 => 0.3,
_ => 0.0,
}
};
let (best, delta) = find_best_community(
candidates,
1, 1e-10,
0, &comm_size,
node_weight,
delta_fn,
);
assert_eq!(best, 2, "should pick community with highest delta");
assert!((delta - 0.3).abs() < 1e-10, "delta should be 0.3");
}
#[test]
fn test_find_best_community_stays_when_no_improvement() {
let comm_size: Vec<f64> = vec![1.0, 1.0, 1.0];
let candidates = [0usize, 2].into_iter();
let delta_fn = |_target: usize| -> f64 { 1e-20 };
let (best, _) = find_best_community(
candidates,
1,
1e-10,
0,
&comm_size,
1.0,
delta_fn,
);
assert_eq!(best, 1, "should stay at current community when all deltas < epsilon");
}
#[test]
fn test_find_best_community_respects_max_comm_size() {
let comm_size: Vec<f64> = vec![5.0, 1.0, 2.0];
let candidates = [0usize, 2].into_iter();
let delta_fn = |target: usize| -> f64 {
if target == 0 { 100.0 } else { 0.5 }
};
let (best, delta) = find_best_community(
candidates,
1,
1e-10,
5, &comm_size,
1.0, delta_fn,
);
assert_eq!(best, 2, "should skip community that exceeds max size");
assert!((delta - 0.5).abs() < 1e-10);
}
#[test]
fn test_apply_move_updates_partition_and_stats() {
let mut partition = Partition::new(3);
assert_eq!(partition.community_of(1), 1);
let mut total_degree: Vec<Vec<f64>> = vec![vec![2.0, 2.0, 2.0]];
let mut in_degree: Vec<Vec<f64>> = vec![vec![2.0, 2.0, 2.0]];
let mut size: Vec<f64> = vec![1.0, 1.0, 1.0];
apply_move(
MoveTarget::Partition(&mut partition),
1, 1, 0, NodeContribution {
k_v_out: &[2.0],
k_v_in: &[2.0],
weight: 1.0,
},
&mut CommunityStats {
total_degree_out: &mut total_degree,
in_degree_out: &mut in_degree,
size: &mut size,
},
);
assert_eq!(partition.community_of(1), 0);
assert!((total_degree[0][0] - 4.0).abs() < 1e-10, "comm 0 total_degree should be 4.0");
assert!((total_degree[0][1] - 0.0).abs() < 1e-10, "comm 1 total_degree should be 0.0");
assert!((in_degree[0][0] - 4.0).abs() < 1e-10, "comm 0 in_degree should be 4.0");
assert!((in_degree[0][1] - 0.0).abs() < 1e-10, "comm 1 in_degree should be 0.0");
assert!((size[0] - 2.0).abs() < 1e-10, "comm 0 size should be 2.0");
assert!((size[1] - 0.0).abs() < 1e-10, "comm 1 size should be 0.0");
}
#[test]
fn test_apply_move_refined_map() {
let mut refined_map: Vec<usize> = vec![0, 1, 2];
let mut total_degree: Vec<Vec<f64>> = vec![vec![2.0, 2.0, 2.0]];
let mut in_degree: Vec<Vec<f64>> = vec![vec![]]; let mut size: Vec<f64> = vec![1.0, 1.0, 1.0];
apply_move(
MoveTarget::RefinedMap(&mut refined_map),
2, 2, 0, NodeContribution {
k_v_out: &[2.0],
k_v_in: &[0.0],
weight: 1.0,
},
&mut CommunityStats {
total_degree_out: &mut total_degree,
in_degree_out: &mut in_degree,
size: &mut size,
},
);
assert_eq!(refined_map[2], 0, "refined map should update node 2 to community 0");
assert!((size[0] - 2.0).abs() < 1e-10);
assert!((size[2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_apply_move_multilayer() {
let mut partition = Partition::new(2);
let mut total_degree: Vec<Vec<f64>> = vec![vec![3.0, 5.0], vec![1.0, 2.0]];
let mut in_degree: Vec<Vec<f64>> = vec![vec![3.0, 5.0], vec![1.0, 2.0]];
let mut size: Vec<f64> = vec![1.0, 1.0];
apply_move(
MoveTarget::Partition(&mut partition),
1, 1, 0,
NodeContribution {
k_v_out: &[5.0, 2.0],
k_v_in: &[5.0, 2.0],
weight: 1.0,
},
&mut CommunityStats {
total_degree_out: &mut total_degree,
in_degree_out: &mut in_degree,
size: &mut size,
},
);
assert_eq!(partition.community_of(1), 0);
assert!((total_degree[0][0] - 8.0).abs() < 1e-10);
assert!((total_degree[0][1] - 0.0).abs() < 1e-10);
assert!((total_degree[1][0] - 3.0).abs() < 1e-10);
assert!((total_degree[1][1] - 0.0).abs() < 1e-10);
assert!((size[0] - 2.0).abs() < 1e-10);
assert!((size[1] - 0.0).abs() < 1e-10);
}
#[test]
fn test_build_aggregated_graph_two_communities() {
let mut b = GraphDataBuilder::new(4);
b.add_edge(0, 1, 1.0).unwrap();
b.add_edge(1, 2, 1.0).unwrap();
b.add_edge(2, 3, 1.0).unwrap();
let graph = b.build().unwrap();
let orig_to_agg: Vec<usize> = vec![0, 0, 1, 1];
let agg_n = 2;
let mut agg_edges_map: FxHashMap<(usize, usize), f64> = FxHashMap::default();
for node in 0..4 {
aggregate_node_edges_into(&graph, node, &orig_to_agg, false, &mut agg_edges_map);
}
let mut coarse_partition = Partition::new(4);
coarse_partition.move_node(0, 0);
coarse_partition.move_node(1, 0);
coarse_partition.move_node(2, 1);
coarse_partition.move_node(3, 1);
let (agg_data, returned_orig_to_agg, agg_initial) = build_aggregated_graph(
orig_to_agg,
agg_n,
false, agg_edges_map,
&coarse_partition,
|node| graph.node_weight(node),
).unwrap();
assert_eq!(agg_data.node_count(), 2, "aggregated graph should have 2 nodes");
assert!((agg_data.node_weight(0) - 2.0).abs() < 1e-10);
assert!((agg_data.node_weight(1) - 2.0).abs() < 1e-10);
let neighbors_0: Vec<(usize, f64)> = agg_data.neighbors(0).collect();
let neighbors_1: Vec<(usize, f64)> = agg_data.neighbors(1).collect();
assert_eq!(neighbors_0.len(), 2, "super-node 0 should have 2 neighbors (self + bridge)");
let has_self_0 = neighbors_0.iter().any(|(n, w)| *n == 0 && (*w - 1.0).abs() < 1e-10);
let has_bridge_from_0 = neighbors_0.iter().any(|(n, w)| *n == 1 && (*w - 1.0).abs() < 1e-10);
assert!(has_self_0, "super-node 0 should have self-loop weight 1.0");
assert!(has_bridge_from_0, "super-node 0 should have edge to super-node 1 weight 1.0");
assert_eq!(neighbors_1.len(), 2, "super-node 1 should have 2 neighbors");
let has_self_1 = neighbors_1.iter().any(|(n, w)| *n == 1 && (*w - 1.0).abs() < 1e-10);
assert!(has_self_1, "super-node 1 should have self-loop weight 1.0");
assert_eq!(returned_orig_to_agg, vec![0, 0, 1, 1]);
assert_eq!(agg_initial.community_of(0), 0);
assert_eq!(agg_initial.community_of(1), 1);
}
#[test]
fn test_build_aggregated_graph_single_community() {
let mut b = GraphDataBuilder::new(3);
b.add_edge(0, 1, 1.0).unwrap();
b.add_edge(1, 2, 1.0).unwrap();
let graph = b.build().unwrap();
let orig_to_agg: Vec<usize> = vec![0, 0, 0];
let agg_n = 1;
let mut agg_edges_map: FxHashMap<(usize, usize), f64> = FxHashMap::default();
for node in 0..3 {
aggregate_node_edges_into(&graph, node, &orig_to_agg, false, &mut agg_edges_map);
}
let coarse_partition = Partition::new(3);
let (agg_data, _, _) = build_aggregated_graph(
orig_to_agg,
agg_n,
false,
agg_edges_map,
&coarse_partition,
|node| graph.node_weight(node),
).unwrap();
assert_eq!(agg_data.node_count(), 1, "single community should produce 1 super-node");
assert!((agg_data.node_weight(0) - 3.0).abs() < 1e-10, "weight should be 3.0");
let neighbors: Vec<(usize, f64)> = agg_data.neighbors(0).collect();
assert_eq!(neighbors.len(), 1, "single super-node should only have self-loop");
assert_eq!(neighbors[0].0, 0, "neighbor should be self");
assert!((neighbors[0].1 - 2.0).abs() < 1e-10, "self-loop weight should be 2.0 (sum of edges)");
}
}