Repository: quantumlib/Cirq

Issue:
`ClassicallyControlledOperation` breaks assumption that disjoint operations commute.
### Description of the issue
`ClassicallyControlledOperation` was recently introduced to enable classical controls in Cirq. The method takes as input a `sub_operation` and a list of `conditions`, which correspond to measurement keys of previous measurements. 

https://github.com/quantumlib/Cirq/blob/6119620178d7d1d0f11c5f2b73998fb2b66fcd2d/cirq-core/cirq/ops/classically_controlled_operation.py#L49-L53

The operations is implemented in a way that the qubit set it acts on remains the same as the set of qubits of the underlying operation to be controlled, and the operation "assumes" that measurements corresponding to the conditions have already occurred to the left of the operation. 

**This is a big problem for transformers because this introduces an implicit dependency between a measurement operation and a classically controlled operation which makes use of these measurement results.** 

### How to reproduce the issue

```python
>>> import cirq
>>> q = cirq.LineQubit.range(2)
>>> c = cirq.Circuit(cirq.H(q[0]), cirq.measure(q[0], key='m'), cirq.X(q[1]).with_classical_controls('m'))
>>> print(c)
0: ───H───M───────
          ║
1: ───────╫───X───
          ║   ║
m: ═══════@═══^═══
>>> cirq.SynchronizeTerminalMeasurements().optimize_circuit(c)
>>> print(c)
0: ───H───────────M───
                  ║
1: ───────────X───╫───
              ║   ║
m: ═══════════^═══@═══
>>> cirq.Simulator().simulate(c)
ValueError: Measurement key m missing when testing classical control
```

**Cirq version**
```
0.14.0.dev
```

cc @95-martin-orion @daxfohl 
Fix `circuit.append()` to use `protocols.commute` to verify if two operations can be "crossed" or not.
**Description of the issue**
Since #4870 breaks the assumption that two operations acting on disjoint qubits always commute, `circuit.append()` should use `protocols.commute` to find the leftmost moment in which an operation can be inserted. 

As part of the work done for `CCO`, this was implemented by adding a special case of checking whether the current operation is a CCO and if yes, whether the previous moment has a measurement with matching control keys. 

https://github.com/quantumlib/Cirq/blob/90679d27e0967c0a7fe59848e789a5d6ba49b66d/cirq-core/cirq/circuits/circuit.py#L1972-L1975

This is problematic because it works only in one direction and breaks optimizers which relies on doing operations on the circuit in reverse order. 

**How to reproduce the issue**

```python
>>> import cirq
>>> q =  cirq.LineQubit.range(2)
>>> c = cirq.Circuit(cirq.Moment(cirq.X(q[0])), cirq.Moment(cirq.measure(q[1], key="m")), cirq.Moment(cirq.X(q[0]).with_classical_controls("m")))
>>> print(c)
0: ───X───────X───
              ║
1: ───────M───╫───
          ║   ║
m: ═══════@═══^═══
>>> cirq.AlignLeft().optimize_circuit(c) # Works as expected.
0: ───X───X───
          ║
1: ───M───╫───
      ║   ║
m: ═══@═══^═══
>>> cirq.AlignRight().optimize_circuit(c) # Does not work as expected because implementation depends on inserting operations in the reverse circuit.
>>> print(c) # Wrong output
          ┌──┐
0: ───X────X─────
           ║
1: ────────╫M────
           ║║
m: ════════^@════
          └──┘

```

**Cirq version**
0.14.0.dev



Discussion:
Marking this as high priority as this has potential to impact the design of either transformers or classically controlled operations. 

My main question is: Can we make `ClassicallyControlledOperation` act on `sub_operation.qubits` + qubits corresponding to previous measurements? 

If not, we need to find another way to capture this implicit dependency for transformers. 
We already have logic in circuit.append to ensure that when you append a control, it does not fall back beyond the measurement that creates the key. https://github.com/quantumlib/Cirq/blob/39ce4f0e7c79479913491f9b434461c9397eb677/cirq-core/cirq/circuits/circuit.py#L1967

I don't think it would be good for usability if we require users to state which qubits correspond to the measurement key when instantiating the classically controlled operation. *Maybe* there'd be some way to hack this in such that there's a lookup somewhere that we can get the qubits corresponding to the key, and can avoid the user needing to specify it manually, but nothing comes to mind.  Also the latter solution won't work once we allow repeated measurement keys, as each measurement could correspond to different qubits (potentially, depending on the design).

