Skip to main content

rust_igraph/algorithms/connectivity/
percolation.rs

1//! Edge-list percolation (ALGO-CC-030).
2//!
3//! Counterpart of `igraph_edgelist_percolation()` from
4//! `references/igraph/src/connectivity/percolation.c:105`. Given a
5//! sequence of vertex-pair edges, returns the evolution of the
6//! largest connected component as each edge is added in order.
7//!
8//! Algorithm: standard union-find with path compression and
9//! union-by-size. Each "add edge" call merges two trees; we track
10//! the size of the largest tree seen so far and the running count of
11//! vertices touched by any edge. Time complexity:
12//! `O(|E| · α(|E|))` where `α` is the inverse Ackermann function —
13//! amortised near-constant per operation.
14//!
15//! Both outputs are always populated (the C API makes them
16//! independently optional; in Rust the marginal cost of always
17//! returning both is one extra `Vec<u32>` alloc).
18
19use crate::core::graph::{EdgeId, VertexId};
20use crate::core::{Graph, IgraphError, IgraphResult};
21
22/// Outputs of [`edgelist_percolation`]. Same length as the input
23/// edge slice. `giant_size[i]` is the size of the largest component
24/// after edge `i` is added; `vertex_count[i]` is the number of
25/// distinct vertices touched by edges `0..=i`.
26#[derive(Debug, Clone, PartialEq)]
27pub struct EdgelistPercolation {
28    /// Size of the largest connected component after edge `i` is added.
29    pub giant_size: Vec<u32>,
30    /// Cumulative count of distinct vertices touched by any edge in `0..=i`.
31    pub vertex_count: Vec<u32>,
32}
33
34/// `links[v]` walks toward the root of `v`'s union-find tree (each
35/// node's parent), with path compression along the way. Returns the
36/// representative.
37fn find_root(links: &mut [u32], mut v: usize) -> usize {
38    while links[v] as usize != v {
39        // Path compression: halve the path by linking each node to
40        // its grandparent (matches the upstream `links[a] =
41        // links[links[a]]` line).
42        links[v] = links[links[v] as usize];
43        v = links[v] as usize;
44    }
45    v
46}
47
48/// Percolation curve as a sequence of vertex-pair edges is added.
49///
50/// Returns an [`EdgelistPercolation`] with two vectors, both of
51/// length `edges.len()`:
52/// - `giant_size[i]`: size of the largest component after edge `i`
53///   is added (1 if no edges or the first edge connects two new
54///   vertices).
55/// - `vertex_count[i]`: number of distinct vertices touched by any
56///   edge in `0..=i`.
57///
58/// Vertex ids are inferred from the edge list — the implicit vertex
59/// count is `max(max(u, v) for (u, v) in edges) + 1`. Endpoints may
60/// be self-loops (a self-loop adds nothing to either output).
61///
62/// Returns [`IgraphError::InvalidArgument`] if any vertex id exceeds
63/// `i32::MAX` (matches upstream's `IGRAPH_EINVVID` semantics).
64///
65/// Counterpart of `igraph_edgelist_percolation` from
66/// `references/igraph/src/connectivity/percolation.c:105`.
67///
68/// # Examples
69///
70/// ```
71/// use rust_igraph::edgelist_percolation;
72///
73/// // Adding edges (0-1), (2-3), (1-2): the giant grows 2, 2, 4.
74/// let edges = [(0u32, 1u32), (2, 3), (1, 2)];
75/// let p = edgelist_percolation(&edges).unwrap();
76/// assert_eq!(p.giant_size, vec![2, 2, 4]);
77/// assert_eq!(p.vertex_count, vec![2, 4, 4]);
78/// ```
79pub fn edgelist_percolation(edges: &[(VertexId, VertexId)]) -> IgraphResult<EdgelistPercolation> {
80    let ecount = edges.len();
81    let mut giant_size: Vec<u32> = Vec::with_capacity(ecount);
82    let mut vertex_count: Vec<u32> = Vec::with_capacity(ecount);
83
84    if ecount == 0 {
85        return Ok(EdgelistPercolation {
86            giant_size,
87            vertex_count,
88        });
89    }
90
91    // Implicit vertex count = max id seen + 1. `max_id` is u32; the
92    // +1 is checked to catch the (extremely unlikely) u32::MAX case.
93    let max_id = edges.iter().flat_map(|&(u, v)| [u, v]).max().unwrap_or(0);
94    let vcount_u32 = max_id
95        .checked_add(1)
96        .ok_or(IgraphError::Internal("vertex count overflow"))?;
97    let vcount = vcount_u32 as usize;
98
99    // Union-find: `links[v]` is v's parent (self if root); `sizes[v]`
100    // is the size of v's tree (only meaningful at roots), with -1
101    // sentinel encoded as 0 here meaning "not yet touched".
102    let mut links: Vec<u32> = (0..vcount_u32).collect();
103    let mut sizes: Vec<u32> = vec![0; vcount];
104
105    let mut biggest: u32 = 1;
106    let mut vertices_added: u32 = 0;
107
108    for &(from, to) in edges {
109        let from_idx = from as usize;
110        let to_idx = to as usize;
111        if sizes[from_idx] == 0 {
112            sizes[from_idx] = 1;
113            vertices_added += 1;
114        }
115        if sizes[to_idx] == 0 {
116            sizes[to_idx] = 1;
117            // Only count `to` if distinct from `from` (self-loop case).
118            if from_idx != to_idx {
119                vertices_added += 1;
120            }
121        }
122        // Union if they're not already connected. Self-loop is a no-op.
123        if from_idx != to_idx {
124            let root_a = find_root(&mut links, from_idx);
125            let root_b = find_root(&mut links, to_idx);
126            if root_a != root_b {
127                // Union-by-size: attach smaller under larger.
128                let (parent, child) = if sizes[root_a] < sizes[root_b] {
129                    (root_b, root_a)
130                } else {
131                    (root_a, root_b)
132                };
133                let parent_u32 = u32::try_from(parent)
134                    .map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
135                links[child] = parent_u32;
136                sizes[parent] = sizes[parent]
137                    .checked_add(sizes[child])
138                    .ok_or(IgraphError::Internal("union-find size overflow"))?;
139                if sizes[parent] > biggest {
140                    biggest = sizes[parent];
141                }
142            }
143        }
144        giant_size.push(biggest);
145        vertex_count.push(vertices_added);
146    }
147
148    Ok(EdgelistPercolation {
149        giant_size,
150        vertex_count,
151    })
152}
153
154/// Bond percolation: the size of the largest component as edges of
155/// `graph` are added in the order specified by `edge_order`.
156///
157/// Returns an [`EdgelistPercolation`] with two vectors, both of
158/// length `edge_order.len()` — same shape as
159/// [`edgelist_percolation`], just resolved from edge ids instead of
160/// raw vertex pairs.
161///
162/// `edge_order[i]` must be a valid edge id (`< graph.ecount()`) and
163/// must not repeat — duplicates return
164/// [`IgraphError::InvalidArgument`]; out-of-range ids return
165/// [`IgraphError::EdgeOutOfRange`].
166///
167/// Edge direction is ignored (matches upstream — percolation reads
168/// edges as undirected connection events).
169///
170/// To shuffle into a random order, generate the permutation with
171/// your own RNG and pass it as `edge_order`. We don't take an RNG
172/// here to keep the API deterministic and to avoid pulling in a
173/// `rand` dependency for callers who already have one.
174///
175/// Counterpart of `igraph_bond_percolation` from
176/// `references/igraph/src/connectivity/percolation.c:214`.
177///
178/// # Examples
179///
180/// ```
181/// use rust_igraph::{Graph, bond_percolation};
182///
183/// // Path 0-1-2-3 with edges added in natural id order.
184/// let mut g = Graph::with_vertices(4);
185/// g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
186/// let p = bond_percolation(&g, &[0, 1, 2]).unwrap();
187/// assert_eq!(p.giant_size, vec![2, 3, 4]);
188/// assert_eq!(p.vertex_count, vec![2, 3, 4]);
189/// ```
190pub fn bond_percolation(graph: &Graph, edge_order: &[EdgeId]) -> IgraphResult<EdgelistPercolation> {
191    let m = graph.ecount();
192    let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
193
194    // Validate up front (matches upstream's two-pass approach): no
195    // duplicates and every id in range. We use a bool bitset over
196    // the graph's edge ids — `edge_order.len()` may exceed `m`
197    // worst-case but a duplicate or oob id will trip the check.
198    let mut seen = vec![false; m];
199    for &eid in edge_order {
200        if (eid as usize) >= m {
201            return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
202        }
203        if seen[eid as usize] {
204            return Err(IgraphError::InvalidArgument(format!(
205                "duplicate edge id {eid} in edge_order"
206            )));
207        }
208        seen[eid as usize] = true;
209    }
210
211    // Resolve each edge id → (u, v) endpoint pair, then delegate.
212    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_order.len());
213    for &eid in edge_order {
214        edges.push(graph.edge(eid)?);
215    }
216    edgelist_percolation(&edges)
217}
218
219// ---------------------------------------------------------------
220// ALGO-CC-032: site percolation.
221// ---------------------------------------------------------------
222
223/// Outputs of [`site_percolation`]. Same length as `vertex_order`.
224/// `giant_size[i]` is the size of the largest connected component
225/// after vertex `vertex_order[i]` is activated; `edge_count[i]` is
226/// the cumulative number of edges added through step `i` (an edge
227/// is "added" the moment both its endpoints become active).
228#[derive(Debug, Clone, PartialEq)]
229pub struct SitePercolation {
230    /// Size of the largest connected component after the i-th vertex
231    /// is activated.
232    pub giant_size: Vec<u32>,
233    /// Cumulative count of edges activated through step i. An edge
234    /// `(u, v)` activates the moment its second endpoint joins the
235    /// active set. Self-loops on the just-activated vertex and
236    /// parallel edges each count separately (matches upstream's
237    /// `IGRAPH_LOOPS | IGRAPH_MULTIPLE` neighbor enumeration).
238    pub edge_count: Vec<u32>,
239}
240
241/// Per-vertex "all-neighbor" lookup. Mirrors upstream's
242/// `igraph_neighbors(graph, vertex, IGRAPH_ALL, IGRAPH_LOOPS,
243/// IGRAPH_MULTIPLE)`: parallel edges produce duplicates, self-loops
244/// appear twice.
245fn all_neighbors(graph: &Graph, v: VertexId) -> IgraphResult<Vec<VertexId>> {
246    if graph.is_directed() {
247        let out_eids = graph.incident(v)?;
248        let in_eids = graph.incident_in(v)?;
249        let mut nb = Vec::with_capacity(out_eids.len() + in_eids.len());
250        for eid in out_eids {
251            nb.push(graph.edge_other(eid, v)?);
252        }
253        for eid in in_eids {
254            nb.push(graph.edge_other(eid, v)?);
255        }
256        Ok(nb)
257    } else {
258        graph.neighbors(v)
259    }
260}
261
262/// Site percolation: the size of the largest component as vertices
263/// (sites) of `graph` are activated in the order specified by
264/// `vertex_order`.
265///
266/// Each activation step:
267/// 1. Mark the vertex active.
268/// 2. For every neighbor that is already active, add the connecting
269///    edge and union the two trees.
270/// 3. Record `giant_size` and `edge_count` after the step.
271///
272/// `vertex_order` must contain valid ids (`< graph.vcount()`) and
273/// must not repeat — duplicates return
274/// [`IgraphError::InvalidArgument`]; out-of-range ids return
275/// [`IgraphError::VertexOutOfRange`]. The order may be a strict
276/// subset of all vertices (skip = "never activated").
277///
278/// Edge direction is ignored (matches upstream — percolation is
279/// purely a connectivity model). Parallel edges count separately;
280/// self-loops on the just-activated vertex contribute two to
281/// `edge_count` (the loop appears twice in the all-neighbor walk
282/// for `IGRAPH_LOOPS` semantics).
283///
284/// To shuffle into a random order, generate the permutation with
285/// your own RNG. We don't take an RNG to keep the API deterministic
286/// and dependency-free.
287///
288/// Counterpart of `igraph_site_percolation` from
289/// `references/igraph/src/connectivity/percolation.c:328`.
290///
291/// # Examples
292///
293/// ```
294/// use rust_igraph::{Graph, site_percolation};
295///
296/// // Path 0-1-2-3, activate vertices in id order.
297/// let mut g = Graph::with_vertices(4);
298/// g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
299/// let p = site_percolation(&g, &[0, 1, 2, 3]).unwrap();
300/// // Activating 0: no edges added (no active neighbors), giant = 1.
301/// // Activating 1: edge (0,1) activates, giant = 2.
302/// // Activating 2: edge (1,2) activates, giant = 3.
303/// // Activating 3: edge (2,3) activates, giant = 4.
304/// assert_eq!(p.giant_size, vec![1, 2, 3, 4]);
305/// assert_eq!(p.edge_count, vec![0, 1, 2, 3]);
306/// ```
307pub fn site_percolation(graph: &Graph, vertex_order: &[VertexId]) -> IgraphResult<SitePercolation> {
308    let n = graph.vcount();
309    let n_us = n as usize;
310
311    // Validate up front: every id in range, no duplicates.
312    let mut seen = vec![false; n_us];
313    for &vid in vertex_order {
314        if vid >= n {
315            return Err(IgraphError::VertexOutOfRange { id: vid, n });
316        }
317        if seen[vid as usize] {
318            return Err(IgraphError::InvalidArgument(format!(
319                "duplicate vertex id {vid} in vertex_order"
320            )));
321        }
322        seen[vid as usize] = true;
323    }
324
325    let mut giant_size: Vec<u32> = Vec::with_capacity(vertex_order.len());
326    let mut edge_count: Vec<u32> = Vec::with_capacity(vertex_order.len());
327
328    if vertex_order.is_empty() {
329        return Ok(SitePercolation {
330            giant_size,
331            edge_count,
332        });
333    }
334
335    // Union-find: links[v] = parent (self if root); sizes[v] is the
336    // tree size at roots, with 0 meaning "not yet activated".
337    let mut links: Vec<u32> = (0..n).collect();
338    let mut sizes: Vec<u32> = vec![0; n_us];
339
340    let mut biggest: u32 = 1;
341    let mut edges_added: u32 = 0;
342
343    for &vid in vertex_order {
344        let vid_idx = vid as usize;
345        // Activate.
346        sizes[vid_idx] = 1;
347
348        // Walk neighbors; union with every already-activated one.
349        let neighbors = all_neighbors(graph, vid)?;
350        for nb in neighbors {
351            let nb_idx = nb as usize;
352            if sizes[nb_idx] == 0 {
353                continue;
354            }
355            edges_added = edges_added
356                .checked_add(1)
357                .ok_or(IgraphError::Internal("edge count overflow"))?;
358            // Same union-find core as edgelist_percolation.
359            let root_a = find_root(&mut links, vid_idx);
360            let root_b = find_root(&mut links, nb_idx);
361            if root_a != root_b {
362                let (parent, child) = if sizes[root_a] < sizes[root_b] {
363                    (root_b, root_a)
364                } else {
365                    (root_a, root_b)
366                };
367                let parent_u32 = u32::try_from(parent)
368                    .map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
369                links[child] = parent_u32;
370                sizes[parent] = sizes[parent]
371                    .checked_add(sizes[child])
372                    .ok_or(IgraphError::Internal("union-find size overflow"))?;
373                if sizes[parent] > biggest {
374                    biggest = sizes[parent];
375                }
376            }
377        }
378
379        giant_size.push(biggest);
380        edge_count.push(edges_added);
381    }
382
383    Ok(SitePercolation {
384        giant_size,
385        edge_count,
386    })
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn empty_input_returns_empty() {
395        let p = edgelist_percolation(&[]).unwrap();
396        assert!(p.giant_size.is_empty());
397        assert!(p.vertex_count.is_empty());
398    }
399
400    #[test]
401    fn single_edge_two_vertices() {
402        let p = edgelist_percolation(&[(0, 1)]).unwrap();
403        assert_eq!(p.giant_size, vec![2]);
404        assert_eq!(p.vertex_count, vec![2]);
405    }
406
407    #[test]
408    fn two_disjoint_edges_then_join() {
409        // (0-1), (2-3), (1-2): two separate pairs of size 2, then a
410        // chain of 4.
411        let p = edgelist_percolation(&[(0, 1), (2, 3), (1, 2)]).unwrap();
412        assert_eq!(p.giant_size, vec![2, 2, 4]);
413        assert_eq!(p.vertex_count, vec![2, 4, 4]);
414    }
415
416    #[test]
417    fn parallel_edge_does_not_change_giant() {
418        // Same edge added twice — second one is a no-op for both metrics.
419        let p = edgelist_percolation(&[(0, 1), (0, 1)]).unwrap();
420        assert_eq!(p.giant_size, vec![2, 2]);
421        assert_eq!(p.vertex_count, vec![2, 2]);
422    }
423
424    #[test]
425    fn self_loop_only_adds_one_vertex() {
426        // Self-loop on vertex 0: 1 new vertex, biggest stays 1.
427        let p = edgelist_percolation(&[(0, 0)]).unwrap();
428        assert_eq!(p.giant_size, vec![1]);
429        assert_eq!(p.vertex_count, vec![1]);
430    }
431
432    #[test]
433    fn chain_grows_linearly() {
434        // 0-1, 1-2, 2-3, 3-4 → giants 2, 3, 4, 5.
435        let p = edgelist_percolation(&[(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
436        assert_eq!(p.giant_size, vec![2, 3, 4, 5]);
437        assert_eq!(p.vertex_count, vec![2, 3, 4, 5]);
438    }
439
440    #[test]
441    fn star_around_center() {
442        // 0-1, 0-2, 0-3 → star, giant grows 2, 3, 4.
443        let p = edgelist_percolation(&[(0, 1), (0, 2), (0, 3)]).unwrap();
444        assert_eq!(p.giant_size, vec![2, 3, 4]);
445        assert_eq!(p.vertex_count, vec![2, 3, 4]);
446    }
447
448    #[test]
449    fn merging_unequal_clusters_picks_max() {
450        // Build a triangle (0-1, 1-2, 0-2) → size 3, then join to a
451        // pair (3-4) by adding (2-3). Final giant = 5.
452        let p = edgelist_percolation(&[(0, 1), (1, 2), (0, 2), (3, 4), (2, 3)]).unwrap();
453        // After (0,1): {0,1} giant=2
454        // After (1,2): {0,1,2} giant=3
455        // After (0,2): same tree (no-op), giant=3
456        // After (3,4): {3,4} new component, giant stays 3
457        // After (2,3): merge → giant=5
458        assert_eq!(p.giant_size, vec![2, 3, 3, 3, 5]);
459        assert_eq!(p.vertex_count, vec![2, 3, 3, 5, 5]);
460    }
461
462    #[test]
463    fn classic_random_order_matches_hand_trace() {
464        // A small example chosen to exercise union-by-size: build a
465        // tree of size 3 first (0-1, 1-2), then a single edge (3-4),
466        // then bridge 2-3. Without union-by-size the leftmost tree
467        // would dominate; with it, the (parent, child) choice is
468        // size-driven. Either way the GIANT (max size) should be 5.
469        let p = edgelist_percolation(&[(0, 1), (1, 2), (3, 4), (2, 3)]).unwrap();
470        assert_eq!(p.giant_size, vec![2, 3, 3, 5]);
471        assert_eq!(p.vertex_count, vec![2, 3, 5, 5]);
472    }
473
474    #[test]
475    fn high_vertex_ids_are_supported() {
476        // Sparse ids: only vertices 100 and 200 touched. Implicit
477        // vcount = 201 (huge sizes vector but cheap).
478        let p = edgelist_percolation(&[(100, 200)]).unwrap();
479        assert_eq!(p.giant_size, vec![2]);
480        assert_eq!(p.vertex_count, vec![2]);
481    }
482
483    // -------- ALGO-CC-031: bond_percolation (graph + edge order) --------
484
485    #[test]
486    fn bond_percolation_natural_order_matches_edgelist() {
487        // Chain 0-1-2-3 added in id order: bond_percolation must
488        // produce the same curve as the equivalent edgelist call.
489        let mut g = Graph::with_vertices(4);
490        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
491        let bond = bond_percolation(&g, &[0, 1, 2]).unwrap();
492        let direct = edgelist_percolation(&[(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
493        assert_eq!(bond, direct);
494    }
495
496    #[test]
497    fn bond_percolation_reordered_changes_curve() {
498        // Same graph, but adding the bridge first: middle vertex
499        // counts grow differently.
500        let mut g = Graph::with_vertices(4);
501        g.add_edges(vec![(0u32, 1u32), (2, 3), (1, 2)]).unwrap();
502        // Order [2, 0, 1]: first add (1,2) → {1,2}; then (0,1) → {0,1,2}; then (2,3) → {0,1,2,3}.
503        let p = bond_percolation(&g, &[2, 0, 1]).unwrap();
504        assert_eq!(p.giant_size, vec![2, 3, 4]);
505        assert_eq!(p.vertex_count, vec![2, 3, 4]);
506    }
507
508    #[test]
509    fn bond_percolation_partial_order_only_processes_listed_edges() {
510        // Graph has 3 edges; order lists only 2 → result reflects
511        // only those two additions.
512        let mut g = Graph::with_vertices(4);
513        g.add_edges(vec![(0u32, 1u32), (2, 3), (1, 2)]).unwrap();
514        let p = bond_percolation(&g, &[0, 2]).unwrap();
515        // (0,1) then (1,2) → giants 2, 3; touched 2, 3.
516        assert_eq!(p.giant_size, vec![2, 3]);
517        assert_eq!(p.vertex_count, vec![2, 3]);
518    }
519
520    #[test]
521    fn bond_percolation_empty_order_returns_empty() {
522        let mut g = Graph::with_vertices(3);
523        g.add_edge(0, 1).unwrap();
524        let p = bond_percolation(&g, &[]).unwrap();
525        assert!(p.giant_size.is_empty());
526        assert!(p.vertex_count.is_empty());
527    }
528
529    #[test]
530    fn bond_percolation_duplicate_edge_id_errors() {
531        let mut g = Graph::with_vertices(3);
532        g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
533        let err = bond_percolation(&g, &[0, 0]).unwrap_err();
534        assert!(matches!(err, IgraphError::InvalidArgument(_)));
535    }
536
537    #[test]
538    fn bond_percolation_out_of_range_edge_id_errors() {
539        let mut g = Graph::with_vertices(3);
540        g.add_edge(0, 1).unwrap();
541        let err = bond_percolation(&g, &[5]).unwrap_err();
542        assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 1 }));
543    }
544
545    #[test]
546    fn bond_percolation_directed_graph_ignores_direction() {
547        // Edge direction must not affect percolation — same curve as
548        // the equivalent undirected version.
549        let mut g = Graph::new(3, true).unwrap();
550        g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
551        let p = bond_percolation(&g, &[0, 1]).unwrap();
552        assert_eq!(p.giant_size, vec![2, 3]);
553        assert_eq!(p.vertex_count, vec![2, 3]);
554    }
555
556    #[test]
557    fn bond_percolation_isolated_vertex_never_counted() {
558        // Graph has 4 vertices but only 2 are touched by edges in the
559        // given order. Vertex 3 stays isolated, never contributes to
560        // vertex_count.
561        let mut g = Graph::with_vertices(4);
562        g.add_edges(vec![(0u32, 1u32), (0, 2)]).unwrap();
563        let p = bond_percolation(&g, &[0, 1]).unwrap();
564        assert_eq!(p.vertex_count, vec![2, 3]);
565    }
566
567    // -------- ALGO-CC-032: site_percolation (vertex activation) --------
568
569    #[test]
570    fn site_percolation_chain_natural_order() {
571        // Path 0-1-2-3, activate in id order.
572        let mut g = Graph::with_vertices(4);
573        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
574        let p = site_percolation(&g, &[0, 1, 2, 3]).unwrap();
575        assert_eq!(p.giant_size, vec![1, 2, 3, 4]);
576        assert_eq!(p.edge_count, vec![0, 1, 2, 3]);
577    }
578
579    #[test]
580    fn site_percolation_reverse_order_same_curve_shape() {
581        // Path 0-1-2-3, activate in reverse: 3, 2, 1, 0.
582        // Activate 3: no active neighbor, edges=0, giant=1.
583        // Activate 2: active nb {3}, edges=1, giant=2.
584        // Activate 1: active nb {2}, edges=2, giant=3.
585        // Activate 0: active nb {1}, edges=3, giant=4.
586        let mut g = Graph::with_vertices(4);
587        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
588        let p = site_percolation(&g, &[3, 2, 1, 0]).unwrap();
589        assert_eq!(p.giant_size, vec![1, 2, 3, 4]);
590        assert_eq!(p.edge_count, vec![0, 1, 2, 3]);
591    }
592
593    #[test]
594    fn site_percolation_isolated_vertex_doesnt_grow_giant() {
595        // 4-vertex graph: only one edge (0,1); activating 2 or 3
596        // doesn't connect anything.
597        let mut g = Graph::with_vertices(4);
598        g.add_edge(0, 1).unwrap();
599        let p = site_percolation(&g, &[0, 1, 2, 3]).unwrap();
600        // After 0: giant=1, edges=0
601        // After 1: giant=2, edges=1 (edge (0,1) activates)
602        // After 2: giant=2, edges=1 (vertex 2 isolated)
603        // After 3: giant=2, edges=1 (vertex 3 isolated)
604        assert_eq!(p.giant_size, vec![1, 2, 2, 2]);
605        assert_eq!(p.edge_count, vec![0, 1, 1, 1]);
606    }
607
608    #[test]
609    fn site_percolation_triangle_jumps_giant_at_third_vertex() {
610        // Triangle: activate 0, 1, 2.
611        // 0: giant=1, edges=0
612        // 1: 1 connects to 0; giant=2, edges=1
613        // 2: 2 connects to 0 and 1 (both active); edges=3, giant=3.
614        let mut g = Graph::with_vertices(3);
615        g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 2)]).unwrap();
616        let p = site_percolation(&g, &[0, 1, 2]).unwrap();
617        assert_eq!(p.giant_size, vec![1, 2, 3]);
618        assert_eq!(p.edge_count, vec![0, 1, 3]);
619    }
620
621    #[test]
622    fn site_percolation_partial_order_stops_early() {
623        // Only activate first 2 vertices of a 4-vertex graph.
624        let mut g = Graph::with_vertices(4);
625        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
626        let p = site_percolation(&g, &[0, 1]).unwrap();
627        assert_eq!(p.giant_size, vec![1, 2]);
628        assert_eq!(p.edge_count, vec![0, 1]);
629    }
630
631    #[test]
632    fn site_percolation_empty_order_returns_empty() {
633        let mut g = Graph::with_vertices(3);
634        g.add_edge(0, 1).unwrap();
635        let p = site_percolation(&g, &[]).unwrap();
636        assert!(p.giant_size.is_empty());
637        assert!(p.edge_count.is_empty());
638    }
639
640    #[test]
641    fn site_percolation_parallel_edges_count_separately() {
642        // Two parallel edges between 0 and 1: activating both yields
643        // edge_count = 2 (each parallel edge counted).
644        let mut g = Graph::with_vertices(2);
645        g.add_edge(0, 1).unwrap();
646        g.add_edge(0, 1).unwrap();
647        let p = site_percolation(&g, &[0, 1]).unwrap();
648        assert_eq!(p.giant_size, vec![1, 2]);
649        assert_eq!(p.edge_count, vec![0, 2]);
650    }
651
652    #[test]
653    fn site_percolation_self_loop_counts_twice_on_activation() {
654        // Self-loop on 0 + edge (0,1). Activate 0 first: self-loop
655        // appears twice in undirected all-neighbors → edges += 2;
656        // the unions are no-ops (same root).
657        let mut g = Graph::with_vertices(2);
658        g.add_edge(0, 0).unwrap();
659        g.add_edge(0, 1).unwrap();
660        let p = site_percolation(&g, &[0, 1]).unwrap();
661        // After 0: no active neighbors yet — but undirected loop?
662        // Sequence: activate 0, walk neighbors of 0 = [0, 0] (loop
663        // twice). For each, sizes[0]==1 (just set) → edges += 2.
664        // Unions are no-ops. giant=1, edges=2.
665        // After 1: neighbor 0 active → edges += 1 = 3; union {0}, {1}
666        // → giant=2.
667        assert_eq!(p.giant_size, vec![1, 2]);
668        assert_eq!(p.edge_count, vec![2, 3]);
669    }
670
671    #[test]
672    fn site_percolation_directed_graph_treats_both_directions() {
673        // Directed 0→1, 1→2: activating 0,1,2 in order treats them
674        // as undirected for percolation (in-edges count too).
675        let mut g = Graph::new(3, true).unwrap();
676        g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
677        let p = site_percolation(&g, &[0, 1, 2]).unwrap();
678        assert_eq!(p.giant_size, vec![1, 2, 3]);
679        assert_eq!(p.edge_count, vec![0, 1, 2]);
680    }
681
682    #[test]
683    fn site_percolation_duplicate_vertex_id_errors() {
684        let g = Graph::with_vertices(3);
685        let err = site_percolation(&g, &[0, 0]).unwrap_err();
686        assert!(matches!(err, IgraphError::InvalidArgument(_)));
687    }
688
689    #[test]
690    fn site_percolation_out_of_range_vertex_errors() {
691        let g = Graph::with_vertices(3);
692        let err = site_percolation(&g, &[99]).unwrap_err();
693        assert!(matches!(
694            err,
695            IgraphError::VertexOutOfRange { id: 99, n: 3 }
696        ));
697    }
698}