From 0aab5aeb5d6a6bdeeebc99aa2ffb7499887d2aaa Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 14 Feb 2023 15:35:59 -0500 Subject: [PATCH] Leverage Rust circuit sequence construction for OneQubitEulerDecomposer This commit is a follow-up to #9578 which added a rust implementation of the second half of the single qubit euler decomposition routines and leveraged them for the Optimize1qGatesDecomposition transpiler pass. With that PR the Optimize1qGatesDecomposition no longer was dependent on the OneQubitEulerDecomposer class. This commit continues from that PR and updates the OneQubitEulerDecomposer to leverage the same Rust implementation internally. Calling a decomposer object will internally call the rust function to generate a circuit sequence and then return either a QuantumCircuit or DAGCircuit (depending on the options). Similarly all the angle computation is done directly in Rust. --- .../synthesis/one_qubit_decompose.py | 369 +++--------------- src/euler_one_qubit_decomposer.rs | 5 +- 2 files changed, 52 insertions(+), 322 deletions(-) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 449b91fd0799..16abe99cc63e 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -13,9 +13,6 @@ """ Decompose a single-qubit unitary via Euler angles. """ -from dataclasses import dataclass -from typing import List - import numpy as np from qiskit._accelerate import euler_one_qubit_decomposer @@ -34,7 +31,6 @@ SXGate, XGate, ) -from qiskit.circuit.gate import Gate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_unitary_matrix @@ -55,6 +51,20 @@ "ZSX": ["rz", "sx"], } +NAME_MAP = { + "u": UGate, + "u1": U1Gate, + "u2": U2Gate, + "u3": U3Gate, + "p": PhaseGate, + "rx": RXGate, + "ry": RYGate, + "rz": RZGate, + "r": RGate, + "sx": SXGate, + "x": XGate, +} + class OneQubitEulerDecomposer: r"""A class for decomposing 1-qubit unitaries into Euler angle rotations. @@ -138,18 +148,31 @@ def __init__(self, basis="U3", use_dag=False): def build_circuit(self, gates, global_phase): """Return the circuit or dag object from a list of gates.""" qr = [Qubit()] + lookup_gate = False + if len(gates) > 0 and isinstance(gates[0], tuple): + lookup_gate = True + if self.use_dag: from qiskit.dagcircuit import dagcircuit dag = dagcircuit.DAGCircuit() dag.global_phase = global_phase dag.add_qubits(qr) - for gate in gates: + for gate_entry in gates: + if lookup_gate: + gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) + else: + gate = gate_entry + dag.apply_operation_back(gate, [qr[0]]) return dag else: circuit = QuantumCircuit(qr, global_phase=global_phase) - for gate in gates: + for gate_entry in gates: + if lookup_gate: + gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) + else: + gate = gate_entry circuit._append(gate, [qr[0]], []) return circuit @@ -187,8 +210,10 @@ def __call__(self, unitary, simplify=True, atol=DEFAULT_ATOL): return self._decompose(unitary, simplify=simplify, atol=atol) def _decompose(self, unitary, simplify=True, atol=DEFAULT_ATOL): - theta, phi, lam, phase = self._params(unitary) - circuit = self._circuit(theta, phi, lam, phase, simplify=simplify, atol=atol) + circuit_sequence = euler_one_qubit_decomposer.unitary_to_gate_sequence( + unitary, [self.basis], 0, None, simplify, atol + ) + circuit = self.build_circuit(circuit_sequence, circuit_sequence.global_phase) return circuit @property @@ -200,23 +225,23 @@ def basis(self): def basis(self, basis): """Set the decomposition basis.""" basis_methods = { - "U321": (self._params_u3, self._circuit_u321), - "U3": (self._params_u3, self._circuit_u3), - "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), - "ZXZ": (self._params_zxz, self._circuit_zxz), - "XYX": (self._params_xyx, self._circuit_xyx), - "XZX": (self._params_xzx, self._circuit_xzx), + "U321": self._params_u3, + "U3": self._params_u3, + "U": self._params_u3, + "PSX": self._params_u1x, + "ZSX": self._params_u1x, + "ZSXX": self._params_u1x, + "U1X": self._params_u1x, + "RR": self._params_zyz, + "ZYZ": self._params_zyz, + "ZXZ": self._params_zxz, + "XYX": self._params_xyx, + "XZX": self._params_xzx, } if basis not in basis_methods: raise QiskitError(f"OneQubitEulerDecomposer: unsupported basis {basis}") self._basis = basis - self._params, self._circuit = basis_methods[self._basis] + self._params = basis_methods[basis] def angles(self, unitary): """Return the Euler angles for input array. @@ -245,306 +270,8 @@ def angles_and_phase(self, unitary): _params_zxz = staticmethod(euler_one_qubit_decomposer.params_zxz) _params_xyx = staticmethod(euler_one_qubit_decomposer.params_xyx) _params_xzx = staticmethod(euler_one_qubit_decomposer.params_xzx) - - @staticmethod - def _params_u3(mat): - """Return the Euler angles and phase for the U3 basis.""" - # The determinant of U3 gate depends on its params - # via det(u3(theta, phi, lam)) = exp(1j*(phi+lam)) - # Since the phase is wrt to a SU matrix we must rescale - # phase to correct this - theta, phi, lam, phase = OneQubitEulerDecomposer._params_zyz(mat) - return theta, phi, lam, phase - 0.5 * (phi + lam) - - @staticmethod - def _params_u1x(mat): - """Return the Euler angles and phase for the U1X basis.""" - # The determinant of this decomposition depends on its params - # Since the phase is wrt to a SU matrix we must rescale - # phase to correct this - theta, phi, lam, phase = OneQubitEulerDecomposer._params_zyz(mat) - return theta, phi, lam, phase - 0.5 * (theta + phi + lam) - - def _circuit_kak( - self, - theta, - phi, - lam, - phase, - simplify=True, - atol=DEFAULT_ATOL, - allow_non_canonical=True, - k_gate=RZGate, - a_gate=RYGate, - ): - """ - Installs the angles phi, theta, and lam into a KAK-type decomposition of the form - K(phi) . A(theta) . K(lam) , where K and A are an orthogonal pair drawn from RZGate, RYGate, - and RXGate. - - Args: - theta (float): The middle KAK parameter. Expected to lie in [0, pi). - phi (float): The first KAK parameter. - lam (float): The final KAK parameter. - phase (float): The input global phase. - k_gate (Callable): The constructor for the K gate Instruction. - a_gate (Callable): The constructor for the A gate Instruction. - simplify (bool): Indicates whether gates should be elided / coalesced where possible. - allow_non_canonical (bool): Indicates whether we are permitted to reverse the sign of - the middle parameter, theta, in the output. When this and `simplify` are both - enabled, we take the opportunity to commute half-rotations in the outer gates past - the middle gate, which permits us to coalesce them at the cost of reversing the sign - of theta. - - Returns: - QuantumCircuit: The assembled circuit. - """ - gphase = phase - (phi + lam) / 2 - circuit = [] - if not simplify: - atol = -1.0 - # Early return for the middle-gate-free case - if abs(theta) < atol: - lam, phi = lam + phi, 0 - # NOTE: The following normalization is safe, because the gphase correction below - # fixes a particular diagonal entry to 1, which prevents any potential phase - # slippage coming from _mod_2pi injecting multiples of 2pi. - lam = _mod_2pi(lam, atol) - if abs(lam) > atol: - - circuit.append(k_gate(lam)) - gphase += lam / 2 - return self.build_circuit(circuit, gphase) - if abs(theta - np.pi) < atol: - gphase += phi - lam, phi = lam - phi, 0 - if allow_non_canonical and ( - abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol - ): - lam, theta, phi = lam + np.pi, -theta, phi + np.pi - lam = _mod_2pi(lam, atol) - if abs(lam) > atol: - gphase += lam / 2 - circuit.append(k_gate(lam)) - circuit.append(a_gate(theta)) - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - gphase += phi / 2 - circuit.append(k_gate(phi)) - return self.build_circuit(circuit, gphase) - - def _circuit_zyz( - self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True - ): - return self._circuit_kak( - theta, - phi, - lam, - phase, - simplify=simplify, - atol=atol, - allow_non_canonical=allow_non_canonical, - k_gate=RZGate, - a_gate=RYGate, - ) - - def _circuit_zxz( - self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True - ): - return self._circuit_kak( - theta, - phi, - lam, - phase, - simplify=simplify, - atol=atol, - allow_non_canonical=allow_non_canonical, - k_gate=RZGate, - a_gate=RXGate, - ) - - def _circuit_xzx( - self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True - ): - return self._circuit_kak( - theta, - phi, - lam, - phase, - simplify=simplify, - atol=atol, - allow_non_canonical=allow_non_canonical, - k_gate=RXGate, - a_gate=RZGate, - ) - - def _circuit_xyx( - self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True - ): - return self._circuit_kak( - theta, - phi, - lam, - phase, - simplify=simplify, - atol=atol, - allow_non_canonical=allow_non_canonical, - k_gate=RXGate, - a_gate=RYGate, - ) - - def _circuit_u3(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = [] - 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)) - return self.build_circuit(circuit, phase) - - def _circuit_u321(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = [] - if not simplify: - atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam, atol) - if abs(tot) > atol: - circuit.append(U1Gate(tot)) - elif abs(theta - np.pi / 2) < atol: - circuit.append(U2Gate(_mod_2pi(phi, atol), _mod_2pi(lam, atol))) - else: - circuit.append(U3Gate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol))) - return self.build_circuit(circuit, phase) - - def _circuit_u(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = [] - 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, phi, lam)) - return self.build_circuit(circuit, phase) - - def _circuit_psx_gen(self, theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): - """ - Generic X90, phase decomposition - - NOTE: `pfun` is responsible for eliding gates where appropriate (e.g., at angle value 0). - """ - circuit = _PSXGenCircuit([], phase) - # Early return for zero SX decomposition - if np.abs(theta) < atol: - pfun(circuit, lam + phi) - return self.build_circuit(circuit.circuit, circuit.phase) - # Early return for single SX decomposition - if abs(theta - np.pi / 2) < atol: - pfun(circuit, lam - np.pi / 2) - xfun(circuit) - pfun(circuit, phi + np.pi / 2) - return self.build_circuit(circuit.circuit, circuit.phase) - # General double SX decomposition - if abs(theta - np.pi) < atol: - circuit.phase += lam - phi, lam = phi - lam, 0 - if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi)) < atol: - lam, theta, phi = lam + np.pi, -theta, phi + np.pi - circuit.phase -= theta - # Shift theta and phi to turn the decomposition from - # RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam) - # into RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) . - theta, phi = theta + np.pi, phi + np.pi - circuit.phase -= np.pi / 2 - # Emit circuit - pfun(circuit, lam) - if xpifun and abs(_mod_2pi(theta)) < atol: - xpifun(circuit) - else: - xfun(circuit) - pfun(circuit, theta) - xfun(circuit) - pfun(circuit, phi) - - return self.build_circuit(circuit.circuit, circuit.phase) - - def _circuit_psx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - if not simplify: - atol = -1.0 - - def fnz(circuit, phi): - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - circuit.circuit.append(PhaseGate(phi)) - - def fnx(circuit): - circuit.circuit.append(SXGate()) - - return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) - - def _circuit_zsx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - if not simplify: - atol = -1.0 - - def fnz(circuit, phi): - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - circuit.circuit.append(RZGate(phi)) - circuit.phase += phi / 2 - - def fnx(circuit): - circuit.circuit.append(SXGate()) - - return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) - - def _circuit_u1x(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - if not simplify: - atol = -1.0 - - def fnz(circuit, phi): - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - circuit.circuit.append(U1Gate(phi)) - - def fnx(circuit): - circuit.phase += np.pi / 4 - circuit.circuit.append(RXGate(np.pi / 2)) - - return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx) - - def _circuit_zsxx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - if not simplify: - atol = -1.0 - - def fnz(circuit, phi): - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - circuit.circuit.append(RZGate(phi)) - circuit.phase += phi / 2 - - def fnx(circuit): - circuit.circuit.append(SXGate()) - - def fnxpi(circuit): - circuit.circuit.append(XGate()) - - return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx, fnxpi) - - def _circuit_rr(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - circuit = [] - if not simplify: - atol = -1.0 - if abs(theta) < atol and abs(phi) < atol and abs(lam) < atol: - return self.build_circuit(circuit, phase) - if abs(theta - np.pi) > atol: - circuit.append(RGate(theta - np.pi, _mod_2pi(np.pi / 2 - lam, atol))) - circuit.append(RGate(np.pi, _mod_2pi(0.5 * (phi - lam + np.pi), atol))) - return self.build_circuit(circuit, phase) - - -@dataclass -class _PSXGenCircuit: - __slots__ = ("circuit", "phase") - circuit: List[Gate] - phase: float + _params_u3 = staticmethod(euler_one_qubit_decomposer.params_u3) + _params_u1x = staticmethod(euler_one_qubit_decomposer.params_u1x) def _mod_2pi(angle: float, atol: float = 0): diff --git a/src/euler_one_qubit_decomposer.rs b/src/euler_one_qubit_decomposer.rs index 2aec4d7b3428..8e272d75e4af 100644 --- a/src/euler_one_qubit_decomposer.rs +++ b/src/euler_one_qubit_decomposer.rs @@ -608,11 +608,14 @@ pub fn compute_error_list( } #[pyfunction] +#[pyo3(signature = (unitary, target_basis_list, qubit, error_map=None, simplify=true, atol=None))] pub fn unitary_to_gate_sequence( unitary: PyReadonlyArray2, target_basis_list: Vec<&str>, qubit: usize, error_map: Option<&OneQubitGateErrorMap>, + simplify: bool, + atol: Option, ) -> PyResult> { const VALID_BASES: [&str; 12] = [ "U321", "U3", "U", "PSX", "ZSX", "ZSXX", "U1X", "RR", "ZYZ", "ZXZ", "XYX", "XZX", @@ -629,7 +632,7 @@ pub fn unitary_to_gate_sequence( .iter() .map(|target_basis| { let [theta, phi, lam, phase] = angles_from_unitary(unitary_mat, target_basis); - generate_circuit(target_basis, theta, phi, lam, phase, true, None).unwrap() + generate_circuit(target_basis, theta, phi, lam, phase, simplify, atol).unwrap() }) .min_by(|a, b| { let error_a = compare_error_fn(a, &error_map, qubit);