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

Qiskit / rustworkx / 14197529291

01 Apr 2025 01:55PM UTC coverage: 95.33% (+0.001%) from 95.329%
14197529291

Pull #1403

github

web-flow
Merge 827f1b148 into 2b8a173a1
Pull Request #1403: Advanced Tests for rust repo

18495 of 19401 relevant lines covered (95.33%)

1085447.46 hits per line

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

98.34
/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, 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

45
use petgraph::algo;
46
use petgraph::graph::{EdgeIndex, NodeIndex};
47
use petgraph::prelude::*;
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, generic_class_getitem, weight_callable, DAGHasCycle, DAGWouldCycle, IsNan,
61
    NoEdgeBetweenNodes, NoSuitableNeighbors, NodesRemoved, StablePyGraph,
62
};
63

64
use super::dag_algo::is_directed_acyclic_graph;
65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

22✔
459
        Ok(())
22✔
460
    }
34✔
461

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

14✔
774
        let filter_edge = |edge: &PyObject| -> PyResult<bool> {
50✔
775
            let res = filter_fn.call1(py, (edge,))?;
50✔
776
            res.extract(py)
50✔
777
        };
50✔
778

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

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

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

14✔
836
        let filter_edge = |edge: &PyObject| -> PyResult<bool> {
52✔
837
            let res = filter_fn.call1(py, (edge,))?;
52✔
838
            res.extract(py)
52✔
839
        };
52✔
840

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

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

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

878
        let data = self.graph.edge_weight(edge_index).unwrap();
72✔
879
        Ok(data)
72✔
880
    }
76✔
881

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3016
    /// Return a new PyDiGraph object for a subgraph of this graph
3017
    ///
3018
    /// :param list[int] nodes: A list of node indices to generate the subgraph
3019
    ///     from. If a node index is included that is not present in the graph
3020
    ///     it will silently be ignored.
3021
    /// :param bool preserve_attrs: If set to ``True`` the attributes of the PyDiGraph
3022
    ///     will be copied by reference to be the attributes of the output
3023
    ///     subgraph. By default this is set to ``False`` and the :attr:`~.PyDiGraph.attrs`
3024
    ///     attribute will be ``None`` in the subgraph.
3025
    ///
3026
    /// :returns: A new PyDiGraph object representing a subgraph of this graph.
3027
    ///     It is worth noting that node and edge weight/data payloads are
3028
    ///     passed by reference so if you update (not replace) an object used
3029
    ///     as the weight in graph or the subgraph it will also be updated in
3030
    ///     the other. Node and edge the indices will be recreated for the subgraph for compactness.
3031
    ///     Therefore, do not access data using the original graph's indices.
3032
    /// :rtype: PyGraph
3033
    ///
3034
    #[pyo3(signature=(nodes, preserve_attrs=false),text_signature = "(self, nodes, /, preserve_attrs=False)")]
3035
    pub fn subgraph(&self, py: Python, nodes: Vec<usize>, preserve_attrs: bool) -> PyDiGraph {
10✔
3036
        let node_set: HashSet<usize> = nodes.iter().cloned().collect();
10✔
3037
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(nodes.len());
10✔
3038
        let node_filter = |node: NodeIndex| -> bool { node_set.contains(&node.index()) };
98✔
3039
        let mut out_graph = StablePyGraph::<Directed>::new();
10✔
3040
        let filtered = NodeFiltered(&self.graph, node_filter);
10✔
3041
        for node in filtered.node_references() {
16✔
3042
            let new_node = out_graph.add_node(node.1.clone_ref(py));
16✔
3043
            node_map.insert(node.0, new_node);
16✔
3044
        }
16✔
3045
        for edge in filtered.edge_references() {
14✔
3046
            let new_source = *node_map.get(&edge.source()).unwrap();
14✔
3047
            let new_target = *node_map.get(&edge.target()).unwrap();
14✔
3048
            out_graph.add_edge(new_source, new_target, edge.weight().clone_ref(py));
14✔
3049
        }
14✔
3050
        let attrs = if preserve_attrs {
10✔
3051
            self.attrs.clone_ref(py)
×
3052
        } else {
3053
            py.None()
10✔
3054
        };
3055
        PyDiGraph {
10✔
3056
            graph: out_graph,
10✔
3057
            node_removed: false,
10✔
3058
            cycle_state: algo::DfsSpace::default(),
10✔
3059
            check_cycle: self.check_cycle,
10✔
3060
            multigraph: self.multigraph,
10✔
3061
            attrs,
10✔
3062
        }
10✔
3063
    }
