• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / rustworkx / 14309387050

07 Apr 2025 12:38PM UTC coverage: 95.842% (+0.5%) from 95.34%
14309387050

Pull #1392

github

web-flow
Merge 39ac17cbb into 51b7c7282
Pull Request #1392: Add parallelism to `cloness_centrality`

79 of 82 new or added lines in 2 files covered. (96.34%)

23 existing lines in 1 file now uncovered.

18647 of 19456 relevant lines covered (95.84%)

1088451.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.04
/src/centrality.rs
1
// Licensed under the Apache License, Version 2.0 (the "License"); you may
2
// not use this file except in compliance with the License. You may obtain
3
// a copy of the License at
4
//
5
//     http://www.apache.org/licenses/LICENSE-2.0
6
//
7
// Unless required by applicable law or agreed to in writing, software
8
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
// License for the specific language governing permissions and limitations
11
// under the License.
12

13
#![allow(clippy::too_many_arguments)]
14

15
use std::convert::TryFrom;
16

17
use crate::digraph;
18
use crate::graph;
19
use crate::iterators::{CentralityMapping, EdgeCentralityMapping};
20
use crate::CostFn;
21
use crate::FailedToConverge;
22

23
use hashbrown::HashMap;
24
use petgraph::graph::NodeIndex;
25
use petgraph::visit::EdgeIndexable;
26
use petgraph::visit::EdgeRef;
27
use petgraph::visit::IntoNodeIdentifiers;
28
use pyo3::exceptions::PyValueError;
29
use pyo3::prelude::*;
30
use rustworkx_core::centrality;
31

32
/// Compute the betweenness centrality of all nodes in a PyGraph.
33
///
34
/// Betweenness centrality of a node :math:`v` is the sum of the
35
/// fraction of all-pairs shortest paths that pass through :math:`v`
36
///
37
/// .. math::
38
///
39
///    c_B(v) =\sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
40
///
41
/// where :math:`V` is the set of nodes, :math:`\sigma(s, t)` is the number of
42
/// shortest :math:`(s, t)` paths, and :math:`\sigma(s, t|v)` is the number of
43
/// those paths  passing through some  node :math:`v` other than :math:`s, t`.
44
/// If :math:`s = t`, :math:`\sigma(s, t) = 1`, and if :math:`v \in {s, t}`,
45
/// :math:`\sigma(s, t|v) = 0`
46
///
47
/// The algorithm used in this function is based on:
48
///
49
/// Ulrik Brandes, A Faster Algorithm for Betweenness Centrality.
50
/// Journal of Mathematical Sociology 25(2):163-177, 2001.
51
///
52
/// This function is multithreaded and will run in parallel if the number
53
/// of nodes in the graph is above the value of ``parallel_threshold`` (it
54
/// defaults to 50). If the function will be running in parallel the env var
55
/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used.
56
///
57
/// See Also
58
/// --------
59
/// :func:`~rustworkx.graph_edge_betweenness_centrality`
60
///
61
/// :param PyGraph graph: The input graph
62
/// :param bool normalized: Whether to normalize the betweenness scores by the number of distinct
63
///    paths between all pairs of nodes.
64
/// :param bool endpoints: Whether to include the endpoints of paths in pathlengths used to
65
///    compute the betweenness.
66
/// :param int parallel_threshold: The number of nodes to calculate the
67
///     the betweenness centrality in parallel at if the number of nodes in
68
///     the graph is less than this value it will run in a single thread. The
69
///     default value is 50
70
///
71
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
72
///      betweenness score for each node.
73
/// :rtype: CentralityMapping
74
#[pyfunction(
14✔
75
    signature = (
14✔
76
        graph,
14✔
77
        normalized=true,
14✔
78
        endpoints=false,
14✔
79
        parallel_threshold=50
14✔
80
    )
14✔
81
)]
14✔
82
#[pyo3(text_signature = "(graph, /, normalized=True, endpoints=False, parallel_threshold=50)")]
83
pub fn graph_betweenness_centrality(
14✔
84
    graph: &graph::PyGraph,
14✔
85
    normalized: bool,
14✔
86
    endpoints: bool,
14✔
87
    parallel_threshold: usize,
14✔
88
) -> CentralityMapping {
14✔
89
    let betweenness =
14✔
90
        centrality::betweenness_centrality(&graph.graph, endpoints, normalized, parallel_threshold);
14✔
91
    CentralityMapping {
14✔
92
        centralities: betweenness
14✔
93
            .into_iter()
14✔
94
            .enumerate()
14✔
95
            .filter_map(|(i, v)| v.map(|x| (i, x)))
74✔
96
            .collect(),
14✔
97
    }
14✔
98
}
14✔
99

100
/// Compute the betweenness centrality of all nodes in a PyDiGraph.
101
///
102
/// Betweenness centrality of a node :math:`v` is the sum of the
103
/// fraction of all-pairs shortest paths that pass through :math:`v`
104
///
105
/// .. math::
106
///
107
///    c_B(v) =\sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
108
///
109
/// where :math:`V` is the set of nodes, :math:`\sigma(s, t)` is the number of
110
/// shortest :math:`(s, t)` paths, and :math:`\sigma(s, t|v)` is the number of
111
/// those paths  passing through some  node :math:`v` other than :math:`s, t`.
112
/// If :math:`s = t`, :math:`\sigma(s, t) = 1`, and if :math:`v \in {s, t}`,
113
/// :math:`\sigma(s, t|v) = 0`
114
///
115
/// The algorithm used in this function is based on:
116
///
117
/// Ulrik Brandes, A Faster Algorithm for Betweenness Centrality.
118
/// Journal of Mathematical Sociology 25(2):163-177, 2001.
119
///
120
/// This function is multithreaded and will run in parallel if the number
121
/// of nodes in the graph is above the value of ``parallel_threshold`` (it
122
/// defaults to 50). If the function will be running in parallel the env var
123
/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used.
124
///
125
/// See Also
126
/// --------
127
/// :func:`~rustworkx.digraph_edge_betweenness_centrality`
128
///
129
/// :param PyDiGraph graph: The input graph
130
/// :param bool normalized: Whether to normalize the betweenness scores by the number of distinct
131
///    paths between all pairs of nodes.
132
/// :param bool endpoints: Whether to include the endpoints of paths in pathlengths used to
133
///    compute the betweenness.
134
/// :param int parallel_threshold: The number of nodes to calculate the
135
///     the betweenness centrality in parallel at if the number of nodes in
136
///     the graph is less than this value it will run in a single thread. The
137
///     default value is 50
138
///
139
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
140
///      betweenness score for each node.
141
/// :rtype: CentralityMapping
142
#[pyfunction(
20✔
143
    signature = (
20✔
144
        graph,
20✔
145
        normalized=true,
20✔
146
        endpoints=false,
20✔
147
        parallel_threshold=50
20✔
148
    )
