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

Qiskit / rustworkx / 18563991601

16 Oct 2025 02:04PM UTC coverage: 94.108% (-0.05%) from 94.156%
18563991601

Pull #1517

github

web-flow
Merge 7d1e5168e into 8d2742d6b
Pull Request #1517: Bugfix/json preserves node gaps

57 of 71 new or added lines in 4 files covered. (80.28%)

2 existing lines in 2 files now uncovered.

18177 of 19315 relevant lines covered (94.11%)

965694.95 hits per line

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

97.05
/src/graph.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::collections::BTreeMap;
17
use std::fs::File;
18
use std::io::prelude::*;
19
use std::io::{BufReader, BufWriter};
20
use std::str;
21

22
use hashbrown::{HashMap, HashSet};
23
use rustworkx_core::dictmap::*;
24
use rustworkx_core::graph_ext::*;
25

26
use pyo3::exceptions::PyIndexError;
27
use pyo3::gc::PyVisit;
28
use pyo3::prelude::*;
29
use pyo3::types::{IntoPyDict, PyBool, PyDict, PyGenericAlias, PyList, PyString, PyTuple, PyType};
30
use pyo3::IntoPyObjectExt;
31
use pyo3::PyTraverseError;
32
use pyo3::Python;
33

34
use ndarray::prelude::*;
35
use num_traits::Zero;
36
use numpy::Complex64;
37
use numpy::PyReadonlyArray2;
38

39
use crate::iterators::NodeMap;
40

41
use super::dot_utils::build_dot;
42
use super::iterators::{EdgeIndexMap, EdgeIndices, EdgeList, NodeIndices, WeightedEdgeList};
43
use super::{
44
    find_node_by_weight, weight_callable, IsNan, NoEdgeBetweenNodes, NodesRemoved, StablePyGraph,
45
};
46

47
use crate::RxPyResult;
48
use petgraph::algo;
49
use petgraph::graph::{EdgeIndex, NodeIndex};
50
use petgraph::prelude::*;
51
use petgraph::visit::{
52
    EdgeIndexable, GraphBase, IntoEdgeReferences, IntoNodeReferences, NodeCount, NodeFiltered,
53
    NodeIndexable,
54
};
55

56
/// A class for creating undirected graphs
57
///
58
/// The PyGraph class is used to create an undirected graph. It can be a
59
/// multigraph (have multiple edges between nodes). Each node and edge
60
/// (although rarely used for edges) is indexed by an integer id. These ids
61
/// are stable for the lifetime of the graph object and on node or edge
62
/// deletions you can have holes in the list of indices for the graph.
63
/// Node indices will be reused on additions after removal. For example:
64
///
65
/// .. jupyter-execute::
66
///
67
///        import rustworkx as rx
68
///
69
///        graph = rx.PyGraph()
70
///        graph.add_nodes_from(list(range(5)))
71
///        graph.add_nodes_from(list(range(2)))
72
///        graph.remove_node(2)
73
///        print("After deletion:", graph.node_indices())
74
///        res_manual = graph.add_node(None)
75
///        print("After adding a new node:", graph.node_indices())
76
///
77
/// Additionally, each node and edge contains an arbitrary Python object as a
78
/// weight/data payload. You can use the index for access to the data payload
79
/// as in the following example:
80
///
81
/// .. jupyter-execute::
82
///
83
///     import rustworkx as rx
84
///
85
///     graph = rx.PyGraph()
86
///     data_payload = "An arbitrary Python object"
87
///     node_index = graph.add_node(data_payload)
88
///     print("Node Index: %s" % node_index)
89
///     print(graph[node_index])
90
///
91
/// The PyGraph implements the Python mapping protocol for nodes so in
92
/// addition to access you can also update the data payload with:
93
///
94
/// .. jupyter-execute::
95
///
96
///     import rustworkx as rx
97
///
98
///     graph = rx.PyGraph()
99
///     data_payload = "An arbitrary Python object"
100
///     node_index = graph.add_node(data_payload)
101
///     graph[node_index] = "New Payload"
102
///     print("Node Index: %s" % node_index)
103
///     print(graph[node_index])
104
///
105
/// By default a ``PyGraph`` is a multigraph (meaning there can be parallel
106
/// edges between nodes) however this can be disabled by setting the
107
/// ``multigraph`` kwarg to ``False`` when calling the ``PyGraph``
108
/// constructor. For example::
109
///
110
///     import rustworkx as rx
111
///     graph = rx.PyGraph(multigraph=False)
112
///
113
/// This can only be set at ``PyGraph`` initialization and not adjusted after
114
/// creation. When :attr:`~rustworkx.PyGraph.multigraph` is set to ``False``
115
/// if a method call is made that would add a parallel edge it will instead
116
/// update the existing edge's weight/data payload.
117
///
118
/// Each ``PyGraph`` object has an :attr:`~.PyGraph.attrs` attribute which is
119
/// used to contain additional attributes/metadata of the graph instance. By
120
/// default this is set to ``None`` but can optionally be specified by using the
121
/// ``attrs`` keyword argument when constructing a new graph::
122
///
123
///     graph = rustworkx.PyGraph(attrs=dict(source_path='/tmp/graph.csv'))
124
///
125
/// This attribute can be set to any Python object. Additionally, you can access
126
/// and modify this attribute after creating an object. For example::
127
///
128
///     source_path = graph.attrs
129
///     graph.attrs = {'new_path': '/tmp/new.csv', 'old_path': source_path}
130
///
131
/// The maximum number of nodes and edges allowed on a ``PyGraph`` object is
132
/// :math:`2^{32} - 1` (4,294,967,294) each. Attempting to add more nodes or
133
/// edges than this will result in an exception being raised.
134
///
135
/// :param bool multigraph: When this is set to ``False`` the created PyGraph
136
///     object will not be a multigraph. When ``False`` if a method call is
137
///     made that would add parallel edges the weight/weight from that
138
///     method call will be used to update the existing edge in place.
139
/// :param Any attrs: An optional attributes payload to assign to the
140
///     :attr:`~.PyGraph.attrs` attribute. This can be any Python object. If
141
///     it is not specified :attr:`~.PyGraph.attrs` will be set to ``None``.
142
/// :param int node_count_hint: An optional hint that will allocate enough capacity to store this
143
///     many nodes before needing to grow. This does not prepopulate any nodes with data, it is
144
///     only a potential performance optimization if the complete size of the graph is known in
145
///     advance.
146
/// :param int edge_count_hint: An optional hint that will allocate enough capacity to store this
147
///     many edges before needing to grow.  This does not prepopulate any edges with data, it is
148
///     only a potential performance optimization if the complete size of the graph is known in
149
///     advance.
150
#[pyclass(mapping, module = "rustworkx", subclass)]
151
#[derive(Clone)]
152
pub struct PyGraph {
153
    pub graph: StablePyGraph<Undirected>,
154
    pub node_removed: bool,
155
    pub multigraph: bool,
156
    #[pyo3(get, set)]
157
    pub attrs: PyObject,
158
}
159

160
impl GraphBase for PyGraph {
161
    type NodeId = NodeIndex;
162
    type EdgeId = EdgeIndex;
163
}
164

165
impl NodesRemoved for &PyGraph {
166
    fn nodes_removed(&self) -> bool {
×
167
        self.node_removed
×
168
    }
×
169
}
170

171
impl NodeCount for PyGraph {
172
    fn node_count(&self) -> usize {
4,102✔
173
        self.graph.node_count()
4,102✔
174
    }
4,102✔
175
}
176

177
impl PyGraph {
178
    fn _add_edge(&mut self, u: NodeIndex, v: NodeIndex, edge: PyObject) -> usize {
8,078✔
179
        if !self.multigraph {
8,078✔
180
            let exists = self.graph.find_edge(u, v);
2,410✔
181
            if let Some(index) = exists {
2,410✔
182
                let edge_weight = self.graph.edge_weight_mut(index).unwrap();
32✔
183
                *edge_weight = edge;
32✔
184
                return index.index();
32✔
185
            }
2,378✔
186
        }
5,668✔
187
        let edge = self.graph.add_edge(u, v, edge);
8,046✔
188
        edge.index()
8,046✔
189
    }
8,078✔
190
}
191

192
#[pymethods]
×
193
impl PyGraph {
194
    #[new]
195
    #[pyo3(signature=(multigraph=true, attrs=None, *, node_count_hint=None, edge_count_hint=None))]
196
    fn new(
1,550✔
197
        py: Python,
1,550✔
198
        multigraph: bool,
1,550✔
199
        attrs: Option<PyObject>,
1,550✔
200
        node_count_hint: Option<usize>,
1,550✔
201
        edge_count_hint: Option<usize>,
1,550✔
202
    ) -> Self {
1,550✔
203
        PyGraph {
204
            graph: StablePyGraph::<Undirected>::with_capacity(
1,550✔
205
                node_count_hint.unwrap_or_default(),
1,550✔
206
                edge_count_hint.unwrap_or_default(),
1,550✔
207
            ),
208
            node_removed: false,
209
            multigraph,
1,550✔
210
            attrs: attrs.unwrap_or_else(|| py.None()),
1,550✔
211
        }
212
    }
1,550✔
213

214
    fn __getnewargs_ex__<'py>(
18✔
215
        &self,
18✔
216
        py: Python<'py>,
18✔
217
    ) -> PyResult<(Bound<'py, PyTuple>, Bound<'py, PyDict>)> {
18✔
218
        Ok((
219
            (self.multigraph, self.attrs.clone_ref(py)).into_pyobject(py)?,
18✔
220
            [
18✔
221
                ("node_count_hint", self.graph.node_bound()),
18✔
222
                ("edge_count_hint", self.graph.edge_bound()),
18✔
223
            ]
18✔
224
            .into_py_dict(py)?,
18✔
225
        ))
226
    }
18✔
227

228
    fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
18✔
229
        let mut nodes: Vec<PyObject> = Vec::with_capacity(self.graph.node_bound());
18✔
230
        let mut edges: Vec<PyObject> = Vec::with_capacity(self.graph.edge_bound());
18✔
231

232
        // save nodes to a list along with its index
233
        for node_idx in self.graph.node_indices() {
62✔
234
            let node_data = self.graph.node_weight(node_idx).unwrap();
62✔
235
            nodes.push((node_idx.index(), node_data).into_py_any(py)?);
62✔
236
        }
237

238
        // edges are saved with none (deleted edges) instead of their index to save space
239
        for i in 0..self.graph.edge_bound() {
56✔
240
            let idx = EdgeIndex::new(i);
56✔
241
            let edge = match self.graph.edge_weight(idx) {
56✔
242
                Some(edge_w) => {
48✔
243
                    let endpoints = self.graph.edge_endpoints(idx).unwrap();
48✔
244
                    (endpoints.0.index(), endpoints.1.index(), edge_w).into_py_any(py)?
48✔
245
                }
246
                None => py.None(),
8✔
247
            };
248
            edges.push(edge);
56✔
249
        }
250

251
        let out_dict = PyDict::new(py);
18✔
252
        let nodes_lst: PyObject = PyList::new(py, nodes)?.into_any().unbind();
18✔
253
        let edges_lst: PyObject = PyList::new(py, edges)?.into_any().unbind();
18✔
254
        out_dict.set_item("nodes", nodes_lst)?;
18✔
255
        out_dict.set_item("edges", edges_lst)?;
18✔
256
        out_dict.set_item("nodes_removed", self.node_removed)?;
18✔
257
        Ok(out_dict.into())
18✔
258
    }
18✔
259

260
    fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> {
18✔
261
        let dict_state = state.downcast_bound::<PyDict>(py)?;
18✔
262
        let binding = dict_state.get_item("nodes")?.unwrap();
18✔
263
        let nodes_lst = binding.downcast::<PyList>()?;
18✔
264
        let binding = dict_state.get_item("edges")?.unwrap();
18✔
265
        let edges_lst = binding.downcast::<PyList>()?;
18✔
266

267
        self.node_removed = dict_state
18✔
268
            .get_item("nodes_removed")?
18✔
269
            .unwrap()
18✔
270
            .downcast::<PyBool>()?
18✔
271
            .extract()?;
18✔
272
        // graph is empty, stop early
273
        if nodes_lst.is_empty() {
18✔
274
            return Ok(());
4✔
275
        }
14✔
276

277
        if !self.node_removed {
14✔
278
            for item in nodes_lst.iter() {
6✔
279
                let node_w = item
6✔
280
                    .downcast::<PyTuple>()
6✔
281
                    .unwrap()
6✔
282
                    .get_item(1)
6✔
283
                    .unwrap()
6✔
284
                    .extract()
6✔
285
                    .unwrap();
6✔
286
                self.graph.add_node(node_w);
6✔
287
            }
6✔
288
        } else if nodes_lst.len() == 1 {
12✔
289
            // graph has only one node, handle logic here to save one if in the loop later
290
            let binding = nodes_lst.get_item(0).unwrap();
×
291
            let item = binding.downcast::<PyTuple>().unwrap();
×
292
            let node_idx: usize = item.get_item(0).unwrap().extract().unwrap();
×
293
            let node_w = item.get_item(1).unwrap().extract().unwrap();
×
294

295
            for _i in 0..node_idx {
×
296
                self.graph.add_node(py.None());
×
297
            }
×
298
            self.graph.add_node(node_w);
×
299
            for i in 0..node_idx {
×
300
                self.graph.remove_node(NodeIndex::new(i));
×
301
            }
×
302
        } else {
303
            let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap();
12✔
304
            let last_item = binding.downcast::<PyTuple>().unwrap();
12✔
305

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

311
            for item in nodes_lst {
68✔
312
                let item = item.downcast::<PyTuple>().unwrap();
56✔
313
                let next_index: usize = item.get_item(0).unwrap().extract().unwrap();
56✔
314
                let weight: PyObject = item.get_item(1).unwrap().extract().unwrap();
56✔
315
                while next_index > self.graph.node_bound() {
80✔
316
                    // node does not exist
24✔
317
                    let tmp_node = self.graph.add_node(py.None());
24✔
318
                    tmp_nodes.push(tmp_node);
24✔
319
                }
24✔
320
                // add node to the graph, and update the next available node index
321
                self.graph.add_node(weight);
56✔
322
            }
323
            // Remove any temporary nodes we added
324
            for tmp_node in tmp_nodes {
36✔
325
                self.graph.remove_node(tmp_node);
24✔
326
            }
24✔
327
        }
328

329
        // to ensure O(1) on edge deletion, use a temporary node to store missing edges
330
        let tmp_node = self.graph.add_node(py.None());
14✔
331

332
        for item in edges_lst {
70✔
333
            if item.is_none() {
56✔
334
                // add a temporary edge that will be deleted later to re-create the hole
8✔
335
                self.graph.add_edge(tmp_node, tmp_node, py.None());
8✔
336
            } else {
48✔
337
                let triple = item.downcast::<PyTuple>().unwrap();
48✔
338
                let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap();
48✔
339
                let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap();
48✔
340
                let edge_w = triple.get_item(2).unwrap().extract().unwrap();
48✔
341
                self.graph
48✔
342
                    .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w);
48✔
343
            }
48✔
344
        }
