use crate::core::{Graph, IgraphError, IgraphResult};
pub fn modularity_matrix(
graph: &Graph,
weights: Option<&[f64]>,
resolution: f64,
directed: bool,
) -> IgraphResult<Vec<Vec<f64>>> {
let n = graph.vcount();
let ecount = graph.ecount();
if !resolution.is_finite() || resolution < 0.0 {
return Err(IgraphError::InvalidArgument(
"resolution parameter must be non-negative and finite".to_string(),
));
}
if let Some(w) = weights {
if w.len() != ecount {
return Err(IgraphError::InvalidArgument(format!(
"modularity_matrix: weights length ({}) does not match edge count ({ecount})",
w.len()
)));
}
}
if n == 0 {
return Ok(Vec::new());
}
let n_usize = n as usize;
let directed = directed && graph.is_directed();
let mut matrix = vec![vec![0.0_f64; n_usize]; n_usize];
let m_u32 =
u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
for eid in 0..m_u32 {
let (from, to) = graph.edge(eid)?;
let f = from as usize;
let t = to as usize;
let w = weights.map_or(1.0, |ws| ws[eid as usize]);
matrix[f][t] += w;
if !directed {
matrix[t][f] += w;
}
}
#[allow(clippy::cast_precision_loss)]
let sw: f64 = weights.map_or(ecount as f64, |ws| ws.iter().sum());
if sw == 0.0 {
return Err(IgraphError::InvalidArgument(
"modularity_matrix: total edge weight is zero; matrix is undefined".to_string(),
));
}
if directed {
let mut k_out = vec![0.0_f64; n_usize];
let mut k_in = vec![0.0_f64; n_usize];
for eid in 0..m_u32 {
let (from, to) = graph.edge(eid)?;
let w = weights.map_or(1.0, |ws| ws[eid as usize]);
k_out[from as usize] += w;
k_in[to as usize] += w;
}
let scaling = resolution / sw;
for (j, &k_in_j) in k_in.iter().enumerate().take(n_usize) {
for i in 0..n_usize {
matrix[i][j] -= k_out[i] * k_in_j * scaling;
}
}
} else {
let mut deg = vec![0.0_f64; n_usize];
for eid in 0..m_u32 {
let (from, to) = graph.edge(eid)?;
let w = weights.map_or(1.0, |ws| ws[eid as usize]);
deg[from as usize] += w;
deg[to as usize] += w;
}
let scaling = resolution / (2.0 * sw);
for j in 0..n_usize {
for i in 0..n_usize {
matrix[i][j] -= deg[i] * deg[j] * scaling;
}
}
}
Ok(matrix)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-10
}
#[test]
fn empty_graph() {
let g = Graph::with_vertices(0);
let m = modularity_matrix(&g, None, 1.0, false).unwrap();
assert!(m.is_empty());
}
#[test]
fn single_vertex_no_edges_error() {
let g = Graph::with_vertices(1);
assert!(modularity_matrix(&g, None, 1.0, false).is_err());
}
#[test]
fn triangle_undirected() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let b = modularity_matrix(&g, None, 1.0, false).unwrap();
for (i, row) in b.iter().enumerate() {
assert!(close(row[i], -2.0 / 3.0));
for (j, &val) in row.iter().enumerate() {
if i != j {
assert!(close(val, 1.0 / 3.0));
}
}
}
}
#[test]
fn path_3_undirected() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let b = modularity_matrix(&g, None, 1.0, false).unwrap();
assert!(close(b[0][0], -0.25));
assert!(close(b[0][1], 0.5));
assert!(close(b[0][2], -0.25));
assert!(close(b[1][0], 0.5));
assert!(close(b[1][1], -1.0));
assert!(close(b[1][2], 0.5));
assert!(close(b[2][0], -0.25));
assert!(close(b[2][1], 0.5));
assert!(close(b[2][2], -0.25));
}
#[test]
fn directed_simple() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let b = modularity_matrix(&g, None, 1.0, true).unwrap();
assert!(close(b[0][0], 0.0));
assert!(close(b[0][1], 0.5));
assert!(close(b[0][2], -0.5));
assert!(close(b[1][0], 0.0));
assert!(close(b[1][1], -0.5));
assert!(close(b[1][2], 0.5));
assert!(close(b[2][0], 0.0));
assert!(close(b[2][1], 0.0));
assert!(close(b[2][2], 0.0));
}
#[test]
fn directed_flag_ignored_for_undirected_graph() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let b1 = modularity_matrix(&g, None, 1.0, true).unwrap();
let b2 = modularity_matrix(&g, None, 1.0, false).unwrap();
for (row1, row2) in b1.iter().zip(b2.iter()) {
for (&v1, &v2) in row1.iter().zip(row2.iter()) {
assert!(close(v1, v2));
}
}
}
#[test]
fn weighted_undirected() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let weights = vec![3.0, 1.0];
let b = modularity_matrix(&g, Some(&weights), 1.0, false).unwrap();
assert!(close(b[0][0], -9.0 / 8.0));
assert!(close(b[0][1], 3.0 - 12.0 / 8.0));
assert!(close(b[0][2], -3.0 / 8.0));
assert!(close(b[1][0], 3.0 - 12.0 / 8.0));
assert!(close(b[1][1], -16.0 / 8.0));
assert!(close(b[1][2], 1.0 - 4.0 / 8.0));
assert!(close(b[2][0], -3.0 / 8.0));
assert!(close(b[2][1], 1.0 - 4.0 / 8.0));
assert!(close(b[2][2], -1.0 / 8.0));
}
#[test]
fn self_loop_undirected() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
let b = modularity_matrix(&g, None, 1.0, false).unwrap();
assert!(close(b[0][0], -1.0 / 4.0));
assert!(close(b[0][1], 1.0 / 4.0));
assert!(close(b[1][0], 1.0 / 4.0));
assert!(close(b[1][1], -1.0 / 4.0));
}
#[test]
fn resolution_parameter() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let b = modularity_matrix(&g, None, 0.0, false).unwrap();
assert!(close(b[0][0], 0.0));
assert!(close(b[0][1], 1.0));
assert!(close(b[0][2], 1.0));
assert!(close(b[1][2], 1.0));
}
#[test]
fn negative_resolution_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(modularity_matrix(&g, None, -1.0, false).is_err());
}
#[test]
fn weights_length_mismatch_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(modularity_matrix(&g, Some(&[1.0, 2.0]), 1.0, false).is_err());
}
#[test]
fn row_sums_zero_for_undirected_gamma_1() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
let b = modularity_matrix(&g, None, 1.0, false).unwrap();
for row in &b {
let row_sum: f64 = row.iter().sum();
assert!(close(row_sum, 0.0));
}
}
}