Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
62f26f9
Initial implementation of cycle_basis_edges
raynelfss May 31, 2023
35eea7e
Hotfix: DocTest now passes
raynelfss May 31, 2023
a8c1b67
Correction: Function returns EdgeIndex
raynelfss Jun 1, 2023
c22c0b5
Docs: Added release note
raynelfss Jun 1, 2023
c6dea4e
CI: Added python tests to test_cycle_basis.py
raynelfss Jun 2, 2023
23f2759
Correction: get_edge_between checks target only
raynelfss Jun 2, 2023
dadb482
Feature: merge basis and basis_edges into one
raynelfss Jun 6, 2023
50d8685
CI: Modify python tests for new changes
raynelfss Jun 6, 2023
7c45634
Docs: made changes to releasenotes and api.rst
raynelfss Jun 6, 2023
c0f9f94
Fix: Make `cycle_basis` and `EdgeOrNode` private
raynelfss Jun 8, 2023
df5b3b5
CI: Modify python tests for new changes
raynelfss Jun 8, 2023
26146af
Docs: Fixed release notes with the latest changes
raynelfss Jun 8, 2023
f3f271a
Correction: remove redundant self-loop check
raynelfss Jun 12, 2023
70541c8
Docs: Add upgrade release note
raynelfss Jun 12, 2023
dc7eb26
Correction: `get_edge_between` returns `EdgeId`.
raynelfss Jun 19, 2023
38bdeac
Docs: Removed fix section of docstring.
raynelfss Jul 11, 2023
075b2f0
Docs: Removed old api.rst file
raynelfss Aug 11, 2023
151a314
Fix: Pop unwrap warning by clippy.
raynelfss Aug 11, 2023
7f52970
Merge branch 'main' into cycle_basis_edges
raynelfss Aug 12, 2023
10c16bf
Merge branch 'main' into cycle_basis_edges
raynelfss Aug 23, 2023
62169b4
Merge branch 'main' into cycle_basis_edges
raynelfss Dec 14, 2023
6d11293
Merge branch 'main' into cycle_basis_edges
raynelfss Jul 16, 2024
bcf8ce1
Merge remote-tracking branch 'upstream/main' into cycle_basis_edges
raynelfss Apr 9, 2025
cdb4a3b
Lint: Fix typos
raynelfss Apr 9, 2025
f8d7ba0
Lint: Fix indentation
raynelfss Apr 9, 2025
b822777
Lint: Fix yet another indentation issue
raynelfss Apr 9, 2025
a44c7e8
Fix: Add missing stubs
raynelfss Apr 9, 2025
54e7009
Merge branch 'main' into cycle_basis_edges
raynelfss Apr 10, 2025
09d366b
Merge branch 'main' into cycle_basis_edges
raynelfss Jun 5, 2026
c61019b
Rebalance `cycle_basis` pipeline
raynelfss Jun 5, 2026
dc66547
FIx: Relax trait bounds for functions
raynelfss Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Connectivity and Cycles
rustworkx.weakly_connected_components
rustworkx.is_weakly_connected
rustworkx.cycle_basis
rustworkx.cycle_basis_edges
rustworkx.simple_cycles
rustworkx.digraph_find_cycle
rustworkx.articulation_points
Expand Down
56 changes: 56 additions & 0 deletions releasenotes/notes/add-cycle-basis-edges-5cb31eac7e41096d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
features:
- |
A function, ``cycle_basis_edges`` was added to the crate
``rustworkx-core`` in the ``connectivity`` module. This function returns
the edge indices that form the cycle basis of a graph.
- |
Added a new function :func:`~rustworkx.cycle_basis_edges` which is similar
to the existing :func:`~.cycle_basis` function but instead of returning node
indices it returns a list of edge indices for the cycle.

.. jupyter-execute::

import rustworkx
from rustworkx.visualization import * # Needs matplotlib/

graph = rustworkx.PyGraph()

# Each time add node is called, it returns a new node index
a = graph.add_node("A")
b = graph.add_node("B")
c = graph.add_node("C")
d = graph.add_node("D")
e = graph.add_node("E")
f = graph.add_node("F")
g = graph.add_node("G")
h = graph.add_node("H")
i = graph.add_node("I")
j = graph.add_node("J")

