Skip to content

Commit 62f26f9

Browse files
committed
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.
1 parent f1de4bc commit 62f26f9

File tree

5 files changed

+265
-1
lines changed

5 files changed

+265
-1
lines changed

docs/source/api/algorithm_functions/connectivity_and_cycles.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Connectivity and Cycles
1515
rustworkx.weakly_connected_components
1616
rustworkx.is_weakly_connected
1717
rustworkx.cycle_basis
18+
rustworkx.cycle_basis_edges
1819
rustworkx.simple_cycles
1920
rustworkx.digraph_find_cycle
2021
rustworkx.articulation_points

rustworkx-core/src/connectivity/cycle_basis.rs

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// under the License.
1212

1313
use hashbrown::{HashMap, HashSet};
14-
use petgraph::visit::{IntoNeighbors, IntoNodeIdentifiers, NodeCount};
14+
use petgraph::visit::{EdgeRef, IntoEdges, IntoNeighbors, IntoNodeIdentifiers, NodeCount};
1515
use std::hash::Hash;
1616

1717
/// Return a list of cycles which form a basis for cycles of a given graph.
@@ -116,9 +116,160 @@ where
116116
cycles
117117
}
118118

119+
/// Return a list of edges representing cycles which form a basis for cycles of a given graph.
120+
///
121+
/// A basis for cycles of a graph is a minimal collection of
122+
/// cycles such that any cycle in the graph can be written
123+
/// as a sum of cycles in the basis. Here summation of cycles
124+
/// is defined as the exclusive-or of the edges.
125+
///
126+
/// This is adapted from
127+
/// Paton, K. An algorithm for finding a fundamental set of
128+
/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
129+
///
130+
/// The function implicitly assumes that there are no parallel edges.
131+
/// It may produce incorrect/unexpected results if the input graph has
132+
/// parallel edges.
133+
///
134+
///
135+
/// Arguments:
136+
///
137+
/// * `graph` - The graph in which to find the basis.
138+
/// * `root` - Optional node index for starting the basis search. If not
139+
/// specified, an arbitrary node is chosen.
140+
///
141+
/// # Example
142+
/// ```rust
143+
/// use petgraph::prelude::*;
144+
/// use rustworkx_core::connectivity::cycle_basis;
145+
///
146+
/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)];
147+
/// let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
148+
/// let mut res: Vec<Vec<(NodeId, NodeId)>> = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
149+
/// ```
150+
pub fn cycle_basis_edges<G>(graph: G, root: Option<G::NodeId>) -> Vec<Vec<(G::NodeId, G::NodeId)>>
151+
where
152+
G: NodeCount,
153+
G: IntoNeighbors,
154+
G: IntoEdges,
155+
G: IntoNodeIdentifiers,
156+
G::NodeId: Eq + Hash,
157+
{
158+
let mut root_node = root;
159+
let mut graph_nodes: HashSet<G::NodeId> = graph.node_identifiers().collect();
160+
161+
let mut cycles: Vec<Vec<(G::NodeId, G::NodeId)>> = Vec::new();
162+
while !graph_nodes.is_empty() {
163+
let temp_value: G::NodeId;
164+
// If root_node is not set get an arbitrary node from the set of graph
165+
// nodes we've not "examined"
166+
let root_index = match root_node {
167+
Some(root_node) => root_node,
168+
None => {
169+
temp_value = *graph_nodes.iter().next().unwrap();
170+
graph_nodes.remove(&temp_value);
171+
temp_value
172+
}
173+
};
174+
// Stack (ie "pushdown list") of vertices already in the spanning tree
175+
let mut stack: Vec<G::NodeId> = vec![root_index];
176+
// Map of node index to predecessor node index
177+
let mut pred: HashMap<G::NodeId, G::NodeId> = HashMap::new();
178+
pred.insert(root_index, root_index);
179+
// Set of examined nodes during this iteration
180+
let mut used: HashMap<G::NodeId, HashSet<G::NodeId>> = HashMap::new();
181+
used.insert(root_index, HashSet::new());
182+
// Walk the spanning tree
183+
while !stack.is_empty() {
184+
// Use the last element added so that cycles are easier to find
185+
let z = stack.pop().unwrap();
186+
// println!("Length of all edges from: {:?}", edges.len());
187+
for neighbor in graph.neighbors(z) {
188+
// A new node was encountered:
189+
if !used.contains_key(&neighbor) {
190+
pred.insert(neighbor, z);
191+
stack.push(neighbor);
192+
let mut temp_set: HashSet<G::NodeId> = HashSet::new();
193+
temp_set.insert(z);
194+
used.insert(neighbor, temp_set);
195+
// A self loop:
196+
} else if z == neighbor {
197+
let cycle_edge: Vec<(G::NodeId, G::NodeId)> = graph
198+
.edges(z)
199+
.filter(|edge: &G::EdgeRef| edge.source() == z && edge.target() == z)
200+
.map(|edge: G::EdgeRef| (edge.source(), edge.target()))
201+
.collect();
202+
cycles.push(cycle_edge);
203+
// A cycle was found:
204+
} else if !used.get(&z).unwrap().contains(&neighbor) {
205+
let pn = used.get(&neighbor).unwrap();
206+
let mut cycle: Vec<(G::NodeId, G::NodeId)> = vec![];
207+
let mut p = pred.get(&z).unwrap();
208+
// Retreive all edges from z to neighbor and vice versa
209+
let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph
210+
.edges(z)
211+
.filter(|edge: &G::EdgeRef| {
212+
edge.source() == neighbor || edge.target() == neighbor
213+
})
214+
.map(|edge: G::EdgeRef| (edge.source(), edge.target()))
215+
.collect();
216+
// Append to cycle
217+
cycle.append(&mut neigh_edge);
218+
// Make last p_node == z
219+
let mut prev_p: &G::NodeId = &z;
220+
// While p is in the neighborhood of neighbor
221+
while !pn.contains(p) {
222+
// Retrieve all edges from prev_p to p and vice versa
223+
let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph
224+
.edges(*prev_p)
225+
.filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p)
226+
.map(|edge: G::EdgeRef| (edge.source(), edge.target()))
227+
.collect();
228+
// Append to cycle
229+
cycle.append(&mut neigh_edge);
230+
// Update prev_p to p
231+
prev_p = p;
232+
// Retreive a new predecessor node from p and replace p
233+
p = pred.get(p).unwrap();
234+
}
235+
// When loop ends add remaining edges from prev_p to p.
236+
let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph
237+
.edges(*prev_p)
238+
.filter(|edge: &G::EdgeRef| edge.target() == *p || edge.source() == *p)
239+
.map(|edge: G::EdgeRef| (edge.source(), edge.target()))
240+
.collect();
241+
cycle.append(&mut neigh_edge);
242+
// Also retreive all edges between the last p and neighbor
243+
let mut neigh_edge: Vec<(G::NodeId, G::NodeId)> = graph
244+
.edges(*p)
245+
.filter(|edge: &G::EdgeRef| {
246+
edge.target() == neighbor || edge.source() == neighbor
247+
})
248+
.map(|edge: G::EdgeRef| (edge.source(), edge.target()))
249+
.collect();
250+
cycle.append(&mut neigh_edge);
251+
252+
// Once all edges within cycle have been found, push to cycle list.
253+
cycles.push(cycle);
254+
let neighbor_set: &mut HashSet<G::NodeId> = used.get_mut(&neighbor).unwrap();
255+
neighbor_set.insert(z);
256+
}
257+
}
258+
}
259+
let mut temp_hashset: HashSet<G::NodeId> = HashSet::new();
260+
for key in pred.keys() {
261+
temp_hashset.insert(*key);
262+
}
263+
graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect();
264+
root_node = None;
265+
}
266+
cycles
267+
}
268+
119269
#[cfg(test)]
120270
mod tests {
121271
use crate::connectivity::cycle_basis;
272+
use crate::connectivity::cycle_basis_edges;
122273
use petgraph::prelude::*;
123274

124275
fn sorted_cycle(cycles: Vec<Vec<NodeIndex>>) -> Vec<Vec<usize>> {
@@ -132,6 +283,26 @@ mod tests {
132283
sorted_cycles
133284
}
134285

286+
fn sorted_cycle_edges(cycles: Vec<Vec<(NodeIndex, NodeIndex)>>) -> Vec<Vec<(usize, usize)>> {
287+
let mut sorted_cycles: Vec<Vec<(usize, usize)>> = vec![];
288+
for cycle in cycles {
289+
let mut cycle: Vec<(usize, usize)> = cycle
290+
.iter()
291+
.map(|x| {
292+
if x.0 < x.1 {
293+
(x.0.index(), x.1.index())
294+
} else {
295+
(x.1.index(), x.0.index())
296+
}
297+
})
298+
.collect();
299+
cycle.sort();
300+
sorted_cycles.push(cycle);
301+
}
302+
sorted_cycles.sort();
303+
sorted_cycles
304+
}
305+
135306
#[test]
136307
fn test_cycle_basis_source() {
137308
let edge_list = vec![
@@ -158,6 +329,28 @@ mod tests {
158329
assert_eq!(sorted_cycle(res_9), expected);
159330
}
160331

332+
#[test]
333+
fn test_cycle_edge_basis_source() {
334+
let edge_list = vec![
335+
(0, 0),
336+
(0, 1),
337+
(1, 2),
338+
(2, 3),
339+
(2, 5),
340+
(5, 6),
341+
(3, 6),
342+
(3, 4),
343+
];
344+
let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
345+
let expected = vec![vec![(0, 0)], vec![(2, 3), (2, 5), (3, 6), (5, 6)]];
346+
let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
347+
assert_eq!(sorted_cycle_edges(res_0), expected);
348+
let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2)));
349+
assert_eq!(sorted_cycle_edges(res_1), expected);
350+
let res_9 = cycle_basis_edges(&graph, Some(NodeIndex::new(6)));
351+
assert_eq!(sorted_cycle_edges(res_9), expected);
352+
}
353+
161354
#[test]
162355
fn test_self_loop() {
163356
let edge_list = vec![
@@ -187,4 +380,34 @@ mod tests {
187380
]
188381
);
189382
}
383+
384+
#[test]
385+
fn test_self_loop_edges() {
386+
let edge_list = vec![
387+
(0, 1),
388+
(0, 3),
389+
(0, 5),
390+
(0, 8),
391+
(1, 2),
392+
(1, 6),
393+
(2, 3),
394+
(3, 4),
395+
(4, 5),
396+
(6, 7),
397+
(7, 8),
398+
(8, 9),
399+
];
400+
let mut graph = UnGraph::<i32, i32>::from_edges(&edge_list);
401+
graph.add_edge(NodeIndex::new(1), NodeIndex::new(1), 0);
402+
let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
403+
assert_eq!(
404+
sorted_cycle_edges(res_0),
405+
vec![
406+
vec![(0, 1), (0, 3), (1, 2), (2, 3)],
407+
vec![(0, 1), (0, 8), (1, 6), (6, 7), (7, 8)],
408+
vec![(0, 3), (0, 5), (3, 4), (4, 5)],
409+
vec![(1, 1)],
410+
]
411+
);
412+
}
190413
}

