Skip to main content

rust_igraph/algorithms/community/
modularity_matrix.rs

1//! Modularity matrix (ALGO-CO-007).
2//!
3//! Counterpart of `igraph_modularity_matrix()` from
4//! `references/igraph/src/community/modularity.c:323`.
5//!
6//! Produces the dense modularity matrix **B** where:
7//! - Undirected: `B_ij = A_ij − γ k_i k_j / (2m)`
8//! - Directed:   `B_ij = A_ij − γ k^out_i k^in_j / m`
9//!
10//! Self-loops in undirected graphs contribute 2 to `A_ii` (matches
11//! upstream). When weights are given, `A` becomes the weighted
12//! adjacency and `k` becomes the strength.
13
14use crate::core::{Graph, IgraphError, IgraphResult};
15
16/// Compute the modularity matrix **B** of a graph.
17///
18/// Returns a dense `n×n` matrix (row-major `Vec<Vec<f64>>`).
19///
20/// - `weights`: optional edge weights (length must equal `ecount()`).
21/// - `resolution`: the γ parameter (≥ 0; default 1.0).
22/// - `directed`: for directed graphs, whether to use directed formula.
23///   Ignored for undirected graphs.
24///
25/// Returns an empty matrix for graphs with no vertices. Returns an error
26/// when the total edge weight is zero (the matrix is undefined in that
27/// case, matching upstream's documentation).
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, modularity_matrix};
33///
34/// // Triangle (K3): A is all-1 off-diagonal (undirected stores both
35/// // directions). Each vertex has degree 2, m = 3, so
36/// // B_ij = A_ij - 2*2/(2*3) = 1 - 2/3 = 1/3 for i≠j,
37/// // B_ii = 0 - 4/6 = -2/3.
38/// let mut g = Graph::with_vertices(3);
39/// g.add_edge(0, 1).unwrap();
40/// g.add_edge(1, 2).unwrap();
41/// g.add_edge(2, 0).unwrap();
42/// let b = modularity_matrix(&g, None, 1.0, true).unwrap();
43/// assert!((b[0][0] - (-2.0/3.0)).abs() < 1e-10);
44/// assert!((b[0][1] - 1.0/3.0).abs() < 1e-10);
45/// ```
46pub fn modularity_matrix(
47    graph: &Graph,
48    weights: Option<&[f64]>,
49    resolution: f64,
50    directed: bool,
51) -> IgraphResult<Vec<Vec<f64>>> {
52    let n = graph.vcount();
53    let ecount = graph.ecount();
54
55    if !resolution.is_finite() || resolution < 0.0 {
56        return Err(IgraphError::InvalidArgument(
57            "resolution parameter must be non-negative and finite".to_string(),
58        ));
59    }
60    if let Some(w) = weights {
61        if w.len() != ecount {
62            return Err(IgraphError::InvalidArgument(format!(
63                "modularity_matrix: weights length ({}) does not match edge count ({ecount})",
64                w.len()
65            )));
66        }
67    }
68
69    if n == 0 {
70        return Ok(Vec::new());
71    }
72
73    let n_usize = n as usize;
74    let directed = directed && graph.is_directed();
75
76    // Build adjacency matrix.
77    let mut matrix = vec![vec![0.0_f64; n_usize]; n_usize];
78
79    let m_u32 =
80        u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
81
82    for eid in 0..m_u32 {
83        let (from, to) = graph.edge(eid)?;
84        let f = from as usize;
85        let t = to as usize;
86        let w = weights.map_or(1.0, |ws| ws[eid as usize]);
87        matrix[f][t] += w;
88        if !directed {
89            matrix[t][f] += w;
90        }
91    }
92
93    // Compute degree/strength vectors.
94    #[allow(clippy::cast_precision_loss)]
95    let sw: f64 = weights.map_or(ecount as f64, |ws| ws.iter().sum());
96    if sw == 0.0 {
97        return Err(IgraphError::InvalidArgument(
98            "modularity_matrix: total edge weight is zero; matrix is undefined".to_string(),
99        ));
100    }
101
102    if directed {
103        // Directed: B_ij = A_ij - γ * k_out_i * k_in_j / m
104        let mut k_out = vec![0.0_f64; n_usize];
105        let mut k_in = vec![0.0_f64; n_usize];
106
107        for eid in 0..m_u32 {
108            let (from, to) = graph.edge(eid)?;
109            let w = weights.map_or(1.0, |ws| ws[eid as usize]);
110            k_out[from as usize] += w;
111            k_in[to as usize] += w;
112        }
113
114        let scaling = resolution / sw;
115        for (j, &k_in_j) in k_in.iter().enumerate().take(n_usize) {
116            for i in 0..n_usize {
117                matrix[i][j] -= k_out[i] * k_in_j * scaling;
118            }
119        }
120    } else {
121        // Undirected: B_ij = A_ij - γ * k_i * k_j / (2m)
122        let mut deg = vec![0.0_f64; n_usize];
123
124        for eid in 0..m_u32 {
125            let (from, to) = graph.edge(eid)?;
126            let w = weights.map_or(1.0, |ws| ws[eid as usize]);
127            deg[from as usize] += w;
128            deg[to as usize] += w;
129        }
130
131        let scaling = resolution / (2.0 * sw);
132        for j in 0..n_usize {
133            for i in 0..n_usize {
134                matrix[i][j] -= deg[i] * deg[j] * scaling;
135            }
136        }
137    }
138
139    Ok(matrix)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn close(a: f64, b: f64) -> bool {
147        (a - b).abs() < 1e-10
148    }
149
150    #[test]
151    fn empty_graph() {
152        let g = Graph::with_vertices(0);
153        let m = modularity_matrix(&g, None, 1.0, false).unwrap();
154        assert!(m.is_empty());
155    }
156
157    #[test]
158    fn single_vertex_no_edges_error() {
159        let g = Graph::with_vertices(1);
160        // No edges → sw = 0, undefined.
161        assert!(modularity_matrix(&g, None, 1.0, false).is_err());
162    }
163
164    #[test]
165    fn triangle_undirected() {
166        let mut g = Graph::with_vertices(3);
167        g.add_edge(0, 1).unwrap();
168        g.add_edge(1, 2).unwrap();
169        g.add_edge(2, 0).unwrap();
170        let b = modularity_matrix(&g, None, 1.0, false).unwrap();
171        // m = 3, k_i = 2 for all. B_ii = 0 - 4/6 = -2/3. B_ij = 1 - 4/6 = 1/3.
172        for (i, row) in b.iter().enumerate() {
173            assert!(close(row[i], -2.0 / 3.0));
174            for (j, &val) in row.iter().enumerate() {
175                if i != j {
176                    assert!(close(val, 1.0 / 3.0));
177                }
178            }
179        }
180    }
181
182    #[test]
183    fn path_3_undirected() {
184        // 0-1-2: deg = [1, 2, 1], m = 2
185        let mut g = Graph::with_vertices(3);
186        g.add_edge(0, 1).unwrap();
187        g.add_edge(1, 2).unwrap();
188        let b = modularity_matrix(&g, None, 1.0, false).unwrap();
189        // B[0][0] = 0 - 1*1/4 = -0.25
190        // B[0][1] = 1 - 1*2/4 = 0.5
191        // B[0][2] = 0 - 1*1/4 = -0.25
192        // B[1][1] = 0 - 2*2/4 = -1.0
193        // B[1][2] = 1 - 2*1/4 = 0.5
194        // B[2][2] = 0 - 1*1/4 = -0.25
195        assert!(close(b[0][0], -0.25));
196        assert!(close(b[0][1], 0.5));
197        assert!(close(b[0][2], -0.25));
198        assert!(close(b[1][0], 0.5));
199        assert!(close(b[1][1], -1.0));
200        assert!(close(b[1][2], 0.5));
201        assert!(close(b[2][0], -0.25));
202        assert!(close(b[2][1], 0.5));
203        assert!(close(b[2][2], -0.25));
204    }
205
206    #[test]
207    fn directed_simple() {
208        // 0→1→2, directed. m = 2.
209        // A = [[0,1,0],[0,0,1],[0,0,0]]
210        // k_out = [1, 1, 0], k_in = [0, 1, 1]
211        // B_ij = A_ij - k_out_i * k_in_j / 2
212        let mut g = Graph::new(3, true).unwrap();
213        g.add_edge(0, 1).unwrap();
214        g.add_edge(1, 2).unwrap();
215        let b = modularity_matrix(&g, None, 1.0, true).unwrap();
216        // B[0][0] = 0 - 1*0/2 = 0
217        // B[0][1] = 1 - 1*1/2 = 0.5
218        // B[0][2] = 0 - 1*1/2 = -0.5
219        // B[1][0] = 0 - 1*0/2 = 0
220        // B[1][1] = 0 - 1*1/2 = -0.5
221        // B[1][2] = 1 - 1*1/2 = 0.5
222        // B[2][0] = 0 - 0*0/2 = 0
223        // B[2][1] = 0 - 0*1/2 = 0
224        // B[2][2] = 0 - 0*1/2 = 0
225        assert!(close(b[0][0], 0.0));
226        assert!(close(b[0][1], 0.5));
227        assert!(close(b[0][2], -0.5));
228        assert!(close(b[1][0], 0.0));
229        assert!(close(b[1][1], -0.5));
230        assert!(close(b[1][2], 0.5));
231        assert!(close(b[2][0], 0.0));
232        assert!(close(b[2][1], 0.0));
233        assert!(close(b[2][2], 0.0));
234    }
235
236    #[test]
237    fn directed_flag_ignored_for_undirected_graph() {
238        let mut g = Graph::with_vertices(3);
239        g.add_edge(0, 1).unwrap();
240        g.add_edge(1, 2).unwrap();
241        g.add_edge(2, 0).unwrap();
242        let b1 = modularity_matrix(&g, None, 1.0, true).unwrap();
243        let b2 = modularity_matrix(&g, None, 1.0, false).unwrap();
244        for (row1, row2) in b1.iter().zip(b2.iter()) {
245            for (&v1, &v2) in row1.iter().zip(row2.iter()) {
246                assert!(close(v1, v2));
247            }
248        }
249    }
250
251    #[test]
252    fn weighted_undirected() {
253        // 0-1 (w=3), 1-2 (w=1). sw = 4.
254        // deg = [3, 4, 1]
255        // B[0][0] = 0 - 3*3/8 = -9/8
256        // B[0][1] = 3 - 3*4/8 = 3 - 12/8 = 12/8
257        // B[0][2] = 0 - 3*1/8 = -3/8
258        // B[1][1] = 0 - 4*4/8 = -16/8 = -2
259        // B[1][2] = 1 - 4*1/8 = 1 - 4/8 = 4/8
260        // B[2][2] = 0 - 1*1/8 = -1/8
261        let mut g = Graph::with_vertices(3);
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 2).unwrap();
264        let weights = vec![3.0, 1.0];
265        let b = modularity_matrix(&g, Some(&weights), 1.0, false).unwrap();
266        assert!(close(b[0][0], -9.0 / 8.0));
267        assert!(close(b[0][1], 3.0 - 12.0 / 8.0));
268        assert!(close(b[0][2], -3.0 / 8.0));
269        assert!(close(b[1][0], 3.0 - 12.0 / 8.0));
270        assert!(close(b[1][1], -16.0 / 8.0));
271        assert!(close(b[1][2], 1.0 - 4.0 / 8.0));
272        assert!(close(b[2][0], -3.0 / 8.0));
273        assert!(close(b[2][1], 1.0 - 4.0 / 8.0));
274        assert!(close(b[2][2], -1.0 / 8.0));
275    }
276
277    #[test]
278    fn self_loop_undirected() {
279        // 0-0 (self-loop), 0-1. m = 2, sw = 2.
280        // Adjacency: A[0][0] = 2 (self-loop counts twice), A[0][1] = A[1][0] = 1.
281        // deg = [0] endpoint contributes: edge(0,0) → deg[0] += 1+1 = 2 from self-loop,
282        //        edge(0,1) → deg[0] += 1, deg[1] += 1. So deg = [3, 1].
283        // B[0][0] = 2 - 3*3/4 = 2 - 9/4 = -1/4
284        // B[0][1] = 1 - 3*1/4 = 1/4
285        // B[1][0] = 1 - 1*3/4 = 1/4
286        // B[1][1] = 0 - 1*1/4 = -1/4
287        let mut g = Graph::with_vertices(2);
288        g.add_edge(0, 0).unwrap();
289        g.add_edge(0, 1).unwrap();
290        let b = modularity_matrix(&g, None, 1.0, false).unwrap();
291        assert!(close(b[0][0], -1.0 / 4.0));
292        assert!(close(b[0][1], 1.0 / 4.0));
293        assert!(close(b[1][0], 1.0 / 4.0));
294        assert!(close(b[1][1], -1.0 / 4.0));
295    }
296
297    #[test]
298    fn resolution_parameter() {
299        // With γ = 0, B = A (the adjacency matrix).
300        let mut g = Graph::with_vertices(3);
301        g.add_edge(0, 1).unwrap();
302        g.add_edge(1, 2).unwrap();
303        g.add_edge(2, 0).unwrap();
304        let b = modularity_matrix(&g, None, 0.0, false).unwrap();
305        // Undirected triangle adjacency: 0 off-diag, 1 on connections.
306        assert!(close(b[0][0], 0.0));
307        assert!(close(b[0][1], 1.0));
308        assert!(close(b[0][2], 1.0));
309        assert!(close(b[1][2], 1.0));
310    }
311
312    #[test]
313    fn negative_resolution_errors() {
314        let mut g = Graph::with_vertices(2);
315        g.add_edge(0, 1).unwrap();
316        assert!(modularity_matrix(&g, None, -1.0, false).is_err());
317    }
318
319    #[test]
320    fn weights_length_mismatch_errors() {
321        let mut g = Graph::with_vertices(2);
322        g.add_edge(0, 1).unwrap();
323        assert!(modularity_matrix(&g, Some(&[1.0, 2.0]), 1.0, false).is_err());
324    }
325
326    #[test]
327    fn row_sums_zero_for_undirected_gamma_1() {
328        // For γ=1, the modularity matrix of a connected undirected graph
329        // has row sums = 0 (since Σ_j A_ij = k_i and Σ_j k_j = 2m).
330        let mut g = Graph::with_vertices(4);
331        g.add_edge(0, 1).unwrap();
332        g.add_edge(1, 2).unwrap();
333        g.add_edge(2, 3).unwrap();
334        g.add_edge(3, 0).unwrap();
335        let b = modularity_matrix(&g, None, 1.0, false).unwrap();
336        for row in &b {
337            let row_sum: f64 = row.iter().sum();
338            assert!(close(row_sum, 0.0));
339        }
340    }
341}