From 1214d511506e9636605b5ffdaf15fb8e6f57ba2d Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:48:36 +0200 Subject: [PATCH] 'Peephole' optimization - or: collecting and optimizing two-qubit blocks - before routing (#12727) * init * up * up * Update builtin_plugins.py * Update builtin_plugins.py * reno * Update builtin_plugins.py * Update builtin_plugins.py * Update peephole-before-routing-c3d184b740bb7a8b.yaml * neko check * check neko * Update builtin_plugins.py * test neko * Update builtin_plugins.py * Update builtin_plugins.py * Update builtin_plugins.py * lint * tests and format * remove FakeTorino test * Update peephole-before-routing-c3d184b740bb7a8b.yaml * Apply suggestions from code review Co-authored-by: Matthew Treinish * comments from code review * fix precision * up * up * update * up * . * cyclic import * cycl import * cyl import * . * circular import * . * lint * Include new pass in docs * Fix Split2QUnitaries dag manipulation This commit fixes the dag handling to do the 1q unitary insertion. Previously the dag manipulation was being done manually using the insert_node_on_in_edges() rustworkx method. However as the original node had 2 incoming edges for each qubit this caused the dag after running the pass to become corrupted. Each of the new 1q unitary nodes would end up with 2 incident edges and they would be in a sequence. This would result in later passes not being able to correctly understand the state of the circuit correctly. This was causing the unit tests to fail. This commit fixes this by just using `substitute_node_with_dag()` to handle the node substition, while doing it manually to avoid the overhead of checking is probably possible, the case where a unitary is the product of two 1q gates is not very common so optimizing it isn't super critical. * Update releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml * stricter check for doing split2q * Update qiskit/transpiler/preset_passmanagers/builtin_plugins.py Co-authored-by: Matthew Treinish * code review * Update qiskit/transpiler/passes/optimization/split_2q_unitaries.py Co-authored-by: Matthew Treinish * new tests * typo * lint * lint --------- Co-authored-by: Matthew Treinish --- qiskit/transpiler/passes/__init__.py | 2 + .../passes/optimization/__init__.py | 1 + .../passes/optimization/split_2q_unitaries.py | 83 +++++++ .../preset_passmanagers/builtin_plugins.py | 65 +++++ ...phole-before-routing-c3d184b740bb7a8b.yaml | 20 ++ test/python/compiler/test_transpiler.py | 38 +++ .../transpiler/test_preset_passmanagers.py | 3 +- .../transpiler/test_split_2q_unitaries.py | 225 ++++++++++++++++++ 8 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 qiskit/transpiler/passes/optimization/split_2q_unitaries.py create mode 100644 releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml create mode 100644 test/python/transpiler/test_split_2q_unitaries.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 400d98304951..1feabeaef048 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -91,6 +91,7 @@ ElidePermutations NormalizeRXAngle OptimizeAnnotated + Split2QUnitaries Calibration ============= @@ -244,6 +245,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated +from .optimization import Split2QUnitaries # circuit analysis from .analysis import ResourceEstimation diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 082cb3f67ec9..a9796850a689 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,3 +38,4 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated +from .split_2q_unitaries import Split2QUnitaries diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py new file mode 100644 index 000000000000..7508c9440a6e --- /dev/null +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" +from typing import Optional + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.quantumcircuitdata import CircuitInstruction +from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.circuit.library.generalized_gates import UnitaryGate +from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition + + +class Split2QUnitaries(TransformationPass): + """Attempt to splits two-qubit gates in a :class:`.DAGCircuit` into two single-qubit gates + + This pass will analyze all the two qubit gates in the circuit and analyze the gate's unitary + matrix to determine if the gate is actually a product of 2 single qubit gates. In these + cases the 2q gate can be simplified into two single qubit gates and this pass will + perform this optimization and will replace the two qubit gate with two single qubit + :class:`.UnitaryGate`. + """ + + def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): + """Split2QUnitaries initializer. + + Args: + fidelity (float): Allowed tolerance for splitting two-qubit unitaries and gate decompositions + """ + super().__init__() + self.requested_fidelity = fidelity + + def run(self, dag: DAGCircuit): + """Run the Split2QUnitaries pass on `dag`.""" + for node in dag.topological_op_nodes(): + # skip operations without two-qubits and for which we can not determine a potential 1q split + if ( + len(node.cargs) > 0 + or len(node.qargs) != 2 + or node.matrix is None + or node.is_parameterized() + ): + continue + + decomp = TwoQubitWeylDecomposition(node.op, fidelity=self.requested_fidelity) + if ( + decomp._inner_decomposition.specialization + == TwoQubitWeylDecomposition._specializations.IdEquiv + ): + new_dag = DAGCircuit() + new_dag.add_qubits(node.qargs) + + ur = decomp.K1r + ur_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag + ) + + ul = decomp.K1l + ul_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag + ) + new_dag._apply_op_node_back(ur_node) + new_dag._apply_op_node_back(ul_node) + new_dag.global_phase = decomp.global_phase + dag.substitute_node_with_dag(node, new_dag) + elif ( + decomp._inner_decomposition.specialization + == TwoQubitWeylDecomposition._specializations.SWAPEquiv + ): + # TODO maybe also look into swap-gate-like gates? Things to consider: + # * As the qubit mapping may change, we'll always need to build a new dag in this pass + # * There may not be many swap-gate-like gates in an arbitrary input circuit + # * Removing swap gates from a user-routed input circuit here is unexpected + pass + return dag diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 5e42c7ba3e3f..d7e6a3b2c174 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -14,6 +14,8 @@ import os +from qiskit.circuit import Instruction +from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BasicSwap @@ -64,12 +66,23 @@ CYGate, SXGate, SXdgGate, + get_standard_gate_name_mapping, ) from qiskit.utils.parallel import CPU_COUNT from qiskit import user_config CONFIG = user_config.get_config() +_discrete_skipped_ops = { + "delay", + "reset", + "measure", + "switch_case", + "if_else", + "for_loop", + "while_loop", +} + class DefaultInitPassManager(PassManagerStagePlugin): """Plugin class for default init stage.""" @@ -160,6 +173,58 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) + # skip peephole optimization before routing if target basis gate set is discrete, + # i.e. only consists of Cliffords that an user might want to keep + # use rz, sx, x, cx as basis, rely on physical optimziation to fix everything later one + stdgates = get_standard_gate_name_mapping() + + def _is_one_op_non_discrete(ops): + """Checks if one operation in `ops` is not discrete, i.e. is parameterizable + Args: + ops (List(Operation)): list of operations to check + Returns + True if at least one operation in `ops` is not discrete, False otherwise + """ + found_one_continuous_gate = False + for op in ops: + if isinstance(op, str): + if op in _discrete_skipped_ops: + continue + op = stdgates.get(op, None) + + if op is not None and op.name in _discrete_skipped_ops: + continue + + if op is None or not isinstance(op, Instruction): + return False + + if len(op.params) > 0: + found_one_continuous_gate = True + return found_one_continuous_gate + + target = pass_manager_config.target + basis = pass_manager_config.basis_gates + # consolidate gates before routing if the user did not specify a discrete basis gate, i.e. + # * no target or basis gate set has been specified + # * target has been specified, and we have one non-discrete gate in the target's spec + # * basis gates have been specified, and we have one non-discrete gate in that set + do_consolidate_blocks_init = target is None and basis is None + do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete( + target.operations + ) + do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis) + + if do_consolidate_blocks_init: + init.append(Collect2qBlocks()) + init.append(ConsolidateBlocks()) + # If approximation degree is None that indicates a request to approximate up to the + # error rates in the target. However, in the init stage we don't yet know the target + # qubits being used to figure out the fidelity so just use the default fidelity parameter + # in this case. + if pass_manager_config.approximation_degree is not None: + init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) + else: + init.append(Split2QUnitaries()) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml new file mode 100644 index 000000000000..b89a622987d0 --- /dev/null +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -0,0 +1,20 @@ +--- +features_transpiler: + - | + Added a new pass :class:`.Split2QUnitaries` that iterates over all two-qubit gates or unitaries in a + circuit and replaces them with two single-qubit unitaries, if possible without introducing errors, i.e. + the two-qubit gate/unitary is actually a (kronecker) product of single-qubit unitaries. + - | + The passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` have been + added to the ``init`` stage of the preset pass managers with optimization level 2 and optimization level 3. + The modification of the `init` stage should allow for a more efficient routing for quantum circuits that either: + + * contain two-qubit unitaries/gates that are actually a product of single-qubit gates + * contain multiple two-qubit gates in a continuous block of two-qubit gates. + + In the former case, the routing of the two-qubit gate can simply be skipped as no real interaction + between a pair of qubits occurs. In the latter case, the lookahead space of routing algorithms is not + 'polluted' by superfluous two-qubit gates, i.e. for routing it is sufficient to only consider one single + two-qubit gate per continuous block of two-qubit gates. These passes are not run if the pass + managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates have are not + parameterized. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 90dda73c0739..f465c9997039 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -84,6 +84,8 @@ from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout +from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries + from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager from qiskit.transpiler.target import ( @@ -872,6 +874,42 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): transpile(circ, coupling_map=coupling_map, initial_layout=layout) self.assertFalse(mock_pass.called) + def tests_conditional_run_split_2q_unitaries(self): + """Tests running `Split2QUnitaries` when basis gate set is (non-) discrete""" + qc = QuantumCircuit(3) + qc.sx(0) + qc.t(0) + qc.cx(0, 1) + qc.cx(1, 2) + + orig_pass = Split2QUnitaries() + with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: + basis = ["t", "sx", "cx"] + backend = GenericBackendV2(3, basis_gates=basis) + transpile(qc, backend=backend) + transpile(qc, basis_gates=basis) + transpile(qc, target=backend.target) + self.assertFalse(mock_pass.called) + + orig_pass = Split2QUnitaries() + with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: + basis = ["rz", "sx", "cx"] + backend = GenericBackendV2(3, basis_gates=basis) + transpile(qc, backend=backend, optimization_level=2) + self.assertTrue(mock_pass.called) + mock_pass.called = False + transpile(qc, basis_gates=basis, optimization_level=2) + self.assertTrue(mock_pass.called) + mock_pass.called = False + transpile(qc, target=backend.target, optimization_level=2) + self.assertTrue(mock_pass.called) + mock_pass.called = False + transpile(qc, backend=backend, optimization_level=3) + self.assertTrue(mock_pass.called) + mock_pass.called = False + transpile(qc, basis_gates=basis, optimization_level=3) + self.assertTrue(mock_pass.called) + def test_optimize_to_nothing(self): """Optimize gates up to fixed point in the default pipeline See https://github.com/Qiskit/qiskit-terra/issues/2035 diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 58f6d35a20d5..aa689b4c4fee 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -14,6 +14,7 @@ import unittest + from test import combine from ddt import ddt, data @@ -279,7 +280,7 @@ def counting_callback_func(pass_, dag, time, property_set, count): callback=counting_callback_func, translation_method="synthesis", ) - self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count) + self.assertEqual(gates_in_basis_true_count + 2, collect_2q_blocks_count) @ddt diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py new file mode 100644 index 000000000000..616d93e5b3f8 --- /dev/null +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -0,0 +1,225 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Tests for the Split2QUnitaries transpiler pass. +""" +from math import pi +from test import QiskitTestCase +import numpy as np + +from qiskit import QuantumCircuit, QuantumRegister, transpile +from qiskit.circuit.library import UnitaryGate, XGate, ZGate, HGate +from qiskit.circuit import Parameter, CircuitInstruction +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.quantum_info import Operator +from qiskit.transpiler import PassManager +from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.transpiler.passes import Collect2qBlocks, ConsolidateBlocks +from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries + + +class TestSplit2QUnitaries(QiskitTestCase): + """ + Tests to verify that splitting two-qubit unitaries into two single-qubit unitaries works correctly. + """ + + def test_splits(self): + """Test that the kronecker product of matrices is correctly identified by the pass and that the + global phase is set correctly.""" + qc = QuantumCircuit(2) + qc.x(0) + qc.z(1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc_split) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + + def test_2q_identity(self): + """Test that a 2q unitary matching the identity is correctly processed.""" + qc = QuantumCircuit(2) + qc.id(0) + qc.id(1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc_split) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + self.assertEqual(qc_split.size(), 2) + + def test_1q_identity(self): + """Test that a Kronecker product with one identity gate on top is correctly processed.""" + qc = QuantumCircuit(2) + qc.x(0) + qc.id(1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc_split) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + self.assertEqual(qc_split.size(), 2) + + def test_1q_identity2(self): + """Test that a Kronecker product with one identity gate on bottom is correctly processed.""" + qc = QuantumCircuit(2) + qc.id(0) + qc.x(1) + qc.global_phase += 1.2345 + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc_split) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + self.assertEqual(qc_split.size(), 2) + + def test_2_1q(self): + """Test that a Kronecker product of two X gates is correctly processed.""" + x_mat = np.array([[0, 1], [1, 0]]) + multi_x = np.kron(x_mat, x_mat) + qr = QuantumRegister(2, "qr") + backend = GenericBackendV2(2) + qc = QuantumCircuit(qr) + qc.unitary(multi_x, qr) + qct = transpile(qc, backend, optimization_level=2) + self.assertTrue(Operator(qc).equiv(qct)) + self.assertTrue(matrix_equal(Operator(qc).data, Operator(qct).data, ignore_phase=False)) + self.assertEqual(qct.size(), 2) + + def test_no_split(self): + """Test that the pass does not split a non-local two-qubit unitary.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.global_phase += 1.2345 + + qc_split = QuantumCircuit(2) + qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) + + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc_split) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + # either not a unitary gate, or the unitary has been consolidated to a 2q-unitary by another pass + self.assertTrue( + all( + op.name != "unitary" or (op.name == "unitary" and len(op.qubits) > 1) + for op in qc_split.data + ) + ) + + def test_almost_identity(self): + """Test that the pass handles QFT correctly.""" + qc = QuantumCircuit(2) + qc.cp(pi * 2 ** -(26), 0, 1) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(fidelity=1.0 - 1e-9)) + qc_split = pm.run(qc) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split2 = pm.run(qc) + self.assertEqual(qc_split.num_nonlocal_gates(), 0) + self.assertEqual(qc_split2.num_nonlocal_gates(), 1) + + def test_almost_identity_param(self): + """Test that the pass handles parameterized gates correctly.""" + qc = QuantumCircuit(2) + param = Parameter("p*2**-26") + qc.cp(param, 0, 1) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(fidelity=1.0 - 1e-9)) + qc_split = pm.run(qc) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split2 = pm.run(qc) + self.assertEqual(qc_split.num_nonlocal_gates(), 1) + self.assertEqual(qc_split2.num_nonlocal_gates(), 1) + + def test_single_q_gates(self): + """Test that the pass handles circuits with single-qubit gates correctly.""" + qr = QuantumRegister(5) + qc = QuantumCircuit(qr) + qc.x(0) + qc.z(1) + qc.h(2) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries(fidelity=1.0 - 1e-9)) + qc_split = pm.run(qc) + self.assertEqual(qc_split.num_nonlocal_gates(), 0) + self.assertEqual(qc_split.size(), 3) + + self.assertTrue(CircuitInstruction(XGate(), qubits=[qr[0]], clbits=[]) in qc.data) + self.assertTrue(CircuitInstruction(ZGate(), qubits=[qr[1]], clbits=[]) in qc.data) + self.assertTrue(CircuitInstruction(HGate(), qubits=[qr[2]], clbits=[]) in qc.data) + + def test_split_qft(self): + """Test that the pass handles QFT correctly.""" + qc = QuantumCircuit(100) + qc.h(0) + for i in range(qc.num_qubits - 2, 0, -1): + qc.cp(pi * 2 ** -(qc.num_qubits - 1 - i), qc.num_qubits - 1, i) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc) + self.assertEqual(26, qc_split.num_nonlocal_gates())