Leverage Rust circuit sequence construction for `OneQubitEulerDecomposer` (#9583)

* 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.

* Add missing atol clamping to mod_2pi

The python version of the OneQubitEulerDecomposer class had atol
clamping on it's output from mod_2pi, when this function was ported to
rust this was not included. At the time it was because nothing set the
atol parameter when calling mod_2pi (the angle calculation in #9185 did
not have atol and the expansion to construct circuits for
Optimize1qGatesDecomposition always used the default value). However,
now that we're expanding OneQubitEulerDecomposer to internally do all
the calculations in rust we need to support an adjustable atol which
includes the missing endpoint clamping in mod_2pi. This commit adds this
missing functionality to the function.

* Add docstring to mod_2pi rust function

* Remove mod_2pi python function
This commit is contained in:
Matthew Treinish 2023-03-16 14:44:44 -04:00 committed by GitHub
parent 0d15506d44
commit bbd023b5e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 353 deletions

View File

@ -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<f64>,
) -> 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![theta - PI, mod_2pi(PI / 2. - lam, atol)],
));
}
circuit.push((String::from("r"), vec![PI, mod_2pi(0.5 * (phi - lam + PI))]));
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<Complex64>,
target_basis_list: Vec<&str>,
qubit: usize,
error_map: Option<&OneQubitGateErrorMap>,
simplify: bool,
atol: Option<f64>,
) -> PyResult<Option<OneQubitGateSequence>> {
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<Complex64>) -> [f64; 4] {
@ -722,8 +743,8 @@ fn params_xyx_inner(mat: ArrayView2<Complex64>) -> [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,

View File

@ -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 @@ from qiskit.circuit.library.standard_gates import (
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 @@ ONE_QUBIT_EULER_BASIS_GATES = {
"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 @@ class OneQubitEulerDecomposer:
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 @@ class OneQubitEulerDecomposer:
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 @@ class OneQubitEulerDecomposer:
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 @@ class OneQubitEulerDecomposer:
_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)