rustworkx-core/src/connectivity/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ pub use conn_components::connected_components;
3131
pub use conn_components::number_connected_components;
3232
pub use core_number::core_number;
3333
pub use cycle_basis::cycle_basis;
34+
pub use cycle_basis::cycle_basis_edges;
3435
pub use find_cycle::find_cycle;
3536
pub use min_cut::stoer_wagner_min_cut;

src/connectivity/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,44 @@ pub fn cycle_basis(graph: &graph::PyGraph, root: Option<usize>) -> Vec<Vec<usize
7474
.collect()
7575
}
7676

77+
/// Return a list of edges representing cycles which form a basis for cycles of a given PyGraph
78+
///
79+
/// A basis for cycles of a graph is a minimal collection of
80+
/// cycles such that any cycle in the graph can be written
81+
/// as a sum of cycles in the basis. Here summation of cycles
82+
/// is defined as the exclusive or of the edges.
83+
///
84+
/// This is adapted from algorithm CACM 491 [1]_.
85+
///
86+
/// .. note::
87+
///
88+
/// The function implicitly assumes that there are no parallel edges.
89+
/// It may produce incorrect/unexpected results if the input graph has
90+
/// parallel edges.
91+
///
92+
/// :param PyGraph graph: The graph to find the cycle basis in
93+
/// :param int root: Optional index for starting node for basis
94+
///
95+
/// :returns: A list of edge lists. Each list is a list of tuples of node ids which
96+
/// forms a cycle (loop) in the input graph
97+
/// :rtype: list
98+
///
99+
/// .. [1] Paton, K. An algorithm for finding a fundamental set of
100+
/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
101+
#[pyfunction]
102+
#[pyo3(text_signature = "(graph, /, root=None)")]
103+
pub fn cycle_basis_edges(graph: &graph::PyGraph, root: Option<usize>) -> Vec<Vec<(usize, usize)>> {
104+
connectivity::cycle_basis_edges(&graph.graph, root.map(NodeIndex::new))
105+
.into_iter()
106+
.map(|res_map| {
107+
res_map
108+
.into_iter()
109+
.map(|x| (x.0.index(), x.1.index()))
110+
.collect()
111+
})
112+
.collect()
113+
}
114+
77115
/// Find all simple cycles of a :class:`~.PyDiGraph`
78116
///
79117
/// A "simple cycle" (called an elementary circuit in [1]) is a cycle (or closed path)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
448448
m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?;
449449
m.add_wrapped(wrap_pyfunction!(random_geometric_graph))?;
450450
m.add_wrapped(wrap_pyfunction!(cycle_basis))?;
451+
m.add_wrapped(wrap_pyfunction!(cycle_basis_edges))?;
451452
m.add_wrapped(wrap_pyfunction!(simple_cycles))?;
452453
m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?;
453454
m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?;

0 commit comments

Comments
 (0)