From 9965ce380e13d8ad4d9730fcefac556963c5a609 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:36:42 +0200 Subject: [PATCH 01/46] init --- qiskit/transpiler/passes/analysis/__init__.py | 1 + .../passes/analysis/check_simplify_2q.py | 57 +++++++++++++++++++ .../passes/optimization/__init__.py | 1 + .../passes/optimization/split_2q_unitaries.py | 29 ++++++++++ .../preset_passmanagers/builtin_plugins.py | 39 ++++++++++++- 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 qiskit/transpiler/passes/analysis/check_simplify_2q.py create mode 100644 qiskit/transpiler/passes/optimization/split_2q_unitaries.py diff --git a/qiskit/transpiler/passes/analysis/__init__.py b/qiskit/transpiler/passes/analysis/__init__.py index 8b366212166d..062ca9ffd0d3 100644 --- a/qiskit/transpiler/passes/analysis/__init__.py +++ b/qiskit/transpiler/passes/analysis/__init__.py @@ -21,3 +21,4 @@ from .num_tensor_factors import NumTensorFactors from .num_qubits import NumQubits from .dag_longest_path import DAGLongestPath +from .check_simplify_2q import CheckSimplify2Q diff --git a/qiskit/transpiler/passes/analysis/check_simplify_2q.py b/qiskit/transpiler/passes/analysis/check_simplify_2q.py new file mode 100644 index 000000000000..a1022ba84c55 --- /dev/null +++ b/qiskit/transpiler/passes/analysis/check_simplify_2q.py @@ -0,0 +1,57 @@ +import numpy as np +from qiskit.dagcircuit import DAGOpNode + +from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants +from qiskit.transpiler import AnalysisPass +from qiskit.transpiler.passes.utils import _block_to_matrix +from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition +from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate + +class CheckSimplify2Q(AnalysisPass): + """Check if the `dag` contains two-qubit gates that can be decomposed into single-qubit gates. + + """ + + def _block_qargs_to_indices(self, dag, block_qargs): + """Map each qubit in block_qargs to its wire position among the block's wires. + Args: + block_qargs (list): list of qubits that a block acts on + global_index_map (dict): mapping from each qubit in the + circuit to its wire position within that circuit + Returns: + dict: mapping from qarg to position in block + """ + block_indices = [dag.find_bit(q).index for q in block_qargs] + ordered_block_indices = {bit: index for index, bit in enumerate(sorted(block_indices))} + block_positions = {q: ordered_block_indices[dag.find_bit(q).index] for q in block_qargs} + return block_positions + + def run(self, dag): + """Run the CheckSimplify2Q pass on `dag`.""" + + blocks = self.property_set["block_list"] or [] + self.property_set['removable_2q'] = 0 + for block in blocks: + block_qargs = set() + block_cargs = set() + for nd in block: + block_qargs |= set(nd.qargs) + if isinstance(nd, DAGOpNode) and getattr(nd.op, "condition", None): + block_cargs |= set(getattr(nd.op, "condition", None)[0]) + + # skip blocks with conditional gates so far + if len(block_cargs) > 0 or len(block_qargs) != 2: + continue + + block_index_map = self._block_qargs_to_indices(dag, block_qargs) + + matrix = _block_to_matrix(block, block_index_map) + + if np.all(two_qubit_local_invariants(matrix) == [1, 0, 3]): + self.property_set['removable_2q'] += 1 + + removable_2q = [node for node in dag.topological_op_nodes() if + len(node.cargs) == 0 and len(node.qargs) == 2 and np.all( + two_qubit_local_invariants(node.op) == [1, 0, 3])] + print("len_remov", len(removable_2q)) + return dag 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..4d7f73815867 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -0,0 +1,29 @@ +import numpy as np +from qiskit.circuit.library import UnitaryGate +from qiskit.dagcircuit import DAGCircuit +from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants +from qiskit.synthesis.two_qubit.two_qubit_decompose import decompose_two_qubit_product_gate +from qiskit.transpiler import TransformationPass + + +class Split2QUnitaries(TransformationPass): + """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error. + + """ + + 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 hasattr(node.op, '_directive') + or (hasattr(node.op, 'is_parameterized') and node.op.is_parameterized())): + continue + + # check if the node can be represented by single-qubit gates + if np.all(two_qubit_local_invariants(node.op) == [1, 0, 3]): + ul, ur, phase = decompose_two_qubit_product_gate(node.op) + dag_node = DAGCircuit() + dag_node.add_qubits(node.qargs) + dag_node.apply_operation_back(UnitaryGate(ul), qargs=(node.qargs[0],)) + dag_node.apply_operation_back(UnitaryGate(ur), qargs=(node.qargs[1],)) + dag.substitute_node_with_dag(node, dag_node) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index f85b4d113c17..c1ada92cd56a 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -38,7 +38,7 @@ CommutativeCancellation, Collect2qBlocks, ConsolidateBlocks, - InverseCancellation, + InverseCancellation, Split2QUnitaries, ) from qiskit.transpiler.passes import Depth, Size, FixedPoint, MinimumPoint from qiskit.transpiler.passes.utils.gates_basis import GatesInBasis @@ -61,10 +61,10 @@ HGate, CYGate, SXGate, - SXdgGate, + SXdgGate, get_standard_gate_name_mapping, ) - +_discrete_skipped_ops = {'while_loop', 'delay', 'reset', 'for_loop', 'switch_case', 'measure', 'if_else'} class DefaultInitPassManager(PassManagerStagePlugin): """Plugin class for default init stage.""" @@ -154,6 +154,39 @@ 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 + """ + for op in ops: + if isinstance(op, str): + op = stdgates.get(op, None) + if op is None or op.name in _discrete_skipped_ops: + continue + if len(op.params) > 0: + return True + return False + + target = pass_manager_config.target + bases = 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 + if (target is None and bases is None) or ( + target is not None and _is_one_op_non_discrete(target.operations) or ( + bases is not None and _is_one_op_non_discrete(bases))): + init.append(Collect2qBlocks()) + init.append(ConsolidateBlocks(target=target, basis_gates=bases)) + init.append(Split2QUnitaries()) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init From c6505030db5036c84a46c448031c00e6cead765d Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:55:26 +0200 Subject: [PATCH 02/46] up --- .../passes/analysis/check_simplify_2q.py | 21 ++--- .../passes/optimization/split_2q_unitaries.py | 38 ++++++--- .../preset_passmanagers/builtin_plugins.py | 27 +++++-- test/python/compiler/test_transpiler.py | 10 +++ .../transpiler/test_preset_passmanagers.py | 17 +++- .../transpiler/test_split_2q_unitaries.py | 78 +++++++++++++++++++ 6 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 test/python/transpiler/test_split_2q_unitaries.py diff --git a/qiskit/transpiler/passes/analysis/check_simplify_2q.py b/qiskit/transpiler/passes/analysis/check_simplify_2q.py index a1022ba84c55..0dddb537a8d5 100644 --- a/qiskit/transpiler/passes/analysis/check_simplify_2q.py +++ b/qiskit/transpiler/passes/analysis/check_simplify_2q.py @@ -7,10 +7,9 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate -class CheckSimplify2Q(AnalysisPass): - """Check if the `dag` contains two-qubit gates that can be decomposed into single-qubit gates. - """ +class CheckSimplify2Q(AnalysisPass): + """Check if the `dag` contains two-qubit gates that can be decomposed into single-qubit gates.""" def _block_qargs_to_indices(self, dag, block_qargs): """Map each qubit in block_qargs to its wire position among the block's wires. @@ -30,7 +29,7 @@ def run(self, dag): """Run the CheckSimplify2Q pass on `dag`.""" blocks = self.property_set["block_list"] or [] - self.property_set['removable_2q'] = 0 + self.property_set["removable_2q"] = 0 for block in blocks: block_qargs = set() block_cargs = set() @@ -48,10 +47,14 @@ def run(self, dag): matrix = _block_to_matrix(block, block_index_map) if np.all(two_qubit_local_invariants(matrix) == [1, 0, 3]): - self.property_set['removable_2q'] += 1 - - removable_2q = [node for node in dag.topological_op_nodes() if - len(node.cargs) == 0 and len(node.qargs) == 2 and np.all( - two_qubit_local_invariants(node.op) == [1, 0, 3])] + self.property_set["removable_2q"] += 1 + + removable_2q = [ + node + for node in dag.topological_op_nodes() + if len(node.cargs) == 0 + and len(node.qargs) == 2 + and np.all(two_qubit_local_invariants(node.op) == [1, 0, 3]) + ] print("len_remov", len(removable_2q)) return dag diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 4d7f73815867..63fc1c919906 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -1,3 +1,15 @@ +# 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. + import numpy as np from qiskit.circuit.library import UnitaryGate from qiskit.dagcircuit import DAGCircuit @@ -7,23 +19,31 @@ class Split2QUnitaries(TransformationPass): - """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error. - - """ + """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" 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 hasattr(node.op, '_directive') - or (hasattr(node.op, 'is_parameterized') and node.op.is_parameterized())): + if ( + len(node.cargs) > 0 + or len(node.qargs) != 2 + or not (hasattr(node.op, "to_matrix") and hasattr(node.op, "__array__")) + or (hasattr(node.op, "is_parameterized") and node.op.is_parameterized()) + ): + # getattr(node.op, "_directive", False) or (hasattr(node.op, 'is_parameterized') and node.op.is_parameterized())): continue # check if the node can be represented by single-qubit gates - if np.all(two_qubit_local_invariants(node.op) == [1, 0, 3]): - ul, ur, phase = decompose_two_qubit_product_gate(node.op) + nmat = node.op.to_matrix() + if np.all(two_qubit_local_invariants(nmat) == [1, 0, 3]): + ul, ur, phase = decompose_two_qubit_product_gate(nmat) dag_node = DAGCircuit() dag_node.add_qubits(node.qargs) - dag_node.apply_operation_back(UnitaryGate(ul), qargs=(node.qargs[0],)) - dag_node.apply_operation_back(UnitaryGate(ur), qargs=(node.qargs[1],)) + + dag_node.apply_operation_back(UnitaryGate(ur), qargs=(node.qargs[0],)) + dag_node.apply_operation_back(UnitaryGate(ul), qargs=(node.qargs[1],)) + dag_node.global_phase += phase dag.substitute_node_with_dag(node, dag_node) + + return dag diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index c1ada92cd56a..c70c91fc05ff 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -38,7 +38,8 @@ CommutativeCancellation, Collect2qBlocks, ConsolidateBlocks, - InverseCancellation, Split2QUnitaries, + InverseCancellation, + Split2QUnitaries, ) from qiskit.transpiler.passes import Depth, Size, FixedPoint, MinimumPoint from qiskit.transpiler.passes.utils.gates_basis import GatesInBasis @@ -61,10 +62,21 @@ HGate, CYGate, SXGate, - SXdgGate, get_standard_gate_name_mapping, + SXdgGate, + get_standard_gate_name_mapping, ) -_discrete_skipped_ops = {'while_loop', 'delay', 'reset', 'for_loop', 'switch_case', 'measure', 'if_else'} +_discrete_skipped_ops = { + "while_loop", + "delay", + "reset", + "for_loop", + "switch_case", + "measure", + "if_else", +} + + class DefaultInitPassManager(PassManagerStagePlugin): """Plugin class for default init stage.""" @@ -182,11 +194,14 @@ def _is_one_op_non_discrete(ops): # * 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 if (target is None and bases is None) or ( - target is not None and _is_one_op_non_discrete(target.operations) or ( - bases is not None and _is_one_op_non_discrete(bases))): + target is not None + and _is_one_op_non_discrete(target.operations) + or (bases is not None and _is_one_op_non_discrete(bases)) + ): init.append(Collect2qBlocks()) - init.append(ConsolidateBlocks(target=target, basis_gates=bases)) + init.append(ConsolidateBlocks()) init.append(Split2QUnitaries()) + pass else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 77b63a3098bc..d9d9fd00a811 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -109,6 +109,16 @@ def _define(self): self._definition = QuantumCircuit(2) self._definition.cx(0, 1) + def to_matrix(self) -> np.ndarray: + return np.asarray( + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + ] + ) + def connected_qubits(physical: int, coupling_map: CouplingMap) -> set: """Get the physical qubits that have a connection to this one in the coupling map.""" diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index ee85dc34ffd6..e87e9b6c4d97 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 @@ -126,14 +127,14 @@ def test_layout_3239(self, level=3): qc.measure(range(4), range(4)) result = transpile( qc, - basis_gates=["u1", "u2", "u3", "cx"], + basis_gates=["h", "z", "cx"], layout_method="trivial", optimization_level=level, ) dag = circuit_to_dag(result) op_nodes = [node.name for node in dag.topological_op_nodes()] - self.assertNotIn("u1", op_nodes) # Check if the diagonal Z-Gates (u1) were removed + self.assertNotIn("z", op_nodes) # Check if the diagonal Z-Gates (u1) were removed @combine(level=[0, 1, 2, 3], name="level{level}") def test_no_basis_gates(self, level): @@ -262,7 +263,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 @@ -1455,6 +1456,16 @@ def _define(self): self._definition = QuantumCircuit(2) self._definition.cx(0, 1) + def to_matrix(self) -> np.ndarray: + return np.asarray( + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + ] + ) + circuit = QuantumCircuit(6, 1) circuit.h(0) circuit.measure(0, 0) 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..18c960fb2a0d --- /dev/null +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -0,0 +1,78 @@ +# 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 qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.transpiler.passes.optimization import Split2QUnitaries + +from qiskit import QuantumCircuit +from qiskit.circuit.library import UnitaryGate +from qiskit.quantum_info import Operator +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import Collect2qBlocks, ConsolidateBlocks +from test import QiskitTestCase + + +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_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 + ) + ) From 092257c387711578cd7e7064d906f418067a653b Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:45:43 +0200 Subject: [PATCH 03/46] up --- qiskit/transpiler/passes/analysis/__init__.py | 1 - .../passes/analysis/check_simplify_2q.py | 60 ------------------- .../passes/optimization/split_2q_unitaries.py | 2 +- .../preset_passmanagers/builtin_plugins.py | 12 ++-- .../transpiler/test_split_2q_unitaries.py | 6 +- 5 files changed, 11 insertions(+), 70 deletions(-) delete mode 100644 qiskit/transpiler/passes/analysis/check_simplify_2q.py diff --git a/qiskit/transpiler/passes/analysis/__init__.py b/qiskit/transpiler/passes/analysis/__init__.py index 062ca9ffd0d3..8b366212166d 100644 --- a/qiskit/transpiler/passes/analysis/__init__.py +++ b/qiskit/transpiler/passes/analysis/__init__.py @@ -21,4 +21,3 @@ from .num_tensor_factors import NumTensorFactors from .num_qubits import NumQubits from .dag_longest_path import DAGLongestPath -from .check_simplify_2q import CheckSimplify2Q diff --git a/qiskit/transpiler/passes/analysis/check_simplify_2q.py b/qiskit/transpiler/passes/analysis/check_simplify_2q.py deleted file mode 100644 index 0dddb537a8d5..000000000000 --- a/qiskit/transpiler/passes/analysis/check_simplify_2q.py +++ /dev/null @@ -1,60 +0,0 @@ -import numpy as np -from qiskit.dagcircuit import DAGOpNode - -from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants -from qiskit.transpiler import AnalysisPass -from qiskit.transpiler.passes.utils import _block_to_matrix -from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition -from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate - - -class CheckSimplify2Q(AnalysisPass): - """Check if the `dag` contains two-qubit gates that can be decomposed into single-qubit gates.""" - - def _block_qargs_to_indices(self, dag, block_qargs): - """Map each qubit in block_qargs to its wire position among the block's wires. - Args: - block_qargs (list): list of qubits that a block acts on - global_index_map (dict): mapping from each qubit in the - circuit to its wire position within that circuit - Returns: - dict: mapping from qarg to position in block - """ - block_indices = [dag.find_bit(q).index for q in block_qargs] - ordered_block_indices = {bit: index for index, bit in enumerate(sorted(block_indices))} - block_positions = {q: ordered_block_indices[dag.find_bit(q).index] for q in block_qargs} - return block_positions - - def run(self, dag): - """Run the CheckSimplify2Q pass on `dag`.""" - - blocks = self.property_set["block_list"] or [] - self.property_set["removable_2q"] = 0 - for block in blocks: - block_qargs = set() - block_cargs = set() - for nd in block: - block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and getattr(nd.op, "condition", None): - block_cargs |= set(getattr(nd.op, "condition", None)[0]) - - # skip blocks with conditional gates so far - if len(block_cargs) > 0 or len(block_qargs) != 2: - continue - - block_index_map = self._block_qargs_to_indices(dag, block_qargs) - - matrix = _block_to_matrix(block, block_index_map) - - if np.all(two_qubit_local_invariants(matrix) == [1, 0, 3]): - self.property_set["removable_2q"] += 1 - - removable_2q = [ - node - for node in dag.topological_op_nodes() - if len(node.cargs) == 0 - and len(node.qargs) == 2 - and np.all(two_qubit_local_invariants(node.op) == [1, 0, 3]) - ] - print("len_remov", len(removable_2q)) - return dag diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 63fc1c919906..205d289357dc 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -9,6 +9,7 @@ # 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.""" import numpy as np from qiskit.circuit.library import UnitaryGate @@ -31,7 +32,6 @@ def run(self, dag: DAGCircuit): or not (hasattr(node.op, "to_matrix") and hasattr(node.op, "__array__")) or (hasattr(node.op, "is_parameterized") and node.op.is_parameterized()) ): - # getattr(node.op, "_directive", False) or (hasattr(node.op, 'is_parameterized') and node.op.is_parameterized())): continue # check if the node can be represented by single-qubit gates diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index c70c91fc05ff..d04c356bc39d 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -193,11 +193,13 @@ def _is_one_op_non_discrete(ops): # * 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 - if (target is None and bases is None) or ( - target is not None - and _is_one_op_non_discrete(target.operations) - or (bases is not None and _is_one_op_non_discrete(bases)) - ): + do_consolidate_blocks_init = target is None and bases is None + do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete( + target.operations + ) + do_consolidate_blocks_init |= bases is not None and _is_one_op_non_discrete(bases) + + if do_consolidate_blocks_init: init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) init.append(Split2QUnitaries()) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 18c960fb2a0d..e21a3b3bdbb9 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -13,15 +13,15 @@ """ Tests for the Split2QUnitaries transpiler pass. """ -from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.transpiler.passes.optimization import Split2QUnitaries +from test import QiskitTestCase from qiskit import QuantumCircuit from qiskit.circuit.library import UnitaryGate 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 test import QiskitTestCase +from qiskit.transpiler.passes.optimization import Split2QUnitaries class TestSplit2QUnitaries(QiskitTestCase): From 3befb8376a2a0d891efd4a2079618a696bf61eb1 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:10:38 +0200 Subject: [PATCH 04/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d04c356bc39d..ff51eeea857d 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -183,7 +183,8 @@ def _is_one_op_non_discrete(ops): op = stdgates.get(op, None) if op is None or op.name in _discrete_skipped_ops: continue - if len(op.params) > 0: + params = op.params + if len(params) > 0: return True return False From a8c5b8cd846d226317810abfa6e9ae7a8b66e712 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:51:15 +0200 Subject: [PATCH 05/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index ff51eeea857d..a33aaab863f3 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -183,8 +183,8 @@ def _is_one_op_non_discrete(ops): op = stdgates.get(op, None) if op is None or op.name in _discrete_skipped_ops: continue - params = op.params - if len(params) > 0: + + if len(list(op.params)) > 0: return True return False From 90e725c150ad363f7cae4aeda111ccca868a7084 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:07:19 +0200 Subject: [PATCH 06/46] reno --- .../peephole-before-routing-c3d184b740bb7a8b.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml new file mode 100644 index 000000000000..971351a66a29 --- /dev/null +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -0,0 +1,15 @@ +--- +prelude: > + 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. In addition, + the passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` are + added to the `init` stage of the preset pass managers with optimization level 2 and optimization level 3 + if the pass managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates + have are not parameterized. The modification of the `init` stage should allow for a more efficient routing + for quantum circuits that (a) contain two-qubit unitaries/gates that are actually a product of single-qubit gates + or (b) 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 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. From fd988e318adfccdfe49866cb13febe25ef00cfe1 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:22:10 +0200 Subject: [PATCH 07/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index a33aaab863f3..d877160c33cf 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. """Built-in transpiler stage plugins for preset pass managers.""" +from typing import Iterable from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError @@ -184,7 +185,8 @@ def _is_one_op_non_discrete(ops): if op is None or op.name in _discrete_skipped_ops: continue - if len(list(op.params)) > 0: + if ((isinstance(op.params, Iterable) and len(op.params) > 0) or + (callable(op.params) and len(op.params()) > 0)): return True return False From e7652858882a2a9dae4f618f0508ab6b2d7fa3dc Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:52:48 +0200 Subject: [PATCH 08/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d877160c33cf..d769f4b76acf 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -185,8 +185,9 @@ def _is_one_op_non_discrete(ops): if op is None or op.name in _discrete_skipped_ops: continue - if ((isinstance(op.params, Iterable) and len(op.params) > 0) or - (callable(op.params) and len(op.params()) > 0)): + if (isinstance(op.params, Iterable) and len(op.params) > 0) or ( + callable(op.params) and len(op.params()) > 0 + ): return True return False From 1805cb84289b0c02106676f79af7216c4976329f Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:12:39 +0200 Subject: [PATCH 09/46] Update peephole-before-routing-c3d184b740bb7a8b.yaml --- .../notes/peephole-before-routing-c3d184b740bb7a8b.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml index 971351a66a29..126e9fef49a1 100644 --- a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -10,6 +10,6 @@ prelude: > for quantum circuits that (a) contain two-qubit unitaries/gates that are actually a product of single-qubit gates or (b) 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 of routing algorithms is not 'polluted' by superfluous + 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. From bce3fcd5f441ec661b1125c7b08215e26bd82e01 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:22:46 +0200 Subject: [PATCH 10/46] neko check --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d769f4b76acf..bb06f4ecdb09 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -185,9 +185,8 @@ def _is_one_op_non_discrete(ops): if op is None or op.name in _discrete_skipped_ops: continue - if (isinstance(op.params, Iterable) and len(op.params) > 0) or ( - callable(op.params) and len(op.params()) > 0 - ): + print(type(op)) + if len(op.params) > 0: return True return False From e6f0b39d311c70d67719e80f48e5d365f30d5329 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:41:41 +0200 Subject: [PATCH 11/46] check neko --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index bb06f4ecdb09..72869248bd4b 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -186,6 +186,8 @@ def _is_one_op_non_discrete(ops): continue print(type(op)) + print(op) + print(op.params) if len(op.params) > 0: return True return False From 6f7445c3fb1dafc4466751ee71b229b3528c988d Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:44:07 +0200 Subject: [PATCH 12/46] Update builtin_plugins.py --- .../preset_passmanagers/builtin_plugins.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 72869248bd4b..c8c8f89acc64 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -11,8 +11,8 @@ # that they have been altered from the originals. """Built-in transpiler stage plugins for preset pass managers.""" -from typing import Iterable +from qiskit.circuit import ControlFlowOp, Instruction from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BasicSwap @@ -67,15 +67,7 @@ get_standard_gate_name_mapping, ) -_discrete_skipped_ops = { - "while_loop", - "delay", - "reset", - "for_loop", - "switch_case", - "measure", - "if_else", -} +_discrete_skipped_ops = {"delay", "reset", "measure"} class DefaultInitPassManager(PassManagerStagePlugin): @@ -184,10 +176,8 @@ def _is_one_op_non_discrete(ops): op = stdgates.get(op, None) if op is None or op.name in _discrete_skipped_ops: continue - - print(type(op)) - print(op) - print(op.params) + if not isinstance(op, Instruction) and issubclass(op, ControlFlowOp): + continue if len(op.params) > 0: return True return False From d3a45e7a4a926e5bf3d8143fa15868e091f65c2d Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:05:12 +0200 Subject: [PATCH 13/46] test neko --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index c8c8f89acc64..841d3764c697 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -176,8 +176,14 @@ def _is_one_op_non_discrete(ops): op = stdgates.get(op, None) if op is None or op.name in _discrete_skipped_ops: continue + if not isinstance(op, Instruction) and issubclass(op, ControlFlowOp): continue + + print(op) + print(type(op)) + print(pass_manager_config.target) + print(pass_manager_config.basis_gates) if len(op.params) > 0: return True return False From dec91de0564db28f81e2d65e71268bff63e4863f Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:14:26 +0200 Subject: [PATCH 14/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 841d3764c697..014d33c8d965 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -177,13 +177,9 @@ def _is_one_op_non_discrete(ops): if op is None or op.name in _discrete_skipped_ops: continue - if not isinstance(op, Instruction) and issubclass(op, ControlFlowOp): + if not isinstance(op, Instruction): continue - print(op) - print(type(op)) - print(pass_manager_config.target) - print(pass_manager_config.basis_gates) if len(op.params) > 0: return True return False From 45912f87329763ce387ce641e26ff976964ce013 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:21:51 +0200 Subject: [PATCH 15/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 014d33c8d965..68178be03800 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -174,10 +174,8 @@ def _is_one_op_non_discrete(ops): for op in ops: if isinstance(op, str): op = stdgates.get(op, None) - if op is None or op.name in _discrete_skipped_ops: - continue - - if not isinstance(op, Instruction): + + if op is None or not isinstance(op, Instruction) or op.name in _discrete_skipped_ops: continue if len(op.params) > 0: From 5404822c5f4709e8061c24f2996a1f6618eb53fb Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:39:30 +0200 Subject: [PATCH 16/46] Update builtin_plugins.py --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 68178be03800..73418b9f0e90 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -174,8 +174,12 @@ def _is_one_op_non_discrete(ops): for op in ops: if isinstance(op, str): op = stdgates.get(op, None) - - if op is None or not isinstance(op, Instruction) or op.name in _discrete_skipped_ops: + + if ( + op is None + or not isinstance(op, Instruction) + or op.name in _discrete_skipped_ops + ): continue if len(op.params) > 0: From ed790c160da01feb39ca876f37478c9b75add01b Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:41:44 +0200 Subject: [PATCH 17/46] lint --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 73418b9f0e90..3f5557dc2b88 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -12,7 +12,7 @@ """Built-in transpiler stage plugins for preset pass managers.""" -from qiskit.circuit import ControlFlowOp, Instruction +from qiskit.circuit import Instruction from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BasicSwap From 1236cd6d2ba1289a888915d474319d8217a80e1a Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:44:16 +0200 Subject: [PATCH 18/46] tests and format --- .../preset_passmanagers/builtin_plugins.py | 1 - test/python/compiler/test_transpiler.py | 54 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 3f5557dc2b88..ff4eb6cfaec2 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -202,7 +202,6 @@ def _is_one_op_non_discrete(ops): init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) init.append(Split2QUnitaries()) - pass else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index d9d9fd00a811..88571078cbed 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -22,6 +22,7 @@ import numpy as np import rustworkx as rx from ddt import data, ddt, unpack +from qiskit_ibm_runtime.fake_provider import FakeTorino from qiskit import ( ClassicalRegister, @@ -84,6 +85,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 +875,57 @@ 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_transpilation_fake_torino(self): + """Running transpilation on `FakeTorino`, which has `ControlFlowOp` classes as basis gates""" + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc_t = transpile(qc, backend=FakeTorino(), optimization_level=2, initial_layout=[0, 1, 2]) + qc_new = QuantumCircuit(qc.num_qubits) + for op in qc_t.data: + if all(qc_t.qubits.index(q) < qc.num_qubits for q in op.qubits): + qc_new.append( + op.operation, qargs=(qc_new.qubits[qc_t.qubits.index(q)] for q in op.qubits) + ) + self.assertTrue(Operator.from_circuit(qc_new).equiv(qc)) + + 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 From db8aa2d8e2873c31858e333a273afaad3dba9dae Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:59:47 +0200 Subject: [PATCH 19/46] remove FakeTorino test --- test/python/compiler/test_transpiler.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 88571078cbed..62dd367019c0 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -22,7 +22,6 @@ import numpy as np import rustworkx as rx from ddt import data, ddt, unpack -from qiskit_ibm_runtime.fake_provider import FakeTorino from qiskit import ( ClassicalRegister, @@ -875,21 +874,6 @@ 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_transpilation_fake_torino(self): - """Running transpilation on `FakeTorino`, which has `ControlFlowOp` classes as basis gates""" - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, 1) - qc.cx(1, 2) - qc_t = transpile(qc, backend=FakeTorino(), optimization_level=2, initial_layout=[0, 1, 2]) - qc_new = QuantumCircuit(qc.num_qubits) - for op in qc_t.data: - if all(qc_t.qubits.index(q) < qc.num_qubits for q in op.qubits): - qc_new.append( - op.operation, qargs=(qc_new.qubits[qc_t.qubits.index(q)] for q in op.qubits) - ) - self.assertTrue(Operator.from_circuit(qc_new).equiv(qc)) - def tests_conditional_run_split_2q_unitaries(self): """Tests running `Split2QUnitaries` when basis gate set is (non-) discrete""" qc = QuantumCircuit(3) From 3e28ec6ba35fb9577ac059fdc07690b6b3ea2712 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Wed, 10 Jul 2024 01:04:05 +0200 Subject: [PATCH 20/46] Update peephole-before-routing-c3d184b740bb7a8b.yaml --- .../notes/peephole-before-routing-c3d184b740bb7a8b.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml index 126e9fef49a1..aa1d2cff3ab9 100644 --- a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -1,5 +1,6 @@ --- -prelude: > +features: + - | 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. In addition, From d064c6653480c8625432c3bbc80becc75c70216e Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:50:36 +0200 Subject: [PATCH 21/46] Apply suggestions from code review Co-authored-by: Matthew Treinish --- .../passes/optimization/split_2q_unitaries.py | 13 +++++++------ .../peephole-before-routing-c3d184b740bb7a8b.yaml | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 205d289357dc..d17e1eba3058 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -29,20 +29,21 @@ def run(self, dag: DAGCircuit): if ( len(node.cargs) > 0 or len(node.qargs) != 2 - or not (hasattr(node.op, "to_matrix") and hasattr(node.op, "__array__")) - or (hasattr(node.op, "is_parameterized") and node.op.is_parameterized()) + or node.matrix is None + or node.is_parameterized()) ): continue # check if the node can be represented by single-qubit gates - nmat = node.op.to_matrix() - if np.all(two_qubit_local_invariants(nmat) == [1, 0, 3]): + nmat = node.matrix + local_invairants = two_qubit_local_invariants(nmat) + if local_invariants[0] == 1 and local_invariants[1] == 0 and local_invariants == 3: ul, ur, phase = decompose_two_qubit_product_gate(nmat) dag_node = DAGCircuit() dag_node.add_qubits(node.qargs) - dag_node.apply_operation_back(UnitaryGate(ur), qargs=(node.qargs[0],)) - dag_node.apply_operation_back(UnitaryGate(ul), qargs=(node.qargs[1],)) + dag_node.apply_operation_back(UnitaryGate(ur, check_input=False), qargs=(node.qargs[0],)) + dag_node.apply_operation_back(UnitaryGate(ul, check_input=False), qargs=(node.qargs[1],)) dag_node.global_phase += phase dag.substitute_node_with_dag(node, dag_node) diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml index aa1d2cff3ab9..22543055b989 100644 --- a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -1,5 +1,5 @@ --- -features: +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. From b88e3f0a9e6c12e626c925e9cd9c7433e0f79e5c Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:49:19 +0200 Subject: [PATCH 22/46] comments from code review --- .../passes/optimization/split_2q_unitaries.py | 35 ++++++++--- .../preset_passmanagers/builtin_plugins.py | 6 +- ...phole-before-routing-c3d184b740bb7a8b.yaml | 4 +- test/python/compiler/test_transpiler.py | 10 --- .../transpiler/test_split_2q_unitaries.py | 63 +++++++++++++++++++ 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index d17e1eba3058..cdd0401b4556 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -12,8 +12,10 @@ """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" import numpy as np + +from qiskit.circuit import CircuitInstruction from qiskit.circuit.library import UnitaryGate -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants from qiskit.synthesis.two_qubit.two_qubit_decompose import decompose_two_qubit_product_gate from qiskit.transpiler import TransformationPass @@ -24,27 +26,40 @@ class Split2QUnitaries(TransformationPass): def run(self, dag: DAGCircuit): """Run the Split2QUnitaries pass on `dag`.""" + sq_id = np.eye(2) 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()) + or node.is_parameterized() ): continue # check if the node can be represented by single-qubit gates nmat = node.matrix - local_invairants = two_qubit_local_invariants(nmat) - if local_invariants[0] == 1 and local_invariants[1] == 0 and local_invariants == 3: + local_invariants = two_qubit_local_invariants(nmat) + if local_invariants[0] == 1 and local_invariants[1] == 0 and local_invariants[2] == 3: ul, ur, phase = decompose_two_qubit_product_gate(nmat) - dag_node = DAGCircuit() - dag_node.add_qubits(node.qargs) - dag_node.apply_operation_back(UnitaryGate(ur, check_input=False), qargs=(node.qargs[0],)) - dag_node.apply_operation_back(UnitaryGate(ul, check_input=False), qargs=(node.qargs[1],)) - dag_node.global_phase += phase - dag.substitute_node_with_dag(node, dag_node) + if not np.allclose(ur, sq_id): + ur_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=dag + ) + ur_node._node_id = dag._multi_graph.add_node(ur_node) + dag._increment_op("unitary") + dag._multi_graph.insert_node_on_in_edges(ur_node._node_id, node._node_id) + + if not np.allclose(ul, sq_id): + ul_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=dag + ) + ul_node._node_id = dag._multi_graph.add_node(ul_node) + dag._increment_op("unitary") + dag._multi_graph.insert_node_on_in_edges(ul_node._node_id, node._node_id) + + dag.global_phase += phase + dag.remove_op_node(node) return dag diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index ff4eb6cfaec2..32fd977799a8 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -187,16 +187,16 @@ def _is_one_op_non_discrete(ops): return False target = pass_manager_config.target - bases = pass_manager_config.basis_gates + 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 bases is None + 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 |= bases is not None and _is_one_op_non_discrete(bases) + do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis) if do_consolidate_blocks_init: init.append(Collect2qBlocks()) diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml index 22543055b989..8b94170701f6 100644 --- a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -3,7 +3,9 @@ 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. In addition, + the two-qubit gate/unitary is actually a (kronecker) product of single-qubit unitaries. + - | + In addition, the passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` are added to the `init` stage of the preset pass managers with optimization level 2 and optimization level 3 if the pass managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 62dd367019c0..57fe7cfb2717 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -111,16 +111,6 @@ def _define(self): self._definition = QuantumCircuit(2) self._definition.cx(0, 1) - def to_matrix(self) -> np.ndarray: - return np.asarray( - [ - [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - ] - ) - def connected_qubits(physical: int, coupling_map: CouplingMap) -> set: """Get the physical qubits that have a connection to this one in the coupling map.""" diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index e21a3b3bdbb9..b649a8af24db 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -50,6 +50,69 @@ def test_splits(self): 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(), 0) + + 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(), 1) + + 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(), 1) + def test_no_split(self): """Test that the pass does not split a non-local two-qubit unitary.""" qc = QuantumCircuit(2) From a6550017f98488b36a89ea558c81f5fd873f7397 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:43:01 +0200 Subject: [PATCH 23/46] fix precision --- qiskit/synthesis/two_qubit/local_invariance.py | 2 +- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/synthesis/two_qubit/local_invariance.py b/qiskit/synthesis/two_qubit/local_invariance.py index 346febdddf9f..3f74b0d0d710 100644 --- a/qiskit/synthesis/two_qubit/local_invariance.py +++ b/qiskit/synthesis/two_qubit/local_invariance.py @@ -65,7 +65,7 @@ def two_qubit_local_invariants(U: np.ndarray) -> np.ndarray: # to better equate to the Weyl chamber coordinates (c0,c1,c2) # and explore the parameter space. # Also do a FP trick -0.0 + 0.0 = 0.0 - return np.round([G1.real, G1.imag, G2.real], 12) + 0.0 + return np.round([G1.real, G1.imag, G2.real], 9) + 0.0 def local_equivalence(weyl: np.ndarray) -> np.ndarray: diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 3269797827e2..45aa1daa0b90 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -113,7 +113,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): temp = np.kron(L, R) deviation = abs(abs(temp.conj().T.dot(special_unitary_matrix).trace()) - 4) - if deviation > 1.0e-13: + if deviation > 1.0e-8: raise QiskitError( "decompose_two_qubit_product_gate: decomposition failed: " f"deviation too large: {deviation}" From 8aa570dbab09deb920cf665a97a75aba6fce82f2 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:20:19 +0200 Subject: [PATCH 24/46] up --- .../synthesis/two_qubit/local_invariance.py | 2 +- .../two_qubit/two_qubit_decompose.py | 2 +- .../passes/optimization/split_2q_unitaries.py | 11 ++++++-- .../transpiler/test_split_2q_unitaries.py | 27 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/qiskit/synthesis/two_qubit/local_invariance.py b/qiskit/synthesis/two_qubit/local_invariance.py index 3f74b0d0d710..fa665ed449e6 100644 --- a/qiskit/synthesis/two_qubit/local_invariance.py +++ b/qiskit/synthesis/two_qubit/local_invariance.py @@ -65,7 +65,7 @@ def two_qubit_local_invariants(U: np.ndarray) -> np.ndarray: # to better equate to the Weyl chamber coordinates (c0,c1,c2) # and explore the parameter space. # Also do a FP trick -0.0 + 0.0 = 0.0 - return np.round([G1.real, G1.imag, G2.real], 9) + 0.0 + return np.round([G1.real, G1.imag, G2.real], 13) + 0.0 def local_equivalence(weyl: np.ndarray) -> np.ndarray: diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 45aa1daa0b90..3269797827e2 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -113,7 +113,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): temp = np.kron(L, R) deviation = abs(abs(temp.conj().T.dot(special_unitary_matrix).trace()) - 4) - if deviation > 1.0e-8: + if deviation > 1.0e-13: raise QiskitError( "decompose_two_qubit_product_gate: decomposition failed: " f"deviation too large: {deviation}" diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index cdd0401b4556..e108f81efd9c 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -27,7 +27,9 @@ class Split2QUnitaries(TransformationPass): def run(self, dag: DAGCircuit): """Run the Split2QUnitaries pass on `dag`.""" sq_id = np.eye(2) - for node in dag.topological_op_nodes(): + identity = np.eye(2 ** 2) + + for i, node in enumerate(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 @@ -37,8 +39,13 @@ def run(self, dag: DAGCircuit): ): continue - # check if the node can be represented by single-qubit gates nmat = node.matrix + + if np.allclose(nmat, identity, atol=1e-8, rtol=0): + dag.remove_op_node(node) + continue + + # check if the node can be represented by single-qubit gates local_invariants = two_qubit_local_invariants(nmat) if local_invariants[0] == 1 and local_invariants[1] == 0 and local_invariants[2] == 3: ul, ur, phase = decompose_two_qubit_product_gate(nmat) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index b649a8af24db..a26c91275004 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -13,6 +13,8 @@ """ Tests for the Split2QUnitaries transpiler pass. """ +from math import pi + from test import QiskitTestCase from qiskit import QuantumCircuit @@ -139,3 +141,28 @@ def test_no_split(self): 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()) + qc_split = pm.run(qc) + self.assertEqual(qc_split.num_nonlocal_gates(), 0) + + def test_split_qft(self): + """Test that the pass handles QFT correctly.""" + #2**-26 + qc = QuantumCircuit(31) + qc.h(0) + for i in range(29, 0, -1): + qc.cp(pi*2**-(30-i), 30, i) + pm = PassManager() + pm.append(Collect2qBlocks()) + pm.append(ConsolidateBlocks()) + pm.append(Split2QUnitaries()) + qc_split = pm.run(qc) + self.assertEqual(qc_split.num_nonlocal_gates(), 23) From c84fa16845b393cc1efe59b31854cf841c0ae196 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:08:33 +0200 Subject: [PATCH 25/46] up --- .../passes/optimization/split_2q_unitaries.py | 4 ++-- test/python/transpiler/test_preset_passmanagers.py | 14 ++------------ test/python/transpiler/test_split_2q_unitaries.py | 6 +++--- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index e108f81efd9c..b6254ddbb6e5 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -27,9 +27,9 @@ class Split2QUnitaries(TransformationPass): def run(self, dag: DAGCircuit): """Run the Split2QUnitaries pass on `dag`.""" sq_id = np.eye(2) - identity = np.eye(2 ** 2) + identity = np.eye(2**2) - for i, node in enumerate(dag.topological_op_nodes()): + 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 diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index e87e9b6c4d97..6704600b48b6 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -127,14 +127,14 @@ def test_layout_3239(self, level=3): qc.measure(range(4), range(4)) result = transpile( qc, - basis_gates=["h", "z", "cx"], + basis_gates=["u1", "u2", "u3", "cx"], layout_method="trivial", optimization_level=level, ) dag = circuit_to_dag(result) op_nodes = [node.name for node in dag.topological_op_nodes()] - self.assertNotIn("z", op_nodes) # Check if the diagonal Z-Gates (u1) were removed + self.assertNotIn("u1", op_nodes) # Check if the diagonal Z-Gates (u1) were removed @combine(level=[0, 1, 2, 3], name="level{level}") def test_no_basis_gates(self, level): @@ -1456,16 +1456,6 @@ def _define(self): self._definition = QuantumCircuit(2) self._definition.cx(0, 1) - def to_matrix(self) -> np.ndarray: - return np.asarray( - [ - [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - ] - ) - circuit = QuantumCircuit(6, 1) circuit.h(0) circuit.measure(0, 0) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index a26c91275004..b3e64c6b68e4 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -145,7 +145,7 @@ def test_no_split(self): def test_almost_identity(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(2) - qc.cp(pi*2**-(26), 0, 1) + qc.cp(pi * 2 ** -(26), 0, 1) pm = PassManager() pm.append(Collect2qBlocks()) pm.append(ConsolidateBlocks()) @@ -155,11 +155,11 @@ def test_almost_identity(self): def test_split_qft(self): """Test that the pass handles QFT correctly.""" - #2**-26 + # 2**-26 qc = QuantumCircuit(31) qc.h(0) for i in range(29, 0, -1): - qc.cp(pi*2**-(30-i), 30, i) + qc.cp(pi * 2 ** -(30 - i), 30, i) pm = PassManager() pm.append(Collect2qBlocks()) pm.append(ConsolidateBlocks()) From b8fea91e585c457fb735ed1b9e2caa1774953ce9 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:46:24 +0200 Subject: [PATCH 26/46] update --- .../passes/optimization/split_2q_unitaries.py | 39 +++++++++++-------- .../transpiler/test_split_2q_unitaries.py | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index b6254ddbb6e5..72b2bf8d8e2d 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -17,17 +17,28 @@ from qiskit.circuit.library import UnitaryGate from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants -from qiskit.synthesis.two_qubit.two_qubit_decompose import decompose_two_qubit_product_gate +from qiskit.synthesis.two_qubit.two_qubit_decompose import ( + decompose_two_qubit_product_gate, + TwoQubitWeylDecomposition, +) from qiskit.transpiler import TransformationPass class Split2QUnitaries(TransformationPass): """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" + def __init__(self, fidelity: float | None = 1.0 - 1.0e-9): + """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`.""" sq_id = np.eye(2) - identity = np.eye(2**2) for node in dag.topological_op_nodes(): # skip operations without two-qubits and for which we can not determine a potential 1q split @@ -39,18 +50,13 @@ def run(self, dag: DAGCircuit): ): continue - nmat = node.matrix - - if np.allclose(nmat, identity, atol=1e-8, rtol=0): - dag.remove_op_node(node) - continue - - # check if the node can be represented by single-qubit gates - local_invariants = two_qubit_local_invariants(nmat) - if local_invariants[0] == 1 and local_invariants[1] == 0 and local_invariants[2] == 3: - ul, ur, phase = decompose_two_qubit_product_gate(nmat) - - if not np.allclose(ur, sq_id): + decomp = TwoQubitWeylDecomposition(node.op, fidelity=self.requested_fidelity) + if ( + decomp._inner_decomposition.specialization + == TwoQubitWeylDecomposition._specializations.IdEquiv + ): + ur = decomp.K1r + if not np.allclose(ur, sq_id, atol=self.requested_fidelity, rtol=0): ur_node = DAGOpNode.from_instruction( CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=dag ) @@ -58,7 +64,8 @@ def run(self, dag: DAGCircuit): dag._increment_op("unitary") dag._multi_graph.insert_node_on_in_edges(ur_node._node_id, node._node_id) - if not np.allclose(ul, sq_id): + ul = decomp.K1l + if not np.allclose(ul, sq_id, atol=self.requested_fidelity, rtol=0): ul_node = DAGOpNode.from_instruction( CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=dag ) @@ -66,7 +73,7 @@ def run(self, dag: DAGCircuit): dag._increment_op("unitary") dag._multi_graph.insert_node_on_in_edges(ul_node._node_id, node._node_id) - dag.global_phase += phase + dag.global_phase += decomp.global_phase dag.remove_op_node(node) return dag diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index b3e64c6b68e4..2aebe1c83a8d 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -165,4 +165,4 @@ def test_split_qft(self): pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries()) qc_split = pm.run(qc) - self.assertEqual(qc_split.num_nonlocal_gates(), 23) + self.assertEqual(14, qc_split.num_nonlocal_gates()) From 6137daa472f15f2106f13ace6514c5d0a415535b Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:58:22 +0200 Subject: [PATCH 27/46] up --- .../passes/optimization/split_2q_unitaries.py | 39 +++++++------------ .../transpiler/test_split_2q_unitaries.py | 24 +++++++----- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 72b2bf8d8e2d..d446dfd80d20 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -10,24 +10,19 @@ # 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.""" - -import numpy as np +from typing import Optional from qiskit.circuit import CircuitInstruction from qiskit.circuit.library import UnitaryGate from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants -from qiskit.synthesis.two_qubit.two_qubit_decompose import ( - decompose_two_qubit_product_gate, - TwoQubitWeylDecomposition, -) +from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition from qiskit.transpiler import TransformationPass class Split2QUnitaries(TransformationPass): """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" - def __init__(self, fidelity: float | None = 1.0 - 1.0e-9): + def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): """Split2QUnitaries initializer. Args: @@ -38,8 +33,6 @@ def __init__(self, fidelity: float | None = 1.0 - 1.0e-9): def run(self, dag: DAGCircuit): """Run the Split2QUnitaries pass on `dag`.""" - sq_id = np.eye(2) - for node in dag.topological_op_nodes(): # skip operations without two-qubits and for which we can not determine a potential 1q split if ( @@ -56,22 +49,20 @@ def run(self, dag: DAGCircuit): == TwoQubitWeylDecomposition._specializations.IdEquiv ): ur = decomp.K1r - if not np.allclose(ur, sq_id, atol=self.requested_fidelity, rtol=0): - ur_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=dag - ) - ur_node._node_id = dag._multi_graph.add_node(ur_node) - dag._increment_op("unitary") - dag._multi_graph.insert_node_on_in_edges(ur_node._node_id, node._node_id) + ur_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=dag + ) + ur_node._node_id = dag._multi_graph.add_node(ur_node) + dag._increment_op("unitary") + dag._multi_graph.insert_node_on_in_edges(ur_node._node_id, node._node_id) ul = decomp.K1l - if not np.allclose(ul, sq_id, atol=self.requested_fidelity, rtol=0): - ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=dag - ) - ul_node._node_id = dag._multi_graph.add_node(ul_node) - dag._increment_op("unitary") - dag._multi_graph.insert_node_on_in_edges(ul_node._node_id, node._node_id) + ul_node = DAGOpNode.from_instruction( + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=dag + ) + ul_node._node_id = dag._multi_graph.add_node(ul_node) + dag._increment_op("unitary") + dag._multi_graph.insert_node_on_in_edges(ul_node._node_id, node._node_id) dag.global_phase += decomp.global_phase dag.remove_op_node(node) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 2aebe1c83a8d..430f74c915ae 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -71,7 +71,7 @@ def test_2q_identity(self): self.assertTrue( matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) ) - self.assertEqual(qc_split.size(), 0) + 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.""" @@ -92,7 +92,7 @@ def test_1q_identity(self): self.assertTrue( matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) ) - self.assertEqual(qc_split.size(), 1) + 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.""" @@ -113,7 +113,7 @@ def test_1q_identity2(self): self.assertTrue( matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) ) - self.assertEqual(qc_split.size(), 1) + self.assertEqual(qc_split.size(), 2) def test_no_split(self): """Test that the pass does not split a non-local two-qubit unitary.""" @@ -146,23 +146,29 @@ def test_almost_identity(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(2) qc.cp(pi * 2 ** -(26), 0, 1) + print(Operator(qc).data) pm = PassManager() pm.append(Collect2qBlocks()) pm.append(ConsolidateBlocks()) - pm.append(Split2QUnitaries()) + 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_split_qft(self): """Test that the pass handles QFT correctly.""" - # 2**-26 - qc = QuantumCircuit(31) + qc = QuantumCircuit(100) qc.h(0) - for i in range(29, 0, -1): - qc.cp(pi * 2 ** -(30 - i), 30, i) + 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(14, qc_split.num_nonlocal_gates()) + self.assertEqual(26, qc_split.num_nonlocal_gates()) From a6ea05b08443df33d37721029b4b649b5d0a2edf Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:02:55 +0200 Subject: [PATCH 28/46] . --- qiskit/synthesis/two_qubit/local_invariance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/two_qubit/local_invariance.py b/qiskit/synthesis/two_qubit/local_invariance.py index fa665ed449e6..346febdddf9f 100644 --- a/qiskit/synthesis/two_qubit/local_invariance.py +++ b/qiskit/synthesis/two_qubit/local_invariance.py @@ -65,7 +65,7 @@ def two_qubit_local_invariants(U: np.ndarray) -> np.ndarray: # to better equate to the Weyl chamber coordinates (c0,c1,c2) # and explore the parameter space. # Also do a FP trick -0.0 + 0.0 = 0.0 - return np.round([G1.real, G1.imag, G2.real], 13) + 0.0 + return np.round([G1.real, G1.imag, G2.real], 12) + 0.0 def local_equivalence(weyl: np.ndarray) -> np.ndarray: From c6f4530646979984a0074c99acac133b10c6dd5f Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Sun, 28 Jul 2024 22:27:55 +0200 Subject: [PATCH 29/46] cyclic import --- qiskit/transpiler/passes/optimization/split_2q_unitaries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index d446dfd80d20..48980d4f6be4 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -12,11 +12,11 @@ """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" from typing import Optional +from qiskit.transpiler import TransformationPass from qiskit.circuit import CircuitInstruction -from qiskit.circuit.library import UnitaryGate from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.circuit.library import UnitaryGate from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition -from qiskit.transpiler import TransformationPass class Split2QUnitaries(TransformationPass): From 6ec02fc6b82da24c1a998e585cf961733cfa41e3 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:46:22 +0200 Subject: [PATCH 30/46] cycl import --- qiskit/transpiler/passes/optimization/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index a9796850a689..082cb3f67ec9 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,4 +38,3 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated -from .split_2q_unitaries import Split2QUnitaries From 44f2f38130250d3562ec51efb0d016f0d4b60d20 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:02:41 +0200 Subject: [PATCH 31/46] cyl import --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 2 +- test/python/transpiler/test_split_2q_unitaries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 32fd977799a8..f7ba37354553 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -13,6 +13,7 @@ """Built-in transpiler stage plugins for preset pass managers.""" 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 @@ -40,7 +41,6 @@ Collect2qBlocks, ConsolidateBlocks, InverseCancellation, - Split2QUnitaries, ) from qiskit.transpiler.passes import Depth, Size, FixedPoint, MinimumPoint from qiskit.transpiler.passes.utils.gates_basis import GatesInBasis diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 430f74c915ae..95845544c91d 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -15,6 +15,7 @@ """ from math import pi +from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from test import QiskitTestCase from qiskit import QuantumCircuit @@ -23,7 +24,6 @@ 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 import Split2QUnitaries class TestSplit2QUnitaries(QiskitTestCase): From df0897f98ae07b975883df3c081bbfe92cc233c0 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:38:03 +0200 Subject: [PATCH 32/46] . --- .../passes/optimization/split_2q_unitaries.py | 5 +++- .../transpiler/test_split_2q_unitaries.py | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 48980d4f6be4..db9f928e2a26 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -66,5 +66,8 @@ def run(self, dag: DAGCircuit): dag.global_phase += decomp.global_phase dag.remove_op_node(node) - + elif ( decomp._inner_decomposition.specialization + == TwoQubitWeylDecomposition._specializations.SWAPEquiv): + # TODO also look at swap-gate-like gates? + pass return dag diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 95845544c91d..a9380e1e3a80 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -15,10 +15,13 @@ """ from math import pi +import numpy as np + +from qiskit.providers.basic_provider import BasicSimulator from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from test import QiskitTestCase -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, QuantumRegister, transpile from qiskit.circuit.library import UnitaryGate from qiskit.quantum_info import Operator from qiskit.transpiler import PassManager @@ -115,6 +118,24 @@ def test_1q_identity2(self): ) self.assertEqual(qc_split.size(), 2) + def test_2_1q(self): + """Test that a Kronechker 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 = BasicSimulator() + + qc = QuantumCircuit(qr) + qc.unitary(multi_x, qr) + #pm = generate_preset_pass_manager(optimization_level=2, backend=backend) + qct = transpile(qc, backend) + + 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) @@ -172,3 +193,5 @@ def test_split_qft(self): pm.append(Split2QUnitaries()) qc_split = pm.run(qc) self.assertEqual(26, qc_split.num_nonlocal_gates()) + + From 66d0a2219a03a7445ea1066a3ca8af87e6d5a5f9 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:59:51 +0200 Subject: [PATCH 33/46] circular import --- qiskit/transpiler/passes/optimization/__init__.py | 1 + .../transpiler/passes/optimization/split_2q_unitaries.py | 8 ++++---- test/python/transpiler/test_split_2q_unitaries.py | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) 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 index db9f928e2a26..de5472db9db2 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -12,10 +12,10 @@ """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" from typing import Optional -from qiskit.transpiler import TransformationPass -from qiskit.circuit import CircuitInstruction -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.circuit.library import UnitaryGate +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 diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index a9380e1e3a80..5ccc96dbd5b3 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.providers.basic_provider import BasicSimulator +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from test import QiskitTestCase @@ -124,10 +125,8 @@ def test_2_1q(self): multi_x = np.kron(x_mat, x_mat) qr = QuantumRegister(2, "qr") backend = BasicSimulator() - qc = QuantumCircuit(qr) qc.unitary(multi_x, qr) - #pm = generate_preset_pass_manager(optimization_level=2, backend=backend) qct = transpile(qc, backend) self.assertTrue(Operator(qc).equiv(qct)) From aff157b61dac907bf8bf65936125665d3895ec47 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:26:12 +0200 Subject: [PATCH 34/46] . --- .../passes/optimization/split_2q_unitaries.py | 6 ++++-- test/python/transpiler/test_split_2q_unitaries.py | 14 ++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index de5472db9db2..930164b003be 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -66,8 +66,10 @@ def run(self, dag: DAGCircuit): dag.global_phase += decomp.global_phase dag.remove_op_node(node) - elif ( decomp._inner_decomposition.specialization - == TwoQubitWeylDecomposition._specializations.SWAPEquiv): + elif ( + decomp._inner_decomposition.specialization + == TwoQubitWeylDecomposition._specializations.SWAPEquiv + ): # TODO also look at swap-gate-like gates? pass return dag diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 5ccc96dbd5b3..593c312c433d 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -15,12 +15,9 @@ """ from math import pi +from test import QiskitTestCase import numpy as np -from qiskit.providers.basic_provider import BasicSimulator -from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries -from test import QiskitTestCase from qiskit import QuantumCircuit, QuantumRegister, transpile from qiskit.circuit.library import UnitaryGate @@ -28,7 +25,8 @@ from qiskit.transpiler import PassManager from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.transpiler.passes import Collect2qBlocks, ConsolidateBlocks - +from qiskit.providers.basic_provider import BasicSimulator +from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries class TestSplit2QUnitaries(QiskitTestCase): """ @@ -130,9 +128,7 @@ def test_2_1q(self): qct = transpile(qc, backend) self.assertTrue(Operator(qc).equiv(qct)) - self.assertTrue( - matrix_equal(Operator(qc).data, Operator(qct).data, ignore_phase=False) - ) + self.assertTrue(matrix_equal(Operator(qc).data, Operator(qct).data, ignore_phase=False)) self.assertEqual(qct.size(), 2) def test_no_split(self): @@ -192,5 +188,3 @@ def test_split_qft(self): pm.append(Split2QUnitaries()) qc_split = pm.run(qc) self.assertEqual(26, qc_split.num_nonlocal_gates()) - - From c333f5bfaa009def730501364171e70ebf6f64ca Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:32:53 +0200 Subject: [PATCH 35/46] lint --- test/python/transpiler/test_split_2q_unitaries.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 593c312c433d..88f20639af49 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -28,6 +28,7 @@ from qiskit.providers.basic_provider import BasicSimulator 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. From 1bdf8604d4f180710650e64fb1db8a4793e4c16c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Jul 2024 16:19:39 -0400 Subject: [PATCH 36/46] Include new pass in docs --- qiskit/transpiler/passes/__init__.py | 2 ++ 1 file changed, 2 insertions(+) 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 From a28a564052e183df8e1188b3eaf7c8f56d9e56af Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Jul 2024 17:14:14 -0400 Subject: [PATCH 37/46] 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. --- .../passes/optimization/split_2q_unitaries.py | 20 +++++++++---------- .../transpiler/test_split_2q_unitaries.py | 1 - 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 930164b003be..245c338bd5a6 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -48,24 +48,22 @@ def run(self, dag: DAGCircuit): 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=dag + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag ) - ur_node._node_id = dag._multi_graph.add_node(ur_node) - dag._increment_op("unitary") - dag._multi_graph.insert_node_on_in_edges(ur_node._node_id, node._node_id) ul = decomp.K1l ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=dag + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag ) - ul_node._node_id = dag._multi_graph.add_node(ul_node) - dag._increment_op("unitary") - dag._multi_graph.insert_node_on_in_edges(ul_node._node_id, node._node_id) - - dag.global_phase += decomp.global_phase - dag.remove_op_node(node) + 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 diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 88f20639af49..abec69fae6f6 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -163,7 +163,6 @@ def test_almost_identity(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(2) qc.cp(pi * 2 ** -(26), 0, 1) - print(Operator(qc).data) pm = PassManager() pm.append(Collect2qBlocks()) pm.append(ConsolidateBlocks()) From 6eaed5d3552c448f48d2b17abb0e41d567a322d5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Jul 2024 17:25:55 -0400 Subject: [PATCH 38/46] Update releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml --- ...phole-before-routing-c3d184b740bb7a8b.yaml | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml index 8b94170701f6..b89a622987d0 100644 --- a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml +++ b/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml @@ -5,14 +5,16 @@ features_transpiler: 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. - | - In addition, - the passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` are - added to the `init` stage of the preset pass managers with optimization level 2 and optimization level 3 - if the pass managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates - have are not parameterized. The modification of the `init` stage should allow for a more efficient routing - for quantum circuits that (a) contain two-qubit unitaries/gates that are actually a product of single-qubit gates - or (b) 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. + 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. From 9964d65a69b2ce136859b618e5399e519eeb99bd Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:30:24 +0200 Subject: [PATCH 39/46] stricter check for doing split2q --- .../passes/optimization/split_2q_unitaries.py | 5 ++++- .../preset_passmanagers/builtin_plugins.py | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 245c338bd5a6..e0480b32e926 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -68,6 +68,9 @@ def run(self, dag: DAGCircuit): decomp._inner_decomposition.specialization == TwoQubitWeylDecomposition._specializations.SWAPEquiv ): - # TODO also look at swap-gate-like gates? + # 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 c29ae490180d..becf7ecfb699 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -73,7 +73,15 @@ CONFIG = user_config.get_config() -_discrete_skipped_ops = {"delay", "reset", "measure"} +_discrete_skipped_ops = { + "delay", + "reset", + "measure", + "switch_case", + "if_else", + "for_loop", + "while_loop", +} class DefaultInitPassManager(PassManagerStagePlugin): @@ -181,11 +189,10 @@ def _is_one_op_non_discrete(ops): if isinstance(op, str): op = stdgates.get(op, None) - if ( - op is None - or not isinstance(op, Instruction) - or op.name in _discrete_skipped_ops - ): + if op is None or not isinstance(op, Instruction): + return False + + if op.name in _discrete_skipped_ops: continue if len(op.params) > 0: @@ -207,7 +214,7 @@ def _is_one_op_non_discrete(ops): if do_consolidate_blocks_init: init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) - init.append(Split2QUnitaries()) + init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init From b90f9f6032d80ed5ea4fcafedbcac940e16577cb Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:38:09 +0200 Subject: [PATCH 40/46] Update qiskit/transpiler/preset_passmanagers/builtin_plugins.py Co-authored-by: Matthew Treinish --- qiskit/transpiler/preset_passmanagers/builtin_plugins.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index becf7ecfb699..64aac334f9a3 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -214,7 +214,14 @@ def _is_one_op_non_discrete(ops): if do_consolidate_blocks_init: init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) - init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) + # 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 From a7d3594ec81ec550837224a0fd32cfe611f774a8 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:19:00 +0200 Subject: [PATCH 41/46] code review --- .../preset_passmanagers/builtin_plugins.py | 13 ++++++----- .../transpiler/test_split_2q_unitaries.py | 22 +++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 64aac334f9a3..d7e6a3b2c174 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -185,19 +185,22 @@ def _is_one_op_non_discrete(ops): 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 op.name in _discrete_skipped_ops: - continue - if len(op.params) > 0: - return True - return False + found_one_continuous_gate = True + return found_one_continuous_gate target = pass_manager_config.target basis = pass_manager_config.basis_gates diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index abec69fae6f6..7835c93ae933 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -15,6 +15,7 @@ """ from math import pi +from qiskit.providers.fake_provider import GenericBackendV2 from test import QiskitTestCase import numpy as np @@ -119,15 +120,14 @@ def test_1q_identity2(self): self.assertEqual(qc_split.size(), 2) def test_2_1q(self): - """Test that a Kronechker product of two X gates is correctly processed.""" + """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 = BasicSimulator() + backend = GenericBackendV2(2) qc = QuantumCircuit(qr) qc.unitary(multi_x, qr) - qct = transpile(qc, backend) - + 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) @@ -176,6 +176,20 @@ def test_almost_identity(self): self.assertEqual(qc_split.num_nonlocal_gates(), 0) 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.""" + qc = QuantumCircuit(5) + 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) + def test_split_qft(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(100) From 89ac7dc518e82583ecc7f10d9fdc0711f1f48429 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:19:18 +0200 Subject: [PATCH 42/46] Update qiskit/transpiler/passes/optimization/split_2q_unitaries.py Co-authored-by: Matthew Treinish --- .../transpiler/passes/optimization/split_2q_unitaries.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index e0480b32e926..d363a8e14409 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -20,7 +20,13 @@ class Split2QUnitaries(TransformationPass): - """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" + """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`\s. def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): """Split2QUnitaries initializer. From 229b028fd0beebe418caa4d71a83c8ee366d2ab5 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:27:15 +0200 Subject: [PATCH 43/46] new tests --- .../transpiler/test_split_2q_unitaries.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 7835c93ae933..616d93e5b3f8 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -14,19 +14,17 @@ Tests for the Split2QUnitaries transpiler pass. """ from math import pi - -from qiskit.providers.fake_provider import GenericBackendV2 from test import QiskitTestCase import numpy as np - from qiskit import QuantumCircuit, QuantumRegister, transpile -from qiskit.circuit.library import UnitaryGate +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.providers.basic_provider import BasicSimulator from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries @@ -176,9 +174,28 @@ def test_almost_identity(self): 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.""" - qc = QuantumCircuit(5) + qr = QuantumRegister(5) + qc = QuantumCircuit(qr) qc.x(0) qc.z(1) qc.h(2) @@ -190,6 +207,10 @@ def test_single_q_gates(self): 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) From 48eacd7636d421444b72e1b0fa78d10b214970ed Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:31:38 +0200 Subject: [PATCH 44/46] typo --- qiskit/transpiler/passes/optimization/split_2q_unitaries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index d363a8e14409..d4cacc9a4e26 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -21,12 +21,13 @@ 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`\s. + """ def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): """Split2QUnitaries initializer. From edde6118988cc8853d673fe421a6152529e121b7 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:39:25 +0200 Subject: [PATCH 45/46] lint --- qiskit/transpiler/passes/optimization/split_2q_unitaries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index d4cacc9a4e26..9512ffbd4a5f 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -26,7 +26,7 @@ class Split2QUnitaries(TransformationPass): 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`\s. + :class:`.UnitaryGate`\ s. """ def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): From 5404bbc2f4592b8a93ea554fab1d6cf8f9960752 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:45:49 +0200 Subject: [PATCH 46/46] lint --- qiskit/transpiler/passes/optimization/split_2q_unitaries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 9512ffbd4a5f..7508c9440a6e 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -26,7 +26,7 @@ class Split2QUnitaries(TransformationPass): 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`\ s. + :class:`.UnitaryGate`. """ def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16):