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

Qiskit / rustworkx / 15950659371

29 Jun 2025 02:17AM UTC coverage: 94.575% (-0.6%) from 95.201%
15950659371

push

github

web-flow
Adding quickcheck for complete graph (#1471)

* Adding quickcheck for complete graph

To verify the structural integrity of complete graph, we check the number of nodes, edges and whether all expected edges exist.

* Add ignore for files temporarily

* Address lint in test target as well

---------

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

17783 of 18803 relevant lines covered (94.58%)

1000773.52 hits per line

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

98.12
/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::exceptions::PyIndexError;
33
use pyo3::gc::PyVisit;
34
use pyo3::prelude::*;
35
use pyo3::types::{IntoPyDict, PyBool, PyDict, PyGenericAlias, PyList, PyString, PyTuple, PyType};
36
use pyo3::IntoPyObjectExt;
37
use pyo3::PyTraverseError;
38
use pyo3::Python;
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
    find_node_by_weight, weight_callable, DAGHasCycle, DAGWouldCycle, IsNan, NoEdgeBetweenNodes,
61
    NoSuitableNeighbors, NodesRemoved, StablePyGraph,
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: PyObject,
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,700✔
215
        self.graph.node_count()
3,700✔
216
    }
3,700✔
217
}
218

219
// Rust side only PyDiGraph methods
220
impl PyDiGraph {
221
    fn add_edge_no_cycle_check(
2,026,834✔
222
        &mut self,
2,026,834✔
223
        p_index: NodeIndex,
2,026,834✔
224
        c_index: NodeIndex,
2,026,834✔
225
        edge: PyObject,
2,026,834✔
226
    ) -> usize {
2,026,834✔
227
        if !self.multigraph {
2,026,834✔
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,696✔
235
        let edge = self.graph.add_edge(p_index, c_index, edge);
2,026,818✔
236
        edge.index()
2,026,818✔
237
    }
2,026,834✔
238

239
    fn _add_edge(
2,026,846✔
240
        &mut self,
2,026,846✔
241
        p_index: NodeIndex,
2,026,846✔
242
        c_index: NodeIndex,
2,026,846✔
243
        edge: PyObject,
2,026,846✔
244
    ) -> PyResult<usize> {
2,026,846✔
245
        // Only check for cycles if instance attribute is set to true
246
        if self.check_cycle {
2,026,846✔
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,820✔
257
        Ok(self.add_edge_no_cycle_check(p_index, c_index, edge))
2,026,834✔
258
    }
2,026,846✔
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, PyObject)> = 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, PyObject)>>();
44✔
285
        for (other_index, edge_index, weight) in edges {
80✔
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,238✔
304
        py: Python,
2,238✔
305
        check_cycle: bool,
2,238✔
306
        multigraph: bool,
2,238✔
307
        attrs: Option<PyObject>,
2,238✔
308
        node_count_hint: Option<usize>,
2,238✔
309
        edge_count_hint: Option<usize>,
2,238✔
310
    ) -> Self {
2,238✔
311
        PyDiGraph {
312
            graph: StablePyGraph::<Directed>::with_capacity(
2,238✔
313
                node_count_hint.unwrap_or_default(),
2,238✔
314
                edge_count_hint.unwrap_or_default(),
2,238✔
315
            ),
316
            cycle_state: algo::DfsSpace::default(),
2,238✔
317
            check_cycle,
2,238✔
318
            node_removed: false,
319
            multigraph,
2,238✔
320
            attrs: attrs.unwrap_or_else(|| py.None()),
2,238✔
321
        }
322
    }
2,238✔
323

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

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

342
        // save nodes to a list along with its index
343
        for node_idx in self.graph.node_indices() {
124✔
344
            let node_data = self.graph.node_weight(node_idx).unwrap();
124✔
345
            nodes.push((node_idx.index(), node_data).into_py_any(py)?);
124✔
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() {
114✔
350
            let idx = EdgeIndex::new(i);
114✔
351
            let edge = match self.graph.edge_weight(idx) {
114✔
352
                Some(edge_w) => {
98✔
353
                    let endpoints = self.graph.edge_endpoints(idx).unwrap();
98✔
354
                    (endpoints.0.index(), endpoints.1.index(), edge_w).into_py_any(py)?
98✔
355
                }
356
                None => py.None(),
16✔
357
            };
358
            edges.push(edge);
114✔
359
        }
360
        let out_dict = PyDict::new(py);
34✔
361
        let nodes_lst = PyList::new(py, nodes)?;
34✔
362
        let edges_lst = PyList::new(py, edges)?;
34✔
363
        out_dict.set_item("nodes", nodes_lst)?;
34✔
364
        out_dict.set_item("edges", edges_lst)?;
34✔
365
        out_dict.set_item("nodes_removed", self.node_removed)?;
34✔
366
        Ok(out_dict.into())
34✔
367
    }
34✔
368

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

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

388
        if !self.node_removed {
22✔
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 {
12✔
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();
12✔
415
            let last_item = binding.downcast::<PyTuple>().unwrap();
12✔
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();
12✔
419
            let mut tmp_nodes: Vec<NodeIndex> =
12✔
420
                Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len());
12✔
421

422
            for item in nodes_lst {
90✔
423
                let item = item.downcast::<PyTuple>().unwrap();
78✔
424
                let next_index: usize = item.get_item(0).unwrap().extract().unwrap();
78✔
425
                let weight: PyObject = item.get_item(1).unwrap().extract().unwrap();
78✔
426
                while next_index > self.graph.node_bound() {
98✔
427
                    // node does not exist
20✔
428
                    let tmp_node = self.graph.add_node(py.None());
20✔
429
                    tmp_nodes.push(tmp_node);
20✔
430
                }
20✔
431
                // add node to the graph, and update the next available node index
432
                self.graph.add_node(weight);
78✔
433
            }
434
            // Remove any temporary nodes we added
435
            for tmp_node in tmp_nodes {
32✔
436
                self.graph.remove_node(tmp_node);
20✔
437
            }
20✔
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());
22✔
442

443
        for item in edges_lst {
136✔
444
            if item.is_none() {
114✔
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 {
98✔
448
                let triple = item.downcast::<PyTuple>().unwrap();
98✔
449
                let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap();
98✔
450
                let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap();
98✔
451
                let edge_w = triple.get_item(2).unwrap().extract().unwrap();
98✔
452
                self.graph
98✔
453
                    .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w);
98✔
454
            }
98✔
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);
22✔
460

461
        Ok(())
22✔
462
    }
