use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphResult};
pub fn assortativity_degree(graph: &Graph) -> IgraphResult<Option<f64>> {
if graph.is_directed() {
return assortativity_degree_directed(graph);
}
let m = graph.ecount();
if m == 0 {
return Ok(None);
}
let n = graph.vcount();
let mut deg = Vec::with_capacity(n as usize);
for v in 0..n {
let d = graph.degree(v)?;
#[allow(clippy::cast_precision_loss)]
deg.push(d as f64);
}
let mut num1 = 0.0_f64;
let mut num2 = 0.0_f64;
let mut den1 = 0.0_f64;
let m_u32 = u32::try_from(m).map_err(|_| {
crate::core::IgraphError::Internal("ecount overflows u32 for assortativity")
})?;
for e in 0..m_u32 {
let (u, v) = graph.edge(e as EdgeId)?;
let du = deg[u as usize];
let dv = deg[v as usize];
num1 += du * dv;
num2 += du + dv;
den1 += du * du + dv * dv;
}
#[allow(clippy::cast_precision_loss)]
let total = m as f64;
num1 /= total;
den1 /= total * 2.0;
num2 /= total * 2.0;
num2 *= num2;
let denom = den1 - num2;
if denom == 0.0 {
return Ok(None);
}
Ok(Some((num1 - num2) / denom))
}
pub fn assortativity_degree_directed(graph: &Graph) -> IgraphResult<Option<f64>> {
if !graph.is_directed() {
return assortativity_degree(graph);
}
let m = graph.ecount();
if m == 0 {
return Ok(None);
}
let n = graph.vcount();
let n_us = n as usize;
let mut out_deg = vec![0.0_f64; n_us];
let mut in_deg = vec![0.0_f64; n_us];
let m_u32 = u32::try_from(m).map_err(|_| {
crate::core::IgraphError::Internal("ecount overflows u32 for assortativity")
})?;
for e in 0..m_u32 {
let (src, tgt) = graph.edge(e as EdgeId)?;
out_deg[src as usize] += 1.0;
in_deg[tgt as usize] += 1.0;
}
let mut num1 = 0.0_f64;
let mut num2 = 0.0_f64;
let mut num3 = 0.0_f64;
let mut den1 = 0.0_f64;
let mut den2 = 0.0_f64;
for e in 0..m_u32 {
let (src, tgt) = graph.edge(e as EdgeId)?;
let from_value = out_deg[src as usize];
let to_value = in_deg[tgt as usize];
num1 += from_value * to_value;
num2 += from_value;
num3 += to_value;
den1 += from_value * from_value;
den2 += to_value * to_value;
}
#[allow(clippy::cast_precision_loss)]
let total = m as f64;
let num = num1 - num2 * num3 / total;
let var_from = den1 - num2 * num2 / total;
let var_to = den2 - num3 * num3 / total;
if var_from <= 0.0 || var_to <= 0.0 {
return Ok(None);
}
let den = var_from.sqrt() * var_to.sqrt();
if den == 0.0 {
return Ok(None);
}
Ok(Some(num / den))
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: f64, b: f64, tol: f64) {
assert!(
(a - b).abs() < tol,
"expected {b} ± {tol}, got {a} (diff {})",
(a - b).abs()
);
}
#[test]
fn empty_graph_is_none() {
let g = Graph::with_vertices(0);
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn isolated_vertices_no_edges_is_none() {
let g = Graph::with_vertices(5);
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn regular_graph_returns_none() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
g.add_edge(i, (i + 1) % 4).unwrap();
}
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn k4_is_regular_returns_none() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn path_3_is_perfectly_disassortative() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let r = assortativity_degree(&g).unwrap().unwrap();
assert_close(r, -1.0, 1e-12);
}
#[test]
fn star_is_perfectly_disassortative() {
let mut g = Graph::with_vertices(4);
for v in 1..4 {
g.add_edge(0, v).unwrap();
}
let r = assortativity_degree(&g).unwrap().unwrap();
assert_close(r, -1.0, 1e-12);
}
#[test]
fn two_disjoint_edges_is_assortative_or_regular() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn directed_graph_routes_to_directed_formula() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
assert_eq!(assortativity_degree(&g).unwrap(), None);
}
#[test]
fn directed_3_cycle_is_regular_returns_none() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(assortativity_degree_directed(&g).unwrap(), None);
}
#[test]
fn directed_path_three_disassortative() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert_eq!(assortativity_degree_directed(&g).unwrap(), None);
}
#[test]
fn directed_chain_with_branch_is_well_defined() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(0, 2).unwrap();
let r = assortativity_degree_directed(&g).unwrap().unwrap();
assert_close(r, -0.5, 1e-12);
}
#[test]
fn directed_empty_graph_returns_none() {
let g = Graph::new(0, true).unwrap();
assert_eq!(assortativity_degree_directed(&g).unwrap(), None);
}
#[test]
fn directed_undirected_graph_routes_to_undirected_formula() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let a = assortativity_degree(&g).unwrap();
let b = assortativity_degree_directed(&g).unwrap();
assert_eq!(a, b);
}
#[test]
fn diamond_k4_minus_edge() {
let mut g = Graph::with_vertices(4);
for &(u, v) in &[(0u32, 1), (0, 2), (1, 2), (1, 3), (2, 3)] {
g.add_edge(u, v).unwrap();
}
let r = assortativity_degree(&g).unwrap().unwrap();
assert_close(r, -2.0 / 3.0, 1e-12);
}
#[test]
fn two_triangles_joined_by_bridge_matches_python_igraph() {
let mut g = Graph::with_vertices(6);
for &(u, v) in &[(0u32, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
g.add_edge(u, v).unwrap();
}
let r = assortativity_degree(&g).unwrap().unwrap();
assert_close(r, -0.166_666_666_666_664_24, 1e-12);
}
}