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

Qiskit / rustworkx / 26694392780

30 May 2026 08:22PM UTC coverage: 94.726% (-0.009%) from 94.735%
26694392780

push

github

web-flow
perf: Use BufWriter when writing json (#1594)

* perf: Use BufWriter when writing json

Writing without a BufWriter is going to generate a syscall for every
emitted JSON token.

* fix: flush buffer after writing

* buffer other calls in the library

---------

Co-authored-by: Ivan Carvalho <ivancarvalho@gatech.edu>

29 of 32 new or added lines in 4 files covered. (90.63%)

1 existing line in 1 file now uncovered.

19148 of 20214 relevant lines covered (94.73%)

916164.69 hits per line

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

98.07
/src/digraph.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::borrow_as_ptr, clippy::redundant_closure)]
14

15
use std::cmp;
16
use std::cmp::Ordering;
17
use std::collections::BTreeMap;
18

19
use std::fs::File;
20
use std::io::prelude::*;
21
use std::io::{BufReader, BufWriter};
22
use std::str;
23

24
use hashbrown::hash_set::Entry;
25
use hashbrown::{HashMap, HashSet};
26

27
use rustworkx_core::dictmap::*;
28
use rustworkx_core::graph_ext::*;
29

30
use smallvec::SmallVec;
31

32
use pyo3::IntoPyObjectExt;
33
use pyo3::PyTraverseError;
34
use pyo3::Python;
35
use pyo3::exceptions::PyIndexError;
36
use pyo3::gc::PyVisit;
37
use pyo3::prelude::*;
38
use pyo3::types::{IntoPyDict, PyBool, PyDict, PyGenericAlias, PyList, PyString, PyTuple, PyType};
39

40
use ndarray::prelude::*;
41
use num_traits::Zero;
42
use numpy::Complex64;
43
use numpy::PyReadonlyArray2;
44
use petgraph::algo;
45
use petgraph::graph::{EdgeIndex, NodeIndex};
46
use petgraph::prelude::*;
47
use rustworkx_core::graph_ext::contraction::can_contract;
48

49
use crate::RxPyResult;
50
use petgraph::visit::{
51
    EdgeIndexable, GraphBase, IntoEdgeReferences, IntoNodeReferences, NodeCount, NodeFiltered,
52
    NodeIndexable, Visitable,
53
};
54

55
use super::dot_utils::build_dot;
56
use super::iterators::{
57
    EdgeIndexMap, EdgeIndices, EdgeList, NodeIndices, NodeMap, WeightedEdgeList,
58
};
59
use super::{
60
    DAGHasCycle, DAGWouldCycle, IsNan, NoEdgeBetweenNodes, NoSuitableNeighbors, NodesRemoved,
61
    StablePyGraph, find_node_by_weight, weight_callable,
62
};
63
use foldhash::fast::RandomState;
64
use indexmap::IndexSet;
65

66
use super::dag_algo::is_directed_acyclic_graph;
67

68
/// A class for creating directed graphs
69
///
70
/// The ``PyDiGraph`` class is used to create a directed graph. It can be a
71
/// multigraph (have multiple edges between nodes). Each node and edge
72
/// (although rarely used for edges) is indexed by an integer id. These ids
73
/// are stable for the lifetime of the graph object and on node or edge
74
/// deletions you can have holes in the list of indices for the graph.
75
/// Node indices will be reused on additions after removal. For example:
76
///
77
/// .. jupyter-execute::
78
///
79
///        import rustworkx as rx
80
///
81
///        graph = rx.PyDiGraph()
82
///        graph.add_nodes_from(list(range(5)))
83
///        graph.add_nodes_from(list(range(2)))
84
///        graph.remove_node(2)
85
///        print("After deletion:", graph.node_indices())
86
///        res_manual = graph.add_parent(6, None, None)
87
///        print("After adding a new node:", graph.node_indices())
88
///
89
/// Additionally, each node and edge contains an arbitrary Python object as a
90
/// weight/data payload. You can use the index for access to the data payload
91
/// as in the following example:
92
///
93
/// .. jupyter-execute::
94
///
95
///     import rustworkx as rx
96
///
97
///     graph = rx.PyDiGraph()
98
///     data_payload = "An arbitrary Python object"
99
///     node_index = graph.add_node(data_payload)
100
///     print("Node Index: %s" % node_index)
101
///     print(graph[node_index])
102
///
103
/// The PyDiGraph implements the Python mapping protocol for nodes so in
104
/// addition to access you can also update the data payload with:
105
///
106
/// .. jupyter-execute::
107
///
108
///     import rustworkx as rx
109
///
110
///     graph = rx.PyDiGraph()
111
///     data_payload = "An arbitrary Python object"
112
///     node_index = graph.add_node(data_payload)
113
///     graph[node_index] = "New Payload"
114
///     print("Node Index: %s" % node_index)
115
///     print(graph[node_index])
116
///
117
/// The PyDiGraph class has an option for real time cycle checking which can
118
/// be used to ensure any edges added to the graph does not introduce a cycle.
119
/// By default the real time cycle checking feature is disabled for performance,
120
/// however you can enable it by setting the ``check_cycle`` attribute to True.
121
/// For example::
122
///
123
///     import rustworkx as rx
124
///     dag = rx.PyDiGraph()
125
///     dag.check_cycle = True
126
///
127
/// or at object creation::
128
///
129
///     import rustworkx as rx
130
///     dag = rx.PyDiGraph(check_cycle=True)
131
///
132
/// With check_cycle set to true any calls to :meth:`PyDiGraph.add_edge` will
133
/// ensure that no cycles are added, ensuring that the PyDiGraph class truly
134
/// represents a directed acyclic graph. Do note that this cycle checking on
135
/// :meth:`~PyDiGraph.add_edge`, :meth:`~PyDiGraph.add_edges_from`,
136
/// :meth:`~PyDiGraph.add_edges_from_no_data`,
137
/// :meth:`~PyDiGraph.extend_from_edge_list`,  and
138
/// :meth:`~PyDiGraph.extend_from_weighted_edge_list` comes with a performance
139
/// penalty that grows as the graph does. If you're adding a node and edge at
140
/// the same time leveraging :meth:`PyDiGraph.add_child` or
141
/// :meth:`PyDiGraph.add_parent` will avoid this overhead.
142
///
143
/// By default a ``PyDiGraph`` is a multigraph (meaning there can be parallel
144
/// edges between nodes) however this can be disabled by setting the
145
/// ``multigraph`` kwarg to ``False`` when calling the ``PyDiGraph``
146
/// constructor. For example::
147
///
148
///     import rustworkx as rx
149
///     graph = rx.PyDiGraph(multigraph=False)
150
///
151
/// This can only be set at ``PyDiGraph`` initialization and not adjusted after
152
/// creation. When :attr:`~rustworkx.PyDiGraph.multigraph` is set to ``False``
153
/// if a method call is made that would add a parallel edge it will instead
154
/// update the existing edge's weight/data payload.
155
///
156
/// Each ``PyDiGraph`` object has an :attr:`~.PyDiGraph.attrs` attribute which is
157
/// used to contain additional attributes/metadata of the graph instance. By
158
/// default this is set to ``None`` but can optionally be specified by using the
159
/// ``attrs`` keyword argument when constructing a new graph::
160
///
161
///     graph = rustworkx.PyDiGraph(attrs=dict(source_path='/tmp/graph.csv'))
162
///
163
/// This attribute can be set to any Python object. Additionally, you can access
164
/// and modify this attribute after creating an object. For example::
165
///
166
///     source_path = graph.attrs
167
///     graph.attrs = {'new_path': '/tmp/new.csv', 'old_path': source_path}
168
///
169
/// The maximum number of nodes and edges allowed on a ``PyGraph`` object is
170
/// :math:`2^{32} - 1` (4,294,967,294) each. Attempting to add more nodes or
171
/// edges than this will result in an exception being raised.
172
///
173
/// :param bool check_cycle: When this is set to ``True`` the created
174
///     ``PyDiGraph`` has runtime cycle detection enabled.
175
/// :param bool multigraph: When this is set to ``False`` the created
176
///     ``PyDiGraph`` object will not be a multigraph. When ``False`` if a
177
///     method call is made that would add parallel edges the weight/weight
178
///     from that method call will be used to update the existing edge in place.
179
/// :param attrs: An optional attributes payload to assign to the
180
///     :attr:`~.PyDiGraph.attrs` attribute. This can be any Python object. If
181
///     it is not specified :attr:`~.PyDiGraph.attrs` will be set to ``None``.
182
/// :param int node_count_hint: An optional hint that will allocate with enough capacity to store this
183
///     many nodes before needing to grow.  This does not prepopulate any nodes with data, it is
184
///     only a potential performance optimization if the complete size of the graph is known in
185
///     advance.
186
/// :param int edge_count_hint: An optional hint that will allocate enough capacity to store this
187
///     many edges before needing to grow.  This does not prepopulate any edges with data, it is
188
///     only a potential performance optimization if the complete size of the graph is known in
189
///     advance.
190
#[pyclass(mapping, module = "rustworkx", subclass)]
191
#[derive(Clone)]
192
pub struct PyDiGraph {
193
    pub graph: StablePyGraph<Directed>,
194
    pub cycle_state: algo::DfsSpace<NodeIndex, <StablePyGraph<Directed> as Visitable>::Map>,
195
    pub check_cycle: bool,
196
    pub node_removed: bool,
197
    pub multigraph: bool,
198
    #[pyo3(get, set)]
199
    pub attrs: Py<PyAny>,
200
}
201

202
impl GraphBase for PyDiGraph {
203
    type NodeId = NodeIndex;
204
    type EdgeId = EdgeIndex;
205
}
206

207
impl NodesRemoved for &PyDiGraph {
208
    fn nodes_removed(&self) -> bool {
×
209
        self.node_removed
×
210
    }
×
211
}
212

213
impl NodeCount for PyDiGraph {
214
    fn node_count(&self) -> usize {
3,668✔
215
        self.graph.node_count()
3,668✔
216
    }
3,668✔
217
}
218

219
// Rust side only PyDiGraph methods
220
impl PyDiGraph {
221
    fn add_edge_no_cycle_check(
2,026,964✔
222
        &mut self,
2,026,964✔
223
        p_index: NodeIndex,
2,026,964✔
224
        c_index: NodeIndex,
2,026,964✔
225
        edge: Py<PyAny>,
2,026,964✔
226
    ) -> usize {
2,026,964✔
227
        if !self.multigraph {
2,026,964✔
228
            let exists = self.graph.find_edge(p_index, c_index);
138✔
229
            if let Some(index) = exists {
138✔
230
                let edge_weight = self.graph.edge_weight_mut(index).unwrap();
16✔
231
                *edge_weight = edge;
16✔
232
                return index.index();
16✔
233
            }
122✔
234
        }
2,026,826✔
235
        let edge = self.graph.add_edge(p_index, c_index, edge);
2,026,948✔
236
        edge.index()
2,026,948✔
237
    }
2,026,964✔
238

239
    fn _add_edge(
2,026,976✔
240
        &mut self,
2,026,976✔
241
        p_index: NodeIndex,
2,026,976✔
242
        c_index: NodeIndex,
2,026,976✔
243
        edge: Py<PyAny>,
2,026,976✔
244
    ) -> PyResult<usize> {
2,026,976✔
245
        // Only check for cycles if instance attribute is set to true
246
        if self.check_cycle {
2,026,976✔
247
            // Only check for a cycle (by running has_path_connecting) if
248
            // the new edge could potentially add a cycle
249
            let cycle_check_required = is_cycle_check_required(self, p_index, c_index);
26✔
250
            let state = Some(&mut self.cycle_state);
26✔
251
            if cycle_check_required
26✔
252
                && algo::has_path_connecting(&self.graph, c_index, p_index, state)
14✔
253
            {
254
                return Err(DAGWouldCycle::new_err("Adding an edge would cycle"));
12✔
255
            }
14✔
256
        }
2,026,950✔
257
        Ok(self.add_edge_no_cycle_check(p_index, c_index, edge))
2,026,964✔
258
    }
2,026,976✔
259

260
    fn insert_between(
44✔
261
        &mut self,
44✔
262
        py: Python,
44✔
263
        node: usize,
44✔
264
        node_between: usize,
44✔
265
        direction: bool,
44✔
266
    ) -> PyResult<()> {
44✔
267
        let dir = if direction {
44✔
268
            petgraph::Direction::Outgoing
22✔
269
        } else {
270
            petgraph::Direction::Incoming
22✔
271
        };
272
        let index = NodeIndex::new(node);
44✔
273
        let node_between_index = NodeIndex::new(node_between);
44✔
274
        let edges: Vec<(NodeIndex, EdgeIndex, Py<PyAny>)> = self
44✔
275
            .graph
44✔
276
            .edges_directed(node_between_index, dir)
44✔
277
            .map(|edge| {
44✔
278
                if direction {
36✔
279
                    (edge.target(), edge.id(), edge.weight().clone_ref(py))
18✔
280
                } else {
281
                    (edge.source(), edge.id(), edge.weight().clone_ref(py))
18✔
282
                }
283
            })
36✔
284
            .collect::<Vec<(NodeIndex, EdgeIndex, Py<PyAny>)>>();
44✔
285
        for (other_index, edge_index, weight) in edges {
44✔
286
            if direction {
36✔
287
                self._add_edge(node_between_index, index, weight.clone_ref(py))?;
18✔
288
                self._add_edge(index, other_index, weight.clone_ref(py))?;
18✔
289
            } else {
290
                self._add_edge(other_index, index, weight.clone_ref(py))?;
18✔
291
                self._add_edge(index, node_between_index, weight.clone_ref(py))?;
18✔
292
            }
293
            self.graph.remove_edge(edge_index);
36✔
294
        }
295
        Ok(())
44✔
296
    }
44✔
297
}
298

299
#[pymethods]
×
300
impl PyDiGraph {
301
    #[new]
302
    #[pyo3(signature=(/, check_cycle=false, multigraph=true, attrs=None, *, node_count_hint=None, edge_count_hint=None))]
303
    fn new(
2,290✔
304
        py: Python,
2,290✔
305
        check_cycle: bool,
2,290✔
306
        multigraph: bool,
2,290✔
307
        attrs: Option<Py<PyAny>>,
2,290✔
308
        node_count_hint: Option<usize>,
2,290✔
309
        edge_count_hint: Option<usize>,
2,290✔
310
    ) -> Self {
2,290✔
311
        PyDiGraph {
312
            graph: StablePyGraph::<Directed>::with_capacity(
2,290✔
313
                node_count_hint.unwrap_or_default(),
2,290✔
314
                edge_count_hint.unwrap_or_default(),
2,290✔
315
            ),
316
            cycle_state: algo::DfsSpace::default(),
2,290✔
317
            check_cycle,
2,290✔
318
            node_removed: false,
319
            multigraph,
2,290✔
320
            attrs: attrs.unwrap_or_else(|| py.None()),
2,290✔
321
        }
322
    }
2,290✔
323

324
    fn __getnewargs_ex__<'py>(
36✔
325
        &self,
36✔
326
        py: Python<'py>,
36✔
327
    ) -> PyResult<(Bound<'py, PyTuple>, Bound<'py, PyDict>)> {
36✔
328
        Ok((
329
            (self.check_cycle, self.multigraph, self.attrs.clone_ref(py)).into_pyobject(py)?,
36✔
330
            [
36✔
331
                ("node_count_hint", self.graph.node_bound()),
36✔
332
                ("edge_count_hint", self.graph.edge_bound()),
36✔
333
            ]
36✔
334
            .into_py_dict(py)?,
36✔
335
        ))
336
    }
36✔
337

338
    fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
36✔
339
        let mut nodes: Vec<Py<PyAny>> = Vec::with_capacity(self.graph.node_bound());
36✔
340
        let mut edges: Vec<Py<PyAny>> = Vec::with_capacity(self.graph.edge_bound());
36✔
341

342
        // save nodes to a list along with its index
343
        for node_idx in self.graph.node_indices() {
128✔
344
            let node_data = self.graph.node_weight(node_idx).unwrap();
128✔
345
            nodes.push((node_idx.index(), node_data).into_py_any(py)?);
128✔
346
        }
347

348
        // edges are saved with none (deleted edges) instead of their index to save space
349
        for i in 0..self.graph.edge_bound() {
116✔
350
            let idx = EdgeIndex::new(i);
116✔
351
            let edge = match self.graph.edge_weight(idx) {
116✔
352
                Some(edge_w) => {
100✔
353
                    let endpoints = self.graph.edge_endpoints(idx).unwrap();
100✔
354
                    (endpoints.0.index(), endpoints.1.index(), edge_w).into_py_any(py)?
100✔
355
                }
356
                None => py.None(),
16✔
357
            };
358
            edges.push(edge);
116✔
359
        }
360
        let out_dict = PyDict::new(py);
36✔
361
        let nodes_lst = PyList::new(py, nodes)?;
36✔
362
        let edges_lst = PyList::new(py, edges)?;
36✔
363
        out_dict.set_item("nodes", nodes_lst)?;
36✔
364
        out_dict.set_item("edges", edges_lst)?;
36✔
365
        out_dict.set_item("nodes_removed", self.node_removed)?;
36✔
366
        Ok(out_dict.into())
36✔
367
    }
36✔
368

369
    fn __setstate__(&mut self, py: Python, state: Py<PyAny>) -> PyResult<()> {
36✔
370
        let dict_state = state.downcast_bound::<PyDict>(py)?;
36✔
371
        let binding = dict_state.get_item("nodes")?.unwrap();
36✔
372
        let nodes_lst = binding.downcast::<PyList>()?;
36✔
373
        let binding = dict_state.get_item("edges")?.unwrap();
36✔
374
        let edges_lst = binding.downcast::<PyList>()?;
36✔
375
        self.graph = StablePyGraph::<Directed>::new();
36✔
376
        let dict_state = state.downcast_bound::<PyDict>(py)?;
36✔
377
        self.node_removed = dict_state
36✔
378
            .get_item("nodes_removed")?
36✔
379
            .unwrap()
36✔
380
            .downcast::<PyBool>()?
36✔
381
            .extract()?;
36✔
382

383
        // graph is empty, stop early
384
        if nodes_lst.is_empty() {
36✔
385
            return Ok(());
12✔
386
        }
24✔
387

388
        if !self.node_removed {
24✔
389
            for item in nodes_lst.iter() {
46✔
390
                let node_w = item
46✔
391
                    .downcast::<PyTuple>()
46✔
392
                    .unwrap()
46✔
393
                    .get_item(1)
46✔
394
                    .unwrap()
46✔
395
                    .extract()
46✔
396
                    .unwrap();
46✔
397
                self.graph.add_node(node_w);
46✔
398
            }
46✔
399
        } else if nodes_lst.len() == 1 {
14✔
400
            // graph has only one node, handle logic here to save one if in the loop later
401
            let binding = nodes_lst.get_item(0).unwrap();
×
402
            let item = binding.downcast::<PyTuple>().unwrap();
×
403
            let node_idx: usize = item.get_item(0).unwrap().extract().unwrap();
×
404
            let node_w = item.get_item(1).unwrap().extract().unwrap();
×
405

406
            for _i in 0..node_idx {
×
407
                self.graph.add_node(py.None());
×
408
            }
×
409
            self.graph.add_node(node_w);
×
410
            for i in 0..node_idx {
×
411
                self.graph.remove_node(NodeIndex::new(i));
×
412
            }
×
413
        } else {
414
            let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap();
14✔
415
            let last_item = binding.downcast::<PyTuple>().unwrap();
14✔
416

417
            // list of temporary nodes that will be removed later to re-create holes
418
            let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap();
14✔
419
            let mut tmp_nodes: Vec<NodeIndex> =
14✔
420
                Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len());
14✔
421

422
            for item in nodes_lst {
82✔
423
                let item = item.downcast::<PyTuple>().unwrap();
82✔
424
                let next_index: usize = item.get_item(0).unwrap().extract().unwrap();
82✔
425
                let weight: Py<PyAny> = item.get_item(1).unwrap().extract().unwrap();
82✔
426
                while next_index > self.graph.node_bound() {
106✔
427
                    // node does not exist
24✔
428
                    let tmp_node = self.graph.add_node(py.None());
24✔
429
                    tmp_nodes.push(tmp_node);
24✔
430
                }
24✔
431
                // add node to the graph, and update the next available node index
432
                self.graph.add_node(weight);
82✔
433
            }
434
            // Remove any temporary nodes we added
435
            for tmp_node in tmp_nodes {
24✔
436
                self.graph.remove_node(tmp_node);
24✔
437
            }
24✔
438
        }
439

440
        // to ensure O(1) on edge deletion, use a temporary node to store missing edges
441
        let tmp_node = self.graph.add_node(py.None());
24✔
442

443
        for item in edges_lst {
116✔
444
            if item.is_none() {
116✔
445
                // add a temporary edge that will be deleted later to re-create the hole
16✔
446
                self.graph.add_edge(tmp_node, tmp_node, py.None());
16✔
447
            } else {
100✔
448
                let triple = item.downcast::<PyTuple>().unwrap();
100✔
449
                let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap();
100✔
450
                let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap();
100✔
451
                let edge_w = triple.get_item(2).unwrap().extract().unwrap();
100✔
452
                self.graph
100✔
453
                    .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w);
100✔
454
            }
100✔
455
        }
456

457
        // remove the temporary node will remove all deleted edges in bulk,
458
        // the cost is equal to the number of edges
459
        self.graph.remove_node(tmp_node);
24✔
460

461
        Ok(())
24✔
462
    }
36✔
463

464
    /// Whether cycle checking is enabled for the DiGraph/DAG.
465
    ///
466
    /// If set to ``True`` adding new edges that would introduce a cycle
467
    /// will raise a :class:`DAGWouldCycle` exception.
468
    #[getter]
469
    fn get_check_cycle(&self) -> bool {
8✔
470
        self.check_cycle
8✔
471
    }
8✔
472

473
    #[setter]
474
    fn set_check_cycle(&mut self, value: bool) -> PyResult<()> {
14✔
475
        if !self.check_cycle && value && !is_directed_acyclic_graph(self) {
14✔
476
            return Err(DAGHasCycle::new_err("PyDiGraph object has a cycle"));
2✔
477
        }
12✔
478
        self.check_cycle = value;
12✔
479
        Ok(())
12✔
480
    }
