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

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 3 additions & 1 deletion qiskit/synthesis/two_qubit/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]):
self.rxx_equivalent_gate = rxx_equivalent_gate
self.scale = self._inner_decomposition.scale

def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit:
def __call__(
self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, atol=DEFAULT_ATOL
) -> QuantumCircuit:
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder what's the motivation for this change. Are approximate or use_dag used in the code? Could it be an oversight?

Copy link
Member Author

Choose a reason for hiding this comment

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

when calling the decomposer2q later in the code it assumes to have both parameters, see e.g

synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=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 see. I keep forgetting that we still have python code for the target aware case (this is kept in case someone uses a non-default plugin). In that case I think we should respect the keyword-only argument:

Suggested change
def __call__(
self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, atol=DEFAULT_ATOL
) -> QuantumCircuit:
def __call__(
self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, *, atol=DEFAULT_ATOL
) -> QuantumCircuit:

I also have realized that the TwoQubitControlledUDecomposer is not documented. If we are going to add it to the default pipeline, I believe we should, as right now it's not part of the public interface. So we should probably make sure to add it to the API docs with the other decomposers and add a release note listing the class as a new feature.

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 plan to add TwoQubitControlledUDecomposer to the API docs following this PR (as I may need to update the API as part of adding it to the transpiler pass)

"""Returns the Weyl decomposition in circuit form.
Args:
unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize.
Expand Down
52 changes: 41 additions & 11 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
RXXGate,
RZXGate,
RZZGate,
RYYGate,
ECRGate,
RXGate,
SXGate,
Expand All @@ -50,6 +51,10 @@
U3Gate,
RYGate,
RGate,
CRXGate,
CRYGate,
CRZGate,
CPhaseGate,
)
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.dagcircuit.dagcircuit import DAGCircuit
Expand All @@ -62,6 +67,7 @@
from qiskit.synthesis.two_qubit.two_qubit_decompose import (
TwoQubitBasisDecomposer,
TwoQubitWeylDecomposition,
TwoQubitControlledUDecomposer,
)
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.coupling import CouplingMap
Expand All @@ -88,16 +94,32 @@
"u3": U3Gate._standard_gate,
"ry": RYGate._standard_gate,
"r": RGate._standard_gate,
"rzz": RZZGate._standard_gate,
"ryy": RYYGate._standard_gate,
"rxx": RXXGate._standard_gate,
"rzx": RXXGate._standard_gate,
"cp": CPhaseGate._standard_gate,
"crx": RXXGate._standard_gate,
"cry": RXXGate._standard_gate,
"crz": RXXGate._standard_gate,
}

KAK_GATE_PARAM_NAMES = {
"rxx": RXXGate,
"rzz": RZZGate,
"ryy": RYYGate,
"rzx": RZXGate,
"cphase": CPhaseGate,
"crx": CRXGate,
"cry": CRYGate,
"crz": CRZGate,
}

KAK_GATE_NAMES = {
"cx": CXGate(),
"cz": CZGate(),
"iswap": iSwapGate(),
"rxx": RXXGate(pi / 2),
"ecr": ECRGate(),
"rzx": RZXGate(pi / 4), # typically pi/6 is also available
}

GateNameToGate = get_standard_gate_name_mapping()
Expand All @@ -107,7 +129,12 @@ def _choose_kak_gate(basis_gates):
"""Choose the first available 2q gate to use in 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.

Question: It seems we are not choosing the element according to some order, but using set pop, which is random, leading to nondeterministic behavior. Can this be avoided (or already avoided and I'm missing something)? e.g.

kak_gate = sorted([KAK_GATE_PARAM_NAMES[kak_gate_param] for kak_gate_params in kak_gates_params])[0]

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good point @gadial, I had always assumed that this randomness was a reflection of the fact that there can be several equally valid gates. And if that's case, I wonder if we should favor the one that comes first in alphabetical order? At the same time, I am not sure how conscious some of these decisions were. There are other parts of the code (the parts that were migrated to Rust) where we actually rely on insertion order when there are several valid decompositions, so at that point I guess it's as arbitrary as sorting alphabetically. It might be also worth noting that these cases don't come up often in real scenarios because we rarely see a basis set with more than one 2q gate (although it can definitely happen).

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the comment. I'll look into it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you @ElePT. My approach is usually to prefer the arbitrary to the nondeterminisitic, since nondeterministic behavior makes testing more difficult (since it's harder to reproduce problems) but also less resilient (since we are only testing the conditions generated nondeterminisically on the current testing machines, whereas a future version of Python might behave differently).

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 agree with @gadial's comment. If the user provides several 2q KAK gates, this "pop" just randomly chooses one of them. We would probably want a more deterministic behavior (perhaps favoring more "commonly" used 2q 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 think that sounds reasonable, it helps with benchmarking and bug-catching.

Copy link
Member Author

Choose a reason for hiding this comment

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

done in eeff4cd

kak_gate = None
kak_gates = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())
if kak_gates:
kak_gates_params = set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())

if kak_gates_params:
kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params.pop()]

elif kak_gates:
kak_gate = KAK_GATE_NAMES[kak_gates.pop()]

return kak_gate
Expand Down Expand Up @@ -151,14 +178,17 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat
kak_gate = _choose_kak_gate(basis_gates)
euler_basis = _choose_euler_basis(basis_gates)
basis_fidelity = approximation_degree or 1.0
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 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).

elif kak_gate is not None:
decomposer2q = TwoQubitBasisDecomposer(
kak_gate,
Expand Down
2 changes: 1 addition & 1 deletion test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,7 @@ def test_block_collection_reduces_1q_gate(self, basis_gates, gate_counts):
basis_gates=[
["u3", "cx"],
["rx", "rz", "iswap"],
["rx", "ry", "rxx"],
["ry", "rz", "rxx"],
],
)
def test_translation_method_synthesis(self, optimization_level, basis_gates):
Expand Down
3 changes: 2 additions & 1 deletion test/python/transpiler/test_unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ def test_empty_basis_gates(self):
@data(
["u3", "cx"],
["u1", "u2", "u3", "cx"],
["rx", "ry", "rxx"],
["ry", "rz", "rxx"],
["ry", "rz", "rzz"],
["rx", "rz", "iswap"],
["u3", "rx", "rz", "cz", "iswap"],
)
Expand Down
Loading