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

Add Misra-Gries edge coloring method #902

Merged
merged 39 commits into from
Jan 10, 2024

Conversation

alexanderivrii
Copy link
Contributor

This is an attempt to add the Misra-Gries edge coloring algorithm to Rustworkx, following @ihincks's python code in #854 and wiki page https://en.wikipedia.org/wiki/Misra_%26_Gries_edge_coloring_algorithm. This is still raw, but I would be happy to get some initial feedback on which parts of the code could be implemented in more rust-friendly way, whether the names and inputs/outputs of different functions look ok, etc.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

I left a few quick initial comments from a first scan through the PR. The other question I have is whether you want to add this to rustworkx-core or not? I think it's probably generally useful enough (and you did just add the node coloring function to the core crate) that it might make sense to put this in rustworkx-core from the start.

src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated
Comment on lines 95 to 100
let used_colors = self.get_used_colors(u);
let mut c: usize = 0;
while used_colors.contains(&c) {
c += 1;
}
c
Copy link
Member

Choose a reason for hiding this comment

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

Just for fun you could do this as an iterator too, but not sure if is any faster. Something like:

Suggested change
let used_colors = self.get_used_colors(u);
let mut c: usize = 0;
while used_colors.contains(&c) {
c += 1;
}
c
let used_colors = self.get_used_colors(u);
(0..)
.position(|color| used_colors.contains(&color))
.unwrap()

src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated
fan_extended = true;
last_node = *z;
fan.push((*edge_index, *z));
let position_z = neighbors.iter().position(|x| x.1 == *z).unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing this it'd probably be more efficient to use neighbors.iter().enumerate() for the for loop. Then we'd have the index for every step and position_z would have no lookup overhead. Right now this is O(n) for this lookup as you have to re-traverse neighbors on each iteration.

src/coloring.rs Outdated Show resolved Hide resolved
src/coloring.rs Outdated Show resolved Hide resolved
@mtreinish mtreinish added this to the 0.14.0 milestone Jun 15, 2023
@alexanderivrii
Copy link
Contributor Author

@mtreinish, thanks for the detailed feedback. I guess putting the functionality in rustworkx-core makes sense. And I really like the iterator-based way of implementing things.

@alexanderivrii
Copy link
Contributor Author

I have made another round of changes, moving the new edge coloring function to rustworkx-core and changing the colors to be a vector indexed by usize (the internal value of EdgeIndex, which we can get using EdgeIndexable). Using a vector rather than a hash map leads to about 20% improvement in performance (which is not surprising). I don't understand the cause for CI failures, @mtreinish could you please take a look?

@coveralls
Copy link

coveralls commented Oct 9, 2023

Pull Request Test Coverage Report for Build 7479009283

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.007%) to 95.915%

Totals Coverage Status
Change from base Build 7451338673: 0.007%
Covered Lines: 15990
Relevant Lines: 16671

💛 - Coveralls

@mtreinish
Copy link
Member

I think the ci failure was a temporary issue related to the release of a new version of something. I just updated the pr branch and it seemed to have work fine.

@alexanderivrii alexanderivrii changed the title [WIP] Add Misra-Gries edge coloring method Add Misra-Gries edge coloring method Oct 18, 2023
Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Overall this LGTM, I just had a few small questions and suggestions inline.

I am a bit worried about the complexity of some of the inner loops in the helper methods as there is a lot of nested looping and those all get called in an outer loop from run_algorithm(). I'll need to re-read the paper to see if that's just inherit to the algorithm. But either way for an initial implementation this is fine anyway, if it becomes a performance bottleneck we can always make improvements down the road.

// 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..)
Copy link
Member

Choose a reason for hiding this comment

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

Just to avoid unbound iteration as a potential source of bugs:

Suggested change
let free_color: usize = (0..)
let free_color: usize = (0..self.colors.len())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4e22ad7 using the stricter upper bound of max_node_degree + 1.

Comment on lines 261 to 262
let used_colors = self.get_used_colors(u);
!used_colors.contains(&c)
Copy link
Member

Choose a reason for hiding this comment

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

Could you just do self.colors.contains(&Some(c)) (or something like this, I might have got the position of & incorrect)? You'd have the same linear overhead.

More generally though I'm wondering if it makes more sense to store a HashSet of used colors in the MisraGries struct so you don't have to generate it on the fly every time. Then you could make this a O(1) lookup every time. It would require doing book keeping on adding or changing colors but it should be manageable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have followed the above suggestion in 4e22ad7, and indeed it gives a significant speedup on medium/large-sized complete graphs. E.g., on my laptop previously it took 16.95 seconds to color a complete graph over 90 vertices, and now it takes 0.58 seconds. For heavy-hex graphs the difference is less pronounced, but the new implementation is still a bit faster.

Comment on lines 272 to 273
let mut fan: Vec<(G::EdgeId, G::NodeId)> = Vec::new();
fan.push((eid, v));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let mut fan: Vec<(G::EdgeId, G::NodeId)> = Vec::new();
fan.push((eid, v));
let mut fan: Vec<(G::EdgeId, G::NodeId)> = vec![(eid, v)];

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 7900916. Also the code has been updated to use G::EdgeRef instead of G::EdgeId, so it's enough to only keep the edge reference.

// 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::EdgeId, usize)> {
let mut path: Vec<(G::EdgeId, usize)> = Vec::new();
let mut cur_node: G::NodeId = u;
Copy link
Member

Choose a reason for hiding this comment

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

The compiler should be able to infer the type here because it's defined on u.

Suggested change
let mut cur_node: G::NodeId = u;
let mut cur_node = u;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in 7e6470d.

@mtreinish
Copy link
Member

Oh, the other thing is the new function needs to be added to the api docs index here: https://github.com/Qiskit/rustworkx/blob/main/docs/source/api/algorithm_functions/other.rst (although maybe we should add a graph coloring page at this point)

@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Jan 9, 2024

I am a bit worried about the complexity of some of the inner loops in the helper methods as there is a lot of nested looping and those all get called in an outer loop from run_algorithm().

Agreed, it would be great if we could remove the extra loop in the get_maximal_fan function, for example by precomputing something at the beginning of the function. Essentially, given a node u and its neighbor node v we try to "extend" the fan by finding an edge (u, w) such that its color is not used at v. If such an edge is found, we add it to the fan, and repeat the above process with w instead of v. The problem is that the node v keeps changing and there are multiple colors that can be unused at a given node. So I don't see a simple way to remove one of the two loops. But would be happy to hear suggestions.

@mtreinish
Copy link
Member

Yeah, I don't have any concrete suggestions off the top of my head yet either. It was more just my impressions as I was reviewing it. But like I said we can leave it like this and if performance becomes an issue or one of us comes up with something it's easy enough to change and improve the internal logic later.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for doing this.

@mtreinish mtreinish added the automerge Queue a approved PR for merging label Jan 10, 2024
@mergify mergify bot merged commit 6d82a11 into Qiskit:main Jan 10, 2024
27 checks passed
@alexanderivrii alexanderivrii deleted the more-edge-coloring branch February 9, 2024 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge Queue a approved PR for merging
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants