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