10✔
3064

3065
    /// Return a new PyDiGraph object for an edge induced subgraph of this graph
3066
    ///
3067
    /// The induced subgraph contains each edge in `edge_list` and each node
3068
    /// incident to any of those edges.
3069
    ///
3070
    /// :param list[tuple[int, int]] edge_list: A list of edge tuples
3071
    ///     (2-tuples with the source and
3072
    ///     target node) to generate the subgraph from. In cases of parallel
3073
    ///     edges for a multigraph all edges between the specified node. In case
3074
    ///     of an edge specified that doesn't exist in the graph it will be
3075
    ///     silently ignored.
3076
    ///
3077
    /// :returns: The edge subgraph
3078
    /// :rtype: PyDiGraph
3079
    ///
3080
    #[pyo3(text_signature = "(self, edge_list, /)")]
3081
    pub fn edge_subgraph(&self, edge_list: Vec<[usize; 2]>) -> PyDiGraph {
8✔
3082
        // Filter non-existent edges
8✔
3083
        let edges: Vec<[usize; 2]> = edge_list
8✔
3084
            .into_iter()
8✔
3085
            .filter(|x| {
14✔
3086
                let source = NodeIndex::new(x[0]);
14✔
3087
                let target = NodeIndex::new(x[1]);
14✔
3088
                self.graph.find_edge(source, target).is_some()
14✔
3089
            })
14✔
3090
            .collect();
8✔
3091

8✔
3092
        let nodes: HashSet<NodeIndex> = edges
8✔
3093
            .iter()
8✔
3094
            .flat_map(|x| x.iter())
12✔
3095
            .copied()
8✔
3096
            .map(NodeIndex::new)
8✔
3097
            .collect();
8✔
3098
        let mut edge_set: HashSet<[NodeIndex; 2]> = HashSet::with_capacity(edges.len());
8✔
3099
        for edge in edges {
20✔
3100
            let source_index = NodeIndex::new(edge[0]);
12✔
3101
            let target_index = NodeIndex::new(edge[1]);
12✔
3102
            edge_set.insert([source_index, target_index]);
12✔
3103
        }
12✔
3104
        let mut out_graph = self.clone();
8✔
3105
        for node in self
14✔
3106
            .graph
8✔
3107
            .node_indices()
8✔
3108
            .filter(|node| !nodes.contains(node))
32✔
3109
        {
14✔
3110
            out_graph.graph.remove_node(node);
14✔
3111
            out_graph.node_removed = true;
14✔
3112
        }
14✔
3113
        for edge in self
28✔
3114
            .graph
8✔
3115
            .edge_references()
8✔
3116
            .filter(|edge| !edge_set.contains(&[edge.source(), edge.target()]))
44✔
3117
        {
28✔
3118
            out_graph.graph.remove_edge(edge.id());
28✔
3119
        }
28✔
3120
        out_graph
8✔
3121
    }
8✔
3122

3123
    /// Check if the graph is symmetric
3124
    ///
3125
    /// :returns: True if the graph is symmetric
3126
    /// :rtype: bool
3127
    #[pyo3(text_signature = "(self)")]
3128
    pub fn is_symmetric(&self) -> bool {
4✔
3129
        let mut edges: HashSet<(NodeIndex, NodeIndex)> = HashSet::new();
4✔
3130
        for (source, target) in self
20✔
3131
            .graph
4✔
3132
            .edge_references()
4✔
3133
            .map(|edge| (edge.source(), edge.target()))
20✔
3134
        {
3135
            let edge = (source, target);
20✔
3136
            let reversed = (target, source);
20✔
3137
            if edges.contains(&reversed) {
20✔
3138
                edges.remove(&reversed);
8✔
3139
            } else {
12✔
3140
                edges.insert(edge);
12✔
3141
            }
12✔
3142
        }
3143
        edges.is_empty()
4✔
3144
    }
4✔
3145

3146
    /// Make edges in graph symmetric