14✔
481

482
    /// Whether the graph is a multigraph (allows multiple edges between
483
    /// nodes) or not
484
    ///
485
    /// If set to ``False`` multiple edges between nodes are not allowed and
486
    /// calls that would add a parallel edge will instead update the existing
487
    /// edge
488
    #[getter]
489
    fn multigraph(&self) -> bool {
20✔
490
        self.multigraph
20✔
491
    }
20✔
492

493
    /// Detect if the graph has parallel edges or not
494
    ///
495
    /// :returns: ``True`` if the graph has parallel edges, otherwise ``False``
496
    /// :rtype: bool
497
    #[pyo3(text_signature = "(self)")]
498
    fn has_parallel_edges(&self) -> bool {
8✔
499
        if !self.multigraph {
8✔
500
            return false;
2✔
501
        }
6✔
502
        self.graph.has_parallel_edges()
6✔
503
    }
8✔
504

505
    /// Clear all nodes and edges
506
    #[pyo3(text_signature = "(self)")]
507
    pub fn clear(&mut self) {
4✔
508
        self.graph.clear();
4✔
509
        self.node_removed = true;
4✔
510
    }
4✔
511

512
    /// Clears all edges, leaves nodes intact
513
    #[pyo3(text_signature = "(self)")]
514
    pub fn clear_edges(&mut self) {
4✔
515
        self.graph.clear_edges();
4✔
516
    }
4✔
517

518
    /// Return the number of nodes in the graph
519
    #[pyo3(text_signature = "(self)")]
520
    pub fn num_nodes(&self) -> usize {
28✔
521
        self.graph.node_count()
28✔
522
    }
28✔
523

524
    /// Return the number of edges in the graph
525
    #[pyo3(text_signature = "(self)")]
526
    pub fn num_edges(&self) -> usize {
34✔
527
        self.graph.edge_count()
34✔
528
    }
34✔
529

530
    /// Return a list of all edge data.
531
    ///
532
    /// :returns: A list of all the edge data objects in the graph
533
    /// :rtype: list[T]
534
    #[pyo3(text_signature = "(self)")]
535
    pub fn edges(&self) -> Vec<&Py<PyAny>> {
264✔
536
        self.graph
264✔
537
            .edge_indices()
264✔
538
            .map(|edge| self.graph.edge_weight(edge).unwrap())
8,658✔
539
            .collect()
264✔
540
    }
264✔
541

542
    /// Return a list of all edge indices.
543
    ///
544
    /// :returns: A list of all the edge indices in the graph
545
    /// :rtype: EdgeIndices
546
    #[pyo3(text_signature = "(self)")]
547
    pub fn edge_indices(&self) -> EdgeIndices {
40✔
548
        EdgeIndices {
549
            edges: self.graph.edge_indices().map(|edge| edge.index()).collect(),
74✔
550
        }
551
    }
40✔
552

553
    /// Return a list of indices of all directed edges between specified nodes
554
    ///
555
    /// :param int node_a: The index of the first node
556
    /// :param int node_b: The index of the second node
557
    ///
558
    /// :returns: A list of all the edge indices connecting the specified start and end node
559
    /// :rtype: EdgeIndices
560
    pub fn edge_indices_from_endpoints(&self, node_a: usize, node_b: usize) -> EdgeIndices {
6✔
561
        let node_a_index = NodeIndex::new(node_a);
6✔
562
        let node_b_index = NodeIndex::new(node_b);
6✔
563

564
        EdgeIndices {
565
            edges: self
6✔
566
                .graph
6✔
567
                .edges_directed(node_a_index, petgraph::Direction::Outgoing)
6✔
568
                .filter(|edge| edge.target() == node_b_index)
24✔
569
                .map(|edge| edge.id().index())
6✔
570
                .collect(),
6✔
571
        }
572
    }
6✔
573

574
    /// Return a list of all node data.
575
    ///
576
    /// :returns: A list of all the node data objects in the graph
577
    /// :rtype: list[S]
578
    #[pyo3(text_signature = "(self)")]
579
    pub fn nodes(&self) -> Vec<&Py<PyAny>> {
196✔
580
        self.graph
196✔
581
            .node_indices()
196✔
582
            .map(|node| self.graph.node_weight(node).unwrap())
8,366✔
583
            .collect()
196✔
584
    }
196✔
585

586
    /// Return a list of all node indices.
587
    ///
588
    /// :returns: A list of all the node indices in the graph
589
    /// :rtype: NodeIndices
590
    #[pyo3(text_signature = "(self)")]
591
    pub fn node_indices(&self) -> NodeIndices {
236✔
592
        NodeIndices {
593
            nodes: self.graph.node_indices().map(|node| node.index()).collect(),
9,584✔
594
        }
595
    }
236✔
596

597
    /// Return a list of all node indices.
598
    ///
599
    /// .. note::
600
    ///
601
    ///     This is identical to :meth:`.node_indices()`, which is the
602
    ///     preferred method to get the node indices in the graph. This
603
    ///     exists for backwards compatibility with earlier releases.
604
    ///
605
    /// :returns: A list of all the node indices in the graph
606
    /// :rtype: NodeIndices
607
    #[pyo3(text_signature = "(self)")]
608
    pub fn node_indexes(&self) -> NodeIndices {
116✔
609
        self.node_indices()
116✔
610
    }
116✔
611

612
    /// Check if the node exists in the graph.
613
    ///
614
    /// :param int node: The node index to check
615
    ///
616
    /// :returns: ``True`` if the node exists, ``False`` otherwise
617
    /// :rtype: bool
618
    #[pyo3(text_signature = "(self, node, /)")]
619
    pub fn has_node(&self, node: usize) -> bool {
10✔
620
        let index = NodeIndex::new(node);
10✔
621
        self.graph.contains_node(index)
10✔
622
    }
10✔
623

624
    /// Check if there is a directed edge from ``node_a`` to ``node_b``.
625
    ///
626
    /// :param int node_a: The index of the source node
627
    /// :param int node_b: The index of the destination node
628
    ///
629
    /// :returns: ``True`` if the edge exists, ``False`` otherwise
630
    /// :rtype: bool
631
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
632
    pub fn has_edge(&self, node_a: usize, node_b: usize) -> bool {
182✔
633
        let index_a = NodeIndex::new(node_a);
182✔
634
        let index_b = NodeIndex::new(node_b);
182✔
635
        self.graph.find_edge(index_a, index_b).is_some()
182✔
636
    }
182✔
637

638
    /// Return a list of data of all node successors in a directed graph
639
    ///
640
    /// A successor is defined as a node that has a directed edge pointing
641
    /// from the specified node. In a multigraph, where two nodes can be connected
642
    /// by multiple edges, each successor node is returned only once.
643
    ///
644
    ///     >>> G = rx.PyDiGraph()
645
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
646
    ///     NodeIndices[0, 1, 2, 3, 4]
647
    ///     >>> G.extend_from_edge_list([(0, 1), (1, 2), (1, 3), (1, 4)])
648
    ///     >>> G.successors(1)  # successors of the 'B' node
649
    ///     ['E', 'D', 'C']
650
    ///     >>> G.successors(10) # successors of an non-existing node
651
    ///     []
652
    ///
653
    /// .. seealso ::
654
    ///   To filter the successors by the attributes of the connecting edge,
655
    ///   see :func:`~find_successors_by_edge`.
656
    ///
657
    ///   See also :func:`~predecessors` and :func:`~neighbors`.
658
    ///
659
    ///   For undirected graphs, see :func:`~PyGraph.neighbors`.
660
    ///
661
    ///   To go beyond the nearest successors, see :func:`~rustworkx.descendants`.
662
    ///
663
    /// :param int node: The index of the node to get the predecessors for
664
    ///
665
    /// :returns: A list of the node data of all node's predecessors
666
    /// :rtype: list[S]
667
    #[pyo3(text_signature = "(self, node, /)")]
668
    pub fn successors(&self, node: usize) -> Vec<&Py<PyAny>> {
8✔
669
        let index = NodeIndex::new(node);
8✔
670
        let children = self
8✔
671
            .graph
8✔
672
            .neighbors_directed(index, petgraph::Direction::Outgoing);
8✔
673
        let mut successors: Vec<&Py<PyAny>> = Vec::new();
8✔
674
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
8✔
675
        for succ in children {
26✔
676
            match used_indices.entry(succ) {
26✔
677
                Entry::Vacant(used_indices_entry) => {
24✔
678
                    used_indices_entry.insert();
24✔
679
                    successors.push(self.graph.node_weight(succ).unwrap());
24✔
680
                }
24✔
681
                Entry::Occupied(_) => {}
2✔
682
            }
683
        }
684
        successors
8✔
685
    }
8✔
686

687
    /// Return a list of data of all node predecessors in a directed graph
688
    ///
689
    /// A predecessor is defined as a node that has a directed edge pointing
690
    /// to the specified node. In a multigraph, where two nodes can be connected
691
    /// by multiple edges, each predecessor node is returned only once.
692
    ///
693
    ///     >>> G = rx.PyDiGraph()
694
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
695
    ///     NodeIndices[0, 1, 2, 3, 4]
696
    ///     >>> G.extend_from_edge_list([(0, 3), (1, 3), (2, 3), (3, 4)])
697
    ///     >>> G.predecessors(3)  # predecessors of the 'D' node
698
    ///     ['C', 'B', 'A']
699
    ///     >>> G.predecessors(10) # predecessors of an non-existing node
700
    ///     []
701
    ///
702
    /// .. seealso ::
703
    ///   To filter the predecessors by the attributes of the connecting edge,
704
    ///   see :func:`~find_predecessors_by_edge`.
705
    ///
706
    ///   See also :func:`~successors`.
707
    ///
708
    ///   For undirected graphs, see :func:`~PyGraph.neighbors`.
709
    ///
710
    ///   To get beyond the nearest predecessors, see :func:`~rustworkx.ancestors`.
711
    ///
712
    /// :param int node: The index of the node to get the predecessors for
713
    ///
714
    /// :returns: A list of the node data of all node's predecessors
715
    /// :rtype: list[S]
716
    #[pyo3(text_signature = "(self, node, /)")]
717
    pub fn predecessors(&self, node: usize) -> Vec<&Py<PyAny>> {
8✔
718
        let index = NodeIndex::new(node);
8✔
719
        let parents = self
8✔
720
            .graph
8✔
721
            .neighbors_directed(index, petgraph::Direction::Incoming);
8✔
722
        let mut predec: Vec<&Py<PyAny>> = Vec::new();
8✔
723
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
8✔
724
        for pred in parents {
26✔
725
            match used_indices.entry(pred) {
26✔
726
                Entry::Vacant(used_indices_entry) => {
24✔
727
                    used_indices_entry.insert();
24✔
728
                    predec.push(self.graph.node_weight(pred).unwrap());
24✔
729
                }
24✔
730
                Entry::Occupied(_) => {}
2✔
731
            }
732
        }
733
        predec
8✔
734
    }
8✔
735

736
    /// Return a list of data associated with the successors of the given node,
737
    /// where the edges connecting to those nodes satisfy the provided
738
    /// filter function.
739
    ///
740
    /// A predecessor is defined as a node that has a directed edge pointing
741
    /// to the specified node. This method returns all nodes where the edges
742
    /// match the condition.
743
    ///
744
    ///     >>> G = rx.PyDiGraph()
745
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
746
    ///     NodeIndices[0, 1, 2, 3, 4]
747
    ///     >>> G.extend_from_weighted_edge_list([(0, 1, 10), (1, 2, 10), (1, 3, 20), (1, 4, 30)])
748
    ///     >>> G.find_successors_by_edge(1, lambda x: x < 25)
749
    ///     ['D', 'C']
750
    ///
751
    /// To get one node only, see :func:`~find_successor_node_by_edge`.
752
    ///
753
    /// See also :func:`~find_predecessors_by_edge`.
754
    ///
755
    /// :param int node: The index of the node to get the successors for
756
    ///
757
    /// :param filter_fn: The filter function to apply on edges. It takes
758
    ///     in one argument, the edge data payload/weight object, and returns a
759
    ///     boolean whether the edge matches the conditions or not.
760
    ///     If any edge returns ``True``, the node will be included.
761
    ///
762
    /// :returns: A list of the node data for all the successor nodes
763
    ///           where at least one edge leading to it matches the filter
764
    /// :rtype: list[S]
765
    #[pyo3(text_signature = "(self, node, filter_fn, /)")]
766
    pub fn find_successors_by_edge(
14✔
767
        &self,
14✔
768
        py: Python,
14✔
769
        node: usize,
14✔
770
        filter_fn: Py<PyAny>,
14✔
771
    ) -> PyResult<Vec<&Py<PyAny>>> {
14✔
772
        let index = NodeIndex::new(node);
14✔
773
        let mut successors: Vec<&Py<PyAny>> = Vec::new();
14✔
774
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
14✔
775

776
        let filter_edge = |edge: &Py<PyAny>| -> PyResult<bool> {
50✔
777
            let res = filter_fn.call1(py, (edge,))?;
50✔
778
            res.extract(py)
50✔
779
        };
50✔
780

781
        let raw_edges = self
14✔
782
            .graph
14✔
783
            .edges_directed(index, petgraph::Direction::Outgoing);
14✔
784

785
        for edge in raw_edges {
52✔
786
            let succ = edge.target();
52✔
787
            match used_indices.entry(succ) {
52✔
788
                Entry::Vacant(used_indices_entry) => {
50✔
789
                    let edge_weight = edge.weight();
50✔
790
                    if filter_edge(edge_weight)? {
50✔
791
                        used_indices_entry.insert();
26✔
792
                        successors.push(self.graph.node_weight(succ).unwrap());
26✔
793
                    }
26✔
794
                }
795
                Entry::Occupied(_) => {}
2✔
796
            }
797
        }
798
        Ok(successors)
14✔
799
    }
14✔
800

801
    /// Return a list of data associated with the predecessors of the given node,
802
    /// where the edges connecting from those nodes satisfy the provided
803
    /// filter function.
804
    ///
805
    /// A predecessor is defined as a node that has a directed edge pointing
806
    /// to the specified node. This method returns all nodes where the edges
807
    /// match the condition.
808
    ///
809
    ///     >>> G = rx.PyDiGraph()
810
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
811
    ///     NodeIndices[0, 1, 2, 3, 4]
812
    ///     >>> G.extend_from_weighted_edge_list([(0, 3, 10), (1, 3, 20), (2, 3, 30), (3, 4, 10)])
813
    ///     >>> G.find_predecessors_by_edge(3, lambda x: x < 25)
814
    ///     ['B', 'A']
815
    ///
816
    /// To get one node only, see :func:`~find_predecessor_node_by_edge`.
817
    ///
818
    /// :param int node: The index of the node to get the predecessors for
819
    /// :param Callable filter_fn: The filter function to apply on edges. It takes
820
    ///     in one argument, the edge data payload/weight object, and returns a
821
    ///     boolean whether the edge matches the conditions or not.
822
    ///     If any edge returns ``True``, the node will be included.
823
    ///
824
    /// :returns: A list of the node data for all the predecessor nodes
825
    ///           where at least one edge leading from it matches the filter
826
    /// :rtype: list[S]
827
    #[pyo3(text_signature = "(self, node, filter_fn, /)")]
828
    pub fn find_predecessors_by_edge(
14✔
829
        &self,
14✔
830
        py: Python,
14✔
831
        node: usize,
14✔
832
        filter_fn: Py<PyAny>,
14✔
833
    ) -> PyResult<Vec<&Py<PyAny>>> {
14✔
834
        let index = NodeIndex::new(node);
14✔
835
        let mut predec: Vec<&Py<PyAny>> = Vec::new();
14✔
836
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
14✔
837

838
        let filter_edge = |edge: &Py<PyAny>| -> PyResult<bool> {
52✔
839
            let res = filter_fn.call1(py, (edge,))?;
52✔
840
            res.extract(py)
52✔
841
        };
52✔
842

843
        let raw_edges = self
14✔
844
            .graph
14✔
845
            .edges_directed(index, petgraph::Direction::Incoming);
14✔
846

847
        for edge in raw_edges {
52✔
848
            let pred = edge.source();
52✔
849
            match used_indices.entry(pred) {
52✔
850
                Entry::Vacant(used_indices_entry) => {
52✔
851
                    let edge_weight = edge.weight();
52✔
852
                    if filter_edge(edge_weight)? {
52✔
853
                        used_indices_entry.insert();
26✔
854
                        predec.push(self.graph.node_weight(pred).unwrap());
26✔
855
                    }
26✔
856
                }
857
                Entry::Occupied(_) => {}
×
858
            }
859
        }
860
        Ok(predec)
14✔
861
    }
14✔
862

863
    /// Return the edge data for an edge between 2 nodes.
864
    ///
865
    /// :param int node_a: The index of the first node
866
    /// :param int node_b: The index of the second node
867
    ///
868
    /// :returns: The data object set for the edge
869
    /// :rtype: S
870
    /// :raises NoEdgeBetweenNodes: When there is no edge between the nodes
871
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
872
    pub fn get_edge_data(&self, node_a: usize, node_b: usize) -> PyResult<&Py<PyAny>> {
88✔
873
        let index_a = NodeIndex::new(node_a);
88✔
874
        let index_b = NodeIndex::new(node_b);
88✔
875
        let edge_index = match self.graph.find_edge(index_a, index_b) {
88✔
876
            Some(edge_index) => edge_index,
84✔
877
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
4✔
878
        };
879

880
        let data = self.graph.edge_weight(edge_index).unwrap();
84✔
881
        Ok(data)
84✔
882
    }
88✔
883

884
    /// Return the edge data for the edge by its given index
885
    ///
886
    /// :param int edge_index: The edge index to get the data for
887
    ///
888
    /// :returns: The data object for the edge
889
    /// :rtype: T
890
    /// :raises IndexError: when there is no edge present with the provided
891
    ///     index
892
    #[pyo3(text_signature = "(self, edge_index, /)")]
893
    pub fn get_edge_data_by_index(&self, edge_index: usize) -> PyResult<&Py<PyAny>> {
4✔
894
        let data = match self.graph.edge_weight(EdgeIndex::new(edge_index)) {
4✔
895
            Some(data) => data,
2✔
896
            None => {
897
                return Err(PyIndexError::new_err(format!(
2✔
898
                    "Provided edge index {edge_index} is not present in the graph"
2✔
899
                )));
2✔
900
            }
901
        };
902
        Ok(data)
2✔
903
    }
4✔
904

905
    /// Return the edge endpoints for the edge by its given index
906
    ///
907
    /// :param int edge_index: The edge index to get the endpoints for
908
    ///
909
    /// :returns: The endpoint tuple for the edge
910
    /// :rtype: tuple[int, int]
911
    /// :raises IndexError: when there is no edge present with the provided
912
    ///     index
913
    #[pyo3(text_signature = "(self, edge_index, /)")]
914
    pub fn get_edge_endpoints_by_index(&self, edge_index: usize) -> PyResult<(usize, usize)> {
4✔
915
        let endpoints = match self.graph.edge_endpoints(EdgeIndex::new(edge_index)) {
4✔
916
            Some(endpoints) => (endpoints.0.index(), endpoints.1.index()),
2✔
917
            None => {
918
                return Err(PyIndexError::new_err(format!(
2✔
919
                    "Provided edge index {edge_index} is not present in the graph"
2✔
920
                )));
2✔
921
            }
922
        };
923
        Ok(endpoints)
2✔
924
    }
4✔
925

926
    /// Update an edge's weight/payload in place
927
    ///
928
    /// If there are parallel edges in the graph only one edge will be updated.
929
    /// if you need to update a specific edge or need to ensure all parallel
930
    /// edges get updated you should use
931
    /// :meth:`~rustworkx.PyDiGraph.update_edge_by_index` instead.
932
    ///
933
    /// :param int source: The index of the first node
934
    /// :param int target: The index of the second node
935
    ///
936
    /// :raises NoEdgeBetweenNodes: When there is no edge between nodes
937
    #[pyo3(text_signature = "(self, source, target, edge, /)")]
938
    pub fn update_edge(&mut self, source: usize, target: usize, edge: Py<PyAny>) -> PyResult<()> {
4✔
939
        let index_a = NodeIndex::new(source);
4✔
940
        let index_b = NodeIndex::new(target);
4✔
941
        let edge_index = match self.graph.find_edge(index_a, index_b) {
4✔
942
            Some(edge_index) => edge_index,
2✔
943
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
2✔
944
        };
945
        let data = self.graph.edge_weight_mut(edge_index).unwrap();
2✔
946
        *data = edge;
2✔
947
        Ok(())
2✔
948
    }
4✔
949

950
    /// Update an edge's weight/data payload in place by the edge index
951
    ///
952
    /// :param int edge_index: The index of the edge
953
    /// :param T edge: The python object to attach to the edge
954
    ///
955
    /// :raises IndexError: when there is no edge present with the provided
956
    ///     index
957
    #[pyo3(text_signature = "(self, edge_index, edge, /)")]
958
    pub fn update_edge_by_index(&mut self, edge_index: usize, edge: Py<PyAny>) -> PyResult<()> {
4,190✔
959
        match self.graph.edge_weight_mut(EdgeIndex::new(edge_index)) {
4,190✔
960
            Some(data) => *data = edge,
4,188✔
961
            None => return Err(PyIndexError::new_err("No edge found for index")),
2✔
962
        };
963
        Ok(())
4,188✔
964
    }
4,190✔
965

966
    /// Return the node data for a given node index
967
    ///
968
    /// :param int node: The index of the node
969
    ///
970
    /// :returns: The data object set for that node
971
    /// :rtype: S
972
    /// :raises IndexError: when an invalid node index is provided
973
    #[pyo3(text_signature = "(self, node, /)")]
974
    pub fn get_node_data(&self, node: usize) -> PyResult<&Py<PyAny>> {
96✔
975
        let index = NodeIndex::new(node);
96✔
976
        let node = match self.graph.node_weight(index) {
96✔
977
            Some(node) => node,
94✔
978
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
979
        };
980
        Ok(node)
94✔
981
    }
