Repository: quantumlib/Cirq

Issue:
Implement equality for CircuitDag
Implement `__eq__` for CircuitDag using `networkx.is_isomorphic()`.  Use the node_match argument of is_isomorphic: `node_match=lambda n: n.val`.

This may be useful for #830.


Discussion:

Resolving patch:
diff --git a/cirq/circuits/circuit_dag.py b/cirq/circuits/circuit_dag.py
--- a/cirq/circuits/circuit_dag.py
+++ b/cirq/circuits/circuit_dag.py
@@ -12,8 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from typing import Any, Callable, Generic, Iterator, TypeVar
+from typing import Any, Callable, Dict, Generic, Iterator, TypeVar
 
+import functools
 import networkx
 
 from cirq import ops, devices
@@ -22,6 +23,7 @@
 
 T = TypeVar('T')
 
+@functools.total_ordering
 class Unique(Generic[T]):
     """A wrapper for a value that doesn't compare equal to other instances.
 
@@ -38,6 +40,11 @@ def __init__(self, val: T) -> None:
     def __repr__(self):
         return 'Unique({}, {!r})'.format(id(self), self.val)
 
+    def __lt__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        return id(self) < id(other)
+
 
 def _disjoint_qubits(op1: ops.Operation, op2: ops.Operation) -> bool:
     """Returns true only if the operations have qubits in common."""
@@ -114,6 +121,24 @@ def append(self, op: ops.Operation) -> None:
                              if not self.can_reorder(node.val, new_node.val)])
         self.add_node(new_node)
 
+    def __eq__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        g1 = self.copy()
+        g2 = other.copy()
+        for node, attr in g1.nodes.items():
+            attr['val'] = node.val
+        for node, attr in g2.nodes.items():
+            attr['val'] = node.val
+        def node_match(attr1: Dict[Any, Any], attr2: Dict[Any, Any]) -> bool:
+            return attr1['val'] == attr2['val']
+        return networkx.is_isomorphic(g1, g2, node_match=node_match)
+
+    def __ne__(self, other):
+        return not self == other
+
+    __hash__ = None  # type: ignore
+
     def ordered_nodes(self) -> Iterator[Unique[ops.Operation]]:
         if not self.nodes:
             return


Test patch:
diff --git a/cirq/circuits/circuit_dag_test.py b/cirq/circuits/circuit_dag_test.py
--- a/cirq/circuits/circuit_dag_test.py
+++ b/cirq/circuits/circuit_dag_test.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
 import networkx
 
 import cirq
@@ -26,6 +28,29 @@ def test_wrapper_eq():
     eq.add_equality_group(cirq.CircuitDag.make_node(cirq.X(q1)))
 
 
+def test_wrapper_cmp():
+    u0 = cirq.Unique(0)
+    u1 = cirq.Unique(1)
+    # The ordering of Unique instances is unpredictable
+    u0, u1 = (u1, u0) if u1 < u0 else (u0, u1)
+    assert u0 == u0
+    assert u0 != u1
+    assert u0 <  u1
+    assert u1 >  u0
+    assert u0 <= u0
+    assert u0 <= u1
+    assert u0 >= u0
+    assert u1 >= u0
+
+
+@cirq.testing.only_test_in_python3
+def test_wrapper_cmp_failure():
+    with pytest.raises(TypeError):
+        _ = object() < cirq.Unique(1)
+    with pytest.raises(TypeError):
+        _ = cirq.Unique(1) < object()
+
+
 def test_wrapper_repr():
     q0 = cirq.LineQubit(0)
 
@@ -128,6 +153,57 @@ def test_to_circuit():
         atol=1e-7)
 
 
+def test_equality():
+    q0, q1 = cirq.LineQubit.range(2)
+    circuit1 = cirq.Circuit.from_ops(
+        cirq.X(q0),
+        cirq.Y(q0),
+        cirq.Z(q1),
+        cirq.CZ(q0, q1),
+        cirq.X(q1),
+        cirq.Y(q1),
+        cirq.Z(q0),
+    )
+    circuit2 = cirq.Circuit.from_ops(
+        cirq.Z(q1),
+        cirq.X(q0),
+        cirq.Y(q0),
+        cirq.CZ(q0, q1),
+        cirq.Z(q0),
+        cirq.X(q1),
+        cirq.Y(q1),
+    )
+    circuit3 = cirq.Circuit.from_ops(
+        cirq.X(q0),
+        cirq.Y(q0),
+        cirq.Z(q1),
+        cirq.CZ(q0, q1),
+        cirq.X(q1),
+        cirq.Y(q1),
+        cirq.Z(q0) ** 0.5,
+    )
+    circuit4 = cirq.Circuit.from_ops(
+        cirq.X(q0),
+        cirq.Y(q0),
+        cirq.Z(q1),
+        cirq.CZ(q0, q1),
+        cirq.X(q1),
+        cirq.Y(q1),
+    )
+
+    eq = cirq.testing.EqualsTester()
+    eq.make_equality_group(
+        lambda: cirq.CircuitDag.from_circuit(circuit1),
+        lambda: cirq.CircuitDag.from_circuit(circuit2),
+    )
+    eq.add_equality_group(
+        cirq.CircuitDag.from_circuit(circuit3),
+    )
+    eq.add_equality_group(
+        cirq.CircuitDag.from_circuit(circuit4),
+    )
+
+
 def test_larger_circuit():
     q0, q1, q2, q3 = cirq.google.Bristlecone.col(5)[:4]
     # This circuit does not have CZ gates on adjacent qubits because the order