Repository: quantumlib/Cirq

Issue:
Global phase in SingleQubitCliffordGate
Filing to continue discussion started in the meeting by @fedimser.

### Background

Clifford gates are often said to be the unitaries that map Paulis to other Paulis under conjugation. This yields an infinite group(*). However, there really is only a finite number of interesting classes of elements in this group, so the actual definition of the Clifford group generally involves the quotient of the above by U(1), i.e. we ignore the global phase. This yields a set of 24 elements in the single-qubit case.

Interestingly, it can be shown that the Clifford group is generated by Hadamard, phase and CNOT gates. Note that if you take this as the definition of the Clifford group then you end up with more elements than above, e.g. 192 in the single-qubit case. However, once again taking the quotient by U(1) saves the day yielding the same group as above.

This means that the fact that the global phase is ignored is important for some well-known properties of the Clifford group to hold.

### Issue

In cirq, we don't keep track of the global phase of Clifford gates. By the statements above, this is the right thing to do. @fedimser pointed out that this may be misleading for the user. @fedimser what particular methods/functions did you have in mind? @Strilanc said we can either rename them to be clear about the loss of global phase or we can return a tuple (Clifford, phase). Is there a use-case in which the knowledge of the global phase is useful?

@cduck 

---
(*) For example, multiples of identity `exp(i theta) I` satisfy the condition for a Clifford gate for any real theta.


Discussion:
I came up with this idea while working on PR #2803.

As @Strilanc suggested, I added a factory method `from_unitary` on class `SingleQubitCliffordGate`. When I am trying to simulate a Clifford gate, I am converting it to `SingleQubitCliffordGate`, then decompose to rotations and decompose rotations to H and S gates.
But we also need to keep track of global phase - it's stored in `_ch_form.omega`. I tried comparing unitary matrix of Clifford gate I am simulating to `cirq.unitary` of constructed gate, but that didn't always work, because in `SingleQubitCliffordGate._decompose_` there is an optimization to return H matrix if this is H gate. Unfortunately, it's not the same matrix which we get if we decompose H gate to rotations.

In other words, `SingleQubitCliffordGate._unitary_` returns *some* unitary matrix which has the same effect on Paulis as given gate, but there is no convention how to choose this matrix. I thought that we can solve this by agreeing on canonical unitary for Clifford gate (e.g. it should agree with `decompose_rotation`, or it's first non-zero element should be real and positive), and also adding field on `SingleQubitCliffordGate._unitary_` to represent phase difference between canconical matrix and gate stored by this object.

**Practical reasons to do this**

1. To make this expression true for every Clifford 2x2 matrix `u`: 
`np.allclose(u, cirq.unitary(cirq.SingleQubitCliffordGate.from_unitary(u)))`.

2. So we can have two `SingleQubitCliffordGate` objects representing two different gates which differ in phase, and they won't be equal.

3. So user can construct `SingleQubitCliffordGate` representing any single qubit Clifford gate, i.e. with any phase. Because by definition, if you apply phase shift - it's still a Clifford gate.

To be honest, real problem is that `SingleQubitCliffordGate._unitary_` is not always in agreement with `SingleQubitCliffordGate.decompose_rotation()`, because of special handling of Hadamard gate in `SingleQubitCliffordGate._decompose_`. I understand that this special case was added to get usual textbook definition of Hadamard gate, so we can't remove it (whoever added it implicitly admitted that Clifford gates differing in phase are not the same).
So I thought how to fix this issue and keeping track of phase is one possible solution.
Another option would be to output a GlobalPhaseOperaton with the appropriate additional phase from `_decompose_`.  This could be done in addition to adding a global phase attribute to SingleQubitCliffordGate.

Resolving patch:
diff --git a/cirq-core/cirq/ops/clifford_gate.py b/cirq-core/cirq/ops/clifford_gate.py
--- a/cirq-core/cirq/ops/clifford_gate.py
+++ b/cirq-core/cirq/ops/clifford_gate.py
@@ -620,6 +620,29 @@ def from_unitary(u: np.ndarray) -> Optional['SingleQubitCliffordGate']:
             _to_clifford_tableau(x_to=x_to, z_to=z_to)
         )
 
