Repository: quantumlib/Cirq

Issue:
router mishandling mid-circuit measurements
**Description of the issue**

`cirq.RouteCQC` will sometimes commute gates through mid-circuit measurements in a way that changes the expected outcome

**How to reproduce the issue**

```python
q0, q1, q2 = cirq.LineQubit.range(3)

circuit = cirq.Circuit(
    cirq.CZ(q0, q1),
    cirq.CZ(q1, q2),
    cirq.CZ(q0, q2),
    cirq.measure(q0, q1, q2, key="key"),
    cirq.H.on_each(q0, q1, q2),
    cirq.measure_each(q0, q21, q2),
)

graph = nx.Graph([[q0, q1], [q1, q2]])
router = cirq.RouteCQC(graph)

print(circuit)
print(router.route_circuit(circuit)[0])
```
prints
```
0: ───@───────@───M('key')───H───M───
      │       │   │
1: ───@───@───┼───M──────────H───M───
          │   │   │
2: ───────@───@───M──────────H───M───
0: ───×───────@───────────────@───M──────────H───M('q(2)')───
      │       │               │   │
1: ───×───@───@───H───M───×───@───M('key')───H───M('q(0)')───
          │               │       │
2: ───────@───────────────×───────M──────────────────────────
```
where one of the hadamards and one of the terminal measurement have incorrectly been commuted through the mid-circuit measurement


**Cirq version**

```
1.3.0.dev20230830191034
```




Discussion:
I can look into this issue and try to reproduce it.
was able to reproduce the issue, gonna start looking through `RouteCQC` class and test. 
It seems like the routing algorithm is sometimes reordering gates in a quantum circuit in a way that they are incorrectly placed before or after measurements, leading to discrepancies between the expected and actual outcomes of the circuit. New to opensource contribution so please let me know if I am misunderstanding the issue.

I am going to start looking at which stage of gates we start seeing a mismatch between the actual and expected circuit.




Resolving patch:
diff --git a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py
--- a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py
+++ b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py
@@ -15,7 +15,7 @@
 """Heuristic qubit routing algorithm based on arxiv:1902.08091."""
 
 from typing import Any, Dict, List, Optional, Set, Sequence, Tuple, TYPE_CHECKING
-from itertools import combinations
+import itertools
 import networkx as nx
 
 from cirq import circuits, ops, protocols
@@ -48,7 +48,9 @@ def _disjoint_nc2_combinations(
     Returns:
         All 2-combinations between qubit pairs that are disjoint.
     """
-    return [pair for pair in combinations(qubit_pairs, 2) if set(pair[0]).isdisjoint(pair[1])]
+    return [
+        pair for pair in itertools.combinations(qubit_pairs, 2) if set(pair[0]).isdisjoint(pair[1])
+    ]
 
 
 @transformer_api.transformer
@@ -245,9 +247,20 @@ def _get_one_and_two_qubit_ops_as_timesteps(
         The i'th entry in the nested two-qubit and single-qubit ops correspond to the two-qubit
         gates and single-qubit gates of the i'th timesteps respectively. When constructing the
         output routed circuit, single-qubit operations are inserted before two-qubit operations.
+
+        Raises:
+            ValueError: if circuit has intermediate measurement op's that act on 3 or more qubits.
         """
         two_qubit_circuit = circuits.Circuit()
         single_qubit_ops: List[List[cirq.Operation]] = []
+
+        if any(
+            protocols.num_qubits(op) > 2 and protocols.is_measurement(op)
+            for op in itertools.chain(*circuit.moments[:-1])
+        ):
+            # There is at least one non-terminal measurement on 3+ qubits
+            raise ValueError('Non-terminal measurements on three or more qubits are not supported')
+
         for moment in circuit:
             for op in moment:
                 timestep = two_qubit_circuit.earliest_available_moment(op)
@@ -255,7 +268,7 @@ def _get_one_and_two_qubit_ops_as_timesteps(
                 two_qubit_circuit.append(
                     circuits.Moment() for _ in range(timestep + 1 - len(two_qubit_circuit))
                 )
-                if protocols.num_qubits(op) == 2 and not protocols.is_measurement(op):
+                if protocols.num_qubits(op) == 2:
                     two_qubit_circuit[timestep] = two_qubit_circuit[timestep].with_operation(op)
                 else:
                     single_qubit_ops[timestep].append(op)


Test patch:
diff --git a/cirq-core/cirq/transformers/routing/route_circuit_cqc_test.py b/cirq-core/cirq/transformers/routing/route_circuit_cqc_test.py
--- a/cirq-core/cirq/transformers/routing/route_circuit_cqc_test.py
+++ b/cirq-core/cirq/transformers/routing/route_circuit_cqc_test.py
@@ -107,6 +107,38 @@ def test_circuit_with_measurement_gates():
     cirq.testing.assert_same_circuits(routed_circuit, circuit)
 
 
+def test_circuit_with_valid_intermediate_multi_qubit_measurement_gates():
+    device = cirq.testing.construct_ring_device(3)
+    device_graph = device.metadata.nx_graph
+    router = cirq.RouteCQC(device_graph)
+    q = cirq.LineQubit.range(2)
+    hard_coded_mapper = cirq.HardCodedInitialMapper({q[i]: q[i] for i in range(2)})
+
+    valid_circuit = cirq.Circuit(cirq.measure_each(*q), cirq.H.on_each(q))
+
+    c_routed = router(
+        valid_circuit, initial_mapper=hard_coded_mapper, context=cirq.TransformerContext(deep=True)
+    )
+    device.validate_circuit(c_routed)
+
+
+def test_circuit_with_invalid_intermediate_multi_qubit_measurement_gates():
+    device = cirq.testing.construct_ring_device(3)
+    device_graph = device.metadata.nx_graph
+    router = cirq.RouteCQC(device_graph)
+    q = cirq.LineQubit.range(3)
+    hard_coded_mapper = cirq.HardCodedInitialMapper({q[i]: q[i] for i in range(3)})
+
+    invalid_circuit = cirq.Circuit(cirq.MeasurementGate(3).on(*q), cirq.H.on_each(*q))
+
+    with pytest.raises(ValueError):
+        _ = router(
+            invalid_circuit,
+            initial_mapper=hard_coded_mapper,
+            context=cirq.TransformerContext(deep=True),
+        )
+
+
 def test_circuit_with_non_unitary_and_global_phase():
     device = cirq.testing.construct_ring_device(4)
     device_graph = device.metadata.nx_graph