Repository: quantumlib/Cirq

Issue:
Remove public fields from gates
We still have a number of gates and operations that have public fields. These should be private, with public getters if necessary. (And those getters should not return mutable references unless it's something performance critical.) Otherwise this breaks the contracts of FrozenCircuit etc. 

Ideally upon fixing this we should create a unit test that checks this to some extent for all gates in Cirq.

This should probably be done before 1.0.

- [x] controlled_gate.py #5061
- [x] controlled_operation.py #5061
- [x] dense_pauli_string.py #5064
- [x] fourier_transform.py #5074
- [x] fsim_gate.py #5075
- [x] measurement_gate.py #5061 
- [x] pauli_interaction_gate.py #5062 
- [x] pauli_measurement_gate.py #5062 
- [x] pauli_string_raw_types.py #5062 
- [x] permutation_gate.py #5074
- [x] random_gate_channel.py #5074
- [x] raw_types.py #5074
- [x] wait_gate.py #5074
```
(cirq-py3) dax@DESKTOP-Q5MLJ3J:~/cirq$ grep -r "self\.[^_][^\ ]*\s=\s" --exclude "*test.py" cirq-core/cirq/ops/*
Binary file cirq-core/cirq/ops/__pycache__/arithmetic_operation.cpython-38.pyc matches
Binary file cirq-core/cirq/ops/__pycache__/arithmetic_operation.cpython-39.pyc matches
cirq-core/cirq/ops/controlled_gate.py:        self.control_qid_shape = tuple(control_qid_shape)
cirq-core/cirq/ops/controlled_gate.py:        self.control_values = cast(
cirq-core/cirq/ops/controlled_gate.py:            self.sub_gate = sub_gate.sub_gate  # type: ignore
cirq-core/cirq/ops/controlled_gate.py:            self.sub_gate = sub_gate
cirq-core/cirq/ops/controlled_operation.py:        self.control_values = cast(
cirq-core/cirq/ops/controlled_operation.py:            self.controls = tuple(controls)
cirq-core/cirq/ops/controlled_operation.py:            self.sub_operation = sub_operation
cirq-core/cirq/ops/controlled_operation.py:            self.controls = tuple(controls) + sub_operation.controls
cirq-core/cirq/ops/controlled_operation.py:            self.sub_operation = sub_operation.sub_operation
cirq-core/cirq/ops/dense_pauli_string.py:        self.pauli_mask = _as_pauli_mask(pauli_mask)
cirq-core/cirq/ops/dense_pauli_string.py:        self.coefficient = (
cirq-core/cirq/ops/dense_pauli_string.py:            self.pauli_mask = np.copy(self.pauli_mask)
cirq-core/cirq/ops/dense_pauli_string.py:            self.pauli_mask[key] = _pauli_index(value)
cirq-core/cirq/ops/dense_pauli_string.py:                self.pauli_mask[key] = value.pauli_mask
cirq-core/cirq/ops/dense_pauli_string.py:                self.pauli_mask[key] = _as_pauli_mask(value)
cirq-core/cirq/ops/dense_pauli_string.py:            self.coefficient = new_coef if isinstance(new_coef, sympy.Basic) else complex(new_coef)
cirq-core/cirq/ops/fourier_transform.py:        self.exponent = exponent
cirq-core/cirq/ops/fsim_gate.py:        self.theta = _canonicalize(theta)
cirq-core/cirq/ops/fsim_gate.py:        self.phi = _canonicalize(phi)
cirq-core/cirq/ops/fsim_gate.py:        self.theta = _canonicalize(theta)
cirq-core/cirq/ops/fsim_gate.py:        self.zeta = _canonicalize(zeta)
cirq-core/cirq/ops/fsim_gate.py:        self.chi = _canonicalize(chi)
cirq-core/cirq/ops/fsim_gate.py:        self.gamma = _canonicalize(gamma)
cirq-core/cirq/ops/fsim_gate.py:        self.phi = _canonicalize(phi)
cirq-core/cirq/ops/measurement_gate.py:        self.key = key  # type: ignore
cirq-core/cirq/ops/measurement_gate.py:        self.invert_mask = invert_mask or ()
cirq-core/cirq/ops/measurement_gate.py:            self.mkey = key
cirq-core/cirq/ops/measurement_gate.py:            self.mkey = value.MeasurementKey(name=key)
cirq-core/cirq/ops/pauli_interaction_gate.py:        self.pauli0 = pauli0
cirq-core/cirq/ops/pauli_interaction_gate.py:        self.invert0 = invert0
cirq-core/cirq/ops/pauli_interaction_gate.py:        self.pauli1 = pauli1
cirq-core/cirq/ops/pauli_interaction_gate.py:        self.invert1 = invert1
cirq-core/cirq/ops/pauli_measurement_gate.py:        self.key = key  # type: ignore
cirq-core/cirq/ops/pauli_measurement_gate.py:        self.mkey = key
cirq-core/cirq/ops/pauli_string_raw_types.py:        self.pauli_string = pauli_string
cirq-core/cirq/ops/permutation_gate.py:        self.permutation = tuple(permutation)
cirq-core/cirq/ops/random_gate_channel.py:        self.sub_gate = sub_gate
cirq-core/cirq/ops/random_gate_channel.py:        self.probability = probability
cirq-core/cirq/ops/random_gate_channel.py:            self.sub_gate = self.sub_gate.sub_gate
cirq-core/cirq/ops/raw_types.py:        self.sub_operation = sub_operation
cirq-core/cirq/ops/wait_gate.py:        self.duration = value.Duration(duration)
```


Discussion:
From cirq sync: 

We should do it. Let's do a deprecation by creating a setter and mark it deprecated. 

Resolving patch:
diff --git a/cirq-core/cirq/ops/fsim_gate.py b/cirq-core/cirq/ops/fsim_gate.py
--- a/cirq-core/cirq/ops/fsim_gate.py
+++ b/cirq-core/cirq/ops/fsim_gate.py
@@ -23,18 +23,18 @@
 
 import cmath
 import math
-from typing import AbstractSet, Any, Dict, Optional, Tuple, Union
+from typing import AbstractSet, Any, Dict, Optional, Tuple
 
 import numpy as np
 import sympy
 
 import cirq
 from cirq import protocols, value
-from cirq._compat import proper_repr
+from cirq._compat import deprecated, proper_repr
 from cirq.ops import gate_features, raw_types
 
 
-def _canonicalize(value: Union[float, sympy.Basic]) -> Union[float, sympy.Basic]:
+def _canonicalize(value: 'cirq.TParamVal') -> 'cirq.TParamVal':
     """Assumes value is 2π-periodic and shifts it into [-π, π)."""
     if protocols.is_parameterized(value):
         return value
@@ -42,12 +42,12 @@ def _canonicalize(value: Union[float, sympy.Basic]) -> Union[float, sympy.Basic]
     return value - period * np.floor((value + np.pi) / period)
 
 
-def _zero_mod_pi(param: Union[float, sympy.Basic]) -> bool:
+def _zero_mod_pi(param: 'cirq.TParamVal') -> bool:
     """Returns True iff param, assumed to be in [-pi, pi), is 0 (mod pi)."""
     return param in (0.0, -np.pi, -sympy.pi)
 
 
-def _half_pi_mod_pi(param: Union[float, sympy.Basic]) -> bool:
+def _half_pi_mod_pi(param: 'cirq.TParamVal') -> bool:
     """Returns True iff param, assumed to be in [-pi, pi), is pi/2 (mod pi)."""
     return param in (-np.pi / 2, np.pi / 2, -sympy.pi / 2, sympy.pi / 2)
 
@@ -78,7 +78,7 @@ class FSimGate(gate_features.InterchangeableQubitsGate, raw_types.Gate):
         FSimGate(θ, φ) = ISWAP**(-2θ/π) CZPowGate(exponent=-φ/π)
     """
 
-    def __init__(self, theta: float, phi: float) -> None:
+    def __init__(self, theta: 'cirq.TParamVal', phi: 'cirq.TParamVal') -> None:
         """Inits FSimGate.
 
         Args:
@@ -90,8 +90,32 @@ 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.
         """
-        self.theta = _canonicalize(theta)
-        self.phi = _canonicalize(phi)
+        self._theta = _canonicalize(theta)
+        self._phi = _canonicalize(phi)
+
+    @property
+    def theta(self) -> 'cirq.TParamVal':
+        return self._theta
+
+    @theta.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def theta(self, theta: 'cirq.TParamVal'):
+        self._theta = theta
+
+    @property
+    def phi(self) -> 'cirq.TParamVal':
+        return self._phi
+
+    @phi.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def phi(self, phi: 'cirq.TParamVal'):
+        self._phi = phi
 
     def _num_qubits_(self) -> int:
         return 2
@@ -249,11 +273,11 @@ class PhasedFSimGate(gate_features.InterchangeableQubitsGate, raw_types.Gate):
 
     def __init__(
         self,
-        theta: Union[float, sympy.Basic],
-        zeta: Union[float, sympy.Basic] = 0.0,
-        chi: Union[float, sympy.Basic] = 0.0,
-        gamma: Union[float, sympy.Basic] = 0.0,
-        phi: Union[float, sympy.Basic] = 0.0,
+        theta: 'cirq.TParamVal',
+        zeta: 'cirq.TParamVal' = 0.0,
+        chi: 'cirq.TParamVal' = 0.0,
+        gamma: 'cirq.TParamVal' = 0.0,
+        phi: 'cirq.TParamVal' = 0.0,
     ) -> None:
         """Inits PhasedFSimGate.
 
@@ -269,18 +293,78 @@ def __init__(
             phi: Controlled phase angle, in radians. See class docstring
                 above for details.
         """
-        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)
+
+    @property
+    def theta(self) -> 'cirq.TParamVal':
+        return self._theta
+
+    @theta.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def theta(self, theta: 'cirq.TParamVal'):
+        self._theta = theta
+
+    @property
+    def zeta(self) -> 'cirq.TParamVal':
+        return self._zeta
+
+    @zeta.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def zeta(self, zeta: 'cirq.TParamVal'):
+        self._zeta = zeta
+
+    @property
+    def chi(self) -> 'cirq.TParamVal':
+        return self._chi
+
+    @chi.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def chi(self, chi: 'cirq.TParamVal'):
+        self._chi = chi
+
+    @property
+    def gamma(self) -> 'cirq.TParamVal':
+        return self._gamma
+
+    @gamma.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def gamma(self, gamma: 'cirq.TParamVal'):
+        self._gamma = gamma
+
+    @property
+    def phi(self) -> 'cirq.TParamVal':
+        return self._phi
+
+    @phi.setter  # type: ignore
+    @deprecated(
+        deadline="v0.15",
+        fix="The mutators of this class are deprecated, instantiate a new object instead.",
+    )
+    def phi(self, phi: 'cirq.TParamVal'):
+        self._phi = phi
 
     @staticmethod
     def from_fsim_rz(
-        theta: Union[float, sympy.Basic],
-        phi: Union[float, sympy.Basic],
-        rz_angles_before: Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]],
-        rz_angles_after: Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]],
+        theta: 'cirq.TParamVal',
+        phi: 'cirq.TParamVal',
+        rz_angles_before: Tuple['cirq.TParamVal', 'cirq.TParamVal'],
+        rz_angles_after: Tuple['cirq.TParamVal', 'cirq.TParamVal'],
     ) -> 'PhasedFSimGate':
         """Creates PhasedFSimGate using an alternate parametrization.
 
@@ -302,14 +386,14 @@ def from_fsim_rz(
         return PhasedFSimGate(theta, zeta, chi, gamma, phi)
 
     @property
-    def rz_angles_before(self) -> Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]]:
+    def rz_angles_before(self) -> Tuple['cirq.TParamVal', 'cirq.TParamVal']:
         """Returns 2-tuple of phase angles applied to qubits before FSimGate."""
         b0 = (-self.gamma + self.zeta + self.chi) / 2.0
         b1 = (-self.gamma - self.zeta - self.chi) / 2.0
         return b0, b1
 
     @property
-    def rz_angles_after(self) -> Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]]:
+    def rz_angles_after(self) -> Tuple['cirq.TParamVal', 'cirq.TParamVal']:
         """Returns 2-tuple of phase angles applied to qubits after FSimGate."""
         a0 = (-self.gamma + self.zeta - self.chi) / 2.0
         a1 = (-self.gamma - self.zeta + self.chi) / 2.0
