Repository: quantumlib/Cirq

Issue:
Inconsistent canonicalization of FSim angles
**Description of the issue**

`FSimGate` and `PhasedFSimGate` disagree on whether or not theta and phi angles that are used by `__init__` and are exposed as properties are canonicalized (by projecting them into [-pi, +pi]) or not.

**How to reproduce the issue**

```
In [1]: import cirq, numpy as np

In [2]: for t in (-3/2, -1/2, 1/2, 3/2):
   ...:     theta = np.pi * t
   ...:     g = cirq.FSimGate(theta, 0)
   ...:     print(theta, g.theta)
   ...: 
-4.71238898038469 -4.71238898038469
-1.5707963267948966 -1.5707963267948966
1.5707963267948966 1.5707963267948966
4.71238898038469 4.71238898038469

In [3]: for t in (-3/2, -1/2, 1/2, 3/2):
   ...:     theta = np.pi * t
   ...:     g = cirq.PhasedFSimGate(theta, 0)
   ...:     print(theta, g.theta)
   ...: 
-4.71238898038469 1.5707963267948966
-1.5707963267948966 -1.5707963267948966
1.5707963267948966 1.5707963267948966
4.71238898038469 -1.5707963267948966
```

**Cirq version**
0.10.0.dev



Discussion:

Resolving patch:
diff --git a/cirq/ops/fsim_gate.py b/cirq/ops/fsim_gate.py
--- a/cirq/ops/fsim_gate.py
+++ b/cirq/ops/fsim_gate.py
@@ -34,6 +34,15 @@
 from cirq.ops import gate_features
 
 
+def _canonicalize(value: Union[float, sympy.Basic]
+                 ) -> Union[float, sympy.Basic]:
+    """Assumes value is 2π-periodic and shifts it into [-π, π]."""
+    if protocols.is_parameterized(value):
+        return value
+    period = 2 * np.pi
+    return value - period * np.round(value / period)
+
+
 def _zero_mod_pi(param: Union[float, sympy.Basic]) -> bool:
     """Returns True iff param, assumed to be in [-pi, pi], is 0 (mod pi)."""
     return param in (-np.pi, 0.0, np.pi, -sympy.pi, sympy.pi)
@@ -82,8 +91,8 @@ def __init__(self, theta: float, phi: float) -> None:
                 ``|11⟩`` state is phased. Note: uses opposite sign convention to
                 the CZPowGate. Maximum strength (full cz) is at pi/2.
         """
-        self.theta = theta
-        self.phi = phi
+        self.theta = _canonicalize(theta)
+        self.phi = _canonicalize(phi)
 
     def _value_equality_values_(self) -> Any:
         return self.theta, self.phi
@@ -256,20 +265,11 @@ def __init__(
             phi: Controlled phase angle, in radians. See class docstring
                 above for details.
         """
-
-        def canonicalize(value: Union[float, sympy.Basic]
-                        ) -> Union[float, sympy.Basic]:
-            """Assumes value is 2π-periodic and shifts it into [-π, π]."""
-            if protocols.is_parameterized(value):
-                return value
-            period = 2 * np.pi
-            return value - period * np.round(value / period)
-
-        self.theta = canonicalize(theta)
-        self.zeta = canonicalize(zeta)
-        self.chi = canonicalize(chi)
-        self.gamma = canonicalize(gamma)
-        self.phi = canonicalize(phi)
+        self.theta = _canonicalize(theta)
+        self.zeta = _canonicalize(zeta)
+        self.chi = _canonicalize(chi)
+        self.gamma = _canonicalize(gamma)
+        self.phi = _canonicalize(phi)
 
     @staticmethod
     def from_fsim_rz(


Test patch:
diff --git a/cirq/google/common_serializers_test.py b/cirq/google/common_serializers_test.py
--- a/cirq/google/common_serializers_test.py
+++ b/cirq/google/common_serializers_test.py
@@ -689,9 +689,9 @@ def test_wait_gate_multi_qubit():
     (cirq.FSimGate(theta=0, phi=0), 0, 0),
     (cirq.FSimGate(theta=0, phi=np.pi), 0, np.pi),
     (cirq.FSimGate(theta=np.pi / 4, phi=0), np.pi / 4, 0),
-    (cirq.FSimGate(theta=7 * np.pi / 4, phi=0), 7 * np.pi / 4, 0),
+    (cirq.FSimGate(theta=7 * np.pi / 4, phi=0), -np.pi / 4, 0),
     (cirq.FSimGate(theta=-np.pi / 4, phi=0), -np.pi / 4, 0),
-    (cirq.FSimGate(theta=-7 * np.pi / 4, phi=0), -7 * np.pi / 4, 0),
+    (cirq.FSimGate(theta=-7 * np.pi / 4, phi=0), np.pi / 4, 0),
     (cirq.google.SYC, np.pi / 2, np.pi / 6),
     (cirq.FSimGate(theta=np.pi / 2, phi=np.pi / 6), np.pi / 2, np.pi / 6),
     (cirq.FSimGate(theta=1.5707963705062866, phi=0.5235987901687622),
diff --git a/cirq/ops/fsim_gate_test.py b/cirq/ops/fsim_gate_test.py
--- a/cirq/ops/fsim_gate_test.py
+++ b/cirq/ops/fsim_gate_test.py
@@ -28,6 +28,10 @@ def test_fsim_init():
     assert f2.theta == 1
     assert f2.phi == 2
 
+    f3 = cirq.FSimGate(theta=4, phi=-5)
+    assert f3.theta == 4 - 2 * np.pi
+    assert f3.phi == -5 + 2 * np.pi
+
 
 def test_fsim_eq():
     eq = cirq.testing.EqualsTester()