34✔
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 {
18✔
490
        self.multigraph
18✔
491
    }
18✔
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<&PyObject> {
246✔
536
        self.graph
246✔
537
            .edge_indices()
246✔
538
            .map(|edge| self.graph.edge_weight(edge).unwrap())
8,618✔
539
            .collect()
246✔
540
    }
246✔
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<&PyObject> {
176✔
580
        self.graph
176✔
581
            .node_indices()
176✔
582
            .map(|node| self.graph.node_weight(node).unwrap())
8,316✔
583
            .collect()
176✔
584
    }
176✔
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 {
212✔
592
        NodeIndices {
593
            nodes: self.graph.node_indices().map(|node| node.index()).collect(),
9,528✔
594
        }
595
    }
212✔
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 {
112✔
609
        self.node_indices()
112✔
610
    }
112✔
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 {
4✔
620
        let index = NodeIndex::new(node);
4✔
621
        self.graph.contains_node(index)
4✔
622
    }
4✔
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 {
170✔
633
        let index_a = NodeIndex::new(node_a);
170✔
634
        let index_b = NodeIndex::new(node_b);
170✔
635
        self.graph.find_edge(index_a, index_b).is_some()
170✔
636
    }
170✔
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<&PyObject> {
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<&PyObject> = Vec::new();
8✔
674
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
8✔
675
        for succ in children {
34✔
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<&PyObject> {
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<&PyObject> = Vec::new();
8✔
723
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
8✔
724
        for pred in parents {
34✔
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: PyObject,
14✔
771
    ) -> PyResult<Vec<&PyObject>> {
14✔
772
        let index = NodeIndex::new(node);
14✔
773
        let mut successors: Vec<&PyObject> = Vec::new();
14✔
774
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
14✔
775

776
        let filter_edge = |edge: &PyObject| -> 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 {
66✔
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: PyObject,
14✔
833
    ) -> PyResult<Vec<&PyObject>> {
14✔
834
        let index = NodeIndex::new(node);
14✔
835
        let mut predec: Vec<&PyObject> = Vec::new();
14✔
836
        let mut used_indices: HashSet<NodeIndex> = HashSet::new();
14✔
837

838
        let filter_edge = |edge: &PyObject| -> 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 {
66✔
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<&PyObject> {
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<&PyObject> {
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 {} is not present in the graph",
2✔
899
                    edge_index
2✔
900
                )));
2✔
901
            }
902
        };
903
        Ok(data)
2✔
904
    }
4✔
905

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1698
        Ok(())
4✔
1699
    }
8✔
1700

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2572
    /// Write an edge list file from the PyDiGraph object
2573
    ///
2574
    /// :param str path: The path to write the output file to
2575
    /// :param str deliminator: The optional character to use as a deliminator
2576
    ///     if not specified ``" "`` is used.
2577
    /// :param callable weight_fn: An optional callback function that will be
2578
    ///     passed an edge's data payload/weight object and is expected to
2579
    ///     return a string (a ``TypeError`` will be raised if it doesn't
2580
    ///     return a string). If specified the weight in the output file
2581
    ///     for each edge will be set to the returned string.
2582
    ///
2583
    ///  For example:
2584
    ///
2585
    ///  .. jupyter-execute::
2586
    ///
2587
    ///     import os
2588
    ///     import tempfile
2589
    ///
2590
    ///     import rustworkx as rx
2591
    ///
2592
    ///     graph = rx.generators.directed_path_graph(5)
2593
    ///     path = os.path.join(tempfile.gettempdir(), "edge_list")
2594
    ///     graph.write_edge_list(path, deliminator=',')
2595
    ///     # Print file contents
2596
    ///     with open(path, 'rt') as edge_file:
2597
    ///         print(edge_file.read())
2598
    ///
2599
    #[pyo3(text_signature = "(self, path, /, deliminator=None, weight_fn=None)", signature = (path, deliminator=None, weight_fn=None))]
2600
    pub fn write_edge_list(
10✔
2601
        &self,
10✔
2602
        py: Python,
10✔
2603
        path: &str,
10✔
2604
        deliminator: Option<char>,
10✔
2605
        weight_fn: Option<PyObject>,
10✔
2606
    ) -> PyResult<()> {
10✔
2607
        let file = File::create(path)?;
10✔
2608
        let mut buf_writer = BufWriter::new(file);
10✔
2609
        let delim = match deliminator {
10✔
2610
            Some(delim) => delim.to_string(),
2✔
2611
            None => " ".to_string(),
8✔
2612
        };
2613

2614
        for edge in self.graph.edge_references() {
20✔
2615
            buf_writer.write_all(
20✔
2616
                format!(
20✔
2617
                    "{}{}{}",
20✔
2618
                    edge.source().index(),
20✔
2619
                    delim,
20✔
2620
                    edge.target().index()
20✔
2621
                )
20✔
2622
                .as_bytes(),
20✔
2623
            )?;
×
2624
            match weight_callable(py, &weight_fn, edge.weight(), None as Option<String>)? {
20✔
2625
                Some(weight) => buf_writer.write_all(format!("{}{}\n", delim, weight).as_bytes()),
8✔
2626
                None => buf_writer.write_all(b"\n"),
8✔
2627
            }?;
×
2628
        }
2629
        buf_writer.flush()?;
6✔
2630
        Ok(())
6✔
2631
    }
10✔
2632

2633
    /// Create a new :class:`~rustworkx.PyDiGraph` object from an adjacency matrix
2634
    /// with matrix elements of type ``float``
2635
    ///
2636
    /// This method can be used to construct a new :class:`~rustworkx.PyDiGraph`
2637
    /// object from an input adjacency matrix. The node weights will be the
2638
    /// index from the matrix. The edge weights will be a float value of the
2639
    /// value from the matrix.
2640
    ///
2641
    /// This differs from the
2642
    /// :meth:`~rustworkx.PyDiGraph.from_complex_adjacency_matrix` in that the
2643
    /// type of the elements of input matrix must be a ``float`` (specifically
2644
    /// a ``numpy.float64``) and the output graph edge weights will be ``float``
2645
    /// too. While in :meth:`~rustworkx.PyDiGraph.from_complex_adjacency_matrix`
2646
    /// the matrix elements are of type ``complex`` (specifically
2647
    /// ``numpy.complex128``) and the edge weights in the output graph will be
2648
    /// ``complex`` too.
2649
    ///
2650
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
2651
    ///     a new :class:`~rustworkx.PyDiGraph` object from. It must be a 2
2652
    ///     dimensional array and be a ``float``/``np.float64`` data type.
2653
    /// :param float null_value: An optional float that will treated as a null
2654
    ///     value. If any element in the input matrix is this value it will be
2655
    ///     treated as not an edge. By default this is ``0.0``
2656
    ///
2657
    /// :returns: A new graph object generated from the adjacency matrix
2658
    /// :rtype: PyDiGraph
2659
    #[staticmethod]
2660
    #[pyo3(signature=(matrix, null_value=0.0), text_signature = "(matrix, /, null_value=0.0)")]
2661
    pub fn from_adjacency_matrix<'p>(
14✔
2662
        py: Python<'p>,
14✔
2663
        matrix: PyReadonlyArray2<'p, f64>,
14✔
2664
        null_value: f64,
14✔
2665
    ) -> PyResult<PyDiGraph> {
14✔
2666
        _from_adjacency_matrix(py, matrix, null_value)
14✔
2667
    }
