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::error::{IgraphError, IgraphResult};
31
32/// Vertex id. The Phase-0 ADR-0007 fixes this to `u32`; `Option<VertexId>`
33/// is the idiomatic "no vertex" sentinel (igraph C uses `-1`).
34pub type VertexId = u32;
35
36/// Edge id. Same width as [`VertexId`]; an edge id is its position in
37/// `from`/`to`.
38pub type EdgeId = u32;
39
40/// Counterpart of `igraph_t` (see `references/igraph/include/igraph_datatype.h`).
41///
42/// Phase-0 callers (`bfs`, `read_edgelist`, oracle tests) only depended on
43/// `with_vertices`, `add_edge`, `add_edges`, `vcount`, `ecount`, `neighbors`,
44/// `degree` — those signatures are preserved here, so existing call sites
45/// compile unchanged. New for Phase 1: `new` (with `directed` flag),
46/// `is_directed`.
47#[derive(Debug, Clone, Default)]
48pub struct Graph {
49    /// Vertex count. Redundant with the highest used id; mirrors `igraph_t::n`.
50    n: u32,
51    /// Whether the graph is directed.
52    directed: bool,
53    /// Source endpoints, one per edge.
54    from: Vec<VertexId>,
55    /// Target endpoints, one per edge.
56    to: Vec<VertexId>,
57    /// Edge ids in `from`-major order.
58    oi: Vec<EdgeId>,
59    /// Edge ids in `to`-major order.
60    ii: Vec<EdgeId>,
61    /// `os[v]..os[v+1]` is the slice of `oi` for vertex `v`'s out-edges.
62    /// Length is `n + 1`; `os[0] == 0`, `os[n] == ecount`.
63    os: Vec<u32>,
64    /// `is[v]..is[v+1]` for incoming. Same shape as `os`.
65    is: Vec<u32>,
66}
67
68impl Graph {
69    /// Construct an empty graph on `n` vertices.
70    ///
71    /// Counterpart of `igraph_empty()`; `directed` defaults to `false` if
72    /// you use [`Graph::with_vertices`] instead.
73    pub fn new(n: u32, directed: bool) -> IgraphResult<Self> {
74        let mut g = Self {
75            n: 0,
76            directed,
77            from: Vec::new(),
78            to: Vec::new(),
79            oi: Vec::new(),
80            ii: Vec::new(),
81            os: vec![0],
82            is: vec![0],
83        };
84        g.add_vertices(n)?;
85        Ok(g)
86    }
87
88    /// Construct an empty *undirected* graph on `n` vertices.
89    ///
90    /// Equivalent to `Graph::new(n, false).unwrap()` for the success path.
91    /// Phase-0 compatibility shim — exact signature preserved.
92    pub fn with_vertices(n: u32) -> Self {
93        Self::new(n, false).expect("with_vertices: n cannot exceed u32::MAX")
94    }
95
96    /// Number of vertices. Counterpart of `igraph_vcount()`.
97    #[must_use]
98    pub fn vcount(&self) -> u32 {
99        self.n
100    }
101
102    /// Number of edges. Counterpart of `igraph_ecount()`.
103    #[must_use]
104    pub fn ecount(&self) -> usize {
105        self.from.len()
106    }
107
108    /// `true` if the graph is directed. Counterpart of `igraph_is_directed()`.
109    #[must_use]
110    pub fn is_directed(&self) -> bool {
111        self.directed
112    }
113
114    /// Append `nv` isolated vertices, returning the inclusive id range
115    /// `(first, last)` of the new vertices. If `nv == 0` returns
116    /// `(self.n, self.n)` and does nothing.
117    ///
118    /// Counterpart of `igraph_add_vertices()`.
119    pub fn add_vertices(&mut self, nv: u32) -> IgraphResult<(VertexId, VertexId)> {
120        let new_n = self
121            .n
122            .checked_add(nv)
123            .ok_or(IgraphError::Internal("vertex count overflow"))?;
124        let first = self.n;
125        // os/is grow by `nv` entries, all initialised to ecount.
126        let ec = u32::try_from(self.ecount())
127            .map_err(|_| IgraphError::Internal("edge count exceeds u32::MAX"))?;
128        for _ in 0..nv {
129            self.os.push(ec);
130            self.is.push(ec);
131        }
132        self.n = new_n;
133        Ok((first, new_n.saturating_sub(1)))
134    }
135
136    /// Add a single edge from `u` to `v`.
137    ///
138    /// Self-loops and parallel edges are allowed. For undirected graphs the
139    /// edge is canonicalised so the stored `from <= to`.
140    pub fn add_edge(&mut self, u: VertexId, v: VertexId) -> IgraphResult<()> {
141        self.add_edges(std::iter::once((u, v)))
142    }
143
144    /// Add a sequence of edges. After all edges are appended, the indexes
145    /// (`oi` / `ii` / `os` / `is`) are rebuilt in one pass — counterpart of
146    /// `igraph_add_edges` (`type_indexededgelist.c:254-367`).
147    pub fn add_edges<I>(&mut self, edges: I) -> IgraphResult<()>
148    where
149        I: IntoIterator<Item = (VertexId, VertexId)>,
150    {
151        for (u, v) in edges {
152            self.check_vertex(u)?;
153            self.check_vertex(v)?;
154            // Undirected canonicalisation: store the smaller endpoint as `from`.
155            if !self.directed && u > v {
156                self.from.push(v);
157                self.to.push(u);
158            } else {
159                self.from.push(u);
160                self.to.push(v);
161            }
162        }
163        self.rebuild_indexes()?;
164        Ok(())
165    }
166
167    /// Out-edge neighbour iterator for vertex `v`.
168    ///
169    /// For undirected graphs this returns *all* neighbours (since the
170    /// indexing tracks both endpoints symmetrically). Order is the upstream
171    /// igraph order — edges are visited in `oi` order, then `ii` order, with
172    /// duplicates suppressed when the same edge is incident on both.
173    ///
174    /// Counterpart of `igraph_neighbors(graph, _, vid, IGRAPH_ALL, ...)`.
175    pub fn neighbors(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
176        self.check_vertex(v)?;
177        let v_idx = v as usize;
178        if self.directed {
179            // Directed: only outgoing neighbours; oi sorted by (from, to)
180            // so the out-neighbour list is already sorted ascending.
181            let out_range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
182            let out: Vec<VertexId> = self.oi[out_range]
183                .iter()
184                .map(|&e| self.to[e as usize])
185                .collect();
186            Ok(out)
187        } else {
188            // Undirected: merge the two already-sorted sublists from oi
189            // (out-side, ascending in `to`) and ii (in-side, ascending
190            // in `from`) into one ascending neighbour list. Matches
191            // upstream `igraph_neighbors(_, _, _, IGRAPH_ALL)` and
192            // python-igraph's `Graph.neighbors(v)` exactly.
193            let out_start = self.os[v_idx] as usize;
194            let out_end = self.os[v_idx + 1] as usize;
195            let in_start = self.is[v_idx] as usize;
196            let in_end = self.is[v_idx + 1] as usize;
197            let mut out = Vec::with_capacity((out_end - out_start) + (in_end - in_start));
198            let mut out_idx = out_start;
199            let mut in_idx = in_start;
200            while out_idx < out_end && in_idx < in_end {
201                let a = self.to[self.oi[out_idx] as usize];
202                let b = self.from[self.ii[in_idx] as usize];
203                if a <= b {
204                    out.push(a);
205                    out_idx += 1;
206                } else {
207                    out.push(b);
208                    in_idx += 1;
209                }
210            }
211            while out_idx < out_end {
212                out.push(self.to[self.oi[out_idx] as usize]);
213                out_idx += 1;
214            }
215            while in_idx < in_end {
216                out.push(self.from[self.ii[in_idx] as usize]);
217                in_idx += 1;
218            }
219            Ok(out)
220        }
221    }
222
223    /// Degree of vertex `v` — number of edges incident to it.
224    ///
225    /// On undirected graphs every edge counts once except a self-loop which
226    /// counts twice (matches upstream igraph's `IGRAPH_LOOPS = TWICE` default
227    /// at `type_indexededgelist.c:1162`).
228    ///
229    /// Counterpart of `igraph_degree_1(_, _, _, IGRAPH_ALL, IGRAPH_LOOPS_TWICE)`.
230    pub fn degree(&self, v: VertexId) -> IgraphResult<usize> {
231        self.check_vertex(v)?;
232        let v_idx = v as usize;
233        let out = (self.os[v_idx + 1] - self.os[v_idx]) as usize;
234        if self.directed {
235            let in_count = (self.is[v_idx + 1] - self.is[v_idx]) as usize;
236            Ok(out + in_count)
237        } else {
238            // Undirected: out + in. Self-loops appear in both (they are
239            // stored once with `from == to` but indexed in both `os` and
240            // `is` slots), so naturally count twice.
241            let in_count = (self.is[v_idx + 1] - self.is[v_idx]) as usize;
242            Ok(out + in_count)
243        }
244    }
245
246    // ---------------------------------------------------------------
247    // ALGO-CORE-001b: edge-id helpers + incident edges.
248    // ---------------------------------------------------------------
249
250    /// Source endpoint of edge `eid`. Counterpart of `IGRAPH_FROM`
251    /// (`igraph_interface.h:115`).
252    pub fn edge_source(&self, eid: EdgeId) -> IgraphResult<VertexId> {
253        self.check_edge(eid)?;
254        Ok(self.from[eid as usize])
255    }
256
257    /// Target endpoint of edge `eid`. Counterpart of `IGRAPH_TO`
258    /// (`igraph_interface.h:128`).
259    pub fn edge_target(&self, eid: EdgeId) -> IgraphResult<VertexId> {
260        self.check_edge(eid)?;
261        Ok(self.to[eid as usize])
262    }
263
264    /// Both endpoints of edge `eid`, ordered as `(from, to)`. Counterpart
265    /// of `igraph_edge` (`igraph_interface.h:71`).
266    pub fn edge(&self, eid: EdgeId) -> IgraphResult<(VertexId, VertexId)> {
267        self.check_edge(eid)?;
268        let i = eid as usize;
269        Ok((self.from[i], self.to[i]))
270    }
271
272    /// The other endpoint of `eid` given one endpoint `vid`. Counterpart
273    /// of `IGRAPH_OTHER` (`igraph_interface.h:145`). Errors if `vid` is
274    /// not actually an endpoint of `eid`.
275    pub fn edge_other(&self, eid: EdgeId, vid: VertexId) -> IgraphResult<VertexId> {
276        let (u, v) = self.edge(eid)?;
277        if vid == u {
278            Ok(v)
279        } else if vid == v {
280            Ok(u)
281        } else {
282            Err(IgraphError::InvalidArgument(format!(
283                "vertex {vid} is not an endpoint of edge {eid} ({u}, {v})"
284            )))
285        }
286    }
287
288    /// Edge ids incident to vertex `v`, in the same iteration order as
289    /// [`Graph::neighbors`].
290    ///
291    /// For undirected graphs returns the union of out-side (`oi`) and
292    /// in-side (`ii`) edges — every edge incident to `v` once, except
293    /// self-loops which appear twice (matching `igraph_neighbors` /
294    /// `igraph_degree`'s `IGRAPH_LOOPS_TWICE` default at
295    /// `type_indexededgelist.c:1162`).
296    ///
297    /// For directed graphs returns out-edges only, mirroring this AWU's
298    /// `neighbors()` choice. (The full mode-aware variant lands later
299    /// alongside `igraph_neighbors(mode = IN/OUT/ALL)`.)
300    ///
301    /// Counterpart of `igraph_incident(_, _, v, IGRAPH_ALL, IGRAPH_LOOPS_TWICE)`
302    /// for undirected; `IGRAPH_OUT` mode for directed.
303    pub fn incident(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
304        self.check_vertex(v)?;
305        let v_idx = v as usize;
306        let out_range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
307        if self.directed {
308            Ok(self.oi[out_range].to_vec())
309        } else {
310            let in_range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
311            let mut out = Vec::with_capacity(out_range.len() + in_range.len());
312            out.extend_from_slice(&self.oi[out_range]);
313            out.extend_from_slice(&self.ii[in_range]);
314            Ok(out)
315        }
316    }
317
318    /// Companion to [`incident`](Self::incident): returns *only* the
319    /// edges incoming to `v` for directed graphs. For undirected
320    /// graphs the result is identical to `incident` (every edge is
321    /// bidirectional).
322    ///
323    /// Counterpart of `igraph_incident(_, _, v, IGRAPH_IN, IGRAPH_LOOPS_TWICE)`.
324    pub(crate) fn incident_in(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
325        self.check_vertex(v)?;
326        let v_idx = v as usize;
327        if self.directed {
328            let in_range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
329            Ok(self.ii[in_range].to_vec())
330        } else {
331            self.incident(v)
332        }
333    }
334
335    /// Edge id between `from` and `to`, if any.
336    ///
337    /// On undirected graphs `(u, v)` and `(v, u)` are equivalent.
338    /// On directed graphs the search follows the edge direction
339    /// `from -> to`. Returns [`crate::IgraphError::InvalidArgument`]
340    /// when no such edge exists; for the "no error, return None" variant
341    /// use [`Self::find_eid`].
342    ///
343    /// Counterpart of
344    /// `igraph_get_eid(_, _, from, to, /*directed=*/true, /*error=*/true)`
345    /// from `references/igraph/src/graph/type_indexededgelist.c:1522-1555`.
346    /// Phase-1 minimal slice: linear scan across the from-bucket; the
347    /// upstream binary-search optimisation lands in a perf pass.
348    pub fn get_eid(&self, from: VertexId, to: VertexId) -> IgraphResult<EdgeId> {
349        self.find_eid(from, to)?
350            .ok_or_else(|| IgraphError::InvalidArgument(format!("no edge between {from} and {to}")))
351    }
352
353    /// Edge id between `from` and `to`, or `None` if not connected.
354    ///
355    /// Same semantics as [`Self::get_eid`] but no-error variant
356    /// matching upstream's `error=false` mode. When parallel edges
357    /// exist, returns the lowest edge id (matching upstream's
358    /// "always returns the same edge ID" guarantee).
359    pub fn find_eid(&self, from: VertexId, to: VertexId) -> IgraphResult<Option<EdgeId>> {
360        self.check_vertex(from)?;
361        self.check_vertex(to)?;
362        if self.directed {
363            // Search out-bucket of `from` for `to[e] == to`.
364            let range = self.os[from as usize] as usize..self.os[from as usize + 1] as usize;
365            for &e in &self.oi[range] {
366                if self.to[e as usize] == to {
367                    return Ok(Some(e));
368                }
369            }
370            Ok(None)
371        } else {
372            // Undirected: edges canonicalised so `from[e] <= to[e]`.
373            // Search the bucket of the smaller endpoint for the larger.
374            let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
375            let range = self.os[lo as usize] as usize..self.os[lo as usize + 1] as usize;
376            for &e in &self.oi[range] {
377                if self.to[e as usize] == hi {
378                    return Ok(Some(e));
379                }
380            }
381            Ok(None)
382        }
383    }
384
385    /// All edge ids between `from` and `to`, including parallel edges
386    /// and (for self-loops) the loop edge once.
387    ///
388    /// Counterpart of
389    /// `igraph_get_all_eids_between()` from
390    /// `references/igraph/src/graph/type_indexededgelist.c:~1700`.
391    /// On undirected graphs `(u, v)` and `(v, u)` are equivalent. The
392    /// returned vector is sorted ascending by edge id.
393    pub fn get_all_eids_between(&self, from: VertexId, to: VertexId) -> IgraphResult<Vec<EdgeId>> {
394        self.check_vertex(from)?;
395        self.check_vertex(to)?;
396        let mut out = Vec::new();
397        if self.directed {
398            let range = self.os[from as usize] as usize..self.os[from as usize + 1] as usize;
399            for &e in &self.oi[range] {
400                if self.to[e as usize] == to {
401                    out.push(e);
402                }
403            }
404        } else {
405            let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
406            let range = self.os[lo as usize] as usize..self.os[lo as usize + 1] as usize;
407            for &e in &self.oi[range] {
408                if self.to[e as usize] == hi {
409                    out.push(e);
410                }
411            }
412        }
413        out.sort_unstable();
414        Ok(out)
415    }
416
417    /// Out-neighbours of `v` (always — directed or undirected). Each
418    /// edge contributes one entry, in `oi[os[v]..os[v+1]]` order
419    /// (lex by `(from, to)`). Self-loops appear once.
420    ///
421    /// Internal helper used by direction-aware algorithms (e.g.
422    /// strongly connected components). The full mode-aware public
423    /// surface ships with the next `igraph_neighbors` AWU.
424    pub(crate) fn out_neighbors_vec(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
425        self.check_vertex(v)?;
426        let v_idx = v as usize;
427        let range = self.os[v_idx] as usize..self.os[v_idx + 1] as usize;
428        Ok(self.oi[range]
429            .iter()
430            .map(|&e| self.to[e as usize])
431            .collect())
432    }
433
434    /// In-neighbours of `v` (always — directed or undirected). Each
435    /// edge contributes one entry, in `ii[is[v]..is[v+1]]` order
436    /// (lex by `(to, from)`). Self-loops appear once.
437    ///
438    /// Companion to [`out_neighbors_vec`](Self::out_neighbors_vec); see
439    /// its doc for context on visibility.
440    pub(crate) fn in_neighbors_vec(&self, v: VertexId) -> IgraphResult<Vec<VertexId>> {
441        self.check_vertex(v)?;
442        let v_idx = v as usize;
443        let range = self.is[v_idx] as usize..self.is[v_idx + 1] as usize;
444        Ok(self.ii[range]
445            .iter()
446            .map(|&e| self.from[e as usize])
447            .collect())
448    }
449
450    // ---------------------------------------------------------------
451    // ALGO-CORE-001c: delete_edges + delete_vertices + delete_vertices_map.
452    // ---------------------------------------------------------------
453
454    /// Remove the given edges from the graph.
455    ///
456    /// `edges` may contain the same id more than once — the second and
457    /// later occurrences are no-ops. Remaining edges keep their
458    /// pairwise relative order but are renumbered so edge ids stay
459    /// contiguous starting at 0. Returns
460    /// [`IgraphError::EdgeOutOfRange`] if any id is `>= ecount()`; on
461    /// error the graph is left unchanged.
462    ///
463    /// Counterpart of `igraph_delete_edges`
464    /// (`references/igraph/src/graph/type_indexededgelist.c:500`).
465    pub fn delete_edges(&mut self, edges: &[EdgeId]) -> IgraphResult<()> {
466        let m = self.ecount();
467        let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
468
469        // Validate up front so a bad id leaves graph state untouched.
470        for &eid in edges {
471            if (eid as usize) >= m {
472                return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
473            }
474        }
475        if edges.is_empty() {
476            return Ok(());
477        }
478
479        let mut remove = vec![false; m];
480        for &eid in edges {
481            remove[eid as usize] = true;
482        }
483
484        let mut new_from: Vec<VertexId> = Vec::with_capacity(m);
485        let mut new_to: Vec<VertexId> = Vec::with_capacity(m);
486        for (e, &is_removed) in remove.iter().enumerate() {
487            if !is_removed {
488                new_from.push(self.from[e]);
489                new_to.push(self.to[e]);
490            }
491        }
492        self.from = new_from;
493        self.to = new_to;
494        self.rebuild_indexes()
495    }
496
497    /// Remove the given vertices and all their incident edges.
498    ///
499    /// `vertices` may repeat ids freely. Surviving vertices get
500    /// renumbered so the new id space is `0..new_vcount` in their
501    /// previous relative order. Returns
502    /// [`IgraphError::VertexOutOfRange`] if any id is `>= vcount()`;
503    /// on error the graph is left unchanged.
504    ///
505    /// Counterpart of `igraph_delete_vertices`
506    /// (`references/igraph/src/graph/type_indexededgelist.c:540`).
507    pub fn delete_vertices(&mut self, vertices: &[VertexId]) -> IgraphResult<()> {
508        self.delete_vertices_map(vertices).map(|_| ())
509    }
510
511    /// Like [`delete_vertices`](Self::delete_vertices), but also returns
512    /// the old↔new vertex id mappings.
513    ///
514    /// Returns `(map, invmap)` where:
515    /// - `map[old_id] == Some(new_id)` if the vertex was retained, else
516    ///   `None`. Length is the *original* vertex count.
517    /// - `invmap[new_id] == old_id`. Length is the *new* vertex count.
518    ///
519    /// Counterpart of `igraph_delete_vertices_map`
520    /// (`references/igraph/src/graph/type_indexededgelist.c:645`).
521    pub fn delete_vertices_map(
522        &mut self,
523        vertices: &[VertexId],
524    ) -> IgraphResult<(Vec<Option<VertexId>>, Vec<VertexId>)> {
525        let n_u32 = self.n;
526        let n = n_u32 as usize;
527
528        // Validate first.
529        for &vid in vertices {
530            if vid >= n_u32 {
531                return Err(IgraphError::VertexOutOfRange { id: vid, n: n_u32 });
532            }
533        }
534
535        let mut remove = vec![false; n];
536        for &vid in vertices {
537            remove[vid as usize] = true;
538        }
539
540        // Build map (old → new) and invmap (new → old).
541        let mut map: Vec<Option<VertexId>> = vec![None; n];
542        let mut invmap: Vec<VertexId> = Vec::new();
543        let mut next_new: u32 = 0;
544        for (i, &is_removed) in remove.iter().enumerate() {
545            if !is_removed {
546                let i_u32 = u32::try_from(i)
547                    .map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
548                map[i] = Some(next_new);
549                invmap.push(i_u32);
550                next_new = next_new
551                    .checked_add(1)
552                    .ok_or(IgraphError::Internal("new vertex count overflow"))?;
553            }
554        }
555
556        // Filter edges: keep only those with both endpoints retained,
557        // renumber endpoints via `map`.
558        let m = self.ecount();
559        let mut new_from: Vec<VertexId> = Vec::with_capacity(m);
560        let mut new_to: Vec<VertexId> = Vec::with_capacity(m);
561        for (u, v) in self.from.iter().zip(self.to.iter()) {
562            if let (Some(nu), Some(nv)) = (map[*u as usize], map[*v as usize]) {
563                new_from.push(nu);
564                new_to.push(nv);
565            }
566        }
567
568        self.n = next_new;
569        self.from = new_from;
570        self.to = new_to;
571        self.rebuild_indexes()?;
572
573        Ok((map, invmap))
574    }
575
576    fn check_vertex(&self, v: VertexId) -> IgraphResult<()> {
577        if v >= self.n {
578            return Err(IgraphError::VertexOutOfRange { id: v, n: self.n });
579        }
580        Ok(())
581    }
582
583    fn check_edge(&self, eid: EdgeId) -> IgraphResult<()> {
584        let m = self.ecount();
585        let m_u32 = u32::try_from(m).unwrap_or(u32::MAX);
586        if (eid as usize) >= m {
587            return Err(IgraphError::EdgeOutOfRange { id: eid, m: m_u32 });
588        }
589        Ok(())
590    }
591
592    /// Recompute `oi`, `ii`, `os`, `is` from `from`/`to`. Called after
593    /// any structural change.
594    ///
595    /// Each side does a stable lexicographic sort: `oi` orders edges by
596    /// `(from[e], to[e])`, `ii` by `(to[e], from[e])`. Time complexity
597    /// is `O(|V| + |E| log |E|)` (Rust stable sort) — same asymptotic
598    /// as upstream's `igraph_vector_int_pair_order`.
599    ///
600    /// The within-bucket secondary sort matches upstream igraph; without
601    /// it, `neighbors(v)` for an unsorted-edge-input graph diverges from
602    /// `python-igraph`'s output and breaks DFS order parity. (Counted
603    /// for an oracle-test failure during ALGO-TR-002 — see
604    /// `tests/oracle.rs::dfs_small_synthetic_matches_python_igraph`.)
605    ///
606    /// Counterpart of `igraph_i_create_start_vectors` + the
607    /// `igraph_vector_int_pair_order` calls in
608    /// `type_indexededgelist.c:309-336`.
609    fn rebuild_indexes(&mut self) -> IgraphResult<()> {
610        let m = self.ecount();
611        let n = self.n as usize;
612
613        // Build (primary_key, secondary_key, edge_id) tuples for each
614        // side, sort them lexicographically, then extract edge ids and
615        // the offset array.
616
617        // ---- Out-side: sort by (from, to). ----
618        let mut tuples: Vec<(VertexId, VertexId, u32)> = (0..m)
619            .map(|e| {
620                Ok::<_, IgraphError>((
621                    self.from[e],
622                    self.to[e],
623                    u32::try_from(e)
624                        .map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX"))?,
625                ))
626            })
627            .collect::<Result<_, _>>()?;
628        tuples.sort_unstable_by_key(|a| (a.0, a.1));
629        self.oi = tuples.iter().map(|t| t.2).collect();
630        // os[v] = number of entries with primary_key < v.
631        self.os = vec![0u32; n + 1];
632        for &(u, _, _) in &tuples {
633            self.os[u as usize + 1] = self.os[u as usize + 1]
634                .checked_add(1)
635                .ok_or(IgraphError::Internal("degree overflow in rebuild_indexes"))?;
636        }
637        for i in 1..=n {
638            self.os[i] = self.os[i]
639                .checked_add(self.os[i - 1])
640                .ok_or(IgraphError::Internal("offset overflow in rebuild_indexes"))?;
641        }
642
643        // ---- In-side: sort by (to, from). ----
644        let mut tuples: Vec<(VertexId, VertexId, u32)> = (0..m)
645            .map(|e| {
646                Ok::<_, IgraphError>((
647                    self.to[e],
648                    self.from[e],
649                    u32::try_from(e)
650                        .map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX"))?,
651                ))
652            })
653            .collect::<Result<_, _>>()?;
654        tuples.sort_unstable_by_key(|a| (a.0, a.1));
655        self.ii = tuples.iter().map(|t| t.2).collect();
656        self.is = vec![0u32; n + 1];
657        for &(v, _, _) in &tuples {
658            self.is[v as usize + 1] = self.is[v as usize + 1]
659                .checked_add(1)
660                .ok_or(IgraphError::Internal("degree overflow in rebuild_indexes"))?;
661        }
662        for i in 1..=n {
663            self.is[i] = self.is[i]
664                .checked_add(self.is[i - 1])
665                .ok_or(IgraphError::Internal("offset overflow in rebuild_indexes"))?;
666        }
667
668        Ok(())
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    #[test]
677    fn empty_graph_counts() {
678        let g = Graph::with_vertices(0);
679        assert_eq!(g.vcount(), 0);
680        assert_eq!(g.ecount(), 0);
681        assert!(!g.is_directed());
682    }
683
684    #[test]
685    fn new_directed_flag() {
686        let g = Graph::new(3, true).unwrap();
687        assert!(g.is_directed());
688        let g = Graph::new(3, false).unwrap();
689        assert!(!g.is_directed());
690    }
691
692    #[test]
693    fn add_vertices_then_edges() {
694        let mut g = Graph::with_vertices(3);
695        g.add_edge(0, 1).unwrap();
696        g.add_edge(1, 2).unwrap();
697        assert_eq!(g.vcount(), 3);
698        assert_eq!(g.ecount(), 2);
699        assert_eq!(g.degree(1).unwrap(), 2);
700        let mut nbrs = g.neighbors(1).unwrap();
701        nbrs.sort_unstable();
702        assert_eq!(nbrs, vec![0, 2]);
703    }
704
705    #[test]
706    fn out_of_range_vertex_errors() {
707        let mut g = Graph::with_vertices(2);
708        let err = g.add_edge(0, 5).unwrap_err();
709        assert!(matches!(err, IgraphError::VertexOutOfRange { id: 5, n: 2 }));
710    }
711
712    #[test]
713    fn self_loop_counted_correctly() {
714        let mut g = Graph::with_vertices(1);
715        g.add_edge(0, 0).unwrap();
716        assert_eq!(g.ecount(), 1);
717        // Undirected self-loop: appears as both out and in, degree == 2.
718        assert_eq!(g.degree(0).unwrap(), 2);
719        let mut nbrs = g.neighbors(0).unwrap();
720        nbrs.sort_unstable();
721        assert_eq!(nbrs, vec![0, 0]);
722    }
723
724    #[test]
725    fn parallel_edges() {
726        let mut g = Graph::with_vertices(2);
727        g.add_edge(0, 1).unwrap();
728        g.add_edge(0, 1).unwrap();
729        assert_eq!(g.ecount(), 2);
730        assert_eq!(g.degree(0).unwrap(), 2);
731        assert_eq!(g.degree(1).unwrap(), 2);
732    }
733
734    #[test]
735    fn undirected_canonicalisation() {
736        // Adding edges (1,0) and (0,1) — both stored canonically as (0,1).
737        let mut g = Graph::with_vertices(2);
738        g.add_edge(1, 0).unwrap();
739        g.add_edge(0, 1).unwrap();
740        assert_eq!(g.ecount(), 2);
741        // Both vertices see each other as a neighbour twice.
742        let mut n0 = g.neighbors(0).unwrap();
743        let mut n1 = g.neighbors(1).unwrap();
744        n0.sort_unstable();
745        n1.sort_unstable();
746        assert_eq!(n0, vec![1, 1]);
747        assert_eq!(n1, vec![0, 0]);
748    }
749
750    #[test]
751    fn directed_neighbors_are_outgoing_only() {
752        let mut g = Graph::new(3, true).unwrap();
753        g.add_edge(0, 1).unwrap();
754        g.add_edge(2, 0).unwrap();
755        // Directed: neighbors(0) returns out-neighbours only.
756        assert_eq!(g.neighbors(0).unwrap(), vec![1]);
757        // Vertex 2 has out-edge to 0.
758        assert_eq!(g.neighbors(2).unwrap(), vec![0]);
759        // Vertex 1 has no out-edges.
760        assert!(g.neighbors(1).unwrap().is_empty());
761        // Degree counts both in and out for directed.
762        assert_eq!(g.degree(0).unwrap(), 2); // out: 0->1, in: 2->0
763        assert_eq!(g.degree(1).unwrap(), 1); // in: 0->1
764        assert_eq!(g.degree(2).unwrap(), 1); // out: 2->0
765    }
766
767    #[test]
768    fn add_edges_batch_then_rebuild() {
769        let mut g = Graph::with_vertices(4);
770        g.add_edges(vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap();
771        assert_eq!(g.ecount(), 4);
772        // Degrees: 0->{1,2} d=2; 1->{0,2} d=2; 2->{0,1,3} d=3; 3->{2} d=1.
773        assert_eq!(g.degree(0).unwrap(), 2);
774        assert_eq!(g.degree(1).unwrap(), 2);
775        assert_eq!(g.degree(2).unwrap(), 3);
776        assert_eq!(g.degree(3).unwrap(), 1);
777    }
778
779    #[test]
780    fn clone_is_deep() {
781        let mut g = Graph::with_vertices(3);
782        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
783        let g2 = g.clone();
784        // Mutate g; g2 must be unaffected.
785        g.add_edge(0, 2).unwrap();
786        assert_eq!(g.ecount(), 3);
787        assert_eq!(g2.ecount(), 2);
788    }
789
790    #[test]
791    fn os_invariant_is_monotone() {
792        let mut g = Graph::with_vertices(5);
793        g.add_edges(vec![(0, 1), (0, 2), (3, 4), (1, 2)]).unwrap();
794        // os should be non-decreasing and end at ecount.
795        for w in g.os.windows(2) {
796            assert!(w[0] <= w[1]);
797        }
798        assert_eq!(g.os[0], 0);
799        assert_eq!(*g.os.last().unwrap() as usize, g.ecount());
800    }
801
802    #[test]
803    fn vertex_out_of_range_when_adding_edge() {
804        let mut g = Graph::with_vertices(2);
805        let e = g.add_edge(2, 0).unwrap_err();
806        assert!(matches!(e, IgraphError::VertexOutOfRange { id: 2, n: 2 }));
807        // Graph state must be unchanged after the failed add.
808        assert_eq!(g.ecount(), 0);
809    }
810
811    // -------- ALGO-CORE-001b: edge-id helpers + incident --------
812
813    #[test]
814    fn edge_endpoints_round_trip() {
815        let mut g = Graph::new(3, true).unwrap();
816        g.add_edges(vec![(0, 1), (2, 0), (1, 2)]).unwrap();
817        // Directed: order preserved. edge_id == position in from/to.
818        assert_eq!(g.edge(0).unwrap(), (0, 1));
819        assert_eq!(g.edge(1).unwrap(), (2, 0));
820        assert_eq!(g.edge(2).unwrap(), (1, 2));
821        assert_eq!(g.edge_source(1).unwrap(), 2);
822        assert_eq!(g.edge_target(1).unwrap(), 0);
823    }
824
825    #[test]
826    fn edge_other_endpoint() {
827        let mut g = Graph::with_vertices(3);
828        g.add_edge(0, 2).unwrap();
829        assert_eq!(g.edge_other(0, 0).unwrap(), 2);
830        assert_eq!(g.edge_other(0, 2).unwrap(), 0);
831        // Vertex not on the edge: error.
832        let err = g.edge_other(0, 1).unwrap_err();
833        assert!(matches!(err, IgraphError::InvalidArgument(_)));
834    }
835
836    #[test]
837    fn edge_out_of_range() {
838        let mut g = Graph::with_vertices(2);
839        g.add_edge(0, 1).unwrap();
840        let err = g.edge(5).unwrap_err();
841        assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 1 }));
842    }
843
844    #[test]
845    fn incident_returns_edge_ids_matching_neighbors_order() {
846        let mut g = Graph::with_vertices(4);
847        g.add_edges(vec![(0, 1), (0, 2), (3, 0)]).unwrap();
848        let eids = g.incident(0).unwrap();
849        // Expect three incident edges; resolving back to neighbours
850        // must equal `neighbors(0)` exactly (same iteration order).
851        let resolved: Vec<u32> = eids.iter().map(|&e| g.edge_other(e, 0).unwrap()).collect();
852        assert_eq!(resolved, g.neighbors(0).unwrap());
853    }
854
855    #[test]
856    fn incident_self_loop_appears_twice_undirected() {
857        let mut g = Graph::with_vertices(1);
858        g.add_edge(0, 0).unwrap();
859        let eids = g.incident(0).unwrap();
860        // Undirected self-loop appears once on the out side and once on
861        // the in side — same edge id, twice. Mirrors `neighbors`.
862        assert_eq!(eids, vec![0, 0]);
863        assert_eq!(g.degree(0).unwrap(), 2);
864    }
865
866    #[test]
867    fn incident_directed_returns_outgoing_only() {
868        let mut g = Graph::new(3, true).unwrap();
869        g.add_edges(vec![(0, 1), (2, 0)]).unwrap();
870        // Directed `incident` mirrors directed `neighbors` (out only).
871        assert_eq!(g.incident(0).unwrap(), vec![0]);
872        assert_eq!(g.incident(2).unwrap(), vec![1]);
873        assert!(g.incident(1).unwrap().is_empty());
874    }
875
876    #[test]
877    fn get_eid_undirected_finds_edge_either_way() {
878        let mut g = Graph::with_vertices(3);
879        g.add_edge(0, 1).unwrap(); // edge 0
880        g.add_edge(1, 2).unwrap(); // edge 1
881        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
882        assert_eq!(g.get_eid(1, 0).unwrap(), 0);
883        assert_eq!(g.get_eid(1, 2).unwrap(), 1);
884        assert_eq!(g.get_eid(2, 1).unwrap(), 1);
885    }
886
887    #[test]
888    fn get_eid_directed_respects_direction() {
889        let mut g = Graph::new(3, true).unwrap();
890        g.add_edge(0, 1).unwrap(); // edge 0
891        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
892        assert!(g.get_eid(1, 0).is_err()); // reverse direction has no edge
893    }
894
895    #[test]
896    fn find_eid_returns_none_for_missing() {
897        let mut g = Graph::with_vertices(3);
898        g.add_edge(0, 1).unwrap();
899        assert_eq!(g.find_eid(0, 2).unwrap(), None);
900        assert!(g.find_eid(0, 99).is_err()); // out-of-range vertex
901    }
902
903    #[test]
904    fn get_eid_self_loop() {
905        let mut g = Graph::with_vertices(2);
906        g.add_edge(0, 0).unwrap(); // self-loop, edge 0
907        g.add_edge(0, 1).unwrap(); // edge 1
908        assert_eq!(g.get_eid(0, 0).unwrap(), 0);
909        assert_eq!(g.get_eid(0, 1).unwrap(), 1);
910    }
911
912    #[test]
913    fn get_all_eids_between_returns_all_parallel() {
914        let mut g = Graph::with_vertices(2);
915        g.add_edge(0, 1).unwrap(); // edge 0
916        g.add_edge(0, 1).unwrap(); // edge 1
917        g.add_edge(0, 1).unwrap(); // edge 2
918        let eids = g.get_all_eids_between(0, 1).unwrap();
919        assert_eq!(eids, vec![0, 1, 2]);
920        // Reverse direction yields the same edges on undirected.
921        let eids = g.get_all_eids_between(1, 0).unwrap();
922        assert_eq!(eids, vec![0, 1, 2]);
923    }
924
925    #[test]
926    fn get_all_eids_between_directed_one_way_only() {
927        let mut g = Graph::new(2, true).unwrap();
928        g.add_edge(0, 1).unwrap(); // edge 0
929        g.add_edge(0, 1).unwrap(); // edge 1 (parallel)
930        assert_eq!(g.get_all_eids_between(0, 1).unwrap(), vec![0, 1]);
931        // Reverse direction has no edges in directed graph.
932        assert_eq!(g.get_all_eids_between(1, 0).unwrap(), Vec::<EdgeId>::new());
933    }
934
935    #[test]
936    fn get_eid_returns_lowest_id_for_parallel() {
937        // Spec: with multiple edges, get_eid always returns the same
938        // edge id (matches upstream's "ignored multi-edges" guarantee).
939        // Our impl returns the lowest from the bucket.
940        let mut g = Graph::with_vertices(2);
941        g.add_edge(0, 1).unwrap(); // edge 0
942        g.add_edge(0, 1).unwrap(); // edge 1
943        assert_eq!(g.get_eid(0, 1).unwrap(), 0);
944    }
945
946    // -------- ALGO-CORE-001c: delete_edges + delete_vertices --------
947
948    #[test]
949    fn delete_edges_empty_input_is_noop() {
950        let mut g = Graph::with_vertices(3);
951        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
952        g.delete_edges(&[]).unwrap();
953        assert_eq!(g.ecount(), 2);
954        assert_eq!(g.degree(1).unwrap(), 2);
955    }
956
957    #[test]
958    fn delete_edges_single_edge_undirected() {
959        let mut g = Graph::with_vertices(3);
960        g.add_edges(vec![(0, 1), (1, 2), (0, 2)]).unwrap();
961        // Remove edge id 1 (the (1,2) edge).
962        g.delete_edges(&[1]).unwrap();
963        assert_eq!(g.ecount(), 2);
964        // Surviving edges renumbered to 0,1: (0,1) and (0,2).
965        assert_eq!(g.find_eid(0, 1).unwrap(), Some(0));
966        assert_eq!(g.find_eid(0, 2).unwrap(), Some(1));
967        assert_eq!(g.find_eid(1, 2).unwrap(), None);
968        // Degrees consistent post-rebuild.
969        assert_eq!(g.degree(1).unwrap(), 1);
970        assert_eq!(g.degree(2).unwrap(), 1);
971    }
972
973    #[test]
974    fn delete_edges_duplicate_ids_tolerated() {
975        let mut g = Graph::with_vertices(3);
976        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
977        g.delete_edges(&[0, 0, 0]).unwrap();
978        assert_eq!(g.ecount(), 1);
979        assert_eq!(g.find_eid(1, 2).unwrap(), Some(0));
980    }
981
982    #[test]
983    fn delete_edges_all_edges_leaves_isolated_vertices() {
984        let mut g = Graph::with_vertices(3);
985        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
986        g.delete_edges(&[0, 1]).unwrap();
987        assert_eq!(g.ecount(), 0);
988        assert_eq!(g.vcount(), 3);
989        for v in 0..3 {
990            assert_eq!(g.degree(v).unwrap(), 0);
991        }
992    }
993
994    #[test]
995    fn delete_edges_out_of_range_errors_and_preserves_state() {
996        let mut g = Graph::with_vertices(3);
997        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
998        let err = g.delete_edges(&[5]).unwrap_err();
999        assert!(matches!(err, IgraphError::EdgeOutOfRange { id: 5, m: 2 }));
1000        // Graph unchanged.
1001        assert_eq!(g.ecount(), 2);
1002    }
1003
1004    #[test]
1005    fn delete_edges_self_loop_directed() {
1006        let mut g = Graph::new(2, true).unwrap();
1007        g.add_edges(vec![(0, 0), (0, 1)]).unwrap();
1008        g.delete_edges(&[0]).unwrap(); // remove the self-loop
1009        assert_eq!(g.ecount(), 1);
1010        assert_eq!(g.degree(0).unwrap(), 1);
1011        assert_eq!(g.find_eid(0, 1).unwrap(), Some(0));
1012    }
1013
1014    #[test]
1015    fn delete_vertices_empty_input_is_noop() {
1016        let mut g = Graph::with_vertices(3);
1017        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1018        g.delete_vertices(&[]).unwrap();
1019        assert_eq!(g.vcount(), 3);
1020        assert_eq!(g.ecount(), 2);
1021    }
1022
1023    #[test]
1024    fn delete_vertices_single_renumbers() {
1025        let mut g = Graph::with_vertices(4);
1026        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap();
1027        // Remove vertex 1: edges (0,1) and (1,2) go with it. (2,3),(0,3)
1028        // survive but get renumbered: 2 → 1, 3 → 2.
1029        g.delete_vertices(&[1]).unwrap();
1030        assert_eq!(g.vcount(), 3);
1031        assert_eq!(g.ecount(), 2);
1032        // (2,3) → (1,2); (0,3) → (0,2).
1033        assert!(g.find_eid(1, 2).unwrap().is_some());
1034        assert!(g.find_eid(0, 2).unwrap().is_some());
1035        assert_eq!(g.find_eid(0, 1).unwrap(), None);
1036    }
1037
1038    #[test]
1039    fn delete_vertices_duplicate_ids_tolerated() {
1040        let mut g = Graph::with_vertices(3);
1041        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1042        g.delete_vertices(&[1, 1, 1]).unwrap();
1043        assert_eq!(g.vcount(), 2);
1044        assert_eq!(g.ecount(), 0);
1045    }
1046
1047    #[test]
1048    fn delete_vertices_all_yields_empty_graph() {
1049        let mut g = Graph::with_vertices(3);
1050        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1051        g.delete_vertices(&[0, 1, 2]).unwrap();
1052        assert_eq!(g.vcount(), 0);
1053        assert_eq!(g.ecount(), 0);
1054    }
1055
1056    #[test]
1057    fn delete_vertices_out_of_range_errors_and_preserves_state() {
1058        let mut g = Graph::with_vertices(3);
1059        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
1060        let err = g.delete_vertices(&[5]).unwrap_err();
1061        assert!(matches!(err, IgraphError::VertexOutOfRange { id: 5, n: 3 }));
1062        assert_eq!(g.vcount(), 3);
1063        assert_eq!(g.ecount(), 2);
1064    }
1065
1066    #[test]
1067    fn delete_vertices_map_returns_correct_mappings() {
1068        let mut g = Graph::with_vertices(5);
1069        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
1070        let (map, invmap) = g.delete_vertices_map(&[1, 3]).unwrap();
1071        // Removed: 1 and 3. Retained: 0 → 0, 2 → 1, 4 → 2.
1072        assert_eq!(map, vec![Some(0), None, Some(1), None, Some(2)]);
1073        assert_eq!(invmap, vec![0, 2, 4]);
1074        assert_eq!(g.vcount(), 3);
1075        // Only edges between retained vertices survive — none do here.
1076        assert_eq!(g.ecount(), 0);
1077    }
1078
1079    #[test]
1080    fn delete_vertices_directed_preserves_direction() {
1081        let mut g = Graph::new(4, true).unwrap();
1082        g.add_edges(vec![(0, 1), (1, 2), (2, 0), (3, 0)]).unwrap();
1083        g.delete_vertices(&[3]).unwrap();
1084        assert_eq!(g.vcount(), 3);
1085        assert!(g.is_directed());
1086        // Surviving directed edges (3 → 0) gone; (0,1),(1,2),(2,0) keep direction.
1087        assert!(g.get_eid(0, 1).is_ok());
1088        assert!(g.get_eid(1, 0).is_err()); // wrong direction
1089    }
1090
1091    #[test]
1092    fn delete_vertices_self_loop_on_removed_vertex() {
1093        let mut g = Graph::with_vertices(3);
1094        g.add_edges(vec![(0, 0), (0, 1), (1, 2)]).unwrap();
1095        g.delete_vertices(&[0]).unwrap();
1096        // Self-loop and edges to vertex 0 gone; only (1,2) → (0,1) survives.
1097        assert_eq!(g.vcount(), 2);
1098        assert_eq!(g.ecount(), 1);
1099        assert!(g.find_eid(0, 1).unwrap().is_some());
1100    }
1101
1102    #[test]
1103    fn delete_vertices_preserves_parallel_edges() {
1104        let mut g = Graph::with_vertices(3);
1105        g.add_edges(vec![(0, 1), (0, 1), (1, 2)]).unwrap();
1106        g.delete_vertices(&[2]).unwrap();
1107        assert_eq!(g.vcount(), 2);
1108        assert_eq!(g.ecount(), 2); // both parallel (0,1) edges retained
1109        assert_eq!(g.degree(0).unwrap(), 2);
1110        assert_eq!(g.degree(1).unwrap(), 2);
1111    }
1112
1113    #[test]
1114    fn add_edges_after_delete_works() {
1115        let mut g = Graph::with_vertices(4);
1116        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
1117        g.delete_vertices(&[0]).unwrap(); // now n=3, vertices 0,1,2
1118        // Add a new edge and check indexes still work.
1119        g.add_edge(0, 2).unwrap();
1120        assert_eq!(g.ecount(), 3);
1121        assert_eq!(g.degree(0).unwrap(), 2); // (0,1)+(0,2)
1122        assert!(g.find_eid(0, 2).unwrap().is_some());
1123    }
1124}