So, I think the best solution is to reuse the logic from Circuit.append if possible.
I think Dax has the right idea - this will require some deeper awareness of measure/control keys in optimizers (possibly handled by a new protocol?) but it shouldn't affect anything which doesn't make use of CCOps.
Note that CCO is currently also broken with the `commutes` protocol, which is even worse and should be fixed (we can probably fix this by overriding the `__commutes__` protocol.). 

In the code snippet above, 
```python
>>> cirq.commutes(c[1][q[0]], c[2][q[1]]) # checking if measurement and CCO X gate commute. Should have returned False. 
True # Should have returned False.
```

Also, just to reiterate my concerns, I think the problem is that the mentality of assuming operations commute if they don't share qubits is very deeply ingrained in Cirq code (and I assume also in Cirq users). This would be a sudden and unexpected change against that mentality, and I'd very much like if we can somehow reduce the impact. 

Another example which breaks:
```python
>>> print(c)
0: ───H───M───────
          ║
1: ───────╫───X───
          ║   ║
m: ═══════@═══^═══
>>> cirq.AlignRight()(c)
>>> print(c)
          ┌──┐
0: ───H─────M────
            ║
1: ────────X╫────
           ║║
m: ════════^@════
          └──┘
```
Though I agree that including control qubits in the CCO operation is also not a good idea because it can then lead to other bigger issues.  For example, it introduces an additional barrier which takes away flexibility of moving the controlled operation around. So, in the following circuit it wouldn't be possible to put `H` and CCO `X` in the same moment because they'd have overlapping qubits if we included qubit 0 in CCO `X`. 

```python
0: ───H───M───H───
          ║
1: ───────╫───X───
          ║   ║
m: ═══════@═══^═══
```
This also inspires an explicit `CircuitDAG` representation where it's easier to add such indirect dependencies between the nodes of the DAG. 
There's a protocol `measurement_keys_touched` that could be used the same way as qubits for `commutes`; we just return False if two ops touch the same measurement keys. It's slightly suboptimal because controlled ops that check the same measurement key *should* be able to commute--they should should be able to commute with the corresponding measurement gate. But as a simple stopgap it should work.


Resolving patch:
diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py
--- a/cirq-core/cirq/circuits/circuit.py
+++ b/cirq-core/cirq/circuits/circuit.py
@@ -1952,12 +1952,17 @@ def _prev_moment_available(self, op: 'cirq.Operation', end_moment_index: int) ->
         last_available = end_moment_index
         k = end_moment_index
         op_control_keys = protocols.control_keys(op)
+        op_measurement_keys = protocols.measurement_key_objs(op)
         op_qubits = op.qubits
         while k > 0:
             k -= 1
             moment = self._moments[k]
-            if moment.operates_on(op_qubits) or (
-                op_control_keys & protocols.measurement_key_objs(moment)
+            # This should also validate that measurement keys are disjoint once we allow repeated
+            # measurements. Search for same message in raw_types.py.
+            if (
+                moment.operates_on(op_qubits)
+                or not op_control_keys.isdisjoint(protocols.measurement_key_objs(moment))
+                or not protocols.control_keys(moment).isdisjoint(op_measurement_keys)
             ):
                 return last_available
             if self._can_add_op_at(k, op):
diff --git a/cirq-core/cirq/ops/moment.py b/cirq-core/cirq/ops/moment.py
--- a/cirq-core/cirq/ops/moment.py
+++ b/cirq-core/cirq/ops/moment.py
@@ -103,6 +103,7 @@ def __init__(self, *contents: 'cirq.OP_TREE') -> None:
 
         self._qubits = frozenset(self._qubit_to_op.keys())
         self._measurement_key_objs: Optional[AbstractSet['cirq.MeasurementKey']] = None
+        self._control_keys: Optional[FrozenSet['cirq.MeasurementKey']] = None
 
     @property
     def operations(self) -> Tuple['cirq.Operation', ...]:
@@ -239,6 +240,13 @@ def _measurement_key_objs_(self) -> AbstractSet['cirq.MeasurementKey']:
             }
         return self._measurement_key_objs
 