14✔
2668

2669
    /// Create a new :class:`~rustworkx.PyDiGraph` object from an adjacency matrix
2670
    /// with matrix elements of type ``complex``
2671
    ///
2672
    /// This method can be used to construct a new :class:`~rustworkx.PyDiGraph`
2673
    /// object from an input adjacency matrix. The node weights will be the
2674
    /// index from the matrix. The edge weights will be a complex value of the
2675
    /// value from the matrix.
2676
    ///
2677
    /// This differs from the
2678
    /// :meth:`~rustworkx.PyDiGraph.from_adjacency_matrix` in that the type of
2679
    /// the elements of the input matrix in this method must be a ``complex``
2680
    /// (specifically a ``numpy.complex128``) and the output graph edge weights
2681
    /// will be ``complex`` too. While in
2682
    /// :meth:`~rustworkx.PyDiGraph.from_adjacency_matrix` the matrix elements
2683
    /// are of type ``float`` (specifically ``numpy.float64``) and the edge
2684
    /// weights in the output graph will be ``float`` too.
2685
    ///
2686
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
2687
    ///     a new :class:`~rustworkx.PyDiGraph` object from. It must be a 2
2688
    ///     dimensional array and be a ``complex``/``np.complex128`` data type.
2689
    /// :param complex null_value: An optional complex that will treated as a
2690
    ///     null value. If any element in the input matrix is this value it
2691
    ///     will be treated as not an edge. By default this is ``0.0+0.0j``
2692
    ///
2693
    /// :returns: A new graph object generated from the adjacency matrix
2694
    /// :rtype: PyDiGraph
2695
    #[staticmethod]
2696
    #[pyo3(signature=(matrix, null_value=Complex64::zero()), text_signature = "(matrix, /, null_value=0.0+0.0j)")]
8✔
2697
    pub fn from_complex_adjacency_matrix<'p>(
12✔
2698
        py: Python<'p>,
12✔
2699
        matrix: PyReadonlyArray2<'p, Complex64>,
12✔
2700
        null_value: Complex64,
12✔
2701
    ) -> PyResult<PyDiGraph> {
12✔
2702
        _from_adjacency_matrix(py, matrix, null_value)
12✔
2703
    }
12✔
2704

2705
    /// Add another PyDiGraph object into this PyDiGraph
2706
    ///
2707
    /// :param PyDiGraph other: The other PyDiGraph object to add onto this
2708
    ///     graph.
2709
    /// :param dict[int, tuple[int, tuple[int, T]]] node_map: A dictionary
2710
    ///     mapping node indices from this
2711
    ///     PyDiGraph object to node indices in the other PyDiGraph object.
2712
    ///     The key is a node index in this graph and the value is a tuple
2713
    ///     of the node index in the other graph to add an edge to and the
2714
    ///     weight of that edge. For example::
2715
    ///
2716
    ///         {
2717
    ///             1: (2, "weight"),
2718
    ///             2: (4, "weight2")
2719
    ///         }
2720
    ///
2721
    /// :param Callable node_map_func: An optional Python callable that will take in a
2722
    ///     single node weight/data object and return a new node weight/data
2723
    ///     object that will be used when adding an node from other onto this
2724
    ///     graph.
2725
    /// :param Callable edge_map_func: An optional Python callable that will take in a
