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

dag_isomorphism::is_isomorphic_matching checks don't work on PyDAG with removed node #27

Closed
mtreinish opened this issue Feb 14, 2020 · 4 comments · Fixed by #115
Closed
Labels
bug Something isn't working

Comments

@mtreinish
Copy link
Member

retworkx::dag_isomorphism::is_isomorphic_matching was written by copy and pasting petgraph's isomorphism functions and changing the input type to be PyDAG instead of petgraph's Graph. However, the code wasn't 100% compatible as originally assumed because of node removals. The function assumes that an isomorphic graph will always have the same number of node indexes, however in the case of petgraph's StableGraph(which PyDAG uses internally) this is not the case. A stable graph's number of indexes increases on each node add, and if a node is removed the old index is not used again, unlike Graph. This will cause a panic like: thread '<unnamed>' panicked at 'index out of bounds: the len is 4 but the index is 4' when trying to compare a graph with removals to a graph without any.

A temporary workaround for this is to use deepcopy (or pickle and unpickle) the pydags. This will reset the index counters so they'll be the same. This is not a great answer, it's just a workaround until the function is updated.

@mtreinish mtreinish added the bug Something isn't working label Feb 14, 2020
@samanthavbarron
Copy link

samanthavbarron commented Aug 16, 2020

Hi @mtreinish! I was wondering what the status of this bug is, since it's referenced here
https://github.com/Qiskit/qiskit-terra/blob/0.15.1/qiskit/dagcircuit/dagcircuit.py#L779

I'm wondering because from my testing, it looks like Aqua's operator flow (CircuitStateFn in particular) uses QuantumCircuit.__eq__ quite a bit in trying to reduce operators, and the deepcopy calls slow it down. Moreover, if you're running a VQE, this happens at every optimization step. Perhaps Aqua should do something instead of using __eq__, but I figured I'd check here first. Thanks!

@mtreinish
Copy link
Member Author

There haven't been any updates on this since I initially opened the issue. I hadn't treated it as a priority because I thought QuantumCircuit.__eq__ was only be used for testing so the use of deepcopy to reset the node indexes wasn't a big deal since for a test case performance doesn't matter so much. It shouldn't be too hard to fix this, there are just a couple of places in the dag_isomorphism.rs module that assume the indices are the same in both graphs where we'll have to factor in that number of indices are the same, but the exact index numbers aren't. If you'd like to take a stab at fixing it I can give you some pointers, otherwise I'll try to circle back to this soon.

@samanthavbarron
Copy link

Unfortunately I don't know anything about Rust, but it seems like something good to learn at some point. For now though I don't think I'll be able to fix it. Also maybe @Cryoris would be interested in this issue as well. But thanks for the info, this is helpful!

@mtreinish
Copy link
Member Author