3147
    ///
3148
    /// This function iterates over all the edges in the graph, adding for each
3149
    /// edge the reversed edge, unless one is already present. Note the edge insertion
3150
    /// is not fixed and the edge indices are not guaranteed to be consistent
3151
    /// between executions of this method on identical graphs.
3152
    ///
3153
    /// :param Callable edge_payload: This optional argument takes in a callable which will
3154
    ///     be passed a single positional argument the data payload for an edge that will
3155
    ///     have a reverse copied in the graph. The returned value from this callable will
3156
    ///     be used as the data payload for the new edge created. If this is not specified
3157
    ///     then by default the data payload will be copied when the reverse edge is added.
3158
    ///     If there are parallel edges, then one of the edges (typically the one with the lower
3159
    ///     index, but this is not a guarantee) will be copied.
3160
    #[pyo3(signature = (edge_payload_fn=None))]
3161
    pub fn make_symmetric(
14✔
3162
        &mut self,
14✔
3163
        py: Python,
14✔
3164
        edge_payload_fn: Option<PyObject>,
14✔
3165
    ) -> PyResult<()> {
14✔
3166
        let edges: HashMap<[NodeIndex; 2], EdgeIndex> = self
14✔
3167
            .graph
14✔
3168
            .edge_references()
14✔
3169
            .map(|edge| ([edge.source(), edge.target()], edge.id()))
38✔
3170
            .collect();
14✔
3171
        for ([edge_source, edge_target], edge_index) in edges.iter() {
34✔
3172
            if !edges.contains_key(&[*edge_target, *edge_source]) {
34✔
3173
                let forward_weight = self.graph.edge_weight(*edge_index).unwrap();
18✔
3174
                let weight: PyObject = match edge_payload_fn.as_ref() {
18✔
3175
                    Some(callback) => callback.call1(py, (forward_weight,))?,
10✔
3176
                    None => forward_weight.clone_ref(py),
8✔
3177
                };
3178
                self._add_edge(*edge_target, *edge_source, weight)?;
16✔
3179
            }
16✔
3180
        }
3181
        Ok(())
12✔
3182
    }
14✔
3183

3184
    /// Generate a new PyGraph object from this graph
3185
    ///
3186
    /// This will create a new :class:`~rustworkx.PyGraph` object from this
3187
    /// graph. All edges in this graph will be created as undirected edges in
3188
    /// the new graph object. For directed graphs with bidirectional edges, you
3189
    /// can set `multigraph=False` to condense them into a single edge and specify
3190
    /// a function to combine the weights/data of the edges.
3191
    /// Do note that the node and edge weights/data payloads will be passed
3192
    /// by reference to the new :class:`~rustworkx.PyGraph` object.
3193
    ///
3194
    /// .. note::
3195
    ///
3196
    ///     The node indices in the output :class:`~rustworkx.PyGraph` may
3197
    ///     differ if nodes have been removed.
3198
    ///
3199
    /// :param bool multigraph: If set to `False` the output graph will not
3200
    ///     allow parallel edges. Instead parallel edges will be condensed
3201
    ///     into a single edge and their data will be combined using
3202
    ///     `weight_combo_fn`. If `weight_combo_fn` is not provided, the data
3203
    ///     of the edge with the largest index will be kept. Default: `True`.
3204
    /// :param Callable weight_combo_fn: An optional python callable that will take in a
3205
    ///     two edge weight/data object and return a new edge weight/data
3206
    ///     object that will be used when adding an edge between two nodes
3207
    ///     connected by multiple edges (of either direction) in the original
3208
    ///     directed graph.
3209
    ///
3210
    /// :returns: A new PyGraph object with an undirected edge for every
3211
    ///     directed edge in this graph
3212
    /// :rtype: PyGraph
3213
    #[pyo3(signature=(multigraph=true, weight_combo_fn=None), text_signature = "(self, /, multigraph=True, weight_combo_fn=None)")]
