impl ParallelLouvain {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self::default()
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_resolution(mut self, resolution: f64) -> Self {
self.resolution = resolution;
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_min_improvement(mut self, min_improvement: f64) -> Self {
self.min_improvement = min_improvement;
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_num_threads(mut self, num_threads: usize) -> Self {
self.num_threads = num_threads;
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn detect(&self, graph: &UndirectedGraph) -> Vec<usize> {
let n = graph.node_count();
if n == 0 {
return Vec::new();
}
let graph_data = GraphData::from_graph(graph);
let mut communities: Vec<usize> = (0..n).collect();
let mut best_modularity = self.calculate_modularity_internal(&graph_data, &communities);
for _iteration in 0..self.max_iterations {
let improved = self.local_moving_phase(&graph_data, &mut communities);
if !improved {
break;
}
let new_modularity = self.calculate_modularity_internal(&graph_data, &communities);
if new_modularity - best_modularity < self.min_improvement {
break;
}
best_modularity = new_modularity;
}
self.renumber_communities(&mut communities);
communities
}
fn local_moving_phase(&self, graph_data: &GraphData, communities: &mut [usize]) -> bool {
let n = communities.len();
let improved = AtomicBool::new(false);
let community_data = CommunityData::new(communities, graph_data);
let best_moves: Vec<Option<(usize, usize)>> = (0..n)
.into_par_iter()
.map(|node| self.find_best_move(node, communities[node], graph_data, &community_data))
.collect();
for (node, best_move) in best_moves.into_iter().enumerate() {
if let Some((_old_comm, new_comm)) = best_move {
if communities[node] != new_comm {
communities[node] = new_comm;
improved.store(true, Ordering::Relaxed);
}
}
}
improved.load(Ordering::Relaxed)
}
fn find_best_move(
&self,
node: usize,
current_community: usize,
graph_data: &GraphData,
community_data: &CommunityData,
) -> Option<(usize, usize)> {
let node_degree = graph_data.degrees[node];
let total_weight = graph_data.total_weight;
if total_weight == 0.0 {
return None;
}
let current_gain = self.modularity_gain(
node,
current_community,
graph_data,
community_data,
node_degree,
total_weight,
);
let mut best_community = current_community;
let mut best_gain = current_gain;
let neighbor_communities = self.get_neighbor_communities(node, graph_data);
for &neighbor_comm in &neighbor_communities {
if neighbor_comm == current_community {
continue;
}
let gain = self.modularity_gain(
node,
neighbor_comm,
graph_data,
community_data,
node_degree,
total_weight,
);
if gain > best_gain {
best_gain = gain;
best_community = neighbor_comm;
}
}
if best_community != current_community {
Some((current_community, best_community))
} else {
None
}
}
fn modularity_gain(
&self,
node: usize,
target_community: usize,
graph_data: &GraphData,
community_data: &CommunityData,
node_degree: f64,
total_weight: f64,
) -> f64 {
let ki_in = graph_data.neighbor_weight_to_community(
node,
target_community,
&community_data.node_to_community,
);
let sigma_tot = community_data
.community_degrees
.get(&target_community)
.copied()
.unwrap_or(0.0);
ki_in - self.resolution * (sigma_tot * node_degree) / (2.0 * total_weight)
}
fn get_neighbor_communities(&self, node: usize, graph_data: &GraphData) -> Vec<usize> {
graph_data.neighbors[node]
.iter()
.map(|(neighbor, _)| graph_data.degrees[*neighbor] as usize % graph_data.n) .collect()
}
fn calculate_modularity_internal(&self, graph_data: &GraphData, communities: &[usize]) -> f64 {
if graph_data.total_weight == 0.0 {
return 0.0;
}
let m2 = 2.0 * graph_data.total_weight;
let mut q = 0.0;
let mut community_degrees: HashMap<usize, f64> = HashMap::new();
for (node, &community) in communities.iter().enumerate() {
*community_degrees.entry(community).or_insert(0.0) += graph_data.degrees[node];
}
for (&(source, target), &weight) in &graph_data.edge_weights {
if communities[source] == communities[target] {
q += weight;
}
}
for degree in community_degrees.values() {
q -= self.resolution * degree * degree / m2;
}
q / m2
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn calculate_modularity(&self, graph: &UndirectedGraph, communities: &[usize]) -> f64 {
let graph_data = GraphData::from_graph(graph);
self.calculate_modularity_internal(&graph_data, communities)
}
fn renumber_communities(&self, communities: &mut [usize]) {
let mut mapping: HashMap<usize, usize> = HashMap::new();
let mut next_id = 0;
for community in communities.iter_mut() {
let new_id = *mapping.entry(*community).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
*community = new_id;
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn num_communities(communities: &[usize]) -> usize {
let unique: std::collections::HashSet<_> = communities.iter().collect();
unique.len()
}
}