Skip to content

Commit efcdfdc

Browse files
mergify[bot]alexanderivriiCryorisElePT
authored
Bug fixes in Solovay Kitaev Decomposition (backport #14217) (#14294)
* Bug fixes in Solovay Kitaev Decomposition (#14217) * Fix _compute_rotation_axis method The method used to return [0, 0, 0] for rotation axis when theta is very close to 0 (and yet not within 1e010). This commit fixes this by noting that the rotation axis of an SO(3) matrix is simply the eigenvector of this matrix corresponding to eigenvalue 1. * Fixes related to SU(2) vs. SO(3) The implemented algorithm has a global phase uncertainty of +-1, due to approximating not the original SU(2) matrix but its projection onto SO(3). This fixes the global phase of the computed approximation. In addition, the product_su2 matrix in GateSequence was not correctly computed in many cases, leading to incorrect values when using this attribute. Since this is not needed for the actual algorithm (and spends unnecessary effort for computing it), this also completely removes this attribute. Co-authored-by: Almudena Carrera Vazquez <[email protected]> * Fix the tests. One of the tests was simply wrong because the reference circuit has the wrong global phase. The tests that check the gates in the final decomposition should not assume that SGate is included and SdgGate is not. * reno * fix to the global phase * bug fix (forgetting that gate_matrix_su2 is not SU(2) was a sequence) * adding test * Restoring the previous fast code for _compute_rotation_axis based on Rodrigues formula, while additionally handling the case of 180-degree rotation * removing old code * review comments * switching back to using _to_dag and _to_circuit I actually no longer think that these methods would ever return a circuit with unitary gates, so it does not really matter * Addressing review comments * pass over release notes combining the original and the suggested wordings * Update releasenotes/notes/fix-sk-decomposition-23da3ee4b6a10d62.yaml Co-authored-by: Julien Gacon <[email protected]> --------- Co-authored-by: Almudena Carrera Vazquez <[email protected]> Co-authored-by: Julien Gacon <[email protected]> (cherry picked from commit 6892608) # Conflicts: # test/python/transpiler/test_solovay_kitaev.py * Removing tests added in PR #13690 that was not backported to 1.4 --------- Co-authored-by: Alexander Ivrii <[email protected]> Co-authored-by: Julien Gacon <[email protected]> Co-authored-by: Elena Peña Tapia <[email protected]>
1 parent b90d14e commit efcdfdc

File tree

6 files changed

+114
-45
lines changed

6 files changed

+114
-45
lines changed

qiskit/synthesis/discrete_basis/commutator_decompose.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,40 @@ def _compute_rotation_axis(matrix: np.ndarray) -> np.ndarray:
5353
"""
5454
_check_is_so3(matrix)
5555

56+
# If theta represents the rotation angle, then trace = 1 + 2cos(theta).
5657
trace = _compute_trace_so3(matrix)
57-
theta = math.acos(0.5 * (trace - 1))
58-
if math.sin(theta) > 1e-10:
59-
x = 1 / (2 * math.sin(theta)) * (matrix[2][1] - matrix[1][2])
60-
y = 1 / (2 * math.sin(theta)) * (matrix[0][2] - matrix[2][0])
61-
z = 1 / (2 * math.sin(theta)) * (matrix[1][0] - matrix[0][1])
62-
else:
58+
59+
if trace >= 3 - 1e-10:
60+
# The matrix is the identity (rotation by 0)
6361
x = 1.0
6462
y = 0.0
6563
z = 0.0
64+
65+
elif trace <= -1 + 1e-10:
66+
# The matrix is the 180-degree rotation
67+
squares = (1 + np.diagonal(matrix)) / 2
68+
index_of_max = np.argmax(squares)
69+
70+
if index_of_max == 0:
71+
x = math.sqrt(squares[0])
72+
y = matrix[0][1] / (2 * x)
73+
z = matrix[0][2] / (2 * x)
74+
elif index_of_max == 1:
75+
y = math.sqrt(squares[1])
76+
x = matrix[0][1] / (2 * y)
77+
z = matrix[1][2] / (2 * y)
78+
else:
79+
z = math.sqrt(squares[2])
80+
x = matrix[0][2] / (2 * z)
81+
y = matrix[1][2] / (2 * z)
82+
83+
else:
84+
# The matrix is the rotation by theta with sin(theta)!=0
85+
theta = math.acos(0.5 * (trace - 1))
86+
x = 1 / (2 * math.sin(theta)) * (matrix[2][1] - matrix[1][2])
87+
y = 1 / (2 * math.sin(theta)) * (matrix[0][2] - matrix[2][0])
88+
z = 1 / (2 * math.sin(theta)) * (matrix[1][0] - matrix[0][1])
89+
6690
return np.array([x, y, z])
6791

6892

qiskit/synthesis/discrete_basis/gate_sequence.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def __init__(self, gates: Sequence[Gate] = ()) -> None:
5555
self.name = " ".join(self.labels)
5656
self.global_phase = global_phase
5757
self.product = so3_matrix
58-
self.product_su2 = su2_matrix
5958

6059
def remove_cancelling_pair(self, indices: Sequence[int]) -> None:
6160
"""Remove a pair of indices that cancel each other and *do not* change the matrices."""
@@ -106,6 +105,16 @@ def to_circuit(self):
106105

107106
return circuit
108107

108+
def _to_u2(self):
109+
"""Creates the U2 matrix corresponding to the stored sequence of gates
110+
and the global phase.
111+
"""
112+
u2 = np.eye(2, dtype=complex)
113+
for mat in self.gates:
114+
u2 = mat.to_matrix().dot(u2)
115+
u2 = np.exp(1j * self.global_phase) * u2
116+
return u2
117+
109118
def to_dag(self):
110119
"""Convert to a :class:`.DAGCircuit`.
111120
@@ -149,7 +158,6 @@ def append(self, gate: Gate) -> "GateSequence":
149158
so3 = _convert_su2_to_so3(su2)
150159

151160
self.product = so3.dot(self.product)
152-
self.product_su2 = su2.dot(self.product_su2)
153161
self.global_phase = self.global_phase + phase
154162

155163
self.gates.append(gate)
@@ -172,7 +180,6 @@ def adjoint(self) -> "GateSequence":
172180
adjoint.labels = [inv.name for inv in adjoint.gates]
173181
adjoint.name = " ".join(adjoint.labels)
174182
adjoint.product = np.conj(self.product).T
175-
adjoint.product_su2 = np.conj(self.product_su2).T
176183
adjoint.global_phase = -self.global_phase
177184

178185
return adjoint
@@ -190,7 +197,6 @@ def copy(self) -> "GateSequence":
190197
out.matrices = self.matrices.copy()
191198
out.global_phase = self.global_phase
192199
out.product = self.product.copy()
193-
out.product_su2 = self.product_su2.copy()
194200
out.name = self.name
195201
out._eulers = self._eulers
196202
return out

qiskit/synthesis/discrete_basis/generate_basis_approximations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _check_candidate_greedy(candidate, existing_sequences, tol=1e-10):
7676
return False
7777

7878
for existing in existing_sequences:
79-
if matrix_equal(existing.product_su2, candidate.product_su2, ignore_phase=True, atol=tol):
79+
if matrix_equal(existing.product, candidate.product, ignore_phase=True, atol=tol):
8080
# is the new sequence less or more efficient?
8181
return len(candidate.gates) < len(existing.gates)
8282
return True

qiskit/synthesis/discrete_basis/solovay_kitaev.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
class SolovayKitaevDecomposition:
2525
"""The Solovay Kitaev discrete decomposition algorithm.
2626
27-
This class is called recursively by the transpiler pass, which is why it is separeted.
27+
This class is called recursively by the transpiler pass, which is why it is separated.
2828
See :class:`qiskit.transpiler.passes.SolovayKitaev` for more information.
2929
"""
3030

@@ -33,7 +33,7 @@ def __init__(
3333
) -> None:
3434
"""
3535
Args:
36-
basic_approximations: A specification of the basic SU(2) approximations in terms
36+
basic_approximations: A specification of the basic SO(3) approximations in terms
3737
of discrete gates. At each iteration this algorithm, the remaining error is
3838
approximated with the closest sequence of gates in this set.
3939
If a ``str``, this specifies a ``.npy`` filename from which to load the
@@ -116,23 +116,33 @@ def run(
116116
"""
117117
# make input matrix SU(2) and get the according global phase
118118
z = 1 / np.sqrt(np.linalg.det(gate_matrix))
119-
gate_matrix_su2 = GateSequence.from_matrix(z * gate_matrix)
119+
120+
gate_matrix_su2 = z * gate_matrix
121+
gate_matrix_as_sequence = GateSequence.from_matrix(gate_matrix_su2)
120122
global_phase = np.arctan2(np.imag(z), np.real(z))
121123

122124
# get the decomposition as GateSequence type
123-
decomposition = self._recurse(gate_matrix_su2, recursion_degree, check_input=check_input)
125+
decomposition = self._recurse(
126+
gate_matrix_as_sequence, recursion_degree, check_input=check_input
127+
)
124128

125129
# simplify
126130
_remove_identities(decomposition)
127131
_remove_inverse_follows_gate(decomposition)
128132

133+
# adjust to the correct SU(2) phase
134+
adjust_phase = (
135+
np.pi if _should_adjust_phase(decomposition._to_u2(), gate_matrix_su2) else 0.0
136+
)
137+
129138
# convert to a circuit and attach the right phases
130139
if return_dag:
131140
out = decomposition.to_dag()
132141
else:
133142
out = decomposition.to_circuit()
134143

135-
out.global_phase = decomposition.global_phase - global_phase
144+
out.global_phase += adjust_phase
145+
out.global_phase -= global_phase
136146

137147
return out
138148

@@ -155,17 +165,20 @@ def _recurse(self, sequence: GateSequence, n: int, check_input: bool = True) ->
155165
raise ValueError("Shape of U must be (3, 3) but is", sequence.shape)
156166

157167
if n == 0:
158-
return self.find_basic_approximation(sequence)
168+
res = self.find_basic_approximation(sequence)
159169

160-
u_n1 = self._recurse(sequence, n - 1, check_input=check_input)
170+
else:
171+
u_n1 = self._recurse(sequence, n - 1, check_input=check_input)
161172

162-
v_n, w_n = commutator_decompose(
163-
sequence.dot(u_n1.adjoint()).product, check_input=check_input
164-
)
173+
v_n, w_n = commutator_decompose(
174+
sequence.dot(u_n1.adjoint()).product, check_input=check_input
175+
)
176+
177+
v_n1 = self._recurse(v_n, n - 1, check_input=check_input)
178+
w_n1 = self._recurse(w_n, n - 1, check_input=check_input)
179+
res = v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1)
165180

166-
v_n1 = self._recurse(v_n, n - 1, check_input=check_input)
167-
w_n1 = self._recurse(w_n, n - 1, check_input=check_input)
168-
return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1)
181+
return res
169182