3214
    pub fn to_undirected(
28✔
3215
        &self,
28✔
3216
        py: Python,
28✔
3217
        multigraph: bool,
28✔
3218
        weight_combo_fn: Option<PyObject>,
28✔
3219
    ) -> PyResult<crate::graph::PyGraph> {
28✔
3220
        let node_count = self.node_count();
28✔
3221
        let mut new_graph = if multigraph {
28✔
3222
            StablePyGraph::<Undirected>::with_capacity(node_count, self.graph.edge_count())
24✔
3223
        } else {
3224
            // If multigraph is false edge count is difficult to predict
3225
            // without counting parallel edges. So, just stick with 0 and
3226
            // reallocate dynamically
3227
            StablePyGraph::<Undirected>::with_capacity(node_count, 0)
4✔
3228
        };
3229

3230
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(node_count);
28✔
3231

28✔
3232
        let combine = |a: &PyObject,
28✔
3233
                       b: &PyObject,
3234
                       combo_fn: &Option<PyObject>|
3235
         -> PyResult<Option<PyObject>> {
10✔
3236
            match combo_fn {
10✔
3237
                Some(combo_fn) => {
2✔
3238
                    let res = combo_fn.call1(py, (a, b))?;
2✔
3239
                    Ok(Some(res))
2✔
3240
                }
3241
                None => Ok(None),
8✔
3242
            }
3243
        };
10✔
3244

3245
        for node_index in self.graph.node_indices() {
134✔
3246
            let node = self.graph[node_index].clone_ref(py);
134✔
3247
            let new_index = new_graph.add_node(node);
134✔
3248
            node_map.insert(node_index, new_index);
134✔
3249
        }
134✔
3250
        for edge in self.graph.edge_references() {
176✔
3251
            let &source = node_map.get(&edge.source()).unwrap();
176✔
3252
            let &target = node_map.get(&edge.target()).unwrap();
176✔
3253
            let weight = edge.weight().clone_ref(py);
176✔
3254
            if multigraph {
176✔
3255
                new_graph.add_edge(source, target, weight);
156✔
3256
            } else {
156✔
3257
                let exists = new_graph.find_edge(source, target);
20✔
3258
                match exists {
20✔
3259
                    Some(index) => {
10✔
3260
                        let old_weight = new_graph.edge_weight_mut(index).unwrap();
10✔
3261
                        match combine(old_weight, edge.weight(), &weight_combo_fn)? {
10✔
3262
                            Some(value) => {
2✔
3263
                                *old_weight = value;
2✔
3264
                            }
2✔
3265
                            None => {
8✔
3266
                                *old_weight = weight;
8✔
3267
                            }
8✔
3268
                        }
3269
                    }
3270
                    None => {
10✔
3271
                        new_graph.add_edge(source, target, weight);
10✔
3272
                    }
10✔
3273
                }
3274
            }
3275
        }
3276
        Ok(crate::graph::PyGraph {
28✔
3277
            graph: new_graph,
28✔
3278
            node_removed: false,
28✔
3279
            multigraph,
28✔
3280
            attrs: py.None(),
28✔
3281
        })
28✔
3282
    }
28✔
3283

3284
    /// Return a shallow copy of the graph
3285
    ///
3286
    /// All node and edge weight/data payloads in the copy will have a
3287
    /// shared reference to the original graph.
3288
    #[pyo3(text_signature = "(self)")]
3289
    pub fn copy(&self) -> PyDiGraph {
10✔
3290
        self.clone()
10✔
3291
    }
10✔
3292

3293
    /// Reverse the direction of all edges in the graph, in place.
3294
    ///
3295
    /// This method modifies the graph instance to reverse the direction of all edges.
3296
    /// It does so by iterating over all edges in the graph and removing each edge,
3297
    /// then adding a new edge in the opposite direction with the same weight.
3298
    ///
3299
    /// For Example::
3300
    ///
3301
    ///     import rustworkx as rx
3302
    ///
3303
    ///     graph = rx.PyDiGraph()
3304
    ///
3305
    ///     # Generate a path directed path graph with weights
3306
    ///     graph.extend_from_weighted_edge_list([
3307
    ///         (0, 1, 3),
3308
    ///         (1, 2, 5),
3309
    ///         (2, 3, 2),
3310
    ///     ])
3311
    ///     # Reverse edges
3312
    ///     graph.reverse()
3313
    ///
3314
    ///     assert graph.weighted_edge_list() == [(3, 2, 2), (2, 1, 5), (1, 0, 3)];
