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

Implementing CouplingMap.__eq__ #9766

Merged
merged 15 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
13 changes: 13 additions & 0 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ def __str__(self):
string += "]"
return string

def __eq__(self, other):
"""Check if the graph in other is isomorphic to the graph in self.

Args:
other (CouplingMap): The other coupling map.

Returns:
bool: Whether or not other is isomorphic to self.
"""
if not isinstance(other, CouplingMap):
return False
return rx.is_isomorphic(other.graph, self.graph)
DanPuzzuoli marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Something I was thinking about overnight is whether this is actually the behavior we want on __eq__ or not. An isomorphic graph is structurally equivalent, but that might not be what people expect with equals. I know For example, the case I was thinking of was if you had two coupling maps:

cmap_a = CouplingMap([[0, 1], [0, 2], [2, 3], [1, 3], [4, 5], [5, 6], [6, 7], [7, 0]])
cmap_b = CouplingMap([[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [4, 6], [6, 7], [5, 7]])

with this PR cmap_a == cmap_b would be True, but would the normal expectation be for that to be False?
I'm wondering if changing this to be a comparison of the edge lists instead of isomorphism would be a better __eq__ definition. If so we can add an equiv() or isomorphic() method to capture this use case too.
I know that I was the one who suggested this approach for __eq__ originally, but do you have any thoughts here?

Copy link
Member

Choose a reason for hiding this comment

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

I agree with this - I think the expectation is that __eq__ should be quite strict. Especially because the CouplingMap implicitly relates physical qubits to their connections - if I have cm1 == cm2 and cm1 has an edge between physical qubits 0 and 1, I'd expect to be able to interact physical qubits 0 and 1 on the machine backed by cm2 too, but if we relax __eq__ to isomorphism, that won't be the case.

imo, especially from my pain dealing with other places in Qiskit where this isn't the case, __eq__ should be a fairly strict but cheap method wherever possible.

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 agree with the point that node labelling matters here. @mtreinish you're suggesting edge list comparison because it uses the actual labels? I guess simplest thing would be to convert edge lists to sets and check equality of sets. Are there any ordering guarantees on the edge lists, or is there no way around something like the set comparison?

Copy link
Contributor Author

@DanPuzzuoli DanPuzzuoli Mar 10, 2023

Choose a reason for hiding this comment

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

Or I guess:

len(list0) == len(list1) and all(x in list1 for x in list0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are the edge lists being equal sufficient? Could there be disconnected nodes to worry about?

Copy link
Member

Choose a reason for hiding this comment

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

Ah that's a good point, in it's current form an edge list comparison is fine because the graph needs to be connected. But after #9710 this won't be the case and there can be disconnected nodes in the graph. But I assume this will merge before #9710 so lets assume we don't have to worry about disconnected nodes for this PR and I'll update __eq__ in #9710 after this merges.

As for the edge list order I think you're correct that wrapping it in a set is probably more robust to ignore edge order. The returned edges in PyDiGraph.edge_list() are always in insertion/creation order, but that could differ between graphs that I think should should be ==. So we should do the comparison in an order agnostic way.


def draw(self):
"""Draws the coupling map.

Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/coupling-map-eq-b0507b703d62a5f3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
upgrade:
- |
The :meth:`.CouplingMap.__eq__`` method has been updated to check isomorphism of the underlying
graphs. Any code using ``CouplingMap() == CouplingMap()`` to check object equality should be
updated to ``CouplingMap() is CouplingMap()``.
16 changes: 16 additions & 0 deletions test/python/transpiler/test_coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,22 @@ def test_implements_iter(self):
expected = [(0, 1), (1, 0), (1, 2), (2, 1)]
self.assertEqual(sorted(coupling), expected)

def test_equality(self):
"""Test that equality is based on graph isomorphism."""

# create two coupling maps with 4 nodes and the same edges
coupling0 = CouplingMap([(0, 1), (0, 2), (2, 3)])
coupling1 = CouplingMap([(0, 1), (0, 2), (2, 3)])

# create an additional coupling map with 5 nodes not equal to the previous 2
coupling2 = CouplingMap([(0, 1), (0, 2), (2, 4)])
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 maybe having a comment here to say this will create a 5 node coupling map with 3 disconnected (it might not be obvious from a quick glance).


self.assertTrue(coupling0 == coupling1)
self.assertTrue(coupling0 != coupling2)

# additional test for comparison to a non-CouplingMap object
self.assertTrue(coupling0 != 1)
DanPuzzuoli marked this conversation as resolved.
Show resolved Hide resolved


class CouplingVisualizationTest(QiskitVisualizationTestCase):
@unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed")
Expand Down