+    def _control_keys_(self) -> FrozenSet['cirq.MeasurementKey']:
+        if self._control_keys is None:
+            self._control_keys = frozenset(
+                k for op in self.operations for k in protocols.control_keys(op)
+            )
+        return self._control_keys
+
     def _with_key_path_(self, path: Tuple[str, ...]):
         return Moment(
             protocols.with_key_path(op, path) if protocols.is_measurement(op) else op
diff --git a/cirq-core/cirq/ops/raw_types.py b/cirq-core/cirq/ops/raw_types.py
--- a/cirq-core/cirq/ops/raw_types.py
+++ b/cirq-core/cirq/ops/raw_types.py
@@ -572,13 +572,26 @@ def _commutes_(
         if not isinstance(other, Operation):
             return NotImplemented
 
+        # This should also validate that measurement keys are disjoint once we allow repeated
+        # measurements. Search for same message in circuit.py.
+        if not protocols.control_keys(self).isdisjoint(
+            protocols.measurement_key_objs(other)
+        ) or not protocols.control_keys(other).isdisjoint(protocols.measurement_key_objs(self)):
+            return False
+
         if hasattr(other, 'qubits') and set(self.qubits).isdisjoint(other.qubits):
             return True
 
         from cirq import circuits
 
-        circuit12 = circuits.Circuit(self, other)
-        circuit21 = circuits.Circuit(other, self)
+        # Remove the classical controls to validate the quantum commutativity. This can be done
+        # because during execution, the two operations will either both be run, in which case they
+        # behave like the suboperations, so if the suboperations commute then these commute. Or
+        # one of them is cold in which case it behaves like the identity, which always commutes.
+        self_raw = self.without_classical_controls()
+        other_raw = other.without_classical_controls()
+        circuit12 = circuits.Circuit(self_raw, other_raw)
+        circuit21 = circuits.Circuit(other_raw, self_raw)
 
         # Don't create gigantic matrices.
         shape = protocols.qid_shape_protocol.qid_shape(circuit12)


Test patch:
diff --git a/cirq-core/cirq/ops/classically_controlled_operation_test.py b/cirq-core/cirq/ops/classically_controlled_operation_test.py
--- a/cirq-core/cirq/ops/classically_controlled_operation_test.py
+++ b/cirq-core/cirq/ops/classically_controlled_operation_test.py
@@ -813,3 +813,20 @@ def test_sympy_scope_simulation():
         assert result.measurements['0:0:m_result'][0][0] == (
             bits[0] and bits[1] or bits[2] and bits[3]  # bits[4] irrelevant
         )
+
+
+def test_commutes():
+    q0, q1 = cirq.LineQubit.range(2)
+    assert cirq.commutes(cirq.measure(q0, key='a'), cirq.X(q1).with_classical_controls('b'))
+    assert cirq.commutes(cirq.X(q1).with_classical_controls('b'), cirq.measure(q0, key='a'))
+    assert cirq.commutes(
+        cirq.X(q0).with_classical_controls('a'), cirq.H(q1).with_classical_controls('a')
+    )
+    assert cirq.commutes(
+        cirq.X(q0).with_classical_controls('a'), cirq.X(q0).with_classical_controls('a')
+    )
+    assert not cirq.commutes(cirq.measure(q0, key='a'), cirq.X(q1).with_classical_controls('a'))
+    assert not cirq.commutes(cirq.X(q1).with_classical_controls('a'), cirq.measure(q0, key='a'))
+    assert not cirq.commutes(
+        cirq.X(q0).with_classical_controls('a'), cirq.H(q0).with_classical_controls('a')
+    )
diff --git a/cirq-core/cirq/transformers/align_test.py b/cirq-core/cirq/transformers/align_test.py
--- a/cirq-core/cirq/transformers/align_test.py
+++ b/cirq-core/cirq/transformers/align_test.py
@@ -97,3 +97,12 @@ def test_align_right_no_compile_context():
             ]
         ),
     )
+
+
+def test_classical_control():
+    q0, q1 = cirq.LineQubit.range(2)
+    circuit = cirq.Circuit(
+        cirq.H(q0), cirq.measure(q0, key='m'), cirq.X(q1).with_classical_controls('m')
+    )
+    cirq.testing.assert_same_circuits(cirq.align_left(circuit), circuit)
+    cirq.testing.assert_same_circuits(cirq.align_right(circuit), circuit)