diff --git a/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml new file mode 100644 index 000000000..d1bf9272b --- /dev/null +++ b/releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml @@ -0,0 +1,8 @@ +--- +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/isolates.rs b/rustworkx-core/src/connectivity/isolates.rs new file mode 100644 index 000000000..df1a5b7c2 --- /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 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. +/// +/// 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]) + } +} 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/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..8274538c7 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -1000,3 +1000,38 @@ 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: connectivity::isolates(&graph.graph) + .into_iter() + .map(|x| x.index()) + .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: connectivity::isolates(&graph.graph) + .into_iter() + .map(|x| x.index()) + .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, [])