# add_edges_from takes tuples of node indices and weights,
# and returns edge indices
graph.add_edges_from([
(a, b, 1.5),
(a, a, 1.0),
(a, c, 5.0),
(b, c, 2.5),
(c, d, 1.3),
(d, e, 0.8),
(e, f, 1.6),
(f, d, 0.7),
(e, g, 0.7),
(g, h, 0.9),
(g, i, 1.0),
(i, j, 0.8),
])

mpl_draw(graph, with_labels=True)
# Retrieve EdgeIDs by enabling the edges flag.
cycles_edges = rustworkx.cycle_basis_edges(graph, a)
edge_list = list(graph.edge_list())
cycle_info = [[edge_list[edge] for edge in cycle] for cycle in cycles_edges]
# Print the EdgeID's that form cycles in the graph
display(cycles_edges)
# Print the data retrieved from the graph.
display(cycle_info)
262 changes: 232 additions & 30 deletions rustworkx-core/src/connectivity/cycle_basis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
// under the License.

use hashbrown::{HashMap, HashSet};
use petgraph::visit::{IntoNeighbors, IntoNodeIdentifiers, NodeCount};
use petgraph::visit::{EdgeRef, IntoEdges, IntoNeighbors, IntoNodeIdentifiers, NodeCount};
use std::hash::Hash;

/// Return a list of cycles which form a basis for cycles of a given graph.
/// Inner private function for `cycle_basis` and `cycle_basis_edges`.
/// Returns a list of cycles which forms a basis of cycles of a given
/// graph.
///
/// A basis for cycles of a graph is a minimal collection of
/// cycles such that any cycle in the graph can be written
Expand All @@ -29,32 +31,37 @@ use std::hash::Hash;
/// It may produce incorrect/unexpected results if the input graph has
/// parallel edges.
///
///
/// Arguments:
///
/// * `graph` - The graph in which to find the basis.
/// * `root` - Optional node index for starting the basis search. If not
/// specified, an arbitrary node is chosen.
///
/// # Example
/// ```rust
/// use petgraph::prelude::*;
/// use rustworkx_core::connectivity::cycle_basis;
///
/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)];
/// let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
/// let mut res: Vec<Vec<NodeIndex>> = cycle_basis(&graph, Some(NodeIndex::new(0)));
/// ```
pub fn cycle_basis<G>(graph: G, root: Option<G::NodeId>) -> Vec<Vec<G::NodeId>>
/// * `self_cycle_filter` - Specifies the behavior when a single node cycle
/// is found.
/// * `cycle_filter` - Specifies the behavior when a cycle is found
fn inner_cycle_basis<G, T>(
graph: G,
root: Option<G::NodeId>,
self_cycle_filter: impl Fn(G, G::NodeId) -> Vec<T>,
cycle_filter: impl Fn(
G,
&HashSet<G::NodeId>,
&HashMap<G::NodeId, G::NodeId>,
G::NodeId,
G::NodeId,
) -> Vec<T>,
) -> Vec<Vec<T>>
where
G: NodeCount,
G: IntoNeighbors,
G: IntoNodeIdentifiers,
T: Eq + Hash,
G::NodeId: Eq + Hash,
{
let mut root_node = root;
let mut root_node: Option<G::NodeId> = root;
let mut graph_nodes: HashSet<G::NodeId> = graph.node_identifiers().collect();
let mut cycles: Vec<Vec<G::NodeId>> = Vec::new();
let mut cycles: Vec<Vec<T>> = 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
Expand All @@ -76,8 +83,8 @@ where
let mut used: HashMap<G::NodeId, HashSet<G::NodeId>> = HashMap::new();
used.insert(root_index, HashSet::new());
// Walk the spanning tree
// Use the last element added so that cycles are easier to find
while let Some(z) = stack.pop() {
// Use the last element added so that cycles are easier to find
for neighbor in graph.neighbors(z) {
// A new node was encountered:
if !used.contains_key(&neighbor) {
Expand All @@ -88,20 +95,12 @@ where
used.insert(neighbor, temp_set);
// A self loop:
} else if z == neighbor {
let cycle: Vec<G::NodeId> = vec![z];
cycles.push(cycle);
cycles.push(self_cycle_filter(graph, z))
// A cycle was found:
} else if !used.get(&z).unwrap().contains(&neighbor) {
let prev_n = used.get(&neighbor).unwrap();
let mut cycle: Vec<G::NodeId> = vec![neighbor, z];
let mut p = pred.get(&z).unwrap();
while !prev_n.contains(p) {
cycle.push(*p);
p = pred.get(p).unwrap();
}
cycle.push(*p);
cycles.push(cycle);
let neighbor_set = used.get_mut(&neighbor).unwrap();
cycles.push(cycle_filter(graph, prev_n, &pred, z, neighbor));
let neighbor_set: &mut HashSet<G::NodeId> = used.get_mut(&neighbor).unwrap();
neighbor_set.insert(z);
}
}
Expand All @@ -116,15 +115,166 @@ where
cycles
}

/// Returns lists of `NodeIndex` representing cycles which form
/// a basis for cycles of a given graph.
///
/// A basis for cycles of a graph is a minimal collection of
/// cycles such that any cycle in the graph can be written
/// as a sum of cycles in the basis. Here summation of cycles
/// is defined as the exclusive-or of the edges.
///
/// This is adapted from
/// Paton, K. An algorithm for finding a fundamental set of
/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
///
/// The function implicitly assumes that there are no parallel edges.
/// It may produce incorrect/unexpected results if the input graph has
/// parallel edges.
///
///
/// Arguments:
///
/// * `graph` - The graph in which to find the basis.
/// * `root` - Optional node index for starting the basis search. If not
/// specified, an arbitrary node is chosen.
///
/// # Example
/// ```rust
/// use petgraph::prelude::*;
/// use rustworkx_core::connectivity::cycle_basis;
///
/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)];
/// let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
/// let mut res: Vec<Vec<NodeIndex>> = cycle_basis(&graph, Some(NodeIndex::new(0)));
/// ```
pub fn cycle_basis<G>(graph: G, root: Option<G::NodeId>) -> Vec<Vec<G::NodeId>>
where
G: NodeCount,
G: IntoNeighbors,
G: IntoNodeIdentifiers,
G::NodeId: Eq + Hash,
{
// inner_cycle_basis(graph, root, false).unwrap_nodes()
let self_cycle_filter = |_graph: G, node: G::NodeId| -> Vec<G::NodeId> { vec![node] };
let cycle_filter = |_graph: G,
prev_n: &HashSet<G::NodeId>,
pred_to_node: &HashMap<G::NodeId, G::NodeId>,
origin: G::NodeId,
neighbor: G::NodeId|
-> Vec<G::NodeId> {
let mut p = pred_to_node.get(&origin).unwrap();
// Append neighbor and z to cycle.
let mut cycle: Vec<G::NodeId> = vec![neighbor, origin];
while !prev_n.contains(p) {
cycle.push(*p);
p = pred_to_node.get(p).unwrap();
}
cycle.push(*p);
cycle
};
inner_cycle_basis(graph, root, self_cycle_filter, cycle_filter)
}