20✔
149
)]
20✔
150
#[pyo3(text_signature = "(graph, /, normalized=True, endpoints=False, parallel_threshold=50)")]
151
pub fn digraph_betweenness_centrality(
20✔
152
    graph: &digraph::PyDiGraph,
20✔
153
    normalized: bool,
20✔
154
    endpoints: bool,
20✔
155
    parallel_threshold: usize,
20✔
156
) -> CentralityMapping {
20✔
157
    let betweenness =
20✔
158
        centrality::betweenness_centrality(&graph.graph, endpoints, normalized, parallel_threshold);
20✔
159
    CentralityMapping {
20✔
160
        centralities: betweenness
20✔
161
            .into_iter()
20✔
162
            .enumerate()
20✔
163
            .filter_map(|(i, v)| v.map(|x| (i, x)))
98✔
164
            .collect(),
20✔
165
    }
20✔
166
}
20✔
167

168
/// Compute the degree centrality for nodes in a PyGraph.
169
///
170
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
171
///
172
/// :param PyGraph graph: The input graph
173
///
174
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
175
///      centrality score for each node.
176
/// :rtype: CentralityMapping
177
#[pyfunction(signature = (graph,))]
10✔
178
#[pyo3(text_signature = "(graph, /,)")]
179
pub fn graph_degree_centrality(graph: &graph::PyGraph) -> PyResult<CentralityMapping> {
10✔
180
    let centrality = centrality::degree_centrality(&graph.graph, None);
10✔
181

10✔
182
    Ok(CentralityMapping {
10✔
183
        centralities: graph
10✔
184
            .graph
10✔
185
            .node_indices()
10✔
186
            .map(|i| (i.index(), centrality[i.index()]))
34✔
187
            .collect(),
10✔
188
    })
10✔
189
}
10✔
190

191
/// Compute the degree centrality for nodes in a PyDiGraph.
192
///
193
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
194
/// This function computes the TOTAL (in + out) degree centrality.
195
///
196
/// :param PyDiGraph graph: The input graph
197
///
198
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
199
///      centrality score for each node.
200
/// :rtype: CentralityMapping
201
#[pyfunction(signature = (graph,))]
6✔
202
#[pyo3(text_signature = "(graph, /,)")]
203
pub fn digraph_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
6✔
204
    let centrality = centrality::degree_centrality(&graph.graph, None);
6✔
205

6✔
206
    Ok(CentralityMapping {
6✔
207
        centralities: graph
6✔
208
            .graph
6✔
209
            .node_indices()
6✔
210
            .map(|i| (i.index(), centrality[i.index()]))
28✔
211
            .collect(),
6✔
212
    })
6✔
213
}
6✔
214
/// Compute the in-degree centrality for nodes in a PyDiGraph.
215
///
216
/// In-degree centrality assigns an importance score based on the number of incoming edges
217
/// to each node.
218
///
219
/// :param PyDiGraph graph: The input graph
220
///
221
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
222
///      centrality score for each node.
223
/// :rtype: CentralityMapping
224
#[pyfunction(signature = (graph,))]
4✔
225
#[pyo3(text_signature = "(graph, /)")]
226
pub fn in_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
4✔
227
    let centrality =
4✔
228
        centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Incoming));
4✔
229

4✔
230
    Ok(CentralityMapping {
4✔
231
        centralities: graph
4✔
232
            .graph
4✔
233
            .node_indices()
4✔
234
            .map(|i| (i.index(), centrality[i.index()]))
18✔
235
            .collect(),
4✔
236
    })
4✔
237
}
4✔
238

239
/// Compute the out-degree centrality for nodes in a PyDiGraph.
240
///
241
/// Out-degree centrality assigns an importance score based on the number of outgoing edges
242
/// from each node.
243
///
244
/// :param PyDiGraph graph: The input graph
245
///
246
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
247
///      centrality score for each node.
248
/// :rtype: CentralityMapping
249
#[pyfunction(signature = (graph,))]
4✔
250
#[pyo3(text_signature = "(graph, /,)")]
251
pub fn out_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
4✔
252
    let centrality =
4✔
253
        centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Outgoing));
4✔
254

4✔
255
    Ok(CentralityMapping {
4✔
256
        centralities: graph
4✔
257
            .graph
4✔
258
            .node_indices()
4✔
259
            .map(|i| (i.index(), centrality[i.index()]))
18✔
260
            .collect(),
4✔
261
    })
4✔
262
}
4✔
263

