use std::collections::BTreeMap;
use rand::rngs::StdRng;
use rand::seq::SliceRandom as _;
use super::graph::LeidenGraph;
use crate::partition::QualityFunction;
const MAX_REFINE_ITER: usize = 10;
#[derive(Debug, Clone)]
pub struct RefinementState {
pub assignment: Vec<usize>,
pub sigma_tot: Vec<f64>,
pub inner_edges: Vec<f64>,
pub size: Vec<usize>,
}
impl RefinementState {
pub fn from_partition(graph: &LeidenGraph, partition: &[usize], capacity: usize) -> Self {
let n = graph.n;
debug_assert_eq!(partition.len(), n);
let max_id = partition.iter().copied().max().map(|m| m + 1).unwrap_or(0);
let cap = capacity.max(max_id);
let mut sigma_tot = vec![0.0f64; cap];
let mut inner_edges = vec![0.0f64; cap];
let mut size = vec![0usize; cap];
for (i, &c) in partition.iter().enumerate().take(n) {
sigma_tot[c] += graph.degree[i];
size[c] += 1;
}
for i in 0..n {
let c_i = partition[i];
inner_edges[c_i] += graph.self_loops[i];
for (idx, &j) in graph.adj[i].iter().enumerate() {
if partition[j] == c_i && j > i {
inner_edges[c_i] += graph.edge_weights[i][idx];
}
}
}
RefinementState {
assignment: partition.to_vec(),
sigma_tot,
inner_edges,
size,
}
}
pub fn apply_move(&mut self, graph: &LeidenGraph, node: usize, from: usize, to: usize) {
for (idx, &nbr) in graph.adj[node].iter().enumerate() {
let w = graph.edge_weights[node][idx];
let nbr_comm = self.assignment[nbr];
if nbr_comm == from {
self.inner_edges[from] -= w;
} else if nbr_comm == to {
self.inner_edges[to] += w;
}
}
self.inner_edges[from] -= graph.self_loops[node];
self.inner_edges[to] += graph.self_loops[node];
self.sigma_tot[from] -= graph.degree[node];
self.sigma_tot[to] += graph.degree[node];
self.size[from] -= 1;
self.size[to] += 1;
self.assignment[node] = to;
}
pub fn move_gain(&self, graph: &LeidenGraph, node: usize, to: usize, k_in_to: f64) -> f64 {
let sigma_to = self.sigma_tot[to];
let m2 = 2.0 * graph.total_weight;
if m2 == 0.0 {
return 0.0;
}
k_in_to - graph.degree[node] * sigma_to / m2
}
}
pub fn well_connected(k_in_to: f64, size_candidate: usize, size_s: usize, gamma: f64) -> bool {
if gamma == 0.0 || size_s == 0 {
return true;
}
let sc = size_candidate as f64;
let ss = size_s as f64;
let threshold = gamma * (sc - sc * sc / ss);
k_in_to >= threshold
}
#[doc(hidden)]
pub fn refine_partition(
graph: &LeidenGraph,
partition: &[usize],
rng: &mut StdRng,
quality: &QualityFunction,
gamma: f64,
) -> Vec<usize> {
if graph.n == 0 {
return vec![];
}
let max_comm = partition.iter().copied().max().unwrap_or(0) + 1;
let mut community_members: Vec<Vec<usize>> = vec![vec![]; max_comm];
for (node, &comm) in partition.iter().enumerate() {
community_members[comm].push(node);
}
let mut refined: Vec<usize> = (0..graph.n).collect();
for members in &community_members {
if members.len() <= 1 {
continue;
}
refine_community(graph, members, &mut refined, rng, quality, gamma);
}
renumber_in_place(&mut refined);
refined
}
fn refine_community(
graph: &LeidenGraph,
members: &[usize],
refined: &mut [usize],
rng: &mut StdRng,
quality: &QualityFunction,
gamma: f64,
) {
let (sub, local_to_global) = graph.induced_subgraph(members);
let local_n = sub.n;
let size_s = local_n;
let singleton: Vec<usize> = (0..local_n).collect();
let mut state = RefinementState::from_partition(&sub, &singleton, local_n);
let mut order: Vec<usize> = (0..local_n).collect();
order.shuffle(rng);
for _ in 0..MAX_REFINE_ITER {
let mut moved = false;
for &node in &order {
let current_comm = state.assignment[node];
let mut k_in_per_comm: BTreeMap<usize, f64> = BTreeMap::new();
for (idx, &nbr) in sub.adj[node].iter().enumerate() {
let nbr_comm = state.assignment[nbr];
if nbr_comm == current_comm {
continue;
}
*k_in_per_comm.entry(nbr_comm).or_insert(0.0) += sub.edge_weights[node][idx];
}
if k_in_per_comm.is_empty() {
continue;
}
let mut best_comm = current_comm;
let mut best_gain = 0.0f64;
for (&comm, &k_in_to) in &k_in_per_comm {
let size_candidate = state.size[comm];
if !well_connected(k_in_to, size_candidate, size_s, gamma) {
continue;
}
let gain = match quality {
QualityFunction::Modularity => state.move_gain(&sub, node, comm, k_in_to),
QualityFunction::Cpm { gamma: _ } => k_in_to - gamma * state.size[comm] as f64,
};
if gain > best_gain {
best_gain = gain;
best_comm = comm;
}
}
if best_gain > 1e-10 {
state.apply_move(&sub, node, current_comm, best_comm);
moved = true;
}
}
if !moved {
break;
}
}
let mut sc_to_global: BTreeMap<usize, usize> = BTreeMap::new();
for (local, &sc) in state.assignment.iter().enumerate() {
let global = local_to_global[local];
let entry = sc_to_global.entry(sc).or_insert(global);
if global < *entry {
*entry = global;
}
}
for (local, &sc) in state.assignment.iter().enumerate() {
refined[local_to_global[local]] = sc_to_global[&sc];
}
}
fn renumber_in_place(assignment: &mut [usize]) {
let mut map = BTreeMap::new();
let mut next = 0usize;
for comm in assignment.iter_mut() {
let entry = map.entry(*comm).or_insert_with(|| {
let c = next;
next += 1;
c
});
*comm = *entry;
}
}