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