264
/// Compute the closeness centrality of each node in a :class:`~.PyGraph` object.
265
///
266
/// The closeness centrality of a node :math:`u` is defined as the
267
/// reciprocal of the average shortest path distance to :math:`u` over all
268
/// :math:`n-1` reachable nodes in the graph. In its general form this can
269
/// be expressed as:
270
///
271
/// .. math::
272
///
273
///     C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
274
///
275
/// where:
276
///
277
///   * :math:`d(v, u)` - the shortest-path distance between :math:`v` and
278
///     :math:`u`
279
///   * :math:`n` - the number of nodes that can reach :math:`u`.
280
///
281
/// In the case of a graphs with more than one connected component there is
282
/// an alternative improved formula that calculates the closeness centrality
283
/// as "a ratio of the fraction of actors in the group who are reachable, to
284
/// the average distance" [WF]_. This can be expressed as
285
///
286
/// .. math::
287
///
288
///     C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
289
///
290
/// where :math:`N` is the number of nodes in the graph. This alternative
291
/// formula can be used with the ``wf_improved`` argument.
292
///
293
/// :param PyGraph graph: The input graph. Can either be a
294
///     :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
295
/// :param bool wf_improved: This is optional; the default is True. If True,
296
///     scale by the fraction of nodes reachable.
297
/// :param int parallel_threshold: The number of nodes to calculate the
298
///     the betweenness centrality in parallel at if the number of nodes in
299
///     the graph is less than this value it will run in a single thread. The
300
///     default value is 50
301
///
302
/// :returns: A dictionary mapping each node index to its closeness centrality.
303
/// :rtype: CentralityMapping
304
#[pyfunction(signature = (graph, wf_improved=true, parallel_threshold=50))]
10✔
305
pub fn graph_closeness_centrality(
10✔
306
    graph: &graph::PyGraph,
10✔
307
    wf_improved: bool,
10✔
308
    parallel_threshold: usize,
10✔
309
) -> CentralityMapping {
10✔
310
    let closeness = centrality::closeness_centrality(&graph.graph, wf_improved, parallel_threshold);
10✔
311
    CentralityMapping {
10✔
312
        centralities: closeness
10✔
313
            .into_iter()
10✔
314
            .enumerate()
10✔
315
            .filter_map(|(i, v)| v.map(|x| (i, x)))
50✔
316
            .collect(),
10✔
317
    }
10✔
318
}
10✔
319

320
/// Compute the closeness centrality of each node in a :class:`~.PyDiGraph` object.
321
///
322
/// The closeness centrality of a node :math:`u` is defined as the
323
/// reciprocal of the average shortest path distance to :math:`u` over all
324
/// :math:`n-1` reachable nodes in the graph. In its general form this can
325
/// be expressed as:
326
///
327
/// .. math::
328
///
329
///     C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
330
///
331
/// where:
332
///
333
///   * :math:`d(v, u)` - the shortest-path distance between :math:`v` and
334
///     :math:`u`
335
///   * :math:`n` - the number of nodes that can reach :math:`u`.
336
///
337
/// In the case of a graphs with more than one connected component there is
338
/// an alternative improved formula that calculates the closeness centrality
339
/// as "a ratio of the fraction of actors in the group who are reachable, to
340
/// the average distance" [WF]_. This can be expressed as
341
///
342
/// .. math::
343
///
344
///     C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
345
///
346
/// where :math:`N` is the number of nodes in the graph. This alternative
347
/// formula can be used with the ``wf_improved`` argument.
348
///
349
/// :param PyDiGraph graph: The input graph. Can either be a
350
///     :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
351
/// :param bool wf_improved: This is optional; the default is True. If True,
352
///     scale by the fraction of nodes reachable.
353
/// :param int parallel_threshold: The number of nodes to calculate the
354
///     the betweenness centrality in parallel at if the number of nodes in
355
///     the graph is less than this value it will run in a single thread. The
356
///     default value is 50
357
///
358
/// :returns: A dictionary mapping each node index to its closeness centrality.
359
/// :rtype: CentralityMapping
360
#[pyfunction(signature = (graph, wf_improved=true, parallel_threshold=50))]
10✔
361
pub fn digraph_closeness_centrality(
10✔
362
    graph: &digraph::PyDiGraph,
10✔
363
    wf_improved: bool,
10✔
364
    parallel_threshold: usize,
10✔
365
) -> CentralityMapping {
10✔
366
    let closeness = centrality::closeness_centrality(&graph.graph, wf_improved, parallel_threshold);
10✔
367
    CentralityMapping {
10✔
368
        centralities: closeness
10✔
369
            .into_iter()
10✔
370
            .enumerate()
10✔
371
            .filter_map(|(i, v)| v.map(|x| (i, x)))
50✔
372
            .collect(),
10✔
373
    }
10✔
374
}
10✔
375

376
/// Compute the weighted closeness centrality of each node in the graph.
377
///
378
/// The weighted closeness centrality is an extension of the standard closeness
379
/// centrality measure where edge weights represent connection strength rather
380
/// than distance. To properly compute shortest paths, weights are inverted
381
/// so that stronger connections correspond to shorter effective distances.
382
/// The algorithm follows the method described by Newman (2001) in analyzing
383
/// weighted graphs.[Newman]
384
///
385
/// The edges originally represent connection strength between nodes.
386
/// The idea is that if two nodes have a strong connection, the computed
387
/// distance between them should be small (shorter), and vice versa.
388
/// Note that this assume that the graph is modelling a measure of
389
/// connection strength (e.g. trust, collaboration, or similarity).
390
/// If the graph is not modelling a measure of connection strength,
391
/// the function `weight_fn` should invert the weights before calling this
392
/// function, if not it is considered as a logical error.
393
///
394
/// In the case of a graphs with more than one connected component there is
395
/// an alternative improved formula that calculates the closeness centrality
396
/// as "a ratio of the fraction of actors in the group who are reachable, to
397
/// the average distance".[WF]
398
/// You can enable this by setting `wf_improved` to `true`.
399
///
400
/// :param PyGraph graph: The input graph. Can either be a
401
///     :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
402
/// :param weight_fn: An optional input callable that will be passed the edge's
403
///    payload object and is expected to return a `float` weight for that edge.
404
///    If this is not specified ``default_weight`` will be used as the weight
405
///    for every edge in ``graph``
406
/// :param bool wf_improved: This is optional; the default is True. If True,
407
///   scale by the fraction of nodes reachable.
408
/// :param float default_weight: If ``weight_fn`` is not set the default weight
409
///     value to use for the weight of all edges
410
/// :param int parallel_threshold: The number of nodes to calculate the
411
///     the betweenness centrality in parallel at if the number of nodes in
412
///     the graph is less than this value it will run in a single thread. The
413
///     default value is 50
414
///
415
/// :returns: A dictionary mapping each node index to its closeness centrality.
416
/// :rtype: CentralityMapping
417
#[pyfunction(signature = (graph, weight_fn=None, wf_improved=true, default_weight = 1.0, parallel_threshold=50))]
4✔
418
pub fn graph_newman_weighted_closeness_centrality(
4✔
419
    py: Python,
4✔
420
    graph: &graph::PyGraph,
4✔
421
    weight_fn: Option<PyObject>,
4✔
422
    wf_improved: bool,
4✔
423
    default_weight: f64,
4✔
424
    parallel_threshold: usize,
4✔
425
) -> PyResult<CentralityMapping> {
4✔
426
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
4✔
427
    if weight_fn.is_some() {
4✔
NEW
428
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
429
        for edge in graph.graph.edge_indices() {
×
UNCOV
430
            edge_weights[edge.index()] =
×
UNCOV
431
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
432
        }
433
    }
4✔
434

435
    let closeness = centrality::newman_weighted_closeness_centrality(
4✔
436
        &graph.graph,
4✔
437
        wf_improved,
4✔
438
        |e| edge_weights[e.id().index()],
48✔
439
        parallel_threshold,
4✔
440
    );
4✔
441

4✔
442
    Ok(CentralityMapping {
4✔
443
        centralities: closeness
4✔
444
            .into_iter()
4✔
445
            .enumerate()
4✔
446
            .filter_map(|(i, v)| v.map(|x| (i, x)))
20✔
447
            .collect(),
4✔
448
    })
4✔
449
}
4✔
450

