Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding line_graph and greedy_edge_color for undirected graphs #870

Merged
merged 29 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
49d8594
trying to implement line_graph and edge_coloring
alexanderivrii May 12, 2023
cbcc239
minor refactoring
alexanderivrii May 13, 2023
6c44a7a
more refactoring
alexanderivrii May 13, 2023
93bad32
moving greedy_color to core
alexanderivrii May 16, 2023
38422e1
running fmt
alexanderivrii May 16, 2023
d3b4238
running clippy
alexanderivrii May 16, 2023
3cacbe3
adapting tests
alexanderivrii May 16, 2023
7921827
merge with main
alexanderivrii Jun 8, 2023
3980a14
fixes
alexanderivrii Jun 8, 2023
07c5ba7
first pass on implementing line_graph in rustworkx-core
alexanderivrii Jun 8, 2023
3c4513e
cleanup
alexanderivrii Jun 8, 2023
d475d08
docs
alexanderivrii Jun 8, 2023
e78d069
docs fix
alexanderivrii Jun 8, 2023
79b7b4d
exposing line_graph to rustworkx python interface
alexanderivrii Jun 9, 2023
7dec989
moving graph_line_graph to a separate package
alexanderivrii Jun 9, 2023
99f0224
minor cleanup
alexanderivrii Jun 9, 2023
97e32ca
more cleanup
alexanderivrii Jun 9, 2023
370407c
more cleanup
alexanderivrii Jun 9, 2023
02e3691
tests for graph_greedy_edge_color
alexanderivrii Jun 9, 2023
ecdbe16
additional test + docs
alexanderivrii Jun 9, 2023
715f270
line graph tests
alexanderivrii Jun 9, 2023
1456573
docs
alexanderivrii Jun 9, 2023
46d1350
release notes
alexanderivrii Jun 9, 2023
bbacc33
minor
alexanderivrii Jun 10, 2023
c324acb
implementing greedy_edge_color in rustworkx-core
alexanderivrii Jun 25, 2023
2e9fcf5
changing test to be different from the one in docs
alexanderivrii Jun 25, 2023
d0c3ee3
using greedy_edge_color from core
alexanderivrii Jun 25, 2023
71f2de6
Merge branch 'main' into edge-coloring
alexanderivrii Jun 25, 2023
5e3af3f
Merge branch 'main' into edge-coloring
mergify[bot] Jul 6, 2023
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
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ Other Algorithm Functions
rustworkx.transitivity
rustworkx.core_number
rustworkx.graph_greedy_color
rustworkx.graph_greedy_edge_color
rustworkx.graph_line_graph
rustworkx.metric_closure
rustworkx.is_planar

Expand Down
1 change: 1 addition & 0 deletions docs/source/sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ rustworkx.graph_distance_matrix.html
rustworkx.graph_floyd_warshall.html
rustworkx.graph_floyd_warshall_numpy.html
rustworkx.graph_greedy_color.html
rustworkx.graph_greedy_edge_color.html
rustworkx.graph_is_isomorphic.html
rustworkx.graph_is_subgraph_isomorphic.html
rustworkx.graph_k_shortest_path_lengths.html
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
features:
- |
Added a new function, :func:`~.graph_line_graph` to construct a line
graph of a :class:`~.PyGraph` object.

The line graph `L(G)` of a graph `G` represents the adjacencies between edges of G.
`L(G)` contains a vertex for every edge in `G`, and `L(G)` contains an edge between two
vertices if the corresponding edges in `G` have a vertex in common.

.. jupyter-execute::

import rustworkx as rx

graph = rx.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")
edge_ab = graph.add_edge(node_a, node_b, 1)
edge_ac = graph.add_edge(node_a, node_c, 1)
edge_bc = graph.add_edge(node_b, node_c, 1)
edge_ad = graph.add_edge(node_a, node_d, 1)

out_graph, out_edge_map = rx.graph_line_graph(graph)
assert out_graph.node_indices() == [0, 1, 2, 3]
assert out_graph.edge_list() == [(3, 1), (3, 0), (1, 0), (2, 0), (2, 1)]
assert out_edge_map == {edge_ab: 0, edge_ac: 1, edge_bc: 2, edge_ad: 3}
Comment on lines +26 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More a note for myself when we release 0.14.0 it'll be good to have this visualized by calling mpl_draw() in the release notes instead of asserts. But I'll just do this as part of the release prep.


