Repository: quantumlib/Cirq

Issue:
Bug in Pauli multiplication
The code
```
import numpy as np
import cirq

q = cirq.NamedQubit('a')
print(cirq.Z(q)**0.5 * cirq.X(q) * cirq.Z(q)**-0.5)
```
incorrectly yields
```
-X(a)
```
(the correct behavior, if we want to allow such expressions, is to yield `Y(a)`). Indeed, the code will yield `-X(a)` no matter what the Z exponent is:
```
import numpy as np
import cirq

q = cirq.NamedQubit('a')
theta = np.random.randn()
print(cirq.Z(q)**theta * cirq.X(q) * cirq.Z(q)**-theta)
```
always yields
```
-X(a)
```
The exponent is being ignored. The issue has to do with ```type(cirq.Z(q)**theta)``` being `cirq.ops.pauli_string.SingleQubitPauliStringGateOperation`, when maybe it shouldn't.


Discussion:
Oh I didn't even notice this when I wrote that :( 
The bug seems to be in line 108 of `pauli_string.py`:
```
                f, p = self[c].phased_pauli_product(other[c])
```
since `phased_pauli_product` does not take exponents into account. This leads to the question of whether we should allow PauliStrings to be initialized with exponentiated Paulis, since this leads to the related buggy behavior
```
import cirq

a = cirq.NamedQubit("a")

S = cirq.PauliString({a: cirq.Z**0.5})
X = cirq.PauliString({a: cirq.X})

print(S * X * S)
```
returning
```
-X(a)
```

Resolving patch:
diff --git a/cirq/ops/eigen_gate.py b/cirq/ops/eigen_gate.py
--- a/cirq/ops/eigen_gate.py
+++ b/cirq/ops/eigen_gate.py
@@ -263,7 +263,8 @@ def _period(self) -> Optional[float]:
         real_periods = [abs(2/e) for e in exponents if e != 0]
         return _approximate_common_period(real_periods)
 
-    def __pow__(self: TSelf, exponent: Union[float, sympy.Symbol]) -> TSelf:
+    def __pow__(self: TSelf,
+                exponent: Union[float, sympy.Symbol]) -> 'EigenGate':
         new_exponent = protocols.mul(self._exponent, exponent, NotImplemented)
         if new_exponent is NotImplemented:
             return NotImplemented
diff --git a/cirq/ops/pauli_gates.py b/cirq/ops/pauli_gates.py
--- a/cirq/ops/pauli_gates.py
+++ b/cirq/ops/pauli_gates.py
@@ -91,18 +91,30 @@ def __init__(self, *, exponent: Union[sympy.Basic, float] = 1.0):
         Pauli.__init__(self, index=0, name='X')
         common_gates.XPowGate.__init__(self, exponent=exponent)
 
+    def __pow__(self: '_PauliX',
+                exponent: Union[sympy.Basic, float]) -> common_gates.XPowGate:
+        return common_gates.XPowGate(exponent=exponent)
+
 
 class _PauliY(Pauli, common_gates.YPowGate):
     def __init__(self, *, exponent: Union[sympy.Basic, float] = 1.0):
         Pauli.__init__(self, index=1, name='Y')
         common_gates.YPowGate.__init__(self, exponent=exponent)
 
+    def __pow__(self: '_PauliY',
+                exponent: Union[sympy.Basic, float]) -> common_gates.YPowGate:
+        return common_gates.YPowGate(exponent=exponent)
+
 
 class _PauliZ(Pauli, common_gates.ZPowGate):
     def __init__(self, *, exponent: Union[sympy.Basic, float] = 1.0):
         Pauli.__init__(self, index=2, name='Z')
         common_gates.ZPowGate.__init__(self, exponent=exponent)
 
+    def __pow__(self: '_PauliZ',
+                exponent: Union[sympy.Basic, float]) -> common_gates.ZPowGate:
+        return common_gates.ZPowGate(exponent=exponent)
+
 
 # The Pauli X gate.
 #
diff --git a/cirq/ops/pauli_string.py b/cirq/ops/pauli_string.py
--- a/cirq/ops/pauli_string.py
+++ b/cirq/ops/pauli_string.py
@@ -44,6 +44,9 @@ def __init__(self,
                  coefficient: Union[int, float, complex] = 1) -> None:
         if qubit_pauli_map is None:
             qubit_pauli_map = {}
+        for p in qubit_pauli_map.values():
+            if not isinstance(p, pauli_gates.Pauli):
+                raise TypeError(f'{p} is not a Pauli')
         self._qubit_pauli_map = dict(qubit_pauli_map)
         self._coefficient = complex(coefficient)
 


Test patch:
diff --git a/cirq/ops/pauli_gates_test.py b/cirq/ops/pauli_gates_test.py
--- a/cirq/ops/pauli_gates_test.py
+++ b/cirq/ops/pauli_gates_test.py
@@ -193,3 +193,15 @@ def test_apply_unitary():
     cirq.testing.assert_has_consistent_apply_unitary(cirq.X)
     cirq.testing.assert_has_consistent_apply_unitary(cirq.Y)
     cirq.testing.assert_has_consistent_apply_unitary(cirq.Z)
+
+
+def test_powers():
+    assert isinstance(cirq.X, cirq.Pauli)
+    assert isinstance(cirq.Y, cirq.Pauli)
+    assert isinstance(cirq.Z, cirq.Pauli)
+    assert not isinstance(cirq.X**-0.5, cirq.Pauli)
+    assert not isinstance(cirq.Y**0.2, cirq.Pauli)
+    assert not isinstance(cirq.Z**0.5, cirq.Pauli)
+    assert isinstance(cirq.X**-0.5, cirq.XPowGate)
+    assert isinstance(cirq.Y**0.2, cirq.YPowGate)
+    assert isinstance(cirq.Z**0.5, cirq.ZPowGate)
diff --git a/cirq/ops/pauli_string_test.py b/cirq/ops/pauli_string_test.py
--- a/cirq/ops/pauli_string_test.py
+++ b/cirq/ops/pauli_string_test.py
@@ -681,3 +681,19 @@ def test_decompose():
         cirq.X(a), cirq.Z(b)
     ]
     assert cirq.decompose_once(cirq.Y(b) * cirq.Z(a)) == [cirq.Z(a), cirq.Y(b)]
+
+
+def test_rejects_non_paulis():
+    q = cirq.NamedQubit('q')
+    with pytest.raises(TypeError):
+        _ = cirq.PauliString({q: cirq.S})
+
+
+def test_cannot_multiply_by_non_paulis():
+    q = cirq.NamedQubit('q')
+    with pytest.raises(TypeError):
+        _ = cirq.X(q) * cirq.Z(q)**0.5
+    with pytest.raises(TypeError):
+        _ = cirq.Z(q)**0.5 * cirq.X(q)
+    with pytest.raises(TypeError):
+        _ = cirq.Y(q) * cirq.S(q)