use std::collections::VecDeque;
use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphResult, VertexId};
pub fn convergence_degree(graph: &Graph) -> IgraphResult<Vec<f64>> {
let (result, _, _) = convergence_degree_full(graph)?;
Ok(result)
}
pub fn convergence_degree_full(graph: &Graph) -> IgraphResult<(Vec<f64>, Vec<f64>, Vec<f64>)> {
let n = graph.vcount();
let m = graph.ecount();
let directed = graph.is_directed();
let mut ins: Vec<f64> = vec![0.0; m];
let mut outs: Vec<f64> = vec![0.0; m];
if n == 0 || m == 0 {
return Ok((vec![f64::NAN; m], ins, outs));
}
let n_us = n as usize;
let mut geodist: Vec<i64> = vec![0; n_us];
let mut queue: VecDeque<(VertexId, i64)> = VecDeque::new();
let passes: &[bool] = if directed { &[true, false] } else { &[true] };
for &is_out_pass in passes {
for src in 0..n {
geodist.fill(0);
queue.clear();
geodist[src as usize] = 1;
queue.push_back((src, 0));
while let Some((actnode, actdist)) = queue.pop_front() {
let eids = if directed && !is_out_pass {
graph.incident_in(actnode)?
} else {
graph.incident(actnode)?
};
for &eid in &eids {
let (a, b) = graph.edge(eid)?;
let neighbor = if a == actnode { b } else { a };
let nd = geodist[neighbor as usize];
if nd != 0 {
if nd - 1 == actdist + 1 {
bump(
directed,
is_out_pass,
actnode,
neighbor,
eid,
&mut ins,
&mut outs,
);
}
} else {
geodist[neighbor as usize] = actdist + 2;
queue.push_back((neighbor, actdist + 1));
bump(
directed,
is_out_pass,
actnode,
neighbor,
eid,
&mut ins,
&mut outs,
);
}
}
}
}
}
let mut result = vec![0.0_f64; m];
for i in 0..m {
let s = ins[i] + outs[i];
result[i] = if s > 0.0 {
(ins[i] - outs[i]) / s
} else {
f64::NAN
};
}
if !directed {
for r in &mut result {
if *r < 0.0 {
*r = -*r;
}
}
}
Ok((result, ins, outs))
}
#[inline]
fn bump(
directed: bool,
is_out_pass: bool,
actnode: VertexId,
neighbor: VertexId,
eid: EdgeId,
ins: &mut [f64],
outs: &mut [f64],
) {
let i = eid as usize;
if directed {
if is_out_pass {
ins[i] += 1.0;
} else {
outs[i] += 1.0;
}
} else if actnode < neighbor {
ins[i] += 1.0;
} else {
outs[i] += 1.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
(a - b).abs() <= tol
}
fn approx_eq_vec(a: &[f64], b: &[f64], tol: f64) -> bool {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| approx_eq(*x, *y, tol))
}
#[test]
fn empty_graph_is_empty() {
let g = Graph::with_vertices(0);
let (r, i, o) = convergence_degree_full(&g).unwrap();
assert!(r.is_empty());
assert!(i.is_empty());
assert!(o.is_empty());
}
#[test]
fn no_edges_returns_empty_vectors() {
let g = Graph::with_vertices(5);
let res = convergence_degree(&g).unwrap();
assert!(res.is_empty());
}
#[test]
fn upstream_undirected_test_case_matches_out() {
let mut g = Graph::with_vertices(7);
for (u, v) in [
(0, 1),
(0, 2),
(0, 3),
(1, 2),
(1, 3),
(2, 3),
(3, 4),
(4, 5),
(4, 6),
(5, 6),
] {
g.add_edge(u, v).unwrap();
}
let res = convergence_degree(&g).unwrap();
let expected = [
0.0,
0.0,
0.6,
0.0,
0.6,
0.6,
1.0 / 7.0,
2.0 / 3.0,
2.0 / 3.0,
0.0,
];
assert!(
approx_eq_vec(&res, &expected, 1e-4),
"got {res:?}, want {expected:?}"
);
}
#[test]
fn upstream_directed_test_case_matches_out() {
let mut g = Graph::new(6, true).unwrap();
for u in [1u32, 2, 3, 4] {
g.add_edge(u, 0).unwrap();
}
g.add_edge(0, 5).unwrap();
let res = convergence_degree(&g).unwrap();
let expected = [-1.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0, 2.0 / 3.0];
assert!(
approx_eq_vec(&res, &expected, 1e-12),
"got {res:?}, want {expected:?}"
);
}
#[test]
fn directed_full_returns_consistent_in_out() {
let mut g = Graph::new(6, true).unwrap();
for u in [1u32, 2, 3, 4] {
g.add_edge(u, 0).unwrap();
}
g.add_edge(0, 5).unwrap();
let (r, ins, outs) = convergence_degree_full(&g).unwrap();
for i in 0..4 {
assert!(approx_eq(ins[i], 1.0, 1e-12), "ins[{i}] = {}", ins[i]);
assert!(approx_eq(outs[i], 2.0, 1e-12), "outs[{i}] = {}", outs[i]);
}
assert!(approx_eq(ins[4], 5.0, 1e-12), "ins[4] = {}", ins[4]);
assert!(approx_eq(outs[4], 1.0, 1e-12), "outs[4] = {}", outs[4]);
for i in 0..r.len() {
let expected = (ins[i] - outs[i]) / (ins[i] + outs[i]);
assert!(approx_eq(r[i], expected, 1e-12));
}
}
#[test]
fn single_undirected_edge_balanced() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let (r, ins, outs) = convergence_degree_full(&g).unwrap();
assert!(approx_eq(ins[0], 1.0, 1e-12));
assert!(approx_eq(outs[0], 1.0, 1e-12));
assert!(approx_eq(r[0], 0.0, 1e-12));
}
#[test]
fn directed_single_edge() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
let (r, ins, outs) = convergence_degree_full(&g).unwrap();
assert!(approx_eq(ins[0], 1.0, 1e-12));
assert!(approx_eq(outs[0], 1.0, 1e-12));
assert!(approx_eq(r[0], 0.0, 1e-12));
}
#[test]
fn undirected_self_loop_yields_nan() {
let mut g = Graph::with_vertices(1);
g.add_edge(0, 0).unwrap();
let res = convergence_degree(&g).unwrap();
assert_eq!(res.len(), 1);
assert!(res[0].is_nan(), "expected NaN, got {}", res[0]);
}
#[test]
fn isolated_vertices_dont_break_things() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let res = convergence_degree(&g).unwrap();
assert_eq!(res.len(), 2);
let expected = 1.0 / 3.0;
for r in &res {
assert!(approx_eq(*r, expected, 1e-12), "got {r}, want {expected}");
}
}
#[test]
fn complete_undirected_is_symmetric() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
let res = convergence_degree(&g).unwrap();
for r in &res {
assert!(approx_eq(*r, 0.0, 1e-12), "K_4 edge gave {r}");
}
}
#[test]
fn star_directed_out_edges_have_negative_convergence() {
let mut g = Graph::new(4, true).unwrap();
for v in [1, 2, 3] {
g.add_edge(0, v).unwrap();
}
let (r, ins, outs) = convergence_degree_full(&g).unwrap();
for &x in &ins {
assert!(approx_eq(x, 1.0, 1e-12), "ins = {x}");
}
for &x in &outs {
assert!(approx_eq(x, 1.0, 1e-12), "outs = {x}");
}
for &x in &r {
assert!(approx_eq(x, 0.0, 1e-12), "result = {x}");
}
}
#[test]
fn parallel_edges_share_credit() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
let (_r, ins, outs) = convergence_degree_full(&g).unwrap();
for i in 0..2 {
assert!(approx_eq(ins[i], 1.0, 1e-12), "ins[{i}] = {}", ins[i]);
assert!(approx_eq(outs[i], 1.0, 1e-12), "outs[{i}] = {}", outs[i]);
}
}
#[test]
fn result_length_matches_ecount() {
let mut g = Graph::with_vertices(5);
for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)] {
g.add_edge(u, v).unwrap();
}
let res = convergence_degree(&g).unwrap();
assert_eq!(res.len(), g.ecount());
}
#[test]
fn undirected_results_are_non_negative() {
let mut g = Graph::with_vertices(6);
for (u, v) in [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)] {
g.add_edge(u, v).unwrap();
}
let res = convergence_degree(&g).unwrap();
for r in &res {
assert!(r.is_nan() || (*r >= -1e-12 && *r <= 1.0 + 1e-12), "got {r}");
}
}
#[test]
fn directed_results_within_unit_interval() {
let mut g = Graph::new(6, true).unwrap();
for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)] {
g.add_edge(u, v).unwrap();
}
let res = convergence_degree(&g).unwrap();
for r in &res {
assert!(
r.is_nan() || (-1.0 - 1e-12 <= *r && *r <= 1.0 + 1e-12),
"got {r}"
);
}
}
#[test]
fn ins_plus_outs_strictly_positive_for_simple_path() {
let mut g = Graph::with_vertices(4);
for (u, v) in [(0, 1), (1, 2), (2, 3)] {
g.add_edge(u, v).unwrap();
}
let (_, ins, outs) = convergence_degree_full(&g).unwrap();
for (i, o) in ins.iter().zip(outs.iter()) {
assert!(*i + *o > 0.0, "i + o = {} on path edge", i + o);
}
}
#[test]
fn balanced_directed_cycle_is_zero() {
let mut g = Graph::new(5, true).unwrap();
for u in 0u32..5 {
g.add_edge(u, (u + 1) % 5).unwrap();
}
let res = convergence_degree(&g).unwrap();
let first = res[0];
for r in &res {
assert!(approx_eq(*r, first, 1e-12), "asymmetric: {res:?}");
}
}
}