Skip to content

Improve single-qubit gate count in TwoQubitControlledUDecomposer#16123

Open
ShellyGarion wants to merge 16 commits intoQiskit:mainfrom
ShellyGarion:two_qubit_control_u_optimize
Open

Improve single-qubit gate count in TwoQubitControlledUDecomposer#16123
ShellyGarion wants to merge 16 commits intoQiskit:mainfrom
ShellyGarion:two_qubit_control_u_optimize

Conversation

@ShellyGarion
Copy link
Copy Markdown
Member

@ShellyGarion ShellyGarion commented May 3, 2026

close #16036

This PR reduces the total number of 1-qubit unitaries needed to synthesize a general 2-qubit unitary using TwoQubitControlledUDecomposer from 24 to 8.

This is in particular useful for the peephole optimization in #13419.

Details:

The TwoQubitControlledUDecomposer consists of the following steps.

  1. We start from a general 4x4 unitary U.

  2. Then we find a cannonical gate W (Weyl gate) s.t.
    U(0,1) = c2r(0) c2l(1) W(0,1) c1r(0) c1l(1)
    this yields 4 1-qubit unitary gates.

  3. Then, we write the Weyl gate W as:
    W(0,1) = RXX(0,1) RYY(0,1) RZZ(0,1)

  4. Now, we rewrite RYY(0,1) and RZZ(0,1) as:
    RYY(0,1) = sdg(0) sdg(0,1) RXX(0,1) s(0,1) s(0,1)
    RZZ(0,1) = h(0) h(0,1) h(0,1) h(0,1) h(0,1)
    yielding 8 additional 1-qubit unitary gates.

  5. If V is the target controlled-U gate (which is locally equivalent to RXX), then we rewrite RXX using V:
    RXX(0,1) = k2r(0) k2l(1) V(0,1) k1r(0) k1l(1)
    yielding 3*4 1-qubit unitary gates.

This gives a total of:
3*4 + 8 + 4 = 24 1-qubit unitary gates.

In this PR we multiply the 1-qubit unitary matrices betwen the 2-qubit gates, to get at most 8 1-qubit unitary gates (in the general case where 3 2-qubit gates are needed).

AI/LLM disclosure

  • I didn't use LLM tooling, or only used it privately.
  • I used the following tool to help write this PR description:
  • I used the following tool to generate or modify code:

@ShellyGarion ShellyGarion added this to the 2.5.0 milestone May 3, 2026
@ShellyGarion ShellyGarion requested a review from mtreinish May 3, 2026 08:09
@ShellyGarion ShellyGarion added synthesis Rust This PR or issue is related to Rust code in the repository labels May 3, 2026
@github-project-automation github-project-automation Bot moved this to Ready in Qiskit 2.5 May 3, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented May 3, 2026

Coverage Report for CI Build 25432069188

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.06%) to 87.627%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 6 uncovered changes across 1 file (125 of 131 lines covered, 95.42%).
  • 318 coverage regressions across 9 files.

Uncovered Changes

File Changed Covered %
crates/synthesis/src/two_qubit_decompose/controlled_u_decomposer.rs 131 125 95.42%

Coverage Regressions

318 previously-covered lines in 9 files lost coverage.

File Lines Losing Coverage Coverage
crates/circuit/src/circuit_drawer.rs 73 95.26%
qiskit/circuit/quantumcircuit.py 71 94.46%
crates/circuit/src/circuit_data.rs 52 87.17%
crates/circuit/src/parameter/parameter_expression.rs 51 91.04%
crates/circuit/src/register_data.rs 42 69.74%
crates/transpiler/src/passes/basis_translator/compose_transforms.rs 16 84.67%
crates/circuit/src/interner.rs 9 97.02%
crates/qasm2/src/lex.rs 3 92.03%
crates/circuit/src/parameter/symbol_expr.rs 1 74.05%

Coverage Stats

Coverage Status
Relevant Lines: 122012
Covered Lines: 106916
Line Coverage: 87.63%
Coverage Strength: 961075.11 hits per line

