Skip to main content

rust_igraph/algorithms/properties/
neighborhood.rs

1//! ALGO-PR-027 / PR-027b — BFS-based k-hop neighbourhoods.
2//!
3//! - [`neighborhood_size`] / [`neighborhood_size_with_mode`] (PR-027):
4//!   for every vertex `v` return *how many* vertices `w` satisfy
5//!   `mindist <= dist(v, w) <= order`.
6//! - [`neighborhood`] / [`neighborhood_with_mode`] (PR-027b): for every
7//!   vertex `v` return *the list of* such vertices, in BFS visitation
8//!   order (matching `igraph_neighborhood()` from
9//!   `references/igraph/src/properties/neighborhood.c:208-303`).
10//!
11//! `dist` is unweighted graph distance; `order < 0` is treated as
12//! infinity; `mode` is ignored on undirected graphs.
13
14use std::collections::VecDeque;
15
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18/// Direction mode for `neighborhood_size_with_mode` on directed graphs.
19/// Ignored on undirected graphs — every mode reduces to [`NeighborhoodMode::All`].
20///
21/// Counterpart of `igraph_neimode_t` (`include/igraph_constants.h`).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum NeighborhoodMode {
24    /// Follow outgoing edges only (`IGRAPH_OUT`). For each source `v`,
25    /// counts vertices reachable by following out-edges.
26    Out,
27    /// Follow incoming edges only (`IGRAPH_IN`). For each source `v`,
28    /// counts vertices that can reach `v` by following out-edges (i.e.
29    /// reachable from `v` along reversed edges).
30    In,
31    /// Ignore direction — treat every edge as bidirectional
32    /// (`IGRAPH_ALL`).
33    All,
34}
35
36/// k-hop neighbourhood size for every vertex (`mode = All`, `mindist = 0`).
37///
38/// For each vertex `v` returns the number of vertices within `order`
39/// hops (inclusive), counting `v` itself. Negative `order` means
40/// infinity (every reachable vertex is counted).
41///
42/// Counterpart of `igraph_neighborhood_size(graph, _, igraph_vss_all(),
43/// order, IGRAPH_ALL, /*mindist=*/0)`.
44///
45/// # Errors
46/// - [`IgraphError::InvalidArgument`] if `order >= 0` but `order` cannot
47///   be represented as a non-negative integer (always satisfied for
48///   `i32 >= 0`, so this can only fail via the with-mode variant when
49///   `mindist > order`).
50///
51/// # Examples
52/// ```
53/// use rust_igraph::{Graph, neighborhood_size};
54///
55/// // Path P5: 0-1-2-3-4
56/// let mut g = Graph::with_vertices(5);
57/// for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
58///     g.add_edge(u, v).unwrap();
59/// }
60/// // Order 1: self + immediate neighbours.
61/// assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 3, 3, 2]);
62/// // Order 2: self + 2-hop ball.
63/// assert_eq!(neighborhood_size(&g, 2).unwrap(), vec![3, 4, 5, 4, 3]);
64/// ```
65pub fn neighborhood_size(graph: &Graph, order: i32) -> IgraphResult<Vec<u32>> {
66    neighborhood_size_with_mode(graph, order, NeighborhoodMode::All, 0)
67}
68
69/// Full mode-aware k-hop neighbourhood size with `mindist` filter.
70///
71/// For each source vertex `v` returns the number of vertices `w` such
72/// that `mindist <= dist(v, w) <= order` (or `dist(v, w) >= mindist`
73/// when `order < 0`, treating order as infinity). Direction follows
74/// `mode` on directed graphs and is ignored on undirected graphs.
75///
76/// `mindist = 0` includes `v` itself; `mindist = 1` excludes `v` but
77/// counts immediate neighbours; `mindist = k` excludes vertices reached
78/// in fewer than `k` hops.
79///
80/// Counterpart of `igraph_neighborhood_size(graph, _, igraph_vss_all(),
81/// order, mode, mindist)`.
82///
83/// # Errors
84/// - [`IgraphError::InvalidArgument`] if `mindist < 0`.
85/// - [`IgraphError::InvalidArgument`] if `order >= 0` and `mindist > order`.
86///
87/// # Examples
88/// ```
89/// use rust_igraph::{Graph, neighborhood_size_with_mode, NeighborhoodMode};
90///
91/// // Directed star: 0->1, 0->2, 0->3.
92/// let mut g = Graph::new(4, true).unwrap();
93/// for v in [1, 2, 3] { g.add_edge(0, v).unwrap(); }
94///
95/// // Out: 0 reaches all; leaves only see themselves.
96/// assert_eq!(
97///     neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap(),
98///     vec![4, 1, 1, 1]
99/// );
100/// // In: leaves can reach 0 via reversed edges (in-mode walks against arc).
101/// assert_eq!(
102///     neighborhood_size_with_mode(&g, -1, NeighborhoodMode::In, 0).unwrap(),
103///     vec![1, 2, 2, 2]
104/// );
105/// // mindist=1 excludes the vertex itself.
106/// assert_eq!(
107///     neighborhood_size_with_mode(&g, 1, NeighborhoodMode::All, 1).unwrap(),
108///     vec![3, 1, 1, 1]
109/// );
110/// ```
111pub fn neighborhood_size_with_mode(
112    graph: &Graph,
113    order: i32,
114    mode: NeighborhoodMode,
115    mindist: i32,
116) -> IgraphResult<Vec<u32>> {
117    if mindist < 0 {
118        return Err(IgraphError::InvalidArgument(format!(
119            "minimum distance must not be negative, got {mindist}"
120        )));
121    }
122    if order >= 0 && mindist > order {
123        return Err(IgraphError::InvalidArgument(format!(
124            "minimum distance must not exceed neighbourhood order ({order}), got {mindist}"
125        )));
126    }
127
128    let n = graph.vcount();
129    if n == 0 {
130        return Ok(Vec::new());
131    }
132    let n_us = n as usize;
133
134    // C uses `order = no_of_nodes` when negative — effectively infinite
135    // because BFS depth is bounded by n-1. We model the same way using
136    // i64 to avoid sign-mixing on the comparisons inside the loop.
137    let inf_order = order < 0;
138    let effective_order: i64 = if inf_order {
139        i64::from(n)
140    } else {
141        i64::from(order)
142    };
143    let mindist_i64 = i64::from(mindist);
144
145    let directed = graph.is_directed();
146    // `added[v] = src + 1` marks "v has been seen by source `src`",
147    // matching the C reference (avoids per-source array allocation).
148    let mut added: Vec<u32> = vec![0; n_us];
149    let mut queue: VecDeque<(VertexId, i64)> = VecDeque::new();
150    let mut result: Vec<u32> = vec![0; n_us];
151
152    for src in 0..n {
153        let marker = src + 1;
154        added[src as usize] = marker;
155        let mut size: u32 = u32::from(mindist_i64 == 0);
156        queue.clear();
157        if effective_order > 0 {
158            queue.push_back((src, 0));
159        }
160
161        while let Some((actnode, actdist)) = queue.pop_front() {
162            let neis = neighbours_for(graph, actnode, mode, directed)?;
163            if actdist < effective_order - 1 {
164                for nei in neis {
165                    if added[nei as usize] != marker {
166                        added[nei as usize] = marker;
167                        queue.push_back((nei, actdist + 1));
168                        if actdist + 1 >= mindist_i64 {
169                            size = size
170                                .checked_add(1)
171                                .ok_or(IgraphError::Internal("neighborhood size overflowed u32"))?;
172                        }
173                    }
174                }
175            } else {
176                // At the frontier: count but don't enqueue.
177                for nei in neis {
178                    if added[nei as usize] != marker {
179                        added[nei as usize] = marker;
180                        if actdist + 1 >= mindist_i64 {
181                            size = size
182                                .checked_add(1)
183                                .ok_or(IgraphError::Internal("neighborhood size overflowed u32"))?;
184                        }
185                    }
186                }
187            }
188        }
189
190        result[src as usize] = size;
191    }
192
193    Ok(result)
194}
195
196/// k-hop neighbourhood vertex list for every vertex (`mode = All`, `mindist = 0`).
197///
198/// For each vertex `v` returns the list of vertices `w` within `order`
199/// hops (inclusive), in BFS visitation order with `v` first. Negative
200/// `order` means infinity.
201///
202/// Counterpart of `igraph_neighborhood(graph, _, igraph_vss_all(),
203/// order, IGRAPH_ALL, /*mindist=*/0)`.
204///
205/// # Errors
206/// Same as [`neighborhood_with_mode`].
207///
208/// # Examples
209/// ```
210/// use rust_igraph::{Graph, neighborhood};
211///
212/// // Path P5: 0-1-2-3-4
213/// let mut g = Graph::with_vertices(5);
214/// for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
215///     g.add_edge(u, v).unwrap();
216/// }
217/// // Order 1: each vertex's 1-hop ball.
218/// let n1 = neighborhood(&g, 1).unwrap();
219/// assert_eq!(n1[0], vec![0, 1]);
220/// assert_eq!(n1[2], vec![2, 1, 3]);
221/// ```
222pub fn neighborhood(graph: &Graph, order: i32) -> IgraphResult<Vec<Vec<u32>>> {
223    neighborhood_with_mode(graph, order, NeighborhoodMode::All, 0)
224}
225
226/// Full mode-aware k-hop neighbourhood vertex list with `mindist` filter.
227///
228/// For each source vertex `v` returns the list of vertices `w` such that
229/// `mindist <= dist(v, w) <= order` (or `dist(v, w) >= mindist` when
230/// `order < 0`). The list is in BFS visitation order; when `mindist = 0`
231/// the source is the first element.
232///
233/// Direction follows `mode` on directed graphs; on undirected graphs
234/// every mode reduces to [`NeighborhoodMode::All`].
235///
236/// Counterpart of `igraph_neighborhood(graph, _, igraph_vss_all(),
237/// order, mode, mindist)`.
238///
239/// # Errors
240/// - [`IgraphError::InvalidArgument`] if `mindist < 0`.
241/// - [`IgraphError::InvalidArgument`] if `order >= 0` and `mindist > order`.
242///
243/// # Examples
244/// ```
245/// use rust_igraph::{Graph, neighborhood_with_mode, NeighborhoodMode};
246///
247/// // Directed star: 0->1, 0->2, 0->3.
248/// let mut g = Graph::new(4, true).unwrap();
249/// for v in [1, 2, 3] { g.add_edge(0, v).unwrap(); }
250///
251/// // Out from 0: hub reaches all (BFS order: self, then 1, 2, 3).
252/// let nbh = neighborhood_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap();
253/// assert_eq!(nbh[0], vec![0, 1, 2, 3]);
254/// // Leaves stay alone.
255/// assert_eq!(nbh[1], vec![1]);
256///
257/// // mindist=1 strips the source.
258/// let nbh1 = neighborhood_with_mode(&g, 1, NeighborhoodMode::All, 1).unwrap();
259/// assert_eq!(nbh1[0].len(), 3);  // 1, 2, 3 (some order)
260/// assert!(!nbh1[0].contains(&0));
261/// ```
262pub fn neighborhood_with_mode(
263    graph: &Graph,
264    order: i32,
265    mode: NeighborhoodMode,
266    mindist: i32,
267) -> IgraphResult<Vec<Vec<u32>>> {
268    if mindist < 0 {
269        return Err(IgraphError::InvalidArgument(format!(
270            "minimum distance must not be negative, got {mindist}"
271        )));
272    }
273    if order >= 0 && mindist > order {
274        return Err(IgraphError::InvalidArgument(format!(
275            "minimum distance must not exceed neighbourhood order ({order}), got {mindist}"
276        )));
277    }
278
279    let n = graph.vcount();
280    if n == 0 {
281        return Ok(Vec::new());
282    }
283    let n_us = n as usize;
284
285    let inf_order = order < 0;
286    let effective_order: i64 = if inf_order {
287        i64::from(n)
288    } else {
289        i64::from(order)
290    };
291    let mindist_i64 = i64::from(mindist);
292
293    let directed = graph.is_directed();
294    let mut added: Vec<u32> = vec![0; n_us];
295    let mut queue: VecDeque<(VertexId, i64)> = VecDeque::new();
296    let mut result: Vec<Vec<u32>> = Vec::with_capacity(n_us);
297
298    for src in 0..n {
299        let marker = src + 1;
300        added[src as usize] = marker;
301        let mut tmp: Vec<u32> = Vec::new();
302        if mindist_i64 == 0 {
303            tmp.push(src);
304        }
305        queue.clear();
306        if effective_order > 0 {
307            queue.push_back((src, 0));
308        }
309
310        while let Some((actnode, actdist)) = queue.pop_front() {
311            let neis = neighbours_for(graph, actnode, mode, directed)?;
312            if actdist < effective_order - 1 {
313                for nei in neis {
314                    if added[nei as usize] != marker {
315                        added[nei as usize] = marker;
316                        queue.push_back((nei, actdist + 1));
317                        if actdist + 1 >= mindist_i64 {
318                            tmp.push(nei);
319                        }
320                    }
321                }
322            } else {
323                for nei in neis {
324                    if added[nei as usize] != marker {
325                        added[nei as usize] = marker;
326                        if actdist + 1 >= mindist_i64 {
327                            tmp.push(nei);
328                        }
329                    }
330                }
331            }
332        }
333
334        result.push(tmp);
335    }
336
337    Ok(result)
338}
339
340/// Per-vertex induced subgraphs of k-hop neighbourhoods (`mode = All`, `mindist = 0`).
341///
342/// For each vertex `v`, computes its `order`-hop neighbourhood (the set
343/// of vertices within distance `order` of `v`, including `v` itself) and
344/// returns the induced subgraph on that vertex set. The result is a
345/// `Vec<Graph>` of length `graph.vcount()`.
346///
347/// Counterpart of `igraph_neighborhood_graphs(graph, _, igraph_vss_all(),
348/// order, IGRAPH_ALL, /*mindist=*/0)`.
349///
350/// # Errors
351/// - [`IgraphError::InvalidArgument`] if `order >= 0` and internal
352///   constraints are violated (same as [`neighborhood_with_mode`]).
353///
354/// # Examples
355/// ```
356/// use rust_igraph::{Graph, neighborhood_graphs};
357///
358/// // Path graph: 0 - 1 - 2 - 3
359/// let mut g = Graph::with_vertices(4);
360/// g.add_edge(0, 1).unwrap();
361/// g.add_edge(1, 2).unwrap();
362/// g.add_edge(2, 3).unwrap();
363///
364/// let gs = neighborhood_graphs(&g, 1).unwrap();
365/// assert_eq!(gs.len(), 4);
366/// // Vertex 0's 1-hop neighborhood: {0, 1}, induced subgraph has 1 edge
367/// assert_eq!(gs[0].vcount(), 2);
368/// assert_eq!(gs[0].ecount(), 1);
369/// // Vertex 1's 1-hop neighborhood: {0, 1, 2}, induced subgraph has 2 edges
370/// assert_eq!(gs[1].vcount(), 3);
371/// assert_eq!(gs[1].ecount(), 2);
372/// ```
373pub fn neighborhood_graphs(graph: &Graph, order: i32) -> IgraphResult<Vec<Graph>> {
374    neighborhood_graphs_with_mode(graph, order, NeighborhoodMode::All, 0)
375}
376
377/// Per-vertex induced subgraphs of k-hop neighbourhoods with full mode control.
378///
379/// Generalises [`neighborhood_graphs`] with direction `mode` and
380/// `mindist` filter (minimum distance to include a vertex in the
381/// neighbourhood).
382///
383/// For each source `v`, collects the set of vertices `w` with
384/// `mindist <= dist(v, w) <= order`, then builds the induced subgraph
385/// on that set. Negative `order` means infinity.
386///
387/// Counterpart of `igraph_neighborhood_graphs(graph, _, igraph_vss_all(),
388/// order, mode, mindist)`.
389///
390/// # Errors
391/// - [`IgraphError::InvalidArgument`] if `mindist < 0`.
392/// - [`IgraphError::InvalidArgument`] if `order >= 0` and `mindist > order`.
393///
394/// # Examples
395/// ```
396/// use rust_igraph::{Graph, neighborhood_graphs_with_mode, NeighborhoodMode};
397///
398/// // Directed star: 0->1, 0->2, 0->3
399/// let mut g = Graph::new(4, true).unwrap();
400/// for v in [1, 2, 3] { g.add_edge(0, v).unwrap(); }
401///
402/// let gs = neighborhood_graphs_with_mode(&g, 1, NeighborhoodMode::Out, 0).unwrap();
403/// // Vertex 0's out-1-hop: {0, 1, 2, 3}, induced subgraph has 3 edges
404/// assert_eq!(gs[0].vcount(), 4);
405/// assert_eq!(gs[0].ecount(), 3);
406/// // Vertex 1's out-1-hop: {1} only (no outgoing edges)
407/// assert_eq!(gs[1].vcount(), 1);
408/// assert_eq!(gs[1].ecount(), 0);
409/// ```
410pub fn neighborhood_graphs_with_mode(
411    graph: &Graph,
412    order: i32,
413    mode: NeighborhoodMode,
414    mindist: i32,
415) -> IgraphResult<Vec<Graph>> {
416    use crate::algorithms::operators::induced_subgraph::induced_subgraph;
417
418    let neighborhoods = neighborhood_with_mode(graph, order, mode, mindist)?;
419    let n = graph.vcount();
420    let mut result: Vec<Graph> = Vec::with_capacity(n as usize);
421
422    for vids in &neighborhoods {
423        if vids.len() == n as usize {
424            result.push(graph.clone());
425        } else {
426            let sub = induced_subgraph(graph, vids)?;
427            result.push(sub.graph);
428        }
429    }
430
431    Ok(result)
432}
433
434/// Direction-aware neighbour list. Undirected graphs use
435/// `Graph::neighbors` regardless of `mode` (matches C semantics).
436fn neighbours_for(
437    graph: &Graph,
438    v: VertexId,
439    mode: NeighborhoodMode,
440    directed: bool,
441) -> IgraphResult<Vec<VertexId>> {
442    if !directed {
443        return graph.neighbors(v);
444    }
445    match mode {
446        NeighborhoodMode::Out => graph.out_neighbors_vec(v),
447        NeighborhoodMode::In => graph.in_neighbors_vec(v),
448        NeighborhoodMode::All => {
449            let mut out = graph.out_neighbors_vec(v)?;
450            out.extend(graph.in_neighbors_vec(v)?);
451            Ok(out)
452        }
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::Graph;
460
461    // ---- C reference fixture: directed n=6 with loops and multi-edges ----
462    // Built from references/igraph/tests/unit/igraph_neighborhood_size.c
463    // edges: 0->1, 0->2, 1->1 (self-loop), 1->3, 2->0, 2->3, 3->4, 3->4
464    fn c_ref_graph() -> Graph {
465        let mut g = Graph::new(6, true).unwrap();
466        for (u, v) in [
467            (0, 1),
468            (0, 2),
469            (1, 1),
470            (1, 3),
471            (2, 0),
472            (2, 3),
473            (3, 4),
474            (3, 4),
475        ] {
476            g.add_edge(u, v).unwrap();
477        }
478        g
479    }
480
481    #[test]
482    fn empty_graph_returns_empty_vector() {
483        let g = Graph::with_vertices(0);
484        assert!(neighborhood_size(&g, 1).unwrap().is_empty());
485    }
486
487    #[test]
488    fn singleton_order_0_is_one() {
489        let g = Graph::with_vertices(1);
490        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1]);
491        assert_eq!(neighborhood_size(&g, 5).unwrap(), vec![1]);
492    }
493
494    #[test]
495    fn no_edges_only_self_at_any_order() {
496        let g = Graph::with_vertices(4);
497        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1, 1, 1, 1]);
498        assert_eq!(neighborhood_size(&g, 5).unwrap(), vec![1, 1, 1, 1]);
499    }
500
501    #[test]
502    fn ring_p5_matches_python_reference_order_1() {
503        // python-igraph testStructural: Ring(10, circular=False) order=1
504        // → [2,3,3,3,3,3,3,3,3,2]. Smaller P5 equivalent: [2,3,3,3,2].
505        let mut g = Graph::with_vertices(5);
506        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
507            g.add_edge(u, v).unwrap();
508        }
509        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 3, 3, 2]);
510    }
511
512    #[test]
513    fn ring_p10_matches_python_order_1() {
514        let mut g = Graph::with_vertices(10);
515        for i in 0..9 {
516            g.add_edge(i, i + 1).unwrap();
517        }
518        assert_eq!(
519            neighborhood_size(&g, 1).unwrap(),
520            vec![2, 3, 3, 3, 3, 3, 3, 3, 3, 2]
521        );
522    }
523
524    #[test]
525    fn ring_p10_matches_python_order_3() {
526        let mut g = Graph::with_vertices(10);
527        for i in 0..9 {
528            g.add_edge(i, i + 1).unwrap();
529        }
530        assert_eq!(
531            neighborhood_size(&g, 3).unwrap(),
532            vec![4, 5, 6, 7, 7, 7, 7, 6, 5, 4]
533        );
534    }
535
536    #[test]
537    fn ring_p10_order_3_mindist_2_matches_python() {
538        let mut g = Graph::with_vertices(10);
539        for i in 0..9 {
540            g.add_edge(i, i + 1).unwrap();
541        }
542        assert_eq!(
543            neighborhood_size_with_mode(&g, 3, NeighborhoodMode::All, 2).unwrap(),
544            vec![2, 2, 3, 4, 4, 4, 4, 3, 2, 2]
545        );
546    }
547
548    #[test]
549    fn c_ref_order_0_is_self_only() {
550        let g = c_ref_graph();
551        // C .out: ( 1 1 1 1 1 1 )
552        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1, 1, 1, 1, 1, 1]);
553    }
554
555    #[test]
556    fn c_ref_order_1_all_mode() {
557        let g = c_ref_graph();
558        // C .out: ( 3 3 3 4 2 1 )
559        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![3, 3, 3, 4, 2, 1]);
560    }
561
562    #[test]
563    fn c_ref_order_1_in_mode() {
564        let g = c_ref_graph();
565        // C .out: ( 2 2 2 3 2 1 )
566        assert_eq!(
567            neighborhood_size_with_mode(&g, 1, NeighborhoodMode::In, 0).unwrap(),
568            vec![2, 2, 2, 3, 2, 1]
569        );
570    }
571
572    #[test]
573    fn c_ref_order_10_all_mode_saturates() {
574        let g = c_ref_graph();
575        // C .out: ( 5 5 5 5 5 1 ) — vertex 5 is isolated.
576        assert_eq!(neighborhood_size(&g, 10).unwrap(), vec![5, 5, 5, 5, 5, 1]);
577    }
578
579    #[test]
580    fn c_ref_order_2_mindist_2_out_mode() {
581        let g = c_ref_graph();
582        // C .out: ( 1 1 2 0 0 0 )
583        assert_eq!(
584            neighborhood_size_with_mode(&g, 2, NeighborhoodMode::Out, 2).unwrap(),
585            vec![1, 1, 2, 0, 0, 0]
586        );
587    }
588
589    #[test]
590    fn c_ref_order_4_mindist_4_all_mode_all_zero() {
591        let g = c_ref_graph();
592        // Diameter is 3, so mindist=4 yields all zeros.
593        assert_eq!(
594            neighborhood_size_with_mode(&g, 4, NeighborhoodMode::All, 4).unwrap(),
595            vec![0, 0, 0, 0, 0, 0]
596        );
597    }
598
599    #[test]
600    fn c_ref_infinite_order_out_mode() {
601        let g = c_ref_graph();
602        // C .out: ( 5 3 5 2 1 1 )
603        assert_eq!(
604            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap(),
605            vec![5, 3, 5, 2, 1, 1]
606        );
607    }
608
609    #[test]
610    fn c_ref_infinite_order_mindist_2_out_mode() {
611        let g = c_ref_graph();
612        // C .out: ( 2 1 2 0 0 0 )
613        assert_eq!(
614            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 2).unwrap(),
615            vec![2, 1, 2, 0, 0, 0]
616        );
617    }
618
619    #[test]
620    fn c_ref_infinite_order_mindist_2_in_mode() {
621        let g = c_ref_graph();
622        // C .out: ( 0 1 0 1 3 0 )
623        assert_eq!(
624            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::In, 2).unwrap(),
625            vec![0, 1, 0, 1, 3, 0]
626        );
627    }
628
629    #[test]
630    fn negative_mindist_errors() {
631        let g = Graph::with_vertices(3);
632        match neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, -1) {
633            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("negative")),
634            other => panic!("expected InvalidArgument for negative mindist, got {other:?}"),
635        }
636    }
637
638    #[test]
639    fn mindist_exceeding_finite_order_errors() {
640        let g = Graph::with_vertices(3);
641        match neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, 3) {
642            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("exceed")),
643            other => panic!("expected InvalidArgument for mindist > order, got {other:?}"),
644        }
645    }
646
647    #[test]
648    fn infinite_order_allows_any_mindist() {
649        // mindist > vcount is fine when order is infinite.
650        let mut g = Graph::with_vertices(3);
651        g.add_edge(0, 1).unwrap();
652        g.add_edge(1, 2).unwrap();
653        // mindist=10: nobody is at distance >= 10 → all zeros.
654        assert_eq!(
655            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::All, 10).unwrap(),
656            vec![0, 0, 0]
657        );
658    }
659
660    #[test]
661    fn k4_complete_undirected_order_1() {
662        let mut g = Graph::with_vertices(4);
663        for u in 0..4 {
664            for v in (u + 1)..4 {
665                g.add_edge(u, v).unwrap();
666            }
667        }
668        // Every vertex sees self + 3 neighbours.
669        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![4, 4, 4, 4]);
670    }
671
672    #[test]
673    fn directed_star_out_in_modes() {
674        // 0 -> 1, 0 -> 2, 0 -> 3
675        let mut g = Graph::new(4, true).unwrap();
676        for v in [1, 2, 3] {
677            g.add_edge(0, v).unwrap();
678        }
679        // Out: hub reaches all, leaves stay alone.
680        assert_eq!(
681            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap(),
682            vec![4, 1, 1, 1]
683        );
684        // In: leaves reach hub by reversed walk.
685        assert_eq!(
686            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::In, 0).unwrap(),
687            vec![1, 2, 2, 2]
688        );
689        // All: everything connected.
690        assert_eq!(
691            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::All, 0).unwrap(),
692            vec![4, 4, 4, 4]
693        );
694    }
695
696    #[test]
697    fn self_loop_does_not_inflate_count() {
698        let mut g = Graph::with_vertices(3);
699        g.add_edge(0, 0).unwrap();
700        g.add_edge(0, 1).unwrap();
701        g.add_edge(1, 2).unwrap();
702        // Self-loop on 0: order 1 still {0, 1} → size 2.
703        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 2]);
704    }
705
706    #[test]
707    fn multi_edge_does_not_double_count() {
708        let mut g = Graph::with_vertices(3);
709        g.add_edge(0, 1).unwrap();
710        g.add_edge(0, 1).unwrap();
711        g.add_edge(1, 2).unwrap();
712        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 2]);
713    }
714
715    #[test]
716    fn mindist_equals_order_counts_frontier_only() {
717        // P5: 0-1-2-3-4. order=2, mindist=2 → only vertices at distance 2.
718        let mut g = Graph::with_vertices(5);
719        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
720            g.add_edge(u, v).unwrap();
721        }
722        // d=2 ball for each vertex: 0→{2}, 1→{3}, 2→{0,4}, 3→{1}, 4→{2}.
723        assert_eq!(
724            neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, 2).unwrap(),
725            vec![1, 1, 2, 1, 1]
726        );
727    }
728
729    // ---- neighborhood (vertex lists) tests ----
730    //
731    // BFS visitation order depends on adjacency-list iteration, so it
732    // differs subtly between Rust / C / Python. The semantically
733    // meaningful claim is set equality, so all neighborhood tests sort
734    // both sides before comparing.
735
736    fn sorted(mut v: Vec<u32>) -> Vec<u32> {
737        v.sort_unstable();
738        v
739    }
740
741    fn sorted_all(nbh: Vec<Vec<u32>>) -> Vec<Vec<u32>> {
742        nbh.into_iter().map(sorted).collect()
743    }
744
745    #[test]
746    fn neighborhood_empty_graph_returns_empty() {
747        let g = Graph::with_vertices(0);
748        assert!(neighborhood(&g, 1).unwrap().is_empty());
749    }
750
751    #[test]
752    fn neighborhood_singleton_order_0_is_self_only() {
753        let g = Graph::with_vertices(1);
754        assert_eq!(neighborhood(&g, 0).unwrap(), vec![vec![0]]);
755        assert_eq!(neighborhood(&g, 5).unwrap(), vec![vec![0]]);
756    }
757
758    #[test]
759    fn neighborhood_no_edges_returns_singletons() {
760        let g = Graph::with_vertices(3);
761        let n0 = neighborhood(&g, 0).unwrap();
762        assert_eq!(n0, vec![vec![0], vec![1], vec![2]]);
763        let n5 = neighborhood(&g, 5).unwrap();
764        assert_eq!(n5, vec![vec![0], vec![1], vec![2]]);
765    }
766
767    #[test]
768    fn neighborhood_p5_order_1_set_eq() {
769        // P5: 0-1-2-3-4
770        let mut g = Graph::with_vertices(5);
771        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
772            g.add_edge(u, v).unwrap();
773        }
774        let got = sorted_all(neighborhood(&g, 1).unwrap());
775        let exp: Vec<Vec<u32>> = vec![
776            vec![0, 1],
777            vec![0, 1, 2],
778            vec![1, 2, 3],
779            vec![2, 3, 4],
780            vec![3, 4],
781        ];
782        assert_eq!(got, exp);
783    }
784
785    #[test]
786    fn neighborhood_p5_order_2_set_eq() {
787        let mut g = Graph::with_vertices(5);
788        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
789            g.add_edge(u, v).unwrap();
790        }
791        let got = sorted_all(neighborhood(&g, 2).unwrap());
792        let exp: Vec<Vec<u32>> = vec![
793            vec![0, 1, 2],
794            vec![0, 1, 2, 3],
795            vec![0, 1, 2, 3, 4],
796            vec![1, 2, 3, 4],
797            vec![2, 3, 4],
798        ];
799        assert_eq!(got, exp);
800    }
801
802    #[test]
803    fn neighborhood_c_ref_order_0_is_self_only() {
804        // C .out: 0:(0) 1:(1) 2:(2) 3:(3) 4:(4) 5:(5)
805        let g = c_ref_graph();
806        let got = neighborhood(&g, 0).unwrap();
807        let exp: Vec<Vec<u32>> = (0..6).map(|i| vec![i]).collect();
808        assert_eq!(got, exp);
809    }
810
811    #[test]
812    fn neighborhood_c_ref_order_1_all_set_eq() {
813        // C .out: 0:(0 1 2) 1:(1 0 3) 2:(2 0 3) 3:(3 1 2 4) 4:(4 3) 5:(5)
814        let g = c_ref_graph();
815        let got = sorted_all(neighborhood(&g, 1).unwrap());
816        let exp: Vec<Vec<u32>> = vec![
817            vec![0, 1, 2],
818            vec![0, 1, 3],
819            vec![0, 2, 3],
820            vec![1, 2, 3, 4],
821            vec![3, 4],
822            vec![5],
823        ];
824        assert_eq!(got, exp);
825    }
826
827    #[test]
828    fn neighborhood_c_ref_order_1_in_set_eq() {
829        // C .out: 0:(0 2) 1:(1 0) 2:(2 0) 3:(3 1 2) 4:(4 3) 5:(5)
830        let g = c_ref_graph();
831        let got = sorted_all(neighborhood_with_mode(&g, 1, NeighborhoodMode::In, 0).unwrap());
832        let exp: Vec<Vec<u32>> = vec![
833            vec![0, 2],
834            vec![0, 1],
835            vec![0, 2],
836            vec![1, 2, 3],
837            vec![3, 4],
838            vec![5],
839        ];
840        assert_eq!(got, exp);
841    }
842
843    #[test]
844    fn neighborhood_c_ref_order_10_all_saturates_set_eq() {
845        // C .out: 0:(0 1 2 3 4) 1:(0 1 2 3 4) 2:(0 1 2 3 4) 3:(0 1 2 3 4)
846        //         4:(0 1 2 3 4) 5:(5)
847        let g = c_ref_graph();
848        let got = sorted_all(neighborhood(&g, 10).unwrap());
849        let big: Vec<u32> = (0..5).collect();
850        let exp: Vec<Vec<u32>> = vec![
851            big.clone(),
852            big.clone(),
853            big.clone(),
854            big.clone(),
855            big,
856            vec![5],
857        ];
858        assert_eq!(got, exp);
859    }
860
861    #[test]
862    fn neighborhood_c_ref_order_2_mindist_2_out_set_eq() {
863        // C .out: 0:(3) 1:(4) 2:(1 4) 3:() 4:() 5:()
864        let g = c_ref_graph();
865        let got = sorted_all(neighborhood_with_mode(&g, 2, NeighborhoodMode::Out, 2).unwrap());
866        let exp: Vec<Vec<u32>> = vec![vec![3], vec![4], vec![1, 4], vec![], vec![], vec![]];
867        assert_eq!(got, exp);
868    }
869
870    #[test]
871    fn neighborhood_c_ref_order_4_mindist_4_all_empty() {
872        // Diameter < 4 ⇒ all empty.
873        let g = c_ref_graph();
874        let got = neighborhood_with_mode(&g, 4, NeighborhoodMode::All, 4).unwrap();
875        let exp: Vec<Vec<u32>> = vec![vec![]; 6];
876        assert_eq!(got, exp);
877    }
878
879    #[test]
880    fn neighborhood_c_ref_infinite_order_out_set_eq() {
881        // C .out: 0:(0 1 2 3 4) 1:(1 3 4) 2:(0 1 2 3 4) 3:(3 4) 4:(4) 5:(5)
882        let g = c_ref_graph();
883        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap());
884        let exp: Vec<Vec<u32>> = vec![
885            vec![0, 1, 2, 3, 4],
886            vec![1, 3, 4],
887            vec![0, 1, 2, 3, 4],
888            vec![3, 4],
889            vec![4],
890            vec![5],
891        ];
892        assert_eq!(got, exp);
893    }
894
895    #[test]
896    fn neighborhood_c_ref_infinite_mindist_2_out_set_eq() {
897        // C .out: 0:(3 4) 1:(4) 2:(1 4) 3:() 4:() 5:()
898        let g = c_ref_graph();
899        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::Out, 2).unwrap());
900        let exp: Vec<Vec<u32>> = vec![vec![3, 4], vec![4], vec![1, 4], vec![], vec![], vec![]];
901        assert_eq!(got, exp);
902    }
903
904    #[test]
905    fn neighborhood_c_ref_infinite_mindist_2_in_set_eq() {
906        // C .out: 0:() 1:(2) 2:() 3:(0) 4:(0 1 2) 5:()
907        let g = c_ref_graph();
908        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::In, 2).unwrap());
909        let exp: Vec<Vec<u32>> = vec![vec![], vec![2], vec![], vec![0], vec![0, 1, 2], vec![]];
910        assert_eq!(got, exp);
911    }
912
913    #[test]
914    fn neighborhood_negative_mindist_errors() {
915        let g = Graph::with_vertices(3);
916        match neighborhood_with_mode(&g, 2, NeighborhoodMode::All, -1) {
917            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("negative")),
918            other => panic!("expected InvalidArgument for negative mindist, got {other:?}"),
919        }
920    }
921
922    #[test]
923    fn neighborhood_mindist_exceeds_order_errors() {
924        let g = Graph::with_vertices(3);
925        match neighborhood_with_mode(&g, 2, NeighborhoodMode::All, 3) {
926            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("exceed")),
927            other => panic!("expected InvalidArgument, got {other:?}"),
928        }
929    }
930
931    #[test]
932    fn neighborhood_lengths_match_neighborhood_size() {
933        // PR-027b list lengths should equal PR-027 sizes for any
934        // (order, mode, mindist) triple — the two are tightly coupled.
935        let g = c_ref_graph();
936        for order in [0_i32, 1, 2, 3, 10, -1] {
937            for &mode in &[
938                NeighborhoodMode::Out,
939                NeighborhoodMode::In,
940                NeighborhoodMode::All,
941            ] {
942                for mindist in [0_i32, 1, 2] {
943                    if order >= 0 && mindist > order {
944                        continue;
945                    }
946                    let sizes = neighborhood_size_with_mode(&g, order, mode, mindist).unwrap();
947                    let lists = neighborhood_with_mode(&g, order, mode, mindist).unwrap();
948                    let list_lens: Vec<u32> = lists
949                        .iter()
950                        .map(|v| u32::try_from(v.len()).unwrap())
951                        .collect();
952                    assert_eq!(
953                        sizes, list_lens,
954                        "size/list-len mismatch at order={order} mode={mode:?} mindist={mindist}",
955                    );
956                }
957            }
958        }
959    }
960
961    #[test]
962    fn neighborhood_self_loop_not_double_visited() {
963        let mut g = Graph::with_vertices(3);
964        g.add_edge(0, 0).unwrap();
965        g.add_edge(0, 1).unwrap();
966        g.add_edge(1, 2).unwrap();
967        let got = sorted_all(neighborhood(&g, 1).unwrap());
968        assert_eq!(got, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
969    }
970
971    #[test]
972    fn neighborhood_multi_edge_not_double_visited() {
973        let mut g = Graph::with_vertices(3);
974        g.add_edge(0, 1).unwrap();
975        g.add_edge(0, 1).unwrap();
976        g.add_edge(1, 2).unwrap();
977        let got = sorted_all(neighborhood(&g, 1).unwrap());
978        assert_eq!(got, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
979    }
980
981    #[test]
982    fn neighborhood_mindist_1_excludes_self() {
983        // P5 order 1 mindist 1 → only neighbours, not self.
984        let mut g = Graph::with_vertices(5);
985        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
986            g.add_edge(u, v).unwrap();
987        }
988        let got = sorted_all(neighborhood_with_mode(&g, 1, NeighborhoodMode::All, 1).unwrap());
989        assert_eq!(
990            got,
991            vec![vec![1], vec![0, 2], vec![1, 3], vec![2, 4], vec![3]]
992        );
993        for list in &got {
994            for (i, neighbors) in got.iter().enumerate() {
995                let i_u32 = u32::try_from(i).unwrap();
996                if std::ptr::eq(list, neighbors) {
997                    assert!(!list.contains(&i_u32), "mindist=1 should exclude self");
998                }
999            }
1000        }
1001    }
1002
1003    // ---- neighborhood_graphs tests ----
1004
1005    #[test]
1006    fn neighborhood_graphs_empty_graph() {
1007        let g = Graph::with_vertices(0);
1008        let gs = neighborhood_graphs(&g, 1).unwrap();
1009        assert!(gs.is_empty());
1010    }
1011
1012    #[test]
1013    fn neighborhood_graphs_isolated_vertices() {
1014        let g = Graph::with_vertices(3);
1015        let gs = neighborhood_graphs(&g, 1).unwrap();
1016        assert_eq!(gs.len(), 3);
1017        for sub in &gs {
1018            assert_eq!(sub.vcount(), 1);
1019            assert_eq!(sub.ecount(), 0);
1020        }
1021    }
1022
1023    #[test]
1024    fn neighborhood_graphs_path_order_1() {
1025        // Path: 0-1-2-3
1026        let mut g = Graph::with_vertices(4);
1027        g.add_edge(0, 1).unwrap();
1028        g.add_edge(1, 2).unwrap();
1029        g.add_edge(2, 3).unwrap();
1030
1031        let gs = neighborhood_graphs(&g, 1).unwrap();
1032        assert_eq!(gs.len(), 4);
1033        // v=0: {0,1} → 1 edge
1034        assert_eq!(gs[0].vcount(), 2);
1035        assert_eq!(gs[0].ecount(), 1);
1036        // v=1: {0,1,2} → 2 edges (0-1, 1-2)
1037        assert_eq!(gs[1].vcount(), 3);
1038        assert_eq!(gs[1].ecount(), 2);
1039        // v=2: {1,2,3} → 2 edges
1040        assert_eq!(gs[2].vcount(), 3);
1041        assert_eq!(gs[2].ecount(), 2);
1042        // v=3: {2,3} → 1 edge
1043        assert_eq!(gs[3].vcount(), 2);
1044        assert_eq!(gs[3].ecount(), 1);
1045    }
1046
1047    #[test]
1048    fn neighborhood_graphs_order_0_is_singletons() {
1049        let mut g = Graph::with_vertices(3);
1050        g.add_edge(0, 1).unwrap();
1051        g.add_edge(1, 2).unwrap();
1052
1053        let gs = neighborhood_graphs(&g, 0).unwrap();
1054        assert_eq!(gs.len(), 3);
1055        for sub in &gs {
1056            assert_eq!(sub.vcount(), 1);
1057            assert_eq!(sub.ecount(), 0);
1058        }
1059    }
1060
1061    #[test]
1062    fn neighborhood_graphs_complete_graph_order_1() {
1063        // K4: all vertices within 1 hop of each other
1064        let mut g = Graph::with_vertices(4);
1065        g.add_edge(0, 1).unwrap();
1066        g.add_edge(0, 2).unwrap();
1067        g.add_edge(0, 3).unwrap();
1068        g.add_edge(1, 2).unwrap();
1069        g.add_edge(1, 3).unwrap();
1070        g.add_edge(2, 3).unwrap();
1071
1072        let gs = neighborhood_graphs(&g, 1).unwrap();
1073        assert_eq!(gs.len(), 4);
1074        for sub in &gs {
1075            // Every vertex's 1-hop neighbourhood is the whole graph
1076            assert_eq!(sub.vcount(), 4);
1077            assert_eq!(sub.ecount(), 6);
1078        }
1079    }
1080
1081    #[test]
1082    fn neighborhood_graphs_directed_out_mode() {
1083        // Star: 0->1, 0->2, 0->3
1084        let mut g = Graph::new(4, true).unwrap();
1085        g.add_edge(0, 1).unwrap();
1086        g.add_edge(0, 2).unwrap();
1087        g.add_edge(0, 3).unwrap();
1088
1089        let gs = neighborhood_graphs_with_mode(&g, 1, NeighborhoodMode::Out, 0).unwrap();
1090        assert_eq!(gs.len(), 4);
1091        // v=0 out-1-hop: {0,1,2,3}, induced subgraph has all 3 edges
1092        assert_eq!(gs[0].vcount(), 4);
1093        assert_eq!(gs[0].ecount(), 3);
1094        // v=1 out-1-hop: {1} only
1095        assert_eq!(gs[1].vcount(), 1);
1096        assert_eq!(gs[1].ecount(), 0);
1097    }
1098
1099    #[test]
1100    fn neighborhood_graphs_directed_in_mode() {
1101        // Star: 0->1, 0->2, 0->3
1102        let mut g = Graph::new(4, true).unwrap();
1103        g.add_edge(0, 1).unwrap();
1104        g.add_edge(0, 2).unwrap();
1105        g.add_edge(0, 3).unwrap();
1106
1107        let gs = neighborhood_graphs_with_mode(&g, 1, NeighborhoodMode::In, 0).unwrap();
1108        // v=0 in-1-hop: {0} only (no in-edges)
1109        assert_eq!(gs[0].vcount(), 1);
1110        assert_eq!(gs[0].ecount(), 0);
1111        // v=1 in-1-hop: {0,1} (0->1 edge exists)
1112        assert_eq!(gs[1].vcount(), 2);
1113        assert_eq!(gs[1].ecount(), 1);
1114    }
1115
1116    #[test]
1117    fn neighborhood_graphs_mindist_excludes_close_vertices() {
1118        // Path: 0-1-2-3
1119        let mut g = Graph::with_vertices(4);
1120        g.add_edge(0, 1).unwrap();
1121        g.add_edge(1, 2).unwrap();
1122        g.add_edge(2, 3).unwrap();
1123
1124        // mindist=1, order=2: exclude self, include dist 1 and 2
1125        let gs = neighborhood_graphs_with_mode(&g, 2, NeighborhoodMode::All, 1).unwrap();
1126        // v=0: mindist=1, order=2 → {1, 2}; induced subgraph has edge 1-2
1127        assert_eq!(gs[0].vcount(), 2);
1128        assert_eq!(gs[0].ecount(), 1);
1129        // v=1: mindist=1, order=2 → {0, 2, 3}; edges: 2-3 (0-1 and 1-2 don't survive without 1)
1130        // Wait — vertex 1 is excluded (mindist=1), so {0, 2, 3}
1131        // Edges between {0,2,3}: only 2-3
1132        assert_eq!(gs[1].vcount(), 3);
1133        assert_eq!(gs[1].ecount(), 1);
1134    }
1135
1136    #[test]
1137    fn neighborhood_graphs_infinite_order_returns_full_component() {
1138        // Two components: {0,1,2}, {3,4}
1139        let mut g = Graph::with_vertices(5);
1140        g.add_edge(0, 1).unwrap();
1141        g.add_edge(1, 2).unwrap();
1142        g.add_edge(3, 4).unwrap();
1143
1144        let gs = neighborhood_graphs(&g, -1).unwrap();
1145        // v=0: reachable {0,1,2}, 2 edges
1146        assert_eq!(gs[0].vcount(), 3);
1147        assert_eq!(gs[0].ecount(), 2);
1148        // v=3: reachable {3,4}, 1 edge
1149        assert_eq!(gs[3].vcount(), 2);
1150        assert_eq!(gs[3].ecount(), 1);
1151    }
1152
1153    #[test]
1154    fn neighborhood_graphs_preserves_directedness() {
1155        let mut g = Graph::new(3, true).unwrap();
1156        g.add_edge(0, 1).unwrap();
1157        g.add_edge(1, 2).unwrap();
1158
1159        let gs = neighborhood_graphs_with_mode(&g, 1, NeighborhoodMode::All, 0).unwrap();
1160        for sub in &gs {
1161            assert!(sub.is_directed());
1162        }
1163    }
1164}