96✔
982

983
    /// Return the edge data for all the edges between 2 nodes.
984
    ///
985
    /// :param int node_a: The index of the first node
986
    /// :param int node_b: The index of the second node
987
    ///
988
    /// :returns: A list with all the data objects for the edges between nodes
989
    /// :rtype: list[T]
990
    /// :raises NoEdgeBetweenNodes: When there is no edge between nodes
991
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
992
    pub fn get_all_edge_data(&self, node_a: usize, node_b: usize) -> PyResult<Vec<&Py<PyAny>>> {
22✔
993
        let index_a = NodeIndex::new(node_a);
22✔
994
        let index_b = NodeIndex::new(node_b);
22✔
995
        let raw_edges = self
22✔
996
            .graph
22✔
997
            .edges_directed(index_a, petgraph::Direction::Outgoing);
22✔
998
        let out: Vec<&Py<PyAny>> = raw_edges
22✔
999
            .filter(|x| x.target() == index_b)
28✔
1000
            .map(|edge| edge.weight())
24✔
1001
            .collect();
22✔
1002
        if out.is_empty() {
22✔
1003
            Err(NoEdgeBetweenNodes::new_err("No edge found between nodes"))
2✔
1004
        } else {
1005
            Ok(out)
20✔
1006
        }
1007
    }
22✔
1008

1009
    /// Get edge list
1010
    ///
1011
    /// Returns a list of tuples of the form ``(source, target)`` where
1012
    /// ``source`` and ``target`` are the node indices.
1013
    ///
1014
    /// :returns: An edge list without weights
1015
    /// :rtype: EdgeList
1016
    pub fn edge_list(&self) -> EdgeList {
222✔
1017
        EdgeList {
1018
            edges: self
222✔
1019
                .graph
222✔
1020
                .edge_references()
222✔
1021
                .map(|edge| (edge.source().index(), edge.target().index()))
2,002,880✔
1022
                .collect(),
222✔
1023
        }
1024
    }
222✔
1025

1026
    /// Get edge list with weights
1027
    ///
1028
    /// Returns a list of tuples of the form ``(source, target, weight)`` where
1029
    /// ``source`` and ``target`` are the node indices and ``weight`` is the
1030
    /// payload of the edge.
1031
    ///
1032
    /// :returns: An edge list with weights
1033
    /// :rtype: WeightedEdgeList
1034
    pub fn weighted_edge_list(&self, py: Python) -> WeightedEdgeList {
198✔
1035
        WeightedEdgeList {
1036
            edges: self
198✔
1037
                .graph
198✔
1038
                .edge_references()
198✔
1039
                .map(|edge| {
29,272✔
1040
                    (
29,272✔
1041
                        edge.source().index(),
29,272✔
1042
                        edge.target().index(),
29,272✔
1043
                        edge.weight().clone_ref(py),
29,272✔
1044
                    )
29,272✔
1045
                })
29,272✔
1046
                .collect(),
198✔
1047
        }
1048
    }
198✔
1049

1050
    /// Get an edge index map
1051
    ///
1052
    /// Returns a read only mapping from edge indices to the weighted edge
1053
    /// tuple in the form: ``{0: (0, 1, "weight"), 1: (2, 3, 2.3)}``
1054
    ///
1055
    /// :returns: An edge index map
1056
    /// :rtype: EdgeIndexMap
1057
    #[pyo3(text_signature = "(self)")]
1058
    pub fn edge_index_map(&self, py: Python) -> EdgeIndexMap {
64✔
1059
        EdgeIndexMap {
1060
            edge_map: self
64✔
1061
                .graph
64✔
1062
                .edge_references()
64✔
1063
                .map(|edge| {
4,246✔
1064
                    (
4,246✔
1065
                        edge.id().index(),
4,246✔
1066
                        (
4,246✔
1067
                            edge.source().index(),
4,246✔
1068
                            edge.target().index(),
4,246✔
1069
                            edge.weight().clone_ref(py),
4,246✔
1070
                        ),
4,246✔
1071
                    )
4,246✔
1072
                })
4,246✔
1073
                .collect(),
64✔
1074
        }
1075
    }
64✔
1076

1077
    /// Remove a node from the graph.
1078
    ///
1079
    /// :param int node: The index of the node to remove. If the index is not
1080
    ///     present in the graph it will be ignored and this function will have
1081
    ///     no effect.
1082
    #[pyo3(text_signature = "(self, node, /)")]
1083
    pub fn remove_node(&mut self, node: usize) -> PyResult<()> {
210✔
1084
        let index = NodeIndex::new(node);
210✔
1085
        self.graph.remove_node(index);
210✔
1086
        self.node_removed = true;
210✔
1087
        Ok(())
210✔
1088
    }
210✔
1089

1090
    /// Remove a node from the graph and add edges from all predecessors to all
1091
    /// successors
1092
    ///
1093
    /// By default the data/weight on edges into the removed node will be used
1094
    /// for the retained edges.
1095
    ///
1096
    /// This function has a minimum time complexity of :math:`\mathcal O(e_i e_o)`, where
1097
    /// :math:`e_i` and :math:`e_o` are the numbers of incoming and outgoing edges respectively.
1098
    /// If your ``condition`` can be cast as an equality between two hashable quantities, consider
1099
    /// using :meth:`remove_node_retain_edges_by_key` instead, or if your ``condition`` is
1100
    /// referential object identity of the edge weights, consider
1101
    /// :meth:`remove_node_retain_edges_by_id`.
1102
    ///
1103
    /// :param int node: The index of the node to remove. If the index is not
1104
    ///     present in the graph it will be ignored and this function will have
1105
    ///     no effect.
1106
    /// :param bool use_outgoing: If set to ``True`` the weight/data from the
1107
    ///     edge outgoing from ``node`` will be used in the retained edge
1108
    ///     instead of the default weight/data from the incoming edge.
1109
    /// :param Callable condition: A callable that will be passed 2 edge weight/data
1110
    ///     objects, one from the incoming edge to ``node`` the other for the
1111
    ///     outgoing edge, and will return a ``bool`` on whether an edge should
1112
    ///     be retained. For example setting this kwarg to::
1113
    ///
1114
    ///         lambda in_edge, out_edge: in_edge == out_edge
1115
    ///
1116
    ///     would only retain edges if the input edge to ``node`` had the same
1117
    ///     data payload as the outgoing edge.
1118
    #[pyo3(text_signature = "(self, node, /, use_outgoing=False, condition=None)")]
1119
    #[pyo3(signature=(node, use_outgoing=false, condition=None))]
1120
    pub fn remove_node_retain_edges(
14✔
1121
        &mut self,
14✔
1122
        py: Python,
14✔
1123
        node: usize,
14✔
1124
        use_outgoing: bool,
14✔
1125
        condition: Option<Py<PyAny>>,
14✔
1126
    ) -> PyResult<()> {
14✔
1127
        let index = NodeIndex::new(node);
14✔
1128
        let mut edge_list: Vec<(NodeIndex, NodeIndex, Py<PyAny>)> = Vec::new();
14✔
1129

1130
        fn check_condition(
28✔
1131
            py: Python,
28✔
1132
            condition: &Option<Py<PyAny>>,
28✔
1133
            in_weight: &Py<PyAny>,
28✔
1134
            out_weight: &Py<PyAny>,
28✔
1135
        ) -> PyResult<bool> {
28✔
1136
            match condition {
28✔
1137
                Some(condition) => {
8✔
1138
                    let res = condition.call1(py, (in_weight, out_weight))?;
8✔
1139
                    Ok(res.extract(py)?)
8✔
1140
                }
1141
                None => Ok(true),
20✔
1142
            }
1143
        }
28✔
1144

1145
        for (source, in_weight) in self
18✔
1146
            .graph
14✔
1147
            .edges_directed(index, petgraph::Direction::Incoming)
14✔
1148
            .map(|x| (x.source(), x.weight()))
18✔
1149
        {
1150
            for (target, out_weight) in self
28✔
1151
                .graph
18✔
1152
                .edges_directed(index, petgraph::Direction::Outgoing)
18✔
1153
                .map(|x| (x.target(), x.weight()))
28✔
1154
            {
1155
                let weight = if use_outgoing { out_weight } else { in_weight };
28✔
1156
                if check_condition(py, &condition, in_weight, out_weight)? {
28✔
1157
                    edge_list.push((source, target, weight.clone_ref(py)));
24✔
1158
                }
24✔
1159
            }
1160
        }
1161
        for (source, target, weight) in edge_list {
24✔
1162
            self._add_edge(source, target, weight)?;
24✔
1163
        }
1164
        self.node_removed = self.graph.remove_node(index).is_some();
14✔
1165
        Ok(())
14✔
1166
    }
14✔
1167

1168
    /// Remove a node from the graph and add edges from predecessors to successors in cases where
1169
    /// an incoming and outgoing edge have the same weight by Python object identity.
1170
    ///
1171
    /// This function has a minimum time complexity of :math:`\mathcal O(e_i + e_o)`, where
1172
    /// :math:`e_i` is the number of incoming edges and :math:`e_o` the number of outgoing edges
1173
    /// (the full complexity depends on the number of new edges to be created).
1174
    ///
1175
    /// Edges will be added between all pairs of predecessor and successor nodes that have the same
1176
    /// weight.  As a consequence, any weight which appears only on predecessor edges will not
1177
    /// appear in the output, as there are no successors to pair it with.
1178
    ///
1179
    /// :param int node: The index of the node to remove. If the index is not present in the graph
1180
    ///     it will be ignored and this function will have no effect.
1181
    #[pyo3(signature=(node, /))]
1182
    pub fn remove_node_retain_edges_by_id(&mut self, py: Python, node: usize) -> PyResult<()> {
8✔
1183
        // As many indices as will fit inline within the minimum inline size of a `SmallVec`.  Many
1184
        // use cases of this likely only have one inbound and outbound edge with each id anyway.
1185
        const INLINE_SIZE: usize =
1186
            2 * ::std::mem::size_of::<usize>() / ::std::mem::size_of::<NodeIndex>();
1187
        let new_node_list = || SmallVec::<[NodeIndex; INLINE_SIZE]>::new();
44✔
1188
        let node_index = NodeIndex::new(node);
8✔
1189
        let in_edges = {
8✔
1190
            let mut in_edges = HashMap::new();
8✔
1191
            for edge in self
26✔
1192
                .graph
8✔
1193
                .edges_directed(node_index, petgraph::Direction::Incoming)
8✔
1194
            {
26✔
1195
                in_edges
26✔
1196
                    .entry(PyAnyId(edge.weight().clone_ref(py)))
26✔
1197
                    .or_insert_with(new_node_list)
26✔
1198
                    .push(edge.source());
26✔
1199
            }
26✔
1200
            in_edges
8✔
1201
        };
1202
        let mut out_edges = {
8✔
1203
            let mut out_edges = HashMap::new();
8✔
1204
            for edge in self
26✔
1205
                .graph
8✔
1206
                .edges_directed(node_index, petgraph::Direction::Outgoing)
8✔
1207
            {
26✔
1208
                out_edges
26✔
1209
                    .entry(PyAnyId(edge.weight().clone_ref(py)))
26✔
1210
                    .or_insert_with(new_node_list)
26✔
1211
                    .push(edge.target());
26✔
1212
            }
26✔
1213
            out_edges
8✔
1214
        };
1215

1216
        for (weight, in_edges_subset) in in_edges {
22✔
1217
            let out_edges_subset = match out_edges.remove(&weight) {
22✔
1218
                Some(out_edges_key) => out_edges_key,
20✔
1219
                None => continue,
2✔
1220
            };
1221
            for source in in_edges_subset {
24✔
1222
                for target in out_edges_subset.iter() {
30✔
1223
                    self._add_edge(source, *target, weight.clone_ref(py))?;
30✔
1224
                }
1225
            }
1226
        }
1227
        self.node_removed = self.graph.remove_node(node_index).is_some();
8✔
1228
        Ok(())
8✔
1229
    }
8✔
1230

1231
    /// Remove a node from the graph and add edges from predecessors to successors in cases where
1232
    /// an incoming and outgoing edge have the same weight by Python object equality.
1233
    ///
1234
    /// This function has a minimum time complexity of :math:`\mathcal O(e_i + e_o)`, where
1235
    /// :math:`e_i` is the number of incoming edges and :math:`e_o` the number of outgoing edges
1236
    /// (the full complexity depends on the number of new edges to be created).
1237
    ///
1238
    /// Edges will be added between all pairs of predecessor and successor nodes that have equal
1239
    /// weights.  As a consequence, any weight which appears only on predecessor edges will not
1240
    /// appear in the output, as there are no successors to pair it with.
1241
    ///
1242
    /// If there are multiple edges with the same weight, the exact Python object used on the new
1243
    /// edges is an implementation detail and may change.  The only guarantees are that it will be
1244
    /// deterministic for a given graph, and that it will be drawn from the incoming edges if
1245
    /// ``use_outgoing=False`` (the default) or from the outgoing edges if ``use_outgoing=True``.
1246
    ///
1247
    /// :param int node: The index of the node to remove. If the index is not present in the graph
1248
    ///     it will be ignored and this function will have no effect.
1249
    /// :param Callable key: A callable Python object that is called once for each connected edge, to
1250
    ///     generate the "key" for that weight.  It is passed exactly one argument positionally
1251
    ///     (the weight of the edge), and should return a Python object that is hashable and
1252
    ///     implements equality checking with all other relevant keys.  If not given, the edge
1253
    ///     weights will be used directly.
1254
    /// :param bool use_outgoing: If ``False`` (default), the new edges will use the weight from
1255
    ///     one of the incoming edges.  If ``True``, they will instead use a weight from one of the
1256
    ///     outgoing edges.
1257
    #[pyo3(signature=(node, /, key=None, *, use_outgoing=false))]
1258
    pub fn remove_node_retain_edges_by_key(
4✔
1259
        &mut self,
4✔
1260
        py: Python,
4✔
1261
        node: usize,
4✔
1262
        key: Option<Py<PyAny>>,
4✔
1263
        use_outgoing: bool,
4✔
1264
    ) -> PyResult<()> {
4✔
1265
        let node_index = NodeIndex::new(node);
4✔
1266
        let in_edges = {
4✔
1267
            let in_edges = PyDict::new(py);
4✔
1268
            for edge in self
14✔
1269
                .graph
4✔
1270
                .edges_directed(node_index, petgraph::Direction::Incoming)
4✔
1271
            {
1272
                let key_value = if let Some(key_fn) = &key {
14✔
1273
                    key_fn.call1(py, (edge.weight(),))?
14✔
1274
                } else {
1275
                    edge.weight().clone_ref(py)
×
1276
                };
1277
                if let Some(edge_data) = in_edges.get_item(key_value.bind(py))? {
14✔
1278
                    let edge_data = edge_data.downcast::<RemoveNodeEdgeValue>()?;
4✔
1279
                    edge_data.borrow_mut().nodes.push(edge.source());
4✔
1280
                } else {
1281
                    in_edges.set_item(
10✔
1282
                        key_value,
10✔
1283
                        RemoveNodeEdgeValue {
10✔
1284
                            weight: edge.weight().clone_ref(py),
10✔
1285
                            nodes: vec![edge.source()],
10✔
1286
                        }
10✔
1287
                        .into_pyobject(py)?,
10✔
1288
                    )?
×
1289
                }
1290
            }
1291
            in_edges
4✔
1292
        };
1293
        let out_edges = {
4✔
1294
            let out_edges = PyDict::new(py);
4✔
1295
            for edge in self
14✔
1296
                .graph
4✔
1297
                .edges_directed(node_index, petgraph::Direction::Outgoing)
4✔
1298
            {
1299
                let key_value = if let Some(key_fn) = &key {
14✔
1300
                    key_fn.call1(py, (edge.weight(),))?
14✔
1301
                } else {
1302
                    edge.weight().clone_ref(py)
×
1303
                };
1304
                if let Some(edge_data) = out_edges.get_item(key_value.bind(py))? {
14✔
1305
                    let edge_data = edge_data.downcast::<RemoveNodeEdgeValue>()?;
4✔
1306
                    edge_data.borrow_mut().nodes.push(edge.target());
4✔
1307
                } else {
1308
                    out_edges.set_item(
10✔
1309
                        key_value,
10✔
1310
                        RemoveNodeEdgeValue {
10✔
1311
                            weight: edge.weight().clone_ref(py),
10✔
1312
                            nodes: vec![edge.target()],
10✔
1313
                        }
10✔
1314
                        .into_pyobject(py)?,
10✔
1315
                    )?
×
1316
                }
1317
            }
1318
            out_edges
4✔
1319
        };
1320

1321
        for (in_key, in_edge_data) in in_edges {
10✔
1322
            let in_edge_data = in_edge_data.downcast::<RemoveNodeEdgeValue>()?.borrow();
10✔
1323
            let out_edge_data = match out_edges.get_item(in_key)? {
10✔
1324
                Some(out_edge_data) => out_edge_data.downcast::<RemoveNodeEdgeValue>()?.borrow(),
8✔
1325
                None => continue,
2✔
1326
            };
1327
            for source in in_edge_data.nodes.iter() {
12✔
1328
                for target in out_edge_data.nodes.iter() {
18✔
1329
                    let weight = if use_outgoing {
18✔
1330
                        out_edge_data.weight.clone_ref(py)
2✔
1331
                    } else {
1332
                        in_edge_data.weight.clone_ref(py)
16✔
1333
                    };
1334
                    self._add_edge(*source, *target, weight)?;
18✔
1335
                }
1336
            }
1337
        }
1338
        self.node_removed = self.graph.remove_node(node_index).is_some();
4✔
1339
        Ok(())
4✔
1340
    }
4✔
1341

1342
    /// Add an edge between 2 nodes.
1343
    ///
1344
    /// Use add_child() or add_parent() to create a node with an edge at the
1345
    /// same time as an edge for better performance. Using this method
1346
    /// allows for adding duplicate edges between nodes if the ``multigraph``
1347
    /// attribute is set to ``True``.
1348
    ///
1349
    /// :param int parent: The index of the parent node
1350
    /// :param int child: The index of the child node
1351
    /// :param T edge: The Python object to attach to the edge.
1352
    ///
1353
    /// :returns: The index of the newly created edge
1354
    /// :rtype: int
1355
    ///
1356
    /// :raises: PyIndexError: When the new edge would create a cycle
1357
    #[pyo3(text_signature = "(self, parent, child, edge, /)")]
1358
    pub fn add_edge(&mut self, parent: usize, child: usize, edge: Py<PyAny>) -> PyResult<usize> {
2,024,392✔
1359
        let p_index = NodeIndex::new(parent);
2,024,392✔
1360
        let c_index = NodeIndex::new(child);
2,024,392✔
1361
        if !self.graph.contains_node(p_index) || !self.graph.contains_node(c_index) {
2,024,392✔
1362
            return Err(PyIndexError::new_err(
6✔
1363
                "One of the endpoints of the edge does not exist in graph",
6✔
1364
            ));
6✔
1365
        }
2,024,386✔
1366
        let out_index = self._add_edge(p_index, c_index, edge)?;
2,024,386✔
1367
        Ok(out_index)
2,024,378✔
1368
    }
2,024,392✔
1369

1370
    /// Add new edges to the graph.
1371
    ///
1372
    /// :param iterable[tuple[int, int, T]] obj_list: An iterable of tuples of the form
1373
    ///     ``(parent, child, obj)`` to attach to the graph. ``parent`` and
1374
    ///     ``child`` are integer indices describing where an edge should be
1375
    ///     added, and obj is the python object for the edge data.
1376
    ///
1377
    /// :returns: A list of indices of the newly created edges
1378
    /// :rtype: EdgeIndices
1379
    #[pyo3(text_signature = "(self, obj_list, /)")]
1380
    pub fn add_edges_from(&mut self, obj_list: Bound<'_, PyAny>) -> PyResult<Vec<usize>> {
358✔
1381
        let mut out_list = Vec::new();
358✔
1382
        for py_obj in obj_list.try_iter()? {
2,021,916✔
1383
            let obj = py_obj?.extract::<(usize, usize, Py<PyAny>)>()?;
2,021,916✔
1384
            let edge = self.add_edge(obj.0, obj.1, obj.2)?;
2,021,916✔
1385
            out_list.push(edge);
2,021,912✔
1386
        }
1387
        Ok(out_list)
354✔
1388
    }
358✔
1389

1390
    /// Add new edges to the graph without python data.
1391
    ///
1392
    /// :param iterable[tuple[int, int]] obj_list: An iterable of tuples of the form
1393
    ///     ``(parent, child)`` to attach to the graph. ``parent`` and
1394
    ///     ``child`` are integer indices describing where an edge should be
1395
    ///     added. Unlike :meth:`add_edges_from` there is no data payload and
1396
    ///     when the edge is created None will be used.
1397
    ///
1398
    /// :returns: A list of indices of the newly created edges
1399
    /// :rtype: EdgeIndices
1400
    #[pyo3(text_signature = "(self, obj_list, /)")]
1401
    pub fn add_edges_from_no_data(
190✔
1402
        &mut self,
190✔
1403
        py: Python,
190✔
1404
        obj_list: Bound<'_, PyAny>,
190✔
1405
    ) -> PyResult<Vec<usize>> {
190✔
1406
        let mut out_list = Vec::new();
190✔
1407
        for py_obj in obj_list.try_iter()? {
1,428✔
1408
            let obj = py_obj?.extract::<(usize, usize)>()?;
1,428✔
1409
            let edge = self.add_edge(obj.0, obj.1, py.None())?;
1,428✔
1410
            out_list.push(edge);
1,424✔
1411
        }
1412
        Ok(out_list)
186✔
1413
    }
190✔
1414

1415
    /// Extend graph from an edge list
1416
    ///
1417
    /// This method differs from :meth:`add_edges_from_no_data` in that it will
1418
    /// add nodes if a node index is not present in the edge list.
1419
    ///
1420
    /// :param iterable[tuple[int, int]] edge_list: An iterable of tuples
1421
    ///     in the form ``(source, target)`` where ``source`` and ``target``
1422
    ///     are integer node indices. If the node index
1423
    ///     is not present in the graph, nodes will be added (with a node