345

346
        // remove the temporary node will remove all deleted edges in bulk,
347
        // the cost is equal to the number of edges
348
        self.graph.remove_node(tmp_node);
14✔
349

350
        Ok(())
14✔
351
    }
18✔
352

353
    /// Whether the graph is a multigraph (allows multiple edges between
354
    /// nodes) or not
355
    ///
356
    /// If set to ``False`` multiple edges between nodes are not allowed and
357
    /// calls that would add a parallel edge will instead update the existing
358
    /// edge
359
    #[getter]
360
    fn multigraph(&self) -> bool {
24✔
361
        self.multigraph
24✔
362
    }
24✔
363

364
    /// Detect if the graph has parallel edges or not
365
    ///
366
    /// :returns: ``True`` if the graph has parallel edges, ``False`` otherwise
367
    /// :rtype: bool
368
    #[pyo3(text_signature = "(self)")]
369
    fn has_parallel_edges(&self) -> bool {
8✔
370
        if !self.multigraph {
8✔
371
            return false;
2✔
372
        }
6✔
373
        self.graph.has_parallel_edges()
6✔
374
    }
8✔
375

376
    /// Clears all nodes and edges
377
    #[pyo3(text_signature = "(self)")]
378
    pub fn clear(&mut self) {
4✔
379
        self.graph.clear();
4✔
380
        self.node_removed = true;
4✔
381
    }
4✔
382

383
    /// Clears all edges, leaves nodes intact
384
    #[pyo3(text_signature = "(self)")]
385
    pub fn clear_edges(&mut self) {
4✔
386
        self.graph.clear_edges();
4✔
387
    }
4✔
388

389
    /// Return the number of nodes in the graph
390
    #[pyo3(text_signature = "(self)")]
391
    pub fn num_nodes(&self) -> usize {
66✔
392
        self.graph.node_count()
66✔
393
    }
66✔
394

395
    /// Return the number of edges in the graph
396
    #[pyo3(text_signature = "(self)")]
397
    pub fn num_edges(&self) -> usize {
46✔
398
        self.graph.edge_count()
46✔
399
    }
46✔
400

401
    /// Return a list of all edge data.
402
    ///
403
    /// :returns: A list of all the edge data objects in the graph
404
    /// :rtype: list[T]
405
    #[pyo3(text_signature = "(self)")]
406
    pub fn edges(&self) -> Vec<&PyObject> {
256✔
407
        self.graph
256✔
408
            .edge_indices()
256✔
409
            .map(|edge| self.graph.edge_weight(edge).unwrap())
6,962✔
410
            .collect()
256✔
411
    }
256✔
412

413
    /// Return a list of all edge indices.
414
    ///
415
    /// :returns: A list of all the edge indices in the graph
416
    /// :rtype: EdgeIndices
417
    #[pyo3(text_signature = "(self)")]
418
    pub fn edge_indices(&self) -> EdgeIndices {
6✔
419
        EdgeIndices {
420
            edges: self.graph.edge_indices().map(|edge| edge.index()).collect(),
90✔
421
        }
422
    }
6✔
423

424
    /// Return a list of indices of all edges between specified nodes
425
    ///
426
    /// :param int node_a: The index of the first node
427
    /// :param int node_b: The index of the second node
428
    ///
429
    /// :returns: A list of all the edge indices connecting the specified start and end node
430
    /// :rtype: EdgeIndices
431
    pub fn edge_indices_from_endpoints(&self, node_a: usize, node_b: usize) -> EdgeIndices {
6✔
432
        let node_a_index = NodeIndex::new(node_a);
6✔
433
        let node_b_index = NodeIndex::new(node_b);
6✔
434

435
        EdgeIndices {
436
            edges: self
6✔
437
                .graph
6✔
438
                .edges_directed(node_a_index, petgraph::Direction::Outgoing)
6✔
439
                .filter(|edge| edge.target() == node_b_index)
30✔
440
                .map(|edge| edge.id().index())
8✔
441
                .collect(),
6✔
442
        }
443
    }
6✔
444

445
    /// Return a list of all node data.
446
    ///
447
    /// :returns: A list of all the node data objects in the graph
448
    /// :rtype: list[S]
449
    #[pyo3(text_signature = "(self)")]
450
    pub fn nodes(&self) -> Vec<&PyObject> {
1,094✔
451
        self.graph
1,094✔
452
            .node_indices()
1,094✔
453
            .map(|node| self.graph.node_weight(node).unwrap())
5,084✔
454
            .collect()
1,094✔
455
    }
1,094✔
456

457
    /// Return a list of all node indices.
458
    ///
459
    /// :returns: A list of all the node indices in the graph
460
    /// :rtype: NodeIndices
461
    #[pyo3(text_signature = "(self)")]
462
    pub fn node_indices(&self) -> NodeIndices {
234✔
463
        NodeIndices {
464
            nodes: self.graph.node_indices().map(|node| node.index()).collect(),
1,620✔
465
        }
466
    }
234✔
467

468
    /// Return a list of all node indices.
469
    ///
470
    /// .. note::
471
    ///
472
    ///     This is identical to :meth:`.node_indices()`, which is the
473
    ///     preferred method to get the node indices in the graph. This
474
    ///     exists for backwards compatibility with earlier releases.
475
    ///
476
    /// :returns: A list of all the node indices in the graph
477
    /// :rtype: NodeIndices
478
    #[pyo3(text_signature = "(self)")]
479
    pub fn node_indexes(&self) -> NodeIndices {
74✔
480
        self.node_indices()
74✔
481
    }
74✔
482

483
    /// Check if the node exists in the graph.
484
    ///
485
    /// :param int node: The index of the node
486
    ///
487
    /// :returns: ``True`` if the node exists, ``False`` otherwise
488
    /// :rtype: bool
489
    #[pyo3(text_signature = "(self, node, /)")]
490
    pub fn has_node(&self, node: usize) -> bool {
42✔
491
        let index = NodeIndex::new(node);
42✔
492
        self.graph.contains_node(index)
42✔
493
    }
42✔
494

495
    /// Check if there is any undirected edge between ``node_a`` and ``node_b``.
496
    ///
497
    /// :param int node_a: The index of the first node
498
    /// :param int node_b: The index of the second node
499
    ///
500
    /// :returns: ``True`` if the edge exists, ``False`` otherwise
501
    /// :rtype: bool
502
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
503
    pub fn has_edge(&self, node_a: usize, node_b: usize) -> bool {
228✔
504
        let index_a = NodeIndex::new(node_a);
228✔
505
        let index_b = NodeIndex::new(node_b);
228✔
506
        self.graph.find_edge(index_a, index_b).is_some()
228✔
507
    }
228✔
508

509
    ///  Return the edge data for the edge between 2 nodes.
510
    ///
511
    ///  Note if there are multiple edges between the nodes only one will be
512
    ///  returned. To get all edge data objects use
513
    ///  :meth:`~rustworkx.PyGraph.get_all_edge_data`
514
    ///
515
    /// :param int node_a: The index of the first node
516
    /// :param int node_b: The index of the second node
517
    ///
518
    /// :returns: The data object set for the edge
519
    /// :rtype: S
520
    /// :raises NoEdgeBetweenNodes: when there is no edge between the provided
521
    ///     nodes
522
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
523
    pub fn get_edge_data(&self, node_a: usize, node_b: usize) -> PyResult<&PyObject> {
70✔
524
        let index_a = NodeIndex::new(node_a);
70✔
525
        let index_b = NodeIndex::new(node_b);
70✔
526
        let edge_index = match self.graph.find_edge(index_a, index_b) {
70✔
527
            Some(edge_index) => edge_index,
66✔
528
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
4✔
529
        };
530

531
        let data = self.graph.edge_weight(edge_index).unwrap();
66✔
532
        Ok(data)
66✔
533
    }
70✔
534

535
    /// Return the list of edge indices incident to a provided node
536
    ///
537
    /// You can later retrieve the data payload of this edge with
538
    /// :meth:`~rustworkx.PyGraph.get_edge_data_by_index` or its
539
    /// endpoints with :meth:`~rustworkx.PyGraph.get_edge_endpoints_by_index`.
540
    ///
541
    /// :param int node: The node index to get incident edges from. If
542
    ///     this node index is not present in the graph this method will
543
    ///     return an empty list and not error.
544
    ///
545
    /// :returns: A list of the edge indices incident to a node in the graph
546
    /// :rtype: EdgeIndices
547
    #[pyo3(text_signature = "(self, node, /)")]
548
    pub fn incident_edges(&self, node: usize) -> EdgeIndices {
4✔
549
        EdgeIndices {
550
            edges: self
4✔
551
                .graph
4✔
552
                .edges(NodeIndex::new(node))
4✔
553
                .map(|e| e.id().index())
4✔
554
                .collect(),
4✔
555
        }
556
    }
4✔
557

558
    /// Return the list of edge indices incident to a provided node.
559
    ///
560
    /// This method returns the indices of all edges connected to the provided
561
    /// ``node``. In undirected graphs, all edges connected to the node are
562
    /// returned as there is no distinction between incoming and outgoing edges.
563
    ///
564
    /// :param int node: The node index to get incident edges from. If
565
    ///     this node index is not present in the graph this method will
566
    ///     return an empty list and not error.
567
    ///
568
    /// :returns: A list of the edge indices incident to the node
569
    /// :rtype: EdgeIndices
570
    #[pyo3(text_signature = "(self, node, /)")]
571
    pub fn in_edge_indices(&self, node: usize) -> EdgeIndices {
4✔
572
        EdgeIndices {
573
            edges: self
4✔
574
                .graph
4✔
575
                .edges(NodeIndex::new(node))
4✔
576
                .map(|e| e.id().index())
4✔
577
                .collect(),
4✔
578
        }
579
    }
4✔
580

581
    /// Return the list of edge indices incident to a provided node.
582
    ///
583
    /// This method returns the indices of all edges connected to the provided
584
    /// ``node``. In undirected graphs, all edges connected to the node are
585
    /// returned as there is no distinction between incoming and outgoing edges.
586
    ///
587
    /// :param int node: The node index to get incident edges from. If
588
    ///     this node index is not present in the graph this method will
589
    ///     return an empty list and not error.
590
    ///
591
    /// :returns: A list of the edge indices incident to the node
592
    /// :rtype: EdgeIndices
593
    #[pyo3(text_signature = "(self, node, /)")]
594
    pub fn out_edge_indices(&self, node: usize) -> EdgeIndices {
4✔
595
        EdgeIndices {
596
            edges: self
4✔
597
                .graph
4✔
598
                .edges(NodeIndex::new(node))
4✔
599
                .map(|e| e.id().index())
4✔
600
                .collect(),
4✔
601
        }
602
    }
4✔
603

604
    /// Return the index map of edges incident to a provided node
605
    ///
606
    /// :param int node: The node index to get incident edges from. If
607
    ///     this node index is not present in the graph this method will
608
    ///     return an empty mapping and not error.
609
    ///
610
    /// :returns: A mapping of incident edge indices to the tuple
611
    ///     ``(source, target, data)``. The source endpoint node index in
612
    ///     this tuple will always be ``node`` to ensure you can easily
613
    ///     identify that the neighbor node is the second element in the
614
    ///     tuple for a given edge index.
615
    /// :rtype: EdgeIndexMap
616
    #[pyo3(text_signature = "(self, node, /)")]
617
    pub fn incident_edge_index_map(&self, py: Python, node: usize) -> EdgeIndexMap {
4✔
618
        let node_index = NodeIndex::new(node);
4✔
619
        EdgeIndexMap {
620
            edge_map: self
4✔
621
                .graph
4✔
622
                .edges_directed(node_index, petgraph::Direction::Outgoing)
4✔
623
                .map(|edge| {
4✔
624
                    (
4✔
625
                        edge.id().index(),
4✔
626
                        (
4✔
627
                            edge.source().index(),
4✔
628
                            edge.target().index(),
4✔
629
                            edge.weight().clone_ref(py),
4✔
630
                        ),
4✔
631
                    )
4✔
632
                })
4✔
633
                .collect(),
4✔
634
        }
635
    }
