Skip to main content

rust_igraph/core/
graph.rs

1//! `Graph` — pure-Rust port of `igraph_t`.
2//!
3//! Storage is the **indexed edge list** that upstream igraph uses (see
4//! `references/igraph/include/igraph_datatype.h:105-116`):
5//!
6//! - `from[e]`, `to[e]` — canonical edge list. Edge `e` runs from
7//!   `from[e]` to `to[e]`; `|from| == |to| == ecount`.
8//! - `oi[i]` — edge ids ordered by `from` (and then `to`).
9//! - `ii[i]` — edge ids ordered by `to` (and then `from`).
10//! - `os[v]..os[v+1]` — slice of `oi` covering vertex `v`'s out-edges.
11//! - `is[v]..is[v+1]` — slice of `ii` covering vertex `v`'s in-edges.
12//!
13//! For undirected graphs the edge list is canonicalised so `from[e] <= to[e]`
14//! (matching upstream igraph's invariant in `type_indexededgelist.c:282-288`).
15//! The doubled in/out indexing makes `neighbors()` symmetric for undirected
16//! graphs without storing each edge twice.
17//!
18//! ALGO-CORE-001a (Phase 1, this file): struct + `new`/`with_vertices` +
19//! `add_vertices`/`add_edge`/`add_edges` + `vcount`/`ecount`/`is_directed` +
20//! `neighbors`/`degree` + `Clone`.
21//!
22//! Follow-up AWUs:
23//! - 001b: `incident`, edge-id helpers.
24//! - 001c: `delete_vertices`/`delete_edges`.
25//! - 001d: `edge`/`edges`/`get_eid`/`get_eids`/`get_all_eids_between`.
26//! - 001e: property cache, `is_same_graph`.
27//!
28//! Attribute system → ALGO-AT-* (out of scope here).
29
30use super::cache::{
31    CachedProperty, PropertyCache, invalidate_after_add_edges, invalidate_after_add_vertices,
32};
33use super::error::{IgraphError, IgraphResult};
34
35/// Vertex id. The Phase-0 ADR-0007 fixes this to `u32`; `Option<VertexId>`
36/// is the idiomatic "no vertex" sentinel (igraph C uses `-1`).
37pub type VertexId = u32;
38
39/// Edge id. Same width as [`VertexId`]; an edge id is its position in
40/// `from`/`to`.
41pub type EdgeId = u32;
42
43/// Counterpart of `igraph_t` (see `references/igraph/include/igraph_datatype.h`).
44///
45/// Phase-0 callers (`bfs`, `read_edgelist`, oracle tests) only depended on
46/// `with_vertices`, `add_edge`, `add_edges`, `vcount`, `ecount`, `neighbors`,
47/// `degree` — those signatures are preserved here, so existing call sites
48/// compile unchanged. New for Phase 1: `new` (with `directed` flag),
49/// `is_directed`.
50#[derive(Debug, Clone, Default)]
51pub struct Graph {
52    /// Vertex count. Redundant with the highest used id; mirrors `igraph_t::n`.
53    n: u32,
54    /// Whether the graph is directed.
55    directed: bool,
56    /// Source endpoints, one per edge.
57    from: Vec<VertexId>,
58    /// Target endpoints, one per edge.
59    to: Vec<VertexId>,
60    /// Edge ids in `from`-major order.
61    oi: Vec<EdgeId>,
62    /// Edge ids in `to`-major order.
63    ii: Vec<EdgeId>,
64    /// `os[v]..os[v+1]` is the slice of `oi` for vertex `v`'s out-edges.
65    /// Length is `n + 1`; `os[0] == 0`, `os[n] == ecount`.
66    os: Vec<u32>,
67    /// `is[v]..is[v+1]` for incoming. Same shape as `os`.
68    is: Vec<u32>,
69    /// Boolean property cache. Mirrors `igraph_t::cache`.
70    cache: PropertyCache,
71}
72
73impl Graph {
74    /// Construct an empty graph on `n` vertices.
75    ///
76    /// Counterpart of `igraph_empty()`; `directed` defaults to `false` if
77    /// you use [`Graph::with_vertices`] instead.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use rust_igraph::Graph;
83    ///
84    /// let g = Graph::new(5, true).unwrap();
85    /// assert_eq!(g.vcount(), 5);
86    /// assert_eq!(g.ecount(), 0);
87    /// assert!(g.is_directed());
88    /// ```
89    pub fn new(n: u32, directed: bool) -> IgraphResult<Self> {
90        let mut g = Self {
91            n: 0,
92            directed,
93            from: Vec::new(),
94            to: Vec::new(),
95            oi: Vec::new(),
96            ii: Vec::new(),
97            os: vec![0],
98            is: vec![0],
99            cache: PropertyCache::new(),
100        };
101        g.add_vertices(n)?;
102        Ok(g)
103    }
104
105    /// Build a graph from an edge list, inferring the vertex count from
106    /// the highest endpoint.
107    ///
108    /// This is the most ergonomic way to create a small graph. The vertex
109    /// count is `max(u, v) + 1` over all `(u, v)` pairs (or 0 if `edges`
110    /// is empty and `n_override` is `None`).
111    ///
112    /// `n_override` can force a minimum vertex count (useful when you want
113    /// isolated vertices beyond the edges). Pass `None` to auto-derive.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use rust_igraph::Graph;
119    ///
120    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false, None).unwrap();
121    /// assert_eq!(g.vcount(), 3);
122    /// assert_eq!(g.ecount(), 3);
123    /// assert!(!g.is_directed());
124    /// ```
125    pub fn from_edges(
126        edges: &[(u32, u32)],
127        directed: bool,
128        n_override: Option<u32>,
129    ) -> IgraphResult<Self> {
130        let max_id = edges
131            .iter()
132            .flat_map(|&(u, v)| [u, v])
133            .max()
134            .map_or(Some(0), |m| m.checked_add(1));
135        let auto_n = max_id.ok_or(IgraphError::InvalidArgument(
136            "vertex id overflow in from_edges".to_owned(),
137        ))?;
138        let n = n_override.map_or(auto_n, |ov| ov.max(auto_n));
139        let mut g = Self::new(n, directed)?;
140        g.add_edges(edges.to_vec())?;
141        Ok(g)
142    }
143
144    /// Construct an empty *undirected* graph on `n` vertices.
145    ///
146    /// Builds the graph directly (no intermediate `Result`) since an
147    /// empty undirected graph with `n` vertices cannot fail to construct.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use rust_igraph::Graph;
153    ///
154    /// let g = Graph::with_vertices(4);
155    /// assert_eq!(g.vcount(), 4);
156    /// assert!(!g.is_directed());
157    /// ```
158    pub fn with_vertices(n: u32) -> Self {
159        let len = n as usize + 1;
160        Self {
161            n,
162            directed: false,
163            from: Vec::new(),
164            to: Vec::new(),
165            oi: Vec::new(),
166            ii: Vec::new(),
167            os: vec![0; len],
168            is: vec![0; len],
169            cache: PropertyCache::new(),
170        }
171    }
172
173    /// Number of vertices. Counterpart of `igraph_vcount()`.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use rust_igraph::Graph;
179    ///
180    /// let g = Graph::with_vertices(10);
181    /// assert_eq!(g.vcount(), 10);
182    /// ```
183    #[must_use]
184    pub fn vcount(&self) -> u32 {
185        self.n
186    }
187
188    /// Number of edges. Counterpart of `igraph_ecount()`.
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// use rust_igraph::Graph;
194    ///
195    /// let mut g = Graph::with_vertices(3);
196    /// g.add_edge(0, 1).unwrap();
197    /// g.add_edge(1, 2).unwrap();
198    /// assert_eq!(g.ecount(), 2);
199    /// ```
200    #[must_use]
201    pub fn ecount(&self) -> usize {
202        self.from.len()
203    }
204
205    /// `true` if the graph is directed. Counterpart of `igraph_is_directed()`.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use rust_igraph::Graph;
211    ///
212    /// let g = Graph::new(3, true).unwrap();
213    /// assert!(g.is_directed());
214    ///
215    /// let g2 = Graph::with_vertices(3);
216    /// assert!(!g2.is_directed());
217    /// ```
218    #[must_use]
219    pub fn is_directed(&self) -> bool {
220        self.directed
221    }
222
223    /// Append `nv` isolated vertices, returning the inclusive id range
224    /// `(first, last)` of the new vertices. If `nv == 0` returns
225    /// `(self.n, self.n)` and does nothing.
226    ///
227    /// Counterpart of `igraph_add_vertices()`.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use rust_igraph::Graph;
233    ///
234    /// let mut g = Graph::with_vertices(3);
235    /// let (first, last) = g.add_vertices(2).unwrap();
236    /// assert_eq!(first, 3);
237    /// assert_eq!(last, 4);
238    /// assert_eq!(g.vcount(), 5);
239    /// ```
240    pub fn add_vertices(&mut self, nv: u32) -> IgraphResult<(VertexId, VertexId)> {
241        let new_n = self
242            .n
243            .checked_add(nv)
244            .ok_or(IgraphError::Internal("vertex count overflow"))?;
245        let first = self.n;
246        // os/is grow by `nv` entries, all initialised to ecount.
247        let ec = u32::try_from(self.ecount())
248            .map_err(|_| IgraphError::Internal("edge count exceeds u32::MAX"))?;
249        for _ in 0..nv {
250            self.os.push(ec);
251            self.is.push(ec);
252        }
253        self.n = new_n;
254        if nv > 0 {
255            invalidate_after_add_vertices(&self.cache);
256        }
257        Ok((first, new_n.saturating_sub(1)))
258    }
259
260    /// Add a single edge from `u` to `v`.
261    ///
262    /// Self-loops and parallel edges are allowed. For undirected graphs the
263    /// edge is canonicalised so the stored `from <= to`.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use rust_igraph::Graph;
269    ///
270    /// let mut g = Graph::with_vertices(3);
271    /// g.add_edge(0, 1).unwrap();
272    /// g.add_edge(1, 2).unwrap();
273    /// assert_eq!(g.ecount(), 2);
274    /// ```
275    pub fn add_edge(&mut self, u: VertexId, v: VertexId) -> IgraphResult<()> {
276        self.add_edges(std::iter::once((u, v)))
277    }
278
279    /// Add a sequence of edges. After all edges are appended, the indexes
280    /// (`oi` / `ii` / `os` / `is`) are rebuilt in one pass — counterpart of
281    /// `igraph_add_edges` (`type_indexededgelist.c:254-367`).
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use rust_igraph::Graph;
287    ///
288    /// let mut g = Graph::with_vertices(4);
289    /// g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
290    /// assert_eq!(g.ecount(), 3);
291    /// ```
292    pub fn add_edges<I>(&mut self, edges: I) -> IgraphResult<()>
293    where
294        I: IntoIterator<Item = (VertexId, VertexId)>,
295    {
296        let m_before = self.ecount();
297        for (u, v) in edges {
298            self.check_vertex(u)?;
299            self.check_vertex(v)?;
300            // Undirected canonicalisation: store the smaller endpoint as `from`.
301            if !self.directed && u > v {
302                self.from.push(v);
303                self.to.push(u);
304            } else {
305                self.from.push(u);
306                self.to.push(v);
307            }
308        }
309        self.rebuild_indexes()?;
310        if self.ecount() > m_before {
311            invalidate_after_add_edges(&self.cache);
312        }
313        Ok(())
314    }
315
316    /// Out-edge neighbour iterator for vertex `v`.
317    ///
318    /// For undirected graphs this returns *all* neighbours (since the
319    /// indexing tracks both endpoints symmetrically). Order is the upstream
320    /// igraph order — edges are visited in `oi` order, then `ii` order, with
321    /// duplicates suppressed when the same edge is incident on both.
322    ///
323    /// Counterpart of `igraph_neighbors(graph, _, vid, IGRAPH_ALL, ...)`.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use rust_igraph::Graph;
329    ///
330    /// let mut g = Graph::with_vertices(4);
331    /// g.add_edge(0, 1).unwrap();
332    /// g.add_edge(0, 2).unwrap();
333    /// g.add_edge(0, 3).unwrap();
334    /// let neis = g.neighbors(0).unwrap();
335    /// assert_eq!(neis, vec![1, 2, 3]);
336    /// ```
337    pub fn neighbors(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
338        self.check_vertex(v)?;
339        let v_idx = v as usize;
340        if self.directed {
341            // Directed: only outgoing neighbours; oi sorted by (from, to)
342            // so the out-neighbour list is already sorted ascending.
343            let out_range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
344            let out: Vec<VertexId> = self.oi[out_range]
345                .iter()
346                .map(|&e| self.to[e as usize])
347                .collect();
348            Ok(out)
349        } else {
350            // Undirected: merge the two already-sorted sublists from oi
351            // (out-side, ascending in `to`) and ii (in-side, ascending
352            // in `from`) into one ascending neighbour list. Matches
353            // upstream `igraph_neighbors(_, _, _, IGRAPH_ALL)` and
354            // python-igraph's `Graph.neighbors(v)` exactly.
355            let out_start = self.os[v_idx] as usize;
356            let out_end = self.os[v_idx + 1] as usize;
357            let in_start = self.is[v_idx] as usize;
358            let in_end = self.is[v_idx + 1] as usize;
359            let mut out = Vec::with_capacity((out_end - out_start) + (in_end - in_start));
360            let mut out_idx = out_start;
361            let mut in_idx = in_start;
362            while out_idx < out_end && in_idx < in_end {
363                let a = self.to[self.oi[out_idx] as usize];
364                let b = self.from[self.ii[in_idx] as usize];
365                if a <= b {
366                    out.push(a);
367                    out_idx += 1;
368                } else {
369                    out.push(b);
370                    in_idx += 1;
371                }
372            }
373            while out_idx < out_end {
374                out.push(self.to[self.oi[out_idx] as usize]);
375                out_idx += 1;
376            }
377            while in_idx < in_end {
378                out.push(self.from[self.ii[in_idx] as usize]);
379                in_idx += 1;
380            }
381            Ok(out)
382        }
383    }
384
385    /// Degree of vertex `v` — number of edges incident to it.
386    ///
387    /// On undirected graphs every edge counts once except a self-loop which
388    /// counts twice (matches upstream igraph's `IGRAPH_LOOPS = TWICE` default
389    /// at `type_indexededgelist.c:1162`).
390    ///
391    /// Counterpart of `igraph_degree_1(_, _, _, IGRAPH_ALL, IGRAPH_LOOPS_TWICE)`.
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// use rust_igraph::Graph;
397    ///
398    /// let mut g = Graph::with_vertices(3);
399    /// g.add_edge(0, 1).unwrap();
400    /// g.add_edge(0, 2).unwrap();
401    /// assert_eq!(g.degree(0).unwrap(), 2);
402    /// assert_eq!(g.degree(1).unwrap(), 1);
403    /// ```
404    pub fn degree(&self, v: VertexId) -> IgraphResult<usize> {
405        self.check_vertex(v)?;
406        let v_idx = v as usize;
407        let out = (self.os[v_idx + 1] - self.os[v_idx]) as usize;
408        if self.directed {
409            let in_count = (self.is[v_idx + 1] - self.is[v_idx]) as usize;
410            Ok(out + in_count)
411        } else {
412            // Undirected: out + in. Self-loops appear in both (they are
413            // stored once with `from == to` but indexed in both `os` and
414            // `is` slots), so naturally count twice.
415            let in_count = (self.is[v_idx + 1] - self.is[v_idx]) as usize;
416            Ok(out + in_count)
417        }
418    }
419
420    // ---------------------------------------------------------------
421    // ALGO-CORE-001b: edge-id helpers + incident edges.
422    // ---------------------------------------------------------------
423
424    /// Source endpoint of edge `eid`. Counterpart of `IGRAPH_FROM`
425    /// (`igraph_interface.h:115`).
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// use rust_igraph::Graph;
431    ///
432    /// let mut g = Graph::with_vertices(3);
433    /// g.add_edge(0, 2).unwrap();
434    /// assert_eq!(g.edge_source(0).unwrap(), 0);
435    /// ```
436    pub fn edge_source(&self, eid: EdgeId) -> IgraphResult<VertexId> {
437        self.check_edge(eid)?;
438        Ok(self.from[eid as usize])
439    }
440
441    /// Target endpoint of edge `eid`. Counterpart of `IGRAPH_TO`
442    /// (`igraph_interface.h:128`).
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use rust_igraph::Graph;
448    ///
449    /// let mut g = Graph::with_vertices(3);
450    /// g.add_edge(0, 2).unwrap();
451    /// assert_eq!(g.edge_target(0).unwrap(), 2);
452    /// ```
453    pub fn edge_target(&self, eid: EdgeId) -> IgraphResult<VertexId> {
454        self.check_edge(eid)?;
455        Ok(self.to[eid as usize])
456    }
457
458    /// Both endpoints of edge `eid`, ordered as `(from, to)`. Counterpart
459    /// of `igraph_edge` (`igraph_interface.h:71`).
460    ///
461    /// # Examples
462    ///
463    /// ```
464    /// use rust_igraph::Graph;
465    ///
466    /// let mut g = Graph::with_vertices(3);
467    /// g.add_edge(0, 1).unwrap();
468    /// let (from, to) = g.edge(0).unwrap();
469    /// assert_eq!(from, 0);
470    /// assert_eq!(to, 1);
471    /// ```
472    pub fn edge(&self, eid: EdgeId) -> IgraphResult<(VertexId, VertexId)> {
473        self.check_edge(eid)?;
474        let i = eid as usize;
475        Ok((self.from[i], self.to[i]))
476    }
477
478    /// The other endpoint of `eid` given one endpoint `vid`. Counterpart
479    /// of `IGRAPH_OTHER` (`igraph_interface.h:145`). Errors if `vid` is
480    /// not actually an endpoint of `eid`.
481    ///
482    /// # Examples
483    ///
484    /// ```
485    /// use rust_igraph::Graph;
486    ///
487    /// let mut g = Graph::with_vertices(3);
488    /// g.add_edge(0, 2).unwrap();
489    /// assert_eq!(g.edge_other(0, 0).unwrap(), 2);
490    /// assert_eq!(g.edge_other(0, 2).unwrap(), 0);
491    /// ```
492    pub fn edge_other(&self, eid: EdgeId, vid: VertexId) -> IgraphResult<VertexId> {
493        let (u, v) = self.edge(eid)?;
494        if vid == u {
495            Ok(v)
496        } else if vid == v {
497            Ok(u)
498        } else {
499            Err(IgraphError::InvalidArgument(format!(
500                "vertex {vid} is not an endpoint of edge {eid} ({u}, {v})"
501            )))
502        }
503    }
504
505    /// Edge ids incident to vertex `v`, in the same iteration order as
506    /// [`Graph::neighbors`].
507    ///
508    /// For undirected graphs returns the union of out-side (`oi`) and
509    /// in-side (`ii`) edges — every edge incident to `v` once, except
510    /// self-loops which appear twice (matching `igraph_neighbors` /
511    /// `igraph_degree`'s `IGRAPH_LOOPS_TWICE` default at
512    /// `type_indexededgelist.c:1162`).
513    ///
514    /// For directed graphs returns out-edges only, mirroring this AWU's
515    /// `neighbors()` choice. (The full mode-aware variant lands later
516    /// alongside `igraph_neighbors(mode = IN/OUT/ALL)`.)
517    ///
518    /// Counterpart of `igraph_incident(_, _, v, IGRAPH_ALL, IGRAPH_LOOPS_TWICE)`
519    /// for undirected; `IGRAPH_OUT` mode for directed.
520    ///
521    /// # Examples
522    ///
523    /// ```
524    /// use rust_igraph::Graph;
525    ///
526    /// let mut g = Graph::with_vertices(3);
527    /// g.add_edge(0, 1).unwrap(); // edge 0
528    /// g.add_edge(0, 2).unwrap(); // edge 1
529    /// let inc = g.incident(0).unwrap();
530    /// assert_eq!(inc.len(), 2);
531    /// ```
532    pub fn incident(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
533        self.check_vertex(v)?;
534        let v_idx = v as usize;
535        let out_range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
536        if self.directed {
537            Ok(self.oi[out_range].to_vec())
538        } else {
539            let in_range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
540            let mut out = Vec::with_capacity(out_range.len() + in_range.len());
541            out.extend_from_slice(&self.oi[out_range]);
542            out.extend_from_slice(&self.ii[in_range]);
543            Ok(out)
544        }
545    }
546
547    /// Companion to [`incident`](Self::incident): returns *only* the
548    /// edges incoming to `v` for directed graphs. For undirected
549    /// graphs the result is identical to `incident` (every edge is
550    /// bidirectional).
551    ///
552    /// Counterpart of `igraph_incident(_, _, v, IGRAPH_IN, IGRAPH_LOOPS_TWICE)`.
553    pub(crate) fn incident_in(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
554        self.check_vertex(v)?;
555        let v_idx = v as usize;
556        if self.directed {
557            let in_range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
558            Ok(self.ii[in_range].to_vec())
559        } else {
560            self.incident(v)
561        }
562    }
563
564    /// Edge id between `from` and `to`, if any.
565    ///
566    /// On undirected graphs `(u, v)` and `(v, u)` are equivalent.
567    /// On directed graphs the search follows the edge direction
568    /// `from -> to`. Returns [`crate::IgraphError::InvalidArgument`]
569    /// when no such edge exists; for the "no error, return None" variant
570    /// use [`Self::find_eid`].
571    ///
572    /// Counterpart of
573    /// `igraph_get_eid(_, _, from, to, /*directed=*/true, /*error=*/true)`
574    /// from `references/igraph/src/graph/type_indexededgelist.c:1522-1555`.
575    /// Phase-1 minimal slice: linear scan across the from-bucket; the
576    /// upstream binary-search optimisation lands in a perf pass.
577    ///
578    /// # Examples
579    ///
580    /// ```
581    /// use rust_igraph::Graph;
582    ///
583    /// let mut g = Graph::with_vertices(3);
584    /// g.add_edge(0, 1).unwrap();
585    /// g.add_edge(1, 2).unwrap();
586    /// assert_eq!(g.get_eid(0, 1).unwrap(), 0);
587    /// assert_eq!(g.get_eid(1, 2).unwrap(), 1);
588    /// assert!(g.get_eid(0, 2).is_err());
589    /// ```
590    pub fn get_eid(&self, from: VertexId, to: VertexId) -> IgraphResult<EdgeId> {
591        self.find_eid(from, to)?
592            .ok_or_else(|| IgraphError::InvalidArgument(format!("no edge between {from} and {to}")))
593    }
594
595    /// Edge id between `from` and `to`, or `None` if not connected.
596    ///
597    /// Same semantics as [`Self::get_eid`] but no-error variant
598    /// matching upstream's `error=false` mode. When parallel edges
599    /// exist, returns the lowest edge id (matching upstream's
600    /// "always returns the same edge ID" guarantee).
601    ///
602    /// # Examples
603    ///
604    /// ```
605    /// use rust_igraph::Graph;
606    ///
607    /// let mut g = Graph::with_vertices(3);
608    /// g.add_edge(0, 1).unwrap();
609    /// assert_eq!(g.find_eid(0, 1).unwrap(), Some(0));
610    /// assert_eq!(g.find_eid(0, 2).unwrap(), None);
611    /// ```
612    pub fn find_eid(&self, from: VertexId, to: VertexId) -> IgraphResult<Option<EdgeId>> {
613        self.check_vertex(from)?;
614        self.check_vertex(to)?;
615        if self.directed {
616            // Search out-bucket of `from` for `to[e] == to`.
617            let range = self.os[from as usize] as usize..self.os[from as usize + 1] as usize;
618            for &e in &self.oi[range] {
619                if self.to[e as usize] == to {
620                    return Ok(Some(e));
621                }
622            }
623            Ok(None)
624        } else {
625            // Undirected: edges canonicalised so `from[e] <= to[e]`.
626            // Search the bucket of the smaller endpoint for the larger.
627            let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
628            let range = self.os[lo as usize] as usize..self.os[lo as usize + 1] as usize;
629            for &e in &self.oi[range] {
630                if self.to[e as usize] == hi {
631                    return Ok(Some(e));
632                }
633            }
634            Ok(None)
635        }
636    }
637
638    /// All edge ids between `from` and `to`, including parallel edges
639    /// and (for self-loops) the loop edge once.
640    ///
641    /// Counterpart of
642    /// `igraph_get_all_eids_between()` from
643    /// `references/igraph/src/graph/type_indexededgelist.c:~1700`.
644    /// On undirected graphs `(u, v)` and `(v, u)` are equivalent. The
645    /// returned vector is sorted ascending by edge id.
646    ///
647    /// # Examples
648    ///
649    /// ```
650    /// use rust_igraph::Graph;
651    ///
652    /// let mut g = Graph::with_vertices(2);
653    /// g.add_edge(0, 1).unwrap();
654    /// g.add_edge(0, 1).unwrap(); // parallel edge
655    /// let eids = g.get_all_eids_between(0, 1).unwrap();
656    /// assert_eq!(eids, vec![0, 1]);
657    /// ```
658    pub fn get_all_eids_between(&self, from: VertexId, to: VertexId) -> IgraphResult<Vec<EdgeId>> {
659        self.check_vertex(from)?;
660        self.check_vertex(to)?;
661        let mut out = Vec::new();
662        if self.directed {
663            let range = self.os[from as usize] as usize..self.os[from as usize + 1] as usize;
664            for &e in &self.oi[range] {
665                if self.to[e as usize] == to {
666                    out.push(e);
667                }
668            }
669        } else {
670            let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
671            let range = self.os[lo as usize] as usize..self.os[lo as usize + 1] as usize;
672            for &e in &self.oi[range] {
673                if self.to[e as usize] == hi {
674                    out.push(e);
675                }
676            }
677        }
678        out.sort_unstable();
679        Ok(out)
680    }
681
682    /// Out-neighbours of `v` (always — directed or undirected). Each
683    /// edge contributes one entry, in `oi[os[v]..os[v+1]]` order
684    /// (lex by `(from, to)`). Self-loops appear once.
685    ///
686    /// Internal helper used by direction-aware algorithms (e.g.
687    /// strongly connected components). The full mode-aware public
688    /// surface ships with the next `igraph_neighbors` AWU.
689    pub(crate) fn out_neighbors_vec(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
690        self.check_vertex(v)?;
691        let v_idx = v as usize;
692        let range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
693        Ok(self.oi[range]
694            .iter()
695            .map(|&e| self.to[e as usize])
696            .collect())
697    }
698
699    /// In-neighbours of `v` (always — directed or undirected). Each
700    /// edge contributes one entry, in `ii[is[v]..is[v+1]]` order
701    /// (lex by `(to, from)`). Self-loops appear once.
702    ///
703    /// Companion to [`out_neighbors_vec`](Self::out_neighbors_vec); see
704    /// its doc for context on visibility.
705    pub(crate) fn in_neighbors_vec(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
706        self.check_vertex(v)?;
707        let v_idx = v as usize;
708        let range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
709        Ok(self.ii[range]
710            .iter()
711            .map(|&e| self.from[e as usize])
712            .collect())
713    }
714
715    // ---------------------------------------------------------------
716    // ALGO-CORE-001c: delete_edges + delete_vertices + delete_vertices_map.
717    // ---------------------------------------------------------------
718
719    /// Remove the given edges from the graph.
720    ///
721    /// `edges` may contain the same id more than once — the second and
722    /// later occurrences are no-ops. Remaining edges keep their
723    /// pairwise relative order but are renumbered so edge ids stay
724    /// contiguous starting at 0. Returns
725    /// [`IgraphError::EdgeOutOfRange`] if any id is `>= ecount()`; on
726    /// error the graph is left unchanged.
727    ///
728    /// Counterpart of `igraph_delete_edges`
729    /// (`references/igraph/src/graph/type_indexededgelist.c:500`).
730    ///
731    /// # Examples
732    ///
733    /// ```
734    /// use rust_igraph::Graph;
735    ///
736    /// let mut g = Graph::with_vertices(3);
737    /// g.add_edge(0, 1).unwrap();
738    /// g.add_edge(1, 2).unwrap();
739    /// g.add_edge(0, 2).unwrap();
740    /// g.delete_edges(&[1]).unwrap(); // remove edge 1-2
741    /// assert_eq!(g.ecount(), 2);
742    /// ```
743    pub fn delete_edges(&mut self, edges: &[EdgeId]) -> IgraphResult<()> {
744        let m = self.ecount();
745        let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
746
747        // Validate up front so a bad id leaves graph state untouched.
748        for &eid in edges {
749            if (eid as usize) >= m {
750                return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
751            }
752        }
753        if edges.is_empty() {
754            return Ok(());
755        }
756
757        let mut remove = vec![false; m];
758        for &eid in edges {
759            remove[eid as usize] = true;
760        }
761
762        let mut new_from: Vec<VertexId> = Vec::with_capacity(m);
763        let mut new_to: Vec<VertexId> = Vec::with_capacity(m);
764        for (e, &is_removed) in remove.iter().enumerate() {
765            if !is_removed {
766                new_from.push(self.from[e]);
767                new_to.push(self.to[e]);
768            }
769        }
770        self.from = new_from;
771        self.to = new_to;
772        self.rebuild_indexes()?;
773        self.cache.invalidate_all();
774        Ok(())
775    }
776
777    /// Remove the given vertices and all their incident edges.
778    ///
779    /// `vertices` may repeat ids freely. Surviving vertices get
780    /// renumbered so the new id space is `0..new_vcount` in their
781    /// previous relative order. Returns
782    /// [`IgraphError::VertexOutOfRange`] if any id is `>= vcount()`;
783    /// on error the graph is left unchanged.
784    ///
785    /// Counterpart of `igraph_delete_vertices`
786    /// (`references/igraph/src/graph/type_indexededgelist.c:540`).
787    ///
788    /// # Examples
789    ///
790    /// ```
791    /// use rust_igraph::Graph;
792    ///
793    /// let mut g = Graph::with_vertices(4);
794    /// g.add_edge(0, 1).unwrap();
795    /// g.add_edge(1, 2).unwrap();
796    /// g.add_edge(2, 3).unwrap();
797    /// g.delete_vertices(&[1]).unwrap();
798    /// assert_eq!(g.vcount(), 3);
799    /// assert_eq!(g.ecount(), 1); // only edge 2-3 survives (renumbered)
800    /// ```
801    pub fn delete_vertices(&mut self, vertices: &[VertexId]) -> IgraphResult<()> {
802        self.delete_vertices_map(vertices).map(|_| ())
803    }
804
805    /// Like [`delete_vertices`](Self::delete_vertices), but also returns
806    /// the old↔new vertex id mappings.
807    ///
808    /// Returns `(map, invmap)` where:
809    /// - `map[old_id] == Some(new_id)` if the vertex was retained, else
810    ///   `None`. Length is the *original* vertex count.
811    /// - `invmap[new_id] == old_id`. Length is the *new* vertex count.
812    ///
813    /// Counterpart of `igraph_delete_vertices_map`
814    /// (`references/igraph/src/graph/type_indexededgelist.c:645`).
815    ///
816    /// # Examples
817    ///
818    /// ```
819    /// use rust_igraph::Graph;
820    ///
821    /// let mut g = Graph::with_vertices(4);
822    /// g.add_edge(0, 1).unwrap();
823    /// g.add_edge(2, 3).unwrap();
824    /// let (map, invmap) = g.delete_vertices_map(&[1, 2]).unwrap();
825    /// assert_eq!(g.vcount(), 2);
826    /// assert_eq!(map, vec![Some(0), None, None, Some(1)]);
827    /// assert_eq!(invmap, vec![0, 3]);
828    /// ```
829    pub fn delete_vertices_map(
830        &mut self,
831        vertices: &[VertexId],
832    ) -> IgraphResult<(Vec<Option<VertexId>>, Vec<VertexId>)> {
833        let n_u32 = self.n;
834        let n = n_u32 as usize;
835
836        // Validate first.
837        for &vid in vertices {
838            if vid >= n_u32 {
839                return Err(IgraphError::VertexOutOfRange { id: vid, n: n_u32 });
840            }
841        }
842
843        let mut remove = vec![false; n];
844        for &vid in vertices {
845            remove[vid as usize] = true;
846        }
847
848        // Build map (old → new) and invmap (new → old).
849        let mut map: Vec<Option<VertexId>> = vec![None; n];
850        let mut invmap: Vec<VertexId> = Vec::new();
851        let mut next_new: u32 = 0;
852        for (i, &is_removed) in remove.iter().enumerate() {
853            if !is_removed {
854                let i_u32 = u32::try_from(i)
855                    .map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
856                map[i] = Some(next_new);
857                invmap.push(i_u32);
858                next_new = next_new
859                    .checked_add(1)
860                    .ok_or(IgraphError::Internal("new vertex count overflow"))?;
861            }
862        }
863
864        // Filter edges: keep only those with both endpoints retained,
865        // renumber endpoints via `map`.
866        let m = self.ecount();
867        let mut new_from: Vec<VertexId> = Vec::with_capacity(m);
868        let mut new_to: Vec<VertexId> = Vec::with_capacity(m);
869        for (u, v) in self.from.iter().zip(self.to.iter()) {
870            if let (Some(nu), Some(nv)) = (map[*u as usize], map[*v as usize]) {
871                new_from.push(nu);
872                new_to.push(nv);
873            }
874        }
875
876        self.n = next_new;
877        self.from = new_from;
878        self.to = new_to;
879        self.rebuild_indexes()?;
880        self.cache.invalidate_all();
881
882        Ok((map, invmap))
883    }
884
885    /// Look up a cached boolean property without computing it.
886    ///
887    /// Returns `None` if the property has not been cached yet. Pair with
888    /// [`Self::cache_set`] in compute functions:
889    ///
890    /// ```ignore
891    /// if let Some(v) = g.cache_get(CachedProperty::IsDag) { return v; }
892    /// let v = compute_is_dag(g);
893    /// g.cache_set(CachedProperty::IsDag, v);
894    /// v
895    /// ```
896    ///
897    /// Counterpart of `igraph_i_property_cache_has` + `_get_bool` from
898    /// `references/igraph/src/graph/caching.c`.
899    #[must_use]
900    pub fn cache_get(&self, prop: CachedProperty) -> Option<bool> {
901        self.cache.get(prop)
902    }
903
904    /// Store the value of a cached boolean property.
905    ///
906    /// Takes `&self` (interior mutability via `Cell`) — populating the
907    /// cache from a compute function is **not** considered a mutation of
908    /// the graph, matching igraph C semantics where compute helpers take
909    /// `const igraph_t *` and still write to the cache.
910    ///
911    /// Counterpart of `igraph_i_property_cache_set_bool`.
912    pub fn cache_set(&self, prop: CachedProperty, value: bool) {
913        self.cache.set(prop, value);
914    }
915
916    /// Drop the cached value of a single property (no-op if not cached).
917    ///
918    /// Use this if you change the graph via a private path that doesn't
919    /// go through `add_edges` / `delete_*`.
920    ///
921    /// Counterpart of `igraph_i_property_cache_invalidate`.
922    pub fn cache_invalidate(&self, prop: CachedProperty) {
923        self.cache.invalidate(prop);
924    }
925
926    /// Drop every cached boolean property.
927    ///
928    /// Counterpart of `igraph_i_property_cache_invalidate_all`.
929    pub fn cache_invalidate_all(&self) {
930        self.cache.invalidate_all();
931    }
932
933    fn check_vertex(&self, v: VertexId) -> IgraphResult<()> {
934        if v >= self.n {
935            return Err(IgraphError::VertexOutOfRange { id: v, n: self.n });
936        }
937        Ok(())
938    }
939
940    fn check_edge(&self, eid: EdgeId) -> IgraphResult<()> {
941        let m = self.ecount();
942        let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
943        if (eid as usize) >= m {
944            return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
945        }
946        Ok(())
947    }
948
949    /// Recompute `oi`, `ii`, `os`, `is` from `from`/`to`. Called after
950    /// any structural change.
951    ///
952    /// Each side does a stable lexicographic sort: `oi` orders edges by
953    /// `(from[e], to[e])`, `ii` by `(to[e], from[e])`. Time complexity
954    /// is `O(|V| + |E| log |E|)` (Rust stable sort) — same asymptotic
955    /// as upstream's `igraph_vector_int_pair_order`.
956    ///
957    /// The within-bucket secondary sort matches upstream igraph; without
958    /// it, `neighbors(v)` for an unsorted-edge-input graph diverges from
959    /// `python-igraph`'s output and breaks DFS order parity. (Counted
960    /// for an oracle-test failure during ALGO-TR-002 — see
961    /// `tests/oracle.rs::dfs_small_synthetic_matches_python_igraph`.)
962    ///
963    /// Counterpart of `igraph_i_create_start_vectors` + the
964    /// `igraph_vector_int_pair_order` calls in
965    /// `type_indexededgelist.c:309-336`.
966    fn rebuild_indexes(&mut self) -> IgraphResult<()> {
967        let m = self.ecount();
968        let n = self.n as usize;
969
970        // Build (primary_key, secondary_key, edge_id) tuples for each
971        // side, sort them lexicographically, then extract edge ids and
972        // the offset array.
973
974        // ---- Out-side: sort by (from, to). ----
975        let mut tuples: Vec<(VertexId, VertexId, u32)> = (0..m)
976            .map(|e| {
977                Ok::<_, IgraphError>((
978                    self.from[e],
979                    self.to[e],
980                    u32::try_from(e)
981                        .map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX"))?,
982                ))
983            })
984            .collect::<Result<_, _>>()?;
985        tuples.sort_unstable_by_key(|a| (a.0, a.1));
986        self.oi = tuples.iter().map(|t| t.2).collect();
987        // os[v] = number of entries with primary_key < v.
988        self.os = vec![0u32; n + 1];
989        for &(u, _, _) in &tuples {
990            self.os[u as usize + 1] = self.os[u as usize + 1]
991                .checked_add(1)
992                .ok_or(IgraphError::Internal("degree overflow in rebuild_indexes"))?;
993        }
994        for i in 1..=n {
995            self.os[i] = self.os[i]
996                .checked_add(self.os[i - 1])
997                .ok_or(IgraphError::Internal("offset overflow in rebuild_indexes"))?;
998        }
999
1000        // ---- In-side: sort by (to, from). ----
1001        let mut tuples: Vec<(VertexId, VertexId, u32)> = (0..m)
1002            .map(|e| {
1003                Ok::<_, IgraphError>((
1004                    self.to[e],
1005                    self.from[e],
1006                    u32::try_from(e)
1007                        .map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX"))?,
1008                ))
1009            })
1010            .collect::<Result<_, _>>()?;
1011        tuples.sort_unstable_by_key(|a| (a.0, a.1));
1012        self.ii = tuples.iter().map(|t| t.2).collect();
1013        self.is = vec![0u32; n + 1];
1014        for &(v, _, _) in &tuples {
1015            self.is[v as usize + 1] = self.is[v as usize + 1]
1016                .checked_add(1)
1017                .ok_or(IgraphError::Internal("degree overflow in rebuild_indexes"))?;
1018        }
1019        for i in 1..=n {
1020            self.is[i] = self.is[i]
1021                .checked_add(self.is[i - 1])
1022                .ok_or(IgraphError::Internal("offset overflow in rebuild_indexes"))?;
1023        }
1024
1025        Ok(())
1026    }
1027}
1028
1029impl std::fmt::Display for Graph {
1030    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1031        let kind = if self.directed {
1032            "Directed"
1033        } else {
1034            "Undirected"
1035        };
1036        write!(
1037            f,
1038            "{kind} graph with {} vertices and {} edges",
1039            self.n,
1040            self.ecount()
1041        )
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048
1049    #[test]
1050    fn empty_graph_counts() {
1051        let g = Graph::with_vertices(0);
1052        assert_eq!(g.vcount(), 0);
1053        assert_eq!(g.ecount(), 0);
1054        assert!(!g.is_directed());
1055    }
1056
1057    #[test]
1058    fn new_directed_flag() {
1059        let g = Graph::new(3, true).unwrap();
1060        assert!(g.is_directed());
1061        let g = Graph::new(3, false).unwrap();
1062        assert!(!g.is_directed());
1063    }
1064
1065    #[test]
1066    fn add_vertices_then_edges() {
1067        let mut g = Graph::with_vertices(3);
1068        g.add_edge(0, 1).unwrap();
1069        g.add_edge(1, 2).unwrap();
1070        assert_eq!(g.vcount(), 3);
1071        assert_eq!(g.ecount(), 2);
1072        assert_eq!(g.degree(1).unwrap(), 2);
1073        let mut nbrs = g.neighbors(1).unwrap();
1074        nbrs.sort_unstable();
1075        assert_eq!(nbrs, vec![0, 2]);
1076    }
1077
1078    #[test]
1079    fn out_of_range_vertex_errors() {
1080        let mut g = Graph::with_vertices(2);
1081        let err = g.add_edge(0, 5).unwrap_err();
1082        assert!(matches!(err, IgraphError::VertexOutOfRange { id: 5, n: 2 }));
1083    }
1084
1085    #[test]
1086    fn self_loop_counted_correctly() {
1087        let mut g = Graph::with_vertices(1);
1088        g.add_edge(0, 0).unwrap();
1089        assert_eq!(g.ecount(), 1);
1090        // Undirected self-loop: appears as both out and in, degree == 2.
1091        assert_eq!(g.degree(0).unwrap(), 2);
1092        let mut nbrs = g.neighbors(0).unwrap();
1093        nbrs.sort_unstable();
1094        assert_eq!(nbrs, vec![0, 0]);
1095    }
1096
1097    #[test]
1098    fn parallel_edges() {
1099        let mut g = Graph::with_vertices(2);
1100        g.add_edge(0, 1).unwrap();
1101        g.add_edge(0, 1).unwrap();
1102        assert_eq!(g.ecount(), 2);
1103        assert_eq!(g.degree(0).unwrap(), 2);
1104        assert_eq!(g.degree(1).unwrap(), 2);
1105    }
1106
1107    #[test]
1108    fn undirected_canonicalisation() {
1109        // Adding edges (1,0) and (0,1) — both stored canonically as (0,1).
1110        let mut g = Graph::with_vertices(2);
1111        g.add_edge(1, 0).unwrap();
1112        g.add_edge(0, 1).unwrap();
1113        assert_eq!(g.ecount(), 2);
1114        // Both vertices see each other as a neighbour twice.
1115        let mut n0 = g.neighbors(0).unwrap();
1116        let mut n1 = g.neighbors(1).unwrap();
1117        n0.sort_unstable();
1118        n1.sort_unstable();
1119        assert_eq!(n0, vec![1, 1]);
1120        assert_eq!(n1, vec![0, 0]);
1121    }
1122
1123    #[test]
1124    fn directed_neighbors_are_outgoing_only() {
1125        let mut g = Graph::new(3, true).unwrap();
1126        g.add_edge(0, 1).unwrap();
1127        g.add_edge(2, 0).unwrap();
1128        // Directed: neighbors(0) returns out-neighbours only.
1129        assert_eq!(g.neighbors(0).unwrap(), vec![1]);
1130        // Vertex 2 has out-edge to 0.
1131        assert_eq!(g.neighbors(2).unwrap(), vec![0]);
1132        // Vertex 1 has no out-edges.
1133        assert!(g.neighbors(1).unwrap().is_empty());
1134        // Degree counts both in and out for directed.
1135        assert_eq!(g.degree(0).unwrap(), 2); // out: 0->1, in: 2->0
1136        assert_eq!(g.degree(1).unwrap(), 1); // in: 0->1
1137        assert_eq!(g.degree(2).unwrap(), 1); // out: 2->0
1138    }
1139
1140    #[test]
1141    fn add_edges_batch_then_rebuild() {
1142        let mut g = Graph::with_vertices(4);
1143        g.add_edges(vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap();
1144        assert_eq!(g.ecount(), 4);
1145        // Degrees: 0->{1,2} d=2; 1->{0,2} d=2; 2->{0,1,3} d=3; 3->{2} d=1.
1146        assert_eq!(g.degree(0).unwrap(), 2);
1147        assert_eq!(g.degree(1).unwrap(), 2);
1148        assert_eq!(g.degree(2).unwrap(), 3);
1149        assert_eq!(g.degree(3).unwrap(), 1);
1150    }
1151
1152    #[test]
1153    fn clone_is_deep() {
1154        let mut g = Graph::with_vertices(3);
1155        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1156        let g2 = g.clone();
1157        // Mutate g; g2 must be unaffected.
1158        g.add_edge(0, 2).unwrap();
1159        assert_eq!(g.ecount(), 3);
1160        assert_eq!(g2.ecount(), 2);
1161    }
1162
1163    #[test]
1164    fn os_invariant_is_monotone() {
1165        let mut g = Graph::with_vertices(5);
1166        g.add_edges(vec![(0, 1), (0, 2), (3, 4), (1, 2)]).unwrap();
1167        // os should be non-decreasing and end at ecount.
1168        for w in g.os.windows(2) {
1169            assert!(w[0] <= w[1]);
1170        }
1171        assert_eq!(g.os[0], 0);
1172        assert_eq!(*g.os.last().unwrap() as usize, g.ecount());
1173    }
1174
1175    #[test]
1176    fn vertex_out_of_range_when_adding_edge() {
1177        let mut g = Graph::with_vertices(2);
1178        let e = g.add_edge(2, 0).unwrap_err();
1179        assert!(matches!(e, IgraphError::VertexOutOfRange { id: 2, n: 2 }));
1180        // Graph state must be unchanged after the failed add.
1181        assert_eq!(g.ecount(), 0);
1182    }
1183
1184    // -------- ALGO-CORE-001b: edge-id helpers + incident --------
1185
1186    #[test]
1187    fn edge_endpoints_round_trip() {
1188        let mut g = Graph::new(3, true).unwrap();
1189        g.add_edges(vec![(0, 1), (2, 0), (1, 2)]).unwrap();
1190        // Directed: order preserved. edge_id == position in from/to.
1191        assert_eq!(g.edge(0).unwrap(), (0, 1));
1192        assert_eq!(g.edge(1).unwrap(), (2, 0));
1193        assert_eq!(g.edge(2).unwrap(), (1, 2));
1194        assert_eq!(g.edge_source(1).unwrap(), 2);
1195        assert_eq!(g.edge_target(1).unwrap(), 0);
1196    }
1197
1198    #[test]
1199    fn edge_other_endpoint() {
1200        let mut g = Graph::with_vertices(3);
1201        g.add_edge(0, 2).unwrap();
1202        assert_eq!(g.edge_other(0, 0).unwrap(), 2);
1203        assert_eq!(g.edge_other(0, 2).unwrap(), 0);
1204        // Vertex not on the edge: error.
1205        let err = g.edge_other(0, 1).unwrap_err();
1206        assert!(matches!(err, IgraphError::InvalidArgument(_)));
1207    }
1208
1209    #[test]
1210    fn edge_out_of_range() {
1211        let mut g = Graph::with_vertices(2);
1212        g.add_edge(0, 1).unwrap();
1213        let err = g.edge(5).unwrap_err();
1214        assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 1 }));
1215    }
1216
1217    #[test]
1218    fn incident_returns_edge_ids_matching_neighbors_order() {
1219        let mut g = Graph::with_vertices(4);
1220        g.add_edges(vec![(0, 1), (0, 2), (3, 0)]).unwrap();
1221        let eids = g.incident(0).unwrap();
1222        // Expect three incident edges; resolving back to neighbours
1223        // must equal `neighbors(0)` exactly (same iteration order).
1224        let resolved: Vec<u32> = eids.iter().map(|&e| g.edge_other(e, 0).unwrap()).collect();
1225        assert_eq!(resolved, g.neighbors(0).unwrap());
1226    }
1227
1228    #[test]
1229    fn incident_self_loop_appears_twice_undirected() {
1230        let mut g = Graph::with_vertices(1);
1231        g.add_edge(0, 0).unwrap();
1232        let eids = g.incident(0).unwrap();
1233        // Undirected self-loop appears once on the out side and once on
1234        // the in side — same edge id, twice. Mirrors `neighbors`.
1235        assert_eq!(eids, vec![0, 0]);
1236        assert_eq!(g.degree(0).unwrap(), 2);
1237    }
1238
1239    #[test]
1240    fn incident_directed_returns_outgoing_only() {
1241        let mut g = Graph::new(3, true).unwrap();
1242        g.add_edges(vec![(0, 1), (2, 0)]).unwrap();
1243        // Directed `incident` mirrors directed `neighbors` (out only).
1244        assert_eq!(g.incident(0).unwrap(), vec![0]);
1245        assert_eq!(g.incident(2).unwrap(), vec![1]);
1246        assert!(g.incident(1).unwrap().is_empty());
1247    }
1248
1249    #[test]
1250    fn get_eid_undirected_finds_edge_either_way() {
1251        let mut g = Graph::with_vertices(3);
1252        g.add_edge(0, 1).unwrap(); // edge 0
1253        g.add_edge(1, 2).unwrap(); // edge 1
1254        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
1255        assert_eq!(g.get_eid(1, 0).unwrap(), 0);
1256        assert_eq!(g.get_eid(1, 2).unwrap(), 1);
1257        assert_eq!(g.get_eid(2, 1).unwrap(), 1);
1258    }
1259
1260    #[test]
1261    fn get_eid_directed_respects_direction() {
1262        let mut g = Graph::new(3, true).unwrap();
1263        g.add_edge(0, 1).unwrap(); // edge 0
1264        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
1265        assert!(g.get_eid(1, 0).is_err()); // reverse direction has no edge
1266    }
1267
1268    #[test]
1269    fn find_eid_returns_none_for_missing() {
1270        let mut g = Graph::with_vertices(3);
1271        g.add_edge(0, 1).unwrap();
1272        assert_eq!(g.find_eid(0, 2).unwrap(), None);
1273        assert!(g.find_eid(0, 99).is_err()); // out-of-range vertex
1274    }
1275
1276    #[test]
1277    fn get_eid_self_loop() {
1278        let mut g = Graph::with_vertices(2);
1279        g.add_edge(0, 0).unwrap(); // self-loop, edge 0
1280        g.add_edge(0, 1).unwrap(); // edge 1
1281        assert_eq!(g.get_eid(0, 0).unwrap(), 0);
1282        assert_eq!(g.get_eid(0, 1).unwrap(), 1);
1283    }
1284
1285    #[test]
1286    fn get_all_eids_between_returns_all_parallel() {
1287        let mut g = Graph::with_vertices(2);
1288        g.add_edge(0, 1).unwrap(); // edge 0
1289        g.add_edge(0, 1).unwrap(); // edge 1
1290        g.add_edge(0, 1).unwrap(); // edge 2
1291        let eids = g.get_all_eids_between(0, 1).unwrap();
1292        assert_eq!(eids, vec![0, 1, 2]);
1293        // Reverse direction yields the same edges on undirected.
1294        let eids = g.get_all_eids_between(1, 0).unwrap();
1295        assert_eq!(eids, vec![0, 1, 2]);
1296    }
1297
1298    #[test]
1299    fn get_all_eids_between_directed_one_way_only() {
1300        let mut g = Graph::new(2, true).unwrap();
1301        g.add_edge(0, 1).unwrap(); // edge 0
1302        g.add_edge(0, 1).unwrap(); // edge 1 (parallel)
1303        assert_eq!(g.get_all_eids_between(0, 1).unwrap(), vec![0, 1]);
1304        // Reverse direction has no edges in directed graph.
1305        assert_eq!(g.get_all_eids_between(1, 0).unwrap(), Vec::<EdgeId>::new());
1306    }
1307
1308    #[test]
1309    fn get_eid_returns_lowest_id_for_parallel() {
1310        // Spec: with multiple edges, get_eid always returns the same
1311        // edge id (matches upstream's "ignored multi-edges" guarantee).
1312        // Our impl returns the lowest from the bucket.
1313        let mut g = Graph::with_vertices(2);
1314        g.add_edge(0, 1).unwrap(); // edge 0
1315        g.add_edge(0, 1).unwrap(); // edge 1
1316        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
1317    }
1318
1319    // -------- ALGO-CORE-001c: delete_edges + delete_vertices --------
1320
1321    #[test]
1322    fn delete_edges_empty_input_is_noop() {
1323        let mut g = Graph::with_vertices(3);
1324        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1325        g.delete_edges(&[]).unwrap();
1326        assert_eq!(g.ecount(), 2);
1327        assert_eq!(g.degree(1).unwrap(), 2);
1328    }
1329
1330    #[test]
1331    fn delete_edges_single_edge_undirected() {
1332        let mut g = Graph::with_vertices(3);
1333        g.add_edges(vec![(0, 1), (1, 2), (0, 2)]).unwrap();
1334        // Remove edge id 1 (the (1,2) edge).
1335        g.delete_edges(&[1]).unwrap();
1336        assert_eq!(g.ecount(), 2);
1337        // Surviving edges renumbered to 0,1: (0,1) and (0,2).
1338        assert_eq!(g.find_eid(0, 1).unwrap(), Some(0));
1339        assert_eq!(g.find_eid(0, 2).unwrap(), Some(1));
1340        assert_eq!(g.find_eid(1, 2).unwrap(), None);
1341        // Degrees consistent post-rebuild.
1342        assert_eq!(g.degree(1).unwrap(), 1);
1343        assert_eq!(g.degree(2).unwrap(), 1);
1344    }
1345
1346    #[test]
1347    fn delete_edges_duplicate_ids_tolerated() {
1348        let mut g = Graph::with_vertices(3);
1349        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1350        g.delete_edges(&[0, 0, 0]).unwrap();
1351        assert_eq!(g.ecount(), 1);
1352        assert_eq!(g.find_eid(1, 2).unwrap(), Some(0));
1353    }
1354
1355    #[test]
1356    fn delete_edges_all_edges_leaves_isolated_vertices() {
1357        let mut g = Graph::with_vertices(3);
1358        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1359        g.delete_edges(&[0, 1]).unwrap();
1360        assert_eq!(g.ecount(), 0);
1361        assert_eq!(g.vcount(), 3);
1362        for v in 0..3 {
1363            assert_eq!(g.degree(v).unwrap(), 0);
1364        }
1365    }
1366
1367    #[test]
1368    fn delete_edges_out_of_range_errors_and_preserves_state() {
1369        let mut g = Graph::with_vertices(3);
1370        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1371        let err = g.delete_edges(&[5]).unwrap_err();
1372        assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 2 }));
1373        // Graph unchanged.
1374        assert_eq!(g.ecount(), 2);
1375    }
1376
1377    #[test]
1378    fn delete_edges_self_loop_directed() {
1379        let mut g = Graph::new(2, true).unwrap();
1380        g.add_edges(vec![(0, 0), (0, 1)]).unwrap();
1381        g.delete_edges(&[0]).unwrap(); // remove the self-loop
1382        assert_eq!(g.ecount(), 1);
1383        assert_eq!(g.degree(0).unwrap(), 1);
1384        assert_eq!(g.find_eid(0, 1).unwrap(), Some(0));
1385    }
1386
1387    #[test]
1388    fn delete_vertices_empty_input_is_noop() {
1389        let mut g = Graph::with_vertices(3);
1390        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1391        g.delete_vertices(&[]).unwrap();
1392        assert_eq!(g.vcount(), 3);
1393        assert_eq!(g.ecount(), 2);
1394    }
1395
1396    #[test]
1397    fn delete_vertices_single_renumbers() {
1398        let mut g = Graph::with_vertices(4);
1399        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap();
1400        // Remove vertex 1: edges (0,1) and (1,2) go with it. (2,3),(0,3)
1401        // survive but get renumbered: 2 → 1, 3 → 2.
1402        g.delete_vertices(&[1]).unwrap();
1403        assert_eq!(g.vcount(), 3);
1404        assert_eq!(g.ecount(), 2);
1405        // (2,3) → (1,2); (0,3) → (0,2).
1406        assert!(g.find_eid(1, 2).unwrap().is_some());
1407        assert!(g.find_eid(0, 2).unwrap().is_some());
1408        assert_eq!(g.find_eid(0, 1).unwrap(), None);
1409    }
1410
1411    #[test]
1412    fn delete_vertices_duplicate_ids_tolerated() {
1413        let mut g = Graph::with_vertices(3);
1414        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1415        g.delete_vertices(&[1, 1, 1]).unwrap();
1416        assert_eq!(g.vcount(), 2);
1417        assert_eq!(g.ecount(), 0);
1418    }
1419
1420    #[test]
1421    fn delete_vertices_all_yields_empty_graph() {
1422        let mut g = Graph::with_vertices(3);
1423        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1424        g.delete_vertices(&[0, 1, 2]).unwrap();
1425        assert_eq!(g.vcount(), 0);
1426        assert_eq!(g.ecount(), 0);
1427    }
1428
1429    #[test]
1430    fn delete_vertices_out_of_range_errors_and_preserves_state() {
1431        let mut g = Graph::with_vertices(3);
1432        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1433        let err = g.delete_vertices(&[5]).unwrap_err();
1434        assert!(matches!(err, IgraphError::VertexOutOfRange { id: 5, n: 3 }));
1435        assert_eq!(g.vcount(), 3);
1436        assert_eq!(g.ecount(), 2);
1437    }
1438
1439    #[test]
1440    fn delete_vertices_map_returns_correct_mappings() {
1441        let mut g = Graph::with_vertices(5);
1442        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
1443        let (map, invmap) = g.delete_vertices_map(&[1, 3]).unwrap();
1444        // Removed: 1 and 3. Retained: 0 → 0, 2 → 1, 4 → 2.
1445        assert_eq!(map, vec![Some(0), None, Some(1), None, Some(2)]);
1446        assert_eq!(invmap, vec![0, 2, 4]);
1447        assert_eq!(g.vcount(), 3);
1448        // Only edges between retained vertices survive — none do here.
1449        assert_eq!(g.ecount(), 0);
1450    }
1451
1452    #[test]
1453    fn delete_vertices_directed_preserves_direction() {
1454        let mut g = Graph::new(4, true).unwrap();
1455        g.add_edges(vec![(0, 1), (1, 2), (2, 0), (3, 0)]).unwrap();
1456        g.delete_vertices(&[3]).unwrap();
1457        assert_eq!(g.vcount(), 3);
1458        assert!(g.is_directed());
1459        // Surviving directed edges (3 → 0) gone; (0,1),(1,2),(2,0) keep direction.
1460        assert!(g.get_eid(0, 1).is_ok());
1461        assert!(g.get_eid(1, 0).is_err()); // wrong direction
1462    }
1463
1464    #[test]
1465    fn delete_vertices_self_loop_on_removed_vertex() {
1466        let mut g = Graph::with_vertices(3);
1467        g.add_edges(vec![(0, 0), (0, 1), (1, 2)]).unwrap();
1468        g.delete_vertices(&[0]).unwrap();
1469        // Self-loop and edges to vertex 0 gone; only (1,2) → (0,1) survives.
1470        assert_eq!(g.vcount(), 2);
1471        assert_eq!(g.ecount(), 1);
1472        assert!(g.find_eid(0, 1).unwrap().is_some());
1473    }
1474
1475    #[test]
1476    fn delete_vertices_preserves_parallel_edges() {
1477        let mut g = Graph::with_vertices(3);
1478        g.add_edges(vec![(0, 1), (0, 1), (1, 2)]).unwrap();
1479        g.delete_vertices(&[2]).unwrap();
1480        assert_eq!(g.vcount(), 2);
1481        assert_eq!(g.ecount(), 2); // both parallel (0,1) edges retained
1482        assert_eq!(g.degree(0).unwrap(), 2);
1483        assert_eq!(g.degree(1).unwrap(), 2);
1484    }
1485
1486    #[test]
1487    fn add_edges_after_delete_works() {
1488        let mut g = Graph::with_vertices(4);
1489        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
1490        g.delete_vertices(&[0]).unwrap(); // now n=3, vertices 0,1,2
1491        // Add a new edge and check indexes still work.
1492        g.add_edge(0, 2).unwrap();
1493        assert_eq!(g.ecount(), 3);
1494        assert_eq!(g.degree(0).unwrap(), 2); // (0,1)+(0,2)
1495        assert!(g.find_eid(0, 2).unwrap().is_some());
1496    }
1497}