From 8188a37a6169011f877ec667a40eb569704f5cf5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 9 Oct 2023 09:45:32 -0400 Subject: [PATCH 1/4] Add isolates() function to rustworkx This commit adds a new function, isolates(), which is used to find all the isolates in a graph. --- .../notes/add-isolates-edc5da3a3d8fb4fd.yaml | 5 ++ rustworkx/__init__.py | 17 +++++++ src/connectivity/mod.rs | 49 +++++++++++++++++++ src/lib.rs | 2 + .../rustworkx_tests/digraph/test_isolates.py | 47 ++++++++++++++++++ tests/rustworkx_tests/graph/test_isolates.py | 37 ++++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml create mode 100644 tests/rustworkx_tests/digraph/test_isolates.py create mode 100644 tests/rustworkx_tests/graph/test_isolates.py diff --git a/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml new file mode 100644 index 000000000..da5ff8035 --- /dev/null +++ b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a new function, :func:`~.isolates`, which is used to find the isolates + (nodes with a degree of 0) in a :class:`~.PyDiGraph` or :class:`~.PyGraph`. diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 48f65fde0..b97eada4f 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -2054,3 +2054,20 @@ def longest_simple_path(graph): longest_simple_path.register(PyDiGraph, digraph_longest_simple_path) longest_simple_path.register(PyGraph, graph_longest_simple_path) + + +@functools.singledispatch +def isolates(graph): + """Return a list of isolates in a graph object + + An isolate is a node without any neighbors meaning it has a degree of 0. For + directed graphs this means the in-degree and out-degree are both 0. + + :param graph: The input graph to find isolates in + :returns: A list of node indices for isolates in the graph + :rtype: NodeIndices + """ + + +isolates.register(PyDiGraph, digraph_isolates) +isolates.register(PyGraph, graph_isolates) diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 7d38b3cf9..fd94b619b 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -1000,3 +1000,52 @@ pub fn chain_decomposition(graph: graph::PyGraph, source: Option) -> Chai .collect(), } } + +/// Return a list of isolates in a :class:`~.PyGraph` object +/// +/// An isolate is a node without any neighbors meaning it has a degree of 0. +/// +/// :param PyGraph graph: The input graph to find isolates in +/// :returns: A list of node indices for isolates in the graph +/// :rtype: NodeIndices +#[pyfunction] +pub fn graph_isolates(graph: graph::PyGraph) -> NodeIndices { + NodeIndices { + nodes: graph + .graph + .node_indices() + .filter_map(|x| { + if graph.graph.neighbors(x).next().is_none() { + Some(x.index()) + } else { + None + } + }) + .collect(), + } +} + +/// Return a list of isolates in a :class:`~.PyGraph` object +/// +/// An isolate is a node without any neighbors meaning it has an in-degree +/// and out-degree of 0. +/// +/// :param PyGraph graph: The input graph to find isolates in +/// :returns: A list of node indices for isolates in the graph +/// :rtype: NodeIndices +#[pyfunction] +pub fn digraph_isolates(graph: digraph::PyDiGraph) -> NodeIndices { + NodeIndices { + nodes: graph + .graph + .node_indices() + .filter_map(|x| { + if graph.graph.neighbors_undirected(x).next().is_none() { + Some(x.index()) + } else { + None + } + }) + .collect(), + } +} diff --git a/src/lib.rs b/src/lib.rs index d34e31f10..6f889be85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -498,6 +498,8 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(articulation_points))?; m.add_wrapped(wrap_pyfunction!(biconnected_components))?; m.add_wrapped(wrap_pyfunction!(chain_decomposition))?; + m.add_wrapped(wrap_pyfunction!(graph_isolates))?; + m.add_wrapped(wrap_pyfunction!(digraph_isolates))?; m.add_wrapped(wrap_pyfunction!(is_planar))?; m.add_wrapped(wrap_pyfunction!(read_graphml))?; m.add_wrapped(wrap_pyfunction!(digraph_node_link_json))?; diff --git a/tests/rustworkx_tests/digraph/test_isolates.py b/tests/rustworkx_tests/digraph/test_isolates.py new file mode 100644 index 000000000..bb1993bb8 --- /dev/null +++ b/tests/rustworkx_tests/digraph/test_isolates.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import rustworkx + + +class TestIsolates(unittest.TestCase): + def test_isolates(self): + graph = rustworkx.PyDiGraph() + graph.add_nodes_from(range(4)) + graph.add_edge(0, 1, None) + res = rustworkx.isolates(graph) + self.assertEqual(res, [2, 3]) + + def test_isolates_with_holes(self): + graph = rustworkx.PyDiGraph() + graph.add_nodes_from(range(4)) + graph.add_edge(0, 1, None) + graph.remove_node(2) + res = rustworkx.isolates(graph) + self.assertEqual(res, [3]) + + def test_isolates_empty_graph(self): + graph = rustworkx.PyDiGraph() + res = rustworkx.isolates(graph) + self.assertEqual(res, []) + + def test_isolates_outgoing_star(self): + graph = rustworkx.generators.directed_star_graph(5) + res = rustworkx.isolates(graph) + self.assertEqual(res, []) + + def test_isolates_incoming_star(self): + graph = rustworkx.generators.directed_star_graph(5, inward=True) + res = rustworkx.isolates(graph) + self.assertEqual(res, []) diff --git a/tests/rustworkx_tests/graph/test_isolates.py b/tests/rustworkx_tests/graph/test_isolates.py new file mode 100644 index 000000000..fc21911cc --- /dev/null +++ b/tests/rustworkx_tests/graph/test_isolates.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import rustworkx + + +class TestIsolates(unittest.TestCase): + def test_isolates(self): + graph = rustworkx.PyGraph() + graph.add_nodes_from(range(4)) + graph.add_edge(0, 1, None) + res = rustworkx.isolates(graph) + self.assertEqual(res, [2, 3]) + + def test_isolates_with_holes(self): + graph = rustworkx.PyGraph() + graph.add_nodes_from(range(4)) + graph.add_edge(0, 1, None) + graph.remove_node(2) + res = rustworkx.isolates(graph) + self.assertEqual(res, [3]) + + def test_isolates_empty_graph(self): + graph = rustworkx.PyGraph() + res = rustworkx.isolates(graph) + self.assertEqual(res, []) From b684aed73ad248c2d6a3cf6f6f60a34ca4d1eb5e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 9 Oct 2023 17:14:44 -0400 Subject: [PATCH 2/4] Move implementation to rustworkx-core --- .../notes/add-isolates-edc5da3a3d8fb4fd.yaml | 3 +++ rustworkx-core/src/connectivity/mod.rs | 2 ++ src/connectivity/mod.rs | 26 +++++-------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml index da5ff8035..d1bf9272b 100644 --- a/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml +++ b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml @@ -3,3 +3,6 @@ features: - | Added a new function, :func:`~.isolates`, which is used to find the isolates (nodes with a degree of 0) in a :class:`~.PyDiGraph` or :class:`~.PyGraph`. + - | + Added a new function, ``isolates()`` to the rustworkx-core ``rustworkx_core::connectivity`` + module which is used to find the isolates (nodes with a degree of 0). diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index b66405c55..bc5851324 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -19,6 +19,7 @@ mod conn_components; mod core_number; mod cycle_basis; mod find_cycle; +mod isolates; mod min_cut; pub use all_simple_paths::{ @@ -32,4 +33,5 @@ pub use conn_components::number_connected_components; pub use core_number::core_number; pub use cycle_basis::cycle_basis; pub use find_cycle::find_cycle; +pub use isolates::isolates; pub use min_cut::stoer_wagner_min_cut; diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index fd94b619b..8274538c7 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -1011,16 +1011,9 @@ pub fn chain_decomposition(graph: graph::PyGraph, source: Option) -> Chai #[pyfunction] pub fn graph_isolates(graph: graph::PyGraph) -> NodeIndices { NodeIndices { - nodes: graph - .graph - .node_indices() - .filter_map(|x| { - if graph.graph.neighbors(x).next().is_none() { - Some(x.index()) - } else { - None - } - }) + nodes: connectivity::isolates(&graph.graph) + .into_iter() + .map(|x| x.index()) .collect(), } } @@ -1036,16 +1029,9 @@ pub fn graph_isolates(graph: graph::PyGraph) -> NodeIndices { #[pyfunction] pub fn digraph_isolates(graph: digraph::PyDiGraph) -> NodeIndices { NodeIndices { - nodes: graph - .graph - .node_indices() - .filter_map(|x| { - if graph.graph.neighbors_undirected(x).next().is_none() { - Some(x.index()) - } else { - None - } - }) + nodes: connectivity::isolates(&graph.graph) + .into_iter() + .map(|x| x.index()) .collect(), } } From 4eb37d4c28f36f84247d72b8804b5dc6ab499374 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 9 Oct 2023 18:55:35 -0400 Subject: [PATCH 3/4] Add missing isolates module --- rustworkx-core/src/connectivity/isolates.rs | 144 ++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 rustworkx-core/src/connectivity/isolates.rs diff --git a/rustworkx-core/src/connectivity/isolates.rs b/rustworkx-core/src/connectivity/isolates.rs new file mode 100644 index 000000000..6eaa25cec --- /dev/null +++ b/rustworkx-core/src/connectivity/isolates.rs @@ -0,0 +1,144 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use petgraph::visit::{IntoNeighborsDirected, IntoNodeIdentifiers, NodeIndexable}; +use petgraph::Direction::{Incoming, Outgoing}; + +/// Return the fisolates in a graph object +/// +/// An isolate is a node without any neighbors meaning it has a degree of 0. For +/// directed graphs this means the in-degree and out-degree are both 0. +/// +/// Arguments: +/// +/// * `graph` - The graph in which to find the isolates. +/// +/// # Example +/// ```rust +/// use petgraph::prelude::*; +/// use rustworkx_core::connectivity::isolates; +/// +/// let edge_list = vec![ +/// (0, 1), +/// (3, 0), +/// (0, 5), +/// (8, 0), +/// (1, 2), +/// (1, 6), +/// (2, 3), +/// (3, 4), +/// (4, 5), +/// (6, 7), +/// (7, 8), +/// (8, 9), +/// ]; +/// let mut graph = DiGraph::::from_edges(&edge_list); +/// graph.add_node(10); +/// graph.add_node(11); +/// let res: Vec = isolates(&graph).into_iter().map(|x| x.index()).collect(); +/// assert_eq!(res, [10, 11]) +/// ``` +pub fn isolates(graph: G) -> Vec +where + G: NodeIndexable + IntoNodeIdentifiers + IntoNeighborsDirected, +{ + graph + .node_identifiers() + .filter(|x| { + graph + .neighbors_directed(*x, Incoming) + .chain(graph.neighbors_directed(*x, Outgoing)) + .next() + .is_none() + }) + .collect() +} + +#[cfg(test)] +mod tests { + use crate::connectivity::isolates; + use petgraph::prelude::*; + + #[test] + fn test_isolates_directed_empty() { + let graph = DiGraph::::new(); + let res: Vec = isolates(&graph); + assert_eq!(res, []); + } + + #[test] + fn test_isolates_undirected_empty() { + let graph = UnGraph::::default(); + let res: Vec = isolates(&graph); + assert_eq!(res, []); + } + + #[test] + fn test_isolates_directed_no_isolates() { + let graph = DiGraph::::from_edges([(0, 1), (1, 2)]); + let res: Vec = isolates(&graph); + assert_eq!(res, []); + } + + #[test] + fn test_isolates_undirected_no_isolates() { + let graph = UnGraph::::from_edges([(0, 1), (1, 2)]); + let res: Vec = isolates(&graph); + assert_eq!(res, []); + } + + #[test] + fn test_isolates_directed() { + let edge_list = vec![ + (0, 1), + (3, 0), + (0, 5), + (8, 0), + (1, 2), + (1, 6), + (2, 3), + (3, 4), + (4, 5), + (6, 7), + (7, 8), + (8, 9), + ]; + let mut graph = DiGraph::::from_edges(&edge_list); + graph.add_node(10); + graph.add_node(11); + let res: Vec = isolates(&graph).into_iter().map(|x| x.index()).collect(); + assert_eq!(res, [10, 11]) + } + + #[test] + fn test_isolates_undirected() { + let edge_list = vec![ + (0, 1), + (3, 0), + (0, 5), + (8, 0), + (1, 2), + (1, 6), + (2, 3), + (3, 4), + (4, 5), + (6, 7), + (7, 8), + (8, 9), + ]; + let mut graph = UnGraph::::from_edges(&edge_list); + graph.add_node(10); + graph.add_node(11); + let res: Vec = isolates(&graph).into_iter().map(|x| x.index()).collect(); + assert_eq!(res, [10, 11]) + } +} From 8507062d459bc3715b25f19811435232849ca941 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 11 Oct 2023 13:56:38 -0400 Subject: [PATCH 4/4] Update rustworkx-core/src/connectivity/isolates.rs --- rustworkx-core/src/connectivity/isolates.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustworkx-core/src/connectivity/isolates.rs b/rustworkx-core/src/connectivity/isolates.rs index 6eaa25cec..df1a5b7c2 100644 --- a/rustworkx-core/src/connectivity/isolates.rs +++ b/rustworkx-core/src/connectivity/isolates.rs @@ -13,7 +13,7 @@ use petgraph::visit::{IntoNeighborsDirected, IntoNodeIdentifiers, NodeIndexable}; use petgraph::Direction::{Incoming, Outgoing}; -/// Return the fisolates in a graph object +/// Return the isolates in a graph object /// /// An isolate is a node without any neighbors meaning it has a degree of 0. For /// directed graphs this means the in-degree and out-degree are both 0.