@@ -413,7 +497,7 @@ def _decompose_(self, qubits) -> 'cirq.OP_TREE':
         makes the top left element of the matrix equal to 1.
         """
 
-        def to_exponent(angle_rads: Union[float, sympy.Basic]) -> Union[float, sympy.Basic]:
+        def to_exponent(angle_rads: 'cirq.TParamVal') -> 'cirq.TParamVal':
             """Divides angle_rads by symbolic or numerical pi."""
             pi = sympy.pi if protocols.is_parameterized(angle_rads) else np.pi
             return angle_rads / pi


Test patch:
diff --git a/cirq-core/cirq/ops/fsim_gate_test.py b/cirq-core/cirq/ops/fsim_gate_test.py
--- a/cirq-core/cirq/ops/fsim_gate_test.py
+++ b/cirq-core/cirq/ops/fsim_gate_test.py
@@ -804,3 +804,39 @@ def test_phased_fsim_json_dict():
         'gamma': 0.78,
         'phi': 0.9,
     }
+
+
+def test_setters_deprecated():
+    gate = cirq.FSimGate(0.1, 0.1)
+    assert gate.theta == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.theta = 0.2
+        assert gate.theta == 0.2
+    assert gate.phi == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.phi = 0.2
+        assert gate.phi == 0.2
+
+
+def test_phased_setters_deprecated():
+    gate = cirq.PhasedFSimGate(0.1, 0.1, 0.1, 0.1, 0.1)
+    assert gate.theta == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.theta = 0.2
+        assert gate.theta == 0.2
+    assert gate.zeta == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.zeta = 0.2
+        assert gate.zeta == 0.2
+    assert gate.chi == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.chi = 0.2
+        assert gate.chi == 0.2
+    assert gate.gamma == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.gamma = 0.2
+        assert gate.gamma == 0.2
+    assert gate.phi == 0.1
+    with cirq.testing.assert_deprecated('mutators', deadline='v0.15'):
+        gate.phi = 0.2
+        assert gate.phi == 0.2