451
/// Compute the weighted closeness centrality of each node in the graph.
452
///
453
/// The weighted closeness centrality is an extension of the standard closeness
454
/// centrality measure where edge weights represent connection strength rather
455
/// than distance. To properly compute shortest paths, weights are inverted
456
/// so that stronger connections correspond to shorter effective distances.
457
/// The algorithm follows the method described by Newman (2001) in analyzing
458
/// weighted graphs.[Newman]
459
///
460
/// The edges originally represent connection strength between nodes.
461
/// The idea is that if two nodes have a strong connection, the computed
462
/// distance between them should be small (shorter), and vice versa.
463
/// Note that this assume that the graph is modelling a measure of
464
/// connection strength (e.g. trust, collaboration, or similarity).
465
/// If the graph is not modelling a measure of connection strength,
466
/// the function `weight_fn` should invert the weights before calling this
467
/// function, if not it is considered as a logical error.
468
///
469
/// In the case of a graphs with more than one connected component there is
470
/// an alternative improved formula that calculates the closeness centrality
471
/// as "a ratio of the fraction of actors in the group who are reachable, to
472
/// the average distance".[WF]
473
/// You can enable this by setting `wf_improved` to `true`.
474
///
475
/// :param PyDiGraph graph: The input graph. Can either be a
476
///     :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
477
/// :param weight_fn: An optional input callable that will be passed the edge's
478
///    payload object and is expected to return a `float` weight for that edge.
479
///    If this is not specified ``default_weight`` will be used as the weight
480
///    for every edge in ``graph``
481
/// :param bool wf_improved: This is optional; the default is True. If True,
482
///   scale by the fraction of nodes reachable.
483
/// :param float default_weight: If ``weight_fn`` is not set the default weight
484
///     value to use for the weight of all edges
485
/// :param int parallel_threshold: The number of nodes to calculate the
486
///     the betweenness centrality in parallel at if the number of nodes in
487
///     the graph is less than this value it will run in a single thread. The
488
///     default value is 50
489
///
490
/// :returns: A dictionary mapping each node index to its closeness centrality.
491
/// :rtype: CentralityMapping
492
#[pyfunction(signature = (graph, weight_fn=None, wf_improved=true, default_weight = 1.0, parallel_threshold=50))]
4✔
493
pub fn digraph_newman_weighted_closeness_centrality(
4✔
494
    py: Python,
4✔
495
    graph: &digraph::PyDiGraph,
4✔
496
    weight_fn: Option<PyObject>,
4✔
497
    wf_improved: bool,
4✔
498
    default_weight: f64,
4✔
499
    parallel_threshold: usize,
4✔
500
) -> PyResult<CentralityMapping> {
4✔
501
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
4✔
502
    if weight_fn.is_some() {
4✔
UNCOV
503
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
504
        for edge in graph.graph.edge_indices() {
×
NEW
505
            edge_weights[edge.index()] =
×
NEW
506
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
507
        }
508
    }
4✔
509

510
    let closeness = centrality::newman_weighted_closeness_centrality(
4✔
511
        &graph.graph,
4✔
512
        wf_improved,
4✔
513
        |e| edge_weights[e.id().index()],
24✔
514
        parallel_threshold,
4✔
515
    );
4✔
516

4✔
517
    Ok(CentralityMapping {
4✔
518
        centralities: closeness
4✔
519
            .into_iter()
4✔
520
            .enumerate()
4✔
521
            .filter_map(|(i, v)| v.map(|x| (i, x)))
20✔
522
            .collect(),
4✔
523
    })
4✔
524
}
4✔
525

