pub mod graph_count;
pub mod isoclass;
pub mod motifs_randesu;
pub mod triad_census;
pub use graph_count::graph_count;
pub use isoclass::{isoclass, isoclass_create, isoclass_subgraph};
pub use motifs_randesu::{
motifs_randesu, motifs_randesu_callback, motifs_randesu_estimate, motifs_randesu_no,
};
pub use triad_census::{TriadCensus, TriadType, triad_census};
use crate::core::{Graph, IgraphResult, VertexId};
#[derive(Debug, Clone, PartialEq)]
pub struct DyadCensus {
pub mutual: f64,
pub asymmetric: f64,
pub null: f64,
}
pub fn dyad_census(graph: &Graph) -> IgraphResult<DyadCensus> {
let n = graph.vcount();
if n < 2 {
return Ok(DyadCensus {
mutual: 0.0,
asymmetric: 0.0,
null: 0.0,
});
}
if !graph.is_directed() {
return dyad_census_undirected(graph);
}
let (in_adj, out_adj) = build_directed_adj(graph)?;
let mut rec: f64 = 0.0;
let mut nonrec: f64 = 0.0;
for v in 0..n {
let in_neis = &in_adj[v as usize];
let out_neis = &out_adj[v as usize];
let mut ip = 0;
let mut op = 0;
while ip < in_neis.len() && op < out_neis.len() {
match in_neis[ip].cmp(&out_neis[op]) {
std::cmp::Ordering::Less => {
nonrec += 1.0;
ip += 1;
}
std::cmp::Ordering::Greater => {
nonrec += 1.0;
op += 1;
}
std::cmp::Ordering::Equal => {
rec += 1.0;
ip += 1;
op += 1;
}
}
}
#[allow(clippy::cast_precision_loss)]
{
nonrec += (in_neis.len() - ip) as f64 + (out_neis.len() - op) as f64;
}
}
let mutual = rec / 2.0;
let asymmetric = nonrec / 2.0;
#[allow(clippy::cast_precision_loss)]
let total_dyads = 0.5 * (f64::from(n)) * ((f64::from(n)) - 1.0);
let null = total_dyads - mutual - asymmetric;
Ok(DyadCensus {
mutual,
asymmetric,
null: if null == 0.0 { 0.0 } else { null },
})
}
fn dyad_census_undirected(graph: &Graph) -> IgraphResult<DyadCensus> {
let n = graph.vcount();
let ecount = graph.ecount();
let mut edge_set = std::collections::HashSet::new();
for eid in 0..ecount {
#[allow(clippy::cast_possible_truncation)]
let (src, tgt) = graph.edge(eid as u32)?;
if src != tgt {
let key = if src < tgt { (src, tgt) } else { (tgt, src) };
edge_set.insert(key);
}
}
#[allow(clippy::cast_precision_loss)]
let mutual = edge_set.len() as f64;
#[allow(clippy::cast_precision_loss)]
let total_dyads = 0.5 * (f64::from(n)) * ((f64::from(n)) - 1.0);
let null = total_dyads - mutual;
Ok(DyadCensus {
mutual,
asymmetric: 0.0,
null: if null == 0.0 { 0.0 } else { null },
})
}
type AdjPair = (Vec<Vec<VertexId>>, Vec<Vec<VertexId>>);
fn build_directed_adj(graph: &Graph) -> IgraphResult<AdjPair> {
let n = graph.vcount() as usize;
let ecount = graph.ecount();
let mut in_adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
let mut out_adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
for eid in 0..ecount {
#[allow(clippy::cast_possible_truncation)]
let (src, tgt) = graph.edge(eid as u32)?;
if src != tgt {
out_adj[src as usize].push(tgt);
in_adj[tgt as usize].push(src);
}
}
for neighbors in &mut in_adj {
neighbors.sort_unstable();
neighbors.dedup();
}
for neighbors in &mut out_adj {
neighbors.sort_unstable();
neighbors.dedup();
}
Ok((in_adj, out_adj))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_graph() {
let g = Graph::with_vertices(0);
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_single_vertex() {
let g = Graph::new(1, true).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_two_vertices_no_edges() {
let g = Graph::new(2, true).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null - 1.0).abs() < 1e-10);
}
#[test]
fn test_two_vertices_one_edge() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric - 1.0).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_two_vertices_mutual() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual - 1.0).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_three_vertices_cycle() {
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();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric - 3.0).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_complete_directed_mutual() {
let mut g = Graph::new(4, true).unwrap();
for i in 0..4u32 {
for j in 0..4 {
if i != j {
g.add_edge(i, j).unwrap();
}
}
}
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual - 6.0).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_mixed() {
let mut g = Graph::new(4, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
g.add_edge(2, 3).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual - 1.0).abs() < 1e-10);
assert!((dc.asymmetric - 1.0).abs() < 1e-10);
assert!((dc.null - 4.0).abs() < 1e-10);
}
#[test]
fn test_self_loops_ignored() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 1).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric - 1.0).abs() < 1e-10);
assert!((dc.null - 2.0).abs() < 1e-10);
}
#[test]
fn test_multi_edges_deduplicated() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual).abs() < 1e-10);
assert!((dc.asymmetric - 1.0).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_undirected_all_mutual() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual - 3.0).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null - 3.0).abs() < 1e-10);
}
#[test]
fn test_undirected_complete() {
let mut g = Graph::with_vertices(5);
for i in 0..5u32 {
for j in (i + 1)..5 {
g.add_edge(i, j).unwrap();
}
}
let dc = dyad_census(&g).unwrap();
assert!((dc.mutual - 10.0).abs() < 1e-10);
assert!((dc.asymmetric).abs() < 1e-10);
assert!((dc.null).abs() < 1e-10);
}
#[test]
fn test_counts_sum_to_total() {
let mut g = Graph::new(5, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 0).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 2).unwrap();
let dc = dyad_census(&g).unwrap();
let total = dc.mutual + dc.asymmetric + dc.null;
assert!((total - 10.0).abs() < 1e-10);
}
}