2726
    ///     single edge weight/data object and return a new edge weight/data
2727
    ///     object that will be used when adding an edge from other onto this
2728
    ///     graph.
2729
    ///
2730
    /// :returns: new_node_ids: A dictionary mapping node index from the other
2731
    ///     PyDiGraph to the corresponding node index in this PyDAG after they've been
2732
    ///     combined
2733
    /// :rtype: dict[int, int]
2734
    ///
2735
    /// For example, start by building a graph:
2736
    ///
2737
    /// .. jupyter-execute::
2738
    ///
2739
    ///   import rustworkx as rx
2740
    ///   from rustworkx.visualization import mpl_draw
2741
    ///
2742
    ///   # Build first graph and visualize:
2743
    ///   graph = rx.PyDiGraph()
2744
    ///   node_a = graph.add_node('A')
2745
    ///   node_b = graph.add_child(node_a, 'B', 'A to B')
2746
    ///   node_c = graph.add_child(node_b, 'C', 'B to C')
2747
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
2748
    ///
2749
    /// Then build a second one:
2750
    ///
2751
    /// .. jupyter-execute::
2752
    ///
2753
    ///   # Build second graph and visualize:
2754
    ///   other_graph = rx.PyDiGraph()
2755
    ///   node_d = other_graph.add_node('D')
2756
    ///   other_graph.add_child(node_d, 'E', 'D to E')
2757
    ///   mpl_draw(other_graph, with_labels=True, labels=str, edge_labels=str)
2758
    ///
2759
    /// Finally compose the ``other_graph`` onto ``graph``
2760
    ///
2761
    /// .. jupyter-execute::
2762
    ///
2763
    ///   node_map = {node_b: (node_d, 'B to D')}
2764
    ///   graph.compose(other_graph, node_map)
2765
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
2766
    ///
2767
    #[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))]
2768
    pub fn compose(
6✔
2769
        &mut self,
6✔
2770
        py: Python,
6✔
2771
        other: &PyDiGraph,
6✔
2772
        node_map: HashMap<usize, (usize, PyObject)>,
6✔
2773
        node_map_func: Option<PyObject>,
6✔
2774
        edge_map_func: Option<PyObject>,
6✔
2775
    ) -> PyResult<PyObject> {
6✔
2776
        let mut new_node_map: DictMap<NodeIndex, NodeIndex> =
6✔
2777
            DictMap::with_capacity(other.node_count());
6✔
2778

2779
        // TODO: Reimplement this without looping over the graphs
2780
        // Loop over other nodes add to self graph
2781
        for node in other.graph.node_indices() {
28✔
2782
            let new_index = self.graph.add_node(weight_transform_callable(
28✔
2783
                py,
28✔
2784
                &node_map_func,
28✔
2785
                &other.graph[node],
28✔
2786
            )?);
×
2787
            new_node_map.insert(node, new_index);
28✔
2788
        }
2789

2790
        // loop over other edges and add to self graph
2791
        for edge in other.graph.edge_references() {
30✔
2792
            let new_p_index = new_node_map.get(&edge.source()).unwrap();
30✔
2793
            let new_c_index = new_node_map.get(&edge.target()).unwrap();
30✔
2794
            let weight = weight_transform_callable(py, &edge_map_func, edge.weight())?;
30✔
2795
            self._add_edge(*new_p_index, *new_c_index, weight)?;
30✔
2796
        }
2797
        // Add edges from map
2798
        for (this_index, (index, weight)) in node_map.iter() {
6✔
2799
            let new_index = new_node_map.get(&NodeIndex::new(*index)).unwrap();
6✔
2800
            self._add_edge(
6✔
2801
                NodeIndex::new(*this_index),
6✔
2802
                *new_index,
6✔
2803
                weight.clone_ref(py),
6✔
2804
            )?;
×
2805
        }
2806
        let out_dict = PyDict::new(py);
6✔
2807
        for (orig_node, new_node) in new_node_map.iter() {
28✔
2808
            out_dict.set_item(orig_node.index(), new_node.index())?;
28✔
2809
        }
2810
        Ok(out_dict.into())
6✔
2811
    }
6✔
2812

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

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

3018
    /// Check if contracting the specified nodes can occur without introducing cycles.
3019
    ///
3020
    /// :param list[int] nodes: A set of node indices to check for contraction.
3021
    /// :returns: `True` if contraction can proceed without creating cycles, `False` otherwise.
3022
    #[pyo3(text_signature = "(self, nodes, /)",signature = (nodes))]
3023
    pub fn can_contract_without_cycle(&self, nodes: Vec<usize>) -> bool {
4✔
3024
        let index_set: IndexSet<NodeIndex, RandomState> =
4✔
3025
            nodes.into_iter().map(NodeIndex::new).collect();
4✔
3026
        can_contract(&self.graph, &index_set)
4✔
3027
    }
4✔
3028

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

3097
    /// Return a new PyDiGraph object for a subgraph of this graph.
3098
    ///
3099
    /// .. note::
3100
    ///     To return a NodeMap object that maps the nodes of the subgraph to
3101
    ///     the nodes of the original graph, use :meth:`.subgraph_with_nodemap()`.
3102
    ///
3103
    /// :param list[int] nodes: A list of node indices to generate the subgraph
3104
    ///     from. If a node index is included that is not present in the graph
3105
    ///     it will silently be ignored.
3106
    /// :param bool preserve_attrs: If set to the True the attributes of the PyDiGraph
3107
    ///     will be copied by reference to be the attributes of the output
3108
    ///     subgraph. By default this is set to False and the :attr:`~.PyDiGraph.attrs`
3109
    ///     attribute will be ``None`` in the subgraph.
3110
    ///
3111
    /// :returns: A new PyDiGraph object representing a subgraph of this graph.