526
/// Compute the edge betweenness centrality of all edges in a :class:`~PyGraph`.
527
///
528
/// Edge betweenness centrality of an edge :math:`e` is the sum of the
529
/// fraction of all-pairs shortest paths that pass through :math:`e`
530
///
531
/// .. math::
532
///
533
///   c_B(e) =\sum_{s,t \in V} \frac{\sigma(s, t|e)}{\sigma(s, t)}
534
///
535
/// where :math:`V` is the set of nodes, :math:`\sigma(s, t)` is the
536
/// number of shortest :math:`(s, t)`-paths, and :math:`\sigma(s, t|e)` is
537
/// the number of those paths passing through edge :math:`e`.
538
///
539
/// The above definition and the algorithm used in this function is based on:
540
///
541
/// Ulrik Brandes, On Variants of Shortest-Path Betweenness Centrality
542
/// and their Generic Computation. Social Networks 30(2):136-145, 2008.
543
///
544
/// This function is multithreaded and will run in parallel if the number
545
/// of nodes in the graph is above the value of ``parallel_threshold`` (it
546
/// defaults to 50). If the function will be running in parallel the env var
547
/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used.
548
///
549
/// See Also
550
/// --------
551
/// :func:`~rustworkx.graph_betweenness_centrality`
552
///
553
/// :param PyGraph graph: The input graph
554
/// :param bool normalized: Whether to normalize the betweenness scores by the number of distinct
555
///    paths between all pairs of nodes.
556
/// :param int parallel_threshold: The number of nodes to calculate the
557
///     the betweenness centrality in parallel at if the number of nodes in
558
///     the graph is less than this value it will run in a single thread. The
559
///     default value is 50
560
///
561
/// :returns: a read-only dict-like object whose keys are the edge indices and values are the
562
///      betweenness score for each edge.
563
/// :rtype: EdgeCentralityMapping
564
#[pyfunction(
12✔
565
    signature = (
12✔
566
        graph,
12✔
567
        normalized=true,
12✔
568
        parallel_threshold=50
12✔
569
    )
12✔
570
)]
12✔
571
#[pyo3(text_signature = "(graph, /, normalized=True, parallel_threshold=50)")]
572
pub fn graph_edge_betweenness_centrality(
12✔
573
    graph: &graph::PyGraph,
12✔
574
    normalized: bool,
12✔
575
    parallel_threshold: usize,
12✔
576
) -> PyResult<EdgeCentralityMapping> {
12✔
577
    let betweenness =
12✔
578
        centrality::edge_betweenness_centrality(&graph.graph, normalized, parallel_threshold);
12✔
579
    Ok(EdgeCentralityMapping {
12✔
580
        centralities: betweenness
12✔
581
            .into_iter()
12✔
582
            .enumerate()
12✔
583
            .filter_map(|(i, v)| v.map(|x| (i, x)))
78✔
584
            .collect(),
12✔
585
    })
12✔
586
}
12✔
587

588
/// Compute the edge betweenness centrality of all edges in a :class:`~PyDiGraph`.
589
///
590
/// Edge betweenness centrality of an edge :math:`e` is the sum of the
591
/// fraction of all-pairs shortest paths that pass through :math:`e`
592
///
593
/// .. math::
594
///
595
///   c_B(e) =\sum_{s,t \in V} \frac{\sigma(s, t|e)}{\sigma(s, t)}
596
///
597
/// where :math:`V` is the set of nodes, :math:`\sigma(s, t)` is the
598
/// number of shortest :math:`(s, t)`-paths, and :math:`\sigma(s, t|e)` is
599
/// the number of those paths passing through edge :math:`e`.
600
///
601
/// The above definition and the algorithm used in this function is based on:
602
///
603
/// Ulrik Brandes, On Variants of Shortest-Path Betweenness Centrality
604
/// and their Generic Computation. Social Networks 30(2):136-145, 2008.
605
///
606
/// This function is multithreaded and will run in parallel if the number
607
/// of nodes in the graph is above the value of ``parallel_threshold`` (it
608
/// defaults to 50). If the function will be running in parallel the env var
609
/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used.
610
///
611
/// See Also
612
/// --------
613
/// :func:`~rustworkx.digraph_betweenness_centrality`
614
///
615
/// :param PyGraph graph: The input graph
616
/// :param bool normalized: Whether to normalize the betweenness scores by the number of distinct
617
///    paths between all pairs of nodes.
618
/// :param int parallel_threshold: The number of nodes to calculate the
619
///     the betweenness centrality in parallel at if the number of nodes in
620
///     the graph is less than this value it will run in a single thread. The
621
///     default value is 50
622
///
623
/// :returns: a read-only dict-like object whose keys are edges and values are the
624
///      betweenness score for each node.
625
/// :rtype: EdgeCentralityMapping
626
#[pyfunction(
10✔
627
    signature = (
10✔
628
        graph,
10✔
629
        normalized=true,
10✔
630
        parallel_threshold=50
10✔
631
    )
10✔
632
)]
10✔
633
#[pyo3(text_signature = "(graph, /, normalized=True, parallel_threshold=50)")]
634
pub fn digraph_edge_betweenness_centrality(
10✔
635
    graph: &digraph::PyDiGraph,
10✔
636
    normalized: bool,
10✔
637
    parallel_threshold: usize,
10✔
638
) -> PyResult<EdgeCentralityMapping> {
10✔
639
    let betweenness =
10✔
640
        centrality::edge_betweenness_centrality(&graph.graph, normalized, parallel_threshold);
10✔
641
    Ok(EdgeCentralityMapping {
10✔
642
        centralities: betweenness
10✔
643
            .into_iter()
10✔
644
            .enumerate()
10✔
645
            .filter_map(|(i, v)| v.map(|x| (i, x)))
90✔
646
            .collect(),
10✔
647
    })
10✔
648
}
10✔
649

650
/// Compute the eigenvector centrality of a :class:`~PyGraph`.
651
///
652
/// For details on the eigenvector centrality refer to:
653
///
654
/// Phillip Bonacich. “Power and Centrality: A Family of Measures.”
655
/// American Journal of Sociology 92(5):1170–1182, 1986
656
/// <https://doi.org/10.1086/228631>
657
///
658
/// This function uses a power iteration method to compute the eigenvector
659
/// and convergence is not guaranteed. The function will stop when `max_iter`
660
/// iterations is reached or when the computed vector between two iterations
661
/// is smaller than the error tolerance multiplied by the number of nodes.
662
/// The implementation of this algorithm is based on the NetworkX
663
/// `eigenvector_centrality() <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.eigenvector_centrality.html>`__
664
/// function.
665
///
666
/// In the case of multigraphs the weights of any parallel edges will be
667
/// summed when computing the eigenvector centrality.
668
///
669
/// :param PyGraph graph: The graph object to run the algorithm on
670
/// :param weight_fn: An optional input callable that will be passed the edge's
671
///     payload object and is expected to return a `float` weight for that edge.
672
///     If this is not specified ``default_weight`` will be used as the weight
673
///     for every edge in ``graph``
674
/// :param float default_weight: If ``weight_fn`` is not set the default weight
675
///     value to use for the weight of all edges
676
/// :param int max_iter: The maximum number of iterations in the power method. If
677
///     not specified a default value of 100 is used.
678
/// :param float tol: The error tolerance used when checking for convergence in the
679
///     power method. If this is not specified default value of 1e-6 is used.
680
///
681
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
682
///      centrality score for that node.
683
/// :rtype: CentralityMapping
684
#[pyfunction(
6✔
685
    signature = (
6✔
686
        graph,
6✔
687
        weight_fn=None,
6✔
688
        default_weight=1.0,
6✔
689
        max_iter=100,
6✔
690
        tol=1e-6
6✔
691
    )
6✔
692
)]
6✔
693
#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6)")]
694
pub fn graph_eigenvector_centrality(
6✔
695
    py: Python,
6✔
696
    graph: &graph::PyGraph,
6✔
697
    weight_fn: Option<PyObject>,
6✔
698
    default_weight: f64,
6✔
699
    max_iter: usize,
6✔
700
    tol: f64,
6✔
701
) -> PyResult<CentralityMapping> {
6✔
702
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
6✔
703
    if weight_fn.is_some() {
6✔
UNCOV
704
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
705
        for edge in graph.graph.edge_indices() {
×
UNCOV
706
            edge_weights[edge.index()] =
×
UNCOV
707
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
708
        }
709
    }
6✔
710
    let ev_centrality = centrality::eigenvector_centrality(
6✔
711
        &graph.graph,
6✔
712
        |e| -> PyResult<f64> { Ok(edge_weights[e.id().index()]) },
144✔
713
        Some(max_iter),
6✔
714
        Some(tol),
6✔
715
    )?;
6✔
716
    match ev_centrality {
6✔
717
        Some(centrality) => Ok(CentralityMapping {
4✔
718
            centralities: centrality
4✔
719
                .iter()
4✔
720
                .enumerate()
4✔
721
                .filter_map(|(k, v)| {
16✔
722
                    if graph.graph.contains_node(NodeIndex::new(k)) {
16✔
723
                        Some((k, *v))
16✔
724
                    } else {
725
                        None
×
726
                    }
727
                })
16✔
728
                .collect(),
4✔
729
        }),
4✔
730
        None => Err(FailedToConverge::new_err(format!(
2✔
731
            "Function failed to converge on a solution in {} iterations",
2✔
732
            max_iter
2✔
733
        ))),
2✔
734
    }
735
}
6✔
736

737
/// Compute the eigenvector centrality of a :class:`~PyDiGraph`.
738
///
739
/// For details on the eigenvector centrality refer to:
740
///
741
/// Phillip Bonacich. “Power and Centrality: A Family of Measures.”
742
/// American Journal of Sociology 92(5):1170–1182, 1986
743
/// <https://doi.org/10.1086/228631>
744
///
745
/// This function uses a power iteration method to compute the eigenvector
746
/// and convergence is not guaranteed. The function will stop when `max_iter`
747
/// iterations is reached or when the computed vector between two iterations
748
/// is smaller than the error tolerance multiplied by the number of nodes.
749
/// The implementation of this algorithm is based on the NetworkX
750
/// `eigenvector_centrality() <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.eigenvector_centrality.html>`__
751
/// function.
752
///
753
/// In the case of multigraphs the weights of any parallel edges will be
754
/// summed when computing the eigenvector centrality.
755
///
756
/// :param PyDiGraph graph: The graph object to run the algorithm on
757
/// :param weight_fn: An optional input callable that will be passed the edge's
758
///     payload object and is expected to return a `float` weight for that edge.
759
///     If this is not specified ``default_weight`` will be used as the weight
760
///     for every edge in ``graph``
761
/// :param float default_weight: If ``weight_fn`` is not set the default weight
762
///     value to use for the weight of all edges
763
/// :param int max_iter: The maximum number of iterations in the power method. If
764
///     not specified a default value of 100 is used.
765
/// :param float tol: The error tolerance used when checking for convergence in the
766
///     power method. If this is not specified default value of 1e-6 is used.
767
///
768
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
769
///      centrality score for that node.
770
/// :rtype: CentralityMapping
771
#[pyfunction(
6✔
772
    signature = (
6✔
773
        graph,
6✔
774
        weight_fn=None,
6✔
775
        default_weight=1.0,
6✔
776
        max_iter=100,
6✔
777
        tol=1e-6
6✔
778
    )
6✔
779
)]
6✔
780
#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6)")]
781
pub fn digraph_eigenvector_centrality(
6✔
782
    py: Python,
6✔
783
    graph: &digraph::PyDiGraph,
6✔
784
    weight_fn: Option<PyObject>,
6✔
785
    default_weight: f64,
6✔
786
    max_iter: usize,
6✔
787
    tol: f64,
6✔
788
) -> PyResult<CentralityMapping> {
6✔
789
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
6✔
790
    if weight_fn.is_some() {
6✔
UNCOV
791
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
792
        for edge in graph.graph.edge_indices() {
×
UNCOV
793
            edge_weights[edge.index()] =
×
UNCOV
794
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
795
        }
796
    }
6✔
797
    let ev_centrality = centrality::eigenvector_centrality(
6✔
798
        &graph.graph,
6✔
799
        |e| -> PyResult<f64> { Ok(edge_weights[e.id().index()]) },
144✔
800
        Some(max_iter),
6✔
801
        Some(tol),
6✔
802
    )?;
6✔
803

804
    match ev_centrality {
6✔
805
        Some(centrality) => Ok(CentralityMapping {
4✔
806
            centralities: centrality
4✔
807
                .iter()
4✔
808
                .enumerate()
4✔
809
                .filter_map(|(k, v)| {
16✔
810
                    if graph.graph.contains_node(NodeIndex::new(k)) {
16✔
811
                        Some((k, *v))
16✔
812
                    } else {
813
                        None
×
814
                    }
815
                })
16✔
816
                .collect(),
4✔
817
        }),
4✔
818
        None => Err(FailedToConverge::new_err(format!(
2✔
819
            "Function failed to converge on a solution in {} iterations",
2✔
820
            max_iter
2✔
821
        ))),
2✔
822
    }
823
}
6✔
824

