Repository: quantumlib/Cirq

Issue:
ControlledGate equality seems too loose
**Description of the issue**

Equality in controlled gate is implemented as follows, intentionally ignoring the order of the controls.

```python
    def _value_equality_values_(self):
        return (
            self.sub_gate,
            self.num_controls(),
            frozenset(zip(self.control_values, self.control_qid_shape)),
        )
```

This means gates are considered equal even if their unitaries don't match.

```python
def test_me():
    g0 = cirq.ControlledGate(cirq.Z, control_values=[[1], [0]])
    g1 = cirq.ControlledGate(cirq.Z, control_values=[[0], [1]])
    assert g0 == g1  # passes
    assert np.allclose(cirq.unitary(g0), cirq.unitary(g1))  # fails
```

In some cases the gate can't even be applied to the same qubits.

```python
def test_me():
    g0 = cirq.ControlledGate(cirq.Z, control_values=[[1], [1, 2]], control_qid_shape=[2, 3])
    g1 = cirq.ControlledGate(cirq.Z, control_values=[[1, 2], [1]], control_qid_shape=[3, 2])
    assert g0 == g1  # passes
    qubits = cirq.LineQid.for_qid_shape((2, 3, 2))
    op0 = g0.on(*qubits)
    op1 = g1.on(*qubits)  # raises "Wrong shape of qids"
    assert op0 == op1
```

So, while there's a relationship between such gates, calling that relationship "equality" doesn't feel right. Was this intentional?

**Cirq version**
0.14.0.dev



Discussion:
I think I see the reason. ControlledOperation is defined in the same way, but it makes a bit more sense in that case. The qubits are applied so the operation does the same thing regardless of order. So, if two operations do the same thing then we call it equal. If two operations are equal, then their gates must be equal.

Not sure if we should consider the two operations equal though. There's certainly an equivalence relation, but the two operations `with_qubits(*qubits)` on the same `qubits` would produce different operations. So they're really not "equal" I'd think. Thoughts?
Cynq notes:  Change this in the gate, but continue to allow operations to ignore ordering. This will be consistent with the behavior of `DensePauliString` vs `PauliString`.

* linking #1179

To the 2nd paragraph of the above comment, there are examples of other things in Python where `x==y` does not imply `f(x)==f(y)`, for example `{1:1,2:2}=={2:2,1:1}` but `list({1:1,2:2}) != list({2:2,1:1})`. So it seems okay that we can specify that two operations are equal even if their underlying gates are not equal.

Resolving patch:
diff --git a/cirq-core/cirq/ops/controlled_gate.py b/cirq-core/cirq/ops/controlled_gate.py
--- a/cirq-core/cirq/ops/controlled_gate.py
+++ b/cirq-core/cirq/ops/controlled_gate.py
@@ -226,7 +226,8 @@ def _value_equality_values_(self):
         return (
             self.sub_gate,
             self.num_controls(),
-            frozenset(zip(self.control_values, self.control_qid_shape)),
+            self.control_values,
+            self.control_qid_shape,
         )
 
     def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> np.ndarray:


Test patch:
diff --git a/cirq-core/cirq/ops/common_gates_test.py b/cirq-core/cirq/ops/common_gates_test.py
--- a/cirq-core/cirq/ops/common_gates_test.py
+++ b/cirq-core/cirq/ops/common_gates_test.py
@@ -158,7 +158,7 @@ def test_specialized_control(input_gate, specialized_output):
     )
     assert input_gate.controlled(control_qid_shape=(2,)).controlled(
         control_qid_shape=(3,)
-    ).controlled(control_qid_shape=(4,)) == specialized_output.controlled(
+    ).controlled(control_qid_shape=(4,)) != specialized_output.controlled(
         num_controls=2, control_qid_shape=(3, 4)
     )
 
@@ -175,7 +175,7 @@ def test_specialized_control(input_gate, specialized_output):
     )
     assert input_gate.controlled(control_qid_shape=(3,)).controlled(
         control_qid_shape=(2,)
-    ).controlled(control_qid_shape=(4,)) == cirq.ControlledGate(
+    ).controlled(control_qid_shape=(4,)) != cirq.ControlledGate(
         input_gate, num_controls=3, control_qid_shape=(3, 2, 4)
     )
 
diff --git a/cirq-core/cirq/ops/controlled_gate_test.py b/cirq-core/cirq/ops/controlled_gate_test.py
--- a/cirq-core/cirq/ops/controlled_gate_test.py
+++ b/cirq-core/cirq/ops/controlled_gate_test.py
@@ -223,16 +223,29 @@ def test_eq():
     eq.add_equality_group(cirq.X)
     eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[1, (0, 2)], control_qid_shape=[2, 3]),
+        cirq.ControlledGate(cirq.H, control_values=(1, [0, 2]), control_qid_shape=(2, 3)),
+    )
+    eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[(2, 0), 1], control_qid_shape=[3, 2]),
     )
     eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[1, 0], control_qid_shape=[2, 3]),
+        cirq.ControlledGate(cirq.H, control_values=(1, 0), control_qid_shape=(2, 3)),
+    )
+    eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[0, 1], control_qid_shape=[3, 2]),
     )
     eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[1, 0]),
+        cirq.ControlledGate(cirq.H, control_values=(1, 0)),
+    )
+    eq.add_equality_group(
         cirq.ControlledGate(cirq.H, control_values=[0, 1]),
     )
+    for group in eq._groups:
+        if isinstance(group[0], cirq.Gate):
+            for item in group:
+                np.testing.assert_allclose(cirq.unitary(item), cirq.unitary(group[0]))
 
 
 def test_control():
@@ -266,11 +279,17 @@ def test_control():
     eq.add_equality_group(
         cirq.ControlledGate(g, control_values=[0, 1]),
         g.controlled(control_values=[0, 1]),
+        g.controlled(control_values=[1]).controlled(control_values=[0]),
+    )
+    eq.add_equality_group(
         g.controlled(control_values=[0]).controlled(control_values=[1]),
     )
     eq.add_equality_group(
         cirq.ControlledGate(g, control_qid_shape=[4, 3]),
         g.controlled(control_qid_shape=[4, 3]),
+        g.controlled(control_qid_shape=[3]).controlled(control_qid_shape=[4]),
+    )
+    eq.add_equality_group(
         g.controlled(control_qid_shape=[4]).controlled(control_qid_shape=[3]),
     )