From fa12111d6bc74646758d836572fe4a59cfb576c6 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 4 Dec 2019 18:35:19 -0500 Subject: [PATCH 1/2] Remove quaternions from XYX synthesis algorithm Start adding infrastructure for phase correct one-qubit synthesis --- .../synthesis/one_qubit_decompose.py | 200 ++++++++---------- test/python/quantum_info/test_synthesis.py | 87 ++++---- 2 files changed, 131 insertions(+), 156 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 877c03bf6687..9326bba2ba4c 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -13,7 +13,6 @@ # that they have been altered from the originals. # pylint: disable=invalid-name - """ Decompose single-qubit unitary into Euler angles. """ @@ -23,6 +22,7 @@ import scipy.linalg as la from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.extensions.standard import HGate, U3Gate, U1Gate, 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 @@ -34,13 +34,12 @@ class OneQubitEulerDecomposer: """A class for decomposing 1-qubit unitaries into Eular angle rotations. Allowed basis and their decompositions are: - U3: U -> phase * U3(theta, phi, lam) - U1X: U -> phase * U1(lam).RX(pi/2).U1(theta+pi).RX(pi/2).U1(phi+pi) - ZYZ: U -> phase * RZ(phi).RY(theta).RZ(lam) - ZXZ: U -> phase * RZ(phi).RX(theta).RZ(lam) - XYX: U -> phase * RX(phi).RY(theta).RX(lam) + U3: U -> exp(1j*phase) * U3(theta, phi, lam) + U1X: U -> exp(1j*phase) * U1(lam).RX(pi/2).U1(theta+pi).RX(pi/2).U1(phi+pi) + ZYZ: U -> exp(1j*phase) * RZ(phi).RY(theta).RZ(lam) + ZXZ: U -> exp(1j*phase) * RZ(phi).RX(theta).RZ(lam) + XYX: U -> exp(1j*phase) * RX(phi).RY(theta).RX(lam) """ - def __init__(self, basis='U3'): if basis not in ['U3', 'U1X', 'ZYZ', 'ZXZ', 'XYX']: raise QiskitError("OneQubitEulerDecomposer: unsupported basis") @@ -81,45 +80,60 @@ def __call__(self, unitary_mat, simplify=True, atol=DEFAULT_ATOL): "input matrix is not unitary.") circuit = self._circuit(unitary_mat, simplify=simplify, atol=atol) # Check circuit is correct - if not Operator(circuit).equiv(unitary_mat): + if not Operator(circuit).equiv(Operator(unitary_mat)): raise QiskitError("OneQubitEulerDecomposer: " "synthesis failed within required accuracy.") return circuit - def _angles(self, unitary_mat, atol=DEFAULT_ATOL): + def _angles(self, unitary_mat): """Return Euler angles for given basis.""" if self._basis in ['U3', 'U1X', 'ZYZ', 'ZXZ']: return self._angles_zyz(unitary_mat) if self._basis == 'XYX': - return self._angles_xyx(unitary_mat, atol=atol) + return self._angles_xyx(unitary_mat) raise QiskitError("OneQubitEulerDecomposer: invalid basis") def _circuit(self, unitary_mat, simplify=True, atol=DEFAULT_ATOL): - # Add phase to matrix to make it special unitary - # This ensure that the quaternion representation is real - angles = self._angles(unitary_mat) + theta, phi, lam, phase = self._angles(unitary_mat) if self._basis == 'U3': - return self._circuit_u3(angles) + return self._circuit_u3(theta, phi, lam) if self._basis == 'U1X': - return self._circuit_u1x(angles, simplify=simplify, atol=atol) + return self._circuit_u1x(theta, + phi, + lam, + simplify=simplify, + atol=atol) if self._basis == 'ZYZ': - return self._circuit_zyz(angles, simplify=simplify, atol=atol) + return self._circuit_zyz(theta, + phi, + lam, + simplify=simplify, + atol=atol) if self._basis == 'ZXZ': - return self._circuit_zxz(angles, simplify=simplify, atol=atol) + return self._circuit_zxz(theta, + phi, + lam, + simplify=simplify, + atol=atol) if self._basis == 'XYX': - return self._circuit_xyx(angles, simplify=simplify, atol=atol) + return self._circuit_xyx(theta, + phi, + lam, + simplify=simplify, + atol=atol) raise QiskitError("OneQubitEulerDecomposer: invalid basis") @staticmethod - def _angles_zyz(unitary_matrix): - """Return euler angles for unitary matrix in ZYZ basis. + def _angles_zyz(unitary_mat): + """Return euler angles for special unitary matrix in ZYZ basis. - In this representation U = Rz(phi).Ry(theta).Rz(lam) + In this representation U = exp(1j * phase) * Rz(phi).Ry(theta).Rz(lam) """ - if unitary_matrix.shape != (2, 2): - raise QiskitError("euler_angles_1q: expected 2x2 matrix") - phase = la.det(unitary_matrix)**(-1.0/2.0) - U = phase * unitary_matrix # U in SU(2) + # We rescale the input matrix to be special unitary (det(U) = 1) + # This ensures that the quaternion representation is real + coeff = la.det(unitary_mat)**(-0.5) + phase = -np.angle(coeff) + U = coeff * unitary_mat # U in SU(2) # OpenQASM SU(2) parameterization: # U[0, 0] = exp(-i(phi+lambda)/2) * cos(theta/2) # U[0, 1] = -exp(-i(phi-lambda)/2) * sin(theta/2) @@ -130,135 +144,91 @@ def _angles_zyz(unitary_matrix): phimlambda = 2 * np.angle(U[1, 0]) phi = (phiplambda + phimlambda) / 2.0 lam = (phiplambda - phimlambda) / 2.0 - return theta, phi, lam + return theta, phi, lam, phase @staticmethod - def _quaternions(unitary_matrix): - """Return quaternions for a special unitary matrix""" - # Get quaternions (q0, q1, q2, q3) - # so that su_mat = q0*I - 1j * (q1*X + q2*Y + q3*Z) - # We get them from the canonical ZYZ euler angles - theta, phi, lam = OneQubitEulerDecomposer._angles_zyz( - unitary_matrix) - quats = np.zeros(4, dtype=complex) - quats[0] = math.cos(0.5 * theta) * math.cos(0.5 * (lam + phi)) - quats[1] = math.sin(0.5 * theta) * math.sin(0.5 * (lam - phi)) - quats[2] = math.sin(0.5 * theta) * math.cos(0.5 * (lam - phi)) - quats[3] = math.cos(0.5 * theta) * math.sin(0.5 * (lam + phi)) - return quats + def _angles_xyx(unitary_mat): + """Return euler angles for special unitary matrix in XYX basis. - @staticmethod - def _angles_xyx(unitary_matrix, atol=DEFAULT_ATOL): - """Return euler angles for unitary matrix in XYX basis. - - In this representation U = Rx(phi).Ry(theta).Rx(lam) + In this representation U = exp(1j * phase) * Rx(phi).Ry(theta).Rx(lam) """ - # pylint: disable=too-many-return-statements - quats = OneQubitEulerDecomposer._quaternions(unitary_matrix) - # Check quaternions for pure Pauli rotations - if np.allclose(abs(quats), np.array([1., 0., 0., 0.]), atol=atol): - # Identity - return np.zeros(3, dtype=float) - if np.allclose(abs(quats), np.array([0., 1., 0., 0.]), atol=atol): - # +/- RX180 - return np.array([0., 0, -quats[1] * np.pi]) - if np.allclose(abs(quats), np.array([0., 0., 1., 0.]), atol=atol): - # +/- RY180 - return np.array([quats[2] * np.pi, 0., 0.]) - if np.allclose(abs(quats), np.array([0., 0., 0., 1.]), atol=atol): - # +/- RZ180 - return np.array([np.pi, quats[3] * np.pi, 0.]) - if np.allclose(quats[[1, 3]], np.array([0., 0.]), atol=atol): - # RY rotation - arg = np.clip(np.real(2 * quats[0] * quats[2]), -1., 1.) - return np.array([math.asin(arg), 0., 0.]) - if np.allclose(quats[[2, 3]], np.array([0., 0.]), atol=atol): - # RX rotation - arg = np.clip(np.real(2 * quats[0] * quats[1]), -1., 1.) - return np.array([0., 0., math.asin(arg)]) - # General case - return np.array([ - math.acos(np.clip(np.real(quats[0] * quats[0] + quats[1] * quats[1] - - quats[2] * quats[2] - quats[3] * quats[3]), - -1., 1.)), - math.atan2(np.real(quats[1] * quats[2] + quats[0] * quats[3]), - np.real(quats[0] * quats[2] - quats[1] * quats[3])), - math.atan2(np.real(quats[1] * quats[2] - quats[0] * quats[3]), - np.real(quats[0] * quats[2] + quats[1] * quats[3]))]) + # We use the fact that + # Rx(a).Ry(b).Rx(c) = H.Rz(a).Ry(-b).Rz(c).H + had = HGate().to_matrix() + mat_zyz = np.dot(np.dot(had, unitary_mat), had) + theta, phi, lam, phase = OneQubitEulerDecomposer._angles_zyz(mat_zyz) + return -theta, phi, lam, phase @staticmethod - def _circuit_u3(angles): - theta, phi, lam = angles + def _circuit_u3(theta, phi, lam): circuit = QuantumCircuit(1) - circuit.u3(theta, phi, lam, 0) + circuit.append(U3Gate(theta, phi, lam), [0]) return circuit @staticmethod - def _circuit_u1x(angles, simplify=True, atol=DEFAULT_ATOL): + def _circuit_u1x(theta, phi, lam, simplify=True, atol=DEFAULT_ATOL): # Check for U1 and U2 decompositions into minimimal # required X90 pulses - theta, phi, lam = angles if simplify and np.allclose([theta, phi], [0., 0.], atol=atol): # zero X90 gate decomposition circuit = QuantumCircuit(1) - circuit.u1(lam, 0) + circuit.append(U1Gate(lam), [0]) return circuit if simplify and np.isclose(theta, np.pi / 2, atol=atol): # single X90 gate decomposition circuit = QuantumCircuit(1) - circuit.u1(lam - np.pi / 2, 0) - circuit.rx(np.pi / 2, 0) - circuit.u1(phi + np.pi / 2, 0) + circuit.append(U1Gate(lam - np.pi / 2), [0]) + circuit.append(RXGate(np.pi / 2), [0]) + circuit.append(U1Gate(phi + np.pi / 2), [0]) return circuit # General two-X90 gate decomposition circuit = QuantumCircuit(1) - circuit.u1(lam, 0) - circuit.rx(np.pi / 2, 0) - circuit.u1(theta + np.pi, 0) - circuit.rx(np.pi / 2, 0) - circuit.u1(phi + np.pi, 0) + circuit.append(U1Gate(lam), [0]) + circuit.append(RXGate(np.pi / 2), [0]) + circuit.append(U1Gate(theta + np.pi), [0]) + circuit.append(RXGate(np.pi / 2), [0]) + circuit.append(U1Gate(phi + np.pi), [0]) return circuit @staticmethod - def _circuit_zyz(angles, simplify=True, atol=DEFAULT_ATOL): - theta, phi, lam = angles + def _circuit_zyz(theta, phi, lam, simplify=True, atol=DEFAULT_ATOL): + circuit = QuantumCircuit(1) if simplify and np.isclose(theta, 0.0, atol=atol): - circuit = QuantumCircuit(1) - circuit.rz(phi + lam, 0) + circuit.append(RZGate(phi + lam), [0]) return circuit - circuit = QuantumCircuit(1) if not simplify or not np.isclose(lam, 0.0, atol=atol): - circuit.rz(lam, 0) + circuit.append(RZGate(lam), [0]) if not simplify or not np.isclose(theta, 0.0, atol=atol): - circuit.ry(theta, 0) + circuit.append(RYGate(theta), [0]) if not simplify or not np.isclose(phi, 0.0, atol=atol): - circuit.rz(phi, 0) + circuit.append(RZGate(phi), [0]) return circuit @staticmethod - def _circuit_xyx(angles, simplify=True, atol=DEFAULT_ATOL): - theta, phi, lam = angles + def _circuit_zxz(theta, phi, lam, simplify=False, atol=DEFAULT_ATOL): + if simplify and np.isclose(theta, 0.0, atol=atol): + circuit = QuantumCircuit(1) + circuit.append(RZGate(phi + lam), [0]) + return circuit circuit = QuantumCircuit(1) - if not simplify or not np.isclose(lam, 0.0, atol=atol): - circuit.rx(lam, 0) + if not simplify or not np.isclose(lam, np.pi / 2, atol=atol): + circuit.append(RZGate(lam - np.pi / 2), [0]) if not simplify or not np.isclose(theta, 0.0, atol=atol): - circuit.ry(theta, 0) - if not simplify or not np.isclose(phi, 0.0, atol=atol): - circuit.rx(phi, 0) + circuit.append(RXGate(theta), [0]) + if not simplify or not np.isclose(phi, -np.pi / 2, atol=atol): + circuit.append(RZGate(phi + np.pi / 2), [0]) return circuit @staticmethod - def _circuit_zxz(angles, simplify=False, atol=DEFAULT_ATOL): - theta, phi, lam = angles + def _circuit_xyx(theta, phi, lam, simplify=True, atol=DEFAULT_ATOL): + circuit = QuantumCircuit(1) if simplify and np.isclose(theta, 0.0, atol=atol): - circuit = QuantumCircuit(1) - circuit.rz(phi + lam, 0) + circuit.append(RXGate(phi + lam), [0]) return circuit - circuit = QuantumCircuit(1) - if not simplify or not np.isclose(lam, np.pi/2, atol=atol): - circuit.rz(lam-np.pi/2, 0) + if not simplify or not np.isclose(lam, 0.0, atol=atol): + circuit.append(RXGate(lam), [0]) if not simplify or not np.isclose(theta, 0.0, atol=atol): - circuit.rx(theta, 0) - if not simplify or not np.isclose(phi, -np.pi/2, atol=atol): - circuit.rz(phi+np.pi/2, 0) + circuit.append(RYGate(theta), [0]) + if not simplify or not np.isclose(phi, 0.0, atol=atol): + circuit.append(RXGate(phi), [0]) return circuit diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index b279ac715235..06d34ec7ae41 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -128,36 +128,29 @@ def check_one_qubit_euler_angles(self, operator, basis='U3', self.assertTrue(np.abs(maxdist) < tolerance, "Worst distance {}".format(maxdist)) + # U3 basis def test_one_qubit_clifford_u3_basis(self): """Verify for u3 basis and all Cliffords.""" for clifford in ONEQ_CLIFFORDS: self.check_one_qubit_euler_angles(clifford, 'U3') - def test_one_qubit_clifford_u1x_basis(self): - """Verify for u1, x90 basis and all Cliffords.""" - for clifford in ONEQ_CLIFFORDS: - self.check_one_qubit_euler_angles(clifford, 'U1X') - - def test_one_qubit_clifford_zyz_basis(self): - """Verify for rz, ry, rz basis and all Cliffords.""" - for clifford in ONEQ_CLIFFORDS: - self.check_one_qubit_euler_angles(clifford, 'ZYZ') - - def test_one_qubit_clifford_zxz_basis(self): - """Verify for rz, rx, rz basis and all Cliffords.""" - for clifford in ONEQ_CLIFFORDS: - self.check_one_qubit_euler_angles(clifford, 'ZXZ') - - def test_one_qubit_clifford_xyx_basis(self): - """Verify for rx, ry, rx basis and all Cliffords.""" - for clifford in ONEQ_CLIFFORDS: - self.check_one_qubit_euler_angles(clifford, 'XYX') - def test_one_qubit_hard_thetas_u3_basis(self): """Verify for u3 basis and close-to-degenerate theta.""" for gate in HARD_THETA_ONEQS: self.check_one_qubit_euler_angles(Operator(gate), 'U3') + def test_one_qubit_random_u3_basis(self, nsamples=50): + """Verify for u3 basis and random unitaries.""" + for _ in range(nsamples): + unitary = random_unitary(2) + self.check_one_qubit_euler_angles(unitary, 'U3') + + # U1, X90 basis + def test_one_qubit_clifford_u1x_basis(self): + """Verify for u1, x90 basis and all Cliffords.""" + for clifford in ONEQ_CLIFFORDS: + self.check_one_qubit_euler_angles(clifford, 'U1X') + def test_one_qubit_hard_thetas_u1x_basis(self): """Verify for u1, x90 basis and close-to-degenerate theta.""" # We lower tolerance for this test since decomposition is @@ -166,45 +159,57 @@ def test_one_qubit_hard_thetas_u1x_basis(self): for gate in HARD_THETA_ONEQS: self.check_one_qubit_euler_angles(Operator(gate), 'U1X', 1e-7) - def test_one_qubit_hard_thetas_zyz_basis(self): - """Verify for rz, ry, rz basis and close-to-degenerate theta.""" - for gate in HARD_THETA_ONEQS: - self.check_one_qubit_euler_angles(Operator(gate), 'ZYZ') - - def test_one_qubit_hard_thetas_zxz_basis(self): - """Verify for rz, rx, rz basis and close-to-degenerate theta.""" - for gate in HARD_THETA_ONEQS: - self.check_one_qubit_euler_angles(Operator(gate), 'ZXZ') - - def test_one_qubit_hard_thetas_xyx_basis(self): - """Verify for rx, ry, rx basis and close-to-degenerate theta.""" - for gate in HARD_THETA_ONEQS: - self.check_one_qubit_euler_angles(Operator(gate), 'XYX') - - def test_one_qubit_random_u3_basis(self, nsamples=50): - """Verify for u3 basis and random unitaries.""" - for _ in range(nsamples): - unitary = random_unitary(2) - self.check_one_qubit_euler_angles(unitary, 'U3') - def test_one_qubit_random_u1x_basis(self, nsamples=50): """Verify for u1, x90 basis and random unitaries.""" for _ in range(nsamples): unitary = random_unitary(2) self.check_one_qubit_euler_angles(unitary, 'U1X') + # Rz, Ry, Rz basis + def test_one_qubit_clifford_zyz_basis(self): + """Verify for rz, ry, rz basis and all Cliffords.""" + for clifford in ONEQ_CLIFFORDS: + self.check_one_qubit_euler_angles(clifford, 'ZYZ') + + def test_one_qubit_hard_thetas_zyz_basis(self): + """Verify for rz, ry, rz basis and close-to-degenerate theta.""" + for gate in HARD_THETA_ONEQS: + self.check_one_qubit_euler_angles(Operator(gate), 'ZYZ') + def test_one_qubit_random_zyz_basis(self, nsamples=50): """Verify for rz, ry, rz basis and random unitaries.""" for _ in range(nsamples): unitary = random_unitary(2) self.check_one_qubit_euler_angles(unitary, 'ZYZ') + # Rz, Rx, Rz basis + def test_one_qubit_clifford_zxz_basis(self): + """Verify for rz, rx, rz basis and all Cliffords.""" + for clifford in ONEQ_CLIFFORDS: + self.check_one_qubit_euler_angles(clifford, 'ZXZ') + + def test_one_qubit_hard_thetas_zxz_basis(self): + """Verify for rz, rx, rz basis and close-to-degenerate theta.""" + for gate in HARD_THETA_ONEQS: + self.check_one_qubit_euler_angles(Operator(gate), 'ZXZ') + def test_one_qubit_random_zxz_basis(self, nsamples=50): """Verify for rz, rx, rz basis and random unitaries.""" for _ in range(nsamples): unitary = random_unitary(2) self.check_one_qubit_euler_angles(unitary, 'ZXZ') + # Rx, Ry, Rx basis + def test_one_qubit_clifford_xyx_basis(self): + """Verify for rx, ry, rx basis and all Cliffords.""" + for clifford in ONEQ_CLIFFORDS: + self.check_one_qubit_euler_angles(clifford, 'XYX') + + def test_one_qubit_hard_thetas_xyx_basis(self): + """Verify for rx, ry, rx basis and close-to-degenerate theta.""" + for gate in HARD_THETA_ONEQS: + self.check_one_qubit_euler_angles(Operator(gate), 'XYX') + def test_one_qubit_random_xyx_basis(self, nsamples=50): """Verify for rx, ry, rx basis and random unitaries.""" for _ in range(nsamples): From 4fb8e21178d091ed40dc8af8717a7f6d09d6ba1f Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 18 Dec 2019 14:48:05 -0500 Subject: [PATCH 2/2] add angles_zxz function --- .../synthesis/one_qubit_decompose.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 9326bba2ba4c..ba7f311ad086 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -87,8 +87,10 @@ def __call__(self, unitary_mat, simplify=True, atol=DEFAULT_ATOL): def _angles(self, unitary_mat): """Return Euler angles for given basis.""" - if self._basis in ['U3', 'U1X', 'ZYZ', 'ZXZ']: + if self._basis in ['U3', 'U1X', 'ZYZ']: return self._angles_zyz(unitary_mat) + if self._basis == 'ZXZ': + return self._angles_zxz(unitary_mat) if self._basis == 'XYX': return self._angles_xyx(unitary_mat) raise QiskitError("OneQubitEulerDecomposer: invalid basis") @@ -146,6 +148,15 @@ def _angles_zyz(unitary_mat): lam = (phiplambda - phimlambda) / 2.0 return theta, phi, lam, phase + @staticmethod + def _angles_zxz(unitary_mat): + """Return euler angles for special unitary matrix in ZXZ basis. + + In this representation U = exp(1j * phase) * Rz(phi).Rx(theta).Rz(lam) + """ + theta, phi, lam, phase = OneQubitEulerDecomposer._angles_zyz(unitary_mat) + return theta, phi + np.pi / 2, lam - np.pi / 2, phase + @staticmethod def _angles_xyx(unitary_mat): """Return euler angles for special unitary matrix in XYX basis. @@ -211,12 +222,12 @@ def _circuit_zxz(theta, phi, lam, simplify=False, atol=DEFAULT_ATOL): circuit.append(RZGate(phi + lam), [0]) return circuit circuit = QuantumCircuit(1) - if not simplify or not np.isclose(lam, np.pi / 2, atol=atol): - circuit.append(RZGate(lam - np.pi / 2), [0]) + if not simplify or not np.isclose(lam, 0.0, atol=atol): + circuit.append(RZGate(lam), [0]) if not simplify or not np.isclose(theta, 0.0, atol=atol): circuit.append(RXGate(theta), [0]) - if not simplify or not np.isclose(phi, -np.pi / 2, atol=atol): - circuit.append(RZGate(phi + np.pi / 2), [0]) + if not simplify or not np.isclose(phi, 0.0, atol=atol): + circuit.append(RZGate(phi), [0]) return circuit @staticmethod