3112
    ///     It is worth noting that node and edge weight/data payloads are
3113
    ///     passed by reference so if you update (not replace) an object used
3114
    ///     as the weight in graph or the subgraph it will also be updated in
3115
    ///     the other.
3116
    /// :rtype: PyDiGraph
3117
    ///
3118
    #[pyo3(signature=(nodes, preserve_attrs=false), text_signature = "(self, nodes, /, preserve_attrs=False)")]
3119
    pub fn subgraph(&self, py: Python, nodes: Vec<usize>, preserve_attrs: bool) -> PyDiGraph {
12✔
3120
        let (subgraph, _) = self.subgraph_with_nodemap(py, nodes, preserve_attrs);
12✔
3121
        subgraph
12✔
3122
    }
12✔
3123

3124
    /// Return a new PyDiGraph object for an edge induced subgraph of this graph
3125
    ///
3126
    /// The induced subgraph contains each edge in `edge_list` and each node
3127
    /// incident to any of those edges.
3128
    ///
3129
    /// :param list[tuple[int, int]] edge_list: A list of edge tuples
3130
    ///     (2-tuples with the source and
3131
    ///     target node) to generate the subgraph from. In cases of parallel
3132
    ///     edges for a multigraph all edges between the specified node. In case
3133
    ///     of an edge specified that doesn't exist in the graph it will be
3134
    ///     silently ignored.
3135
    ///
3136
    /// :returns: The edge subgraph
3137
    /// :rtype: PyDiGraph
3138
    ///
3139
    #[pyo3(text_signature = "(self, edge_list, /)")]
3140
    pub fn edge_subgraph(&self, edge_list: Vec<[usize; 2]>) -> PyDiGraph {
8✔
3141
        // Filter non-existent edges
3142
        let edges: Vec<[usize; 2]> = edge_list
8✔
3143
            .into_iter()
8✔
3144
            .filter(|x| {
14✔
3145
                let source = NodeIndex::new(x[0]);
14✔
3146
                let target = NodeIndex::new(x[1]);
14✔
3147
                self.graph.find_edge(source, target).is_some()
14✔
3148
            })
14✔
3149
            .collect();
8✔
3150

3151
        let nodes: HashSet<NodeIndex> = edges
8✔
3152
            .iter()
8✔
3153
            .flat_map(|x| x.iter())
12✔
3154
            .copied()
8✔
3155
            .map(NodeIndex::new)
8✔
3156
            .collect();
8✔
3157
        let mut edge_set: HashSet<[NodeIndex; 2]> = HashSet::with_capacity(edges.len());
8✔
3158
        for edge in edges {
20✔
3159
            let source_index = NodeIndex::new(edge[0]);
12✔
3160
            let target_index = NodeIndex::new(edge[1]);
12✔
3161
            edge_set.insert([source_index, target_index]);
12✔
3162
        }
12✔
3163
        let mut out_graph = self.clone();
8✔
3164
        for node in self
14✔
3165
            .graph
8✔
3166
            .node_indices()
8✔
3167
            .filter(|node| !nodes.contains(node))
32✔
3168
        {
14✔
3169
            out_graph.graph.remove_node(node);
14✔
3170
            out_graph.node_removed = true;
14✔
3171
        }
14✔
3172
        for edge in self
28✔
3173
            .graph
8✔
3174
            .edge_references()
8✔
3175
            .filter(|edge| !edge_set.contains(&[edge.source(), edge.target()]))
44✔
3176
        {
28✔
3177
            out_graph.graph.remove_edge(edge.id());
28✔
3178
        }
28✔
3179
        out_graph
8✔
3180
    }
8✔
3181

3182
    /// Check if the graph is symmetric
3183
    ///
3184
    /// :returns: True if the graph is symmetric
3185
    /// :rtype: bool
3186
    #[pyo3(text_signature = "(self)")]
3187
    pub fn is_symmetric(&self) -> bool {
4✔
3188
        let mut edges: HashSet<(NodeIndex, NodeIndex)> = HashSet::new();
4✔
3189
        for (source, target) in self
20✔
3190
            .graph
4✔
3191
            .edge_references()
4✔
3192
            .map(|edge| (edge.source(), edge.target()))
20✔
3193
        {
3194
            let edge = (source, target);
20✔
3195
            let reversed = (target, source);
20✔
3196
            if edges.contains(&reversed) {
20✔
3197
                edges.remove(&reversed);
8✔
3198
            } else {
12✔
3199
                edges.insert(edge);
12✔
3200
            }
12✔
3201
        }
3202
        edges.is_empty()
4✔
3203
    }
4✔
3204

3205
    /// Make edges in graph symmetric
3206
    ///
3207
    /// This function iterates over all the edges in the graph, adding for each
3208
    /// edge the reversed edge, unless one is already present. Note the edge insertion
3209
    /// is not fixed and the edge indices are not guaranteed to be consistent
3210
    /// between executions of this method on identical graphs.
3211
    ///
3212
    /// :param Callable edge_payload: This optional argument takes in a callable which will
3213
    ///     be passed a single positional argument the data payload for an edge that will
3214
    ///     have a reverse copied in the graph. The returned value from this callable will
3215
    ///     be used as the data payload for the new edge created. If this is not specified
3216
    ///     then by default the data payload will be copied when the reverse edge is added.
3217
    ///     If there are parallel edges, then one of the edges (typically the one with the lower
3218
    ///     index, but this is not a guarantee) will be copied.
3219
    #[pyo3(signature = (edge_payload_fn=None))]
