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 substitute_node_with_subgraph method to PyDiGraph #312

Merged
merged 23 commits into from
Jun 24, 2021

Conversation

mtreinish
Copy link
Member

@mtreinish mtreinish commented Apr 26, 2021

This commit adds a new method substitute_node_with_subgraph that takes
in a node id and a PyDiGraph object and then procedes to replace the
specified node with the subgraph.

TODO:

  • Add tests
  • Add release notes
  • Verify functionality for use with qiskit-terra's substitute_node_with_dag

This commit adds a new method substitute_node_with_subgraph that takes
in a node id and a PyDiGraph object and then procedes to replace the
specified node with the subgraph.
mtreinish added a commit to mtreinish/qiskit-core that referenced this pull request Apr 26, 2021
This commit leverage the substitute_node_with_subgraph method being
added Qiskit/rustworkx#312 for the dagcircuit method
substitute_node_with_dag.
@coveralls
Copy link

coveralls commented Apr 26, 2021

Pull Request Test Coverage Report for Build 968084539

  • 123 of 124 (99.19%) changed or added relevant lines in 3 files are covered.
  • 2 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+0.001%) to 97.516%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/digraph.rs 99 100 99.0%
Files with Coverage Reduction New Missed Lines %
src/isomorphism.rs 2 97.51%
Totals Coverage Status
Change from base Build 968015678: 0.001%
Covered Lines: 8285
Relevant Lines: 8496

💛 - Coveralls

@mtreinish mtreinish changed the title [WIP] Add substitute_node_with_subgraph method to PyDiGraph Add substitute_node_with_subgraph method to PyDiGraph Jun 4, 2021
@mtreinish mtreinish added this to the 0.10.0 milestone Jun 4, 2021
@mtreinish
Copy link
Member Author

Ok, I've updated this and I think it's ready for review now. There is a matching terra PR which shows this in use: Qiskit/qiskit#6302

@mtreinish mtreinish requested a review from kdk June 4, 2021 19:04
@mtreinish mtreinish requested a review from nahumsa June 4, 2021 19:04
@nahumsa
Copy link
Contributor

nahumsa commented Jun 10, 2021

I think that the node index ordering of the substituted graph needs to be explicit in the documentation or maybe reindexing all nodes of the new graph.

For instance, I have the original_graph being
orig_graph

and the other_graph being
other_graph

using substitute_node_with_subgraph on node 2, we get
result

All new nodes start at the max value of the 'original_graph` node index, but this is not explicit in the documentation. Or maybe it could be useful to reindexing all nodes or even giving the choice for the user to choose the index of the added nodes.

Here is the code to reproduce what I said:

import retworkx
from retworkx.visualization import mpl_draw
original_graph = retworkx.generators.directed_path_graph(5)
other_graph = retworkx.generators.directed_path_graph(2)
original_graph.substitute_node_with_subgraph(2, other_graph, lambda _, __, ___: 0)
mpl_draw(original_graph, with_labels=True)

This concerns me because if I add a node on the graph after this, the node added will have index 2 and I would expect index to be 7.

original_graph.add_parent(6, None, None)
mpl_draw(original_graph, with_labels=True)

add_node

@mtreinish
Copy link
Member Author

mtreinish commented Jun 10, 2021

This is unavoidable because we can't preserve the indices between the 2 graphs as there might be overlap and we're essentially just calling add_node() for each node in the graph. But this is why the return from the function is a custom mapping class of old node indices in other_graph to it's new index in original_graph so you can run this and then use that dictionary I will try to improve the documentation around this to make this clear.

For the second half I'm actually not actually sure how node index 2 is getting added there. I agree it should be 7, let me dig into that with your example locally and see if I can figure out what's going on

@mtreinish
Copy link
Member Author

So I dug into this it's a behavior of petgraph's stable graph. It will reuse node indices after removal:

https://github.com/petgraph/petgraph/blob/0.5.1/src/graph_impl/stable_graph/mod.rs#L246-L253

I was able to reproduce this manually with:

graph = retworkx.PyDiGraph()
graph.add_nodes_from(list(range(5)))
graph.add_nodes_from(list(range(2)))
graph.remove_node(2)
res_manual = graph.add_parent(6, None, None)
print(res_manual)

which returns 2. I'm guessing it does this to preserve memory. But, this caught me by surprise too, but then I haven't manually inspected the indices post removal like this before. When I've been using graphs with removals I been interacting with the indices programmatically and never looked at them directly. I think we should definitely document this in the class docstrings for PyGraph, PyDiGraph, PyDAG because it definitely isn't straightforward I will open an issue about this.

@nahumsa
Copy link
Contributor

nahumsa commented Jun 10, 2021

This is unavoidable because we can't preserve the indices between the 2 graphs as there might be overlap and we're essentially just calling add_node() for each node in the graph. But this is why the return from the function is a custom mapping class of old node indices in other_graph to it's new index in original_graph so you can run this and then use that dictionary I will try to improve the documentation around this to make this clear.

Sure, that makes sense. I forgot about the return, sorry about that.

@mtreinish mtreinish mentioned this pull request Jun 18, 2021
2 tasks
/// when iterated over (although the same object will have a consistent
/// order when iterated over multiple times).
///
#[text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None)"]
Copy link
Member

Choose a reason for hiding this comment

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

I think edge_map_fn should be optional.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is required right now because we without it we don't know what to do with edges to or from node. If we switched it to optional what do you think the default behavior should it was not set? The only option I could think of is to just drop all the edges into or out of node if this wasn't set, but if we did this it wouldn't really feel like it was substituting the node with 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.

Is this a typo?

Suggested change
#[text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None)"]
#[text_signature = "(self, node, other, edge_map_fn, node_filter=None, edge_weight_map=None)"]

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually intentional, the / indicates the end of positional only arguments (it's part of the cPython interface and is actually a valid python syntax for >=3.8). See the docs for the rust python interface here: https://pyo3.rs/v0.9.2/function.html#making-the-function-signature-available-to-python which explains this.

@mtreinish mtreinish merged commit c831e35 into Qiskit:main Jun 24, 2021
@mtreinish mtreinish deleted the substitute-node branch June 24, 2021 20:59
mergify bot added a commit to Qiskit/qiskit that referenced this pull request Sep 7, 2021
* Use retworkx for substitute_node_with_dag

This commit leverage the substitute_node_with_subgraph method being
added Qiskit/rustworkx#312 for the dagcircuit method
substitute_node_with_dag.

* DNM: Add retworkx from PR branch to CI

* Fix handling of input dag with direct edge from input to output

* Update requirements for testing

* Run black

* Avoid node copy

* Avoid op func call overhead

* Fix lint

* Expand substitute tests

* Make failing test deterministic

* Fix qasm example

* Fix lint

* Update retworkx source path

* Fix rebase issues

* Bump minimum retworkx version to latest release

* Reduce iterations building wire maps

* Use a plain list comprehension instead of a lambda map

* Apply suggestions from code review

Co-authored-by: Kevin Krsulich <[email protected]>

* Improve code comments

* Add reno touting performance benefits

Co-authored-by: Kevin Krsulich <[email protected]>
Co-authored-by: Jake Lishman <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
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.

5 participants