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}