825
/// Compute the Katz centrality of a :class:`~PyGraph`.
826
///
827
/// For details on the Katz centrality refer to:
828
///
829
/// Leo Katz. “A New Status Index Derived from Sociometric Index.”
830
/// Psychometrika 18(1):39–43, 1953
831
/// <https://link.springer.com/content/pdf/10.1007/BF02289026.pdf>
832
///
833
/// This function uses a power iteration method to compute the eigenvector
834
/// and convergence is not guaranteed. The function will stop when `max_iter`
835
/// iterations is reached or when the computed vector between two iterations
836
/// is smaller than the error tolerance multiplied by the number of nodes.
837
/// The implementation of this algorithm is based on the NetworkX
838
/// `katz_centrality() <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.katz_centrality.html>`__
839
/// function.
840
///
841
/// In the case of multigraphs the weights of any parallel edges will be
842
/// summed when computing the Katz centrality.
843
///
844
/// :param PyGraph graph: The graph object to run the algorithm on
845
/// :param float alpha: Attenuation factor. If this is not specified default value of 0.1 is used.
846
/// :param float | dict beta: Immediate neighbourhood weights. If a float is provided, the neighbourhood
847
///     weight is used for all nodes. If a dictionary is provided, it must contain all node indices.
848
///     If beta is not specified, a default value of 1.0 is used.
849
/// :param weight_fn: An optional input callable that will be passed the edge's
850
///     payload object and is expected to return a `float` weight for that edge.
851
///     If this is not specified ``default_weight`` will be used as the weight
852
///     for every edge in ``graph``
853
/// :param float default_weight: If ``weight_fn`` is not set the default weight
854
///     value to use for the weight of all edges
855
/// :param int max_iter: The maximum number of iterations in the power method. If
856
///     not specified a default value of 1000 is used.
857
/// :param float tol: The error tolerance used when checking for convergence in the
858
///     power method. If this is not specified default value of 1e-6 is used.
859
///
860
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
861
///      centrality score for that node.
862
/// :rtype: CentralityMapping
863
#[pyfunction(
4✔
864
    signature = (
4✔
865
        graph,
4✔
866
        alpha=0.1,
4✔
867
        beta=None,
4✔
868
        weight_fn=None,
10✔
869
        default_weight=1.0,
10✔
870
        max_iter=1000,
10✔
871
        tol=1e-6
10✔
872
    )
10✔
873
)]
10✔
874
#[pyo3(
875
    text_signature = "(graph, /, alpha=0.1, beta=None, weight_fn=None, default_weight=1.0, max_iter=1000, tol=1e-6)"
876
)]
877
pub fn graph_katz_centrality(
10✔
878
    py: Python,
10✔
879
    graph: &graph::PyGraph,
10✔
880
    alpha: f64,
10✔
881
    beta: Option<PyObject>,
10✔
882
    weight_fn: Option<PyObject>,
10✔
883
    default_weight: f64,
10✔
884
    max_iter: usize,
10✔
885
    tol: f64,
10✔
886
) -> PyResult<CentralityMapping> {
10✔
887
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
10✔
888
    if weight_fn.is_some() {
10✔
UNCOV
889
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
890
        for edge in graph.graph.edge_indices() {
×
UNCOV
891
            edge_weights[edge.index()] =
×
UNCOV
892
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
893
        }
894
    }
10✔
895

896
    let mut beta_map: HashMap<usize, f64> = HashMap::new();
10✔
897

898
    if let Some(beta) = beta {
10✔
899
        match beta.extract::<f64>(py) {
6✔
900
            Ok(beta_scalar) => {
2✔
901
                // User provided a scalar, populate beta_map with the value
902
                for node_index in graph.graph.node_identifiers() {
20✔
903
                    beta_map.insert(node_index.index(), beta_scalar);
20✔
904
                }
20✔
905
            }
906
            Err(_) => {
907
                beta_map = beta.extract::<HashMap<usize, f64>>(py)?;
4✔
908

909
                for node_index in graph.graph.node_identifiers() {
24✔
910
                    if !beta_map.contains_key(&node_index.index()) {
24✔
911
                        return Err(PyValueError::new_err(
2✔
912
                            "Beta does not contain all node indices",
2✔
913
                        ));
2✔
914
                    }
22✔
915
                }
916
            }
917
        }
918
    } else {
919
        // Populate with 1.0
920
        for node_index in graph.graph.node_identifiers() {
20✔
921
            beta_map.insert(node_index.index(), 1.0);
20✔
922
        }
20✔
923
    }
924

925
    let ev_centrality = centrality::katz_centrality(
8✔
926
        &graph.graph,
8✔
927
        |e| -> PyResult<f64> { Ok(edge_weights[e.id().index()]) },
4,760✔
928
        Some(alpha),
8✔
929
        Some(beta_map),
8✔
930
        None,
8✔
931
        Some(max_iter),
8✔
932
        Some(tol),
8✔
933
    )?;
8✔
934
    match ev_centrality {
8✔
935
        Some(centrality) => Ok(CentralityMapping {
6✔
936
            centralities: centrality
6✔
937
                .iter()
6✔
938
                .enumerate()
6✔
939
                .filter_map(|(k, v)| {
50✔
940
                    if graph.graph.contains_node(NodeIndex::new(k)) {
50✔
941
                        Some((k, *v))
50✔
942
                    } else {
UNCOV
943
                        None
×
944
                    }
945
                })
50✔
946
                .collect(),
6✔
947
        }),
6✔
948
        None => Err(FailedToConverge::new_err(format!(
2✔
949
            "Function failed to converge on a solution in {} iterations",
2✔
950
            max_iter
2✔
951
        ))),
2✔
952
    }
953
}
10✔
954

