diff --git a/docs/source/api/algorithm_functions/coloring.rst b/docs/source/api/algorithm_functions/coloring.rst new file mode 100644 index 000000000..873b4e79e --- /dev/null +++ b/docs/source/api/algorithm_functions/coloring.rst @@ -0,0 +1,11 @@ +.. _coloring: + +Coloring +======== + +.. autosummary:: + :toctree: ../../apiref + + rustworkx.graph_greedy_color + rustworkx.graph_greedy_edge_color + rustworkx.graph_misra_gries_edge_color diff --git a/docs/source/api/algorithm_functions/index.rst b/docs/source/api/algorithm_functions/index.rst index 849f4cb85..1241cae34 100644 --- a/docs/source/api/algorithm_functions/index.rst +++ b/docs/source/api/algorithm_functions/index.rst @@ -7,6 +7,7 @@ Algorithm Functions :maxdepth: 2 centrality + coloring connectivity_and_cycles dag_algorithms graph_operations diff --git a/docs/source/api/algorithm_functions/other.rst b/docs/source/api/algorithm_functions/other.rst index 26f191a9a..3a7af3bc6 100644 --- a/docs/source/api/algorithm_functions/other.rst +++ b/docs/source/api/algorithm_functions/other.rst @@ -9,8 +9,6 @@ Other Algorithm Functions 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 diff --git a/releasenotes/notes/add-graph-misra-gries-edge-color-e55a27de2b667db1.yaml b/releasenotes/notes/add-graph-misra-gries-edge-color-e55a27de2b667db1.yaml new file mode 100644 index 000000000..457c829f9 --- /dev/null +++ b/releasenotes/notes/add-graph-misra-gries-edge-color-e55a27de2b667db1.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added a new function, :func:`~.graph_misra_gries_edge_color` to color edges + of a :class:`~.PyGraph` object using the Misra-Gries edge coloring algorithm. + + The above algorithm is described in the paper paper: "A constructive proof of + Vizing's theorem" by Misra and Gries, 1992. + + The coloring produces at most d + 1 colors where d is the maximum degree + of the graph. + + .. jupyter-execute:: + + import rustworkx as rx + + graph = rx.generators.cycle_graph(7) + edge_colors = rx.graph_misra_gries_edge_color(graph) + assert edge_colors == {0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 0, 6: 2} + diff --git a/rustworkx-core/src/coloring.rs b/rustworkx-core/src/coloring.rs index 60fda14da..8f67d9efa 100644 --- a/rustworkx-core/src/coloring.rs +++ b/rustworkx-core/src/coloring.rs @@ -15,10 +15,11 @@ use std::hash::Hash; use crate::dictmap::*; use crate::line_graph::line_graph; + use hashbrown::{HashMap, HashSet}; use petgraph::graph::NodeIndex; use petgraph::visit::{ - EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdges, IntoNeighborsDirected, + EdgeCount, EdgeIndexable, EdgeRef, GraphBase, GraphProp, IntoEdges, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, NodeIndexable, }; use petgraph::{Incoming, Outgoing}; @@ -220,8 +221,269 @@ where edge_colors } -#[cfg(test)] +struct MisraGries { + // The input graph + graph: G, + // Maximum node degree in the graph + max_node_degree: usize, + // Partially assigned colors (indexed by internal edge index) + colors: Vec>, + // Performance optimization: explicitly storing edge colors used at each node + node_used_colors: Vec>, +} + +impl MisraGries +where + G: EdgeIndexable + IntoEdges + NodeIndexable + IntoNodeIdentifiers, +{ + pub fn new(graph: G) -> Self { + let colors = vec![None; graph.edge_bound()]; + let max_node_degree = graph + .node_identifiers() + .map(|node| graph.edges(node).count()) + .max() + .unwrap_or(0); + let empty_set = HashSet::new(); + let node_used_colors = vec![empty_set; graph.node_bound()]; + + MisraGries { + graph, + max_node_degree, + colors, + node_used_colors, + } + } + + // Updates edge colors for a set of edges while keeping track of + // explicitly stored used node colors + fn update_edge_colors(&mut self, new_colors: &Vec<(G::EdgeRef, usize)>) { + // First, remove node colors that are going to be unassigned + for (e, _) in new_colors { + if let Some(old_color) = self.get_edge_color(*e) { + self.remove_node_used_color(e.source(), old_color); + self.remove_node_used_color(e.target(), old_color); + } + } + // Next, add node colors that are going to be assigned + for (e, c) in new_colors { + self.add_node_used_color(e.source(), *c); + self.add_node_used_color(e.target(), *c); + } + for (e, c) in new_colors { + self.colors[EdgeIndexable::to_index(&self.graph, e.id())] = Some(*c); + } + } + + // Updates used node colors at u adding c + fn add_node_used_color(&mut self, u: G::NodeId, c: usize) { + let uindex = NodeIndexable::to_index(&self.graph, u); + self.node_used_colors[uindex].insert(c); + } + + // Updates used node colors at u removing c + fn remove_node_used_color(&mut self, u: G::NodeId, c: usize) { + let uindex = NodeIndexable::to_index(&self.graph, u); + self.node_used_colors[uindex].remove(&c); + } + + // Gets edge color + fn get_edge_color(&self, e: G::EdgeRef) -> Option { + self.colors[EdgeIndexable::to_index(&self.graph, e.id())] + } + + // Returns colors used at node u + fn get_used_colors(&self, u: G::NodeId) -> &HashSet { + let uindex = NodeIndexable::to_index(&self.graph, u); + &self.node_used_colors[uindex] + } + + // Returns the smallest free (aka unused) color at node u + fn get_free_color(&self, u: G::NodeId) -> usize { + let used_colors = self.get_used_colors(u); + let free_color: usize = (0..self.max_node_degree + 1) + .position(|color| !used_colors.contains(&color)) + .unwrap(); + free_color + } + + // Returns true iff color c is free at node u + fn is_free_color(&self, u: G::NodeId, c: usize) -> bool { + !self.get_used_colors(u).contains(&c) + } + + // Returns the maximal fan on edge (u, v) at u + fn get_maximal_fan(&self, e: G::EdgeRef, u: G::NodeId, v: G::NodeId) -> Vec { + let mut fan: Vec = vec![e]; + + let mut neighbors: Vec = self.graph.edges(u).collect(); + + let mut last_node_in_fan = v; + neighbors.remove( + neighbors + .iter() + .position(|x| x.target() == last_node_in_fan) + .unwrap(), + ); + + let mut fan_extended: bool = true; + while fan_extended { + fan_extended = false; + + for edge in &neighbors { + if let Some(color) = self.get_edge_color(*edge) { + if self.is_free_color(last_node_in_fan, color) { + fan.push(*edge); + last_node_in_fan = edge.target(); + fan_extended = true; + neighbors.remove( + neighbors + .iter() + .position(|x| x.target() == last_node_in_fan) + .unwrap(), + ); + break; + } + } + } + } + + fan + } + + // Assuming that color is either c or d, returns the other color + fn flip_color(&self, c: usize, d: usize, e: usize) -> usize { + if e == c { + d + } else { + c + } + } + + // Returns the longest path starting at node u with alternating colors c, d, c, d, c, etc. + fn get_cdu_path(&self, u: G::NodeId, c: usize, d: usize) -> Vec<(G::EdgeRef, usize)> { + let mut path: Vec<(G::EdgeRef, usize)> = Vec::new(); + let mut cur_node = u; + let mut cur_color = c; + let mut path_extended = true; + + while path_extended { + path_extended = false; + for edge in self.graph.edges(cur_node) { + if let Some(color) = self.get_edge_color(edge) { + if color == cur_color { + path_extended = true; + path.push((edge, cur_color)); + cur_node = edge.target(); + cur_color = self.flip_color(c, d, cur_color); + break; + } + } + } + } + path + } + + // Main function + pub fn run_algorithm(&mut self) -> &Vec> { + for edge in self.graph.edge_references() { + let u: G::NodeId = edge.source(); + let v: G::NodeId = edge.target(); + let fan = self.get_maximal_fan(edge, u, v); + let c = self.get_free_color(u); + let d = self.get_free_color(fan.last().unwrap().target()); + + // find cdu-path + let cdu_path = self.get_cdu_path(u, d, c); + + // invert colors on cdu-path + let mut new_cdu_path_colors: Vec<(G::EdgeRef, usize)> = + Vec::with_capacity(cdu_path.len()); + for (cdu_edge, color) in cdu_path { + let flipped_color = self.flip_color(c, d, color); + new_cdu_path_colors.push((cdu_edge, flipped_color)); + } + self.update_edge_colors(&new_cdu_path_colors); + + // find sub-fan fan[0..w] such that d is free on fan[w] + let mut w = 0; + for (i, x) in fan.iter().enumerate() { + if self.is_free_color(x.target(), d) { + w = i; + break; + } + } + + // rotate fan + fill additional color + let mut new_fan_colors: Vec<(G::EdgeRef, usize)> = Vec::with_capacity(w + 1); + for i in 1..w + 1 { + let next_color = self.get_edge_color(fan[i]).unwrap(); + new_fan_colors.push((fan[i - 1], next_color)); + } + new_fan_colors.push((fan[w], d)); + self.update_edge_colors(&new_fan_colors); + } + + &self.colors + } +} + +/// Color edges of a graph using the Misra-Gries edge coloring algorithm. +/// +/// Based on the paper: "A constructive proof of Vizing's theorem" by +/// Misra and Gries, 1992. +/// +/// +/// The coloring produces at most d + 1 colors where d is the maximum degree +/// of the graph. +/// +/// The coloring problem is NP-hard and this is a heuristic algorithm +/// which may not return an optimal solution. +/// +/// Arguments: +/// +/// * `graph` - The graph object to run the algorithm on +/// +/// # Example +/// ```rust +/// +/// use petgraph::graph::Graph; +/// use petgraph::graph::EdgeIndex; +/// use petgraph::Undirected; +/// use rustworkx_core::dictmap::*; +/// use rustworkx_core::coloring::misra_gries_edge_color;/// +/// let g = Graph::<(), (), Undirected>::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)]); +/// let colors = misra_gries_edge_color(&g); +/// +/// let expected_colors: DictMap = [ +/// (EdgeIndex::new(0), 2), +/// (EdgeIndex::new(1), 1), +/// (EdgeIndex::new(2), 0), +/// (EdgeIndex::new(3), 2), +/// ] +/// .into_iter() +/// .collect(); +/// +/// assert_eq!(colors, expected_colors); +/// ``` +/// +pub fn misra_gries_edge_color(graph: G) -> DictMap +where + G: EdgeIndexable + IntoEdges + EdgeCount + NodeIndexable + IntoNodeIdentifiers, + G::EdgeId: Eq + Hash, +{ + let mut mg: MisraGries = MisraGries::new(graph); + let colors = mg.run_algorithm(); + + let mut edge_colors: DictMap = DictMap::with_capacity(graph.edge_count()); + for edge in graph.edge_references() { + let edge_index = edge.id(); + let color = colors[EdgeIndexable::to_index(&graph, edge_index)].unwrap(); + edge_colors.insert(edge_index, color); + } + edge_colors +} +#[cfg(test)] mod test_node_coloring { use crate::coloring::greedy_node_color; @@ -399,3 +661,119 @@ mod test_edge_coloring { assert_eq!(colors, expected_colors); } } + +#[cfg(test)] +mod test_misra_gries_edge_coloring { + use crate::coloring::misra_gries_edge_color; + use crate::dictmap::DictMap; + use crate::generators::{complete_graph, cycle_graph, heavy_hex_graph, path_graph}; + use crate::petgraph::Graph; + + use hashbrown::HashSet; + use petgraph::graph::EdgeIndex; + use petgraph::visit::{EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeIndexable}; + use petgraph::Undirected; + use std::fmt::Debug; + use std::hash::Hash; + + fn check_edge_coloring(graph: G, colors: &DictMap) + where + G: NodeIndexable + IntoEdges + IntoNodeIdentifiers, + G::EdgeId: Eq + Hash + Debug, + { + // Check that every edge has valid color + for edge in graph.edge_references() { + if !colors.contains_key(&edge.id()) { + panic!("Problem: edge {:?} has no color assigned.", &edge.id()); + } + } + + // Check that for every node all of its edges have different colors + // (i.e. the number of used colors is equal to the degree). + // Also compute maximum color used and maximum node degree. + let mut max_color = 0; + let mut max_node_degree = 0; + let node_indices: Vec = graph.node_identifiers().collect(); + for node in node_indices { + let mut cur_node_degree = 0; + let mut used_colors: HashSet = HashSet::new(); + for edge in graph.edges(node) { + let color = colors.get(&edge.id()).unwrap(); + used_colors.insert(*color); + cur_node_degree += 1; + if max_color < *color { + max_color = *color; + } + } + if used_colors.len() < cur_node_degree { + panic!( + "Problem: node {:?} does not have enough colors.", + NodeIndexable::to_index(&graph, node) + ); + } + + if cur_node_degree > max_node_degree { + max_node_degree = cur_node_degree + } + } + + // Check that number of colors used is at most max_node_degree + 1 + // (note that number of colors is max_color + 1). + if max_color > max_node_degree { + panic!( + "Problem: too many colors are used ({} colors used, {} max node degree)", + max_color + 1, + max_node_degree + ); + } + } + + #[test] + fn test_simple_graph() { + let graph = Graph::<(), (), Undirected>::from_edges(&[(0, 1), (0, 2), (0, 3), (3, 4)]); + let colors = misra_gries_edge_color(&graph); + check_edge_coloring(&graph, &colors); + + let expected_colors: DictMap = [ + (EdgeIndex::new(0), 0), + (EdgeIndex::new(1), 2), + (EdgeIndex::new(2), 1), + (EdgeIndex::new(3), 3), + ] + .into_iter() + .collect(); + assert_eq!(colors, expected_colors); + } + + #[test] + fn test_path_graph() { + let graph: petgraph::graph::UnGraph<(), ()> = + path_graph(Some(7), None, || (), || (), false).unwrap(); + let colors = misra_gries_edge_color(&graph); + check_edge_coloring(&graph, &colors); + } + + #[test] + fn test_cycle_graph() { + let graph: petgraph::graph::UnGraph<(), ()> = + cycle_graph(Some(15), None, || (), || (), false).unwrap(); + let colors = misra_gries_edge_color(&graph); + check_edge_coloring(&graph, &colors); + } + + #[test] + fn test_heavy_hex_graph() { + let graph: petgraph::graph::UnGraph<(), ()> = + heavy_hex_graph(7, || (), || (), false).unwrap(); + let colors = misra_gries_edge_color(&graph); + check_edge_coloring(&graph, &colors); + } + + #[test] + fn test_complete_graph() { + let graph: petgraph::graph::UnGraph<(), ()> = + complete_graph(Some(10), None, || (), || ()).unwrap(); + let colors = misra_gries_edge_color(&graph); + check_edge_coloring(&graph, &colors); + } +} diff --git a/src/coloring.rs b/src/coloring.rs index 83eca1c49..bbd34e350 100644 --- a/src/coloring.rs +++ b/src/coloring.rs @@ -12,11 +12,12 @@ use crate::{digraph, graph}; -use rustworkx_core::coloring::{greedy_edge_color, greedy_node_color, two_color}; - use pyo3::prelude::*; use pyo3::types::PyDict; use pyo3::Python; +use rustworkx_core::coloring::{ + greedy_edge_color, greedy_node_color, misra_gries_edge_color, two_color, +}; /// Color a :class:`~.PyGraph` object using a greedy graph coloring algorithm. /// @@ -72,11 +73,11 @@ pub fn graph_greedy_color(py: Python, graph: &graph::PyGraph) -> PyResult PyResult

+/// +/// The coloring produces at most d + 1 colors where d is the maximum degree +/// of the graph. +/// +/// :param PyGraph: The input PyGraph object to edge-color +/// +/// :returns: A dictionary where keys are edge indices and the value is the color +/// :rtype: dict +/// +/// .. jupyter-execute:: +/// +/// import rustworkx as rx +/// +/// graph = rx.generators.cycle_graph(7) +/// edge_colors = rx.graph_misra_gries_edge_color(graph) +/// assert edge_colors == {0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 0, 6: 2} +/// +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn graph_misra_gries_edge_color(py: Python, graph: &graph::PyGraph) -> PyResult { + let colors = misra_gries_edge_color(&graph.graph); + let out_dict = PyDict::new(py); + for (node, color) in colors { + out_dict.set_item(node.index(), color)?; + } + Ok(out_dict.into()) +} + /// Compute a two-coloring of a graph /// /// If a two coloring is not possible for the input graph (meaning it is not diff --git a/src/lib.rs b/src/lib.rs index 13d531317..5720e697c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -449,6 +449,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(graph_astar_shortest_path))?; m.add_wrapped(wrap_pyfunction!(digraph_astar_shortest_path))?; m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?; + m.add_wrapped(wrap_pyfunction!(graph_misra_gries_edge_color))?; m.add_wrapped(wrap_pyfunction!(graph_greedy_edge_color))?; m.add_wrapped(wrap_pyfunction!(graph_two_color))?; m.add_wrapped(wrap_pyfunction!(digraph_two_color))?; diff --git a/tests/rustworkx_tests/graph/test_coloring.py b/tests/rustworkx_tests/graph/test_coloring.py index 675729662..04b41a074 100644 --- a/tests/rustworkx_tests/graph/test_coloring.py +++ b/tests/rustworkx_tests/graph/test_coloring.py @@ -106,3 +106,82 @@ def test_cycle_graph(self): graph = rustworkx.generators.cycle_graph(7) edge_colors = rustworkx.graph_greedy_edge_color(graph) self.assertEqual({0: 0, 1: 1, 2: 0, 3: 1, 4: 0, 5: 1, 6: 2}, edge_colors) + + +class TestMisraGriesColoring(unittest.TestCase): + def test_simple_graph(self): + graph = rustworkx.PyGraph() + node0 = graph.add_node(0) + node1 = graph.add_node(1) + node2 = graph.add_node(2) + node3 = graph.add_node(3) + + graph.add_edge(node0, node1, 1) + graph.add_edge(node0, node2, 1) + graph.add_edge(node1, node2, 1) + graph.add_edge(node2, node3, 1) + + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + self.assertEqual({0: 2, 1: 1, 2: 0, 3: 2}, edge_colors) + + def test_graph_with_holes(self): + """Graph with missing node and edge indices.""" + graph = rustworkx.PyGraph() + node_a = graph.add_node("a") + node_b = graph.add_node("b") + node_c = graph.add_node("c") + node_d = graph.add_node("d") + node_e = graph.add_node("e") + graph.add_edge(node_a, node_b, 1) + graph.add_edge(node_b, node_c, 1) + graph.add_edge(node_c, node_d, 1) + graph.add_edge(node_d, node_e, 1) + graph.remove_node(node_c) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + self.assertEqual({0: 0, 3: 0}, edge_colors) + + def test_graph_without_edges(self): + graph = rustworkx.PyGraph() + graph.add_node("a") + graph.add_node("b") + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + self.assertEqual({}, edge_colors) + + def test_graph_multiple_edges(self): + """Graph with multiple edges between two nodes.""" + graph = rustworkx.PyGraph() + node_a = graph.add_node("a") + node_b = graph.add_node("b") + graph.add_edge(node_a, node_b, 1) + graph.add_edge(node_a, node_b, 1) + graph.add_edge(node_a, node_b, 1) + graph.add_edge(node_a, node_b, 1) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + self.assertEqual({0: 0, 1: 1, 2: 2, 3: 3}, edge_colors) + + def test_cycle_graph(self): + """Test on a small cycle graph with an odd number of virtices.""" + graph = rustworkx.generators.cycle_graph(7) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + assert edge_colors == {0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 0, 6: 2} + + def test_grid(self): + """Test that Misra-Gries colors the grid with at most 5 colors (max degree + 1).""" + graph = rustworkx.generators.grid_graph(10, 10) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + num_colors = max(edge_colors.values()) + 1 + self.assertLessEqual(num_colors, 5) + + def test_heavy_hex(self): + """Test that Misra-Gries colors the heavy hex with at most 4 colors (max degree + 1).""" + graph = rustworkx.generators.heavy_hex_graph(9) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + num_colors = max(edge_colors.values()) + 1 + self.assertLessEqual(num_colors, 4) + + def test_complete_graph(self): + """Test that Misra-Gries colors the complete graph with at most n+1 colors.""" + graph = rustworkx.generators.complete_graph(10) + edge_colors = rustworkx.graph_misra_gries_edge_color(graph) + num_colors = max(edge_colors.values()) + 1 + self.assertLessEqual(num_colors, 11)