- |
Added a new function, :func:`~.graph_greedy_edge_color` to color edges
of a :class:`~.PyGraph` object using a greedy approach.

This function works by greedily coloring the line graph of the given graph.

.. jupyter-execute::

import rustworkx as rx

graph = rx.generators.cycle_graph(7)
edge_colors = rx.graph_greedy_edge_color(graph)
assert edge_colors == {0: 0, 1: 1, 2: 0, 3: 1, 4: 0, 5: 1, 6: 2}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More a note for myself when we release 0.14.0 it'll be good to have this visualized by calling mpl_draw() in the release notes instead of asserts. But I'll just do this as part of the release prep.

111 changes: 108 additions & 3 deletions rustworkx-core/src/coloring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ use std::cmp::Reverse;
use std::hash::Hash;

use crate::dictmap::*;
use crate::line_graph::line_graph;
use hashbrown::{HashMap, HashSet};
use petgraph::visit::{EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeCount};
use petgraph::graph::NodeIndex;
use petgraph::visit::{EdgeCount, EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeCount};
use rayon::prelude::*;

/// Color a graph using a greedy graph coloring algorithm.
Expand Down Expand Up @@ -52,13 +54,12 @@ use rayon::prelude::*;
/// assert_eq!(colors, expected_colors);
/// ```
///
///
pub fn greedy_node_color<G>(graph: G) -> DictMap<G::NodeId, usize>
where
G: NodeCount + IntoNodeIdentifiers + IntoEdges,
G::NodeId: Hash + Eq + Send + Sync,
{
let mut colors: DictMap<G::NodeId, usize> = DictMap::new();
let mut colors: DictMap<G::NodeId, usize> = DictMap::with_capacity(graph.node_count());
let mut node_vec: Vec<G::NodeId> = graph.node_identifiers().collect();

let mut sort_map: HashMap<G::NodeId, usize> = HashMap::with_capacity(graph.node_count());
Expand Down Expand Up @@ -90,6 +91,60 @@ where
colors
}

/// Color edges of a graph using a greedy approach.
///
/// This function works by greedily coloring the line graph of the given 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::greedy_edge_color;
///
/// let g = Graph::<(), (), Undirected>::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)]);
/// let colors = greedy_edge_color(&g);
/// let mut expected_colors = DictMap::new();
/// expected_colors.insert(EdgeIndex::new(0), 2);
/// expected_colors.insert(EdgeIndex::new(1), 0);
/// expected_colors.insert(EdgeIndex::new(2), 1);
/// expected_colors.insert(EdgeIndex::new(3), 2);
/// assert_eq!(colors, expected_colors);
/// ```
///
pub fn greedy_edge_color<G>(graph: G) -> DictMap<G::EdgeId, usize>
where
G: EdgeCount + IntoNodeIdentifiers + IntoEdges,
G::EdgeId: Hash + Eq,
{
let (new_graph, edge_to_node_map): (
petgraph::graph::UnGraph<(), ()>,
HashMap<G::EdgeId, NodeIndex>,
) = line_graph(&graph, || (), || ());

let colors = greedy_node_color(&new_graph);

let mut edge_colors: DictMap<G::EdgeId, usize> = DictMap::with_capacity(graph.edge_count());

for edge in graph.edge_references() {
let edge_index = edge.id();
let node_index = edge_to_node_map.get(&edge_index).unwrap();
let edge_color = colors.get(node_index).unwrap();
edge_colors.insert(edge_index, *edge_color);
}

edge_colors
Comment on lines +136 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters from a performance perspective, but I think this could be rewritten as:

Suggested change
let mut edge_colors: DictMap<G::EdgeId, usize> = DictMap::with_capacity(graph.edge_count());
for edge in graph.edge_references() {
let edge_index = edge.id();
let node_index = edge_to_node_map.get(&edge_index).unwrap();
let edge_color = colors.get(node_index).unwrap();
edge_colors.insert(edge_index, *edge_color);
}
edge_colors
graph
.edge_references()
.map(|edge| {
let edge_index = edge.id();
let node_index = edge_to_node_map.get(&edge_index).unwrap();
let edge_color = colors.get(node_index).unwrap();
(edge_index, *edge_color)
})
.collect()

}