4✔
636

637
    /// Get the endpoint indices and edge data for all edges of a node.
638
    ///
639
    /// This will return a list of tuples with the parent index, the node index
640
    /// and the edge data. This can be used to recreate add_edge() calls. As
641
    /// :class:`~rustworkx.PyGraph` is undirected this will return all edges
642
    /// with the second endpoint node index always being ``node``.
643
    ///
644
    /// :param int node: The index of the node to get the edges for
645
    ///
646
    /// :returns: A list of tuples of the form:
647
    ///     ``(parent_index, node_index, edge_data)```
648
    /// :rtype: WeightedEdgeList
649
    #[pyo3(text_signature = "(self, node, /)")]
650
    pub fn in_edges(&self, py: Python, node: usize) -> WeightedEdgeList {
2✔
651
        let index = NodeIndex::new(node);
2✔
652
        let dir = petgraph::Direction::Incoming;
2✔
653
        let raw_edges = self.graph.edges_directed(index, dir);
2✔
654
        let out_list: Vec<(usize, usize, PyObject)> = raw_edges
2✔
655
            .map(|x| (x.source().index(), node, x.weight().clone_ref(py)))
4✔
656
            .collect();
2✔
657
        WeightedEdgeList { edges: out_list }
2✔
658
    }
2✔
659

660
    /// Get the endpoint indices and edge data for all edges of a node.
661
    ///
662
    /// This will return a list of tuples with the child index, the node index
663
    /// and the edge data. This can be used to recreate add_edge() calls. As
664
    /// :class:`~rustworkx.PyGraph` is undirected this will return all edges
665
    /// with the first endpoint node index always being ``node``.
666
    ///
667
    /// :param int node: The index of the node to get the edges for
668
    ///
669
    /// :returns out_edges: A list of tuples of the form:
670
    ///     ```(node_index, child_index, edge_data)```
671
    /// :rtype: WeightedEdgeList
672
    #[pyo3(text_signature = "(self, node, /)")]
673
    pub fn out_edges(&self, py: Python, node: usize) -> WeightedEdgeList {
4✔
674
        let index = NodeIndex::new(node);
4✔
675
        let dir = petgraph::Direction::Outgoing;
4✔
676
        let raw_edges = self.graph.edges_directed(index, dir);
4✔
677
        let out_list: Vec<(usize, usize, PyObject)> = raw_edges
4✔
678
            .map(|x| (node, x.target().index(), x.weight().clone_ref(py)))
8✔
679
            .collect();
4✔
680
        WeightedEdgeList { edges: out_list }
4✔
681
    }
4✔
682

683
    /// Return the edge data for the edge by its given index
684
    ///
685
    /// :param int edge_index: The edge index to get the data for
686
    ///
687
    /// :returns: The data object for the edge
688
    /// :rtype: T
689
    /// :raises IndexError: when there is no edge present with the provided
690
    ///     index
691
    #[pyo3(text_signature = "(self, edge_index, /)")]
692
    pub fn get_edge_data_by_index(&self, edge_index: usize) -> PyResult<&PyObject> {
4✔
693
        let data = match self.graph.edge_weight(EdgeIndex::new(edge_index)) {
4✔
694
            Some(data) => data,
2✔
695
            None => {
696
                return Err(PyIndexError::new_err(format!(
2✔
697
                    "Provided edge index {edge_index} is not present in the graph"
2✔
698
                )));
2✔
699
            }
700
        };
701
        Ok(data)
2✔
702
    }
4✔
703

704
    /// Return the edge endpoints for the edge by its given index
705
    ///
706
    /// :param int edge_index: The edge index to get the endpoints for
707
    ///
708
    /// :returns: The endpoint tuple for the edge
709
    /// :rtype: tuple[int, int]
710
    /// :raises IndexError: when there is no edge present with the provided
711
    ///     index
712
    #[pyo3(text_signature = "(self, edge_index, /)")]
713
    pub fn get_edge_endpoints_by_index(&self, edge_index: usize) -> PyResult<(usize, usize)> {
4✔
714
        let endpoints = match self.graph.edge_endpoints(EdgeIndex::new(edge_index)) {
4✔
715
            Some(endpoints) => (endpoints.0.index(), endpoints.1.index()),
2✔
716
            None => {
717
                return Err(PyIndexError::new_err(format!(
2✔
718
                    "Provided edge index {edge_index} is not present in the graph"
2✔
719
                )));
2✔
720
            }
721
        };
722
        Ok(endpoints)
2✔
723
    }
4✔
724

725
    /// Update an edge's weight/payload in place
726
    ///
727
    /// If there are parallel edges in the graph only one edge will be updated.
728
    /// if you need to update a specific edge or need to ensure all parallel
729
    /// edges get updated you should use
730
    /// :meth:`~rustworkx.PyGraph.update_edge_by_index` instead.
731
    ///
732
    /// :param int source: The index of the first node
733
    /// :param int target: The index of the second node
734
    ///
735
    /// :raises NoEdgeBetweenNodes: When there is no edge between nodes
736
    #[pyo3(text_signature = "(self, source, target, edge, /)")]
737
    pub fn update_edge(&mut self, source: usize, target: usize, edge: PyObject) -> PyResult<()> {
277,750✔
738
        let index_a = NodeIndex::new(source);
277,750✔
739
        let index_b = NodeIndex::new(target);
277,750✔
740
        let edge_index = match self.graph.find_edge(index_a, index_b) {
277,750✔
741
            Some(edge_index) => edge_index,
277,748✔
742
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
2✔
743
        };
744
        let data = self.graph.edge_weight_mut(edge_index).unwrap();
277,748✔
745
        *data = edge;
277,748✔
746
        Ok(())
277,748✔
747
    }
277,750✔
748

749
    /// Update an edge's weight/data payload in place by the edge index
750
    ///
751
    /// :param int edge_index: The index of the edge
752
    /// :param T edge: The python object to attach to the edge
753
    ///
754
    /// :raises IndexError: when there is no edge present with the provided
755
    ///     index
756
    #[pyo3(text_signature = "(self, edge_index, edge, /)")]
757
    pub fn update_edge_by_index(&mut self, edge_index: usize, edge: PyObject) -> PyResult<()> {
22✔
758
        match self.graph.edge_weight_mut(EdgeIndex::new(edge_index)) {
22✔
759
            Some(data) => *data = edge,
20✔
760
            None => return Err(PyIndexError::new_err("No edge found for index")),
2✔
761
        };
762
        Ok(())
20✔
763
    }
22✔
764

765
    /// Return the node data for a given node index
766
    ///
767
    /// :param int node: The index of the node
768
    ///
769
    /// :returns: The data object set for that node
770
    /// :rtype: S
771
    /// :raises IndexError: when an invalid node index is provided
772
    #[pyo3(text_signature = "(self, node, /)")]
773
    pub fn get_node_data(&self, node: usize) -> PyResult<&PyObject> {
64✔
774
        let index = NodeIndex::new(node);
64✔
775
        let node = match self.graph.node_weight(index) {
64✔
776
            Some(node) => node,
62✔
777
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
778
        };
779
        Ok(node)
62✔
780
    }
64✔
781

782
    /// Return the edge data for all the edges between 2 nodes.
783
    ///
784
    /// :param int node_a: The index of the first node
785
    /// :param int node_b: The index of the second node
786
    ///
787
    /// :returns: A list with all the data objects for the edges between nodes
788
    /// :rtype: list[T]
789
    /// :raises NoEdgeBetweenNodes: When there is no edge between nodes
790
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
791
    pub fn get_all_edge_data(&self, node_a: usize, node_b: usize) -> PyResult<Vec<&PyObject>> {
20✔
792
        let index_a = NodeIndex::new(node_a);
20✔
793
        let index_b = NodeIndex::new(node_b);
20✔
794
        let out: Vec<&PyObject> = self
20✔
795
            .graph
20✔
796
            .edges(index_a)
20✔
797
            .filter(|edge| edge.target() == index_b)
20✔
798
            .map(|edge| edge.weight())
20✔
799
            .collect();
20✔
800
        if out.is_empty() {
20✔
801
            Err(NoEdgeBetweenNodes::new_err("No edge found between nodes"))
4✔
802
        } else {
803
            Ok(out)
16✔
804
        }
805
    }
20✔
806

807
    /// Get edge list
808
    ///
809
    /// Returns a list of tuples of the form ``(source, target)`` where
810
    /// ``source`` and ``target`` are the node indices.
811
    ///
812
    /// :returns: An edge list without weights
813
    /// :rtype: EdgeList
814
    #[pyo3(text_signature = "(self)")]
815
    pub fn edge_list(&self) -> EdgeList {
10,416✔
816
        EdgeList {
817
            edges: self
10,416✔
818
                .graph
10,416✔
819
                .edge_references()
10,416✔
820
                .map(|edge| (edge.source().index(), edge.target().index()))
349,218✔
821
                .collect(),
10,416✔
822
        }
823
    }
10,416✔
824

825
    /// Get edge list with weights
826
    ///
827
    /// Returns a list of tuples of the form ``(source, target, weight)`` where
828
    /// ``source`` and ``target`` are the node indices and ``weight`` is the
829
    /// payload of the edge.
830
    ///
831
    /// :returns: An edge list with weights
832
    /// :rtype: WeightedEdgeList
833
    #[pyo3(text_signature = "(self)")]
834
    pub fn weighted_edge_list(&self, py: Python) -> WeightedEdgeList {
8,340✔
835
        WeightedEdgeList {
836
            edges: self
8,340✔
837
                .graph
8,340✔
838
                .edge_references()
8,340✔
839
                .map(|edge| {
277,802✔
840
                    (
277,802✔
841
                        edge.source().index(),
277,802✔
842
                        edge.target().index(),
277,802✔
843
                        edge.weight().clone_ref(py),
277,802✔
844
                    )
277,802✔
845
                })
277,802✔
846
                .collect(),
8,340✔
847
        }
848
    }
8,340✔
849

850
    /// Get an edge index map
851
    ///
852
    /// Returns a read only mapping from edge indices to the weighted edge
853
    /// tuple in the form: ``{0: (0, 1, "weight"), 1: (2, 3, 2.3)}``
854
    ///
855
    /// :returns: An edge index map
856
    /// :rtype: EdgeIndexMap
857
    #[pyo3(text_signature = "(self)")]
858
    pub fn edge_index_map(&self, py: Python) -> EdgeIndexMap {
20✔
859
        EdgeIndexMap {
860
            edge_map: self
20✔
861
                .graph
20✔
862
                .edge_references()
20✔
863
                .map(|edge| {
46✔
864
                    (
46✔
865
                        edge.id().index(),
46✔
866
                        (
46✔
867
                            edge.source().index(),
46✔
868
                            edge.target().index(),
46✔
869
                            edge.weight().clone_ref(py),
46✔
870
                        ),
46✔
871
                    )
46✔
872
                })
46✔
873
                .collect(),
20✔
874
        }
875
    }
20✔
876

877
    /// Remove a node from the graph.
878
    ///
879
    /// :param int node: The index of the node to remove. If the index is not
880
    ///     present in the graph it will be ignored and this function will
881
    ///     have no effect.
882
    #[pyo3(text_signature = "(self, node, /)")]
883
    pub fn remove_node(&mut self, node: usize) -> PyResult<()> {
174✔
884
        let index = NodeIndex::new(node);
174✔
885
        self.graph.remove_node(index);
174✔
886
        self.node_removed = true;
174✔
887
        Ok(())
174✔
888
    }
174✔
889

890
    /// Add an edge between 2 nodes.
891
    ///
892
    /// If :attr:`~rustworkx.PyGraph.multigraph` is ``False`` and an edge already
893
    /// exists between ``node_a`` and ``node_b`` the weight/payload of that
894
    /// existing edge will be updated to be ``edge``.
895
    ///
896
    /// :param int node_a: The index of the parent node
897
    /// :param int node_b: The index of the child node
898
    /// :param T edge: The python object to attach to the edge
899
    ///
900
    /// :returns: The index of the newly created (or updated in the case
901
    ///     of an existing edge with ``multigraph=False``) edge.
902
    /// :rtype: int
903
    #[pyo3(text_signature = "(self, node_a, node_b, edge, /)")]
904
    pub fn add_edge(&mut self, node_a: usize, node_b: usize, edge: PyObject) -> PyResult<usize> {
5,818✔
905
        let p_index = NodeIndex::new(node_a);
5,818✔
906
        let c_index = NodeIndex::new(node_b);
5,818✔
907
        if !self.graph.contains_node(p_index) || !self.graph.contains_node(c_index) {
5,818✔
908
            return Err(PyIndexError::new_err(
6✔
909
                "One of the endpoints of the edge does not exist in graph",
6✔
910
            ));
6✔
911
        }
5,812✔
912
        Ok(self._add_edge(p_index, c_index, edge))
5,812✔
913
    }
5,818✔
914

915
    /// Add new edges to the graph.
916
    ///
917
    /// :param iterable[tuple[int, int, T]] obj_list: An iterable of tuples of the form
918
    ///     ``(node_a, node_b, T)`` to attach to the graph. ``node_a`` and
919
    ///     ``node_b`` are integer indices describing where an edge should be
920
    ///     added, and ``T`` is the python object for the edge data.
921
    ///
922
    /// If :attr:`~rustworkx.PyGraph.multigraph` is ``False`` and an edge already
923
    /// exists between ``node_a`` and ``node_b`` the weight/payload of that
924
    /// existing edge will be updated to be ``edge``. This will occur in order
