diff --git a/rustworkx-core/src/connectivity/minimal_cycle_basis.rs b/rustworkx-core/src/connectivity/minimal_cycle_basis.rs new file mode 100644 index 000000000..a659db9c0 --- /dev/null +++ b/rustworkx-core/src/connectivity/minimal_cycle_basis.rs @@ -0,0 +1,539 @@ +use crate::connectivity::conn_components::connected_components; +use crate::dictmap::*; +use crate::shortest_path::{astar, dijkstra}; +use crate::Result; +use hashbrown::{HashMap, HashSet}; +use petgraph::algo::{min_spanning_tree, Measure}; +use petgraph::csr::{DefaultIx, IndexType}; +use petgraph::data::{DataMap, Element}; +use petgraph::graph::Graph; +use petgraph::graph::{EdgeIndex, NodeIndex}; +use petgraph::visit::{ + EdgeCount, EdgeIndexable, EdgeRef, GraphProp, IntoEdgeReferences, IntoEdges, + IntoNeighborsDirected, IntoNodeIdentifiers, IntoNodeReferences, NodeIndexable, Visitable, +}; +use petgraph::Undirected; +use std::cmp::Ordering; +use std::convert::Infallible; +use std::hash::Hash; + +type SubgraphResult = Result>, E>; +type SubgraphData = ( + Graph, + HashMap, + HashMap, +); + +fn create_subgraphs_from_components( + graph: G, + components: Vec>, + mut weight_fn: F, +) -> SubgraphResult +where + G: IntoEdgeReferences + + NodeIndexable + + IntoNodeIdentifiers + + EdgeIndexable + + EdgeCount + + Visitable + + IntoEdges, + G::NodeId: Eq + Hash, + G::EdgeWeight: Clone, + F: FnMut(G::EdgeRef) -> Result, + K: Clone + PartialOrd + Copy + Measure + Default, +{ + components + .into_iter() + .map(|component| { + let mut subgraph = Graph::::default(); + let mut node_subnode_map: HashMap = HashMap::new(); + let mut edge_subedge_map: HashMap = HashMap::new(); + for nodeid in graph.node_identifiers() { + if component.contains(&nodeid) { + let node = NodeIndexable::to_index(&graph, nodeid); + let subnode = subgraph.add_node(node); + node_subnode_map.insert(node, subnode); + } + } + let mut edge_weights: Vec> = vec![None; graph.edge_bound()]; + for edge in graph.edge_references() { + let index = EdgeIndexable::to_index(&graph, edge.id()); + edge_weights[index] = Some(weight_fn(edge)?); + } + let edge_cost = |e: G::EdgeRef| -> Result { + Ok(edge_weights[EdgeIndexable::to_index(&graph, e.id())].unwrap()) + }; + for edge in graph.edge_references() { + let source = edge.source(); + let target = edge.target(); + if component.contains(&source) && component.contains(&target) { + let subsource = node_subnode_map[&NodeIndexable::to_index(&graph, source)]; + let subtarget = node_subnode_map[&NodeIndexable::to_index(&graph, target)]; + let weight = edge_cost(edge); + let subedge = subgraph.add_edge(subsource, subtarget, weight.unwrap()); + edge_subedge_map.insert(EdgeIndexable::to_index(&graph, edge.id()), subedge); + } + } + Ok((subgraph, node_subnode_map, edge_subedge_map)) + }) + .collect() +} + +fn _min_cycle( + subgraph: H, + orth: HashSet<(usize, usize)>, + mut weight_fn: F, +) -> Result, E> +where + H: IntoNodeReferences + IntoEdgeReferences + DataMap + NodeIndexable + EdgeIndexable, + H::NodeId: Eq + Hash, + F: FnMut(H::EdgeRef) -> Result, + K: Clone + PartialOrd + Copy + Measure + Default, +{ + let mut gi = Graph::<_, _, petgraph::Undirected>::default(); + let mut subgraph_gi_map = HashMap::new(); + let mut gi_subgraph_map = HashMap::new(); + for node in subgraph.node_identifiers() { + let gi_node = gi.add_node(node); + let gi_lifted_node = gi.add_node(node); + gi_subgraph_map.insert(gi_node, node); + gi_subgraph_map.insert(gi_lifted_node, node); + subgraph_gi_map.insert(node, (gi_node, gi_lifted_node)); + } + // # Add 2 copies of each edge in G to Gi. + // # If edge is in orth, add cross edge; otherwise in-plane edge + + for edge in subgraph.edge_references() { + let u_id = edge.source(); + let v_id = edge.target(); + let u = NodeIndexable::to_index(&subgraph, u_id); + let v = NodeIndexable::to_index(&subgraph, v_id); + let weight = weight_fn(edge)?; + // For each pair of (u, v) from the subgraph, there is a corresponding double pair of (u_node, v_node) and (u_lifted_node, v_lifted_node) in the gi + let (u_node, u_lifted_node) = subgraph_gi_map[&u_id]; + let (v_node, v_lifted_node) = subgraph_gi_map[&v_id]; + if orth.contains(&(u, v)) || orth.contains(&(v, u)) { + // Add cross edges with weight + gi.add_edge(u_node, v_lifted_node, weight); + gi.add_edge(u_lifted_node, v_node, weight); + } else { + // Add in-plane edges with weight + gi.add_edge(u_node, v_node, weight); + gi.add_edge(u_lifted_node, v_lifted_node, weight); + } + } + // Instead of finding the shortest path between each node and its lifted node, store the shortest paths in a list to find the shortest paths among them + let mut shortest_path_map: HashMap = HashMap::new(); + for subnodeid in subgraph.node_identifiers() { + let (gi_nodeidx, gi_lifted_nodeidx) = subgraph_gi_map[&subnodeid]; + + let result: Result> = dijkstra( + &gi, + gi_nodeidx, + Some(gi_lifted_nodeidx), + |edge| Ok(*edge.weight()), + None, + ); + let spl = result.unwrap()[&gi_lifted_nodeidx]; + shortest_path_map.insert(subnodeid, spl); + } + let min_start = shortest_path_map + .iter() + .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(Ordering::Equal)) + .unwrap() + .0; + let min_start_node = subgraph_gi_map[min_start].0; + let min_start_lifted_node = subgraph_gi_map[min_start].1; + let result: Result)>> = astar( + &gi, + min_start_node, + |finish| Ok(finish == min_start_lifted_node), + |e| Ok(*e.weight()), + |_| Ok(K::default()), + ); + + let mut min_path: Vec = Vec::new(); + match result { + Ok(Some((_cost, path))) => { + for node in path { + if let Some(&subgraph_nodeid) = gi_subgraph_map.get(&node) { + let subgraph_node = NodeIndexable::to_index(&subgraph, subgraph_nodeid); + min_path.push(subgraph_node.index()); + } + } + } + Ok(None) => {} + Err(_) => {} + } + let edgelist = min_path + .windows(2) + .map(|w| (w[0], w[1])) + .collect::>(); + let mut edgeset: HashSet<(usize, usize)> = HashSet::new(); + for e in edgelist.iter() { + if edgeset.contains(e) { + edgeset.remove(e); + } else if edgeset.contains(&(e.1, e.0)) { + edgeset.remove(&(e.1, e.0)); + } else { + edgeset.insert(*e); + } + } + let mut min_edgelist: Vec<(usize, usize)> = Vec::new(); + for e in edgelist.iter() { + if edgeset.contains(e) { + min_edgelist.push(*e); + edgeset.remove(e); + } else if edgeset.contains(&(e.1, e.0)) { + min_edgelist.push((e.1, e.0)); + edgeset.remove(&(e.1, e.0)); + } + } + Ok(min_edgelist) +} + +fn _min_cycle_basis( + subgraph: H, + mut weight_fn: F, + node_subnode_map: &HashMap, +) -> Result>, E> +where + H: EdgeCount + + IntoNodeReferences + + IntoEdgeReferences + + NodeIndexable + + DataMap + + EdgeIndexable, + H::NodeWeight: Clone, + H::EdgeWeight: Clone + PartialOrd, + H::NodeId: Eq + Hash, + F: FnMut(H::EdgeRef) -> Result, + K: Clone + PartialOrd + Copy + Measure + Default, +{ + let mut sub_cb: Vec> = Vec::new(); + let num_edges = subgraph.edge_count(); + let mut sub_edges: Vec<(usize, usize)> = Vec::with_capacity(num_edges); + let node_map: HashMap = subgraph + .node_identifiers() + .enumerate() + .map(|(index, node_index)| (node_index, index)) + .collect(); + for edge in subgraph.edge_references() { + sub_edges.push((node_map[&edge.source()], node_map[&edge.target()])); + } + let mst = min_spanning_tree(&subgraph); + let sub_mst_edges: Vec<_> = mst + .filter_map(|element| { + if let Element::Edge { + source, + target, + weight: _, + } = element + { + Some((source, target)) + } else { + None + } + }) + .collect(); + + let mut chords: Vec<(usize, usize)> = Vec::new(); + for edge in sub_edges.iter() { + if !sub_mst_edges.contains(edge) { + // If it's not in the MST, it's a chord + chords.push(*edge); + } + } + let mut set_orth: Vec> = Vec::new(); + // Fill `set_orth` with individual chords + for chord in chords.iter() { + let mut chord_set = HashSet::new(); + chord_set.insert(*chord); + set_orth.push(chord_set); + } + while let Some(chord_pop) = set_orth.pop() { + let base = chord_pop; + let cycle_edges = _min_cycle(subgraph, base.clone(), &mut weight_fn)?; + let mut cb_temp: Vec = Vec::new(); + for edge in cycle_edges.iter() { + cb_temp.push(edge.1); + } + sub_cb.push(cb_temp); + for orth in &mut set_orth { + let mut new_orth = HashSet::new(); + if cycle_edges + .iter() + .filter(|edge| orth.contains(*edge) || orth.contains(&(edge.1, edge.0))) + .count() + % 2 + == 1 + { + for e in orth.iter() { + if !base.contains(e) && !base.contains(&(e.1, e.0)) { + new_orth.insert(*e); + } + } + for e in base.iter() { + if !orth.contains(e) && !orth.contains(&(e.1, e.0)) { + new_orth.insert(*e); + } + } + *orth = new_orth; + } else { + let orth_clone = orth.clone(); + orth.clone_from(&orth_clone); + } + } + } + // Using the node_subnode_map, convert the subnode usize in cb via NodeIndex::new() to original graph NodeIndex + let cb: Vec> = sub_cb + .iter() + .map(|cycle| { + cycle + .iter() + .map(|node| { + let subnode = NodeIndex::new(*node); + let nodeid = node_subnode_map + .iter() + .find(|(_node, &subnode_index)| subnode_index == subnode) + .unwrap() + .0; + // convert node to NodeIndex + NodeIndex::new(*nodeid) + }) + .collect() + }) + .collect(); + Ok(cb) +} + +pub fn minimal_cycle_basis(graph: G, mut weight_fn: F) -> Result>, E> +where + G: EdgeCount + + IntoNodeIdentifiers + + NodeIndexable + + EdgeIndexable + + DataMap + + GraphProp + + IntoNeighborsDirected + + Visitable + + IntoEdges, + G::EdgeWeight: Clone, + G::NodeId: Eq + Hash, + F: FnMut(G::EdgeRef) -> Result, + K: Clone + PartialOrd + Copy + Measure + Default, +{ + let conn_components = connected_components(&graph); + let mut min_cycle_basis = Vec::new(); + let subgraphs_with_maps = + create_subgraphs_from_components(&graph, conn_components, &mut weight_fn)?; + // Convert weight_fn to a closure that takes a subgraph edge and returns the weight of the original graph edge + for (subgraph, node_subnode_map, edge_subedge_map) in subgraphs_with_maps { + // Find the key of edge_subedge_map that corresponds to the value e.id() + let mut subgraph_weight_fn = |e: <&Graph as IntoEdgeReferences>::EdgeRef| -> Result { + // use the edge_subedge_map to find the key that corresponds to the value e.id() + let edge = edge_subedge_map.iter().find(|(_key, &value)| value == e.id()).unwrap().0; + match weight_fn( + graph.edge_references().nth(*edge).unwrap(), + ) { + Ok(weight) => Ok(weight), + Err(_) => { + // Handle the error here. Since the error type is Infallible, this branch should never be reached. + unreachable!() + } + } + }; + let num_cycles: Result>, Infallible> = + _min_cycle_basis(&subgraph, &mut subgraph_weight_fn, &node_subnode_map); + min_cycle_basis.extend(num_cycles.unwrap()); + } + Ok(min_cycle_basis) +} + +#[cfg(test)] +mod test_minimal_cycle_basis { + use crate::connectivity::minimal_cycle_basis::minimal_cycle_basis; + use petgraph::graph::{Graph, NodeIndex}; + use petgraph::Undirected; + use std::convert::Infallible; + + #[test] + fn test_empty_graph() { + let graph = Graph::::new_undirected(); + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let output = minimal_cycle_basis(&graph, weight_fn).unwrap(); + assert_eq!(output.len(), 0); + } + + #[test] + fn test_triangle() { + let mut graph = Graph::<_, _, Undirected>::new_undirected(); + let a = graph.add_node("A".to_string()); + let b = graph.add_node("B".to_string()); + let c = graph.add_node("C".to_string()); + graph.add_edge(a, b, 1); + graph.add_edge(b, c, 1); + graph.add_edge(c, a, 1); + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let cycles = minimal_cycle_basis(&graph, weight_fn); + assert_eq!(cycles.unwrap().len(), 1); + } + + #[test] + fn test_two_separate_triangles() { + let mut graph = Graph::::new(); + let nodes = vec!["A", "B", "C", "D", "E", "F"] + .iter() + .map(|&n| graph.add_node(n.to_string())) + .collect::>(); + graph.add_edge(nodes[0], nodes[1], 1); + graph.add_edge(nodes[1], nodes[2], 1); + graph.add_edge(nodes[2], nodes[0], 1); + graph.add_edge(nodes[3], nodes[4], 1); + graph.add_edge(nodes[4], nodes[5], 1); + graph.add_edge(nodes[5], nodes[3], 1); + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let cycles = minimal_cycle_basis(&graph, weight_fn); + assert_eq!(cycles.unwrap().len(), 2); + } + + #[test] + fn test_non_trivial_graph() { + let mut g = Graph::<&str, i32, Undirected>::new_undirected(); + let a = g.add_node("A"); + let b = g.add_node("B"); + let c = g.add_node("C"); + let d = g.add_node("D"); + let e = g.add_node("E"); + let f = g.add_node("F"); + + g.add_edge(a, b, 7); + g.add_edge(c, a, 9); + g.add_edge(a, d, 11); + g.add_edge(b, c, 10); + g.add_edge(d, c, 2); + g.add_edge(d, e, 9); + g.add_edge(b, f, 15); + g.add_edge(c, f, 11); + g.add_edge(e, f, 6); + + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let output = minimal_cycle_basis(&g, weight_fn); + let mut actual_output = output.unwrap(); + for cycle in &mut actual_output { + cycle.sort(); + } + actual_output.sort(); + + let expected_output: Vec> = vec![ + vec![ + NodeIndex::new(5), + NodeIndex::new(2), + NodeIndex::new(3), + NodeIndex::new(4), + ], + vec![NodeIndex::new(2), NodeIndex::new(5), NodeIndex::new(1)], + vec![NodeIndex::new(0), NodeIndex::new(2), NodeIndex::new(1)], + vec![NodeIndex::new(2), NodeIndex::new(3), NodeIndex::new(0)], + ]; + let mut sorted_expected_output = expected_output.clone(); + for cycle in &mut sorted_expected_output { + cycle.sort(); + } + sorted_expected_output.sort(); + + assert_eq!(actual_output, sorted_expected_output); + } + + #[test] + fn test_weighted_diamond_graph() { + let mut weighted_diamond = Graph::<(), i32, Undirected>::new_undirected(); + let ud_node1 = weighted_diamond.add_node(()); + let ud_node2 = weighted_diamond.add_node(()); + let ud_node3 = weighted_diamond.add_node(()); + let ud_node4 = weighted_diamond.add_node(()); + weighted_diamond.add_edge(ud_node1, ud_node2, 1); + weighted_diamond.add_edge(ud_node2, ud_node3, 1); + weighted_diamond.add_edge(ud_node3, ud_node4, 1); + weighted_diamond.add_edge(ud_node4, ud_node1, 1); + weighted_diamond.add_edge(ud_node2, ud_node4, 5); + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let output = minimal_cycle_basis(&weighted_diamond, weight_fn); + let expected_output1: Vec> = vec![vec![0, 1, 3], vec![0, 1, 2, 3]]; + let expected_output2: Vec> = vec![vec![1, 2, 3], vec![0, 1, 2, 3]]; + for cycle in output.unwrap().iter() { + let mut node_indices: Vec = Vec::new(); + for node in cycle.iter() { + node_indices.push(node.index()); + } + node_indices.sort(); + assert!( + expected_output1.contains(&node_indices) + || expected_output2.contains(&node_indices) + ); + } + } + + #[test] + fn test_unweighted_diamond_graph() { + let mut unweighted_diamond = Graph::<(), (), Undirected>::new_undirected(); + let ud_node0 = unweighted_diamond.add_node(()); + let ud_node1 = unweighted_diamond.add_node(()); + let ud_node2 = unweighted_diamond.add_node(()); + let ud_node3 = unweighted_diamond.add_node(()); + unweighted_diamond.add_edge(ud_node0, ud_node1, ()); + unweighted_diamond.add_edge(ud_node1, ud_node2, ()); + unweighted_diamond.add_edge(ud_node2, ud_node3, ()); + unweighted_diamond.add_edge(ud_node3, ud_node0, ()); + unweighted_diamond.add_edge(ud_node1, ud_node3, ()); + let weight_fn = + |_edge: petgraph::graph::EdgeReference<()>| -> Result { Ok(1) }; + + let output = minimal_cycle_basis(&unweighted_diamond, weight_fn); + let expected_output: Vec> = vec![vec![0, 1, 3], vec![1, 2, 3]]; + for cycle in output.unwrap().iter() { + let mut node_indices: Vec = Vec::new(); + for node in cycle.iter() { + node_indices.push(node.index()); + } + node_indices.sort(); + assert!(expected_output.contains(&node_indices)); + } + } + #[test] + fn test_complete_graph() { + let mut complete_graph = Graph::<(), i32, Undirected>::new_undirected(); + let cg_node1 = complete_graph.add_node(()); + let cg_node2 = complete_graph.add_node(()); + let cg_node3 = complete_graph.add_node(()); + let cg_node4 = complete_graph.add_node(()); + let cg_node5 = complete_graph.add_node(()); + complete_graph.add_edge(cg_node1, cg_node2, 1); + complete_graph.add_edge(cg_node1, cg_node3, 1); + complete_graph.add_edge(cg_node1, cg_node4, 1); + complete_graph.add_edge(cg_node1, cg_node5, 1); + complete_graph.add_edge(cg_node2, cg_node3, 1); + complete_graph.add_edge(cg_node2, cg_node4, 1); + complete_graph.add_edge(cg_node2, cg_node5, 1); + complete_graph.add_edge(cg_node3, cg_node4, 1); + complete_graph.add_edge(cg_node3, cg_node5, 1); + complete_graph.add_edge(cg_node4, cg_node5, 1); + let weight_fn = |edge: petgraph::graph::EdgeReference| -> Result { + Ok(*edge.weight()) + }; + let output = minimal_cycle_basis(&complete_graph, weight_fn); + for cycle in output.unwrap().iter() { + assert_eq!(cycle.len(), 3); + } + } +} diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index fa236d8b6..3df41b25a 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -21,6 +21,7 @@ mod cycle_basis; mod find_cycle; mod isolates; mod min_cut; +mod minimal_cycle_basis; pub use all_simple_paths::{ all_simple_paths_multiple_targets, longest_simple_path_multiple_targets, @@ -36,3 +37,4 @@ pub use cycle_basis::cycle_basis; pub use find_cycle::find_cycle; pub use isolates::isolates; pub use min_cut::stoer_wagner_min_cut; +pub use minimal_cycle_basis::minimal_cycle_basis;