/// Returns lists of `EdgeIndex` representing cycles which form
/// a basis for cycles of a given graph.
///
/// A basis for cycles of a graph is a minimal collection of
/// cycles such that any cycle in the graph can be written
/// as a sum of cycles in the basis. Here summation of cycles
/// is defined as the exclusive-or of the edges.
///
/// This is adapted from
/// Paton, K. An algorithm for finding a fundamental set of
/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
///
/// The function implicitly assumes that there are no parallel edges.
/// It may produce incorrect/unexpected results if the input graph has
/// parallel edges.
///
///
/// Arguments:
///
/// * `graph` - The graph in which to find the basis.
/// * `root` - Optional node index for starting the basis search. If not
/// specified, an arbitrary node is chosen.
///
/// # Example
/// ```rust
/// use petgraph::prelude::*;
/// use rustworkx_core::connectivity::cycle_basis_edges;
///
/// let edge_list = [(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (3, 4), (4, 5)];
/// let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
/// let mut res: Vec<Vec<EdgeIndex>> = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
/// ```
pub fn cycle_basis_edges<G>(graph: G, root: Option<G::NodeId>) -> Vec<Vec<G::EdgeId>>
where
G: NodeCount,
G: IntoEdges,
G: IntoNodeIdentifiers,
G::NodeId: Eq + Hash,
G::EdgeId: Eq + Hash,
{
/// Method used to retrieve all the edges between an origin node and a target node.
fn get_edge_between<G>(orig_graph: G, origin: G::NodeId, target: G::NodeId) -> G::EdgeId
where
G: IntoEdges,
{
orig_graph
.edges(origin)
.filter_map(|edge: G::EdgeRef| (edge.target() == target).then_some(edge.id()))
.next()
.expect("An edge should exist between origin and target node")
}

// inner_cycle_basis(graph, root, false).unwrap_nodes()
let self_cycle_filter =
|graph: G, node: G::NodeId| -> Vec<G::EdgeId> { vec![get_edge_between(graph, node, node)] };
let cycle_filter = |graph: G,
prev_n: &HashSet<G::NodeId>,
pred_to_node: &HashMap<G::NodeId, G::NodeId>,
origin: G::NodeId,
neighbor: G::NodeId|
-> Vec<G::EdgeId> {
let mut p = pred_to_node.get(&origin).unwrap();
let mut cycle: Vec<G::EdgeId> = Vec::new();
// Retrieve all edges from z to neighbor and push to cycle
cycle.push(get_edge_between(graph, origin, neighbor));

// Make last p_node == z
let mut prev_p: &G::NodeId = &origin;
// While p is in the neighborhood of neighbor
while !prev_n.contains(p) {
// Retrieve all edges from prev_p to p and vice versa append to cycle
cycle.push(get_edge_between(graph, *prev_p, *p));
// Update prev_p to p
prev_p = p;
// Retrieve a new predecessor node from p and replace p
p = pred_to_node.get(p).unwrap();
}
// When loop ends add remaining edges from prev_p to p.
cycle.push(get_edge_between(graph, *prev_p, *p));
// Also retrieve all edges between the last p and neighbor
cycle.push(get_edge_between(graph, *p, neighbor));
cycle
};
inner_cycle_basis(graph, root, self_cycle_filter, cycle_filter)
}

