use alloc::vec::Vec;
use num_traits::{AsPrimitive, ToPrimitive};
use super::modularity::{ModularityError, UndirectedView, build_working_graph};
use crate::traits::{Finite, Number, SparseValuedMatrix2D};
pub trait UndirectedModularity<Marker: AsPrimitive<usize> = usize>:
SparseValuedMatrix2D + Sized
where
Self::RowIndex: AsPrimitive<usize>,
Self::ColumnIndex: AsPrimitive<usize>,
Self::Value: Number + ToPrimitive + Finite,
{
#[inline]
fn undirected_modularity(
&self,
partition: &[Marker],
resolution: f64,
) -> Result<f64, ModularityError> {
if !resolution.is_finite() || resolution <= 0.0 {
return Err(ModularityError::InvalidResolution);
}
let graph = build_working_graph(self)?;
let view = UndirectedView::from_working_graph(&graph);
let number_of_nodes = view.number_of_nodes();
if partition.len() != number_of_nodes {
return Err(ModularityError::PartitionLengthMismatch {
expected: number_of_nodes,
found: partition.len(),
});
}
let usize_partition: Vec<usize> =
partition.iter().map(|community| community.as_()).collect();
Ok(view.modularity(&usize_partition, resolution))
}
}
impl<G, Marker> UndirectedModularity<Marker> for G
where
G: SparseValuedMatrix2D + Sized,
Marker: AsPrimitive<usize>,
G::RowIndex: AsPrimitive<usize>,
G::ColumnIndex: AsPrimitive<usize>,
G::Value: Number + ToPrimitive + Finite,
{
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::{
super::modularity::{UndirectedView, build_working_graph},
ModularityError, UndirectedModularity,
};
use crate::{impls::ValuedCSR2D, naive_structs::GenericEdgesBuilder, traits::EdgesBuilder};
type WeightedMatrix = ValuedCSR2D<usize, usize, usize, f64>;
const TOLERANCE: f64 = 1e-12;
fn build(node_count: usize, edges: Vec<(usize, usize, f64)>) -> WeightedMatrix {
let mut edges = edges;
edges.sort_unstable_by(|(ls, ld, _), (rs, rd, _)| (ls, ld).cmp(&(rs, rd)));
GenericEdgesBuilder::<_, WeightedMatrix>::default()
.expected_number_of_edges(edges.len())
.expected_shape((node_count, node_count))
.edges(edges.into_iter())
.build()
.unwrap()
}
fn build_undirected(node_count: usize, edges: &[(usize, usize, f64)]) -> WeightedMatrix {
let mut directed = Vec::with_capacity(edges.len() * 2);
for &(source, destination, weight) in edges {
directed.push((source, destination, weight));
if source != destination {
directed.push((destination, source, weight));
}
}
build(node_count, directed)
}
fn view_modularity(matrix: &WeightedMatrix, partition: &[usize], resolution: f64) -> f64 {
let graph = build_working_graph(matrix).unwrap();
UndirectedView::from_working_graph(&graph).modularity(partition, resolution)
}
#[test]
fn test_two_disjoint_edges() {
let matrix = build_undirected(4, &[(0, 1, 1.0), (2, 3, 1.0)]);
assert!((view_modularity(&matrix, &[0, 0, 1, 1], 1.0) - 0.5).abs() < TOLERANCE);
assert!(view_modularity(&matrix, &[0, 0, 0, 0], 1.0).abs() < TOLERANCE);
assert!((view_modularity(&matrix, &[0, 1, 2, 3], 1.0) - (-0.25)).abs() < TOLERANCE);
}
#[test]
fn test_single_edge() {
let matrix = build_undirected(2, &[(0, 1, 1.0)]);
assert!(view_modularity(&matrix, &[0, 0], 1.0).abs() < TOLERANCE);
assert!((view_modularity(&matrix, &[0, 1], 1.0) - (-0.5)).abs() < TOLERANCE);
}
#[test]
fn test_resolution_scales_expected_term() {
let matrix = build_undirected(4, &[(0, 1, 1.0), (2, 3, 1.0)]);
assert!(view_modularity(&matrix, &[0, 0, 1, 1], 2.0).abs() < TOLERANCE);
}
#[test]
fn test_trait_matches_view() {
let matrix = build_undirected(4, &[(0, 1, 1.0), (1, 2, 2.0), (2, 3, 1.0), (3, 0, 2.0)]);
for partition in [vec![0, 0, 1, 1], vec![0, 1, 0, 1], vec![0, 0, 0, 0]] {
let trait_value =
UndirectedModularity::<usize>::undirected_modularity(&matrix, &partition, 1.0)
.unwrap();
let view_value = view_modularity(&matrix, &partition, 1.0);
assert!((trait_value - view_value).abs() < TOLERANCE);
}
}
#[test]
fn test_trait_validates_partition_length() {
let matrix = build_undirected(4, &[(0, 1, 1.0), (2, 3, 1.0)]);
let error = UndirectedModularity::<usize>::undirected_modularity(&matrix, &[0, 0, 1], 1.0)
.unwrap_err();
assert!(matches!(
error,
ModularityError::PartitionLengthMismatch { expected: 4, found: 3 }
));
}
#[test]
fn test_trait_rejects_invalid_resolution() {
let matrix = build_undirected(2, &[(0, 1, 1.0)]);
for resolution in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let error =
UndirectedModularity::<usize>::undirected_modularity(&matrix, &[0, 0], resolution)
.unwrap_err();
assert!(matches!(error, ModularityError::InvalidResolution));
}
}
#[test]
fn test_trait_rejects_non_symmetric_matrix() {
let matrix = build(2, vec![(0, 1, 1.0)]);
let error = UndirectedModularity::<usize>::undirected_modularity(&matrix, &[0, 0], 1.0)
.unwrap_err();
assert!(matches!(error, ModularityError::NonSymmetricEdge { .. }));
}
#[test]
fn test_trait_rejects_non_square_matrix() {
let matrix: WeightedMatrix = GenericEdgesBuilder::<_, WeightedMatrix>::default()
.expected_number_of_edges(1)
.expected_shape((2, 3))
.edges(vec![(0, 1, 1.0)].into_iter())
.build()
.unwrap();
let error = UndirectedModularity::<usize>::undirected_modularity(&matrix, &[0, 0], 1.0)
.unwrap_err();
assert!(matches!(error, ModularityError::NonSquareMatrix { rows: 2, columns: 3 }));
}
}