Skip to content

Add 2q fractional gates to the ConsolidateBlocks transpiler pass #13884

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

Merged
merged 16 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 21 additions & 6 deletions crates/accelerate/src/consolidate_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ use num_complex::Complex64;
use numpy::PyReadonlyArray2;
use pyo3::intern;
use pyo3::prelude::*;
use rustworkx_core::petgraph::stable_graph::NodeIndex;
use smallvec::smallvec;

use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes;
use qiskit_circuit::dag_circuit::DAGCircuit;
Expand All @@ -28,12 +25,21 @@ use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT};
use qiskit_circuit::operations::{ArrayType, Operation, Param, UnitaryGate};
use qiskit_circuit::packed_instruction::PackedOperation;
use qiskit_circuit::Qubit;
use rustworkx_core::petgraph::stable_graph::NodeIndex;
use smallvec::smallvec;

use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst};
use crate::euler_one_qubit_decomposer::matmul_1q;
use crate::nlayout::PhysicalQubit;
use crate::target_transpiler::Target;
use crate::two_qubit_decompose::TwoQubitBasisDecomposer;
use crate::two_qubit_decompose::{TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer};

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, FromPyObject)]
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 see any public uses of DecomposerType, so the enum can probably be kept private, right? Similarly, I didn't find where is the FromPyObject trait used, but I might have missed 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.

FromPyObject is used to call either the TwoQubitBasisDecomposer or the TwoQubitControlledUDecomposer from python, see this comment:
#13884 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

removing pub from the enum produces this warning:
warning: type consolidate_blocks::DecomposerType is more private than the item consolidate_blocks

pub enum DecomposerType {
TwoQubitBasis(TwoQubitBasisDecomposer),
TwoQubitControlledU(TwoQubitControlledUDecomposer),
}

