-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
base: main
Are you sure you want to change the base?
Conversation
One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 12927458790Details
💛 - Coveralls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ShellyGarion! thanks for opening the PR, I left a couple of preliminary comments. I hope they make sense :)
# if isinstance(kak_gate, RZXGate): | ||
# backup_optimizer = TwoQubitBasisDecomposer( | ||
# CXGate(), | ||
# basis_fidelity=basis_fidelity, | ||
# euler_basis=euler_basis, | ||
# pulse_optimize=pulse_optimize, | ||
# ) | ||
# decomposer2q = XXDecomposer(euler_basis=euler_basis, backup_optimizer=backup_optimizer) | ||
|
||
if kak_gate in KAK_GATE_PARAM_NAMES.values(): | ||
decomposer2q = TwoQubitControlledUDecomposer(kak_gate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double checking our discussion from earlier in the week: for the target-aware code we discussed adding a third check (apart from is_controlled
or is_supercontrolled
) to differentiate cases for the 3 available decomposers: TwoQubitDecomposer
, XXDecomposer
, and TwoQubitControlledUDecomposer
. However, for the Python code (just basis gates), it looks like we are simply substituting the XXDecomposer
for the TwoQubitControlledUDecomposer
. Is this the plan?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of basis gates, I'm not sure if we can have 3 options or only 2 (this is why this code is only commented out and not removed yet).
If a user provides the basis gates [rzz, rz, ry]
how can we tell if they want an RZZGate
with an arbitrary angle or an RZZGate
with a fixed angle (and if so - which fixed value)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another comment: I ran the tests in qiskit/test/python/transpiler/test_unitary_synthesis.py
(main branch), and couldn't find any test that goes into the following "if" statement (lines 181-188 above), so I wonder if the XXDecomposer
here has ever been used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not surprised, the coverage of these tests is not good because a lot of the tests are in the synthesis folder and not testing the transpilation pipeline, but calling the synthesis methods directly. Maybe this is a good opportunity to do some test refactoring before merging this PR. During the Rust migration I also noticed there weren't any tests for the UnitarySynthesis
class that used the QSD method with a target and had to add a couple of them. I think it would make sense to ensure that we have a couple of tests per decomposer, and that there are sufficient tests for the target (HW-aware) path (this I could do in a separate PR because I didn't have time to look more into it during the Rust migration).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a user provides the basis gates [rzz, rz, ry] how can we tell if they want an RZZGate with an arbitrary angle or an RZZGate with a fixed angle?
I think this is up to us to decide and document the behavior change. I am not sure how many users actually rely or expect the fixed angle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, I started the test cleanup with this PR: #13582, that is not much but I think can be helpful for future development. Let me know if you have any thoughts on it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another question is what to expect when given a parametrized gate in the target? see e.g this test.
Currently, we replace the parametrized gates by gates with a constant angle, without even checking if this angle is actually calibrated in the target:
def _replace_parameterized_gate(op): |
Again, I could not find any test that checks the XXDecomposer
with a given target, namely the tests don't go into here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that _decomposer_2q_from_target
is not being tested at all, since the tests for UnitarySynthesis
pass for the target use the rust code.
Should this function be removed or updated to include TwoQubitControlledUDecomposer
? (and then some tests should be added).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the function _decomposer_2q_from_target is part of the DefaultUnitarySynthesis
plugin we don't update it as part of this PR. We should consider porting this code to rust in a follow-up PR.
@@ -270,32 +270,46 @@ class TwoQubitControlledUDecomposer: | |||
:math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` | |||
gate that is locally equivalent to an :class:`.RXXGate`.""" | |||
|
|||
def __init__(self, rxx_equivalent_gate: Type[Gate]): | |||
def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZYZ"): | |||
r"""Initialize the KAK decomposition. |
There was a problem hiding this comment.
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 to "ZXZ" to match the current fractional gates rx
and rz
?
(If so, we should do this more generally in other places of the code in this file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I replaced "ZYZ" to "ZXZ". Should we also do it in other classes ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm I don't really know what motivated the original choice of default gates, maybe it was done on purpose not to match the current fractional gates? @mtreinish do you have any insight?
/// 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"))] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
UnitarySynthesis
tranpiler passUnitarySynthesis
tranpiler pass
I think this closes #13428. How about adding a test case of consecutive RZZ (RXX, and RYY) gates? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ShellyGarion, thanks for your PR!! I think that this is a very good start, I left some comments that are mostly non-crucial improvements, as well as one important question on the check_parametrized_gate
function. I will make sure to review the new unit tests when you add them, and feel free to reach out if you have any question (or objection) :)
/// Initialize the KAK decomposition. | ||
pub fn new_inner( | ||
py: Python, | ||
rxx_equivalent_gate: RXXEquivalent, | ||
euler_basis: &str, | ||
) -> PyResult<Self> { | ||
let atol = DEFAULT_ATOL; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have come up with a nicer solution at the time, but back when we only had 2 decomposer types and the code for the scoring_info
was slightly different, I was relatively ok with the code duplication. Now that two of the three arms look almost exactly the same, I wonder if we could abstract the code in a smarter way to avoid so much repetition. I don't think this will affect performance so it can also be left to a follow-up.
if PARAM_SET.contains(&std_gate.name()) { | ||
if let Param::ParameterExpression(_) = op.params[0] { | ||
return true; |
There was a problem hiding this comment.
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
".
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
qiskit/crates/accelerate/src/two_qubit_decompose.rs
Lines 2777 to 2822 in e9ccd3f
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.
@@ -270,32 +270,46 @@ class TwoQubitControlledUDecomposer: | |||
:math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` | |||
gate that is locally equivalent to an :class:`.RXXGate`.""" | |||
|
|||
def __init__(self, rxx_equivalent_gate: Type[Gate]): | |||
def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZYZ"): | |||
r"""Initialize the KAK decomposition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm I don't really know what motivated the original choice of default gates, maybe it was done on purpose not to match the current fractional gates? @mtreinish do you have any insight?
@t-imamichi - unfortunately this PR does not close #13428 yet, since the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the tests! I took a look and they look good to me.
"""Test synthesis with parameterized RXX gate.""" | ||
@data(True, False) | ||
def test_parameterized_basis_gate_in_target(self, is_random): | ||
"""Test synthesis with parameterized RZZ gate.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to keep both the test for RXX and RZZ? Given that we used to have it...
) | ||
def test_parameterized_backend(self, basis_gates): | ||
"""Test synthesis with parameterized backend.""" | ||
backend = GenericBackendV2(3, basis_gates=basis_gates) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will have a big effect on this particular test, but as a good practice, I recommend always setting the seed of the generic backend to make sure the errors and durations are always the same (this affects especially the routing tests)
backend = GenericBackendV2(3, basis_gates=basis_gates) | |
backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0) |
Summary
close #13320
With the introduction of new 2q-fractional gates, such as
RZZGate
as part of the QPU, we add them to theUnitarySynthesis
transpiler pass.https://www.ibm.com/quantum/blog/fractional-gates
Details and comments