Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
89ca81e
TwoQubitControlledUDecomposer to _decomposer_2q_from_basis_gates
ShellyGarion Dec 16, 2024
077ecb9
update (temporarily) basis gates in test
ShellyGarion Dec 16, 2024
18b20f6
minor fix
ShellyGarion Dec 18, 2024
967da13
add EulerBasis as a parameter to TwoQubitControlledUDecomposer
ShellyGarion Dec 19, 2024
3bb10fd
fix global_phase calculation in TwoQubitContolledUDecomposer
ShellyGarion Dec 22, 2024
14c7aac
add TwoQubitControlledUDecomposer to the docs
ShellyGarion Dec 22, 2024
eeff4cd
make the choice of kak_gate deterministic
ShellyGarion Dec 22, 2024
043a795
Merge branch 'main' into unitary_synth
ShellyGarion Jan 13, 2025
b94070a
remove XXDecomposer from _decomposer_2q_from_basis_gates
ShellyGarion Jan 13, 2025
0a1644b
make call_inner pub, add Clone, Debug
ShellyGarion Jan 15, 2025
48fec9b
add TwoQubitControlledUDecomposer to unitary_synthesis.rs
ShellyGarion Jan 15, 2025
009d87e
merge main branch, fix conflict
ShellyGarion Jan 15, 2025
85e2962
Fix exit condition for GOODBYE_SET and PARAM_SET
ElePT Jan 16, 2025
c5d0c97
fix conflict with main branch
ShellyGarion Jan 16, 2025
d7b84e9
make DEFAULT_ATOL public
ShellyGarion Jan 16, 2025
ea9a2d0
add TwoQubitControlledUDecomposer to synth_su4_sequence
ShellyGarion Jan 16, 2025
132a44f
Add support for parametrized decomposer gate in apply_synth_sequence
ElePT Jan 20, 2025
b1cb5a0
change DecomposerType enum to fix clippy error
ShellyGarion Jan 20, 2025
9cdf2da
add a random unitary test to test_parametrized_basis_gate_in_target
ShellyGarion Jan 20, 2025
1be749d
add public new_inner for TwoQubitControlledUDecomposer
ShellyGarion Jan 21, 2025
1495781
replace default 'ZYZ' by 'ZXZ' in TwoQubitControlledUDecomposer
ShellyGarion Jan 21, 2025
231af7f
remove using py in rust functions
ShellyGarion Jan 23, 2025
bb874c1
minor update to test
ShellyGarion Jan 23, 2025
c2a9ad4
make atol optional
ShellyGarion Jan 23, 2025
d39c005
fix conflict with main branch
ShellyGarion Jan 23, 2025
106ae4a
add a test with fractional gates in the backend
ShellyGarion Jan 23, 2025
9131e3d
add release notes
ShellyGarion Jan 23, 2025
6a82649
enhance tests following review
ShellyGarion Jan 26, 2025
23b9e6b
Add support for non-standard parametrized gates, add new tests. TODO:…
ElePT Jan 29, 2025
5bb55b5
decompose S, Sdg, H into euler_basis
ShellyGarion Jan 30, 2025
f4da2da
update test
ShellyGarion Jan 30, 2025
1bd71c5
Overwrite Python-side gate parameters as well as Rust-side parameters.
ElePT Feb 3, 2025
dd1b38d
fix conflict with mai branchn
ShellyGarion Feb 10, 2025
55556f8
add examples to release notes
ShellyGarion Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 51 additions & 43 deletions crates/accelerate/src/two_qubit_decompose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2448,23 +2448,25 @@ pub enum RXXEquivalent {
}

impl RXXEquivalent {
fn matrix(&self, py: Python, param: f64) -> PyResult<Array2<Complex64>> {
fn matrix(&self, param: f64) -> PyResult<Array2<Complex64>> {
match self {
Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()),
Self::CustomPython(gate_cls) => {
Self::CustomPython(gate_cls) => Python::with_gil(|py: Python| {
let gate_obj = gate_cls.bind(py).call1((param,))?;
let raw_matrix = gate_obj
.call_method0(intern!(py, "to_matrix"))?
.extract::<PyReadonlyArray2<Complex64>>()?;
Ok(raw_matrix.as_array().to_owned())
}
}),
}
}
}