3315
    #[pyo3(text_signature = "(self)")]
3316
    pub fn reverse(&mut self, py: Python) {
18✔
3317
        let indices = self.graph.edge_indices().collect::<Vec<EdgeIndex>>();
18✔
3318
        for idx in indices {
2,000,106✔
3319
            let (source_node, dest_node) = self.graph.edge_endpoints(idx).unwrap();
2,000,088✔
3320
            let weight = self.graph.edge_weight(idx).unwrap().clone_ref(py);
2,000,088✔
3321
            self.graph.remove_edge(idx);
2,000,088✔
3322
            self.graph.add_edge(dest_node, source_node, weight);
2,000,088✔
3323
        }
2,000,088✔
3324
    }
18✔
3325

3326
    /// Filters a graph's nodes by some criteria conditioned on a node's data payload and returns those nodes' indices.
3327
    ///
3328
    /// This function takes in a function as an argument. This filter function will be passed in a node's data payload and is
3329
    /// required to return a boolean value stating whether the node's data payload fits some criteria.
3330
    ///
3331
    /// For example::
3332
    ///
3333
    ///     from rustworkx import PyDiGraph
3334
    ///
3335
    ///     graph = PyDiGraph()
3336
    ///     graph.add_nodes_from(list(range(5)))
3337
    ///
3338
    ///     def my_filter_function(node):
3339
    ///         return node > 2
3340
    ///
3341
    ///     indices = graph.filter_nodes(my_filter_function)
3342
    ///     assert indices == [3, 4]
3343
    ///
3344
    /// :param Callable filter_function: Function with which to filter nodes
3345
    ///
3346
    /// :returns: The node indices that match the filter
3347
    /// :rtype: NodeIndices
3348
    #[pyo3(text_signature = "(self, filter_function)")]
3349
    pub fn filter_nodes(&self, py: Python, filter_function: PyObject) -> PyResult<NodeIndices> {
8✔
3350
        let filter = |nindex: NodeIndex| -> PyResult<bool> {
32✔
3351
            let res = filter_function.call1(py, (&self.graph[nindex],))?;
32✔
3352
            res.extract(py)
30✔
3353
        };
32✔
3354

3355
        let mut n = Vec::with_capacity(self.graph.node_count());
8✔
3356
        for node_index in self.graph.node_indices() {
32✔
3357
            if filter(node_index)? {
32✔
3358
                n.push(node_index.index())
8✔
3359
            };
22✔
3360
        }
3361
        Ok(NodeIndices { nodes: n })
6✔
3362
    }
8✔
3363

3364
    /// Filters a graph's edges by some criteria conditioned on a edge's data payload and returns those edges' indices.
3365
    ///
3366
    /// This function takes in a function as an argument. This filter function will be passed in an edge's data payload and is
3367
    /// required to return a boolean value stating whether the edge's data payload fits some criteria.
3368
    ///
3369
    /// For example::
3370
    ///
3371
    ///     from rustworkx import PyGraph
3372
    ///     from rustworkx.generators import complete_graph
3373
    ///
3374
    ///     graph = PyGraph()
3375
    ///     graph.add_nodes_from(range(3))
3376
    ///     graph.add_edges_from([(0, 1, 'A'), (0, 1, 'B'), (1, 2, 'C')])
3377
    ///
3378
    ///     def my_filter_function(edge):
3379
    ///         if edge:
3380
    ///             return edge == 'B'
3381
    ///         return False
3382
    ///
3383
    ///     indices = graph.filter_edges(my_filter_function)
3384
    ///     assert indices == [1]
3385
    ///
3386
    /// :param Callable filter_function: Function with which to filter edges
3387
    ///
3388
    /// :returns: The edge indices that match the filter
3389
    /// :rtype: EdgeIndices
3390
    #[pyo3(text_signature = "(self, filter_function)")]
3391
    pub fn filter_edges(&self, py: Python, filter_function: PyObject) -> PyResult<EdgeIndices> {
8✔
3392
        let filter = |eindex: EdgeIndex| -> PyResult<bool> {
20✔
3393
            let res = filter_function.call1(py, (&self.graph[eindex],))?;
20✔
3394
            res.extract(py)
18✔
3395
        };
20✔
3396

3397
        let mut e = Vec::with_capacity(self.graph.edge_count());
8✔
3398
        for edge_index in self.graph.edge_indices() {
20✔
3399
            if filter(edge_index)? {
20✔
3400
                e.push(edge_index.index())
6✔
3401
            };
12✔
3402
        }
3403
        Ok(EdgeIndices { edges: e })
6✔
3404
    }