1424
    ///     weight of ``None``) to that index.
1425
    #[pyo3(text_signature = "(self, edge_list, /)")]
1426
    pub fn extend_from_edge_list(
164✔
1427
        &mut self,
164✔
1428
        py: Python,
164✔
1429
        edge_list: Bound<'_, PyAny>,
164✔
1430
    ) -> PyResult<()> {
164✔
1431
        for py_obj in edge_list.try_iter()? {
1,674✔
1432
            let (source, target) = py_obj?.extract::<(usize, usize)>()?;
1,674✔
1433
            let max_index = cmp::max(source, target);
1,674✔
1434
            while max_index >= self.node_count() {
2,844✔
1435
                self.graph.add_node(py.None());
1,170✔
1436
            }
1,170✔
1437
            self._add_edge(NodeIndex::new(source), NodeIndex::new(target), py.None())?;
1,674✔
1438
        }
1439
        Ok(())
162✔
1440
    }
164✔
1441

1442
    /// Extend graph from a weighted edge list
1443
    ///
1444
    /// This method differs from :meth:`add_edges_from` in that it will
1445
    /// add nodes if a node index is not present in the edge list.
1446
    ///
1447
    /// :param iterable[tuple[int, int, T]] edge_list: An iterable of tuples
1448
    ///     in the form ``(source, target, weight)`` where ``source`` and ``target``
1449
    ///     are integer node indices. If the node index is not present in the graph
1450
    ///     nodes will be added (with a node weight of ``None``) to that index.
1451
    #[pyo3(text_signature = "(self, edge_list, /)")]
1452
    pub fn extend_from_weighted_edge_list(
64✔
1453
        &mut self,
64✔
1454
        py: Python,
64✔
1455
        edge_list: Bound<'_, PyAny>,
64✔
1456
    ) -> PyResult<()> {
64✔
1457
        for py_obj in edge_list.try_iter()? {
328✔
1458
            let (source, target, weight) = py_obj?.extract::<(usize, usize, Py<PyAny>)>()?;
328✔
1459
            let max_index = cmp::max(source, target);
328✔
1460
            while max_index >= self.node_count() {
568✔
1461
                self.graph.add_node(py.None());
240✔
1462
            }
240✔
1463
            self._add_edge(NodeIndex::new(source), NodeIndex::new(target), weight)?;
328✔
1464
        }
1465
        Ok(())
62✔
1466
    }
64✔
1467

1468
    /// Insert a node between a list of reference nodes and all their predecessors
1469
    ///
1470
    /// This essentially iterates over all edges leading into the reference nodes
1471
    /// specified in the ``ref_nodes`` parameter, removes those edges and then
1472
    /// adds 2 edges, one from the predecessor of ``ref_node`` to ``node``
1473
    /// and the other one from ``node`` to ``ref_node``. The edge payloads of
1474
    /// the two newly created edges are copied by reference from the original
1475
    /// edge that gets removed.
1476
    ///
1477
    /// :param int node: The index of the node to insert
1478
    /// :param list[int] ref_nodes: The list of indices of reference nodes
1479
    ///      to be prepended by ``node``
1480
    #[pyo3(text_signature = "(self, node, ref_nodes, /)")]
1481
    pub fn insert_node_on_in_edges_multiple(
8✔
1482
        &mut self,
8✔
1483
        py: Python,
8✔
1484
        node: usize,
8✔
1485
        ref_nodes: Vec<usize>,
8✔
1486
    ) -> PyResult<()> {
8✔
1487
        for ref_node in ref_nodes {
14✔
1488
            self.insert_between(py, node, ref_node, false)?;
14✔
1489
        }
1490
        Ok(())
8✔
1491
    }
8✔
1492

1493
    /// Insert a node between a list of reference nodes and all their successors
1494
    ///
1495
    /// This essentially iterates over all edges leading out of the reference nodes
1496
    /// specified in the ``ref_nodes`` parameter, removes those edges and then
1497
    /// adds 2 edges, one from ``ref_node`` to ``node`` and the other one from
1498
    /// ``node`` to the successor of ``ref_node``. The edge payloads of the two
1499
    /// newly created edges are copied by reference from the original edge that
1500
    /// gets removed.
1501
    ///
1502
    /// :param int node: The index of the node to insert
1503
    /// :param list[int] ref_nodes: The list of indices of reference nodes
1504
    ///     to be appended by ``node``
1505
    #[pyo3(text_signature = "(self, node, ref_nodes, /)")]
1506
    pub fn insert_node_on_out_edges_multiple(
8✔
1507
        &mut self,
8✔
1508
        py: Python,
8✔
1509
        node: usize,
8✔
1510
        ref_nodes: Vec<usize>,
8✔
1511
    ) -> PyResult<()> {
8✔
1512
        for ref_node in ref_nodes {
14✔
1513
            self.insert_between(py, node, ref_node, true)?;
14✔
1514
        }
1515
        Ok(())
8✔
1516
    }
8✔
1517

1518
    /// Insert a node between a reference node and all its predecessor nodes
1519
    ///
1520
    /// This essentially iterates over all edges leading into the reference node
1521
    /// specified in the ``ref_node`` parameter, removes those edges and then
1522
    /// adds 2 edges, one from the predecessor of ``ref_node`` to ``node`` and
1523
    /// the other one from ``node`` to ``ref_node``. The edge payloads of the two
1524
    /// newly created edges are copied by reference from the original edge that
1525
    /// gets removed.
1526
    ///
1527
    /// :param int node: The index of the node to insert
1528
    /// :param int ref_node: The index of the reference node to be prepended by ``node``
1529
    #[pyo3(text_signature = "(self, node, ref_node, /)")]
1530
    pub fn insert_node_on_in_edges(
8✔
1531
        &mut self,
8✔
1532
        py: Python,
8✔
1533
        node: usize,
8✔
1534
        ref_node: usize,
8✔
1535
    ) -> PyResult<()> {
8✔
1536
        self.insert_between(py, node, ref_node, false)?;
8✔
1537
        Ok(())
8✔
1538
    }
8✔
1539

1540
    /// Insert a node between a reference node and all its successor nodes
1541
    ///
1542
    /// This essentially iterates over all edges leading out of the reference node
1543
    /// specified in the ``ref_node`` parameter, removes those edges and then
1544
    /// adds 2 edges, one from ``ref_node`` to ``node`` and the other one from
1545
    /// ``node`` to the successor of ``ref_node``. The edge payloads of the two
1546
    /// newly created edges are copied by reference from the original edge
1547
    /// that gets removed.
1548
    ///
1549
    /// :param int node: The index of the node to insert
1550
    /// :param int ref_node: The index of the reference node to be appended by ``node``
1551
    #[pyo3(text_signature = "(self, node, ref_node, /)")]
1552
    pub fn insert_node_on_out_edges(
8✔
1553
        &mut self,
8✔
1554
        py: Python,
8✔
1555
        node: usize,
8✔
1556
        ref_node: usize,
8✔
1557
    ) -> PyResult<()> {
8✔
1558
        self.insert_between(py, node, ref_node, true)?;
8✔
1559
        Ok(())
8✔
1560
    }
8✔
1561

1562
    /// Remove an edge between 2 nodes.
1563
    ///
1564
    /// Note if there are multiple edges between the specified nodes only one
1565
    /// will be removed.
1566
    ///
1567
    /// :param int parent: The index of the parent node
1568
    /// :param int child: The index of the child node
1569
    ///
1570
    /// :raises NoEdgeBetweenNodes: If there is no edge between the nodes
1571
    ///     specified
1572
    #[pyo3(text_signature = "(self, parent, child, /)")]
1573
    pub fn remove_edge(&mut self, parent: usize, child: usize) -> PyResult<()> {
8✔
1574
        let p_index = NodeIndex::new(parent);
8✔
1575
        let c_index = NodeIndex::new(child);
8✔
1576
        let edge_index = match self.graph.find_edge(p_index, c_index) {
8✔
1577
            Some(edge_index) => edge_index,
4✔
1578
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
4✔
1579
        };
1580
        self.graph.remove_edge(edge_index);
4✔
1581
        Ok(())
4✔
1582
    }
8✔
1583

1584
    /// Remove an edge identified by the provided index
1585
    ///
1586
    /// :param int edge: The index of the edge to remove
1587
    #[pyo3(text_signature = "(self, edge, /)")]
1588
    pub fn remove_edge_from_index(&mut self, edge: usize) -> PyResult<()> {
12✔
1589
        let edge_index = EdgeIndex::new(edge);
12✔
1590
        self.graph.remove_edge(edge_index);
12✔
1591
        Ok(())
12✔
1592
    }
12✔
1593

1594
    /// Remove edges from the graph.
1595
    ///
1596
    /// Note if there are multiple edges between the specified nodes only one
1597
    /// will be removed.
1598
    ///
1599
    /// :param iterable[tuple[int, int]] index_list: An iterable of node index pairs
1600
    ///     to remove from the graph
1601
    ///
1602
    /// :raises NoEdgeBetweenNodes: If there are no edges between a specified
1603
    ///     pair of nodes.
1604
    #[pyo3(text_signature = "(self, index_list, /)")]
1605
    pub fn remove_edges_from(&mut self, index_list: Bound<'_, PyAny>) -> PyResult<()> {
6✔
1606
        for py_obj in index_list.try_iter()? {
10✔
1607
            let (x, y) = py_obj?.extract::<(usize, usize)>()?;
10✔
1608
            let (p_index, c_index) = (NodeIndex::new(x), NodeIndex::new(y));
10✔
1609
            let edge_index = match self.graph.find_edge(p_index, c_index) {
10✔
1610
                Some(edge_index) => edge_index,
8✔
1611
                None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
2✔
1612
            };
1613
            self.graph.remove_edge(edge_index);
8✔
1614
        }
1615
        Ok(())
4✔
1616
    }
6✔
1617

1618
    /// Add a new node to the graph.
1619
    ///
1620
    /// :param S obj: The Python object to attach to the node
1621
    ///
1622
    /// :returns: The index of the newly created node
1623
    /// :rtype: int
1624
    #[pyo3(text_signature = "(self, obj, /)")]
1625
    pub fn add_node(&mut self, obj: Py<PyAny>) -> PyResult<usize> {
403,188✔
1626
        let index = self.graph.add_node(obj);
403,188✔
1627
        Ok(index.index())
403,188✔
1628
    }
403,188✔
1629

1630
    /// Find node within this graph given a specific weight
1631
    ///
1632
    /// This algorithm has a worst case of O(n) since it searches the node
1633
    /// indices in order. If there is more than one node in the graph with the
1634
    /// same weight only the first match (by node index) will be returned.
1635
    ///
1636
    /// :param T obj: The weight to look for in the graph.
1637
    ///
1638
    /// :returns: the index of the first node in the graph that is equal to the
1639
    ///     weight. If no match is found ``None`` will be returned.
1640
    /// :rtype: int
1641
    #[pyo3(text_signature = "(self, obj, /)")]
1642
    pub fn find_node_by_weight(&self, py: Python, obj: Py<PyAny>) -> PyResult<Option<usize>> {
6✔
1643
        find_node_by_weight(py, &self.graph, &obj).map(|node| node.map(|x| x.index()))
6✔
1644
    }
6✔
1645

1646
    /// Merge two nodes in the graph.
1647
    ///
1648
    /// If the nodes have equal weight objects then all the edges
1649
    /// leading into and out of `u` will be redirected
1650
    /// to `v` and `u` will be removed from the graph. If the nodes don't have equal weight
1651
    /// objects then no changes will be made and no error will be raised
1652
    ///
1653
    /// :param int u: The index of the source node that is going to be merged
1654
    /// :param int v: The index of the target node that is going to persist
1655
    #[pyo3(text_signature = "(self, u, v, /)")]
1656
    pub fn merge_nodes(&mut self, py: Python, u: usize, v: usize) -> PyResult<()> {
8✔
1657
        let source_node = NodeIndex::new(u);
8✔
1658
        let target_node = NodeIndex::new(v);
8✔
1659

1660
        let source_weight = match self.graph.node_weight(source_node) {
8✔
1661
            Some(weight) => weight,
6✔
1662
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
1663
        };
1664

1665
        let target_weight = match self.graph.node_weight(target_node) {
6✔
1666
            Some(weight) => weight,
4✔
1667
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
1668
        };
1669

1670
        let have_same_weights =
4✔
1671
            source_weight.bind(py).compare(target_weight.bind(py))? == Ordering::Equal;
4✔
1672

1673
        if have_same_weights {
4✔
1674
            const DIRECTIONS: [petgraph::Direction; 2] =
1675
                [petgraph::Direction::Outgoing, petgraph::Direction::Incoming];
1676

1677
            let mut edges_to_add: Vec<(usize, usize, Py<PyAny>)> = Vec::new();
2✔
1678
            for dir in &DIRECTIONS {
4✔
1679
                for edge in self.graph.edges_directed(NodeIndex::new(u), *dir) {
4✔
1680
                    let s = edge.source();
4✔
1681
                    let d = edge.target();
4✔
1682

1683
                    if s.index() == u {
4✔
1684
                        edges_to_add.push((v, d.index(), edge.weight().clone_ref(py)));
2✔
1685
                    } else {
2✔
1686
                        edges_to_add.push((s.index(), v, edge.weight().clone_ref(py)));
2✔
1687
                    }
2✔
1688
                }
1689
            }
1690
            self.remove_node(u)?;
2✔
1691
            for edge in edges_to_add {
4✔
1692
                self.add_edge(edge.0, edge.1, edge.2)?;
4✔
1693
            }
1694
        }
2✔
1695

1696
        Ok(())
4✔
1697
    }
8✔
1698

1699
    /// Add a new child node to the graph.
1700
    ///
1701
    /// This will create a new node on the graph and add an edge leading
1702
    /// from an existing parent to that new node.
1703
    ///
1704
    /// :param int parent: The index of the parent node
1705
    /// :param S obj: The Python object to attach to the new node
1706
    /// :param edge: The python object to attach to the edge
1707
    ///
1708
    /// :returns: The index of the newly created child node
1709
    /// :rtype: int
1710
    #[pyo3(text_signature = "(self, parent, obj, edge, /)")]
1711
    pub fn add_child(&mut self, parent: usize, obj: Py<PyAny>, edge: Py<PyAny>) -> PyResult<usize> {
401,394✔
1712
        let index = NodeIndex::new(parent);
401,394✔
1713
        let child_node = self.graph.add_node(obj);
401,394✔
1714
        self.graph.add_edge(index, child_node, edge);
401,394✔
1715
        Ok(child_node.index())
401,394✔
1716
    }
401,394✔
1717

1718
    /// Add a new parent node to the graph.
1719
    ///
1720
    /// This will create a new node on the graph and add an edge leading
1721
    /// from that new node to an existing child.
1722
    ///
1723
    /// :param int child: The index of the child node
1724
    /// :param obj: The Python object to attach to the new node
1725
    /// :param edge: The python object to attach to the edge
1726
    ///
1727
    /// :returns index: The index of the newly created parent node
1728
    /// :rtype: int
1729
    #[pyo3(text_signature = "(self, child, obj, edge, /)")]
1730
    pub fn add_parent(&mut self, child: usize, obj: Py<PyAny>, edge: Py<PyAny>) -> PyResult<usize> {
70✔
1731
        let index = NodeIndex::new(child);
70✔
1732
        let parent_node = self.graph.add_node(obj);
70✔
1733
        self.graph.add_edge(parent_node, index, edge);
70✔
1734
        Ok(parent_node.index())
70✔
1735
    }
70✔
1736

1737
    /// Get the index and data for the neighbors of a node.
1738
    ///
1739
    /// This will return a dictionary where the keys are the node indices of
1740
    /// the adjacent nodes (inbound or outbound) and the value is the edge data
1741
    /// objects between that adjacent node and the provided node. Note in
1742
    /// the case of a multigraph only one edge will be used, not all of the
1743
    /// edges between two node.
1744
    ///
1745
    ///     >>> G = rx.PyDiGraph()
1746
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
1747
    ///     >>> NodeIndices[0, 1, 2, 3, 4]
1748
    ///     >>> G.extend_from_weighted_edge_list([(0, 2, "A->C"), (1, 2, "B->C"), (2, 3, "C->D"), (3, 4, "D->E")])
1749
    ///     >>> G.adj(2)  # neighbors of the node "C"
1750
    ///     {1: 'B->C', 0: 'A->C', 3: 'C->D'}
1751
    ///
1752
    /// For direction-aware neighbors, see :func:`~adj_direction`.
1753
    ///
1754
    /// :param int node: The index of the node to get the neighbors of
1755
    ///
1756
    /// :returns: A dictionary where the keys are the node indices and the values
1757
    ///     are the edge data objects for all nodes that share an edge with the
1758
    ///     specified node.
1759
    /// :rtype: dict[int, T]
1760
    #[pyo3(text_signature = "(self, node, /)")]
1761
    pub fn adj(&mut self, node: usize) -> DictMap<usize, &Py<PyAny>> {
6✔
1762
        let index = NodeIndex::new(node);
6✔
1763
        self.graph
6✔
1764
            .edges_directed(index, petgraph::Direction::Incoming)
6✔
1765
            .map(|edge| (edge.source().index(), edge.weight()))
6✔
1766
            .chain(
6✔
1767
                self.graph
6✔
1768
                    .edges_directed(index, petgraph::Direction::Outgoing)
6✔
1769
                    .map(|edge| (edge.target().index(), edge.weight())),
6✔
1770
            )
1771
            .collect()
6✔
1772
    }
6✔
1773

1774
    /// Get the index and data for either the parents or children of a node.
1775
    ///
1776
    /// This will return a dictionary where the keys are the node indices of
1777
    /// the adjacent nodes (inbound or outbound as specified) and the value
1778
    /// is the edge data objects for the edges between that adjacent node
1779
    /// and the provided node. Note in the case of a multigraph only one edge
1780
    /// one edge will be used, not all of the edges between two node.
1781
    ///
1782
    ///     >>> G = rx.PyDiGraph()
1783
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
1784
    ///     >>> NodeIndices[0, 1, 2, 3, 4]
1785
    ///     >>> G.extend_from_weighted_edge_list([(0, 2, "A->C"), (1, 2, "B->C"), (2, 3, "C->D"), (3, 4, "D->E")])
1786
    ///     >>> G.adj_direction(2, True)
1787
    ///     {1: 'B->C', 0: 'A->C'}
1788
    ///     >>> G.adj_direction(2, False)
1789
    ///     {3: 'C->D'}
1790
    ///
1791
    /// For direction-naive neighbors, see :func:`~adj`.
1792
    ///
1793
    /// :param int node: The index of the node to get the neighbors of
1794
    /// :param bool direction: The direction to use for finding nodes,
1795
    ///     ``True`` means inbound edges and ``False`` means outbound edges.
1796
    ///
1797
    /// :returns: A dictionary where the keys are node indices and
1798
    ///     the value is the edge data object for all nodes that share an
1799
    ///     edge with the specified node.
1800
    /// :rtype: dict[int, T]
1801
    #[pyo3(text_signature = "(self, node, direction, /)")]
1802
    pub fn adj_direction(&mut self, node: usize, direction: bool) -> DictMap<usize, &Py<PyAny>> {
8✔
1803
        let index = NodeIndex::new(node);
8✔
1804
        if direction {
8✔
1805
            self.graph
4✔
1806
                .edges_directed(index, petgraph::Direction::Incoming)
4✔
1807
                .map(|edge| (edge.source().index(), edge.weight()))
4✔
1808
                .collect()
4✔
1809
        } else {
1810
            self.graph
4✔
1811
                .edges_directed(index, petgraph::Direction::Outgoing)
4✔
1812
                .map(|edge| (edge.target().index(), edge.weight()))
6✔
1813
                .collect()
4✔
1814
        }
1815
    }
8✔
1816

1817
    /// Return a list of node indices of neighbors (i.e. successors) in a directed graph
1818
    ///
1819
    /// A successor is defined as a node that has a directed edge pointing
1820
    /// from the specified node. In a multigraph, where two nodes can be connected
1821
    /// by multiple edges, each successor node index is returned only once.
1822
    ///
1823
    /// This function is equivalent to :meth:`successor_indices`.
1824
    ///
1825
    ///     >>> G = rx.PyDiGraph()
1826
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
1827
    ///     NodeIndices[0, 1, 2, 3, 4]
1828
    ///     >>> G.extend_from_edge_list([(0, 1), (1, 2), (1, 3), (1, 4)])
1829
    ///     >>> G.neighbors(1)  # neighbors of the 'B' node
1830
    ///     NodeIndices[4, 3, 2]
1831
    ///
1832
    /// To get the data of these nodes, see :func:`~successors`.
1833
    ///
1834
    /// To filter the successors by the attributes of the connecting edge,
1835
    /// see :func:`~find_successors_by_edge`.
1836
    ///
1837
    /// See also :func:`~predecessor_indices`.
1838
    ///
1839
    /// For undirected graphs, see :func:`~PyGraph.neighbors`.
1840
    ///
1841
    /// :param int node: The index of the node to get the neighbors of
1842
    ///
1843
    /// :returns: A list of the neighboring node indices
1844
    /// :rtype: NodeIndices
1845
    #[pyo3(text_signature = "(self, node, /)")]
1846
    pub fn neighbors(&self, node: usize) -> NodeIndices {
8✔
1847
        NodeIndices {
1848
            nodes: self
8✔
1849
                .graph
8✔
1850
                .neighbors(NodeIndex::new(node))
8✔
1851
                .map(|node| node.index())
10✔
1852
                .collect::<HashSet<usize>>()
8✔
1853
                .drain()
8✔
1854
                .collect(),
8✔
1855
        }
1856
    }
8✔
1857

1858
    /// Get the direction-agnostic neighbors (i.e. successors and predecessors) of a node.
1859
    ///
1860
    /// This is functionally equivalent to converting the directed graph to an undirected
1861
    /// graph, and calling ``neighbors`` thereon.
1862
    ///
1863
    ///     >>> G = rx.PyDiGraph()
1864
    ///     >>> G.extend_from_edge_list([(0, 1), (1, 2), (2, 3), (3, 4)])
1865
    ///     >>> node = 2
1866
    ///     >>> G.neighbors_undirected(node)
