Repository: quantumlib/Cirq

Issue:
Changing qubits in PointOptimizer causes ValueError
The cirq.PointOptimizer class assumes that an optimizer won't change qubits (or at least won't change qubits in a way that pushes the new gate backwards).   If two gates are modified in the same moment to use the same qubits, a ValueError is raised.

A check similar to https://github.com/quantumlib/Cirq/blob/master/cirq/circuits/optimization_pass.py#L128
needs to be added at or around Line 148 to detect this condition and adjust the 'i' index or frontier accordingly.

A minimal example that causes the issue is below.
```
class EverythingIs42(cirq.PointOptimizer):
      def optimization_at(self, circuit, index, op):
        if len(op.qubits) == 1:
          gate = op.gate
          new_op = op.gate(cirq.LineQubit(42))
          return cirq.PointOptimizationSummary(clear_span=1,
                                             clear_qubits=op.qubits,
                                             new_operations=new_op)

c=cirq.Circuit(cirq.X(cirq.LineQubit(0)), cirq.X(cirq.LineQubit(1)))
EverythingIs42().optimize_circuit(c)
```


Discussion:

Resolving patch:
diff --git a/cirq/circuits/optimization_pass.py b/cirq/circuits/optimization_pass.py
--- a/cirq/circuits/optimization_pass.py
+++ b/cirq/circuits/optimization_pass.py
@@ -145,6 +145,18 @@ def optimize_circuit(self, circuit: Circuit):
                     [e for e in range(i, i + opt.clear_span)])
                 new_operations = self.post_clean_up(
                     cast(Tuple[ops.Operation], opt.new_operations))
-                circuit.insert_at_frontier(new_operations, i, frontier)
 
+                flat_new_operations = tuple(ops.flatten_to_ops(new_operations))
+
+                new_qubits = set()
+                for flat_op in flat_new_operations:
+                    for q in flat_op.qubits:
+                        new_qubits.add(q)
+
+                if not new_qubits.issubset(set(opt.clear_qubits)):
+                    raise ValueError(
+                        'New operations in PointOptimizer should not act on new'
+                        ' qubits.')
+
+                circuit.insert_at_frontier(flat_new_operations, i, frontier)
             i += 1


Test patch:
diff --git a/cirq/circuits/optimization_pass_test.py b/cirq/circuits/optimization_pass_test.py
--- a/cirq/circuits/optimization_pass_test.py
+++ b/cirq/circuits/optimization_pass_test.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
 import cirq
 from cirq import PointOptimizer, PointOptimizationSummary
 from cirq.testing import EqualsTester
@@ -142,6 +143,23 @@ def clean_up(operations):
     assert actual_text_diagram == expected_text_diagram
 
 
+def test_point_optimizer_raises_on_gates_changing_qubits():
+
+    class EverythingIs42(cirq.PointOptimizer):
+        """Changes all single qubit operations to act on LineQubit(42)"""
+
+        def optimization_at(self, circuit, index, op):
+            if len(op.qubits) == 1:
+                new_op = op.gate(cirq.LineQubit(42))
+                return cirq.PointOptimizationSummary(clear_span=1,
+                                                     clear_qubits=op.qubits,
+                                                     new_operations=new_op)
+
+    c = cirq.Circuit(cirq.X(cirq.LineQubit(0)), cirq.X(cirq.LineQubit(1)))
+    with pytest.raises(ValueError, match='new qubits'):
+        EverythingIs42().optimize_circuit(c)
+
+
 def test_repr():
     assert repr(cirq.PointOptimizationSummary(clear_span=0, clear_qubits=[
     ], new_operations=[])) == 'cirq.PointOptimizationSummary(0, (), ())'