diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index d1ecb165e1..56b44f0394 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -17,6 +17,7 @@ Connectivity and Cycles rustworkx.weakly_connected_components rustworkx.is_weakly_connected rustworkx.cycle_basis + rustworkx.cycle_basis_edges rustworkx.simple_cycles rustworkx.digraph_find_cycle rustworkx.articulation_points diff --git a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml new file mode 100644 index 0000000000..948e79aee4 --- /dev/null +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -0,0 +1,61 @@ +--- +features: + - | + A function, ``cycle_basis_edges`` was added to the crate + ``rustworkx-core`` in the ``connectivity`` module. This function returns + the edge indices that form the cycle basis of a graph. + - | + Added a new function :func:`~rustworkx.cycle_basis_edges` which is similar + to the existing :func:`~.cycle_basis` function but instead of returning node + indices it returns a list of edge indices for the cycle. + + .. jupyter-execute:: + + import rustworkx + from rustworkx.visualization import * # Needs matplotlib/ + + graph = rustworkx.PyGraph() + + # Each time add node is called, it returns a new node index + a = graph.add_node("A") + b = graph.add_node("B") + c = graph.add_node("C") + d = graph.add_node("D") + e = graph.add_node("E") + f = graph.add_node("F") + g = graph.add_node("G") + h = graph.add_node("H") + i = graph.add_node("I") + j = graph.add_node("J") + + # add_edges_from takes tuples of node indices and weights, + # and returns edge indices + graph.add_edges_from([ + (a, b, 1.5), + (a, a, 1.0), + (a, c, 5.0), + (b, c, 2.5), + (c, d, 1.3), + (d, e, 0.8), + (e, f, 1.6), + (f, d, 0.7), + (e, g, 0.7), + (g, h, 0.9), + (g, i, 1.0), + (i, j, 0.8), + ]) + + mpl_draw(graph, with_labels=True) + # Retrieve EdgeIDs by enabling the edges flag. + cycles_edges = rustworkx.cycle_basis_edges(graph, a) + edge_list = list(graph.edge_list()) + cycle_info = [[edge_list[edge] for edge in cycle] for cycle in cycles_edges] + # Print the EdgeID's that form cycles in the graph + display(cycles_edges) + # Print the data retrieved from the graph. + display(cycle_info) +upgrade: + - | + The trait bounds of :func:`rustworkx_core::connectivity::cycle_basis` now + requires graphs to be compatible with the trait ``IntoEdges`` and that the + attribute ``EdgeId`` conforms to `Eq` and `Hash`. \ No newline at end of file diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 380fddb8d6..36e9d14bb4 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -11,10 +11,12 @@ // under the License. use hashbrown::{HashMap, HashSet}; -use petgraph::visit::{IntoNeighbors, IntoNodeIdentifiers, NodeCount}; +use petgraph::visit::{EdgeRef, IntoEdges, IntoNeighbors, IntoNodeIdentifiers, NodeCount}; use std::hash::Hash; -/// Return a list of cycles which form a basis for cycles of a given graph. +/// Inner private function for `cycle_basis` and `cycle_basis_edges`. +/// Returns a list of cycles which forms a basis of cycles of a given +/// graph. /// /// A basis for cycles of a graph is a minimal collection of /// cycles such that any cycle in the graph can be written @@ -29,32 +31,44 @@ use std::hash::Hash; /// It may produce incorrect/unexpected results if the input graph has /// parallel edges. /// -/// /// Arguments: /// /// * `graph` - The graph in which to find the basis. /// * `root` - Optional node index for starting the basis search. If not /// specified, an arbitrary node is chosen. -/// -/// # Example -/// ```rust -/// use petgraph::prelude::*; -/// use rustworkx_core::connectivity::cycle_basis; -/// -/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)]; -/// let graph = UnGraph::::from_edges(&edge_list); -/// let mut res: Vec> = cycle_basis(&graph, Some(NodeIndex::new(0))); -/// ``` -pub fn cycle_basis(graph: G, root: Option) -> Vec> +/// * `edges` - bool for when the user requests the edges instead +/// of the nodes of the cycles. +fn inner_cycle_basis( + graph: G, + root: Option, + edges: bool, +) -> EdgesOrNodes where G: NodeCount, G: IntoNeighbors, + G: IntoEdges, G: IntoNodeIdentifiers, G::NodeId: Eq + Hash, + G::EdgeId: Eq + Hash, { - let mut root_node = root; + let mut root_node: Option = root; let mut graph_nodes: HashSet = graph.node_identifiers().collect(); - let mut cycles: Vec> = Vec::new(); + let mut cycles_edges: Vec> = Vec::new(); + let mut cycles_nodes: Vec> = Vec::new(); + + /// Method used to retrieve all the edges between an origin node and a target node. + fn get_edge_between(orig_graph: G, origin: G::NodeId, target: G::NodeId) -> G::EdgeId + where + G: IntoEdges, + { + orig_graph + .edges(origin) + .filter(|edge: &G::EdgeRef| edge.target() == target) + .map(|edge: G::EdgeRef| edge.id()) + .next() + .unwrap() + } + while !graph_nodes.is_empty() { let temp_value: G::NodeId; // If root_node is not set get an arbitrary node from the set of graph @@ -76,8 +90,8 @@ where let mut used: HashMap> = HashMap::new(); used.insert(root_index, HashSet::new()); // Walk the spanning tree - // Use the last element added so that cycles are easier to find while let Some(z) = stack.pop() { + // Use the last element added so that cycles are easier to find for neighbor in graph.neighbors(z) { // A new node was encountered: if !used.contains_key(&neighbor) { @@ -88,20 +102,50 @@ where used.insert(neighbor, temp_set); // A self loop: } else if z == neighbor { - let cycle: Vec = vec![z]; - cycles.push(cycle); + if edges { + let cycle_edge: Vec = vec![get_edge_between(graph, z, z)]; + cycles_edges.push(cycle_edge); + } else { + let cycle: Vec = vec![z]; + cycles_nodes.push(cycle); + } // A cycle was found: } else if !used.get(&z).unwrap().contains(&neighbor) { let prev_n = used.get(&neighbor).unwrap(); - let mut cycle: Vec = vec![neighbor, z]; let mut p = pred.get(&z).unwrap(); - while !prev_n.contains(p) { + if edges { + let mut cycle: Vec = Vec::new(); + // Retrieve all edges from z to neighbor and push to cycle + cycle.push(get_edge_between(graph, z, neighbor)); + + // Make last p_node == z + let mut prev_p: &G::NodeId = &z; + // While p is in the neighborhood of neighbor + while !prev_n.contains(p) { + // Retrieve all edges from prev_p to p and vice versa append to cycle + cycle.push(get_edge_between(graph, *prev_p, *p)); + // Update prev_p to p + prev_p = p; + // Retrieve a new predecessor node from p and replace p + p = pred.get(p).unwrap(); + } + // When loop ends add remaining edges from prev_p to p. + cycle.push(get_edge_between(graph, *prev_p, *p)); + // Also retrieve all edges between the last p and neighbor + cycle.push(get_edge_between(graph, *p, neighbor)); + // Once all edges within cycle have been found, push to cycle list. + cycles_edges.push(cycle); + } else { + // Append neighbor and z to cycle. + let mut cycle: Vec = vec![neighbor, z]; + while !prev_n.contains(p) { + cycle.push(*p); + p = pred.get(p).unwrap(); + } cycle.push(*p); - p = pred.get(p).unwrap(); + cycles_nodes.push(cycle); } - cycle.push(*p); - cycles.push(cycle); - let neighbor_set = used.get_mut(&neighbor).unwrap(); + let neighbor_set: &mut HashSet = used.get_mut(&neighbor).unwrap(); neighbor_set.insert(z); } } @@ -113,18 +157,140 @@ where graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); root_node = None; } - cycles + if edges { + EdgesOrNodes::Edges(cycles_edges) + } else { + EdgesOrNodes::Nodes(cycles_nodes) + } +} + +/// Enum for custom return types of `cycle_basis()`. +enum EdgesOrNodes { + Nodes(Vec>), + Edges(Vec>), +} +/// Functions used to unwrap the desired datatype of `EdgesOrNodes`. +impl EdgesOrNodes { + fn unwrap_nodes(self) -> Vec> { + match self { + Self::Nodes(x) => x, + Self::Edges(_) => unreachable!( + "Function should only return instances of {}.", + std::any::type_name::() + ), + } + } + fn unwrap_edges(self) -> Vec> { + match self { + Self::Edges(x) => x, + Self::Nodes(_) => unreachable!( + "Function should only return instances of {}.", + std::any::type_name::() + ), + } + } +} + +/// Returns lists of `NodeIndex` representing cycles which form +/// a basis for cycles of a given graph. +/// +/// A basis for cycles of a graph is a minimal collection of +/// cycles such that any cycle in the graph can be written +/// as a sum of cycles in the basis. Here summation of cycles +/// is defined as the exclusive-or of the edges. +/// +/// This is adapted from +/// Paton, K. An algorithm for finding a fundamental set of +/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. +/// +/// The function implicitly assumes that there are no parallel edges. +/// It may produce incorrect/unexpected results if the input graph has +/// parallel edges. +/// +/// +/// Arguments: +/// +/// * `graph` - The graph in which to find the basis. +/// * `root` - Optional node index for starting the basis search. If not +/// specified, an arbitrary node is chosen. +/// +/// # Example +/// ```rust +/// use petgraph::prelude::*; +/// use rustworkx_core::connectivity::cycle_basis; +/// +/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)]; +/// let graph = UnGraph::::from_edges(&edge_list); +/// let mut res: Vec> = cycle_basis(&graph, Some(NodeIndex::new(0))); +/// ``` +pub fn cycle_basis(graph: G, root: Option) -> Vec> +where + G: NodeCount, + G: IntoEdges, + G: IntoNodeIdentifiers, + G::NodeId: Eq + Hash, + G::EdgeId: Eq + Hash, +{ + inner_cycle_basis(graph, root, false).unwrap_nodes() +} + +/// Returns lists of `EdgeIndex` representing cycles which form +/// a basis for cycles of a given graph. +/// +/// A basis for cycles of a graph is a minimal collection of +/// cycles such that any cycle in the graph can be written +/// as a sum of cycles in the basis. Here summation of cycles +/// is defined as the exclusive-or of the edges. +/// +/// This is adapted from +/// Paton, K. An algorithm for finding a fundamental set of +/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. +/// +/// The function implicitly assumes that there are no parallel edges. +/// It may produce incorrect/unexpected results if the input graph has +/// parallel edges. +/// +/// +/// Arguments: +/// +/// * `graph` - The graph in which to find the basis. +/// * `root` - Optional node index for starting the basis search. If not +/// specified, an arbitrary node is chosen. +/// +/// # Example +/// ```rust +/// use petgraph::prelude::*; +/// use rustworkx_core::connectivity::cycle_basis_edges; +/// +/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)]; +/// let graph = UnGraph::::from_edges(&edge_list); +/// let mut res: Vec> = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); +/// ``` +pub fn cycle_basis_edges(graph: G, root: Option) -> Vec> +where + G: NodeCount, + G: IntoEdges, + G: IntoNodeIdentifiers, + G::NodeId: Eq + Hash, + G::EdgeId: Eq + Hash, +{ + inner_cycle_basis(graph, root, true).unwrap_edges() } #[cfg(test)] mod tests { use crate::connectivity::cycle_basis; + use crate::connectivity::cycle_basis_edges; use petgraph::prelude::*; + use petgraph::stable_graph::GraphIndex; - fn sorted_cycle(cycles: Vec>) -> Vec> { + fn sorted_cycle(cycles: Vec>) -> Vec> + where + T: GraphIndex, + { let mut sorted_cycles: Vec> = vec![]; for cycle in cycles { - let mut cycle: Vec = cycle.iter().map(|x| x.index()).collect(); + let mut cycle: Vec = cycle.iter().map(|x: &T| x.index()).collect(); cycle.sort(); sorted_cycles.push(cycle); } @@ -158,6 +324,28 @@ mod tests { assert_eq!(sorted_cycle(res_9), expected); } + #[test] + fn test_cycle_edge_basis_source() { + let edge_list = vec![ + (0, 0), + (0, 1), + (1, 2), + (2, 3), + (2, 5), + (5, 6), + (3, 6), + (3, 4), + ]; + let graph = UnGraph::::from_edges(&edge_list); + let expected = vec![vec![0], vec![3, 4, 5, 6]]; + let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); + assert_eq!(sorted_cycle(res_0), expected); + let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2))); + assert_eq!(sorted_cycle(res_1), expected); + let res_9 = cycle_basis_edges(&graph, Some(NodeIndex::new(6))); + assert_eq!(sorted_cycle(res_9), expected); + } + #[test] fn test_self_loop() { let edge_list = vec![ @@ -187,4 +375,34 @@ mod tests { ] ); } + + #[test] + fn test_self_loop_edges() { + let edge_list = vec![ + (0, 1), + (0, 3), + (0, 5), + (0, 8), + (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_edge(NodeIndex::new(1), NodeIndex::new(1), 0); + let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); + assert_eq!( + sorted_cycle(res_0), + vec![ + vec![0, 1, 4, 6], + vec![0, 3, 5, 9, 10], + vec![1, 2, 7, 8], + vec![12], + ] + ); + } } diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index 30d42d509d..d9929d57d0 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -34,6 +34,7 @@ pub use conn_components::connected_components; pub use conn_components::number_connected_components; pub use core_number::core_number; pub use cycle_basis::cycle_basis; +pub use cycle_basis::cycle_basis_edges; pub use find_cycle::find_cycle; pub use isolates::isolates; pub use johnson_simple_cycles::{johnson_simple_cycles, SimpleCycleIter}; diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 918d646435..06852eeded 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -90,6 +90,7 @@ from .rustworkx import weakly_connected_components as weakly_connected_component from .rustworkx import digraph_adjacency_matrix as digraph_adjacency_matrix from .rustworkx import graph_adjacency_matrix as graph_adjacency_matrix from .rustworkx import cycle_basis as cycle_basis +from .rustworkx import cycle_basis_edges as cycle_basis_edges from .rustworkx import articulation_points as articulation_points from .rustworkx import bridges as bridges from .rustworkx import biconnected_components as biconnected_components diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index 4da22541ec..9e334f1c31 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -237,6 +237,7 @@ def graph_adjacency_matrix( parallel_edge: str = ..., ) -> npt.NDArray[np.float64]: ... def cycle_basis(graph: PyGraph, /, root: int | None = ...) -> list[list[int]]: ... +def cycle_basis_edges(graph: PyGraph, /, root: int | None = ...) -> list[list[int]]: ... def articulation_points(graph: PyGraph, /) -> set[int]: ... def bridges(graph: PyGraph, /) -> set[tuple[int]]: ... def biconnected_components(graph: PyGraph, /) -> BiconnectedComponents: ... diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 7951f1486c..d677794cce 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -64,9 +64,10 @@ use rustworkx_core::dag_algo::longest_path; /// /// :param PyGraph graph: The graph to find the cycle basis in /// :param int root: Optional index for starting node for basis +/// :param bool edges: Optional for retrieving edges instead of indices. /// -/// :returns: A list of cycle lists. Each list is a list of node ids which -/// forms a cycle (loop) in the input graph +/// :returns: A list of cycle lists. Each list is a list of node ids +/// which forms a cycle (loop) in the input graph /// :rtype: list /// /// .. [1] Paton, K. An algorithm for finding a fundamental set of @@ -75,6 +76,39 @@ use rustworkx_core::dag_algo::longest_path; #[pyo3(text_signature = "(graph, /, root=None)", signature = (graph, root=None))] pub fn cycle_basis(graph: &graph::PyGraph, root: Option) -> Vec> { connectivity::cycle_basis(&graph.graph, root.map(NodeIndex::new)) + .into_iter() + .map(|res_map| res_map.into_iter().map(|x: NodeIndex| x.index()).collect()) + .collect() +} + +/// Return a list of cycles which form a basis for cycles of a given PyGraph +/// +/// A basis for cycles of a graph is a minimal collection of +/// cycles such that any cycle in the graph can be written +/// as a sum of cycles in the basis. Here summation of cycles +/// is defined as the exclusive or of the edges. +/// +/// This is adapted from algorithm CACM 491 [1]_. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges. +/// It may produce incorrect/unexpected results if the input graph has +/// parallel edges. +/// +/// :param PyGraph graph: The graph to find the cycle basis in +/// :param int root: Optional index for starting node for basis +/// +/// :returns: A list of cycle lists. Each list is a list of edge ids +/// which forms a cycle (loop) in the input graph +/// :rtype: list +/// +/// .. [1] Paton, K. An algorithm for finding a fundamental set of +/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. +#[pyfunction] +#[pyo3(text_signature = "(graph, /, root=None)")] +pub fn cycle_basis_edges(graph: &graph::PyGraph, root: Option) -> Vec> { + connectivity::cycle_basis_edges(&graph.graph, root.map(NodeIndex::new)) .into_iter() .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) .collect() diff --git a/src/lib.rs b/src/lib.rs index 23b7771c1b..95c0777cc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -611,6 +611,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(directed_random_bipartite_graph))?; m.add_wrapped(wrap_pyfunction!(undirected_random_bipartite_graph))?; m.add_wrapped(wrap_pyfunction!(cycle_basis))?; + m.add_wrapped(wrap_pyfunction!(cycle_basis_edges))?; m.add_wrapped(wrap_pyfunction!(simple_cycles))?; m.add_wrapped(wrap_pyfunction!(number_strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?; diff --git a/tests/graph/test_cycle_basis.py b/tests/graph/test_cycle_basis.py index c38e8ece6e..220e6a12cc 100644 --- a/tests/graph/test_cycle_basis.py +++ b/tests/graph/test_cycle_basis.py @@ -67,3 +67,54 @@ def test_self_loop(self): self.graph.add_edge(1, 1, None) res = sorted(sorted(c) for c in rustworkx.cycle_basis(self.graph, 0)) self.assertEqual([[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], [1]], res) + + +class TestCycleBasisEdges(unittest.TestCase): + def setUp(self): + self.graph = rustworkx.PyGraph() + self.graph.add_nodes_from(list(range(10))) + self.graph.add_edges_from_no_data( + [ + (0, 1), + (0, 2), + (1, 2), + (2, 3), + (3, 4), + (3, 5), + (4, 5), + (4, 6), + (6, 7), + (6, 8), + (8, 9), + ] + ) + + def test_cycle_basis_edges(self): + graph = self.graph + res = sorted(sorted(c) for c in rustworkx.cycle_basis_edges(graph, 0)) + self.assertEqual([[0, 1, 2], [4, 5, 6]], res) + + def test_cycle_basis_edges_multiple_roots_same_cycles(self): + res = sorted(sorted(x) for x in rustworkx.cycle_basis_edges(self.graph, 0)) + self.assertEqual([[0, 1, 2], [4, 5, 6]], res) + res = sorted(sorted(x) for x in rustworkx.cycle_basis_edges(self.graph, 5)) + self.assertEqual([[0, 1, 2], [4, 5, 6]], res) + res = sorted(sorted(x) for x in rustworkx.cycle_basis_edges(self.graph, 7)) + self.assertEqual([[0, 1, 2], [4, 5, 6]], res) + + def test_cycle_basis_edges_disconnected_graphs(self): + self.graph.add_nodes_from(["A", "B", "C"]) + self.graph.add_edges_from_no_data([(10, 11), (10, 12), (11, 12)]) + cycles = rustworkx.cycle_basis_edges(self.graph, 9) + res = sorted(sorted(x) for x in cycles[:-1]) + [sorted(cycles[-1])] + self.assertEqual(res, [[0, 1, 2], [4, 5, 6], [11, 12, 13]]) + + def test_invalid_types(self): + digraph = rustworkx.PyDiGraph() + with self.assertRaises(TypeError): + rustworkx.cycle_basis_edges(digraph) + + def test_self_loop(self): + self.graph.add_edge(1, 1, None) + res = sorted(sorted(c) for c in rustworkx.cycle_basis_edges(self.graph, 0)) + self.assertEqual([[0, 1, 2], [4, 5, 6], [11]], res)