diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 2aec4d7b342..995d0e4810c 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -155,7 +155,7 @@ fn circuit_kak( // 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); + lam = mod_2pi(lam, atol); if lam.abs() > atol { circuit.push((String::from(k_gate), vec![lam])); global_phase += lam / 2.; @@ -170,18 +170,18 @@ fn circuit_kak( lam -= phi; phi = 0.; } - if mod_2pi(lam + PI).abs() < atol || mod_2pi(phi + PI).abs() < atol { + if mod_2pi(lam + PI, atol).abs() < atol || mod_2pi(phi + PI, atol).abs() < atol { lam += PI; theta = -theta; phi += PI; } - lam = mod_2pi(lam); + lam = mod_2pi(lam, atol); if lam.abs() > atol { global_phase += lam / 2.; circuit.push((String::from(k_gate), vec![lam])); } circuit.push((String::from(a_gate), vec![theta])); - phi = mod_2pi(phi); + phi = mod_2pi(phi, atol); if phi.abs() > atol { global_phase += phi / 2.; circuit.push((String::from(k_gate), vec![phi])); @@ -201,12 +201,12 @@ fn circuit_u3( atol: Option, ) -> OneQubitGateSequence { let mut circuit = Vec::new(); - let phi = mod_2pi(phi); - let lam = mod_2pi(lam); let atol = match atol { Some(atol) => atol, None => DEFAULT_ATOL, }; + let phi = mod_2pi(phi, atol); + let lam = mod_2pi(lam, atol); if !simplify || theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { circuit.push((String::from("u3"), vec![theta, phi, lam])); } @@ -233,14 +233,20 @@ fn circuit_u321( atol = -1.0; } if theta.abs() < atol { - let tot = mod_2pi(phi + lam); + let tot = mod_2pi(phi + lam, atol); if tot.abs() > atol { circuit.push((String::from("u1"), vec![tot])); } } else if (theta - PI / 2.).abs() < atol { - circuit.push((String::from("u2"), vec![mod_2pi(phi), mod_2pi(lam)])); + circuit.push(( + String::from("u2"), + vec![mod_2pi(phi, atol), mod_2pi(lam, atol)], + )); } else { - circuit.push((String::from("u3"), vec![theta, mod_2pi(phi), mod_2pi(lam)])); + circuit.push(( + String::from("u3"), + vec![theta, mod_2pi(phi, atol), mod_2pi(lam, atol)], + )); } OneQubitGateSequence { gates: circuit, @@ -264,8 +270,8 @@ fn circuit_u( if !simplify { atol = -1.0; } - let phi = mod_2pi(phi); - let lam = mod_2pi(lam); + let phi = mod_2pi(phi, atol); + let lam = mod_2pi(lam, atol); if theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { circuit.push((String::from("u"), vec![theta, phi, lam])); } @@ -323,7 +329,7 @@ where phi -= lam; lam = 0.; } - if mod_2pi(lam + PI).abs() < atol || mod_2pi(phi).abs() < atol { + if mod_2pi(lam + PI, atol).abs() < atol || mod_2pi(phi, atol).abs() < atol { lam += PI; theta = -theta; phi += PI; @@ -338,7 +344,7 @@ where // emit circuit pfun(&mut circuit, lam); match xpifun { - Some(xpifun) if mod_2pi(theta).abs() < atol => xpifun(&mut circuit), + Some(xpifun) if mod_2pi(theta, atol).abs() < atol => xpifun(&mut circuit), _ => { xfun(&mut circuit); pfun(&mut circuit, theta); @@ -372,9 +378,15 @@ fn circuit_rr( }; } if (theta - PI).abs() > atol { - circuit.push((String::from("r"), vec![theta - PI, mod_2pi(PI / 2. - lam)])); - } - circuit.push((String::from("r"), vec![PI, mod_2pi(0.5 * (phi - lam + PI))])); + circuit.push(( + String::from("r"), + vec![theta - PI, mod_2pi(PI / 2. - lam, atol)], + )); + } + circuit.push(( + String::from("r"), + vec![PI, mod_2pi(0.5 * (phi - lam + PI), atol)], + )); OneQubitGateSequence { gates: circuit, global_phase: phase, @@ -408,7 +420,7 @@ pub fn generate_circuit( inner_atol = -1.0; } let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { - let phi = mod_2pi(phi); + let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { circuit.gates.push((String::from("p"), vec![phi])); } @@ -438,7 +450,7 @@ pub fn generate_circuit( inner_atol = -1.0; } let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { - let phi = mod_2pi(phi); + let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { circuit.gates.push((String::from("rz"), vec![phi])); circuit.global_phase += phi / 2.; @@ -468,7 +480,7 @@ pub fn generate_circuit( inner_atol = -1.0; } let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { - let phi = mod_2pi(phi); + let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { circuit.gates.push((String::from("u1"), vec![phi])); } @@ -498,7 +510,7 @@ pub fn generate_circuit( inner_atol = -1.0; } let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { - let phi = mod_2pi(phi); + let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { circuit.gates.push((String::from("rz"), vec![phi])); circuit.global_phase += phi / 2.; @@ -608,11 +620,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 +644,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); @@ -649,12 +664,18 @@ fn complex_phase(x: Complex64) -> f64 { x.im.atan2(x.re) } +/// Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π #[inline] -fn mod_2pi(angle: f64) -> f64 { +fn mod_2pi(angle: f64, atol: f64) -> f64 { // f64::rem_euclid() isn't exactly the same as Python's % operator, but because // the RHS here is a constant and positive it is effectively equivalent for // this case - (angle + PI).rem_euclid(2. * PI) - PI + let wrapped = (angle + PI).rem_euclid(2. * PI) - PI; + if (wrapped - PI).abs() < atol { + -PI + } else { + wrapped + } } fn params_zyz_inner(mat: ArrayView2) -> [f64; 4] { @@ -722,8 +743,8 @@ fn params_xyx_inner(mat: ArrayView2) -> [f64; 4] { ], ]); let [theta, phi, lam, phase] = params_zyz_inner(mat_zyz.view()); - let new_phi = mod_2pi(phi + PI); - let new_lam = mod_2pi(lam + PI); + let new_phi = mod_2pi(phi + PI, 0.); + let new_lam = mod_2pi(lam + PI, 0.); [ theta, new_phi, diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 449b91fd079..cffa0b05682 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,311 +270,5 @@ 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 - - -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 + _params_u3 = staticmethod(euler_one_qubit_decomposer.params_u3) + _params_u1x = staticmethod(euler_one_qubit_decomposer.params_u1x)