925
    /// from ``obj_list`` so if there are multiple parallel edges in ``obj_list``
926
    /// the last entry will be used.
927
    ///
928
    /// :returns: A list of indices of the newly created edges
929
    /// :rtype: EdgeIndices
930
    #[pyo3(text_signature = "(self, obj_list, /)")]
931
    pub fn add_edges_from(&mut self, obj_list: Bound<'_, PyAny>) -> PyResult<EdgeIndices> {
314✔
932
        let mut out_list = Vec::new();
314✔
933
        for py_obj in obj_list.try_iter()? {
3,024✔
934
            let obj = py_obj?.extract::<(usize, usize, PyObject)>()?;
3,024✔
935
            out_list.push(self.add_edge(obj.0, obj.1, obj.2)?);
3,024✔
936
        }
937
        Ok(EdgeIndices { edges: out_list })
312✔
938
    }
314✔
939

940
    /// Add new edges to the graph without python data.
941
    ///
942
    /// :param iterable[tuple[int, int]] obj_list: An iterable of tuples of the form
943
    ///     ``(parent, child)`` to attach to the graph. ``parent`` and
944
    ///     ``child`` are integer indices describing where an edge should be
945
    ///     added. Unlike :meth:`add_edges_from` there is no data payload and
946
    ///     when the edge is created None will be used.
947
    ///
948
    /// If :attr:`~rustworkx.PyGraph.multigraph` is ``False`` and an edge already
949
    /// exists between ``node_a`` and ``node_b`` the weight/payload of that
950
    /// existing edge will be updated to be ``None``.
951
    ///
952
    /// :returns: A list of indices of the newly created edges
953
    /// :rtype: EdgeIndices
954
    #[pyo3(text_signature = "(self, obj_list, /)")]
955
    pub fn add_edges_from_no_data(
130✔
956
        &mut self,
130✔
957
        py: Python,
130✔
958
        obj_list: Bound<'_, PyAny>,
130✔
959
    ) -> PyResult<EdgeIndices> {
130✔
960
        let mut out_list: Vec<usize> = Vec::new();
130✔
961
        for py_obj in obj_list.try_iter()? {
1,144✔
962
            let obj = py_obj?.extract::<(usize, usize)>()?;
1,144✔
963
            out_list.push(self.add_edge(obj.0, obj.1, py.None())?);
1,144✔
964
        }
965
        Ok(EdgeIndices { edges: out_list })
128✔
966
    }
130✔
967

968
    /// Extend graph from an edge list
969
    ///
970
    /// This method differs from :meth:`add_edges_from_no_data` in that it will
971
    /// add nodes if a node index is not present in the edge list.
972
    ///
973
    /// If :attr:`~rustworkx.PyGraph.multigraph` is ``False`` and an edge already
974
    /// exists between ``node_a`` and ``node_b`` the weight/payload of that
975
    /// existing edge will be updated to be ``None``.
976
    ///
977
    /// :param iterable[tuple[int, int]] edge_list: An iterable of tuples
978
    ///     in the form ``(source, target)`` where ``source`` and ``target``
979
    ///     are integer node indices. If the node index
980
    ///     is not present in the graph, nodes will be added (with a node
981
    ///     weight of ``None``) to that index.
982
    #[pyo3(text_signature = "(self, edge_list, /)")]
983
    pub fn extend_from_edge_list(
176✔
984
        &mut self,
176✔
985
        py: Python,
176✔
986
        edge_list: Bound<'_, PyAny>,
176✔
987
    ) -> PyResult<()> {
176✔
988
        for py_obj in edge_list.try_iter()? {
1,330✔
989
            let (source, target) = py_obj?.extract::<(usize, usize)>()?;
1,330✔
990
            let max_index = cmp::max(source, target);
1,330✔
991
            while max_index >= self.node_count() {
2,418✔
992
                self.graph.add_node(py.None());
1,088✔
993
            }
1,088✔
994
            let source_index = NodeIndex::new(source);
1,330✔
995
            let target_index = NodeIndex::new(target);
1,330✔
996
            self._add_edge(source_index, target_index, py.None());
1,330✔
997
        }
998
        Ok(())
176✔
999
    }
176✔
1000

1001
    /// Extend graph from a weighted edge list
1002
    ///
1003
    /// This method differs from :meth:`add_edges_from` in that it will
1004
    /// add nodes if a node index is not present in the edge list.
1005
    ///
1006
    /// If :attr:`~rustworkx.PyGraph.multigraph` is ``False`` and an edge already
1007
    /// exists between ``node_a`` and ``node_b`` the weight/payload of that
1008
    /// existing edge will be updated to be ``edge``. This will occur in order
1009
    /// from ``obj_list`` so if there are multiple parallel edges in ``obj_list``
1010
    /// the last entry will be used.
1011
    ///
1012
    /// :param iterable[tuple[int, int, T]] edge_list: An iterable of tuples in the form
1013
    ///     ``(source, target, weight)`` where source and target are integer
1014
    ///     node indices. If the node index is not present in the graph,
1015
    ///     nodes will be added (with a node weight of ``None``) to that index.
1016
    #[pyo3(text_signature = "(self, edge_list, /)")]
1017
    pub fn extend_from_weighted_edge_list(
146✔
1018
        &mut self,
146✔
1019
        py: Python,
146✔
1020
        edge_list: Bound<'_, PyAny>,
146✔
1021
    ) -> PyResult<()> {
146✔
1022
        for py_obj in edge_list.try_iter()? {
800✔
1023
            let (source, target, weight) = py_obj?.extract::<(usize, usize, PyObject)>()?;
800✔
1024
            let max_index = cmp::max(source, target);
800✔
1025
            while max_index >= self.node_count() {
1,484✔
1026
                self.graph.add_node(py.None());
684✔
1027
            }
684✔
1028
            let source_index = NodeIndex::new(source);
800✔
1029
            let target_index = NodeIndex::new(target);
800✔
1030
            self._add_edge(source_index, target_index, weight);
800✔
1031
        }
1032
        Ok(())
146✔
1033
    }
146✔
1034

1035
    /// Remove an edge between 2 nodes.
1036
    ///
1037
    /// Note if there are multiple edges between the specified nodes only one
1038
    /// will be removed.
1039
    ///
1040
    /// :param int parent: The index of the parent node
1041
    /// :param int child: The index of the child node
1042
    ///
1043
    /// :raises NoEdgeBetweenNodes: If there is no edge between the nodes
1044
    ///     specified
1045
    #[pyo3(text_signature = "(self, node_a, node_b, /)")]
1046
    pub fn remove_edge(&mut self, node_a: usize, node_b: usize) -> PyResult<()> {
286✔
1047
        let p_index = NodeIndex::new(node_a);
286✔
1048
        let c_index = NodeIndex::new(node_b);
286✔
1049
        let edge_index = match self.graph.find_edge(p_index, c_index) {
286✔
1050
            Some(edge_index) => edge_index,
78✔
1051
            None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
208✔
1052
        };
1053
        self.graph.remove_edge(edge_index);
78✔
1054
        Ok(())
78✔
1055
    }
286✔
1056

1057
    /// Remove an edge identified by the provided index
1058
    ///
1059
    /// :param int edge: The index of the edge to remove
1060
    #[pyo3(text_signature = "(self, edge, /)")]
1061
    pub fn remove_edge_from_index(&mut self, edge: usize) -> PyResult<()> {
54✔
1062
        let edge_index = EdgeIndex::new(edge);
54✔
1063
        self.graph.remove_edge(edge_index);
54✔
1064
        Ok(())
54✔
1065
    }
54✔
1066

1067
    /// Remove edges from the graph.
1068
    ///
1069
    /// Note if there are multiple edges between the specified nodes only one
1070
    /// will be removed.
1071
    ///
1072
    /// :param iterable[tuple[int, int]] index_list: An iterable of node index pairs
1073
    ///     to remove from the graph
1074
    ///
1075
    /// :raises NoEdgeBetweenNodes: If there are no edges between a specified
1076
    ///     pair of nodes.
1077
    #[pyo3(text_signature = "(self, index_list, /)")]
1078
    pub fn remove_edges_from(&mut self, index_list: Bound<'_, PyAny>) -> PyResult<()> {
6✔
1079
        for py_obj in index_list.try_iter()? {
10✔
1080
            let (x, y) = py_obj?.extract::<(usize, usize)>()?;
10✔
1081
            let (p_index, c_index) = (NodeIndex::new(x), NodeIndex::new(y));
10✔
1082
            let edge_index = match self.graph.find_edge(p_index, c_index) {
10✔
1083
                Some(edge_index) => edge_index,
8✔
1084
                None => return Err(NoEdgeBetweenNodes::new_err("No edge found between nodes")),
2✔
1085
            };
1086
            self.graph.remove_edge(edge_index);
8✔
1087
        }
1088
        Ok(())
4✔
1089
    }
6✔
1090

1091
    /// Add a new node to the graph.
1092
    ///
1093
    /// :param S obj: The python object to attach to the node
1094
    ///
1095
    /// :returns: The index of the newly created node
1096
    /// :rtype: int
1097
    #[pyo3(text_signature = "(self, obj, /)")]
1098
    pub fn add_node(&mut self, obj: PyObject) -> PyResult<usize> {
2,222✔
1099
        let index = self.graph.add_node(obj);
2,222✔
1100
        Ok(index.index())
2,222✔
1101
    }
2,222✔
1102

1103
    /// Add new nodes to the graph.
1104
    ///
1105
    /// :param iterable[S] obj_list: An iterable of python object to attach to the graph
1106
    ///
1107
    /// :returns indices: A list of indices of the newly created nodes
1108
    /// :rtype: NodeIndices
1109
    #[pyo3(text_signature = "(self, obj_list, /)")]
1110
    pub fn add_nodes_from(&mut self, obj_list: Bound<'_, PyAny>) -> PyResult<NodeIndices> {
450✔
1111
        let mut out_list = Vec::new();
450✔
1112
        for py_obj in obj_list.try_iter()? {
15,858✔
1113
            let obj = py_obj?.extract::<PyObject>()?;
15,858✔
1114
            out_list.push(self.graph.add_node(obj).index());
15,858✔
1115
        }
1116
        Ok(NodeIndices { nodes: out_list })
450✔
1117
    }
450✔
1118

1119
    /// Remove nodes from the graph.
1120
    ///
1121
    /// If a node index in the list is not present in the graph it will be
1122
    /// ignored.
1123
    ///
1124
    /// :param iterable[int] index_list: An iterable of node indices to remove from the
1125
    ///     graph
1126
    #[pyo3(text_signature = "(self, index_list, /)")]
1127
    pub fn remove_nodes_from(&mut self, index_list: Bound<'_, PyAny>) -> PyResult<()> {
30✔
1128
        for py_obj in index_list.try_iter()? {
58✔
1129
            let node = py_obj?.extract::<usize>()?;
58✔
1130
            self.remove_node(node)?;
58✔
1131
        }
1132
        Ok(())
30✔
1133
    }
30✔
1134

1135
    /// Find node within this graph given a specific weight
1136
    ///
1137
    /// This algorithm has a worst case of O(n) since it searches the node
1138
    /// indices in order. If there is more than one node in the graph with the
1139
    /// same weight only the first match (by node index) will be returned.
1140
    ///
1141
    /// :param T obj: The weight to look for in the graph.
1142
    ///
1143
    /// :returns: the index of the first node in the graph that is equal to the
1144
    ///     weight. If no match is found ``None`` will be returned.
1145
    /// :rtype: int
1146
    #[pyo3(text_signature = "(self, obj, /)")]
1147
    pub fn find_node_by_weight(&self, py: Python, obj: PyObject) -> PyResult<Option<usize>> {
×
1148
        find_node_by_weight(py, &self.graph, &obj).map(|node| node.map(|x| x.index()))
×
1149
    }
×
1150

1151
    /// Get the index and data for the neighbors of a node.
1152
    ///
1153
    /// This will return a dictionary where the keys are the node indices of
1154
    /// the adjacent nodes (inbound or outbound) and the value is the edge data
1155
    /// objects between that adjacent node and the provided node. Note, that
1156
    /// in the case of multigraphs only a single edge data object will be
1157
    /// returned
1158
    ///
1159
    /// :param int node: The index of the node to get the neighbors of
1160
    ///
1161
    /// :returns neighbors: A dictionary where the keys are node indices and
1162
    ///     the value is the edge data object for all nodes that share an
1163
    ///     edge with the specified node.
1164
    /// :rtype: dict[int, T]
1165
    #[pyo3(text_signature = "(self, node, /)")]
1166
    pub fn adj(&mut self, node: usize) -> DictMap<usize, &PyObject> {
4✔
1167
        let index = NodeIndex::new(node);
4✔
1168
        self.graph
4✔
1169
            .edges_directed(index, petgraph::Direction::Outgoing)
4✔
1170
            .map(|edge| (edge.target().index(), edge.weight()))
4✔
1171
            .collect()
4✔
1172
    }
4✔
1173

1174
    /// Get the neighbors of a node.
1175
    ///
1176
    /// This with return a list of neighbor node indices
1177
    ///
1178
    /// :param int node: The index of the node to get the neighbors of
1179
    ///
1180
    /// :returns: A list of the neighbor node indices
1181
    /// :rtype: NodeIndices
1182
    #[pyo3(text_signature = "(self, node, /)")]
1183
    pub fn neighbors(&self, node: usize) -> NodeIndices {
26✔
1184
        NodeIndices {
1185
            nodes: self
26✔
1186
                .graph
26✔
1187
                .neighbors(NodeIndex::new(node))
26✔
1188
                .map(|node| node.index())
50✔
1189
                .collect::<HashSet<usize>>()
26✔
1190
                .drain()
26✔
1191
                .collect(),
26✔
1192
        }
1193
    }
