Skip to main content

igraph/
conversion.rs

1//! Conversion of graphs to matrices, edge lists, Prüfer sequences, and
2//! between directed and undirected graphs (`igraph_conversion.h`).
3//!
4//! This module binds the whole of igraph's *conversion* chapter. Every
5//! function is a method of [`Graph`]:
6//!
7//! | Method | C function | Result |
8//! |---|---|---|
9//! | [`Graph::get_adjacency`] | [`igraph_get_adjacency`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_adjacency) | dense adjacency [`Matrix`] (optionally weighted) |
10//! | [`Graph::get_adjacency_sparse`] | [`igraph_get_adjacency_sparse`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_adjacency_sparse) | sparse adjacency matrix as a [`CooMatrix`] |
11//! | [`Graph::get_stochastic`] | [`igraph_get_stochastic`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_stochastic) | row- or column-stochastic transition [`Matrix`] |
12//! | [`Graph::get_stochastic_sparse`] | [`igraph_get_stochastic_sparse`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_stochastic_sparse) | the same, as a [`CooMatrix`] |
13//! | [`Graph::get_edgelist`] | [`igraph_get_edgelist`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_edgelist) | flat edge list, row- or column-wise |
14//! | [`Graph::to_directed`] / [`Graph::into_directed`] | [`igraph_to_directed`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_directed) | undirected → directed, in place / by value |
15//! | [`Graph::to_undirected`] / [`Graph::into_undirected`] | [`igraph_to_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_undirected) | directed → undirected, in place / by value |
16//! | [`Graph::to_undirected_with_comb`] | [`igraph_to_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_undirected) | the same, combining the edge attributes of merged edges |
17//! | [`Graph::to_prufer`] | [`igraph_to_prufer`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_prufer) | Prüfer sequence of a labelled tree |
18//!
19//! # Sparse results
20//!
21//! igraph returns sparse matrices as `igraph_sparsemat_t` (a CXSparse
22//! matrix, exposed by this crate as [`SparseMat`]). The sparse wrappers of
23//! this module read that matrix out into a plain-Rust [`CooMatrix`]
24//! (coordinate / triplet format), with duplicate entries summed up and the
25//! entries sorted in row-major order. It is easy to inspect, compare and
26//! iterate over; convert it back with [`CooMatrix::to_sparsemat`] when you
27//! need the sparse linear algebra of [`crate::linalg`] (solvers,
28//! factorizations, ARPACK eigensolvers).
29//!
30//! # See also
31//!
32//! - The inverse conversions are constructors in [`crate::constructors`]:
33//!   [`Graph::adjacency`], [`Graph::weighted_adjacency`],
34//!   [`Graph::sparse_adjacency`] and [`Graph::sparse_weighted_adjacency`]
35//!   build a graph from an adjacency matrix, [`Graph::from_flat_edges`]
36//!   from the output of [`Graph::get_edgelist`], and [`Graph::from_prufer`]
37//!   from a Prüfer sequence.
38//! - Other matrices of a graph: the Laplacian ([`Graph::get_laplacian`],
39//!   [`Graph::get_laplacian_sparse`]) in [`crate::structural`], and the
40//!   spectra of adjacency matrices ([`Graph::eigen_adjacency`]) in
41//!   [`crate::linalg`].
42//! - Random walks: [`Graph::random_walk`] simulates the walk whose
43//!   transition matrix is [`Graph::get_stochastic`], and
44//!   [`Graph::pagerank`] computes its (damped) stationary distribution.
45//! - Directedness: [`Graph::is_mutual`], [`Graph::has_mutual`] and
46//!   [`Graph::reciprocity`] tell how much [`ToUndirected::Mutual`] and
47//!   [`ToUndirected::Collapse`] will differ; [`Graph::is_dag`] checks the
48//!   result of [`ToDirected::Acyclic`].
49//!
50//! # Example
51//!
52//! ```
53//! use igraph::prelude::*;
54//!
55//! // A directed 3-cycle 0 → 1 → 2 → 0.
56//! let mut g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
57//! let a = g.get_adjacency(GetAdjacency::Both, None, Loops::Twice).unwrap();
58//! assert_eq!(a.to_rows(), vec![vec![0.0, 1.0, 0.0], vec![0.0, 0.0, 1.0], vec![1.0, 0.0, 0.0]]);
59//!
60//! // Forget the directions: the adjacency matrix becomes symmetric.
61//! g.to_undirected(ToUndirected::Collapse).unwrap();
62//! let a = g.get_adjacency(GetAdjacency::Both, None, Loops::Twice).unwrap();
63//! assert_eq!(a, a.transposed());
64//! // ... and it is enough to rebuild the graph.
65//! let h = Graph::adjacency(&a, Adjacency::Undirected, Loops::Twice).unwrap();
66//! assert_eq!(h.ecount(), 3);
67//!
68//! // Random walk transition probabilities: each row sums to one.
69//! let p = g.get_stochastic(false, None).unwrap();
70//! assert!(p.rows().all(|r| (r.iter().sum::<f64>() - 1.0).abs() < 1e-12));
71//!
72//! // A path is a tree: its Prüfer sequence lists the inner vertices.
73//! let path = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
74//! assert_eq!(path.to_prufer().unwrap(), vec![1, 2]);
75//! assert!(Graph::from_prufer(&[1, 2]).unwrap().is_same_graph(&path).unwrap());
76//! ```
77//!
78//! On a larger scale, with Zachary's karate club network: the sparse
79//! adjacency matrix stores `2|E|` entries, and its row sums are the degrees.
80//!
81//! ```
82//! use igraph::prelude::*;
83//!
84//! let karate = Graph::famous("Zachary").unwrap();
85//! let a = karate.get_adjacency_sparse(GetAdjacency::Both, None, Loops::Twice).unwrap();
86//! assert_eq!(a.nnz(), 2 * karate.ecount());
87//! let degrees = karate.degree(.., NeighborMode::All, Loops::Twice).unwrap();
88//! let row_sums: Vec<i64> = a.row_sums().iter().map(|&s| s as i64).collect();
89//! assert_eq!(row_sums, degrees);
90//! ```
91
92use crate::{
93    attributes::AttributeCombination,
94    constants::{GetAdjacency, Loops, NeighborMode, ToDirected, ToUndirected},
95    error::{Error, Result},
96    ffi::*,
97    graph::{Graph, VertexId},
98    igraph_call,
99    linalg::SparseMat,
100    matrix::Matrix,
101    vector::{Vector, VectorInt},
102};
103use std::collections::BTreeMap;
104
105/// A sparse real matrix in coordinate (triplet) format.
106///
107/// Returned by the sparse conversion functions of this module
108/// ([`Graph::get_adjacency_sparse`], [`Graph::get_stochastic_sparse`]).
109/// Each stored element appears exactly once in [`entries`](Self::entries)
110/// as a `(row, column, value)` triple; entries are sorted by row, then by
111/// column. Elements that are not listed are zero.
112///
113/// It is a plain-Rust snapshot of an igraph [`SparseMat`], with a few
114/// helpers (products with vectors, row and column sums, transposition);
115/// [`to_sparsemat`](Self::to_sparsemat) and [`to_dense`](Self::to_dense)
116/// convert it to igraph's sparse and dense matrices.
117///
118/// ```
119/// use igraph::prelude::*;
120/// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
121/// let a = g.get_adjacency_sparse(GetAdjacency::Upper, None, Loops::Twice).unwrap();
122/// assert_eq!((a.nrow, a.ncol), (3, 3));
123/// assert_eq!(a.entries, vec![(0, 1, 1.0), (1, 2, 1.0)]);
124/// assert_eq!(a.get(1, 2), 1.0);
125/// assert_eq!(a.get(2, 1), 0.0);
126/// assert_eq!(a.to_dense(), g.get_adjacency(GetAdjacency::Upper, None, Loops::Twice).unwrap());
127/// ```
128#[derive(Debug, Clone, PartialEq)]
129pub struct CooMatrix {
130    /// Number of rows.
131    pub nrow: usize,
132    /// Number of columns.
133    pub ncol: usize,
134    /// The stored `(row, column, value)` triples, sorted by `(row, column)`,
135    /// without duplicate positions.
136    pub entries: Vec<(i64, i64, f64)>,
137}
138
139impl CooMatrix {
140    /// Number of stored entries.
141    ///
142    /// Every structurally non-zero position is stored once. A stored value
143    /// may still be `0.0`, e.g. for an edge of weight zero, or in the rows
144    /// of zero-strength vertices of a stochastic matrix.
145    pub fn nnz(&self) -> usize {
146        self.entries.len()
147    }
148
149    /// The element at `(row, col)`, zero if it is not stored.
150    ///
151    /// Runs in `O(log nnz)` time with a binary search, so it relies on the
152    /// entries being sorted and unique, as they are in every matrix returned
153    /// by this module.
154    pub fn get(&self, row: i64, col: i64) -> f64 {
155        self.entries
156            .binary_search_by(|&(i, j, _)| (i, j).cmp(&(row, col)))
157            .map_or(0.0, |k| self.entries[k].2)
158    }
159
160    /// Iterates over the stored `(row, column, value)` triples, in
161    /// row-major order.
162    pub fn iter(&self) -> impl Iterator<Item = (i64, i64, f64)> + '_ {
163        self.entries.iter().copied()
164    }
165
166    /// The transposed matrix (`ncol × nrow`), again sorted in row-major
167    /// order.
168    ///
169    /// ```
170    /// use igraph::prelude::*;
171    /// let g = Graph::from_edges(&[(0, 1), (0, 2)], 3, true).unwrap();
172    /// let a = g.get_adjacency_sparse(GetAdjacency::Both, None, Loops::Once).unwrap();
173    /// assert_eq!(a.transpose().entries, vec![(1, 0, 1.0), (2, 0, 1.0)]);
174    /// ```
175    pub fn transpose(&self) -> CooMatrix {
176        let mut entries: Vec<_> = self.entries.iter().map(|&(i, j, x)| (j, i, x)).collect();
177        entries.sort_by_key(|&(i, j, _)| (i, j));
178        CooMatrix {
179            nrow: self.ncol,
180            ncol: self.nrow,
181            entries,
182        }
183    }
184
185    /// The matrix-vector product `A · x`, a vector of length
186    /// [`nrow`](Self::nrow). Runs in `O(nrow + nnz)` time.
187    ///
188    /// # Panics
189    /// If `x.len() != ncol`, or if an entry lies outside the matrix.
190    ///
191    /// ```
192    /// use igraph::prelude::*;
193    /// // Out-degrees of a directed graph: A · 1.
194    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (2, 1)], 3, true).unwrap();
195    /// let a = g.get_adjacency_sparse(GetAdjacency::Both, None, Loops::Once).unwrap();
196    /// assert_eq!(a.mul_vec(&[1.0; 3]), vec![2.0, 0.0, 1.0]);
197    /// ```
198    pub fn mul_vec(&self, x: &[f64]) -> Vec<f64> {
199        assert_eq!(x.len(), self.ncol, "vector length must equal ncol");
200        let mut y = vec![0.0; self.nrow];
201        for &(i, j, v) in &self.entries {
202            y[i as usize] += v * x[j as usize];
203        }
204        y
205    }
206
207    /// The vector-matrix product `xᵀ · A`, a vector of length
208    /// [`ncol`](Self::ncol). Runs in `O(ncol + nnz)` time.
209    ///
210    /// With a row-stochastic matrix `P` (see
211    /// [`Graph::get_stochastic_sparse`]) this is one step of a random walk:
212    /// if `x` is the distribution of the walker, `xᵀ · P` is its distribution
213    /// after one more step.
214    ///
215    /// # Panics
216    /// If `x.len() != nrow`, or if an entry lies outside the matrix.
217    ///
218    /// ```
219    /// use igraph::prelude::*;
220    /// // A walker on the directed cycle 0 → 1 → 2 → 0 moves one step ahead.
221    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
222    /// let p = g.get_stochastic_sparse(false, None).unwrap();
223    /// assert_eq!(p.vec_mul(&[1.0, 0.0, 0.0]), vec![0.0, 1.0, 0.0]);
224    /// ```
225    pub fn vec_mul(&self, x: &[f64]) -> Vec<f64> {
226        assert_eq!(x.len(), self.nrow, "vector length must equal nrow");
227        let mut y = vec![0.0; self.ncol];
228        for &(i, j, v) in &self.entries {
229            y[j as usize] += x[i as usize] * v;
230        }
231        y
232    }
233
234    /// Converts to a dense [`Matrix`] of size `nrow × ncol`.
235    ///
236    /// # Panics
237    /// If an entry lies outside the matrix (only possible for a
238    /// hand-built `CooMatrix`).
239    pub fn to_dense(&self) -> Matrix {
240        let mut m = Matrix::zeros(self.nrow, self.ncol);
241        for &(i, j, x) in &self.entries {
242            m[(i as usize, j as usize)] += x;
243        }
244        m
245    }
246
247    /// Row sums, a vector of length [`nrow`](Self::nrow).
248    ///
249    /// # Panics
250    /// If an entry lies outside the matrix.
251    pub fn row_sums(&self) -> Vec<f64> {
252        let mut s = vec![0.0; self.nrow];
253        for &(i, _, x) in &self.entries {
254            s[i as usize] += x;
255        }
256        s
257    }
258
259    /// Column sums, a vector of length [`ncol`](Self::ncol).
260    ///
261    /// # Panics
262    /// If an entry lies outside the matrix.
263    pub fn col_sums(&self) -> Vec<f64> {
264        let mut s = vec![0.0; self.ncol];
265        for &(_, j, x) in &self.entries {
266            s[j as usize] += x;
267        }
268        s
269    }
270
271    /// Converts to an igraph sparse matrix ([`SparseMat`], in triplet
272    /// format), for the sparse linear algebra of [`crate::linalg`]. Duplicate
273    /// positions (possible only in a hand-built `CooMatrix`) are summed.
274    /// Most [`SparseMat`] operations accept the triplet format and compress a
275    /// temporary copy when they need to; call [`SparseMat::compress`] once
276    /// up front when the matrix is used repeatedly.
277    ///
278    /// # Errors
279    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if an
280    /// entry has a negative index or lies outside the matrix (only possible
281    /// for a hand-built `CooMatrix`).
282    ///
283    /// # Examples
284    /// ```
285    /// use igraph::prelude::*;
286    /// // The squared adjacency matrix of a path counts the walks of length 2.
287    /// let path = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
288    /// let a = path
289    ///     .get_adjacency_sparse(GetAdjacency::Both, None, Loops::Twice)
290    ///     .unwrap()
291    ///     .to_sparsemat()
292    ///     .unwrap()
293    ///     .compress()
294    ///     .unwrap();
295    /// let a2 = a.multiply(&a).unwrap();
296    /// assert_eq!(a2.get(0, 2), 1.0); // 0 - 1 - 2
297    /// assert_eq!(a2.get(1, 1), 2.0); // 1 - 0 - 1 and 1 - 2 - 1
298    /// assert_eq!(a2.get(0, 3), 0.0);
299    /// ```
300    pub fn to_sparsemat(&self) -> Result<SparseMat> {
301        let triplets = self
302            .entries
303            .iter()
304            .map(
305                |&(i, j, x)| match (usize::try_from(i), usize::try_from(j)) {
306                    (Ok(i), Ok(j)) => Ok((i, j, x)),
307                    _ => Err(Error::invalid(format!(
308                        "negative index ({i}, {j}) in a sparse matrix"
309                    ))),
310                },
311            )
312            .collect::<Result<Vec<_>>>()?;
313        SparseMat::from_triplets(self.nrow, self.ncol, &triplets)
314    }
315}
316
317impl From<&CooMatrix> for Matrix {
318    fn from(m: &CooMatrix) -> Self {
319        m.to_dense()
320    }
321}
322
323/// Reads a sparse matrix returned by igraph (triplet or column-compressed)
324/// into coordinate format, summing duplicate positions and sorting the
325/// entries in row-major order.
326fn sparsemat_to_coo(a: &SparseMat) -> CooMatrix {
327    let mut acc: BTreeMap<(i64, i64), f64> = BTreeMap::new();
328    for (i, j, x) in a.iter() {
329        *acc.entry((i as i64, j as i64)).or_insert(0.0) += x;
330    }
331    CooMatrix {
332        nrow: a.nrow(),
333        ncol: a.ncol(),
334        entries: acc.into_iter().map(|((i, j), x)| (i, j, x)).collect(),
335    }
336}
337
338/// Clears the rows (or columns, if `column_wise`) of the dense stochastic
339/// matrix `res` that belong to vertices of zero strength.
340///
341/// igraph's dense `igraph_get_stochastic` divides by the strength without
342/// checking it (still so in igraph 1.0.0 and 1.0.1), so such rows are
343/// `0 / 0 = NaN` whenever a vertex has edges but they all have weight zero.
344/// The sparse version (normalizing with `allow_zeros = true`) and the C docs
345/// give zeros instead.
346///
347/// The strengths come from the very call the C function makes
348/// (`igraph_strength` with `IGRAPH_LOOPS`, in the same mode), so the zero
349/// test sees bit-for-bit the same sums as the division did, even when
350/// mixed-sign weights make a sum depend on the order of the additions.
351fn zero_out_null_strength(
352    graph: &Graph,
353    res: &mut Matrix,
354    column_wise: bool,
355    weights: &[f64],
356) -> Result<()> {
357    let mode = match (graph.is_directed(), column_wise) {
358        (false, _) => NeighborMode::All,
359        (true, false) => NeighborMode::Out,
360        (true, true) => NeighborMode::In,
361    };
362    let strength = graph.strength(.., mode, Loops::Twice, Some(weights))?;
363    let n = strength.len();
364    for (v, _) in strength.iter().enumerate().filter(|&(_, &s)| s == 0.0) {
365        for u in 0..n {
366            if column_wise {
367                res[(u, v)] = 0.0;
368            } else {
369                res[(v, u)] = 0.0;
370            }
371        }
372    }
373    Ok(())
374}
375
376/// Checks that an optional weight vector has one entry per edge.
377///
378/// igraph's conversion functions index the weight vector by edge id without
379/// checking its length, so this check is required for memory safety.
380fn check_weights(graph: &Graph, weights: Option<&[f64]>) -> Result<()> {
381    match weights {
382        Some(w) if w.len() != graph.ecount() => Err(Error::invalid(format!(
383            "weight vector length ({}) must match the number of edges ({})",
384            w.len(),
385            graph.ecount()
386        ))),
387        _ => Ok(()),
388    }
389}
390
391impl igraph_t {
392    /// The (dense) adjacency matrix of the graph.
393    ///
394    /// Entry `(i, j)` of the returned `n × n` [`Matrix`] is the number of
395    /// edges from vertex `i` to vertex `j` or, when `weights` are given, the
396    /// total weight of those edges (so multi-edges add up).
397    ///
398    /// - `kind` selects which part of the matrix is filled for *undirected*
399    ///   graphs: [`GetAdjacency::Upper`] (upper-right triangle only, each edge
400    ///   stored once), [`GetAdjacency::Lower`] (lower-left triangle) or
401    ///   [`GetAdjacency::Both`] (the full symmetric matrix). It is ignored
402    ///   for directed graphs.
403    /// - `weights`: optional edge weights, one per edge; `None` means every
404    ///   edge has weight 1.
405    /// - `loops` controls the diagonal: [`Loops::None`] ignores self-loops
406    ///   (zero diagonal), [`Loops::Once`] counts each loop once and
407    ///   [`Loops::Twice`] counts loops twice in *undirected* graphs (it
408    ///   counts edge *stems*, the convention that makes row sums equal to
409    ///   degrees). In directed graphs `Twice` behaves like `Once`.
410    ///
411    /// Time complexity: `O(|V|²)`.
412    ///
413    /// Binds [`igraph_get_adjacency`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_adjacency).
414    /// See [`get_adjacency_sparse`](Self::get_adjacency_sparse) for large,
415    /// sparse graphs.
416    ///
417    /// # Errors
418    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `weights` has the
419    /// wrong length.
420    ///
421    /// # Examples
422    /// ```
423    /// use igraph::prelude::*;
424    /// // A triangle with a double edge 0-1 and a loop on vertex 2.
425    /// let g = Graph::from_edges(&[(0, 1), (0, 1), (1, 2), (2, 0), (2, 2)], 3, false).unwrap();
426    /// let a = g.get_adjacency(GetAdjacency::Both, None, Loops::Twice).unwrap();
427    /// assert_eq!(a.to_rows(), vec![
428    ///     vec![0.0, 2.0, 1.0],
429    ///     vec![2.0, 0.0, 1.0],
430    ///     vec![1.0, 1.0, 2.0],
431    /// ]);
432    /// // Row sums are the degrees.
433    /// let degrees: Vec<f64> = a.rows().map(|r| r.iter().sum()).collect();
434    /// assert_eq!(degrees, vec![3.0, 3.0, 4.0]);
435    ///
436    /// // Weighted, upper triangle, loops ignored.
437    /// let w = [0.5, 1.5, 2.0, 3.0, 9.0];
438    /// let a = g.get_adjacency(GetAdjacency::Upper, Some(&w), Loops::None).unwrap();
439    /// assert_eq!(a.to_rows(), vec![
440    ///     vec![0.0, 2.0, 3.0],
441    ///     vec![0.0, 0.0, 2.0],
442    ///     vec![0.0, 0.0, 0.0],
443    /// ]);
444    /// ```
445    pub fn get_adjacency(
446        &self,
447        kind: GetAdjacency,
448        weights: Option<&[f64]>,
449        loops: Loops,
450    ) -> Result<Matrix> {
451        check_weights(self, weights)?;
452        let w = weights.map(Vector::view);
453        let wp = w.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
454        let mut res = Matrix::new();
455        igraph_call!(igraph_get_adjacency(
456            self,
457            &mut res,
458            kind.into(),
459            wp,
460            loops.into()
461        ))?;
462        Ok(res)
463    }
464
465    /// The adjacency matrix of the graph in sparse (coordinate) format.
466    ///
467    /// Same semantics as [`get_adjacency`](Self::get_adjacency) (`kind`,
468    /// `weights` and `loops` mean exactly the same), but only the non-zero
469    /// entries are stored, so the memory use is `O(|V| + |E|)` instead of
470    /// `O(|V|²)`. The C result (an `igraph_sparsemat_t`) is read into a
471    /// [`CooMatrix`] whose parallel entries (from multi-edges) are summed.
472    ///
473    /// Binds [`igraph_get_adjacency_sparse`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_adjacency_sparse).
474    ///
475    /// # Errors
476    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `weights` has the
477    /// wrong length.
478    ///
479    /// # Examples
480    /// ```
481    /// use igraph::prelude::*;
482    /// // A star with 1000 leaves: 1001² ≈ 10⁶ dense entries, only 2000 stored.
483    /// let edges: Vec<(i64, i64)> = (1..=1000).map(|i| (0, i)).collect();
484    /// let star = Graph::from_edges(&edges, 1001, false).unwrap();
485    /// let a = star.get_adjacency_sparse(GetAdjacency::Both, None, Loops::Twice).unwrap();
486    /// assert_eq!(a.nnz(), 2000);
487    /// assert_eq!(a.row_sums()[0], 1000.0);
488    /// assert_eq!(a.get(7, 0), 1.0);
489    /// ```
490    pub fn get_adjacency_sparse(
491        &self,
492        kind: GetAdjacency,
493        weights: Option<&[f64]>,
494        loops: Loops,
495    ) -> Result<CooMatrix> {
496        check_weights(self, weights)?;
497        let w = weights.map(Vector::view);
498        let wp = w.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
499        let mut res = SparseMat::new(0, 0)?;
500        igraph_call!(igraph_get_adjacency_sparse(
501            self,
502            &mut res,
503            kind.into(),
504            wp,
505            loops.into()
506        ))?;
507        Ok(sparsemat_to_coo(&res))
508    }
509
510    /// The stochastic (random walk transition) matrix of the graph.
511    ///
512    /// This is the adjacency matrix normalized so that each row sums to one
513    /// (`column_wise = false`, a *right-stochastic* matrix) or each column
514    /// sums to one (`column_wise = true`, *left-stochastic*). Row-wise,
515    /// entry `(i, j)` is the probability that a random walker at `i` steps
516    /// to `j` following the edge directions; the column-wise matrix relates
517    /// to walks moving against the edge directions. Rows (columns) of
518    /// vertices with zero out-strength (in-strength) are all zeros.
519    ///
520    /// Undirected self-loops are counted twice, consistently with degrees.
521    /// `weights` are optional edge weights (`None`: all 1), which make the
522    /// transition probabilities proportional to the weights. They should be
523    /// non-negative, otherwise the result is not a probability matrix.
524    ///
525    /// A vertex whose incident edges all have weight zero has zero strength:
526    /// its row (column) is all zeros, like for a vertex without edges. (The
527    /// C function of igraph 1.0.0 and 1.0.1 would fill it with
528    /// `0 / 0 = NaN`; this wrapper clears those rows so that the dense and
529    /// sparse versions agree, as the C documentation promises.)
530    ///
531    /// With negative weights a vertex can also have zero strength because
532    /// its weights cancel out; its row (column) is cleared as well, like in
533    /// [`get_stochastic_sparse`](Self::get_stochastic_sparse).
534    ///
535    /// The strengths used for the normalization are those of
536    /// [`Graph::strength`] with [`Loops::Twice`]
537    /// (out-strengths row-wise, in-strengths column-wise). Time complexity:
538    /// `O(|V|²)`.
539    ///
540    /// Binds [`igraph_get_stochastic`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_stochastic).
541    /// See also [`Graph::random_walk`], which simulates this walk, and
542    /// [`Graph::pagerank`], its damped stationary distribution.
543    ///
544    /// # Errors
545    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `weights` has the
546    /// wrong length.
547    ///
548    /// # Examples
549    /// ```
550    /// use igraph::prelude::*;
551    /// // Star 0-1, 0-2, 0-3: from the center, each leaf with probability 1/3.
552    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], 4, false).unwrap();
553    /// let p = g.get_stochastic(false, None).unwrap();
554    /// assert_eq!(p.row(0), vec![0.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]);
555    /// assert_eq!(p.row(2), vec![1.0, 0.0, 0.0, 0.0]);
556    /// // Column-wise it is the transpose, for an undirected graph.
557    /// assert_eq!(g.get_stochastic(true, None).unwrap(), p.transposed());
558    /// ```
559    pub fn get_stochastic(&self, column_wise: bool, weights: Option<&[f64]>) -> Result<Matrix> {
560        check_weights(self, weights)?;
561        let w = weights.map(Vector::view);
562        let wp = w.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
563        let mut res = Matrix::new();
564        igraph_call!(igraph_get_stochastic(self, &mut res, column_wise, wp))?;
565        if let Some(w) = weights {
566            zero_out_null_strength(self, &mut res, column_wise, w)?;
567        }
568        Ok(res)
569    }
570
571    /// The stochastic matrix of the graph in sparse (coordinate) format.
572    ///
573    /// Same as [`get_stochastic`](Self::get_stochastic), but computed in
574    /// `O(|V| + |E|)` time and returned as a [`CooMatrix`]. Entries of
575    /// zero-strength vertices (all incident weights zero) are stored as
576    /// explicit `0.0` values; so are those of a vertex whose (mixed-sign)
577    /// weights sum to zero, as igraph scales such a row (column) by zero.
578    ///
579    /// Binds [`igraph_get_stochastic_sparse`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_stochastic_sparse).
580    ///
581    /// # Errors
582    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `weights` has the
583    /// wrong length.
584    ///
585    /// # Examples
586    /// ```
587    /// use igraph::prelude::*;
588    /// // Directed: 0 → 1 (weight 3), 0 → 2 (weight 1); vertices 1, 2 are sinks.
589    /// let g = Graph::from_edges(&[(0, 1), (0, 2)], 3, true).unwrap();
590    /// let p = g.get_stochastic_sparse(false, Some(&[3.0, 1.0])).unwrap();
591    /// assert_eq!(p.entries, vec![(0, 1, 0.75), (0, 2, 0.25)]);
592    /// assert_eq!(p.row_sums(), vec![1.0, 0.0, 0.0]);
593    /// ```
594    pub fn get_stochastic_sparse(
595        &self,
596        column_wise: bool,
597        weights: Option<&[f64]>,
598    ) -> Result<CooMatrix> {
599        check_weights(self, weights)?;
600        let w = weights.map(Vector::view);
601        let wp = w.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
602        let mut res = SparseMat::new(0, 0)?;
603        igraph_call!(igraph_get_stochastic_sparse(
604            self,
605            &mut res,
606            column_wise,
607            wp
608        ))?;
609        Ok(sparsemat_to_coo(&res))
610    }
611
612    /// The list of all edges as a flat vector, in edge id order.
613    ///
614    /// With `bycol = false` the result is `[from0, to0, from1, to1, …]`, the
615    /// format accepted by [`Graph::from_flat_edges`] and
616    /// [`Graph::add_edges_from_vector`]. With `bycol = true` it is
617    /// "column-wise": first all the sources, then all the targets,
618    /// `[from0, from1, …, to0, to1, …]`, i.e. edge `e` is
619    /// `res[e] → res[|E| + e]`. For `(from, to)` pairs see also
620    /// [`Graph::edge_list`].
621    ///
622    /// Time complexity: `O(|E|)`.
623    ///
624    /// Binds [`igraph_get_edgelist`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_edgelist).
625    ///
626    /// # Examples
627    /// ```
628    /// use igraph::prelude::*;
629    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (3, 2)], 4, true).unwrap();
630    /// assert_eq!(g.get_edgelist(false).unwrap(), vec![0, 1, 1, 2, 3, 2]);
631    /// assert_eq!(g.get_edgelist(true).unwrap(), vec![0, 1, 3, 1, 2, 2]);
632    /// // Round trip.
633    /// let h = Graph::from_flat_edges(&g.get_edgelist(false).unwrap(), 4, true).unwrap();
634    /// assert!(g.is_same_graph(&h).unwrap());
635    /// ```
636    pub fn get_edgelist(&self, bycol: bool) -> Result<Vec<VertexId>> {
637        let mut res = VectorInt::new();
638        igraph_call!(igraph_get_edgelist(self, &mut res, bycol))?;
639        Ok(res.into())
640    }
641
642    /// Converts an undirected graph into a directed one, in place.
643    ///
644    /// Does nothing if the graph is already directed. The vertex ids are
645    /// kept; how edges are directed depends on `mode`:
646    ///
647    /// - [`ToDirected::Arbitrary`]: each edge becomes one directed edge with
648    ///   an arbitrary (implementation-defined) direction; edge ids are kept.
649    /// - [`ToDirected::Mutual`]: each edge `e` becomes two directed edges,
650    ///   one per direction; the first `|E|` edges keep the ids and endpoint
651    ///   order of the original ones, edge `|E| + e` is the reverse of `e`.
652    /// - [`ToDirected::Random`]: each edge gets a uniformly random direction
653    ///   (drawn from the calling thread's default RNG, so reproducible
654    ///   after [`rng::seed`](crate::rng::seed)).
655    /// - [`ToDirected::Acyclic`]: each edge is directed from the smaller to
656    ///   the larger vertex id; without self-loops the result is a DAG (see
657    ///   [`Graph::is_dag`]), and the identity is a topological order.
658    ///
659    /// `Random` and `Acyclic` keep the edge ids too.
660    ///
661    /// Graph, vertex and edge attributes (if an attribute handler is
662    /// installed) are kept; with `Mutual` both copies of an edge get its
663    /// attributes. Time complexity: `O(|V| + |E|)`.
664    ///
665    /// Binds [`igraph_to_directed`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_directed).
666    /// See [`into_directed`](Self::into_directed) for a by-value variant.
667    ///
668    /// # Examples
669    /// ```
670    /// use igraph::prelude::*;
671    /// let mut g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
672    /// g.to_directed(ToDirected::Mutual).unwrap();
673    /// assert!(g.is_directed());
674    /// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2), (1, 0), (2, 1)]);
675    /// ```
676    pub fn to_directed(&mut self, mode: ToDirected) -> Result<()> {
677        igraph_call!(igraph_to_directed(self, mode.into()))
678    }
679
680    /// Consumes the graph and returns its directed version, see
681    /// [`to_directed`](Self::to_directed).
682    ///
683    /// Handy in builder chains:
684    /// ```
685    /// use igraph::prelude::*;
686    /// let dag = Graph::from_edges(&[(2, 0), (1, 2), (0, 1)], 3, false)
687    ///     .and_then(|g| g.into_directed(ToDirected::Acyclic))
688    ///     .unwrap();
689    /// assert!(dag.edge_list().iter().all(|&(a, b)| a < b));
690    /// ```
691    pub fn into_directed(mut self, mode: ToDirected) -> Result<Graph> {
692        self.to_directed(mode)?;
693        Ok(self)
694    }
695
696    /// Converts a directed graph into an undirected one, in place.
697    ///
698    /// Does nothing if the graph is already undirected. `mode` decides
699    /// which undirected edges are created:
700    ///
701    /// - [`ToUndirected::Each`]: one undirected edge per directed edge; the
702    ///   number of edges (and their ids) is unchanged, so multi-edges may
703    ///   appear (e.g. from a mutual pair `a → b`, `b → a`).
704    /// - [`ToUndirected::Collapse`]: one undirected edge per pair of vertices
705    ///   connected by at least one directed edge in either direction; no
706    ///   multi-edges are created.
707    /// - [`ToUndirected::Mutual`]: one undirected edge per *mutual pair* of
708    ///   directed edges (`a → b` matched with `b → a`); unreciprocated edges
709    ///   are dropped, while self-loops are kept unconditionally. May create
710    ///   multi-edges when several mutual pairs join the same vertices.
711    ///
712    /// Edge attributes (with the
713    /// [attribute handler](crate::attributes::enable) on) are kept with
714    /// `Each` and dropped with `Collapse` and `Mutual`, because the C
715    /// `edge_comb` attribute combination argument is passed as `NULL`; use
716    /// [`to_undirected_with_comb`](Self::to_undirected_with_comb) or
717    /// [`to_undirected_with_attributes`](Self::to_undirected_with_attributes)
718    /// to combine the attributes of the merged edges instead. Graph and
719    /// vertex attributes are always kept.
720    ///
721    /// [`Graph::is_mutual`] and [`Graph::reciprocity`] tell which edges
722    /// `Mutual` keeps. Time complexity: `O(|V| + |E|)`.
723    ///
724    /// Binds [`igraph_to_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_undirected).
725    /// See [`into_undirected`](Self::into_undirected) for a by-value variant.
726    ///
727    /// # Examples
728    /// ```
729    /// use igraph::prelude::*;
730    /// // 0 ⇄ 1 is mutual, 1 → 2 is not.
731    /// let edges = [(0, 1), (1, 0), (1, 2)];
732    /// let mut each = Graph::from_edges(&edges, 3, true).unwrap();
733    /// each.to_undirected(ToUndirected::Each).unwrap();
734    /// assert_eq!(each.ecount(), 3);
735    ///
736    /// let mut collapse = Graph::from_edges(&edges, 3, true).unwrap();
737    /// collapse.to_undirected(ToUndirected::Collapse).unwrap();
738    /// assert_eq!(collapse.edge_list(), vec![(0, 1), (1, 2)]);
739    ///
740    /// let mut mutual = Graph::from_edges(&edges, 3, true).unwrap();
741    /// mutual.to_undirected(ToUndirected::Mutual).unwrap();
742    /// assert_eq!(mutual.edge_list(), vec![(0, 1)]);
743    /// ```
744    pub fn to_undirected(&mut self, mode: ToUndirected) -> Result<()> {
745        igraph_call!(igraph_to_undirected(self, mode.into(), std::ptr::null()))
746    }
747
748    /// Converts a directed graph into an undirected one, in place, combining
749    /// the edge attributes of the directed edges merged into each undirected
750    /// edge according to `edge_comb`.
751    ///
752    /// The structure of the result is the same as with
753    /// [`to_undirected`](Self::to_undirected). With
754    /// [`ToUndirected::Collapse`] and [`ToUndirected::Mutual`], each new
755    /// edge gets the attribute values combined over the directed edges it
756    /// replaces (e.g. the sum of their weights, see
757    /// [`AttributeCombination`]); with [`ToUndirected::Each`] edges are not
758    /// merged and keep their own values. Without an attribute handler (see
759    /// [`attributes::enable`](crate::attributes::enable)) this is the same
760    /// as [`to_undirected`](Self::to_undirected).
761    ///
762    /// Binds [`igraph_to_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_undirected)
763    /// with a non-null `edge_comb`.
764    ///
765    /// # Errors
766    /// [`ErrorKind::AttributeCombination`](crate::ErrorKind::AttributeCombination)
767    /// (or [`ErrorKind::Unimplemented`](crate::ErrorKind::Unimplemented)) if
768    /// the combination is not supported for the type of an attribute.
769    ///
770    /// # Examples
771    /// ```
772    /// use igraph::prelude::*;
773    /// use igraph::attributes::{AttributeCombination, AttributeCombinationType as Comb};
774    ///
775    /// // Traffic between two cities, in each direction.
776    /// let mut g = Graph::from_edges(&[(0, 1), (1, 0), (1, 2)], 3, true).unwrap();
777    /// g.set_edge_attr_numeric_values("traffic", &[10.0, 7.0, 3.0]).unwrap();
778    ///
779    /// let comb = AttributeCombination::from_pairs(&[(Some("traffic"), Comb::Sum)]).unwrap();
780    /// g.to_undirected_with_comb(ToUndirected::Collapse, &comb).unwrap();
781    /// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2)]);
782    /// assert_eq!(g.edge_attr_numeric_values("traffic", ..).unwrap(), vec![17.0, 3.0]);
783    /// ```
784    pub fn to_undirected_with_comb(
785        &mut self,
786        mode: ToUndirected,
787        edge_comb: &AttributeCombination,
788    ) -> Result<()> {
789        igraph_call!(igraph_to_undirected(self, mode.into(), edge_comb.as_ptr()))
790    }
791
792    /// Consumes the graph and returns its undirected version, see
793    /// [`to_undirected`](Self::to_undirected).
794    ///
795    /// ```
796    /// use igraph::prelude::*;
797    /// let g = Graph::from_edges(&[(0, 1), (1, 0)], 2, true).unwrap();
798    /// let u = g.into_undirected(ToUndirected::Collapse).unwrap();
799    /// assert_eq!((u.is_directed(), u.ecount()), (false, 1));
800    /// ```
801    pub fn into_undirected(mut self, mode: ToUndirected) -> Result<Graph> {
802        self.to_undirected(mode)?;
803        Ok(self)
804    }
805
806    /// The Prüfer sequence of a tree.
807    ///
808    /// A labelled tree on `n ≥ 2` vertices corresponds one-to-one to a
809    /// sequence of `n - 2` vertex ids in `0..n` (Cayley's formula `nⁿ⁻²`
810    /// follows). The sequence is obtained by repeatedly removing the leaf with
811    /// the smallest id and recording its neighbor. Each vertex appears
812    /// exactly `degree - 1` times, so leaves never appear.
813    ///
814    /// The inverse operation is the constructor [`Graph::from_prufer`];
815    /// [`Graph::is_tree`] checks the precondition.
816    ///
817    /// Binds [`igraph_to_prufer`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_to_prufer).
818    ///
819    /// # Errors
820    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the graph is not a
821    /// tree (edge directions are ignored; the null graph is not a tree) or has
822    /// fewer than two vertices.
823    ///
824    /// # Examples
825    /// ```
826    /// use igraph::prelude::*;
827    /// // A star centered at 3: the center appears n - 2 times.
828    /// let star = Graph::from_edges(&[(3, 0), (3, 1), (3, 2), (3, 4)], 5, false).unwrap();
829    /// assert_eq!(star.to_prufer().unwrap(), vec![3, 3, 3]);
830    ///
831    /// // Round trip through the constructor.
832    /// let back = Graph::from_prufer(&[3, 3, 3]).unwrap();
833    /// assert!(back.is_same_graph(&star).unwrap());
834    ///
835    /// // A cycle is not a tree.
836    /// let tri = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, false).unwrap();
837    /// assert_eq!(tri.to_prufer().unwrap_err().kind(), ErrorKind::InvalidValue);
838    /// ```
839    pub fn to_prufer(&self) -> Result<Vec<VertexId>> {
840        let mut res = VectorInt::new();
841        igraph_call!(igraph_to_prufer(self, &mut res))?;
842        Ok(res.into())
843    }
844}