From 13746ecaa90b500b00bec1ce677b59100e028560 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Mon, 1 Feb 2021 19:46:11 +0200 Subject: [PATCH 01/28] global phase in TwoQubitBasisDecomposer --- .../synthesis/two_qubit_decompose.py | 27 ++++++++++++++++--- test/python/quantum_info/test_synthesis.py | 16 +++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index b0da4db8dea8..0e48627c19b9 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -63,6 +63,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix): if abs(detL) < 0.9: raise QiskitError("decompose_two_qubit_product_gate: unable to decompose: detL < 0.9") L /= np.sqrt(detL) + phase = np.angle(detL) / 2 temp = np.kron(L, R) deviation = np.abs(np.abs(temp.conj(temp).T.dot(special_unitary_matrix).trace()) - 4) @@ -70,7 +71,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix): raise QiskitError("decompose_two_qubit_product_gate: decomposition failed: " "deviation too large: {}".format(deviation)) - return L, R + return L, R, phase _B = (1.0/math.sqrt(2)) * np.array([[1, 1j, 0, 0], @@ -102,12 +103,15 @@ def __init__(self, unitary_matrix, eps=1e-15): The overall decomposition scheme is taken from Drury and Love, arXiv:0806.4015 [quant-ph]. """ + pi = np.pi pi2 = np.pi/2 pi4 = np.pi/4 # Make U be in SU(4) U = unitary_matrix.copy() - U *= la.det(U)**(-0.25) + detU = la.det(U) + U *= detU**(-0.25) + global_phase = np.angle(detU) / 4 Up = _Bd.dot(U).dot(_B) M2 = Up.T.dot(Up) @@ -151,8 +155,9 @@ def __init__(self, unitary_matrix, eps=1e-15): K2.real[abs(K2.real) < eps] = 0.0 K2.imag[abs(K2.imag) < eps] = 0.0 - K1l, K1r = decompose_two_qubit_product_gate(K1) - K2l, K2r = decompose_two_qubit_product_gate(K2) + K1l, K1r, phase_l = decompose_two_qubit_product_gate(K1) + K2l, K2r, phase_r = decompose_two_qubit_product_gate(K2) + global_phase += phase_l + phase_r K1l = K1l.copy() @@ -161,33 +166,44 @@ def __init__(self, unitary_matrix, eps=1e-15): cs[0] -= 3*pi2 K1l = K1l.dot(_ipy) K1r = K1r.dot(_ipy) + global_phase += pi2 if cs[1] > pi2: cs[1] -= 3*pi2 K1l = K1l.dot(_ipx) K1r = K1r.dot(_ipx) + global_phase += pi2 conjs = 0 if cs[0] > pi4: cs[0] = pi2-cs[0] K1l = K1l.dot(_ipy) K2r = _ipy.dot(K2r) conjs += 1 + global_phase -= pi2 if cs[1] > pi4: cs[1] = pi2-cs[1] K1l = K1l.dot(_ipx) K2r = _ipx.dot(K2r) conjs += 1 + global_phase += pi2 + if conjs == 1: + global_phase -= pi if cs[2] > pi2: cs[2] -= 3*pi2 K1l = K1l.dot(_ipz) K1r = K1r.dot(_ipz) + global_phase += pi2 + if conjs == 1: + global_phase -= pi if conjs == 1: cs[2] = pi2-cs[2] K1l = K1l.dot(_ipz) K2r = _ipz.dot(K2r) + global_phase += pi2 if cs[2] > pi4: cs[2] -= pi2 K1l = K1l.dot(_ipz) K1r = K1r.dot(_ipz) + global_phase -= pi2 self.a = cs[1] self.b = cs[0] self.c = cs[2] @@ -195,6 +211,7 @@ def __init__(self, unitary_matrix, eps=1e-15): self.K1r = K1r self.K2l = K2l self.K2r = K2r + self.global_phase = global_phase def __repr__(self): # FIXME: this is worth making prettier since it's very useful for debugging @@ -435,6 +452,8 @@ def __call__(self, target, basis_fidelity=None): q = QuantumRegister(2) return_circuit = QuantumCircuit(q) + return_circuit.global_phase = target_decomposed.global_phase + return_circuit.global_phase += (-1)**best_nbasis * best_nbasis * self.basis.global_phase for i in range(best_nbasis): return_circuit.compose(decomposition_euler[2*i], [q[0]], inplace=True) return_circuit.compose(decomposition_euler[2*i+1], [q[1]], inplace=True) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index d450449d7519..04fbaae933b9 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -102,7 +102,7 @@ def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-7): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name decomp = TwoQubitWeylDecomposition(target_unitary) - op = Operator(np.eye(4)) + op = np.exp(1j * decomp.global_phase) * Operator(np.eye(4)) for u, qs in ( (decomp.K2r, [0]), (decomp.K2l, [1]), @@ -112,24 +112,16 @@ def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-7): ): op = op.compose(u, qs) decomp_unitary = op.data - target_unitary *= la.det(target_unitary) ** (-0.25) - decomp_unitary *= la.det(decomp_unitary) ** (-0.25) - maxdists = [np.max(np.abs(target_unitary + phase * decomp_unitary)) - for phase in [1, 1j, -1, -1j]] - maxdist = np.min(maxdists) + maxdist = np.max(np.abs(target_unitary - decomp_unitary)) self.assertTrue(np.abs(maxdist) < tolerance, "Unitary {}: Worst distance {}".format(target_unitary, maxdist)) def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-7): """Check exact decomposition for a particular target""" decomp_circuit = decomposer(target_unitary) - result = execute(decomp_circuit, UnitarySimulatorPy()).result() + result = execute(decomp_circuit, UnitarySimulatorPy(), optimization_level=0).result() decomp_unitary = result.get_unitary() - target_unitary *= la.det(target_unitary) ** (-0.25) - decomp_unitary *= la.det(decomp_unitary) ** (-0.25) - maxdists = [np.max(np.abs(target_unitary + phase * decomp_unitary)) - for phase in [1, 1j, -1, -1j]] - maxdist = np.min(maxdists) + maxdist = np.max(np.abs(target_unitary - decomp_unitary)) self.assertTrue(np.abs(maxdist) < tolerance, "Unitary {}: Worst distance {}".format(target_unitary, maxdist)) From 39a5491ec46fa6c9d8de4c65ca9b03c1e58f087a Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Fri, 5 Feb 2021 13:05:00 -0500 Subject: [PATCH 02/28] Tolerances for checks --- .../synthesis/one_qubit_decompose.py | 87 ++++++---- .../synthesis/two_qubit_decompose.py | 10 +- test/python/quantum_info/test_synthesis.py | 156 ++++++++++++------ 3 files changed, 160 insertions(+), 93 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index aab3f50c77a3..cf8875a1a449 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -265,14 +265,14 @@ def _circuit_zyz(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and np.isclose(theta, 0.0, atol=atol): + if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): circuit.append(RZGate(phi + lam), [0]) return circuit - if not simplify or not np.isclose(lam, 0.0, atol=atol): + if not simplify or not np.isclose(lam, 0.0, atol=atol, rtol=0): circuit.append(RZGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol): + if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): circuit.append(RYGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol): + if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): circuit.append(RZGate(phi), [0]) return circuit @@ -281,18 +281,18 @@ def _circuit_zxz(theta, phi, lam, phase, - simplify=False, + simplify=True, atol=DEFAULT_ATOL): - if simplify and np.isclose(theta, 0.0, atol=atol): + if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): circuit = QuantumCircuit(1, global_phase=phase) circuit.append(RZGate(phi + lam), [0]) return circuit circuit = QuantumCircuit(1, global_phase=phase) - if not simplify or not np.isclose(lam, 0.0, atol=atol): + if not simplify or not np.isclose(lam, 0.0, atol=atol, rtol=0): circuit.append(RZGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol): + if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): circuit.append(RXGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol): + if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): circuit.append(RZGate(phi), [0]) return circuit @@ -304,14 +304,14 @@ def _circuit_xyx(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and np.isclose(theta, 0.0, atol=atol): + if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): circuit.append(RXGate(phi + lam), [0]) return circuit - if not simplify or not np.isclose(lam, 0.0, atol=atol): + if not simplify or not np.isclose(lam, 0.0, atol=atol, rtol=0): circuit.append(RXGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol): + if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): circuit.append(RYGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol): + if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): circuit.append(RXGate(phi), [0]) return circuit @@ -352,31 +352,37 @@ def _circuit_psx(theta, phi = _mod2pi(phi + np.pi) circuit = QuantumCircuit(1, global_phase=phase - np.pi / 2) # Check for decomposition into minimimal number required SX gates - if simplify and np.isclose(abs(theta), np.pi, atol=atol): + if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): if not np.isclose(_mod2pi(abs(lam + phi + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(_mod2pi(lam + phi + theta)), [0]) circuit.global_phase += np.pi / 2 elif simplify and np.isclose(abs(theta), - [np.pi/2, 3*np.pi/2], atol=atol).any(): + [np.pi/2, 3*np.pi/2], atol=atol, rtol=0).any(): if not np.isclose(_mod2pi(abs(lam + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(_mod2pi(lam + theta)), [0]) circuit.append(SXGate(), [0]) if not np.isclose(_mod2pi(abs(phi + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(_mod2pi(phi + theta)), [0]) - if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol).any(): + if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol, rtol=0).any(): circuit.global_phase += np.pi / 2 - else: - if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol).any(): + elif simplify: + if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(lam), [0]) circuit.append(SXGate(), [0]) - if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol).any(): + if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(theta), [0]) circuit.append(SXGate(), [0]) - if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol).any(): + if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(PhaseGate(phi), [0]) + else: + circuit.append(PhaseGate(lam), [0]) + circuit.append(SXGate(), [0]) + circuit.append(PhaseGate(theta), [0]) + circuit.append(SXGate(), [0]) + circuit.append(PhaseGate(phi), [0]) return circuit @staticmethod @@ -392,37 +398,46 @@ def _circuit_zsx(theta, phi = _mod2pi(phi + np.pi) circuit = QuantumCircuit(1, global_phase=phase - np.pi / 2) # Check for decomposition into minimimal number required SX gates - if simplify and np.isclose(abs(theta), np.pi, atol=atol): + if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): if not np.isclose(_mod2pi(abs(lam + phi + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(_mod2pi(lam + phi + theta)), [0]) circuit.global_phase += 0.5 * _mod2pi(lam + phi + theta) circuit.global_phase += np.pi / 2 elif simplify and np.isclose(abs(theta), - [np.pi/2, 3*np.pi/2], atol=atol).any(): + [np.pi/2, 3*np.pi/2], atol=atol, rtol=0).any(): if not np.isclose(_mod2pi(abs(lam + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(_mod2pi(lam + theta)), [0]) circuit.global_phase += 0.5 * _mod2pi(lam + theta) circuit.append(SXGate(), [0]) if not np.isclose(_mod2pi(abs(phi + theta)), - [0., 2*np.pi], atol=atol).any(): + [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(_mod2pi(phi + theta)), [0]) circuit.global_phase += 0.5 * _mod2pi(phi + theta) - if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol).any(): + if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol, rtol=0).any(): circuit.global_phase += np.pi / 2 - else: - if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol).any(): + elif simplify: + if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(lam), [0]) circuit.global_phase += 0.5 * lam circuit.append(SXGate(), [0]) - if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol).any(): + if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(theta), [0]) circuit.global_phase += 0.5 * theta circuit.append(SXGate(), [0]) - if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol).any(): + if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol, rtol=0).any(): circuit.append(RZGate(phi), [0]) circuit.global_phase += 0.5 * phi + else: + circuit.append(RZGate(lam), [0]) + circuit.global_phase += 0.5 * lam + circuit.append(SXGate(), [0]) + circuit.append(RZGate(theta), [0]) + circuit.global_phase += 0.5 * theta + circuit.append(SXGate(), [0]) + circuit.append(RZGate(phi), [0]) + circuit.global_phase += 0.5 * phi return circuit @staticmethod @@ -437,12 +452,12 @@ def _circuit_u1x(theta, theta += np.pi phi += np.pi # Check for decomposition into minimimal number required X90 pulses - if simplify and np.isclose(abs(theta), np.pi, atol=atol): + if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): # Zero X90 gate decomposition circuit = QuantumCircuit(1, global_phase=phase) circuit.append(U1Gate(lam + phi + theta), [0]) return circuit - if simplify and np.isclose(abs(theta), np.pi/2, atol=atol): + if simplify and np.isclose(abs(theta), np.pi/2, atol=atol, rtol=0): # Single X90 gate decomposition circuit = QuantumCircuit(1, global_phase=phase) circuit.append(U1Gate(lam + theta), [0]) @@ -466,7 +481,7 @@ def _circuit_rr(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if not simplify or not np.isclose(theta, -np.pi, atol=atol): + if not simplify or not np.isclose(theta, -np.pi, atol=atol, rtol=0): circuit.append(RGate(theta + np.pi, np.pi / 2 - lam), [0]) circuit.append(RGate(-np.pi, 0.5 * (phi - lam + np.pi)), [0]) return circuit diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 0e48627c19b9..c6a6dd2b5fd3 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -420,9 +420,11 @@ def decomp3_supercontrolled(self, target): return U3r, U3l, U2r, U2l, U1r, U1l, U0r, U0l - def __call__(self, target, basis_fidelity=None): + def __call__(self, target, basis_fidelity=None, num_basis_uses=None): """Decompose a two-qubit unitary over fixed basis + SU(2) using the best approximation given that each basis application has a finite fidelity. + + You can force a particular approximation by passing num_basis_uses. """ basis_fidelity = basis_fidelity or self.basis_fidelity if hasattr(target, 'to_operator'): @@ -447,13 +449,17 @@ def __call__(self, target, basis_fidelity=None): expected_fidelities = [trace_to_fid(traces[i]) * basis_fidelity**i for i in range(4)] best_nbasis = np.argmax(expected_fidelities) + if num_basis_uses is not None: + best_nbasis = num_basis_uses decomposition = self.decomposition_fns[best_nbasis](target_decomposed) decomposition_euler = [self._decomposer1q(x) for x in decomposition] q = QuantumRegister(2) return_circuit = QuantumCircuit(q) return_circuit.global_phase = target_decomposed.global_phase - return_circuit.global_phase += (-1)**best_nbasis * best_nbasis * self.basis.global_phase + return_circuit.global_phase -= best_nbasis * self.basis.global_phase + if best_nbasis == 2: + return_circuit.global_phase += np.pi for i in range(best_nbasis): return_circuit.compose(decomposition_euler[2*i], [q[0]], inplace=True) return_circuit.compose(decomposition_euler[2*i+1], [q[1]], inplace=True) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 04fbaae933b9..ee7f11022398 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -17,7 +17,6 @@ from ddt import ddt import numpy as np -import scipy.linalg as la from qiskit import execute from qiskit.circuit import QuantumCircuit, QuantumRegister @@ -78,8 +77,7 @@ def make_hard_thetas_oneq(smallest=1e-18, factor=3.2, steps=22, phi=0.7, lam=0.9 class CheckDecompositions(QiskitTestCase): """Implements decomposition checkers.""" - def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-12, - phase_equal=True): + def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-14, simplify=False): """Check OneQubitEulerDecomposer works for the given unitary""" target_unitary = operator.data if basis is None: @@ -87,18 +85,13 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-12, decomp_unitary = U3Gate(*angles).to_matrix() else: decomposer = OneQubitEulerDecomposer(basis) - decomp_unitary = Operator(decomposer(target_unitary)).data - # Add global phase to make special unitary - target_unitary *= la.det(target_unitary) ** (-0.5) - decomp_unitary *= la.det(decomp_unitary) ** (-0.5) + decomp_unitary = Operator(decomposer(target_unitary, simplify=simplify)).data maxdist = np.max(np.abs(target_unitary - decomp_unitary)) - if not phase_equal and maxdist > 0.1: - maxdist = np.max(np.abs(target_unitary + decomp_unitary)) self.assertTrue(np.abs(maxdist) < tolerance, "Operator {}: Worst distance {}".format(operator, maxdist)) # FIXME: should be possible to set this tolerance tighter after improving the function - def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-7): + def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name decomp = TwoQubitWeylDecomposition(target_unitary) @@ -116,9 +109,12 @@ def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-7): self.assertTrue(np.abs(maxdist) < tolerance, "Unitary {}: Worst distance {}".format(target_unitary, maxdist)) - def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-7): + def check_exact_decomposition(self, target_unitary, decomposer, + tolerance=1.e-13, num_basis_uses=None): """Check exact decomposition for a particular target""" - decomp_circuit = decomposer(target_unitary) + decomp_circuit = decomposer(target_unitary, num_basis_uses=num_basis_uses) + if num_basis_uses is not None: + self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get('unitary', 0)) result = execute(decomp_circuit, UnitarySimulatorPy(), optimization_level=0).result() decomp_unitary = result.get_unitary() maxdist = np.max(np.abs(target_unitary - decomp_unitary)) @@ -148,57 +144,35 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) +ONEQ_BASES = ['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'] +SIMP_TOL = [(False, 1.e-14), (True, 1.E-12)] @ddt class TestOneQubitEulerDecomposer(CheckDecompositions): """Test OneQubitEulerDecomposer""" - def check_one_qubit_euler_angles(self, operator, basis='U3', - tolerance=1e-12, - phase_equal=True): - """Check euler_angles_1q works for the given unitary""" - decomposer = OneQubitEulerDecomposer(basis) - with self.subTest(operator=operator): - target_unitary = operator.data - decomp_unitary = Operator(decomposer(target_unitary)).data - # Add global phase to make special unitary - target_unitary *= la.det(target_unitary) ** (-0.5) - decomp_unitary *= la.det(decomp_unitary) ** (-0.5) - maxdist = np.max(np.abs(target_unitary - decomp_unitary)) - if not phase_equal and maxdist > 0.1: - maxdist = np.max(np.abs(target_unitary + decomp_unitary)) - self.assertTrue(np.abs(maxdist) < tolerance, "Worst distance {}".format(maxdist)) - - @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'], - name='test_one_qubit_clifford_{basis}_basis') - def test_one_qubit_clifford_all_basis(self, basis): + @combine(basis=ONEQ_BASES, simp_tol=SIMP_TOL, + name='test_one_qubit_clifford_{basis}_basis_simplify_{simp_tol[0]}') + def test_one_qubit_clifford_all_basis(self, basis, simp_tol): """Verify for {basis} basis and all Cliffords.""" for clifford in ONEQ_CLIFFORDS: - self.check_one_qubit_euler_angles(clifford, basis) - - @combine(basis_tolerance=[('U3', 1e-12), - ('XYX', 1e-12), - ('ZXZ', 1e-12), - ('ZYZ', 1e-12), - ('U1X', 1e-7), - ('PSX', 1e-7), - ('ZSX', 1e-7), - ('RR', 1e-12)], - name='test_one_qubit_hard_thetas_{basis_tolerance[0]}_basis') - # Lower tolerance for U1X test since decomposition since it is - # less numerically accurate. This is due to it having 5 matrix - # multiplications and the X90 gates - def test_one_qubit_hard_thetas_all_basis(self, basis_tolerance): - """Verify for {basis_tolerance[0]} basis and close-to-degenerate theta.""" + self.check_one_qubit_euler_angles( + clifford, basis, simplify=simp_tol[0], tolerance=simp_tol[1]) + + @combine(basis=ONEQ_BASES, simp_tol=SIMP_TOL, + name='test_one_qubit_hard_thetas_{basis}_basis_simplify_{simp_tol[0]}') + def test_one_qubit_hard_thetas_all_basis(self, basis, simp_tol): + """Verify for {basis} basis and close-to-degenerate theta.""" for gate in HARD_THETA_ONEQS: - self.check_one_qubit_euler_angles(Operator(gate), basis_tolerance[0], - basis_tolerance[1]) + self.check_one_qubit_euler_angles( + Operator(gate), basis, simplify=simp_tol[0], tolerance=simp_tol[1]) - @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'], seed=range(50), - name='test_one_qubit_random_{basis}_basis_{seed}') - def test_one_qubit_random_all_basis(self, basis, seed): + @combine(basis=ONEQ_BASES, simp_tol=SIMP_TOL, seed=range(50), + name='test_one_qubit_random_{basis}_basis_simplify_{simp_tol[0]}_{seed}') + def test_one_qubit_random_all_basis(self, basis, simp_tol, seed): """Verify for {basis} basis and random_unitary (seed={seed}).""" unitary = random_unitary(2, seed=seed) - self.check_one_qubit_euler_angles(unitary, basis) + self.check_one_qubit_euler_angles( + unitary, basis, simplify=simp_tol[0], tolerance=simp_tol[1]) def test_psx_zsx_special_cases(self): """Test decompositions of psx and zsx at special values of parameters""" @@ -363,7 +337,7 @@ def test_two_qubit_weyl_decomposition_aac(self, smallest=1e-18, factor=9.8, step self.check_two_qubit_weyl_decomposition(k1 @ a @ k2) def test_two_qubit_weyl_decomposition_abc(self, smallest=1e-18, factor=9.8, steps=11): - """Verify Weyl KAK decomposition for U~Ud(a,a,b)""" + """Verify Weyl KAK decomposition for U~Ud(a,b,c)""" for aaa in ([smallest * factor ** i for i in range(steps)] + [np.pi / 4 - smallest * factor ** i for i in range(steps)] + [np.pi / 8, 0.113 * np.pi, 0.1972 * np.pi]): @@ -372,7 +346,7 @@ def test_two_qubit_weyl_decomposition_abc(self, smallest=1e-18, factor=9.8, step for k1l, k1r, k2l, k2r in K1K2S: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) - a = Ud(aaa, aaa, ccc) + a = Ud(aaa, bbb, ccc) self.check_two_qubit_weyl_decomposition(k1 @ a @ k2) @@ -415,6 +389,78 @@ def test_exact_supercontrolled_decompose_random(self, seed): decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) self.check_exact_decomposition(random_unitary(4, seed=seed + 4).data, decomposer) + @combine(seed=range(10), name='seed_{seed}') + def test_exact_supercontrolled_decompose_phase_0_use_random(self, seed): + """Exact decomposition supercontrolled basis, random target (0 basis uses) (seed={seed})""" + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(0, 0, 0) @ tgt_k2 + self.check_exact_decomposition(tgt_unitary, decomposer, num_basis_uses=0) + + @combine(seed=range(10), name='seed_{seed}') + def test_exact_supercontrolled_decompose_phase_1_use_random(self, seed): + """Exact decomposition supercontrolled basis, random tgt (1 basis uses) (seed={seed})""" + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(np.pi / 4, basis_b, 0) @ tgt_k2 + self.check_exact_decomposition(tgt_unitary, decomposer, num_basis_uses=1) + + @combine(seed=range(10), name='seed_{seed}') + def test_exact_supercontrolled_decompose_phase_2_use_random(self, seed): + """Exact decomposition supercontrolled basis, random tgt (2 basis uses) (seed={seed})""" + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + tgt_a, tgt_b = state.random(size=2) * np.pi / 4 + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(tgt_a, tgt_b, 0) @ tgt_k2 + self.check_exact_decomposition(tgt_unitary, decomposer, num_basis_uses=2) + + @combine(seed=range(10), name='seed_{seed}') + def test_exact_supercontrolled_decompose_phase_3_use_random(self, seed): + """Exact decomposition supercontrolled basis, random tgt (3 basis uses) (seed={seed})""" + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + + tgt_a, tgt_b, tgt_c = state.random(size=3) * np.pi / 4 + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(tgt_a, tgt_b, tgt_c) @ tgt_k2 + self.check_exact_decomposition(tgt_unitary, decomposer, num_basis_uses=3) + def test_exact_nonsupercontrolled_decompose(self): """Check that the nonsupercontrolled basis throws a warning""" with self.assertWarns(UserWarning, msg="Supposed to warn when basis non-supercontrolled"): From 80944e810ae46e2aeaa1bee9ef3f4f2dd5eefedc Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:57:23 -0500 Subject: [PATCH 03/28] Subclass Weyl decompositions for numeric stability --- .../synthesis/two_qubit_decompose.py | 407 ++++++++++++++++-- test/python/quantum_info/test_synthesis.py | 3 +- 2 files changed, 365 insertions(+), 45 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 10de159a37f5..99d4f223fafd 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -25,24 +25,25 @@ """ import math import warnings +from typing import Optional +import logging import numpy as np import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.library.standard_gates.x import CXGate -from qiskit.circuit.tools import pi_check +from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.operators.predicates import is_unitary_matrix from qiskit.quantum_info.synthesis.weyl import weyl_coordinates -from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer +from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer, DEFAULT_ATOL -_CUTOFF_PRECISION = 1e-12 +logger = logging.getLogger(__name__) -def decompose_two_qubit_product_gate(special_unitary_matrix): +def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): """Decompose U = Ul⊗Ur where U in SU(4), and Ul, Ur in SU(2). Throws QiskitError if this isn't possible. """ @@ -86,21 +87,55 @@ def decompose_two_qubit_product_gate(special_unitary_matrix): [-1, 0]], dtype=complex) _ipz = np.array([[1j, 0], [0, -1j]], dtype=complex) +_id = np.array([[1, 0], + [0, 1]], dtype=complex) -class TwoQubitWeylDecomposition: - """ Decompose two-qubit unitary U = (K1l⊗K1r).Exp(i a xx + i b yy + i c zz).(K2l⊗K2r) , - where U ∈ U(4), (K1l|K1r|K2l|K2r) ∈ SU(2), and we stay in the "Weyl Chamber" - 𝜋/4 ≥ a ≥ b ≥ |c| +class TwoQubitWeylDecomposition(): + """Decompose two-qubit unitary U = (K1l⊗K1r).Exp(i a xx + i b yy + i c zz).(K2l⊗K2r) , where U ∈ + U(4), (K1l|K1r|K2l|K2r) ∈ SU(2), and we stay in the "Weyl Chamber" 𝜋/4 ≥ a ≥ b ≥ |c| + + This is an abstract factory class that instantiates itself as specialized subclasses based on + the fidelity, such that the approximation error from specialization has an average gate fidelity + at least as high as requested. The specialized subclasses have canonical representations thus + avoiding problems of numerical stability. + + Passing non-None fidelity to specializations is treated as an assertion, raising QiskitError if + the specialization is more approximate. """ - def __init__(self, unitary_matrix, eps=1e-15): - """The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, - Phys. Rev. A 63, 062309 (2001). + a: float + b: float + c: float + global_phase: float + K1l: np.ndarray + K2l: np.ndarray + K1r: np.ndarray + K2r: np.ndarray + unitary_matrix: np.ndarray # The unitary that was input + requested_fidelity: Optional[float] # None for no automatic specialization + calculated_fidelity: float # Fidelity after specialization + + _original_decomposition: Optional["TwoQubitWeylDecomposition"] + _is_flipped_from_original: bool + + def __init_subclass__(cls, **kwargs): + """Subclasses should be concrete, not factories. + + Have explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" + super().__init_subclass__(**kwargs) + cls.__new__ = (lambda cls, *a, fidelity=None, **k: + TwoQubitWeylDecomposition.__new__(cls, *a, fidelity=None, **k)) + + def __new__(cls, unitary_matrix, fidelity=(1.-1.E-9)): + """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. + + The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, + 062309 (2001). - FIXME: There's a cleaner-seeming method based on choosing branch cuts carefully, in - Andrew M. Childs, Henry L. Haselgrove, and Michael A. Nielsen, Phys. Rev. A 68, 052311, - but I wasn't able to get that to work. + FIXME: There's a cleaner-seeming method based on choosing branch cuts carefully, in Andrew + M. Childs, Henry L. Haselgrove, and Michael A. Nielsen, Phys. Rev. A 68, 052311, but I + wasn't able to get that to work. The overall decomposition scheme is taken from Drury and Love, arXiv:0806.4015 [quant-ph]. """ @@ -116,8 +151,6 @@ def __init__(self, unitary_matrix, eps=1e-15): Up = _Bd.dot(U).dot(_B) M2 = Up.T.dot(Up) - M2.real[abs(M2.real) < eps] = 0.0 - M2.imag[abs(M2.imag) < eps] = 0.0 # M2 is a symmetric complex matrix. We need to decompose it as M2 = P D P^T where # P ∈ SO(4), D is diagonal with unit-magnitude elements. @@ -127,7 +160,7 @@ def __init__(self, unitary_matrix, eps=1e-15): M2real = state.normal()*M2.real + state.normal()*M2.imag _, P = np.linalg.eigh(M2real) D = P.T.dot(M2).dot(P).diagonal() - if np.allclose(P.dot(np.diag(D)).dot(P.T), M2, rtol=1.0e-13, atol=1.0e-13): + if np.allclose(P.dot(np.diag(D)).dot(P.T), M2, rtol=0, atol=1.0E-13): break else: raise QiskitError("TwoQubitWeylDecomposition: failed to diagonalize M2") @@ -150,11 +183,7 @@ def __init__(self, unitary_matrix, eps=1e-15): # Find K1, K2 so that U = K1.A.K2, with K being product of single-qubit unitaries K1 = _B.dot(Up).dot(P).dot(np.diag(np.exp(1j*d))).dot(_Bd) - K1.real[abs(K1.real) < eps] = 0.0 - K1.imag[abs(K1.imag) < eps] = 0.0 K2 = _B.dot(P.T).dot(_Bd) - K2.real[abs(K2.real) < eps] = 0.0 - K2.imag[abs(K2.imag) < eps] = 0.0 K1l, K1r, phase_l = decompose_two_qubit_product_gate(K1) K2l, K2r, phase_r = decompose_two_qubit_product_gate(K2) @@ -205,24 +234,317 @@ def __init__(self, unitary_matrix, eps=1e-15): K1l = K1l.dot(_ipz) K1r = K1r.dot(_ipz) global_phase -= pi2 - self.a = cs[1] - self.b = cs[0] - self.c = cs[2] - self.K1l = K1l - self.K1r = K1r - self.K2l = K2l - self.K2r = K2r - self.global_phase = global_phase + + a, b, c = cs[1], cs[0], cs[2] + + # Save the non-specialized decomposition for later comparison + od = super().__new__(TwoQubitWeylDecomposition) + od.a = a + od.b = b + od.c = c + od.K1l = K1l + od.K1r = K1r + od.K2l = K2l + od.K2r = K2r + od.global_phase = global_phase + od.unitary_matrix = unitary_matrix + od.requested_fidelity = fidelity + + def is_close(ap, bp, cp): + da, db, dc = a-ap, b-bp, c-cp + tr = 4*complex(np.cos(da)*np.cos(db)*np.cos(dc) + np.sin(da)*np.sin(db)*np.sin(dc)) + fid = trace_to_fid(tr) + return fid >= fidelity + + if fidelity is None: # Don't specialize if None + instance = super().__new__(TwoQubitWeylGeneral + if cls is TwoQubitWeylDecomposition else cls) + elif is_close(0, 0, 0): + instance = super().__new__(TwoQubitWeylIdEquiv) + elif is_close(pi4, pi4, pi4) or is_close(pi4, pi4, -pi4): + instance = super().__new__(TwoQubitWeylSWAPEquiv) + elif (lambda x: is_close(x, x, x))(_closest_partial_swap(a, b, c)): + instance = super().__new__(TwoQubitWeylPartialSWAPEquiv) + elif (lambda x: is_close(x, x, -x))(_closest_partial_swap(a, b, -c)): + instance = super().__new__(TwoQubitWeylPartialSWAPFlipEquiv) + elif is_close(a, 0, 0): + instance = super().__new__(TwoQubitWeylControlledEquiv) + elif is_close(pi4, pi4, c): + instance = super().__new__(TwoQubitWeylMirrorControlledEquiv) + elif is_close((a+b)/2, (a+b)/2, c): + instance = super().__new__(TwoQubitWeylfSimaabEquiv) + elif is_close(a, (b+c)/2, (b+c)/2): + instance = super().__new__(TwoQubitWeylfSimabbEquiv) + elif is_close(a, (b-c)/2, (c-b)/2): + instance = super().__new__(TwoQubitWeylfSimabmbEquiv) + else: + instance = super().__new__(TwoQubitWeylGeneral) + + instance._original_decomposition = od + return instance + + def __init__(self, unitary_matrix: np.ndarray, *args, fidelity=None, **kwargs): + super().__init__(*args, **kwargs) + od = self._original_decomposition + self.a, self.b, self.c = od.a, od.b, od.c + self.K1l, self.K1r = od.K1l, od.K1r + self.K2l, self.K2r = od.K2l, od.K2r + self.global_phase = od.global_phase + self.unitary_matrix = unitary_matrix + self.requested_fidelity = fidelity + self._is_flipped_from_original = False + self.specialize() + + # Update the phase after specialization: + if self._is_flipped_from_original: + da, db, dc = (np.pi/2-od.a)-self.a, od.b-self.b, -od.c-self.c + tr = 4 * complex(np.cos(da)*np.cos(db)*np.cos(dc), np.sin(da)*np.sin(db)*np.sin(dc)) + else: + da, db, dc = od.a-self.a, od.b-self.b, od.c-self.c + tr = 4 * complex(np.cos(da)*np.cos(db)*np.cos(dc), np.sin(da)*np.sin(db)*np.sin(dc)) + self.global_phase += np.angle(tr) + self.calculated_fidelity = trace_to_fid(tr) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Requested fidelity: %s calculated fidelity: %s actual fidelity %s", + self.requested_fidelity, self.calculated_fidelity, self.actual_fidelity()) + if self.requested_fidelity and self.calculated_fidelity + 1.E-13 < self.requested_fidelity: + raise QiskitError(f"{self.__class__.__name__}: " + f"calculated fidelity: {self.calculated_fidelity} " + f"is worse than requested fidelity: {self.requested_fidelity}.") + + def specialize(self): + """Make changes to the decomposition to comply with any specialization. + + Do not update the global phase, since gets done in generic __init__()""" + raise NotImplementedError + + def circuit(self, *, euler_basis='ZYZ', simplify=False, atol=DEFAULT_ATOL, **kwargs): + """Weyl decomposition in circuit form. + + Arguments are passed to OneQubitEulerDecomposer""" + oneq_decompose = OneQubitEulerDecomposer(euler_basis) + circ = QuantumCircuit(2, global_phase=self.global_phase) + c1l, c1r, c2l, c2r = (oneq_decompose(k, simplify=simplify, atol=atol, **kwargs) + for k in (self.K1l, self.K1r, self.K2l, self.K2r)) + circ.compose(c2r, [0], inplace=True) + circ.compose(c2l, [1], inplace=True) + if not simplify or abs(self.a) > atol: + circ.rxx(-self.a*2, 0, 1) + if not simplify or abs(self.b) > atol: + circ.ryy(-self.b*2, 0, 1) + if not simplify or abs(self.c) > atol: + circ.rzz(-self.c*2, 0, 1) + circ.compose(c1r, [0], inplace=True) + circ.compose(c1l, [1], inplace=True) + return circ + + def actual_fidelity(self, **kwargs): + """Calculates the actual fidelity of the decomposed circuit to the input unitary""" + circ = self.circuit(**kwargs) + trace = np.trace(Operator(circ).data.T.conj() @ self.unitary_matrix) + if abs(np.imag(trace)) > 1.E-13: + logger.warning("Trace has a non-zero imaginary component: %s", trace) + return trace_to_fid(trace) def __repr__(self): - # FIXME: this is worth making prettier since it's very useful for debugging - return ("{}\n{}\n{}\nUd({}, {}, {})\n{}\n{}\n".format( - pi_check(self.global_phase), - np.array_str(self.K1l), - np.array_str(self.K1r), - self.a, self.b, self.c, - np.array_str(self.K2l), - np.array_str(self.K2r))) + prefix = f"{self.__class__.__name__}(" + prefix_nd = " np.array(" + suffix = ")," + array = np.array2string(self.unitary_matrix, prefix=prefix+prefix_nd, suffix=suffix, + separator=', ', max_line_width=1000, + floatmode='maxprec_equal', precision=100) + return (f"{prefix}{prefix_nd}{array}{suffix}\n" + f"{' '*len(prefix)}fidelity={self.requested_fidelity})") + + def __str__(self): + pre = f"{self.__class__.__name__}(\n\t" + circ_indent = "\n\t".join(self.circuit(simplify=True).draw("text").lines(-1)) + return f"{pre}{circ_indent}\n)" + + +class TwoQubitWeylIdEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(0,0,0) ~ Id + + This gate binds 0 parameters, we make it canonical by setting + K2l = Id , + K2r = Id . + """ + + def specialize(self): + self.a = self.b = self.c = 0. + self.K1l = self.K1l @ self.K2l + self.K1r = self.K1r @ self.K2r + self.K2l = _id.copy() + self.K2r = _id.copy() + + +class TwoQubitWeylSWAPEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(𝜋/4, 𝜋/4, 𝜋/4) ~ U(𝜋/4, 𝜋/4, -𝜋/4) ~ SWAP + + This gate binds 0 parameters, we make it canonical by setting + K2l = Id , + K2r = Id . + """ + def specialize(self): + if self.c > 0: + self.K1l = self.K1l @ self.K2r + self.K1r = self.K1r @ self.K2l + else: + self._is_flipped_from_original = True + self.K1l = self.K1l @ _ipz @ self.K2r + self.K1r = self.K1r @ _ipz @ self.K2l + self.global_phase = self.global_phase + np.pi/2 + self.a = self.b = self.c = np.pi/4 + self.K2l = _id.copy() + self.K2r = _id.copy() + + +def _closest_partial_swap(a, b, c): + """A good approximation to the best value x to get the minimum + trace distance for Ud(x, x, x) from Ud(a, b, c) + """ + m = (a + b + c) / 3 + am, bm, cm = a-m, b-m, c-m + ab, bc, ca = a-b, b-c, c-a + + return m + am * bm * cm * (6 + ab*ab + bc*bc * ca*ca) / 18 + + +class TwoQubitWeylPartialSWAPEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α𝜋/4, α𝜋/4, α𝜋/4) ~ SWAP**α + + This gate binds 3 parameters, we make it canonical by setting: + K2l = Id . + """ + def specialize(self): + self.a = self.b = self.c = _closest_partial_swap(self.a, self.b, self.c) + self.K1l = self.K1l @ self.K2l + self.K1r = self.K1r @ self.K2l + self.K2r = self.K2l.T.conj() @ self.K2r + self.K2l = _id.copy() + + +class TwoQubitWeylPartialSWAPFlipEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α𝜋/4, α𝜋/4, -α𝜋/4) ~ SWAP**α + + (a non-equivalent root of SWAP from the TwoQubitWeylPartialSWAPEquiv + similar to how x = (±sqrt(x))**2 ) + + This gate binds 3 parameters, we make it canonical by setting: + K2l = Id . + """ + + def specialize(self): + self.a = self.b = _closest_partial_swap(self.a, self.b, -self.c) + self.c = -self.a + self.K1l = self.K1l @ self.K2l + self.K1r = self.K1r @ _ipz @ self.K2l @ _ipz + self.K2r = _ipz @ self.K2l.T.conj() @ _ipz @ self.K2r + self.K2l = _id.copy() + + +_oneq_xyx = OneQubitEulerDecomposer('XYX') +_oneq_zyz = OneQubitEulerDecomposer('ZYZ') + + +class TwoQubitWeylControlledEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α, 0, 0) ~ Ctrl-U + + This gate binds 4 parameters, we make it canonical by setting: + K2l = Ry(θl).Rx(λl) , + K2r = Ry(θr).Rx(λr) . + """ + def specialize(self): + self.b = self.c = 0 + k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) + k2rtheta, k2rphi, k2rlambda = _oneq_xyx.angles(self.K2r) + self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) + self.K1r = self.K1r @ np.asarray(RXGate(k2rphi)) + self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) + self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) + + def circuit(self, *, euler_basis='XYX', **kwargs): + return super().circuit(euler_basis=euler_basis, **kwargs) + + +class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U + + This gate binds 4 parameters, we make it canonical by setting: + K2l = Ry(θl).Rz(λl) , + K2r = Ry(θr).Rz(λr) . + """ + def specialize(self): + self.a = self.b = np.pi/4 + k2ltheta, k2lphi, k2llambda = _oneq_zyz.angles(self.K2l) + k2rtheta, k2rphi, k2rlambda = _oneq_zyz.angles(self.K2r) + self.K1r = self.K1r @ np.asarray(RZGate(k2lphi)) + self.K1l = self.K1l @ np.asarray(RZGate(k2rphi)) + self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RZGate(k2llambda)) + self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RZGate(k2rlambda)) + + +# These next 3 gates use the definition of fSim from https://arxiv.org/pdf/2001.08343.pdf eq (1) +class TwoQubitWeylfSimaabEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α, α, β), α ≥ |β| + + This gate binds 5 parameters, we make it canonical by setting: + K2l = Ry(θl).Rz(λl) . + """ + def specialize(self): + self.a = self.b = (self.a + self.b)/2 + k2ltheta, k2lphi, k2llambda = _oneq_zyz.angles(self.K2l) + self.K1r = self.K1r @ np.asarray(RZGate(k2lphi)) + self.K1l = self.K1l @ np.asarray(RZGate(k2lphi)) + self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RZGate(k2llambda)) + self.K2r = np.asarray(RZGate(-k2lphi)) @ self.K2r + + +class TwoQubitWeylfSimabbEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α, β, β), α ≥ β + + This gate binds 5 parameters, we make it canonical by setting: + K2l = Ry(θl).Rx(λl) . + """ + def specialize(self): + self.b = self.c = (self.b + self.c)/2 + k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) + self.K1r = self.K1r @ np.asarray(RXGate(k2lphi)) + self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) + self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) + self.K2r = np.asarray(RXGate(-k2lphi)) @ self.K2r + + def circuit(self, *, euler_basis='XYX', **kwargs): + return super().circuit(euler_basis=euler_basis, **kwargs) + + +class TwoQubitWeylfSimabmbEquiv(TwoQubitWeylDecomposition): + """U ~ Ud(α, β, -β), α ≥ β ≥ 0 + + This gate binds 5 parameters, we make it canonical by setting: + K2l = Ry(θl).Rx(λl) . + """ + def specialize(self): + self.b = (self.b - self.c)/2 + self.c = -self.b + k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) + self.K1r = self.K1r @ _ipz @ np.asarray(RXGate(k2lphi)) @ _ipz + self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) + self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) + self.K2r = _ipz @ np.asarray(RXGate(-k2lphi)) @ _ipz @ self.K2r + + def circuit(self, *, euler_basis='XYX', **kwargs): + return super().circuit(euler_basis=euler_basis, **kwargs) + + +class TwoQubitWeylGeneral(TwoQubitWeylDecomposition): + """U has no special symmetry. + + This gate binds all 6 possible parameters, so there is no need to make the single-qubit + pre-/post-gates canonical. + """ + def specialize(self): + pass # Nothing to do def Ud(a, b, c): @@ -330,7 +652,8 @@ def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): # In the future could use different decomposition functions for different basis classes, etc if not self.is_supercontrolled: warnings.warn("Only know how to decompose properly for supercontrolled basis gate. " - "This gate is ~Ud({}, {}, {})".format(basis.a, basis.b, basis.c)) + "This gate is ~Ud({}, {}, {})".format(basis.a, basis.b, basis.c), + stacklevel=2) self.decomposition_fns = [self.decomp0, self.decomp1, self.decomp2_supercontrolled, @@ -351,7 +674,7 @@ def traces(self, target): 4] @staticmethod - def decomp0(target, eps=1e-15): + def decomp0(target): """Decompose target ~Ud(x, y, z) with 0 uses of the basis gate. Result Ur has trace: :math:`|Tr(Ur.Utarget^dag)| = 4|(cos(x)cos(y)cos(z)+ j sin(x)sin(y)sin(z)|`, @@ -359,10 +682,6 @@ def decomp0(target, eps=1e-15): U0l = target.K1l.dot(target.K2l) U0r = target.K1r.dot(target.K2r) - U0l.real[abs(U0l.real) < eps] = 0.0 - U0l.imag[abs(U0l.imag) < eps] = 0.0 - U0r.real[abs(U0r.real) < eps] = 0.0 - U0r.imag[abs(U0r.imag) < eps] = 0.0 return U0r, U0l def decomp1(self, target): diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 3fd909845297..e5a2884b5d7e 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -114,7 +114,8 @@ def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-7): decomp_unitary = op.data maxdist = np.max(np.abs(target_unitary - decomp_unitary)) self.assertTrue(np.abs(maxdist) < tolerance, - "Unitary {}: Worst distance {}".format(target_unitary, maxdist)) + f"{decomp}\nactual fid: {decomp.actual_fidelity()}\n" + f"Unitary {target_unitary}:\nWorst distance {maxdist}") def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-7): """Check exact decomposition for a particular target""" From 916c146963551d0a8b7842b0fdf5f88253c117be Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 3 Mar 2021 14:00:41 -0500 Subject: [PATCH 04/28] improve 1q decomposition and testing --- .../synthesis/one_qubit_decompose.py | 225 ++++------ .../synthesis/two_qubit_decompose.py | 12 +- test/python/quantum_info/test_synthesis.py | 408 +++++++++++++++++- 3 files changed, 501 insertions(+), 144 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 3d28b1cdc640..580c46c47caf 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -15,6 +15,7 @@ """ import math +import cmath import numpy as np import scipy.linalg as la @@ -204,7 +205,7 @@ def _params_zyz(mat): # We rescale the input matrix to be special unitary (det(U) = 1) # This ensures that the quaternion representation is real coeff = la.det(mat)**(-0.5) - phase = -np.angle(coeff) + phase = -cmath.phase(coeff) su_mat = coeff * mat # U in SU(2) # OpenQASM SU(2) parameterization: # U[0, 0] = exp(-i(phi+lambda)/2) * cos(theta/2) @@ -212,8 +213,8 @@ def _params_zyz(mat): # U[1, 0] = exp(i(phi-lambda)/2) * sin(theta/2) # U[1, 1] = exp(i(phi+lambda)/2) * cos(theta/2) theta = 2 * math.atan2(abs(su_mat[1, 0]), abs(su_mat[0, 0])) - phiplambda = 2 * np.angle(su_mat[1, 1]) - phimlambda = 2 * np.angle(su_mat[1, 0]) + phiplambda = 2 * cmath.phase(su_mat[1, 1]) + phimlambda = 2 * cmath.phase(su_mat[1, 0]) phi = (phiplambda + phimlambda) / 2.0 lam = (phiplambda - phimlambda) / 2.0 return theta, phi, lam, phase @@ -269,14 +270,17 @@ def _circuit_zyz(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): - circuit.append(RZGate(phi + lam), [0]) + + if simplify and abs(theta) < atol: + if abs(phi + lam) > atol: + circuit.append(RZGate(phi + lam), [0]) return circuit - if not simplify or not np.isclose(lam, 0.0, atol=atol, rtol=0): + if simplify and abs(theta - np.pi) < atol: + lam, phi = (lam-phi) / 2, -(lam-phi) / 2 + if not simplify or abs(lam) > atol: circuit.append(RZGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): - circuit.append(RYGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): + circuit.append(RYGate(theta), [0]) + if not simplify or abs(phi) > atol: circuit.append(RZGate(phi), [0]) return circuit @@ -288,15 +292,16 @@ def _circuit_zxz(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): - if not np.isclose(phi + lam, 0.0, atol=atol, rtol=0): + if simplify and abs(theta) < atol: + if abs(phi + lam) > atol: circuit.append(RZGate(phi + lam), [0]) return circuit - if not simplify or not np.isclose(lam, 0.0, atol=atol): + if simplify and abs(theta - np.pi) < atol: + lam, phi = (lam-phi) / 2, -(lam-phi) / 2 + if not simplify or abs(lam) > atol: circuit.append(RZGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): - circuit.append(RXGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): + circuit.append(RXGate(theta), [0]) + if not simplify or abs(phi) > atol: circuit.append(RZGate(phi), [0]) return circuit @@ -308,15 +313,14 @@ def _circuit_xyx(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and np.isclose(theta, 0.0, atol=atol, rtol=0): - if not np.isclose(phi + lam, 0.0, atol=atol, rtol=0): + if simplify and abs(theta) < atol: + if abs(phi + lam) > atol: circuit.append(RXGate(phi + lam), [0]) return circuit - if not simplify or not np.isclose(lam, 0.0, atol=atol, rtol=0): + if not simplify or abs(lam) > atol: circuit.append(RXGate(lam), [0]) - if not simplify or not np.isclose(theta, 0.0, atol=atol, rtol=0): - circuit.append(RYGate(theta), [0]) - if not simplify or not np.isclose(phi, 0.0, atol=atol, rtol=0): + circuit.append(RYGate(theta), [0]) + if not simplify or abs(phi) > atol: circuit.append(RXGate(phi), [0]) return circuit @@ -327,9 +331,9 @@ def _circuit_u3(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # pylint: disable=unused-argument circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(U3Gate(theta, phi, lam), [0]) + if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: + circuit.append(U3Gate(theta, phi, lam), [0]) return circuit @staticmethod @@ -340,10 +344,10 @@ def _circuit_u321(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if simplify and (np.isclose(theta, 0.0, atol=atol, rtol=0)): - if not np.isclose(phi+lam, [0.0, 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(U1Gate(_mod2pi(phi+lam)), [0]) - elif simplify and np.isclose(theta, np.pi/2, atol=atol, rtol=0): + if simplify and abs(theta) < atol: + if abs(phi+lam) > atol: + circuit.append(U1Gate(phi + lam), [0]) + elif simplify and abs(theta - np.pi/2) < atol: circuit.append(U2Gate(phi, lam), [0]) else: circuit.append(U3Gate(theta, phi, lam), [0]) @@ -356,9 +360,9 @@ def _circuit_u(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # pylint: disable=unused-argument circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(UGate(theta, phi, lam), [0]) + if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: + circuit.append(UGate(theta, phi, lam), [0]) return circuit @staticmethod @@ -368,43 +372,30 @@ def _circuit_psx(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # Shift theta and phi so decomposition is - # Phase(phi+pi).SX.Phase(theta+pi).SX.Phase(lam) - theta = _mod2pi(theta + np.pi) - phi = _mod2pi(phi + np.pi) - circuit = QuantumCircuit(1, global_phase=phase - np.pi / 2) - # Check for decomposition into minimimal number required SX gates - if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): - if not np.isclose(_mod2pi(abs(lam + phi + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(_mod2pi(lam + phi + theta)), [0]) - circuit.global_phase += np.pi / 2 - elif simplify and np.isclose(abs(theta), - [np.pi/2, 3*np.pi/2], atol=atol, rtol=0).any(): - if not np.isclose(_mod2pi(abs(lam + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(_mod2pi(lam + theta)), [0]) - circuit.append(SXGate(), [0]) - if not np.isclose(_mod2pi(abs(phi + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(_mod2pi(phi + theta)), [0]) - if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol, rtol=0).any(): - circuit.global_phase += np.pi / 2 - elif simplify: - if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(lam), [0]) - circuit.append(SXGate(), [0]) - if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(theta), [0]) - circuit.append(SXGate(), [0]) - if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(PhaseGate(phi), [0]) - else: - circuit.append(PhaseGate(lam), [0]) - circuit.append(SXGate(), [0]) - circuit.append(PhaseGate(theta), [0]) + # Check for decomposition into minimimal number required SX pulses + if simplify and np.abs(theta) < atol: + # Zero SX gate decomposition + circuit = QuantumCircuit(1, global_phase=phase) + circuit.append(PhaseGate(lam + phi), [0]) + return circuit + if simplify and abs(theta - np.pi/2) < atol: + # Single SX gate decomposition + circuit = QuantumCircuit(1, global_phase=phase) + circuit.append(PhaseGate(lam - np.pi/2), [0]) circuit.append(SXGate(), [0]) - circuit.append(PhaseGate(phi), [0]) + circuit.append(PhaseGate(phi + np.pi/2), [0]) + return circuit + # General two-SX gate decomposition + # Shift theta and phi so decomposition is + # P(phi).SX.P(theta).SX.P(lam) + theta += np.pi + phi += np.pi + circuit = QuantumCircuit(1, global_phase=phase-np.pi/2) + circuit.append(PhaseGate(lam), [0]) + circuit.append(SXGate(), [0]) + circuit.append(PhaseGate(theta), [0]) + circuit.append(SXGate(), [0]) + circuit.append(PhaseGate(phi), [0]) return circuit @staticmethod @@ -414,52 +405,30 @@ def _circuit_zsx(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # Shift theta and phi so decomposition is - # RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) - theta = _mod2pi(theta + np.pi) - phi = _mod2pi(phi + np.pi) - circuit = QuantumCircuit(1, global_phase=phase - np.pi / 2) - # Check for decomposition into minimimal number required SX gates - if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): - if not np.isclose(_mod2pi(abs(lam + phi + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(_mod2pi(lam + phi + theta)), [0]) - circuit.global_phase += 0.5 * _mod2pi(lam + phi + theta) - circuit.global_phase += np.pi / 2 - elif simplify and np.isclose(abs(theta), - [np.pi/2, 3*np.pi/2], atol=atol, rtol=0).any(): - if not np.isclose(_mod2pi(abs(lam + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(_mod2pi(lam + theta)), [0]) - circuit.global_phase += 0.5 * _mod2pi(lam + theta) - circuit.append(SXGate(), [0]) - if not np.isclose(_mod2pi(abs(phi + theta)), - [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(_mod2pi(phi + theta)), [0]) - circuit.global_phase += 0.5 * _mod2pi(phi + theta) - if np.isclose(theta, [-np.pi / 2, 3 * np.pi / 2], atol=atol, rtol=0).any(): - circuit.global_phase += np.pi / 2 - elif simplify: - if not np.isclose(abs(lam), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(lam), [0]) - circuit.global_phase += 0.5 * lam - circuit.append(SXGate(), [0]) - if not np.isclose(abs(theta), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(theta), [0]) - circuit.global_phase += 0.5 * theta - circuit.append(SXGate(), [0]) - if not np.isclose(abs(phi), [0., 2*np.pi], atol=atol, rtol=0).any(): - circuit.append(RZGate(phi), [0]) - circuit.global_phase += 0.5 * phi - else: - circuit.append(RZGate(lam), [0]) - circuit.global_phase += 0.5 * lam - circuit.append(SXGate(), [0]) - circuit.append(RZGate(theta), [0]) - circuit.global_phase += 0.5 * theta + # Check for decomposition into minimimal number required SX pulses + if simplify and abs(theta) < atol: + # Zero SX gate decomposition + circuit = QuantumCircuit(1, global_phase=phase + (lam+phi)/2) + circuit.append(RZGate(lam + phi), [0]) + return circuit + if simplify and abs(theta-np.pi/2) < atol: + # Single SX gate decomposition + circuit = QuantumCircuit(1, global_phase=phase + (lam+phi)/2) + circuit.append(RZGate(lam - np.pi/2), [0]) circuit.append(SXGate(), [0]) - circuit.append(RZGate(phi), [0]) - circuit.global_phase += 0.5 * phi + circuit.append(RZGate(phi + np.pi/2), [0]) + return circuit + # General two-SX gate decomposition + # Shift theta and phi so decomposition is + # RZ(phi).SX.RZ(theta).SX.RZ(lam) + theta += np.pi + phi += np.pi + circuit = QuantumCircuit(1, global_phase=phase + (lam+phi+theta)/2 - np.pi/2) + circuit.append(RZGate(lam), [0]) + circuit.append(SXGate(), [0]) + circuit.append(RZGate(theta), [0]) + circuit.append(SXGate(), [0]) + circuit.append(RZGate(phi), [0]) return circuit @staticmethod @@ -469,24 +438,25 @@ def _circuit_u1x(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # Shift theta and phi so decomposition is - # U1(phi).X90.U1(theta).X90.U1(lam) - theta += np.pi - phi += np.pi + # Check for decomposition into minimimal number required X90 pulses - if simplify and np.isclose(abs(theta), np.pi, atol=atol, rtol=0): + if simplify and abs(theta) < atol: # Zero X90 gate decomposition circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(U1Gate(lam + phi + theta), [0]) + circuit.append(U1Gate(lam + phi), [0]) return circuit - if simplify and np.isclose(abs(theta), np.pi/2, atol=atol, rtol=0): + if simplify and abs(theta-np.pi/2) < atol: # Single X90 gate decomposition - circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(U1Gate(lam + theta), [0]) + circuit = QuantumCircuit(1, global_phase=phase+np.pi/4) + circuit.append(U1Gate(lam - np.pi/2), [0]) circuit.append(RXGate(np.pi / 2), [0]) - circuit.append(U1Gate(phi + theta), [0]) + circuit.append(U1Gate(phi + np.pi/2), [0]) return circuit # General two-X90 gate decomposition + # Shift theta and phi so decomposition is + # U1(phi).X90.U1(theta).X90.U1(lam) + theta += np.pi + phi += np.pi circuit = QuantumCircuit(1, global_phase=phase) circuit.append(U1Gate(lam), [0]) circuit.append(RXGate(np.pi / 2), [0]) @@ -503,14 +473,9 @@ def _circuit_rr(theta, simplify=True, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) - if not simplify or not np.isclose(theta, -np.pi, atol=atol, rtol=0): - circuit.append(RGate(theta + np.pi, np.pi / 2 - lam), [0]) - circuit.append(RGate(-np.pi, 0.5 * (phi - lam + np.pi)), [0]) + if simplify and abs(theta) < atol and abs(phi) < atol and abs(lam) < atol: + return circuit + if not simplify or abs(theta - np.pi) > atol: + circuit.append(RGate(theta - np.pi, np.pi / 2 - lam), [0]) + circuit.append(RGate(np.pi, 0.5 * (phi - lam + np.pi)), [0]) return circuit - - -def _mod2pi(angle): - if angle >= 0: - return np.mod(angle, 2*np.pi) - else: - return np.mod(angle, -2*np.pi) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 959a56dd8b0d..383dac611759 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -127,7 +127,7 @@ def __init_subclass__(cls, **kwargs): cls.__new__ = (lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__(cls, *a, fidelity=None, **k)) - def __new__(cls, unitary_matrix, fidelity=(1.-1.E-9)): + def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, @@ -290,7 +290,7 @@ def __init__(self, unitary_matrix: np.ndarray, *args, fidelity=None, **kwargs): self.K1l, self.K1r = od.K1l, od.K1r self.K2l, self.K2r = od.K2l, od.K2r self.global_phase = od.global_phase - self.unitary_matrix = unitary_matrix + self.unitary_matrix = unitary_matrix.copy() self.requested_fidelity = fidelity self._is_flipped_from_original = False self.specialize() @@ -305,8 +305,12 @@ def __init__(self, unitary_matrix: np.ndarray, *args, fidelity=None, **kwargs): self.global_phase += np.angle(tr) self.calculated_fidelity = trace_to_fid(tr) if logger.isEnabledFor(logging.DEBUG): + actual_fidelity = self.actual_fidelity() logger.debug("Requested fidelity: %s calculated fidelity: %s actual fidelity %s", - self.requested_fidelity, self.calculated_fidelity, self.actual_fidelity()) + self.requested_fidelity, self.calculated_fidelity, actual_fidelity) + if abs(self.calculated_fidelity - actual_fidelity) > 1.E-12: + logger.warning("Requested fidelity different from actual by %s", + self.calculated_fidelity - actual_fidelity) if self.requested_fidelity and self.calculated_fidelity + 1.E-13 < self.requested_fidelity: raise QiskitError(f"{self.__class__.__name__}: " f"calculated fidelity: {self.calculated_fidelity} " @@ -342,8 +346,6 @@ def actual_fidelity(self, **kwargs): """Calculates the actual fidelity of the decomposed circuit to the input unitary""" circ = self.circuit(**kwargs) trace = np.trace(Operator(circ).data.T.conj() @ self.unitary_matrix) - if abs(np.imag(trace)) > 1.E-13: - logger.warning("Trace has a non-zero imaginary component: %s", trace) return trace_to_fid(trace) def __repr__(self): diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ae47c31d70cf..bb1f001a0ece 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -18,17 +18,27 @@ import numpy as np -from qiskit import execute +from qiskit import execute, QiskitError from qiskit.circuit import QuantumCircuit, QuantumRegister from qiskit.extensions import UnitaryGate from qiskit.circuit.library import (HGate, IGate, SdgGate, SGate, U3Gate, UGate, XGate, YGate, ZGate, CXGate, CZGate, - iSwapGate, RXXGate) + iSwapGate, RXXGate, RXGate, RYGate, RZGate) from qiskit.providers.basicaer import UnitarySimulatorPy from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.random import random_unitary from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer from qiskit.quantum_info.synthesis.two_qubit_decompose import (TwoQubitWeylDecomposition, + TwoQubitWeylIdEquiv, + TwoQubitWeylSWAPEquiv, + TwoQubitWeylPartialSWAPEquiv, + TwoQubitWeylPartialSWAPFlipEquiv, + TwoQubitWeylfSimaabEquiv, + TwoQubitWeylfSimabbEquiv, + TwoQubitWeylfSimabmbEquiv, + TwoQubitWeylControlledEquiv, + TwoQubitWeylMirrorControlledEquiv, + TwoQubitWeylGeneral, two_qubit_cnot_decompose, TwoQubitBasisDecomposer, Ud) @@ -90,11 +100,24 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-14, si self.assertTrue(np.abs(maxdist) < tolerance, "Operator {}: Worst distance {}".format(operator, maxdist)) - # FIXME: should be possible to set this tolerance tighter after improving the function + def check_oneq_special_cases(self, target, basis, expected_gates=None, tolerance=1.E-12,): + """Check OneQubitEulerDecomposer produces the expected gates""" + decomposer = OneQubitEulerDecomposer(basis) + circ = decomposer(target, simplify=True) + data = Operator(circ).data + maxdist = np.max(np.abs(target.data - data)) + trace = np.trace(data.T.conj() @ target) + self.assertLess(np.abs(maxdist), tolerance, + f"Worst case distance: {maxdist}, trace: {trace}\n" + f"Target:\n{target}\nActual:\n{data}\n{circ}") + if expected_gates is not None: + self.assertDictEqual(dict(circ.count_ops()), expected_gates, + f"Circuit:\n{circ}") + def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name - decomp = TwoQubitWeylDecomposition(target_unitary) + decomp = TwoQubitWeylDecomposition(target_unitary, fidelity=None) op = np.exp(1j * decomp.global_phase) * Operator(np.eye(4)) for u, qs in ( (decomp.K2r, [0]), @@ -106,10 +129,37 @@ def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): op = op.compose(u, qs) decomp_unitary = op.data maxdist = np.max(np.abs(target_unitary - decomp_unitary)) - self.assertTrue(np.abs(maxdist) < tolerance, + self.assertLess(np.abs(maxdist), tolerance, f"{decomp}\nactual fid: {decomp.actual_fidelity()}\n" f"Unitary {target_unitary}:\nWorst distance {maxdist}") + def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, + expected_specialization, expected_gates): + """Check that the two qubit Weyl decomposition gets specialized as expected""" + + # Loop to check both for implicit and explicity specialization + for decomposer in (TwoQubitWeylDecomposition, expected_specialization): + decomp = decomposer(target_unitary, fidelity=fidelity) + self.assertEqual(np.max(np.abs(decomp.unitary_matrix - target_unitary)), 0, + "Incorrect saved unitary in the decomposition.") + self.assertIsInstance(decomp, expected_specialization, + "Incorrect Weyl specialization.") + circ = decomp.circuit(simplify=True) + self.assertDictEqual(dict(circ.count_ops()), expected_gates, + f"Gate counts of {decomposer.__name__}") + actual_fid = decomp.actual_fidelity() + self.assertAlmostEqual(decomp.calculated_fidelity, actual_fid, places=13) + self.assertGreaterEqual(actual_fid, fidelity, + f"fidelity of {decomposer.__name__}") + actual_unitary = Operator(circ).data + trace = np.trace(actual_unitary.T.conj() @ target_unitary) + self.assertAlmostEqual(trace.imag, 0, places=13, + msg=f"Real trace for {decomposer.__name__}") + _ = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise + if expected_specialization is not TwoQubitWeylGeneral: + with self.assertRaises(QiskitError): + _ = expected_specialization(target_unitary, fidelity=1.) + def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-13, num_basis_uses=None): """Check exact decomposition for a particular target""" @@ -145,8 +195,119 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) -ONEQ_BASES = ['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'] -SIMP_TOL = [(False, 1.e-14), (True, 1.E-12)] +class TestOneQubitEulerSpecial(CheckDecompositions): + """Test special cases for OneQubitEulerDecomposer. + + FIXME: Currently these are more like smoke tests that exercise each of the code paths + and shapes of decompositions that can be made, but they don't check all the corner cases + where a wrap by 2*pi might happen, etc + """ + def test_special_ZYZ(self): + """Special cases of ZYZ""" + self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, -0.1).to_matrix(), 'ZYZ', {}) + self.check_oneq_special_cases(U3Gate(np.pi, 0.1, 0.2).to_matrix(), 'ZYZ', + {'rz': 2, 'ry': 1}) + self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, 0.2).to_matrix(), 'ZYZ', {'rz': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 1.E-13).to_matrix(), 'ZYZ', + {'rz': 1, 'ry': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 1.E-13, 0.2).to_matrix(), 'ZYZ', + {'rz': 1, 'ry': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'ZYZ', {'rz': 2, 'ry': 1}) + + def test_special_ZXZ(self): + """Special cases of ZXZ""" + def myr(a, b, c): + return RZGate(b).to_matrix() @ RXGate(a).to_matrix() @ RZGate(c).to_matrix() + self.check_oneq_special_cases(myr(0.0, 0.1, -0.1), 'ZXZ', {}) + self.check_oneq_special_cases(myr(np.pi, 0.1, 0.2), 'ZXZ', {'rz': 2, 'rx': 1}) + self.check_oneq_special_cases(myr(0.0, 0.1, 0.2), 'ZXZ', {'rz': 1}) + self.check_oneq_special_cases(myr(0.1, 0.2, 0.0), 'ZXZ', {'rz': 1, 'rx': 1}) + self.check_oneq_special_cases(myr(0.1, 0.0, 0.2), 'ZXZ', {'rz': 1, 'rx': 1}) + self.check_oneq_special_cases(myr(0.1, 0.2, 0.3), 'ZXZ', {'rz': 2, 'rx': 1}) + + def test_special_XYX(self): + """Special cases of XYX""" + def myr(a, b, c): + return RXGate(b).to_matrix() @ RYGate(a).to_matrix() @ RXGate(c).to_matrix() + self.check_oneq_special_cases(myr(0.0, 0.1, -0.1), 'XYX', {}) + self.check_oneq_special_cases(myr(0.0, 0.1, 0.2), 'XYX', {'rx': 1}) + self.check_oneq_special_cases(myr(-0.1, 0.2, 0.0), 'XYX', {'rx': 1, 'ry': 1}) + self.check_oneq_special_cases(myr(-0.1, 0.0, 0.2), 'XYX', {'rx': 1, 'ry': 1}) + self.check_oneq_special_cases(myr(0.1, 0.2, 0.3), 'XYX', {'rx': 2, 'ry': 1}) + + def test_special_U321(self): + """Special cases of U321""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U321', {}) + self.check_oneq_special_cases(U3Gate(0.0, 0.11, 0.2).to_matrix(), 'U321', {'u1': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.2, 0.0).to_matrix(), 'U321', {'u2': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.2).to_matrix(), 'U321', {'u2': 1}) + self.check_oneq_special_cases(U3Gate(0.11, 0.27, 0.3).to_matrix(), 'U321', {'u3': 1}) + + def test_special_U3(self): + """Special cases of U3""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U3', {}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'U3', {'u3': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.2, 0.0).to_matrix(), 'U3', {'u3': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.2).to_matrix(), 'U3', {'u3': 1}) + self.check_oneq_special_cases(U3Gate(0.11, 0.27, 0.3).to_matrix(), 'U3', {'u3': 1}) + + def test_special_U(self): + """Special cases of U""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U', {}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'U', {'u': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.2, 0.0).to_matrix(), 'U', {'u': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.2).to_matrix(), 'U', {'u': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'U', {'u': 1}) + + def test_special_RR(self): + """Special cases of RR""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'RR', {}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'RR', {'r': 2}) + self.check_oneq_special_cases(U3Gate(-np.pi, 0.2, 0.0).to_matrix(), 'RR', {'r': 1}) + self.check_oneq_special_cases(U3Gate(np.pi, 0.0, 0.2).to_matrix(), 'RR', {'r': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'RR', {'r': 2}) + + def test_special_U1X(self): + """Special cases of U1X""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U1X', {'u1': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'U1X', {'u1': 1}) + self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'U1X', + {'u1': 2, 'rx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.21).to_matrix(), 'U1X', + {'u1': 2, 'rx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.12, 0.2).to_matrix(), 'U1X', + {'u1': 2, 'rx': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'U1X', {'u1': 3, 'rx': 2}) + + def test_special_PSX(self): + """Special cases of PSX""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'PSX', {'p': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'PSX', {'p': 1}) + self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'PSX', + {'p': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.21).to_matrix(), 'PSX', + {'p': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.12, 0.2).to_matrix(), 'PSX', + {'p': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'PSX', {'p': 3, 'sx': 2}) + + def test_special_ZSX(self): + """Special cases of ZSX""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'ZSX', {'rz': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'ZSX', {'rz': 1}) + self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'ZSX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.21).to_matrix(), 'ZSX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.12, 0.2).to_matrix(), 'ZSX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'ZSX', {'rz': 3, 'sx': 2}) + + +ONEQ_BASES = ['U3', "U321", 'U', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'] +SIMP_TOL = [(False, 1.e-14), (True, 1.E-12)] # Please don't broaden the tolerance (fix the decomp) + + @ddt class TestOneQubitEulerDecomposer(CheckDecompositions): """Test OneQubitEulerDecomposer""" @@ -349,6 +510,148 @@ def test_two_qubit_weyl_decomposition_abc(self, smallest=1e-18, factor=9.8, step self.check_two_qubit_weyl_decomposition(k1 @ a @ k2) +K1K2Sb = [[Operator(U3Gate(*xyz)) for xyz in xyzs] for xyzs in + [[(0.2, 0.3, 0.1), (0.7, 0.15, 0.22), (0.1, 0.97, 2.2), (3.14, 2.1, 0.9)], + [(0.21, 0.13, 0.45), (2.1, 0.77, 0.88), (1.5, 2.3, 2.3), (2.1, 0.4, 1.7)]]] +DELTAS = [(-0.019, 0.018, 0.021), (0.01, 0.015, 0.02), (-0.01, -0.009, 0.011), + (-0.002, -0.003, -0.004)] + + +class TestTwoQubitWeylDecompositionSpecialization(CheckDecompositions): + """Check TwoQubitWeylDecomposition specialized subclasses""" + + def test_weyl_specialize_id(self): + """Weyl specialization for Id gate""" + a, b, c = 0., 0., 0. + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylIdEquiv, + {'rz': 4, 'ry': 2}) + + def test_weyl_specialize_swap(self): + """Weyl specialization for swap gate""" + a, b, c = np.pi/4, np.pi/4, np.pi/4 + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylSWAPEquiv, + {'rz': 4, 'ry': 2, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_flip_swap(self): + """Weyl specialization for flip swap gate""" + a, b, c = np.pi/4, np.pi/4, -np.pi/4 + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylSWAPEquiv, + {'rz': 4, 'ry': 2, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_pswap(self, theta=0.123): + """Weyl specialization for partial swap gate""" + a, b, c = theta, theta, theta + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylPartialSWAPEquiv, + {'rz': 6, 'ry': 3, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_flip_pswap(self, theta=0.123): + """Weyl specialization for flipped partial swap gate""" + a, b, c = theta, theta, -theta + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylPartialSWAPFlipEquiv, + {'rz': 6, 'ry': 3, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_fsim_aab(self, aaa=0.456, bbb=0.132): + """Weyl specialization for partial swap gate""" + a, b, c = aaa, aaa, bbb + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylfSimaabEquiv, + {'rz': 7, 'ry': 4, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_fsim_abb(self, aaa=0.456, bbb=0.132): + """Weyl specialization for partial swap gate""" + a, b, c = aaa, bbb, bbb + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylfSimabbEquiv, + {'rx': 7, 'ry': 4, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_fsim_abmb(self, aaa=0.456, bbb=0.132): + """Weyl specialization for partial swap gate""" + a, b, c = aaa, bbb, -bbb + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylfSimabmbEquiv, + {'rx': 7, 'ry': 4, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_ctrl(self, aaa=0.456): + """Weyl specialization for partial swap gate""" + a, b, c = aaa, 0., 0. + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylControlledEquiv, + {'rx': 6, 'ry': 4, + 'rxx': 1}) + + def test_weyl_specialize_mirror_ctrl(self, aaa=-0.456): + """Weyl specialization for partial swap gate""" + a, b, c = np.pi/4, np.pi/4, aaa + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylMirrorControlledEquiv, + {'rz': 6, 'ry': 4, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + def test_weyl_specialize_general(self, aaa=0.456, bbb=0.345, ccc=0.123): + """Weyl specialization for partial swap gate""" + a, b, c = aaa, bbb, ccc + for da, db, dc in DELTAS: + for k1l, k1r, k2l, k2r in K1K2Sb: + k1 = np.kron(k1l.data, k1r.data) + k2 = np.kron(k2l.data, k2r.data) + self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, + 0.999, TwoQubitWeylGeneral, + {'rz': 8, 'ry': 4, + 'rxx': 1, 'ryy': 1, 'rzz': 1}) + + @ddt class TestTwoQubitDecomposeExact(CheckDecompositions): """Test TwoQubitBasisDecomposer() for exact decompositions @@ -455,7 +758,8 @@ def test_exact_supercontrolled_decompose_phase_3_use_random(self, seed): tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_phase = state.random() * 2 * np.pi - tgt_a, tgt_b, tgt_c = state.random(size=3) * np.pi / 4 + tgt_a, tgt_b = state.random(size=2) * np.pi / 4 + tgt_c = state.random() * np.pi/2 - np.pi/4 tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(tgt_a, tgt_b, tgt_c) @ tgt_k2 self.check_exact_decomposition(tgt_unitary, decomposer, num_basis_uses=3) @@ -592,7 +896,93 @@ def test_euler_basis_selection(self, euler_bases, kak_gates, seed): decomposition_basis.issubset(requested_basis)) -# FIXME: need to write tests for the approximate decompositions +@ddt +class TestTwoQubitDecomposeApprox(CheckDecompositions): + """Smoke tests for approximate decompositions""" + + def check_approx_decomposition(self, target_unitary, decomposer, num_basis_uses): + """Check approx decomposition for a particular target""" + decomp_circuit = decomposer(target_unitary, num_basis_uses=num_basis_uses) + self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get('unitary', 0)) + # Now check the fidelity? + + @combine(seed=range(10), name='seed_{seed}') + def test_approx_supercontrolled_decompose_phase_0_use_random(self, seed, delta=0.01): + """Approx decomposition supercontrolled basis, random target (0 basis uses) (seed={seed}) + """ + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = 0.4 # how to safely randomize? + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + d1, d2, d3 = state.random(size=3) * delta + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(d1, d2, d3) @ tgt_k2 + self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=0) + + @combine(seed=range(10), name='seed_{seed}') + def test_approx_supercontrolled_decompose_phase_1_use_random(self, seed, delta=0.01): + """Approximate decomposition supercontrolled basis, random tgt (1 basis uses) (seed={seed}) + """ + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = 0.4 # how to safely randomize? + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + d1, d2, d3 = state.random(size=3) * delta + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(np.pi / 4-d1, basis_b+d2, d3) @ tgt_k2 + self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=1) + + @combine(seed=range(10), name='seed_{seed}') + def test_approx_supercontrolled_decompose_phase_2_use_random(self, seed, delta=0.01): + """Approximate decomposition supercontrolled basis, random tgt (2 basis uses) (seed={seed}) + """ + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = 0.4 # how to safely randomize? + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + tgt_a, tgt_b = 0.3, 0.2 # how to safely randomize? + d1, d2, d3 = state.random(size=3) * delta + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(tgt_a+d1, tgt_b+d2, d3) @ tgt_k2 + self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=2) + + @combine(seed=range(10), name='seed_{seed}') + def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0.01): + """Approximate decomposition supercontrolled basis, random tgt (3 basis uses) (seed={seed}) + """ + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + + tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + tgt_phase = state.random() * 2 * np.pi + tgt_a, tgt_b, tgt_c = 0.5, 0.4, 0.3 + d1, d2, d3 = state.random(size=3) * delta + tgt_unitary = np.exp(1j * tgt_phase) * tgt_k1 @ Ud(tgt_a+d1, tgt_b+d2, tgt_c+d3) @ tgt_k2 + self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=3) if __name__ == '__main__': From 19d26333d39889d4b6fd986325b32a2f94348618 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 3 Mar 2021 14:03:23 -0500 Subject: [PATCH 05/28] Streamline TwoQubitBasisDecomposer testing --- test/python/quantum_info/test_synthesis.py | 74 +++++++++++++--------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ae47c31d70cf..e97fa054daa0 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -90,7 +90,6 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-14, si self.assertTrue(np.abs(maxdist) < tolerance, "Operator {}: Worst distance {}".format(operator, maxdist)) - # FIXME: should be possible to set this tolerance tighter after improving the function def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name @@ -145,8 +144,11 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) +# Please think at least 3 times before making tolerances wider. +# We want to avoid introducing inaccuracy that can build up. ONEQ_BASES = ['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'] SIMP_TOL = [(False, 1.e-14), (True, 1.E-12)] + @ddt class TestOneQubitEulerDecomposer(CheckDecompositions): """Test OneQubitEulerDecomposer""" @@ -350,10 +352,9 @@ def test_two_qubit_weyl_decomposition_abc(self, smallest=1e-18, factor=9.8, step @ddt -class TestTwoQubitDecomposeExact(CheckDecompositions): - """Test TwoQubitBasisDecomposer() for exact decompositions +class TestTwoQubitDecompose(CheckDecompositions): + """Test TwoQubitBasisDecomposer() for exact/approx decompositions """ - def test_cnot_rxx_decompose(self): """Verify CNOT decomposition into RXX gate is correct""" cnot = Operator(CXGate()) @@ -378,25 +379,30 @@ def test_exact_two_qubit_cnot_decompose_paulis(self): unitary = Operator.from_label('XZ') self.check_exact_decomposition(unitary.data, two_qubit_cnot_decompose) + def make_random_supercontrolled_decomposer(self, seed): + """Return a random supercontrolled unitary given a seed""" + state = np.random.default_rng(seed) + basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) + basis_phase = state.random() * 2 * np.pi + basis_b = state.random() * np.pi / 4 + basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + return decomposer + @combine(seed=range(10), name='test_exact_supercontrolled_decompose_random_{seed}') def test_exact_supercontrolled_decompose_random(self, seed): """Exact decomposition for random supercontrolled basis and random target (seed={seed})""" - k1 = np.kron(random_unitary(2, seed=seed).data, random_unitary(2, seed=seed + 1).data) - k2 = np.kron(random_unitary(2, seed=seed + 2).data, random_unitary(2, seed=seed + 3).data) - basis_unitary = k1 @ Ud(np.pi / 4, 0, 0) @ k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) - self.check_exact_decomposition(random_unitary(4, seed=seed + 4).data, decomposer) + # pylint: disable=invalid-name + state = np.random.default_rng(seed) + decomposer = self.make_random_supercontrolled_decomposer(state) + self.check_exact_decomposition(random_unitary(4, seed=state).data, decomposer) @combine(seed=range(10), name='seed_{seed}') def test_exact_supercontrolled_decompose_phase_0_use_random(self, seed): """Exact decomposition supercontrolled basis, random target (0 basis uses) (seed={seed})""" state = np.random.default_rng(seed) - basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_phase = state.random() * 2 * np.pi - basis_b = state.random() * np.pi / 4 - basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = self.make_random_supercontrolled_decomposer(state) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -426,12 +432,7 @@ def test_exact_supercontrolled_decompose_phase_1_use_random(self, seed): def test_exact_supercontrolled_decompose_phase_2_use_random(self, seed): """Exact decomposition supercontrolled basis, random tgt (2 basis uses) (seed={seed})""" state = np.random.default_rng(seed) - basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_phase = state.random() * 2 * np.pi - basis_b = state.random() * np.pi / 4 - basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = self.make_random_supercontrolled_decomposer(state) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -444,12 +445,7 @@ def test_exact_supercontrolled_decompose_phase_2_use_random(self, seed): def test_exact_supercontrolled_decompose_phase_3_use_random(self, seed): """Exact decomposition supercontrolled basis, random tgt (3 basis uses) (seed={seed})""" state = np.random.default_rng(seed) - basis_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) - basis_phase = state.random() * 2 * np.pi - basis_b = state.random() * np.pi / 4 - basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = self.make_random_supercontrolled_decomposer(state) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -464,6 +460,26 @@ def test_exact_nonsupercontrolled_decompose(self): with self.assertWarns(UserWarning, msg="Supposed to warn when basis non-supercontrolled"): TwoQubitBasisDecomposer(UnitaryGate(Ud(np.pi / 4, 0.2, 0.1))) + @combine(seed=range(10), name='seed_{seed}') + def test_approx_supercontrolled_decompose_random(self, seed): + """Check that n-uses of supercontrolled basis give the expected trace distance""" + state = np.random.default_rng(seed) + decomposer = self.make_random_supercontrolled_decomposer(state) + + tgt_phase = state.random() * 2 * np.pi + tgt = random_unitary(4, seed=state).data + tgt *= np.exp(1j * tgt_phase) + + traces_pred = decomposer.traces(TwoQubitWeylDecomposition(tgt)) + + for i in range(4): + decomp_circuit = decomposer(tgt, num_basis_uses=i) + result = execute(decomp_circuit, UnitarySimulatorPy(), optimization_level=0).result() + decomp_unitary = result.get_unitary() + tr_actual = np.trace(decomp_unitary.conj().T @ tgt) + self.assertAlmostEqual(traces_pred[i], tr_actual, places=13, + msg=f"Trace doesn't match for {i}-basis decomposition") + def test_cx_equivalence_0cx(self, seed=0): """Check circuits with 0 cx gates locally equivalent to identity """ @@ -591,9 +607,5 @@ def test_euler_basis_selection(self, euler_bases, kak_gates, seed): self.assertTrue( decomposition_basis.issubset(requested_basis)) - -# FIXME: need to write tests for the approximate decompositions - - if __name__ == '__main__': unittest.main() From f82a03b0a790a47c2eac57ca9bc78299453f0139 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Thu, 4 Mar 2021 18:13:16 -0500 Subject: [PATCH 06/28] Pass tests --- qiskit/circuit/quantumcircuit.py | 4 +- .../synthesis/one_qubit_decompose.py | 133 ++++++++---------- .../synthesis/two_qubit_decompose.py | 98 +++++++------ test/python/circuit/test_unitary.py | 9 +- test/python/compiler/test_transpiler.py | 11 +- .../quantum_info/states/test_statevector.py | 2 +- test/python/quantum_info/test_synthesis.py | 37 +++-- .../transpiler/test_basis_translator.py | 3 +- .../test_optimize_1q_decomposition.py | 6 +- 9 files changed, 153 insertions(+), 150 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 224d155b6979..f5d0a5480397 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1852,12 +1852,10 @@ def global_phase(self, angle): if isinstance(angle, ParameterExpression) and angle.parameters: self._global_phase = angle else: - # Set the phase to the [-2 * pi, 2 * pi] interval + # Set the phase to the [0, 2π) interval angle = float(angle) if not angle: self._global_phase = 0 - elif angle < 0: - self._global_phase = angle % (-2 * np.pi) else: self._global_phase = angle % (2 * np.pi) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 580c46c47caf..d9287d29f104 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -213,10 +213,10 @@ def _params_zyz(mat): # U[1, 0] = exp(i(phi-lambda)/2) * sin(theta/2) # U[1, 1] = exp(i(phi+lambda)/2) * cos(theta/2) theta = 2 * math.atan2(abs(su_mat[1, 0]), abs(su_mat[0, 0])) - phiplambda = 2 * cmath.phase(su_mat[1, 1]) - phimlambda = 2 * cmath.phase(su_mat[1, 0]) - phi = (phiplambda + phimlambda) / 2.0 - lam = (phiplambda - phimlambda) / 2.0 + phiplambda2 = cmath.phase(su_mat[1, 1]) + phimlambda2 = cmath.phase(su_mat[1, 0]) + phi = (phiplambda2 + phimlambda2) + lam = (phiplambda2 - phimlambda2) return theta, phi, lam, phase @staticmethod @@ -345,8 +345,9 @@ def _circuit_u321(theta, atol=DEFAULT_ATOL): circuit = QuantumCircuit(1, global_phase=phase) if simplify and abs(theta) < atol: - if abs(phi+lam) > atol: - circuit.append(U1Gate(phi + lam), [0]) + tot = _mod_2pi(phi + lam) + if abs(tot) > atol: + circuit.append(U1Gate(tot), [0]) elif simplify and abs(theta - np.pi/2) < atol: circuit.append(U2Gate(phi, lam), [0]) else: @@ -366,38 +367,48 @@ def _circuit_u(theta, return circuit @staticmethod - def _circuit_psx(theta, - phi, - lam, - phase, - simplify=True, - atol=DEFAULT_ATOL): + def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun): + """Generic X90, phase decomposition""" + circuit = QuantumCircuit(1, global_phase=phase) # Check for decomposition into minimimal number required SX pulses if simplify and np.abs(theta) < atol: # Zero SX gate decomposition - circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(PhaseGate(lam + phi), [0]) + pfun(circuit, lam + phi) return circuit if simplify and abs(theta - np.pi/2) < atol: # Single SX gate decomposition - circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(PhaseGate(lam - np.pi/2), [0]) - circuit.append(SXGate(), [0]) - circuit.append(PhaseGate(phi + np.pi/2), [0]) + pfun(circuit, lam - np.pi/2) + xfun(circuit) + pfun(circuit, phi + np.pi/2) return circuit # General two-SX gate decomposition # Shift theta and phi so decomposition is # P(phi).SX.P(theta).SX.P(lam) - theta += np.pi - phi += np.pi - circuit = QuantumCircuit(1, global_phase=phase-np.pi/2) - circuit.append(PhaseGate(lam), [0]) - circuit.append(SXGate(), [0]) - circuit.append(PhaseGate(theta), [0]) - circuit.append(SXGate(), [0]) - circuit.append(PhaseGate(phi), [0]) + circuit.global_phase -= np.pi/2 + pfun(circuit, lam) + xfun(circuit) + pfun(circuit, theta + np.pi) + xfun(circuit) + pfun(circuit, phi + np.pi) return circuit + @staticmethod + def _circuit_psx(theta, + phi, + lam, + phase, + simplify=True, + atol=DEFAULT_ATOL): + def fnz(circuit, phi): + phi = _mod_2pi(phi) + if not simplify or abs(phi) > atol: + circuit.append(PhaseGate(phi), [0]) + + def fnx(circuit): + circuit.append(SXGate(), [0]) + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, + fnz, fnx) + @staticmethod def _circuit_zsx(theta, phi, @@ -405,31 +416,16 @@ def _circuit_zsx(theta, phase, simplify=True, atol=DEFAULT_ATOL): - # Check for decomposition into minimimal number required SX pulses - if simplify and abs(theta) < atol: - # Zero SX gate decomposition - circuit = QuantumCircuit(1, global_phase=phase + (lam+phi)/2) - circuit.append(RZGate(lam + phi), [0]) - return circuit - if simplify and abs(theta-np.pi/2) < atol: - # Single SX gate decomposition - circuit = QuantumCircuit(1, global_phase=phase + (lam+phi)/2) - circuit.append(RZGate(lam - np.pi/2), [0]) + def fnz(circuit, phi): + phi = _mod_2pi(phi) + if not simplify or abs(phi) > atol: + circuit.append(RZGate(phi), [0]) + circuit.global_phase += phi/2 + + def fnx(circuit): circuit.append(SXGate(), [0]) - circuit.append(RZGate(phi + np.pi/2), [0]) - return circuit - # General two-SX gate decomposition - # Shift theta and phi so decomposition is - # RZ(phi).SX.RZ(theta).SX.RZ(lam) - theta += np.pi - phi += np.pi - circuit = QuantumCircuit(1, global_phase=phase + (lam+phi+theta)/2 - np.pi/2) - circuit.append(RZGate(lam), [0]) - circuit.append(SXGate(), [0]) - circuit.append(RZGate(theta), [0]) - circuit.append(SXGate(), [0]) - circuit.append(RZGate(phi), [0]) - return circuit + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, + fnz, fnx) @staticmethod def _circuit_u1x(theta, @@ -438,32 +434,16 @@ def _circuit_u1x(theta, phase, simplify=True, atol=DEFAULT_ATOL): + def fnz(circuit, phi): + phi = _mod_2pi(phi) + if not simplify or abs(phi) > atol: + circuit.append(U1Gate(phi), [0]) - # Check for decomposition into minimimal number required X90 pulses - if simplify and abs(theta) < atol: - # Zero X90 gate decomposition - circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(U1Gate(lam + phi), [0]) - return circuit - if simplify and abs(theta-np.pi/2) < atol: - # Single X90 gate decomposition - circuit = QuantumCircuit(1, global_phase=phase+np.pi/4) - circuit.append(U1Gate(lam - np.pi/2), [0]) + def fnx(circuit): + circuit.global_phase += np.pi/4 circuit.append(RXGate(np.pi / 2), [0]) - circuit.append(U1Gate(phi + np.pi/2), [0]) - return circuit - # General two-X90 gate decomposition - # Shift theta and phi so decomposition is - # U1(phi).X90.U1(theta).X90.U1(lam) - theta += np.pi - phi += np.pi - circuit = QuantumCircuit(1, global_phase=phase) - circuit.append(U1Gate(lam), [0]) - circuit.append(RXGate(np.pi / 2), [0]) - circuit.append(U1Gate(theta), [0]) - circuit.append(RXGate(np.pi / 2), [0]) - circuit.append(U1Gate(phi), [0]) - return circuit + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, + fnz, fnx) @staticmethod def _circuit_rr(theta, @@ -479,3 +459,8 @@ def _circuit_rr(theta, circuit.append(RGate(theta - np.pi, np.pi / 2 - lam), [0]) circuit.append(RGate(np.pi, 0.5 * (phi - lam + np.pi)), [0]) return circuit + + +def _mod_2pi(angle: float): + """Wrap angle into interval [-π,π)""" + return (angle+np.pi) % (2*np.pi) - np.pi diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 383dac611759..099931f3f802 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -23,6 +23,7 @@ Gambetta, J. M. Validating quantum computers using randomized model circuits. arXiv:1811.12926 [quant-ph] (2018). """ +import cmath import math import warnings from typing import Optional @@ -65,10 +66,10 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): if abs(detL) < 0.9: raise QiskitError("decompose_two_qubit_product_gate: unable to decompose: detL < 0.9") L /= np.sqrt(detL) - phase = np.angle(detL) / 2 + phase = cmath.phase(detL) / 2 temp = np.kron(L, R) - deviation = np.abs(np.abs(temp.conj(temp).T.dot(special_unitary_matrix).trace()) - 4) + deviation = abs(abs(temp.conj(temp).T.dot(special_unitary_matrix).trace()) - 4) if deviation > 1.E-13: raise QiskitError("decompose_two_qubit_product_gate: decomposition failed: " "deviation too large: {}".format(deviation)) @@ -147,7 +148,7 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): U = unitary_matrix.copy() detU = la.det(U) U *= detU**(-0.25) - global_phase = np.angle(detU) / 4 + global_phase = cmath.phase(detU) / 4 Up = _Bd.dot(U).dot(_B) M2 = Up.T.dot(Up) @@ -252,7 +253,8 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): def is_close(ap, bp, cp): da, db, dc = a-ap, b-bp, c-cp - tr = 4*complex(np.cos(da)*np.cos(db)*np.cos(dc) + np.sin(da)*np.sin(db)*np.sin(dc)) + tr = 4*complex(math.cos(da)*math.cos(db)*math.cos(dc), + math.sin(da)*math.sin(db)*math.sin(dc)) fid = trace_to_fid(tr) return fid >= fidelity @@ -298,11 +300,13 @@ def __init__(self, unitary_matrix: np.ndarray, *args, fidelity=None, **kwargs): # Update the phase after specialization: if self._is_flipped_from_original: da, db, dc = (np.pi/2-od.a)-self.a, od.b-self.b, -od.c-self.c - tr = 4 * complex(np.cos(da)*np.cos(db)*np.cos(dc), np.sin(da)*np.sin(db)*np.sin(dc)) + tr = 4 * complex(math.cos(da)*math.cos(db)*math.cos(dc), + math.sin(da)*math.sin(db)*math.sin(dc)) else: da, db, dc = od.a-self.a, od.b-self.b, od.c-self.c - tr = 4 * complex(np.cos(da)*np.cos(db)*np.cos(dc), np.sin(da)*np.sin(db)*np.sin(dc)) - self.global_phase += np.angle(tr) + tr = 4 * complex(math.cos(da)*math.cos(db)*math.cos(dc), + math.sin(da)*math.sin(db)*math.sin(dc)) + self.global_phase += cmath.phase(tr) self.calculated_fidelity = trace_to_fid(tr) if logger.isEnabledFor(logging.DEBUG): actual_fidelity = self.actual_fidelity() @@ -552,16 +556,17 @@ def specialize(self): def Ud(a, b, c): """Generates the array Exp(i(a xx + b yy + c zz)) """ - return np.array([[np.exp(1j*c)*np.cos(a-b), 0, 0, 1j*np.exp(1j*c)*np.sin(a-b)], - [0, np.exp(-1j*c)*np.cos(a+b), 1j*np.exp(-1j*c)*np.sin(a+b), 0], - [0, 1j*np.exp(-1j*c)*np.sin(a+b), np.exp(-1j*c)*np.cos(a+b), 0], - [1j*np.exp(1j*c)*np.sin(a-b), 0, 0, np.exp(1j*c)*np.cos(a-b)]], dtype=complex) + return np.array([[cmath.exp(1j*c)*math.cos(a-b), 0, 0, 1j*cmath.exp(1j*c)*math.sin(a-b)], + [0, cmath.exp(-1j*c)*math.cos(a+b), 1j*cmath.exp(-1j*c)*math.sin(a+b), 0], + [0, 1j*cmath.exp(-1j*c)*math.sin(a+b), cmath.exp(-1j*c)*math.cos(a+b), 0], + [1j*cmath.exp(1j*c)*math.sin(a-b), 0, 0, cmath.exp(1j*c)*math.cos(a-b)]], + dtype=complex) def trace_to_fid(trace): """Average gate fidelity is :math:`Fbar = (d + |Tr (Utarget \\cdot U^dag)|^2) / d(d+1)` M. Horodecki, P. Horodecki and R. Horodecki, PRA 60, 1888 (1999)""" - return (4 + np.abs(trace)**2)/20 + return (4 + abs(trace)**2)/20 def rz_array(theta): @@ -569,8 +574,8 @@ def rz_array(theta): Rz(theta) = diag(exp(-i*theta/2),exp(i*theta/2)) """ - return np.array([[np.exp(-1j*theta/2.0), 0], - [0, np.exp(1j*theta/2.0)]], dtype=complex) + return np.array([[cmath.exp(-1j*theta/2.0), 0], + [0, cmath.exp(1j*theta/2.0)]], dtype=complex) class TwoQubitBasisDecomposer(): @@ -601,27 +606,27 @@ def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): # Create some useful matrices U1, U2, U3 are equivalent to the basis, # expand as Ui = Ki1.Ubasis.Ki2 b = basis.b - K11l = 1/(1+1j) * np.array([[-1j*np.exp(-1j*b), np.exp(-1j*b)], - [-1j*np.exp(1j*b), -np.exp(1j*b)]], dtype=complex) - K11r = 1/np.sqrt(2) * np.array([[1j*np.exp(-1j*b), -np.exp(-1j*b)], - [np.exp(1j*b), -1j*np.exp(1j*b)]], dtype=complex) + K11l = 1/(1+1j) * np.array([[-1j*cmath.exp(-1j*b), cmath.exp(-1j*b)], + [-1j*cmath.exp(1j*b), -cmath.exp(1j*b)]], dtype=complex) + K11r = 1/math.sqrt(2) * np.array([[1j*cmath.exp(-1j*b), -cmath.exp(-1j*b)], + [cmath.exp(1j*b), -1j*cmath.exp(1j*b)]], dtype=complex) K12l = 1/(1+1j) * np.array([[1j, 1j], [-1, 1]], dtype=complex) - K12r = 1/np.sqrt(2) * np.array([[1j, 1], - [-1, -1j]], dtype=complex) - K32lK21l = 1/np.sqrt(2) * np.array([[1+1j*np.cos(2*b), 1j*np.sin(2*b)], - [1j*np.sin(2*b), 1-1j*np.cos(2*b)]], dtype=complex) - K21r = 1/(1-1j) * np.array([[-1j*np.exp(-2j*b), np.exp(-2j*b)], - [1j*np.exp(2j*b), np.exp(2j*b)]], dtype=complex) - K22l = 1/np.sqrt(2) * np.array([[1, -1], - [1, 1]], dtype=complex) + K12r = 1/math.sqrt(2) * np.array([[1j, 1], + [-1, -1j]], dtype=complex) + K32lK21l = 1/math.sqrt(2) * np.array([[1+1j*np.cos(2*b), 1j*np.sin(2*b)], + [1j*np.sin(2*b), 1-1j*np.cos(2*b)]], dtype=complex) + K21r = 1/(1-1j) * np.array([[-1j*cmath.exp(-2j*b), cmath.exp(-2j*b)], + [1j*cmath.exp(2j*b), cmath.exp(2j*b)]], dtype=complex) + K22l = 1/math.sqrt(2) * np.array([[1, -1], + [1, 1]], dtype=complex) K22r = np.array([[0, 1], [-1, 0]], dtype=complex) - K31l = 1/np.sqrt(2) * np.array([[np.exp(-1j*b), np.exp(-1j*b)], - [-np.exp(1j*b), np.exp(1j*b)]], dtype=complex) - K31r = 1j * np.array([[np.exp(1j*b), 0], - [0, -np.exp(-1j*b)]], dtype=complex) - K32r = 1/(1-1j) * np.array([[np.exp(1j*b), -np.exp(-1j*b)], - [-1j*np.exp(1j*b), -1j*np.exp(-1j*b)]], dtype=complex) + K31l = 1/math.sqrt(2) * np.array([[cmath.exp(-1j*b), cmath.exp(-1j*b)], + [-cmath.exp(1j*b), cmath.exp(1j*b)]], dtype=complex) + K31r = 1j * np.array([[cmath.exp(1j*b), 0], + [0, -cmath.exp(-1j*b)]], dtype=complex) + K32r = 1/(1-1j) * np.array([[cmath.exp(1j*b), -cmath.exp(-1j*b)], + [-1j*cmath.exp(1j*b), -1j*cmath.exp(-1j*b)]], dtype=complex) k1ld = basis.K1l.T.conj() k1rd = basis.K1r.T.conj() k2ld = basis.K2l.T.conj() @@ -667,12 +672,13 @@ def traces(self, target): # Future gotcha: extending this to non-supercontrolled basis. # Careful: closest distance between a1,b1,c1 and a2,b2,c2 may be between reflections. # This doesn't come up if either c1==0 or c2==0 but otherwise be careful. - - return [4*(np.cos(target.a)*np.cos(target.b)*np.cos(target.c) + - 1j*np.sin(target.a)*np.sin(target.b)*np.sin(target.c)), - 4*(np.cos(np.pi/4-target.a)*np.cos(self.basis.b-target.b)*np.cos(target.c) + - 1j*np.sin(np.pi/4-target.a)*np.sin(self.basis.b-target.b)*np.sin(target.c)), - 4*np.cos(target.c), + ta, tb, tc = target.a, target.b, target.c + bb = self.basis.b + return [4*complex(math.cos(ta)*math.cos(tb)*math.cos(tc), + math.sin(ta)*math.sin(tb)*math.sin(tc)), + 4*complex(math.cos(math.pi/4-ta)*math.cos(bb-tb)*math.cos(tc), + math.sin(math.pi/4-ta)*math.sin(bb-tb)*math.sin(tc)), + 4*math.cos(tc), 4] @staticmethod @@ -743,11 +749,11 @@ def decomp3_supercontrolled(self, target): return U3r, U3l, U2r, U2l, U1r, U1l, U0r, U0l - def __call__(self, target, basis_fidelity=None, num_basis_uses=None): + def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None): """Decompose a two-qubit unitary over fixed basis + SU(2) using the best approximation given that each basis application has a finite fidelity. - You can force a particular approximation by passing num_basis_uses. + You can force a particular approximation by passing _num_basis_uses. """ basis_fidelity = basis_fidelity or self.basis_fidelity if hasattr(target, 'to_operator'): @@ -772,8 +778,8 @@ def __call__(self, target, basis_fidelity=None, num_basis_uses=None): expected_fidelities = [trace_to_fid(traces[i]) * basis_fidelity**i for i in range(4)] best_nbasis = np.argmax(expected_fidelities) - if num_basis_uses is not None: - best_nbasis = num_basis_uses + if _num_basis_uses is not None: + best_nbasis = _num_basis_uses decomposition = self.decomposition_fns[best_nbasis](target_decomposed) decomposition_euler = [self._decomposer1q(x) for x in decomposition] @@ -802,10 +808,10 @@ def num_basis_gates(self, unitary): unitary = unitary.to_matrix() unitary = np.asarray(unitary, dtype=complex) a, b, c = weyl_coordinates(unitary)[:] - traces = [4*(np.cos(a)*np.cos(b)*np.cos(c)+1j*np.sin(a)*np.sin(b)*np.sin(c)), - 4*(np.cos(np.pi/4-a)*np.cos(self.basis.b-b)*np.cos(c) + - 1j*np.sin(np.pi/4-a)*np.sin(self.basis.b-b)*np.sin(c)), - 4*np.cos(c), + traces = [4*(math.cos(a)*math.cos(b)*math.cos(c)+1j*math.sin(a)*math.sin(b)*math.sin(c)), + 4*(math.cos(np.pi/4-a)*math.cos(self.basis.b-b)*math.cos(c) + + 1j*math.sin(np.pi/4-a)*math.sin(self.basis.b-b)*math.sin(c)), + 4*math.cos(c), 4] return np.argmax([trace_to_fid(traces[i]) * self.basis_fidelity**i for i in range(4)]) diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 3e9fd5224d87..2fa99ce00e7c 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -261,7 +261,10 @@ def test_qasm_2q_unitary(self): qr = QuantumRegister(2, 'q0') cr = ClassicalRegister(1, 'c0') qc = QuantumCircuit(qr, cr) - matrix = numpy.eye(4) + matrix = numpy.asarray([[0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0], + [1, 0, 0, 0]]) unitary_gate = UnitaryGate(matrix, label="custom_gate") qc.x(qr[0]) @@ -274,8 +277,8 @@ def test_qasm_2q_unitary(self): "creg c0[1];\n" \ "x q0[0];\n" \ "gate custom_gate p0,p1 {\n" \ - "\tu3(0,0,0) p0;\n" \ - "\tu3(0,0,0) p1;\n" \ + "\tu3(pi,-pi/2,pi/2) p0;\n" \ + "\tu3(pi,pi/2,-pi/2) p1;\n" \ "}\n" \ "custom_gate q0[0],q0[1];\n" \ "custom_gate q0[1],q0[0];\n" diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index e8068421df4e..faebb3366def 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -539,7 +539,8 @@ def test_optimize_to_nothing(self): basis_gates=['u3', 'u2', 'u1', 'cx']) expected = QuantumCircuit(QuantumRegister(2, 'q'), global_phase=-np.pi/2) - self.assertEqual(after, expected) + msg = f"after:\n{after}\nexpected:\n{expected}" + self.assertEqual(after, expected, msg=msg) def test_pass_manager_empty(self): """Test passing an empty PassManager() to the transpiler. @@ -993,12 +994,12 @@ def test_no_infinite_loop(self, optimization_level): # for the second and third RZ gates in the U3 decomposition. expected = QuantumCircuit(1, global_phase=-np.pi/2 - 0.5 * (0.2 + np.pi) - 0.5 * 3 * np.pi) expected.sx(0) - expected.p(np.pi + 0.2, 0) + expected.p(-np.pi + 0.2, 0) expected.sx(0) - expected.p(np.pi, 0) + expected.p(-np.pi, 0) - error_message = "\nOutput circuit:\n%s\nExpected circuit:\n%s" % ( - str(out), str(expected)) + error_message = (f"\nOutput circuit:\n{out!s}\n{Operator(out).data}\n" + f"Expected circuit:\n{expected!s}\n{Operator(expected).data}") self.assertEqual(out, expected, error_message) @data(0, 1, 2, 3) diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index d6e021396c4c..9a3e353d2ee3 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -908,7 +908,7 @@ def test_global_phase(self): qc2 = transpile(qc, basis_gates=['p']) sv = Statevector.from_instruction(qc2) expected = np.array([0.96891242-0.24740396j, 0]) - self.assertEqual(float(qc2.global_phase), -1/4) + self.assertEqual(float(qc2.global_phase), 2*np.pi - 0.25) self.assertEqual(sv, Statevector(expected)) def test_reverse_qargs(self): diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 8ce706015fc3..a057611bda64 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -163,7 +163,7 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-13, num_basis_uses=None): """Check exact decomposition for a particular target""" - decomp_circuit = decomposer(target_unitary, num_basis_uses=num_basis_uses) + decomp_circuit = decomposer(target_unitary, _num_basis_uses=num_basis_uses) if num_basis_uses is not None: self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get('unitary', 0)) result = execute(decomp_circuit, UnitarySimulatorPy(), optimization_level=0).result() @@ -205,6 +205,14 @@ class TestOneQubitEulerSpecial(CheckDecompositions): def test_special_ZYZ(self): """Special cases of ZYZ""" self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, -0.1).to_matrix(), 'ZYZ', {}) + self.check_oneq_special_cases(U3Gate(1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', + {}) + self.check_oneq_special_cases(-U3Gate(1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', + {'rz': 1}) # XXX + self.check_oneq_special_cases(U3Gate(np.pi-1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', + {'rz': 2, 'ry': 1}) # XXX + self.check_oneq_special_cases(-U3Gate(np.pi-1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', + {'ry': 1}) self.check_oneq_special_cases(U3Gate(np.pi, 0.1, 0.2).to_matrix(), 'ZYZ', {'rz': 2, 'ry': 1}) self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, 0.2).to_matrix(), 'ZYZ', {'rz': 1}) @@ -269,7 +277,7 @@ def test_special_RR(self): def test_special_U1X(self): """Special cases of U1X""" - self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U1X', {'u1': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'U1X', {}) self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'U1X', {'u1': 1}) self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'U1X', {'u1': 2, 'rx': 1}) @@ -281,7 +289,7 @@ def test_special_U1X(self): def test_special_PSX(self): """Special cases of PSX""" - self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'PSX', {'p': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'PSX', {}) self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'PSX', {'p': 1}) self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'PSX', {'p': 2, 'sx': 1}) @@ -293,7 +301,7 @@ def test_special_PSX(self): def test_special_ZSX(self): """Special cases of ZSX""" - self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'ZSX', {'rz': 1}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'ZSX', {}) self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'ZSX', {'rz': 1}) self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'ZSX', {'rz': 2, 'sx': 1}) @@ -690,7 +698,7 @@ def make_random_supercontrolled_decomposer(self, seed): basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) return decomposer - + @combine(seed=range(10), name='test_exact_supercontrolled_decompose_random_{seed}') def test_exact_supercontrolled_decompose_random(self, seed): """Exact decomposition for random supercontrolled basis and random target (seed={seed})""" @@ -773,14 +781,14 @@ def test_approx_supercontrolled_decompose_random(self, seed): tgt *= np.exp(1j * tgt_phase) traces_pred = decomposer.traces(TwoQubitWeylDecomposition(tgt)) - + for i in range(4): - decomp_circuit = decomposer(tgt, num_basis_uses=i) + decomp_circuit = decomposer(tgt, _num_basis_uses=i) result = execute(decomp_circuit, UnitarySimulatorPy(), optimization_level=0).result() decomp_unitary = result.get_unitary() tr_actual = np.trace(decomp_unitary.conj().T @ tgt) self.assertAlmostEqual(traces_pred[i], tr_actual, places=13, - msg=f"Trace doesn't match for {i}-basis decomposition") + msg=f"Trace doesn't match for {i}-basis decomposition") def test_cx_equivalence_0cx(self, seed=0): """Check circuits with 0 cx gates locally equivalent to identity @@ -912,11 +920,12 @@ def test_euler_basis_selection(self, euler_bases, kak_gates, seed): @ddt class TestTwoQubitDecomposeApprox(CheckDecompositions): - """Smoke tests for approximate decompositions""" + """Smoke tests for automatically-chosen approximate decompositions""" def check_approx_decomposition(self, target_unitary, decomposer, num_basis_uses): """Check approx decomposition for a particular target""" - decomp_circuit = decomposer(target_unitary, num_basis_uses=num_basis_uses) + self.assertEqual(decomposer.num_basis_gates(target_unitary), num_basis_uses) + decomp_circuit = decomposer(target_unitary) self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get('unitary', 0)) # Now check the fidelity? @@ -930,7 +939,7 @@ def test_approx_supercontrolled_decompose_phase_0_use_random(self, seed, delta=0 basis_phase = state.random() * 2 * np.pi basis_b = 0.4 # how to safely randomize? basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary), basis_fidelity=0.99) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -949,7 +958,7 @@ def test_approx_supercontrolled_decompose_phase_1_use_random(self, seed, delta=0 basis_phase = state.random() * 2 * np.pi basis_b = 0.4 # how to safely randomize? basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary), basis_fidelity=0.99) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -968,7 +977,7 @@ def test_approx_supercontrolled_decompose_phase_2_use_random(self, seed, delta=0 basis_phase = state.random() * 2 * np.pi basis_b = 0.4 # how to safely randomize? basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary), basis_fidelity=0.99) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) @@ -988,7 +997,7 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 basis_phase = state.random() * 2 * np.pi basis_b = state.random() * np.pi / 4 basis_unitary = np.exp(1j * basis_phase) * basis_k1 @ Ud(np.pi / 4, basis_b, 0) @ basis_k2 - decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary)) + decomposer = TwoQubitBasisDecomposer(UnitaryGate(basis_unitary), basis_fidelity=0.99) tgt_k1 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) tgt_k2 = np.kron(random_unitary(2, seed=state).data, random_unitary(2, seed=state).data) diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index fc37b1dca465..6b5e41f967a1 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -775,5 +775,6 @@ def test_global_phase(self): expected.global_phase = circ_angle - gate_angle / 2 expected_dag = circuit_to_dag(expected) self.assertEqual(out_dag, expected_dag) - self.assertEqual(float(out_dag.global_phase), float(expected_dag.global_phase)) + self.assertAlmostEqual(float(out_dag.global_phase), float(expected_dag.global_phase), + places=14) self.assertEqual(Operator(dag_to_circuit(out_dag)), Operator(expected)) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 3e72dccfe69c..1042c75b361f 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -344,7 +344,7 @@ def test_euler_decomposition_worse(self): result = passmanager.run(circuit) # decomposition of circuit will result in 3 gates instead of 2 # assert optimization pass doesn't use it. - self.assertEqual(result, circuit) + self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") def test_optimize_u_to_phase_gate(self): """U(0, 0, pi/4) -> p(pi/4). Basis [p, sx].""" @@ -361,7 +361,7 @@ def test_optimize_u_to_phase_gate(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result) + self.assertEqual(expected, result, f"Expected:\n{expected}\nResult:\n{result}") def test_optimize_u_to_p_sx_p(self): """U(pi/2, 0, pi/4) -> p(-pi/4)-sx-p(p/2). Basis [p, sx].""" @@ -380,7 +380,7 @@ def test_optimize_u_to_p_sx_p(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result) + self.assertEqual(expected, result, f"Expected:\n{expected}\nResult:\n{result}") def test_optimize_u3_to_u1(self): """U3(0, 0, pi/4) -> U1(pi/4). Basis [u1, u2, u3].""" From 63837c694fedd635a26eddc0bb5db09edd1d0567 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Thu, 4 Mar 2021 04:57:55 -0500 Subject: [PATCH 07/28] Specialize the circuit representation --- .../synthesis/two_qubit_decompose.py | 51 ++++++++++++------- test/python/quantum_info/test_synthesis.py | 6 +-- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 099931f3f802..20ce5db189b7 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -26,7 +26,7 @@ import cmath import math import warnings -from typing import Optional +from typing import ClassVar, Optional import logging import numpy as np @@ -98,8 +98,8 @@ class TwoQubitWeylDecomposition(): This is an abstract factory class that instantiates itself as specialized subclasses based on the fidelity, such that the approximation error from specialization has an average gate fidelity - at least as high as requested. The specialized subclasses have canonical representations thus - avoiding problems of numerical stability. + at least as high as requested. The specialized subclasses have unique canonical representations + thus avoiding problems of numerical stability. Passing non-None fidelity to specializations is treated as an assertion, raising QiskitError if the specialization is more approximate. @@ -119,6 +119,7 @@ class TwoQubitWeylDecomposition(): _original_decomposition: Optional["TwoQubitWeylDecomposition"] _is_flipped_from_original: bool + _default_1q_basis: ClassVar[str] = 'ZYZ' def __init_subclass__(cls, **kwargs): """Subclasses should be concrete, not factories. @@ -326,25 +327,34 @@ def specialize(self): Do not update the global phase, since gets done in generic __init__()""" raise NotImplementedError - def circuit(self, *, euler_basis='ZYZ', simplify=False, atol=DEFAULT_ATOL, **kwargs): - """Weyl decomposition in circuit form. + def circuit(self, *, euler_basis: Optional[str] = None, + simplify=False, atol=DEFAULT_ATOL, **kwargs) -> QuantumCircuit: + """Returns Weyl decomposition in circuit form. - Arguments are passed to OneQubitEulerDecomposer""" + Extra arguments are passed to OneQubitEulerDecomposer""" + if euler_basis is None: + euler_basis = self._default_1q_basis oneq_decompose = OneQubitEulerDecomposer(euler_basis) - circ = QuantumCircuit(2, global_phase=self.global_phase) c1l, c1r, c2l, c2r = (oneq_decompose(k, simplify=simplify, atol=atol, **kwargs) for k in (self.K1l, self.K1r, self.K2l, self.K2r)) + circ = QuantumCircuit(2, global_phase=self.global_phase) circ.compose(c2r, [0], inplace=True) circ.compose(c2l, [1], inplace=True) + self._weyl_gate(simplify, circ, atol) + circ.compose(c1r, [0], inplace=True) + circ.compose(c1l, [1], inplace=True) + return circ + + def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): + """Appends Ud(a, b, c) to the circuit. + + Override in subclasses for special cases""" if not simplify or abs(self.a) > atol: circ.rxx(-self.a*2, 0, 1) if not simplify or abs(self.b) > atol: circ.ryy(-self.b*2, 0, 1) if not simplify or abs(self.c) > atol: circ.rzz(-self.c*2, 0, 1) - circ.compose(c1r, [0], inplace=True) - circ.compose(c1l, [1], inplace=True) - return circ def actual_fidelity(self, **kwargs): """Calculates the actual fidelity of the decomposed circuit to the input unitary""" @@ -404,6 +414,11 @@ def specialize(self): self.K2l = _id.copy() self.K2r = _id.copy() + def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): + del self, simplify, atol # unused + circ.swap(0, 1) + circ.global_phase -= 3*np.pi/4 + def _closest_partial_swap(a, b, c): """A good approximation to the best value x to get the minimum @@ -460,6 +475,8 @@ class TwoQubitWeylControlledEquiv(TwoQubitWeylDecomposition): K2l = Ry(θl).Rx(λl) , K2r = Ry(θr).Rx(λr) . """ + _default_1q_basis = 'XYX' + def specialize(self): self.b = self.c = 0 k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) @@ -469,9 +486,6 @@ def specialize(self): self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) - def circuit(self, *, euler_basis='XYX', **kwargs): - return super().circuit(euler_basis=euler_basis, **kwargs) - class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U @@ -512,6 +526,8 @@ class TwoQubitWeylfSimabbEquiv(TwoQubitWeylDecomposition): This gate binds 5 parameters, we make it canonical by setting: K2l = Ry(θl).Rx(λl) . """ + _default_1q_basis = 'XYX' + def specialize(self): self.b = self.c = (self.b + self.c)/2 k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) @@ -520,9 +536,6 @@ def specialize(self): self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) self.K2r = np.asarray(RXGate(-k2lphi)) @ self.K2r - def circuit(self, *, euler_basis='XYX', **kwargs): - return super().circuit(euler_basis=euler_basis, **kwargs) - class TwoQubitWeylfSimabmbEquiv(TwoQubitWeylDecomposition): """U ~ Ud(α, β, -β), α ≥ β ≥ 0 @@ -530,6 +543,9 @@ class TwoQubitWeylfSimabmbEquiv(TwoQubitWeylDecomposition): This gate binds 5 parameters, we make it canonical by setting: K2l = Ry(θl).Rx(λl) . """ + + _default_1q_basis = 'XYX' + def specialize(self): self.b = (self.b - self.c)/2 self.c = -self.b @@ -539,9 +555,6 @@ def specialize(self): self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) self.K2r = _ipz @ np.asarray(RXGate(-k2lphi)) @ _ipz @ self.K2r - def circuit(self, *, euler_basis='XYX', **kwargs): - return super().circuit(euler_basis=euler_basis, **kwargs) - class TwoQubitWeylGeneral(TwoQubitWeylDecomposition): """U has no special symmetry. diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index a057611bda64..8a7f66ef8eb1 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -548,8 +548,7 @@ def test_weyl_specialize_swap(self): k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, 0.999, TwoQubitWeylSWAPEquiv, - {'rz': 4, 'ry': 2, - 'rxx': 1, 'ryy': 1, 'rzz': 1}) + {'rz': 4, 'ry': 2, 'swap': 1}) def test_weyl_specialize_flip_swap(self): """Weyl specialization for flip swap gate""" @@ -560,8 +559,7 @@ def test_weyl_specialize_flip_swap(self): k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, 0.999, TwoQubitWeylSWAPEquiv, - {'rz': 4, 'ry': 2, - 'rxx': 1, 'ryy': 1, 'rzz': 1}) + {'rz': 4, 'ry': 2, 'swap': 1}) def test_weyl_specialize_pswap(self, theta=0.123): """Weyl specialization for partial swap gate""" From 3a7e7da81ca597233ecb14105e96af5e018a4cef Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Fri, 5 Mar 2021 12:50:33 -0500 Subject: [PATCH 08/28] testing, special case fixes --- .../synthesis/one_qubit_decompose.py | 22 ++- .../synthesis/two_qubit_decompose.py | 6 +- test/python/quantum_info/test_synthesis.py | 164 ++++++++++-------- 3 files changed, 108 insertions(+), 84 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index d9287d29f104..845c08f69a3b 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -310,18 +310,29 @@ def _circuit_xyx(theta, phi, lam, phase, - simplify=True, + simplify=False, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + gphase = phase - (phi+lam)/2 + circuit = QuantumCircuit(1) + if simplify and abs(theta) < atol: - if abs(phi + lam) > atol: - circuit.append(RXGate(phi + lam), [0]) + tot = _mod_2pi(phi + lam) + if abs(tot) > atol: + circuit.append(RXGate(tot), [0]) + gphase += tot/2 + circuit.global_phase = gphase return circuit + if simplify and abs(theta - np.pi) < atol: + gphase += phi + lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: + gphase += lam/2 circuit.append(RXGate(lam), [0]) circuit.append(RYGate(theta), [0]) if not simplify or abs(phi) > atol: + gphase += phi/2 circuit.append(RXGate(phi), [0]) + circuit.global_phase = gphase return circuit @staticmethod @@ -384,6 +395,9 @@ def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun): # General two-SX gate decomposition # Shift theta and phi so decomposition is # P(phi).SX.P(theta).SX.P(lam) + if simplify and abs(theta-np.pi) < atol: + circuit.global_phase += lam + phi, lam = phi-lam, 0 circuit.global_phase -= np.pi/2 pfun(circuit, lam) xfun(circuit) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 20ce5db189b7..77a693f9937f 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -114,7 +114,7 @@ class TwoQubitWeylDecomposition(): K1r: np.ndarray K2r: np.ndarray unitary_matrix: np.ndarray # The unitary that was input - requested_fidelity: Optional[float] # None for no automatic specialization + requested_fidelity: Optional[float] # None means no automatic specialization calculated_fidelity: float # Fidelity after specialization _original_decomposition: Optional["TwoQubitWeylDecomposition"] @@ -348,7 +348,7 @@ def circuit(self, *, euler_basis: Optional[str] = None, def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): """Appends Ud(a, b, c) to the circuit. - Override in subclasses for special cases""" + Can be overriden in subclasses for special cases""" if not simplify or abs(self.a) > atol: circ.rxx(-self.a*2, 0, 1) if not simplify or abs(self.b) > atol: @@ -614,7 +614,7 @@ def __init__(self, gate, basis_fidelity=1.0, euler_basis=None): self._decomposer1q = OneQubitEulerDecomposer('U3') # FIXME: find good tolerances - self.is_supercontrolled = np.isclose(basis.a, np.pi/4) and np.isclose(basis.c, 0.) + self.is_supercontrolled = math.isclose(basis.a, np.pi/4) and math.isclose(basis.c, 0.) # Create some useful matrices U1, U2, U3 are equivalent to the basis, # expand as Ui = Ki1.Ubasis.Ki2 diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 8a7f66ef8eb1..bdcd3e584c1b 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -100,20 +100,6 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-14, si self.assertTrue(np.abs(maxdist) < tolerance, "Operator {}: Worst distance {}".format(operator, maxdist)) - def check_oneq_special_cases(self, target, basis, expected_gates=None, tolerance=1.E-12,): - """Check OneQubitEulerDecomposer produces the expected gates""" - decomposer = OneQubitEulerDecomposer(basis) - circ = decomposer(target, simplify=True) - data = Operator(circ).data - maxdist = np.max(np.abs(target.data - data)) - trace = np.trace(data.T.conj() @ target) - self.assertLess(np.abs(maxdist), tolerance, - f"Worst case distance: {maxdist}, trace: {trace}\n" - f"Target:\n{target}\nActual:\n{data}\n{circ}") - if expected_gates is not None: - self.assertDictEqual(dict(circ.count_ops()), expected_gates, - f"Circuit:\n{circ}") - def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name @@ -195,6 +181,36 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) +ANGEXP_ZYZ = [ # Special cases for ZYZ type expansions + [(1.E-13, 0.1, -0.1, 0), (0, 0)], + [(1.E-13, 0.2, -0.1, 0), (1, 0)], + [(1.E-13, np.pi, np.pi, 0), (0, 0)], + [(1.E-13, np.pi, np.pi, np.pi), (0, 0)], + [(np.pi, np.pi, np.pi, 0), (0, 1)], + [(np.pi-1.E-13, np.pi, np.pi, np.pi), (0, 1)], + [(np.pi, 0.1, 0.2, 0), (1, 1)], + [(np.pi, 0.2, 0.2, 0), (0, 1)], + [(1.E-13, 0.1, 0.2, 0), (1, 0)], + [(0.1, 0.2, 1.E-13, 0), (1, 1)], + [(0.1, 0., 0., 0), (0, 1)], + [(0.1, 1.E-13, 0.2, 0), (1, 1)], + [(0.1, 0.2, 0.3, 0), (2, 1)] +] +ANGEXP_PSX = [ # Special cases for Z.X90.Z.X90.Z type expansions + [(0.0, 0.1, -0.1), (0, 0)], + [(0.0, 0.1, 0.2), (1, 0)], + [(-np.pi/2, 0.2, 0.0), (2, 1)], + [(np.pi/2, 0.0, 0.21), (2, 1)], + [(np.pi/2, 0.12, 0.2), (2, 1)], + [(np.pi/2, -np.pi/2, 0.21), (1, 1)], + [(np.pi, np.pi, 0), (0, 2)], + [(np.pi, np.pi+0.1, 0.1), (0, 2)], + [(np.pi, np.pi+0.2, -0.1), (1, 2)], + [(0.1, 0.2, 0.3), (3, 2)], +] + + +@ddt class TestOneQubitEulerSpecial(CheckDecompositions): """Test special cases for OneQubitEulerDecomposer. @@ -202,46 +218,44 @@ class TestOneQubitEulerSpecial(CheckDecompositions): and shapes of decompositions that can be made, but they don't check all the corner cases where a wrap by 2*pi might happen, etc """ - def test_special_ZYZ(self): - """Special cases of ZYZ""" - self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, -0.1).to_matrix(), 'ZYZ', {}) - self.check_oneq_special_cases(U3Gate(1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', - {}) - self.check_oneq_special_cases(-U3Gate(1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', - {'rz': 1}) # XXX - self.check_oneq_special_cases(U3Gate(np.pi-1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', - {'rz': 2, 'ry': 1}) # XXX - self.check_oneq_special_cases(-U3Gate(np.pi-1.E-13, np.pi, np.pi).to_matrix(), 'ZYZ', - {'ry': 1}) - self.check_oneq_special_cases(U3Gate(np.pi, 0.1, 0.2).to_matrix(), 'ZYZ', - {'rz': 2, 'ry': 1}) - self.check_oneq_special_cases(U3Gate(1.E-13, 0.1, 0.2).to_matrix(), 'ZYZ', {'rz': 1}) - self.check_oneq_special_cases(U3Gate(0.1, 0.2, 1.E-13).to_matrix(), 'ZYZ', - {'rz': 1, 'ry': 1}) - self.check_oneq_special_cases(U3Gate(0.1, 1.E-13, 0.2).to_matrix(), 'ZYZ', - {'rz': 1, 'ry': 1}) - self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'ZYZ', {'rz': 2, 'ry': 1}) - - def test_special_ZXZ(self): - """Special cases of ZXZ""" - def myr(a, b, c): - return RZGate(b).to_matrix() @ RXGate(a).to_matrix() @ RZGate(c).to_matrix() - self.check_oneq_special_cases(myr(0.0, 0.1, -0.1), 'ZXZ', {}) - self.check_oneq_special_cases(myr(np.pi, 0.1, 0.2), 'ZXZ', {'rz': 2, 'rx': 1}) - self.check_oneq_special_cases(myr(0.0, 0.1, 0.2), 'ZXZ', {'rz': 1}) - self.check_oneq_special_cases(myr(0.1, 0.2, 0.0), 'ZXZ', {'rz': 1, 'rx': 1}) - self.check_oneq_special_cases(myr(0.1, 0.0, 0.2), 'ZXZ', {'rz': 1, 'rx': 1}) - self.check_oneq_special_cases(myr(0.1, 0.2, 0.3), 'ZXZ', {'rz': 2, 'rx': 1}) - - def test_special_XYX(self): - """Special cases of XYX""" - def myr(a, b, c): - return RXGate(b).to_matrix() @ RYGate(a).to_matrix() @ RXGate(c).to_matrix() - self.check_oneq_special_cases(myr(0.0, 0.1, -0.1), 'XYX', {}) - self.check_oneq_special_cases(myr(0.0, 0.1, 0.2), 'XYX', {'rx': 1}) - self.check_oneq_special_cases(myr(-0.1, 0.2, 0.0), 'XYX', {'rx': 1, 'ry': 1}) - self.check_oneq_special_cases(myr(-0.1, 0.0, 0.2), 'XYX', {'rx': 1, 'ry': 1}) - self.check_oneq_special_cases(myr(0.1, 0.2, 0.3), 'XYX', {'rx': 2, 'ry': 1}) + + def check_oneq_special_cases(self, target, basis, expected_gates=None, tolerance=1.E-12,): + """Check OneQubitEulerDecomposer produces the expected gates""" + decomposer = OneQubitEulerDecomposer(basis) + circ = decomposer(target, simplify=True) + data = Operator(circ).data + maxdist = np.max(np.abs(target.data - data)) + trace = np.trace(data.T.conj() @ target) + self.assertLess(np.abs(maxdist), tolerance, + f"Worst case distance: {maxdist}, trace: {trace}\n" + f"Target:\n{target}\nActual:\n{data}\n{circ}") + # if expected_gates is not None: + # self.assertDictEqual(dict(circ.count_ops()), expected_gates, + # f"Circuit:\n{circ}") + + @combine(angexp=ANGEXP_ZYZ) + def test_special_ZYZ(self, angexp): + """Special cases of ZYZ. {angexp[0]}""" + a, b, c, d = angexp[0] + exp = {('rz', 'ry')[g]: angexp[1][g] for g in (0, 1) if angexp[1][g]} + tgt = np.exp(1j*d)*RZGate(b).to_matrix() @ RYGate(a).to_matrix() @ RZGate(c).to_matrix() + self.check_oneq_special_cases(tgt, 'ZYZ', exp) + + @combine(angexp=ANGEXP_ZYZ) + def test_special_ZXZ(self, angexp): + """Special cases of ZXZ. {angexp[0]}""" + a, b, c, d = angexp[0] + exp = {('rz', 'rx')[g]: angexp[1][g] for g in (0, 1) if angexp[1][g]} + tgt = np.exp(1j*d)*RZGate(b).to_matrix() @ RXGate(a).to_matrix() @ RZGate(c).to_matrix() + self.check_oneq_special_cases(tgt, 'ZXZ', exp) + + @combine(angexp=ANGEXP_ZYZ) + def test_special_XYX(self, angexp): + """Special cases of XYX. {angexp[0]}""" + a, b, c, d = angexp[0] + exp = {('rx', 'ry')[g]: angexp[1][g] for g in (0, 1) if angexp[1][g]} + tgt = np.exp(1j*d)*RXGate(b).to_matrix() @ RYGate(a).to_matrix() @ RXGate(c).to_matrix() + self.check_oneq_special_cases(tgt, 'XYX', exp) def test_special_U321(self): """Special cases of U321""" @@ -287,17 +301,13 @@ def test_special_U1X(self): {'u1': 2, 'rx': 1}) self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'U1X', {'u1': 3, 'rx': 2}) - def test_special_PSX(self): - """Special cases of PSX""" - self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'PSX', {}) - self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'PSX', {'p': 1}) - self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'PSX', - {'p': 2, 'sx': 1}) - self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.21).to_matrix(), 'PSX', - {'p': 2, 'sx': 1}) - self.check_oneq_special_cases(U3Gate(np.pi/2, 0.12, 0.2).to_matrix(), 'PSX', - {'p': 2, 'sx': 1}) - self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'PSX', {'p': 3, 'sx': 2}) + @combine(angexp=ANGEXP_PSX) + def test_special_PSX(self, angexp): + """Special cases of PSX. {angexp[0]}""" + a, b, c = angexp[0] + tgt = U3Gate(a, b, c).to_matrix() + exp = {('p', 'sx')[g]: angexp[1][g] for g in (0, 1) if angexp[1][g]} + self.check_oneq_special_cases(tgt, 'PSX', exp) def test_special_ZSX(self): """Special cases of ZSX""" @@ -518,7 +528,7 @@ def test_two_qubit_weyl_decomposition_abc(self, smallest=1e-18, factor=9.8, step self.check_two_qubit_weyl_decomposition(k1 @ a @ k2) -K1K2Sb = [[Operator(U3Gate(*xyz)) for xyz in xyzs] for xyzs in +K1K2SB = [[Operator(U3Gate(*xyz)) for xyz in xyzs] for xyzs in [[(0.2, 0.3, 0.1), (0.7, 0.15, 0.22), (0.1, 0.97, 2.2), (3.14, 2.1, 0.9)], [(0.21, 0.13, 0.45), (2.1, 0.77, 0.88), (1.5, 2.3, 2.3), (2.1, 0.4, 1.7)]]] DELTAS = [(-0.019, 0.018, 0.021), (0.01, 0.015, 0.02), (-0.01, -0.009, 0.011), @@ -532,7 +542,7 @@ def test_weyl_specialize_id(self): """Weyl specialization for Id gate""" a, b, c = 0., 0., 0. for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -543,7 +553,7 @@ def test_weyl_specialize_swap(self): """Weyl specialization for swap gate""" a, b, c = np.pi/4, np.pi/4, np.pi/4 for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -554,7 +564,7 @@ def test_weyl_specialize_flip_swap(self): """Weyl specialization for flip swap gate""" a, b, c = np.pi/4, np.pi/4, -np.pi/4 for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -565,7 +575,7 @@ def test_weyl_specialize_pswap(self, theta=0.123): """Weyl specialization for partial swap gate""" a, b, c = theta, theta, theta for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -577,7 +587,7 @@ def test_weyl_specialize_flip_pswap(self, theta=0.123): """Weyl specialization for flipped partial swap gate""" a, b, c = theta, theta, -theta for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -589,7 +599,7 @@ def test_weyl_specialize_fsim_aab(self, aaa=0.456, bbb=0.132): """Weyl specialization for partial swap gate""" a, b, c = aaa, aaa, bbb for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -601,7 +611,7 @@ def test_weyl_specialize_fsim_abb(self, aaa=0.456, bbb=0.132): """Weyl specialization for partial swap gate""" a, b, c = aaa, bbb, bbb for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -613,7 +623,7 @@ def test_weyl_specialize_fsim_abmb(self, aaa=0.456, bbb=0.132): """Weyl specialization for partial swap gate""" a, b, c = aaa, bbb, -bbb for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -625,7 +635,7 @@ def test_weyl_specialize_ctrl(self, aaa=0.456): """Weyl specialization for partial swap gate""" a, b, c = aaa, 0., 0. for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -637,7 +647,7 @@ def test_weyl_specialize_mirror_ctrl(self, aaa=-0.456): """Weyl specialization for partial swap gate""" a, b, c = np.pi/4, np.pi/4, aaa for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, @@ -649,7 +659,7 @@ def test_weyl_specialize_general(self, aaa=0.456, bbb=0.345, ccc=0.123): """Weyl specialization for partial swap gate""" a, b, c = aaa, bbb, ccc for da, db, dc in DELTAS: - for k1l, k1r, k2l, k2r in K1K2Sb: + for k1l, k1r, k2l, k2r in K1K2SB: k1 = np.kron(k1l.data, k1r.data) k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, From 01e980622db3f10c4eb96ac0f641a30be1b6c31c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 29 Jan 2021 10:45:59 -0500 Subject: [PATCH 09/28] Add ZSXX basis to 1q euler decomposer This commit adds a new basis ZSXX to the 1q euler decomposer. This basis is identical to the ZSX basis except it does an additional simplification pass so that instead of having 2 sx gates in the synthesized output a single X gate is used instead. Fixes #5722 --- .../synthesis/one_qubit_decompose.py | 74 ++++++++++++++++++- test/python/quantum_info/test_synthesis.py | 11 ++- .../test_optimize_1q_decomposition.py | 14 ++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 49ae29cfab29..f76406284db4 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -22,7 +22,8 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.library.standard_gates import (UGate, PhaseGate, U3Gate, U2Gate, U1Gate, RXGate, RYGate, - RZGate, RGate, SXGate) + RZGate, RGate, SXGate, + XGate) from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_unitary_matrix @@ -38,6 +39,7 @@ 'ZYZ': ['rz', 'ry'], 'ZXZ': ['rz', 'rx'], 'XYX': ['rx', 'ry'], + 'ZSXX': ['rz', 'sx', 'x'], 'ZSX': ['rz', 'sx'], } @@ -85,6 +87,10 @@ class OneQubitEulerDecomposer: - :math:`Z(\phi) Y(\theta) Z(\lambda)` - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` :math:`R_Z(\theta+\pi).S_X\left(\frac{\pi}{2}\right).U_1(\lambda)` + * - 'ZSXX' + - :math:`Z(\phi) Y(\theta) Z(\lambda)` + - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` + :math:`R_Z(\theta+\pi).S_X\left(\frac{\pi}{2}\right).U_1(\lambda)` * - 'U1X' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` @@ -98,7 +104,8 @@ class OneQubitEulerDecomposer: def __init__(self, basis='U3'): """Initialize decomposer - Supported bases are: 'U', 'PSX', 'ZSX', 'U321', 'U3', 'U1X', 'RR', 'ZYZ', 'ZXZ', 'XYX'. + Supported bases are: 'U', 'PSX', 'ZSXX', 'ZSX', 'U321', 'U3', 'U1X', 'RR', 'ZYZ', 'ZXZ', + 'XYX'. Args: basis (str): the decomposition basis [Default: 'U3'] @@ -167,6 +174,7 @@ def basis(self, basis): 'U': (self._params_u3, self._circuit_u), 'PSX': (self._params_u1x, self._circuit_psx), 'ZSX': (self._params_u1x, self._circuit_zsx), + 'ZSXX': (self._params_u1x, self._circuit_zsxx), 'U1X': (self._params_u1x, self._circuit_u1x), 'RR': (self._params_zyz, self._circuit_rr), 'ZYZ': (self._params_zyz, self._circuit_zyz), @@ -521,6 +529,68 @@ def _circuit_u1x(theta, circuit._append(U1Gate(phi), [qr[0]], []) return circuit + @staticmethod + def _circuit_zsxx(theta, + phi, + lam, + phase, + simplify=True, + atol=DEFAULT_ATOL): + # Shift theta and phi so decomposition is + # RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) + theta = _mod2pi(theta + np.pi) + phi = _mod2pi(phi + np.pi) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase - np.pi / 2) + # Check for decomposition into minimimal number required SX gates + abs_theta = abs(theta) + if simplify and math.isclose(abs_theta, np.pi, abs_tol=atol): + lam_phi_theta = _mod2pi(lam + phi + theta) + abs_lam_phi_theta = _mod2pi(abs(lam + phi + theta)) + if not (math.isclose(abs_lam_phi_theta, 0., abs_tol=atol) or + math.isclose(abs_lam_phi_theta, 2*np.pi, abs_tol=atol)): + circuit._append(RZGate(lam_phi_theta), [qr[0]], []) + circuit.global_phase += 0.5 * lam_phi_theta + circuit.global_phase += np.pi / 2 + elif simplify and (math.isclose(abs_theta, np.pi/2, abs_tol=atol) or + math.isclose(abs_theta, 3*np.pi/2, abs_tol=atol)): + lam_theta = _mod2pi(lam + theta) + abs_lam_theta = _mod2pi(abs(lam + theta)) + if not (math.isclose(abs_lam_theta, 0, abs_tol=atol) or + math.isclose(abs_lam_theta, 2*np.pi, abs_tol=atol)): + circuit._append(RZGate(lam_theta), [qr[0]], []) + circuit.global_phase += 0.5 * lam_theta + circuit._append(SXGate(), [qr[0]], []) + phi_theta = _mod2pi(phi + theta) + abs_phi_theta = _mod2pi(abs(phi_theta)) + if not (math.isclose(abs_phi_theta, 0, abs_tol=atol) or + math.isclose(abs_phi_theta, 2*np.pi, abs_tol=atol)): + circuit._append(RZGate(phi_theta), [qr[0]], []) + circuit.global_phase += 0.5 * phi_theta + if (math.isclose(theta, -np.pi / 2, abs_tol=atol) or + math.isclose(theta, 3 * np.pi / 2, abs_tol=atol)): + circuit.global_phase += np.pi / 2 + else: + abs_lam = abs(lam) + if not (math.isclose(abs_lam, 0., abs_tol=atol) or + math.isclose(abs_lam, 2*np.pi, abs_tol=atol)): + circuit._append(RZGate(lam), [qr[0]], []) + circuit.global_phase += 0.5 * lam + if not (math.isclose(abs_theta, 0., abs_tol=atol) or + math.isclose(abs_theta, 2*np.pi, abs_tol=atol)): + circuit._append(SXGate(), [qr[0]], []) + circuit._append(RZGate(theta), [qr[0]], []) + circuit.global_phase += 0.5 * theta + circuit._append(SXGate(), [qr[0]], []) + else: + circuit._append(XGate(), [qr[0]], []) + abs_phi = abs(phi) + if not (math.isclose(abs_phi, 0., abs_tol=atol) or + math.isclose(abs_phi, 2*np.pi, abs_tol=atol)): + circuit._append(RZGate(phi), [qr[0]], []) + circuit.global_phase += 0.5 * phi + return circuit + @staticmethod def _circuit_rr(theta, phi, diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 3fd909845297..18258b1235ed 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -168,7 +168,7 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', maxdist = np.max(np.abs(target_unitary + decomp_unitary)) self.assertTrue(np.abs(maxdist) < tolerance, "Worst distance {}".format(maxdist)) - @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'], + @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZSXX', 'ZYZ', 'ZXZ', 'XYX', 'RR'], name='test_one_qubit_clifford_{basis}_basis') def test_one_qubit_clifford_all_basis(self, basis): """Verify for {basis} basis and all Cliffords.""" @@ -182,6 +182,7 @@ def test_one_qubit_clifford_all_basis(self, basis): ('U1X', 1e-7), ('PSX', 1e-7), ('ZSX', 1e-7), + ('ZSXX', 1e-7), ('RR', 1e-12)], name='test_one_qubit_hard_thetas_{basis_tolerance[0]}_basis') # Lower tolerance for U1X test since decomposition since it is @@ -193,7 +194,7 @@ def test_one_qubit_hard_thetas_all_basis(self, basis_tolerance): self.check_one_qubit_euler_angles(Operator(gate), basis_tolerance[0], basis_tolerance[1]) - @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR'], seed=range(50), + @combine(basis=['U3', 'U1X', 'PSX', 'ZSX', 'ZYZ', 'ZXZ', 'XYX', 'RR', 'ZSXX'], seed=range(50), name='test_one_qubit_random_{basis}_basis_{seed}') def test_one_qubit_random_all_basis(self, basis, seed): """Verify for {basis} basis and random_unitary (seed={seed}).""" @@ -204,6 +205,7 @@ def test_psx_zsx_special_cases(self): """Test decompositions of psx and zsx at special values of parameters""" oqed_psx = OneQubitEulerDecomposer(basis='PSX') oqed_zsx = OneQubitEulerDecomposer(basis='ZSX') + oqed_zsxx = OneQubitEulerDecomposer(basis='ZSXX') theta = np.pi / 3 phi = np.pi / 5 lam = np.pi / 7 @@ -227,8 +229,10 @@ def test_psx_zsx_special_cases(self): unitary = gate.to_matrix() qc_psx = oqed_psx(unitary) qc_zsx = oqed_zsx(unitary) + qc_zsxx = oqed_zsxx(unitary) self.assertTrue(np.allclose(unitary, Operator(qc_psx).data)) self.assertTrue(np.allclose(unitary, Operator(qc_zsx).data)) + self.assertTrue(np.allclose(unitary, Operator(qc_zsxx).data)) # FIXME: streamline the set of test cases @@ -525,7 +529,8 @@ def test_seed_289(self): euler_bases=[('U321', ['u3', 'u2', 'u1']), ('U3', ['u3']), ('U', ['u']), ('U1X', ['u1', 'rx']), ('RR', ['r']), ('PSX', ['p', 'sx']), ('ZYZ', ['rz', 'ry']), ('ZXZ', ['rz', 'rx']), - ('XYX', ['rx', 'ry']), ('ZSX', ['rz', 'sx'])], + ('XYX', ['rx', 'ry']), ('ZSX', ['rz', 'sx']), + ('ZSXX', ['rz', 'sx', 'x'])], kak_gates=[(CXGate(), 'cx'), (CZGate(), 'cz'), (iSwapGate(), 'iswap'), (RXXGate(np.pi / 2), 'rxx')], name='test_euler_basis_selection_{seed}_{euler_bases[0]}_{kak_gates[1]}') diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index b59c47bcb72b..3e63860f9a50 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -415,7 +415,21 @@ def test_optimize_u3_to_u2(self): passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) + self.assert_equal(expected, result) + def test_y_simplification_rz_sx_x(self): + """Test that a y gate gets decomposed to x-zx with ibmq basis.""" + qc = QuantumCircuit(1) + qc.y(0) + basis = ["id", "rz", "sx", "x", "cx"] + passmanager = PassManager() + passmanager.append(BasisTranslator(sel, basis)) + passmanager.append(Optimize1qGatesDecomposition(basis)) + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1) + expected.x(0) + expected.rz(np.pi, 0) self.assertEqual(expected, result) From a369f9ffb51b1a9078e1183438c048a375f7fbe1 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Mon, 8 Mar 2021 13:46:02 -0500 Subject: [PATCH 10/28] more tweaks --- .../synthesis/one_qubit_decompose.py | 36 +++++++--- .../synthesis/two_qubit_decompose.py | 65 ++++++++++--------- test/python/quantum_info/test_synthesis.py | 12 ++-- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 845c08f69a3b..cbe217d79798 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -241,7 +241,8 @@ def _params_xyx(mat): ]], dtype=complex) theta, phi, lam, phase = OneQubitEulerDecomposer._params_zyz(mat_zyz) - return -theta, phi, lam, phase + newphi, newlam = _mod_2pi(phi+np.pi), _mod_2pi(lam+np.pi) + return theta, newphi, newlam, phase + (newphi + newlam - phi - lam)/2 @staticmethod def _params_u3(mat): @@ -269,19 +270,27 @@ def _circuit_zyz(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + gphase = phase - (phi+lam)/2 + circuit = QuantumCircuit(1) if simplify and abs(theta) < atol: - if abs(phi + lam) > atol: - circuit.append(RZGate(phi + lam), [0]) + tot = _mod_2pi(phi + lam) + if abs(tot) > atol: + circuit.append(RZGate(tot), [0]) + gphase += tot/2 + circuit.global_phase = gphase return circuit if simplify and abs(theta - np.pi) < atol: - lam, phi = (lam-phi) / 2, -(lam-phi) / 2 + gphase += phi + lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: + gphase += lam/2 circuit.append(RZGate(lam), [0]) circuit.append(RYGate(theta), [0]) if not simplify or abs(phi) > atol: + gphase += phi/2 circuit.append(RZGate(phi), [0]) + circuit.global_phase = gphase return circuit @staticmethod @@ -291,18 +300,27 @@ def _circuit_zxz(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + gphase = phase - (phi+lam)/2 + circuit = QuantumCircuit(1) + if simplify and abs(theta) < atol: - if abs(phi + lam) > atol: - circuit.append(RZGate(phi + lam), [0]) + tot = _mod_2pi(phi + lam) + if abs(tot) > atol: + circuit.append(RZGate(tot), [0]) + gphase += tot/2 + circuit.global_phase = gphase return circuit if simplify and abs(theta - np.pi) < atol: - lam, phi = (lam-phi) / 2, -(lam-phi) / 2 + gphase += phi + lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: + gphase += lam/2 circuit.append(RZGate(lam), [0]) circuit.append(RXGate(theta), [0]) if not simplify or abs(phi) > atol: + gphase += phi/2 circuit.append(RZGate(phi), [0]) + circuit.global_phase = gphase return circuit @staticmethod diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 77a693f9937f..2238b4ee79dc 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -27,9 +27,11 @@ import math import warnings from typing import ClassVar, Optional + import logging import numpy as np +from numpy.typing import ArrayLike import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister @@ -37,17 +39,17 @@ from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator -from qiskit.quantum_info.operators.predicates import is_unitary_matrix from qiskit.quantum_info.synthesis.weyl import weyl_coordinates from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer, DEFAULT_ATOL logger = logging.getLogger(__name__) -def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): +def decompose_two_qubit_product_gate(special_unitary_matrix: ArrayLike): """Decompose U = Ul⊗Ur where U in SU(4), and Ul, Ur in SU(2). Throws QiskitError if this isn't possible. """ + special_unitary_matrix = np.asarray(special_unitary_matrix) # extract the right component R = special_unitary_matrix[:2, :2].copy() detR = R[0, 0]*R[1, 1] - R[0, 1]*R[1, 0] @@ -119,6 +121,7 @@ class TwoQubitWeylDecomposition(): _original_decomposition: Optional["TwoQubitWeylDecomposition"] _is_flipped_from_original: bool + _default_1q_basis: ClassVar[str] = 'ZYZ' def __init_subclass__(cls, **kwargs): @@ -129,7 +132,7 @@ def __init_subclass__(cls, **kwargs): cls.__new__ = (lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__(cls, *a, fidelity=None, **k)) - def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): + def __new__(cls, unitary_matrix: ArrayLike, *, fidelity=(1.-1.E-9)): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, @@ -146,7 +149,7 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): pi4 = np.pi/4 # Make U be in SU(4) - U = unitary_matrix.copy() + U = np.array(unitary_matrix, copy=True) detU = la.det(U) U *= detU**(-0.25) global_phase = cmath.phase(detU) / 4 @@ -251,6 +254,11 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): od.global_phase = global_phase od.unitary_matrix = unitary_matrix od.requested_fidelity = fidelity + od.calculated_fidelity = 1.0 + od.unitary_matrix = np.array(unitary_matrix, copy=True) + od.unitary_matrix.setflags(write=False) + od._original_decomposition = None + od._is_flipped_from_original = False def is_close(ap, bp, cp): da, db, dc = a-ap, b-bp, c-cp @@ -286,14 +294,14 @@ def is_close(ap, bp, cp): instance._original_decomposition = od return instance - def __init__(self, unitary_matrix: np.ndarray, *args, fidelity=None, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, unitary_matrix: ArrayLike, *args, fidelity=None): + super().__init__(*args) od = self._original_decomposition self.a, self.b, self.c = od.a, od.b, od.c self.K1l, self.K1r = od.K1l, od.K1r self.K2l, self.K2r = od.K2l, od.K2r self.global_phase = od.global_phase - self.unitary_matrix = unitary_matrix.copy() + self.unitary_matrix = od.unitary_matrix self.requested_fidelity = fidelity self._is_flipped_from_original = False self.specialize() @@ -363,6 +371,8 @@ def actual_fidelity(self, **kwargs): return trace_to_fid(trace) def __repr__(self): + """Represent with enough precision to allow copy-paste debugging of all corner cases + """ prefix = f"{self.__class__.__name__}(" prefix_nd = " np.array(" suffix = ")," @@ -479,8 +489,9 @@ class TwoQubitWeylControlledEquiv(TwoQubitWeylDecomposition): def specialize(self): self.b = self.c = 0 - k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) - k2rtheta, k2rphi, k2rlambda = _oneq_xyx.angles(self.K2r) + k2ltheta, k2lphi, k2llambda, k2lphase = _oneq_xyx.angles_and_phase(self.K2l) + k2rtheta, k2rphi, k2rlambda, k2rphase = _oneq_xyx.angles_and_phase(self.K2r) + self.global_phase += k2lphase + k2rphase self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) self.K1r = self.K1r @ np.asarray(RXGate(k2rphi)) self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) @@ -496,13 +507,19 @@ class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """ def specialize(self): self.a = self.b = np.pi/4 - k2ltheta, k2lphi, k2llambda = _oneq_zyz.angles(self.K2l) - k2rtheta, k2rphi, k2rlambda = _oneq_zyz.angles(self.K2r) + k2ltheta, k2lphi, k2llambda, k2lphase = _oneq_zyz.angles_and_phase(self.K2l) + k2rtheta, k2rphi, k2rlambda, k2rphase = _oneq_zyz.angles_and_phase(self.K2r) + self.global_phase += k2lphase + k2rphase self.K1r = self.K1r @ np.asarray(RZGate(k2lphi)) self.K1l = self.K1l @ np.asarray(RZGate(k2rphi)) self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RZGate(k2llambda)) self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RZGate(k2rlambda)) + def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): + circ.swap(0, 1) + circ.rzz((np.pi/4 - self.c) * 2, 0, 1) + circ.global_phase += np.pi/4 + # These next 3 gates use the definition of fSim from https://arxiv.org/pdf/2001.08343.pdf eq (1) class TwoQubitWeylfSimaabEquiv(TwoQubitWeylDecomposition): @@ -513,7 +530,8 @@ class TwoQubitWeylfSimaabEquiv(TwoQubitWeylDecomposition): """ def specialize(self): self.a = self.b = (self.a + self.b)/2 - k2ltheta, k2lphi, k2llambda = _oneq_zyz.angles(self.K2l) + k2ltheta, k2lphi, k2llambda, k2lphase = _oneq_zyz.angles_and_phase(self.K2l) + self.global_phase += k2lphase self.K1r = self.K1r @ np.asarray(RZGate(k2lphi)) self.K1l = self.K1l @ np.asarray(RZGate(k2lphi)) self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RZGate(k2llambda)) @@ -530,7 +548,8 @@ class TwoQubitWeylfSimabbEquiv(TwoQubitWeylDecomposition): def specialize(self): self.b = self.c = (self.b + self.c)/2 - k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) + k2ltheta, k2lphi, k2llambda, k2lphase = _oneq_xyx.angles_and_phase(self.K2l) + self.global_phase += k2lphase self.K1r = self.K1r @ np.asarray(RXGate(k2lphi)) self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) @@ -549,7 +568,8 @@ class TwoQubitWeylfSimabmbEquiv(TwoQubitWeylDecomposition): def specialize(self): self.b = (self.b - self.c)/2 self.c = -self.b - k2ltheta, k2lphi, k2llambda = _oneq_xyx.angles(self.K2l) + k2ltheta, k2lphi, k2llambda, k2lphase = _oneq_xyx.angles_and_phase(self.K2l) + self.global_phase += k2lphase self.K1r = self.K1r @ _ipz @ np.asarray(RXGate(k2lphi)) @ _ipz self.K1l = self.K1l @ np.asarray(RXGate(k2lphi)) self.K2l = np.asarray(RYGate(k2ltheta)) @ np.asarray(RXGate(k2llambda)) @@ -762,29 +782,14 @@ def decomp3_supercontrolled(self, target): return U3r, U3l, U2r, U2l, U1r, U1l, U0r, U0l - def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None): + def __call__(self, target: ArrayLike, basis_fidelity=None, *, _num_basis_uses=None): """Decompose a two-qubit unitary over fixed basis + SU(2) using the best approximation given that each basis application has a finite fidelity. You can force a particular approximation by passing _num_basis_uses. """ basis_fidelity = basis_fidelity or self.basis_fidelity - if hasattr(target, 'to_operator'): - # If input is a BaseOperator subclass this attempts to convert - # the object to an Operator so that we can extract the underlying - # numpy matrix from `Operator.data`. - target = target.to_operator().data - if hasattr(target, 'to_matrix'): - # If input is Gate subclass or some other class object that has - # a to_matrix method this will call that method. - target = target.to_matrix() - # Convert to numpy array incase not already an array target = np.asarray(target, dtype=complex) - # Check input is a 2-qubit unitary - if target.shape != (4, 4): - raise QiskitError("TwoQubitBasisDecomposer: expected 4x4 matrix for target") - if not is_unitary_matrix(target): - raise QiskitError("TwoQubitBasisDecomposer: target matrix is not unitary.") target_decomposed = TwoQubitWeylDecomposition(target) traces = self.traces(target_decomposed) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index bdcd3e584c1b..3039b600f080 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -229,9 +229,9 @@ def check_oneq_special_cases(self, target, basis, expected_gates=None, tolerance self.assertLess(np.abs(maxdist), tolerance, f"Worst case distance: {maxdist}, trace: {trace}\n" f"Target:\n{target}\nActual:\n{data}\n{circ}") - # if expected_gates is not None: - # self.assertDictEqual(dict(circ.count_ops()), expected_gates, - # f"Circuit:\n{circ}") + if expected_gates is not None: + self.assertDictEqual(dict(circ.count_ops()), expected_gates, + f"Circuit:\n{circ}") @combine(angexp=ANGEXP_ZYZ) def test_special_ZYZ(self, angexp): @@ -640,8 +640,7 @@ def test_weyl_specialize_ctrl(self, aaa=0.456): k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, 0.999, TwoQubitWeylControlledEquiv, - {'rx': 6, 'ry': 4, - 'rxx': 1}) + {'rx': 6, 'ry': 4, 'rxx': 1}) def test_weyl_specialize_mirror_ctrl(self, aaa=-0.456): """Weyl specialization for partial swap gate""" @@ -652,8 +651,7 @@ def test_weyl_specialize_mirror_ctrl(self, aaa=-0.456): k2 = np.kron(k2l.data, k2r.data) self.check_two_qubit_weyl_specialization(k1 @ Ud(a+da, b+db, c+dc) @ k2, 0.999, TwoQubitWeylMirrorControlledEquiv, - {'rz': 6, 'ry': 4, - 'rxx': 1, 'ryy': 1, 'rzz': 1}) + {'rz': 6, 'ry': 4, 'rzz': 1, 'swap': 1}) def test_weyl_specialize_general(self, aaa=0.456, bbb=0.345, ccc=0.123): """Weyl specialization for partial swap gate""" From d180fee2f41443cbce7fc7f48af21ec9fb9fcaa6 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Mon, 8 Mar 2021 14:57:15 -0500 Subject: [PATCH 11/28] circuit __eq__ check phase mod 2pi --- qiskit/dagcircuit/dagcircuit.py | 2 +- test/python/transpiler/test_commutative_cancellation.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 7c804c005960..183f44934fe8 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -889,7 +889,7 @@ def __eq__(self, other): try: self_phase = float(self.global_phase) other_phase = float(other.global_phase) - if not np.isclose(self_phase, other_phase): + if (self_phase - other_phase) % (2 * np.pi) > 1.E-10: # FIXME: tolerance? return False except TypeError: if self.global_phase != other.global_phase: diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 6a7434dc0cb9..3af7d203cbb7 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -417,7 +417,8 @@ def test_commutative_circuit3(self): expected.append(RZGate(np.pi * 2 / 3), [qr[3]]) expected.cx(qr[2], qr[1]) - self.assertEqual(expected, new_circuit) + self.assertEqual(expected, new_circuit, + msg=f'expected:\n{expected}\nnew_circuit:\n{new_circuit}') def test_cnot_cascade(self): """ From 34b625f2a704792e958c0ec1b8067bd03259c92f Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 9 Mar 2021 00:00:25 -0500 Subject: [PATCH 12/28] fixup merge, better phase comparison --- qiskit/dagcircuit/dagcircuit.py | 4 +- .../synthesis/one_qubit_decompose.py | 102 ++++++++++-------- .../test_optimize_1q_decomposition.py | 12 ++- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 183f44934fe8..9ab0c5c40582 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -886,10 +886,11 @@ def __eq__(self, other): # Try to convert to float, but in case of unbound ParameterExpressions # a TypeError will be raise, fallback to normal equality in those # cases + try: self_phase = float(self.global_phase) other_phase = float(other.global_phase) - if (self_phase - other_phase) % (2 * np.pi) > 1.E-10: # FIXME: tolerance? + if (self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi > 1.E-10: # XXX: tolerance return False except TypeError: if self.global_phase != other.global_phase: @@ -913,7 +914,6 @@ def __eq__(self, other): for regname, reg in other.qregs.items()] other_creg_indices = [(regname, [other_bit_indices[bit] for bit in reg]) for regname, reg in other.cregs.items()] - if ( self_qreg_indices != other_qreg_indices or self_creg_indices != other_creg_indices diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index b8c19c3d60be..23796c7c23da 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -274,12 +274,13 @@ def _circuit_zyz(theta, simplify=True, atol=DEFAULT_ATOL): gphase = phase - (phi+lam)/2 - circuit = QuantumCircuit(1) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr) if simplify and abs(theta) < atol: tot = _mod_2pi(phi + lam) if abs(tot) > atol: - circuit._append(RZGate(tot), [0]) + circuit._append(RZGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit @@ -288,11 +289,11 @@ def _circuit_zyz(theta, lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: gphase += lam/2 - circuit._append(RZGate(lam), [0]) - circuit._append(RYGate(theta), [0]) + circuit._append(RZGate(lam), [qr[0]], []) + circuit._append(RYGate(theta), [qr[0]], []) if not simplify or abs(phi) > atol: gphase += phi/2 - circuit._append(RZGate(phi), [0]) + circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase = gphase return circuit @@ -304,12 +305,13 @@ def _circuit_zxz(theta, simplify=True, atol=DEFAULT_ATOL): gphase = phase - (phi+lam)/2 - circuit = QuantumCircuit(1) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr) if simplify and abs(theta) < atol: tot = _mod_2pi(phi + lam) if abs(tot) > atol: - circuit._append(RZGate(tot), [0]) + circuit._append(RZGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit @@ -318,11 +320,11 @@ def _circuit_zxz(theta, lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: gphase += lam/2 - circuit._append(RZGate(lam), [0]) - circuit._append(RXGate(theta), [0]) + circuit._append(RZGate(lam), [qr[0]], []) + circuit._append(RXGate(theta), [qr[0]], []) if not simplify or abs(phi) > atol: gphase += phi/2 - circuit._append(RZGate(phi), [0]) + circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase = gphase return circuit @@ -334,12 +336,13 @@ def _circuit_xyx(theta, simplify=False, atol=DEFAULT_ATOL): gphase = phase - (phi+lam)/2 - circuit = QuantumCircuit(1) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr) if simplify and abs(theta) < atol: tot = _mod_2pi(phi + lam) if abs(tot) > atol: - circuit._append(RXGate(tot), [0]) + circuit._append(RXGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit @@ -348,11 +351,11 @@ def _circuit_xyx(theta, lam, phi = _mod_2pi(lam-phi), 0 if not simplify or abs(lam) > atol: gphase += lam/2 - circuit._append(RXGate(lam), [0]) - circuit._append(RYGate(theta), [0]) + circuit._append(RXGate(lam), [qr[0]], []) + circuit._append(RYGate(theta), [qr[0]], []) if not simplify or abs(phi) > atol: gphase += phi/2 - circuit._append(RXGate(phi), [0]) + circuit._append(RXGate(phi), [qr[0]], []) circuit.global_phase = gphase return circuit @@ -363,9 +366,10 @@ def _circuit_u3(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase) if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: - circuit._append(U3Gate(theta, phi, lam), [0]) + circuit._append(U3Gate(theta, phi, lam), [qr[0]], []) return circuit @staticmethod @@ -375,13 +379,14 @@ def _circuit_u321(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase) if simplify and abs(theta) < atol: tot = _mod_2pi(phi + lam) if abs(tot) > atol: - circuit._append(U1Gate(tot), [0]) + circuit._append(U1Gate(tot), [qr[0]], []) elif simplify and abs(theta - np.pi/2) < atol: - circuit._append(U2Gate(phi, lam), [0]) + circuit._append(U2Gate(phi, lam), [qr[0]], []) else: circuit._append(U3Gate(theta, phi, lam), [qr[0]], []) return circuit @@ -393,25 +398,27 @@ def _circuit_u(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase) if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: - circuit._append(UGate(theta, phi, lam), [0]) + circuit._append(UGate(theta, phi, lam), [qr[0]], []) return circuit @staticmethod def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun): """Generic X90, phase decomposition""" - circuit = QuantumCircuit(1, global_phase=phase) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase) # Check for decomposition into minimimal number required SX pulses if simplify and np.abs(theta) < atol: # Zero SX gate decomposition - pfun(circuit, lam + phi) + pfun(circuit, qr, lam + phi) return circuit if simplify and abs(theta - np.pi/2) < atol: # Single SX gate decomposition - pfun(circuit, lam - np.pi/2) - xfun(circuit) - pfun(circuit, phi + np.pi/2) + pfun(circuit, qr, lam - np.pi/2) + xfun(circuit, qr) + pfun(circuit, qr, phi + np.pi/2) return circuit # General two-SX gate decomposition # Shift theta and phi so decomposition is @@ -420,11 +427,11 @@ def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun): circuit.global_phase += lam phi, lam = phi-lam, 0 circuit.global_phase -= np.pi/2 - pfun(circuit, lam) - xfun(circuit) - pfun(circuit, theta + np.pi) - xfun(circuit) - pfun(circuit, phi + np.pi) + pfun(circuit, qr, lam) + xfun(circuit, qr) + pfun(circuit, qr, theta + np.pi) + xfun(circuit, qr) + pfun(circuit, qr, phi + np.pi) return circuit @staticmethod @@ -434,13 +441,13 @@ def _circuit_psx(theta, phase, simplify=True, atol=DEFAULT_ATOL): - def fnz(circuit, phi): + def fnz(circuit, qr, phi): phi = _mod_2pi(phi) if not simplify or abs(phi) > atol: - circuit._append(PhaseGate(phi), [0]) + circuit._append(PhaseGate(phi), [qr[0]], []) - def fnx(circuit): - circuit._append(SXGate(), [0]) + def fnx(circuit, qr): + circuit._append(SXGate(), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, fnz, fnx) @@ -451,14 +458,14 @@ def _circuit_zsx(theta, phase, simplify=True, atol=DEFAULT_ATOL): - def fnz(circuit, phi): + def fnz(circuit, qr, phi): phi = _mod_2pi(phi) if not simplify or abs(phi) > atol: - circuit._append(RZGate(phi), [0]) + circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase += phi/2 - def fnx(circuit): - circuit._append(SXGate(), [0]) + def fnx(circuit, qr): + circuit._append(SXGate(), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, fnz, fnx) @@ -469,14 +476,14 @@ def _circuit_u1x(theta, phase, simplify=True, atol=DEFAULT_ATOL): - def fnz(circuit, phi): + def fnz(circuit, qr, phi): phi = _mod_2pi(phi) if not simplify or abs(phi) > atol: - circuit._append(U1Gate(phi), [0]) + circuit._append(U1Gate(phi), [qr[0]], []) - def fnx(circuit): + def fnx(circuit, qr): circuit.global_phase += np.pi/4 - circuit._append(RXGate(np.pi / 2), [0]) + circuit._append(RXGate(np.pi / 2), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, fnz, fnx) @@ -487,12 +494,13 @@ def _circuit_rr(theta, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = QuantumCircuit(1, global_phase=phase) + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr, global_phase=phase) if simplify and abs(theta) < atol and abs(phi) < atol and abs(lam) < atol: return circuit if not simplify or abs(theta - np.pi) > atol: - circuit._append(RGate(theta - np.pi, np.pi / 2 - lam), [0]) - circuit._append(RGate(np.pi, 0.5 * (phi - lam + np.pi)), [0]) + circuit._append(RGate(theta - np.pi, np.pi / 2 - lam), [qr[0]], []) + circuit._append(RGate(np.pi, 0.5 * (phi - lam + np.pi)), [qr[0]], []) return circuit diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 50281cce4678..6d6b7d4695e3 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -363,7 +363,8 @@ def test_optimize_u_to_phase_gate(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result, f"Expected:\n{expected}\nResult:\n{result}") + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) def test_optimize_u_to_p_sx_p(self): """U(pi/2, 0, pi/4) -> p(-pi/4)-sx-p(p/2). Basis [p, sx].""" @@ -382,7 +383,8 @@ def test_optimize_u_to_p_sx_p(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result, f"Expected:\n{expected}\nResult:\n{result}") + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) def test_optimize_u3_to_u1(self): """U3(0, 0, pi/4) -> U1(pi/4). Basis [u1, u2, u3].""" @@ -399,7 +401,8 @@ def test_optimize_u3_to_u1(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) def test_optimize_u3_to_u2(self): """U3(pi/2, 0, pi/4) -> U2(0, pi/4). Basis [u1, u2, u3].""" @@ -416,7 +419,8 @@ def test_optimize_u3_to_u2(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assertEqual(expected, result) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) if __name__ == '__main__': From 1dc6c8a5327922bc4514c1e5c5ff32f1cbe1072c Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 9 Mar 2021 01:34:37 -0500 Subject: [PATCH 13/28] drop numpy.typing hints (1.20+ feature) --- .../synthesis/two_qubit_decompose.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 601fbef89fd3..8daabab962ff 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -31,7 +31,6 @@ import logging import numpy as np -from numpy.typing import ArrayLike import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister @@ -45,7 +44,7 @@ logger = logging.getLogger(__name__) -def decompose_two_qubit_product_gate(special_unitary_matrix: ArrayLike): +def decompose_two_qubit_product_gate(special_unitary_matrix): """Decompose U = Ul⊗Ur where U in SU(4), and Ul, Ur in SU(2). Throws QiskitError if this isn't possible. """ @@ -119,7 +118,7 @@ class TwoQubitWeylDecomposition(): requested_fidelity: Optional[float] # None means no automatic specialization calculated_fidelity: float # Fidelity after specialization - _original_decomposition: Optional["TwoQubitWeylDecomposition"] + _original_decomposition: "TwoQubitWeylDecomposition" _is_flipped_from_original: bool _default_1q_basis: ClassVar[str] = 'ZYZ' @@ -132,7 +131,8 @@ def __init_subclass__(cls, **kwargs): cls.__new__ = (lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__(cls, *a, fidelity=None, **k)) - def __new__(cls, unitary_matrix: ArrayLike, *, fidelity=(1.-1.E-9)): + @staticmethod + def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): """Perform the Weyl chamber decomposition, and optionally choose a specialized subclass. The flip into the Weyl Chamber is described in B. Kraus and J. I. Cirac, Phys. Rev. A 63, @@ -294,8 +294,8 @@ def is_close(ap, bp, cp): instance._original_decomposition = od return instance - def __init__(self, unitary_matrix: ArrayLike, *args, fidelity=None): - super().__init__(*args) + def __init__(self, unitary_matrix, fidelity=None): + del unitary_matrix # unused in __init__ (used in new) od = self._original_decomposition self.a, self.b, self.c = od.a, od.b, od.c self.K1l, self.K1r = od.K1l, od.K1r @@ -336,14 +336,14 @@ def specialize(self): raise NotImplementedError def circuit(self, *, euler_basis: Optional[str] = None, - simplify=False, atol=DEFAULT_ATOL, **kwargs) -> QuantumCircuit: + simplify=False, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns Weyl decomposition in circuit form. Extra arguments are passed to OneQubitEulerDecomposer""" if euler_basis is None: euler_basis = self._default_1q_basis oneq_decompose = OneQubitEulerDecomposer(euler_basis) - c1l, c1r, c2l, c2r = (oneq_decompose(k, simplify=simplify, atol=atol, **kwargs) + c1l, c1r, c2l, c2r = (oneq_decompose(k, simplify=simplify, atol=atol) for k in (self.K1l, self.K1r, self.K2l, self.K2r)) circ = QuantumCircuit(2, global_phase=self.global_phase) circ.compose(c2r, [0], inplace=True) @@ -364,7 +364,7 @@ def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): if not simplify or abs(self.c) > atol: circ.rzz(-self.c*2, 0, 1) - def actual_fidelity(self, **kwargs): + def actual_fidelity(self, **kwargs) -> float: """Calculates the actual fidelity of the decomposed circuit to the input unitary""" circ = self.circuit(**kwargs) trace = np.trace(Operator(circ).data.T.conj() @ self.unitary_matrix) @@ -430,7 +430,7 @@ def _weyl_gate(self, simplify, circ: QuantumCircuit, atol): circ.global_phase -= 3*np.pi/4 -def _closest_partial_swap(a, b, c): +def _closest_partial_swap(a, b, c) -> float: """A good approximation to the best value x to get the minimum trace distance for Ud(x, x, x) from Ud(a, b, c) """ @@ -782,7 +782,7 @@ def decomp3_supercontrolled(self, target): return U3r, U3l, U2r, U2l, U1r, U1l, U0r, U0l - def __call__(self, target: ArrayLike, basis_fidelity=None, *, _num_basis_uses=None): + def __call__(self, target, basis_fidelity=None, *, _num_basis_uses=None) -> QuantumCircuit: """Decompose a two-qubit unitary over fixed basis + SU(2) using the best approximation given that each basis application has a finite fidelity. @@ -795,7 +795,7 @@ def __call__(self, target: ArrayLike, basis_fidelity=None, *, _num_basis_uses=No traces = self.traces(target_decomposed) expected_fidelities = [trace_to_fid(traces[i]) * basis_fidelity**i for i in range(4)] - best_nbasis = np.argmax(expected_fidelities) + best_nbasis = int(np.argmax(expected_fidelities)) if _num_basis_uses is not None: best_nbasis = _num_basis_uses decomposition = self.decomposition_fns[best_nbasis](target_decomposed) @@ -820,10 +820,6 @@ def num_basis_gates(self, unitary): """ Computes the number of basis gates needed in a decomposition of input unitary """ - if hasattr(unitary, 'to_operator'): - unitary = unitary.to_operator().data - if hasattr(unitary, 'to_matrix'): - unitary = unitary.to_matrix() unitary = np.asarray(unitary, dtype=complex) a, b, c = weyl_coordinates(unitary)[:] traces = [4*(math.cos(a)*math.cos(b)*math.cos(c)+1j*math.sin(a)*math.sin(b)*math.sin(c)), From f99712e3a54dc8418cb41414a5fcd3c16ea825cc Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Fri, 12 Mar 2021 14:51:29 -0500 Subject: [PATCH 14/28] Comments and test coverage --- .../synthesis/two_qubit_decompose.py | 15 ++-- test/python/quantum_info/test_synthesis.py | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 8daabab962ff..d3306472f56a 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -70,7 +70,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix): phase = cmath.phase(detL) / 2 temp = np.kron(L, R) - deviation = abs(abs(temp.conj(temp).T.dot(special_unitary_matrix).trace()) - 4) + deviation = abs(abs(temp.conj().T.dot(special_unitary_matrix).trace()) - 4) if deviation > 1.E-13: raise QiskitError("decompose_two_qubit_product_gate: decomposition failed: " "deviation too large: {}".format(deviation)) @@ -103,9 +103,10 @@ class TwoQubitWeylDecomposition(): thus avoiding problems of numerical stability. Passing non-None fidelity to specializations is treated as an assertion, raising QiskitError if - the specialization is more approximate. + forcing the specialization is more approximate than asserted. """ + # The parameters of the decomposition: a: float b: float c: float @@ -114,14 +115,15 @@ class TwoQubitWeylDecomposition(): K2l: np.ndarray K1r: np.ndarray K2r: np.ndarray + unitary_matrix: np.ndarray # The unitary that was input requested_fidelity: Optional[float] # None means no automatic specialization calculated_fidelity: float # Fidelity after specialization _original_decomposition: "TwoQubitWeylDecomposition" - _is_flipped_from_original: bool + _is_flipped_from_original: bool # The approx is closest to a Weyl reflection of the original? - _default_1q_basis: ClassVar[str] = 'ZYZ' + _default_1q_basis: ClassVar[str] = 'ZYZ' # Default one qubit basis (explicit parameterization) def __init_subclass__(cls, **kwargs): """Subclasses should be concrete, not factories. @@ -252,7 +254,6 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): od.K2l = K2l od.K2r = K2r od.global_phase = global_phase - od.unitary_matrix = unitary_matrix od.requested_fidelity = fidelity od.calculated_fidelity = 1.0 od.unitary_matrix = np.array(unitary_matrix, copy=True) @@ -332,7 +333,9 @@ def __init__(self, unitary_matrix, fidelity=None): def specialize(self): """Make changes to the decomposition to comply with any specialization. - Do not update the global phase, since gets done in generic __init__()""" + Do update a, b, c, k1l, k1r, k2l, k2r, _is_flipped_from_original to round to the + specialization. Do not update the global phase, since this gets done in generic + __init__()""" raise NotImplementedError def circuit(self, *, euler_basis: Optional[str] = None, diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 3039b600f080..ce9f405c7fb3 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -13,6 +13,8 @@ """Tests for quantum synthesis methods.""" import unittest +import contextlib +import logging from test import combine from ddt import ddt @@ -41,7 +43,9 @@ TwoQubitWeylGeneral, two_qubit_cnot_decompose, TwoQubitBasisDecomposer, - Ud) + Ud, + decompose_two_qubit_product_gate) + from qiskit.quantum_info.synthesis.ion_decompose import cnot_rxx_decompose from qiskit.test import QiskitTestCase @@ -100,10 +104,20 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', tolerance=1e-14, si self.assertTrue(np.abs(maxdist) < tolerance, "Operator {}: Worst distance {}".format(operator, maxdist)) + @contextlib.contextmanager + def assertDebugOnly(self): # FIXME: when at python 3.10+ replace with assertNoLogs + """Context manager, asserts log is emitted at level DEBUG but no higher""" + with self.assertLogs("qiskit.quantum_info.synthesis", "DEBUG") as ctx: + yield + for i in range(len(ctx.records)): + self.assertLessEqual(ctx.records[i].levelno, logging.DEBUG, + msg=f"Unexpected logging entry: {ctx.output[i]}") + def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name - decomp = TwoQubitWeylDecomposition(target_unitary, fidelity=None) + with self.assertDebugOnly(): + decomp = TwoQubitWeylDecomposition(target_unitary, fidelity=None) op = np.exp(1j * decomp.global_phase) * Operator(np.eye(4)) for u, qs in ( (decomp.K2r, [0]), @@ -125,7 +139,8 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, # Loop to check both for implicit and explicity specialization for decomposer in (TwoQubitWeylDecomposition, expected_specialization): - decomp = decomposer(target_unitary, fidelity=fidelity) + with self.assertDebugOnly(): + decomp = decomposer(target_unitary, fidelity=fidelity) self.assertEqual(np.max(np.abs(decomp.unitary_matrix - target_unitary)), 0, "Incorrect saved unitary in the decomposition.") self.assertIsInstance(decomp, expected_specialization, @@ -141,10 +156,12 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, trace = np.trace(actual_unitary.T.conj() @ target_unitary) self.assertAlmostEqual(trace.imag, 0, places=13, msg=f"Real trace for {decomposer.__name__}") - _ = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise + with self.assertDebugOnly(): + _ = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise if expected_specialization is not TwoQubitWeylGeneral: - with self.assertRaises(QiskitError): + with self.assertRaises(QiskitError) as exc: _ = expected_specialization(target_unitary, fidelity=1.) + self.assertIn("worse than requested", exc.exception.message) def check_exact_decomposition(self, target_unitary, decomposer, tolerance=1.e-13, num_basis_uses=None): @@ -786,7 +803,8 @@ def test_approx_supercontrolled_decompose_random(self, seed): tgt = random_unitary(4, seed=state).data tgt *= np.exp(1j * tgt_phase) - traces_pred = decomposer.traces(TwoQubitWeylDecomposition(tgt)) + with self.assertDebugOnly(): + traces_pred = decomposer.traces(TwoQubitWeylDecomposition(tgt)) for i in range(4): decomp_circuit = decomposer(tgt, _num_basis_uses=i) @@ -933,7 +951,6 @@ def check_approx_decomposition(self, target_unitary, decomposer, num_basis_uses) self.assertEqual(decomposer.num_basis_gates(target_unitary), num_basis_uses) decomp_circuit = decomposer(target_unitary) self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get('unitary', 0)) - # Now check the fidelity? @combine(seed=range(10), name='seed_{seed}') def test_approx_supercontrolled_decompose_phase_0_use_random(self, seed, delta=0.01): @@ -1014,5 +1031,64 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=3) +class TestDecomposeProductRaises(QiskitTestCase): + """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" + def test_decompose_two_qubit_product_gate_detr_too_small(self): + """Check that exception raised for too-small right component""" + kl = np.eye(2) + kr = 0.05*np.eye(2) + klkr = np.kron(kl, kr) + with self.assertRaises(QiskitError) as exc: + decompose_two_qubit_product_gate(klkr) + self.assertIn("detR <", exc.exception.message) + + def test_decompose_two_qubit_product_gate_detl_too_small(self): + """Check that exception raised for too-small left component""" + kl = np.array([[1, 0], [0, 0]]) + kr = np.eye(2) + klkr = np.kron(kl, kr) + with self.assertRaises(QiskitError) as exc: + decompose_two_qubit_product_gate(klkr) + self.assertIn("detL <", exc.exception.message) + + def test_decompose_two_qubit_product_gate_not_product(self): + """Check that exception raised for non-product unitary""" + klkr = Ud(1.E-6, 0, 0) + with self.assertRaises(QiskitError) as exc: + decompose_two_qubit_product_gate(klkr) + self.assertIn("decomposition failed", exc.exception.message) + + +class TestTwoQubitWeylDecompositionMisc(QiskitTestCase): + """Miscellaneous checks for TwoQubitWeylDecomposition""" + + def test_TwoQubitWeylDecomposition_log_debug_fidelity(self, fidelity=0.99): + """Check DEBUG log issued""" + target = Ud(0.3, 0.2, 0.1) + with self.assertLogs("qiskit.quantum_info.synthesis", "DEBUG") as ctx: + TwoQubitWeylDecomposition(target, fidelity=fidelity) + self.assertIn("Requested fidelity:", ctx.records[0].getMessage()) + + def test_TwoQubitWeylDecomposition_repr(self, seed=42): + """Check that eval(__repr__) is exact round trip""" + target = random_unitary(4, seed=seed) + weyl1 = TwoQubitWeylDecomposition(target, fidelity=0.99) + weyl2: TwoQubitWeylDecomposition = eval(repr(weyl1)) # pylint: disable=eval-used + maxdiff = np.max(abs(weyl1.unitary_matrix-weyl2.unitary_matrix)) + self.assertEqual(maxdiff, 0, msg=f"Unitary matrix element differs by {maxdiff}") + self.assertEqual(weyl1.a, weyl2.a) + self.assertEqual(weyl1.b, weyl2.b) + self.assertEqual(weyl1.c, weyl2.c) + maxdiff = np.max(np.abs(weyl1.K1l - weyl2.K1l)) + self.assertEqual(maxdiff, 0, msg=f"K1l matrix element differs by {maxdiff}") + maxdiff = np.max(np.abs(weyl1.K1r - weyl2.K1r)) + self.assertEqual(maxdiff, 0, msg=f"K1r matrix element differs by {maxdiff}") + maxdiff = np.max(np.abs(weyl1.K2l - weyl2.K2l)) + self.assertEqual(maxdiff, 0, msg=f"K2l matrix element differs by {maxdiff}") + maxdiff = np.max(np.abs(weyl1.K2r - weyl2.K2r)) + self.assertEqual(maxdiff, 0, msg=f"K2r matrix element differs by {maxdiff}") + self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity) + + if __name__ == '__main__': unittest.main() From 222f4ad8adf2ede32729af02663bfb4fad845d74 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Sun, 14 Mar 2021 18:51:47 -0400 Subject: [PATCH 15/28] Update __repr__ and testing Make __repr__ use numpy.save/numpy.load to work around bug in numpy.array2string --- .../synthesis/two_qubit_decompose.py | 38 ++++++++---- test/python/quantum_info/test_synthesis.py | 59 +++++++++---------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index d3306472f56a..81f7642fae93 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -25,6 +25,8 @@ """ import cmath import math +import io +import base64 import warnings from typing import ClassVar, Optional @@ -162,8 +164,8 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): # M2 is a symmetric complex matrix. We need to decompose it as M2 = P D P^T where # P ∈ SO(4), D is diagonal with unit-magnitude elements. # D, P = la.eig(M2) # this can fail for certain kinds of degeneracy - for i in range(100): # FIXME: this randomized algorithm is horrendous - state = np.random.default_rng(i) + state = np.random.default_rng(2020) + for _ in range(100): # FIXME: this randomized algorithm is horrendous M2real = state.normal()*M2.real + state.normal()*M2.imag _, P = np.linalg.eigh(M2real) D = P.T.dot(M2).dot(P).diagonal() @@ -376,14 +378,30 @@ def actual_fidelity(self, **kwargs) -> float: def __repr__(self): """Represent with enough precision to allow copy-paste debugging of all corner cases """ - prefix = f"{self.__class__.__name__}(" - prefix_nd = " np.array(" - suffix = ")," - array = np.array2string(self.unitary_matrix, prefix=prefix+prefix_nd, suffix=suffix, - separator=', ', max_line_width=1000, - floatmode='maxprec_equal', precision=100) - return (f"{prefix}{prefix_nd}{array}{suffix}\n" - f"{' '*len(prefix)}fidelity={self.requested_fidelity})") + prefix = f"{type(self).__qualname__}.from_bytes(" + with io.BytesIO() as f: + np.save(f, self.unitary_matrix, allow_pickle=False) + b64 = base64.encodebytes(f.getvalue()).splitlines() + b64ascii = [repr(x) for x in b64] + b64ascii[-1] += "," + pretty = [f'# {x.rstrip()}' for x in str(self).splitlines()] + indent = '\n' + ' '*4 + lines = ([prefix] + pretty + b64ascii + + [f"requested_fidelity={self.requested_fidelity},", + f"calculated_fidelity={self.calculated_fidelity},", + f"actual_fidelity={self.actual_fidelity()},", + f"abc={(self.a, self.b, self.c)})"]) + return indent.join(lines) + + @classmethod + def from_bytes(cls, bytes_in: bytes, *, requested_fidelity: float, **kwargs + ) -> "TwoQubitWeylDecomposition": + """Decode bytes into TwoQubitWeylDecomposition. Used by __repr__""" + del kwargs # Unused (just for display) + b64 = base64.decodebytes(bytes_in) + with io.BytesIO(b64) as f: + arr = np.load(f, allow_pickle=False) + return cls(arr, fidelity=requested_fidelity) def __str__(self): pre = f"{self.__class__.__name__}(\n\t" diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ce9f405c7fb3..c9d3c90b504d 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -112,12 +112,36 @@ def assertDebugOnly(self): # FIXME: when at python 3.10+ replace with assertNoL for i in range(len(ctx.records)): self.assertLessEqual(ctx.records[i].levelno, logging.DEBUG, msg=f"Unexpected logging entry: {ctx.output[i]}") + self.assertIn("Requested fidelity:", ctx.records[i].getMessage()) + + def assertRoundTrip(self, weyl1: TwoQubitWeylDecomposition): + """Fail if eval(repr(weyl1)) not equal to weyl1""" + repr1 = repr(weyl1) + with self.assertDebugOnly(): + weyl2: TwoQubitWeylDecomposition = eval(repr1) # pylint: disable=eval-used + msg_base = f"weyl1:\n{repr1}\nweyl2:\n{repr(weyl2)}" + self.assertEqual(type(weyl1), type(weyl2), msg_base) + maxdiff = np.max(abs(weyl1.unitary_matrix-weyl2.unitary_matrix)) + self.assertEqual(maxdiff, 0, msg=f"Unitary matrix differs by {maxdiff}\n" + msg_base) + self.assertEqual(weyl1.a, weyl2.a, msg=msg_base) + self.assertEqual(weyl1.b, weyl2.b, msg=msg_base) + self.assertEqual(weyl1.c, weyl2.c, msg=msg_base) + maxdiff = np.max(np.abs(weyl1.K1l - weyl2.K1l)) + self.assertEqual(maxdiff, 0, msg=f"K1l matrix differs by {maxdiff}" + msg_base) + maxdiff = np.max(np.abs(weyl1.K1r - weyl2.K1r)) + self.assertEqual(maxdiff, 0, msg=f"K1r matrix differs by {maxdiff}" + msg_base) + maxdiff = np.max(np.abs(weyl1.K2l - weyl2.K2l)) + self.assertEqual(maxdiff, 0, msg=f"K2l matrix differs by {maxdiff}" + msg_base) + maxdiff = np.max(np.abs(weyl1.K2r - weyl2.K2r)) + self.assertEqual(maxdiff, 0, msg=f"K2r matrix differs by {maxdiff}" + msg_base) + self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity, msg_base) def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name with self.assertDebugOnly(): decomp = TwoQubitWeylDecomposition(target_unitary, fidelity=None) + self.assertRoundTrip(decomp) op = np.exp(1j * decomp.global_phase) * Operator(np.eye(4)) for u, qs in ( (decomp.K2r, [0]), @@ -141,6 +165,7 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, for decomposer in (TwoQubitWeylDecomposition, expected_specialization): with self.assertDebugOnly(): decomp = decomposer(target_unitary, fidelity=fidelity) + self.assertRoundTrip(decomp) self.assertEqual(np.max(np.abs(decomp.unitary_matrix - target_unitary)), 0, "Incorrect saved unitary in the decomposition.") self.assertIsInstance(decomp, expected_specialization, @@ -157,7 +182,8 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, self.assertAlmostEqual(trace.imag, 0, places=13, msg=f"Real trace for {decomposer.__name__}") with self.assertDebugOnly(): - _ = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise + decomp2 = expected_specialization(target_unitary, fidelity=None) # Shouldn't raise + self.assertRoundTrip(decomp2) if expected_specialization is not TwoQubitWeylGeneral: with self.assertRaises(QiskitError) as exc: _ = expected_specialization(target_unitary, fidelity=1.) @@ -1059,36 +1085,5 @@ def test_decompose_two_qubit_product_gate_not_product(self): self.assertIn("decomposition failed", exc.exception.message) -class TestTwoQubitWeylDecompositionMisc(QiskitTestCase): - """Miscellaneous checks for TwoQubitWeylDecomposition""" - - def test_TwoQubitWeylDecomposition_log_debug_fidelity(self, fidelity=0.99): - """Check DEBUG log issued""" - target = Ud(0.3, 0.2, 0.1) - with self.assertLogs("qiskit.quantum_info.synthesis", "DEBUG") as ctx: - TwoQubitWeylDecomposition(target, fidelity=fidelity) - self.assertIn("Requested fidelity:", ctx.records[0].getMessage()) - - def test_TwoQubitWeylDecomposition_repr(self, seed=42): - """Check that eval(__repr__) is exact round trip""" - target = random_unitary(4, seed=seed) - weyl1 = TwoQubitWeylDecomposition(target, fidelity=0.99) - weyl2: TwoQubitWeylDecomposition = eval(repr(weyl1)) # pylint: disable=eval-used - maxdiff = np.max(abs(weyl1.unitary_matrix-weyl2.unitary_matrix)) - self.assertEqual(maxdiff, 0, msg=f"Unitary matrix element differs by {maxdiff}") - self.assertEqual(weyl1.a, weyl2.a) - self.assertEqual(weyl1.b, weyl2.b) - self.assertEqual(weyl1.c, weyl2.c) - maxdiff = np.max(np.abs(weyl1.K1l - weyl2.K1l)) - self.assertEqual(maxdiff, 0, msg=f"K1l matrix element differs by {maxdiff}") - maxdiff = np.max(np.abs(weyl1.K1r - weyl2.K1r)) - self.assertEqual(maxdiff, 0, msg=f"K1r matrix element differs by {maxdiff}") - maxdiff = np.max(np.abs(weyl1.K2l - weyl2.K2l)) - self.assertEqual(maxdiff, 0, msg=f"K2l matrix element differs by {maxdiff}") - maxdiff = np.max(np.abs(weyl1.K2r - weyl2.K2r)) - self.assertEqual(maxdiff, 0, msg=f"K2r matrix element differs by {maxdiff}") - self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity) - - if __name__ == '__main__': unittest.main() From 86d7543a86b7bd5eccbad6920468892eea562c90 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Sun, 14 Mar 2021 19:20:48 -0400 Subject: [PATCH 16/28] Broader test numerical tolerance --- test/python/quantum_info/test_synthesis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index c9d3c90b504d..8b4723cf2e83 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -136,12 +136,12 @@ def assertRoundTrip(self, weyl1: TwoQubitWeylDecomposition): self.assertEqual(maxdiff, 0, msg=f"K2r matrix differs by {maxdiff}" + msg_base) self.assertEqual(weyl1.requested_fidelity, weyl2.requested_fidelity, msg_base) - def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-13): + def check_two_qubit_weyl_decomposition(self, target_unitary, tolerance=1.e-12): """Check TwoQubitWeylDecomposition() works for a given operator""" # pylint: disable=invalid-name with self.assertDebugOnly(): decomp = TwoQubitWeylDecomposition(target_unitary, fidelity=None) - self.assertRoundTrip(decomp) + # self.assertRoundTrip(decomp) # Too slow op = np.exp(1j * decomp.global_phase) * Operator(np.eye(4)) for u, qs in ( (decomp.K2r, [0]), @@ -190,7 +190,7 @@ def check_two_qubit_weyl_specialization(self, target_unitary, fidelity, self.assertIn("worse than requested", exc.exception.message) def check_exact_decomposition(self, target_unitary, decomposer, - tolerance=1.e-13, num_basis_uses=None): + tolerance=1.e-12, num_basis_uses=None): """Check exact decomposition for a particular target""" decomp_circuit = decomposer(target_unitary, _num_basis_uses=num_basis_uses) if num_basis_uses is not None: From 0081a06f94773878c7f7feb1a7a1903c59ced73c Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Sun, 14 Mar 2021 21:33:46 -0400 Subject: [PATCH 17/28] Add back previously failing test --- test/python/quantum_info/test_synthesis.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 8b4723cf2e83..1af94286371b 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -433,6 +433,12 @@ class TestTwoQubitWeylDecomposition(CheckDecompositions): """Test TwoQubitWeylDecomposition() """ + def test_TwoQubitWeylDecomposition_repr(self, seed=42): + """Check that eval(__repr__) is exact round trip""" + target = random_unitary(4, seed=seed) + weyl1 = TwoQubitWeylDecomposition(target, fidelity=0.99) + self.assertRoundTrip(weyl1) + def test_two_qubit_weyl_decomposition_cnot(self): """Verify Weyl KAK decomposition for U~CNOT""" for k1l, k1r, k2l, k2r in K1K2S: From 1558c04e5457b8028d03b10af136db8460d23188 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 16 Mar 2021 10:50:27 -0400 Subject: [PATCH 18/28] Avoid accidentally making object ndarrays --- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 81f7642fae93..f5f92984041b 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -50,7 +50,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix): """Decompose U = Ul⊗Ur where U in SU(4), and Ul, Ur in SU(2). Throws QiskitError if this isn't possible. """ - special_unitary_matrix = np.asarray(special_unitary_matrix) + special_unitary_matrix = np.asarray(special_unitary_matrix, dtype=complex) # extract the right component R = special_unitary_matrix[:2, :2].copy() detR = R[0, 0]*R[1, 1] - R[0, 1]*R[1, 0] @@ -153,7 +153,7 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): pi4 = np.pi/4 # Make U be in SU(4) - U = np.array(unitary_matrix, copy=True) + U = np.array(unitary_matrix, dtype=complex, copy=True) detU = la.det(U) U *= detU**(-0.25) global_phase = cmath.phase(detU) / 4 @@ -258,7 +258,7 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.-1.E-9)): od.global_phase = global_phase od.requested_fidelity = fidelity od.calculated_fidelity = 1.0 - od.unitary_matrix = np.array(unitary_matrix, copy=True) + od.unitary_matrix = np.array(unitary_matrix, dtype=complex, copy=True) od.unitary_matrix.setflags(write=False) od._original_decomposition = None od._is_flipped_from_original = False From c2ab47ddcac66dbf9158157d9257e80f6d8578c7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 16 Mar 2021 12:23:41 -0400 Subject: [PATCH 19/28] Update test/python/transpiler/test_optimize_1q_decomposition.py --- test/python/transpiler/test_optimize_1q_decomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 3e63860f9a50..2bfd6ecfdea0 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -415,7 +415,7 @@ def test_optimize_u3_to_u2(self): passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - self.assert_equal(expected, result) + self.assertEqual(expected, result) def test_y_simplification_rz_sx_x(self): """Test that a y gate gets decomposed to x-zx with ibmq basis.""" From b977685b81c5941018e453dd94b42b094b9ba9b5 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 17 Mar 2021 02:52:53 -0400 Subject: [PATCH 20/28] Fix decomposition and tests --- .../synthesis/one_qubit_decompose.py | 33 ++++++++++--------- test/python/quantum_info/test_synthesis.py | 16 +++++++++ .../test_optimize_1q_decomposition.py | 3 +- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 365670d77d47..7e8cb291f9f9 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -413,7 +413,7 @@ def _circuit_u(theta, return circuit @staticmethod - def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun, simplify_x=False): + def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun, xpifun=None): """Generic X90, phase decomposition""" qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) @@ -436,13 +436,14 @@ def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun, simplif phi, lam = phi-lam, 0 circuit.global_phase -= np.pi/2 pfun(circuit, qr, lam) - xfun(circuit, qr, theta + np.pi) - pfun(circuit, qr, theta + np.pi) - if not simplify or not simplify_x or abs(_mod_2pi(theta + np.pi)) > atol: - xfun(circuit, qr) - pfun(circuit, qr, phi + np.pi) + if simplify and xpifun and abs(_mod_2pi(theta + np.pi)) < atol: + xpifun(circuit, qr) else: - pfun(circuit, qr, phi - np.pi) + xfun(circuit, qr) + pfun(circuit, qr, theta + np.pi) + xfun(circuit, qr) + pfun(circuit, qr, phi + np.pi) + return circuit @staticmethod @@ -457,7 +458,7 @@ def fnz(circuit, qr, phi): if not simplify or abs(phi) > atol: circuit._append(PhaseGate(phi), [qr[0]], []) - def fnx(circuit, qr, _=None): + def fnx(circuit, qr): circuit._append(SXGate(), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, fnz, fnx) @@ -475,7 +476,7 @@ def fnz(circuit, qr, phi): circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase += phi/2 - def fnx(circuit, qr, _=None): + def fnx(circuit, qr): circuit._append(SXGate(), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, @@ -493,7 +494,7 @@ def fnz(circuit, qr, phi): if not simplify or abs(phi) > atol: circuit._append(U1Gate(phi), [qr[0]], []) - def fnx(circuit, qr, _=None): + def fnx(circuit, qr): circuit.global_phase += np.pi/4 circuit._append(RXGate(np.pi / 2), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, @@ -512,14 +513,14 @@ def fnz(circuit, qr, phi): circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase += phi/2 - def fnx(circuit, qr, phi=None): - if phi is not None and simplify and not abs(_mod_2pi(phi)) > atol: - circuit._append(XGate(), [qr[0]], []) - else: - circuit._append(SXGate(), [qr[0]], []) + def fnx(circuit, qr): + circuit._append(SXGate(), [qr[0]], []) + + def fnxpi(circuit, qr): + circuit._append(XGate(), [qr[0]], []) return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, - fnz, fnx, simplify_x=True) + fnz, fnx, fnxpi) @staticmethod def _circuit_rr(theta, diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index f798b0e9b342..3d5c40c2c326 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -364,6 +364,22 @@ def test_special_ZSX(self): {'rz': 2, 'sx': 1}) self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'ZSX', {'rz': 3, 'sx': 2}) + def test_special_ZSXX(self): + """Special cases of ZSXX""" + self.check_oneq_special_cases(U3Gate(0.0, 0.1, -0.1).to_matrix(), 'ZSXX', {}) + self.check_oneq_special_cases(U3Gate(0.0, 0.1, 0.2).to_matrix(), 'ZSXX', {'rz': 1}) + self.check_oneq_special_cases(U3Gate(-np.pi/2, 0.2, 0.0).to_matrix(), 'ZSXX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.0, 0.21).to_matrix(), 'ZSXX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(np.pi/2, 0.12, 0.2).to_matrix(), 'ZSXX', + {'rz': 2, 'sx': 1}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), 'ZSXX', {'rz': 3, 'sx': 2}) + self.check_oneq_special_cases(U3Gate(np.pi, 0.2, 0.3).to_matrix(), 'ZSXX', + {'rz': 1, 'x': 1}) + self.check_oneq_special_cases(U3Gate(np.pi, -np.pi/2, np.pi/2).to_matrix(), 'ZSXX', + {'x': 1}) + ONEQ_BASES = ['U3', "U321", 'U', 'U1X', 'PSX', 'ZSX', 'ZSXX', 'ZYZ', 'ZXZ', 'XYX', 'RR'] SIMP_TOL = [(False, 1.e-14), (True, 1.E-12)] # Please don't broaden the tolerance (fix the decomp) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 55a8e73ff401..47cd0b8c6afb 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -430,11 +430,10 @@ def test_y_simplification_rz_sx_x(self): passmanager = PassManager() passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) - passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) expected = QuantumCircuit(1) expected.x(0) - expected.rz(np.pi, 0) + expected.rz(-np.pi, 0) msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) From dbf1c36e978a99f4c766796c96237e8d83fedd17 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 17 Mar 2021 10:45:18 -0400 Subject: [PATCH 21/28] TIghten test tolerance, clamp angle wrapping --- qiskit/circuit/instruction.py | 4 +- .../synthesis/one_qubit_decompose.py | 139 +++++++++++------- test/python/compiler/test_transpiler.py | 6 +- 3 files changed, 90 insertions(+), 59 deletions(-) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 74285a0a7332..f3b630eed915 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -117,14 +117,14 @@ def __eq__(self, other): try: if numpy.shape(self_param) == numpy.shape(other_param) \ and numpy.allclose(self_param, other_param, - atol=_CUTOFF_PRECISION): + atol=_CUTOFF_PRECISION, rtol=0): continue except TypeError: pass try: if numpy.isclose(float(self_param), float(other_param), - atol=_CUTOFF_PRECISION): + atol=_CUTOFF_PRECISION, rtol=0): continue except TypeError: pass diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 7e8cb291f9f9..765450c3293f 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -284,22 +284,25 @@ def _circuit_zyz(theta, gphase = phase - (phi+lam)/2 qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr) - - if simplify and abs(theta) < atol: - tot = _mod_2pi(phi + lam) + if not simplify: + atol = -1.0 + if abs(theta) < atol: + tot = _mod_2pi(phi + lam, atol) if abs(tot) > atol: circuit._append(RZGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit - if simplify and abs(theta - np.pi) < atol: + if abs(theta - np.pi) < atol: gphase += phi - lam, phi = _mod_2pi(lam-phi), 0 - if not simplify or abs(lam) > atol: + lam, phi = lam-phi, 0 + lam = _mod_2pi(lam, atol) + if abs(lam) > atol: gphase += lam/2 circuit._append(RZGate(lam), [qr[0]], []) circuit._append(RYGate(theta), [qr[0]], []) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: gphase += phi/2 circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase = gphase @@ -315,22 +318,25 @@ def _circuit_zxz(theta, gphase = phase - (phi+lam)/2 qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr) - - if simplify and abs(theta) < atol: + if not simplify: + atol = -1.0 + if abs(theta) < atol: tot = _mod_2pi(phi + lam) if abs(tot) > atol: circuit._append(RZGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit - if simplify and abs(theta - np.pi) < atol: + if abs(theta - np.pi) < atol: gphase += phi - lam, phi = _mod_2pi(lam-phi), 0 - if not simplify or abs(lam) > atol: + lam, phi = lam - phi, 0 + lam = _mod_2pi(lam, atol) + if abs(lam) > atol: gphase += lam/2 circuit._append(RZGate(lam), [qr[0]], []) circuit._append(RXGate(theta), [qr[0]], []) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: gphase += phi/2 circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase = gphase @@ -346,22 +352,25 @@ def _circuit_xyx(theta, gphase = phase - (phi+lam)/2 qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr) - - if simplify and abs(theta) < atol: - tot = _mod_2pi(phi + lam) + if not simplify: + atol = -1.0 + if abs(theta) < atol: + tot = _mod_2pi(phi + lam, atol) if abs(tot) > atol: circuit._append(RXGate(tot), [qr[0]], []) gphase += tot/2 circuit.global_phase = gphase return circuit - if simplify and abs(theta - np.pi) < atol: + if abs(theta - np.pi) < atol: gphase += phi - lam, phi = _mod_2pi(lam-phi), 0 - if not simplify or abs(lam) > atol: + lam, phi = lam-phi, 0 + lam = _mod_2pi(lam, atol) + if abs(lam) > atol: gphase += lam/2 circuit._append(RXGate(lam), [qr[0]], []) circuit._append(RYGate(theta), [qr[0]], []) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: gphase += phi/2 circuit._append(RXGate(phi), [qr[0]], []) circuit.global_phase = gphase @@ -376,6 +385,8 @@ def _circuit_u3(theta, atol=DEFAULT_ATOL): qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) + phi = _mod_2pi(phi, atol) + lam = _mod_2pi(lam, atol) if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: circuit._append(U3Gate(theta, phi, lam), [qr[0]], []) return circuit @@ -389,14 +400,16 @@ def _circuit_u321(theta, atol=DEFAULT_ATOL): qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) - if simplify and abs(theta) < atol: - tot = _mod_2pi(phi + lam) + if not simplify: + atol = -1.0 + if abs(theta) < atol: + tot = _mod_2pi(phi + lam, atol) if abs(tot) > atol: circuit._append(U1Gate(tot), [qr[0]], []) - elif simplify and abs(theta - np.pi/2) < atol: - circuit._append(U2Gate(phi, lam), [qr[0]], []) + elif abs(theta - np.pi/2) < atol: + circuit._append(U2Gate(_mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], []) else: - circuit._append(U3Gate(theta, phi, lam), [qr[0]], []) + circuit._append(U3Gate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], []) return circuit @staticmethod @@ -408,21 +421,23 @@ def _circuit_u(theta, atol=DEFAULT_ATOL): qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) - if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: - circuit._append(UGate(theta, phi, lam), [qr[0]], []) + if not simplify: + atol = -1.0 + if abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: + circuit._append(UGate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], []) return circuit @staticmethod - def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun, xpifun=None): + def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): """Generic X90, phase decomposition""" qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) # Check for decomposition into minimimal number required SX pulses - if simplify and np.abs(theta) < atol: + if np.abs(theta) < atol: # Zero SX gate decomposition pfun(circuit, qr, lam + phi) return circuit - if simplify and abs(theta - np.pi/2) < atol: + if abs(theta - np.pi/2) < atol: # Single SX gate decomposition pfun(circuit, qr, lam - np.pi/2) xfun(circuit, qr) @@ -431,12 +446,12 @@ def _circuit_psx_gen(theta, phi, lam, phase, simplify, atol, pfun, xfun, xpifun= # General two-SX gate decomposition # Shift theta and phi so decomposition is # P(phi).SX.P(theta).SX.P(lam) - if simplify and abs(theta-np.pi) < atol: + if abs(theta-np.pi) < atol: circuit.global_phase += lam phi, lam = phi-lam, 0 circuit.global_phase -= np.pi/2 pfun(circuit, qr, lam) - if simplify and xpifun and abs(_mod_2pi(theta + np.pi)) < atol: + if xpifun and abs(_mod_2pi(theta + np.pi)) < atol: xpifun(circuit, qr) else: xfun(circuit, qr) @@ -453,15 +468,18 @@ def _circuit_psx(theta, phase, simplify=True, atol=DEFAULT_ATOL): + if not simplify: + atol = -1.0 + def fnz(circuit, qr, phi): - phi = _mod_2pi(phi) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: circuit._append(PhaseGate(phi), [qr[0]], []) def fnx(circuit, qr): circuit._append(SXGate(), [qr[0]], []) - return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, - fnz, fnx) + + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) @staticmethod def _circuit_zsx(theta, @@ -470,17 +488,19 @@ def _circuit_zsx(theta, phase, simplify=True, atol=DEFAULT_ATOL): + if not simplify: + atol = -1.0 + def fnz(circuit, qr, phi): - phi = _mod_2pi(phi) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase += phi/2 def fnx(circuit, qr): circuit._append(SXGate(), [qr[0]], []) - return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, - fnz, fnx) + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) @staticmethod def _circuit_u1x(theta, @@ -489,16 +509,19 @@ def _circuit_u1x(theta, phase, simplify=True, atol=DEFAULT_ATOL): + if not simplify: + atol = -1.0 + def fnz(circuit, qr, phi): - phi = _mod_2pi(phi) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: circuit._append(U1Gate(phi), [qr[0]], []) def fnx(circuit, qr): circuit.global_phase += np.pi/4 circuit._append(RXGate(np.pi / 2), [qr[0]], []) - return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, - fnz, fnx) + + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) @staticmethod def _circuit_zsxx(theta, @@ -507,9 +530,12 @@ def _circuit_zsxx(theta, phase, simplify=True, atol=DEFAULT_ATOL): + if not simplify: + atol = -1.0 + def fnz(circuit, qr, phi): - phi = _mod_2pi(phi) - if not simplify or abs(phi) > atol: + phi = _mod_2pi(phi, atol) + if abs(phi) > atol: circuit._append(RZGate(phi), [qr[0]], []) circuit.global_phase += phi/2 @@ -519,7 +545,7 @@ def fnx(circuit, qr): def fnxpi(circuit, qr): circuit._append(XGate(), [qr[0]], []) - return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, simplify, atol, + return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx, fnxpi) @staticmethod @@ -531,14 +557,19 @@ def _circuit_rr(theta, atol=DEFAULT_ATOL): qr = QuantumRegister(1, 'qr') circuit = QuantumCircuit(qr, global_phase=phase) - if simplify and abs(theta) < atol and abs(phi) < atol and abs(lam) < atol: + if not simplify: + atol = -1.0 + if abs(theta) < atol and abs(phi) < atol and abs(lam) < atol: return circuit - if not simplify or abs(theta - np.pi) > atol: - circuit._append(RGate(theta - np.pi, np.pi / 2 - lam), [qr[0]], []) - circuit._append(RGate(np.pi, 0.5 * (phi - lam + np.pi)), [qr[0]], []) + if abs(theta - np.pi) > atol: + circuit._append(RGate(theta - np.pi, _mod_2pi(np.pi / 2 - lam, atol)), [qr[0]], []) + circuit._append(RGate(np.pi, _mod_2pi(0.5 * (phi - lam + np.pi), atol)), [qr[0]], []) return circuit -def _mod_2pi(angle: float): - """Wrap angle into interval [-π,π)""" - return (angle+np.pi) % (2*np.pi) - np.pi +def _mod_2pi(angle: float, atol: float = 0): + """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" + wrapped = (angle+np.pi) % (2*np.pi) - np.pi + if abs(wrapped - np.pi) < atol: + wrapped = -np.pi + return wrapped diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 518ecfa02848..95f7e5a17268 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -599,12 +599,12 @@ def test_initialize_reset_should_be_removed(self): qc.initialize([1.0 / math.sqrt(2), -1.0 / math.sqrt(2)], [qr[0]]) expected = QuantumCircuit(qr) - expected.append(U3Gate(1.5708, 0, 0), [qr[0]]) + expected.append(U3Gate(np.pi/2, 0, 0), [qr[0]]) expected.reset(qr[0]) - expected.append(U3Gate(1.5708, 3.1416, 0), [qr[0]]) + expected.append(U3Gate(np.pi/2, -np.pi, 0), [qr[0]]) after = transpile(qc, basis_gates=['reset', 'u3'], optimization_level=1) - self.assertEqual(after, expected) + self.assertEqual(after, expected, msg=f"after:\n{after}\nexpected:\n{expected}") def test_initialize_FakeMelbourne(self): """Test that the zero-state resets are remove in a device not supporting them. From b0af2090a34248f10194023448c01af4261d4b71 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 17 Mar 2021 12:50:37 -0400 Subject: [PATCH 22/28] circuit.__eq__ false equalities, fixup test --- qiskit/dagcircuit/dagcircuit.py | 2 +- test/python/transpiler/test_optimize_1q_decomposition.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index c5626f9896ed..d8cdccdc6718 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -893,7 +893,7 @@ def __eq__(self, other): try: self_phase = float(self.global_phase) other_phase = float(other.global_phase) - if (self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi > 1.E-10: # XXX: tolerance + if abs((self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi) > 1.E-10: # XXX: atol? return False except TypeError: if self.global_phase != other.global_phase: diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 47cd0b8c6afb..6cce7a03dca1 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -434,6 +434,7 @@ def test_y_simplification_rz_sx_x(self): expected = QuantumCircuit(1) expected.x(0) expected.rz(-np.pi, 0) + expected.global_phase += np.pi msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg) From b45dd1bbe805bae68cecaafdb13025871e95c128 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Wed, 17 Mar 2021 14:33:47 -0400 Subject: [PATCH 23/28] Default simplify=True typo --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 765450c3293f..8447f0a146f1 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -347,7 +347,7 @@ def _circuit_xyx(theta, phi, lam, phase, - simplify=False, + simplify=True, atol=DEFAULT_ATOL): gphase = phase - (phi+lam)/2 qr = QuantumRegister(1, 'qr') From a20fe639edf670cce2a05f33a8b745b02fc959ca Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 23 Mar 2021 11:16:06 -0400 Subject: [PATCH 24/28] Fixes for review --- qiskit/dagcircuit/dagcircuit.py | 2 +- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index d8cdccdc6718..54f85e4f8d08 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -893,7 +893,7 @@ def __eq__(self, other): try: self_phase = float(self.global_phase) other_phase = float(other.global_phase) - if abs((self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi) > 1.E-10: # XXX: atol? + if abs((self_phase - other_phase + np.pi) % (2*np.pi) - np.pi) > 1.E-10: # TODO: atol? return False except TypeError: if self.global_phase != other.global_phase: diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 8447f0a146f1..825b89ac227f 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -86,12 +86,13 @@ class OneQubitEulerDecomposer: :math:`U_1(\theta+\pi).R_X\left(\frac{\pi}{2}\right).U_1(\lambda)` * - 'ZSX' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` - :math:`R_Z(\theta+\pi).S_X\left(\frac{\pi}{2}\right).U_1(\lambda)` + - :math:`e^{i\gamma} RZ(\phi+\pi).S_X.` + :math:`R_Z(\theta+\pi).S_X.RZ(\lambda)` * - 'ZSXX' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` - :math:`R_Z(\theta+\pi).S_X\left(\frac{\pi}{2}\right).U_1(\lambda)` + - :math:`e^{i\gamma} RZ(\phi+\pi).S_X.R_Z(\theta+\pi).S_X.RZ(\lambda)` + or + :math:`e^{i\gamma} RZ(\phi+\pi).X.RZ(\lambda)` * - 'U1X' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` From e796f576ff52fc9625572255088f393ea22ca29c Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 23 Mar 2021 12:14:31 -0400 Subject: [PATCH 25/28] propagate circuit.global_phase changes to dags --- qiskit/dagcircuit/dagcircuit.py | 4 +--- qiskit/dagcircuit/dagdependency.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 54f85e4f8d08..ef0d539a56f1 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -172,12 +172,10 @@ def global_phase(self, angle): if isinstance(angle, ParameterExpression): self._global_phase = angle else: - # Set the phase to the [-2 * pi, 2 * pi] interval + # Set the phase to the [0, 2π) interval angle = float(angle) if not angle: self._global_phase = 0 - elif angle < 0: - self._global_phase = angle % (-2 * math.pi) else: self._global_phase = angle % (2 * math.pi) diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index c49ac130cfbd..4b594020bd5b 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -107,12 +107,10 @@ def global_phase(self, angle): if isinstance(angle, ParameterExpression): self._global_phase = angle else: - # Set the phase to the [-2 * pi, 2 * pi] interval + # Set the phase to the [0, 2π) interval angle = float(angle) if not angle: self._global_phase = 0 - elif angle < 0: - self._global_phase = angle % (-2 * math.pi) else: self._global_phase = angle % (2 * math.pi) From 42c9803ef934fa4b9befc84db4d9bd16aae244bf Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 13 Apr 2021 12:05:21 -0400 Subject: [PATCH 26/28] Docstrings and fix failing test Some tests were failing in TestSingleControlledRotationGates due to different global phase tracking causing an extra P() gate in some circumstances. Since the CCRZ gates are not minimal in single-qubit rotations anyway, and should be single-qubit optimized afterwards I fixed this by changing the test to pass for the slightly longer result circuit. --- qiskit/quantum_info/synthesis/one_qubit_decompose.py | 12 +++++++----- qiskit/quantum_info/synthesis/two_qubit_decompose.py | 4 ++-- test/python/circuit/test_controlled_gate.py | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index d0d81a5fab9a..6759217363fc 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -86,13 +86,13 @@ class OneQubitEulerDecomposer: :math:`U_1(\theta+\pi).R_X\left(\frac{\pi}{2}\right).U_1(\lambda)` * - 'ZSX' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - - :math:`e^{i\gamma} RZ(\phi+\pi).S_X.` - :math:`R_Z(\theta+\pi).S_X.RZ(\lambda)` + - :math:`e^{i\gamma} R_Z(\phi+\pi).\sqrt{X}.` + :math:`R_Z(\theta+\pi).\sqrt{X}.R_Z(\lambda)` * - 'ZSXX' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - - :math:`e^{i\gamma} RZ(\phi+\pi).S_X.R_Z(\theta+\pi).S_X.RZ(\lambda)` + - :math:`e^{i\gamma} R_Z(\phi+\pi).\sqrt{X}.R_Z(\theta+\pi).\sqrt{X}.R_Z(\lambda)` or - :math:`e^{i\gamma} RZ(\phi+\pi).X.RZ(\lambda)` + :math:`e^{i\gamma} R_Z(\phi+\pi).X.R_Z(\lambda)` * - 'U1X' - :math:`Z(\phi) Y(\theta) Z(\lambda)` - :math:`e^{i\gamma} U_1(\phi+\pi).R_X\left(\frac{\pi}{2}\right).` @@ -424,8 +424,10 @@ def _circuit_u(theta, circuit = QuantumCircuit(qr, global_phase=phase) if not simplify: atol = -1.0 + phi = _mod_2pi(phi, atol) + lam = _mod_2pi(lam, atol) if abs(theta) > atol or abs(phi) > atol or abs(lam) > atol: - circuit._append(UGate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], []) + circuit._append(UGate(theta, phi, lam), [qr[0]], []) return circuit @staticmethod diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index f5f92984041b..6da1548b4967 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -130,7 +130,7 @@ class TwoQubitWeylDecomposition(): def __init_subclass__(cls, **kwargs): """Subclasses should be concrete, not factories. - Have explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" + Make explicitly-instantiated subclass __new__ call base __new__ with fidelity=None""" super().__init_subclass__(**kwargs) cls.__new__ = (lambda cls, *a, fidelity=None, **k: TwoQubitWeylDecomposition.__new__(cls, *a, fidelity=None, **k)) @@ -344,7 +344,7 @@ def circuit(self, *, euler_basis: Optional[str] = None, simplify=False, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns Weyl decomposition in circuit form. - Extra arguments are passed to OneQubitEulerDecomposer""" + simplify, atol arguments are passed to OneQubitEulerDecomposer""" if euler_basis is None: euler_basis = self._default_1q_basis oneq_decompose = OneQubitEulerDecomposer(euler_basis) diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 3a0382379dd7..a0048397fa1c 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -1190,11 +1190,11 @@ def test_single_controlled_rotation_gates(self, gate, cgate): self.log.info('\n%s', str(uqc)) # these limits could be changed if gate.name == 'ry': - self.assertLessEqual(uqc.size(), 32) + self.assertLessEqual(uqc.size(), 32, f"\n{uqc}") elif gate.name == 'rz': - self.assertLessEqual(uqc.size(), 42) + self.assertLessEqual(uqc.size(), 43, f"\n{uqc}") else: - self.assertLessEqual(uqc.size(), 20) + self.assertLessEqual(uqc.size(), 20, f"\n{uqc}") def test_composite(self): """Test composite gate count.""" @@ -1209,7 +1209,7 @@ def test_composite(self): unroller = Unroller(['u', 'cx']) uqc = dag_to_circuit(unroller.run(dag)) self.log.info('%s gate count: %d', uqc.name, uqc.size()) - self.assertLessEqual(uqc.size(), 95) # this limit could be changed + self.assertLessEqual(uqc.size(), 96, f"\n{uqc}") # this limit could be changed @ddt From 5d4afa1520b8ee5b122d4f983f194db4d554dbb0 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 13 Apr 2021 12:26:36 -0400 Subject: [PATCH 27/28] Add release note --- .../notes/ZSXX-decomposition-90fead72778e410e.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml diff --git a/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml b/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml new file mode 100644 index 000000000000..9bdbf8208ab3 --- /dev/null +++ b/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + The decomposition methods for single-qubit gates in + :class:`~qiskit.quantum_info.synthesis.one_qubit_decompose.OneQubitEulerDecomposer` has been expanded to now + also include the ``'ZSXX'`` basis, for making use of direct :math:`X` gate as well as :math:`\sqrt{X}`. +fixes: + - | + Tightened up tolerances, improved repeatability and simplification, fixed several global phase-tracking bugs + for one- and two-qubit gate synthesis. From af762482fc7706753d04d60fd171d24018ca5f50 Mon Sep 17 00:00:00 2001 From: "Lev S. Bishop" <18673315+levbishop@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:16:24 -0400 Subject: [PATCH 28/28] Release note --- releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml b/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml index 9bdbf8208ab3..d5c878aafb37 100644 --- a/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml +++ b/releasenotes/notes/ZSXX-decomposition-90fead72778e410e.yaml @@ -2,9 +2,9 @@ features: - | The decomposition methods for single-qubit gates in - :class:`~qiskit.quantum_info.synthesis.one_qubit_decompose.OneQubitEulerDecomposer` has been expanded to now + :class:`~qiskit.quantum_info.OneQubitEulerDecomposer` has been expanded to now also include the ``'ZSXX'`` basis, for making use of direct :math:`X` gate as well as :math:`\sqrt{X}`. fixes: - | - Tightened up tolerances, improved repeatability and simplification, fixed several global phase-tracking bugs + Tightened up tolerances, improved repeatability and simplification, fixed several global-phase-tracking bugs for one- and two-qubit gate synthesis.