+    @classmethod
+    def from_unitary_with_global_phase(
+        cls, u: np.ndarray
+    ) -> Optional[Tuple['SingleQubitCliffordGate', complex]]:
+        """Creates Clifford gate with given unitary, including global phase.
+
+        Args:
+            u: 2x2 unitary matrix of a Clifford gate.
+
+        Returns:
+            A tuple of a SingleQubitCliffordGate and a global phase, such that
+            the gate unitary (as given by `cirq.unitary`) times the global phase
+            is identical to the given unitary `u`; or `None` if `u` is not the
+            matrix of a single-qubit Clifford gate.
+        """
+        gate = cls.from_unitary(u)
+        if gate is None:
+            return None
+        # Find the entry with the largest magnitude in the input unitary, to find
+        # the global phase difference between the input unitary and the gate unitary.
+        k = max(np.ndindex(*u.shape), key=lambda t: abs(u[t]))
+        return gate, u[k] / protocols.unitary(gate)[k]
+
     def pauli_tuple(self, pauli: Pauli) -> Tuple[Pauli, bool]:
         """Returns a tuple of a Pauli operator and a boolean.
 
@@ -730,10 +753,7 @@ def _act_on_(
     # Single Clifford Gate decomposition is more efficient than the general Tableau decomposition.
     def _decompose_(self, qubits: Sequence['cirq.Qid']) -> 'cirq.OP_TREE':
         (qubit,) = qubits
-        if self == SingleQubitCliffordGate.H:
-            return (common_gates.H(qubit),)
-        rotations = self.decompose_rotation()
-        return tuple(r.on(qubit) ** (qt / 2) for r, qt in rotations)
+        return tuple(gate.on(qubit) for gate in self.decompose_gate())
 
     def _commutes_(
         self, other: Any, *, atol: float = 1e-8
@@ -773,10 +793,29 @@ def _unitary_(self) -> np.ndarray:
             mat = protocols.unitary(op).dot(mat)
         return mat
 
+    def decompose_gate(self) -> Sequence['cirq.Gate']:
+        """Decomposes this clifford into a series of H and pauli rotation gates.
+
+        Returns:
+            A sequence of H and pauli rotation gates which are equivalent to this
+            clifford gate if applied in order. This decomposition agrees with
+            cirq.unitary(self), including global phase.
+        """
+        if self == SingleQubitCliffordGate.H:
+            return [common_gates.H]
+        rotations = self.decompose_rotation()
+        return [r ** (qt / 2) for r, qt in rotations]
+
     def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
-        """Returns ((first_rotation_axis, first_rotation_quarter_turns), ...)
+        """Decomposes this clifford into a series of pauli rotations.
 
-        This is a sequence of zero, one, or two rotations."""
+        Each rotation is given as a tuple of (axis, quarter_turns),
+        where axis is a Pauli giving the axis to rotate about. The
+        result will be a sequence of zero, one, or two rotations.
+
+        Note that the combined unitary effect of these rotations may
+        differ from cirq.unitary(self) by a global phase.
+        """
         x_rot = self.pauli_tuple(pauli_gates.X)
         y_rot = self.pauli_tuple(pauli_gates.Y)
         z_rot = self.pauli_tuple(pauli_gates.Z)
diff --git a/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py b/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py
--- a/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py
+++ b/cirq-core/cirq/sim/clifford/stabilizer_simulation_state.py
@@ -161,21 +161,14 @@ def _strat_act_from_single_qubit_decompose(
             if not has_unitary(val):
                 return NotImplemented
             u = unitary(val)
-            clifford_gate = SingleQubitCliffordGate.from_unitary(u)
-            if clifford_gate is not None:
-                # Gather the effective unitary applied so as to correct for the
-                # global phase later.
-                final_unitary = np.eye(2)
-                for axis, quarter_turns in clifford_gate.decompose_rotation():
-                    gate = axis ** (quarter_turns / 2)
+            gate_and_phase = SingleQubitCliffordGate.from_unitary_with_global_phase(u)
+            if gate_and_phase is not None:
+                clifford_gate, global_phase = gate_and_phase
+                # Apply gates.
+                for gate in clifford_gate.decompose_gate():
                     self._strat_apply_gate(gate, qubits)
-                    final_unitary = np.matmul(unitary(gate), final_unitary)
-
-                # Find the entry with the largest magnitude in the input unitary.
-                k = max(np.ndindex(*u.shape), key=lambda t: abs(u[t]))
-                # Correct the global phase that wasn't conserved in the above
-                # decomposition.
-                self._state.apply_global_phase(u[k] / final_unitary[k])
+                # Apply global phase.
+                self._state.apply_global_phase(global_phase)
                 return True
 
         return NotImplemented


Test patch:
diff --git a/cirq-core/cirq/ops/clifford_gate_test.py b/cirq-core/cirq/ops/clifford_gate_test.py
--- a/cirq-core/cirq/ops/clifford_gate_test.py
+++ b/cirq-core/cirq/ops/clifford_gate_test.py
@@ -530,6 +530,10 @@ def test_from_unitary(clifford_gate):
     result_gate = cirq.SingleQubitCliffordGate.from_unitary(u)
     assert result_gate == clifford_gate
 
+    result_gate2, global_phase = cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u)
+    assert result_gate2 == result_gate
+    assert np.allclose(cirq.unitary(result_gate2) * global_phase, u)
+
 
 def test_from_unitary_with_phase_shift():
     u = np.exp(0.42j) * cirq.unitary(cirq.SingleQubitCliffordGate.Y_sqrt)
@@ -537,19 +541,48 @@ def test_from_unitary_with_phase_shift():
 
     assert gate == cirq.SingleQubitCliffordGate.Y_sqrt
 
+    gate2, global_phase = cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u)
+    assert gate2 == gate
+    assert np.allclose(cirq.unitary(gate2) * global_phase, u)
+
 
 def test_from_unitary_not_clifford():
     # Not a single-qubit gate.
     u = cirq.unitary(cirq.CNOT)
     assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
+    assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
 
     # Not an unitary matrix.
     u = 2 * cirq.unitary(cirq.X)
     assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
+    assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
 
     # Not a Clifford gate.
     u = cirq.unitary(cirq.T)
     assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
+    assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
+
+
+@pytest.mark.parametrize(
+    "clifford_gate",
+    (
+        cirq.SingleQubitCliffordGate.I,
+        cirq.SingleQubitCliffordGate.H,
+        cirq.SingleQubitCliffordGate.X,
+        cirq.SingleQubitCliffordGate.Y,
+        cirq.SingleQubitCliffordGate.Z,
+        cirq.SingleQubitCliffordGate.X_sqrt,
+        cirq.SingleQubitCliffordGate.Y_sqrt,
+        cirq.SingleQubitCliffordGate.Z_sqrt,
+        cirq.SingleQubitCliffordGate.X_nsqrt,
+        cirq.SingleQubitCliffordGate.Y_nsqrt,
+        cirq.SingleQubitCliffordGate.Z_nsqrt,
+    ),
+)
+def test_decompose_gate(clifford_gate):
+    gates = clifford_gate.decompose_gate()
+    u = functools.reduce(np.dot, [np.eye(2), *(cirq.unitary(gate) for gate in reversed(gates))])
+    assert np.allclose(u, cirq.unitary(clifford_gate))  # No global phase difference.
 
 
 @pytest.mark.parametrize('trans_x,trans_z', _all_rotation_pairs())