Repository: quantumlib/Cirq

Issue:
Added Optimization to move terminal measurements to final moment.
fixes #2141 .


Discussion:

Resolving patch:
diff --git a/cirq/__init__.py b/cirq/__init__.py
--- a/cirq/__init__.py
+++ b/cirq/__init__.py
@@ -269,6 +269,7 @@
     single_qubit_matrix_to_pauli_rotations,
     single_qubit_matrix_to_phased_x_z,
     single_qubit_op_to_framed_phase_form,
+    SynchronizeTerminalMeasurements,
     two_qubit_matrix_to_operations,
 )
 
diff --git a/cirq/optimizers/__init__.py b/cirq/optimizers/__init__.py
--- a/cirq/optimizers/__init__.py
+++ b/cirq/optimizers/__init__.py
@@ -48,5 +48,8 @@
     single_qubit_op_to_framed_phase_form,
 )
 
+from cirq.optimizers.synchronize_terminal_measurements import (
+    SynchronizeTerminalMeasurements,)
+
 from cirq.optimizers.two_qubit_decompositions import (
     two_qubit_matrix_to_operations,)
diff --git a/cirq/optimizers/synchronize_terminal_measurements.py b/cirq/optimizers/synchronize_terminal_measurements.py
new file mode 100644
--- /dev/null
+++ b/cirq/optimizers/synchronize_terminal_measurements.py
@@ -0,0 +1,65 @@
+# Copyright 2018 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""An optimization pass to put as many measurements possible at the end."""
+
+from typing import List, Set, Tuple, cast
+from cirq import circuits, ops, protocols
+
+
+class SynchronizeTerminalMeasurements():
+    """Move measurements to the end of the circuit.
+
+    Move all measurements in a circuit to the final moment if it can accomodate
+    them (without overlapping with other operations). If
+    self._after_other_operations is true then a new moment will be added to the
+    end of the circuit containing all the measurements that should be brought
+    forward.
+    """
+
+    def __init__(self, after_other_operations: bool = True):
+        """
+        Args:
+            after_other_operations: Set by default. If the circuit's final
+                moment contains non-measurement operations and this is set then
+                a new empty moment is appended to the circuit before pushing
+                measurements to the end.
+        """
+        self._after_other_operations = after_other_operations
+
+    def __call__(self, circuit: circuits.Circuit):
+        self.optimize_circuit(circuit)
+
+    def optimize_circuit(self, circuit: circuits.Circuit) -> None:
+        deletions: List[Tuple[int, ops.Operation]] = []
+        terminal_measures: Set[ops.Operation] = set()
+        qubits = circuit.all_qubits()
+        for qubit in qubits:
+            moment_index = cast(int, circuit.prev_moment_operating_on((qubit,)))
+            op = cast(ops.Operation, circuit.operation_at(qubit, moment_index))
+            if protocols.is_measurement(op):
+                deletions.append((moment_index, op))
+                terminal_measures.add(op)
+
+        if not deletions:
+            return
+
+        circuit.batch_remove(deletions)
+        if circuit[-1] and self._after_other_operations:
+            # Can safely add to the end if
+            # self._after_other_operations is false or we happen to get an
+            # empty final moment before re-adding all the measurements.
+            circuit.append(ops.Moment())
+
+        for op in terminal_measures:
+            circuit[-1] = circuit[-1].with_operation(op)


Test patch:
diff --git a/cirq/optimizers/synchronize_terminal_measurements_test.py b/cirq/optimizers/synchronize_terminal_measurements_test.py
new file mode 100644
--- /dev/null
+++ b/cirq/optimizers/synchronize_terminal_measurements_test.py
@@ -0,0 +1,150 @@
+# Copyright 2018 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cirq
+
+
+def assert_optimizes(before, after, measure_only_moment=True):
+    opt = cirq.SynchronizeTerminalMeasurements(measure_only_moment)
+    opt(before)
+    assert before == after
+
+
+def test_no_move():
+    q1 = cirq.NamedQubit('q1')
+    before = cirq.Circuit([cirq.Moment([cirq.H(q1)])])
+    after = before
+    assert_optimizes(before=before, after=after)
+
+
+def test_simple_align():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.measure(q2)])
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.Z(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.measure(q2)])
+    ])
+    assert_optimizes(before=before, after=after)
+
+
+def test_simple_partial_align():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.Z(q1), cirq.measure(q2)]),
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.Z(q1)]),
+        cirq.Moment([cirq.measure(q2)])
+    ])
+    assert_optimizes(before=before, after=after)
+
+
+def test_slide_forward_one():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    q3 = cirq.NamedQubit('q3')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.measure(q2),
+                     cirq.measure(q3)]),
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1)]),
+        cirq.Moment([cirq.measure(q2), cirq.measure(q3)])
+    ])
+    assert_optimizes(before=before, after=after)
+
+
+def test_no_slide_forward_one():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    q3 = cirq.NamedQubit('q3')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.measure(q2),
+                     cirq.measure(q3)]),
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.measure(q2),
+                     cirq.measure(q3)]),
+    ])
+    assert_optimizes(before=before, after=after, measure_only_moment=False)
+
+
+def test_blocked_shift_one():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1), cirq.measure(q2)])
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1)]),
+        cirq.Moment([cirq.measure(q2)])
+    ])
+    assert_optimizes(before=before, after=after)
+
+
+def test_complex_move():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    q3 = cirq.NamedQubit('q3')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1), cirq.measure(q2)]),
+        cirq.Moment([cirq.H(q3)]),
+        cirq.Moment([cirq.X(q1), cirq.measure(q3)])
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1)]),
+        cirq.Moment([cirq.H(q3)]),
+        cirq.Moment([cirq.X(q1)]),
+        cirq.Moment([cirq.measure(q2), cirq.measure(q3)])
+    ])
+    assert_optimizes(before=before, after=after)
+
+
+def test_complex_move_no_slide():
+    q1 = cirq.NamedQubit('q1')
+    q2 = cirq.NamedQubit('q2')
+    q3 = cirq.NamedQubit('q3')
+    before = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1), cirq.measure(q2)]),
+        cirq.Moment([cirq.H(q3)]),
+        cirq.Moment([cirq.X(q1), cirq.measure(q3)])
+    ])
+    after = cirq.Circuit([
+        cirq.Moment([cirq.H(q1), cirq.H(q2)]),
+        cirq.Moment([cirq.measure(q1), cirq.Z(q2)]),
+        cirq.Moment([cirq.H(q1)]),
+        cirq.Moment([cirq.H(q3)]),
+        cirq.Moment([cirq.X(q1), cirq.measure(q2),
+                     cirq.measure(q3)])
+    ])
+    assert_optimizes(before=before, after=after, measure_only_moment=False)
diff --git a/cirq/protocols/json_test.py b/cirq/protocols/json_test.py
--- a/cirq/protocols/json_test.py
+++ b/cirq/protocols/json_test.py
@@ -287,6 +287,7 @@ def test_fail_to_resolve():
     'MergeInteractions',
     'MergeSingleQubitGates',
     'PointOptimizer',
+    'SynchronizeTerminalMeasurements',
 
     # global objects
     'CONTROL_TAG',