170183
def find_basic_approximation(self, sequence: GateSequence) -> GateSequence:
171184
"""Find ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``.
@@ -215,3 +228,13 @@ def _remove_identities(sequence):
215228
sequence.gates.pop(index)
216229
else:
217230
index += 1
231+
232+
233+
def _should_adjust_phase(computed: np.ndarray, target: np.ndarray) -> bool:
234+
"""
235+
The implemented SolovayKitaevDecomposition has a global phase uncertainty of +-1,
236+
due to approximating not the original SU(2) matrix but its projection onto SO(3).
237+
This function returns ``True`` if the global phase of the computed approximation
238+
should be adjusted (by adding pi) to better much the target.
239+
"""
240+
return np.linalg.norm(-computed - target) < np.linalg.norm(computed - target)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed a problem in the :class:`.SolovayKitaev` transpiler pass where the pass could
5+
crash due to encountering a 180 degree rotation in the internal recursion,
6+
which was not handled correctly.
7+
- |
8+
Fixed a problem in the :class:`.SolovayKitaev` transpiler pass where the generated
9+
approximation could have a phase that differs by :math:`\pi` from the correct value.
10+
This resulted due to the internal :math:`SO(3)` representation, which requires additional
11+
handling to obtain the correct sign of the qubit gate matrix.
12+
Fixed `#9552 <https://github.com/Qiskit/qiskit-terra/issues/9552>`__

test/python/transpiler/test_solovay_kitaev.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,7 @@ def test_unitary_synthesis(self):
9898
passes = PassManager([_1q, _cons, _synth])
9999
compiled = passes.run(circuit)
100100

101-
diff = np.linalg.norm(Operator(compiled) - Operator(circuit))
102-
self.assertLess(diff, 1)
103-
self.assertEqual(set(compiled.count_ops().keys()), {"h", "s", "cx"})
101+
self.assertLessEqual(set(compiled.count_ops().keys()), {"h", "s", "sdg", "cx"})
104102

105103
def test_plugin(self):
106104
"""Test calling the plugin directly."""
@@ -112,12 +110,7 @@ def test_plugin(self):
112110
plugin = SolovayKitaevSynthesis()
113111
out = plugin.run(unitary, basis_gates=["h", "s"])
114112

115-
reference = QuantumCircuit(1, global_phase=3 * np.pi / 4)
116-
reference.h(0)
117-
reference.s(0)
118-
reference.h(0)
119-
120-
self.assertEqual(dag_to_circuit(out), reference)
113+
self.assertLessEqual(set(out.count_ops().keys()), {"h", "s", "sdg", "cx"})
121114

122115
def test_multiple_plugins(self):
123116
"""Test calling the plugins directly but with different instances of basis set."""
@@ -199,11 +192,14 @@ def test_str_basis_gates(self):
199192
dag = circuit_to_dag(circuit)
200193
discretized = dag_to_circuit(synth.run(dag))
201194

202-
reference = QuantumCircuit(1, global_phase=7 * np.pi / 8)
195+
reference = QuantumCircuit(1, global_phase=15 * np.pi / 8)
203196
reference.h(0)
204197
reference.t(0)
205198
reference.h(0)
206199

200+
# Make sure that the discretized circuit gives a valid approximation
201+
diff = _trace_distance(circuit, discretized)
202+
self.assertLess(diff, 0.01)
207203
self.assertEqual(discretized, reference)
208204

209205
def test_approximation_on_qft(self):
@@ -242,14 +238,14 @@ def test_u_gates_work(self):
242238
circuit.u(np.pi / 2, 0, 15 * np.pi / 16, 0)
243239

244240
depth = 4
245-
basis_gates = ["h", "t", "tdg", "s", "z"]
241+
basis_gates = ["h", "t", "tdg", "s", "sdg", "z"]
246242
gate_approx_library = generate_basic_approximations(basis_gates=basis_gates, depth=depth)
247243

248244
skd = SolovayKitaev(recursion_degree=2, basic_approximations=gate_approx_library)
249245
discretized = skd(circuit)
250246

251247
included_gates = set(discretized.count_ops().keys())
252-
self.assertEqual(set(basis_gates), included_gates)
248+
self.assertLessEqual(included_gates, set(basis_gates))
253249

254250
def test_load_from_file(self):
255251
"""Test loading basic approximations from a file works.
@@ -262,24 +258,32 @@ def test_load_from_file(self):
262258
fullpath = os.path.join(tmp_dir, filename)
263259

