Repository: quantumlib/Cirq

Issue:
Make _TurnGate public
I want to define a gate like 
```
class XXYYGate(_TurnGate,
               cirq.InterchangeableQubitsGate,
               cirq.TwoQubitGate):
    """XX + YY interaction.

    This gate implements the unitary exp(-i pi half_turns (XX + YY) / 2)
    """

    def __init__(self, *positional_args,
                 half_turns: float=1.0) -> None:
        assert not positional_args
        super().__init__(half_turns=half_turns)

    def _matrix_impl_assuming_unparameterized(self):
        c = numpy.cos(numpy.pi * self.half_turns)
        s = numpy.sin(numpy.pi * self.half_turns)
        return numpy.array([[1, 0, 0, 0],
                            [0, c, -1j * s, 0],
                            [0, -1j * s, c, 0],
                            [0, 0, 0, 1]])

    def ascii_wire_symbols(self):
        return 'XXYY', 'XXYY'
```

Although _TurnGate is documented as "A gate with exactly two eigenvalues", I think it actually works for Gates that have this property within a two-dimensional subspace and act as the identity on the orthogonal space. So it's allowed to have a third eigenvalue which is always fixed. The only method of _TurnGate I don't completely understand is trace_distance_bound but I'm pretty sure that's not a problem.


Discussion:

Resolving patch:
diff --git a/cirq/ops/common_gates.py b/cirq/ops/common_gates.py
--- a/cirq/ops/common_gates.py
+++ b/cirq/ops/common_gates.py
@@ -18,110 +18,13 @@
 
 import numpy as np
 
-from cirq import abc
-from cirq.extension import PotentialImplementation
 from cirq.ops import gate_features
+from cirq.ops.partial_reflection_gate import PartialReflectionGate
 from cirq.ops.raw_types import InterchangeableQubitsGate
 from cirq.value import Symbol
 
 