1867
    ///     NodeIndices[3, 1]
1868
    ///     >>> G.to_undirected().neighbors(node)
1869
    ///     NodeIndices[1, 3]
1870
    ///
1871
    /// For direction-aware neighbors, see :func:`~predecessors` and :func:`~successors`.
1872
    ///
1873
    /// :param int node: The index of the node to get the neighbors of
1874
    ///
1875
    /// :returns: A list of the neighboring node indices
1876
    /// :rtype: NodeIndices
1877
    #[pyo3(text_signature = "(self, node, /)")]
1878
    pub fn neighbors_undirected(&self, node: usize) -> NodeIndices {
22✔
1879
        NodeIndices {
1880
            nodes: self
22✔
1881
                .graph
22✔
1882
                .neighbors_undirected(NodeIndex::new(node))
22✔
1883
                .map(|node| node.index())
42✔
1884
                .collect::<HashSet<usize>>()
22✔
1885
                .drain()
22✔
1886
                .collect(),
22✔
1887
        }
1888
    }
22✔
1889

1890
    /// Return a list of successor node indices in a directed graph
1891
    ///
1892
    /// A successor is defined as a node that has a directed edge pointing
1893
    /// from the specified node. In a multigraph, where two nodes can be connected
1894
    /// by multiple edges, each successor node index is returned only once.
1895
    ///
1896
    ///     >>> G = rx.PyDiGraph()
1897
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
1898
    ///     NodeIndices[0, 1, 2, 3, 4]
1899
    ///     >>> G.extend_from_edge_list([(0, 1), (1, 2), (1, 3), (1, 4)])
1900
    ///     >>> G.successor_indices(1)  # successors of the 'B' node
1901
    ///     NodeIndices[4, 3, 2]
1902
    ///
1903
    /// To get the data of these nodes, see :func:`~successors`.`
1904
    ///
1905
    /// To filter the successors by the attributes of the connecting edge,
1906
    /// see :func:`~find_successors_by_edge`.
1907
    ///
1908
    /// See also :func:`~predecessor_indices` and :func:`~neighbors`.
1909
    ///
1910
    /// For undirected graphs, see :func:`~PyGraph.neighbors`.
1911
    ///
1912
    /// :param int node: The index of the node to get the successors for
1913
    ///
1914
    /// :returns: A list of the node indices of all node's successors
1915
    /// :rtype: NodeIndices
1916
    #[pyo3(text_signature = "(self, node, /)")]
1917
    pub fn successor_indices(&self, node: usize) -> NodeIndices {
4✔
1918
        NodeIndices {
1919
            nodes: self
4✔
1920
                .graph
4✔
1921
                .neighbors_directed(NodeIndex::new(node), petgraph::Direction::Outgoing)
4✔
1922
                .map(|node| node.index())
6✔
1923
                .collect(),
4✔
1924
        }
1925
    }
4✔
1926

1927
    /// Return a list of predecessor node indices in a directed graph
1928
    ///
1929
    /// A predecessor is defined as a node that has a directed edge pointing
1930
    /// to the specified node. In a multigraph, where two nodes can be connected
1931
    /// by multiple edges, each predecessor node index is returned only once.
1932
    ///
1933
    ///     >>> G = rx.PyDiGraph()
1934
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
1935
    ///     NodeIndices[0, 1, 2, 3, 4]
1936
    ///     >>> G.extend_from_edge_list([(0, 3), (1, 3), (2, 3), (3, 4)])
1937
    ///     >>> G.predecessor_indices(3)  # predecessors of the 'D' node
1938
    ///     NodeIndices[2, 1, 0]
1939
    ///
1940
    /// To get the data of these nodes, see [predecessors]
1941
    ///
1942
    /// To filter the predecessors by the attributes of the connecting edge,
1943
    /// see :func:`~find_predecessors_by_edge`.
1944
    ///
1945
    /// See also :func:`~successor_indices`.
1946
    ///
1947
    /// For undirected graphs, see :func:`~PyGraph.neighbors`.
1948
    ///
1949
    /// :param int node: The index of the node to get the predecessors for
1950
    ///
1951
    /// :returns: A list of the node indices of all node's predecessors
1952
    /// :rtype: NodeIndices
1953
    #[pyo3(text_signature = "(self, node, /)")]
1954
    pub fn predecessor_indices(&self, node: usize) -> NodeIndices {
96✔
1955
        NodeIndices {
1956
            nodes: self
96✔
1957
                .graph
96✔
1958
                .neighbors_directed(NodeIndex::new(node), petgraph::Direction::Incoming)
96✔
1959
                .map(|node| node.index())
108✔
1960
                .collect(),
96✔
1961
        }
1962
    }
96✔
1963

1964
    /// Return the list of edge indices incident to a provided node
1965
    ///
1966
    /// You can later retrieve the data payload of this edge with
1967
    /// :meth:`~rustworkx.PyDiGraph.get_edge_data_by_index` or its
1968
    /// endpoints with :meth:`~rustworkx.PyDiGraph.get_edge_endpoints_by_index`.
1969
    ///
1970
    /// By default this method will only return the outgoing edges of
1971
    /// the provided ``node``. If you would like to access both the
1972
    /// incoming and outgoing edges you can set the ``all_edges``
1973
    /// kwarg to ``True``.
1974
    ///
1975
    /// :param int node: The node index to get incident edges from. If
1976
    ///     this node index is not present in the graph this method will
1977
    ///     return an empty list and not raise an error.
1978
    /// :param bool all_edges: If set to ``True`` both incoming and outgoing
1979
    ///     edges to ``node`` will be returned.
1980
    ///
1981
    /// :returns: A list of the edge indices incident to a node in the graph
1982
    /// :rtype: EdgeIndices
1983
    #[pyo3(text_signature = "(self, node, /, all_edges=False)")]
1984
    #[pyo3(signature=(node, all_edges=false))]
1985
    pub fn incident_edges(&self, node: usize, all_edges: bool) -> EdgeIndices {
6✔
1986
        let node_index = NodeIndex::new(node);
6✔
1987
        if all_edges {
6✔
1988
            EdgeIndices {
1989
                edges: self
2✔
1990
                    .graph
2✔
1991
                    .edges_directed(node_index, petgraph::Direction::Outgoing)
2✔
1992
                    .chain(
2✔
1993
                        self.graph
2✔
1994
                            .edges_directed(node_index, petgraph::Direction::Incoming),
2✔
1995
                    )
1996
                    .map(|e| e.id().index())
4✔
1997
                    .collect(),
2✔
1998
            }
1999
        } else {
2000
            EdgeIndices {
2001
                edges: self
4✔
2002
                    .graph
4✔
2003
                    .edges(node_index)
4✔
2004
                    .map(|e| e.id().index())
4✔
2005
                    .collect(),
4✔
2006
            }
2007
        }
2008
    }
6✔
2009

2010
    /// Return the list of incoming edge indices to a provided node
2011
    ///
2012
    /// This method will return the incoming edges of the provided
2013
    /// ``node``.
2014
    ///
2015
    /// :param int node: The node index to get incoming edges from. If
2016
    ///     this node index is not present in the graph this method will
2017
    ///     return an empty list and not raise an error.
2018
    ///
2019
    /// :returns: A list of the incoming edge indices to a node in the graph
2020
    /// :rtype: EdgeIndices
2021
    #[pyo3(text_signature = "(self, node, /)")]
2022
    pub fn in_edge_indices(&self, node: usize) -> EdgeIndices {
4✔
2023
        let node_index = NodeIndex::new(node);
4✔
2024
        let dir = petgraph::Direction::Incoming;
4✔
2025
        EdgeIndices {
2026
            edges: self
4✔
2027
                .graph
4✔
2028
                .edges_directed(node_index, dir)
4✔
2029
                .map(|e| e.id().index())
4✔
2030
                .collect(),
4✔
2031
        }
2032
    }
4✔
2033

2034
    /// Return the list of outgoing edge indices from a provided node
2035
    ///
2036
    /// This method will return the outgoing edges of the provided
2037
    /// ``node``.
2038
    ///
2039
    /// :param int node: The node index to get outgoing edges from. If
2040
    ///     this node index is not present in the graph this method will
2041
    ///     return an empty list and not raise an error.
2042
    ///
2043
    /// :returns: A list of the outgoing edge indices from a node in the graph
2044
    /// :rtype: EdgeIndices
2045
    #[pyo3(text_signature = "(self, node, /)")]
2046
    pub fn out_edge_indices(&self, node: usize) -> EdgeIndices {
4✔
2047
        let node_index = NodeIndex::new(node);
4✔
2048
        let dir = petgraph::Direction::Outgoing;
4✔
2049
        EdgeIndices {
2050
            edges: self
4✔
2051
                .graph
4✔
2052
                .edges_directed(node_index, dir)
4✔
2053
                .map(|e| e.id().index())
4✔
2054
                .collect(),
4✔
2055
        }
2056
    }
4✔
2057

2058
    /// Return the index map of edges incident to a provided node
2059
    ///
2060
    /// By default this method will only return the outgoing edges of
2061
    /// the provided ``node``. If you would like to access both the
2062
    /// incoming and outgoing edges you can set the ``all_edges``
2063
    /// kwarg to ``True``.
2064
    ///
2065
    /// :param int node: The node index to get incident edges from. If
2066
    ///     this node index is not present in the graph this method will
2067
    ///     return an empty list and not error.
2068
    /// :param bool all_edges: If set to ``True`` both incoming and outgoing
2069
    ///     edges to ``node`` will be returned. If set to ``False``, only outgoing ones.
2070
    ///
2071
    /// :returns: A mapping of incident edge indices to the tuple
2072
    ///     ``(source, target, data)``
2073
    /// :rtype: EdgeIndexMap
2074
    #[pyo3(text_signature = "(self, node, /, all_edges=False)")]
2075
    #[pyo3(signature=(node, all_edges=false))]
2076
    pub fn incident_edge_index_map(
6✔
2077
        &self,
6✔
2078
        py: Python,
6✔
2079
        node: usize,
6✔
2080
        all_edges: bool,
6✔
2081
    ) -> EdgeIndexMap {
6✔
2082
        let node_index = NodeIndex::new(node);
6✔
2083
        if all_edges {
6✔
2084
            EdgeIndexMap {
2085
                edge_map: self
2✔
2086
                    .graph
2✔
2087
                    .edges_directed(node_index, petgraph::Direction::Outgoing)
2✔
2088
                    .chain(
2✔
2089
                        self.graph
2✔
2090
                            .edges_directed(node_index, petgraph::Direction::Incoming),
2✔
2091
                    )
2092
                    .map(|edge| {
4✔
2093
                        (
4✔
2094
                            edge.id().index(),
4✔
2095
                            (
4✔
2096
                                edge.source().index(),
4✔
2097
                                edge.target().index(),
4✔
2098
                                edge.weight().clone_ref(py),
4✔
2099
                            ),
4✔
2100
                        )
4✔
2101
                    })
4✔
2102
                    .collect(),
2✔
2103
            }
2104
        } else {
2105
            EdgeIndexMap {
2106
                edge_map: self
4✔
2107
                    .graph
4✔
2108
                    .edges(node_index)
4✔
2109
                    .map(|edge| {
4✔
2110
                        (
2✔
2111
                            edge.id().index(),
2✔
2112
                            (
2✔
2113
                                edge.source().index(),
2✔
2114
                                edge.target().index(),
2✔
2115
                                edge.weight().clone_ref(py),
2✔
2116
                            ),
2✔
2117
                        )
2✔
2118
                    })
2✔
2119
                    .collect(),
4✔
2120
            }
2121
        }
2122
    }
6✔
2123

2124
    /// Get the index and edge data for all parents of a node.
2125
    ///
2126
    /// This will return a list of tuples with the parent index, the node (itself) index,
2127
    /// and the edge data. This can be used to recreate add_edge() calls.
2128
    ///
2129
    /// :param int node: The index of the node to get the edges for
2130
    ///
2131
    /// :returns: A list of tuples in the form:
2132
    ///     ``(parent_index, node_index, edge_data)```
2133
    /// :rtype: WeightedEdgeList
2134
    #[pyo3(text_signature = "(self, node, /)")]
2135
    pub fn in_edges(&self, py: Python, node: usize) -> WeightedEdgeList {
286✔
2136
        let index = NodeIndex::new(node);
286✔
2137
        let dir = petgraph::Direction::Incoming;
286✔
2138
        let raw_edges = self.graph.edges_directed(index, dir);
286✔
2139
        let out_list: Vec<(usize, usize, Py<PyAny>)> = raw_edges
286✔
2140
            .map(|x| (x.source().index(), node, x.weight().clone_ref(py)))
548✔
2141
            .collect();
286✔
2142
        WeightedEdgeList { edges: out_list }
286✔
2143
    }
286✔
2144

2145
    /// Get the index and edge data for all children of a node.
2146
    ///
2147
    /// This will return a list of tuples with the child index, the node (itself) index,
2148
    /// and the edge data. This can be used to recreate add_edge() calls.
2149
    ///
2150
    /// :param int node: The index of the node to get the edges for
2151
    ///
2152
    /// :returns out_edges: A list of tuples of the form:
2153
    ///     ```(node_index, child_index, edge_data)```
2154
    /// :rtype: WeightedEdgeList
2155
    #[pyo3(text_signature = "(self, node, /)")]
2156
    pub fn out_edges(&self, py: Python, node: usize) -> WeightedEdgeList {
528✔
2157
        let index = NodeIndex::new(node);
528✔
2158
        let dir = petgraph::Direction::Outgoing;
528✔
2159
        let raw_edges = self.graph.edges_directed(index, dir);
528✔
2160
        let out_list: Vec<(usize, usize, Py<PyAny>)> = raw_edges
528✔
2161
            .map(|x| (node, x.target().index(), x.weight().clone_ref(py)))
2,228✔
2162
            .collect();
528✔
2163
        WeightedEdgeList { edges: out_list }
528✔
2164
    }
528✔
2165

2166
    /// Add new nodes to the graph.
2167
    ///
2168
    /// :param iterable[S] obj_list: An iterable of Python objects to attach to the new nodes
2169
    ///
2170
    /// :returns: A list of indices of the newly created nodes
2171
    /// :rtype: NodeIndices
2172
    #[pyo3(text_signature = "(self, obj_list, /)")]
2173
    pub fn add_nodes_from(&mut self, obj_list: Bound<'_, PyAny>) -> PyResult<NodeIndices> {
458✔
2174
        let mut out_list = Vec::new();
458✔
2175
        for py_obj in obj_list.try_iter()? {
2,015,184✔
2176
            let obj = py_obj?.extract::<Py<PyAny>>()?;
2,015,184✔
2177
            out_list.push(self.graph.add_node(obj).index());
2,015,184✔
2178
        }
2179
        Ok(NodeIndices { nodes: out_list })
458✔
2180
    }
458✔
2181

2182
    /// Remove nodes from the graph.
2183
    ///
2184
    /// If a node index in the list is not present in the graph, it will be
2185
    /// ignored.
2186
    ///
2187
    /// :param iterable[int] index_list: An iterable of node indices to remove from the
2188
    ///     graph.
2189
    #[pyo3(text_signature = "(self, index_list, /)")]
2190
    pub fn remove_nodes_from(&mut self, index_list: Bound<'_, PyAny>) -> PyResult<()> {
30✔
2191
        for py_obj in index_list.try_iter()? {
58✔
2192
            let node = py_obj?.extract::<usize>()?;
58✔
2193
            self.remove_node(node)?;
58✔
2194
        }
2195
        Ok(())
30✔
2196
    }
30✔
2197

2198
    /// Get the degree of a node for inbound edges.
2199
    ///
2200
    /// :param int node: The index of the node to find the inbound degree of
2201
    ///
2202
    /// :returns: The inbound degree for the specified node (i.e. number of inbound edges)
2203
    /// :rtype: int
2204
    #[pyo3(text_signature = "(self, node, /)")]
2205
    pub fn in_degree(&self, node: usize) -> usize {
96✔
2206
        let index = NodeIndex::new(node);
96✔
2207
        let dir = petgraph::Direction::Incoming;
96✔
2208
        let neighbors = self.graph.edges_directed(index, dir);
96✔
2209
        neighbors.count()
96✔
2210
    }
96✔
2211

2212
    /// Get the degree of a node for outbound edges.
2213
    ///
2214
    /// :param int node: The index of the node to find the outbound degree of
2215
    ///
2216
    /// :returns: The outbound degree for the specified node (i.e. number of outbound endges)
2217
    /// :rtype: int
2218
    #[pyo3(text_signature = "(self, node, /)")]
2219
    pub fn out_degree(&self, node: usize) -> usize {
64✔
2220
        let index = NodeIndex::new(node);
64✔
2221
        let dir = petgraph::Direction::Outgoing;
64✔
2222
        let neighbors = self.graph.edges_directed(index, dir);
64✔
2223
        neighbors.count()
64✔
2224
    }
64✔
2225

2226
    /// Find any adjacent (successor) node connected with an edge that matches the condition
2227
    ///
2228
    /// This method is used to find a target node that is a adjacent to a given
2229
    /// node as a successor given an edge condition. This method returns one
2230
    /// arbitrary node where the edge matches the condition.
2231
    ///
2232
    ///     >>> G = rx.PyDiGraph()
2233
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
2234
    ///     NodeIndices[0, 1, 2, 3, 4]
2235
    ///     >>> G.extend_from_weighted_edge_list([(0, 1, 10), (1, 2, 10), (1, 3, 20), (1, 4, 30)])
2236
    ///     >>> G.find_adjacent_node_by_edge(1, lambda x: x < 25)
2237
    ///     'D'
2238
    ///
2239
    /// :param int node: The index of the node to use as the source of the search
2240
    /// :param Callable predicate: A Python callable that will take a single
2241
    ///     parameter, the edge object, and will return a boolean if the
2242
    ///     edge matches or not
2243
    ///
2244
    /// :returns: The node object that is connected by an edge to the given node
2245
    ///     as a successor which matches the provided condition
2246
    /// :rtype: S
2247
    /// :raises: NoSuitableNeighbors: If there are no suitable nodes
2248
    #[pyo3(text_signature = "(self, node, predicate, /)")]
2249
    pub fn find_adjacent_node_by_edge(
4✔
2250
        &self,
4✔
2251
        py: Python,
4✔
2252
        node: usize,
4✔
2253
        predicate: Py<PyAny>,
4✔
2254
    ) -> PyResult<&Py<PyAny>> {
4✔
2255
        let predicate_callable = |a: &Py<PyAny>| -> PyResult<Py<PyAny>> {
6✔
2256
            let res = predicate.call1(py, (a,))?;
6✔
2257
            res.into_py_any(py)
6✔
2258
        };
6✔
2259
        let index = NodeIndex::new(node);
4✔
2260
        let dir = petgraph::Direction::Outgoing;
4✔
2261
        let edges = self.graph.edges_directed(index, dir);
4✔
2262
        for edge in edges {
6✔
2263
            let edge_predicate_raw = predicate_callable(edge.weight())?;
6✔
2264
            let edge_predicate: bool = edge_predicate_raw.extract(py)?;
6✔
2265
            if edge_predicate {
6✔
2266
                return Ok(self.graph.node_weight(edge.target()).unwrap());
2✔
2267
            }
4✔
2268
        }
2269
        Err(NoSuitableNeighbors::new_err("No suitable neighbor"))
2✔
2270
    }
4✔
2271

2272
    /// Find any predecessor node connected with an edge that matches the condition
2273
    ///
2274
    /// A predecessor is defined as a node that has a directed edge pointing
2275
    /// to the specified node. This method returns one arbitrary node where the edge
2276
    /// matches the condition.
2277
    ///
2278
    ///     >>> G = rx.PyDiGraph()
2279
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
2280
    ///     NodeIndices[0, 1, 2, 3, 4]
2281
    ///     >>> G.extend_from_weighted_edge_list([(0, 3, 10), (1, 3, 20), (2, 3, 30), (3, 4, 10)])
2282
    ///     >>> G.find_predecessor_node_by_edge(3, lambda x: x < 25)
2283
    ///     'B'
2284
    ///
2285
    /// To get all such nodes, see :func:`~find_predecessors_by_edge`.
2286
    ///
2287
    /// :param int node: The node to use as the source of the search
2288
    /// :param Callable predicate: A Python callable that will take a single
2289
    ///     parameter, the edge object, and will return a boolean if the
2290
    ///     edge matches or not
2291
    ///
2292
    /// :returns: The node object that is connected as a predecessor
2293
    ///     by an edge to the given node which matches the provided condition
2294
    /// :rtype: S
2295
    /// :raises: NoSuitableNeighbors: If there are no suitable nodes
2296
    #[pyo3(text_signature = "(self, node, predicate, /)")]
2297
    pub fn find_predecessor_node_by_edge(
4✔
2298
        &self,
4✔
2299
        py: Python,
4✔
2300
        node: usize,
4✔
2301
        predicate: Py<PyAny>,
4✔
2302
    ) -> PyResult<&Py<PyAny>> {
4✔
2303
        let predicate_callable = |a: &Py<PyAny>| -> PyResult<Py<PyAny>> {
4✔
2304
            let res = predicate.call1(py, (a,))?;
4✔
2305
            res.into_py_any(py)
4✔
2306
        };
4✔
2307
        let index = NodeIndex::new(node);
4✔
2308
        let dir = petgraph::Direction::Incoming;
4✔
2309
        let edges = self.graph.edges_directed(index, dir);
4✔
2310
        for edge in edges {
4✔
2311
            let edge_predicate_raw = predicate_callable(edge.weight())?;
4✔
2312
            let edge_predicate: bool = edge_predicate_raw.extract(py)?;
4✔
2313
            if edge_predicate {
4✔
2314
                return Ok(self.graph.node_weight(edge.source()).unwrap());
2✔
2315
            }
2✔
2316
        }
2317
        Err(NoSuitableNeighbors::new_err("No suitable neighbor"))
2✔
2318
    }
4✔
2319

2320
    /// Find any successor node connected with an edge that matches the condition
2321
    ///
2322
    /// A successor is defined as a node that has a directed edge pointing
2323
    /// from the specified node. This method returns one arbitrary node where the edge