#[cfg(test)]

mod test_node_coloring {
Expand Down Expand Up @@ -147,3 +202,53 @@ mod test_node_coloring {
assert_eq!(colors, expected_colors);
}
}

#[cfg(test)]
mod test_edge_coloring {
use crate::coloring::greedy_edge_color;
use crate::dictmap::DictMap;
use crate::petgraph::Graph;

use petgraph::graph::{edge_index, EdgeIndex};
use petgraph::Undirected;

#[test]
fn test_greedy_edge_color_empty_graph() {
// Empty graph
let graph = Graph::<(), (), Undirected>::new_undirected();
let colors = greedy_edge_color(&graph);
let expected_colors: DictMap<EdgeIndex, usize> = [].into_iter().collect();
assert_eq!(colors, expected_colors);
}

#[test]
fn test_greedy_edge_color_simple_graph() {
// Graph with an edge removed
let graph = Graph::<(), (), Undirected>::from_edges(&[(0, 1), (1, 2), (2, 3)]);
let colors = greedy_edge_color(&graph);
let expected_colors: DictMap<EdgeIndex, usize> = [
(EdgeIndex::new(0), 1),
(EdgeIndex::new(1), 0),
(EdgeIndex::new(2), 1),
]
.into_iter()
.collect();
assert_eq!(colors, expected_colors);
}

#[test]
fn test_greedy_edge_color_graph_with_removed_edges() {
// Simple graph
let mut graph = Graph::<(), (), Undirected>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)]);
graph.remove_edge(edge_index(1));
let colors = greedy_edge_color(&graph);
let expected_colors: DictMap<EdgeIndex, usize> = [
(EdgeIndex::new(0), 1),
(EdgeIndex::new(1), 0),
(EdgeIndex::new(2), 1),
]
.into_iter()
.collect();
assert_eq!(colors, expected_colors);
}
}
1 change: 1 addition & 0 deletions rustworkx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub mod centrality;
pub mod coloring;
pub mod connectivity;
pub mod generators;
pub mod line_graph;
/// Module for maximum weight matching algorithms.
pub mod max_weight_matching;
pub mod planar;
Expand Down
145 changes: 145 additions & 0 deletions rustworkx-core/src/line_graph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

use std::hash::Hash;

use hashbrown::HashMap;
use petgraph::data::Create;
use petgraph::visit::{Data, EdgeCount, EdgeRef, IntoEdges, IntoNodeIdentifiers};

/// Constructs the line graph of an undirected graph.
///
/// The line graph `L(G)` of a graph `G` represents the adjacencies between edges of G.
/// `L(G)` contains a vertex for every edge in `G`, and `L(G)` contains an edge between two
/// vertices if the corresponding edges in `G` have a vertex in common.
///
/// Arguments:
///
/// * `input_graph` - The input graph `G`.
/// * `default_node_weight` - A callable that will return the weight to use
/// for newly created nodes.
/// * `default_edge_weight` - A callable that will return the weight object
/// to use for newly created edges.
///
/// Returns the constructed line graph `L(G)`, and the map from the edges of `L(G)` to
/// the vertices of `G`.
///
/// # Example
/// ```rust
/// use rustworkx_core::line_graph::line_graph;
/// use rustworkx_core::petgraph::visit::EdgeRef;
/// use rustworkx_core::petgraph::Graph;
/// use hashbrown::HashMap;
/// use petgraph::graph::{EdgeIndex, NodeIndex};
/// use petgraph::Undirected;
///
/// let input_graph =
/// Graph::<(), (), Undirected>::from_edges(&[(0, 1), (0, 2), (1, 2), (0, 3)]);
///
/// let (output_graph, output_edge_map): (
/// petgraph::graph::UnGraph<(), ()>,
/// HashMap<petgraph::prelude::EdgeIndex, petgraph::prelude::NodeIndex>,
/// ) = line_graph(&input_graph, || (), || ());
///
/// let output_edge_list = output_graph
/// .edge_references()
/// .map(|edge| (edge.source().index(), edge.target().index()))
/// .collect::<Vec<(usize, usize)>>();
///
/// let expected_edge_list = vec![(3, 1), (3, 0), (1, 0), (2, 0), (2, 1)];
/// let expected_edge_map: HashMap<EdgeIndex, NodeIndex> = [
/// (EdgeIndex::new(0), NodeIndex::new(0)),
/// (EdgeIndex::new(1), NodeIndex::new(1)),
/// (EdgeIndex::new(2), NodeIndex::new(2)),
/// (EdgeIndex::new(3), NodeIndex::new(3)),
/// ]
/// .into_iter()
/// .collect();
///
/// assert_eq!(output_edge_list, expected_edge_list);
/// assert_eq!(output_edge_map, expected_edge_map);
/// ```
pub fn line_graph<K, G, T, F, H, M>(
input_graph: K,
mut default_node_weight: F,
mut default_edge_weight: H,
) -> (G, HashMap<K::EdgeId, G::NodeId>)
where
K: EdgeCount + IntoNodeIdentifiers + IntoEdges,
G: Create + Data<NodeWeight = T, EdgeWeight = M>,
F: FnMut() -> T,
H: FnMut() -> M,
K::EdgeId: Hash + Eq,
{
let num_edges = input_graph.edge_count();
let mut output_graph = G::with_capacity(num_edges, 0);
let mut output_edge_map =
HashMap::<K::EdgeId, G::NodeId>::with_capacity(input_graph.edge_count());

for edge in input_graph.edge_references() {
let new_node = output_graph.add_node(default_node_weight());
output_edge_map.insert(edge.id(), new_node);
}

for node in input_graph.node_identifiers() {
let edges: Vec<K::EdgeRef> = input_graph.edges(node).collect();
for i in 0..edges.len() {
for j in i + 1..edges.len() {
let node0 = output_edge_map.get(&edges[i].id()).unwrap();
let node1 = output_edge_map.get(&edges[j].id()).unwrap();
output_graph.add_edge(*node0, *node1, default_edge_weight());
}
}
}
(output_graph, output_edge_map)
}

