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 27 commits into
base: main
Choose a base branch
from

Conversation

ShellyGarion
Copy link
Member

Summary

close #13320

With the introduction of new 2q-fractional gates, such as RZZGate as part of the QPU, we add them to the UnitarySynthesis transpiler pass.
https://www.ibm.com/quantum/blog/fractional-gates

Details and comments

@ShellyGarion ShellyGarion added the mod: transpiler Issues and PRs related to Transpiler label Dec 16, 2024
@ShellyGarion ShellyGarion added this to the 2.0.0 milestone Dec 16, 2024
@ShellyGarion ShellyGarion requested a review from ElePT December 16, 2024 11:06
@ShellyGarion ShellyGarion requested review from alexanderivrii and a team as code owners December 16, 2024 11:06
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core
  • @levbishop

@ElePT ElePT self-assigned this Dec 16, 2024
@coveralls
Copy link

coveralls commented Dec 16, 2024

Pull Request Test Coverage Report for Build 12927458790

Details

  • 135 of 136 (99.26%) changed or added relevant lines in 4 files are covered.
  • 6 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.03%) to 88.972%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/accelerate/src/unitary_synthesis.rs 89 90 98.89%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/two_qubit_decompose.rs 1 92.08%
crates/accelerate/src/unitary_synthesis.rs 2 93.48%
crates/qasm2/src/lex.rs 3 92.48%
Totals Coverage Status
Change from base Build 12913135837: 0.03%
Covered Lines: 79528
Relevant Lines: 89385

💛 - Coveralls

Copy link
Contributor

@ElePT ElePT left a 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 :)

qiskit/synthesis/two_qubit/two_qubit_decompose.py Outdated Show resolved Hide resolved
Comment on lines 181 to 191
# 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)
Copy link
Contributor

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?

Copy link
Member Author

@ShellyGarion ShellyGarion Dec 17, 2024

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)?

Copy link
Member Author

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.

Copy link
Contributor

@ElePT ElePT Dec 18, 2024

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

Copy link
Contributor

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?

Copy link
Contributor

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!

Copy link
Member Author

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.

Copy link
Member Author

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

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.

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.
Copy link
Member Author

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)

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 "ZYZ" to "ZXZ". Should we also do it in other classes ?

Copy link
Contributor

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"))]
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?

@ShellyGarion ShellyGarion changed the title [WIP] Add 2q fractional gates to the UnitarySynthesis tranpiler pass Add 2q fractional gates to the UnitarySynthesis tranpiler pass Jan 21, 2025
@t-imamichi
Copy link
Member

I think this closes #13428. How about adding a test case of consecutive RZZ (RXX, and RYY) gates?

Copy link
Contributor

@ElePT ElePT left a 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) :)

crates/accelerate/src/two_qubit_decompose.rs Show resolved Hide resolved
crates/accelerate/src/two_qubit_decompose.rs Show resolved Hide resolved
Comment on lines 2747 to 2753
/// Initialize the KAK decomposition.
pub fn new_inner(
py: Python,
rxx_equivalent_gate: RXXEquivalent,
euler_basis: &str,
) -> PyResult<Self> {
let atol = DEFAULT_ATOL;
Copy link
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
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
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
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.

crates/accelerate/src/two_qubit_decompose.rs Outdated Show resolved Hide resolved
Comment on lines +513 to +540
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));
Copy link
Contributor

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.

Comment on lines +637 to +639
if PARAM_SET.contains(&std_gate.name()) {
if let Param::ParameterExpression(_) = op.params[0] {
return true;
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.

crates/accelerate/src/unitary_synthesis.rs Outdated Show resolved Hide resolved
crates/accelerate/src/unitary_synthesis.rs Outdated Show resolved Hide resolved
@@ -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.
Copy link
Contributor

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?

crates/accelerate/src/unitary_synthesis.rs Outdated Show resolved Hide resolved
@ShellyGarion
Copy link
Member Author

ShellyGarion commented Jan 23, 2025

I think this closes #13428. How about adding a test case of consecutive RZZ (RXX, and RYY) gates?

@t-imamichi - unfortunately this PR does not close #13428 yet, since the ConsolidateBlocks transpiler pass does not consolidate the consecutive RZZ gates into a single unitary.
This should be fixed in #13419.
I will therefore transfer your comment there.

Copy link
Contributor

@ElePT ElePT left a 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."""
Copy link
Contributor

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)
Copy link
Contributor

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)

Suggested change
backend = GenericBackendV2(3, basis_gates=basis_gates)
backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
mod: transpiler Issues and PRs related to Transpiler synthesis
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add TwoQubitControlledUDecomposer to the UnitarySynthesis tranpiler pass
6 participants