2324
    /// matches the condition.
2325
    ///
2326
    ///     >>> G = rx.PyDiGraph()
2327
    ///     >>> G.add_nodes_from(["A", "B", "C", "D", "E"])
2328
    ///     NodeIndices[0, 1, 2, 3, 4]
2329
    ///     >>> G.extend_from_weighted_edge_list([(0, 1, 10), (1, 2, 10), (1, 3, 20), (1, 4, 30)])
2330
    ///     >>> G.find_successor_node_by_edge(1, lambda x: x < 25)
2331
    ///     'D'
2332
    ///
2333
    /// To get all such nodes, see :func:`~find_successors_by_edge`.
2334
    ///
2335
    /// :param int node: The node to use as the source of the search
2336
    /// :param Callable predicate: A Python callable that will take a single
2337
    ///     parameter, the edge object, and will return a boolean if the
2338
    ///     edge matches or not
2339
    ///
2340
    /// :returns: The node object that is connected as a successor
2341
    ///     by an edge to the given node which matches the provided condition
2342
    /// :rtype: S
2343
    /// :raises: NoSuitableNeighbors: If there are no suitable nodes
2344
    #[pyo3(text_signature = "(self, node, predicate, /)")]
2345
    pub fn find_successor_node_by_edge(
4✔
2346
        &self,
4✔
2347
        py: Python,
4✔
2348
        node: usize,
4✔
2349
        predicate: Py<PyAny>,
4✔
2350
    ) -> PyResult<&Py<PyAny>> {
4✔
2351
        let predicate_callable = |a: &Py<PyAny>| -> PyResult<Py<PyAny>> {
4✔
2352
            let res = predicate.call1(py, (a,))?;
4✔
2353
            res.into_py_any(py)
4✔
2354
        };
4✔
2355
        let index = NodeIndex::new(node);
4✔
2356
        let dir = petgraph::Direction::Outgoing;
4✔
2357
        let edges = self.graph.edges_directed(index, dir);
4✔
2358
        for edge in edges {
4✔
2359
            let edge_predicate_raw = predicate_callable(edge.weight())?;
4✔
2360
            let edge_predicate: bool = edge_predicate_raw.extract(py)?;
4✔
2361
            if edge_predicate {
4✔
2362
                return Ok(self.graph.node_weight(edge.target()).unwrap());
2✔
2363
            }
2✔
2364
        }
2365
        Err(NoSuitableNeighbors::new_err("No suitable neighbor"))
2✔
2366
    }
4✔
2367

2368
    /// Generate a dot file from the graph
2369
    ///
2370
    /// :param node_attr: A callable that will take in a node data object
2371
    ///     and return a dictionary of attributes to be associated with the
2372
    ///     node in the dot file. The key and value of this dictionary **must**
2373
    ///     be strings. If they're not strings rustworkx will raise TypeError
2374
    ///     (unfortunately without an error message because of current
2375
    ///     limitations in the PyO3 type checking)
2376
    /// :param edge_attr: A callable that will take in an edge data object
2377
    ///     and return a dictionary of attributes to be associated with the
2378
    ///     node in the dot file. The key and value of this dictionary **must**
2379
    ///     be a string. If they're not strings rustworkx will raise TypeError
2380
    ///     (unfortunately without an error message because of current
2381
    ///     limitations in the PyO3 type checking)
2382
    /// :param dict graph_attr: An optional dictionary that specifies any graph
2383
    ///     attributes for the output dot file. The key and value of this
2384
    ///     dictionary **must** be a string. If they're not strings rustworkx
2385
    ///     will raise TypeError (unfortunately without an error message
2386
    ///     because of current limitations in the PyO3 type checking)
2387
    /// :param str filename: An optional path to write the dot file to
2388
    ///     if specified there is no return from the function
2389
    ///
2390
    /// :returns: A string with the dot file contents if filename is not
2391
    ///     specified.
2392
    /// :rtype: str
2393
    ///
2394
    /// Using this method enables you to leverage graphviz to visualize a
2395
    /// :class:`rustworkx.PyDiGraph` object. For example:
2396
    ///
2397
    /// .. jupyter-execute::
2398
    ///
2399
    ///   import os
2400
    ///   import tempfile
2401
    ///
2402
    ///   import pydot
2403
    ///   from PIL import Image
2404
    ///
2405
    ///   import rustworkx as rx
2406
    ///
2407
    ///   graph = rx.directed_gnp_random_graph(15, .25)
2408
    ///   dot_str = graph.to_dot(
2409
    ///       lambda node: dict(
2410
    ///           color='black', fillcolor='lightblue', style='filled'))
2411
    ///   dot = pydot.graph_from_dot_data(dot_str)[0]
2412
    ///
2413
    ///   with tempfile.TemporaryDirectory() as tmpdirname:
2414
    ///       tmp_path = os.path.join(tmpdirname, 'dag.png')
2415
    ///       dot.write_png(tmp_path)
2416
    ///       image = Image.open(tmp_path)
2417
    ///       os.remove(tmp_path)
2418
    ///   image
2419
    ///
2420
    #[pyo3(
2421
        text_signature = "(self, /, node_attr=None, edge_attr=None, graph_attr=None, filename=None)",
2422
        signature = (node_attr=None, edge_attr=None, graph_attr=None, filename=None)
2423
    )]
2424
    pub fn to_dot<'py>(
10✔
2425
        &self,
10✔
2426
        py: Python<'py>,
10✔
2427
        node_attr: Option<Py<PyAny>>,
10✔
2428
        edge_attr: Option<Py<PyAny>>,
10✔
2429
        graph_attr: Option<BTreeMap<String, String>>,
10✔
2430
        filename: Option<String>,
10✔
2431
    ) -> PyResult<Option<Bound<'py, PyString>>> {
10✔
2432
        match filename {
10✔
2433
            Some(filename) => {
2✔
2434
                let file = File::create(filename)?;
2✔
2435
                let mut buf_writer = BufWriter::new(file);
2✔
2436
                build_dot(
2✔
2437
                    py,
2✔
2438
                    &self.graph,
2✔
2439
                    &mut buf_writer,
2✔
2440
                    graph_attr,
2✔
2441
                    node_attr,
2✔
2442
                    edge_attr,
2✔
NEW
2443
                )?;
×
2444
                buf_writer.flush()?;
2✔
2445
                Ok(None)
2✔
2446
            }
2447
            None => {
2448
                let mut file = Vec::<u8>::new();
8✔
2449
                build_dot(py, &self.graph, &mut file, graph_attr, node_attr, edge_attr)?;
8✔
2450
                Ok(Some(PyString::new(py, str::from_utf8(&file)?)))
8✔
2451
            }
2452
        }
2453
    }
10✔
2454

2455
    /// Read an edge list file and create a new PyDiGraph object from the
2456
    /// contents
2457
    ///
2458
    /// The expected format for the edge list file is a line separated list
2459
    /// of delimited node ids. If there are more than 3 elements on
2460
    /// a line the 3rd on will be treated as a string weight for the edge
2461
    ///
2462
    /// :param str path: The path of the file to open
2463
    /// :param str comment: Optional character to use as a comment by default
2464
    ///     there are no comment characters
2465
    /// :param str deliminator: Optional character to use as a deliminator by
2466
    ///     default any whitespace will be used
2467
    /// :param bool labels: If set to ``True`` the first two separated fields
2468
    ///     will be treated as string labels uniquely identifying a node
2469
    ///     instead of node indices.
2470
    /// :param bool multigraph: If ``False``, duplicate edges do not create
2471
    ///     parallel edges. Instead, repeated edges overwrite the previous
2472
    ///     weight, with the last one in the input determining the final weight.
2473
    /// :param bool selfloops: If set to ``False``, self-loops are not created.
2474
    ///
2475
    /// For example:
2476
    ///
2477
    /// .. jupyter-execute::
2478
    ///
2479
    ///   import tempfile
2480
    ///
2481
    ///   import rustworkx as rx
2482
    ///   from rustworkx.visualization import mpl_draw
2483
    ///
2484
    ///   with tempfile.NamedTemporaryFile('wt') as fd:
2485
    ///       path = fd.name
2486
    ///       fd.write('0 1\n')
2487
    ///       fd.write('0 2\n')
2488
    ///       fd.write('0 3\n')
2489
    ///       fd.write('1 2\n')
2490
    ///       fd.write('2 3\n')
2491
    ///       fd.flush()
2492
    ///       graph = rx.PyDiGraph.read_edge_list(path)
2493
    ///   mpl_draw(graph)
2494
    ///
2495
    #[staticmethod]
2496
    #[pyo3(signature=(path, comment=None, deliminator=None, labels=false, multigraph=true, allow_self_loops=true))]
2497
    #[pyo3(
2498
        text_signature = "(path, /, comment=None, deliminator=None, labels=False, multigraph=True, allow_self_loops=True)"
2499
    )]
2500
    pub fn read_edge_list(
26✔
2501
        py: Python,
26✔
2502
        path: &str,
26✔
2503
        comment: Option<String>,
26✔
2504
        deliminator: Option<String>,
26✔
2505
        labels: bool,
26✔
2506
        multigraph: bool,
26✔
2507
        allow_self_loops: bool,
26✔
2508
    ) -> PyResult<PyDiGraph> {
26✔
2509
        let file = File::open(path)?;
26✔
2510
        let buf_reader = BufReader::new(file);
24✔
2511
        let mut out_graph = StablePyGraph::<Directed>::new();
24✔
2512
        let mut label_map: HashMap<String, usize> = HashMap::new();
24✔
2513
        for line_raw in buf_reader.lines() {
74✔
2514
            let line = line_raw?;
74✔
2515
            let skip = match &comment {
74✔
2516
                Some(comm) => line.trim().starts_with(comm),
36✔
2517
                None => line.trim().is_empty(),
38✔
2518
            };
2519
            if skip {
74✔
2520
                continue;
12✔
2521
            }
62✔
2522
            let line_no_comments = match &comment {
62✔
2523
                Some(comm) => line
26✔
2524
                    .find(comm)
26✔
2525
                    .map(|idx| &line[..idx])
26✔
2526
                    .unwrap_or(&line)
26✔
2527
                    .trim()
26✔
2528
                    .to_string(),
26✔
2529
                None => line,
36✔
2530
            };
2531
            let pieces: Vec<&str> = match &deliminator {
62✔
2532
                Some(del) => line_no_comments.split(del).collect(),
14✔
2533
                None => line_no_comments.split_whitespace().collect(),
48✔
2534
            };
2535
            let src: usize;
2536
            let target: usize;
2537
            if labels {
62✔
2538
                let src_str = pieces[0];
10✔
2539
                let target_str = pieces[1];
10✔
2540
                src = match label_map.get(src_str) {
10✔
2541
                    Some(index) => *index,
6✔
2542
                    None => {
2543
                        let index = out_graph.add_node(src_str.into_py_any(py)?).index();
4✔
2544
                        label_map.insert(src_str.to_string(), index);
4✔
2545
                        index
4✔
2546
                    }
2547
                };
2548
                target = match label_map.get(target_str) {
10✔
2549
                    Some(index) => *index,
2✔
2550
                    None => {
2551
                        let index = out_graph.add_node(target_str.into_py_any(py)?).index();
8✔
2552
                        label_map.insert(target_str.to_string(), index);
8✔
2553
                        index
8✔
2554
                    }
2555
                };
2556
            } else {
2557
                src = pieces[0].parse::<usize>()?;
52✔
2558
                target = pieces[1].parse::<usize>()?;
52✔
2559
                let max_index = cmp::max(src, target);
52✔
2560
                // Add nodes to graph
2561
                while max_index >= out_graph.node_count() {
110✔
2562
                    out_graph.add_node(py.None());
58✔
2563
                }
58✔
2564
            }
2565
            // Add edges to graph
2566
            let weight = if pieces.len() > 2 {
62✔
2567
                let weight_str = match &deliminator {
40✔
2568
                    Some(del) => pieces[2..].join(del),
12✔
2569
                    None => pieces[2..].join(&' '.to_string()),
28✔
2570
                };
2571
                PyString::new(py, &weight_str).into()
40✔
2572
            } else {
2573
                py.None()
22✔
2574
            };
2575
            if allow_self_loops || src != target {
62✔
2576
                if multigraph {
58✔
2577
                    out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
48✔
2578
                } else {
48✔
2579
                    out_graph.update_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
10✔
2580
                }
10✔
2581
            }
4✔
2582
        }
2583
        Ok(PyDiGraph {
24✔
2584
            graph: out_graph,
24✔
2585
            cycle_state: algo::DfsSpace::default(),
24✔
2586
            check_cycle: false,
24✔
2587
            node_removed: false,
24✔
2588
            multigraph,
24✔
2589
            attrs: py.None(),
24✔
2590
        })
24✔
2591
    }
26✔
2592

2593
    /// Write an edge list file from the PyDiGraph object
2594
    ///
2595
    /// :param str path: The path to write the output file to
2596
    /// :param str deliminator: The optional character to use as a deliminator
2597
    ///     if not specified ``" "`` is used.
2598
    /// :param callable weight_fn: An optional callback function that will be
2599
    ///     passed an edge's data payload/weight object and is expected to
2600
    ///     return a string (a ``TypeError`` will be raised if it doesn't
2601
    ///     return a string). If specified the weight in the output file
2602
    ///     for each edge will be set to the returned string.
2603
    ///
2604
    ///  For example:
2605
    ///
2606
    ///  .. jupyter-execute::
2607
    ///
2608
    ///     import os
2609
    ///     import tempfile
2610
    ///
2611
    ///     import rustworkx as rx
2612
    ///
2613
    ///     graph = rx.generators.directed_path_graph(5)
2614
    ///     path = os.path.join(tempfile.gettempdir(), "edge_list")
2615
    ///     graph.write_edge_list(path, deliminator=',')
2616
    ///     # Print file contents
2617
    ///     with open(path, 'rt') as edge_file:
2618
    ///         print(edge_file.read())
2619
    ///
2620
    #[pyo3(text_signature = "(self, path, /, deliminator=None, weight_fn=None)", signature = (path, deliminator=None, weight_fn=None))]
2621
    pub fn write_edge_list(
10✔
2622
        &self,
10✔
2623
        py: Python,
10✔
2624
        path: &str,
10✔
2625
        deliminator: Option<char>,
10✔
2626
        weight_fn: Option<Py<PyAny>>,
10✔
2627
    ) -> PyResult<()> {
10✔
2628
        let file = File::create(path)?;
10✔
2629
        let mut buf_writer = BufWriter::new(file);
10✔
2630
        let delim = match deliminator {
10✔
2631
            Some(delim) => delim.to_string(),
2✔
2632
            None => " ".to_string(),
8✔
2633
        };
2634

2635
        for edge in self.graph.edge_references() {
20✔
2636
            buf_writer.write_all(
20✔
2637
                format!(
20✔
2638
                    "{}{}{}",
20✔
2639
                    edge.source().index(),
20✔
2640
                    delim,
20✔
2641
                    edge.target().index()
20✔
2642
                )
20✔
2643
                .as_bytes(),
20✔
2644
            )?;
×
2645
            match weight_callable(py, &weight_fn, edge.weight(), None as Option<String>)? {
20✔
2646
                Some(weight) => buf_writer.write_all(format!("{delim}{weight}\n").as_bytes()),
8✔
2647
                None => buf_writer.write_all(b"\n"),
8✔
2648
            }?;
×
2649
        }
2650
        buf_writer.flush()?;
6✔
2651
        Ok(())
6✔
2652
    }
10✔
2653

2654
    /// Create a new :class:`~rustworkx.PyDiGraph` object from an adjacency matrix
2655
    /// with matrix elements of type ``float``
2656
    ///
2657
    /// This method can be used to construct a new :class:`~rustworkx.PyDiGraph`
2658
    /// object from an input adjacency matrix. The node weights will be the
2659
    /// index from the matrix. The edge weights will be a float value of the
2660
    /// value from the matrix.
2661
    ///
2662
    /// This differs from the
2663
    /// :meth:`~rustworkx.PyDiGraph.from_complex_adjacency_matrix` in that the
2664
    /// type of the elements of input matrix must be a ``float`` (specifically
2665
    /// a ``numpy.float64``) and the output graph edge weights will be ``float``
2666
    /// too. While in :meth:`~rustworkx.PyDiGraph.from_complex_adjacency_matrix`
2667
    /// the matrix elements are of type ``complex`` (specifically
2668
    /// ``numpy.complex128``) and the edge weights in the output graph will be
2669
    /// ``complex`` too.
2670
    ///
2671
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
2672
    ///     a new :class:`~rustworkx.PyDiGraph` object from. It must be a 2
2673
    ///     dimensional array and be a ``float``/``np.float64`` data type.
2674
    /// :param float null_value: An optional float that will treated as a null
2675
    ///     value. If any element in the input matrix is this value it will be
2676
    ///     treated as not an edge. By default this is ``0.0``
2677
    ///
2678
    /// :returns: A new graph object generated from the adjacency matrix
2679
    /// :rtype: PyDiGraph
2680
    #[staticmethod]
2681
    #[pyo3(signature=(matrix, null_value=0.0), text_signature = "(matrix, /, null_value=0.0)")]
2682
    pub fn from_adjacency_matrix<'p>(
14✔
2683
        py: Python<'p>,
14✔
2684
        matrix: PyReadonlyArray2<'p, f64>,
14✔
2685
        null_value: f64,
14✔
2686
    ) -> PyResult<PyDiGraph> {
14✔
2687
        _from_adjacency_matrix(py, matrix, null_value)
14✔
2688
    }
14✔
2689

2690
    /// Create a new :class:`~rustworkx.PyDiGraph` object from an adjacency matrix
2691
    /// with matrix elements of type ``complex``
2692
    ///
2693
    /// This method can be used to construct a new :class:`~rustworkx.PyDiGraph`
2694
    /// object from an input adjacency matrix. The node weights will be the
2695
    /// index from the matrix. The edge weights will be a complex value of the
2696
    /// value from the matrix.
2697
    ///
2698
    /// This differs from the
2699
    /// :meth:`~rustworkx.PyDiGraph.from_adjacency_matrix` in that the type of
2700
    /// the elements of the input matrix in this method must be a ``complex``
2701
    /// (specifically a ``numpy.complex128``) and the output graph edge weights
2702
    /// will be ``complex`` too. While in
2703
    /// :meth:`~rustworkx.PyDiGraph.from_adjacency_matrix` the matrix elements
2704
    /// are of type ``float`` (specifically ``numpy.float64``) and the edge
2705
    /// weights in the output graph will be ``float`` too.
2706
    ///
2707
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
2708
    ///     a new :class:`~rustworkx.PyDiGraph` object from. It must be a 2
2709
    ///     dimensional array and be a ``complex``/``np.complex128`` data type.
2710
    /// :param complex null_value: An optional complex that will treated as a
2711
    ///     null value. If any element in the input matrix is this value it
2712
    ///     will be treated as not an edge. By default this is ``0.0+0.0j``
2713
    ///
2714
    /// :returns: A new graph object generated from the adjacency matrix
2715
    /// :rtype: PyDiGraph
2716
    #[staticmethod]
2717
    #[pyo3(signature=(matrix, null_value=Complex64::zero()), text_signature = "(matrix, /, null_value=0.0+0.0j)")]
8✔
2718
    pub fn from_complex_adjacency_matrix<'p>(
12✔
2719
        py: Python<'p>,
12✔
2720
        matrix: PyReadonlyArray2<'p, Complex64>,
12✔
2721
        null_value: Complex64,
12✔
2722
    ) -> PyResult<PyDiGraph> {
12✔
2723
        _from_adjacency_matrix(py, matrix, null_value)
12✔
2724
    }
12✔
2725

2726
    /// Add another PyDiGraph object into this PyDiGraph
2727
    ///
2728
    /// :param PyDiGraph other: The other PyDiGraph object to add onto this
2729
    ///     graph.
2730
    /// :param dict[int, tuple[int, tuple[int, T]]] node_map: A dictionary
2731
    ///     mapping node indices from this
2732
    ///     PyDiGraph object to node indices in the other PyDiGraph object.
2733
    ///     The key is a node index in this graph and the value is a tuple
2734
    ///     of the node index in the other graph to add an edge to and the
2735
    ///     weight of that edge. For example::
2736
    ///
2737
    ///         {
2738
    ///             1: (2, "weight"),
2739
    ///             2: (4, "weight2")
2740
    ///         }
2741
    ///
2742
    /// :param Callable node_map_func: An optional Python callable that will take in a
2743
    ///     single node weight/data object and return a new node weight/data
2744
    ///     object that will be used when adding an node from other onto this
2745
    ///     graph.
2746
    /// :param Callable edge_map_func: An optional Python callable that will take in a
2747
    ///     single edge weight/data object and return a new edge weight/data
2748
    ///     object that will be used when adding an edge from other onto this
2749
    ///     graph.
2750
    ///
2751
    /// :returns: new_node_ids: A dictionary mapping node index from the other
2752
    ///     PyDiGraph to the corresponding node index in this PyDAG after they've been
2753
    ///     combined
2754
    /// :rtype: dict[int, int]
2755
    ///
2756
    /// For example, start by building a graph:
2757
    ///
2758
    /// .. jupyter-execute::
2759
    ///
2760
    ///   import rustworkx as rx
2761
    ///   from rustworkx.visualization import mpl_draw
2762
    ///
2763
    ///   # Build first graph and visualize:
2764
    ///   graph = rx.PyDiGraph()
2765
    ///   node_a = graph.add_node('A')
2766
    ///   node_b = graph.add_child(node_a, 'B', 'A to B')
2767
    ///   node_c = graph.add_child(node_b, 'C', 'B to C')
2768
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
2769
    ///
2770
    /// Then build a second one:
2771
    ///
2772
    /// .. jupyter-execute::
2773
    ///
2774
    ///   # Build second graph and visualize:
2775
    ///   other_graph = rx.PyDiGraph()
2776
    ///   node_d = other_graph.add_node('D')
2777
    ///   other_graph.add_child(node_d, 'E', 'D to E')
2778
    ///   mpl_draw(other_graph, with_labels=True, labels=str, edge_labels=str)
2779
    ///
2780
    /// Finally compose the ``other_graph`` onto ``graph``
