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/// Direction-aware neighbour list. Undirected graphs use
341/// `Graph::neighbors` regardless of `mode` (matches C semantics).
342fn neighbours_for(
343    graph: &Graph,
344    v: VertexId,
345    mode: NeighborhoodMode,
346    directed: bool,
347) -> IgraphResult<Vec<VertexId>> {
348    if !directed {
349        return graph.neighbors(v);
350    }
351    match mode {
352        NeighborhoodMode::Out => graph.out_neighbors_vec(v),
353        NeighborhoodMode::In => graph.in_neighbors_vec(v),
354        NeighborhoodMode::All => {
355            let mut out = graph.out_neighbors_vec(v)?;
356            out.extend(graph.in_neighbors_vec(v)?);
357            Ok(out)
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::Graph;
366
367    // ---- C reference fixture: directed n=6 with loops and multi-edges ----
368    // Built from references/igraph/tests/unit/igraph_neighborhood_size.c
369    // edges: 0->1, 0->2, 1->1 (self-loop), 1->3, 2->0, 2->3, 3->4, 3->4
370    fn c_ref_graph() -> Graph {
371        let mut g = Graph::new(6, true).unwrap();
372        for (u, v) in [
373            (0, 1),
374            (0, 2),
375            (1, 1),
376            (1, 3),
377            (2, 0),
378            (2, 3),
379            (3, 4),
380            (3, 4),
381        ] {
382            g.add_edge(u, v).unwrap();
383        }
384        g
385    }
386
387    #[test]
388    fn empty_graph_returns_empty_vector() {
389        let g = Graph::with_vertices(0);
390        assert!(neighborhood_size(&g, 1).unwrap().is_empty());
391    }
392
393    #[test]
394    fn singleton_order_0_is_one() {
395        let g = Graph::with_vertices(1);
396        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1]);
397        assert_eq!(neighborhood_size(&g, 5).unwrap(), vec![1]);
398    }
399
400    #[test]
401    fn no_edges_only_self_at_any_order() {
402        let g = Graph::with_vertices(4);
403        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1, 1, 1, 1]);
404        assert_eq!(neighborhood_size(&g, 5).unwrap(), vec![1, 1, 1, 1]);
405    }
406
407    #[test]
408    fn ring_p5_matches_python_reference_order_1() {
409        // python-igraph testStructural: Ring(10, circular=False) order=1
410        // → [2,3,3,3,3,3,3,3,3,2]. Smaller P5 equivalent: [2,3,3,3,2].
411        let mut g = Graph::with_vertices(5);
412        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
413            g.add_edge(u, v).unwrap();
414        }
415        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 3, 3, 2]);
416    }
417
418    #[test]
419    fn ring_p10_matches_python_order_1() {
420        let mut g = Graph::with_vertices(10);
421        for i in 0..9 {
422            g.add_edge(i, i + 1).unwrap();
423        }
424        assert_eq!(
425            neighborhood_size(&g, 1).unwrap(),
426            vec![2, 3, 3, 3, 3, 3, 3, 3, 3, 2]
427        );
428    }
429
430    #[test]
431    fn ring_p10_matches_python_order_3() {
432        let mut g = Graph::with_vertices(10);
433        for i in 0..9 {
434            g.add_edge(i, i + 1).unwrap();
435        }
436        assert_eq!(
437            neighborhood_size(&g, 3).unwrap(),
438            vec![4, 5, 6, 7, 7, 7, 7, 6, 5, 4]
439        );
440    }
441
442    #[test]
443    fn ring_p10_order_3_mindist_2_matches_python() {
444        let mut g = Graph::with_vertices(10);
445        for i in 0..9 {
446            g.add_edge(i, i + 1).unwrap();
447        }
448        assert_eq!(
449            neighborhood_size_with_mode(&g, 3, NeighborhoodMode::All, 2).unwrap(),
450            vec![2, 2, 3, 4, 4, 4, 4, 3, 2, 2]
451        );
452    }
453
454    #[test]
455    fn c_ref_order_0_is_self_only() {
456        let g = c_ref_graph();
457        // C .out: ( 1 1 1 1 1 1 )
458        assert_eq!(neighborhood_size(&g, 0).unwrap(), vec![1, 1, 1, 1, 1, 1]);
459    }
460
461    #[test]
462    fn c_ref_order_1_all_mode() {
463        let g = c_ref_graph();
464        // C .out: ( 3 3 3 4 2 1 )
465        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![3, 3, 3, 4, 2, 1]);
466    }
467
468    #[test]
469    fn c_ref_order_1_in_mode() {
470        let g = c_ref_graph();
471        // C .out: ( 2 2 2 3 2 1 )
472        assert_eq!(
473            neighborhood_size_with_mode(&g, 1, NeighborhoodMode::In, 0).unwrap(),
474            vec![2, 2, 2, 3, 2, 1]
475        );
476    }
477
478    #[test]
479    fn c_ref_order_10_all_mode_saturates() {
480        let g = c_ref_graph();
481        // C .out: ( 5 5 5 5 5 1 ) — vertex 5 is isolated.
482        assert_eq!(neighborhood_size(&g, 10).unwrap(), vec![5, 5, 5, 5, 5, 1]);
483    }
484
485    #[test]
486    fn c_ref_order_2_mindist_2_out_mode() {
487        let g = c_ref_graph();
488        // C .out: ( 1 1 2 0 0 0 )
489        assert_eq!(
490            neighborhood_size_with_mode(&g, 2, NeighborhoodMode::Out, 2).unwrap(),
491            vec![1, 1, 2, 0, 0, 0]
492        );
493    }
494
495    #[test]
496    fn c_ref_order_4_mindist_4_all_mode_all_zero() {
497        let g = c_ref_graph();
498        // Diameter is 3, so mindist=4 yields all zeros.
499        assert_eq!(
500            neighborhood_size_with_mode(&g, 4, NeighborhoodMode::All, 4).unwrap(),
501            vec![0, 0, 0, 0, 0, 0]
502        );
503    }
504
505    #[test]
506    fn c_ref_infinite_order_out_mode() {
507        let g = c_ref_graph();
508        // C .out: ( 5 3 5 2 1 1 )
509        assert_eq!(
510            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap(),
511            vec![5, 3, 5, 2, 1, 1]
512        );
513    }
514
515    #[test]
516    fn c_ref_infinite_order_mindist_2_out_mode() {
517        let g = c_ref_graph();
518        // C .out: ( 2 1 2 0 0 0 )
519        assert_eq!(
520            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 2).unwrap(),
521            vec![2, 1, 2, 0, 0, 0]
522        );
523    }
524
525    #[test]
526    fn c_ref_infinite_order_mindist_2_in_mode() {
527        let g = c_ref_graph();
528        // C .out: ( 0 1 0 1 3 0 )
529        assert_eq!(
530            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::In, 2).unwrap(),
531            vec![0, 1, 0, 1, 3, 0]
532        );
533    }
534
535    #[test]
536    fn negative_mindist_errors() {
537        let g = Graph::with_vertices(3);
538        match neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, -1) {
539            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("negative")),
540            other => panic!("expected InvalidArgument for negative mindist, got {other:?}"),
541        }
542    }
543
544    #[test]
545    fn mindist_exceeding_finite_order_errors() {
546        let g = Graph::with_vertices(3);
547        match neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, 3) {
548            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("exceed")),
549            other => panic!("expected InvalidArgument for mindist > order, got {other:?}"),
550        }
551    }
552
553    #[test]
554    fn infinite_order_allows_any_mindist() {
555        // mindist > vcount is fine when order is infinite.
556        let mut g = Graph::with_vertices(3);
557        g.add_edge(0, 1).unwrap();
558        g.add_edge(1, 2).unwrap();
559        // mindist=10: nobody is at distance >= 10 → all zeros.
560        assert_eq!(
561            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::All, 10).unwrap(),
562            vec![0, 0, 0]
563        );
564    }
565
566    #[test]
567    fn k4_complete_undirected_order_1() {
568        let mut g = Graph::with_vertices(4);
569        for u in 0..4 {
570            for v in (u + 1)..4 {
571                g.add_edge(u, v).unwrap();
572            }
573        }
574        // Every vertex sees self + 3 neighbours.
575        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![4, 4, 4, 4]);
576    }
577
578    #[test]
579    fn directed_star_out_in_modes() {
580        // 0 -> 1, 0 -> 2, 0 -> 3
581        let mut g = Graph::new(4, true).unwrap();
582        for v in [1, 2, 3] {
583            g.add_edge(0, v).unwrap();
584        }
585        // Out: hub reaches all, leaves stay alone.
586        assert_eq!(
587            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap(),
588            vec![4, 1, 1, 1]
589        );
590        // In: leaves reach hub by reversed walk.
591        assert_eq!(
592            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::In, 0).unwrap(),
593            vec![1, 2, 2, 2]
594        );
595        // All: everything connected.
596        assert_eq!(
597            neighborhood_size_with_mode(&g, -1, NeighborhoodMode::All, 0).unwrap(),
598            vec![4, 4, 4, 4]
599        );
600    }
601
602    #[test]
603    fn self_loop_does_not_inflate_count() {
604        let mut g = Graph::with_vertices(3);
605        g.add_edge(0, 0).unwrap();
606        g.add_edge(0, 1).unwrap();
607        g.add_edge(1, 2).unwrap();
608        // Self-loop on 0: order 1 still {0, 1} → size 2.
609        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 2]);
610    }
611
612    #[test]
613    fn multi_edge_does_not_double_count() {
614        let mut g = Graph::with_vertices(3);
615        g.add_edge(0, 1).unwrap();
616        g.add_edge(0, 1).unwrap();
617        g.add_edge(1, 2).unwrap();
618        assert_eq!(neighborhood_size(&g, 1).unwrap(), vec![2, 3, 2]);
619    }
620
621    #[test]
622    fn mindist_equals_order_counts_frontier_only() {
623        // P5: 0-1-2-3-4. order=2, mindist=2 → only vertices at distance 2.
624        let mut g = Graph::with_vertices(5);
625        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
626            g.add_edge(u, v).unwrap();
627        }
628        // d=2 ball for each vertex: 0→{2}, 1→{3}, 2→{0,4}, 3→{1}, 4→{2}.
629        assert_eq!(
630            neighborhood_size_with_mode(&g, 2, NeighborhoodMode::All, 2).unwrap(),
631            vec![1, 1, 2, 1, 1]
632        );
633    }
634
635    // ---- neighborhood (vertex lists) tests ----
636    //
637    // BFS visitation order depends on adjacency-list iteration, so it
638    // differs subtly between Rust / C / Python. The semantically
639    // meaningful claim is set equality, so all neighborhood tests sort
640    // both sides before comparing.
641
642    fn sorted(mut v: Vec<u32>) -> Vec<u32> {
643        v.sort_unstable();
644        v
645    }
646
647    fn sorted_all(nbh: Vec<Vec<u32>>) -> Vec<Vec<u32>> {
648        nbh.into_iter().map(sorted).collect()
649    }
650
651    #[test]
652    fn neighborhood_empty_graph_returns_empty() {
653        let g = Graph::with_vertices(0);
654        assert!(neighborhood(&g, 1).unwrap().is_empty());
655    }
656
657    #[test]
658    fn neighborhood_singleton_order_0_is_self_only() {
659        let g = Graph::with_vertices(1);
660        assert_eq!(neighborhood(&g, 0).unwrap(), vec![vec![0]]);
661        assert_eq!(neighborhood(&g, 5).unwrap(), vec![vec![0]]);
662    }
663
664    #[test]
665    fn neighborhood_no_edges_returns_singletons() {
666        let g = Graph::with_vertices(3);
667        let n0 = neighborhood(&g, 0).unwrap();
668        assert_eq!(n0, vec![vec![0], vec![1], vec![2]]);
669        let n5 = neighborhood(&g, 5).unwrap();
670        assert_eq!(n5, vec![vec![0], vec![1], vec![2]]);
671    }
672
673    #[test]
674    fn neighborhood_p5_order_1_set_eq() {
675        // P5: 0-1-2-3-4
676        let mut g = Graph::with_vertices(5);
677        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
678            g.add_edge(u, v).unwrap();
679        }
680        let got = sorted_all(neighborhood(&g, 1).unwrap());
681        let exp: Vec<Vec<u32>> = vec![
682            vec![0, 1],
683            vec![0, 1, 2],
684            vec![1, 2, 3],
685            vec![2, 3, 4],
686            vec![3, 4],
687        ];
688        assert_eq!(got, exp);
689    }
690
691    #[test]
692    fn neighborhood_p5_order_2_set_eq() {
693        let mut g = Graph::with_vertices(5);
694        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
695            g.add_edge(u, v).unwrap();
696        }
697        let got = sorted_all(neighborhood(&g, 2).unwrap());
698        let exp: Vec<Vec<u32>> = vec![
699            vec![0, 1, 2],
700            vec![0, 1, 2, 3],
701            vec![0, 1, 2, 3, 4],
702            vec![1, 2, 3, 4],
703            vec![2, 3, 4],
704        ];
705        assert_eq!(got, exp);
706    }
707
708    #[test]
709    fn neighborhood_c_ref_order_0_is_self_only() {
710        // C .out: 0:(0) 1:(1) 2:(2) 3:(3) 4:(4) 5:(5)
711        let g = c_ref_graph();
712        let got = neighborhood(&g, 0).unwrap();
713        let exp: Vec<Vec<u32>> = (0..6).map(|i| vec![i]).collect();
714        assert_eq!(got, exp);
715    }
716
717    #[test]
718    fn neighborhood_c_ref_order_1_all_set_eq() {
719        // C .out: 0:(0 1 2) 1:(1 0 3) 2:(2 0 3) 3:(3 1 2 4) 4:(4 3) 5:(5)
720        let g = c_ref_graph();
721        let got = sorted_all(neighborhood(&g, 1).unwrap());
722        let exp: Vec<Vec<u32>> = vec![
723            vec![0, 1, 2],
724            vec![0, 1, 3],
725            vec![0, 2, 3],
726            vec![1, 2, 3, 4],
727            vec![3, 4],
728            vec![5],
729        ];
730        assert_eq!(got, exp);
731    }
732
733    #[test]
734    fn neighborhood_c_ref_order_1_in_set_eq() {
735        // C .out: 0:(0 2) 1:(1 0) 2:(2 0) 3:(3 1 2) 4:(4 3) 5:(5)
736        let g = c_ref_graph();
737        let got = sorted_all(neighborhood_with_mode(&g, 1, NeighborhoodMode::In, 0).unwrap());
738        let exp: Vec<Vec<u32>> = vec![
739            vec![0, 2],
740            vec![0, 1],
741            vec![0, 2],
742            vec![1, 2, 3],
743            vec![3, 4],
744            vec![5],
745        ];
746        assert_eq!(got, exp);
747    }
748
749    #[test]
750    fn neighborhood_c_ref_order_10_all_saturates_set_eq() {
751        // 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)
752        //         4:(0 1 2 3 4) 5:(5)
753        let g = c_ref_graph();
754        let got = sorted_all(neighborhood(&g, 10).unwrap());
755        let big: Vec<u32> = (0..5).collect();
756        let exp: Vec<Vec<u32>> = vec![
757            big.clone(),
758            big.clone(),
759            big.clone(),
760            big.clone(),
761            big,
762            vec![5],
763        ];
764        assert_eq!(got, exp);
765    }
766
767    #[test]
768    fn neighborhood_c_ref_order_2_mindist_2_out_set_eq() {
769        // C .out: 0:(3) 1:(4) 2:(1 4) 3:() 4:() 5:()
770        let g = c_ref_graph();
771        let got = sorted_all(neighborhood_with_mode(&g, 2, NeighborhoodMode::Out, 2).unwrap());
772        let exp: Vec<Vec<u32>> = vec![vec![3], vec![4], vec![1, 4], vec![], vec![], vec![]];
773        assert_eq!(got, exp);
774    }
775
776    #[test]
777    fn neighborhood_c_ref_order_4_mindist_4_all_empty() {
778        // Diameter < 4 ⇒ all empty.
779        let g = c_ref_graph();
780        let got = neighborhood_with_mode(&g, 4, NeighborhoodMode::All, 4).unwrap();
781        let exp: Vec<Vec<u32>> = vec![vec![]; 6];
782        assert_eq!(got, exp);
783    }
784
785    #[test]
786    fn neighborhood_c_ref_infinite_order_out_set_eq() {
787        // 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)
788        let g = c_ref_graph();
789        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::Out, 0).unwrap());
790        let exp: Vec<Vec<u32>> = vec![
791            vec![0, 1, 2, 3, 4],
792            vec![1, 3, 4],
793            vec![0, 1, 2, 3, 4],
794            vec![3, 4],
795            vec![4],
796            vec![5],
797        ];
798        assert_eq!(got, exp);
799    }
800
801    #[test]
802    fn neighborhood_c_ref_infinite_mindist_2_out_set_eq() {
803        // C .out: 0:(3 4) 1:(4) 2:(1 4) 3:() 4:() 5:()
804        let g = c_ref_graph();
805        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::Out, 2).unwrap());
806        let exp: Vec<Vec<u32>> = vec![vec![3, 4], vec![4], vec![1, 4], vec![], vec![], vec![]];
807        assert_eq!(got, exp);
808    }
809
810    #[test]
811    fn neighborhood_c_ref_infinite_mindist_2_in_set_eq() {
812        // C .out: 0:() 1:(2) 2:() 3:(0) 4:(0 1 2) 5:()
813        let g = c_ref_graph();
814        let got = sorted_all(neighborhood_with_mode(&g, -1, NeighborhoodMode::In, 2).unwrap());
815        let exp: Vec<Vec<u32>> = vec![vec![], vec![2], vec![], vec![0], vec![0, 1, 2], vec![]];
816        assert_eq!(got, exp);
817    }
818
819    #[test]
820    fn neighborhood_negative_mindist_errors() {
821        let g = Graph::with_vertices(3);
822        match neighborhood_with_mode(&g, 2, NeighborhoodMode::All, -1) {
823            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("negative")),
824            other => panic!("expected InvalidArgument for negative mindist, got {other:?}"),
825        }
826    }
827
828    #[test]
829    fn neighborhood_mindist_exceeds_order_errors() {
830        let g = Graph::with_vertices(3);
831        match neighborhood_with_mode(&g, 2, NeighborhoodMode::All, 3) {
832            Err(IgraphError::InvalidArgument(msg)) => assert!(msg.contains("exceed")),
833            other => panic!("expected InvalidArgument, got {other:?}"),
834        }
835    }
836
837    #[test]
838    fn neighborhood_lengths_match_neighborhood_size() {
839        // PR-027b list lengths should equal PR-027 sizes for any
840        // (order, mode, mindist) triple — the two are tightly coupled.
841        let g = c_ref_graph();
842        for order in [0_i32, 1, 2, 3, 10, -1] {
843            for &mode in &[
844                NeighborhoodMode::Out,
845                NeighborhoodMode::In,
846                NeighborhoodMode::All,
847            ] {
848                for mindist in [0_i32, 1, 2] {
849                    if order >= 0 && mindist > order {
850                        continue;
851                    }
852                    let sizes = neighborhood_size_with_mode(&g, order, mode, mindist).unwrap();
853                    let lists = neighborhood_with_mode(&g, order, mode, mindist).unwrap();
854                    let list_lens: Vec<u32> = lists
855                        .iter()
856                        .map(|v| u32::try_from(v.len()).unwrap())
857                        .collect();
858                    assert_eq!(
859                        sizes, list_lens,
860                        "size/list-len mismatch at order={order} mode={mode:?} mindist={mindist}",
861                    );
862                }
863            }
864        }
865    }
866
867    #[test]
868    fn neighborhood_self_loop_not_double_visited() {
869        let mut g = Graph::with_vertices(3);
870        g.add_edge(0, 0).unwrap();
871        g.add_edge(0, 1).unwrap();
872        g.add_edge(1, 2).unwrap();
873        let got = sorted_all(neighborhood(&g, 1).unwrap());
874        assert_eq!(got, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
875    }
876
877    #[test]
878    fn neighborhood_multi_edge_not_double_visited() {
879        let mut g = Graph::with_vertices(3);
880        g.add_edge(0, 1).unwrap();
881        g.add_edge(0, 1).unwrap();
882        g.add_edge(1, 2).unwrap();
883        let got = sorted_all(neighborhood(&g, 1).unwrap());
884        assert_eq!(got, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
885    }
886
887    #[test]
888    fn neighborhood_mindist_1_excludes_self() {
889        // P5 order 1 mindist 1 → only neighbours, not self.
890        let mut g = Graph::with_vertices(5);
891        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4)] {
892            g.add_edge(u, v).unwrap();
893        }
894        let got = sorted_all(neighborhood_with_mode(&g, 1, NeighborhoodMode::All, 1).unwrap());
895        assert_eq!(
896            got,
897            vec![vec![1], vec![0, 2], vec![1, 3], vec![2, 4], vec![3]]
898        );
899        for list in &got {
900            for (i, neighbors) in got.iter().enumerate() {
901                let i_u32 = u32::try_from(i).unwrap();
902                if std::ptr::eq(list, neighbors) {
903                    assert!(!list.contains(&i_u32), "mindist=1 should exclude self");
904                }
905            }
906        }
907    }
908}