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

Implementation of cycle_basis_edges #885

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

raynelfss
Copy link
Contributor

Aims to fix #551

  • Inspired by the existing procedure cycle_basis.
  • Modified to return a tuple of NodeId in the format (source, target).
  • Added API calls so that method works in Python.
  • TO-DO: return weights:
    • weight() method from EdgeRef returns a reference and cannot be copied over.

@CLAassistant
Copy link

CLAassistant commented May 31, 2023

CLA assistant check
All committers have signed the CLA.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@raynelfss
Copy link
Contributor Author

Results from testing in Python:

image

@coveralls
Copy link

coveralls commented May 31, 2023

Pull Request Test Coverage Report for Build 9962098330

Details

  • 87 of 102 (85.29%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.06%) to 95.744%

Changes Missing Coverage Covered Lines Changed/Added Lines %
rustworkx-core/src/connectivity/cycle_basis.rs 79 94 84.04%
Totals Coverage Status
Change from base Build 9859812730: -0.06%
Covered Lines: 18108
Relevant Lines: 18913

💛 - Coveralls

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.

So overall this looks like a good start. The big things that I think are missing are python side tests to validate the python api is working as expected and also a release note (https://github.com/Qiskit/rustworkx/blob/main/CONTRIBUTING.md#release-notes).

The other small concern I have is the amount of duplication between cycle_basis_edges and cycle_basis. The code is mostly the same between the function and I'm thinking it would be good to combine them into a single code path.

As for the edge weight for the python side you can call weight.clone_ref(py) as it's a PyObject type and we can clone it with a GIL handle (which is what the Python type gives us).

/// let graph = UnGraph::<i32, i32>::from_edges(&edge_list);
/// let mut res: Vec<Vec<(NodeIndex, NodeIndex)>> = cycle_basis_edges(&graph, Some(NodeIndex::new(0)));
/// ```
pub fn cycle_basis_edges<G>(graph: G, root: Option<G::NodeId>) -> Vec<Vec<(G::NodeId, G::NodeId)>>
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 returning Vec<(G::NodeId, G::NodeId)> here it might be better to return Vec<G::EdgeId>. Then if we want to return the edge endpoint or weight we can just look it up from the index.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's funny you mention this, as in an earlier version, I had the function return the EdgeId attribute of each EdgeRef but replaced it with the tuple thinking that it should instead return the edge objects.

Also, I'm assuming that by returning the edge endpoint you mean returning it using the wrapper located in rustworkx/src/connectivity/mod.rs lines 100 to 110. Should I use the graph.edges() method fetch the edges by index?

Copy link
Member

@mtreinish mtreinish Jun 1, 2023

Choose a reason for hiding this comment

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

By edge endpoints I meant what you're doing now. For each edge you find you're mapping to (edge.source(), edge.target()) (the endpoints being the tuple of nodes for the edge), but instead if you did edge.id() that would give you the unique G::EdgeId for the edge. Then if you returned a Vec<G::EdgeId> callers of this function could look up any attributes of the edge they needed from that list. For examples if you wanted a list of endpoint tuples you could do something like:

let edge_endpoints = cycle_basis(graph, None)
    .into_iter()
    .filter_map(|e| graph.edge_endpoints(e))
    .collect()

but it would also give you the flexibility to get the weights, or other details. It basically is just returning a reference to the edge in graph instead of one of properties of the edge.

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 see, I will make those changes accordingly.

@mtreinish mtreinish added this to the 0.14.0 milestone Jun 1, 2023
@danielleodigie
Copy link
Contributor

This looks pretty good to me, just a quick question: if there a reason to declare both a cycles_edges and a cycles_nodes if from what I understand we only return one or the other? Is it possible in Rust to declare variables like:
if want_edge: let mut cycles: Vec<Vec<G::EdgeId>> = Vec::new(); else: let mut cycles: Vec<Vec<G::NodeId>> = Vec::new();. This is low-key just for me to learn

@raynelfss
Copy link
Contributor Author

if there a reason to declare both a cycles_edges and a cycles_nodes if from what I understand we only return one or the other? Is it possible in Rust to declare variables like:
if want_edge: let mut cycles: Vec<Vec<G::EdgeId>> = Vec::new(); else: let mut cycles: Vec<Vec<G::NodeId>> = Vec::new();

Well, there might be a better way of doing so that doesn't involve two vectors. The reason I did it this way is that I couldn't conditionally assign this variable with different types (G::EdgeId || G::NodeId) without having to wrap two similar processes under if and else statements, which could be a bit redundant.

I could, however, change the enum so that it represents the individual nodes/edges (instead of Vec<Vec<T>> for each type) and make the function return an instance of Vec<Vec<EdgeOrNodes<E, N>>> and then unwrap the values of said Vec individually.

The problem happens when importing this enum into the wrapper function, which complains about the types in use. I could try and get this to work with enough time.

@danielleodigie
Copy link
Contributor

if there a reason to declare both a cycles_edges and a cycles_nodes if from what I understand we only return one or the other? Is it possible in Rust to declare variables like:
if want_edge: let mut cycles: Vec<Vec<G::EdgeId>> = Vec::new(); else: let mut cycles: Vec<Vec<G::NodeId>> = Vec::new();

Well, there might be a better way of doing so that doesn't involve two vectors. The reason I did it this way is that I couldn't conditionally assign this variable with different types (G::EdgeId || G::NodeId) without having to wrap two similar processes under if and else statements, which could be a bit redundant.

I could, however, change the enum so that it represents the individual nodes/edges (instead of Vec<Vec<T>> for each type) and make the function return an instance of Vec<Vec<EdgeOrNodes<E, N>>> and then unwrap the values of said Vec individually.

The problem happens when importing this enum into the wrapper function, which complains about the types in use. I could try and get this to work with enough time.

I mean if it works fine, I don't see any reason to change it, I was just asking. Maybe ask @mtreinish. Otherwise, looks good

Copy link
Contributor

@danielleodigie danielleodigie left a comment

Choose a reason for hiding this comment

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

LGTM!

@raynelfss raynelfss requested a review from mtreinish June 7, 2023 14:10
@mtreinish mtreinish marked this pull request as ready for review June 7, 2023 19:30
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.

Thanks for the fast update. This is look better from a code duplication standpoint and is making good progress. I left some inline comments mainly about the interface split. I have some concerns that the current version of the code has some breaking API changes to rustworkx-core that we can't make without potentially impacting users. I left some inline suggestions on how we can tweak this a bit to avoid that kind of impact.

rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
src/connectivity/mod.rs Outdated Show resolved Hide resolved
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
Comment on lines 207 to 194
impl<N, E> EdgesOrNodes<N, E> {
pub fn unwrap_nodes(self) -> Vec<Vec<N>> {
use EdgesOrNodes::*;
match self {
Nodes(x) => x,
Edges(_) => vec![],
}
}
pub fn unwrap_edges(self) -> Vec<Vec<E>> {
use EdgesOrNodes::*;
match self {
Edges(x) => x,
Nodes(_) => vec![],
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Are these methods used for anything outside of the tests? If not I don't think we should add them to the public interface since they're a bit custom purpose to simplify testing and I think they'd be better served as a function in the tests (although based on my earlier comments about the interface split I don't think we should expose EdgesOrNodes publicly and if not I think we can just handle all of this internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, since we're using a custom return type that can't be interpreted natively as Vec<Vec<NodeId>> or Vec<Vec<EdgeId>> the values need to be unwrapped.

Although this could technically be done inside of the public-facing functions that were part of your review, we'd have to rewrite the same procedure twice over (in testing and in the public-facing functions, which is redundant.

rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
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.

This is looking good, thanks for the fast updates on this, I have a few more comments inline.

Comment on lines 53 to 56
fixes:
- |
Support for edges when getting the cycle basis. Refer to
`#551 <https://github.com/Qiskit/rustworkx/issues/551>`__ for more details.
Copy link
Member

Choose a reason for hiding this comment

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

You don't actually need this release note what you have for the feature note is more than sufficient to document the new feature for this release. This is really just a new feature there isn't any bug being fixed here. The github "issues" tracker is really both for tracking bugs and also for feature requests.

Comment on lines 3 to 6
- |
A function, :func:`~rustworkx.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.
Copy link
Member

Choose a reason for hiding this comment

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

You need to split this into 2 release notes, one for the rustworkx-core function and one for the python side function being added.

Suggested change
- |
A function, :func:`~rustworkx.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.
- |
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.

rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
@raynelfss raynelfss requested a review from mtreinish June 13, 2023 19:22
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
rustworkx-core/src/connectivity/cycle_basis.rs Outdated Show resolved Hide resolved
- Inspired from the existing procedure `cycle_basis`.
- Modified to return a tuple of `NodeId` in format `(source, target)`.
- Added API calls so that method works in Python.
- Cycle_basis_edges now returns a list of edgeIDs.
- Added `TestCycleBasisEdges` class for unit testing.
- Added similar tests to those present on `TestCycleBasis`.
    - Tests are similar due to the same methodology.
    - Graphs used for testing are different.
- Modified get_edge_between to only check target when equiv == false.
- Cycle basis edges is now within the codebase of cycle basis.
- The `EdgesOrNodes` enum handles the different return data types.
- Release notes now include an example with a jupyter notebook.
- A previous commit inadvertedly made cycle_basis prublic.
    - To address this, two new public-facing functions were added:
        - `cycle_basis` for NodeId returns.
        - `cycle_basis_edges` for EdgeId returns.
- When customizing cycle_basis a new enum EdgesOrNodes was made.
    - Enum should not be have been shared by definition.
        - Correction makes it private.
    - Removed redundant import from unwrap methods.
- Tests have been adjusted accordingly.
- The method returns the first edge found between nodes.
- The docstring for this method was corrected.
@IvanIsCoding IvanIsCoding modified the milestones: 0.14.0, 0.15.0 Oct 18, 2023
@IvanIsCoding IvanIsCoding removed this from the 0.15.0 milestone Jun 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add equivalent of cycle_basis that returns an edge list
6 participants