#[cfg(test)]

mod test_line_graph {
use crate::line_graph::line_graph;
use crate::petgraph::visit::EdgeRef;
use crate::petgraph::Graph;
use hashbrown::HashMap;
use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::Undirected;

#[test]
fn test_simple_graph() {
// Simple graph
let input_graph =
Graph::<(), (), Undirected>::from_edges(&[(0, 1), (2, 3), (3, 4), (4, 5)]);

let (output_graph, output_edge_map): (
petgraph::graph::UnGraph<(), ()>,
HashMap<petgraph::prelude::EdgeIndex, petgraph::prelude::NodeIndex>,
) = line_graph(&input_graph, || (), || ());

let output_edge_list = output_graph
.edge_references()
.map(|edge| (edge.source().index(), edge.target().index()))
.collect::<Vec<(usize, usize)>>();

let expected_edge_list = vec![(2, 1), (3, 2)];
let expected_edge_map: HashMap<EdgeIndex, NodeIndex> = [
(EdgeIndex::new(0), NodeIndex::new(0)),
(EdgeIndex::new(1), NodeIndex::new(1)),
(EdgeIndex::new(2), NodeIndex::new(2)),
(EdgeIndex::new(3), NodeIndex::new(3)),
]
.into_iter()
.collect();

assert_eq!(output_edge_list, expected_edge_list);
assert_eq!(output_edge_map, expected_edge_map);
}
}
31 changes: 30 additions & 1 deletion src/coloring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
// under the License.

use crate::graph;
use rustworkx_core::coloring::greedy_node_color;

use rustworkx_core::coloring::{greedy_edge_color, greedy_node_color};

use pyo3::prelude::*;
use pyo3::types::PyDict;
Expand Down Expand Up @@ -59,3 +60,31 @@ pub fn graph_greedy_color(py: Python, graph: &graph::PyGraph) -> PyResult<PyObje
}
Ok(out_dict.into())
}

/// Color edges of a :class:`~.PyGraph` object using a greedy approach.
///
/// This function works by greedily coloring the line graph of the given 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_greedy_edge_color(graph)
/// assert edge_colors == {0: 0, 1: 1, 2: 0, 3: 1, 4: 0, 5: 1, 6: 2}
///
#[pyfunction]
#[pyo3(text_signature = "(graph, /)")]
pub fn graph_greedy_edge_color(py: Python, graph: &graph::PyGraph) -> PyResult<PyObject> {
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
let colors = greedy_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())
}
Loading