fn is_supported(
target: Option<&Target>,
Expand Down Expand Up @@ -62,7 +68,7 @@ const MAX_2Q_DEPTH: usize = 20;
pub(crate) fn consolidate_blocks(
py: Python,
dag: &mut DAGCircuit,
decomposer: &TwoQubitBasisDecomposer,
decomposer: DecomposerType,
basis_gate_name: &str,
force_consolidate: bool,
target: Option<&Target>,
Expand Down Expand Up @@ -212,8 +218,17 @@ pub(crate) fn consolidate_blocks(
];
let matrix = blocks_to_matrix(py, dag, &block, block_index_map).ok();
if let Some(matrix) = matrix {
let num_basis_gates = match decomposer {
DecomposerType::TwoQubitBasis(ref decomp) => {
decomp.num_basis_gates_inner(matrix.view())
}
DecomposerType::TwoQubitControlledU(ref decomp) => {
decomp.num_basis_gates_inner(matrix.view())?
}
};

if force_consolidate
|| decomposer.num_basis_gates_inner(matrix.view()) < basis_count
|| num_basis_gates < basis_count
|| block.len() > MAX_2Q_DEPTH
|| (basis_gates.is_some() && outside_basis)
|| (target.is_some() && outside_basis)
Expand Down
10 changes: 10 additions & 0 deletions crates/accelerate/src/two_qubit_decompose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,16 @@ type InverseReturn = (Option<StandardGate>, SmallVec<[f64; 3]>, SmallVec<[u8; 2]
/// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}`
/// gate that is locally equivalent to an :class:`.RXXGate`.
impl TwoQubitControlledUDecomposer {
/// Compute the number of basis gates needed for a given unitary
pub fn num_basis_gates_inner(&self, unitary: ArrayView2<Complex64>) -> PyResult<usize> {
let target_decomposed =
TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?;
let num_basis_gates = (((target_decomposed.a).abs() > DEFAULT_ATOL) as usize)
+ (((target_decomposed.b).abs() > DEFAULT_ATOL) as usize)
+ (((target_decomposed.c).abs() > DEFAULT_ATOL) as usize);
Ok(num_basis_gates)
}

/// invert 2q gate sequence
fn invert_2q_gate(
&self,
Expand Down
1 change: 1 addition & 0 deletions qiskit/synthesis/two_qubit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
TwoQubitBasisDecomposer,
two_qubit_cnot_decompose,
TwoQubitWeylDecomposition,
TwoQubitControlledUDecomposer,
)
10 changes: 6 additions & 4 deletions qiskit/synthesis/two_qubit/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,16 @@ def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZXZ"):
QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`.
"""
if rxx_equivalent_gate._standard_gate is not None:
self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer(
self._inner_decomposer = two_qubit_decompose.TwoQubitControlledUDecomposer(
rxx_equivalent_gate._standard_gate, euler_basis
)
self.gate_name = rxx_equivalent_gate._standard_gate.name
else:
self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer(
self._inner_decomposer = two_qubit_decompose.TwoQubitControlledUDecomposer(
rxx_equivalent_gate, euler_basis
)
self.rxx_equivalent_gate = rxx_equivalent_gate
self.scale = self._inner_decomposition.scale
self.scale = self._inner_decomposer.scale
self.euler_basis = euler_basis

def __call__(
Expand All @@ -312,7 +313,7 @@ def __call__(

Note: atol is passed to OneQubitEulerDecomposer.
"""
circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol)
circ_data = self._inner_decomposer(np.asarray(unitary, dtype=complex), atol)
return QuantumCircuit._from_circuit_data(circ_data, add_regs=True)


Expand Down Expand Up @@ -353,6 +354,7 @@ def __init__(
gate_name = "cx"
else:
gate_name = "USER_GATE"
self.gate_name = gate_name

self._inner_decomposer = two_qubit_decompose.TwoQubitBasisDecomposer(
gate_name,
Expand Down
41 changes: 32 additions & 9 deletions qiskit/transpiler/passes/optimization/consolidate_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@

"""Replace each block of consecutive gates by a single Unitary node."""
from __future__ import annotations
from math import pi

from qiskit.synthesis.two_qubit import TwoQubitBasisDecomposer
from qiskit.circuit.library.standard_gates import CXGate, CZGate, iSwapGate, ECRGate, RXXGate
from qiskit.synthesis.two_qubit import TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer
from qiskit.circuit.library.standard_gates import (
CXGate,
CZGate,
iSwapGate,
ECRGate,
RXXGate,
RYYGate,
RZZGate,
RZXGate,
CRXGate,
CRYGate,
CRZGate,
CPhaseGate,
)

from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.passmanager import PassManager
Expand All @@ -29,7 +41,17 @@
"cz": CZGate(),
"iswap": iSwapGate(),
"ecr": ECRGate(),
"rxx": RXXGate(pi / 2),
}

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


Expand Down Expand Up @@ -77,13 +99,14 @@ def __init__(
self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate)
elif basis_gates is not None:
kak_gates = KAK_GATE_NAMES.keys() & (basis_gates or [])
kak_param_gates = KAK_GATE_PARAM_NAMES.keys() & (basis_gates or [])
if kak_gates:
self.decomposer = TwoQubitBasisDecomposer(
KAK_GATE_NAMES[kak_gates.pop()], basis_fidelity=approximation_degree or 1.0
KAK_GATE_NAMES[list(kak_gates)[0]], basis_fidelity=approximation_degree or 1.0
)
elif "rzx" in basis_gates:
self.decomposer = TwoQubitBasisDecomposer(
CXGate(), basis_fidelity=approximation_degree or 1.0
elif kak_param_gates:
self.decomposer = TwoQubitControlledUDecomposer(
KAK_GATE_PARAM_NAMES[list(kak_param_gates)[0]]
)
else:
self.decomposer = None
Expand All @@ -109,7 +132,7 @@ def run(self, dag):
consolidate_blocks(
dag,
self.decomposer._inner_decomposer,
self.decomposer.gate.name,
self.decomposer.gate_name,
self.force_consolidate,
target=self.target,
basis_gates=self.basis_gates,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
features_transpiler:
- |
Added support for two-qubit fractional basis gates, such as :class:`.RZZGate`, to the
:class:`.ConsolidateBlocks` transpiler pass. The decomposition itself is done using the
:class:`.TwoQubitControlledUDecomposer`.

For example::

from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.transpiler.passes import ConsolidateBlocks

qc = QuantumCircuit(2)
qc.rzz(0.1, 0, 1)
qc.rzz(0.2, 0, 1)
consolidate_pass = ConsolidateBlocks(basis_gates=["rz", "rzz", "sx", "x", "rx"])
block = consolidate_pass(qc) # consolidate the circuit into a single unitary block
block.draw(output='mpl')

pm = generate_preset_pass_manager(
optimization_level=2, basis_gates=["rz", "rzz", "sx", "x", "rx"]
)
tqc = pm.run(qc) # synthesizing the circuit into basis gates
tqc.draw(output='mpl')
16 changes: 15 additions & 1 deletion test/python/transpiler/test_consolidate_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,11 @@ def test_no_kak_gates_in_preset_pm(self, opt_level):
optimization_level=opt_level, basis_gates=["rz", "rzz", "sx", "x", "rx"]
)
tqc = pm.run(qc)
self.assertEqual(ref_tqc, tqc)
# it's enough to check that the number of 2-qubit gates does not change
count_rzz_ref = ref_tqc.count_ops()["rzz"]
count_rzz_tqc = tqc.count_ops()["rzz"]
self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(tqc))
self.assertEqual(count_rzz_ref, count_rzz_tqc)

def test_non_cx_basis_gate(self):
"""Test a non-cx kak gate is consolidated correctly."""
Expand Down Expand Up @@ -666,6 +670,16 @@ def test_non_cx_target(self):
self.assertEqual({"unitary": 1}, res.count_ops())
self.assertEqual(Operator.from_circuit(qc), Operator(res.data[0].operation.params[0]))

def test_collect_rzz(self):
"""Collect blocks with RZZ gates."""
qc = QuantumCircuit(2)
qc.rzz(0.1, 0, 1)
qc.rzz(0.2, 0, 1)
consolidate_pass = ConsolidateBlocks(basis_gates=["rzz", "rx", "rz"])
res = consolidate_pass(qc)
self.assertEqual({"unitary": 1}, res.count_ops())
self.assertEqual(Operator.from_circuit(qc), Operator(res.data[0].operation.params[0]))
Copy link
Member Author

@ShellyGarion ShellyGarion Feb 25, 2025

Choose a reason for hiding this comment

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

Note that due to the TwoQubitControlledUDecomposer synthesis algorithm, the synthesized circuit is:

q_0: ──────────■──────────────────
     ┌───────┐ │ZZ(-0.3) ┌───────┐
q_1: ┤ Rx(π) ├─■─────────┤ Rx(π) ├
     └───────┘           └───────┘

and not just RZZGate(0.3)



if __name__ == "__main__":
unittest.main()