So I've come up with a simple test case in case to reproduce this easily if someone wants to take a crack at it. I took a quick look at it yesterday and it wasn't the 2 line fix I initially thought it would be, but I still don't think it should be too bad to solve.

    def test_isomorphic_compare_nodes_with_removals(self):
        dag_a = retworkx.PyDAG()
        dag_b = retworkx.PyDAG()

        qr_0_in = dag_a.add_node('qr[0]')
        qr_1_in = dag_a.add_node('qr[1]')
        cr_0_in = dag_a.add_node('cr[0]')
        qr_0_out = dag_a.add_node('qr[0]')
        qr_1_out = dag_a.add_node('qr[1]')
        cr_0_out = dag_a.add_node('qr[0]')
        cu1 = dag_a.add_child(qr_0_in, 'cu1', 'qr[0]')
        dag_a.add_edge(qr_1_in, cu1, 'qr[1]')
        measure_0 = dag_a.add_child(cr_0_in, 'measure', 'cr[0]')
        dag_a.add_edge(cu1, measure_0, 'qr[0]')
        measure_1 = dag_a.add_child(cu1, 'measure', 'qr[1]')
        dag_a.add_edge(measure_0, measure_1, 'cr[0]')
        dag_a.add_edge(measure_1, qr_1_out, 'qr[1]')
        dag_a.add_edge(measure_1, cr_0_out, 'cr[0]')
        dag_a.add_edge(measure_0, qr_0_out, 'qr[0]')
        dag_a.remove_node(cu1)
        dag_a.add_edge(qr_0_in, measure_0, 'qr[0]')
        dag_a.add_edge(qr_1_in, measure_1, 'qr[1]')

        qr_0_in = dag_b.add_node('qr[0]')
        qr_1_in = dag_b.add_node('qr[1]')
        cr_0_in = dag_b.add_node('cr[0]')
        qr_0_out = dag_b.add_node('qr[0]')
        qr_1_out = dag_b.add_node('qr[1]')
        cr_0_out = dag_b.add_node('qr[0]')
        measure_0 = dag_b.add_child(cr_0_in, 'measure', 'cr[0]')
        dag_b.add_edge(qr_0_in, measure_0, 'qr[0]')
        measure_1 = dag_b.add_child(qr_1_in, 'measure', 'qr[1]')
        dag_b.add_edge(measure_1, qr_1_out, 'qr[1]')
        dag_b.add_edge(measure_1, cr_0_out, 'cr[0]')
        dag_b.add_edge(measure_0, measure_1, 'cr[0]')
        dag_b.add_edge(measure_0, qr_0_out, 'qr[0]')

        self.assertTrue(
            retworkx.is_isomorphic_node_match(
                dag_a, dag_b, lambda x, y: x == y))

    def test_isomorphic_compare_nodes_with_removals_deepcopy(self):
        dag_a = retworkx.PyDAG()
        dag_b = retworkx.PyDAG()

        qr_0_in = dag_a.add_node('qr[0]')
        qr_1_in = dag_a.add_node('qr[1]')
        cr_0_in = dag_a.add_node('cr[0]')
        qr_0_out = dag_a.add_node('qr[0]')
        qr_1_out = dag_a.add_node('qr[1]')
        cr_0_out = dag_a.add_node('qr[0]')
        cu1 = dag_a.add_child(qr_0_in, 'cu1', 'qr[0]')
        dag_a.add_edge(qr_1_in, cu1, 'qr[1]')
        measure_0 = dag_a.add_child(cr_0_in, 'measure', 'cr[0]')
        dag_a.add_edge(cu1, measure_0, 'qr[0]')
        measure_1 = dag_a.add_child(cu1, 'measure', 'qr[1]')
        dag_a.add_edge(measure_0, measure_1, 'cr[0]')
        dag_a.add_edge(measure_1, qr_1_out, 'qr[1]')
        dag_a.add_edge(measure_1, cr_0_out, 'cr[0]')
        dag_a.add_edge(measure_0, qr_0_out, 'qr[0]')
        dag_a.remove_node(cu1)
        dag_a.add_edge(qr_0_in, measure_0, 'qr[0]')
        dag_a.add_edge(qr_1_in, measure_1, 'qr[1]')

        qr_0_in = dag_b.add_node('qr[0]')
        qr_1_in = dag_b.add_node('qr[1]')
        cr_0_in = dag_b.add_node('cr[0]')
        qr_0_out = dag_b.add_node('qr[0]')
        qr_1_out = dag_b.add_node('qr[1]')
        cr_0_out = dag_b.add_node('qr[0]')
        measure_0 = dag_b.add_child(cr_0_in, 'measure', 'cr[0]')
        dag_b.add_edge(qr_0_in, measure_0, 'qr[0]')
        measure_1 = dag_b.add_child(qr_1_in, 'measure', 'qr[1]')
        dag_b.add_edge(measure_1, qr_1_out, 'qr[1]')
        dag_b.add_edge(measure_1, cr_0_out, 'cr[0]')
        dag_b.add_edge(measure_0, measure_1, 'cr[0]')
        dag_b.add_edge(measure_0, qr_0_out, 'qr[0]')

        self.assertTrue(
            retworkx.is_isomorphic_node_match(
                copy.deepcopy(dag_a), copy.deepcopy(dag_b),
                lambda x, y: x == y))

