Repository: quantumlib/Cirq

Issue:
ParallelGateOperation has wrong unitary
The unitary returned by `ParallelGateOperation._unitary_()` is incorrect. For example, for XX it returns a 4x4 singular (hence non-unitary) matrix and for XXX it returns a rectangular 16x4 matrix.

Unit tests don't check internal consistency of the operation.

```
In [1]: import cirq

In [2]: q0, q1, q2 = cirq.LineQubit.range(3)

In [3]: xx = cirq.ParallelGateOperation(cirq.X, (q0, q1))

In [4]: xxx = cirq.ParallelGateOperation(cirq.X, (q0, q1, q2))

In [5]: cirq.unitary(cirq.Circuit.from_ops(cirq.decompose(xx)))
Out[5]: 
array([[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
       [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
       [1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])

In [6]: cirq.unitary(xx)
Out[6]: 
array([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])

In [7]: cirq.unitary(cirq.Circuit.from_ops(cirq.decompose(xxx)))
Out[7]: 
array([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])

In [8]: cirq.unitary(xxx)
Out[8]: 
array([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])

In [9]: cirq.unitary(cirq.Circuit.from_ops(cirq.decompose(xxx))).shape
Out[9]: (8, 8)

In [10]: cirq.unitary(xxx).shape
Out[10]: (16, 4)
```

Found when trying to use the operation in a new unit test for #1612.


Discussion:

Resolving patch:
diff --git a/cirq/ops/parallel_gate_operation.py b/cirq/ops/parallel_gate_operation.py
--- a/cirq/ops/parallel_gate_operation.py
+++ b/cirq/ops/parallel_gate_operation.py
@@ -121,7 +121,7 @@ def _unitary_(self) -> Union[np.ndarray, NotImplementedType]:
         # unitary to each qubit. This will blow up memory fast.
         unitary = single_unitary
         for _ in range(len(self.qubits) - 1):
-            unitary = np.outer(unitary, single_unitary)
+            unitary = np.kron(unitary, single_unitary)
 
         return unitary
 
@@ -149,14 +149,6 @@ def _circuit_diagram_info_(self,
                                             exponent=diagram_info.exponent,
                                             connected=False)
 
-    def _phase_by_(self, phase_turns: float,
-                   qubit_index: int) -> 'ParallelGateOperation':
-        phased_gate = protocols.phase_by(self._gate, phase_turns, qubit_index,
-                                         default=None)
-        if phased_gate is None:
-            return NotImplemented
-        return self.with_gate(phased_gate)
-
     def __pow__(self, exponent: Any) -> 'ParallelGateOperation':
         """Raise gate to a power, then reapply to the same qubits.
 


Test patch:
diff --git a/cirq/ops/parallel_gate_operation_test.py b/cirq/ops/parallel_gate_operation_test.py
--- a/cirq/ops/parallel_gate_operation_test.py
+++ b/cirq/ops/parallel_gate_operation_test.py
@@ -103,12 +103,11 @@ def test_unitary():
     assert not cirq.has_unitary(p)
     assert cirq.unitary(p, None) is None
     np.testing.assert_allclose(cirq.unitary(q),
-                               np.array([
-                                   [0. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j],
-                                   [0. + 0.j, 1. + 0.j, 1. + 0.j, 0. + 0.j],
-                                   [0. + 0.j, 1. + 0.j, 1. + 0.j, 0. + 0.j],
-                                   [0. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]
-                               ]),
+                               np.array(
+                                   [[0. + 0.j, 0. + 0.j, 0. + 0.j, 1. + 0.j],
+                                    [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j],
+                                    [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j],
+                                    [1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]]),
                                atol=1e-8)
 
 
@@ -132,18 +131,6 @@ def test_str():
     assert str(cirq.ParallelGateOperation(cirq.X, (a, b))) == 'X(0, 1)'
 
 
-def test_phase():
-    g1 = cirq.SingleQubitGate()
-    g2 = cirq.S
-    g3 = cirq.phase_by(g2, 1, 0)
-    qreg = cirq.LineQubit.range(2)
-    op1 = cirq.ParallelGateOperation(g1, qreg)
-    op2 = cirq.ParallelGateOperation(g2, qreg)
-    assert cirq.phase_by(op2, 1, 0) == cirq.ParallelGateOperation(g3, qreg)
-    with pytest.raises(TypeError):
-        cirq.phase_by(op1, 1, 0)
-
-
 def test_equivalent_circuit():
     qreg = cirq.LineQubit.range(4)
     oldc = cirq.Circuit()
@@ -161,3 +148,14 @@ def test_equivalent_circuit():
                                                                            newc,
                                                                            atol=
                                                                            1e-6)
+
+
+@pytest.mark.parametrize('gate, qubits', (
+    (cirq.X, (cirq.NamedQubit('q'),)),
+    (cirq.Y, cirq.LineQubit.range(2)),
+    (cirq.Z, cirq.LineQubit.range(3)),
+    (cirq.H, cirq.LineQubit.range(4)),
+))
+def test_parallel_gate_operation_is_consistent(gate, qubits):
+    op = cirq.ParallelGateOperation(gate, qubits)
+    cirq.testing.assert_implements_consistent_protocols(op)