Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 2q fractional gates to the UnitarySynthesis tranpiler pass #13568

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 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
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
31 changes: 19 additions & 12 deletions crates/accelerate/src/two_qubit_decompose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2445,14 +2445,16 @@ impl RXXEquivalent {
}
}

#[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,
}

const DEFAULT_ATOL: f64 = 1e-12;
pub const DEFAULT_ATOL: f64 = 1e-12;
ShellyGarion marked this conversation as resolved.
Show resolved Hide resolved
type InverseReturn = (Option<StandardGate>, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>);

/// Decompose two-qubit unitary in terms of a desired
Expand Down Expand Up @@ -2544,9 +2546,8 @@ impl TwoQubitControlledUDecomposer {
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 @@ -2583,14 +2584,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;
ElePT marked this conversation as resolved.
Show resolved Hide 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 Down Expand Up @@ -2678,7 +2679,7 @@ 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>,
Expand All @@ -2687,9 +2688,8 @@ impl TwoQubitControlledUDecomposer {
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 @@ -2728,13 +2728,13 @@ impl TwoQubitControlledUDecomposer {
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;
ElePT marked this conversation as resolved.
Show resolved Hide resolved
for gate in unitary_c1l.gates.into_iter() {
gates1.gates.push((Some(gate.0), gate.1, smallvec![1]));
}
Expand All @@ -2751,11 +2751,17 @@ impl TwoQubitControlledUDecomposer {
/// 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))]
pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult<Self> {
#[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZYZ"))]
Copy link
Member Author

@ShellyGarion ShellyGarion Jan 20, 2025

Choose a reason for hiding this comment

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

should we replace the default value here to "ZXZ"? If so, we should better do this in other places of this file.

Copy link
Member Author

Choose a reason for hiding this comment

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

I replaced the default to "ZXZ". Should we do it in other classes as well?

pub fn new(
py: Python,
rxx_equivalent_gate: RXXEquivalent,
euler_basis: &str,
) -> PyResult<Self> {
let atol = DEFAULT_ATOL;
let test_angles = [0.2, 0.3, PI2];

Expand Down Expand Up @@ -2819,6 +2825,7 @@ impl TwoQubitControlledUDecomposer {
Ok(TwoQubitControlledUDecomposer {
scale,
rxx_equivalent_gate,
euler_basis: EulerBasis::__new__(euler_basis)?,
})
}

Expand Down
152 changes: 124 additions & 28 deletions crates/accelerate/src/unitary_synthesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ use crate::euler_one_qubit_decomposer::{
use crate::nlayout::PhysicalQubit;
use crate::target_transpiler::{NormalOperation, Target};
use crate::two_qubit_decompose::{
TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition,
RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence,
TwoQubitWeylDecomposition, DEFAULT_ATOL,
};
use crate::QiskitError;

Expand All @@ -54,6 +55,7 @@ const PI4: f64 = PI / 4.;
#[derive(Clone, Debug)]
enum DecomposerType {
TwoQubitBasisDecomposer(Box<TwoQubitBasisDecomposer>),
TwoQubitControlledUDecomposer(Box<TwoQubitControlledUDecomposer>),
XXDecomposer(PyObject),
}

Expand All @@ -72,6 +74,7 @@ struct TwoQubitUnitarySequence {
// then we know TwoQubitBasisDecomposer is an ideal decomposition and there is
// no need to bother trying the XXDecomposer.
static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"];
static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"];

fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet {
let mut target_basis_set: EulerBasisSet = EulerBasisSet::new();
Expand Down Expand Up @@ -410,6 +413,17 @@ fn run_2q_unitary_synthesis(
match decomposer_item.decomposer {
DecomposerType::TwoQubitBasisDecomposer(_) => {
let synth = synth_su4_sequence(
py,
&unitary,
decomposer_item,
preferred_dir,
approximation_degree,
)?;
apply_synth_sequence(py, out_dag, out_qargs, &synth)?;
}
DecomposerType::TwoQubitControlledUDecomposer(_) => {
let synth = synth_su4_sequence(
py,
&unitary,
decomposer_item,
preferred_dir,
Expand Down Expand Up @@ -443,8 +457,50 @@ fn run_2q_unitary_synthesis(
)?;
match &decomposer.decomposer {
DecomposerType::TwoQubitBasisDecomposer(_) => {
let sequence =
synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?;
let sequence = synth_su4_sequence(
py,
&unitary,
decomposer,
preferred_dir,
approximation_degree,
)?;
let scoring_info =
sequence
.gate_sequence
.gates()
.iter()
.map(|(gate, params, qubit_ids)| {
let inst_qubits =
qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect();
match gate {
Some(gate) => (
gate.name().to_string(),
Some(params.iter().map(|p| Param::Float(*p)).collect()),
inst_qubits,
),
None => (
sequence
.decomp_gate
.operation
.standard_gate()
.name()
.to_string(),
Some(params.iter().map(|p| Param::Float(*p)).collect()),
inst_qubits,
),
}
});
let synth_error_from_target = synth_error(py, scoring_info, target);
synth_errors_sequence.push((sequence, synth_error_from_target));
}
DecomposerType::TwoQubitControlledUDecomposer(_) => {
let sequence = synth_su4_sequence(
py,
&unitary,
decomposer,
preferred_dir,
approximation_degree,
)?;
let scoring_info =
sequence
.gate_sequence
Expand Down Expand Up @@ -543,6 +599,8 @@ fn get_2q_decomposers_from_target(
let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect();
let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new();
let mut available_2q_props: IndexMap<&str, (Option<f64>, Option<f64>)> = IndexMap::new();
let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new();
let mut available_2q_param_props: IndexMap<&str, (Option<f64>, Option<f64>)> = IndexMap::new();

let mut qubit_gate_map = IndexMap::new();

Expand All @@ -565,28 +623,15 @@ fn get_2q_decomposers_from_target(
}

#[inline]
fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation {
fn check_parametrized_gate(op: NormalOperation) -> bool {
if let Some(std_gate) = op.operation.try_standard_gate() {
match std_gate.name() {
"rxx" => {
if let Param::ParameterExpression(_) = op.params[0] {
op.params[0] = Param::Float(PI2)
}
}
"rzx" => {
if let Param::ParameterExpression(_) = op.params[0] {
op.params[0] = Param::Float(PI4)
}
if PARAM_SET.contains(&std_gate.name()) {
if let Param::ParameterExpression(_) = op.params[0] {
return true;
Comment on lines +625 to +627
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we might be missing out a group of gates here. As far as I understand it, the TwoQubitControlledUDecomposer works not only for standard gates (the ones defined in PARAM_SET), but should also be at least a candidate for user-defined parametrized unitary gates that match the input conditions. In that case, we should probably not check PARAM_SET.contains(&std_gate.name()), and instead consider any 2q unitary parametrized gate for the "available_2q_param_basis".

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that not any parametrized gate can fit here, only certain parametrized gates which are equivalent (up to single qubit gates) to an RXXGate. The TwoQubitControlledUDecomposer works for such gates if the user was able to calibrate them somehow. Should the UnitarySynthesis transpiler pass fit any calibrated gates?
So I'm not sure what exactly should be checked here. See the function TwoQubitControlledUDecomposer.to_rxx_gate

Copy link
Contributor

Choose a reason for hiding this comment

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

We currently support non-standard gates too, so the pass should work if there is a non-standard gate that meets the condition of rxx equivalence. TwoQubitControlledUDecomposer::new already checks if the gate is rxx equivalent:

let test_angles = [0.2, 0.3, PI2];
let scales: PyResult<Vec<f64>> = test_angles
.into_iter()
.map(|test_angle| {
match &rxx_equivalent_gate {
RXXEquivalent::Standard(gate) => {
if gate.num_params() != 1 {
return Err(QiskitError::new_err(
"Equivalent gate needs to take exactly 1 angle parameter.",
));
}
}
RXXEquivalent::CustomPython(gate_cls) => {
if gate_cls.bind(py).call1((test_angle,)).ok().is_none() {
return Err(QiskitError::new_err(
"Equivalent gate needs to take exactly 1 angle parameter.",
));
}
}
};
let mat = rxx_equivalent_gate.matrix(py, test_angle)?;
let decomp =
TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?;
let mat_rxx = StandardGate::RXXGate
.matrix(&[Param::Float(test_angle)])
.unwrap();
let decomposer_rxx = TwoQubitWeylDecomposition::new_inner(
mat_rxx.view(),
None,
Some(Specialization::ControlledEquiv),
)?;
let decomposer_equiv = TwoQubitWeylDecomposition::new_inner(
mat.view(),
Some(DEFAULT_FIDELITY),
Some(Specialization::ControlledEquiv),
)?;
let scale_a = decomposer_rxx.a / decomposer_equiv.a;
if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol {
return Err(QiskitError::new_err(
"The provided gate is not equivalent to an RXXGate.",
));
}
Ok(scale_a)
})
.collect();

So I was thinking that we could abstract that logic and add a check to UnitarySynthesis similar to is_controlled or is_supercontrolled, but in this case it could be called is_rxx_equivalent(for example). The idea for the check is to filter out the non-rxx-equivalent gates.

}
"rzz" => {
if let Param::ParameterExpression(_) = op.params[0] {
op.params[0] = Param::Float(PI2)
}
}
_ => (),
}
}
op
false
}

for (q_pair, gates) in qubit_gate_map {
Expand All @@ -602,8 +647,21 @@ fn get_2q_decomposers_from_target(
if op.operation.num_qubits() != 2 {
continue;
}
available_2q_basis.insert(key, replace_parametrized_gate(op.clone()));

if check_parametrized_gate(op.clone()) {
available_2q_param_basis.insert(key, op.clone());
if target.contains_key(key) {
available_2q_param_props.insert(
key,
match &target[key].get(Some(q_pair)) {
Some(Some(props)) => (props.duration, props.error),
_ => (None, None),
},
);
} else {
continue;
}
}
available_2q_basis.insert(key, op.clone());
if target.contains_key(key) {
available_2q_props.insert(
key,
Expand All @@ -620,7 +678,8 @@ fn get_2q_decomposers_from_target(
}
}
}
if available_2q_basis.is_empty() {

if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() {
return Err(QiskitError::new_err(
"Target has no gates available on qubits to synthesize over.",
));
Expand Down Expand Up @@ -655,7 +714,6 @@ fn get_2q_decomposers_from_target(
}
}

// Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers
let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis
.iter()
.filter(|(_, v)| is_supercontrolled(v))
Expand Down Expand Up @@ -687,19 +745,48 @@ fn get_2q_decomposers_from_target(
}

// If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer
// is an ideal decomposition and there is no need to bother calculating the XX embodiments
// or try the XX decomposer
// is an ideal decomposition and there is no need to try other decomposers
let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect();

#[inline]
fn check_goodbye(basis_set: &IndexSet<&str>) -> bool {
basis_set.iter().all(|gate| GOODBYE_SET.contains(gate))
!basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate))
}

if check_goodbye(&available_basis_set) {
return Ok(Some(decomposers));
}

for basis_1q in &available_1q_basis {
for (_basis_2q, gate) in available_2q_param_basis.iter() {
let decomposer = TwoQubitControlledUDecomposer::new(
py,
ElePT marked this conversation as resolved.
Show resolved Hide resolved
RXXEquivalent::Standard(gate.operation.standard_gate()),
basis_1q,
)?;

decomposers.push(DecomposerElement {
decomposer: DecomposerType::TwoQubitControlledUDecomposer(Box::new(decomposer)),
gate: gate.clone(),
});
}
}

// If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer
// and there is no need to try other decomposers

let available_basis_param_set: IndexSet<&str> =
available_2q_param_basis.keys().copied().collect();

#[inline]
fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool {
!basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate))
}

if check_parametrized_goodbye(&available_basis_param_set) {
return Ok(Some(decomposers));
}

// Let's now look for possible controlled decomposers (i.e. XXDecomposer)
let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis
.iter()
Expand Down Expand Up @@ -881,6 +968,7 @@ fn preferred_direction(
}

fn synth_su4_sequence(
py: Python,
ShellyGarion marked this conversation as resolved.
Show resolved Hide resolved
su4_mat: &Array2<Complex64>,
decomposer_2q: &DecomposerElement,
preferred_direction: Option<bool>,
Expand All @@ -889,6 +977,9 @@ fn synth_su4_sequence(
let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0;
let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer {
decomp.call_inner(su4_mat.view(), None, is_approximate, None)?
} else if let DecomposerType::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer
{
decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)?
ShellyGarion marked this conversation as resolved.
Show resolved Hide resolved
} else {
unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.")
};
Expand Down Expand Up @@ -920,6 +1011,7 @@ fn synth_su4_sequence(
};
if synth_dir != preferred_dir {
reversed_synth_su4_sequence(
py,
su4_mat.clone(),
decomposer_2q,
approximation_degree,
Expand All @@ -934,6 +1026,7 @@ fn synth_su4_sequence(
}

fn reversed_synth_su4_sequence(
py: Python,
ShellyGarion marked this conversation as resolved.
Show resolved Hide resolved
mut su4_mat: Array2<Complex64>,
decomposer_2q: &DecomposerElement,
approximation_degree: Option<f64>,
Expand All @@ -949,6 +1042,9 @@ fn reversed_synth_su4_sequence(

let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer {
decomp.call_inner(su4_mat.view(), None, is_approximate, None)?
} else if let DecomposerType::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer
{
decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)?
} else {
unreachable!(
"reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer."
Expand Down
Loading
Loading