3220
    pub fn make_symmetric(
14✔
3221
        &mut self,
14✔
3222
        py: Python,
14✔
3223
        edge_payload_fn: Option<PyObject>,
14✔
3224
    ) -> PyResult<()> {
14✔
3225
        let edges: HashMap<[NodeIndex; 2], EdgeIndex> = self
14✔
3226
            .graph
14✔
3227
            .edge_references()
14✔
3228
            .map(|edge| ([edge.source(), edge.target()], edge.id()))
38✔
3229
            .collect();
14✔
3230
        for ([edge_source, edge_target], edge_index) in edges.iter() {
34✔
3231
            if !edges.contains_key(&[*edge_target, *edge_source]) {
34✔
3232
                let forward_weight = self.graph.edge_weight(*edge_index).unwrap();
18✔
3233
                let weight: PyObject = match edge_payload_fn.as_ref() {
18✔
3234
                    Some(callback) => callback.call1(py, (forward_weight,))?,
10✔
3235
                    None => forward_weight.clone_ref(py),
8✔
3236
                };
3237
                self._add_edge(*edge_target, *edge_source, weight)?;
16✔
3238
            }
16✔
3239
        }
3240
        Ok(())
12✔
3241
    }
14✔
3242

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

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

3291
        let combine = |a: &PyObject,
30✔
3292
                       b: &PyObject,
3293
                       combo_fn: &Option<PyObject>|
3294
         -> PyResult<Option<PyObject>> {
10✔
3295
            match combo_fn {
10✔
3296
                Some(combo_fn) => {
2✔
3297
                    let res = combo_fn.call1(py, (a, b))?;
2✔
3298
                    Ok(Some(res))
2✔
3299
                }
3300
                None => Ok(None),
8✔
3301
            }
3302
        };
10✔
3303

3304
        for node_index in self.graph.node_indices() {
142✔
3305
            let node = self.graph[node_index].clone_ref(py);
142✔
3306
            let new_index = new_graph.add_node(node);
142✔
3307
            node_map.insert(node_index, new_index);
142✔
3308
        }
142✔
3309
        for edge in self.graph.edge_references() {
184✔
3310
            let &source = node_map.get(&edge.source()).unwrap();
184✔
3311
            let &target = node_map.get(&edge.target()).unwrap();
184✔
3312
            let weight = edge.weight().clone_ref(py);
184✔
3313
            if multigraph {
184✔
3314
                new_graph.add_edge(source, target, weight);
164✔
3315
            } else {
164✔
3316
                let exists = new_graph.find_edge(source, target);
20✔
3317
                match exists {
20✔
3318
                    Some(index) => {
10✔
3319
                        let old_weight = new_graph.edge_weight_mut(index).unwrap();
10✔
3320
                        match combine(old_weight, edge.weight(), &weight_combo_fn)? {
10✔
3321
                            Some(value) => {
2✔
3322
                                *old_weight = value;
2✔
3323
                            }
2✔
3324
                            None => {
8✔
3325
                                *old_weight = weight;
8✔
3326
                            }
8✔
3327
                        }
3328
                    }
3329
                    None => {
10✔
3330
                        new_graph.add_edge(source, target, weight);
10✔
3331
                    }
10✔
3332
                }
3333
            }
3334
        }
3335
        Ok(crate::graph::PyGraph {
30✔
3336
            graph: new_graph,
30✔
3337
            node_removed: false,
30✔
3338
            multigraph,
30✔
3339
            attrs: py.None(),
30✔
3340
        })
30✔
3341
    }
30✔
3342

3343
    /// Return a shallow copy of the graph
3344
    ///
3345
    /// All node and edge weight/data payloads in the copy will have a
3346
    /// shared reference to the original graph.
3347
    #[pyo3(text_signature = "(self)")]
3348
    pub fn copy(&self) -> PyDiGraph {
10✔
3349
        self.clone()
10✔
3350
    }
10✔
3351

3352
    /// Reverse the direction of all edges in the graph, in place.
3353
    ///
3354
    /// This method modifies the graph instance to reverse the direction of all edges.
3355
    /// It does so by iterating over all edges in the graph and removing each edge,
3356
    /// then adding a new edge in the opposite direction with the same weight.
3357
    ///
3358
    /// For Example::
3359
    ///
3360
    ///     import rustworkx as rx
3361
    ///
3362
    ///     graph = rx.PyDiGraph()
3363
    ///
3364
    ///     # Generate a path directed path graph with weights
3365
    ///     graph.extend_from_weighted_edge_list([
3366
    ///         (0, 1, 3),
3367
    ///         (1, 2, 5),
3368
    ///         (2, 3, 2),
3369
    ///     ])
3370
    ///     # Reverse edges
3371
    ///     graph.reverse()
3372
    ///
3373
    ///     assert graph.weighted_edge_list() == [(3, 2, 2), (2, 1, 5), (1, 0, 3)];
3374
    #[pyo3(text_signature = "(self)")]
3375
    pub fn reverse(&mut self, py: Python) {
18✔
3376
        let indices = self.graph.edge_indices().collect::<Vec<EdgeIndex>>();
18✔
3377
        for idx in indices {
2,000,106✔
3378
            let (source_node, dest_node) = self.graph.edge_endpoints(idx).unwrap();
2,000,088✔
3379
            let weight = self.graph.edge_weight(idx).unwrap().clone_ref(py);
2,000,088✔
3380
            self.graph.remove_edge(idx);
2,000,088✔
3381
            self.graph.add_edge(dest_node, source_node, weight);
2,000,088✔
3382
        }
2,000,088✔
3383
    }
18✔
3384

3385
    /// Filters a graph's nodes by some criteria conditioned on a node's data payload and returns those nodes' indices.
3386
    ///
3387
    /// This function takes in a function as an argument. This filter function will be passed in a node's data payload and is
3388
    /// required to return a boolean value stating whether the node's data payload fits some criteria.
3389
    ///
3390
    /// For example::
3391
    ///
3392
    ///     from rustworkx import PyDiGraph
3393
    ///