8✔
3405

3406
    /// Return the number of nodes in the graph
3407
    fn __len__(&self) -> PyResult<usize> {
208✔
3408
        Ok(self.graph.node_count())
208✔
3409
    }
208✔
3410

3411
    fn __getitem__(&self, idx: usize) -> PyResult<&PyObject> {
52✔
3412
        match self.graph.node_weight(NodeIndex::new(idx)) {
52✔
3413
            Some(data) => Ok(data),
50✔
3414
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3415
        }
3416
    }
52✔
3417

3418
    fn __setitem__(&mut self, idx: usize, value: PyObject) -> PyResult<()> {
3,770✔
3419
        let data = match self.graph.node_weight_mut(NodeIndex::new(idx)) {
3,770✔
3420
            Some(node_data) => node_data,
3,768✔
3421
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
3422
        };
3423
        *data = value;
3,768✔
3424
        Ok(())
3,768✔
3425
    }
3,770✔
3426

3427
    fn __delitem__(&mut self, idx: usize) -> PyResult<()> {
4✔
3428
        match self.graph.remove_node(NodeIndex::new(idx)) {
4✔
3429
            Some(_) => {
3430
                self.node_removed = true;
2✔
3431
                Ok(())
2✔
3432
            }
3433
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
3434
        }
3435
    }
4✔
3436

3437
    #[classmethod]
3438
    #[pyo3(signature = (key, /))]
3439
    pub fn __class_getitem__(
4✔
3440
        cls: &Bound<'_, PyType>,
4✔
3441
        key: &Bound<'_, PyAny>,
4✔
3442
    ) -> PyResult<PyObject> {
4✔
3443
        generic_class_getitem(cls, key)
4✔
3444
    }
4✔
3445

3446
    // Functions to enable Python Garbage Collection
3447

3448
    // Function for PyTypeObject.tp_traverse [1][2] used to tell Python what
3449
    // objects the PyDiGraph has strong references to.
3450
    //
3451
    // [1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
3452
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3453
    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
132✔
3454
        for node in self
14,720,776✔
3455
            .graph
132✔
3456
            .node_indices()
132✔
3457
            .map(|node| self.graph.node_weight(node).unwrap())
14,720,776✔
3458
        {
3459
            visit.call(node)?;
14,720,776✔
3460
        }
3461
        for edge in self
1,358,104✔
3462
            .graph
132✔
3463
            .edge_indices()
132✔
3464
            .map(|edge| self.graph.edge_weight(edge).unwrap())
1,358,104✔
3465
        {
3466
            visit.call(edge)?;
1,358,104✔
3467
        }
3468
        visit.call(&self.attrs)?;
132✔
3469
        Ok(())
132✔
3470
    }
132✔
3471

3472
    // Function for PyTypeObject.tp_clear [1][2] used to tell Python's GC how
3473
    // to drop all references held by a PyDiGraph object when the GC needs to
3474
    // break reference cycles.
3475
    //
3476
    // ]1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_clear
3477
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
3478
    fn __clear__(&mut self, py: Python) {
×
3479
        self.graph = StablePyGraph::<Directed>::new();
×
3480
        self.node_removed = false;
×
3481
        self.attrs = py.None();
×
3482
    }
×
3483
}
3484

3485
fn is_cycle_check_required(dag: &PyDiGraph, a: NodeIndex, b: NodeIndex) -> bool {
26✔
3486
    let mut parents_a = dag
26✔
3487
        .graph
26✔
3488
        .neighbors_directed(a, petgraph::Direction::Incoming);
26✔
3489
    let mut children_b = dag
26✔
3490
        .graph
26✔
3491
        .neighbors_directed(b, petgraph::Direction::Outgoing);
26✔
3492
    parents_a.next().is_some() && children_b.next().is_some() && dag.graph.find_edge(a, b).is_none()
26✔
3493
}
26✔
3494

