use crate::core::{Graph, IgraphResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReciprocityMode {
Default,
Ratio,
}
pub fn reciprocity(graph: &Graph) -> IgraphResult<Option<f64>> {
reciprocity_with_mode(graph, false, ReciprocityMode::Default)
}
pub fn reciprocity_with_mode(
graph: &Graph,
ignore_loops: bool,
mode: ReciprocityMode,
) -> IgraphResult<Option<f64>> {
let n = graph.vcount();
let m = graph.ecount();
if m == 0 {
return Ok(None);
}
if !graph.is_directed() {
return Ok(Some(1.0));
}
let mut rec: u64 = 0;
let mut nonrec: u64 = 0;
let mut loops: u64 = 0;
for v in 0..n {
let outneis = graph.out_neighbors_vec(v)?;
let inneis = graph.in_neighbors_vec(v)?;
let mut ip = 0usize;
let mut op = 0usize;
while ip < inneis.len() && op < outneis.len() {
match inneis[ip].cmp(&outneis[op]) {
std::cmp::Ordering::Less => {
nonrec += 1;
ip += 1;
}
std::cmp::Ordering::Greater => {
nonrec += 1;
op += 1;
}
std::cmp::Ordering::Equal => {
if inneis[ip] == v {
loops += 1;
if !ignore_loops {
rec += 1;
}
} else {
rec += 1;
}
ip += 1;
op += 1;
}
}
}
nonrec += (inneis.len() - ip) as u64 + (outneis.len() - op) as u64;
}
#[allow(clippy::cast_precision_loss)]
let result = match mode {
ReciprocityMode::Default => {
let denom = if ignore_loops {
m as u64 - loops
} else {
m as u64
};
if denom == 0 {
return Ok(None);
}
rec as f64 / denom as f64
}
ReciprocityMode::Ratio => {
let denom = rec + nonrec;
if denom == 0 {
return Ok(None);
}
rec as f64 / denom as f64
}
};
Ok(Some(result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_returns_none() {
let g = Graph::with_vertices(0);
assert_eq!(reciprocity(&g).unwrap(), None);
}
#[test]
fn isolated_vertices_return_none() {
let g = Graph::with_vertices(5);
assert_eq!(reciprocity(&g).unwrap(), None);
}
#[test]
fn undirected_graph_is_always_1() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert_eq!(reciprocity(&g).unwrap(), Some(1.0));
}
#[test]
fn directed_one_way_edge_has_zero() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
assert_eq!(reciprocity(&g).unwrap(), Some(0.0));
}
#[test]
fn directed_mutual_pair_has_one() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
assert_eq!(reciprocity(&g).unwrap(), Some(1.0));
}
#[test]
fn directed_partial_reciprocity() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
g.add_edge(0, 2).unwrap();
let two_thirds = 2.0_f64 / 3.0;
assert_eq!(reciprocity(&g).unwrap(), Some(two_thirds));
}
#[test]
fn directed_3_cycle_no_reciprocity() {
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!(reciprocity(&g).unwrap(), Some(0.0));
}
#[test]
fn directed_self_loop_is_counted_as_mutual() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 0).unwrap();
assert_eq!(reciprocity(&g).unwrap(), Some(1.0));
}
#[test]
fn ratio_and_default_diverge_with_one_way_edges() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
g.add_edge(0, 2).unwrap();
let two_thirds = 2.0_f64 / 3.0;
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Default).unwrap(),
Some(two_thirds)
);
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Ratio).unwrap(),
Some(0.5)
);
}
#[test]
fn ratio_and_default_agree_on_fully_mutual_graph() {
let mut g = Graph::new(3, true).unwrap();
for &(u, v) in &[(0u32, 1), (1, 0), (1, 2), (2, 1), (0, 2), (2, 0)] {
g.add_edge(u, v).unwrap();
}
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Default).unwrap(),
Some(1.0)
);
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Ratio).unwrap(),
Some(1.0)
);
}
#[test]
fn ignore_loops_drops_self_loop_from_default_denominator() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Default).unwrap(),
Some(1.0)
);
assert_eq!(
reciprocity_with_mode(&g, true, ReciprocityMode::Default).unwrap(),
Some(1.0)
);
assert_eq!(
reciprocity_with_mode(&g, true, ReciprocityMode::Ratio).unwrap(),
Some(1.0)
);
}
#[test]
fn ratio_mode_with_one_way_edge() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
assert_eq!(
reciprocity_with_mode(&g, false, ReciprocityMode::Ratio).unwrap(),
Some(0.0)
);
}
#[test]
fn ratio_mode_self_loop_only_with_ignore_loops_returns_none() {
let mut g = Graph::new(1, true).unwrap();
g.add_edge(0, 0).unwrap();
assert_eq!(
reciprocity_with_mode(&g, true, ReciprocityMode::Ratio).unwrap(),
None
);
}
#[test]
fn default_mode_ignore_loops_only_loop_returns_none() {
let mut g = Graph::new(1, true).unwrap();
g.add_edge(0, 0).unwrap();
assert_eq!(
reciprocity_with_mode(&g, true, ReciprocityMode::Default).unwrap(),
None
);
}
#[test]
fn undirected_unconditional_one() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 0).unwrap();
for &mode in &[ReciprocityMode::Default, ReciprocityMode::Ratio] {
for &skip in &[false, true] {
assert_eq!(
reciprocity_with_mode(&g, skip, mode).unwrap(),
Some(1.0),
"undirected mode={mode:?} ignore_loops={skip}"
);
}
}
}
#[test]
fn ratio_mode_3_cycle_zero() {
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!(
reciprocity_with_mode(&g, false, ReciprocityMode::Ratio).unwrap(),
Some(0.0)
);
}
}