Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isolates() function to rustworkx #998

Merged
merged 5 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 fisolates in a graph object
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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, [])