From 62f26f9403f6c06199158e26f3940b0df0368f17 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 31 May 2023 13:31:40 -0400 Subject: [PATCH 01/22] Initial implementation of cycle_basis_edges - Inspired from the existing procedure `cycle_basis`. - Modified to return a tuple of `NodeId` in format `(source, target)`. - Added API calls so that method works in Python. --- .../connectivity_and_cycles.rst | 1 + .../src/connectivity/cycle_basis.rs | 225 +++++++++++++++++- rustworkx-core/src/connectivity/mod.rs | 1 + src/connectivity/mod.rs | 38 +++ src/lib.rs | 1 + 5 files changed, 265 insertions(+), 1 deletion(-) diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index b4f08f5ad0..f58f9828b6 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -15,6 +15,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/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 630d24c13b..c3885a68ae 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -11,7 +11,7 @@ // 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. @@ -116,9 +116,160 @@ where cycles } +/// Return a list of edges 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_edges(&graph, Some(NodeIndex::new(0))); +/// ``` +pub fn cycle_basis_edges(graph: G, root: Option) -> Vec> +where + G: NodeCount, + G: IntoNeighbors, + G: IntoEdges, + G: IntoNodeIdentifiers, + G::NodeId: Eq + Hash, +{ + let mut root_node = root; + let mut graph_nodes: HashSet = graph.node_identifiers().collect(); + + 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 + // nodes we've not "examined" + let root_index = match root_node { + Some(root_node) => root_node, + None => { + temp_value = *graph_nodes.iter().next().unwrap(); + graph_nodes.remove(&temp_value); + temp_value + } + }; + // Stack (ie "pushdown list") of vertices already in the spanning tree + let mut stack: Vec = vec![root_index]; + // Map of node index to predecessor node index + let mut pred: HashMap = HashMap::new(); + pred.insert(root_index, root_index); + // Set of examined nodes during this iteration + let mut used: HashMap> = HashMap::new(); + used.insert(root_index, HashSet::new()); + // Walk the spanning tree + while !stack.is_empty() { + // Use the last element added so that cycles are easier to find + let z = stack.pop().unwrap(); + // println!("Length of all edges from: {:?}", edges.len()); + for neighbor in graph.neighbors(z) { + // A new node was encountered: + if !used.contains_key(&neighbor) { + pred.insert(neighbor, z); + stack.push(neighbor); + let mut temp_set: HashSet = HashSet::new(); + temp_set.insert(z); + used.insert(neighbor, temp_set); + // A self loop: + } else if z == neighbor { + let cycle_edge: Vec<(G::NodeId, G::NodeId)> = graph + .edges(z) + .filter(|edge: &G::EdgeRef| edge.source() == z && edge.target() == z) + .map(|edge: G::EdgeRef| (edge.source(), edge.target())) + .collect(); + cycles.push(cycle_edge); + // A cycle was found: + } else if !used.get(&z).unwrap().contains(&neighbor) { + let pn = used.get(&neighbor).unwrap(); + let mut cycle: Vec<(G::NodeId, G::NodeId)> = vec![]; + let mut p = pred.get(&z).unwrap(); + // Retreive all edges from z to neighbor and vice versa + let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph + .edges(z) + .filter(|edge: &G::EdgeRef| { + edge.source() == neighbor || edge.target() == neighbor + }) + .map(|edge: G::EdgeRef| (edge.source(), edge.target())) + .collect(); + // Append to cycle + cycle.append(&mut neigh_edge); + // Make last p_node == z + let mut prev_p: &G::NodeId = &z; + // While p is in the neighborhood of neighbor + while !pn.contains(p) { + // Retrieve all edges from prev_p to p and vice versa + let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph + .edges(*prev_p) + .filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p) + .map(|edge: G::EdgeRef| (edge.source(), edge.target())) + .collect(); + // Append to cycle + cycle.append(&mut neigh_edge); + // Update prev_p to p + prev_p = p; + // Retreive 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. + let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph + .edges(*prev_p) + .filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p) + .map(|edge: G::EdgeRef| (edge.source(), edge.target())) + .collect(); + cycle.append(&mut neigh_edge); + // Also retreive all edges between the last p and neighbor + let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph + .edges(*p) + .filter(|edge: &G::EdgeRef| { + edge.target() == neighbor || edge.source() == neighbor + }) + .map(|edge: G::EdgeRef| (edge.source(), edge.target())) + .collect(); + cycle.append(&mut neigh_edge); + + // Once all edges within cycle have been found, push to cycle list. + cycles.push(cycle); + let neighbor_set: &mut HashSet = used.get_mut(&neighbor).unwrap(); + neighbor_set.insert(z); + } + } + } + let mut temp_hashset: HashSet = HashSet::new(); + for key in pred.keys() { + temp_hashset.insert(*key); + } + graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); + root_node = None; + } + cycles +} + #[cfg(test)] mod tests { use crate::connectivity::cycle_basis; + use crate::connectivity::cycle_basis_edges; use petgraph::prelude::*; fn sorted_cycle(cycles: Vec>) -> Vec> { @@ -132,6 +283,26 @@ mod tests { sorted_cycles } + fn sorted_cycle_edges(cycles: Vec>) -> Vec> { + let mut sorted_cycles: Vec> = vec![]; + for cycle in cycles { + let mut cycle: Vec<(usize, usize)> = cycle + .iter() + .map(|x| { + if x.0 < x.1 { + (x.0.index(), x.1.index()) + } else { + (x.1.index(), x.0.index()) + } + }) + .collect(); + cycle.sort(); + sorted_cycles.push(cycle); + } + sorted_cycles.sort(); + sorted_cycles + } + #[test] fn test_cycle_basis_source() { let edge_list = vec![ @@ -158,6 +329,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, 0)], vec![(2, 3), (2, 5), (3, 6), (5, 6)]]; + let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); + assert_eq!(sorted_cycle_edges(res_0), expected); + let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2))); + assert_eq!(sorted_cycle_edges(res_1), expected); + let res_9 = cycle_basis_edges(&graph, Some(NodeIndex::new(6))); + assert_eq!(sorted_cycle_edges(res_9), expected); + } + #[test] fn test_self_loop() { let edge_list = vec![ @@ -187,4 +380,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_edges(res_0), + vec![ + vec![(0, 1), (0, 3), (1, 2), (2, 3)], + vec![(0, 1), (0, 8), (1, 6), (6, 7), (7, 8)], + vec![(0, 3), (0, 5), (3, 4), (4, 5)], + vec![(1, 1)], + ] + ); + } } diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index b66405c554..6a316407e1 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -31,5 +31,6 @@ 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 min_cut::stoer_wagner_min_cut; diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 7d38b3cf95..51f312a56d 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -74,6 +74,44 @@ pub fn cycle_basis(graph: &graph::PyGraph, root: Option) -> Vec) -> Vec> { + connectivity::cycle_basis_edges(&graph.graph, root.map(NodeIndex::new)) + .into_iter() + .map(|res_map| { + res_map + .into_iter() + .map(|x| (x.0.index(), x.1.index())) + .collect() + }) + .collect() +} + /// Find all simple cycles of a :class:`~.PyDiGraph` /// /// A "simple cycle" (called an elementary circuit in [1]) is a cycle (or closed path) diff --git a/src/lib.rs b/src/lib.rs index 1b15d44592..f8631d0d44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -448,6 +448,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?; m.add_wrapped(wrap_pyfunction!(random_geometric_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!(strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?; From 35eea7e074edc2a31cecaeabe5b35dea295deab4 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 31 May 2023 15:42:24 -0400 Subject: [PATCH 02/22] Hotfix: DocTest now passes --- rustworkx-core/src/connectivity/cycle_basis.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index c3885a68ae..79b506e465 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -141,11 +141,11 @@ where /// # Example /// ```rust /// use petgraph::prelude::*; -/// use rustworkx_core::connectivity::cycle_basis; +/// 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))); +/// let mut res: Vec> = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); /// ``` pub fn cycle_basis_edges(graph: G, root: Option) -> Vec> where From a8c1b67695b3ce8e0a5cfc2327e8b968722ac27a Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:44:02 -0400 Subject: [PATCH 03/22] Correction: Function returns EdgeIndex - Cycle_basis_edges now returns a list of edgeIDs. --- .../src/connectivity/cycle_basis.rs | 99 +++++++++---------- src/connectivity/mod.rs | 13 +-- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 79b506e465..1bc32024c4 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -116,7 +116,8 @@ where cycles } -/// Return a list of edges representing cycles which form a basis for cycles of a given graph. +/// Return a list of edge indices 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 @@ -145,20 +146,45 @@ where /// /// 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))); +/// let mut res: Vec> = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); /// ``` -pub fn cycle_basis_edges(graph: G, root: Option) -> Vec> +pub fn cycle_basis_edges(graph: G, root: Option) -> Vec> where G: NodeCount, G: IntoNeighbors, G: IntoEdges, G: IntoNodeIdentifiers, G::NodeId: Eq + Hash, + G::EdgeId: Eq + Hash, { let mut root_node = root; let mut graph_nodes: HashSet = graph.node_identifiers().collect(); + let mut cycles: Vec> = Vec::new(); + + // Method used to retrieve all the edges between an origin node and a target node. + // Can be used to check if a node loops back to itself by enabling equiv + fn get_edge_between( + orig_graph: G, + origin: G::NodeId, + target: G::NodeId, + equiv: bool, + ) -> Vec + where + G: IntoEdges, + { + orig_graph + .edges(origin) + .filter(|edge: &G::EdgeRef| { + if equiv { + edge.source() == target && edge.target() == target + } else { + edge.source() == target || edge.target() == target + } + }) + .map(|edge: G::EdgeRef| edge.id()) + .collect() + } - 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 @@ -194,25 +220,16 @@ where used.insert(neighbor, temp_set); // A self loop: } else if z == neighbor { - let cycle_edge: Vec<(G::NodeId, G::NodeId)> = graph - .edges(z) - .filter(|edge: &G::EdgeRef| edge.source() == z && edge.target() == z) - .map(|edge: G::EdgeRef| (edge.source(), edge.target())) - .collect(); + let cycle_edge: Vec = get_edge_between(graph, z, z, true); cycles.push(cycle_edge); // A cycle was found: } else if !used.get(&z).unwrap().contains(&neighbor) { let pn = used.get(&neighbor).unwrap(); - let mut cycle: Vec<(G::NodeId, G::NodeId)> = vec![]; + let mut cycle: Vec = vec![]; let mut p = pred.get(&z).unwrap(); // Retreive all edges from z to neighbor and vice versa - let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph - .edges(z) - .filter(|edge: &G::EdgeRef| { - edge.source() == neighbor || edge.target() == neighbor - }) - .map(|edge: G::EdgeRef| (edge.source(), edge.target())) - .collect(); + let mut neigh_edge: Vec = + get_edge_between(graph, z, neighbor, false); // Append to cycle cycle.append(&mut neigh_edge); // Make last p_node == z @@ -220,11 +237,8 @@ where // While p is in the neighborhood of neighbor while !pn.contains(p) { // Retrieve all edges from prev_p to p and vice versa - let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph - .edges(*prev_p) - .filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p) - .map(|edge: G::EdgeRef| (edge.source(), edge.target())) - .collect(); + let mut neigh_edge: Vec = + get_edge_between(graph, *prev_p, *p, false); // Append to cycle cycle.append(&mut neigh_edge); // Update prev_p to p @@ -233,20 +247,12 @@ where p = pred.get(p).unwrap(); } // When loop ends add remaining edges from prev_p to p. - let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph - .edges(*prev_p) - .filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p) - .map(|edge: G::EdgeRef| (edge.source(), edge.target())) - .collect(); + let mut neigh_edge: Vec = + get_edge_between(graph, *prev_p, *p, false); cycle.append(&mut neigh_edge); // Also retreive all edges between the last p and neighbor - let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph - .edges(*p) - .filter(|edge: &G::EdgeRef| { - edge.target() == neighbor || edge.source() == neighbor - }) - .map(|edge: G::EdgeRef| (edge.source(), edge.target())) - .collect(); + let mut neigh_edge: Vec = + get_edge_between(graph, *p, neighbor, false); cycle.append(&mut neigh_edge); // Once all edges within cycle have been found, push to cycle list. @@ -283,19 +289,10 @@ mod tests { sorted_cycles } - fn sorted_cycle_edges(cycles: Vec>) -> Vec> { - let mut sorted_cycles: Vec> = vec![]; + fn sorted_cycle_edges(cycles: Vec>) -> Vec> { + let mut sorted_cycles: Vec> = vec![]; for cycle in cycles { - let mut cycle: Vec<(usize, usize)> = cycle - .iter() - .map(|x| { - if x.0 < x.1 { - (x.0.index(), x.1.index()) - } else { - (x.1.index(), x.0.index()) - } - }) - .collect(); + let mut cycle: Vec = cycle.iter().map(|x| x.index()).collect(); cycle.sort(); sorted_cycles.push(cycle); } @@ -342,7 +339,7 @@ mod tests { (3, 4), ]; let graph = UnGraph::::from_edges(&edge_list); - let expected = vec![vec![(0, 0)], vec![(2, 3), (2, 5), (3, 6), (5, 6)]]; + 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_edges(res_0), expected); let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2))); @@ -403,10 +400,10 @@ mod tests { assert_eq!( sorted_cycle_edges(res_0), vec![ - vec![(0, 1), (0, 3), (1, 2), (2, 3)], - vec![(0, 1), (0, 8), (1, 6), (6, 7), (7, 8)], - vec![(0, 3), (0, 5), (3, 4), (4, 5)], - vec![(1, 1)], + vec![0, 1, 4, 6], + vec![0, 3, 5, 9, 10], + vec![1, 2, 7, 8], + vec![12], ] ); } diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 51f312a56d..c2f2c6e36a 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -92,23 +92,18 @@ pub fn cycle_basis(graph: &graph::PyGraph, root: Option) -> Vec) -> Vec> { +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.0.index(), x.1.index())) - .collect() - }) + .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) .collect() } From c22c0b5da39a80ae7bb04a8236c41b8623131759 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:45:48 -0400 Subject: [PATCH 04/22] Docs: Added release note --- .../notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml 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..431dd24b0c --- /dev/null +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A function, :func:`~rustworkx.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. +fixes: + - | + Support for edges when getting the cycle basis. Refer to + `#551 `__ for more details. \ No newline at end of file From c6dea4ec2655dce42a65ec1df479813629ddfd4e Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 2 Jun 2023 11:34:06 -0400 Subject: [PATCH 05/22] CI: Added python tests to test_cycle_basis.py - Added `TestCycleBasisEdges` class for unit testing. - Added similar tests to those present on `TestCycleBasis`. - Tests are similar due to the same methodology. - Graphs used for testing are different. --- .../rustworkx_tests/graph/test_cycle_basis.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/rustworkx_tests/graph/test_cycle_basis.py b/tests/rustworkx_tests/graph/test_cycle_basis.py index c38e8ece6e..7c69c0df19 100644 --- a/tests/rustworkx_tests/graph/test_cycle_basis.py +++ b/tests/rustworkx_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_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) From 23f2759d476bcd7b8f2445fba2b96620f9d04871 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 2 Jun 2023 12:02:29 -0400 Subject: [PATCH 06/22] Correction: get_edge_between checks target only - Modified get_edge_between to only check target when equiv == false. --- rustworkx-core/src/connectivity/cycle_basis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 1bc32024c4..08ae4d5f18 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -178,7 +178,7 @@ where if equiv { edge.source() == target && edge.target() == target } else { - edge.source() == target || edge.target() == target + edge.target() == target } }) .map(|edge: G::EdgeRef| edge.id()) From dadb482a9e12872217c792599f9dbe3bfd5173b8 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 5 Jun 2023 20:39:11 -0400 Subject: [PATCH 07/22] Feature: merge basis and basis_edges into one - Cycle basis edges is now within the codebase of cycle basis. - The `EdgesOrNodes` enum handles the different return data types. --- .../src/connectivity/cycle_basis.rs | 289 ++++++++---------- rustworkx-core/src/connectivity/mod.rs | 1 - src/connectivity/mod.rs | 63 ++-- src/lib.rs | 1 - 4 files changed, 143 insertions(+), 211 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 08ae4d5f18..e4a624ab4f 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -35,6 +35,8 @@ use std::hash::Hash; /// * `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. +/// * `edges` - Optional bool for when the user requests the edges instead +/// of the nodes of the cycles. /// /// # Example /// ```rust @@ -43,112 +45,14 @@ use std::hash::Hash; /// /// 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))); +/// let mut res: Vec> = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(false)).unwrap_nodes(); /// ``` -pub fn cycle_basis(graph: G, root: Option) -> Vec> -where - G: NodeCount, - G: IntoNeighbors, - G: IntoNodeIdentifiers, - G::NodeId: Eq + Hash, -{ - let mut root_node = root; - let mut graph_nodes: HashSet = graph.node_identifiers().collect(); - 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 - // nodes we've not "examined" - let root_index = match root_node { - Some(root_node) => root_node, - None => { - temp_value = *graph_nodes.iter().next().unwrap(); - graph_nodes.remove(&temp_value); - temp_value - } - }; - // Stack (ie "pushdown list") of vertices already in the spanning tree - let mut stack: Vec = vec![root_index]; - // Map of node index to predecessor node index - let mut pred: HashMap = HashMap::new(); - pred.insert(root_index, root_index); - // Set of examined nodes during this iteration - 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() { - for neighbor in graph.neighbors(z) { - // A new node was encountered: - if !used.contains_key(&neighbor) { - pred.insert(neighbor, z); - stack.push(neighbor); - let mut temp_set: HashSet = HashSet::new(); - temp_set.insert(z); - used.insert(neighbor, temp_set); - // A self loop: - } else if z == neighbor { - let cycle: Vec = vec![z]; - cycles.push(cycle); - // A cycle was found: - } else if !used.get(&z).unwrap().contains(&neighbor) { - let pn = used.get(&neighbor).unwrap(); - let mut cycle: Vec = vec![neighbor, z]; - let mut p = pred.get(&z).unwrap(); - while !pn.contains(p) { - cycle.push(*p); - p = pred.get(p).unwrap(); - } - cycle.push(*p); - cycles.push(cycle); - let neighbor_set = used.get_mut(&neighbor).unwrap(); - neighbor_set.insert(z); - } - } - } - let mut temp_hashset: HashSet = HashSet::new(); - for key in pred.keys() { - temp_hashset.insert(*key); - } - graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); - root_node = None; - } - cycles -} -/// Return a list of edge indices 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> +pub fn cycle_basis( + graph: G, + root: Option, + edges: Option, +) -> EdgesOrNodes where G: NodeCount, G: IntoNeighbors, @@ -157,9 +61,11 @@ where G::NodeId: Eq + Hash, G::EdgeId: Eq + Hash, { + let want_edges = edges == Some(true); let mut root_node = 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. // Can be used to check if a node loops back to itself by enabling equiv @@ -220,43 +126,59 @@ where used.insert(neighbor, temp_set); // A self loop: } else if z == neighbor { - let cycle_edge: Vec = get_edge_between(graph, z, z, true); - cycles.push(cycle_edge); + if want_edges { + let cycle_edge: Vec = get_edge_between(graph, z, z, true); + 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 pn = used.get(&neighbor).unwrap(); - let mut cycle: Vec = vec![]; let mut p = pred.get(&z).unwrap(); - // Retreive all edges from z to neighbor and vice versa - let mut neigh_edge: Vec = - get_edge_between(graph, z, neighbor, false); - // Append to cycle - cycle.append(&mut neigh_edge); - // Make last p_node == z - let mut prev_p: &G::NodeId = &z; - // While p is in the neighborhood of neighbor - while !pn.contains(p) { - // Retrieve all edges from prev_p to p and vice versa + if want_edges { + let mut cycle: Vec = Vec::new(); + // Retreive all edges from z to neighbor. let mut neigh_edge: Vec = - get_edge_between(graph, *prev_p, *p, false); + get_edge_between(graph, z, neighbor, false); // Append to cycle cycle.append(&mut neigh_edge); - // Update prev_p to p - prev_p = p; - // Retreive 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. - let mut neigh_edge: Vec = - get_edge_between(graph, *prev_p, *p, false); - cycle.append(&mut neigh_edge); - // Also retreive all edges between the last p and neighbor - let mut neigh_edge: Vec = - get_edge_between(graph, *p, neighbor, false); - cycle.append(&mut neigh_edge); - // Once all edges within cycle have been found, push to cycle list. - cycles.push(cycle); + // Make last p_node == z + let mut prev_p: &G::NodeId = &z; + // While p is in the neighborhood of neighbor + while !pn.contains(p) { + // Retrieve all edges from prev_p to p and vice versa + let mut neigh_edge: Vec = + get_edge_between(graph, *prev_p, *p, false); + // Append to cycle + cycle.append(&mut neigh_edge); + // Update prev_p to p + prev_p = p; + // Retreive 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. + let mut neigh_edge: Vec = + get_edge_between(graph, *prev_p, *p, false); + cycle.append(&mut neigh_edge); + // Also retreive all edges between the last p and neighbor + let mut neigh_edge: Vec = + get_edge_between(graph, *p, neighbor, false); + cycle.append(&mut neigh_edge); + // 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 !pn.contains(p) { + cycle.push(*p); + p = pred.get(p).unwrap(); + } + cycle.push(*p); + cycles_nodes.push(cycle); + } let neighbor_set: &mut HashSet = used.get_mut(&neighbor).unwrap(); neighbor_set.insert(z); } @@ -269,39 +191,67 @@ where graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); root_node = None; } - cycles + if want_edges { + EdgesOrNodes::Edges(cycles_edges) + } else { + EdgesOrNodes::Nodes(cycles_nodes) + } +} + +/// Enum for custom return types of `cycle_basis()`. +pub enum EdgesOrNodes { + Nodes(Vec>), + Edges(Vec>), +} +/// Functions used to unwrap the desired datatype of `EdgesOrNodes`. +impl EdgesOrNodes { + pub fn unwrap_nodes(self) -> Vec> { + use EdgesOrNodes::*; + match self { + Nodes(x) => x, + Edges(_) => vec![], + } + } + pub fn unwrap_edges(self) -> Vec> { + use EdgesOrNodes::*; + match self { + Edges(x) => x, + Nodes(_) => vec![], + } + } } #[cfg(test)] mod tests { use crate::connectivity::cycle_basis; - use crate::connectivity::cycle_basis_edges; use petgraph::prelude::*; - fn sorted_cycle(cycles: Vec>) -> Vec> { - let mut sorted_cycles: Vec> = vec![]; - for cycle in cycles { - let mut cycle: Vec = cycle.iter().map(|x| x.index()).collect(); - cycle.sort(); - sorted_cycles.push(cycle); - } - sorted_cycles.sort(); - sorted_cycles - } + use super::EdgesOrNodes; - fn sorted_cycle_edges(cycles: Vec>) -> Vec> { + fn sorted_cycle(cycles: EdgesOrNodes, edges: bool) -> Vec> { let mut sorted_cycles: Vec> = vec![]; - for cycle in cycles { - let mut cycle: Vec = cycle.iter().map(|x| x.index()).collect(); - cycle.sort(); - sorted_cycles.push(cycle); - } + if edges { + let cycles: Vec> = cycles.unwrap_edges(); + for cycle in cycles { + let mut cycle: Vec = cycle.iter().map(|x: &EdgeIndex| x.index()).collect(); + cycle.sort(); + sorted_cycles.push(cycle); + } + } else { + let cycles: Vec> = cycles.unwrap_nodes(); + for cycle in cycles { + let mut cycle: Vec = cycle.iter().map(|x: &NodeIndex| x.index()).collect(); + cycle.sort(); + sorted_cycles.push(cycle); + } + }; sorted_cycles.sort(); sorted_cycles } #[test] fn test_cycle_basis_source() { + let edges: bool = false; let edge_list = vec![ (0, 1), (0, 3), @@ -318,16 +268,17 @@ mod tests { ]; let graph = UnGraph::::from_edges(&edge_list); let expected = vec![vec![0, 1, 2, 3], vec![0, 1, 6, 7, 8], vec![0, 3, 4, 5]]; - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0))); - assert_eq!(sorted_cycle(res_0), expected); - let res_1 = cycle_basis(&graph, Some(NodeIndex::new(1))); - assert_eq!(sorted_cycle(res_1), expected); - let res_9 = cycle_basis(&graph, Some(NodeIndex::new(9))); - assert_eq!(sorted_cycle(res_9), expected); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); + assert_eq!(sorted_cycle(res_0, false), expected); + let res_1 = cycle_basis(&graph, Some(NodeIndex::new(1)), Some(edges)); + assert_eq!(sorted_cycle(res_1, false), expected); + let res_9 = cycle_basis(&graph, Some(NodeIndex::new(9)), Some(edges)); + assert_eq!(sorted_cycle(res_9, false), expected); } #[test] fn test_cycle_edge_basis_source() { + let edges: bool = true; let edge_list = vec![ (0, 0), (0, 1), @@ -340,16 +291,17 @@ mod tests { ]; 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_edges(res_0), expected); - let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2))); - assert_eq!(sorted_cycle_edges(res_1), expected); - let res_9 = cycle_basis_edges(&graph, Some(NodeIndex::new(6))); - assert_eq!(sorted_cycle_edges(res_9), expected); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); + assert_eq!(sorted_cycle(res_0, true), expected); + let res_1 = cycle_basis(&graph, Some(NodeIndex::new(2)), Some(edges)); + assert_eq!(sorted_cycle(res_1, true), expected); + let res_9 = cycle_basis(&graph, Some(NodeIndex::new(6)), Some(edges)); + assert_eq!(sorted_cycle(res_9, true), expected); } #[test] fn test_self_loop() { + let edges: bool = false; let edge_list = vec![ (0, 1), (0, 3), @@ -366,9 +318,9 @@ mod tests { ]; let mut graph = UnGraph::::from_edges(&edge_list); graph.add_edge(NodeIndex::new(1), NodeIndex::new(1), 0); - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0))); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); assert_eq!( - sorted_cycle(res_0), + sorted_cycle(res_0, edges), vec![ vec![0, 1, 2, 3], vec![0, 1, 6, 7, 8], @@ -380,6 +332,7 @@ mod tests { #[test] fn test_self_loop_edges() { + let edges: bool = true; let edge_list = vec![ (0, 1), (0, 3), @@ -396,9 +349,9 @@ mod tests { ]; 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))); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); assert_eq!( - sorted_cycle_edges(res_0), + sorted_cycle(res_0, edges), vec![ vec![0, 1, 4, 6], vec![0, 3, 5, 9, 10], diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index 6a316407e1..b66405c554 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -31,6 +31,5 @@ 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 min_cut::stoer_wagner_min_cut; diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index c2f2c6e36a..ad7a3cbc93 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -58,53 +58,34 @@ use rustworkx_core::connectivity; /// /// :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 or 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(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| x.index()).collect()) - .collect() -} - -/// Return a list of edges representing 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 edge indices list. Each list is a list of edge indices which -/// form 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() +#[pyo3(text_signature = "(graph, /, root=None, edges=False)")] +pub fn cycle_basis( + graph: &graph::PyGraph, + root: Option, + edges: Option, +) -> Vec> { + if edges == Some(true) { + connectivity::cycle_basis(&graph.graph, root.map(NodeIndex::new), edges) + .unwrap_edges() + .into_iter() + .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) + .collect() + } else { + connectivity::cycle_basis(&graph.graph, root.map(NodeIndex::new), edges) + .unwrap_nodes() + .into_iter() + .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) + .collect() + } } /// Find all simple cycles of a :class:`~.PyDiGraph` diff --git a/src/lib.rs b/src/lib.rs index f8631d0d44..1b15d44592 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -448,7 +448,6 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?; m.add_wrapped(wrap_pyfunction!(random_geometric_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!(strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?; From 50d86853524998051b285a2cdb65bac58b452617 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 5 Jun 2023 20:40:11 -0400 Subject: [PATCH 08/22] CI: Modify python tests for new changes --- tests/rustworkx_tests/graph/test_cycle_basis.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/rustworkx_tests/graph/test_cycle_basis.py b/tests/rustworkx_tests/graph/test_cycle_basis.py index 7c69c0df19..7f60d9dd79 100644 --- a/tests/rustworkx_tests/graph/test_cycle_basis.py +++ b/tests/rustworkx_tests/graph/test_cycle_basis.py @@ -91,30 +91,30 @@ def setUp(self): def test_cycle_basis_edges(self): graph = self.graph - res = sorted(sorted(c) for c in rustworkx.cycle_basis_edges(graph, 0)) + res = sorted(sorted(c) for c in rustworkx.cycle_basis(graph, 0, True)) 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)) + res = sorted(sorted(x) for x in rustworkx.cycle_basis(self.graph, 0, True)) self.assertEqual([[0, 1, 2], [4, 5, 6]], res) - res = sorted(sorted(x) for x in rustworkx.cycle_basis_edges(self.graph, 5)) + res = sorted(sorted(x) for x in rustworkx.cycle_basis(self.graph, 5, True)) self.assertEqual([[0, 1, 2], [4, 5, 6]], res) - res = sorted(sorted(x) for x in rustworkx.cycle_basis_edges(self.graph, 7)) + res = sorted(sorted(x) for x in rustworkx.cycle_basis(self.graph, 7, True)) self.assertEqual([[0, 1, 2], [4, 5, 6]], res) def test_cycle_basis_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) + cycles = rustworkx.cycle_basis(self.graph, 9, True) 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) + rustworkx.cycle_basis(digraph, edges=True) 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)) + res = sorted(sorted(c) for c in rustworkx.cycle_basis(self.graph, 0, edges=True)) self.assertEqual([[0, 1, 2], [4, 5, 6], [11]], res) From 7c456341e65c820a623e430b020606909e40e26d Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 5 Jun 2023 20:40:58 -0400 Subject: [PATCH 09/22] Docs: made changes to releasenotes and api.rst - Release notes now include an example with a jupyter notebook. --- docs/source/api.rst | 451 ++++++++++++++++++ ...dd-cycle-basis-edges-5cb31eac7e41096d.yaml | 53 +- 2 files changed, 501 insertions(+), 3 deletions(-) create mode 100644 docs/source/api.rst diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 0000000000..0bf5780753 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,451 @@ +.. _rustworkx: + +####################### +Rustworkx API Reference +####################### + +Graph Classes +============= + +.. autosummary:: + :toctree: apiref + + rustworkx.PyGraph + rustworkx.PyDiGraph + rustworkx.PyDAG + +.. _algorithm_api: + +Algorithm Functions +=================== + +.. _shortest-paths: + +Shortest Paths +-------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.dijkstra_shortest_paths + rustworkx.dijkstra_shortest_path_lengths + rustworkx.all_pairs_dijkstra_shortest_paths + rustworkx.all_pairs_dijkstra_path_lengths + rustworkx.bellman_ford_shortest_paths + rustworkx.bellman_ford_shortest_path_lengths + rustworkx.all_pairs_bellman_ford_shortest_paths + rustworkx.all_pairs_bellman_ford_path_lengths + rustworkx.negative_edge_cycle + rustworkx.find_negative_cycle + rustworkx.distance_matrix + rustworkx.floyd_warshall + rustworkx.floyd_warshall_numpy + rustworkx.astar_shortest_path + rustworkx.k_shortest_path_lengths + rustworkx.num_shortest_paths_unweighted + rustworkx.unweighted_average_shortest_path_length + +.. _centrality: + +Centrality +---------- + +.. autosummary:: + :toctree: apiref + + rustworkx.betweenness_centrality + rustworkx.edge_betweenness_centrality + rustworkx.eigenvector_centrality + rustworkx.katz_centrality + rustworkx.closeness_centrality + +.. _link-analysis: + +Link Analysis +-------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.pagerank + rustworkx.hits + +.. _traversal: + +Traversal +--------- + +.. autosummary:: + :toctree: apiref + + rustworkx.dfs_edges + rustworkx.dfs_search + rustworkx.bfs_successors + rustworkx.bfs_predecessors + rustworkx.bfs_search + rustworkx.dijkstra_search + rustworkx.topological_sort + rustworkx.lexicographical_topological_sort + rustworkx.descendants + rustworkx.ancestors + rustworkx.collect_runs + rustworkx.collect_bicolor_runs + rustworkx.visit.DFSVisitor + rustworkx.visit.BFSVisitor + rustworkx.visit.DijkstraVisitor + rustworkx.TopologicalSorter + +.. _dag-algorithms: + +DAG Algorithms +-------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.dag_longest_path + rustworkx.dag_longest_path_length + rustworkx.dag_weighted_longest_path + rustworkx.dag_weighted_longest_path_length + rustworkx.is_directed_acyclic_graph + rustworkx.layers + +.. _tree: + +Tree +---- + +.. autosummary:: + :toctree: apiref + + rustworkx.minimum_spanning_edges + rustworkx.minimum_spanning_tree + rustworkx.steiner_tree + +.. _isomorphism: + +Isomorphism +----------- + +.. autosummary:: + :toctree: apiref + + rustworkx.is_isomorphic + rustworkx.is_subgraph_isomorphic + rustworkx.is_isomorphic_node_match + rustworkx.vf2_mapping + +.. _matching: + +Matching +-------- + +.. autosummary:: + :toctree: apiref + + rustworkx.max_weight_matching + rustworkx.is_matching + rustworkx.is_maximal_matching + +.. _connectivity-cycle-finding: + +Connectivity and Cycles +----------------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.number_connected_components + rustworkx.connected_components + rustworkx.node_connected_component + rustworkx.is_connected + rustworkx.strongly_connected_components + rustworkx.number_weakly_connected_components + rustworkx.weakly_connected_components + rustworkx.is_weakly_connected + rustworkx.cycle_basis + rustworkx.simple_cycles + rustworkx.digraph_find_cycle + rustworkx.articulation_points + rustworkx.biconnected_components + rustworkx.chain_decomposition + rustworkx.all_simple_paths + rustworkx.all_pairs_all_simple_paths + rustworkx.stoer_wagner_min_cut + rustworkx.longest_simple_path + +.. _graph-ops: + +Graph Operations +---------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.complement + rustworkx.union + rustworkx.cartesian_product + +.. _other-algorithms: + +Other Algorithm Functions +------------------------- + +.. autosummary:: + :toctree: apiref + + rustworkx.adjacency_matrix + rustworkx.transitivity + rustworkx.core_number + rustworkx.graph_greedy_color + rustworkx.graph_greedy_edge_color + rustworkx.graph_line_graph + rustworkx.metric_closure + rustworkx.is_planar + +.. _generator_funcs: + +Generators +========== + +.. autosummary:: + :toctree: apiref + + rustworkx.generators.cycle_graph + rustworkx.generators.directed_cycle_graph + rustworkx.generators.path_graph + rustworkx.generators.directed_path_graph + rustworkx.generators.star_graph + rustworkx.generators.directed_star_graph + rustworkx.generators.mesh_graph + rustworkx.generators.directed_mesh_graph + rustworkx.generators.grid_graph + rustworkx.generators.directed_grid_graph + rustworkx.generators.binomial_tree_graph + rustworkx.generators.directed_binomial_tree_graph + rustworkx.generators.hexagonal_lattice_graph + rustworkx.generators.directed_hexagonal_lattice_graph + rustworkx.generators.heavy_square_graph + rustworkx.generators.directed_heavy_square_graph + rustworkx.generators.heavy_hex_graph + rustworkx.generators.directed_heavy_hex_graph + rustworkx.generators.lollipop_graph + rustworkx.generators.generalized_petersen_graph + rustworkx.generators.barbell_graph + rustworkx.generators.full_rary_tree + rustworkx.generators.empty_graph + rustworkx.generators.directed_empty_graph + rustworkx.generators.complete_graph + rustworkx.generators.directed_complete_graph + +.. _random_generators: + +Random Graph Generator Functions +================================ + +.. autosummary:: + :toctree: apiref + + rustworkx.directed_gnp_random_graph + rustworkx.undirected_gnp_random_graph + rustworkx.directed_gnm_random_graph + rustworkx.undirected_gnm_random_graph + rustworkx.random_geometric_graph + +.. _layout-functions: + +Layout Functions +================ + +.. autosummary:: + :toctree: apiref + + rustworkx.random_layout + rustworkx.spring_layout + rustworkx.bipartite_layout + rustworkx.circular_layout + rustworkx.shell_layout + rustworkx.spiral_layout + + +.. _serialization: + +Serialization +============= + +.. autosummary:: + :toctree: apiref + + rustworkx.node_link_json + rustworkx.read_graphml + +.. _converters: + +Converters +========== + +.. autosummary:: + :toctree: apiref + + rustworkx.networkx_converter + +.. _api-functions-pydigraph: + +API functions for PyDigraph +=========================== + +These functions are algorithm functions that are type specific for +:class:`~rustworkx.PyDiGraph` or :class:`~rustworkx.PyDAG` objects. Universal +functions from Retworkx API that work for both graph types internally call +the functions from the explicitly typed based on the data type. + +.. autosummary:: + :toctree: apiref + + rustworkx.digraph_is_isomorphic + rustworkx.digraph_is_subgraph_isomorphic + rustworkx.digraph_vf2_mapping + rustworkx.digraph_distance_matrix + rustworkx.digraph_floyd_warshall + rustworkx.digraph_floyd_warshall_numpy + rustworkx.digraph_adjacency_matrix + rustworkx.digraph_all_simple_paths + rustworkx.digraph_all_pairs_all_simple_paths + rustworkx.digraph_astar_shortest_path + rustworkx.digraph_dijkstra_shortest_paths + rustworkx.digraph_all_pairs_dijkstra_shortest_paths + rustworkx.digraph_dijkstra_shortest_path_lengths + rustworkx.digraph_all_pairs_dijkstra_path_lengths + rustworkx.digraph_bellman_ford_shortest_path_lengths + rustworkx.digraph_bellman_ford_shortest_path_lengths + rustworkx.digraph_all_pairs_bellman_ford_shortest_paths + rustworkx.digraph_all_pairs_bellman_ford_path_lengths + rustworkx.digraph_k_shortest_path_lengths + rustworkx.digraph_dfs_edges + rustworkx.digraph_dfs_search + rustworkx.digraph_find_cycle + rustworkx.digraph_transitivity + rustworkx.digraph_core_number + rustworkx.digraph_complement + rustworkx.digraph_union + rustworkx.digraph_tensor_product + rustworkx.digraph_cartesian_product + rustworkx.digraph_random_layout + rustworkx.digraph_bipartite_layout + rustworkx.digraph_circular_layout + rustworkx.digraph_shell_layout + rustworkx.digraph_spiral_layout + rustworkx.digraph_spring_layout + rustworkx.digraph_num_shortest_paths_unweighted + rustworkx.digraph_betweenness_centrality + rustworkx.digraph_edge_betweenness_centrality + rustworkx.digraph_closeness_centrality + rustworkx.digraph_eigenvector_centrality + rustworkx.digraph_katz_centrality + rustworkx.digraph_unweighted_average_shortest_path_length + rustworkx.digraph_bfs_search + rustworkx.digraph_dijkstra_search + rustworkx.digraph_node_link_json + rustworkx.digraph_longest_simple_path + +.. _api-functions-pygraph: + +API functions for PyGraph +========================= + +These functions are algorithm functions that are type specific for +:class:`~rustworkx.PyGraph` objects. Universal functions from Rustworkx API that +work for both graph types internally call the functions from the explicitly +typed API based on the data type. + +.. autosummary:: + :toctree: apiref + + rustworkx.graph_is_isomorphic + rustworkx.graph_is_subgraph_isomorphic + rustworkx.graph_vf2_mapping + rustworkx.graph_distance_matrix + rustworkx.graph_floyd_warshall + rustworkx.graph_floyd_warshall_numpy + rustworkx.graph_adjacency_matrix + rustworkx.graph_all_simple_paths + rustworkx.graph_all_pairs_all_simple_paths + rustworkx.graph_astar_shortest_path + rustworkx.graph_dijkstra_shortest_paths + rustworkx.graph_dijkstra_shortest_path_lengths + rustworkx.graph_all_pairs_dijkstra_shortest_paths + rustworkx.graph_k_shortest_path_lengths + rustworkx.graph_all_pairs_dijkstra_path_lengths + rustworkx.graph_bellman_ford_shortest_path_lengths + rustworkx.graph_bellman_ford_shortest_path_lengths + rustworkx.graph_all_pairs_bellman_ford_shortest_paths + rustworkx.graph_all_pairs_bellman_ford_path_lengths + rustworkx.graph_dfs_edges + rustworkx.graph_dfs_search + rustworkx.graph_transitivity + rustworkx.graph_core_number + rustworkx.graph_complement + rustworkx.graph_union + rustworkx.graph_tensor_product + rustworkx.graph_token_swapper + rustworkx.graph_cartesian_product + rustworkx.graph_random_layout + rustworkx.graph_bipartite_layout + rustworkx.graph_circular_layout + rustworkx.graph_shell_layout + rustworkx.graph_spiral_layout + rustworkx.graph_spring_layout + rustworkx.graph_num_shortest_paths_unweighted + rustworkx.graph_betweenness_centrality + rustworkx.graph_edge_betweenness_centrality + rustworkx.graph_closeness_centrality + rustworkx.graph_eigenvector_centrality + rustworkx.graph_katz_centrality + rustworkx.graph_unweighted_average_shortest_path_length + rustworkx.graph_bfs_search + rustworkx.graph_dijkstra_search + rustworkx.graph_node_link_json + rustworkx.graph_longest_simple_path + +Exceptions +========== + +.. autosummary:: + :toctree: apiref + + rustworkx.InvalidNode + rustworkx.DAGWouldCycle + rustworkx.NoEdgeBetweenNodes + rustworkx.DAGHasCycle + rustworkx.NegativeCycle + rustworkx.NoSuitableNeighbors + rustworkx.NoPathFound + rustworkx.NullGraph + rustworkx.visit.StopSearch + rustworkx.visit.PruneSearch + rustworkx.JSONSerializationError + +Custom Return Types +=================== + +.. autosummary:: + :toctree: apiref + + rustworkx.BFSSuccessors + rustworkx.BFSPredecessors + rustworkx.NodeIndices + rustworkx.EdgeIndices + rustworkx.EdgeList + rustworkx.WeightedEdgeList + rustworkx.EdgeIndexMap + rustworkx.PathMapping + rustworkx.PathLengthMapping + rustworkx.Pos2DMapping + rustworkx.AllPairsPathMapping + rustworkx.AllPairsPathLengthMapping + rustworkx.CentralityMapping + rustworkx.EdgeCentralityMapping + rustworkx.Chains + rustworkx.NodeMap + rustworkx.ProductNodeMap + rustworkx.BiconnectedComponents diff --git a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml index 431dd24b0c..f334f425fe 100644 --- a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -1,9 +1,56 @@ --- features: - | - A function, :func:`~rustworkx.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. + The function :func:`~rustworkx.cycle_basis` from the crate ``rustworkx-core`` + in the ``connectivity`` module was modified. This function now has the option + of returning the edge indices that form the cycle basis of a graph. + + .. 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(graph, a, edges=True) + 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) + fixes: - | Support for edges when getting the cycle basis. Refer to From c0f9f94816c0425f80d392efe207b0410b8bc1df Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:30:42 -0400 Subject: [PATCH 10/22] Fix: Make `cycle_basis` and `EdgeOrNode` private - A previous commit inadvertedly made cycle_basis prublic. - To address this, two new public-facing functions were added: - `cycle_basis` for NodeId returns. - `cycle_basis_edges` for EdgeId returns. - When customizing cycle_basis a new enum EdgesOrNodes was made. - Enum should not be have been shared by definition. - Correction makes it private. - Removed redundant import from unwrap methods. - Tests have been adjusted accordingly. --- docs/source/api.rst | 1 + .../src/connectivity/cycle_basis.rs | 201 ++++++++++++------ rustworkx-core/src/connectivity/mod.rs | 1 + src/connectivity/mod.rs | 60 ++++-- src/lib.rs | 1 + 5 files changed, 178 insertions(+), 86 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 0bf5780753..a23d5d3eb8 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -164,6 +164,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/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index e4a624ab4f..0b88c2a3d2 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -14,7 +14,9 @@ use hashbrown::{HashMap, HashSet}; 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,29 +31,18 @@ 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. -/// * `edges` - Optional bool for when the user requests the edges instead +/// * `edges` - bool for when the user requests the edges instead /// of the nodes of the cycles. -/// -/// # 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)), Some(false)).unwrap_nodes(); -/// ``` -pub fn cycle_basis( +fn inner_cycle_basis( graph: G, root: Option, - edges: Option, + edges: bool, ) -> EdgesOrNodes where G: NodeCount, @@ -61,7 +52,6 @@ where G::NodeId: Eq + Hash, G::EdgeId: Eq + Hash, { - let want_edges = edges == Some(true); let mut root_node = root; let mut graph_nodes: HashSet = graph.node_identifiers().collect(); let mut cycles_edges: Vec> = Vec::new(); @@ -126,7 +116,7 @@ where used.insert(neighbor, temp_set); // A self loop: } else if z == neighbor { - if want_edges { + if edges { let cycle_edge: Vec = get_edge_between(graph, z, z, true); cycles_edges.push(cycle_edge); } else { @@ -137,7 +127,7 @@ where } else if !used.get(&z).unwrap().contains(&neighbor) { let pn = used.get(&neighbor).unwrap(); let mut p = pred.get(&z).unwrap(); - if want_edges { + if edges { let mut cycle: Vec = Vec::new(); // Retreive all edges from z to neighbor. let mut neigh_edge: Vec = @@ -191,7 +181,7 @@ where graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); root_node = None; } - if want_edges { + if edges { EdgesOrNodes::Edges(cycles_edges) } else { EdgesOrNodes::Nodes(cycles_nodes) @@ -199,59 +189,141 @@ where } /// Enum for custom return types of `cycle_basis()`. -pub enum EdgesOrNodes { +enum EdgesOrNodes { Nodes(Vec>), Edges(Vec>), } /// Functions used to unwrap the desired datatype of `EdgesOrNodes`. impl EdgesOrNodes { - pub fn unwrap_nodes(self) -> Vec> { - use EdgesOrNodes::*; + fn unwrap_nodes(self) -> Vec> { match self { - Nodes(x) => x, - Edges(_) => vec![], + Self::Nodes(x) => x, + Self::Edges(_) => unreachable!( + "Function should only return instances of {}.", + std::any::type_name::() + ), } } - pub fn unwrap_edges(self) -> Vec> { - use EdgesOrNodes::*; + fn unwrap_edges(self) -> Vec> { match self { - Edges(x) => x, - Nodes(_) => vec![], + 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; - use super::EdgesOrNodes; - - fn sorted_cycle(cycles: EdgesOrNodes, edges: bool) -> Vec> { + fn sorted_cycle(cycles: Vec>) -> Vec> + where + T: GraphIndex, + { let mut sorted_cycles: Vec> = vec![]; - if edges { - let cycles: Vec> = cycles.unwrap_edges(); - for cycle in cycles { - let mut cycle: Vec = cycle.iter().map(|x: &EdgeIndex| x.index()).collect(); - cycle.sort(); - sorted_cycles.push(cycle); - } - } else { - let cycles: Vec> = cycles.unwrap_nodes(); - for cycle in cycles { - let mut cycle: Vec = cycle.iter().map(|x: &NodeIndex| x.index()).collect(); - cycle.sort(); - sorted_cycles.push(cycle); - } - }; + for cycle in cycles { + let mut cycle: Vec = cycle.iter().map(|x: &T| x.index()).collect(); + cycle.sort(); + sorted_cycles.push(cycle); + } sorted_cycles.sort(); sorted_cycles } #[test] fn test_cycle_basis_source() { - let edges: bool = false; let edge_list = vec![ (0, 1), (0, 3), @@ -268,17 +340,16 @@ mod tests { ]; let graph = UnGraph::::from_edges(&edge_list); let expected = vec![vec![0, 1, 2, 3], vec![0, 1, 6, 7, 8], vec![0, 3, 4, 5]]; - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); - assert_eq!(sorted_cycle(res_0, false), expected); - let res_1 = cycle_basis(&graph, Some(NodeIndex::new(1)), Some(edges)); - assert_eq!(sorted_cycle(res_1, false), expected); - let res_9 = cycle_basis(&graph, Some(NodeIndex::new(9)), Some(edges)); - assert_eq!(sorted_cycle(res_9, false), expected); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0))); + assert_eq!(sorted_cycle(res_0), expected); + let res_1 = cycle_basis(&graph, Some(NodeIndex::new(1))); + assert_eq!(sorted_cycle(res_1), expected); + let res_9 = cycle_basis(&graph, Some(NodeIndex::new(9))); + assert_eq!(sorted_cycle(res_9), expected); } #[test] fn test_cycle_edge_basis_source() { - let edges: bool = true; let edge_list = vec![ (0, 0), (0, 1), @@ -291,17 +362,16 @@ mod tests { ]; let graph = UnGraph::::from_edges(&edge_list); let expected = vec![vec![0], vec![3, 4, 5, 6]]; - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); - assert_eq!(sorted_cycle(res_0, true), expected); - let res_1 = cycle_basis(&graph, Some(NodeIndex::new(2)), Some(edges)); - assert_eq!(sorted_cycle(res_1, true), expected); - let res_9 = cycle_basis(&graph, Some(NodeIndex::new(6)), Some(edges)); - assert_eq!(sorted_cycle(res_9, true), expected); + 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 edges: bool = false; let edge_list = vec![ (0, 1), (0, 3), @@ -318,9 +388,9 @@ mod tests { ]; let mut graph = UnGraph::::from_edges(&edge_list); graph.add_edge(NodeIndex::new(1), NodeIndex::new(1), 0); - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); + let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0))); assert_eq!( - sorted_cycle(res_0, edges), + sorted_cycle(res_0), vec![ vec![0, 1, 2, 3], vec![0, 1, 6, 7, 8], @@ -332,7 +402,6 @@ mod tests { #[test] fn test_self_loop_edges() { - let edges: bool = true; let edge_list = vec![ (0, 1), (0, 3), @@ -349,9 +418,9 @@ mod tests { ]; let mut graph = UnGraph::::from_edges(&edge_list); graph.add_edge(NodeIndex::new(1), NodeIndex::new(1), 0); - let res_0 = cycle_basis(&graph, Some(NodeIndex::new(0)), Some(edges)); + let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0))); assert_eq!( - sorted_cycle(res_0, edges), + sorted_cycle(res_0), vec![ vec![0, 1, 4, 6], vec![0, 3, 5, 9, 10], diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index b66405c554..6a316407e1 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -31,5 +31,6 @@ 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 min_cut::stoer_wagner_min_cut; diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index ad7a3cbc93..ce430a83ae 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -60,32 +60,52 @@ use rustworkx_core::connectivity; /// :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 or edge ids +/// :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 /// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. #[pyfunction] -#[pyo3(text_signature = "(graph, /, root=None, edges=False)")] -pub fn cycle_basis( - graph: &graph::PyGraph, - root: Option, - edges: Option, -) -> Vec> { - if edges == Some(true) { - connectivity::cycle_basis(&graph.graph, root.map(NodeIndex::new), edges) - .unwrap_edges() - .into_iter() - .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) - .collect() - } else { - connectivity::cycle_basis(&graph.graph, root.map(NodeIndex::new), edges) - .unwrap_nodes() - .into_iter() - .map(|res_map| res_map.into_iter().map(|x| x.index()).collect()) - .collect() - } +#[pyo3(text_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() } /// Find all simple cycles of a :class:`~.PyDiGraph` diff --git a/src/lib.rs b/src/lib.rs index 1b15d44592..f8631d0d44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -448,6 +448,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?; m.add_wrapped(wrap_pyfunction!(random_geometric_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!(strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?; From df5b3b5115e08e727bca871c1d09b348205e453c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:31:48 -0400 Subject: [PATCH 11/22] CI: Modify python tests for new changes --- tests/rustworkx_tests/graph/test_cycle_basis.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/rustworkx_tests/graph/test_cycle_basis.py b/tests/rustworkx_tests/graph/test_cycle_basis.py index 7f60d9dd79..220e6a12cc 100644 --- a/tests/rustworkx_tests/graph/test_cycle_basis.py +++ b/tests/rustworkx_tests/graph/test_cycle_basis.py @@ -91,30 +91,30 @@ def setUp(self): def test_cycle_basis_edges(self): graph = self.graph - res = sorted(sorted(c) for c in rustworkx.cycle_basis(graph, 0, True)) + 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(self.graph, 0, True)) + 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(self.graph, 5, True)) + 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(self.graph, 7, True)) + 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_disconnected_graphs(self): + 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(self.graph, 9, True) + 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(digraph, edges=True) + 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(self.graph, 0, edges=True)) + res = sorted(sorted(c) for c in rustworkx.cycle_basis_edges(self.graph, 0)) self.assertEqual([[0, 1, 2], [4, 5, 6], [11]], res) From 26146afd0a6c569bc4b46b73fed48735a00d3427 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:39:36 -0400 Subject: [PATCH 12/22] Docs: Fixed release notes with the latest changes --- .../notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml index f334f425fe..f655a7e4c5 100644 --- a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -1,9 +1,9 @@ --- features: - | - The function :func:`~rustworkx.cycle_basis` from the crate ``rustworkx-core`` - in the ``connectivity`` module was modified. This function now has the option - of returning the edge indices that form the cycle basis of a graph. + A function, :func:`~rustworkx.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. .. jupyter-execute:: @@ -43,14 +43,13 @@ features: mpl_draw(graph, with_labels=True) # Retrieve EdgeIDs by enabling the edges flag. - cycles_edges = rustworkx.cycle_basis(graph, a, edges=True) + 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) - fixes: - | Support for edges when getting the cycle basis. Refer to From f3f271ae534e70676281aa405a6783d5e292d700 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:16:54 -0400 Subject: [PATCH 13/22] Correction: remove redundant self-loop check --- .../src/connectivity/cycle_basis.rs | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 0b88c2a3d2..1e572251ee 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -59,24 +59,13 @@ where // Method used to retrieve all the edges between an origin node and a target node. // Can be used to check if a node loops back to itself by enabling equiv - fn get_edge_between( - orig_graph: G, - origin: G::NodeId, - target: G::NodeId, - equiv: bool, - ) -> Vec + fn get_edge_between(orig_graph: G, origin: G::NodeId, target: G::NodeId) -> Vec where G: IntoEdges, { orig_graph .edges(origin) - .filter(|edge: &G::EdgeRef| { - if equiv { - edge.source() == target && edge.target() == target - } else { - edge.target() == target - } - }) + .filter(|edge: &G::EdgeRef| edge.target() == target) .map(|edge: G::EdgeRef| edge.id()) .collect() } @@ -105,7 +94,6 @@ where while !stack.is_empty() { // Use the last element added so that cycles are easier to find let z = stack.pop().unwrap(); - // println!("Length of all edges from: {:?}", edges.len()); for neighbor in graph.neighbors(z) { // A new node was encountered: if !used.contains_key(&neighbor) { @@ -117,7 +105,7 @@ where // A self loop: } else if z == neighbor { if edges { - let cycle_edge: Vec = get_edge_between(graph, z, z, true); + let cycle_edge: Vec = get_edge_between(graph, z, z); cycles_edges.push(cycle_edge); } else { let cycle: Vec = vec![z]; @@ -130,8 +118,7 @@ where if edges { let mut cycle: Vec = Vec::new(); // Retreive all edges from z to neighbor. - let mut neigh_edge: Vec = - get_edge_between(graph, z, neighbor, false); + let mut neigh_edge: Vec = get_edge_between(graph, z, neighbor); // Append to cycle cycle.append(&mut neigh_edge); @@ -141,7 +128,7 @@ where while !pn.contains(p) { // Retrieve all edges from prev_p to p and vice versa let mut neigh_edge: Vec = - get_edge_between(graph, *prev_p, *p, false); + get_edge_between(graph, *prev_p, *p); // Append to cycle cycle.append(&mut neigh_edge); // Update prev_p to p @@ -150,12 +137,10 @@ where p = pred.get(p).unwrap(); } // When loop ends add remaining edges from prev_p to p. - let mut neigh_edge: Vec = - get_edge_between(graph, *prev_p, *p, false); + let mut neigh_edge: Vec = get_edge_between(graph, *prev_p, *p); cycle.append(&mut neigh_edge); // Also retreive all edges between the last p and neighbor - let mut neigh_edge: Vec = - get_edge_between(graph, *p, neighbor, false); + let mut neigh_edge: Vec = get_edge_between(graph, *p, neighbor); cycle.append(&mut neigh_edge); // Once all edges within cycle have been found, push to cycle list. cycles_edges.push(cycle); From 70541c896491ce152565515ab72e814d2dfb344b Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:17:46 -0400 Subject: [PATCH 14/22] Docs: Add upgrade release note --- .../notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml index f655a7e4c5..bd07e6d9f1 100644 --- a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -1,9 +1,13 @@ --- features: - | - A function, :func:`~rustworkx.cycle_basis_edges` was added to the crate + 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:: @@ -50,6 +54,11 @@ features: 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`. fixes: - | Support for edges when getting the cycle basis. Refer to From dc7eb2614cb35ec77528a5c6a7f3446467971e7c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:16:31 -0400 Subject: [PATCH 15/22] Correction: `get_edge_between` returns `EdgeId`. - The method returns the first edge found between nodes. - The docstring for this method was corrected. --- .../src/connectivity/cycle_basis.rs | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 1e572251ee..dd0b3b2c68 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -52,14 +52,13 @@ where 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_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. - // Can be used to check if a node loops back to itself by enabling equiv - fn get_edge_between(orig_graph: G, origin: G::NodeId, target: G::NodeId) -> Vec + /// 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, { @@ -67,7 +66,8 @@ where .edges(origin) .filter(|edge: &G::EdgeRef| edge.target() == target) .map(|edge: G::EdgeRef| edge.id()) - .collect() + .next() + .unwrap() } while !graph_nodes.is_empty() { @@ -105,7 +105,7 @@ where // A self loop: } else if z == neighbor { if edges { - let cycle_edge: Vec = get_edge_between(graph, z, z); + let cycle_edge: Vec = vec![get_edge_between(graph, z, z)]; cycles_edges.push(cycle_edge); } else { let cycle: Vec = vec![z]; @@ -117,31 +117,24 @@ where let mut p = pred.get(&z).unwrap(); if edges { let mut cycle: Vec = Vec::new(); - // Retreive all edges from z to neighbor. - let mut neigh_edge: Vec = get_edge_between(graph, z, neighbor); - // Append to cycle - cycle.append(&mut neigh_edge); + // Retreive 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 !pn.contains(p) { - // Retrieve all edges from prev_p to p and vice versa - let mut neigh_edge: Vec = - get_edge_between(graph, *prev_p, *p); - // Append to cycle - cycle.append(&mut neigh_edge); + // 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; // Retreive 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. - let mut neigh_edge: Vec = get_edge_between(graph, *prev_p, *p); - cycle.append(&mut neigh_edge); + cycle.push(get_edge_between(graph, *prev_p, *p)); // Also retreive all edges between the last p and neighbor - let mut neigh_edge: Vec = get_edge_between(graph, *p, neighbor); - cycle.append(&mut neigh_edge); + 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 { From 38bdeac5553ed35ea0e8dbccbf6f9ec87e52d811 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:20:56 -0400 Subject: [PATCH 16/22] Docs: Removed fix section of docstring. --- .../notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml index bd07e6d9f1..948e79aee4 100644 --- a/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml +++ b/releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml @@ -58,8 +58,4 @@ 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`. -fixes: - - | - Support for edges when getting the cycle basis. Refer to - `#551 `__ for more details. \ No newline at end of file + attribute ``EdgeId`` conforms to `Eq` and `Hash`. \ No newline at end of file From 075b2f00257256f7420d826e63c7012ef63f45b3 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:11:56 -0400 Subject: [PATCH 17/22] Docs: Removed old api.rst file --- docs/source/api.rst | 452 -------------------------------------------- 1 file changed, 452 deletions(-) delete mode 100644 docs/source/api.rst diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index a23d5d3eb8..0000000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,452 +0,0 @@ -.. _rustworkx: - -####################### -Rustworkx API Reference -####################### - -Graph Classes -============= - -.. autosummary:: - :toctree: apiref - - rustworkx.PyGraph - rustworkx.PyDiGraph - rustworkx.PyDAG - -.. _algorithm_api: - -Algorithm Functions -=================== - -.. _shortest-paths: - -Shortest Paths --------------- - -.. autosummary:: - :toctree: apiref - - rustworkx.dijkstra_shortest_paths - rustworkx.dijkstra_shortest_path_lengths - rustworkx.all_pairs_dijkstra_shortest_paths - rustworkx.all_pairs_dijkstra_path_lengths - rustworkx.bellman_ford_shortest_paths - rustworkx.bellman_ford_shortest_path_lengths - rustworkx.all_pairs_bellman_ford_shortest_paths - rustworkx.all_pairs_bellman_ford_path_lengths - rustworkx.negative_edge_cycle - rustworkx.find_negative_cycle - rustworkx.distance_matrix - rustworkx.floyd_warshall - rustworkx.floyd_warshall_numpy - rustworkx.astar_shortest_path - rustworkx.k_shortest_path_lengths - rustworkx.num_shortest_paths_unweighted - rustworkx.unweighted_average_shortest_path_length - -.. _centrality: - -Centrality ----------- - -.. autosummary:: - :toctree: apiref - - rustworkx.betweenness_centrality - rustworkx.edge_betweenness_centrality - rustworkx.eigenvector_centrality - rustworkx.katz_centrality - rustworkx.closeness_centrality - -.. _link-analysis: - -Link Analysis --------------- - -.. autosummary:: - :toctree: apiref - - rustworkx.pagerank - rustworkx.hits - -.. _traversal: - -Traversal ---------- - -.. autosummary:: - :toctree: apiref - - rustworkx.dfs_edges - rustworkx.dfs_search - rustworkx.bfs_successors - rustworkx.bfs_predecessors - rustworkx.bfs_search - rustworkx.dijkstra_search - rustworkx.topological_sort - rustworkx.lexicographical_topological_sort - rustworkx.descendants - rustworkx.ancestors - rustworkx.collect_runs - rustworkx.collect_bicolor_runs - rustworkx.visit.DFSVisitor - rustworkx.visit.BFSVisitor - rustworkx.visit.DijkstraVisitor - rustworkx.TopologicalSorter - -.. _dag-algorithms: - -DAG Algorithms --------------- - -.. autosummary:: - :toctree: apiref - - rustworkx.dag_longest_path - rustworkx.dag_longest_path_length - rustworkx.dag_weighted_longest_path - rustworkx.dag_weighted_longest_path_length - rustworkx.is_directed_acyclic_graph - rustworkx.layers - -.. _tree: - -Tree ----- - -.. autosummary:: - :toctree: apiref - - rustworkx.minimum_spanning_edges - rustworkx.minimum_spanning_tree - rustworkx.steiner_tree - -.. _isomorphism: - -Isomorphism ------------ - -.. autosummary:: - :toctree: apiref - - rustworkx.is_isomorphic - rustworkx.is_subgraph_isomorphic - rustworkx.is_isomorphic_node_match - rustworkx.vf2_mapping - -.. _matching: - -Matching --------- - -.. autosummary:: - :toctree: apiref - - rustworkx.max_weight_matching - rustworkx.is_matching - rustworkx.is_maximal_matching - -.. _connectivity-cycle-finding: - -Connectivity and Cycles ------------------------ - -.. autosummary:: - :toctree: apiref - - rustworkx.number_connected_components - rustworkx.connected_components - rustworkx.node_connected_component - rustworkx.is_connected - rustworkx.strongly_connected_components - rustworkx.number_weakly_connected_components - 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 - rustworkx.biconnected_components - rustworkx.chain_decomposition - rustworkx.all_simple_paths - rustworkx.all_pairs_all_simple_paths - rustworkx.stoer_wagner_min_cut - rustworkx.longest_simple_path - -.. _graph-ops: - -Graph Operations ----------------- - -.. autosummary:: - :toctree: apiref - - rustworkx.complement - rustworkx.union - rustworkx.cartesian_product - -.. _other-algorithms: - -Other Algorithm Functions -------------------------- - -.. autosummary:: - :toctree: apiref - - rustworkx.adjacency_matrix - rustworkx.transitivity - rustworkx.core_number - rustworkx.graph_greedy_color - rustworkx.graph_greedy_edge_color - rustworkx.graph_line_graph - rustworkx.metric_closure - rustworkx.is_planar - -.. _generator_funcs: - -Generators -========== - -.. autosummary:: - :toctree: apiref - - rustworkx.generators.cycle_graph - rustworkx.generators.directed_cycle_graph - rustworkx.generators.path_graph - rustworkx.generators.directed_path_graph - rustworkx.generators.star_graph - rustworkx.generators.directed_star_graph - rustworkx.generators.mesh_graph - rustworkx.generators.directed_mesh_graph - rustworkx.generators.grid_graph - rustworkx.generators.directed_grid_graph - rustworkx.generators.binomial_tree_graph - rustworkx.generators.directed_binomial_tree_graph - rustworkx.generators.hexagonal_lattice_graph - rustworkx.generators.directed_hexagonal_lattice_graph - rustworkx.generators.heavy_square_graph - rustworkx.generators.directed_heavy_square_graph - rustworkx.generators.heavy_hex_graph - rustworkx.generators.directed_heavy_hex_graph - rustworkx.generators.lollipop_graph - rustworkx.generators.generalized_petersen_graph - rustworkx.generators.barbell_graph - rustworkx.generators.full_rary_tree - rustworkx.generators.empty_graph - rustworkx.generators.directed_empty_graph - rustworkx.generators.complete_graph - rustworkx.generators.directed_complete_graph - -.. _random_generators: - -Random Graph Generator Functions -================================ - -.. autosummary:: - :toctree: apiref - - rustworkx.directed_gnp_random_graph - rustworkx.undirected_gnp_random_graph - rustworkx.directed_gnm_random_graph - rustworkx.undirected_gnm_random_graph - rustworkx.random_geometric_graph - -.. _layout-functions: - -Layout Functions -================ - -.. autosummary:: - :toctree: apiref - - rustworkx.random_layout - rustworkx.spring_layout - rustworkx.bipartite_layout - rustworkx.circular_layout - rustworkx.shell_layout - rustworkx.spiral_layout - - -.. _serialization: - -Serialization -============= - -.. autosummary:: - :toctree: apiref - - rustworkx.node_link_json - rustworkx.read_graphml - -.. _converters: - -Converters -========== - -.. autosummary:: - :toctree: apiref - - rustworkx.networkx_converter - -.. _api-functions-pydigraph: - -API functions for PyDigraph -=========================== - -These functions are algorithm functions that are type specific for -:class:`~rustworkx.PyDiGraph` or :class:`~rustworkx.PyDAG` objects. Universal -functions from Retworkx API that work for both graph types internally call -the functions from the explicitly typed based on the data type. - -.. autosummary:: - :toctree: apiref - - rustworkx.digraph_is_isomorphic - rustworkx.digraph_is_subgraph_isomorphic - rustworkx.digraph_vf2_mapping - rustworkx.digraph_distance_matrix - rustworkx.digraph_floyd_warshall - rustworkx.digraph_floyd_warshall_numpy - rustworkx.digraph_adjacency_matrix - rustworkx.digraph_all_simple_paths - rustworkx.digraph_all_pairs_all_simple_paths - rustworkx.digraph_astar_shortest_path - rustworkx.digraph_dijkstra_shortest_paths - rustworkx.digraph_all_pairs_dijkstra_shortest_paths - rustworkx.digraph_dijkstra_shortest_path_lengths - rustworkx.digraph_all_pairs_dijkstra_path_lengths - rustworkx.digraph_bellman_ford_shortest_path_lengths - rustworkx.digraph_bellman_ford_shortest_path_lengths - rustworkx.digraph_all_pairs_bellman_ford_shortest_paths - rustworkx.digraph_all_pairs_bellman_ford_path_lengths - rustworkx.digraph_k_shortest_path_lengths - rustworkx.digraph_dfs_edges - rustworkx.digraph_dfs_search - rustworkx.digraph_find_cycle - rustworkx.digraph_transitivity - rustworkx.digraph_core_number - rustworkx.digraph_complement - rustworkx.digraph_union - rustworkx.digraph_tensor_product - rustworkx.digraph_cartesian_product - rustworkx.digraph_random_layout - rustworkx.digraph_bipartite_layout - rustworkx.digraph_circular_layout - rustworkx.digraph_shell_layout - rustworkx.digraph_spiral_layout - rustworkx.digraph_spring_layout - rustworkx.digraph_num_shortest_paths_unweighted - rustworkx.digraph_betweenness_centrality - rustworkx.digraph_edge_betweenness_centrality - rustworkx.digraph_closeness_centrality - rustworkx.digraph_eigenvector_centrality - rustworkx.digraph_katz_centrality - rustworkx.digraph_unweighted_average_shortest_path_length - rustworkx.digraph_bfs_search - rustworkx.digraph_dijkstra_search - rustworkx.digraph_node_link_json - rustworkx.digraph_longest_simple_path - -.. _api-functions-pygraph: - -API functions for PyGraph -========================= - -These functions are algorithm functions that are type specific for -:class:`~rustworkx.PyGraph` objects. Universal functions from Rustworkx API that -work for both graph types internally call the functions from the explicitly -typed API based on the data type. - -.. autosummary:: - :toctree: apiref - - rustworkx.graph_is_isomorphic - rustworkx.graph_is_subgraph_isomorphic - rustworkx.graph_vf2_mapping - rustworkx.graph_distance_matrix - rustworkx.graph_floyd_warshall - rustworkx.graph_floyd_warshall_numpy - rustworkx.graph_adjacency_matrix - rustworkx.graph_all_simple_paths - rustworkx.graph_all_pairs_all_simple_paths - rustworkx.graph_astar_shortest_path - rustworkx.graph_dijkstra_shortest_paths - rustworkx.graph_dijkstra_shortest_path_lengths - rustworkx.graph_all_pairs_dijkstra_shortest_paths - rustworkx.graph_k_shortest_path_lengths - rustworkx.graph_all_pairs_dijkstra_path_lengths - rustworkx.graph_bellman_ford_shortest_path_lengths - rustworkx.graph_bellman_ford_shortest_path_lengths - rustworkx.graph_all_pairs_bellman_ford_shortest_paths - rustworkx.graph_all_pairs_bellman_ford_path_lengths - rustworkx.graph_dfs_edges - rustworkx.graph_dfs_search - rustworkx.graph_transitivity - rustworkx.graph_core_number - rustworkx.graph_complement - rustworkx.graph_union - rustworkx.graph_tensor_product - rustworkx.graph_token_swapper - rustworkx.graph_cartesian_product - rustworkx.graph_random_layout - rustworkx.graph_bipartite_layout - rustworkx.graph_circular_layout - rustworkx.graph_shell_layout - rustworkx.graph_spiral_layout - rustworkx.graph_spring_layout - rustworkx.graph_num_shortest_paths_unweighted - rustworkx.graph_betweenness_centrality - rustworkx.graph_edge_betweenness_centrality - rustworkx.graph_closeness_centrality - rustworkx.graph_eigenvector_centrality - rustworkx.graph_katz_centrality - rustworkx.graph_unweighted_average_shortest_path_length - rustworkx.graph_bfs_search - rustworkx.graph_dijkstra_search - rustworkx.graph_node_link_json - rustworkx.graph_longest_simple_path - -Exceptions -========== - -.. autosummary:: - :toctree: apiref - - rustworkx.InvalidNode - rustworkx.DAGWouldCycle - rustworkx.NoEdgeBetweenNodes - rustworkx.DAGHasCycle - rustworkx.NegativeCycle - rustworkx.NoSuitableNeighbors - rustworkx.NoPathFound - rustworkx.NullGraph - rustworkx.visit.StopSearch - rustworkx.visit.PruneSearch - rustworkx.JSONSerializationError - -Custom Return Types -=================== - -.. autosummary:: - :toctree: apiref - - rustworkx.BFSSuccessors - rustworkx.BFSPredecessors - rustworkx.NodeIndices - rustworkx.EdgeIndices - rustworkx.EdgeList - rustworkx.WeightedEdgeList - rustworkx.EdgeIndexMap - rustworkx.PathMapping - rustworkx.PathLengthMapping - rustworkx.Pos2DMapping - rustworkx.AllPairsPathMapping - rustworkx.AllPairsPathLengthMapping - rustworkx.CentralityMapping - rustworkx.EdgeCentralityMapping - rustworkx.Chains - rustworkx.NodeMap - rustworkx.ProductNodeMap - rustworkx.BiconnectedComponents From 151a314c57f155690e46af4baeb2a0c85225bb67 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:27:55 -0400 Subject: [PATCH 18/22] Fix: Pop unwrap warning by clippy. - Use while let statement to continuously pop the queue. --- rustworkx-core/src/connectivity/cycle_basis.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index dd0b3b2c68..0d794d979e 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -91,9 +91,8 @@ where let mut used: HashMap> = HashMap::new(); used.insert(root_index, HashSet::new()); // Walk the spanning tree - while !stack.is_empty() { + while let Some(z) = stack.pop() { // Use the last element added so that cycles are easier to find - let z = stack.pop().unwrap(); for neighbor in graph.neighbors(z) { // A new node was encountered: if !used.contains_key(&neighbor) { From cdb4a3b9b0f119b0e2f1c08052af0d84142daabc Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:40:41 -0400 Subject: [PATCH 19/22] Lint: Fix typos --- rustworkx-core/src/connectivity/cycle_basis.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index e80665bc49..6578861654 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -116,7 +116,7 @@ where let mut p = pred.get(&z).unwrap(); if edges { let mut cycle: Vec = Vec::new(); - // Retreive all edges from z to neighbor and push to cycle + // Retrieve all edges from z to neighbor and push to cycle cycle.push(get_edge_between(graph, z, neighbor)); // Make last p_node == z @@ -127,12 +127,12 @@ where cycle.push(get_edge_between(graph, *prev_p, *p)); // Update prev_p to p prev_p = p; - // Retreive a new predecessor node from p and replace 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 retreive all edges between the last p and neighbor + // 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); From f8d7ba020d5a99cb8df9b53d50719a5451305d52 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:44:55 -0400 Subject: [PATCH 20/22] Lint: Fix indentation --- rustworkx-core/src/connectivity/cycle_basis.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index 6578861654..a34d05eeb7 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -35,10 +35,9 @@ use std::hash::Hash; /// /// * `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. +/// specified, an arbitrary node is chosen. /// * `edges` - bool for when the user requests the edges instead -/// of the nodes of the cycles. - +/// of the nodes of the cycles. fn inner_cycle_basis( graph: G, root: Option, From b822777fdb88c1eeaf3e8aebdabb602cc54c31cd Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:49:52 -0400 Subject: [PATCH 21/22] Lint: Fix yet another indentation issue --- rustworkx-core/src/connectivity/cycle_basis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustworkx-core/src/connectivity/cycle_basis.rs b/rustworkx-core/src/connectivity/cycle_basis.rs index a34d05eeb7..36e9d14bb4 100644 --- a/rustworkx-core/src/connectivity/cycle_basis.rs +++ b/rustworkx-core/src/connectivity/cycle_basis.rs @@ -255,7 +255,7 @@ where /// /// * `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. +/// specified, an arbitrary node is chosen. /// /// # Example /// ```rust From a44c7e84e349e8df8fb0f024c13b2964e8b27234 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:17:20 -0400 Subject: [PATCH 22/22] Fix: Add missing stubs --- rustworkx/__init__.pyi | 1 + rustworkx/rustworkx.pyi | 1 + 2 files changed, 2 insertions(+) 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: ...