3495
fn weight_transform_callable(
58✔
3496
    py: Python,
58✔
3497
    map_fn: &Option<PyObject>,
58✔
3498
    value: &PyObject,
58✔
3499
) -> PyResult<PyObject> {
58✔
3500
    match map_fn {
58✔
3501
        Some(map_fn) => {
10✔
3502
            let res = map_fn.call1(py, (value,))?;
10✔
3503
            res.into_py_any(py)
10✔
3504
        }
3505
        None => Ok(value.clone_ref(py)),
48✔
3506
    }
3507
}
58✔
3508

3509
fn _from_adjacency_matrix<'p, T>(
26✔
3510
    py: Python<'p>,
26✔
3511
    matrix: PyReadonlyArray2<'p, T>,
26✔
3512
    null_value: T,
26✔
3513
) -> PyResult<PyDiGraph>
26✔
3514
where
26✔
3515
    T: Copy + std::cmp::PartialEq + numpy::Element + pyo3::IntoPyObject<'p> + IsNan,
26✔
3516
{
26✔
3517
    let array = matrix.as_array();
26✔
3518
    let shape = array.shape();
26✔
3519
    let mut out_graph = StablePyGraph::<Directed>::new();
26✔
3520
    let _node_indices: Vec<NodeIndex> = (0..shape[0])
26✔
3521
        .map(|node| Ok(out_graph.add_node(node.into_py_any(py)?)))
272✔
3522
        .collect::<PyResult<Vec<NodeIndex>>>()?;
26✔
3523
    for (index, row) in array.axis_iter(Axis(0)).enumerate() {
272✔
3524
        let source_index = NodeIndex::new(index);
272✔
3525
        for (target_index, elem) in row.iter().enumerate() {
20,216✔
3526
            if null_value.is_nan() {
20,216✔
3527
                if !elem.is_nan() {
36✔
3528
                    out_graph.add_edge(
16✔
3529
                        source_index,
16✔
3530
                        NodeIndex::new(target_index),
16✔
3531
                        elem.into_py_any(py)?,
16✔
3532
                    );
3533
                }
20✔
3534
            } else if *elem != null_value {
20,180✔
3535
                out_graph.add_edge(
18,882✔
3536
                    source_index,
18,882✔
3537
                    NodeIndex::new(target_index),
18,882✔
3538
                    elem.into_py_any(py)?,
18,882✔
3539
                );
3540
            }
1,298✔
3541
        }
3542
    }
3543
    Ok(PyDiGraph {
26✔
3544
        graph: out_graph,
26✔
3545
        cycle_state: algo::DfsSpace::default(),
26✔
3546
        check_cycle: false,
26✔
3547
        node_removed: false,
26✔
3548
        multigraph: true,
26✔
3549
        attrs: py.None(),
26✔
3550
    })
26✔
3551
}
26✔
3552

3553
/// Simple wrapper newtype that lets us use `Py` pointers as hash keys with the equality defined by
3554
/// the pointer address.  This is equivalent to using Python's `is` operator for comparisons.
3555
/// Using a newtype rather than casting the pointer to `usize` inline lets us retrieve a copy of
3556
/// the reference from the key entry.
3557
struct PyAnyId(Py<PyAny>);
3558
impl PyAnyId {
3559
    fn clone_ref(&self, py: Python) -> Py<PyAny> {
30✔
3560
        self.0.clone_ref(py)
30✔
3561
    }
30✔
3562
}
3563
impl ::std::hash::Hash for PyAnyId {
3564
    fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
86✔
3565
        (self.0.as_ptr() as usize).hash(state)
86✔
3566
    }
86✔
3567
}
3568
impl PartialEq for PyAnyId {
3569
    fn eq(&self, other: &Self) -> bool {
28✔
3570
        self.0.as_ptr() == other.0.as_ptr()
28✔
3571
    }
28✔
3572
}
3573
impl Eq for PyAnyId {}
3574

3575
/// Internal-only helper class used by `remove_node_retain_edges_by_key` to store its data as a
3576
/// typed object in a Python dictionary.
3577
#[pyclass]
24✔
3578
struct RemoveNodeEdgeValue {
3579
    weight: Py<PyAny>,
3580
    nodes: Vec<NodeIndex>,
3581
}
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