2781
    ///
2782
    /// .. jupyter-execute::
2783
    ///
2784
    ///   node_map = {node_b: (node_d, 'B to D')}
2785
    ///   graph.compose(other_graph, node_map)
2786
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
2787
    ///
2788
    #[pyo3(text_signature = "(self, other, node_map, /, node_map_func=None, edge_map_func=None)", signature = (other, node_map, node_map_func=None, edge_map_func=None))]
2789
    pub fn compose(
6✔
2790
        &mut self,
6✔
2791
        py: Python,
6✔
2792
        other: &PyDiGraph,
6✔
2793
        node_map: HashMap<usize, (usize, Py<PyAny>)>,
6✔
2794
        node_map_func: Option<Py<PyAny>>,
6✔
2795
        edge_map_func: Option<Py<PyAny>>,
6✔
2796
    ) -> PyResult<Py<PyAny>> {
6✔
2797
        let mut new_node_map: DictMap<NodeIndex, NodeIndex> =
6✔
2798
            DictMap::with_capacity(other.node_count());
6✔
2799

2800
        // TODO: Reimplement this without looping over the graphs
2801
        // Loop over other nodes add to self graph
2802
        for node in other.graph.node_indices() {
28✔
2803
            let new_index = self.graph.add_node(weight_transform_callable(
28✔
2804
                py,
28✔
2805
                &node_map_func,
28✔
2806
                &other.graph[node],
28✔
2807
            )?);
×
2808
            new_node_map.insert(node, new_index);
28✔
2809
        }
2810

2811
        // loop over other edges and add to self graph
2812
        for edge in other.graph.edge_references() {
30✔
2813
            let new_p_index = new_node_map.get(&edge.source()).unwrap();
30✔
2814
            let new_c_index = new_node_map.get(&edge.target()).unwrap();
30✔
2815
            let weight = weight_transform_callable(py, &edge_map_func, edge.weight())?;
30✔
2816
            self._add_edge(*new_p_index, *new_c_index, weight)?;
30✔
2817
        }
2818
        // Add edges from map
2819
        for (this_index, (index, weight)) in node_map.iter() {
6✔
2820
            let new_index = new_node_map.get(&NodeIndex::new(*index)).unwrap();
6✔
2821
            self._add_edge(
6✔
2822
                NodeIndex::new(*this_index),
6✔
2823
                *new_index,
6✔
2824
                weight.clone_ref(py),
6✔
2825
            )?;
×
2826
        }
2827
        let out_dict = PyDict::new(py);
6✔
2828
        for (orig_node, new_node) in new_node_map.iter() {
28✔
2829
            out_dict.set_item(orig_node.index(), new_node.index())?;
28✔
2830
        }
2831
        Ok(out_dict.into())
6✔
2832
    }
6✔
2833

2834
    /// Substitute a node with a PyDigraph object
2835
    ///
2836
    /// :param int node: The index of the node to be replaced with the PyDiGraph object
2837
    /// :param PyDiGraph other: The other graph to replace ``node`` with
2838
    /// :param Callable edge_map_fn: A callable object that will take 3 position
2839
    ///     parameters, ``(source, target, weight)`` to represent an edge either to
2840
    ///     or from ``node`` in this graph. The expected return value from this
2841
    ///     callable is the node index of the node in ``other`` that an edge should
2842
    ///     be to/from. If None is returned, that edge will be skipped and not
2843
    ///     be copied.
2844
    /// :param Callable node_filter: An optional callable object that when used
2845
    ///     will receive a node's payload object from ``other`` and return
2846
    ///     ``True`` if that node is to be included in the graph or not.
2847
    /// :param Callable edge_weight_map: An optional callable object that when
2848
    ///     used will receive an edge's weight/data payload from ``other`` and
2849
    ///     will return an object to use as the weight for a newly created edge
2850
    ///     after the edge is mapped from ``other``. If not specified the weight
2851
    ///     from the edge in ``other`` will be copied by reference and used.
2852
    ///
2853
    /// :returns: A mapping of node indices in ``other`` to the equivalent node
2854
    ///     in this graph.
2855
    /// :rtype: NodeMap
2856
    ///
2857
    /// .. note::
2858
    ///
2859
    ///    The return type is a :class:`rustworkx.NodeMap` which is an unordered
2860
    ///    type. So it does not provide a deterministic ordering between objects
2861
    ///    when iterated over (although the same object will have a consistent
2862
    ///    order when iterated over multiple times).
2863
    ///
2864
    #[pyo3(
2865
        text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None)",
2866
        signature = (node, other, edge_map_fn, node_filter=None, edge_weight_map=None)
2867
    )]
2868
    fn substitute_node_with_subgraph(
70✔
2869
        &mut self,
70✔
2870
        py: Python,
70✔
2871
        node: usize,
70✔
2872
        other: &PyDiGraph,
70✔
2873
        edge_map_fn: Py<PyAny>,
70✔
2874
        node_filter: Option<Py<PyAny>>,
70✔
2875
        edge_weight_map: Option<Py<PyAny>>,
70✔
2876
    ) -> PyResult<NodeMap> {
70✔
2877
        let weight_map_fn =
70✔
2878
            |obj: &Py<PyAny>, weight_fn: &Option<Py<PyAny>>| -> PyResult<Py<PyAny>> {
356✔
2879
                match weight_fn {
356✔
2880
                    Some(weight_fn) => weight_fn.call1(py, (obj,)),
4✔
2881
                    None => Ok(obj.clone_ref(py)),
352✔
2882
                }
2883
            };
356✔
2884
        let map_fn =
70✔
2885
            |source: usize, target: usize, weight: &Py<PyAny>| -> PyResult<Option<usize>> {
46✔
2886
                let res = edge_map_fn.call1(py, (source, target, weight))?;
46✔
2887
                res.extract(py)
46✔
2888
            };
46✔
2889
        let filter_fn = |obj: &Py<PyAny>, filter_fn: &Option<Py<PyAny>>| -> PyResult<bool> {
290✔
2890
            match filter_fn {
290✔
2891
                Some(filter) => {
10✔
2892
                    let res = filter.call1(py, (obj,))?;
10✔
2893
                    res.extract(py)
10✔
2894
                }
2895
                None => Ok(true),
280✔
2896
            }
2897
        };
290✔
2898
        let node_index: NodeIndex = NodeIndex::new(node);
70✔
2899
        if self.graph.node_weight(node_index).is_none() {
70✔
2900
            return Err(PyIndexError::new_err(format!(
2✔
2901
                "Specified node {node} is not in this graph"
2✔
2902
            )));
2✔
2903
        }
68✔
2904
        // Copy nodes from other to self
2905
        let mut out_map: DictMap<usize, usize> = DictMap::with_capacity(other.node_count());
68✔
2906
        for node in other.graph.node_indices() {
290✔
2907
            let node_weight = other.graph[node].clone_ref(py);
290✔
2908
            if !filter_fn(&node_weight, &node_filter)? {
290✔
2909
                continue;
4✔
2910
            }
286✔
2911
            let new_index = self.graph.add_node(node_weight);
286✔
2912
            out_map.insert(node.index(), new_index.index());
286✔
2913
        }
2914
        // If no nodes are copied bail here since there is nothing left
2915
        // to do.
2916
        if out_map.is_empty() {
68✔
2917
            self.remove_node(node_index.index())?;
2✔
2918
            // Return a new empty map to clear allocation from out_map
2919
            return Ok(NodeMap {
2✔
2920
                node_map: DictMap::new(),
2✔
2921
            });
2✔
2922
        }
66✔
2923
        // Copy edges from other to self
2924
        for edge in other.graph.edge_references().filter(|edge| {
362✔
2925
            out_map.contains_key(&edge.target().index())
362✔
2926
                && out_map.contains_key(&edge.source().index())
356✔
2927
        }) {
362✔
2928
            self._add_edge(
356✔
2929
                NodeIndex::new(out_map[&edge.source().index()]),
356✔
2930
                NodeIndex::new(out_map[&edge.target().index()]),
356✔
2931
                weight_map_fn(edge.weight(), &edge_weight_map)?,
356✔
2932
            )?;
×
2933
        }
2934
        // Add edges to/from node to nodes in other
2935
        let in_edges: Vec<(NodeIndex, NodeIndex, Py<PyAny>)> = self
66✔
2936
            .graph
66✔
2937
            .edges_directed(node_index, petgraph::Direction::Incoming)
66✔
2938
            .map(|edge| (edge.source(), edge.target(), edge.weight().clone_ref(py)))
66✔
2939
            .collect();
66✔
2940
        let out_edges: Vec<(NodeIndex, NodeIndex, Py<PyAny>)> = self
66✔
2941
            .graph
66✔
2942
            .edges_directed(node_index, petgraph::Direction::Outgoing)
66✔
2943
            .map(|edge| (edge.source(), edge.target(), edge.weight().clone_ref(py)))
66✔
2944
            .collect();
66✔
2945
        for (source, target, weight) in in_edges {
66✔
2946
            let old_index = map_fn(source.index(), target.index(), &weight)?;
14✔
2947
            let target_out = match old_index {
14✔
2948
                Some(old_index) => match out_map.get(&old_index) {
12✔
2949
                    Some(new_index) => NodeIndex::new(*new_index),
10✔
2950
                    None => {
2951
                        return Err(PyIndexError::new_err(format!(
2✔
2952
                            "No mapped index {old_index} found"
2✔
2953
                        )));
2✔
2954
                    }
2955
                },
2956
                None => continue,
2✔
2957
            };
2958
            self._add_edge(source, target_out, weight)?;
10✔
2959
        }
2960
        for (source, target, weight) in out_edges {
64✔
2961
            let old_index = map_fn(source.index(), target.index(), &weight)?;
32✔
2962
            let source_out = match old_index {
32✔
2963
                Some(old_index) => match out_map.get(&old_index) {
30✔
2964
                    Some(new_index) => NodeIndex::new(*new_index),
26✔
2965
                    None => {
2966
                        return Err(PyIndexError::new_err(format!(
4✔
2967
                            "No mapped index {old_index} found"
4✔
2968
                        )));
4✔
2969
                    }
2970
                },
2971
                None => continue,
2✔
2972
            };
2973
            self._add_edge(source_out, target, weight)?;
26✔
2974
        }
2975
        // Remove node
2976
        self.remove_node(node_index.index())?;
60✔
2977
        Ok(NodeMap { node_map: out_map })
60✔
2978
    }
70✔
2979

2980
    /// Substitute a set of nodes with a single new node.
2981
    ///
2982
    /// :param list[int] nodes: A set of nodes to be removed and replaced
2983
    ///     by the new node. Any nodes not in the graph are ignored.
2984
    ///     If empty, this method behaves like :meth:`~PyDiGraph.add_node`
2985
    ///     (but slower).
2986
    /// :param S obj: The data/weight to associate with the new node.
2987
    /// :param bool check_cycle: If set to ``True``, validates
2988
    ///     that the contraction will not introduce cycles before
2989
    ///     modifying the graph. If set to ``False``, validation is
2990
    ///     skipped. If not provided, inherits the value
2991
    ///     of ``check_cycle`` from this instance of
2992
    ///     :class:`~rustworkx.PyDiGraph`.
2993
    /// :param Callable weight_combo_fn: An optional Python callable that, when
2994
    ///     specified, is used to merge parallel edges introduced by the
2995
    ///     contraction, which will occur when multiple nodes in
2996
    ///     ``nodes`` have an incoming edge
2997
    ///     from the same source node or when multiple nodes in
2998
    ///     ``nodes`` have an outgoing edge to the same target node.
2999
    ///     If this instance of :class:`~rustworkx.PyDiGraph` is a multigraph,
3000
    ///     leave this unspecified to preserve parallel edges. If unspecified
3001
    ///     when not a multigraph, parallel edges and their weights will be
3002
    ///     combined by choosing one of the edge's weights arbitrarily based
3003
    ///     on an internal iteration order, subject to change.
3004
    /// :returns: The index of the newly created node
3005
    /// :rtype: int
3006
    /// :raises DAGWouldCycle: The cycle check is enabled and the
3007
    ///     contraction would introduce cycle(s).
3008
    #[pyo3(text_signature = "(self, nodes, obj, /, check_cycle=None, weight_combo_fn=None)", signature = (nodes, obj, check_cycle=None, weight_combo_fn=None))]
3009
    pub fn contract_nodes(
36✔
3010
        &mut self,
36✔
3011
        py: Python,
36✔
3012
        nodes: Vec<usize>,
36✔
3013
        obj: Py<PyAny>,
36✔
3014
        check_cycle: Option<bool>,
36✔
3015
        weight_combo_fn: Option<Py<PyAny>>,
36✔
3016
    ) -> RxPyResult<usize> {
36✔
3017
        let nodes = nodes.into_iter().map(|i| NodeIndex::new(i));
82✔
3018
        let check_cycle = check_cycle.unwrap_or(self.check_cycle);
36✔
3019
        let res = match (weight_combo_fn, &self.multigraph) {
36✔
3020
            (Some(user_callback), _) => {
2✔
3021
                self.graph
2✔
3022
                    .contract_nodes_simple(nodes, obj, check_cycle, |w1, w2| {
8✔
3023
                        user_callback.call1(py, (w1, w2))
8✔
3024
                    })?
8✔
3025
            }
3026
            (None, false) => {
3027
                // By default, just take first edge.
3028
                self.graph
4✔
3029
                    .contract_nodes_simple(nodes, obj, check_cycle, move |w1, _| {
8✔
3030
                        Ok::<_, PyErr>(w1.clone_ref(py))
8✔
3031
                    })?
8✔
3032
            }
3033
            (None, true) => self.graph.contract_nodes(nodes, obj, check_cycle)?,
30✔
3034
        };
3035
        self.node_removed = true;
26✔
3036
        Ok(res.index())
26✔
3037
    }
36✔
3038

3039
    /// Check if contracting the specified nodes can occur without introducing cycles.
3040
    ///
3041
    /// :param list[int] nodes: A set of node indices to check for contraction.
3042
    /// :returns: `True` if contraction can proceed without creating cycles, `False` otherwise.
3043
    #[pyo3(text_signature = "(self, nodes, /)",signature = (nodes))]
3044
    pub fn can_contract_without_cycle(&self, nodes: Vec<usize>) -> bool {
4✔
3045
        let index_set: IndexSet<NodeIndex, RandomState> =
4✔
3046
            nodes.into_iter().map(NodeIndex::new).collect();
4✔
3047
        can_contract(&self.graph, &index_set)
4✔
3048
    }
4✔
3049

3050
    /// Return a new PyDiGraph object for a subgraph of this graph and a NodeMap
3051
    /// object that maps the nodes of the subgraph to the nodes of the original graph.
3052
    ///
3053
    /// .. note::
3054
    ///     This method is identical to :meth:`.subgraph()` but includes a
3055
    ///     NodeMap object that maps the nodes of the subgraph to the nodes of
3056
    ///     the original graph.
3057
    ///
3058
    /// :param list[int] nodes: A list of node indices to generate the subgraph
3059
    ///     from. If a node index is included that is not present in the graph
3060
    ///     it will silently be ignored.
3061
    /// :param bool preserve_attrs: If set to ``True`` the attributes of the PyDiGraph
3062
    ///     will be copied by reference to be the attributes of the output
3063
    ///     subgraph. By default this is set to ``False`` and the :attr:`~.PyDiGraph.attrs`
3064
    ///     attribute will be ``None`` in the subgraph.
3065
    ///
3066
    /// :returns: A tuple containing a new PyDiGraph object representing a subgraph of this graph
3067
    ///     and a NodeMap object that maps the nodes of the subgraph to the nodes of the original graph.
3068
    ///     It is worth noting that node and edge weight/data payloads are
3069
    ///     passed by reference so if you update (not replace) an object used
3070
    ///     as the weight in graph or the subgraph it will also be updated in
3071
    ///     the other.
3072
    /// :rtype: tuple[PyDiGraph, NodeMap]
3073
    ///
3074
    #[pyo3(signature=(nodes, preserve_attrs=false), text_signature = "(self, nodes, /, preserve_attrs=False)")]
3075
    pub fn subgraph_with_nodemap(
28✔
3076
        &self,
28✔
3077
        py: Python,
28✔
3078
        nodes: Vec<usize>,
28✔
3079
        preserve_attrs: bool,
28✔
3080
    ) -> (PyDiGraph, NodeMap) {
28✔
3081
        let node_set: HashSet<usize> = nodes.iter().cloned().collect();
28✔
3082
        // mapping from original node index to new node index
3083
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(nodes.len());
28✔
3084
        // mapping from new node index to original node index
3085
        let mut node_dict: DictMap<usize, usize> = DictMap::with_capacity(nodes.len());
28✔
3086
        let node_filter = |node: NodeIndex| -> bool { node_set.contains(&node.index()) };
246✔
3087
        let mut out_graph = StablePyGraph::<Directed>::new();
28✔
3088
        let filtered = NodeFiltered(&self.graph, node_filter);
28✔
3089
        for node in filtered.node_references() {
48✔
3090
            let new_node = out_graph.add_node(node.1.clone_ref(py));
48✔
3091
            node_map.insert(node.0, new_node);
48✔
3092
            node_dict.insert(new_node.index(), node.0.index());
48✔
3093
        }
48✔
3094
        for edge in filtered.edge_references() {
28✔
3095
            let new_source = *node_map.get(&edge.source()).unwrap();
28✔
3096
            let new_target = *node_map.get(&edge.target()).unwrap();
28✔
3097
            out_graph.add_edge(new_source, new_target, edge.weight().clone_ref(py));
28✔
3098
        }
28✔
3099
        let attrs = if preserve_attrs {
28✔
3100
            self.attrs.clone_ref(py)
4✔
3101
        } else {
3102
            py.None()
24✔
3103
        };
3104
        let node_map = NodeMap {
28✔
3105
            node_map: node_dict,
28✔
3106
        };
28✔
3107
        let subgraph = PyDiGraph {
28✔
3108
            graph: out_graph,
28✔
3109
            node_removed: false,
28✔
3110
            cycle_state: algo::DfsSpace::default(),
28✔
3111
            check_cycle: self.check_cycle,
28✔
3112
            multigraph: self.multigraph,
28✔
3113
            attrs,
28✔
3114
        };
28✔
3115
        (subgraph, node_map)
28✔
3116
    }
28✔
3117

3118
    /// Return a new PyDiGraph object for a subgraph of this graph.
3119
    ///
3120
    /// .. note::
3121
    ///     To return a NodeMap object that maps the nodes of the subgraph to
3122
    ///     the nodes of the original graph, use :meth:`.subgraph_with_nodemap()`.
3123
    ///
3124
    /// :param list[int] nodes: A list of node indices to generate the subgraph
3125
    ///     from. If a node index is included that is not present in the graph
3126
    ///     it will silently be ignored.
3127
    /// :param bool preserve_attrs: If set to the True the attributes of the PyDiGraph
3128
    ///     will be copied by reference to be the attributes of the output
3129
    ///     subgraph. By default this is set to False and the :attr:`~.PyDiGraph.attrs`
3130
    ///     attribute will be ``None`` in the subgraph.
3131
    ///
3132
    /// :returns: A new PyDiGraph object representing a subgraph of this graph.
3133
    ///     It is worth noting that node and edge weight/data payloads are
3134
    ///     passed by reference so if you update (not replace) an object used
3135
    ///     as the weight in graph or the subgraph it will also be updated in
3136
    ///     the other.
3137
    /// :rtype: PyDiGraph
3138
    ///
3139
    #[pyo3(signature=(nodes, preserve_attrs=false), text_signature = "(self, nodes, /, preserve_attrs=False)")]
3140
    pub fn subgraph(&self, py: Python, nodes: Vec<usize>, preserve_attrs: bool) -> PyDiGraph {
12✔
3141
        let (subgraph, _) = self.subgraph_with_nodemap(py, nodes, preserve_attrs);
12✔
3142
        subgraph
12✔
3143
    }
12✔
3144

3145
    /// Return a new PyDiGraph object for an edge induced subgraph of this graph
3146
    ///
3147
    /// The induced subgraph contains each edge in `edge_list` and each node
3148
    /// incident to any of those edges.
3149
    ///
3150
    /// :param list[tuple[int, int]] edge_list: A list of edge tuples
3151
    ///     (2-tuples with the source and
3152
    ///     target node) to generate the subgraph from. In cases of parallel
3153
    ///     edges for a multigraph all edges between the specified node. In case
3154
    ///     of an edge specified that doesn't exist in the graph it will be
3155
    ///     silently ignored.
3156
    ///
3157
    /// :returns: The edge subgraph
3158
    /// :rtype: PyDiGraph
3159
    ///
3160
    #[pyo3(text_signature = "(self, edge_list, /)")]
3161
    pub fn edge_subgraph(&self, edge_list: Vec<[usize; 2]>) -> PyDiGraph {
8✔
3162
        // Filter non-existent edges
3163
        let edges: Vec<[usize; 2]> = edge_list
8✔
3164
            .into_iter()
8✔
3165
            .filter(|x| {
14✔
3166
                let source = NodeIndex::new(x[0]);
14✔
3167
                let target = NodeIndex::new(x[1]);
14✔
3168
                self.graph.find_edge(source, target).is_some()
14✔
3169
            })
14✔
3170
            .collect();
8✔
3171

3172
        let nodes: HashSet<NodeIndex> = edges
8✔
3173
            .iter()
8✔
3174
            .flat_map(|x| x.iter())
12✔
3175
            .copied()
8✔
3176
            .map(NodeIndex::new)
8✔
3177
            .collect();
8✔
3178
        let mut edge_set: HashSet<[NodeIndex; 2]> = HashSet::with_capacity(edges.len());
8✔
3179
        for edge in edges {
12✔
3180
            let source_index = NodeIndex::new(edge[0]);
12✔
3181
            let target_index = NodeIndex::new(edge[1]);
12✔
3182
            edge_set.insert([source_index, target_index]);
12✔
3183
        }
12✔
3184
        let mut out_graph = self.clone();
8✔
3185
        for node in self
14✔
3186
            .graph
8✔
3187
            .node_indices()
8✔
3188
            .filter(|node| !nodes.contains(node))
32✔
3189
        {
14✔
3190
            out_graph.graph.remove_node(node);
14✔
3191
            out_graph.node_removed = true;
14✔
3192
        }
14✔
3193
        for edge in self
28✔
3194
            .graph
8✔
3195
            .edge_references()
8✔
3196
            .filter(|edge| !edge_set.contains(&[edge.source(), edge.target()]))
44✔
3197
        {
28✔
3198
            out_graph.graph.remove_edge(edge.id());
28✔
3199
        }
28✔
3200
        out_graph
8✔
3201
    }
