Skip to content

Commit

Permalink
Add isolates() function to rustworkx (#998)
Browse files Browse the repository at this point in the history
* Add isolates() function to rustworkx

This commit adds a new function, isolates(), which is used to find all
the isolates in a graph.

* Move implementation to rustworkx-core

* Add missing isolates module

* Update rustworkx-core/src/connectivity/isolates.rs

---------

Co-authored-by: Edwin Navarro <enavarro@comcast.net>
  • Loading branch information
mtreinish and enavarro51 committed Oct 12, 2023
1 parent 2062a1c commit 274b004
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 0 deletions.
8 changes: 8 additions & 0 deletions releasenotes/notes/add-isolates-edc5da3a3d8fb4fd.yaml
Original file line number Diff line number Diff line change
@@ -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).
144 changes: 144 additions & 0 deletions rustworkx-core/src/connectivity/isolates.rs
Original file line number Diff line number Diff line change
@@ -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::<i32, i32>::from_edges(&edge_list);
/// graph.add_node(10);
/// graph.add_node(11);
/// let res: Vec<usize> = isolates(&graph).into_iter().map(|x| x.index()).collect();
/// assert_eq!(res, [10, 11])
/// ```
pub fn isolates<G>(graph: G) -> Vec<G::NodeId>
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::<i32, i32>::new();
let res: Vec<NodeIndex> = isolates(&graph);
assert_eq!(res, []);
}

#[test]
fn test_isolates_undirected_empty() {
let graph = UnGraph::<i32, i32>::default();
let res: Vec<NodeIndex> = isolates(&graph);
assert_eq!(res, []);
}

#[test]
fn test_isolates_directed_no_isolates() {
let graph = DiGraph::<i32, i32>::from_edges([(0, 1), (1, 2)]);
let res: Vec<NodeIndex> = isolates(&graph);
assert_eq!(res, []);
}

#[test]
fn test_isolates_undirected_no_isolates() {
let graph = UnGraph::<i32, i32>::from_edges([(0, 1), (1, 2)]);
let res: Vec<NodeIndex> = 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::<i32, i32>::from_edges(&edge_list);
graph.add_node(10);
graph.add_node(11);
let res: Vec<usize> = 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::<i32, i32>::from_edges(&edge_list);
graph.add_node(10);
graph.add_node(11);
let res: Vec<usize> = isolates(&graph).into_iter().map(|x| x.index()).collect();
assert_eq!(res, [10, 11])
}
}
2 changes: 2 additions & 0 deletions rustworkx-core/src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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;
17 changes: 17 additions & 0 deletions rustworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 35 additions & 0 deletions src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,3 +1000,38 @@ pub fn chain_decomposition(graph: graph::PyGraph, source: Option<usize>) -> 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(),
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))?;
Expand Down
47 changes: 47 additions & 0 deletions tests/rustworkx_tests/digraph/test_isolates.py
Original file line number Diff line number Diff line change
@@ -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, [])
37 changes: 37 additions & 0 deletions tests/rustworkx_tests/graph/test_isolates.py
Original file line number Diff line number Diff line change
@@ -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, [])

0 comments on commit 274b004

Please sign in to comment.