Fwiw, deep rust experience shouldn't be required to solve this and I'll gladly help anyone that was interested in diving in on it. It would probably actually be a good intro to rust project (except for that the code in dag_isomorphism.rs is pretty hairy mostly because of the coding style that is used in the upstream petgraph it was forked from) because it's a logic bug in a existing algorithm. The steepest part of the learning curve with rust the first time is normally dealing with the extra checking and requirements the compiler enforces (how it provides safety) but starting with a working piece of code means at least you have a known compiling starting point.

mtreinish added a commit to mtreinish/retworkx that referenced this issue Aug 18, 2020
The dag_isomorphism module was forked from upstream petgraph to handle
the PyDiGraph type and also to enable handling exceptions in the python
check functions gracefully. However, when this was done it neglected
that here were limitations with that module which causes failures in
certain scenarios after node removals. This was because the upstream
petgraph implementation was built on the Graph type instead of the
StableGraph type. The only difference between these types is that
StableGraph does not reuse indexes on removals but Graph does. This
can cause there to be holes in the list of node ids. This breaks
assumptions in multiple places of the VF2 implementation causing a
panic if isomorphism checks are run on a PyDiGraph that has nodes
removed. This commit worksaround this limitation by checking if we've
removed nodes from the PyDiGraph object and if we have it iterates over
the graph and clones it into a copy with a condensed set of node ids.

This fix is less than ideal in that it results in a copy of the graph
which will potentially have performance implications, especially for
larger graphs. But after attempting to fix the VF2 implementation that
seems to be a more involved project than I originally hoped. This will
at least workaround the bug until a better more robust VF2
implementation can be written (and likely should be contributed back
upstream to petgraph).

Fixes Qiskit#27
mtreinish added a commit that referenced this issue Aug 25, 2020
The dag_isomorphism module was forked from upstream petgraph to handle
the PyDiGraph type and also to enable handling exceptions in the python
check functions gracefully. However, when this was done it neglected
that here were limitations with that module which causes failures in
certain scenarios after node removals. This was because the upstream
petgraph implementation was built on the Graph type instead of the
StableGraph type. The only difference between these types is that
StableGraph does not reuse indexes on removals but Graph does. This
can cause there to be holes in the list of node ids. This breaks
assumptions in multiple places of the VF2 implementation causing a
panic if isomorphism checks are run on a PyDiGraph that has nodes
removed. This commit worksaround this limitation by checking if we've
removed nodes from the PyDiGraph object and if we have it iterates over
the graph and clones it into a copy with a condensed set of node ids.

This fix is less than ideal in that it results in a copy of the graph
which will potentially have performance implications, especially for
larger graphs. But after attempting to fix the VF2 implementation that
seems to be a more involved project than I originally hoped. This will
at least workaround the bug until a better more robust VF2
implementation can be written (and likely should be contributed back
upstream to petgraph).

Fixes #27
mtreinish added a commit to mtreinish/qiskit-core that referenced this issue Aug 28, 2020
The __eq__ method of the dagcircuit class was previously deep copying
the retworkx PyDAG objects for both self and other to force a reindexing
of the node indices in the graphs to acvoid a bug in retworkx (see
Qiskit/rustworkx#27 for more details). In the next retworkx a workaround
for that bug was introduced which removes the need for doing the deepcopy
calls. This commit does that and removes the deep copies which should
improve the performance of run __eq__ o dagcircuit objects.
kdk pushed a commit to Qiskit/qiskit that referenced this issue Sep 18, 2020
The __eq__ method of the dagcircuit class was previously deep copying
the retworkx PyDAG objects for both self and other to force a reindexing
of the node indices in the graphs to acvoid a bug in retworkx (see
Qiskit/rustworkx#27 for more details). In the next retworkx a workaround
for that bug was introduced which removes the need for doing the deepcopy
calls. This commit does that and removes the deep copies which should
improve the performance of run __eq__ o dagcircuit objects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants