scirs2_graph/algorithms/community/
modularity.rs

1//! Modularity calculation and optimization algorithms
2
3use super::types::{CommunityResult, CommunityStructure};
4use crate::base::{EdgeWeight, Graph, IndexType, Node};
5use std::collections::{HashMap, HashSet};
6use std::hash::Hash;
7
8/// Computes the modularity of a given community partition
9///
10/// Modularity measures the quality of a partition by comparing the number
11/// of edges within communities to what would be expected in a random graph.
12///
13/// # Arguments
14/// * `graph` - The graph to analyze
15/// * `communities` - Map from nodes to community IDs
16///
17/// # Returns
18/// * The modularity score (typically between -1 and 1, higher is better)
19///
20/// # Time Complexity
21/// O(m + n) where m is the number of edges and n is the number of nodes.
22/// This is the optimized implementation that avoids the O(n²) naive approach.
23///
24/// # Space Complexity
25/// O(n) for storing degree information and community assignments.
26#[allow(dead_code)]
27pub fn modularity<N, E, Ix>(graph: &Graph<N, E, Ix>, communities: &HashMap<N, usize>) -> f64
28where
29    N: Node + std::fmt::Debug,
30    E: EdgeWeight + Into<f64> + Copy,
31    Ix: IndexType,
32{
33    let n = graph.node_count();
34    if n == 0 || communities.is_empty() {
35        return 0.0;
36    }
37
38    // Calculate total edge weight
39    let mut m = 0.0;
40    for edge in graph.inner().edge_references() {
41        m += (*edge.weight()).into();
42    }
43
44    if m == 0.0 {
45        return 0.0;
46    }
47
48    // Calculate node degrees
49    let mut node_degrees: HashMap<N, f64> = HashMap::new();
50    for node in graph.nodes() {
51        let mut degree = 0.0;
52        if let Ok(neighbors) = graph.neighbors(node) {
53            for neighbor in neighbors {
54                if let Ok(weight) = graph.edge_weight(node, &neighbor) {
55                    degree += weight.into();
56                }
57            }
58        }
59        node_degrees.insert(node.clone(), degree);
60    }
61
62    // Calculate modularity
63    let mut q = 0.0;
64    for node_i in graph.nodes() {
65        for node_j in graph.nodes() {
66            if communities.get(node_i) == communities.get(node_j) {
67                // Check if edge exists
68                let a_ij = if let Ok(weight) = graph.edge_weight(node_i, node_j) {
69                    weight.into()
70                } else {
71                    0.0
72                };
73
74                let k_i = node_degrees.get(node_i).unwrap_or(&0.0);
75                let k_j = node_degrees.get(node_j).unwrap_or(&0.0);
76
77                q += a_ij - (k_i * k_j) / (2.0 * m);
78            }
79        }
80    }
81
82    q / (2.0 * m)
83}
84
85/// Optimizes modularity using simulated annealing
86///
87/// This algorithm tries to maximize modularity by iteratively moving nodes
88/// between communities using simulated annealing to escape local optima.
89///
90/// # Arguments
91/// * `graph` - The graph to analyze
92/// * `initial_temp` - Initial temperature for simulated annealing
93/// * `cooling_rate` - Rate at which temperature decreases (0 < rate < 1)
94/// * `max_iterations` - Maximum number of iterations
95///
96/// # Returns
97/// * A community structure with optimized modularity
98///
99/// # Time Complexity
100/// O(k * n * m) where k is the number of iterations, n is the number of nodes,
101/// and m is the number of edges. Each iteration involves evaluating modularity
102/// changes for potential node moves.
103///
104/// # Space Complexity
105/// O(n) for storing community assignments and modularity calculations.
106#[deprecated(
107    note = "Use `modularity_optimization_result` instead for standardized community detection API"
108)]
109#[allow(dead_code)]
110pub fn modularity_optimization<N, E, Ix>(
111    graph: &Graph<N, E, Ix>,
112    initial_temp: f64,
113    cooling_rate: f64,
114    max_iterations: usize,
115) -> CommunityStructure<N>
116where
117    N: Node + Clone + Hash + Eq + std::fmt::Debug,
118    E: EdgeWeight + Into<f64> + Copy,
119    Ix: IndexType,
120{
121    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
122    let n = nodes.len();
123
124    if n == 0 {
125        return CommunityStructure {
126            node_communities: HashMap::new(),
127            modularity: 0.0,
128        };
129    }
130
131    // Initialize with each node in its own community
132    let mut current_communities: HashMap<N, usize> = nodes
133        .iter()
134        .enumerate()
135        .map(|(i, node)| (node.clone(), i))
136        .collect();
137
138    let mut current_modularity = modularity(graph, &current_communities);
139    let mut best_communities = current_communities.clone();
140    let mut best_modularity = current_modularity;
141
142    let mut temp = initial_temp;
143    let mut rng = scirs2_core::random::rng();
144
145    for _iteration in 0..max_iterations {
146        // Choose a random node to move
147        use scirs2_core::random::Rng;
148        let node_idx = rng.random_range(0..n);
149        let node = &nodes[node_idx];
150        let current_community = current_communities[node];
151
152        // Find possible communities to move to (neighboring communities + new community)
153        let mut candidate_communities = HashSet::new();
154        candidate_communities.insert(n); // New community
155
156        if let Ok(neighbors) = graph.neighbors(node) {
157            for neighbor in neighbors {
158                if let Some(&comm) = current_communities.get(&neighbor) {
159                    candidate_communities.insert(comm);
160                }
161            }
162        }
163
164        // Try moving to a random candidate community
165        let candidates: Vec<usize> = candidate_communities.into_iter().collect();
166        if candidates.is_empty() {
167            continue;
168        }
169
170        let new_community = candidates[rng.random_range(0..candidates.len())];
171
172        if new_community == current_community {
173            continue;
174        }
175
176        // Make the move temporarily
177        current_communities.insert(node.clone(), new_community);
178        let new_modularity = modularity(graph, &current_communities);
179        let delta = new_modularity - current_modularity;
180
181        // Accept or reject the move
182        let accept = if delta > 0.0 {
183            true
184        } else {
185            // Accept with probability exp(delta / temp)
186            let prob = (delta / temp).exp();
187            rng.random::<f64>() < prob
188        };
189
190        if accept {
191            current_modularity = new_modularity;
192            if current_modularity > best_modularity {
193                best_modularity = current_modularity;
194                best_communities = current_communities.clone();
195            }
196        } else {
197            // Revert the move
198            current_communities.insert(node.clone(), current_community);
199        }
200
201        // Cool down
202        temp *= cooling_rate;
203
204        // Early stopping if temperature is too low
205        if temp < 1e-8 {
206            break;
207        }
208    }
209
210    // Renumber communities to be consecutive
211    let mut community_map: HashMap<usize, usize> = HashMap::new();
212    let mut next_id = 0;
213    for &comm in best_communities.values() {
214        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
215            e.insert(next_id);
216            next_id += 1;
217        }
218    }
219
220    // Apply renumbering
221    for (_, comm) in best_communities.iter_mut() {
222        *comm = community_map[comm];
223    }
224
225    CommunityStructure {
226        node_communities: best_communities,
227        modularity: best_modularity,
228    }
229}
230
231/// Greedy modularity optimization algorithm
232///
233/// This is a simplified version of modularity optimization that uses a greedy
234/// approach without simulated annealing. It's faster but may get stuck in local optima.
235///
236/// # Arguments
237/// * `graph` - The graph to analyze
238/// * `max_iterations` - Maximum number of iterations
239///
240/// # Returns
241/// * A community structure with optimized modularity
242///
243/// # Time Complexity
244/// O(k * n * d) where k is the number of iterations, n is the number of nodes,
245/// and d is the average degree. Each iteration involves finding the best community
246/// for each node based on local modularity improvements.
247///
248/// # Space Complexity
249/// O(n) for storing community assignments and tracking modularity gains.
250#[deprecated(
251    note = "Use `greedy_modularity_optimization_result` instead for standardized community detection API"
252)]
253#[allow(dead_code)]
254pub fn greedy_modularity_optimization<N, E, Ix>(
255    graph: &Graph<N, E, Ix>,
256    max_iterations: usize,
257) -> CommunityStructure<N>
258where
259    N: Node + Clone + Hash + Eq + std::fmt::Debug,
260    E: EdgeWeight + Into<f64> + Copy,
261    Ix: IndexType,
262{
263    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
264    let n = nodes.len();
265
266    if n == 0 {
267        return CommunityStructure {
268            node_communities: HashMap::new(),
269            modularity: 0.0,
270        };
271    }
272
273    // Initialize with each node in its own community
274    let mut communities: HashMap<N, usize> = nodes
275        .iter()
276        .enumerate()
277        .map(|(i, node)| (node.clone(), i))
278        .collect();
279
280    let mut improved = true;
281    let mut _iterations = 0;
282
283    while improved && _iterations < max_iterations {
284        improved = false;
285        _iterations += 1;
286
287        let current_modularity = modularity(graph, &communities);
288
289        // Try moving each node to each neighboring community
290        for node in &nodes {
291            let original_community = communities[node];
292            let mut best_modularity = current_modularity;
293            let mut best_community = original_community;
294
295            // Get neighboring communities
296            let mut neighboring_communities = HashSet::new();
297            if let Ok(neighbors) = graph.neighbors(node) {
298                for neighbor in neighbors {
299                    if let Some(&comm) = communities.get(&neighbor) {
300                        neighboring_communities.insert(comm);
301                    }
302                }
303            }
304
305            // Try each neighboring community
306            for &candidate_community in &neighboring_communities {
307                if candidate_community != original_community {
308                    communities.insert(node.clone(), candidate_community);
309                    let new_modularity = modularity(graph, &communities);
310
311                    if new_modularity > best_modularity {
312                        best_modularity = new_modularity;
313                        best_community = candidate_community;
314                    }
315                }
316            }
317
318            // Move to best community if it's better
319            if best_community != original_community {
320                communities.insert(node.clone(), best_community);
321                improved = true;
322            } else {
323                // Restore original community
324                communities.insert(node.clone(), original_community);
325            }
326        }
327    }
328
329    // Renumber communities to be consecutive
330    let mut community_map: HashMap<usize, usize> = HashMap::new();
331    let mut next_id = 0;
332    for &comm in communities.values() {
333        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
334            e.insert(next_id);
335            next_id += 1;
336        }
337    }
338
339    // Apply renumbering
340    for (_, comm) in communities.iter_mut() {
341        *comm = community_map[comm];
342    }
343
344    let final_modularity = modularity(graph, &communities);
345
346    CommunityStructure {
347        node_communities: communities,
348        modularity: final_modularity,
349    }
350}
351
352/// Optimizes modularity using simulated annealing (modern API)
353///
354/// Returns a standardized `CommunityResult` type that provides multiple ways
355/// to access the community structure.
356///
357/// # Arguments
358/// * `graph` - The graph to analyze
359/// * `initial_temp` - Initial temperature for simulated annealing
360/// * `cooling_rate` - Rate at which temperature decreases (0 < rate < 1)
361/// * `max_iterations` - Maximum number of iterations
362///
363/// # Returns
364/// * A `CommunityResult` with comprehensive community information
365#[allow(dead_code)]
366pub fn modularity_optimization_result<N, E, Ix>(
367    graph: &Graph<N, E, Ix>,
368    initial_temp: f64,
369    cooling_rate: f64,
370    max_iterations: usize,
371) -> CommunityResult<N>
372where
373    N: Node + Clone + Hash + Eq + std::fmt::Debug,
374    E: EdgeWeight + Into<f64> + Copy,
375    Ix: IndexType,
376{
377    #[allow(deprecated)]
378    let structure = modularity_optimization(graph, initial_temp, cooling_rate, max_iterations);
379    CommunityResult::from_community_structure(structure)
380}
381
382/// Greedy modularity optimization algorithm (modern API)
383///
384/// Returns a standardized `CommunityResult` type that provides multiple ways
385/// to access the community structure.
386///
387/// # Arguments
388/// * `graph` - The graph to analyze
389/// * `max_iterations` - Maximum number of iterations
390///
391/// # Returns
392/// * A `CommunityResult` with comprehensive community information
393#[allow(dead_code)]
394pub fn greedy_modularity_optimization_result<N, E, Ix>(
395    graph: &Graph<N, E, Ix>,
396    max_iterations: usize,
397) -> CommunityResult<N>
398where
399    N: Node + Clone + Hash + Eq + std::fmt::Debug,
400    E: EdgeWeight + Into<f64> + Copy,
401    Ix: IndexType,
402{
403    #[allow(deprecated)]
404    let structure = greedy_modularity_optimization(graph, max_iterations);
405    CommunityResult::from_community_structure(structure)
406}