3394
    ///     graph = PyDiGraph()
3395
    ///     graph.add_nodes_from(list(range(5)))
3396
    ///
3397
    ///     def my_filter_function(node):
3398
    ///         return node > 2
3399
    ///
3400
    ///     indices = graph.filter_nodes(my_filter_function)
3401
    ///     assert indices == [3, 4]
3402
    ///
3403
    /// :param Callable filter_function: Function with which to filter nodes
3404
    ///
3405
    /// :returns: The node indices that match the filter
3406
    /// :rtype: NodeIndices
3407
    #[pyo3(text_signature = "(self, filter_function)")]
3408
    pub fn filter_nodes(&self, py: Python, filter_function: PyObject) -> PyResult<NodeIndices> {
8✔
3409
        let filter = |nindex: NodeIndex| -> PyResult<bool> {
32✔
3410
            let res = filter_function.call1(py, (&self.graph[nindex],))?;
32✔
3411
            res.extract(py)
30✔
3412
        };
32✔
3413

3414
        let mut n = Vec::with_capacity(self.graph.node_count());
8✔
3415
        for node_index in self.graph.node_indices() {
32✔
3416
            if filter(node_index)? {
32✔
3417
                n.push(node_index.index())
8✔
3418
            };
22✔
3419
        }
3420
        Ok(NodeIndices { nodes: n })
6✔
3421
    }
8✔
3422

3423
    /// Filters a graph's edges by some criteria conditioned on a edge's data payload and returns those edges' indices.
3424
    ///
3425
    /// This function takes in a function as an argument. This filter function will be passed in an edge's data payload and is
3426
    /// required to return a boolean value stating whether the edge's data payload fits some criteria.
3427
    ///
3428
    /// For example::
3429
    ///
3430
    ///     from rustworkx import PyGraph
3431
    ///     from rustworkx.generators import complete_graph
3432
    ///
3433
    ///     graph = PyGraph()
3434
    ///     graph.add_nodes_from(range(3))
3435
    ///     graph.add_edges_from([(0, 1, 'A'), (0, 1, 'B'), (1, 2, 'C')])
3436
    ///
3437
    ///     def my_filter_function(edge):
3438
    ///         if edge:
3439
    ///             return edge == 'B'
3440
    ///         return False
3441
    ///
3442
    ///     indices = graph.filter_edges(my_filter_function)
3443
    ///     assert indices == [1]
3444
    ///
3445
    /// :param Callable filter_function: Function with which to filter edges
3446
    ///
3447
    /// :returns: The edge indices that match the filter
3448
    /// :rtype: EdgeIndices
3449
    #[pyo3(text_signature = "(self, filter_function)")]
3450
    pub fn filter_edges(&self, py: Python, filter_function: PyObject) -> PyResult<EdgeIndices> {
8✔
3451
        let filter = |eindex: EdgeIndex| -> PyResult<bool> {
20✔
3452
            let res = filter_function.call1(py, (&self.graph[eindex],))?;
20✔
3453
            res.extract(py)
18✔
3454
        };
20✔
3455

3456
        let mut e = Vec::with_capacity(self.graph.edge_count());
8✔
3457
        for edge_index in self.graph.edge_indices() {
20✔
3458
            if filter(edge_index)? {
20✔
3459
                e.push(edge_index.index())
6✔
3460
            };
12✔
3461
        }
3462
        Ok(EdgeIndices { edges: e })
6✔
3463
    }
8✔
3464

3465
    /// Return the number of nodes in the graph
3466
    fn __len__(&self) -> PyResult<usize> {
218✔
3467
        Ok(self.graph.node_count())
218✔
3468
    }
218✔
3469

3470
    fn __getitem__(&self, idx: usize) -> PyResult<&PyObject> {
56✔
3471
        match self.graph.node_weight(NodeIndex::new(idx)) {
56✔
3472
            Some(data) => Ok(data),
54✔
3473
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3474
        }
3475
    }
56✔
3476

3477
    fn __setitem__(&mut self, idx: usize, value: PyObject) -> PyResult<()> {
3,770✔
3478
        let data = match self.graph.node_weight_mut(NodeIndex::new(idx)) {
3,770✔
3479
            Some(node_data) => node_data,
3,768✔
3480
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
3481
        };
3482
        *data = value;
3,768✔
3483
        Ok(())
3,768✔
3484
    }
3,770✔
3485

3486
    fn __delitem__(&mut self, idx: usize) -> PyResult<()> {
4✔
3487
        match self.graph.remove_node(NodeIndex::new(idx)) {
4✔
3488
            Some(_) => {
3489
                self.node_removed = true;
2✔
3490
                Ok(())
2✔
3491
            }
3492
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3493
        }
3494
    }
4✔
3495

3496
    #[classmethod]
3497
    #[pyo3(signature = (key, /))]
3498
    pub fn __class_getitem__(
4✔
3499
        cls: &Bound<'_, PyType>,
4✔
3500
        key: &Bound<'_, PyAny>,
4✔
3501
    ) -> PyResult<PyObject> {
4✔
3502
        let alias = PyGenericAlias::new(cls.py(), cls.as_any(), key)?;
4✔
3503
        Ok(alias.into())
4✔
3504
    }
4✔
3505

3506
    // Functions to enable Python Garbage Collection
3507

3508
    // Function for PyTypeObject.tp_traverse [1][2] used to tell Python what
3509
    // objects the PyDiGraph has strong references to.
3510
    //
3511
    // [1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
3512
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3513
    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
132✔
3514
        for node in self
13,786,788✔
3515
            .graph
132✔
3516
            .node_indices()
132✔
3517
            .map(|node| self.graph.node_weight(node).unwrap())
13,786,788✔
3518
        {
3519
            visit.call(node)?;
13,786,788✔
3520
        }