-def _canonicalize_half_turns(
-        half_turns: Union[Symbol, float]
-) -> Union[Symbol, float]:
-    if isinstance(half_turns, Symbol):
-        return half_turns
-    half_turns %= 2
-    if half_turns > 1:
-        half_turns -= 2
-    return half_turns
-
-
-class _TurnGate(gate_features.BoundedEffectGate,
-                gate_features.TextDiagrammableGate,
-                PotentialImplementation):
-    """A gate with exactly two eigenvalues.
-
-    Extrapolating the gate phases one eigenspace relative to the other, with
-    half_turns=1 corresponding to the point where the relative phase factor is
-    exactly -1.
-    """
-
-    def __init__(self,
-                 *positional_args,
-                 half_turns: Union[Symbol, float] = 1.0) -> None:
-        assert not positional_args
-        self.half_turns = _canonicalize_half_turns(half_turns)
-
-    @abc.abstractmethod
-    def text_diagram_wire_symbols(self,
-                                  qubit_count=None,
-                                  use_unicode_characters=True,
-                                  precision=3
-                                  ) -> Tuple[str, ...]:
-        pass
-
-    def text_diagram_exponent(self):
-        return self.half_turns
-
-    def __pow__(self, power: float) -> '_TurnGate':
-        return self.extrapolate_effect(power)
-
-    def inverse(self) -> '_TurnGate':
-        return self.extrapolate_effect(-1)
-
-    def __repr__(self):
-        base = ''.join(self.text_diagram_wire_symbols())
-        if self.half_turns == 1:
-            return base
-        return '{}**{}'.format(base, repr(self.half_turns))
-
-    def __eq__(self, other):
-        if not isinstance(other, type(self)):
-            return NotImplemented
-        return self.half_turns == other.half_turns
-
-    def __ne__(self, other):
-        return not self == other
-
-    def __hash__(self):
-        return hash((type(self), self.half_turns))
-
-    def trace_distance_bound(self):
-        if isinstance(self.half_turns, Symbol):
-            return 1
-        return abs(self.half_turns) * 3.5
-
-    def try_cast_to(self, desired_type):
-        if (desired_type in [gate_features.ExtrapolatableGate,
-                             gate_features.ReversibleGate] and
-                self.can_extrapolate_effect()):
-            return self
-        if desired_type is gate_features.KnownMatrixGate and self.has_matrix():
-            return self
-        return super().try_cast_to(desired_type)
-
-    @abc.abstractmethod
-    def _matrix_impl_assuming_unparameterized(self) -> np.ndarray:
-        pass
-
-    def has_matrix(self) -> bool:
-        return not isinstance(self.half_turns, Symbol)
-
-    def matrix(self) -> np.ndarray:
-        if not self.has_matrix():
-            raise ValueError("Parameterized. Don't have a known matrix.")
-        return self._matrix_impl_assuming_unparameterized()
-
-    def can_extrapolate_effect(self) -> bool:
-        return not isinstance(self.half_turns, Symbol)
-
-    def extrapolate_effect(self, factor) -> '_TurnGate':
-        if not self.can_extrapolate_effect():
-            raise ValueError("Parameterized. Don't have a known matrix.")
-        return type(self)(half_turns=self.half_turns * factor)
-
-
-class Rot11Gate(_TurnGate,
+class Rot11Gate(PartialReflectionGate,
                 gate_features.TwoQubitGate,
                 InterchangeableQubitsGate):
     """Phases the |11> state of two adjacent qubits by a fixed amount.
@@ -129,11 +32,10 @@ class Rot11Gate(_TurnGate,
     A ParameterizedCZGate guaranteed to not be using the parameter key field.
     """
 
-    def __init__(self,
-                 *positional_args,
-                 half_turns: Union[float, Symbol]=1.0) -> None:
-        assert not positional_args
-        super().__init__(half_turns=half_turns)
+    def _with_half_turns(self,
+                         half_turns: Union[Symbol, float] = 1.0
+                         ) -> 'Rot11Gate':
+        return Rot11Gate(half_turns=half_turns)
 
     def text_diagram_wire_symbols(self,
                                   qubit_count=None,
@@ -141,19 +43,21 @@ def text_diagram_wire_symbols(self,
                                   precision=3):
         return 'Z', 'Z'
 
-    def _matrix_impl_assuming_unparameterized(self):
+    def _reflection_matrix(self):
         """See base class."""
-        return np.diag([1, 1, 1, np.exp(1j * np.pi * self.half_turns)])
+        return np.diag([1, 1, 1, -1])
+
+    def __repr__(self) -> str:
+        return self.__str__()
 
 
-class RotXGate(_TurnGate, gate_features.SingleQubitGate):
+class RotXGate(PartialReflectionGate, gate_features.SingleQubitGate):
     """Fixed rotation around the X axis of the Bloch sphere."""
 
-    def __init__(self,
-                 *positional_args,
-                 half_turns: Union[float, Symbol]=1.0) -> None:
-        assert not positional_args
-        super().__init__(half_turns=half_turns)
+    def _with_half_turns(self,
+                         half_turns: Union[Symbol, float] = 1.0
+                         ) -> 'RotXGate':
+        return RotXGate(half_turns=half_turns)
 
     def text_diagram_wire_symbols(self,
                                   qubit_count=None,
@@ -161,20 +65,20 @@ def text_diagram_wire_symbols(self,
                                   precision=3):
         return 'X',
 
-    def _matrix_impl_assuming_unparameterized(self):
-        c = np.exp(1j * np.pi * self.half_turns)
-        return np.array([[1 + c, 1 - c],
-                         [1 - c, 1 + c]]) / 2
+    def _reflection_matrix(self):
+        return np.array([[0, 1], [1, 0]])
 
+    def __repr__(self) -> str:
+        return self.__str__()
 
-class RotYGate(_TurnGate, gate_features.SingleQubitGate):
+
+class RotYGate(PartialReflectionGate, gate_features.SingleQubitGate):
     """Fixed rotation around the Y axis of the Bloch sphere."""
 
-    def __init__(self,
-                 *positional_args,
-                 half_turns: Union[float, Symbol]=1.0) -> None:
-        assert not positional_args
-        super().__init__(half_turns=half_turns)
+    def _with_half_turns(self,
+                         half_turns: Union[Symbol, float] = 1.0
+                         ) -> 'RotYGate':
+        return RotYGate(half_turns=half_turns)
 
     def text_diagram_wire_symbols(self,
                                   qubit_count=None,
@@ -182,21 +86,20 @@ def text_diagram_wire_symbols(self,
                                   precision=3):
         return 'Y',
 
-    def _matrix_impl_assuming_unparameterized(self):
-        s = np.sin(np.pi * self.half_turns)
-        c = np.cos(np.pi * self.half_turns)
-        return np.array([[1 + s*1j + c, c*1j - s - 1j],
-                         [1j + s - c*1j, 1 + s*1j + c]]) / 2
+    def _reflection_matrix(self):
+        return np.array([[0, -1j], [1j, 0]])
+
+    def __repr__(self) -> str:
+        return self.__str__()
 
 
-class RotZGate(_TurnGate, gate_features.SingleQubitGate):
+class RotZGate(PartialReflectionGate, gate_features.SingleQubitGate):
     """Fixed rotation around the Z axis of the Bloch sphere."""
 
-    def __init__(self,
-                 *positional_args,
-                 half_turns: Union[float, Symbol]=1.0) -> None:
-        assert not positional_args
-        super().__init__(half_turns=half_turns)
+    def _with_half_turns(self,
+                         half_turns: Union[Symbol, float] = 1.0
+                         ) -> 'RotZGate':
+        return RotZGate(half_turns=half_turns)
 
     def text_diagram_wire_symbols(self,
                                   qubit_count=None,
@@ -207,9 +110,11 @@ def text_diagram_wire_symbols(self,
     def phase_by(self, phase_turns, qubit_index):
         return self
 
-    def _matrix_impl_assuming_unparameterized(self):
-        """See base class."""
-        return np.diag([1, np.exp(1j * np.pi * self.half_turns)])
+    def _reflection_matrix(self):
+        return np.diag([1, -1])
+
+    def __repr__(self) -> str:
+        return self.__str__()
 
 
 class MeasurementGate(gate_features.TextDiagrammableGate):
diff --git a/cirq/ops/partial_reflection_gate.py b/cirq/ops/partial_reflection_gate.py
new file mode 100644
--- /dev/null
+++ b/cirq/ops/partial_reflection_gate.py
@@ -0,0 +1,134 @@
+# Copyright 2018 Google LLC
+#
+# 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.
+
+"""Partial reflection gate."""
+from typing import Tuple, Union
+
+import numpy as np
+
+from cirq import abc
+from cirq.extension import PotentialImplementation
+from cirq.linalg import reflection_matrix_pow
+from cirq.ops import gate_features
+from cirq.value import Symbol
+
+
+def _canonicalize_half_turns(
+        half_turns: Union[Symbol, float]
+) -> Union[Symbol, float]:
+    if isinstance(half_turns, Symbol):
+        return half_turns
+    half_turns %= 2
+    if half_turns > 1:
+        half_turns -= 2
+    return half_turns
+
+
+class PartialReflectionGate(gate_features.BoundedEffectGate,
+                            gate_features.TextDiagrammableGate,
+                            PotentialImplementation):
+    """An interpolated reflection operation.
+
+    A reflection operation is an operation that has exactly two eigenvalues
+    which differ by 180 degrees (i.e. equal x and -x for some x).
+    For an interpolated reflection operation, the eigenvalues differ by a
+    relative phase not necessarily equal to 180 degrees.
+    A PartialReflectionGate has a direct sum decomposition I ⊕ U or simply U,
+    where I is the identity and U is an interpolated reflection operation.
+    Extrapolating the gate phases one eigenspace of U relative to the other,
+    with half_turns=1 corresponding to the point where U is a reflection
+    operation (i.e., the relative phase is exactly -1).
+    """
+    def __init__(self,
+                 *positional_args,
+                 half_turns: Union[Symbol, float] = 1.0) -> None:
+        assert not positional_args
+        self.half_turns = _canonicalize_half_turns(half_turns)
+
+    @abc.abstractmethod
+    def _with_half_turns(self,
+                         half_turns: Union[Symbol, float] = 1.0
+                         ) -> 'PartialReflectionGate':
+        """Initialize an instance by specifying the number of half-turns."""
+        pass
+
+    @abc.abstractmethod
+    def text_diagram_wire_symbols(self,
+                                  qubit_count = None,
+                                  use_unicode_characters = True,
+                                  precision: int = 3
+                                  ) -> Tuple[str, ...]:
+        pass
+
+    def text_diagram_exponent(self):
+        return self.half_turns
+
+    def __pow__(self, power: float) -> 'PartialReflectionGate':
+        return self.extrapolate_effect(power)
+
+    def inverse(self) -> 'PartialReflectionGate':
+        return self.extrapolate_effect(-1)
+
+    def __str__(self):
+        base = ''.join(self.text_diagram_wire_symbols())
+        if self.half_turns == 1:
+            return base
+        return '{}**{}'.format(base, repr(self.half_turns))
+
+    def __eq__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        return self.half_turns == other.half_turns
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __hash__(self):
+        return hash((type(self), self.half_turns))
+
+    def trace_distance_bound(self):
+        if isinstance(self.half_turns, Symbol):
+            return 1
+        return abs(self.half_turns) * 3.5
+
+    def try_cast_to(self, desired_type):
+        if (desired_type in [gate_features.ExtrapolatableGate,
+                             gate_features.ReversibleGate] and
+                self.can_extrapolate_effect()):
+            return self
+        if desired_type is gate_features.KnownMatrixGate and self.has_matrix():
+            return self
+        return super().try_cast_to(desired_type)
+
+    @abc.abstractmethod
+    def _reflection_matrix(self) -> np.ndarray:
+        """The reflection matrix corresponding to half_turns=1."""
+        pass
+
+    def has_matrix(self) -> bool:
+        return not isinstance(self.half_turns, Symbol)
+
+    def matrix(self) -> np.ndarray:
+        if isinstance(self.half_turns, Symbol):
+            raise ValueError("Parameterized. Don't have a known matrix.")
+        return reflection_matrix_pow(
+                self._reflection_matrix(), self.half_turns)
+
+    def can_extrapolate_effect(self) -> bool:
+        return not isinstance(self.half_turns, Symbol)
+
+    def extrapolate_effect(self, factor) -> 'PartialReflectionGate':
+        if not self.can_extrapolate_effect():
+            raise ValueError("Parameterized. Don't have a known matrix.")
+        return self._with_half_turns(half_turns=self.half_turns * factor)


Test patch:
diff --git a/cirq/ops/partial_reflection_gate_test.py b/cirq/ops/partial_reflection_gate_test.py
new file mode 100644
--- /dev/null
+++ b/cirq/ops/partial_reflection_gate_test.py
@@ -0,0 +1,63 @@
+# Copyright 2018 Google LLC
+#
+# 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.
+
+from typing import Union
+
+import numpy as np
+
+from cirq.ops.partial_reflection_gate import PartialReflectionGate
+from cirq.testing import EqualsTester
+from cirq.value import Symbol
+
+
+class DummyGate(PartialReflectionGate):
+
+    def _with_half_turns(self, half_turns: Union[Symbol, float] = 1.0):
+        return DummyGate(half_turns=half_turns)
+
+    def text_diagram_wire_symbols(self):
+        return 'D'
+
+    def _reflection_matrix(self):
+        return np.diag([1, -1])
+
+
+def test_partial_reflection_gate_init():
+    assert DummyGate(half_turns=0.5).half_turns == 0.5
+    assert DummyGate(half_turns=5).half_turns == 1
+
+
+def test_partial_reflection_gate_eq():
+    eq = EqualsTester()
+    eq.add_equality_group(DummyGate(), DummyGate(half_turns=1))
+    eq.add_equality_group(DummyGate(half_turns=3.5), DummyGate(half_turns=-0.5))
+    eq.make_equality_pair(lambda: DummyGate(half_turns=Symbol('a')))
+    eq.make_equality_pair(lambda: DummyGate(half_turns=Symbol('b')))
+    eq.make_equality_pair(lambda: DummyGate(half_turns=0))
+    eq.make_equality_pair(lambda: DummyGate(half_turns=0.5))
+
+
+def test_partial_reflection_gate_extrapolate():
+    assert (DummyGate(half_turns=1).extrapolate_effect(0.5) ==
+            DummyGate(half_turns=0.5))
+    assert DummyGate()**-0.25 == DummyGate(half_turns=1.75)
+
+
+def test_partial_reflection_gate_inverse():
+    assert DummyGate().inverse() == DummyGate(half_turns=-1)
+    assert DummyGate(half_turns=0.25).inverse() == DummyGate(half_turns=-0.25)
+
+
+def test_partial_reflection_gate_str():
+    assert str(DummyGate(half_turns=.25)) == 'D**0.25'