26✔
1194

1195
    /// Get the degree for a node
1196
    ///
1197
    /// :param int node: The index of the node to find the inbound degree of
1198
    ///
1199
    /// :returns degree: The inbound degree of the specified node
1200
    /// :rtype: int
1201
    #[pyo3(text_signature = "(self, node, /)")]
1202
    pub fn degree(&self, node: usize) -> usize {
1,382✔
1203
        let index = NodeIndex::new(node);
1,382✔
1204
        let neighbors = self.graph.edges(index);
1,382✔
1205
        neighbors.fold(0, |count, edge| {
4,102✔
1206
            if edge.source() == edge.target() {
4,102✔
1207
                return count + 2;
4✔
1208
            }
4,098✔
1209
            count + 1
4,098✔
1210
        })
4,102✔
1211
    }
1,382✔
1212

1213
    /// Generate a new :class:`~rustworkx.PyDiGraph` object from this graph
1214
    ///
1215
    /// This will create a new :class:`~rustworkx.PyDiGraph` object from this
1216
    /// graph. All edges in this graph will result in a bidirectional edge
1217
    /// pair in the output graph.
1218
    ///
1219
    /// .. note::
1220
    ///
1221
    ///     The node indices in the output :class:`~rustworkx.PyDiGraph` may
1222
    ///     differ if nodes have been removed.
1223
    ///
1224
    /// :returns: A new :class:`~rustworkx.PyDiGraph` object with a
1225
    ///     bidirectional edge pair for each edge in this graph. Also all
1226
    ///     node and edge weights/data payloads are copied by reference to
1227
    ///     the output graph
1228
    /// :rtype: PyDiGraph
1229
    #[pyo3(text_signature = "(self)")]
1230
    pub fn to_directed(&self, py: Python) -> crate::digraph::PyDiGraph {
20✔
1231
        let node_count = self.node_count();
20✔
1232
        let mut new_graph =
20✔
1233
            StablePyGraph::<Directed>::with_capacity(node_count, 2 * self.graph.edge_count());
20✔
1234
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(node_count);
20✔
1235
        for node_index in self.graph.node_indices() {
96✔
1236
            let node = self.graph[node_index].clone_ref(py);
96✔
1237
            let new_index = new_graph.add_node(node);
96✔
1238
            node_map.insert(node_index, new_index);
96✔
1239
        }
96✔
1240
        for edge in self.graph.edge_references() {
80✔
1241
            let &source = node_map.get(&edge.source()).unwrap();
80✔
1242
            let &target = node_map.get(&edge.target()).unwrap();
80✔
1243
            let weight = edge.weight();
80✔
1244
            new_graph.add_edge(source, target, weight.clone_ref(py));
80✔
1245
            new_graph.add_edge(target, source, weight.clone_ref(py));
80✔
1246
        }
80✔
1247
        crate::digraph::PyDiGraph {
20✔
1248
            graph: new_graph,
20✔
1249
            node_removed: false,
20✔
1250
            cycle_state: algo::DfsSpace::default(),
20✔
1251
            check_cycle: false,
20✔
1252
            multigraph: self.multigraph,
20✔
1253
            attrs: py.None(),
20✔
1254
        }
20✔
1255
    }
20✔
1256

1257
    /// Generate a dot file from the graph
1258
    ///
1259
    /// :param node_attr: A callable that will take in a node data object
1260
    ///     and return a dictionary of attributes to be associated with the
1261
    ///     node in the dot file. The key and value of this dictionary **must**
1262
    ///     be a string. If they're not strings rustworkx will raise TypeError
1263
    ///     (unfortunately without an error message because of current
1264
    ///     limitations in the PyO3 type checking)
1265
    /// :param edge_attr: A callable that will take in an edge data object
1266
    ///     and return a dictionary of attributes to be associated with the
1267
    ///     node in the dot file. The key and value of this dictionary **must**
1268
    ///     be a string. If they're not strings rustworkx will raise TypeError
1269
    ///     (unfortunately without an error message because of current
1270
    ///     limitations in the PyO3 type checking)
1271
    /// :param dict[str, str] graph_attr: An optional dictionary that specifies any graph
1272
    ///     attributes for the output dot file. The key and value of this
1273
    ///     dictionary **must** be a string. If they're not strings rustworkx
1274
    ///     will raise TypeError (unfortunately without an error message
1275
    ///     because of current limitations in the PyO3 type checking)
1276
    /// :param str filename: An optional path to write the dot file to
1277
    ///     if specified there is no return from the function
1278
    ///
1279
    /// :returns: A string with the dot file contents if filename is not
1280
    ///     specified.
1281
    /// :rtype: str
1282
    ///
1283
    /// Using this method enables you to leverage graphviz to visualize a
1284
    /// :class:`rustworkx.PyGraph` object. For example:
1285
    ///
1286
    /// .. jupyter-execute::
1287
    ///
1288
    ///   import os
1289
    ///   import tempfile
1290
    ///
1291
    ///   import pydot
1292
    ///   from PIL import Image
1293
    ///
1294
    ///   import rustworkx as rx
1295
    ///
1296
    ///   graph = rx.undirected_gnp_random_graph(15, .25)
1297
    ///   dot_str = graph.to_dot(
1298
    ///       lambda node: dict(
1299
    ///           color='black', fillcolor='lightblue', style='filled'))
1300
    ///   dot = pydot.graph_from_dot_data(dot_str)[0]
1301
    ///
1302
    ///   with tempfile.TemporaryDirectory() as tmpdirname:
1303
    ///       tmp_path = os.path.join(tmpdirname, 'dag.png')
1304
    ///       dot.write_png(tmp_path)
1305
    ///       image = Image.open(tmp_path)
1306
    ///       os.remove(tmp_path)
1307
    ///   image
1308
    ///
1309
    #[pyo3(
1310
        text_signature = "(self, /, node_attr=None, edge_attr=None, graph_attr=None, filename=None)",
1311
        signature = (node_attr=None, edge_attr=None, graph_attr=None, filename=None)
1312
    )]
1313
    pub fn to_dot<'py>(
14✔
1314
        &self,
14✔
1315
        py: Python<'py>,
14✔
1316
        node_attr: Option<PyObject>,
14✔
1317
        edge_attr: Option<PyObject>,
14✔
1318
        graph_attr: Option<BTreeMap<String, String>>,
14✔
1319
        filename: Option<String>,
14✔
1320
    ) -> PyResult<Option<Bound<'py, PyString>>> {
14✔
1321
        match filename {
14✔
1322
            Some(filename) => {
2✔
1323
                let mut file = File::create(filename)?;
2✔
1324
                build_dot(py, &self.graph, &mut file, graph_attr, node_attr, edge_attr)?;
2✔
1325
                Ok(None)
2✔
1326
            }
1327
            None => {
1328
                let mut file = Vec::<u8>::new();
12✔
1329
                build_dot(py, &self.graph, &mut file, graph_attr, node_attr, edge_attr)?;
12✔
1330
                Ok(Some(PyString::new(py, str::from_utf8(&file)?)))
12✔
1331
            }
1332
        }
1333
    }
14✔
1334

1335
    /// Read an edge list file and create a new PyGraph object from the
1336
    /// contents
1337
    ///
1338
    /// The expected format of the edge list file is a line separated list
1339
    /// of delimited node ids. If there are more than 3 elements on
1340
    /// a line the 3rd on will be treated as a string weight for the edge
1341
    ///
1342
    /// :param str path: The path of the file to read from
1343
    /// :param str comment: Optional character to use as a comment prefix
1344
    ///     (by default there are no comment characters)
1345
    /// :param str deliminator: Optional character to use as a deliminator
1346
    ///     (by default any whitespace will be used)
1347
    /// :param bool labels: If set to ``True`` the first two separated fields
1348
    ///     will be treated as string labels uniquely identifying a node
1349
    ///     instead of node indices
1350
    ///
1351
    /// For example:
1352
    ///
1353
    /// .. jupyter-execute::
1354
    ///
1355
    ///   import tempfile
1356
    ///
1357
    ///   import rustworkx as rx
1358
    ///   from rustworkx.visualization import mpl_draw
1359
    ///
1360
    ///   with tempfile.NamedTemporaryFile('wt') as fd:
1361
    ///       path = fd.name
1362
    ///       fd.write('0 1\n')
1363
    ///       fd.write('0 2\n')
1364
    ///       fd.write('0 3\n')
1365
    ///       fd.write('1 2\n')
1366
    ///       fd.write('2 3\n')
1367
    ///       fd.flush()
1368
    ///       graph = rx.PyGraph.read_edge_list(path=path)
1369
    ///   mpl_draw(graph)
1370
    ///
1371
    #[staticmethod]
1372
    #[pyo3(signature=(path, comment=None, deliminator=None, labels=false),  text_signature = "(path, /, comment=None, deliminator=None, labels=False)")]
1373
    pub fn read_edge_list(
22✔
1374
        py: Python,
22✔
1375
        path: &str,
22✔
1376
        comment: Option<String>,
22✔
1377
        deliminator: Option<String>,
22✔
1378
        labels: bool,
22✔
1379
    ) -> PyResult<PyGraph> {
22✔
1380
        let file = File::open(path)?;
22✔
1381
        let buf_reader = BufReader::new(file);
20✔
1382
        let mut out_graph = StablePyGraph::<Undirected>::default();
20✔
1383
        let mut label_map: HashMap<String, usize> = HashMap::new();
20✔
1384
        for line_raw in buf_reader.lines() {
54✔
1385
            let line = line_raw?;
54✔
1386
            let skip = match &comment {
54✔
1387
                Some(comm) => line.trim().starts_with(comm),
36✔
1388
                None => line.trim().is_empty(),
18✔
1389
            };
1390
            if skip {
54✔
1391
                continue;
12✔
1392
            }
42✔
1393
            let line_no_comments = match &comment {
42✔
1394
                Some(comm) => line
26✔
1395
                    .find(comm)
26✔
1396
                    .map(|idx| &line[..idx])
26✔
1397
                    .unwrap_or(&line)
26✔
1398
                    .trim()
26✔
1399
                    .to_string(),
26✔
1400
                None => line,
16✔
1401
            };
1402
            let pieces: Vec<&str> = match &deliminator {
42✔
1403
                Some(del) => line_no_comments.split(del).collect(),
14✔
1404
                None => line_no_comments.split_whitespace().collect(),
28✔
1405
            };
1406
            let src: usize;
1407
            let target: usize;
1408
            if labels {
42✔
1409
                let src_str = pieces[0];
10✔
1410
                let target_str = pieces[1];
10✔
1411
                src = match label_map.get(src_str) {
10✔
1412
                    Some(index) => *index,
6✔
1413
                    None => {
1414
                        let index = out_graph.add_node(src_str.into_py_any(py)?).index();
4✔
1415
                        label_map.insert(src_str.to_string(), index);
4✔
1416
                        index
4✔
1417
                    }
1418
                };
1419
                target = match label_map.get(target_str) {
10✔
1420
                    Some(index) => *index,
2✔
1421
                    None => {
1422
                        let index = out_graph.add_node(target_str.into_py_any(py)?).index();
8✔
1423
                        label_map.insert(target_str.to_string(), index);
8✔
1424
                        index
8✔
1425
                    }
1426
                };
1427
            } else {
1428
                src = pieces[0].parse::<usize>()?;
32✔
1429
                target = pieces[1].parse::<usize>()?;
32✔
1430
                let max_index = cmp::max(src, target);
32✔
1431
                // Add nodes to graph
1432
                while max_index >= out_graph.node_count() {
78✔
1433
                    out_graph.add_node(py.None());
46✔
1434
                }
46✔
1435
            }
1436
            // Add edges tp graph
1437
            let weight = if pieces.len() > 2 {
42✔
1438
                let weight_str = match &deliminator {
24✔
1439
                    Some(del) => pieces[2..].join(del),
12✔
1440
                    None => pieces[2..].join(&' '.to_string()),
12✔
1441
                };
1442
                PyString::new(py, &weight_str).into_any().unbind()
24✔
1443
            } else {
1444
                py.None()
18✔
1445
            };
1446
            out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
42✔
1447
        }
1448
        Ok(PyGraph {
20✔
1449
            graph: out_graph,
20✔
1450
            node_removed: false,
20✔
1451
            multigraph: true,
20✔
1452
            attrs: py.None(),
20✔
1453
        })
20✔
1454
    }
22✔
1455

1456
    /// Write an edge list file from the PyGraph object
1457
    ///
1458
    /// :param str path: The path to write the output file to
1459
    /// :param str deliminator: The optional character to use as a deliminator
1460
    ///     if not specified ``" "`` is used.
1461
    /// :param callable weight_fn: An optional callback function that will be
1462
    ///     passed an edge's data payload/weight object and is expected to
1463
    ///     return a string (a ``TypeError`` will be raised if it doesn't
1464
    ///     return a string). If specified the weight in the output file
1465
    ///     for each edge will be set to the returned string.
1466
    ///
1467
    ///  For example:
1468
    ///
1469
    ///  .. jupyter-execute::
1470
    ///
1471
    ///     import os
1472
    ///     import tempfile
1473
    ///
1474
    ///     import rustworkx as rx
1475
    ///
1476
    ///     graph = rx.generators.path_graph(5)
1477
    ///     path = os.path.join(tempfile.gettempdir(), "edge_list")
1478
    ///     graph.write_edge_list(path, deliminator=',')