8✔
3202

3203
    /// Check if the graph is symmetric
3204
    ///
3205
    /// :returns: True if the graph is symmetric
3206
    /// :rtype: bool
3207
    #[pyo3(text_signature = "(self)")]
3208
    pub fn is_symmetric(&self) -> bool {
4✔
3209
        let mut edges: HashSet<(NodeIndex, NodeIndex)> = HashSet::new();
4✔
3210
        for (source, target) in self
20✔
3211
            .graph
4✔
3212
            .edge_references()
4✔
3213
            .map(|edge| (edge.source(), edge.target()))
20✔
3214
        {
3215
            let edge = (source, target);
20✔
3216
            let reversed = (target, source);
20✔
3217
            if edges.contains(&reversed) {
20✔
3218
                edges.remove(&reversed);
8✔
3219
            } else {
12✔
3220
                edges.insert(edge);
12✔
3221
            }
12✔
3222
        }
3223
        edges.is_empty()
4✔
3224
    }
4✔
3225

3226
    /// Make edges in graph symmetric
3227
    ///
3228
    /// This function iterates over all the edges in the graph, adding for each
3229
    /// edge the reversed edge, unless one is already present. Note the edge insertion
3230
    /// is not fixed and the edge indices are not guaranteed to be consistent
3231
    /// between executions of this method on identical graphs.
3232
    ///
3233
    /// :param Callable edge_payload: This optional argument takes in a callable which will
3234
    ///     be passed a single positional argument the data payload for an edge that will
3235
    ///     have a reverse copied in the graph. The returned value from this callable will
3236
    ///     be used as the data payload for the new edge created. If this is not specified
3237
    ///     then by default the data payload will be copied when the reverse edge is added.
3238
    ///     If there are parallel edges, then one of the edges (typically the one with the lower
3239
    ///     index, but this is not a guarantee) will be copied.
3240
    #[pyo3(signature = (edge_payload_fn=None))]
3241
    pub fn make_symmetric(
14✔
3242
        &mut self,
14✔
3243
        py: Python,
14✔
3244
        edge_payload_fn: Option<Py<PyAny>>,
14✔
3245
    ) -> PyResult<()> {
14✔
3246
        let edges: HashMap<[NodeIndex; 2], EdgeIndex> = self
14✔
3247
            .graph
14✔
3248
            .edge_references()
14✔
3249
            .map(|edge| ([edge.source(), edge.target()], edge.id()))
38✔
3250
            .collect();
14✔
3251
        for ([edge_source, edge_target], edge_index) in edges.iter() {
34✔
3252
            if !edges.contains_key(&[*edge_target, *edge_source]) {
34✔
3253
                let forward_weight = self.graph.edge_weight(*edge_index).unwrap();
18✔
3254
                let weight: Py<PyAny> = match edge_payload_fn.as_ref() {
18✔
3255
                    Some(callback) => callback.call1(py, (forward_weight,))?,
10✔
3256
                    None => forward_weight.clone_ref(py),
8✔
3257
                };
3258
                self._add_edge(*edge_target, *edge_source, weight)?;
16✔
3259
            }
16✔
3260
        }
3261
        Ok(())
12✔
3262
    }
14✔
3263

3264
    /// Generate a new PyGraph object from this graph
3265
    ///
3266
    /// This will create a new :class:`~rustworkx.PyGraph` object from this
3267
    /// graph. All edges in this graph will be created as undirected edges in
3268
    /// the new graph object. For directed graphs with bidirectional edges, you
3269
    /// can set `multigraph=False` to condense them into a single edge and specify
3270
    /// a function to combine the weights/data of the edges.
3271
    /// Do note that the node and edge weights/data payloads will be passed
3272
    /// by reference to the new :class:`~rustworkx.PyGraph` object.
3273
    ///
3274
    /// .. note::
3275
    ///
3276
    ///     The node indices in the output :class:`~rustworkx.PyGraph` may
3277
    ///     differ if nodes have been removed.
3278
    ///
3279
    /// :param bool multigraph: If set to `False` the output graph will not
3280
    ///     allow parallel edges. Instead parallel edges will be condensed
3281
    ///     into a single edge and their data will be combined using
3282
    ///     `weight_combo_fn`. If `weight_combo_fn` is not provided, the data
3283
    ///     of the edge with the largest index will be kept. Default: `True`.
3284
    /// :param Callable weight_combo_fn: An optional python callable that will take in a
3285
    ///     two edge weight/data object and return a new edge weight/data
3286
    ///     object that will be used when adding an edge between two nodes
3287
    ///     connected by multiple edges (of either direction) in the original
3288
    ///     directed graph.
3289
    ///
3290
    /// :returns: A new PyGraph object with an undirected edge for every
3291
    ///     directed edge in this graph
3292
    /// :rtype: PyGraph
3293
    #[pyo3(signature=(multigraph=true, weight_combo_fn=None), text_signature = "(self, /, multigraph=True, weight_combo_fn=None)")]
3294
    pub fn to_undirected(
30✔
3295
        &self,
30✔
3296
        py: Python,
30✔
3297
        multigraph: bool,
30✔
3298
        weight_combo_fn: Option<Py<PyAny>>,
30✔
3299
    ) -> PyResult<crate::graph::PyGraph> {
30✔
3300
        let node_count = self.node_count();
30✔
3301
        let mut new_graph = if multigraph {
30✔
3302
            StablePyGraph::<Undirected>::with_capacity(node_count, self.graph.edge_count())
26✔
3303
        } else {
3304
            // If multigraph is false edge count is difficult to predict
3305
            // without counting parallel edges. So, just stick with 0 and
3306
            // reallocate dynamically
3307
            StablePyGraph::<Undirected>::with_capacity(node_count, 0)
4✔
3308
        };
3309

3310
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(node_count);
30✔
3311

3312
        let combine = |a: &Py<PyAny>,
30✔
3313
                       b: &Py<PyAny>,
3314
                       combo_fn: &Option<Py<PyAny>>|
3315
         -> PyResult<Option<Py<PyAny>>> {
10✔
3316
            match combo_fn {
10✔
3317
                Some(combo_fn) => {
2✔
3318
                    let res = combo_fn.call1(py, (a, b))?;
2✔
3319
                    Ok(Some(res))
2✔
3320
                }
3321
                None => Ok(None),
8✔
3322
            }
3323
        };
10✔
3324

3325
        for node_index in self.graph.node_indices() {
142✔
3326
            let node = self.graph[node_index].clone_ref(py);
142✔
3327
            let new_index = new_graph.add_node(node);
142✔
3328
            node_map.insert(node_index, new_index);
142✔
3329
        }
142✔
3330
        for edge in self.graph.edge_references() {
184✔
3331
            let &source = node_map.get(&edge.source()).unwrap();
184✔
3332
            let &target = node_map.get(&edge.target()).unwrap();
184✔
3333
            let weight = edge.weight().clone_ref(py);
184✔
3334
            if multigraph {
184✔
3335
                new_graph.add_edge(source, target, weight);
164✔
3336
            } else {
164✔
3337
                let exists = new_graph.find_edge(source, target);
20✔
3338
                match exists {
20✔
3339
                    Some(index) => {
10✔
3340
                        let old_weight = new_graph.edge_weight_mut(index).unwrap();
10✔
3341
                        match combine(old_weight, edge.weight(), &weight_combo_fn)? {
10✔
3342
                            Some(value) => {
2✔
3343
                                *old_weight = value;
2✔
3344
                            }
2✔
3345
                            None => {
8✔
3346
                                *old_weight = weight;
8✔
3347
                            }
8✔
3348
                        }
3349
                    }
3350
                    None => {
10✔
3351
                        new_graph.add_edge(source, target, weight);
10✔
3352
                    }
10✔
3353
                }
3354
            }
3355
        }
3356
        Ok(crate::graph::PyGraph {
30✔
3357
            graph: new_graph,
30✔
3358
            node_removed: false,
30✔
3359
            multigraph,
30✔
3360
            attrs: py.None(),
30✔
3361
        })
30✔
3362
    }
30✔
3363

3364
    /// Return a shallow copy of the graph
3365
    ///
3366
    /// All node and edge weight/data payloads in the copy will have a
3367
    /// shared reference to the original graph.
3368
    #[pyo3(text_signature = "(self)")]
3369
    pub fn copy(&self) -> PyDiGraph {
10✔
3370
        self.clone()
10✔
3371
    }
10✔
3372

3373
    /// Reverse the direction of all edges in the graph, in place.
3374
    ///
3375
    /// This method modifies the graph instance to reverse the direction of all edges.
3376
    /// It does so by iterating over all edges in the graph and removing each edge,
3377
    /// then adding a new edge in the opposite direction with the same weight.
3378
    ///
3379
    /// For Example::
3380
    ///
3381
    ///     import rustworkx as rx
3382
    ///
3383
    ///     graph = rx.PyDiGraph()
3384
    ///
3385
    ///     # Generate a path directed path graph with weights
3386
    ///     graph.extend_from_weighted_edge_list([
3387
    ///         (0, 1, 3),
3388
    ///         (1, 2, 5),
3389
    ///         (2, 3, 2),
3390
    ///     ])
3391
    ///     # Reverse edges
3392
    ///     graph.reverse()
3393
    ///
3394
    ///     assert graph.weighted_edge_list() == [(3, 2, 2), (2, 1, 5), (1, 0, 3)];
3395
    #[pyo3(text_signature = "(self)")]
3396
    pub fn reverse(&mut self, py: Python) {
18✔
3397
        let indices = self.graph.edge_indices().collect::<Vec<EdgeIndex>>();
18✔
3398
        for idx in indices {
2,000,088✔
3399
            let (source_node, dest_node) = self.graph.edge_endpoints(idx).unwrap();
2,000,088✔
3400
            let weight = self.graph.edge_weight(idx).unwrap().clone_ref(py);
2,000,088✔
3401
            self.graph.remove_edge(idx);
2,000,088✔
3402
            self.graph.add_edge(dest_node, source_node, weight);
2,000,088✔
3403
        }
2,000,088✔
3404
    }
18✔
3405

3406
    /// Filters a graph's nodes by some criteria conditioned on a node's data payload and returns those nodes' indices.
3407
    ///
3408
    /// This function takes in a function as an argument. This filter function will be passed in a node's data payload and is
3409
    /// required to return a boolean value stating whether the node's data payload fits some criteria.
3410
    ///
3411
    /// For example::
3412
    ///
3413
    ///     from rustworkx import PyDiGraph
3414
    ///
3415
    ///     graph = PyDiGraph()
3416
    ///     graph.add_nodes_from(list(range(5)))
3417
    ///
3418
    ///     def my_filter_function(node):
3419
    ///         return node > 2
3420
    ///
3421
    ///     indices = graph.filter_nodes(my_filter_function)
3422
    ///     assert indices == [3, 4]
3423
    ///
3424
    /// :param Callable filter_function: Function with which to filter nodes
3425
    ///
3426
    /// :returns: The node indices that match the filter
3427
    /// :rtype: NodeIndices
3428
    #[pyo3(text_signature = "(self, filter_function)")]
3429
    pub fn filter_nodes(&self, py: Python, filter_function: Py<PyAny>) -> PyResult<NodeIndices> {
8✔
3430
        let filter = |nindex: NodeIndex| -> PyResult<bool> {
32✔
3431
            let res = filter_function.call1(py, (&self.graph[nindex],))?;
32✔
3432
            res.extract(py)
30✔
3433
        };
32✔
3434

3435
        let mut n = Vec::with_capacity(self.graph.node_count());
8✔
3436
        for node_index in self.graph.node_indices() {
32✔
3437
            if filter(node_index)? {
32✔
3438
                n.push(node_index.index())
8✔
3439
            };
22✔
3440
        }
3441
        Ok(NodeIndices { nodes: n })
6✔
3442
    }
8✔
3443

3444
    /// Filters a graph's edges by some criteria conditioned on a edge's data payload and returns those edges' indices.
3445
    ///
3446
    /// This function takes in a function as an argument. This filter function will be passed in an edge's data payload and is
3447
    /// required to return a boolean value stating whether the edge's data payload fits some criteria.
3448
    ///
3449
    /// For example::
3450
    ///
3451
    ///     from rustworkx import PyGraph
3452
    ///     from rustworkx.generators import complete_graph
3453
    ///
3454
    ///     graph = PyGraph()
3455
    ///     graph.add_nodes_from(range(3))
3456
    ///     graph.add_edges_from([(0, 1, 'A'), (0, 1, 'B'), (1, 2, 'C')])
3457
    ///
3458
    ///     def my_filter_function(edge):
3459
    ///         if edge:
3460
    ///             return edge == 'B'
3461
    ///         return False
3462
    ///
3463
    ///     indices = graph.filter_edges(my_filter_function)
3464
    ///     assert indices == [1]
3465
    ///
3466
    /// :param Callable filter_function: Function with which to filter edges
3467
    ///
3468
    /// :returns: The edge indices that match the filter
3469
    /// :rtype: EdgeIndices
3470
    #[pyo3(text_signature = "(self, filter_function)")]
3471
    pub fn filter_edges(&self, py: Python, filter_function: Py<PyAny>) -> PyResult<EdgeIndices> {
8✔
3472
        let filter = |eindex: EdgeIndex| -> PyResult<bool> {
20✔
3473
            let res = filter_function.call1(py, (&self.graph[eindex],))?;
20✔
3474
            res.extract(py)
18✔
3475
        };
20✔
3476

3477
        let mut e = Vec::with_capacity(self.graph.edge_count());
8✔
3478
        for edge_index in self.graph.edge_indices() {
20✔
3479
            if filter(edge_index)? {
20✔
3480
                e.push(edge_index.index())
6✔
3481
            };
12✔
3482
        }
3483
        Ok(EdgeIndices { edges: e })
6✔
3484
    }
8✔
3485

3486
    /// Return the number of nodes in the graph
3487
    fn __len__(&self) -> PyResult<usize> {
218✔
3488
        Ok(self.graph.node_count())
218✔
3489
    }
218✔
3490

3491
    fn __getitem__(&self, idx: usize) -> PyResult<&Py<PyAny>> {
56✔
3492
        match self.graph.node_weight(NodeIndex::new(idx)) {
56✔
3493
            Some(data) => Ok(data),
54✔
3494
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3495
        }
3496
    }
56✔
3497

3498
    fn __setitem__(&mut self, idx: usize, value: Py<PyAny>) -> PyResult<()> {
3,770✔
3499
        let data = match self.graph.node_weight_mut(NodeIndex::new(idx)) {
3,770✔
3500
            Some(node_data) => node_data,
3,768✔
3501
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
3502
        };
3503
        *data = value;
3,768✔
3504
        Ok(())
3,768✔
3505
    }
3,770✔
3506

3507
    fn __delitem__(&mut self, idx: usize) -> PyResult<()> {
4✔
3508
        match self.graph.remove_node(NodeIndex::new(idx)) {
4✔
3509
            Some(_) => {
3510
                self.node_removed = true;
2✔
3511
                Ok(())
2✔
3512
            }
3513
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3514
        }
3515
    }
4✔
3516

3517
    #[classmethod]
3518
    #[pyo3(signature = (key, /))]
3519
    pub fn __class_getitem__(
4✔
3520
        cls: &Bound<'_, PyType>,
4✔
3521
        key: &Bound<'_, PyAny>,
4✔
3522
    ) -> PyResult<Py<PyAny>> {
4✔
3523
        let alias = PyGenericAlias::new(cls.py(), cls.as_any(), key)?;
4✔
3524
        Ok(alias.into())
4✔
3525
    }
4✔
3526

3527
    // Functions to enable Python Garbage Collection
3528

3529
    // Function for PyTypeObject.tp_traverse [1][2] used to tell Python what
3530
    // objects the PyDiGraph has strong references to.
3531
    //
3532
    // [1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
3533
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3534
    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
104✔
3535
        for node in self
8,159,316✔
3536
            .graph
104✔
3537
            .node_indices()
104✔
3538
            .map(|node| self.graph.node_weight(node).unwrap())
8,159,316✔
3539
        {
3540
            visit.call(node)?;
8,159,316✔
3541
        }
3542
        for edge in self
76,956✔
3543
            .graph
104✔
3544
            .edge_indices()
104✔
3545
            .map(|edge| self.graph.edge_weight(edge).unwrap())
76,956✔
3546
        {
3547
            visit.call(edge)?;
76,956✔
3548
        }
3549
        visit.call(&self.attrs)?;
104✔
3550
        Ok(())
104✔
3551
    }
104✔
3552

3553
    // Function for PyTypeObject.tp_clear [1][2] used to tell Python's GC how
3554
    // to drop all references held by a PyDiGraph object when the GC needs to
3555
    // break reference cycles.
3556
    //
3557
    // ]1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_clear
3558
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3559
    fn __clear__(&mut self, py: Python) {
×
3560
        self.graph = StablePyGraph::<Directed>::new();
×
3561
        self.node_removed = false;
×
3562
        self.attrs = py.None();
×
3563
    }
×
3564
}
3565

3566
fn is_cycle_check_required(dag: &PyDiGraph, a: NodeIndex, b: NodeIndex) -> bool {
26✔
3567
    let mut parents_a = dag
26✔
3568
        .graph
26✔
3569
        .neighbors_directed(a, petgraph::Direction::Incoming);
26✔
3570
    let mut children_b = dag
26✔
3571
        .graph
26✔
3572
        .neighbors_directed(b, petgraph::Direction::Outgoing);
26✔
3573
    parents_a.next().is_some() && children_b.next().is_some() && dag.graph.find_edge(a, b).is_none()
26✔
3574
}
26✔
3575

3576
fn weight_transform_callable(
58✔
3577
    py: Python,
58✔
3578
    map_fn: &Option<Py<PyAny>>,
58✔
3579
    value: &Py<PyAny>,
58✔
3580
) -> PyResult<Py<PyAny>> {
58✔
3581
    match map_fn {
58✔
3582
        Some(map_fn) => {
10✔
3583
            let res = map_fn.call1(py, (value,))?;
10✔
3584
            res.into_py_any(py)
10✔
3585
        }
3586
        None => Ok(value.clone_ref(py)),
48✔
3587
    }
3588
}
58✔
3589

3590
fn _from_adjacency_matrix<'p, T>(
26✔
3591
    py: Python<'p>,
26✔
3592
    matrix: PyReadonlyArray2<'p, T>,
26✔
3593
    null_value: T,
26✔
3594
) -> PyResult<PyDiGraph>
26✔
3595
where
26✔
3596
    T: Copy + std::cmp::PartialEq + numpy::Element + pyo3::IntoPyObject<'p> + IsNan,
26✔
3597
{
3598
    let array = matrix.as_array();
26✔
3599
    let shape = array.shape();
26✔
3600
    let mut out_graph = StablePyGraph::<Directed>::new();
26✔
3601
    let _node_indices: Vec<NodeIndex> = (0..shape[0])
26✔
3602
        .map(|node| Ok(out_graph.add_node(node.into_py_any(py)?)))
272✔
3603
        .collect::<PyResult<Vec<NodeIndex>>>()?;
26✔
3604
    for (index, row) in array.axis_iter(Axis(0)).enumerate() {
272✔
3605
        let source_index = NodeIndex::new(index);
272✔
3606
        for (target_index, elem) in row.iter().enumerate() {
20,216✔
3607
            if null_value.is_nan() {
20,216✔
3608
                if !elem.is_nan() {
36✔
3609
                    out_graph.add_edge(
16✔
3610
                        source_index,
16✔
3611
                        NodeIndex::new(target_index),
16✔
3612
                        elem.into_py_any(py)?,
16✔
3613
                    );
3614
                }
20✔
3615
            } else if *elem != null_value {
20,180✔
3616
                out_graph.add_edge(
18,882✔
3617
                    source_index,
18,882✔
3618
                    NodeIndex::new(target_index),
18,882✔
3619
                    elem.into_py_any(py)?,
18,882✔
3620
                );
3621
            }
1,298✔
3622
        }
3623
    }
3624
    Ok(PyDiGraph {
26✔
3625
        graph: out_graph,
26✔
3626
        cycle_state: algo::DfsSpace::default(),
26✔
3627
        check_cycle: false,
26✔
3628
        node_removed: false,
26✔
3629
        multigraph: true,
26✔
3630
        attrs: py.None(),
26✔
3631
    })
26✔
3632
}
26✔
3633

3634
/// Simple wrapper newtype that lets us use `Py` pointers as hash keys with the equality defined by
3635
/// the pointer address.  This is equivalent to using Python's `is` operator for comparisons.
3636
/// Using a newtype rather than casting the pointer to `usize` inline lets us retrieve a copy of
3637
/// the reference from the key entry.
3638
struct PyAnyId(Py<PyAny>);
3639
impl PyAnyId {
3640
    fn clone_ref(&self, py: Python) -> Py<PyAny> {
30✔
3641
        self.0.clone_ref(py)
30✔
3642
    }
30✔
3643
}
3644
impl ::std::hash::Hash for PyAnyId {
3645
    fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
86✔
3646
        (self.0.as_ptr() as usize).hash(state)
86✔
3647
    }
86✔
3648
}
3649
impl PartialEq for PyAnyId {
3650
    fn eq(&self, other: &Self) -> bool {
28✔
3651
        self.0.as_ptr() == other.0.as_ptr()
28✔
3652
    }
28✔
3653
}
3654
impl Eq for PyAnyId {}
3655

3656
/// Internal-only helper class used by `remove_node_retain_edges_by_key` to store its data as a
3657
/// typed object in a Python dictionary.
3658
#[pyclass]
3659
struct RemoveNodeEdgeValue {
3660
    weight: Py<PyAny>,
3661
    nodes: Vec<NodeIndex>,
3662
}
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