💛 - Coveralls

@ShellyGarion ShellyGarion marked this pull request as ready for review May 6, 2026 10:03
@ShellyGarion ShellyGarion requested a review from a team as a code owner May 6, 2026 10:03
@qiskit-bot
Copy link
Copy Markdown
Collaborator

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

  • @Qiskit/terra-core
  • @levbishop

@ShellyGarion ShellyGarion changed the title [WIP] Improve single-qubit gate count in TwoQubitControlledUDecomposer Improve single-qubit gate count in TwoQubitControlledUDecomposer May 6, 2026
@ShellyGarion ShellyGarion added the Changelog: Added Add an "Added" entry in the GitHub Release changelog. label May 6, 2026
Copy link
Copy Markdown
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Overall this looks good, thanks for fixing this. I just had a few comments inline.

fn to_rxx_gate(
&self,
angle: f64,
) -> PyResult<(TwoQubitGateSequence, SmallVec<[Matrix2<Complex64>; 4]>)> {
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.

Since this is always 4 matrices you don't need the smallvec. Smallvec lets you have a dynamically sized array, like a vec, that is stack allocated up to a specified size and above that size is heap allocated like a normal vec. But we don't need that in this case

Suggested change
) -> PyResult<(TwoQubitGateSequence, SmallVec<[Matrix2<Complex64>; 4]>)> {
) -> PyResult<(TwoQubitGateSequence, [Matrix2<Complex64>; 4])> {

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.

done in 0d11c70

gates,
global_phase,
},
smallvec![k2r, k2l, k1r, k1l],
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.

Suggested change
smallvec![k2r, k2l, k1r, k1l],
[k2r, k2l, k1r, k1l],

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.

done in 0d11c70

let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate);
gates.push((inv_gate_name, inv_gate_params, smallvec![0]));
}
let mut gates = Vec::with_capacity(1);
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 don't think we need the vec here anymore there is only going to be one gate now so allocating a vec is extra overhead we don't need yet. We can just return a (RXXEquivalent, f64, [Matrix2<Complex64>; 4])

Comment on lines +398 to +399
global_phase += weyl_gates.global_phase;
weyl_gates.global_phase = global_phase;
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.

Is there a reason to no just do?

Suggested change
global_phase += weyl_gates.global_phase;
weyl_gates.global_phase = global_phase;
weyl_gates.global_phase += global_phase;

target_decomposed: TwoQubitWeylDecomposition,
target_decomposed: &TwoQubitWeylDecomposition,
atol: f64,
c_mats: SmallVec<[Matrix2<Complex64>; 4]>,
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.

Same comment here about the type we don't need the small vec since there are always 4:

Suggested change
c_mats: SmallVec<[Matrix2<Complex64>; 4]>,
c_mats: [Matrix2<Complex64>; 4],

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.

done in 0d11c70

Comment on lines +318 to +320
rzz_k2r = rzz_mats[2]
.try_inverse()
.expect("matrix k2r is not invertible"); // before RZZ(c), qubit 0
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.

Do you need to preserve the original input here? It doesn't look like it's used anywhere else. If not I would replace this by try_inverse_mut() which will invert the matrix in place and avoid an extra copy.

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.

Actually we don't need to invert these matrices, since these are the original matrices calculated in to_rxx_gates.
This code is removed now, and the function to_rxx_gates was updated to cover this option of returning the original K matirces in 0d11c70

Comment on lines +484 to +485
let mut target_1q_basis_list = EulerBasisSet::new();
target_1q_basis_list.add_basis(self.euler_basis);
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.

It's not a huge deal, but it would maybe be better to do this once per decomposer. This is a fixed value based on the euler_basis set when we build the struct so it feels a bit unnecessary to reconstruct the EulerBasisSet for each 1q component of the decomposition.

@mtreinish mtreinish self-assigned this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Added Add an "Added" entry in the GitHub Release changelog. Rust This PR or issue is related to Rust code in the repository synthesis

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

Improve single-qubit gate count in TwoQubitControlledUDecomposer

4 participants