3521
        for edge in self
911,112✔
3522
            .graph
132✔
3523
            .edge_indices()
132✔
3524
            .map(|edge| self.graph.edge_weight(edge).unwrap())
911,112✔
3525
        {
3526
            visit.call(edge)?;
911,112✔
3527
        }
3528
        visit.call(&self.attrs)?;
132✔
3529
        Ok(())
132✔
3530
    }
132✔
3531

3532
    // Function for PyTypeObject.tp_clear [1][2] used to tell Python's GC how
3533
    // to drop all references held by a PyDiGraph object when the GC needs to
3534
    // break reference cycles.
3535
    //
3536
    // ]1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_clear
3537
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3538
    fn __clear__(&mut self, py: Python) {
×
3539
        self.graph = StablePyGraph::<Directed>::new();
×
3540
        self.node_removed = false;
×
3541
        self.attrs = py.None();
×
3542
    }
×
3543
}
3544

3545
fn is_cycle_check_required(dag: &PyDiGraph, a: NodeIndex, b: NodeIndex) -> bool {
26✔
3546
    let mut parents_a = dag
26✔
3547
        .graph
26✔
3548
        .neighbors_directed(a, petgraph::Direction::Incoming);
26✔
3549
    let mut children_b = dag
26✔
3550
        .graph
26✔
3551
        .neighbors_directed(b, petgraph::Direction::Outgoing);
26✔
3552
    parents_a.next().is_some() && children_b.next().is_some() && dag.graph.find_edge(a, b).is_none()
26✔
3553
}
26✔
3554

3555
fn weight_transform_callable(
58✔
3556
    py: Python,
58✔
3557
    map_fn: &Option<PyObject>,
58✔
3558
    value: &PyObject,
58✔
3559
) -> PyResult<PyObject> {
58✔
3560
    match map_fn {
58✔
3561
        Some(map_fn) => {
10✔
3562
            let res = map_fn.call1(py, (value,))?;
10✔
3563
            res.into_py_any(py)
10✔
3564
        }
3565
        None => Ok(value.clone_ref(py)),
48✔
3566
    }
3567
}
58✔
3568

3569
fn _from_adjacency_matrix<'p, T>(
26✔
3570
    py: Python<'p>,
26✔
3571
    matrix: PyReadonlyArray2<'p, T>,
26✔
3572
    null_value: T,
26✔
3573
) -> PyResult<PyDiGraph>
26✔
3574
where
26✔
3575
    T: Copy + std::cmp::PartialEq + numpy::Element + pyo3::IntoPyObject<'p> + IsNan,
26✔
3576
{
3577
    let array = matrix.as_array();
26✔
3578
    let shape = array.shape();
26✔
3579
    let mut out_graph = StablePyGraph::<Directed>::new();
26✔
3580
    let _node_indices: Vec<NodeIndex> = (0..shape[0])
26✔
3581
        .map(|node| Ok(out_graph.add_node(node.into_py_any(py)?)))
272✔
3582
        .collect::<PyResult<Vec<NodeIndex>>>()?;
26✔
3583
    for (index, row) in array.axis_iter(Axis(0)).enumerate() {
272✔
3584
        let source_index = NodeIndex::new(index);
272✔
3585
        for (target_index, elem) in row.iter().enumerate() {
20,216✔
3586
            if null_value.is_nan() {
20,216✔
3587
                if !elem.is_nan() {
36✔
3588
                    out_graph.add_edge(
16✔
3589
                        source_index,
16✔
3590
                        NodeIndex::new(target_index),
16✔
3591
                        elem.into_py_any(py)?,
16✔
3592
                    );
3593
                }
20✔
3594
            } else if *elem != null_value {
20,180✔
3595
                out_graph.add_edge(
18,882✔
3596
                    source_index,
18,882✔
3597
                    NodeIndex::new(target_index),
18,882✔
3598
                    elem.into_py_any(py)?,
18,882✔
3599
                );
3600
            }
1,298✔
3601
        }
3602
    }
3603
    Ok(PyDiGraph {
26✔
3604
        graph: out_graph,
26✔
3605
        cycle_state: algo::DfsSpace::default(),
26✔
3606
        check_cycle: false,
26✔
3607
        node_removed: false,
26✔
3608
        multigraph: true,
26✔
3609
        attrs: py.None(),
26✔
3610
    })
26✔
3611
}
26✔
3612

3613
/// Simple wrapper newtype that lets us use `Py` pointers as hash keys with the equality defined by
3614
/// the pointer address.  This is equivalent to using Python's `is` operator for comparisons.
3615
/// Using a newtype rather than casting the pointer to `usize` inline lets us retrieve a copy of
3616
/// the reference from the key entry.
3617
struct PyAnyId(Py<PyAny>);
3618
impl PyAnyId {
3619
    fn clone_ref(&self, py: Python) -> Py<PyAny> {
30✔
3620
        self.0.clone_ref(py)
30✔
3621
    }
30✔
3622
}
3623
impl ::std::hash::Hash for PyAnyId {
3624
    fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
86✔
3625
        (self.0.as_ptr() as usize).hash(state)
86✔
3626
    }
86✔
3627
}
3628
impl PartialEq for PyAnyId {
3629
    fn eq(&self, other: &Self) -> bool {
28✔
3630
        self.0.as_ptr() == other.0.as_ptr()
28✔
3631
    }
28✔
3632
}
3633
impl Eq for PyAnyId {}
3634

3635
/// Internal-only helper class used by `remove_node_retain_edges_by_key` to store its data as a
3636
/// typed object in a Python dictionary.
3637
#[pyclass]
3638
struct RemoveNodeEdgeValue {
3639
    weight: Py<PyAny>,
3640
    nodes: Vec<NodeIndex>,
3641
}
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