Skip to content

Commit 05b2052

Browse files
authored
Add method to get a Clifford gate and phase global phase from unitary (quantumlib#5568)
Fixes quantumlib#2847 Review: @viathor
1 parent 6ab8628 commit 05b2052

File tree

3 files changed

+85
-20
lines changed

3 files changed

+85
-20
lines changed

cirq/ops/clifford_gate.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,29 @@ def from_unitary(u: np.ndarray) -> Optional['SingleQubitCliffordGate']:
620620
_to_clifford_tableau(x_to=x_to, z_to=z_to)
621621
)
622622

623+
@classmethod
624+
def from_unitary_with_global_phase(
625+
cls, u: np.ndarray
626+
) -> Optional[Tuple['SingleQubitCliffordGate', complex]]:
627+
"""Creates Clifford gate with given unitary, including global phase.
628+
629+
Args:
630+
u: 2x2 unitary matrix of a Clifford gate.
631+
632+
Returns:
633+
A tuple of a SingleQubitCliffordGate and a global phase, such that
634+
the gate unitary (as given by `cirq.unitary`) times the global phase
635+
is identical to the given unitary `u`; or `None` if `u` is not the
636+
matrix of a single-qubit Clifford gate.
637+
"""
638+
gate = cls.from_unitary(u)
639+
if gate is None:
640+
return None
641+
# Find the entry with the largest magnitude in the input unitary, to find
642+
# the global phase difference between the input unitary and the gate unitary.
643+
k = max(np.ndindex(*u.shape), key=lambda t: abs(u[t]))
644+
return gate, u[k] / protocols.unitary(gate)[k]
645+
623646
def pauli_tuple(self, pauli: Pauli) -> Tuple[Pauli, bool]:
624647
"""Returns a tuple of a Pauli operator and a boolean.
625648
@@ -730,10 +753,7 @@ def _act_on_(
730753
# Single Clifford Gate decomposition is more efficient than the general Tableau decomposition.
731754
def _decompose_(self, qubits: Sequence['cirq.Qid']) -> 'cirq.OP_TREE':
732755
(qubit,) = qubits
733-
if self == SingleQubitCliffordGate.H:
734-
return (common_gates.H(qubit),)
735-
rotations = self.decompose_rotation()
736-
return tuple(r.on(qubit) ** (qt / 2) for r, qt in rotations)
756+
return tuple(gate.on(qubit) for gate in self.decompose_gate())
737757

738758
def _commutes_(
739759
self, other: Any, *, atol: float = 1e-8
@@ -773,10 +793,29 @@ def _unitary_(self) -> np.ndarray:
773793
mat = protocols.unitary(op).dot(mat)
774794
return mat
775795

796+
def decompose_gate(self) -> Sequence['cirq.Gate']:
797+
"""Decomposes this clifford into a series of H and pauli rotation gates.
798+
799+
Returns:
800+
A sequence of H and pauli rotation gates which are equivalent to this
801+
clifford gate if applied in order. This decomposition agrees with
802+
cirq.unitary(self), including global phase.
803+
"""
804+
if self == SingleQubitCliffordGate.H:
805+
return [common_gates.H]
806+
rotations = self.decompose_rotation()
807+
return [r ** (qt / 2) for r, qt in rotations]
808+
776809
def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
777-
"""Returns ((first_rotation_axis, first_rotation_quarter_turns), ...)
810+
"""Decomposes this clifford into a series of pauli rotations.
778811
779-
This is a sequence of zero, one, or two rotations."""
812+
Each rotation is given as a tuple of (axis, quarter_turns),
813+
where axis is a Pauli giving the axis to rotate about. The
814+
result will be a sequence of zero, one, or two rotations.
815+
816+
Note that the combined unitary effect of these rotations may
817+
differ from cirq.unitary(self) by a global phase.
818+
"""
780819
x_rot = self.pauli_tuple(pauli_gates.X)
781820
y_rot = self.pauli_tuple(pauli_gates.Y)
782821
z_rot = self.pauli_tuple(pauli_gates.Z)

cirq/ops/clifford_gate_test.py

+33
Original file line numberDiff line numberDiff line change
@@ -530,26 +530,59 @@ def test_from_unitary(clifford_gate):
530530
result_gate = cirq.SingleQubitCliffordGate.from_unitary(u)
531531
assert result_gate == clifford_gate
532532

533+
result_gate2, global_phase = cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u)
534+
assert result_gate2 == result_gate
535+
assert np.allclose(cirq.unitary(result_gate2) * global_phase, u)
536+
533537

534538
def test_from_unitary_with_phase_shift():
535539
u = np.exp(0.42j) * cirq.unitary(cirq.SingleQubitCliffordGate.Y_sqrt)
536540
gate = cirq.SingleQubitCliffordGate.from_unitary(u)
537541

538542
assert gate == cirq.SingleQubitCliffordGate.Y_sqrt
539543

544+
gate2, global_phase = cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u)
545+
assert gate2 == gate
546+
assert np.allclose(cirq.unitary(gate2) * global_phase, u)
547+
540548

541549
def test_from_unitary_not_clifford():
542550
# Not a single-qubit gate.
543551
u = cirq.unitary(cirq.CNOT)
544552
assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
553+
assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
545554

546555
# Not an unitary matrix.
547556
u = 2 * cirq.unitary(cirq.X)
548557
assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
558+
assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
549559

550560
# Not a Clifford gate.
551561
u = cirq.unitary(cirq.T)
552562
assert cirq.SingleQubitCliffordGate.from_unitary(u) is None
563+
assert cirq.SingleQubitCliffordGate.from_unitary_with_global_phase(u) is None
564+
565+
566+
@pytest.mark.parametrize(
567+
"clifford_gate",
568+
(
569+
cirq.SingleQubitCliffordGate.I,
570+
cirq.SingleQubitCliffordGate.H,
571+
cirq.SingleQubitCliffordGate.X,
572+
cirq.SingleQubitCliffordGate.Y,
573+
cirq.SingleQubitCliffordGate.Z,
574+
cirq.SingleQubitCliffordGate.X_sqrt,
575+
cirq.SingleQubitCliffordGate.Y_sqrt,
576+
cirq.SingleQubitCliffordGate.Z_sqrt,
577+
cirq.SingleQubitCliffordGate.X_nsqrt,
578+
cirq.SingleQubitCliffordGate.Y_nsqrt,
579+
cirq.SingleQubitCliffordGate.Z_nsqrt,
580+
),
581+
)
582+
def test_decompose_gate(clifford_gate):
583+
gates = clifford_gate.decompose_gate()
584+
u = functools.reduce(np.dot, [np.eye(2), *(cirq.unitary(gate) for gate in reversed(gates))])
585+
assert np.allclose(u, cirq.unitary(clifford_gate)) # No global phase difference.
553586

554587

555588
@pytest.mark.parametrize('trans_x,trans_z', _all_rotation_pairs())

cirq/sim/clifford/stabilizer_simulation_state.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,14 @@ def _strat_act_from_single_qubit_decompose(
161161
if not has_unitary(val):
162162
return NotImplemented
163163
u = unitary(val)
164-
clifford_gate = SingleQubitCliffordGate.from_unitary(u)
165-
if clifford_gate is not None:
166-
# Gather the effective unitary applied so as to correct for the
167-
# global phase later.
168-
final_unitary = np.eye(2)
169-
for axis, quarter_turns in clifford_gate.decompose_rotation():
170-
gate = axis ** (quarter_turns / 2)
164+
gate_and_phase = SingleQubitCliffordGate.from_unitary_with_global_phase(u)
165+
if gate_and_phase is not None:
166+
clifford_gate, global_phase = gate_and_phase
167+
# Apply gates.
168+
for gate in clifford_gate.decompose_gate():
171169
self._strat_apply_gate(gate, qubits)
172-
final_unitary = np.matmul(unitary(gate), final_unitary)
173-
174-
# Find the entry with the largest magnitude in the input unitary.
175-
k = max(np.ndindex(*u.shape), key=lambda t: abs(u[t]))
176-
# Correct the global phase that wasn't conserved in the above
177-
# decomposition.
178-
self._state.apply_global_phase(u[k] / final_unitary[k])
170+
# Apply global phase.
171+
self._state.apply_global_phase(global_phase)
179172
return True
180173

181174
return NotImplemented

0 commit comments

Comments
 (0)