igraph/mixing.rs
1//! Clustering, degree correlations and graphicality: transitivity,
2//! assortativity, mixing matrices and degree-sequence realizability.
3//!
4//! This module binds three igraph headers:
5//!
6//! * `igraph_transitivity.h` — *how clustered is a graph?* The global
7//! transitivity (fraction of closed connected triples), the local clustering
8//! coefficient of Watts and Strogatz, its average, Barrat's weighted variant
9//! and the edge clustering coefficient of Radicchi *et al.*;
10//! * `igraph_mixing.h` — *who connects to whom?* Newman's assortativity
11//! coefficients (for numeric values, for categories and for degrees), the
12//! joint degree matrix, the joint degree distribution and the mixing matrix
13//! of vertex categories;
14//! * `igraph_graphicality.h` — *can a degree sequence be realized?* The
15//! Erdős–Gallai / Fulkerson–Chen–Anstee / Gale–Ryser tests, extended to
16//! graphs with self-loops and/or multi-edges.
17//!
18//! Functions that take a graph are methods of [`Graph`]; the graphicality tests
19//! work on plain degree slices and are free functions.
20//!
21//! | Rust | C function | Computes |
22//! |------|------------|----------|
23//! | [`Graph::transitivity_undirected`] | `igraph_transitivity_undirected` | global clustering coefficient |
24//! | [`Graph::transitivity_local_undirected`] | `igraph_transitivity_local_undirected` | local clustering coefficient of vertices |
25//! | [`Graph::transitivity_avglocal_undirected`] | `igraph_transitivity_avglocal_undirected` | average local clustering coefficient |
26//! | [`Graph::transitivity_barrat`] | `igraph_transitivity_barrat` | Barrat's weighted local clustering |
27//! | [`Graph::ecc`] | `igraph_ecc` | edge clustering coefficient (3- and 4-cycles) |
28//! | [`Graph::assortativity`] | `igraph_assortativity` | assortativity by numeric vertex values |
29//! | [`Graph::assortativity_nominal`] | `igraph_assortativity_nominal` | assortativity by vertex categories |
30//! | [`Graph::assortativity_degree`] | `igraph_assortativity_degree` | degree assortativity |
31//! | [`Graph::joint_degree_matrix`] | `igraph_joint_degree_matrix` | edge counts between degree classes |
32//! | [`Graph::joint_degree_distribution`] | `igraph_joint_degree_distribution` | joint degree distribution `P_ij` |
33//! | [`Graph::joint_type_distribution`] | `igraph_joint_type_distribution` | mixing matrix of vertex categories |
34//! | [`is_graphical`] | `igraph_is_graphical` | is a (bi-)degree sequence realizable? |
35//! | [`is_bigraphical`] | `igraph_is_bigraphical` | is a pair of sequences realizable as a bipartite graph? |
36//!
37//! Which kinds of edges the graphicality tests may use is described by
38//! [`AllowedEdgeTypes`] (defined in [`constants`](crate::constants) and shared
39//! with the [`games`](crate::games) and [`constructors`](crate::constructors)
40//! modules), which also converts from [`EdgeTypeSw`].
41//!
42//! All functions of this module are deterministic.
43//!
44//! # See also
45//!
46//! * Triangles themselves: [`Graph::count_triangles`],
47//! [`Graph::count_adjacent_triangles`] and [`Graph::list_triangles`]
48//! (the global transitivity is `3 × triangles / connected triples`), the
49//! [triad census](Graph::triad_census) and [motifs](Graph::motifs_randesu).
50//! * Degree correlations as functions of the degree: the average nearest
51//! neighbor degree [`Graph::avg_nearest_neighbor_degree`] and
52//! [`Graph::degree_correlation_vector`] (`k_nn(k)`, derivable from
53//! [`Graph::joint_degree_distribution`]); vertex [strengths](Graph::strength)
54//! for weighted degrees; the [rich-club coefficient](Graph::rich_club_sequence).
55//! * Categories: the unnormalized nominal assortativity is the
56//! [modularity](Graph::modularity) of the partition, and community detection
57//! (e.g. [`Graph::community_multilevel`]) finds partitions with high values.
58//! * Degree sequences: build a graph from a graphical sequence with
59//! [`Graph::realize_degree_sequence`] /
60//! [`Graph::realize_bipartite_degree_sequence`] (deterministic) or
61//! [`Graph::degree_sequence_game`] / [`Graph::k_regular_game`] (random);
62//! randomize a graph while keeping its degrees with [`Graph::rewire`], the
63//! usual null model against which clustering and assortativity are compared.
64//!
65//! # Example
66//!
67//! ```
68//! use igraph::mixing::{is_graphical, AllowedEdgeTypes};
69//! use igraph::prelude::*;
70//!
71//! // A "bow tie": two triangles sharing vertex 2.
72//! let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 2)], 5, false)?;
73//!
74//! // 2 triangles, 3 * 2 = 6 closed triples out of 10 connected triples.
75//! let t = g.transitivity_undirected(TransitivityMode::Nan)?;
76//! assert!((t - 0.6).abs() < 1e-12);
77//! assert_eq!(g.count_triangles()?, 2.0);
78//!
79//! // The hub closes 2 out of the 6 pairs of its neighbors, the others all theirs.
80//! let local = g.transitivity_local_undirected(.., TransitivityMode::Zero)?;
81//! assert!((local[2] - 1.0 / 3.0).abs() < 1e-12);
82//! assert_eq!(local[0], 1.0);
83//!
84//! // High-degree hub attached to low-degree vertices: disassortative.
85//! assert!(g.assortativity_degree(false)? < 0.0);
86//!
87//! // Its degree sequence is, of course, graphical.
88//! let degrees = g.degree(.., NeighborMode::All, Loops::Twice)?;
89//! assert!(is_graphical(°rees, None, AllowedEdgeTypes::SIMPLE)?);
90//! // ...while an odd degree sum never is.
91//! assert!(!is_graphical(&[3, 3, 3], None, AllowedEdgeTypes::ALL)?);
92//!
93//! // Zachary's karate club: clustered, and its hubs avoid each other.
94//! let karate = Graph::famous("Zachary")?;
95//! let c = karate.transitivity_undirected(TransitivityMode::Nan)?;
96//! assert!((c - 0.2556818).abs() < 1e-6);
97//! assert!((karate.assortativity_degree(false)? + 0.475613).abs() < 1e-6);
98//! # Ok::<(), igraph::Error>(())
99//! ```
100
101use crate::{
102 constants::*,
103 error::{Error, Result},
104 ffi::*,
105 graph::Graph,
106 igraph_call,
107 matrix::Matrix,
108 selector::{EdgeSelector, VertexSelector},
109 vector::{Vector, VectorInt},
110};
111
112/// Which kinds of edges a realization of a degree sequence may contain
113/// (`igraph_edge_type_sw_t`), used by [`is_graphical`] and [`is_bigraphical`].
114///
115/// Re-exported from [`constants`](crate::constants::AllowedEdgeTypes): the
116/// same type is also available as `games::AllowedEdgeTypes` and
117/// `constructors::AllowedEdgeTypes`, so a graphicality test and the generator
118/// realizing the sequence (e.g. [`Graph::realize_degree_sequence`]) can share
119/// the same value.
120pub use crate::constants::AllowedEdgeTypes;
121
122/// Options of [`Graph::joint_degree_distribution`].
123///
124/// The defaults describe the classical joint degree distribution: for each
125/// directed connection `u -> v`, the out-degree of `u` and the in-degree of
126/// `v`, normalized so that the entries sum to one, with automatically sized
127/// rows and columns.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct JointDegreeDistributionOptions {
130 /// How to compute the degree of source vertices ([`NeighborMode::Out`],
131 /// [`NeighborMode::In`] or [`NeighborMode::All`]). Ignored for undirected
132 /// graphs. Default: [`NeighborMode::Out`].
133 pub from_mode: NeighborMode,
134 /// How to compute the degree of target vertices. Ignored for undirected
135 /// graphs. Default: [`NeighborMode::In`].
136 pub to_mode: NeighborMode,
137 /// Whether to consider `u -> v` connections as directed. When `false`,
138 /// every edge is counted in both directions. Ignored for undirected
139 /// graphs. Default: `true`.
140 pub directed_neighbors: bool,
141 /// Whether to normalize the matrix so that its entries sum to 1. If
142 /// `false`, entries are connection counts (or total weights).
143 /// Default: `true`.
144 pub normalized: bool,
145 /// Largest source degree to consider (the result has one more row);
146 /// `None` uses the largest source degree in the graph. Default: `None`.
147 pub max_from_degree: Option<usize>,
148 /// Largest target degree to consider (the result has one more column);
149 /// `None` uses the largest target degree in the graph. Default: `None`.
150 pub max_to_degree: Option<usize>,
151}
152
153impl Default for JointDegreeDistributionOptions {
154 fn default() -> Self {
155 Self {
156 from_mode: NeighborMode::Out,
157 to_mode: NeighborMode::In,
158 directed_neighbors: true,
159 normalized: true,
160 max_from_degree: None,
161 max_to_degree: None,
162 }
163 }
164}
165
166impl JointDegreeDistributionOptions {
167 /// Sets how source and target degrees are computed.
168 pub fn with_modes(mut self, from_mode: NeighborMode, to_mode: NeighborMode) -> Self {
169 self.from_mode = from_mode;
170 self.to_mode = to_mode;
171 self
172 }
173
174 /// Sets [`directed_neighbors`](Self::directed_neighbors).
175 pub fn with_directed_neighbors(mut self, directed_neighbors: bool) -> Self {
176 self.directed_neighbors = directed_neighbors;
177 self
178 }
179
180 /// Sets [`normalized`](Self::normalized).
181 pub fn with_normalized(mut self, normalized: bool) -> Self {
182 self.normalized = normalized;
183 self
184 }
185
186 /// Sets the largest source and target degrees to consider.
187 pub fn with_max_degrees(mut self, max_from: Option<usize>, max_to: Option<usize>) -> Self {
188 self.max_from_degree = max_from;
189 self.max_to_degree = max_to;
190 self
191 }
192}
193
194/// Converts an optional limit to igraph's convention (negative = unlimited).
195///
196/// Limits that do not fit an `igraph_int_t` (or whose successor, the matrix
197/// dimension, would overflow) are rejected: a plain cast would turn them into
198/// negative numbers, silently meaning "unlimited", and `i64::MAX + 1` is
199/// signed overflow (undefined behavior) in the C code.
200fn limit(max: Option<usize>) -> Result<igraph_int_t> {
201 match max {
202 None => Ok(IGRAPH_UNLIMITED as igraph_int_t),
203 Some(m) if m < igraph_int_t::MAX as usize => Ok(m as igraph_int_t),
204 Some(m) => Err(Error::invalid(format!("degree limit {m} is too large"))),
205 }
206}
207
208/// Checks that vertex types are valid category indices: non-negative, and
209/// small enough for `type + 1` (a matrix dimension) not to overflow.
210///
211/// igraph 1.0.0 and 1.0.1 do not check the *target* types of
212/// `igraph_joint_type_distribution` (`mixing_matrix()` in `misc/mixing.c`
213/// validates `from_types` twice), so a negative target type would make it
214/// write before the start of the matrix: this check must run before every
215/// such call.
216fn check_types(what: &str, types: &[igraph_int_t]) -> Result<()> {
217 match types
218 .iter()
219 .find(|&&t| !(0..igraph_int_t::MAX).contains(&t))
220 {
221 Some(t) => Err(Error::invalid(format!(
222 "invalid {what} value {t}: vertex types must be non-negative (and less than i64::MAX)"
223 ))),
224 None => Ok(()),
225 }
226}
227
228/// Pointer to an optional real vector view, or null.
229fn opt_ptr<V>(v: &Option<crate::vector::View<'_, V>>) -> *const V {
230 v.as_ref().map_or(std::ptr::null(), |v| v.as_ptr())
231}
232
233/// Number of rows/columns of a mixing matrix: `max + 1` if given, otherwise
234/// one more than the largest type (0 when there are no vertices).
235fn dimension(max: Option<usize>, types: &[igraph_int_t]) -> usize {
236 match max {
237 Some(m) => m + 1,
238 None => types.iter().max().map_or(0, |&t| t.max(-1) as usize + 1),
239 }
240}
241
242impl Graph {
243 /// Rust implementation of igraph's (static) `mixing_matrix`, used for the
244 /// cases in which the C code of igraph 1.0.0 and 1.0.1 would write out of
245 /// bounds.
246 ///
247 /// Every edge `u -> v` adds its weight to entry
248 /// `(from_types[u], to_types[v])` and, unless `directed_neighbors`, the
249 /// reverse connection `v -> u` adds it to `(from_types[v], to_types[u])`;
250 /// connections falling outside the `dims` matrix are ignored. Types must be
251 /// non-negative and of the right lengths (checked by the callers).
252 fn mixing_matrix(
253 &self,
254 weights: Option<&[f64]>,
255 from_types: &[igraph_int_t],
256 to_types: &[igraph_int_t],
257 directed_neighbors: bool,
258 normalized: bool,
259 (nrow, ncol): (usize, usize),
260 ) -> Result<Matrix> {
261 // Allocate through igraph so that huge (user-given) dimensions give an
262 // error instead of aborting. Dimensions fit `igraph_int_t`: they are
263 // one more than a validated limit or a non-negative type/degree.
264 let mut p = Matrix::new();
265 igraph_call!(igraph_matrix_resize(
266 &mut p,
267 nrow as igraph_int_t,
268 ncol as igraph_int_t
269 ))?;
270 p.as_mut_slice().fill(0.0);
271 let mut sum = 0.0;
272 let mut add = |a: igraph_int_t, b: igraph_int_t, w: f64| {
273 let (a, b) = (a as usize, b as usize);
274 if a < nrow && b < ncol {
275 p[(a, b)] += w;
276 sum += w;
277 }
278 };
279 for (eid, (u, v)) in self.edge_list().into_iter().enumerate() {
280 let w = weights.map_or(1.0, |w| w[eid]);
281 add(from_types[u as usize], to_types[v as usize], w);
282 if !directed_neighbors {
283 add(from_types[v as usize], to_types[u as usize], w);
284 }
285 }
286 if normalized && self.ecount() > 0 {
287 p.as_mut_slice().iter_mut().for_each(|x| *x /= sum);
288 }
289 Ok(p)
290 }
291
292 fn mixing_check_weights(&self, weights: Option<&[f64]>) -> Result<()> {
293 match weights {
294 Some(w) if w.len() != self.ecount() => Err(Error::invalid(format!(
295 "weight vector length ({}) does not match the number of edges ({})",
296 w.len(),
297 self.ecount()
298 ))),
299 _ => Ok(()),
300 }
301 }
302
303 fn check_vertex_values<T>(&self, what: &str, values: &[T]) -> Result<()> {
304 if values.len() != self.vcount() {
305 return Err(Error::invalid(format!(
306 "{what} vector length ({}) does not match the number of vertices ({})",
307 values.len(),
308 self.vcount()
309 )));
310 }
311 Ok(())
312 }
313
314 // ------------------------------------------------------------------
315 // igraph_transitivity.h
316 // ------------------------------------------------------------------
317
318 /// Global transitivity (clustering coefficient) of the graph.
319 ///
320 /// The transitivity is the probability that two neighbors of a vertex are
321 /// connected; more precisely, it is the ratio between the number of
322 /// *closed* connected triples (three times the number of triangles) and
323 /// the number of connected triples. Edge directions and multiplicities are
324 /// ignored. This single number differs from the
325 /// [average local transitivity](Self::transitivity_avglocal_undirected),
326 /// which weights all vertices equally.
327 ///
328 /// `mode` says what to return for graphs without connected triples:
329 /// [`TransitivityMode::Nan`] gives `NaN`, [`TransitivityMode::Zero`] gives 0.
330 ///
331 /// Binds [`igraph_transitivity_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_transitivity_undirected).
332 /// Reference: S. Wasserman and K. Faust, *Social Network Analysis: Methods
333 /// and Applications*, Cambridge University Press (1994).
334 ///
335 /// See also [`Graph::count_triangles`]: for a simple graph with degrees
336 /// `d_v`, the transitivity is `3 T / Σ_v d_v (d_v - 1) / 2`.
337 ///
338 /// Time complexity: O(|V| d²), d being the average degree.
339 ///
340 /// # Examples
341 ///
342 /// ```
343 /// use igraph::prelude::*;
344 ///
345 /// // A triangle with a pendant edge: 3 closed triples out of 5.
346 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, false)?;
347 /// assert!((g.transitivity_undirected(TransitivityMode::Nan)? - 0.6).abs() < 1e-12);
348 ///
349 /// // A path has connected triples but no triangle; a single edge has neither.
350 /// let path = Graph::from_edges(&[(0, 1), (1, 2)], 3, false)?;
351 /// assert_eq!(path.transitivity_undirected(TransitivityMode::Nan)?, 0.0);
352 /// let edge = Graph::from_edges(&[(0, 1)], 2, false)?;
353 /// assert!(edge.transitivity_undirected(TransitivityMode::Nan)?.is_nan());
354 /// assert_eq!(edge.transitivity_undirected(TransitivityMode::Zero)?, 0.0);
355 /// # Ok::<(), igraph::Error>(())
356 /// ```
357 pub fn transitivity_undirected(&self, mode: TransitivityMode) -> Result<f64> {
358 let mut res = 0.0;
359 self.with_fresh_multi_cache(|| {
360 igraph_call!(igraph_transitivity_undirected(self, &mut res, mode.into()))
361 })?;
362 Ok(res)
363 }
364
365 /// Local transitivity (clustering coefficient) of the selected vertices.
366 ///
367 /// For each vertex, the fraction of pairs of its neighbors that are
368 /// themselves connected (Watts–Strogatz clustering coefficient). Edge
369 /// directions and multiplicities are ignored. Vertices with fewer than two
370 /// neighbors get `NaN` with [`TransitivityMode::Nan`] and 0 with
371 /// [`TransitivityMode::Zero`]. The result follows the order of `vids`.
372 ///
373 /// Binds [`igraph_transitivity_local_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_transitivity_local_undirected).
374 /// Reference: D. J. Watts and S. Strogatz, *Collective dynamics of
375 /// small-world networks*, Nature 393, 440–442 (1998).
376 ///
377 /// See also [`Graph::count_adjacent_triangles`]: in a simple graph the
378 /// local transitivity of `v` is `t_v / (d_v (d_v - 1) / 2)`, `t_v` being
379 /// the number of triangles through `v`.
380 ///
381 /// Time complexity: O(n d²), n being the number of selected vertices and d
382 /// the average degree.
383 ///
384 /// # Errors
385 ///
386 /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) if the
387 /// selector contains a non-existent vertex.
388 ///
389 /// # Examples
390 ///
391 /// ```
392 /// use igraph::prelude::*;
393 ///
394 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, false)?;
395 /// let c = g.transitivity_local_undirected(.., TransitivityMode::Zero)?;
396 /// assert_eq!(c[..2], [1.0, 1.0]);
397 /// assert!((c[2] - 1.0 / 3.0).abs() < 1e-12);
398 /// assert_eq!(c[3], 0.0); // a leaf
399 /// # Ok::<(), igraph::Error>(())
400 /// ```
401 pub fn transitivity_local_undirected<'a>(
402 &self,
403 vids: impl Into<VertexSelector<'a>>,
404 mode: TransitivityMode,
405 ) -> Result<Vec<f64>> {
406 let vs = vids.into().to_raw()?;
407 let mut res = Vector::new();
408 self.with_fresh_multi_cache(|| {
409 igraph_call!(igraph_transitivity_local_undirected(
410 self,
411 &mut res,
412 vs.get(),
413 mode.into()
414 ))
415 })?;
416 Ok(res.into())
417 }
418
419 /// Average local transitivity (average clustering coefficient).
420 ///
421 /// The mean of the [local transitivities](Self::transitivity_local_undirected)
422 /// of all vertices. Vertices with fewer than two neighbors are left out of
423 /// the average with [`TransitivityMode::Nan`] (the result is `NaN` if no
424 /// vertex has two neighbors), and counted as zero with
425 /// [`TransitivityMode::Zero`]. Edge directions and multiplicities are
426 /// ignored.
427 ///
428 /// Binds [`igraph_transitivity_avglocal_undirected`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_transitivity_avglocal_undirected).
429 /// Reference: D. J. Watts and S. Strogatz, *Collective dynamics of
430 /// small-world networks*, Nature 393, 440–442 (1998). A small-world graph
431 /// (e.g. [`Graph::watts_strogatz_game`] with a small rewiring probability)
432 /// has a much higher average clustering than an
433 /// [Erdős–Rényi graph](Graph::erdos_renyi_game_gnm) of the same density.
434 ///
435 /// Time complexity: O(|V| d²).
436 ///
437 /// # Examples
438 ///
439 /// ```
440 /// use igraph::prelude::*;
441 ///
442 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, false)?;
443 /// // Local values: 1, 1, 1/3 and (leaf) NaN or 0.
444 /// let skip = g.transitivity_avglocal_undirected(TransitivityMode::Nan)?;
445 /// let zero = g.transitivity_avglocal_undirected(TransitivityMode::Zero)?;
446 /// assert!((skip - 7.0 / 9.0).abs() < 1e-12);
447 /// assert!((zero - 7.0 / 12.0).abs() < 1e-12);
448 /// # Ok::<(), igraph::Error>(())
449 /// ```
450 pub fn transitivity_avglocal_undirected(&self, mode: TransitivityMode) -> Result<f64> {
451 let mut res = 0.0;
452 self.with_fresh_multi_cache(|| {
453 igraph_call!(igraph_transitivity_avglocal_undirected(
454 self,
455 &mut res,
456 mode.into()
457 ))
458 })?;
459 Ok(res)
460 }
461
462 /// Barrat's weighted local transitivity of the selected vertices.
463 ///
464 /// For a vertex `i`, every triangle `i`, `j`, `h` contributes the total
465 /// weight `w_ij + w_ih` of the two triangle edges incident on `i` (in
466 /// equation (5) each triangle appears twice, as the ordered pairs `(j, h)`
467 /// and `(h, j)`, with the mean weight `(w_ij + w_ih) / 2`); the sum is
468 /// divided by `s_i (k_i - 1)`, where `s_i` is the strength and `k_i` the
469 /// degree of `i` (equation (5) of A. Barrat, M. Barthélemy,
470 /// R. Pastor-Satorras and A. Vespignani, *The architecture of complex
471 /// weighted networks*, PNAS 101, 3747 (2004)). With equal weights it
472 /// coincides with the [unweighted local transitivity](Self::transitivity_local_undirected).
473 ///
474 /// Edge directions are ignored; the graph must not have multi-edges (for
475 /// directed graphs, not even mutual pairs `u -> v`, `v -> u`, which become
476 /// multi-edges once directions are ignored). If `weights`
477 /// is `None`, igraph emits a warning and falls back to the unweighted
478 /// local transitivity. When the denominator `s_i (k_i - 1)` is zero
479 /// (fewer than two incident edges, or zero strength),
480 /// [`TransitivityMode::Zero`] gives 0, while [`TransitivityMode::Nan`]
481 /// performs the division: `NaN` (`0 / 0`), or `±∞` if a zero strength
482 /// comes from weights of mixed signs around closed triangles.
483 ///
484 /// Binds [`igraph_transitivity_barrat`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_transitivity_barrat).
485 /// See also [`Graph::strength`] for the `s_i`.
486 ///
487 /// Time complexity: O(|V| d²).
488 ///
489 /// # Errors
490 ///
491 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
492 /// weight vector has the wrong length or the graph has multi-edges (or
493 /// mutual directed edges);
494 /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for
495 /// invalid vertices.
496 ///
497 /// # Examples
498 ///
499 /// ```
500 /// use igraph::prelude::*;
501 ///
502 /// // igraph's unit test graph: two triangles 0-1-2 and 1-2-3, a tail 3-4, isolated 5.
503 /// let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4)], 6, false)?;
504 /// let w = [-1.0, 0.0, 1.0, 2.0, 3.0, 4.0];
505 /// let t = g.transitivity_barrat(.., Some(&w), TransitivityMode::Zero)?;
506 /// let expected = [1.0, 0.75, 0.625, 0.277778, 0.0, 0.0];
507 /// for (a, b) in t.iter().zip(expected) {
508 /// assert!((a - b).abs() < 1e-6);
509 /// }
510 /// # Ok::<(), igraph::Error>(())
511 /// ```
512 pub fn transitivity_barrat<'a>(
513 &self,
514 vids: impl Into<VertexSelector<'a>>,
515 weights: Option<&[f64]>,
516 mode: TransitivityMode,
517 ) -> Result<Vec<f64>> {
518 self.mixing_check_weights(weights)?;
519 let vs = vids.into().to_raw()?;
520 let w = weights.map(Vector::view);
521 let mut res = Vector::new();
522 self.with_fresh_multi_cache(|| {
523 igraph_call!(igraph_transitivity_barrat(
524 self,
525 &mut res,
526 vs.get(),
527 opt_ptr(&w),
528 mode.into()
529 ))
530 })?;
531 Ok(res.into())
532 }
533
534 /// Edge clustering coefficient of the selected edges.
535 ///
536 /// For an edge `(i, j)`, let `z` be the number of `k`-cycles it belongs
537 /// to and `s` the largest such number compatible with the degrees of its
538 /// endpoints: `s = min(d_i - 1, d_j - 1)` for `k = 3` and
539 /// `s = (d_i - 1)(d_j - 1)` for `k = 4`. The coefficient is
540 ///
541 /// ```text
542 /// C = (z + offset) / s (normalize = true)
543 /// C = z + offset (normalize = false)
544 /// ```
545 ///
546 /// where `offset` is 1 if `offset` is `true` and 0 otherwise. The original
547 /// definition of Radicchi *et al.* (PNAS 101, 2658 (2004)) uses
548 /// `offset = true, normalize = true`; with `offset = false` the normalized
549 /// value is at most 1, which for `k = 3` is achieved by every edge of a
550 /// complete graph. When normalizing, edges with `s = 0` (an endpoint of
551 /// degree 1, or a self-loop, which igraph assigns `z = s = 0`) get `NaN`
552 /// without offset (`0 / 0`) and `+∞` with it (`1 / 0`). Multiplicities
553 /// are ignored when listing cycles but not in the degrees. The result
554 /// follows the order of `eids`.
555 ///
556 /// Only `k = 3` and `k = 4` are currently supported.
557 ///
558 /// Binds [`igraph_ecc`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_ecc).
559 /// See also [`Graph::list_triangles`] (for `k = 3`, the unnormalized,
560 /// offset-free coefficient of an edge is the number of listed triangles
561 /// containing it) and [`Graph::community_edge_betweenness`], the other
562 /// classic edge-removal criterion for divisive community detection.
563 ///
564 /// Time complexity: O(|V| d log d + |E| d) for `k = 3`,
565 /// O(|V| d log d + |E| d²) for `k = 4`.
566 ///
567 /// # Errors
568 ///
569 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `k < 3`;
570 /// [`ErrorKind::Unimplemented`](crate::ErrorKind::Unimplemented) if `k > 4`;
571 /// [`ErrorKind::InvalidEdgeId`](crate::ErrorKind::InvalidEdgeId) for invalid edges.
572 ///
573 /// # Examples
574 ///
575 /// ```
576 /// use igraph::prelude::*;
577 ///
578 /// // In K4 each edge is in 2 triangles, the most its degree-3 endpoints allow.
579 /// let k4 = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], 4, false)?;
580 /// assert!(k4.ecc(.., 3, false, true)?.iter().all(|&c| c == 1.0));
581 /// assert_eq!(k4.ecc(0, 3, true, false)?, vec![3.0]); // 2 triangles, plus one
582 /// # Ok::<(), igraph::Error>(())
583 /// ```
584 pub fn ecc<'a>(
585 &self,
586 eids: impl Into<EdgeSelector<'a>>,
587 k: usize,
588 offset: bool,
589 normalize: bool,
590 ) -> Result<Vec<f64>> {
591 let es = eids.into().to_raw()?;
592 let mut res = Vector::new();
593 self.with_fresh_multi_cache(|| {
594 igraph_call!(igraph_ecc(
595 self,
596 &mut res,
597 es.get(),
598 k as igraph_int_t,
599 offset,
600 normalize
601 ))
602 })?;
603 Ok(res.into())
604 }
605
606 // ------------------------------------------------------------------
607 // igraph_mixing.h
608 // ------------------------------------------------------------------
609
610 /// Assortativity coefficient based on numeric vertex values.
611 ///
612 /// With `normalized = true` this is the Pearson correlation of the values
613 /// `x` found at the two ends of the edges (Newman's assortativity
614 /// coefficient, in `[-1, 1]`); with `normalized = false` it is the
615 /// covariance
616 ///
617 /// ```text
618 /// cov(x_out, x_in) = 1/m Σ_ij (A_ij - k_i^out k_j^in / m) x_i x_j
619 /// ```
620 ///
621 /// For directed graphs (with `directed = true`) the value of the edge
622 /// source is taken from `values` and the one of the target from
623 /// `values_in`, if given (otherwise from `values` as well). Undirected
624 /// graphs (and directed ones with `directed = false`) are treated as
625 /// directed graphs with every edge reciprocated, so self-loops count
626 /// twice; in that case `values_in` is ignored, with a warning if given.
627 /// `directed` is ignored for undirected graphs.
628 ///
629 /// When `weights` are given they act as edge multiplicities: `m` becomes
630 /// the total weight and degrees become strengths.
631 ///
632 /// Binds [`igraph_assortativity`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_assortativity).
633 /// See also [`Graph::assortativity_degree`] (values = degrees) and
634 /// [`Graph::strength`] (weighted degrees as values).
635 /// References: M. E. J. Newman, *Mixing patterns in networks*, Phys. Rev.
636 /// E 67, 026126 (2003); *Assortative mixing in networks*, Phys. Rev. Lett.
637 /// 89, 208701 (2002).
638 ///
639 /// Time complexity: O(|E|).
640 ///
641 /// # Errors
642 ///
643 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if a value
644 /// or weight vector has the wrong length.
645 ///
646 /// # Examples
647 ///
648 /// ```
649 /// use igraph::prelude::*;
650 ///
651 /// // A path 0-1-2-3 with values increasing along it: neighbors are alike.
652 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false)?;
653 /// let r = g.assortativity(None, &[1.0, 2.0, 3.0, 4.0], None, false, true)?;
654 /// assert!(r > 0.0);
655 /// // Alternating values: every edge joins a "low" and a "high" vertex.
656 /// let r = g.assortativity(None, &[0.0, 1.0, 0.0, 1.0], None, false, true)?;
657 /// assert!((r + 1.0).abs() < 1e-12);
658 /// # Ok::<(), igraph::Error>(())
659 /// ```
660 pub fn assortativity(
661 &self,
662 weights: Option<&[f64]>,
663 values: &[f64],
664 values_in: Option<&[f64]>,
665 directed: bool,
666 normalized: bool,
667 ) -> Result<f64> {
668 self.mixing_check_weights(weights)?;
669 self.check_vertex_values("values", values)?;
670 if let Some(vi) = values_in {
671 self.check_vertex_values("values_in", vi)?;
672 }
673 let w = weights.map(Vector::view);
674 let v = Vector::view(values);
675 let vi = values_in.map(Vector::view);
676 let mut res = 0.0;
677 igraph_call!(igraph_assortativity(
678 self,
679 opt_ptr(&w),
680 v.as_ptr(),
681 opt_ptr(&vi),
682 &mut res,
683 directed,
684 normalized
685 ))?;
686 Ok(res)
687 }
688
689 /// Assortativity coefficient based on vertex categories.
690 ///
691 /// `types[v]` is the (non-negative integer) category of vertex `v`. The
692 /// normalized coefficient (`normalized = true`, the usual choice) is 1 when
693 /// all edges stay within categories, -1 for a perfectly disassortative
694 /// network, and asymptotically 0 for random connections. The unnormalized
695 /// version equals the [modularity](Graph::modularity) of the partition
696 /// into categories (with resolution 1):
697 ///
698 /// ```text
699 /// Q = 1/m Σ_ij (A_ij - k_i^out k_j^in / m) δ(t_i, t_j)
700 /// ```
701 ///
702 /// and the normalized one is `Q` divided by its largest possible value
703 /// `1 - 1/m² Σ_ij k_i^out k_j^in δ(t_i, t_j)`, i.e. `Q / (1 - Σ_t a_t b_t)`
704 /// with `a_t` (`b_t`) the fraction of edges starting (ending) in category
705 /// `t`. (The C documentation of 1.0.1 writes this denominator as
706 /// `1/m Σ_ij (m - k_i^out k_j^in δ(t_i, t_j) / m)`, which is not what the
707 /// code computes.)
708 ///
709 /// `directed` says whether to consider edge directions (ignored for
710 /// undirected graphs, which are treated as directed graphs with reciprocal
711 /// edges, so self-loops count twice). The null graph gives `NaN`.
712 ///
713 /// Weighted nominal assortativity is not implemented by igraph (1.0.0 and
714 /// 1.0.1 fail with `IGRAPH_UNIMPLEMENTED` when weights are given), so this
715 /// wrapper takes no weights; use [`Graph::modularity`] for the weighted
716 /// unnormalized value.
717 ///
718 /// Binds [`igraph_assortativity_nominal`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_assortativity_nominal).
719 /// Reference: M. E. J. Newman, *Mixing patterns in networks*, Phys. Rev. E
720 /// 67, 026126 (2003).
721 ///
722 /// Time complexity: O(|E| + t), t being the number of categories.
723 ///
724 /// See also [`Graph::joint_type_distribution`], the full mixing matrix of
725 /// the categories.
726 ///
727 /// # Errors
728 ///
729 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `types`
730 /// does not have one entry per vertex or contains negative values.
731 ///
732 /// # Examples
733 ///
734 /// ```
735 /// use igraph::prelude::*;
736 ///
737 /// // Two triangles joined by one bridge; categories = triangles.
738 /// let g = Graph::from_edges(
739 /// &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)], 6, false)?;
740 /// let r = g.assortativity_nominal(&[0, 0, 0, 1, 1, 1], false, true)?;
741 /// assert!(r > 0.7);
742 /// // A bipartite labeling of a bipartite graph is perfectly disassortative.
743 /// let square = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], 4, false)?;
744 /// let r = square.assortativity_nominal(&[0, 1, 0, 1], false, true)?;
745 /// assert!((r + 1.0).abs() < 1e-12);
746 /// # Ok::<(), igraph::Error>(())
747 /// ```
748 pub fn assortativity_nominal(
749 &self,
750 types: &[igraph_int_t],
751 directed: bool,
752 normalized: bool,
753 ) -> Result<f64> {
754 self.check_vertex_values("types", types)?;
755 check_types("types", types)?;
756 let t = VectorInt::view(types);
757 let mut res = 0.0;
758 igraph_call!(igraph_assortativity_nominal(
759 self,
760 std::ptr::null(),
761 t.as_ptr(),
762 &mut res,
763 directed,
764 normalized
765 ))?;
766 Ok(res)
767 }
768
769 /// Degree assortativity: do high-degree vertices link to each other?
770 ///
771 /// The [assortativity](Self::assortativity) coefficient with the vertex
772 /// degrees as values, normalized (Pearson correlation of the degrees at
773 /// the two ends of the edges). With `directed = true` on a directed graph,
774 /// out-degrees are used for edge sources and in-degrees for edge targets;
775 /// otherwise total degrees are used. Social networks tend to be
776 /// assortative (> 0), technological and biological ones disassortative
777 /// (< 0). For regular graphs the correlation is undefined and the result
778 /// is `NaN`.
779 ///
780 /// Binds [`igraph_assortativity_degree`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_assortativity_degree).
781 /// Loops count twice in the degrees and multi-edges are counted with
782 /// their multiplicity; the unnormalized covariance can be obtained with
783 /// [`Graph::assortativity`] and [`Graph::strength`] values.
784 ///
785 /// See also [`Graph::avg_nearest_neighbor_degree`] and
786 /// [`Graph::degree_correlation_vector`], which show the degree correlation
787 /// as a function of the degree instead of summarizing it in one number.
788 ///
789 /// Time complexity: O(|E| + |V|).
790 ///
791 /// # Examples
792 ///
793 /// ```
794 /// use igraph::prelude::*;
795 ///
796 /// // In a star, the hub is only linked to leaves: perfectly disassortative.
797 /// let star = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], 5, false)?;
798 /// assert!((star.assortativity_degree(false)? + 1.0).abs() < 1e-12);
799 /// // A cycle is regular: the coefficient is undefined.
800 /// let c = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, false)?;
801 /// assert!(c.assortativity_degree(false)?.is_nan());
802 /// # Ok::<(), igraph::Error>(())
803 /// ```
804 pub fn assortativity_degree(&self, directed: bool) -> Result<f64> {
805 let mut res = 0.0;
806 igraph_call!(igraph_assortativity_degree(self, &mut res, directed))?;
807 Ok(res)
808 }
809
810 /// Joint degree matrix: number (or total weight) of edges between degree classes.
811 ///
812 /// Entry `(i - 1, j - 1)` of the result holds `J_ij`, the number of edges
813 /// (or the total weight, if `weights` are given) between vertices of
814 /// (out-)degree `i` and vertices of (in-)degree `j`. Each edge, self-loops
815 /// included, is counted exactly once: for ordered degree pairs `(i, j)` in
816 /// directed graphs, whose entries then sum to the number of edges `m` (or
817 /// total weight), and for unordered pairs in undirected graphs, whose
818 /// matrix is symmetric and whose upper triangle (diagonal included) sums
819 /// to `m` (without limits; with limits, only the part that fits).
820 /// `J_ij / m` is the probability that a random edge joins degrees `i` and
821 /// `j`.
822 ///
823 /// `max_out_degree` / `max_in_degree` set the number of rows / columns;
824 /// `None` uses the largest (out-/in-)degree of the graph. Edges whose
825 /// degree pair falls outside the matrix are not counted. Unlike
826 /// [`joint_degree_distribution`](Self::joint_degree_distribution), there
827 /// is no row or column for degree zero, and undirected same-degree
828 /// connections are counted once instead of twice.
829 ///
830 /// Binds [`igraph_joint_degree_matrix`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_joint_degree_matrix).
831 /// It is a finer description of a network than its degree sequence:
832 /// degree-preserving [rewiring](Graph::rewire) keeps the degrees but in
833 /// general changes this matrix. See also
834 /// [`joint_degree_distribution`](Self::joint_degree_distribution).
835 /// Reference: I. Stanton and A. Pinar, *Constructing and sampling graphs
836 /// with a prescribed joint degree distribution*, ACM J. Exp. Algorithmics
837 /// 17, 3.5 (2012).
838 ///
839 /// Time complexity: O(|E|).
840 ///
841 /// # Errors
842 ///
843 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
844 /// weight vector has the wrong length or a limit does not fit an `i64`.
845 ///
846 /// # Examples
847 ///
848 /// ```
849 /// use igraph::prelude::*;
850 ///
851 /// // A star with 3 leaves: 3 edges between degree 3 and degree 1.
852 /// let star = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], 4, false)?;
853 /// let j = star.joint_degree_matrix(None, None, None)?;
854 /// assert_eq!(j.to_rows(), vec![
855 /// vec![0.0, 0.0, 3.0],
856 /// vec![0.0, 0.0, 0.0],
857 /// vec![3.0, 0.0, 0.0],
858 /// ]);
859 /// # Ok::<(), igraph::Error>(())
860 /// ```
861 pub fn joint_degree_matrix(
862 &self,
863 weights: Option<&[f64]>,
864 max_out_degree: Option<usize>,
865 max_in_degree: Option<usize>,
866 ) -> Result<Matrix> {
867 self.mixing_check_weights(weights)?;
868 let (max_out, max_in) = (limit(max_out_degree)?, limit(max_in_degree)?);
869 let w = weights.map(Vector::view);
870 let mut jdm = Matrix::new();
871 igraph_call!(igraph_joint_degree_matrix(
872 self,
873 opt_ptr(&w),
874 &mut jdm,
875 max_out,
876 max_in
877 ))?;
878 Ok(jdm)
879 }
880
881 /// Joint degree distribution `P_ij` of connected vertex pairs.
882 ///
883 /// Entry `(i, j)` is the probability that a randomly chosen *ordered* pair
884 /// of connected vertices `u -> v` has degrees `i` (for `u`, computed with
885 /// [`from_mode`](JointDegreeDistributionOptions::from_mode)) and `j` (for
886 /// `v`, computed with [`to_mode`](JointDegreeDistributionOptions::to_mode)).
887 /// An undirected graph behaves like the directed graph with all edges
888 /// reciprocated. Without normalization the entries are connection counts
889 /// (or total weights): without degree limits they sum to the number of
890 /// edges of a directed graph (twice that with `directed_neighbors =
891 /// false`) and to twice that of an undirected one. Rows and columns for
892 /// degree 0 are included.
893 ///
894 /// Related quantities: the degree correlation function is
895 /// `k_nn(k) = Σ_j j P_kj / Σ_j P_kj` and the unnormalized degree
896 /// assortativity is `Σ_ij i j (P_ij - q_i r_j)` with `q` and `r` the row
897 /// and column sums. Compare with [`joint_degree_matrix`](Self::joint_degree_matrix),
898 /// whose undirected diagonal is half of the unnormalized `P_ii`.
899 ///
900 /// When connections are counted in both directions (undirected graphs,
901 /// or `directed_neighbors = false`), each reverse connection `v -> u`
902 /// contributes to entry `(deg_from(v), deg_to(u))` if it falls within
903 /// the matrix, and normalization divides by the total weight of the
904 /// connections that fall within it. In the cases where igraph 1.0.0 and
905 /// 1.0.1 would index out of bounds (non-square limits, or
906 /// `from_mode != to_mode` without `directed_neighbors`), this wrapper
907 /// computes the matrix in Rust with exactly these semantics.
908 ///
909 /// Binds [`igraph_joint_degree_distribution`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_joint_degree_distribution).
910 /// See also [`Graph::degree_correlation_vector`], which computes `k_nn(k)`
911 /// directly, and [`Graph::assortativity`] with degree values.
912 ///
913 /// Time complexity: O(|E|).
914 ///
915 /// # Errors
916 ///
917 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
918 /// weight vector has the wrong length or a degree limit is `i64::MAX` or
919 /// more.
920 ///
921 /// # Examples
922 ///
923 /// ```
924 /// use igraph::mixing::JointDegreeDistributionOptions;
925 /// use igraph::prelude::*;
926 ///
927 /// let star = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], 4, false)?;
928 /// let p = star.joint_degree_distribution(None, &JointDegreeDistributionOptions::default())?;
929 /// assert_eq!(p.shape(), (4, 4));
930 /// // Half of the ordered pairs go hub -> leaf, half leaf -> hub.
931 /// assert_eq!(p[(3, 1)], 0.5);
932 /// assert_eq!(p[(1, 3)], 0.5);
933 /// # Ok::<(), igraph::Error>(())
934 /// ```
935 pub fn joint_degree_distribution(
936 &self,
937 weights: Option<&[f64]>,
938 options: &JointDegreeDistributionOptions,
939 ) -> Result<Matrix> {
940 self.mixing_check_weights(weights)?;
941 let max_from = limit(options.max_from_degree)?;
942 let max_to = limit(options.max_to_degree)?;
943 let directed = self.is_directed();
944 if !(directed && options.directed_neighbors) {
945 // igraph 1.0.0 and 1.0.1 write the reverse entry of each connection without
946 // bounds checking: unless the matrix is square and the source and
947 // target degrees coincide, it would write out of bounds (or into
948 // the wrong cell). Compute such cases on the Rust side.
949 let (from_mode, to_mode) = if directed {
950 (options.from_mode, options.to_mode)
951 } else {
952 (NeighborMode::All, NeighborMode::All)
953 };
954 let deg_from = self.degree(.., from_mode, Loops::Twice)?;
955 let deg_to = if to_mode == from_mode {
956 deg_from.clone()
957 } else {
958 self.degree(.., to_mode, Loops::Twice)?
959 };
960 let nrow = dimension(options.max_from_degree, °_from);
961 let ncol = dimension(options.max_to_degree, °_to);
962 if from_mode != to_mode || nrow != ncol {
963 return self.mixing_matrix(
964 weights,
965 °_from,
966 °_to,
967 false,
968 options.normalized,
969 (nrow, ncol),
970 );
971 }
972 }
973 let w = weights.map(Vector::view);
974 let mut p = Matrix::new();
975 igraph_call!(igraph_joint_degree_distribution(
976 self,
977 opt_ptr(&w),
978 &mut p,
979 options.from_mode.into(),
980 options.to_mode.into(),
981 options.directed_neighbors,
982 options.normalized,
983 max_from,
984 max_to
985 ))?;
986 Ok(p)
987 }
988
989 /// Mixing matrix of vertex categories.
990 ///
991 /// Entry `(i, j)` is proportional to the probability that a randomly
992 /// chosen ordered pair of connected vertices `u -> v` has `from_types[u] = i`
993 /// and `to_types[v] = j` (`to_types = None` reuses `from_types`). Types
994 /// must be non-negative integers; the matrix has one more row/column than
995 /// the largest source/target type, so re-index sparse labels first.
996 /// Undirected graphs (or `directed = false`) count each edge in both
997 /// directions. With `normalized = true` the entries sum to 1; otherwise
998 /// they are connection counts (or total weights). When connections are
999 /// counted in both directions, the reverse connection `v -> u` of an edge
1000 /// contributes to `(from_types[v], to_types[u])`; with distinct
1001 /// `to_types` this case is computed in Rust, because igraph 1.0.0 and
1002 /// 1.0.1 would index out of bounds.
1003 ///
1004 /// With a single normalized categorization `M`, row sums `a` and column
1005 /// sums `b`, the [modularity](Graph::modularity) of the partition is
1006 /// `Q = Σ_i M_ii - Σ_i a_i b_i` and the
1007 /// [nominal assortativity](Self::assortativity_nominal) is
1008 /// `Q / (1 - Σ_i a_i b_i)`.
1009 ///
1010 /// Binds [`igraph_joint_type_distribution`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_joint_type_distribution).
1011 ///
1012 /// Time complexity: O(|E|).
1013 ///
1014 /// # Errors
1015 ///
1016 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
1017 /// weight vector or a type vector has the wrong length, or a type vector
1018 /// contains negative values (checked on the Rust side for `to_types` too,
1019 /// which igraph 1.0.0 and 1.0.1 themselves forget to validate).
1020 ///
1021 /// # Examples
1022 ///
1023 /// ```
1024 /// use igraph::prelude::*;
1025 ///
1026 /// // igraph's unit test: a small undirected multigraph with loops and 3 types.
1027 /// let g = Graph::from_flat_edges(
1028 /// &[3, 0, 0, 3, 0, 2, 3, 1, 5, 5, 4, 2, 1, 1, 1, 1, 0, 1, 5, 1], 6, false)?;
1029 /// let m = g.joint_type_distribution(None, &[0, 0, 1, 1, 2, 2], None, false, false)?;
1030 /// assert_eq!(m.to_rows(), vec![
1031 /// vec![6.0, 4.0, 1.0],
1032 /// vec![4.0, 0.0, 1.0],
1033 /// vec![1.0, 1.0, 2.0],
1034 /// ]);
1035 /// # Ok::<(), igraph::Error>(())
1036 /// ```
1037 pub fn joint_type_distribution(
1038 &self,
1039 weights: Option<&[f64]>,
1040 from_types: &[igraph_int_t],
1041 to_types: Option<&[igraph_int_t]>,
1042 directed: bool,
1043 normalized: bool,
1044 ) -> Result<Matrix> {
1045 self.mixing_check_weights(weights)?;
1046 self.check_vertex_values("from_types", from_types)?;
1047 check_types("from_types", from_types)?;
1048 if let Some(tt) = to_types {
1049 self.check_vertex_values("to_types", tt)?;
1050 // Mandatory for soundness: igraph 1.0.0 and 1.0.1 never check target types.
1051 check_types("to_types", tt)?;
1052 }
1053 if let Some(tt) = to_types
1054 && tt != from_types
1055 && !(directed && self.is_directed())
1056 {
1057 // Distinct source and target types with reciprocal counting: igraph
1058 // 1.0.0 and 1.0.1 would write the reverse entries out of bounds, see
1059 // `joint_degree_distribution`. Compute it on the Rust side.
1060 let dims = (dimension(None, from_types), dimension(None, tt));
1061 return self.mixing_matrix(weights, from_types, tt, false, normalized, dims);
1062 }
1063 let w = weights.map(Vector::view);
1064 let ft = VectorInt::view(from_types);
1065 let tt = to_types.map(VectorInt::view);
1066 let mut p = Matrix::new();
1067 igraph_call!(igraph_joint_type_distribution(
1068 self,
1069 opt_ptr(&w),
1070 &mut p,
1071 ft.as_ptr(),
1072 opt_ptr(&tt),
1073 directed,
1074 normalized
1075 ))?;
1076 Ok(p)
1077 }
1078}
1079
1080// ----------------------------------------------------------------------
1081// igraph_graphicality.h
1082// ----------------------------------------------------------------------
1083
1084/// Pre-screens degree sequences before handing them to the graphicality
1085/// tests of igraph 1.0.0 and 1.0.1 (`src/misc/graphicality.c`), which add up
1086/// degrees in `igraph_int_t` without overflow checks (`dsum += d`,
1087/// `2*dmax`, `sumdiff += din - dout`, `sum1 += d`, even the parity update
1088/// `sum_parity + d`): signed overflow is undefined behavior in C, so huge
1089/// degrees must never reach them.
1090///
1091/// Returns `Some(false)` if an entry is negative (every igraph test answers
1092/// "not graphical" for those, but may overflow on earlier entries before
1093/// seeing the negative one), an error if the total exceeds `i64::MAX / 2`
1094/// (which keeps every intermediate value of the C code in range), and `None`
1095/// if igraph can be called.
1096fn screen_degrees(seqs: &[&[igraph_int_t]]) -> Result<Option<bool>> {
1097 let mut total: i128 = 0;
1098 for &d in seqs.iter().flat_map(|s| s.iter()) {
1099 if d < 0 {
1100 return Ok(Some(false));
1101 }
1102 total += i128::from(d);
1103 }
1104 if total > i128::from(igraph_int_t::MAX / 2) {
1105 return Err(Error::invalid(format!(
1106 "degree sum {total} is too large (more than i64::MAX / 2)"
1107 )));
1108 }
1109 Ok(None)
1110}
1111
1112/// Is there a graph with the given degree sequence?
1113///
1114/// For an undirected graph pass the degrees as `out_degrees` and
1115/// `in_degrees = None`; for a directed graph pass both the out- and the
1116/// in-degree sequences (of equal length). `allowed` says which edges the
1117/// realization may use (anything convertible into [`AllowedEdgeTypes`], e.g.
1118/// [`EdgeTypeSw::Simple`] or `AllowedEdgeTypes::ALL`). Sequences with negative
1119/// entries are simply not graphical.
1120///
1121/// The tests used are, for undirected graphs: the Erdős–Gallai conditions
1122/// (Cloteaux's linear-time algorithm) for simple graphs; an even degree sum
1123/// when loops and multi-edges are allowed; additionally a degree sum at least
1124/// twice the maximum degree for loopless multigraphs; Cairns–Mendan's modified
1125/// Erdős–Gallai conditions for at most one self-loop per vertex. For directed
1126/// graphs: the Fulkerson–Chen–Anstee theorem with Berger's relaxation for
1127/// simple digraphs; equal in- and out-degree sums with loops and multi-edges;
1128/// additionally an out-degree sum at least the maximum total degree for
1129/// loopless multigraphs; the Gale–Ryser theorem when single self-loops are
1130/// allowed.
1131///
1132/// Binds [`igraph_is_graphical`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_is_graphical).
1133/// See also [`Graph::realize_degree_sequence`], which builds a realization
1134/// of a graphical sequence (for the edge types it implements: simple, loopless
1135/// multi- and loopy multigraphs when undirected, simple digraphs when
1136/// directed, it succeeds exactly when this test says yes; single self-loops
1137/// without multi-edges, and non-simple digraphs, fail with
1138/// [`ErrorKind::Unimplemented`](crate::ErrorKind::Unimplemented)), and
1139/// [`Graph::degree_sequence_game`], which samples one at random.
1140///
1141/// Time complexity: O(n), n being the length of the sequence(s).
1142///
1143/// # Errors
1144///
1145/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the out- and
1146/// in-degree sequences have different lengths, or if the sum of the
1147/// (non-negative) degrees exceeds `i64::MAX / 2`: igraph would add them up
1148/// with signed overflow, which is undefined behavior in C, so such
1149/// sequences are rejected on the Rust side.
1150///
1151/// # Examples
1152///
1153/// ```
1154/// use igraph::mixing::{is_graphical, AllowedEdgeTypes};
1155/// use igraph::prelude::*;
1156///
1157/// // (3, 3): two vertices of degree 3 need a triple edge or loops.
1158/// assert!(!is_graphical(&[3, 3], None, EdgeTypeSw::Simple)?);
1159/// assert!(is_graphical(&[3, 3], None, EdgeTypeSw::Multi)?);
1160/// assert!(is_graphical(&[3, 3], None, EdgeTypeSw::Loops)?);
1161/// // (1, 2, 5) needs multi-edges *and* loops.
1162/// assert!(!is_graphical(&[1, 2, 5], None, EdgeTypeSw::Multi)?);
1163/// assert!(is_graphical(&[1, 2, 5], None, AllowedEdgeTypes::ALL)?);
1164/// // Directed: a 3-cycle has out- and in-degrees all equal to 1.
1165/// assert!(is_graphical(&[1, 1, 1], Some(&[1, 1, 1]), EdgeTypeSw::Simple)?);
1166/// assert!(!is_graphical(&[2, 0], Some(&[0, 2]), EdgeTypeSw::Simple)?);
1167///
1168/// // A graphical sequence can be realized with the same edge types.
1169/// let seq = [3, 3, 2, 2, 2, 1, 1];
1170/// assert!(is_graphical(&seq, None, AllowedEdgeTypes::SIMPLE)?);
1171/// let g = Graph::realize_degree_sequence(
1172/// &seq, None, AllowedEdgeTypes::SIMPLE, RealizeDegseq::Smallest)?;
1173/// assert_eq!(g.degree(.., NeighborMode::All, Loops::Twice)?, seq);
1174/// # Ok::<(), igraph::Error>(())
1175/// ```
1176pub fn is_graphical(
1177 out_degrees: &[igraph_int_t],
1178 in_degrees: Option<&[igraph_int_t]>,
1179 allowed: impl Into<AllowedEdgeTypes>,
1180) -> Result<bool> {
1181 // With sequences of different lengths igraph errors out before reading
1182 // any degree; otherwise pre-screen the values (see `screen_degrees`).
1183 if in_degrees.is_none_or(|inn| inn.len() == out_degrees.len())
1184 && let Some(answer) = screen_degrees(&[out_degrees, in_degrees.unwrap_or(&[])])?
1185 {
1186 return Ok(answer);
1187 }
1188 let out = VectorInt::view(out_degrees);
1189 let inn = in_degrees.map(VectorInt::view);
1190 let mut res = false;
1191 igraph_call!(igraph_is_graphical(
1192 out.as_ptr(),
1193 opt_ptr(&inn),
1194 allowed.into().to_raw(),
1195 &mut res
1196 ))?;
1197 Ok(res)
1198}
1199
1200/// Is there a bipartite graph with the given pair of degree sequences?
1201///
1202/// `degrees1` and `degrees2` are the degrees of the vertices in the two
1203/// partitions. When multi-edges are allowed it suffices that both sequences
1204/// have the same sum (and no negative entry); for simple graphs the Gale–Ryser
1205/// theorem is used with Berger's relaxation. Self-loops are meaningless in
1206/// bipartite graphs, so only [`AllowedEdgeTypes::SIMPLE`] and
1207/// [`AllowedEdgeTypes::MULTI`] matter (the `loops` flag is ignored).
1208///
1209/// Binds [`igraph_is_bigraphical`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_is_bigraphical).
1210/// See also [`Graph::realize_bipartite_degree_sequence`], which builds a
1211/// realization, and [`Graph::is_bipartite`].
1212///
1213/// Time complexity: O(n), n being the length of the longer sequence.
1214///
1215/// # Errors
1216///
1217/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the sum of
1218/// the (non-negative) degrees exceeds `i64::MAX / 2` (see [`is_graphical`]).
1219///
1220/// # Examples
1221///
1222/// ```
1223/// use igraph::mixing::is_bigraphical;
1224/// use igraph::prelude::*;
1225///
1226/// // K_{2,3}: two vertices of degree 3 and three of degree 2.
1227/// assert!(is_bigraphical(&[3, 3], &[2, 2, 2], EdgeTypeSw::Simple)?);
1228/// // One vertex cannot have 4 distinct neighbors among 3.
1229/// assert!(!is_bigraphical(&[4], &[2, 1, 1], EdgeTypeSw::Simple)?);
1230/// assert!(is_bigraphical(&[4], &[2, 1, 1], EdgeTypeSw::Multi)?);
1231///
1232/// // Realize K_{2,3}'s sequences: the result is the complete bipartite graph.
1233/// let g = Graph::realize_bipartite_degree_sequence(
1234/// &[3, 3], &[2, 2, 2], EdgeTypeSw::Simple, RealizeDegseq::Smallest)?;
1235/// assert_eq!((g.vcount(), g.ecount()), (5, 6));
1236/// assert!(g.is_bipartite()?);
1237/// # Ok::<(), igraph::Error>(())
1238/// ```
1239pub fn is_bigraphical(
1240 degrees1: &[igraph_int_t],
1241 degrees2: &[igraph_int_t],
1242 allowed: impl Into<AllowedEdgeTypes>,
1243) -> Result<bool> {
1244 if let Some(answer) = screen_degrees(&[degrees1, degrees2])? {
1245 return Ok(answer);
1246 }
1247 let d1 = VectorInt::view(degrees1);
1248 let d2 = VectorInt::view(degrees2);
1249 let mut res = false;
1250 igraph_call!(igraph_is_bigraphical(
1251 d1.as_ptr(),
1252 d2.as_ptr(),
1253 allowed.into().to_raw(),
1254 &mut res
1255 ))?;
1256 Ok(res)
1257}