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 832862bc222a..9c573e1af8e9 100644 --- a/src/euler_one_qubit_decomposer.rs +++ b/src/euler_one_qubit_decomposer.rs @@ -612,11 +612,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> { for basis in &target_basis_list { if !VALID_BASIS.contains(basis) { @@ -630,7 +633,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);