955
/// Compute the Katz centrality of a :class:`~PyDiGraph`.
956
///
957
/// For details on the Katz centrality refer to:
958
///
959
/// Leo Katz. “A New Status Index Derived from Sociometric Index.”
960
/// Psychometrika 18(1):39–43, 1953
961
/// <https://link.springer.com/content/pdf/10.1007/BF02289026.pdf>
962
///
963
/// This function uses a power iteration method to compute the eigenvector
964
/// and convergence is not guaranteed. The function will stop when `max_iter`
965
/// iterations is reached or when the computed vector between two iterations
966
/// is smaller than the error tolerance multiplied by the number of nodes.
967
/// The implementation of this algorithm is based on the NetworkX
968
/// `katz_centrality() <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.katz_centrality.html>`__
969
/// function.
970
///
971
/// In the case of multigraphs the weights of any parallel edges will be
972
/// summed when computing the Katz centrality.
973
///
974
/// :param PyDiGraph graph: The graph object to run the algorithm on
975
/// :param float alpha: Attenuation factor. If this is not specified default value of 0.1 is used.
976
/// :param float | dict beta: Immediate neighbourhood weights. If a float is provided, the neighbourhood
977
///     weight is used for all nodes. If a dictionary is provided, it must contain all node indices.
978
///     If beta is not specified, a default value of 1.0 is used.
979
/// :param weight_fn: An optional input callable that will be passed the edge's
980
///     payload object and is expected to return a `float` weight for that edge.
981
///     If this is not specified ``default_weight`` will be used as the weight
982
///     for every edge in ``graph``
983
/// :param float default_weight: If ``weight_fn`` is not set the default weight
984
///     value to use for the weight of all edges
985
/// :param int max_iter: The maximum number of iterations in the power method. If
986
///     not specified a default value of 1000 is used.
987
/// :param float tol: The error tolerance used when checking for convergence in the
988
///     power method. If this is not specified default value of 1e-6 is used.
989
///
990
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
991
///      centrality score for that node.
992
/// :rtype: CentralityMapping
993
#[pyfunction(
4✔
994
    signature = (
4✔
995
        graph,
4✔
996
        alpha=0.1,
4✔
997
        beta=None,
4✔
998
        weight_fn=None,
10✔
999
        default_weight=1.0,
10✔
1000
        max_iter=1000,
10✔
1001
        tol=1e-6
10✔
1002
    )
10✔
1003
)]
10✔
1004
#[pyo3(
1005
    text_signature = "(graph, /, alpha=0.1, beta=None, weight_fn=None, default_weight=1.0, max_iter=1000, tol=1e-6)"
1006
)]
1007
pub fn digraph_katz_centrality(
10✔
1008
    py: Python,
10✔
1009
    graph: &digraph::PyDiGraph,
10✔
1010
    alpha: f64,
10✔
1011
    beta: Option<PyObject>,
10✔
1012
    weight_fn: Option<PyObject>,
10✔
1013
    default_weight: f64,
10✔
1014
    max_iter: usize,
10✔
1015
    tol: f64,
10✔
1016
) -> PyResult<CentralityMapping> {
10✔
1017
    let mut edge_weights = vec![default_weight; graph.graph.edge_bound()];
10✔
1018
    if weight_fn.is_some() {
10✔
UNCOV
1019
        let cost_fn = CostFn::try_from((weight_fn, default_weight))?;
×
UNCOV
1020
        for edge in graph.graph.edge_indices() {
×
UNCOV
1021
            edge_weights[edge.index()] =
×
UNCOV
1022
                cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?;
×
1023
        }
1024
    }
10✔
1025

1026
    let mut beta_map: HashMap<usize, f64> = HashMap::new();
10✔
1027

1028
    if let Some(beta) = beta {
10✔
1029
        match beta.extract::<f64>(py) {
6✔
1030
            Ok(beta_scalar) => {
2✔
1031
                // User provided a scalar, populate beta_map with the value
1032
                for node_index in graph.graph.node_identifiers() {
20✔
1033
                    beta_map.insert(node_index.index(), beta_scalar);
20✔
1034
                }
20✔
1035
            }
1036
            Err(_) => {
1037
                beta_map = beta.extract::<HashMap<usize, f64>>(py)?;
4✔
1038

1039
                for node_index in graph.graph.node_identifiers() {
24✔
1040
                    if !beta_map.contains_key(&node_index.index()) {
24✔
1041
                        return Err(PyValueError::new_err(
2✔
1042
                            "Beta does not contain all node indices",
2✔
1043
                        ));
2✔
1044
                    }
22✔
1045
                }
1046
            }
1047
        }
1048
    } else {
1049
        // Populate with 1.0
1050
        for node_index in graph.graph.node_identifiers() {
20✔
1051
            beta_map.insert(node_index.index(), 1.0);
20✔
1052
        }
20✔
1053
    }
1054

1055
    let ev_centrality = centrality::katz_centrality(
8✔
1056
        &graph.graph,
8✔
1057
        |e| -> PyResult<f64> { Ok(edge_weights[e.id().index()]) },
1,018✔
1058
        Some(alpha),
8✔
1059
        Some(beta_map),
8✔
1060
        None,
8✔
1061
        Some(max_iter),
8✔
1062
        Some(tol),
8✔
1063
    )?;
8✔
1064

1065
    match ev_centrality {
8✔
1066
        Some(centrality) => Ok(CentralityMapping {
6✔
1067
            centralities: centrality
6✔
1068
                .iter()
6✔
1069
                .enumerate()
6✔
1070
                .filter_map(|(k, v)| {
50✔
1071
                    if graph.graph.contains_node(NodeIndex::new(k)) {
50✔
1072
                        Some((k, *v))
50✔
1073
                    } else {
UNCOV
1074
                        None
×
1075
                    }
1076
                })
50✔
1077
                .collect(),
6✔
1078
        }),
6✔
1079
        None => Err(FailedToConverge::new_err(format!(
2✔
1080
            "Function failed to converge on a solution in {} iterations",
2✔
1081
            max_iter
2✔
1082
        ))),
2✔
1083
    }
1084
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc