use crate::core::graph::{EdgeId, VertexId};
use crate::core::{Graph, IgraphError, IgraphResult};
#[derive(Debug, Clone, PartialEq)]
pub struct EdgelistPercolation {
pub giant_size: Vec<u32>,
pub vertex_count: Vec<u32>,
}
fn find_root(links: &mut [u32], mut v: usize) -> usize {
while links[v] as usize != v {
links[v] = links[links[v] as usize];
v = links[v] as usize;
}
v
}
pub fn edgelist_percolation(edges: &[(VertexId, VertexId)]) -> IgraphResult<EdgelistPercolation> {
let ecount = edges.len();
let mut giant_size: Vec<u32> = Vec::with_capacity(ecount);
let mut vertex_count: Vec<u32> = Vec::with_capacity(ecount);
if ecount == 0 {
return Ok(EdgelistPercolation {
giant_size,
vertex_count,
});
}
let max_id = edges.iter().flat_map(|&(u, v)| [u, v]).max().unwrap_or(0);
let vcount_u32 = max_id
.checked_add(1)
.ok_or(IgraphError::Internal("vertex count overflow"))?;
let vcount = vcount_u32 as usize;
let mut links: Vec<u32> = (0..vcount_u32).collect();
let mut sizes: Vec<u32> = vec![0; vcount];
let mut biggest: u32 = 1;
let mut vertices_added: u32 = 0;
for &(from, to) in edges {
let from_idx = from as usize;
let to_idx = to as usize;
if sizes[from_idx] == 0 {
sizes[from_idx] = 1;
vertices_added += 1;
}
if sizes[to_idx] == 0 {
sizes[to_idx] = 1;
if from_idx != to_idx {
vertices_added += 1;
}
}
if from_idx != to_idx {
let root_a = find_root(&mut links, from_idx);
let root_b = find_root(&mut links, to_idx);
if root_a != root_b {
let (parent, child) = if sizes[root_a] < sizes[root_b] {
(root_b, root_a)
} else {
(root_a, root_b)
};
let parent_u32 = u32::try_from(parent)
.map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
links[child] = parent_u32;
sizes[parent] = sizes[parent]
.checked_add(sizes[child])
.ok_or(IgraphError::Internal("union-find size overflow"))?;
if sizes[parent] > biggest {
biggest = sizes[parent];
}
}
}
giant_size.push(biggest);
vertex_count.push(vertices_added);
}
Ok(EdgelistPercolation {
giant_size,
vertex_count,
})
}
pub fn bond_percolation(graph: &Graph, edge_order: &[EdgeId]) -> IgraphResult<EdgelistPercolation> {
let m = graph.ecount();
let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
let mut seen = vec![false; m];
for &eid in edge_order {
if (eid as usize) >= m {
return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
}
if seen[eid as usize] {
return Err(IgraphError::InvalidArgument(format!(
"duplicate edge id {eid} in edge_order"
)));
}
seen[eid as usize] = true;
}
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_order.len());
for &eid in edge_order {
edges.push(graph.edge(eid)?);
}
edgelist_percolation(&edges)
}
#[derive(Debug, Clone, PartialEq)]
pub struct SitePercolation {
pub giant_size: Vec<u32>,
pub edge_count: Vec<u32>,
}
fn all_neighbors(graph: &Graph, v: VertexId) -> IgraphResult<Vec<VertexId>> {
if graph.is_directed() {
let out_eids = graph.incident(v)?;
let in_eids = graph.incident_in(v)?;
let mut nb = Vec::with_capacity(out_eids.len() + in_eids.len());
for eid in out_eids {
nb.push(graph.edge_other(eid, v)?);
}
for eid in in_eids {
nb.push(graph.edge_other(eid, v)?);
}
Ok(nb)
} else {
graph.neighbors(v)
}
}
pub fn site_percolation(graph: &Graph, vertex_order: &[VertexId]) -> IgraphResult<SitePercolation> {
let n = graph.vcount();
let n_us = n as usize;
let mut seen = vec![false; n_us];
for &vid in vertex_order {
if vid >= n {
return Err(IgraphError::VertexOutOfRange { id: vid, n });
}
if seen[vid as usize] {
return Err(IgraphError::InvalidArgument(format!(
"duplicate vertex id {vid} in vertex_order"
)));
}
seen[vid as usize] = true;
}
let mut giant_size: Vec<u32> = Vec::with_capacity(vertex_order.len());
let mut edge_count: Vec<u32> = Vec::with_capacity(vertex_order.len());
if vertex_order.is_empty() {
return Ok(SitePercolation {
giant_size,
edge_count,
});
}
let mut links: Vec<u32> = (0..n).collect();
let mut sizes: Vec<u32> = vec![0; n_us];
let mut biggest: u32 = 1;
let mut edges_added: u32 = 0;
for &vid in vertex_order {
let vid_idx = vid as usize;
sizes[vid_idx] = 1;
let neighbors = all_neighbors(graph, vid)?;
for nb in neighbors {
let nb_idx = nb as usize;
if sizes[nb_idx] == 0 {
continue;
}
edges_added = edges_added
.checked_add(1)
.ok_or(IgraphError::Internal("edge count overflow"))?;
let root_a = find_root(&mut links, vid_idx);
let root_b = find_root(&mut links, nb_idx);
if root_a != root_b {
let (parent, child) = if sizes[root_a] < sizes[root_b] {
(root_b, root_a)
} else {
(root_a, root_b)
};
let parent_u32 = u32::try_from(parent)
.map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
links[child] = parent_u32;
sizes[parent] = sizes[parent]
.checked_add(sizes[child])
.ok_or(IgraphError::Internal("union-find size overflow"))?;
if sizes[parent] > biggest {
biggest = sizes[parent];
}
}
}
giant_size.push(biggest);
edge_count.push(edges_added);
}
Ok(SitePercolation {
giant_size,
edge_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_returns_empty() {
let p = edgelist_percolation(&[]).unwrap();
assert!(p.giant_size.is_empty());
assert!(p.vertex_count.is_empty());
}
#[test]
fn single_edge_two_vertices() {
let p = edgelist_percolation(&[(0, 1)]).unwrap();
assert_eq!(p.giant_size, vec![2]);
assert_eq!(p.vertex_count, vec![2]);
}
#[test]
fn two_disjoint_edges_then_join() {
let p = edgelist_percolation(&[(0, 1), (2, 3), (1, 2)]).unwrap();
assert_eq!(p.giant_size, vec![2, 2, 4]);
assert_eq!(p.vertex_count, vec![2, 4, 4]);
}
#[test]
fn parallel_edge_does_not_change_giant() {
let p = edgelist_percolation(&[(0, 1), (0, 1)]).unwrap();
assert_eq!(p.giant_size, vec![2, 2]);
assert_eq!(p.vertex_count, vec![2, 2]);
}
#[test]
fn self_loop_only_adds_one_vertex() {
let p = edgelist_percolation(&[(0, 0)]).unwrap();
assert_eq!(p.giant_size, vec![1]);
assert_eq!(p.vertex_count, vec![1]);
}
#[test]
fn chain_grows_linearly() {
let p = edgelist_percolation(&[(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
assert_eq!(p.giant_size, vec![2, 3, 4, 5]);
assert_eq!(p.vertex_count, vec![2, 3, 4, 5]);
}
#[test]
fn star_around_center() {
let p = edgelist_percolation(&[(0, 1), (0, 2), (0, 3)]).unwrap();
assert_eq!(p.giant_size, vec![2, 3, 4]);
assert_eq!(p.vertex_count, vec![2, 3, 4]);
}
#[test]
fn merging_unequal_clusters_picks_max() {
let p = edgelist_percolation(&[(0, 1), (1, 2), (0, 2), (3, 4), (2, 3)]).unwrap();
assert_eq!(p.giant_size, vec![2, 3, 3, 3, 5]);
assert_eq!(p.vertex_count, vec![2, 3, 3, 5, 5]);
}
#[test]
fn classic_random_order_matches_hand_trace() {
let p = edgelist_percolation(&[(0, 1), (1, 2), (3, 4), (2, 3)]).unwrap();
assert_eq!(p.giant_size, vec![2, 3, 3, 5]);
assert_eq!(p.vertex_count, vec![2, 3, 5, 5]);
}
#[test]
fn high_vertex_ids_are_supported() {
let p = edgelist_percolation(&[(100, 200)]).unwrap();
assert_eq!(p.giant_size, vec![2]);
assert_eq!(p.vertex_count, vec![2]);
}
#[test]
fn bond_percolation_natural_order_matches_edgelist() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
let bond = bond_percolation(&g, &[0, 1, 2]).unwrap();
let direct = edgelist_percolation(&[(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
assert_eq!(bond, direct);
}
#[test]
fn bond_percolation_reordered_changes_curve() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (2, 3), (1, 2)]).unwrap();
let p = bond_percolation(&g, &[2, 0, 1]).unwrap();
assert_eq!(p.giant_size, vec![2, 3, 4]);
assert_eq!(p.vertex_count, vec![2, 3, 4]);
}
#[test]
fn bond_percolation_partial_order_only_processes_listed_edges() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (2, 3), (1, 2)]).unwrap();
let p = bond_percolation(&g, &[0, 2]).unwrap();
assert_eq!(p.giant_size, vec![2, 3]);
assert_eq!(p.vertex_count, vec![2, 3]);
}
#[test]
fn bond_percolation_empty_order_returns_empty() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let p = bond_percolation(&g, &[]).unwrap();
assert!(p.giant_size.is_empty());
assert!(p.vertex_count.is_empty());
}
#[test]
fn bond_percolation_duplicate_edge_id_errors() {
let mut g = Graph::with_vertices(3);
g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
let err = bond_percolation(&g, &[0, 0]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn bond_percolation_out_of_range_edge_id_errors() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let err = bond_percolation(&g, &[5]).unwrap_err();
assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 1 }));
}
#[test]
fn bond_percolation_directed_graph_ignores_direction() {
let mut g = Graph::new(3, true).unwrap();
g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
let p = bond_percolation(&g, &[0, 1]).unwrap();
assert_eq!(p.giant_size, vec![2, 3]);
assert_eq!(p.vertex_count, vec![2, 3]);
}
#[test]
fn bond_percolation_isolated_vertex_never_counted() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (0, 2)]).unwrap();
let p = bond_percolation(&g, &[0, 1]).unwrap();
assert_eq!(p.vertex_count, vec![2, 3]);
}
#[test]
fn site_percolation_chain_natural_order() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
let p = site_percolation(&g, &[0, 1, 2, 3]).unwrap();
assert_eq!(p.giant_size, vec![1, 2, 3, 4]);
assert_eq!(p.edge_count, vec![0, 1, 2, 3]);
}
#[test]
fn site_percolation_reverse_order_same_curve_shape() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
let p = site_percolation(&g, &[3, 2, 1, 0]).unwrap();
assert_eq!(p.giant_size, vec![1, 2, 3, 4]);
assert_eq!(p.edge_count, vec![0, 1, 2, 3]);
}
#[test]
fn site_percolation_isolated_vertex_doesnt_grow_giant() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
let p = site_percolation(&g, &[0, 1, 2, 3]).unwrap();
assert_eq!(p.giant_size, vec![1, 2, 2, 2]);
assert_eq!(p.edge_count, vec![0, 1, 1, 1]);
}
#[test]
fn site_percolation_triangle_jumps_giant_at_third_vertex() {
let mut g = Graph::with_vertices(3);
g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 2)]).unwrap();
let p = site_percolation(&g, &[0, 1, 2]).unwrap();
assert_eq!(p.giant_size, vec![1, 2, 3]);
assert_eq!(p.edge_count, vec![0, 1, 3]);
}
#[test]
fn site_percolation_partial_order_stops_early() {
let mut g = Graph::with_vertices(4);
g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
let p = site_percolation(&g, &[0, 1]).unwrap();
assert_eq!(p.giant_size, vec![1, 2]);
assert_eq!(p.edge_count, vec![0, 1]);
}
#[test]
fn site_percolation_empty_order_returns_empty() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let p = site_percolation(&g, &[]).unwrap();
assert!(p.giant_size.is_empty());
assert!(p.edge_count.is_empty());
}
#[test]
fn site_percolation_parallel_edges_count_separately() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
let p = site_percolation(&g, &[0, 1]).unwrap();
assert_eq!(p.giant_size, vec![1, 2]);
assert_eq!(p.edge_count, vec![0, 2]);
}
#[test]
fn site_percolation_self_loop_counts_twice_on_activation() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
let p = site_percolation(&g, &[0, 1]).unwrap();
assert_eq!(p.giant_size, vec![1, 2]);
assert_eq!(p.edge_count, vec![2, 3]);
}
#[test]
fn site_percolation_directed_graph_treats_both_directions() {
let mut g = Graph::new(3, true).unwrap();
g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
let p = site_percolation(&g, &[0, 1, 2]).unwrap();
assert_eq!(p.giant_size, vec![1, 2, 3]);
assert_eq!(p.edge_count, vec![0, 1, 2]);
}
#[test]
fn site_percolation_duplicate_vertex_id_errors() {
let g = Graph::with_vertices(3);
let err = site_percolation(&g, &[0, 0]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn site_percolation_out_of_range_vertex_errors() {
let g = Graph::with_vertices(3);
let err = site_percolation(&g, &[99]).unwrap_err();
assert!(matches!(
err,
IgraphError::VertexOutOfRange { id: 99, n: 3 }
));
}
}