1479
    ///     # Print file contents
1480
    ///     with open(path, 'rt') as edge_file:
1481
    ///         print(edge_file.read())
1482
    ///
1483
    #[pyo3(text_signature = "(self, path, /, deliminator=None, weight_fn=None)", signature = (path, deliminator=None, weight_fn=None))]
1484
    pub fn write_edge_list(
10✔
1485
        &self,
10✔
1486
        py: Python,
10✔
1487
        path: &str,
10✔
1488
        deliminator: Option<char>,
10✔
1489
        weight_fn: Option<PyObject>,
10✔
1490
    ) -> PyResult<()> {
10✔
1491
        let file = File::create(path)?;
10✔
1492
        let mut buf_writer = BufWriter::new(file);
10✔
1493
        let delim = match deliminator {
10✔
1494
            Some(delim) => delim.to_string(),
2✔
1495
            None => " ".to_string(),
8✔
1496
        };
1497

1498
        for edge in self.graph.edge_references() {
20✔
1499
            buf_writer.write_all(
20✔
1500
                format!(
20✔
1501
                    "{}{}{}",
20✔
1502
                    edge.source().index(),
20✔
1503
                    delim,
20✔
1504
                    edge.target().index()
20✔
1505
                )
20✔
1506
                .as_bytes(),
20✔
1507
            )?;
×
1508
            match weight_callable(py, &weight_fn, edge.weight(), None as Option<String>)? {
20✔
1509
                Some(weight) => buf_writer.write_all(format!("{delim}{weight}\n").as_bytes()),
8✔
1510
                None => buf_writer.write_all(b"\n"),
8✔
1511
            }?;
×
1512
        }
1513
        buf_writer.flush()?;
6✔
1514
        Ok(())
6✔
1515
    }
10✔
1516

1517
    /// Create a new :class:`~rustworkx.PyGraph` object from an adjacency matrix
1518
    /// with matrix elements of type ``float``
1519
    ///
1520
    /// This method can be used to construct a new :class:`~rustworkx.PyGraph`
1521
    /// object from an input adjacency matrix. The node weights will be the
1522
    /// index from the matrix. The edge weights will be a float value of the
1523
    /// value from the matrix.
1524
    ///
1525
    /// This differs from the
1526
    /// :meth:`~rustworkx.PyGraph.from_complex_adjacency_matrix` in that the
1527
    /// type of the elements of input matrix must be a ``float`` (specifically
1528
    /// a ``numpy.float64``) and the output graph edge weights will be ``float``
1529
    /// too. While in :meth:`~rustworkx.PyGraph.from_complex_adjacency_matrix`
1530
    /// the matrix elements are of type ``complex`` (specifically
1531
    /// ``numpy.complex128``) and the edge weights in the output graph will be
1532
    /// ``complex`` too.
1533
    ///
1534
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
1535
    ///     a new :class:`~rustworkx.PyGraph` object from. It must be a 2
1536
    ///     dimensional array and be a ``float``/``np.float64`` data type.
1537
    /// :param float null_value: An optional float that will treated as a null
1538
    ///     value. If any element in the input matrix is this value it will be
1539
    ///     treated as not an edge. By default this is ``0.0``.
1540
    ///
1541
    /// :returns: A new graph object generated from the adjacency matrix
1542
    /// :rtype: PyGraph
1543
    #[staticmethod]
1544
    #[pyo3(signature=(matrix, null_value=0.0), text_signature = "(matrix, /, null_value=0.0)")]
1545
    pub fn from_adjacency_matrix<'p>(
14✔
1546
        py: Python<'p>,
14✔
1547
        matrix: PyReadonlyArray2<'p, f64>,
14✔
1548
        null_value: f64,
14✔
1549
    ) -> PyResult<PyGraph> {
14✔
1550
        _from_adjacency_matrix(py, matrix, null_value)
14✔
1551
    }
14✔
1552

1553
    /// Create a new :class:`~rustworkx.PyGraph` object from an adjacency matrix
1554
    /// with matrix elements of type ``complex``
1555
    ///
1556
    /// This method can be used to construct a new :class:`~rustworkx.PyGraph`
1557
    /// object from an input adjacency matrix. The node weights will be the
1558
    /// index from the matrix. The edge weights will be a complex value of the
1559
    /// value from the matrix.
1560
    ///
1561
    /// This differs from the
1562
    /// :meth:`~rustworkx.PyGraph.from_adjacency_matrix` in that the type of
1563
    /// the elements of the input matrix in this method must be a ``complex``
1564
    /// (specifically a ``numpy.complex128``) and the output graph edge weights
1565
    /// will be ``complex`` too. While in
1566
    /// :meth:`~rustworkx.PyGraph.from_adjacency_matrix` the matrix elements
1567
    /// are of type ``float`` (specifically ``numpy.float64``) and the edge
1568
    /// weights in the output graph will be ``float`` too.
1569
    ///
1570
    /// :param ndarray matrix: The input numpy array adjacency matrix to create
1571
    ///     a new :class:`~rustworkx.PyGraph` object from. It must be a 2
1572
    ///     dimensional array and be a ``complex``/``np.complex128`` data type.
1573
    /// :param float null_value: An optional complex that will treated as a null
1574
    ///     value. If any element in the input matrix is this value it will be
1575
    ///     treated as not an edge. By default this is ``0.0+0.0j``
1576
    ///
1577
    /// :returns: A new graph object generated from the adjacency matrix
1578
    /// :rtype: PyGraph
1579
    ///
1580
    #[staticmethod]
1581
    #[pyo3(signature=(matrix, null_value=Complex64::zero()), text_signature = "(matrix, /, null_value=0.0+0.0j)")]
8✔
1582
    pub fn from_complex_adjacency_matrix<'p>(
12✔
1583
        py: Python<'p>,
12✔
1584
        matrix: PyReadonlyArray2<'p, Complex64>,
12✔
1585
        null_value: Complex64,
12✔
1586
    ) -> PyResult<PyGraph> {
12✔
1587
        _from_adjacency_matrix(py, matrix, null_value)
12✔
1588
    }
12✔
1589

1590
    /// Add another PyGraph object into this PyGraph
1591
    ///
1592
    /// :param PyGraph other: The other PyGraph object to add onto this
1593
    ///     graph.
1594
    /// :param dict[int, tuple[int, tuple[int, T]]] node_map: A dictionary
1595
    ///     mapping node indices from this
1596
    ///     PyGraph object to node indices in the other PyGraph object.
1597
    ///     The key is a node index in this graph and the value is a tuple
1598
    ///     of the node index in the other graph to add an edge to and the
1599
    ///     weight of that edge. For example::
1600
    ///
1601
    ///         {
1602
    ///             1: (2, "weight"),
1603
    ///             2: (4, "weight2")
1604
    ///         }
1605
    ///
1606
    /// :param Callable node_map_func: An optional python callable that will take in a
1607
    ///     single node weight/data object and return a new node weight/data
1608
    ///     object that will be used when adding an node from other onto this
1609
    ///     graph.
1610
    /// :param Callable edge_map_func: An optional python callable that will take in a
1611
    ///     single edge weight/data object and return a new edge weight/data
1612
    ///     object that will be used when adding an edge from other onto this
1613
    ///     graph.
1614
    ///
1615
    /// :returns: new_node_ids: A dictionary mapping node index from the other
1616
    ///     PyGraph to the equivalent node index in this PyDAG after they've
1617
    ///     been combined
1618
    /// :rtype: dict[int, int]
1619
    ///
1620
    /// For example, start by building a graph:
1621
    ///
1622
    /// .. jupyter-execute::
1623
    ///
1624
    ///   import os
1625
    ///   import tempfile
1626
    ///
1627
    ///   import pydot
1628
    ///   from PIL import Image
1629
    ///
1630
    ///   import rustworkx as rx
1631
    ///   from rustworkx.visualization import mpl_draw
1632
    ///
1633
    ///   # Build first graph and visualize:
1634
    ///   graph = rx.PyGraph()
1635
    ///   node_a, node_b, node_c = graph.add_nodes_from(['A', 'B', 'C'])
1636
    ///   graph.add_edges_from([(node_a, node_b, 'A to B'),
1637
    ///                         (node_b, node_c, 'B to C')])
1638
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
1639
    ///
1640
    /// Then build a second one:
1641
    ///
1642
    /// .. jupyter-execute::
1643
    ///
1644
    ///   # Build second graph and visualize:
1645
    ///   other_graph = rx.PyGraph()
1646
    ///   node_d, node_e = other_graph.add_nodes_from(['D', 'E'])
1647
    ///   other_graph.add_edge(node_d, node_e, 'D to E')
1648
    ///   mpl_draw(other_graph, with_labels=True, labels=str, edge_labels=str)
1649
    ///
1650
    /// Finally compose the ``other_graph`` onto ``graph``
1651
    ///
1652
    /// .. jupyter-execute::
1653
    ///
1654
    ///   node_map = {node_b: (node_d, 'B to D')}
1655
    ///   graph.compose(other_graph, node_map)
1656
    ///   mpl_draw(graph, with_labels=True, labels=str, edge_labels=str)
1657
    ///
1658
    #[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))]
1659
    pub fn compose(
8✔
1660
        &mut self,
8✔
1661
        py: Python,
8✔
1662
        other: &PyGraph,
8✔
1663
        node_map: HashMap<usize, (usize, PyObject)>,
8✔
1664
        node_map_func: Option<PyObject>,
8✔
1665
        edge_map_func: Option<PyObject>,
8✔
1666
    ) -> PyResult<PyObject> {
8✔
1667
        let mut new_node_map: DictMap<NodeIndex, NodeIndex> =
8✔
1668
            DictMap::with_capacity(other.node_count());
8✔
1669

1670
        // TODO: Reimplement this without looping over the graphs
1671
        // Loop over other nodes add to self graph
1672
        for node in other.graph.node_indices() {
36✔
1673
            let new_index = self.graph.add_node(weight_transform_callable(
36✔
1674
                py,
36✔
1675
                &node_map_func,
36✔
1676
                &other.graph[node],
36✔
1677
            )?);
×
1678
            new_node_map.insert(node, new_index);
36✔
1679
        }
1680

1681
        // loop over other edges and add to self graph
1682
        for edge in other.graph.edge_references() {
42✔
1683
            let new_p_index = new_node_map.get(&edge.source()).unwrap();
42✔
1684
            let new_c_index = new_node_map.get(&edge.target()).unwrap();
42✔
1685
            let weight = weight_transform_callable(py, &edge_map_func, edge.weight())?;
42✔
1686
            self.graph.add_edge(*new_p_index, *new_c_index, weight);
42✔
1687
        }
1688
        // Add edges from map
1689
        for (this_index, (index, weight)) in node_map.iter() {
8✔
1690
            let new_index = new_node_map.get(&NodeIndex::new(*index)).unwrap();
8✔
1691
            self.graph.add_edge(
8✔
1692
                NodeIndex::new(*this_index),
8✔
1693
                *new_index,
8✔
1694
                weight.clone_ref(py),
8✔
1695
            );
8✔
1696
        }
8✔
1697
        let out_dict = PyDict::new(py);
8✔
1698
        for (orig_node, new_node) in new_node_map.iter() {
36✔
1699
            out_dict.set_item(orig_node.index(), new_node.index())?;
36✔
1700
        }
1701
        Ok(out_dict.into())
8✔
1702
    }
8✔
1703

1704
    /// Substitute a node with a PyGraph object
1705
    ///
1706
    /// :param int node: The index of the node to be replaced with the PyGraph object
1707
    /// :param PyGraph other: The other graph to replace ``node`` with
1708
    /// :param Callable edge_map_fn: A callable object that will take 3 position
1709
    ///     parameters, ``(source, target, weight)`` to represent an edge either to
1710
    ///     or from ``node`` in this graph. The expected return value from this
1711
    ///     callable is the node index of the node in ``other`` that an edge should
1712
    ///     be to/from. If None is returned, that edge will be skipped and not
1713
    ///     be copied.
1714
    /// :param Callable node_filter: An optional callable object that when used
1715
    ///     will receive a node's payload object from ``other`` and return
1716
    ///     ``True`` if that node is to be included in the graph or not.
1717
    /// :param Callable edge_weight_map: An optional callable object that when
1718
    ///     used will receive an edge's weight/data payload from ``other`` and
1719
    ///     will return an object to use as the weight for a newly created edge
1720
    ///     after the edge is mapped from ``other``. If not specified the weight
1721
    ///     from the edge in ``other`` will be copied by reference and used.
1722
    ///
1723
    /// :returns: A mapping of node indices in ``other`` to the equivalent node
1724
    ///     in this graph.
1725
    /// :rtype: NodeMap
1726
    ///
1727
    /// .. note::
1728
    ///
1729
    ///    The return type is a :class:`rustworkx.NodeMap` which is an unordered
1730
    ///    type. So it does not provide a deterministic ordering between objects
1731
    ///    when iterated over (although the same object will have a consistent
1732
    ///    order when iterated over multiple times).
1733
    ///
1734
    #[pyo3(
1735
        text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None",
1736
        signature = (node, other, edge_map_fn, node_filter=None, edge_weight_map=None)
1737
    )]