#[derive(Clone, Debug)]
#[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)]
pub struct TwoQubitControlledUDecomposer {
rxx_equivalent_gate: RXXEquivalent,
euler_basis: EulerBasis,
#[pyo3(get)]
scale: f64,
}
Expand All @@ -2479,7 +2481,6 @@ impl TwoQubitControlledUDecomposer {
/// invert 2q gate sequence
fn invert_2q_gate(
&self,
py: Python,
gate: (Option<StandardGate>, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>),
) -> PyResult<InverseReturn> {
let (gate, params, qubits) = gate;
Expand Down Expand Up @@ -2516,7 +2517,7 @@ impl TwoQubitControlledUDecomposer {
.collect::<SmallVec<_>>();
Ok((Some(inv_gate.0), inv_gate_params, qubits))
}
RXXEquivalent::CustomPython(gate_cls) => {
RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this changes since longer term we'll need to do this when running this in a parallel context. But I'm curious what prompted these changes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the discussion here: #13568 (comment)
I wanted to have a purely-rust new_inner function, so I copied the solution from your PR :)

let gate_obj = gate_cls.bind(py).call1(PyTuple::new(py, params)?)?;
let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?;
let inverse: OperationFromPython = raw_inverse.extract()?;
Expand All @@ -2537,7 +2538,7 @@ impl TwoQubitControlledUDecomposer {
"rxx gate inverse is not valid for this decomposer",
))
}
}
}),
}
}
}
Expand All @@ -2550,20 +2551,19 @@ impl TwoQubitControlledUDecomposer {
/// Circuit: Circuit equivalent to an RXXGate.
/// Raises:
/// QiskitError: If the circuit is not equivalent to an RXXGate.
fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult<TwoQubitGateSequence> {
fn to_rxx_gate(&self, angle: f64) -> PyResult<TwoQubitGateSequence> {
// The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate
// but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl
// parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e.
// :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters
// (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate.

let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?;
let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?;
let decomposer_inv =
TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?;

let euler_basis = EulerBasis::ZYZ;
let mut target_1q_basis_list = EulerBasisSet::new();
target_1q_basis_list.add_basis(euler_basis);
target_1q_basis_list.add_basis(self.euler_basis);

// Express the RXXGate in terms of the user-provided RXXGate equivalent gate.
let mut gates = Vec::with_capacity(13);
Expand Down Expand Up @@ -2600,14 +2600,14 @@ impl TwoQubitControlledUDecomposer {
gates.push((None, smallvec![self.scale * angle], smallvec![0, 1]));

if let Some(unitary_k1r) = unitary_k1r {
global_phase += unitary_k1r.global_phase;
global_phase -= unitary_k1r.global_phase;
for gate in unitary_k1r.gates.into_iter().rev() {
let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate);
gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0]));
}
}
if let Some(unitary_k1l) = unitary_k1l {
global_phase += unitary_k1l.global_phase;
global_phase -= unitary_k1l.global_phase;
Comment thread
ElePT marked this conversation as resolved.
for gate in unitary_k1l.gates.into_iter().rev() {
let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate);
gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1]));
Expand All @@ -2623,18 +2623,17 @@ impl TwoQubitControlledUDecomposer {
/// Appends U_d(a, b, c) to the circuit.
fn weyl_gate(
&self,
py: Python,
circ: &mut TwoQubitGateSequence,
target_decomposed: TwoQubitWeylDecomposition,
atol: f64,
) -> PyResult<()> {
let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?;
let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?;
circ.gates.extend(circ_a.gates);
let mut global_phase = circ_a.global_phase;

// translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate.
if (target_decomposed.b).abs() > atol {
let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?;
let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?;
global_phase += circ_b.global_phase;
circ.gates
.push((Some(StandardGate::SdgGate), smallvec![], smallvec![0]));
Expand All @@ -2656,7 +2655,7 @@ impl TwoQubitControlledUDecomposer {
// circuit if c < 0.
let mut gamma = -2.0 * target_decomposed.c;
if gamma <= 0.0 {
let circ_c = self.to_rxx_gate(py, gamma)?;
let circ_c = self.to_rxx_gate(gamma)?;
global_phase += circ_c.global_phase;
circ.gates
.push((Some(StandardGate::HGate), smallvec![], smallvec![0]));
Expand All @@ -2670,15 +2669,15 @@ impl TwoQubitControlledUDecomposer {
} else {
// invert the circuit above
gamma *= -1.0;
let circ_c = self.to_rxx_gate(py, gamma)?;
let circ_c = self.to_rxx_gate(gamma)?;
global_phase -= circ_c.global_phase;
circ.gates
.push((Some(StandardGate::HGate), smallvec![], smallvec![0]));
circ.gates
.push((Some(StandardGate::HGate), smallvec![], smallvec![1]));
for gate in circ_c.gates.into_iter().rev() {
let (inv_gate_name, inv_gate_params, inv_gate_qubits) =
self.invert_2q_gate(py, gate)?;
self.invert_2q_gate(gate)?;
circ.gates
.push((inv_gate_name, inv_gate_params, inv_gate_qubits));
}
Expand All @@ -2695,18 +2694,16 @@ impl TwoQubitControlledUDecomposer {

/// Returns the Weyl decomposition in circuit form.
/// Note: atol is passed to OneQubitEulerDecomposer.
fn call_inner(
pub fn call_inner(
&self,
py: Python,
unitary: ArrayView2<Complex64>,
atol: f64,
atol: Option<f64>,
) -> PyResult<TwoQubitGateSequence> {
let target_decomposed =
TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?;

let euler_basis = EulerBasis::ZYZ;
let mut target_1q_basis_list = EulerBasisSet::new();
target_1q_basis_list.add_basis(euler_basis);
target_1q_basis_list.add_basis(self.euler_basis);

let c1r = target_decomposed.K1r.view();
let c2r = target_decomposed.K2r.view();
Expand Down Expand Up @@ -2741,17 +2738,17 @@ impl TwoQubitControlledUDecomposer {
gates,
global_phase,
};
self.weyl_gate(py, &mut gates1, target_decomposed, atol)?;
self.weyl_gate(&mut gates1, target_decomposed, atol.unwrap_or(DEFAULT_ATOL))?;
global_phase += gates1.global_phase;

if let Some(unitary_c1r) = unitary_c1r {
global_phase -= unitary_c1r.global_phase;
global_phase += unitary_c1r.global_phase;
for gate in unitary_c1r.gates.into_iter() {
gates1.gates.push((Some(gate.0), gate.1, smallvec![0]));
}
}
if let Some(unitary_c1l) = unitary_c1l {
global_phase -= unitary_c1l.global_phase;
global_phase += unitary_c1l.global_phase;
Comment thread
ElePT marked this conversation as resolved.
for gate in unitary_c1l.gates.into_iter() {
gates1.gates.push((Some(gate.0), gate.1, smallvec![1]));
}
Expand All @@ -2760,19 +2757,9 @@ impl TwoQubitControlledUDecomposer {
gates1.global_phase = global_phase;
Ok(gates1)
}
}

#[pymethods]
impl TwoQubitControlledUDecomposer {
/// Initialize the KAK decomposition.
/// Args:
/// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`:
/// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate.
/// Raises:
/// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`.
#[new]
#[pyo3(signature=(rxx_equivalent_gate))]
pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult<Self> {
/// Initialize the KAK decomposition.
pub fn new_inner(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult<Self> {
Comment thread
ElePT marked this conversation as resolved.
let atol = DEFAULT_ATOL;
Comment on lines +2747 to 2824
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we discussed the addition of a new_inner class, but the current implementation doesn't seem to add (or remove) anything to the existing new class, so I would just keep and use new in the rust code.

Copy link
Copy Markdown
Member Author

@ShellyGarion ShellyGarion Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new function is pyo3 method -- isn't the new_inner function faster? if not, I can change it back.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to remove the need of py in 231af7f
by replacing it with gil, as suggested by Matthew in #13419.
I still think we need both functions new and new_inner since one of them is in pure rust and the other is pyo3.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's keep them.

let test_angles = [0.2, 0.3, PI2];

Expand All @@ -2788,14 +2775,17 @@ impl TwoQubitControlledUDecomposer {
}
}
RXXEquivalent::CustomPython(gate_cls) => {
if gate_cls.bind(py).call1((test_angle,)).ok().is_none() {
let takes_param = Python::with_gil(|py: Python| {
gate_cls.bind(py).call1((test_angle,)).ok().is_none()
});
if takes_param {
return Err(QiskitError::new_err(
"Equivalent gate needs to take exactly 1 angle parameter.",
));
}
}
};
let mat = rxx_equivalent_gate.matrix(py, test_angle)?;
let mat = rxx_equivalent_gate.matrix(test_angle)?;
let decomp =
TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?;
let mat_rxx = StandardGate::RXXGate
Expand Down Expand Up @@ -2836,17 +2826,35 @@ impl TwoQubitControlledUDecomposer {
Ok(TwoQubitControlledUDecomposer {
scale,
rxx_equivalent_gate,
euler_basis: EulerBasis::__new__(euler_basis)?,
})
}
}

#[pymethods]
impl TwoQubitControlledUDecomposer {
/// Initialize the KAK decomposition.
/// Args:
/// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`:
/// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate.
/// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer`
/// for 1Q synthesis.
/// Raises:
/// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`.
#[new]
#[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))]
pub fn new(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult<Self> {
TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis)
}

#[pyo3(signature=(unitary, atol))]
#[pyo3(signature=(unitary, atol=None))]
fn __call__(
&self,
py: Python,
unitary: PyReadonlyArray2<Complex64>,
atol: f64,
atol: Option<f64>,
) -> PyResult<CircuitData> {
let sequence = self.call_inner(py, unitary.as_array(), atol)?;
let sequence = self.call_inner(unitary.as_array(), atol)?;
match &self.rxx_equivalent_gate {
RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates(
py,
Expand Down
Loading