264260
# dump approximations to file
265-
generate_basic_approximations(basis_gates=["h", "s", "sdg"], depth=3, filename=fullpath)
261+
gate_approx_library = generate_basic_approximations(
262+
basis_gates=["h", "s", "sdg"], depth=3, filename=fullpath
263+
)
266264

267265
# circuit to decompose and reference decomp
268266
circuit = QuantumCircuit(1)
269267
circuit.rx(0.8, 0)
270268

271-
reference = QuantumCircuit(1, global_phase=3 * np.pi / 4)
272-
reference.h(0)
273-
reference.s(0)
274-
reference.h(0)
269+
# Run SK pass using gate_approx_library
270+
reference = SolovayKitaev(basic_approximations=gate_approx_library)(circuit)
275271

276-
# load the decomp and compare to reference
277-
skd = SolovayKitaev(basic_approximations=fullpath)
278-
# skd = SolovayKitaev(basic_approximations=filename)
279-
discretized = skd(circuit)
272+
# Run SK pass using stored basis_approximations
273+
discretized = SolovayKitaev(basic_approximations=fullpath)(circuit)
280274

275+
# Check that both flows produce the same result
281276
self.assertEqual(discretized, reference)
282277

278+
def test_y_gate(self):
279+
"""Test the Solovay-Kitaev decomposition on the circuit with a Y-gate (see issue #9552)."""
280+
circuit = QuantumCircuit(1)
281+
circuit.y(0)
282+
283+
transpiled = SolovayKitaev()(circuit)
284+
diff = _trace_distance(circuit, transpiled)
285+
self.assertLess(diff, 1e-6)
286+
283287

284288
@ddt
285289
class TestGateSequence(QiskitTestCase):

0 commit comments

Comments
 (0)