1738
    fn substitute_node_with_subgraph(
18✔
1739
        &mut self,
18✔
1740
        py: Python,
18✔
1741
        node: usize,
18✔
1742
        other: &PyGraph,
18✔
1743
        edge_map_fn: PyObject,
18✔
1744
        node_filter: Option<PyObject>,
18✔
1745
        edge_weight_map: Option<PyObject>,
18✔
1746
    ) -> PyResult<NodeMap> {
18✔
1747
        let filter_fn = |obj: &PyObject, filter_fn: &Option<PyObject>| -> PyResult<bool> {
82✔
1748
            match filter_fn {
82✔
1749
                Some(filter) => {
16✔
1750
                    let res = filter.call1(py, (obj,))?;
16✔
1751
                    res.extract(py)
16✔
1752
                }
1753
                None => Ok(true),
66✔
1754
            }
1755
        };
82✔
1756

1757
        let weight_map_fn = |obj: &PyObject, weight_fn: &Option<PyObject>| -> PyResult<PyObject> {
110✔
1758
            match weight_fn {
110✔
1759
                Some(weight_fn) => weight_fn.call1(py, (obj,)),
4✔
1760
                None => Ok(obj.clone_ref(py)),
106✔
1761
            }
1762
        };
110✔
1763

1764
        let map_fn = |source: usize, target: usize, weight: &PyObject| -> PyResult<Option<usize>> {
32✔
1765
            let res = edge_map_fn.call1(py, (source, target, weight))?;
32✔
1766
            res.extract(py)
32✔
1767
        };
32✔
1768

1769
        let node_index = NodeIndex::new(node);
18✔
1770
        if self.graph.node_weight(node_index).is_none() {
18✔
1771
            return Err(PyIndexError::new_err(format!(
2✔
1772
                "Specified node {node} is not in this graph"
2✔
1773
            )));
2✔
1774
        }
16✔
1775

1776
        // Copy all nodes from other to self
1777
        let mut out_map: DictMap<usize, usize> = DictMap::with_capacity(other.node_count());
16✔
1778
        for node in other.graph.node_indices() {
82✔
1779
            let node_weight: Py<PyAny> = other.graph[node].clone_ref(py);
82✔
1780
            if !filter_fn(&node_weight, &node_filter)? {
82✔
1781
                continue;
2✔
1782
            }
80✔
1783
            let new_index: NodeIndex = self.graph.add_node(node_weight);
80✔
1784
            out_map.insert(node.index(), new_index.index());
80✔
1785
        }
1786

1787
        if out_map.is_empty() {
16✔
1788
            self.graph.remove_node(node_index);
2✔
1789
            return Ok(NodeMap {
2✔
1790
                node_map: DictMap::new(),
2✔
1791
            });
2✔
1792
        }
14✔
1793

1794
        // Copy all edges
1795
        for edge in other.graph.edge_references().filter(|edge| {
114✔
1796
            out_map.contains_key(&edge.target().index())
114✔
1797
                && out_map.contains_key(&edge.source().index())
114✔
1798
        }) {
114✔
1799
            self._add_edge(
110✔
1800
                NodeIndex::new(out_map[&edge.source().index()]),
110✔
1801
                NodeIndex::new(out_map[&edge.target().index()]),
110✔
1802
                weight_map_fn(edge.weight(), &edge_weight_map)?,
110✔
1803
            );
1804
        }
1805
        // Incoming and outgoing edges.
1806
        let in_edges: Vec<(NodeIndex, NodeIndex, PyObject)> = self
14✔
1807
            .graph
14✔
1808
            .edge_references()
14✔
1809
            .filter(|edge| edge.target() == node_index)
166✔
1810
            .map(|edge| (edge.source(), edge.target(), edge.weight().clone_ref(py)))
14✔
1811
            .collect();
14✔
1812
        // Keep track of what's present on incoming edges
1813
        let in_set: HashSet<(NodeIndex, NodeIndex)> =
14✔
1814
            in_edges.iter().map(|edge| (edge.0, edge.1)).collect();
14✔
1815
        // Retrieve outgoing edges. Make sure to not include any incoming edge.
1816
        let out_edges: Vec<(NodeIndex, NodeIndex, PyObject)> = self
14✔
1817
            .graph
14✔
1818
            .edges(node_index)
14✔
1819
            .filter(|edge| !in_set.contains(&(edge.target(), edge.source())))
32✔
1820
            .map(|edge| (edge.source(), edge.target(), edge.weight().clone_ref(py)))
26✔
1821
            .collect();
14✔
1822
        for (source, target, weight) in in_edges {
20✔
1823
            let old_index: Option<usize> = map_fn(source.index(), target.index(), &weight)?;
6✔
1824
            let target_out: NodeIndex = match old_index {
6✔
1825
                Some(old_index) => match out_map.get(&old_index) {
4✔
1826
                    Some(new_index) => NodeIndex::new(*new_index),
4✔
1827
                    None => {
1828
                        return Err(PyIndexError::new_err(format!(
×
1829
                            "No mapped index {old_index} found"
×
1830
                        )))
×
1831
                    }
1832
                },
1833
                None => continue,
2✔
1834
            };
1835
            self._add_edge(source, target_out, weight);
4✔
1836
        }
1837
        for (source, target, weight) in out_edges {
38✔
1838
            let old_index: Option<usize> = map_fn(source.index(), target.index(), &weight)?;
26✔
1839
            let source_out: NodeIndex = match old_index {
26✔
1840
                Some(old_index) => match out_map.get(&old_index) {
24✔
1841
                    Some(new_index) => NodeIndex::new(*new_index),
22✔
1842
                    None => {
1843
                        return Err(PyIndexError::new_err(format!(
2✔
1844
                            "No mapped index {old_index} found"
2✔
1845
                        )))
2✔
1846
                    }
1847
                },
1848
                None => continue,
2✔
1849
            };
1850
            self._add_edge(source_out, target, weight);
22✔
1851
        }
1852
        // Remove original node
1853
        self.graph.remove_node(node_index);
12✔
1854
        Ok(NodeMap { node_map: out_map })
12✔
1855
    }
18✔
1856

1857
    /// Substitute a set of nodes with a single new node.
1858
    ///
1859
    /// .. note::
1860
    ///     This method does not preserve the ordering of endpoints in
1861
    ///     edge tuple representations (e.g. the tuples returned from
1862
    ///     :meth:`~rustworkx.PyGraph.edge_list`).
1863
    ///
1864
    /// :param list[int] nodes: A set of nodes to be removed and replaced
1865
    ///     by the new node. Any nodes not in the graph are ignored.
1866
    ///     If empty, this method behaves like :meth:`~PyGraph.add_node`
1867
    ///     (but slower).
1868
    /// :param S obj: The data/weight to associate with the new node.
1869
    /// :param Callable weight_combo_fn: An optional python callable that, when
1870
    ///     specified, is used to merge parallel edges introduced by the
1871
    ///     contraction, which will occur if any two edges between ``nodes``
1872
    ///     and the rest of the graph share an endpoint.
1873
    ///     If this instance of :class:`~rustworkx.PyGraph` is a multigraph,
1874
    ///     leave this unspecified to preserve parallel edges. If unspecified
1875
    ///     when not a multigraph, parallel edges and their weights will be
1876
    ///     combined by choosing one of the edge's weights arbitrarily based
1877
    ///     on an internal iteration order, subject to change.
1878
    /// :returns: The index of the newly created node.
1879
    /// :rtype: int
1880
    #[pyo3(text_signature = "(self, nodes, obj, /, weight_combo_fn=None)", signature = (nodes, obj, weight_combo_fn=None))]
1881
    pub fn contract_nodes(
24✔
1882
        &mut self,
24✔
1883
        py: Python,
24✔
1884
        nodes: Vec<usize>,
24✔
1885
        obj: PyObject,
24✔
1886
        weight_combo_fn: Option<PyObject>,
24✔
1887
    ) -> RxPyResult<usize> {
24✔
1888
        let nodes = nodes.into_iter().map(|i| NodeIndex::new(i));
56✔
1889
        let res = match (weight_combo_fn, &self.multigraph) {
24✔
1890
            (Some(user_callback), _) => {
2✔
1891
                self.graph
2✔
1892
                    .contract_nodes_simple(nodes, obj, |w1, w2| user_callback.call1(py, (w1, w2)))?
8✔
1893
            }
1894
            (None, false) => {
1895
                // By default, just take first edge.
1896
                self.graph.contract_nodes_simple(nodes, obj, move |w1, _| {
8✔
1897
                    Ok::<_, PyErr>(w1.clone_ref(py))
8✔
1898
                })?
8✔
1899
            }
1900
            (None, true) => self.graph.contract_nodes(nodes, obj),
18✔
1901
        };
1902
        self.node_removed = true;
24✔
1903
        Ok(res.index())
24✔
1904
    }
24✔
1905

1906
    /// Return a new PyGraph object for a subgraph of this graph and a NodeMap
1907
    /// object that maps the nodes of the subgraph to the nodes of the original graph.
1908
    ///
1909
    /// .. note::
1910
    ///     This method is identical to :meth:`.subgraph()` but includes a
1911
    ///     NodeMap object that maps the nodes of the subgraph to the nodes of
1912
    ///     the original graph.
1913
    ///
1914
    /// :param list[int] nodes: A list of node indices to generate the subgraph
1915
    ///     from. If a node index is included that is not present in the graph
1916
    ///     it will silently be ignored.
1917
    /// :param bool preserve_attrs: If set to the True the attributes of the PyGraph
1918
    ///     will be copied by reference to be the attributes of the output
1919
    ///     subgraph. By default this is set to False and the :attr:`~.PyGraph.attrs`
1920
    ///     attribute will be ``None`` in the subgraph.
1921
    ///
1922
    /// :returns: A tuple containing a new PyGraph object representing a subgraph of this graph
1923
    ///     and a NodeMap object that maps the nodes of the subgraph to the nodes of the original graph.
1924
    ///     It is worth noting that node and edge weight/data payloads are
1925
    ///     passed by reference so if you update (not replace) an object used
1926
    ///     as the weight in graph or the subgraph it will also be updated in
1927
    ///     the other.
1928
    /// :rtype: tuple[PyGraph, NodeMap]
1929
    ///
1930
    #[pyo3(signature=(nodes, preserve_attrs=false), text_signature = "(self, nodes, /, preserve_attrs=False)")]
1931
    pub fn subgraph_with_nodemap(
3,116✔
1932
        &self,
3,116✔
1933
        py: Python,
3,116✔
1934
        nodes: Vec<usize>,
3,116✔
1935
        preserve_attrs: bool,
3,116✔
1936
    ) -> (PyGraph, NodeMap) {
3,116✔
1937
        let node_set: HashSet<usize> = nodes.iter().cloned().collect();
3,116✔
1938
        // mapping from original node index to new node index
1939
        let mut node_map: HashMap<NodeIndex, NodeIndex> = HashMap::with_capacity(nodes.len());
3,116✔
1940
        // mapping from new node index to original node index
1941
        let mut node_dict: DictMap<usize, usize> = DictMap::with_capacity(nodes.len());
3,116✔
1942
        let node_filter = |node: NodeIndex| -> bool { node_set.contains(&node.index()) };
68,558✔
1943
        let mut out_graph = StablePyGraph::<Undirected>::default();
3,116✔
1944
        let filtered = NodeFiltered(&self.graph, node_filter);
3,116✔
1945
        for node in filtered.node_references() {
12,596✔
1946
            let new_node = out_graph.add_node(node.1.clone_ref(py));
12,596✔
1947
            node_map.insert(node.0, new_node);
12,596✔
1948
            node_dict.insert(new_node.index(), node.0.index());
12,596✔
1949
        }
12,596✔
1950
        for edge in filtered.edge_references() {
7,270✔
1951
            let new_source = *node_map.get(&edge.source()).unwrap();
7,270✔
1952
            let new_target = *node_map.get(&edge.target()).unwrap();
7,270✔
1953
            out_graph.add_edge(new_source, new_target, edge.weight().clone_ref(py));
7,270✔
1954
        }
7,270✔
1955
        let attrs = if preserve_attrs {
3,116✔
1956
            self.attrs.clone_ref(py)
2✔
1957
        } else {
1958
            py.None()
3,114✔
1959
        };
1960
        let node_map = NodeMap {
3,116✔
1961
            node_map: node_dict,
3,116✔
1962
        };
3,116✔
1963
        let subgraph = PyGraph {
3,116✔
1964
            graph: out_graph,
3,116✔
1965
            node_removed: false,
3,116✔
1966
            multigraph: self.multigraph,
3,116✔
1967
            attrs,
3,116✔
1968
        };
3,116✔
1969
        (subgraph, node_map)
3,116✔
1970
    }
3,116✔
1971

1972
    /// Return a new PyGraph object for a subgraph of this graph.
1973
    ///
1974
    /// .. note::
1975
    ///     To return a NodeMap object that maps the nodes of the subgraph to
1976
    ///     the nodes of the original graph, use :meth:`.subgraph_with_nodemap()`.
1977
    ///
1978
    /// :param list[int] nodes: A list of node indices to generate the subgraph
1979
    ///     from. If a node index is included that is not present in the graph
1980
    ///     it will silently be ignored.
1981
    /// :param bool preserve_attrs: If set to the True the attributes of the PyGraph
1982
    ///     will be copied by reference to be the attributes of the output
1983
    ///     subgraph. By default this is set to False and the :attr:`~.PyGraph.attrs`
1984
    ///     attribute will be ``None`` in the subgraph.
1985
    ///
1986
    /// :returns: A new PyGraph object representing a subgraph of this graph.
1987
    ///     It is worth noting that node and edge weight/data payloads are
1988
    ///     passed by reference so if you update (not replace) an object used
1989
    ///     as the weight in graph or the subgraph it will also be updated in
1990
    ///     the other.
1991
    /// :rtype: PyGraph
1992
    ///
1993
    #[pyo3(signature=(nodes, preserve_attrs=false), text_signature = "(self, nodes, /, preserve_attrs=False)")]
