Skip to main content

igraph/
centrality.rs

1//! Centrality measures, graph centralization and local scan statistics
2//! (`igraph_centrality.h`, `igraph_scan.h`).
3//!
4//! A *centrality* assigns every vertex (or edge) a score that tells how
5//! "important" it is in the network. Different notions of importance lead to
6//! different measures: being close to everybody else ([closeness](igraph_t::closeness),
7//! [harmonic centrality](igraph_t::harmonic_centrality)), lying on many shortest
8//! paths ([betweenness](igraph_t::betweenness)), being connected to other
9//! important vertices ([eigenvector centrality](igraph_t::eigenvector_centrality),
10//! [PageRank](igraph_t::pagerank), [hub and authority scores](igraph_t::hub_and_authority_scores)),
11//! or bridging structural holes ([Burt's constraint](igraph_t::constraint)).
12//!
13//! A *centralization* index condenses vertex-level scores into a single number
14//! describing how much the whole graph is dominated by a single vertex; it is
15//! usually normalized by its value on the most centralized graph of the same
16//! size (typically a star), the *theoretical maximum*.
17//!
18//! *Local scan statistics* count edges (or sum edge weights) within the
19//! neighborhoods of vertices, and are used for anomaly detection in (time
20//! series of) graphs.
21//!
22//! All the functions are methods of [`Graph`], except for the graph-free
23//! [`centralization`] and the `*_tmax` free functions that compute theoretical
24//! maxima from a number of vertices only.
25//!
26//! # Example
27//!
28//! ```
29//! use igraph::prelude::*;
30//! use igraph::centrality::PageRankOptions;
31//!
32//! // A star with 5 leaves: the center is the most central in every sense.
33//! let star = Graph::star(6, StarMode::Undirected, 0).unwrap();
34//!
35//! let btw = star.betweenness(None, .., true, false).unwrap();
36//! assert_eq!(btw, vec![10.0, 0.0, 0.0, 0.0, 0.0, 0.0]); // C(5, 2) = 10 pairs of leaves
37//!
38//! let clo = star.closeness(.., NeighborMode::All, None, true).unwrap();
39//! assert_eq!(clo[0], 1.0);
40//!
41//! let pr = star.pagerank(None, .., &PageRankOptions::default()).unwrap();
42//! assert!((pr.scores.iter().sum::<f64>() - 1.0).abs() < 1e-12);
43//! assert!(pr.scores[0] > pr.scores[1]);
44//!
45//! // The star is the most centralized graph: normalized centralization is 1.
46//! let c = star.centralization_degree(NeighborMode::All, Loops::None, true).unwrap();
47//! assert!((c.centralization - 1.0).abs() < 1e-12);
48//!
49//! // In Zachary's karate club the instructor (0) and the administrator (33)
50//! // are the two most "between" members.
51//! let karate = Graph::famous("Zachary").unwrap();
52//! let btw = karate.betweenness(None, .., false, false).unwrap();
53//! let mut order: Vec<usize> = (0..34).collect();
54//! order.sort_by(|&a, &b| btw[b].total_cmp(&btw[a]));
55//! assert_eq!(&order[..2], &[0, 33]);
56//! ```
57//!
58//! # Provided functionality
59//!
60//! | Measure | Methods | C functions |
61//! |---|---|---|
62//! | Closeness | [`closeness`](igraph_t::closeness), [`closeness_cutoff`](igraph_t::closeness_cutoff), [`closeness_reachability`](igraph_t::closeness_reachability) | `igraph_closeness`, `igraph_closeness_cutoff` |
63//! | Harmonic centrality | [`harmonic_centrality`](igraph_t::harmonic_centrality), [`harmonic_centrality_cutoff`](igraph_t::harmonic_centrality_cutoff) | `igraph_harmonic_centrality[_cutoff]` |
64//! | Vertex betweenness | [`betweenness`](igraph_t::betweenness), [`betweenness_cutoff`](igraph_t::betweenness_cutoff), [`betweenness_subset`](igraph_t::betweenness_subset) | `igraph_betweenness[_cutoff/_subset]` |
65//! | Edge betweenness | [`edge_betweenness`](igraph_t::edge_betweenness), [`edge_betweenness_cutoff`](igraph_t::edge_betweenness_cutoff), [`edge_betweenness_subset`](igraph_t::edge_betweenness_subset) | `igraph_edge_betweenness[_cutoff/_subset]` |
66//! | PageRank | [`pagerank`](igraph_t::pagerank), [`personalized_pagerank`](igraph_t::personalized_pagerank), [`personalized_pagerank_vs`](igraph_t::personalized_pagerank_vs) | `igraph_pagerank`, `igraph_personalized_pagerank[_vs]` |
67//! | Spectral | [`eigenvector_centrality`](igraph_t::eigenvector_centrality), [`hub_and_authority_scores`](igraph_t::hub_and_authority_scores) | `igraph_eigenvector_centrality`, `igraph_hub_and_authority_scores` |
68//! | Structural holes | [`constraint`](igraph_t::constraint) | `igraph_constraint` |
69//! | Edge convergence | [`convergence_degree`](igraph_t::convergence_degree) | `igraph_convergence_degree` |
70//! | Centralization | [`centralization`], [`centralization_degree`](igraph_t::centralization_degree), [`centralization_betweenness`](igraph_t::centralization_betweenness), [`centralization_closeness`](igraph_t::centralization_closeness), [`centralization_eigenvector_centrality`](igraph_t::centralization_eigenvector_centrality) | `igraph_centralization*` |
71//! | Theoretical maxima | [`centralization_degree_tmax`], [`centralization_betweenness_tmax`], [`centralization_closeness_tmax`], [`centralization_eigenvector_centrality_tmax`] and the homonymous methods of [`Graph`] | `igraph_centralization_*_tmax` |
72//! | Local scan statistics | [`local_scan_0`](igraph_t::local_scan_0), [`local_scan_1_ecount`](igraph_t::local_scan_1_ecount), [`local_scan_k_ecount`](igraph_t::local_scan_k_ecount), their `_them` variants, [`local_scan_subset_ecount`](igraph_t::local_scan_subset_ecount), [`local_scan_neighborhood_ecount`](igraph_t::local_scan_neighborhood_ecount) | `igraph_local_scan_*` |
73//!
74//! # Conventions
75//!
76//! - Edge weights are passed as `Option<&[f64]>`, one weight per edge, `None`
77//!   meaning an unweighted computation. The shortest-path based measures
78//!   (closeness, harmonic centrality, betweenness) and PageRank reject NaN
79//!   weights; betweenness also requires them to be strictly positive.
80//! - Cutoffs are `Option<f64>`: `None` means no limit on the path lengths
81//!   (the exact measure is computed); `Some(c)` only considers paths of length
82//!   at most `c` (a negative `c` also means "no limit", as in C).
83//! - Vertex and edge sets are anything convertible into a
84//!   [`VertexSelector`]/[`EdgeSelector`], e.g. `..` (all), a single id, a
85//!   slice or a range of ids. Results follow the order of the selector.
86//! - ARPACK-based computations (eigenvector centrality, hub and authority
87//!   scores, [`PageRankAlgo::Arpack`]) always run with igraph's default ARPACK
88//!   options, which are adequate for virtually every graph.
89//! - igraph reports non-fatal problems (e.g. eigenvector centrality of a
90//!   disconnected graph) as *warnings*, not errors: collect them with
91//!   [`take_warnings`](crate::error::take_warnings).
92//!
93//! # See also
94//!
95//! - Shortest-path quantities that closeness and betweenness are built on:
96//!   [`distances`](igraph_t::distances),
97//!   [`eccentricity`](igraph_t::eccentricity), [`radius`](igraph_t::radius)
98//!   and [`average_path_length`](igraph_t::average_path_length) in
99//!   [`paths`](crate::paths).
100//! - Degree-like measures: [`degree`](igraph_t::degree),
101//!   [`strength`](igraph_t::strength) and [`maxdegree`](igraph_t::maxdegree);
102//!   k-core decomposition with [`coreness`](igraph_t::coreness).
103//! - Community detection by removing high-betweenness edges:
104//!   [`community_edge_betweenness`](igraph_t::community_edge_betweenness).
105//! - The whole spectrum of the adjacency matrix, of which eigenvector
106//!   centrality is the leading eigenvector:
107//!   [`eigen_adjacency`](igraph_t::eigen_adjacency).
108//! - Local clustering, the other classic ego-network measure next to Burt's
109//!   constraint: [`transitivity_local_undirected`](igraph_t::transitivity_local_undirected)
110//!   and [`count_adjacent_triangles`](igraph_t::count_adjacent_triangles).
111
112use crate::{
113    constants::{Loops, NeighborMode},
114    error::{Error, Result},
115    ffi::*,
116    graph::{Graph, VertexId},
117    igraph_call,
118    list::VectorIntList,
119    selector::{EdgeSelector, VertexSelector},
120    vector::{Vector, VectorInt},
121};
122
123crate::ffi_enum! {
124    /// The algorithm used to compute PageRank (`igraph_pagerank_algo_t`).
125    pub enum PageRankAlgo: igraph_pagerank_algo_t {
126        /// Phrase PageRank as an eigenvalue problem and solve it with ARPACK
127        /// (the default before igraph 0.7). The returned
128        /// [`value`](EigenScores::value) is the eigenvalue, which should be 1:
129        /// checking it detects convergence failures.
130        Arpack = igraph_pagerank_algo_t_IGRAPH_PAGERANK_ALGO_ARPACK,
131        /// Solve a linear system with the PRPACK library
132        /// (<https://github.com/dgleich/prpack>). Recommended, and the default.
133        Prpack = igraph_pagerank_algo_t_IGRAPH_PAGERANK_ALGO_PRPACK,
134    }
135}
136
137impl Default for PageRankAlgo {
138    /// [`PageRankAlgo::Prpack`], igraph's recommended implementation.
139    fn default() -> Self {
140        Self::Prpack
141    }
142}
143
144/// Tuning parameters of the PageRank family of functions
145/// ([`pagerank`](igraph_t::pagerank), [`personalized_pagerank`](igraph_t::personalized_pagerank),
146/// [`personalized_pagerank_vs`](igraph_t::personalized_pagerank_vs)).
147///
148/// The [`Default`] is the classic setting: damping `0.85`, directed paths,
149/// [`PageRankAlgo::Prpack`].
150///
151/// ```
152/// use igraph::centrality::{PageRankAlgo, PageRankOptions};
153///
154/// let opts = PageRankOptions::default().with_damping(0.5).with_algo(PageRankAlgo::Arpack);
155/// assert_eq!((opts.damping, opts.directed, opts.algo), (0.5, true, PageRankAlgo::Arpack));
156/// ```
157#[derive(Debug, Clone, Copy, PartialEq)]
158pub struct PageRankOptions {
159    /// The damping factor (`d` in the original paper): the probability that the
160    /// random walker follows an edge instead of restarting. Must be in `[0, 1]`.
161    pub damping: f64,
162    /// Whether to follow edge directions in directed graphs (ignored for
163    /// undirected graphs).
164    pub directed: bool,
165    /// The implementation to use.
166    pub algo: PageRankAlgo,
167}
168
169impl Default for PageRankOptions {
170    fn default() -> Self {
171        Self {
172            damping: 0.85,
173            directed: true,
174            algo: PageRankAlgo::Prpack,
175        }
176    }
177}
178
179impl PageRankOptions {
180    /// Sets the [`damping`](Self::damping) factor.
181    pub fn with_damping(mut self, damping: f64) -> Self {
182        self.damping = damping;
183        self
184    }
185
186    /// Sets whether edge directions are followed ([`directed`](Self::directed)).
187    pub fn with_directed(mut self, directed: bool) -> Self {
188        self.directed = directed;
189        self
190    }
191
192    /// Sets the implementation ([`algo`](Self::algo)).
193    pub fn with_algo(mut self, algo: PageRankAlgo) -> Self {
194        self.algo = algo;
195        self
196    }
197}
198
199/// Closeness scores together with reachability information, returned by
200/// [`closeness_reachability`](igraph_t::closeness_reachability).
201#[derive(Debug, Clone, PartialEq)]
202pub struct Closeness {
203    /// The closeness centrality of each requested vertex (NaN for vertices
204    /// that reach no other vertex).
205    pub scores: Vec<f64>,
206    /// For each requested vertex, the number of vertices reachable from it
207    /// (within the cutoff, if any), not counting the vertex itself.
208    pub reachable_count: Vec<i64>,
209    /// Whether every vertex of the graph was reachable from each requested
210    /// vertex. `false` proves that the graph is disconnected; `true` proves it
211    /// is connected if the graph is undirected, or if it is directed and all
212    /// vertices were requested.
213    pub all_reachable: bool,
214}
215
216/// Vertex scores that are an eigenvector, with the corresponding eigenvalue.
217///
218/// Returned by [`pagerank`](igraph_t::pagerank) and its personalized
219/// variants, and by [`eigenvector_centrality`](igraph_t::eigenvector_centrality).
220#[derive(Debug, Clone, PartialEq)]
221pub struct EigenScores {
222    /// The score of each vertex (for PageRank: of each requested vertex).
223    pub scores: Vec<f64>,
224    /// The eigenvalue. For PageRank it is always `1.0` with PRPACK and should
225    /// be very close to one with ARPACK. For eigenvector centrality it is the
226    /// leading eigenvalue of the adjacency matrix (zero for acyclic graphs,
227    /// where the measure is not meaningful).
228    pub value: f64,
229}
230
231/// Kleinberg's hub and authority scores, see
232/// [`hub_and_authority_scores`](igraph_t::hub_and_authority_scores).
233#[derive(Debug, Clone, PartialEq)]
234pub struct HubAuthority {
235    /// Hub score of each vertex, scaled so that the maximum is 1.
236    pub hubs: Vec<f64>,
237    /// Authority score of each vertex, scaled so that the maximum is 1.
238    pub authorities: Vec<f64>,
239    /// The leading eigenvalue of `A Aᵀ` (equivalently, of `Aᵀ A`).
240    pub value: f64,
241}
242
243/// Convergence degrees of the edges, see
244/// [`convergence_degree`](igraph_t::convergence_degree).
245#[derive(Debug, Clone, PartialEq)]
246pub struct ConvergenceDegree {
247    /// The convergence degree of each edge, in `(-1, 1)`.
248    pub result: Vec<f64>,
249    /// The size of the input set of each edge.
250    pub ins: Vec<f64>,
251    /// The size of the output set of each edge.
252    pub outs: Vec<f64>,
253}
254
255/// Vertex-level scores together with the graph-level centralization index.
256///
257/// Returned by [`centralization_degree`](igraph_t::centralization_degree),
258/// [`centralization_betweenness`](igraph_t::centralization_betweenness) and
259/// [`centralization_closeness`](igraph_t::centralization_closeness).
260#[derive(Debug, Clone, PartialEq)]
261pub struct Centralization {
262    /// The vertex-level centrality scores of all vertices.
263    pub scores: Vec<f64>,
264    /// The graph-level centralization index (normalized if requested).
265    pub centralization: f64,
266    /// The centralization of the most centralized graph with the same number
267    /// of vertices (and directedness).
268    pub theoretical_max: f64,
269}
270
271/// Eigenvector centralities together with the graph-level centralization
272/// index, see [`centralization_eigenvector_centrality`](igraph_t::centralization_eigenvector_centrality).
273#[derive(Debug, Clone, PartialEq)]
274pub struct EigenvectorCentralization {
275    /// The eigenvector centrality of all vertices, scaled so that the maximum is 1.
276    pub scores: Vec<f64>,
277    /// The leading eigenvalue.
278    pub value: f64,
279    /// The graph-level centralization index (normalized if requested).
280    pub centralization: f64,
281    /// The centralization of the most centralized graph with the same number
282    /// of vertices (and directedness).
283    pub theoretical_max: f64,
284}
285
286/// Validates an optional per-edge weight vector and builds a view over it.
287fn weights_view<'w>(
288    graph: &Graph,
289    weights: Option<&'w [f64]>,
290) -> Result<Option<crate::vector::View<'w, Vector>>> {
291    if let Some(w) = weights
292        && w.len() != graph.ecount()
293    {
294        return Err(Error::invalid(format!(
295            "the weight vector has length {}, but the graph has {} edges",
296            w.len(),
297            graph.ecount()
298        )));
299    }
300    Ok(weights.map(Vector::view))
301}
302
303/// Largest sum of absolute weights passed unchanged to the eigenvector-based
304/// (ARPACK / PRPACK) routines: squares of such sums stay finite.
305const MAX_ABS_WEIGHT_SUM: f64 = 1e150;
306
307/// Validates weights for the eigenvector-based centralities and, if they are
308/// so large that the iterations could overflow, rescales them.
309///
310/// igraph (1.0.0 and 1.0.1) does not reject infinite weights (nor NaN ones,
311/// except for PageRank with ARPACK), and huge finite weights overflow the
312/// matrix-vector products: ARPACK then aborts the whole process (an f2c
313/// `STOP`), PRPACK silently returns NaN scores. All these measures are
314/// invariant under scaling the weights (only the eigenvalue scales, by the
315/// returned factor for the adjacency matrix), so weights whose absolute sum
316/// exceeds [`MAX_ABS_WEIGHT_SUM`] are divided by the smallest power of two
317/// that brings the sum below it. A power of two keeps the division exact
318/// and, unlike normalizing the largest weight to `1`, does not flush small
319/// weights to (sub)normal zero: e.g. PageRank divides by the out-strength,
320/// and a vertex whose only out-weight became `1e-320` would get infinite,
321/// then `NaN`, transition probabilities.
322/// Returns the rescaled weights (if any) and the scale factor.
323fn spectral_weights(weights: Option<&[f64]>) -> Result<(Option<Vec<f64>>, f64)> {
324    let Some(w) = weights else {
325        return Ok((None, 1.0));
326    };
327    if let Some(&x) = w.iter().find(|x| !x.is_finite()) {
328        return Err(Error::invalid(format!(
329            "the weights must be finite, found {x}"
330        )));
331    }
332    let sum: f64 = w.iter().map(|x| x.abs()).sum();
333    if sum <= MAX_ABS_WEIGHT_SUM {
334        return Ok((None, 1.0));
335    }
336    // `sum` may have overflowed: compute log2(sum) as log2(max) + log2(sum / max).
337    let max = w.iter().fold(0.0_f64, |m, x| m.max(x.abs()));
338    let rel: f64 = w.iter().map(|x| x.abs() / max).sum();
339    let excess = max.log2() + rel.log2() - MAX_ABS_WEIGHT_SUM.log2();
340    // One extra halving absorbs the rounding of the logarithms. The exponent
341    // is at most about 1024 + 64 - 498, so the factor is finite.
342    let scale = 2f64.powi(excess.ceil() as i32 + 1);
343    Ok((Some(w.iter().map(|x| x / scale).collect()), scale))
344}
345
346/// Validates the PageRank damping factor: igraph's own range check lets NaN
347/// through (and ARPACK then aborts the process).
348fn check_damping(damping: f64) -> Result<()> {
349    if !(0.0..=1.0).contains(&damping) {
350        return Err(Error::invalid(format!(
351            "the PageRank damping factor must be in [0, 1], got {damping}"
352        )));
353    }
354    Ok(())
355}
356
357fn ptr_of(view: &Option<crate::vector::View<'_, Vector>>) -> *const igraph_vector_t {
358    view.as_ref().map_or(std::ptr::null(), |v| v.as_ptr())
359}
360
361fn raw_cutoff(cutoff: Option<f64>) -> f64 {
362    cutoff.unwrap_or(-1.0)
363}
364
365fn raw_nodes(nodes: usize) -> Result<igraph_int_t> {
366    igraph_int_t::try_from(nodes).map_err(|_| Error::invalid("too many vertices"))
367}
368
369/// Checks that `them` is compatible with `us` for the `_them` scan statistics.
370fn check_them(us: &Graph, them: &Graph) -> Result<()> {
371    if us.vcount() != them.vcount() {
372        return Err(Error::invalid(
373            "the two graphs must have the same number of vertices",
374        ));
375    }
376    if us.is_directed() != them.is_directed() {
377        return Err(Error::invalid(
378            "the two graphs must have the same directedness",
379        ));
380    }
381    Ok(())
382}
383
384/// Computes the graph-level centralization index from vertex-level scores
385/// (`igraph_centralization`).
386///
387/// The (unnormalized) centralization is `C = Σ_v (max_u c_u − c_v)`, the sum of
388/// the deviations from the largest score. If `normalized` is true,
389/// `C / theoretical_max` is returned instead, where `theoretical_max` is the
390/// centralization of the most centralized structure with the same number of
391/// vertices (usually a star, see e.g. [`centralization_degree_tmax`]); it is
392/// ignored otherwise. An empty `scores` slice gives NaN. Time complexity:
393/// O(n), the number of scores.
394///
395/// Binds [`igraph_centralization`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization).
396///
397/// # Examples
398///
399/// ```
400/// use igraph::centrality::centralization;
401///
402/// assert_eq!(centralization(&[3.0, 1.0, 1.0, 1.0], 0.0, false), 6.0);
403/// assert_eq!(centralization(&[3.0, 1.0, 1.0, 1.0], 6.0, true), 1.0);
404/// ```
405pub fn centralization(scores: &[f64], theoretical_max: f64, normalized: bool) -> f64 {
406    let view = Vector::view(scores);
407    unsafe { igraph_centralization(view.as_ptr(), theoretical_max, normalized) }
408}
409
410/// Theoretical maximum of degree centralization for a graph with `nodes`
411/// vertices (`igraph_centralization_degree_tmax` with a null graph).
412///
413/// The graph is considered directed unless `mode` is [`NeighborMode::All`].
414/// The most centralized structure is the star (the in- or out-star for
415/// directed graphs). `loops` tells whether self-loops count (and how) in the
416/// degree, since they change the maximum. For `nodes == 0` the result is NaN.
417/// See [`igraph_t::centralization_degree_tmax`] to read size and directedness
418/// from a graph. Time complexity: O(1).
419///
420/// Binds [`igraph_centralization_degree_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_degree_tmax).
421///
422/// # Examples
423///
424/// ```
425/// use igraph::{centrality::centralization_degree_tmax, prelude::*};
426///
427/// // Undirected star on 5 vertices: (n - 1)(n - 2) = 12.
428/// assert_eq!(centralization_degree_tmax(5, NeighborMode::All, Loops::None).unwrap(), 12.0);
429/// ```
430pub fn centralization_degree_tmax(nodes: usize, mode: NeighborMode, loops: Loops) -> Result<f64> {
431    let mut res = 0.0;
432    igraph_call!(igraph_centralization_degree_tmax(
433        std::ptr::null(),
434        raw_nodes(nodes)?,
435        mode.into(),
436        loops.into(),
437        &mut res
438    ))?;
439    Ok(res)
440}
441
442/// Theoretical maximum of betweenness centralization for a graph with `nodes`
443/// vertices (`igraph_centralization_betweenness_tmax` with a null graph).
444///
445/// `directed` tells whether directed paths are used. The most centralized
446/// structure is the star. See [`igraph_t::centralization_betweenness_tmax`]
447/// to read size and directedness from a graph. Time complexity: O(1).
448///
449/// Binds [`igraph_centralization_betweenness_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_betweenness_tmax).
450///
451/// # Examples
452///
453/// ```
454/// use igraph::centrality::centralization_betweenness_tmax;
455///
456/// // Undirected: (n - 1)^2 (n - 2) / 2.
457/// assert_eq!(centralization_betweenness_tmax(5, false).unwrap(), 24.0);
458/// ```
459pub fn centralization_betweenness_tmax(nodes: usize, directed: bool) -> Result<f64> {
460    let mut res = 0.0;
461    igraph_call!(igraph_centralization_betweenness_tmax(
462        std::ptr::null(),
463        raw_nodes(nodes)?,
464        directed,
465        &mut res
466    ))?;
467    Ok(res)
468}
469
470/// Theoretical maximum of closeness centralization for a graph with `nodes`
471/// vertices (`igraph_centralization_closeness_tmax` with a null graph).
472///
473/// The graph is considered directed unless `mode` is [`NeighborMode::All`].
474/// The most centralized structure is the star. The maximum refers to
475/// *normalized* closeness scores, as used by
476/// [`centralization_closeness`](igraph_t::centralization_closeness). Time
477/// complexity: O(1).
478///
479/// Binds [`igraph_centralization_closeness_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_closeness_tmax).
480///
481/// # Examples
482///
483/// ```
484/// use igraph::{centrality::centralization_closeness_tmax, prelude::*};
485///
486/// // Undirected: (n - 1)(n - 2) / (2n - 3).
487/// let t = centralization_closeness_tmax(5, NeighborMode::All).unwrap();
488/// assert!((t - 12.0 / 7.0).abs() < 1e-12);
489/// ```
490pub fn centralization_closeness_tmax(nodes: usize, mode: NeighborMode) -> Result<f64> {
491    let mut res = 0.0;
492    igraph_call!(igraph_centralization_closeness_tmax(
493        std::ptr::null(),
494        raw_nodes(nodes)?,
495        mode.into(),
496        &mut res
497    ))?;
498    Ok(res)
499}
500
501/// Theoretical maximum of eigenvector centralization for a graph with
502/// `nodes` vertices (`igraph_centralization_eigenvector_centrality_tmax`
503/// with a null graph).
504///
505/// The graph is considered directed unless `mode` is [`NeighborMode::All`].
506/// The most centralized undirected structure is a graph with a single edge;
507/// the directed one is the in-star (for [`NeighborMode::Out`]) or the out-star
508/// (for [`NeighborMode::In`]). Scores are assumed to be scaled so that the
509/// maximum is 1. Time complexity: O(1).
510///
511/// Binds [`igraph_centralization_eigenvector_centrality_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_eigenvector_centrality_tmax).
512///
513/// # Examples
514///
515/// ```
516/// use igraph::{centrality::centralization_eigenvector_centrality_tmax, prelude::*};
517///
518/// // Undirected: n - 2 (one edge, all other scores zero).
519/// assert_eq!(centralization_eigenvector_centrality_tmax(10, NeighborMode::All).unwrap(), 8.0);
520/// ```
521pub fn centralization_eigenvector_centrality_tmax(nodes: usize, mode: NeighborMode) -> Result<f64> {
522    let mut res = 0.0;
523    igraph_call!(igraph_centralization_eigenvector_centrality_tmax(
524        std::ptr::null(),
525        raw_nodes(nodes)?,
526        mode.into(),
527        &mut res
528    ))?;
529    Ok(res)
530}
531
532impl igraph_t {
533    // ------------------------------------------------------------------
534    // Closeness and harmonic centrality
535    // ------------------------------------------------------------------
536
537    /// Closeness centrality of the selected vertices (`igraph_closeness`).
538    ///
539    /// The closeness of a vertex is the inverse of the mean distance to (or
540    /// from) all other vertices; it measures how easily other vertices are
541    /// reached from it. `mode` selects the paths in directed graphs:
542    /// [`NeighborMode::Out`] uses distances *from* the vertex,
543    /// [`NeighborMode::In`] distances *to* it and [`NeighborMode::All`] ignores
544    /// directions. With `weights`, path lengths are the sums of edge weights.
545    ///
546    /// If `normalized` is true the result is the inverse of the *mean* distance,
547    /// otherwise the inverse of the *sum* of distances.
548    ///
549    /// Closeness is meaningful for connected graphs only: in disconnected
550    /// graphs igraph only considers *reachable* vertices (in undirected graphs
551    /// this is closeness computed per component). Isolated vertices get NaN.
552    /// Use [`closeness_reachability`](Self::closeness_reachability) to detect
553    /// disconnectedness, or consider [`harmonic_centrality`](Self::harmonic_centrality).
554    ///
555    /// Time complexity: O(n|E|) unweighted, O(n|E|log|V| + |V|) weighted, for
556    /// `n` requested vertices.
557    ///
558    /// See also [`distances`](Self::distances) for the underlying distance
559    /// matrix and [`eccentricity`](Self::eccentricity) for the *largest*
560    /// (instead of the mean) distance from a vertex.
561    ///
562    /// Binds [`igraph_closeness`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_closeness).
563    ///
564    /// # Errors
565    ///
566    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
567    /// invalid vertex, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
568    /// for weights of the wrong length or containing NaN.
569    ///
570    /// # Examples
571    ///
572    /// ```
573    /// use igraph::prelude::*;
574    ///
575    /// // Path 0 - 1 - 2: the middle vertex is at distance 1 from both ends.
576    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
577    /// assert_eq!(g.closeness(.., NeighborMode::All, None, false).unwrap(), vec![1.0 / 3.0, 0.5, 1.0 / 3.0]);
578    /// assert_eq!(g.closeness(1, NeighborMode::All, None, true).unwrap(), vec![1.0]);
579    /// ```
580    pub fn closeness<'a>(
581        &self,
582        vids: impl Into<VertexSelector<'a>>,
583        mode: NeighborMode,
584        weights: Option<&[f64]>,
585        normalized: bool,
586    ) -> Result<Vec<f64>> {
587        let vs = vids.into().to_raw()?;
588        let w = weights_view(self, weights)?;
589        let mut res = Vector::new();
590        igraph_call!(igraph_closeness(
591            self,
592            &mut res,
593            std::ptr::null_mut(),
594            std::ptr::null_mut(),
595            vs.get(),
596            mode.into(),
597            ptr_of(&w),
598            normalized
599        ))?;
600        Ok(res.into())
601    }
602
603    /// Range-limited closeness centrality (`igraph_closeness_cutoff`).
604    ///
605    /// Like [`closeness`](Self::closeness), but only shortest paths of length
606    /// at most `cutoff` are considered (vertices farther away count as
607    /// unreachable). `None` (or a negative cutoff) computes the exact closeness.
608    /// Smaller cutoffs make the computation faster.
609    ///
610    /// Binds [`igraph_closeness_cutoff`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_closeness_cutoff).
611    ///
612    /// # Errors
613    ///
614    /// As for [`closeness`](Self::closeness).
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use igraph::prelude::*;
620    ///
621    /// // Path 0 - 1 - 2 - 3: within distance 1, vertex 0 only reaches vertex 1.
622    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
623    /// let c = g.closeness_cutoff(0, NeighborMode::All, None, true, Some(1.0)).unwrap();
624    /// assert_eq!(c, vec![1.0]);
625    /// ```
626    pub fn closeness_cutoff<'a>(
627        &self,
628        vids: impl Into<VertexSelector<'a>>,
629        mode: NeighborMode,
630        weights: Option<&[f64]>,
631        normalized: bool,
632        cutoff: Option<f64>,
633    ) -> Result<Vec<f64>> {
634        Ok(self
635            .closeness_reachability(vids, mode, weights, normalized, cutoff)?
636            .scores)
637    }
638
639    /// Closeness centrality with reachability information
640    /// (`igraph_closeness_cutoff` with all outputs).
641    ///
642    /// Besides the (possibly range-limited, see
643    /// [`closeness_cutoff`](Self::closeness_cutoff)) closeness scores, it
644    /// returns the number of vertices reachable from each requested vertex and
645    /// whether all vertices were reachable, see [`Closeness`]. These make it
646    /// possible to compute the generalizations of closeness to disconnected
647    /// graphs that rescale scores by the size of the reachable set.
648    ///
649    /// Binds [`igraph_closeness_cutoff`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_closeness_cutoff).
650    ///
651    /// # Errors
652    ///
653    /// As for [`closeness`](Self::closeness).
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// use igraph::prelude::*;
659    ///
660    /// // Two disjoint edges: closeness is computed within each component.
661    /// let g = Graph::from_edges(&[(0, 1), (2, 3)], 4, false).unwrap();
662    /// let c = g.closeness_reachability(.., NeighborMode::All, None, true, None).unwrap();
663    /// assert_eq!(c.scores, vec![1.0; 4]);
664    /// assert_eq!(c.reachable_count, vec![1; 4]);
665    /// assert!(!c.all_reachable);
666    /// ```
667    pub fn closeness_reachability<'a>(
668        &self,
669        vids: impl Into<VertexSelector<'a>>,
670        mode: NeighborMode,
671        weights: Option<&[f64]>,
672        normalized: bool,
673        cutoff: Option<f64>,
674    ) -> Result<Closeness> {
675        let vs = vids.into().to_raw()?;
676        let w = weights_view(self, weights)?;
677        let mut res = Vector::new();
678        let mut reach = VectorInt::new();
679        let mut all = false;
680        igraph_call!(igraph_closeness_cutoff(
681            self,
682            &mut res,
683            &mut reach,
684            &mut all,
685            vs.get(),
686            mode.into(),
687            ptr_of(&w),
688            normalized,
689            raw_cutoff(cutoff)
690        ))?;
691        Ok(Closeness {
692            scores: res.into(),
693            reachable_count: reach.into(),
694            all_reachable: all,
695        })
696    }
697
698    /// Harmonic centrality of the selected vertices (`igraph_harmonic_centrality`).
699    ///
700    /// The harmonic centrality of a vertex is the sum (or, if `normalized`,
701    /// the mean over the other `|V| - 1` vertices) of the inverse distances to
702    /// all other vertices; unreachable vertices contribute zero, which makes
703    /// this measure well-behaved on disconnected graphs, unlike closeness.
704    /// `mode` and `weights` are as in [`closeness`](Self::closeness).
705    ///
706    /// References: M. Marchiori and V. Latora, *Harmony in the small-world*,
707    /// Physica A 285 (2000); S. Vigna and P. Boldi, *Axioms for Centrality*,
708    /// Internet Mathematics 10 (2014).
709    ///
710    /// Time complexity: O(n|E|) unweighted, O(n|E|log|V| + |V|) weighted.
711    ///
712    /// See also [`global_efficiency`](Self::global_efficiency): the mean of
713    /// the normalized harmonic centralities of all vertices is the global
714    /// efficiency of the graph.
715    ///
716    /// Binds [`igraph_harmonic_centrality`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_harmonic_centrality).
717    ///
718    /// # Errors
719    ///
720    /// As for [`closeness`](Self::closeness).
721    ///
722    /// # Examples
723    ///
724    /// ```
725    /// use igraph::prelude::*;
726    ///
727    /// // Path 0 - 1 - 2: vertex 0 has 1/1 + 1/2 = 1.5.
728    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
729    /// assert_eq!(g.harmonic_centrality(.., NeighborMode::All, None, false).unwrap(), vec![1.5, 2.0, 1.5]);
730    /// ```
731    pub fn harmonic_centrality<'a>(
732        &self,
733        vids: impl Into<VertexSelector<'a>>,
734        mode: NeighborMode,
735        weights: Option<&[f64]>,
736        normalized: bool,
737    ) -> Result<Vec<f64>> {
738        let vs = vids.into().to_raw()?;
739        let w = weights_view(self, weights)?;
740        let mut res = Vector::new();
741        igraph_call!(igraph_harmonic_centrality(
742            self,
743            &mut res,
744            vs.get(),
745            mode.into(),
746            ptr_of(&w),
747            normalized
748        ))?;
749        Ok(res.into())
750    }
751
752    /// Range-limited harmonic centrality (`igraph_harmonic_centrality_cutoff`).
753    ///
754    /// Like [`harmonic_centrality`](Self::harmonic_centrality), but vertices
755    /// farther than `cutoff` contribute zero. `None` (or a negative value)
756    /// computes the exact harmonic centrality. Note that normalization still
757    /// divides by `|V| - 1`.
758    ///
759    /// Binds [`igraph_harmonic_centrality_cutoff`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_harmonic_centrality_cutoff).
760    ///
761    /// # Errors
762    ///
763    /// As for [`closeness`](Self::closeness).
764    ///
765    /// # Examples
766    ///
767    /// ```
768    /// use igraph::prelude::*;
769    ///
770    /// // With cutoff 1 the harmonic centrality is just degree / (n - 1).
771    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
772    /// let h = g.harmonic_centrality_cutoff(.., NeighborMode::All, None, true, Some(1.0)).unwrap();
773    /// assert_eq!(h, vec![0.5, 1.0, 0.5]);
774    /// ```
775    pub fn harmonic_centrality_cutoff<'a>(
776        &self,
777        vids: impl Into<VertexSelector<'a>>,
778        mode: NeighborMode,
779        weights: Option<&[f64]>,
780        normalized: bool,
781        cutoff: Option<f64>,
782    ) -> Result<Vec<f64>> {
783        let vs = vids.into().to_raw()?;
784        let w = weights_view(self, weights)?;
785        let mut res = Vector::new();
786        igraph_call!(igraph_harmonic_centrality_cutoff(
787            self,
788            &mut res,
789            vs.get(),
790            mode.into(),
791            ptr_of(&w),
792            normalized,
793            raw_cutoff(cutoff)
794        ))?;
795        Ok(res.into())
796    }
797
798    // ------------------------------------------------------------------
799    // Betweenness
800    // ------------------------------------------------------------------
801
802    /// Betweenness centrality of the selected vertices (`igraph_betweenness`).
803    ///
804    /// The betweenness of a vertex `v` is the number of shortest paths passing
805    /// through it; when two vertices are joined by several shortest paths,
806    /// only the fraction of them passing through `v` is counted (Brandes'
807    /// algorithm). With `weights`, weighted shortest paths are used.
808    /// `directed` tells whether to follow edge directions (ignored for
809    /// undirected graphs). If `normalized`, scores are divided by the number of
810    /// vertex pairs: `n(n-1)` ordered pairs when directed paths are used,
811    /// `n(n-1)/2` unordered pairs otherwise (note: *not* the `(n-1)(n-2)`
812    /// convention of some textbooks).
813    ///
814    /// `vids` only selects which scores are returned: internally the
815    /// betweenness of all vertices is computed. Time complexity: O(|V||E|).
816    ///
817    /// Reference: U. Brandes, *A faster algorithm for betweenness centrality*,
818    /// J. Math. Sociol. 25(2), 163–177 (2001).
819    ///
820    /// See also [`get_all_shortest_paths`](Self::get_all_shortest_paths) to
821    /// list the shortest paths that are being counted.
822    ///
823    /// Binds [`igraph_betweenness`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_betweenness).
824    ///
825    /// # Errors
826    ///
827    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
828    /// invalid vertex, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
829    /// for invalid weights.
830    ///
831    /// # Examples
832    ///
833    /// ```
834    /// use igraph::prelude::*;
835    ///
836    /// // Path 0 - 1 - 2 - 3: the inner vertices each lie on 2 shortest paths.
837    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
838    /// assert_eq!(g.betweenness(None, .., false, false).unwrap(), vec![0.0, 2.0, 2.0, 0.0]);
839    /// ```
840    pub fn betweenness<'a>(
841        &self,
842        weights: Option<&[f64]>,
843        vids: impl Into<VertexSelector<'a>>,
844        directed: bool,
845        normalized: bool,
846    ) -> Result<Vec<f64>> {
847        let vs = vids.into().to_raw()?;
848        let w = weights_view(self, weights)?;
849        let mut res = Vector::new();
850        igraph_call!(igraph_betweenness(
851            self,
852            ptr_of(&w),
853            &mut res,
854            vs.get(),
855            directed,
856            normalized
857        ))?;
858        Ok(res.into())
859    }
860
861    /// Range-limited betweenness centrality (`igraph_betweenness_cutoff`).
862    ///
863    /// Like [`betweenness`](Self::betweenness), but only shortest paths of
864    /// length at most `cutoff` are counted. `None` (or a negative value)
865    /// computes the exact betweenness.
866    ///
867    /// Binds [`igraph_betweenness_cutoff`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_betweenness_cutoff).
868    ///
869    /// # Errors
870    ///
871    /// As for [`betweenness`](Self::betweenness).
872    ///
873    /// # Examples
874    ///
875    /// ```
876    /// use igraph::prelude::*;
877    ///
878    /// // With cutoff 2 only the paths 0-1-2 and 1-2-3 pass through inner vertices.
879    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
880    /// assert_eq!(g.betweenness_cutoff(None, .., false, false, Some(2.0)).unwrap(), vec![0.0, 1.0, 1.0, 0.0]);
881    /// ```
882    pub fn betweenness_cutoff<'a>(
883        &self,
884        weights: Option<&[f64]>,
885        vids: impl Into<VertexSelector<'a>>,
886        directed: bool,
887        normalized: bool,
888        cutoff: Option<f64>,
889    ) -> Result<Vec<f64>> {
890        let vs = vids.into().to_raw()?;
891        let w = weights_view(self, weights)?;
892        let mut res = Vector::new();
893        igraph_call!(igraph_betweenness_cutoff(
894            self,
895            ptr_of(&w),
896            &mut res,
897            vs.get(),
898            directed,
899            normalized,
900            raw_cutoff(cutoff)
901        ))?;
902        Ok(res.into())
903    }
904
905    /// Betweenness restricted to paths between a set of sources and a set of
906    /// targets (`igraph_betweenness_subset`).
907    ///
908    /// Only the shortest paths starting in `sources` and ending in `targets`
909    /// are counted. Scores are returned for `vids`. In undirected graphs each
910    /// source-target pair contributes with weight 1/2, so that with
911    /// `sources == targets` (where every pair is met from both ends) the
912    /// result agrees with [`betweenness`](Self::betweenness); in particular
913    /// selecting all vertices as sources and targets gives the ordinary
914    /// betweenness. Normalization is not
915    /// implemented by igraph for this variant, so the scores are always raw
916    /// path counts. Time complexity: O(|S||E|), `S` being the source set.
917    ///
918    /// Binds [`igraph_betweenness_subset`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_betweenness_subset).
919    ///
920    /// # Errors
921    ///
922    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
923    /// invalid vertex in any of the selectors.
924    ///
925    /// # Examples
926    ///
927    /// ```
928    /// use igraph::prelude::*;
929    ///
930    /// // Directed path 0 -> 1 -> 2 -> 3, only paths from 0 to 3 count.
931    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, true).unwrap();
932    /// assert_eq!(g.betweenness_subset(None, 0, 3, .., true).unwrap(), vec![0.0, 1.0, 1.0, 0.0]);
933    /// // Undirected, the single pair {0, 3} counts 1/2.
934    /// let u = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
935    /// assert_eq!(u.betweenness_subset(None, 0, 3, .., false).unwrap(), vec![0.0, 0.5, 0.5, 0.0]);
936    /// // With all vertices as sources and targets it is the usual betweenness.
937    /// assert_eq!(u.betweenness_subset(None, .., .., .., false).unwrap(), u.betweenness(None, .., false, false).unwrap());
938    /// ```
939    pub fn betweenness_subset<'a>(
940        &self,
941        weights: Option<&[f64]>,
942        sources: impl Into<VertexSelector<'a>>,
943        targets: impl Into<VertexSelector<'a>>,
944        vids: impl Into<VertexSelector<'a>>,
945        directed: bool,
946    ) -> Result<Vec<f64>> {
947        let src = sources.into().to_raw()?;
948        let tgt = targets.into().to_raw()?;
949        let vs = vids.into().to_raw()?;
950        let w = weights_view(self, weights)?;
951        let mut res = Vector::new();
952        igraph_call!(igraph_betweenness_subset(
953            self,
954            ptr_of(&w),
955            &mut res,
956            src.get(),
957            tgt.get(),
958            vs.get(),
959            directed,
960            false
961        ))?;
962        Ok(res.into())
963    }
964
965    /// Betweenness centrality of the selected edges (`igraph_edge_betweenness`).
966    ///
967    /// The betweenness of an edge is the number of shortest paths passing
968    /// through it (fractionally, when there are several shortest paths).
969    /// Parameters are as in [`betweenness`](Self::betweenness); `eids` only
970    /// selects the returned scores. Removing the edge with the highest
971    /// betweenness is the basic step of the Girvan–Newman community detection
972    /// method, available as
973    /// [`community_edge_betweenness`](Self::community_edge_betweenness).
974    /// Time complexity: O(|V||E|).
975    ///
976    /// Binds [`igraph_edge_betweenness`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_edge_betweenness).
977    ///
978    /// # Errors
979    ///
980    /// [`ErrorKind::InvalidEdgeId`](crate::ErrorKind::InvalidEdgeId) for an
981    /// invalid edge, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
982    /// for invalid weights.
983    ///
984    /// # Examples
985    ///
986    /// ```
987    /// use igraph::prelude::*;
988    ///
989    /// // Two triangles joined by the bridge 2 - 3: all 9 cross pairs use it.
990    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (4, 5), (3, 5)], 6, false).unwrap();
991    /// let eb = g.edge_betweenness(None, .., false, false).unwrap();
992    /// assert_eq!(eb[3], 9.0);
993    /// ```
994    pub fn edge_betweenness<'a>(
995        &self,
996        weights: Option<&[f64]>,
997        eids: impl Into<EdgeSelector<'a>>,
998        directed: bool,
999        normalized: bool,
1000    ) -> Result<Vec<f64>> {
1001        let es = eids.into().to_raw()?;
1002        let w = weights_view(self, weights)?;
1003        let mut res = Vector::new();
1004        igraph_call!(igraph_edge_betweenness(
1005            self,
1006            ptr_of(&w),
1007            &mut res,
1008            es.get(),
1009            directed,
1010            normalized
1011        ))?;
1012        Ok(res.into())
1013    }
1014
1015    /// Range-limited edge betweenness (`igraph_edge_betweenness_cutoff`).
1016    ///
1017    /// Like [`edge_betweenness`](Self::edge_betweenness), counting only
1018    /// shortest paths of length at most `cutoff` (`None` = no limit).
1019    ///
1020    /// Binds [`igraph_edge_betweenness_cutoff`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_edge_betweenness_cutoff).
1021    ///
1022    /// # Errors
1023    ///
1024    /// As for [`edge_betweenness`](Self::edge_betweenness).
1025    ///
1026    /// # Examples
1027    ///
1028    /// ```
1029    /// use igraph::prelude::*;
1030    ///
1031    /// // With cutoff 1, every edge only carries the path between its endpoints.
1032    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
1033    /// assert_eq!(g.edge_betweenness_cutoff(None, .., false, false, Some(1.0)).unwrap(), vec![1.0; 3]);
1034    /// ```
1035    pub fn edge_betweenness_cutoff<'a>(
1036        &self,
1037        weights: Option<&[f64]>,
1038        eids: impl Into<EdgeSelector<'a>>,
1039        directed: bool,
1040        normalized: bool,
1041        cutoff: Option<f64>,
1042    ) -> Result<Vec<f64>> {
1043        let es = eids.into().to_raw()?;
1044        let w = weights_view(self, weights)?;
1045        let mut res = Vector::new();
1046        igraph_call!(igraph_edge_betweenness_cutoff(
1047            self,
1048            ptr_of(&w),
1049            &mut res,
1050            es.get(),
1051            directed,
1052            normalized,
1053            raw_cutoff(cutoff)
1054        ))?;
1055        Ok(res.into())
1056    }
1057
1058    /// Edge betweenness restricted to paths between a set of sources and a
1059    /// set of targets (`igraph_edge_betweenness_subset`).
1060    ///
1061    /// Only shortest paths from `sources` to `targets` are counted; scores are
1062    /// returned for `eids`. As in [`betweenness_subset`](Self::betweenness_subset),
1063    /// in undirected graphs each source-target pair has weight 1/2.
1064    /// Normalization is not implemented by igraph for this variant. Time complexity: O(|S||E|).
1065    ///
1066    /// Binds [`igraph_edge_betweenness_subset`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_edge_betweenness_subset).
1067    ///
1068    /// # Errors
1069    ///
1070    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
1071    /// invalid source or target vertex.
1072    ///
1073    /// # Examples
1074    ///
1075    /// ```
1076    /// use igraph::prelude::*;
1077    ///
1078    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, true).unwrap();
1079    /// assert_eq!(g.edge_betweenness_subset(None, 0, 2, .., true).unwrap(), vec![1.0, 1.0, 0.0]);
1080    /// ```
1081    pub fn edge_betweenness_subset<'a>(
1082        &self,
1083        weights: Option<&[f64]>,
1084        sources: impl Into<VertexSelector<'a>>,
1085        targets: impl Into<VertexSelector<'a>>,
1086        eids: impl Into<EdgeSelector<'a>>,
1087        directed: bool,
1088    ) -> Result<Vec<f64>> {
1089        let src = sources.into().to_raw()?;
1090        let tgt = targets.into().to_raw()?;
1091        let es = eids.into().to_raw()?;
1092        let w = weights_view(self, weights)?;
1093        let mut res = Vector::new();
1094        igraph_call!(igraph_edge_betweenness_subset(
1095            self,
1096            ptr_of(&w),
1097            &mut res,
1098            src.get(),
1099            tgt.get(),
1100            es.get(),
1101            directed,
1102            false
1103        ))?;
1104        Ok(res.into())
1105    }
1106
1107    // ------------------------------------------------------------------
1108    // PageRank
1109    // ------------------------------------------------------------------
1110
1111    /// Google PageRank of the selected vertices (`igraph_pagerank`).
1112    ///
1113    /// The PageRank of a vertex is the fraction of time a random walker
1114    /// spends on it. The walker follows out-edges with probabilities
1115    /// proportional to their `weights` (which must be non-negative), and at
1116    /// each step restarts from a uniformly random vertex with probability
1117    /// `1 - damping`; it also restarts when stuck in a sink vertex. Scores of
1118    /// all vertices sum to one. In undirected graphs PageRank tends to be
1119    /// proportional to degree as the damping approaches 1, so it is mostly
1120    /// useful for directed graphs. See [`PageRankOptions`] for the damping,
1121    /// directedness and algorithm.
1122    ///
1123    /// `vids` only selects the returned scores: all of them are computed
1124    /// anyway. Time complexity: usually O(|E|).
1125    ///
1126    /// Reference: S. Brin and L. Page, *The Anatomy of a Large-Scale
1127    /// Hypertextual Web Search Engine*, WWW7 (1998).
1128    ///
1129    /// Binds [`igraph_pagerank`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_pagerank).
1130    ///
1131    /// # Errors
1132    ///
1133    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
1134    /// invalid vertex, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
1135    /// for a damping outside `[0, 1]` (or `NaN`) or invalid (negative, `NaN`
1136    /// or infinite) weights. These are checked on the Rust side: igraph lets
1137    /// a `NaN` damping and infinite weights through, and then either aborts
1138    /// the process (ARPACK) or returns `NaN` scores (PRPACK). Huge finite
1139    /// weights are rescaled (PageRank does not depend on the scale of the
1140    /// weights).
1141    ///
1142    /// # Examples
1143    ///
1144    /// ```
1145    /// use igraph::prelude::*;
1146    /// use igraph::centrality::PageRankOptions;
1147    ///
1148    /// // A directed cycle: by symmetry every vertex has PageRank 1/4.
1149    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], 4, true).unwrap();
1150    /// let pr = g.pagerank(None, .., &PageRankOptions::default()).unwrap();
1151    /// assert_eq!(pr.value, 1.0);
1152    /// assert!(pr.scores.iter().all(|&p| (p - 0.25).abs() < 1e-12));
1153    /// ```
1154    pub fn pagerank<'a>(
1155        &self,
1156        weights: Option<&[f64]>,
1157        vids: impl Into<VertexSelector<'a>>,
1158        options: &PageRankOptions,
1159    ) -> Result<EigenScores> {
1160        let vs = vids.into().to_raw()?;
1161        check_damping(options.damping)?;
1162        let (scaled, _) = spectral_weights(weights)?;
1163        let w = weights_view(self, scaled.as_deref().or(weights))?;
1164        let mut res = Vector::new();
1165        let mut value = 0.0;
1166        // ARPACK keeps thread-local state: refuse to nest it (e.g. from an
1167        // interruption handler running inside another ARPACK computation).
1168        let _arpack = if options.algo == PageRankAlgo::Arpack {
1169            Some(crate::linalg::ArpackGuard::enter()?)
1170        } else {
1171            None
1172        };
1173        igraph_call!(igraph_pagerank(
1174            self,
1175            ptr_of(&w),
1176            &mut res,
1177            &mut value,
1178            options.damping,
1179            options.directed,
1180            vs.get(),
1181            options.algo.into(),
1182            std::ptr::null_mut()
1183        ))?;
1184        Ok(EigenScores {
1185            scores: res.into(),
1186            value,
1187        })
1188    }
1189
1190    /// Personalized PageRank with an arbitrary restart distribution
1191    /// (`igraph_personalized_pagerank`).
1192    ///
1193    /// Like [`pagerank`](Self::pagerank), but when the random walker restarts
1194    /// (with probability `1 - damping`, or when stuck in a sink), the new
1195    /// starting vertex is drawn from the distribution `reset` (one
1196    /// non-negative entry per vertex, not necessarily normalized) instead of
1197    /// uniformly. `reset = None` gives the ordinary PageRank.
1198    ///
1199    /// Binds [`igraph_personalized_pagerank`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_personalized_pagerank).
1200    ///
1201    /// # Errors
1202    ///
1203    /// As for [`pagerank`](Self::pagerank); also
1204    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `reset`
1205    /// has the wrong length, negative entries or sums to zero.
1206    ///
1207    /// # Examples
1208    ///
1209    /// ```
1210    /// use igraph::prelude::*;
1211    /// use igraph::centrality::PageRankOptions;
1212    ///
1213    /// // Always restarting from the leaf 0 of a path breaks its symmetry.
1214    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
1215    /// let reset = [1.0, 0.0, 0.0, 0.0];
1216    /// let pr = g.personalized_pagerank(None, Some(&reset), .., &PageRankOptions::default()).unwrap();
1217    /// assert!(pr.scores[0] > pr.scores[3] && pr.scores[1] > pr.scores[2]);
1218    /// ```
1219    pub fn personalized_pagerank<'a>(
1220        &self,
1221        weights: Option<&[f64]>,
1222        reset: Option<&[f64]>,
1223        vids: impl Into<VertexSelector<'a>>,
1224        options: &PageRankOptions,
1225    ) -> Result<EigenScores> {
1226        if let Some(r) = reset
1227            && r.len() != self.vcount()
1228        {
1229            return Err(Error::invalid(format!(
1230                "the reset vector has length {}, but the graph has {} vertices",
1231                r.len(),
1232                self.vcount()
1233            )));
1234        }
1235        let vs = vids.into().to_raw()?;
1236        check_damping(options.damping)?;
1237        let (scaled, _) = spectral_weights(weights)?;
1238        let w = weights_view(self, scaled.as_deref().or(weights))?;
1239        let r = reset.map(Vector::view);
1240        let mut res = Vector::new();
1241        let mut value = 0.0;
1242        // ARPACK keeps thread-local state: refuse to nest it (e.g. from an
1243        // interruption handler running inside another ARPACK computation).
1244        let _arpack = if options.algo == PageRankAlgo::Arpack {
1245            Some(crate::linalg::ArpackGuard::enter()?)
1246        } else {
1247            None
1248        };
1249        igraph_call!(igraph_personalized_pagerank(
1250            self,
1251            ptr_of(&w),
1252            &mut res,
1253            &mut value,
1254            ptr_of(&r),
1255            options.damping,
1256            options.directed,
1257            vs.get(),
1258            options.algo.into(),
1259            std::ptr::null_mut()
1260        ))?;
1261        Ok(EigenScores {
1262            scores: res.into(),
1263            value,
1264        })
1265    }
1266
1267    /// Personalized PageRank restarting from a set of vertices
1268    /// (`igraph_personalized_pagerank_vs`).
1269    ///
1270    /// Like [`personalized_pagerank`](Self::personalized_pagerank), with the
1271    /// restart vertex chosen uniformly among `reset_vids` (duplicates count
1272    /// multiple times). Restarting always from a single vertex gives a
1273    /// "proximity to this vertex" measure, widely used for recommendations.
1274    ///
1275    /// Binds [`igraph_personalized_pagerank_vs`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_personalized_pagerank_vs).
1276    ///
1277    /// # Errors
1278    ///
1279    /// As for [`pagerank`](Self::pagerank); an empty or invalid `reset_vids`
1280    /// is an error too.
1281    ///
1282    /// # Examples
1283    ///
1284    /// ```
1285    /// use igraph::prelude::*;
1286    /// use igraph::centrality::PageRankOptions;
1287    ///
1288    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
1289    /// let pr = g.personalized_pagerank_vs(None, 3, .., &PageRankOptions::default()).unwrap();
1290    /// assert!(pr.scores[3] > pr.scores[0]);
1291    /// ```
1292    pub fn personalized_pagerank_vs<'a>(
1293        &self,
1294        weights: Option<&[f64]>,
1295        reset_vids: impl Into<VertexSelector<'a>>,
1296        vids: impl Into<VertexSelector<'a>>,
1297        options: &PageRankOptions,
1298    ) -> Result<EigenScores> {
1299        let reset = reset_vids.into().to_raw()?;
1300        let vs = vids.into().to_raw()?;
1301        check_damping(options.damping)?;
1302        let (scaled, _) = spectral_weights(weights)?;
1303        let w = weights_view(self, scaled.as_deref().or(weights))?;
1304        let mut res = Vector::new();
1305        let mut value = 0.0;
1306        // ARPACK keeps thread-local state: refuse to nest it (e.g. from an
1307        // interruption handler running inside another ARPACK computation).
1308        let _arpack = if options.algo == PageRankAlgo::Arpack {
1309            Some(crate::linalg::ArpackGuard::enter()?)
1310        } else {
1311            None
1312        };
1313        igraph_call!(igraph_personalized_pagerank_vs(
1314            self,
1315            ptr_of(&w),
1316            &mut res,
1317            &mut value,
1318            reset.get(),
1319            options.damping,
1320            options.directed,
1321            vs.get(),
1322            options.algo.into(),
1323            std::ptr::null_mut()
1324        ))?;
1325        Ok(EigenScores {
1326            scores: res.into(),
1327            value,
1328        })
1329    }
1330
1331    // ------------------------------------------------------------------
1332    // Spectral centralities
1333    // ------------------------------------------------------------------
1334
1335    /// Eigenvector centrality of all vertices (`igraph_eigenvector_centrality`).
1336    ///
1337    /// The eigenvector centrality of a vertex is proportional to the sum of
1338    /// the centralities of its neighbors: it is the eigenvector of the
1339    /// adjacency matrix belonging to the largest positive eigenvalue, which is
1340    /// non-negative when weights are non-negative. Scores are scaled so that
1341    /// the maximum is 1 (unless all are zero). In undirected graphs a self-loop
1342    /// counts twice on the diagonal; weights of parallel edges add up.
1343    ///
1344    /// `mode` matters for directed graphs only: [`NeighborMode::Out`] (the
1345    /// standard choice) gives each vertex the sum of the centralities of the
1346    /// vertices *pointing to it* (left eigenvector); [`NeighborMode::In`] the
1347    /// sum over the vertices it points to; [`NeighborMode::All`] ignores
1348    /// directions.
1349    ///
1350    /// The measure is meaningful only for (strongly) connected graphs: in a
1351    /// disconnected undirected graph all but one component typically get
1352    /// zeros (igraph emits a warning). Directed acyclic graphs have no positive
1353    /// eigenvalue: the returned [`value`](EigenScores::value) is then zero.
1354    /// For directed graphs, consider
1355    /// [`hub_and_authority_scores`](Self::hub_and_authority_scores). Time
1356    /// complexity: usually O(|V| + |E|).
1357    ///
1358    /// See also [`connected_components`](Self::connected_components) to split
1359    /// a disconnected graph first (and [`is_dag`](Self::is_dag) to detect the
1360    /// acyclic case), and [`eigen_adjacency`](Self::eigen_adjacency) for other
1361    /// eigenpairs of the adjacency matrix.
1362    ///
1363    /// Binds [`igraph_eigenvector_centrality`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_eigenvector_centrality).
1364    ///
1365    /// # Errors
1366    ///
1367    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for invalid
1368    /// weights (wrong length, `NaN` or infinite: igraph does not check the
1369    /// latter and ARPACK would abort the process);
1370    /// [`ErrorKind::Arpack`](crate::ErrorKind::Arpack) if the eigensolver
1371    /// fails. Weights so large that the iterations could overflow (absolute
1372    /// sum above `1e150`) are divided by a power of two before the call, and
1373    /// the eigenvalue is scaled back (it may then be infinite, when it is not
1374    /// representable): the scores do not depend on the scale of the weights.
1375    ///
1376    /// # Examples
1377    ///
1378    /// ```
1379    /// use igraph::prelude::*;
1380    ///
1381    /// // Weighted star with center 0 and weights 1..9 (igraph's example).
1382    /// let edges: Vec<(i64, i64)> = (1..10).map(|i| (0, i)).collect();
1383    /// let g = Graph::from_edges(&edges, 10, false).unwrap();
1384    /// let w: Vec<f64> = (1..10).map(f64::from).collect();
1385    /// let ec = g.eigenvector_centrality(NeighborMode::Out, Some(&w)).unwrap();
1386    /// assert!((ec.value - 16.8819).abs() < 1e-4);
1387    /// assert_eq!(ec.scores[0], 1.0);
1388    /// assert!((ec.scores[1] - 0.0592349).abs() < 1e-6);
1389    /// ```
1390    pub fn eigenvector_centrality(
1391        &self,
1392        mode: NeighborMode,
1393        weights: Option<&[f64]>,
1394    ) -> Result<EigenScores> {
1395        let (scaled, scale) = spectral_weights(weights)?;
1396        let w = weights_view(self, scaled.as_deref().or(weights))?;
1397        let mut res = Vector::new();
1398        let mut value = 0.0;
1399        // ARPACK keeps thread-local state: refuse to nest it.
1400        let _arpack = crate::linalg::ArpackGuard::enter()?;
1401        igraph_call!(igraph_eigenvector_centrality(
1402            self,
1403            &mut res,
1404            &mut value,
1405            mode.into(),
1406            ptr_of(&w),
1407            std::ptr::null_mut()
1408        ))?;
1409        Ok(EigenScores {
1410            scores: res.into(),
1411            value: value * scale,
1412        })
1413    }
1414
1415    /// Kleinberg's hub and authority scores (HITS)
1416    /// (`igraph_hub_and_authority_scores`).
1417    ///
1418    /// The authority score of a vertex is proportional to the sum of the hub
1419    /// scores of the vertices pointing to it, and its hub score to the sum of
1420    /// the authority scores of the vertices it points to. Hubs and authorities
1421    /// are the principal eigenvectors of `A Aᵀ` and `Aᵀ A`; igraph guarantees
1422    /// that the two returned vectors match (`h = A a`, `a = Aᵀ h`, up to
1423    /// scaling) even when the eigenvalue is degenerate. Both are scaled to
1424    /// have maximum 1. Edge weights should be non-negative (igraph warns
1425    /// otherwise).
1426    ///
1427    /// In undirected graphs both vectors coincide with the [eigenvector
1428    /// centrality](Self::eigenvector_centrality) (computed by it directly,
1429    /// with a warning) and [`value`](HubAuthority::value) is the square of its
1430    /// eigenvalue. A graph without edges gives all-ones scores and value 0.
1431    ///
1432    /// In extremely sparse graphs, where no single connected component
1433    /// dominates the graphs of `A Aᵀ` and `Aᵀ A`, the solution is not unique
1434    /// and many scores are zero: igraph then emits a warning (retrieve it with
1435    /// [`take_warnings`](crate::error::take_warnings)) when *more than 30%* of
1436    /// the hub scores are zero (below `10 ε` in absolute value), on directed
1437    /// graphs with at least 10 vertices. (In igraph 1.0.0 a rounding bug made
1438    /// it warn for any zero score; this was fixed in 1.0.1.)
1439    ///
1440    /// Time complexity: usually O(|V|).
1441    ///
1442    /// Reference: J. Kleinberg, *Authoritative sources in a hyperlinked
1443    /// environment*, J. ACM 46 (1999).
1444    ///
1445    /// See also [`pagerank`](Self::pagerank), another random-walk based
1446    /// ranking for directed graphs.
1447    ///
1448    /// Binds [`igraph_hub_and_authority_scores`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_hub_and_authority_scores).
1449    ///
1450    /// # Errors
1451    ///
1452    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for invalid
1453    /// weights (wrong length, `NaN` or infinite: igraph does not check the
1454    /// latter and ARPACK would abort the process);
1455    /// [`ErrorKind::Arpack`](crate::ErrorKind::Arpack) if the eigensolver
1456    /// fails. Weights so large that the iterations could overflow (absolute
1457    /// sum above `1e150`) are divided by a power of two before the call, and
1458    /// the eigenvalue is scaled back (it may then be infinite, when it is not
1459    /// representable): the scores do not depend on the scale of the weights.
1460    ///
1461    /// # Examples
1462    ///
1463    /// ```
1464    /// use igraph::prelude::*;
1465    ///
1466    /// // Two pages linking to a third one: two perfect hubs, one authority.
1467    /// let g = Graph::from_edges(&[(0, 2), (1, 2)], 3, true).unwrap();
1468    /// let hits = g.hub_and_authority_scores(None).unwrap();
1469    /// assert_eq!(hits.hubs, vec![1.0, 1.0, 0.0]);
1470    /// assert_eq!(hits.authorities, vec![0.0, 0.0, 1.0]);
1471    /// assert!((hits.value - 2.0).abs() < 1e-9);
1472    /// ```
1473    pub fn hub_and_authority_scores(&self, weights: Option<&[f64]>) -> Result<HubAuthority> {
1474        let (scaled, scale) = spectral_weights(weights)?;
1475        let w = weights_view(self, scaled.as_deref().or(weights))?;
1476        let mut hubs = Vector::new();
1477        let mut auth = Vector::new();
1478        // Always request `value`: for undirected graphs igraph (1.0.0 and
1479        // 1.0.1) squares `*value` without checking it for NULL.
1480        let mut value = 0.0;
1481        // ARPACK keeps thread-local state: refuse to nest it.
1482        let _arpack = crate::linalg::ArpackGuard::enter()?;
1483        igraph_call!(igraph_hub_and_authority_scores(
1484            self,
1485            &mut hubs,
1486            &mut auth,
1487            &mut value,
1488            ptr_of(&w),
1489            std::ptr::null_mut()
1490        ))?;
1491        Ok(HubAuthority {
1492            hubs: hubs.into(),
1493            authorities: auth.into(),
1494            // The eigenvalue of `A Aᵀ` scales quadratically.
1495            value: value * scale * scale,
1496        })
1497    }
1498
1499    // ------------------------------------------------------------------
1500    // Structural holes and convergence
1501    // ------------------------------------------------------------------
1502
1503    /// Burt's constraint scores of the selected vertices (`igraph_constraint`).
1504    ///
1505    /// Constraint measures how much a vertex's ego network is closed: it is
1506    /// high when ego has few, or mutually strongly related (redundant),
1507    /// contacts, and low for vertices bridging *structural holes*. Formally
1508    /// `C[i] = Σ_{j ≠ i} (p[i,j] + Σ_{q ≠ i,j} p[i,q] p[q,j])²` over the
1509    /// neighbors `j`, with proportional tie strengths
1510    /// `p[i,j] = (a[i,j] + a[j,i]) / Σ_k (a[i,k] + a[k,i])`, `a` being the
1511    /// (weighted) adjacency matrix. It is undefined (NaN) for isolated
1512    /// vertices. Time complexity: O(|V| + |E| + n d²), `d` the average degree.
1513    ///
1514    /// Reference: R. S. Burt, *Structural holes and good ideas*, American
1515    /// Journal of Sociology 110, 349–399 (2004).
1516    ///
1517    /// See also [`transitivity_local_undirected`](Self::transitivity_local_undirected),
1518    /// the local clustering coefficient, a related measure of ego-network
1519    /// closure.
1520    ///
1521    /// Binds [`igraph_constraint`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_constraint).
1522    ///
1523    /// # Errors
1524    ///
1525    /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for an
1526    /// invalid vertex, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
1527    /// for weights of the wrong length.
1528    ///
1529    /// # Examples
1530    ///
1531    /// ```
1532    /// use igraph::prelude::*;
1533    ///
1534    /// // In a path, the end vertices are fully constrained by their only contact.
1535    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
1536    /// assert_eq!(g.constraint(.., None).unwrap(), vec![1.0, 0.5, 0.5, 1.0]);
1537    /// ```
1538    pub fn constraint<'a>(
1539        &self,
1540        vids: impl Into<VertexSelector<'a>>,
1541        weights: Option<&[f64]>,
1542    ) -> Result<Vec<f64>> {
1543        let vs = vids.into().to_raw()?;
1544        let w = weights_view(self, weights)?;
1545        let mut res = Vector::new();
1546        igraph_call!(igraph_constraint(self, &mut res, vs.get(), ptr_of(&w)))?;
1547        Ok(res.into())
1548    }
1549
1550    /// Convergence degree of every edge (`igraph_convergence_degree`).
1551    ///
1552    /// The *input set* of an edge is the set of vertices where the shortest
1553    /// paths passing through it originate, the *output set* where they
1554    /// terminate. The convergence degree is `(|in| − |out|) / (|in| + |out|)`,
1555    /// in `(-1, 1)`: positive values mark *convergent* edges (paths coming
1556    /// from many vertices and going to few), negative ones *divergent* edges.
1557    /// In undirected graphs the edge is oriented arbitrarily and the absolute
1558    /// value is reported. Time complexity: O(|V||E|).
1559    ///
1560    /// Binds [`igraph_convergence_degree`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_convergence_degree).
1561    ///
1562    /// # Examples
1563    ///
1564    /// ```
1565    /// use igraph::prelude::*;
1566    ///
1567    /// // An in-star 1,2,3,4 -> 0 followed by 0 -> 5 (igraph's unit test).
1568    /// let g = Graph::from_edges(&[(1, 0), (2, 0), (3, 0), (4, 0), (0, 5)], 6, true).unwrap();
1569    /// let cd = g.convergence_degree().unwrap();
1570    /// assert!((cd.result[4] - 2.0 / 3.0).abs() < 1e-9);
1571    /// ```
1572    pub fn convergence_degree(&self) -> Result<ConvergenceDegree> {
1573        let mut result = Vector::new();
1574        let mut ins = Vector::new();
1575        let mut outs = Vector::new();
1576        igraph_call!(igraph_convergence_degree(
1577            self,
1578            &mut result,
1579            &mut ins,
1580            &mut outs
1581        ))?;
1582        Ok(ConvergenceDegree {
1583            result: result.into(),
1584            ins: ins.into(),
1585            outs: outs.into(),
1586        })
1587    }
1588
1589    // ------------------------------------------------------------------
1590    // Centralization
1591    // ------------------------------------------------------------------
1592
1593    /// Degree centralization of the graph (`igraph_centralization_degree`).
1594    ///
1595    /// Computes the degrees of all vertices (with `mode` for directed graphs
1596    /// and the `loops` counting convention, see [`Loops`]) and their
1597    /// [`centralization`] index, normalized by the theoretical maximum (the
1598    /// star) if `normalized` is true. Time complexity: O(|V| + |E|).
1599    ///
1600    /// See also [`degree`](Self::degree) and [`maxdegree`](Self::maxdegree).
1601    ///
1602    /// Binds [`igraph_centralization_degree`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_degree).
1603    ///
1604    /// # Examples
1605    ///
1606    /// ```
1607    /// use igraph::prelude::*;
1608    ///
1609    /// // A cycle is perfectly decentralized.
1610    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], 4, false).unwrap();
1611    /// let c = g.centralization_degree(NeighborMode::All, Loops::None, true).unwrap();
1612    /// assert_eq!(c.scores, vec![2.0; 4]);
1613    /// assert_eq!(c.centralization, 0.0);
1614    /// assert_eq!(c.theoretical_max, 6.0);
1615    /// ```
1616    pub fn centralization_degree(
1617        &self,
1618        mode: NeighborMode,
1619        loops: Loops,
1620        normalized: bool,
1621    ) -> Result<Centralization> {
1622        let mut res = Vector::new();
1623        let (mut cent, mut tmax) = (0.0, 0.0);
1624        igraph_call!(igraph_centralization_degree(
1625            self,
1626            &mut res,
1627            mode.into(),
1628            loops.into(),
1629            &mut cent,
1630            &mut tmax,
1631            normalized
1632        ))?;
1633        Ok(Centralization {
1634            scores: res.into(),
1635            centralization: cent,
1636            theoretical_max: tmax,
1637        })
1638    }
1639
1640    /// Theoretical maximum of degree centralization for graphs with the
1641    /// size and directedness of `self` (`igraph_centralization_degree_tmax`).
1642    ///
1643    /// `mode` is ignored for undirected graphs. See the free function
1644    /// [`centralization_degree_tmax`] to specify the number of vertices
1645    /// directly.
1646    ///
1647    /// Binds [`igraph_centralization_degree_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_degree_tmax).
1648    pub fn centralization_degree_tmax(&self, mode: NeighborMode, loops: Loops) -> Result<f64> {
1649        let mut res = 0.0;
1650        igraph_call!(igraph_centralization_degree_tmax(
1651            self,
1652            0,
1653            mode.into(),
1654            loops.into(),
1655            &mut res
1656        ))?;
1657        Ok(res)
1658    }
1659
1660    /// Betweenness centralization of the graph
1661    /// (`igraph_centralization_betweenness`).
1662    ///
1663    /// Computes the (unweighted) [`betweenness`](Self::betweenness) of all
1664    /// vertices, following directions if `directed`, and its
1665    /// [`centralization`] index, normalized by the theoretical maximum (the
1666    /// star) if `normalized`. Time complexity: O(|V||E|).
1667    ///
1668    /// Binds [`igraph_centralization_betweenness`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_betweenness).
1669    ///
1670    /// # Examples
1671    ///
1672    /// ```
1673    /// use igraph::prelude::*;
1674    ///
1675    /// let star = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], 5, false).unwrap();
1676    /// let c = star.centralization_betweenness(false, true).unwrap();
1677    /// assert_eq!(c.centralization, 1.0);
1678    /// ```
1679    pub fn centralization_betweenness(
1680        &self,
1681        directed: bool,
1682        normalized: bool,
1683    ) -> Result<Centralization> {
1684        let mut res = Vector::new();
1685        let (mut cent, mut tmax) = (0.0, 0.0);
1686        igraph_call!(igraph_centralization_betweenness(
1687            self, &mut res, directed, &mut cent, &mut tmax, normalized
1688        ))?;
1689        Ok(Centralization {
1690            scores: res.into(),
1691            centralization: cent,
1692            theoretical_max: tmax,
1693        })
1694    }
1695
1696    /// Theoretical maximum of betweenness centralization for graphs with the
1697    /// size and directedness of `self`
1698    /// (`igraph_centralization_betweenness_tmax`).
1699    ///
1700    /// `directed` is ignored for undirected graphs. See the free function
1701    /// [`centralization_betweenness_tmax`] to give the number of vertices.
1702    ///
1703    /// Binds [`igraph_centralization_betweenness_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_betweenness_tmax).
1704    pub fn centralization_betweenness_tmax(&self, directed: bool) -> Result<f64> {
1705        let mut res = 0.0;
1706        igraph_call!(igraph_centralization_betweenness_tmax(
1707            self, 0, directed, &mut res
1708        ))?;
1709        Ok(res)
1710    }
1711
1712    /// Closeness centralization of the graph
1713    /// (`igraph_centralization_closeness`).
1714    ///
1715    /// Computes the (unweighted, normalized) [`closeness`](Self::closeness)
1716    /// of all vertices, using `mode` for directed graphs, and its
1717    /// [`centralization`] index, normalized by the theoretical maximum (the
1718    /// star) if `normalized`. Time complexity: O(|V||E|).
1719    ///
1720    /// Binds [`igraph_centralization_closeness`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_closeness).
1721    ///
1722    /// # Examples
1723    ///
1724    /// ```
1725    /// use igraph::prelude::*;
1726    ///
1727    /// let star = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], 5, false).unwrap();
1728    /// let c = star.centralization_closeness(NeighborMode::All, true).unwrap();
1729    /// assert!((c.centralization - 1.0).abs() < 1e-12);
1730    /// ```
1731    pub fn centralization_closeness(
1732        &self,
1733        mode: NeighborMode,
1734        normalized: bool,
1735    ) -> Result<Centralization> {
1736        let mut res = Vector::new();
1737        let (mut cent, mut tmax) = (0.0, 0.0);
1738        igraph_call!(igraph_centralization_closeness(
1739            self,
1740            &mut res,
1741            mode.into(),
1742            &mut cent,
1743            &mut tmax,
1744            normalized
1745        ))?;
1746        Ok(Centralization {
1747            scores: res.into(),
1748            centralization: cent,
1749            theoretical_max: tmax,
1750        })
1751    }
1752
1753    /// Theoretical maximum of closeness centralization for graphs with the
1754    /// size and directedness of `self` (`igraph_centralization_closeness_tmax`).
1755    ///
1756    /// `mode` is ignored for undirected graphs. See the free function
1757    /// [`centralization_closeness_tmax`] to give the number of vertices.
1758    ///
1759    /// Binds [`igraph_centralization_closeness_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_closeness_tmax).
1760    pub fn centralization_closeness_tmax(&self, mode: NeighborMode) -> Result<f64> {
1761        let mut res = 0.0;
1762        igraph_call!(igraph_centralization_closeness_tmax(
1763            self,
1764            0,
1765            mode.into(),
1766            &mut res
1767        ))?;
1768        Ok(res)
1769    }
1770
1771    /// Eigenvector centralization of the graph
1772    /// (`igraph_centralization_eigenvector_centrality`).
1773    ///
1774    /// Computes the (unweighted) [eigenvector
1775    /// centrality](Self::eigenvector_centrality) of all vertices, scaled so
1776    /// that the maximum is 1, and its [`centralization`] index, normalized by
1777    /// the theoretical maximum if `normalized`. Note that eigenvector scores
1778    /// have no natural scale, so the centralization depends on the choice of
1779    /// scaling by the maximum (∞-norm). The most centralized undirected graph
1780    /// is a single edge (plus isolated vertices). `mode` is as in
1781    /// [`eigenvector_centrality`](Self::eigenvector_centrality).
1782    ///
1783    /// Binds [`igraph_centralization_eigenvector_centrality`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_eigenvector_centrality).
1784    ///
1785    /// # Errors
1786    ///
1787    /// [`ErrorKind::Arpack`](crate::ErrorKind::Arpack) if the eigensolver fails.
1788    ///
1789    /// # Examples
1790    ///
1791    /// ```
1792    /// use igraph::prelude::*;
1793    ///
1794    /// let g = Graph::from_edges(&[(0, 1)], 10, false).unwrap();
1795    /// let c = g.centralization_eigenvector_centrality(NeighborMode::All, true).unwrap();
1796    /// assert!((c.centralization - 1.0).abs() < 1e-9);
1797    /// ```
1798    pub fn centralization_eigenvector_centrality(
1799        &self,
1800        mode: NeighborMode,
1801        normalized: bool,
1802    ) -> Result<EigenvectorCentralization> {
1803        let mut res = Vector::new();
1804        let (mut value, mut cent, mut tmax) = (0.0, 0.0, 0.0);
1805        // ARPACK keeps thread-local state: refuse to nest it.
1806        let _arpack = crate::linalg::ArpackGuard::enter()?;
1807        igraph_call!(igraph_centralization_eigenvector_centrality(
1808            self,
1809            &mut res,
1810            &mut value,
1811            mode.into(),
1812            std::ptr::null_mut(),
1813            &mut cent,
1814            &mut tmax,
1815            normalized
1816        ))?;
1817        Ok(EigenvectorCentralization {
1818            scores: res.into(),
1819            value,
1820            centralization: cent,
1821            theoretical_max: tmax,
1822        })
1823    }
1824
1825    /// Theoretical maximum of eigenvector centralization for graphs with the
1826    /// size and directedness of `self`
1827    /// (`igraph_centralization_eigenvector_centrality_tmax`).
1828    ///
1829    /// `mode` is ignored for undirected graphs. See the free function
1830    /// [`centralization_eigenvector_centrality_tmax`] to give the number of
1831    /// vertices.
1832    ///
1833    /// Binds [`igraph_centralization_eigenvector_centrality_tmax`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_centralization_eigenvector_centrality_tmax).
1834    pub fn centralization_eigenvector_centrality_tmax(&self, mode: NeighborMode) -> Result<f64> {
1835        let mut res = 0.0;
1836        igraph_call!(igraph_centralization_eigenvector_centrality_tmax(
1837            self,
1838            0,
1839            mode.into(),
1840            &mut res
1841        ))?;
1842        Ok(res)
1843    }
1844
1845    // ------------------------------------------------------------------
1846    // Local scan statistics
1847    // ------------------------------------------------------------------
1848
1849    /// Local scan statistic with `k = 0` (`igraph_local_scan_0`).
1850    ///
1851    /// By convention the 0-scan of a vertex is its degree (`weights = None`)
1852    /// or its strength (the sum of incident edge weights, as computed by
1853    /// [`strength`](Self::strength)). `mode` selects out-, in- or all edges in
1854    /// directed graphs.
1855    ///
1856    /// Reference: C. E. Priebe et al., *Scan Statistics on Enron Graphs*,
1857    /// Comput. Math. Organ. Theory (2005).
1858    ///
1859    /// Binds [`igraph_local_scan_0`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_0).
1860    ///
1861    /// # Examples
1862    ///
1863    /// ```
1864    /// use igraph::prelude::*;
1865    ///
1866    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
1867    /// assert_eq!(g.local_scan_0(None, NeighborMode::All).unwrap(), vec![1.0, 2.0, 1.0]);
1868    /// assert_eq!(g.local_scan_0(Some(&[0.5, 2.0]), NeighborMode::All).unwrap(), vec![0.5, 2.5, 2.0]);
1869    /// ```
1870    pub fn local_scan_0(&self, weights: Option<&[f64]>, mode: NeighborMode) -> Result<Vec<f64>> {
1871        let w = weights_view(self, weights)?;
1872        let mut res = Vector::new();
1873        igraph_call!(igraph_local_scan_0(self, &mut res, ptr_of(&w), mode.into()))?;
1874        Ok(res.into())
1875    }
1876
1877    /// "Them" local scan statistic with `k = 0` (`igraph_local_scan_0_them`).
1878    ///
1879    /// Neighborhoods are taken from `self` ("us"), but edges are counted (or
1880    /// their `weights_them` summed) in the graph `them`, which must have the
1881    /// same vertices and directedness: the result is, for every vertex, the
1882    /// number of `them` edges incident to it that also connect it in `us`.
1883    /// Useful to compare two snapshots of a network.
1884    ///
1885    /// Binds [`igraph_local_scan_0_them`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_0_them).
1886    ///
1887    /// # Errors
1888    ///
1889    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the two
1890    /// graphs differ in size or directedness, or `weights_them` does not have
1891    /// one entry per edge of `them`.
1892    ///
1893    /// # Examples
1894    ///
1895    /// ```
1896    /// use igraph::prelude::*;
1897    ///
1898    /// let us = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
1899    /// let them = Graph::from_edges(&[(0, 1), (0, 2)], 3, false).unwrap();
1900    /// assert_eq!(us.local_scan_0_them(&them, None, NeighborMode::All).unwrap(), vec![1.0, 1.0, 0.0]);
1901    /// ```
1902    pub fn local_scan_0_them(
1903        &self,
1904        them: &Graph,
1905        weights_them: Option<&[f64]>,
1906        mode: NeighborMode,
1907    ) -> Result<Vec<f64>> {
1908        check_them(self, them)?;
1909        let w = weights_view(them, weights_them)?;
1910        let mut res = Vector::new();
1911        igraph_call!(igraph_local_scan_0_them(
1912            self,
1913            them,
1914            &mut res,
1915            ptr_of(&w),
1916            mode.into()
1917        ))?;
1918        Ok(res.into())
1919    }
1920
1921    /// Local scan statistic with `k = 1` (`igraph_local_scan_1_ecount`).
1922    ///
1923    /// For every vertex, the number of edges (or the sum of their weights) in
1924    /// the subgraph induced by its closed 1-neighborhood (the vertex and its
1925    /// neighbors along `mode`). For undirected simple graphs this is
1926    /// `degree + number of triangles` through the vertex (see
1927    /// [`count_adjacent_triangles`](Self::count_adjacent_triangles)).
1928    ///
1929    /// Binds [`igraph_local_scan_1_ecount`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_1_ecount).
1930    ///
1931    /// # Examples
1932    ///
1933    /// ```
1934    /// use igraph::prelude::*;
1935    ///
1936    /// // A triangle with a pendant vertex 3 attached to 2.
1937    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], 4, false).unwrap();
1938    /// assert_eq!(g.local_scan_1_ecount(None, NeighborMode::All).unwrap(), vec![3.0, 3.0, 4.0, 1.0]);
1939    /// ```
1940    pub fn local_scan_1_ecount(
1941        &self,
1942        weights: Option<&[f64]>,
1943        mode: NeighborMode,
1944    ) -> Result<Vec<f64>> {
1945        let w = weights_view(self, weights)?;
1946        let mut res = Vector::new();
1947        igraph_call!(igraph_local_scan_1_ecount(
1948            self,
1949            &mut res,
1950            ptr_of(&w),
1951            mode.into()
1952        ))?;
1953        Ok(res.into())
1954    }
1955
1956    /// "Them" local scan statistic with `k = 1`
1957    /// (`igraph_local_scan_1_ecount_them`).
1958    ///
1959    /// For every vertex, the number of edges of `them` (or the sum of their
1960    /// `weights_them`) inside the closed 1-neighborhood of the vertex in
1961    /// `self`. The graphs must have the same vertices and directedness.
1962    ///
1963    /// Binds [`igraph_local_scan_1_ecount_them`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_1_ecount_them).
1964    ///
1965    /// # Errors
1966    ///
1967    /// As for [`local_scan_0_them`](Self::local_scan_0_them).
1968    pub fn local_scan_1_ecount_them(
1969        &self,
1970        them: &Graph,
1971        weights_them: Option<&[f64]>,
1972        mode: NeighborMode,
1973    ) -> Result<Vec<f64>> {
1974        check_them(self, them)?;
1975        let w = weights_view(them, weights_them)?;
1976        let mut res = Vector::new();
1977        self.with_fresh_multi_cache(|| {
1978            igraph_call!(igraph_local_scan_1_ecount_them(
1979                self,
1980                them,
1981                &mut res,
1982                ptr_of(&w),
1983                mode.into()
1984            ))
1985        })?;
1986        Ok(res.into())
1987    }
1988
1989    /// Local scan statistic for `k`-neighborhoods (`igraph_local_scan_k_ecount`).
1990    ///
1991    /// For every vertex, the number of edges (or the sum of their weights) in
1992    /// the subgraph induced by the vertices within distance `k` along `mode`.
1993    /// `k = 0` is special-cased to [`local_scan_0`](Self::local_scan_0) (degree
1994    /// or strength), `k = 1` to [`local_scan_1_ecount`](Self::local_scan_1_ecount).
1995    /// The neighborhoods themselves are given by
1996    /// [`neighborhood`](Self::neighborhood) with `order = k`.
1997    ///
1998    /// Binds [`igraph_local_scan_k_ecount`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_k_ecount).
1999    ///
2000    /// # Errors
2001    ///
2002    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for weights
2003    /// of the wrong length.
2004    ///
2005    /// # Examples
2006    ///
2007    /// ```
2008    /// use igraph::prelude::*;
2009    ///
2010    /// // In a path of 5 vertices, the 2-neighborhood of the center is everything.
2011    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], 5, false).unwrap();
2012    /// assert_eq!(g.local_scan_k_ecount(2, None, NeighborMode::All).unwrap(), vec![2.0, 3.0, 4.0, 3.0, 2.0]);
2013    /// ```
2014    pub fn local_scan_k_ecount(
2015        &self,
2016        k: usize,
2017        weights: Option<&[f64]>,
2018        mode: NeighborMode,
2019    ) -> Result<Vec<f64>> {
2020        let k = igraph_int_t::try_from(k).map_err(|_| Error::invalid("k is too large"))?;
2021        let w = weights_view(self, weights)?;
2022        let mut res = Vector::new();
2023        igraph_call!(igraph_local_scan_k_ecount(
2024            self,
2025            k,
2026            &mut res,
2027            ptr_of(&w),
2028            mode.into()
2029        ))?;
2030        Ok(res.into())
2031    }
2032
2033    /// "Them" local scan statistic for `k`-neighborhoods
2034    /// (`igraph_local_scan_k_ecount_them`).
2035    ///
2036    /// For every vertex, the number of edges of `them` (or the sum of their
2037    /// `weights_them`) inside the `k`-neighborhood of the vertex computed in
2038    /// `self`. The graphs must have the same vertices and directedness.
2039    ///
2040    /// Binds [`igraph_local_scan_k_ecount_them`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_k_ecount_them).
2041    ///
2042    /// # Errors
2043    ///
2044    /// As for [`local_scan_0_them`](Self::local_scan_0_them).
2045    pub fn local_scan_k_ecount_them(
2046        &self,
2047        them: &Graph,
2048        k: usize,
2049        weights_them: Option<&[f64]>,
2050        mode: NeighborMode,
2051    ) -> Result<Vec<f64>> {
2052        check_them(self, them)?;
2053        let k = igraph_int_t::try_from(k).map_err(|_| Error::invalid("k is too large"))?;
2054        let w = weights_view(them, weights_them)?;
2055        let mut res = Vector::new();
2056        self.with_fresh_multi_cache(|| {
2057            igraph_call!(igraph_local_scan_k_ecount_them(
2058                self,
2059                them,
2060                k,
2061                &mut res,
2062                ptr_of(&w),
2063                mode.into()
2064            ))
2065        })?;
2066        Ok(res.into())
2067    }
2068
2069    /// Edge counts in the subgraphs induced by arbitrary vertex subsets
2070    /// (`igraph_local_scan_subset_ecount`).
2071    ///
2072    /// Returns, for each subset, the number of edges (or the sum of their
2073    /// weights) of the subgraph it induces. Multi-edges and self-loops count
2074    /// (a loop counts once). Each subset should be a *set*: igraph does not
2075    /// deduplicate, so a repeated vertex makes its incident edges count more
2076    /// than once. Without weights, the count for a duplicate-free subset
2077    /// equals the edge count of the corresponding
2078    /// [`induced_subgraph`](Self::induced_subgraph). Time complexity:
2079    /// O(Σ_S Σ_{v ∈ S} deg(v)).
2080    ///
2081    /// Binds [`igraph_local_scan_subset_ecount`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_subset_ecount).
2082    ///
2083    /// # Errors
2084    ///
2085    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for an
2086    /// invalid vertex in a subset (igraph reports it as `IGRAPH_EINVAL`, not as
2087    /// an invalid vertex id) or for weights of the wrong length.
2088    ///
2089    /// # Examples
2090    ///
2091    /// ```
2092    /// use igraph::prelude::*;
2093    ///
2094    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], 4, false).unwrap();
2095    /// let counts = g.local_scan_subset_ecount(None, &[vec![0, 1, 2], vec![2, 3], vec![]]).unwrap();
2096    /// assert_eq!(counts, vec![3.0, 1.0, 0.0]);
2097    /// ```
2098    pub fn local_scan_subset_ecount<S: AsRef<[VertexId]>>(
2099        &self,
2100        weights: Option<&[f64]>,
2101        subsets: &[S],
2102    ) -> Result<Vec<f64>> {
2103        let w = weights_view(self, weights)?;
2104        let list: VectorIntList = subsets.iter().map(|s| s.as_ref()).collect();
2105        let mut res = Vector::new();
2106        igraph_call!(igraph_local_scan_subset_ecount(
2107            self,
2108            &mut res,
2109            ptr_of(&w),
2110            &list
2111        ))?;
2112        Ok(res.into())
2113    }
2114
2115    /// Edge counts in precomputed neighborhoods, one per vertex
2116    /// (`igraph_local_scan_neighborhood_ecount`).
2117    ///
2118    /// Like [`local_scan_subset_ecount`](Self::local_scan_subset_ecount), but
2119    /// `neighborhoods` must contain exactly one vertex set per vertex of the
2120    /// graph. The C documentation marks this function as deprecated in favor
2121    /// of `igraph_local_scan_subset_ecount` since igraph 0.10, hence the
2122    /// Rust `#[deprecated]` attribute.
2123    ///
2124    /// Binds [`igraph_local_scan_neighborhood_ecount`](https://igraph.org/c/html/latest/igraph-Structural.html#igraph_local_scan_neighborhood_ecount).
2125    ///
2126    /// # Errors
2127    ///
2128    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
2129    /// number of neighborhoods differs from the number of vertices, or as for
2130    /// [`local_scan_subset_ecount`](Self::local_scan_subset_ecount).
2131    #[deprecated(
2132        since = "1.0.1",
2133        note = "deprecated in igraph 0.10: use `local_scan_subset_ecount`"
2134    )]
2135    pub fn local_scan_neighborhood_ecount<S: AsRef<[VertexId]>>(
2136        &self,
2137        weights: Option<&[f64]>,
2138        neighborhoods: &[S],
2139    ) -> Result<Vec<f64>> {
2140        let w = weights_view(self, weights)?;
2141        let list: VectorIntList = neighborhoods.iter().map(|s| s.as_ref()).collect();
2142        let mut res = Vector::new();
2143        igraph_call!(igraph_local_scan_neighborhood_ecount(
2144            self,
2145            &mut res,
2146            ptr_of(&w),
2147            &list
2148        ))?;
2149        Ok(res.into())
2150    }
2151}