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