1994
    pub fn subgraph(&self, py: Python, nodes: Vec<usize>, preserve_attrs: bool) -> PyGraph {
3,104✔
1995
        let (subgraph, _) = self.subgraph_with_nodemap(py, nodes, preserve_attrs);
3,104✔
1996
        subgraph
3,104✔
1997
    }
3,104✔
1998

1999
    /// Return a new PyGraph object for an edge induced subgraph of this graph
2000
    ///
2001
    /// The induced subgraph contains each edge in `edge_list` and each node
2002
    /// incident to any of those edges.
2003
    ///
2004
    /// :param list[tuple[int, int]] edge_list: A list of edge tuples (2-tuples with the source
2005
    ///     and target node) to generate the subgraph from. In cases of parallel
2006
    ///     edges for a multigraph all edges between the specified node. In case
2007
    ///     of an edge specified that doesn't exist in the graph it will be
2008
    ///     silently ignored.
2009
    ///
2010
    /// :returns: The edge subgraph
2011
    /// :rtype: PyGraph
2012
    ///
2013
    #[pyo3(text_signature = "(self, edge_list, /)")]
2014
    pub fn edge_subgraph(&self, edge_list: Vec<[usize; 2]>) -> PyGraph {
8✔
2015
        // Filter non-existent edges
2016
        let edges: Vec<[usize; 2]> = edge_list
8✔
2017
            .into_iter()
8✔
2018
            .filter(|x| {
14✔
2019
                let source = NodeIndex::new(x[0]);
14✔
2020
                let target = NodeIndex::new(x[1]);
14✔
2021
                self.graph.find_edge(source, target).is_some()
14✔
2022
            })
14✔
2023
            .collect();
8✔
2024

2025
        let nodes: HashSet<NodeIndex> = edges
8✔
2026
            .iter()
8✔
2027
            .flat_map(|x| x.iter())
12✔
2028
            .copied()
8✔
2029
            .map(NodeIndex::new)
8✔
2030
            .collect();
8✔
2031
        let mut edge_set: HashSet<[NodeIndex; 2]> = HashSet::with_capacity(edges.len());
8✔
2032
        for edge in edges {
20✔
2033
            let source_index = NodeIndex::new(edge[0]);
12✔
2034
            let target_index = NodeIndex::new(edge[1]);
12✔
2035
            edge_set.insert([source_index, target_index]);
12✔
2036
        }
12✔
2037
        let mut out_graph = self.clone();
8✔
2038
        for node in self
14✔
2039
            .graph
8✔
2040
            .node_indices()
8✔
2041
            .filter(|node| !nodes.contains(node))
32✔
2042
        {
14✔
2043
            out_graph.graph.remove_node(node);
14✔
2044
            out_graph.node_removed = true;
14✔
2045
        }
14✔
2046
        for edge in self.graph.edge_references().filter(|edge| {
44✔
2047
            !edge_set.contains(&[edge.source(), edge.target()])
44✔
2048
                && !edge_set.contains(&[edge.target(), edge.source()])
28✔
2049
        }) {
44✔
2050
            out_graph.graph.remove_edge(edge.id());
28✔
2051
        }
28✔
2052
        out_graph
8✔
2053
    }
8✔
2054

2055
    /// Return a shallow copy of the graph
2056
    ///
2057
    /// All node and edge weight/data payloads in the copy will have a
2058
    /// shared reference to the original graph.
2059
    /// :returns: A shallow copy of the graph
2060
    /// :rtype: PyGraph
2061
    #[pyo3(text_signature = "(self)")]
2062
    pub fn copy(&self) -> PyGraph {
10✔
2063
        self.clone()
10✔
2064
    }
10✔
2065

2066
    /// Return the number of nodes in the graph
2067
    fn __len__(&self) -> PyResult<usize> {
210✔
2068
        Ok(self.graph.node_count())
210✔
2069
    }
210✔
2070

2071
    fn __getitem__(&self, idx: usize) -> PyResult<&PyObject> {
512✔
2072
        match self.graph.node_weight(NodeIndex::new(idx)) {
512✔
2073
            Some(data) => Ok(data),
510✔
2074
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
2075
        }
2076
    }
512✔
2077

2078
    fn __setitem__(&mut self, idx: usize, value: PyObject) -> PyResult<()> {
282✔
2079
        let data = match self.graph.node_weight_mut(NodeIndex::new(idx)) {
282✔
2080
            Some(node_data) => node_data,
280✔
2081
            None => return Err(PyIndexError::new_err("No node found for index")),
2✔
2082
        };
2083
        *data = value;
280✔
2084
        Ok(())
280✔
2085
    }
282✔
2086

2087
    fn __delitem__(&mut self, idx: usize) -> PyResult<()> {
4✔
2088
        match self.graph.remove_node(NodeIndex::new(idx)) {
4✔
2089
            Some(_) => {
2090
                self.node_removed = true;
2✔
2091
                Ok(())
2✔
2092
            }
2093
            None => Err(PyIndexError::new_err("No node found for index")),
2✔
2094
        }
2095
    }
4✔
2096

2097
    #[classmethod]
2098
    #[pyo3(signature = (key, /))]
2099
    pub fn __class_getitem__(
2✔
2100
        cls: &Bound<'_, PyType>,
2✔
2101
        key: &Bound<'_, PyAny>,
2✔
2102
    ) -> PyResult<PyObject> {
2✔
2103
        let alias = PyGenericAlias::new(cls.py(), cls.as_any(), key)?;
2✔
2104
        Ok(alias.into())
2✔
2105
    }
2✔
2106

2107
    // Functions to enable Python Garbage Collection
2108

2109
    // Function for PyTypeObject.tp_traverse [1][2] used to tell Python what
2110
    // objects the PyGraph has strong references to.
2111
    //
2112
    // [1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
2113
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
2114
    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
2,856✔
2115
        for node in self
57,588✔
2116
            .graph
2,856✔
2117
            .node_indices()
2,856✔
2118
            .map(|node| self.graph.node_weight(node).unwrap())
57,588✔
2119
        {
2120
            visit.call(node)?;
57,588✔
2121
        }
2122
        for edge in self
100,472✔
2123
            .graph
2,856✔
2124
            .edge_indices()
2,856✔
2125
            .map(|edge| self.graph.edge_weight(edge).unwrap())
100,472✔
2126
        {
2127
            visit.call(edge)?;
100,472✔
2128
        }
2129
        visit.call(&self.attrs)?;
2,856✔
2130
        Ok(())
2,856✔
2131
    }
2,856✔
2132

2133
    // Function for PyTypeObject.tp_clear [1][2] used to tell Python's GC how
2134
    // to drop all references held by a PyGraph object when the GC needs to
2135
    // break reference cycles.
2136
    //
2137
    // ]1] https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_clear
2138
    // [2] https://pyo3.rs/v0.12.4/class/protocols.html#garbage-collector-integration
UNCOV
2139
    fn __clear__(&mut self, py: Python) {
×
2140
        self.graph = StablePyGraph::<Undirected>::default();
×
2141
        self.node_removed = false;
×
2142
        self.attrs = py.None();
×
2143
    }
×
2144

2145
    /// Filters a graph's nodes by some criteria conditioned on a node's data payload and returns those nodes' indices.
2146
    ///
2147
    /// This function takes in a function as an argument. This filter function will be passed in a node's data payload and is
2148
    /// required to return a boolean value stating whether the node's data payload fits some criteria.
2149
    ///
2150
    /// For example::
2151
    ///
2152
    ///     from rustworkx import PyGraph
2153
    ///
2154
    ///     graph = PyGraph()
2155
    ///     graph.add_nodes_from(list(range(5)))
2156
    ///
2157
    ///     def my_filter_function(node):
2158
    ///         return node > 2
2159
    ///
2160
    ///     indices = graph.filter_nodes(my_filter_function)
2161
    ///     assert indices == [3, 4]
2162
    ///
2163
    /// :param Callable filter_function: Function to filter nodes with
2164
    /// :returns: The node indices that match the filter
2165
    /// :rtype: NodeIndices
2166
    #[pyo3(text_signature = "(self, filter_function)")]
2167
    pub fn filter_nodes(&self, py: Python, filter_function: PyObject) -> PyResult<NodeIndices> {
8✔
2168
        let filter = |nindex: NodeIndex| -> PyResult<bool> {
32✔
2169
            let res = filter_function.call1(py, (&self.graph[nindex],))?;
32✔
2170
            res.extract(py)
30✔
2171
        };
32✔
2172

2173
        let mut n = Vec::with_capacity(self.graph.node_count());
8✔
2174
        for node_index in self.graph.node_indices() {
32✔
2175
            if filter(node_index)? {
32✔
2176
                n.push(node_index.index())
8✔
2177
            };
22✔
2178
        }
2179
        Ok(NodeIndices { nodes: n })
6✔
2180
    }
8✔
2181

2182
    /// Filters a graph's edges by some criteria conditioned on a edge's data payload and returns those edges' indices.
2183
    ///
2184
    /// This function takes in a function as an argument. This filter function will be passed in an edge's data payload and is
2185
    /// required to return a boolean value stating whether the edge's data payload fits some criteria.
2186
    ///
2187
    /// For example::
2188
    ///
2189
    ///     from rustworkx import PyGraph
2190
    ///     from rustworkx.generators import complete_graph
2191
    ///
2192
    ///     graph = PyGraph()
2193
    ///     graph.add_nodes_from(range(3))
2194
    ///     graph.add_edges_from([(0, 1, 'A'), (0, 1, 'B'), (1, 2, 'C')])
2195
    ///
2196
    ///     def my_filter_function(edge):
2197
    ///         if edge:
2198
    ///             return edge == 'B'
2199
    ///         return False
2200
    ///
2201
    ///     indices = graph.filter_edges(my_filter_function)
2202
    ///     assert indices == [1]
2203
    ///
2204
    /// :param Callable filter_function: Function to filter edges with
2205
    /// :returns: The edge indices that match the filter
2206
    /// :rtype: EdgeIndices
2207
    #[pyo3(text_signature = "(self, filter_function)")]
2208
    pub fn filter_edges(&self, py: Python, filter_function: PyObject) -> PyResult<EdgeIndices> {
8✔
2209
        let filter = |eindex: EdgeIndex| -> PyResult<bool> {
20✔
2210
            let res = filter_function.call1(py, (&self.graph[eindex],))?;
20✔
2211
            res.extract(py)
18✔
2212
        };
20✔
2213

2214
        let mut e = Vec::with_capacity(self.graph.edge_count());
8✔
2215
        for edge_index in self.graph.edge_indices() {
20✔
2216
            if filter(edge_index)? {
20✔
2217
                e.push(edge_index.index())
6✔
2218
            };
12✔
2219
        }
2220
        Ok(EdgeIndices { edges: e })
6✔
2221
    }
8✔
2222
}
2223

2224
fn weight_transform_callable(
78✔
2225
    py: Python,
78✔
2226
    map_fn: &Option<PyObject>,
78✔
2227
    value: &PyObject,
78✔
2228
) -> PyResult<PyObject> {
78✔
2229
    match map_fn {
78✔
2230
        Some(map_fn) => {
10✔
2231
            let res = map_fn.call1(py, (value,))?;
10✔
2232
            res.into_py_any(py)
10✔
2233
        }
2234
        None => Ok(value.clone_ref(py)),
68✔
2235
    }
2236
}
78✔
2237

2238
fn _from_adjacency_matrix<'p, T>(
26✔
2239
    py: Python<'p>,
26✔
2240
    matrix: PyReadonlyArray2<'p, T>,
26✔
2241
    null_value: T,
26✔
2242
) -> PyResult<PyGraph>
26✔
2243
where
26✔
2244
    T: Copy + std::cmp::PartialEq + numpy::Element + pyo3::IntoPyObject<'p> + IsNan,
26✔
2245
{
2246
    let array = matrix.as_array();
26✔
2247
    let shape = array.shape();
26✔
2248
    let mut out_graph = StablePyGraph::<Undirected>::default();
26✔
2249
    let _node_indices: Vec<NodeIndex> = (0..shape[0])
26✔
2250
        .map(|node| Ok(out_graph.add_node(node.into_py_any(py)?)))
272✔
2251
        .collect::<PyResult<Vec<NodeIndex>>>()?;
26✔
2252
    for (index, row) in array.axis_iter(Axis(0)).enumerate() {
272✔
2253
        let source_index = NodeIndex::new(index);
272✔
2254
        for (target_index, elem) in row.iter().enumerate() {
20,216✔
2255
            if target_index < index {
20,216✔
2256
                continue;
9,972✔
2257
            }
10,244✔
2258
            if null_value.is_nan() {
10,244✔
2259
                if !elem.is_nan() {
24✔
2260
                    out_graph.add_edge(
8✔
2261
                        source_index,
8✔
2262
                        NodeIndex::new(target_index),
8✔
2263
                        elem.into_py_any(py)?,
8✔
2264
                    );
2265
                }
16✔
2266
            } else if *elem != null_value {
10,220✔
2267
                out_graph.add_edge(
9,430✔
2268
                    source_index,
9,430✔
2269
                    NodeIndex::new(target_index),
9,430✔
2270
                    elem.into_py_any(py)?,
9,430✔
2271
                );
2272
            }
790✔
2273
        }
2274
    }
2275
    Ok(PyGraph {
26✔
2276
        graph: out_graph,
26✔
2277
        node_removed: false,
26✔
2278
        multigraph: true,
26✔
2279
        attrs: py.None(),
26✔
2280
    })
26✔
2281
}
26✔
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