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