use crate::types::{CommunityResult, CsrGraph};
use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ModularityScore {
metadata: KernelMetadata,
}
impl ModularityScore {
#[must_use]
pub fn new() -> Self {
Self {
metadata: KernelMetadata::batch("graph/modularity-score", Domain::GraphAnalytics)
.with_description("Modularity score Q calculation")
.with_throughput(100_000)
.with_latency_us(10.0),
}
}
pub fn compute(graph: &CsrGraph, communities: &[u64]) -> f64 {
let n = graph.num_nodes;
if n == 0 || graph.num_edges == 0 {
return 0.0;
}
let m = graph.num_edges as f64;
let two_m = 2.0 * m;
let degrees: Vec<f64> = (0..n).map(|i| graph.out_degree(i as u64) as f64).collect();
let mut q = 0.0;
for i in 0..n {
let ci = communities[i];
let ki = degrees[i];
for &j in graph.neighbors(i as u64) {
let j = j as usize;
let cj = communities[j];
if ci == cj {
let kj = degrees[j];
q += 1.0 - (ki * kj) / two_m;
}
}
}
q / two_m
}
pub fn delta_modularity(
graph: &CsrGraph,
node: usize,
old_community: u64,
new_community: u64,
communities: &[u64],
community_degrees: &HashMap<u64, f64>,
m: f64,
) -> f64 {
if old_community == new_community {
return 0.0;
}
let ki = graph.out_degree(node as u64) as f64;
let two_m = 2.0 * m;
let mut edges_to_old = 0.0;
let mut edges_to_new = 0.0;
for &neighbor in graph.neighbors(node as u64) {
let nc = communities[neighbor as usize];
if nc == old_community {
edges_to_old += 1.0;
} else if nc == new_community {
edges_to_new += 1.0;
}
}
let sigma_old = community_degrees
.get(&old_community)
.copied()
.unwrap_or(0.0);
let sigma_new = community_degrees
.get(&new_community)
.copied()
.unwrap_or(0.0);
let delta = (edges_to_new - sigma_new * ki / two_m)
- (edges_to_old - (sigma_old - ki) * ki / two_m);
delta / m
}
}
impl Default for ModularityScore {
fn default() -> Self {
Self::new()
}
}
impl GpuKernel for ModularityScore {
fn metadata(&self) -> &KernelMetadata {
&self.metadata
}
}
#[derive(Debug, Clone)]
pub struct LouvainCommunity {
metadata: KernelMetadata,
}
impl LouvainCommunity {
#[must_use]
pub fn new() -> Self {
Self {
metadata: KernelMetadata::batch("graph/louvain-community", Domain::GraphAnalytics)
.with_description("Louvain community detection (modularity optimization)")
.with_throughput(10_000)
.with_latency_us(100.0),
}
}
pub fn compute(
graph: &CsrGraph,
max_iterations: u32,
min_modularity_gain: f64,
) -> CommunityResult {
let n = graph.num_nodes;
if n == 0 {
return CommunityResult {
assignments: Vec::new(),
num_communities: 0,
modularity: 0.0,
};
}
let mut communities: Vec<u64> = (0..n).map(|i| i as u64).collect();
let m = graph.num_edges as f64;
if m == 0.0 {
return CommunityResult {
assignments: communities,
num_communities: n,
modularity: 0.0,
};
}
let mut community_degrees: HashMap<u64, f64> = HashMap::new();
for i in 0..n {
let degree = graph.out_degree(i as u64) as f64;
*community_degrees.entry(i as u64).or_insert(0.0) += degree;
}
let mut community_internal_edges: HashMap<u64, f64> = HashMap::new();
let mut improved = true;
let mut iteration = 0;
while improved && iteration < max_iterations {
improved = false;
iteration += 1;
let mut total_gain = 0.0;
for node in 0..n {
let current_community = communities[node];
let node_degree = graph.out_degree(node as u64) as f64;
let mut neighbor_communities: HashMap<u64, f64> = HashMap::new();
for &neighbor in graph.neighbors(node as u64) {
let nc = communities[neighbor as usize];
*neighbor_communities.entry(nc).or_insert(0.0) += 1.0;
}
let mut best_community = current_community;
let mut best_gain = 0.0;
for (&comm, &edges_to_comm) in &neighbor_communities {
if comm == current_community {
continue;
}
let sigma_comm = community_degrees.get(&comm).copied().unwrap_or(0.0);
let sigma_current = community_degrees
.get(¤t_community)
.copied()
.unwrap_or(0.0);
let edges_to_current = neighbor_communities
.get(¤t_community)
.copied()
.unwrap_or(0.0);
let gain = (edges_to_comm - edges_to_current)
- node_degree * (sigma_comm - sigma_current + node_degree) / (2.0 * m);
if gain > best_gain {
best_gain = gain;
best_community = comm;
}
}
if best_gain > min_modularity_gain {
if let Some(d) = community_degrees.get_mut(¤t_community) {
*d -= node_degree;
}
*community_degrees.entry(best_community).or_insert(0.0) += node_degree;
let edges_to_current = neighbor_communities
.get(¤t_community)
.copied()
.unwrap_or(0.0);
if let Some(e) = community_internal_edges.get_mut(¤t_community) {
*e -= edges_to_current;
}
let edges_to_best = neighbor_communities
.get(&best_community)
.copied()
.unwrap_or(0.0);
*community_internal_edges
.entry(best_community)
.or_insert(0.0) += edges_to_best;
communities[node] = best_community;
improved = true;
total_gain += best_gain;
}
}
if total_gain < min_modularity_gain {
break;
}
}
let mut community_map: HashMap<u64, u64> = HashMap::new();
let mut next_id = 0u64;
for c in &mut communities {
let new_id = *community_map.entry(*c).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
*c = new_id;
}
let modularity = ModularityScore::compute(graph, &communities);
CommunityResult {
assignments: communities,
num_communities: next_id as usize,
modularity,
}
}
}
impl Default for LouvainCommunity {
fn default() -> Self {
Self::new()
}
}
impl GpuKernel for LouvainCommunity {
fn metadata(&self) -> &KernelMetadata {
&self.metadata
}
}
#[derive(Debug, Clone)]
pub struct LabelPropagation {
metadata: KernelMetadata,
}
impl LabelPropagation {
#[must_use]
pub fn new() -> Self {
Self {
metadata: KernelMetadata::batch("graph/label-propagation", Domain::GraphAnalytics)
.with_description("Label propagation community detection")
.with_throughput(100_000)
.with_latency_us(10.0),
}
}
pub fn compute(graph: &CsrGraph, max_iterations: u32) -> CommunityResult {
let n = graph.num_nodes;
if n == 0 {
return CommunityResult {
assignments: Vec::new(),
num_communities: 0,
modularity: 0.0,
};
}
let mut labels: Vec<u64> = (0..n).map(|i| i as u64).collect();
for _ in 0..max_iterations {
let mut changed = false;
for node in 0..n {
let mut label_counts: HashMap<u64, usize> = HashMap::new();
for &neighbor in graph.neighbors(node as u64) {
*label_counts.entry(labels[neighbor as usize]).or_insert(0) += 1;
}
if let Some((&best_label, _)) = label_counts.iter().max_by_key(|(_, count)| *count)
{
if labels[node] != best_label {
labels[node] = best_label;
changed = true;
}
}
}
if !changed {
break;
}
}
let mut label_map: HashMap<u64, u64> = HashMap::new();
let mut next_id = 0u64;
for label in &mut labels {
let new_id = *label_map.entry(*label).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
*label = new_id;
}
let modularity = ModularityScore::compute(graph, &labels);
CommunityResult {
assignments: labels,
num_communities: next_id as usize,
modularity,
}
}
}
impl Default for LabelPropagation {
fn default() -> Self {
Self::new()
}
}
impl GpuKernel for LabelPropagation {
fn metadata(&self) -> &KernelMetadata {
&self.metadata
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_two_cliques() -> CsrGraph {
CsrGraph::from_edges(
6,
&[
(0, 1),
(1, 0),
(0, 2),
(2, 0),
(1, 2),
(2, 1),
(3, 4),
(4, 3),
(3, 5),
(5, 3),
(4, 5),
(5, 4),
(2, 3),
(3, 2),
],
)
}
#[test]
fn test_modularity_score_metadata() {
let kernel = ModularityScore::new();
assert_eq!(kernel.metadata().id, "graph/modularity-score");
assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
}
#[test]
fn test_modularity_perfect_partition() {
let graph = create_two_cliques();
let communities = vec![0, 0, 0, 1, 1, 1];
let q = ModularityScore::compute(&graph, &communities);
assert!(q > 0.0, "Expected positive modularity, got {}", q);
}
#[test]
fn test_modularity_single_community() {
let graph = create_two_cliques();
let communities = vec![0, 0, 0, 0, 0, 0];
let q_single = ModularityScore::compute(&graph, &communities);
let communities_perfect = vec![0, 0, 0, 1, 1, 1];
let q_perfect = ModularityScore::compute(&graph, &communities_perfect);
assert!(
q_single.is_finite(),
"Single community modularity should be finite"
);
assert!(
q_perfect.is_finite(),
"Perfect partition modularity should be finite"
);
assert!(
q_perfect > 0.0,
"Perfect partition should have positive modularity"
);
}
#[test]
fn test_louvain_finds_communities() {
let graph = create_two_cliques();
let result = LouvainCommunity::compute(&graph, 100, 1e-6);
assert_eq!(
result.num_communities, 2,
"Expected 2 communities, got {}",
result.num_communities
);
assert_eq!(result.assignments[0], result.assignments[1]);
assert_eq!(result.assignments[1], result.assignments[2]);
assert_eq!(result.assignments[3], result.assignments[4]);
assert_eq!(result.assignments[4], result.assignments[5]);
assert_ne!(result.assignments[0], result.assignments[3]);
assert!(result.modularity > 0.0);
}
#[test]
fn test_label_propagation_finds_communities() {
let graph = create_two_cliques();
let result = LabelPropagation::compute(&graph, 100);
assert!(
result.num_communities >= 1 && result.num_communities <= 2,
"Expected 1-2 communities, got {}",
result.num_communities
);
assert_eq!(result.assignments[0], result.assignments[1]);
assert_eq!(result.assignments[1], result.assignments[2]);
assert_eq!(result.assignments[3], result.assignments[4]);
assert_eq!(result.assignments[4], result.assignments[5]);
assert!(result.modularity >= 0.0);
}
}