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..952d461485 --- /dev/null +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -0,0 +1,56 @@ +--- +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) \ 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..6e580e178c 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,37 @@ 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> +/// * `self_cycle_filter` - Specifies the behavior when a single node cycle +/// is found. +/// * `cycle_filter` - Specifies the behavior when a cycle is found +fn inner_cycle_basis( + graph: G, + root: Option, + self_cycle_filter: impl Fn(G, G::NodeId) -> Vec, + cycle_filter: impl Fn( + G, + &HashSet, + &HashMap, + G::NodeId, + G::NodeId, + ) -> Vec, +) -> Vec> where G: NodeCount, G: IntoNeighbors, G: IntoNodeIdentifiers, + T: Eq + Hash, G::NodeId: 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: Vec> = Vec::new(); + 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 +83,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 +95,12 @@ where used.insert(neighbor, temp_set); // A self loop: } else if z == neighbor { - let cycle: Vec = vec![z]; - cycles.push(cycle); + cycles.push(self_cycle_filter(graph, z)) // 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) { - cycle.push(*p); - p = pred.get(p).unwrap(); - } - cycle.push(*p); - cycles.push(cycle); - let neighbor_set = used.get_mut(&neighbor).unwrap(); + cycles.push(cycle_filter(graph, prev_n, &pred, z, neighbor)); + let neighbor_set: &mut HashSet = used.get_mut(&neighbor).unwrap(); neighbor_set.insert(z); } } @@ -116,15 +115,166 @@ where cycles } +/// 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: IntoNeighbors, + G: IntoNodeIdentifiers, + G::NodeId: Eq + Hash, +{ + // inner_cycle_basis(graph, root, false).unwrap_nodes() + let self_cycle_filter = |_graph: G, node: G::NodeId| -> Vec { vec![node] }; + let cycle_filter = |_graph: G, + prev_n: &HashSet, + pred_to_node: &HashMap, + origin: G::NodeId, + neighbor: G::NodeId| + -> Vec { + let mut p = pred_to_node.get(&origin).unwrap(); + // Append neighbor and z to cycle. + let mut cycle: Vec = vec![neighbor, origin]; + while !prev_n.contains(p) { + cycle.push(*p); + p = pred_to_node.get(p).unwrap(); + } + cycle.push(*p); + cycle + }; + inner_cycle_basis(graph, root, self_cycle_filter, cycle_filter) +} + +/// 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, +{ + /// 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_map(|edge: G::EdgeRef| (edge.target() == target).then_some(edge.id())) + .next() + .expect("An edge should exist between origin and target node") + } + + // inner_cycle_basis(graph, root, false).unwrap_nodes() + let self_cycle_filter = + |graph: G, node: G::NodeId| -> Vec { vec![get_edge_between(graph, node, node)] }; + let cycle_filter = |graph: G, + prev_n: &HashSet, + pred_to_node: &HashMap, + origin: G::NodeId, + neighbor: G::NodeId| + -> Vec { + let mut p = pred_to_node.get(&origin).unwrap(); + let mut cycle: Vec = Vec::new(); + // Retrieve all edges from z to neighbor and push to cycle + cycle.push(get_edge_between(graph, origin, neighbor)); + + // Make last p_node == z + let mut prev_p: &G::NodeId = &origin; + // 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_to_node.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)); + cycle + }; + inner_cycle_basis(graph, root, self_cycle_filter, cycle_filter) +} + #[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 +308,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 +359,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 d773a63bbf..4208228012 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::{SimpleCycleIter, johnson_simple_cycles}; diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index bdd554d8cd..b65f01d9e8 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -101,6 +101,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 0df0ee667d..aad3d33638 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -299,6 +299,7 @@ def graph_adjacency_matrix( node_list: Sequence[int] | None = ..., ) -> 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 335f87e070..17bc6e33d2 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 9a333075d2..0df6613d56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -626,6 +626,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)