Skip to main content

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(&degrees, 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, &deg_from);
961            let ncol = dimension(options.max_to_degree, &deg_to);
962            if from_mode != to_mode || nrow != ncol {
963                return self.mixing_matrix(
964                    weights,
965                    &deg_from,
966                    &deg_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}