use crate::core::{Graph, IgraphError, IgraphResult};
pub fn avg_nearest_neighbor_degree(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
let n = graph.vcount();
let n_us = n as usize;
let mut deg: Vec<u32> = Vec::with_capacity(n_us);
for v in 0..n {
deg.push(
u32::try_from(graph.degree(v)?)
.map_err(|_| crate::core::IgraphError::Internal("degree exceeds u32 in knn"))?,
);
}
let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
for v in 0..n {
let neis = graph.neighbors(v)?;
if neis.is_empty() {
out.push(None);
continue;
}
let mut sum: u64 = 0;
for &w in &neis {
sum += u64::from(deg[w as usize]);
}
#[allow(clippy::cast_precision_loss)]
let avg = (sum as f64) / (neis.len() as f64);
out.push(Some(avg));
}
Ok(out)
}
pub fn avg_nearest_neighbor_degree_weighted(
graph: &Graph,
weights: &[f64],
) -> IgraphResult<Vec<Option<f64>>> {
let n = graph.vcount();
let n_us = n as usize;
let m = graph.ecount();
if weights.len() != m {
return Err(IgraphError::Internal(
"weights length does not match ecount in knn_weighted",
));
}
for &w in weights {
if !w.is_finite() || w < 0.0 {
return Err(IgraphError::Internal(
"weights must be finite and non-negative in knn_weighted",
));
}
}
let mut deg: Vec<u32> = Vec::with_capacity(n_us);
for v in 0..n {
deg.push(
u32::try_from(graph.degree(v)?)
.map_err(|_| IgraphError::Internal("degree exceeds u32 in knn_weighted"))?,
);
}
let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
for v in 0..n {
let inc = graph.incident(v)?;
let mut sum = 0.0_f64;
let mut strength = 0.0_f64;
for &e in &inc {
let u = graph.edge_other(e, v)?;
let w = weights[e as usize];
strength += w;
sum += w * f64::from(deg[u as usize]);
}
if strength > 0.0 {
out.push(Some(sum / strength));
} else {
out.push(None);
}
}
Ok(out)
}
pub fn knnk(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
let n = graph.vcount();
let knn = avg_nearest_neighbor_degree(graph)?;
let mut max_deg: u32 = 0;
for v in 0..n {
let d = u32::try_from(graph.degree(v)?)
.map_err(|_| IgraphError::Internal("degree exceeds u32 in knnk"))?;
if d > max_deg {
max_deg = d;
}
}
if max_deg == 0 {
return Ok(Vec::new());
}
let max_deg_us = max_deg as usize;
let mut sums: Vec<f64> = vec![0.0; max_deg_us];
let mut counts: Vec<u32> = vec![0; max_deg_us];
for v in 0..n {
if let Some(k) = knn[v as usize] {
let d = graph.degree(v)?;
sums[d - 1] += k;
counts[d - 1] += 1;
}
}
Ok(sums
.iter()
.zip(counts.iter())
.map(|(&s, &c)| if c == 0 { None } else { Some(s / f64::from(c)) })
.collect())
}
pub fn knnk_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
let n = graph.vcount();
let m = graph.ecount();
if weights.len() != m {
return Err(IgraphError::Internal(
"weights length does not match ecount in knnk_weighted",
));
}
for &w in weights {
if !w.is_finite() || w < 0.0 {
return Err(IgraphError::Internal(
"weights must be finite and non-negative in knnk_weighted",
));
}
}
let mut max_deg: u32 = 0;
let mut deg: Vec<u32> = Vec::with_capacity(n as usize);
for v in 0..n {
let d = u32::try_from(graph.degree(v)?)
.map_err(|_| IgraphError::Internal("degree exceeds u32 in knnk_weighted"))?;
deg.push(d);
if d > max_deg {
max_deg = d;
}
}
if max_deg == 0 {
return Ok(Vec::new());
}
let max_deg_us = max_deg as usize;
let mut sums: Vec<f64> = vec![0.0; max_deg_us];
let mut strs: Vec<f64> = vec![0.0; max_deg_us];
for v in 0..n {
let inc = graph.incident(v)?;
if inc.is_empty() {
continue;
}
let mut sum_v = 0.0_f64;
let mut str_v = 0.0_f64;
for &e in &inc {
let u = graph.edge_other(e, v)?;
let w = weights[e as usize];
str_v += w;
sum_v += w * f64::from(deg[u as usize]);
}
let bucket = deg[v as usize] as usize - 1;
sums[bucket] += sum_v;
strs[bucket] += str_v;
}
Ok(sums
.iter()
.zip(strs.iter())
.map(|(&s, &t)| if t > 0.0 { Some(s / t) } else { None })
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_yields_empty_vec() {
let g = Graph::with_vertices(0);
assert!(avg_nearest_neighbor_degree(&g).unwrap().is_empty());
}
#[test]
fn isolated_vertices_have_none() {
let g = Graph::with_vertices(3);
assert_eq!(
avg_nearest_neighbor_degree(&g).unwrap(),
vec![None, None, None]
);
}
#[test]
fn star_centre_has_avg_1_leaves_have_3() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
assert_eq!(
avg_nearest_neighbor_degree(&g).unwrap(),
vec![Some(1.0), Some(3.0), Some(3.0), Some(3.0)]
);
}
#[test]
fn path_5_endpoints_see_internal_neighbours() {
let mut g = Graph::with_vertices(5);
for i in 0..4 {
g.add_edge(i, i + 1).unwrap();
}
assert_eq!(
avg_nearest_neighbor_degree(&g).unwrap(),
vec![Some(2.0), Some(1.5), Some(2.0), Some(1.5), Some(2.0)]
);
}
#[test]
fn k4_uniform_degree_3() {
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!(avg_nearest_neighbor_degree(&g).unwrap(), vec![Some(3.0); 4]);
}
#[test]
fn weighted_uniform_weights_match_unweighted() {
let mut g = Graph::with_vertices(5);
for i in 0..4u32 {
g.add_edge(i, i + 1).unwrap();
}
let weights = vec![1.0; 4];
let unweighted = avg_nearest_neighbor_degree(&g).unwrap();
let weighted = avg_nearest_neighbor_degree_weighted(&g, &weights).unwrap();
assert_eq!(unweighted.len(), weighted.len());
for i in 0..unweighted.len() {
match (unweighted[i], weighted[i]) {
(Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
(None, None) => {}
_ => panic!("uniform weights diverged at vertex {i}"),
}
}
}
#[test]
fn weighted_triangle_unequal_weights() {
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 r = avg_nearest_neighbor_degree_weighted(&g, &[1.0, 2.0, 4.0]).unwrap();
assert_eq!(r, vec![Some(2.0); 3]);
}
#[test]
fn weighted_isolated_vertex_yields_none() {
let g = Graph::with_vertices(2);
let r = avg_nearest_neighbor_degree_weighted(&g, &[]).unwrap();
assert_eq!(r, vec![None, None]);
}
#[test]
fn weighted_zero_weight_edge_yields_none() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let r = avg_nearest_neighbor_degree_weighted(&g, &[0.0]).unwrap();
assert_eq!(r, vec![None, None]);
}
#[test]
fn weighted_invalid_weights_length_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(avg_nearest_neighbor_degree_weighted(&g, &[]).is_err());
}
#[test]
fn weighted_negative_weights_error() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(avg_nearest_neighbor_degree_weighted(&g, &[-1.0]).is_err());
}
#[test]
fn knnk_star_distinct_degrees() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
let r = knnk(&g).unwrap();
assert_eq!(r, vec![Some(3.0), None, Some(1.0)]);
}
#[test]
fn knnk_path_5() {
let mut g = Graph::with_vertices(5);
for i in 0..4u32 {
g.add_edge(i, i + 1).unwrap();
}
let r = knnk(&g).unwrap();
assert_eq!(r.len(), 2);
assert_eq!(r[0], Some(2.0));
assert!((r[1].unwrap() - 5.0_f64 / 3.0).abs() < 1e-12);
}
#[test]
fn knnk_empty_graph_yields_empty() {
let g = Graph::with_vertices(0);
assert!(knnk(&g).unwrap().is_empty());
}
#[test]
fn knnk_no_edges_yields_empty() {
let g = Graph::with_vertices(5);
assert!(knnk(&g).unwrap().is_empty());
}
#[test]
fn knnk_weighted_uniform_matches_knnk() {
let mut g = Graph::with_vertices(5);
for i in 0..4u32 {
g.add_edge(i, i + 1).unwrap();
}
let unw = knnk(&g).unwrap();
let w = knnk_weighted(&g, &[1.0; 4]).unwrap();
assert_eq!(unw.len(), w.len());
for i in 0..unw.len() {
match (unw[i], w[i]) {
(Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
(None, None) => {}
_ => panic!("knnk_weighted uniform diverged at idx {i}"),
}
}
}
#[test]
fn self_loop_inflates_neighbour_degree() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
let r = avg_nearest_neighbor_degree(&g).unwrap();
let seven_thirds = 7.0_f64 / 3.0;
assert_eq!(r[0], Some(seven_thirds));
assert_eq!(r[1], Some(3.0));
}
}