#[cfg(test)]
mod tests {
use crate::connectivity::cycle_basis;
use crate::connectivity::cycle_basis_edges;
use petgraph::prelude::*;
use petgraph::stable_graph::GraphIndex;

fn sorted_cycle(cycles: Vec<Vec<NodeIndex>>) -> Vec<Vec<usize>> {
fn sorted_cycle<T>(cycles: Vec<Vec<T>>) -> Vec<Vec<usize>>
where
T: GraphIndex,
{
let mut sorted_cycles: Vec<Vec<usize>> = vec![];
for cycle in cycles {
let mut cycle: Vec<usize> = cycle.iter().map(|x| x.index()).collect();
let mut cycle: Vec<usize> = cycle.iter().map(|x: &T| x.index()).collect();
cycle.sort();
sorted_cycles.push(cycle);
}
Expand Down Expand Up @@ -158,6 +308,28 @@ mod tests {
assert_eq!(sorted_cycle(res_9), expected);
}

#[test]
fn test_cycle_edge_basis_source() {
let edge_list = vec![
(0, 0),
(0, 1),
(1, 2),
(2, 3),
(2, 5),
(5, 6),
(3, 6),
(3, 4),
];
let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
let expected = vec![vec![0], vec![3, 4, 5, 6]];
let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
assert_eq!(sorted_cycle(res_0), expected);
let res_1 = cycle_basis_edges(&graph, Some(NodeIndex::new(2)));
assert_eq!(sorted_cycle(res_1), expected);
let res_9 = cycle_basis_edges(&graph, Some(NodeIndex::new(6)));
assert_eq!(sorted_cycle(res_9), expected);
}

#[test]
fn test_self_loop() {
let edge_list = vec![
Expand Down Expand Up @@ -187,4 +359,34 @@ mod tests {
]
);
}

#[test]
fn test_self_loop_edges() {
let edge_list = vec![
(0, 1),
(0, 3),
(0, 5),
(0, 8),
(1, 2),
(1, 6),
(2, 3),
(3, 4),
(4, 5),
(6, 7),
(7, 8),
(8, 9),
];
let mut graph = UnGraph::<i32, i32>::from_edges(&edge_list);
graph.add_edge(NodeIndex::new(1), NodeIndex::new(1), 0);
let res_0 = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
assert_eq!(
sorted_cycle(res_0),
vec![
vec![0, 1, 4, 6],
vec![0, 3, 5, 9, 10],
vec![1, 2, 7, 8],
vec![12],
]
);
}
}
Loading
Loading