/
single_qubit_decompositions.py
233 lines (178 loc) · 8.24 KB
/
single_qubit_decompositions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Copyright 2018 The Cirq Developers
#
# 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.
"""Utility methods related to optimizing quantum circuits."""
import math
from typing import List, Optional, Tuple
import numpy as np
import sympy
from cirq import ops, linalg, protocols
from cirq.linalg.tolerance import near_zero_mod
def is_negligible_turn(turns: float, tolerance: float) -> bool:
"""Returns True is the number of turns in a gate is close to zero."""
if isinstance(turns, sympy.Expr):
if not turns.is_constant():
return False
turns = float(turns)
return abs(_signed_mod_1(turns)) <= tolerance
def _signed_mod_1(x: float) -> float:
return (x + 0.5) % 1 - 0.5
def single_qubit_matrix_to_pauli_rotations(
mat: np.ndarray, atol: float = 0
) -> List[Tuple[ops.Pauli, float]]:
"""Implements a single-qubit operation with few rotations.
Args:
mat: The 2x2 unitary matrix of the operation to implement.
atol: A limit on the amount of absolute error introduced by the
construction.
Returns:
A list of (Pauli, half_turns) tuples that, when applied in order,
perform the desired operation.
"""
def is_clifford_rotation(half_turns):
return near_zero_mod(half_turns, 0.5, atol=atol)
def to_quarter_turns(half_turns):
return round(2 * half_turns) % 4
def is_quarter_turn(half_turns):
return is_clifford_rotation(half_turns) and to_quarter_turns(half_turns) % 2 == 1
def is_half_turn(half_turns):
return is_clifford_rotation(half_turns) and to_quarter_turns(half_turns) == 2
def is_no_turn(half_turns):
return is_clifford_rotation(half_turns) and to_quarter_turns(half_turns) == 0
# Decompose matrix
z_rad_before, y_rad, z_rad_after = linalg.deconstruct_single_qubit_matrix_into_angles(mat)
z_ht_before = z_rad_before / np.pi - 0.5
m_ht = y_rad / np.pi
m_pauli: ops.Pauli = ops.X
z_ht_after = z_rad_after / np.pi + 0.5
# Clean up angles
if is_clifford_rotation(z_ht_before):
if (is_quarter_turn(z_ht_before) or is_quarter_turn(z_ht_after)) ^ (
is_half_turn(m_ht) and is_no_turn(z_ht_before - z_ht_after)
):
z_ht_before += 0.5
z_ht_after -= 0.5
m_pauli = ops.Y
if is_half_turn(z_ht_before) or is_half_turn(z_ht_after):
z_ht_before -= 1
z_ht_after += 1
m_ht = -m_ht
if is_no_turn(m_ht):
z_ht_before += z_ht_after
z_ht_after = 0
elif is_half_turn(m_ht):
z_ht_after -= z_ht_before
z_ht_before = 0
# Generate operations
rotation_list = [(ops.Z, z_ht_before), (m_pauli, m_ht), (ops.Z, z_ht_after)]
return [(pauli, ht) for pauli, ht in rotation_list if not is_no_turn(ht)]
def single_qubit_matrix_to_gates(mat: np.ndarray, tolerance: float = 0) -> List[ops.Gate]:
"""Implements a single-qubit operation with few gates.
Args:
mat: The 2x2 unitary matrix of the operation to implement.
tolerance: A limit on the amount of error introduced by the
construction.
Returns:
A list of gates that, when applied in order, perform the desired
operation.
"""
rotations = single_qubit_matrix_to_pauli_rotations(mat, tolerance)
return [pauli**ht for pauli, ht in rotations]
def single_qubit_op_to_framed_phase_form(mat: np.ndarray) -> Tuple[np.ndarray, complex, complex]:
"""Decomposes a 2x2 unitary M into U^-1 * diag(1, r) * U * diag(g, g).
U translates the rotation axis of M to the Z axis.
g fixes a global phase factor difference caused by the translation.
r's phase is the amount of rotation around M's rotation axis.
This decomposition can be used to decompose controlled single-qubit
rotations into controlled-Z operations bordered by single-qubit operations.
Args:
mat: The qubit operation as a 2x2 unitary matrix.
Returns:
A 2x2 unitary U, the complex relative phase factor r, and the complex
global phase factor g. Applying M is equivalent (up to global phase) to
applying U, rotating around the Z axis to apply r, then un-applying U.
When M is controlled, the control must be rotated around the Z axis to
apply g.
"""
vals, vecs = linalg.unitary_eig(mat)
u = np.conj(vecs).T
r = vals[1] / vals[0]
g = vals[0]
return u, r, g
def _deconstruct_single_qubit_matrix_into_gate_turns(mat: np.ndarray) -> Tuple[float, float, float]:
"""Breaks down a 2x2 unitary into gate parameters.
Args:
mat: The 2x2 unitary matrix to break down.
Returns:
A tuple containing the amount to rotate around an XY axis, the phase of
that axis, and the amount to phase around Z. All results will be in
fractions of a whole turn, with values canonicalized into the range
[-0.5, 0.5).
"""
pre_phase, rotation, post_phase = linalg.deconstruct_single_qubit_matrix_into_angles(mat)
# Figure out parameters of the actual gates we will do.
tau = 2 * np.pi
xy_turn = rotation / tau
xy_phase_turn = 0.25 - pre_phase / tau
total_z_turn = (post_phase + pre_phase) / tau
# Normalize turns into the range [-0.5, 0.5).
return (_signed_mod_1(xy_turn), _signed_mod_1(xy_phase_turn), _signed_mod_1(total_z_turn))
def single_qubit_matrix_to_phased_x_z(mat: np.ndarray, atol: float = 0) -> List[ops.Gate]:
"""Implements a single-qubit operation with a PhasedX and Z gate.
If one of the gates isn't needed, it will be omitted.
Args:
mat: The 2x2 unitary matrix of the operation to implement.
atol: A limit on the amount of error introduced by the
construction.
Returns:
A list of gates that, when applied in order, perform the desired
operation.
"""
xy_turn, xy_phase_turn, total_z_turn = _deconstruct_single_qubit_matrix_into_gate_turns(mat)
# Build the intended operation out of non-negligible XY and Z rotations.
result = [
ops.PhasedXPowGate(exponent=2 * xy_turn, phase_exponent=2 * xy_phase_turn),
ops.Z ** (2 * total_z_turn),
]
result = [g for g in result if protocols.trace_distance_bound(g) > atol]
# Special case: XY half-turns can absorb Z rotations.
if len(result) == 2 and math.isclose(abs(xy_turn), 0.5, abs_tol=atol):
return [ops.PhasedXPowGate(phase_exponent=2 * xy_phase_turn + total_z_turn)]
return result
def single_qubit_matrix_to_phxz(mat: np.ndarray, atol: float = 0) -> Optional[ops.PhasedXZGate]:
"""Implements a single-qubit operation with a PhasedXZ gate.
Under the hood, this uses deconstruct_single_qubit_matrix_into_angles which
converts the given matrix to a series of three rotations around the Z, Y, Z
axes. This is then converted to a phased X rotation followed by a Z, in the
form of a single PhasedXZ gate.
Args:
mat: The 2x2 unitary matrix of the operation to implement.
atol: A limit on the amount of error introduced by the
construction.
Returns:
A PhasedXZ gate that implements the given matrix, or None if it is
close to identity (trace distance <= atol).
"""
xy_turn, xy_phase_turn, total_z_turn = _deconstruct_single_qubit_matrix_into_gate_turns(mat)
# Build the intended operation out of non-negligible XY and Z rotations.
g = ops.PhasedXZGate(
axis_phase_exponent=2 * xy_phase_turn, x_exponent=2 * xy_turn, z_exponent=2 * total_z_turn
)
if protocols.trace_distance_bound(g) <= atol:
return None
# Special case: XY half-turns can absorb Z rotations.
if math.isclose(abs(xy_turn), 0.5, abs_tol=atol):
g = ops.PhasedXZGate(
axis_phase_exponent=2 * xy_phase_turn + total_z_turn, x_exponent=1, z_exponent=0
)
return g