diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 8568b8eac1a2..c0cb58b3f63e 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -177,6 +177,7 @@ GatesInBasis ConvertConditionsToIfOps UnrollForLoops + FilterOpNodes """ # layout selection (placement) @@ -292,3 +293,4 @@ from .utils import GatesInBasis from .utils import ConvertConditionsToIfOps from .utils import UnrollForLoops +from .utils import FilterOpNodes diff --git a/qiskit/transpiler/passes/utils/__init__.py b/qiskit/transpiler/passes/utils/__init__.py index c5d605f7d732..7227409a213c 100644 --- a/qiskit/transpiler/passes/utils/__init__.py +++ b/qiskit/transpiler/passes/utils/__init__.py @@ -27,6 +27,7 @@ from .convert_conditions_to_if_ops import ConvertConditionsToIfOps from .unroll_forloops import UnrollForLoops from .minimum_point import MinimumPoint +from .filter_op_nodes import FilterOpNodes # Utility functions from . import control_flow diff --git a/qiskit/transpiler/passes/utils/barrier_before_final_measurements.py b/qiskit/transpiler/passes/utils/barrier_before_final_measurements.py index 2222ef064010..4633cc57af54 100644 --- a/qiskit/transpiler/passes/utils/barrier_before_final_measurements.py +++ b/qiskit/transpiler/passes/utils/barrier_before_final_measurements.py @@ -27,6 +27,10 @@ class BarrierBeforeFinalMeasurements(TransformationPass): other measurements or barriers.) """ + def __init__(self, label=None): + super().__init__() + self.label = label + def run(self, dag): """Run the BarrierBeforeFinalMeasurements pass on `dag`.""" # Collect DAG nodes which are followed only by barriers or other measures. @@ -64,7 +68,7 @@ def run(self, dag): final_qubits = dag.qubits barrier_layer.apply_operation_back( - Barrier(len(final_qubits)), final_qubits, (), check=False + Barrier(len(final_qubits), label=self.label), final_qubits, (), check=False ) # Preserve order of final ops collected earlier from the original DAG. @@ -83,6 +87,9 @@ def run(self, dag): dag.compose(barrier_layer) - # Merge the new barrier into any other barriers - adjacent_pass = MergeAdjacentBarriers() - return adjacent_pass.run(dag) + if self.label is None: + # Merge the new barrier into any other barriers + adjacent_pass = MergeAdjacentBarriers() + return adjacent_pass.run(dag) + else: + return dag diff --git a/qiskit/transpiler/passes/utils/filter_op_nodes.py b/qiskit/transpiler/passes/utils/filter_op_nodes.py new file mode 100644 index 000000000000..344d2280e3f4 --- /dev/null +++ b/qiskit/transpiler/passes/utils/filter_op_nodes.py @@ -0,0 +1,65 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +"""Filter ops from a circuit""" + +from typing import Callable + +from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.utils import control_flow + + +class FilterOpNodes(TransformationPass): + """Remove all operations that match a filter function + + This transformation pass is used to remove any operations that matches a + the provided filter function. + + Args: + predicate: A given callable that will be passed the :class:`.DAGOpNode` + for each node in the :class:`.DAGCircuit`. If the callable returns + ``True`` the :class:`.DAGOpNode` is retained in the circuit and if it + returns ``False`` it is removed from the circuit. + + Example: + + Filter out operations that are labelled ``"foo"`` + + .. plot:: + :include-source: + + from qiskit import QuantumCircuit + from qiskit.transpiler.passes import FilterOpNodes + + circuit = QuantumCircuit(1) + circuit.x(0, label='foo') + circuit.barrier() + circuit.h(0) + + circuit = FilterOpNodes( + lambda node: getattr(node.op, "label") != "foo" + )(circuit) + circuit.draw('mpl') + """ + + def __init__(self, predicate: Callable[[DAGOpNode], bool]): + super().__init__() + self.predicate = predicate + + @control_flow.trivial_recurse + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the RemoveBarriers pass on `dag`.""" + for node in dag.op_nodes(): + if not self.predicate(node): + 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 ca21bd2a3b57..a473fdf6533d 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -684,7 +684,13 @@ def _swap_mapped(property_set): and pass_manager_config.routing_method != "sabre", ) layout.append( - [BarrierBeforeFinalMeasurements(), choose_layout_2], condition=_vf2_match_not_found + [ + BarrierBeforeFinalMeasurements( + "qiskit.transpiler.internal.routing.protection.barrier" + ), + choose_layout_2, + ], + condition=_vf2_match_not_found, ) elif optimization_level == 2: choose_layout_0 = VF2Layout( @@ -706,7 +712,13 @@ def _swap_mapped(property_set): and pass_manager_config.routing_method != "sabre", ) layout.append( - [BarrierBeforeFinalMeasurements(), choose_layout_1], condition=_vf2_match_not_found + [ + BarrierBeforeFinalMeasurements( + "qiskit.transpiler.internal.routing.protection.barrier" + ), + choose_layout_1, + ], + condition=_vf2_match_not_found, ) elif optimization_level == 3: choose_layout_0 = VF2Layout( @@ -728,7 +740,13 @@ def _swap_mapped(property_set): and pass_manager_config.routing_method != "sabre", ) layout.append( - [BarrierBeforeFinalMeasurements(), choose_layout_1], condition=_vf2_match_not_found + [ + BarrierBeforeFinalMeasurements( + "qiskit.transpiler.internal.routing.protection.barrier" + ), + choose_layout_1, + ], + condition=_vf2_match_not_found, ) else: raise TranspilerError(f"Invalid optimization level: {optimization_level}") @@ -882,7 +900,13 @@ def _swap_mapped(property_set): else: raise TranspilerError(f"Invalid optimization level: {optimization_level}") layout.append( - [BarrierBeforeFinalMeasurements(), layout_pass], condition=_choose_layout_condition + [ + BarrierBeforeFinalMeasurements( + "qiskit.transpiler.internal.routing.protection.barrier" + ), + layout_pass, + ], + condition=_choose_layout_condition, ) embed = common.generate_embed_passmanager(coupling_map) layout.append( diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 494aa7a2159e..c5402855a2ba 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -41,6 +41,7 @@ from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passes import RemoveResetInZeroState +from qiskit.transpiler.passes import FilterOpNodes from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PadDelay from qiskit.transpiler.passes import InstructionDurationCheck @@ -328,7 +329,15 @@ def _swap_condition(property_set): return not property_set["routing_not_needed"] if use_barrier_before_measurement: - routing.append([BarrierBeforeFinalMeasurements(), routing_pass], condition=_swap_condition) + routing.append( + [ + BarrierBeforeFinalMeasurements( + label="qiskit.transpiler.internal.routing.protection.barrier" + ), + routing_pass, + ], + condition=_swap_condition, + ) else: routing.append([routing_pass], condition=_swap_condition) @@ -348,6 +357,14 @@ def _swap_condition(property_set): ) routing.append(ApplyLayout(), condition=_apply_post_layout_condition) + def filter_fn(node): + return ( + getattr(node.op, "label", None) + != "qiskit.transpiler.internal.routing.protection.barrier" + ) + + routing.append([FilterOpNodes(filter_fn)]) + return routing diff --git a/releasenotes/notes/add-filter-op-nodes-aa024a0f1058e4b7.yaml b/releasenotes/notes/add-filter-op-nodes-aa024a0f1058e4b7.yaml new file mode 100644 index 000000000000..9c5a0a5c43f8 --- /dev/null +++ b/releasenotes/notes/add-filter-op-nodes-aa024a0f1058e4b7.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added a new transpiler pass :class:`.FilterOpNodes` which is used to filter + :class:`.DAGOpNode`\s in a :class:`.DAGCircuit`. + - | + Added a new keyword argument, ``label``, to the constructor on the + :class:`.BarrierBeforeFinalMeasurements` transpiler pass. If specified the + inserted barrier will be assigned the specified label. This also prevents + the inserted barrier from being merged with any any other pre-existing + adjacent barriers. +other: + - | + The preset pass managers used by :func:`.transpile` and returned with + :class:`.generate_preset_pass_manager` will no longer insert barriers + before final measurements in the output circuits. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 98db50a28b08..2a13f35adfb9 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1696,6 +1696,38 @@ def test_paulis_to_constrained_1q_basis(self, opt_level, basis): self.assertGreaterEqual(set(basis) | {"barrier"}, transpiled.count_ops().keys()) self.assertEqual(Operator(qc), Operator(transpiled)) + @data(0, 1, 2, 3) + def test_barrier_not_output(self, opt_level): + """Test that barriers added as part internal transpiler operations do not leak out.""" + qc = QuantumCircuit(2, 2) + qc.cx(0, 1) + qc.measure(range(2), range(2)) + tqc = transpile( + qc, + initial_layout=[1, 4], + coupling_map=[[1, 2], [2, 3], [3, 4]], + optimization_level=opt_level, + ) + self.assertNotIn("barrier", tqc.count_ops()) + + @data(0, 1, 2, 3) + def test_barrier_not_output_input_preservered(self, opt_level): + """Test that barriers added as part internal transpiler operations do not leak out.""" + qc = QuantumCircuit(2, 2) + qc.cx(0, 1) + qc.measure_all() + tqc = transpile( + qc, + initial_layout=[1, 4], + coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4]], + optimization_level=opt_level, + ) + op_counts = tqc.count_ops() + self.assertEqual(op_counts["barrier"], 1) + for inst in tqc.data: + if inst.operation.name == "barrier": + self.assertEqual(len(inst.qubits), 2) + @combine(opt_level=[0, 1, 2, 3]) def test_transpile_annotated_ops(self, opt_level): """Test transpilation of circuits with annotated operations.""" diff --git a/test/python/transpiler/test_filter_op_nodes.py b/test/python/transpiler/test_filter_op_nodes.py new file mode 100644 index 000000000000..0084dd35d4c0 --- /dev/null +++ b/test/python/transpiler/test_filter_op_nodes.py @@ -0,0 +1,78 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# 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. + +"""FilterOpNodes pass testing""" + + +from qiskit import QuantumCircuit +from qiskit.transpiler.passes import FilterOpNodes +from qiskit.test import QiskitTestCase + + +class TestFilterOpNodes(QiskitTestCase): + """Tests for FilterOpNodes transformation pass.""" + + def test_empty_circuit(self): + """Empty DAG has does nothing.""" + circuit = QuantumCircuit() + self.assertEqual(FilterOpNodes(lambda x: False)(circuit), circuit) + + def test_remove_x_gate(self): + """Test filter removes matching gates.""" + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.cx(0, 1) + circuit.cx(1, 0) + circuit.cx(0, 1) + circuit.measure_all() + + filter_pass = FilterOpNodes(lambda node: node.op.name != "x") + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.cx(1, 0) + expected.cx(0, 1) + expected.measure_all() + + self.assertEqual(filter_pass(circuit), expected) + + def test_filter_exception(self): + """Test a filter function exception passes through.""" + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.cx(0, 1) + circuit.cx(1, 0) + circuit.cx(0, 1) + circuit.measure_all() + + def filter_fn(node): + raise TypeError("Failure") + + filter_pass = FilterOpNodes(filter_fn) + with self.assertRaises(TypeError): + filter_pass(circuit) + + def test_no_matches(self): + """Test the pass does nothing if there are no filter matches.""" + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.cx(0, 1) + circuit.cx(1, 0) + circuit.cx(0, 1) + circuit.measure_all() + + filter_pass = FilterOpNodes(lambda node: node.op.name != "cz") + + self.assertEqual(filter_pass(circuit), circuit) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index abc48f63ae68..8cd38222691c 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -254,7 +254,7 @@ def test_layout_many_search_trials(self): self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( - [layout[q] for q in qc.qubits], [22, 7, 2, 12, 1, 5, 14, 4, 11, 0, 16, 15, 3, 10] + [layout[q] for q in qc.qubits], [10, 16, 8, 4, 11, 20, 15, 19, 18, 7, 12, 1, 2, 0] ) diff --git a/test/python/visualization/references/pass_manager_standard.dot b/test/python/visualization/references/pass_manager_standard.dot index ae4233665a10..e1c5d3979ca2 100644 --- a/test/python/visualization/references/pass_manager_standard.dot +++ b/test/python/visualization/references/pass_manager_standard.dot @@ -61,27 +61,29 @@ fontname=helvetica; label="[6] do_while"; labeljust=l; 18 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +19 [color=black, fontname=helvetica, fontsize=10, label=label, shape=ellipse, style=dashed]; +19 -> 18; 14 -> 18; } -subgraph cluster_19 { +subgraph cluster_20 { fontname=helvetica; label="[7] "; labeljust=l; -20 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -21 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -21 -> 20; -22 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -22 -> 20; -18 -> 20; +21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +22 -> 21; +23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +23 -> 21; +18 -> 21; } -subgraph cluster_23 { +subgraph cluster_24 { fontname=helvetica; label="[8] "; labeljust=l; -24 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -20 -> 24; +25 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +21 -> 25; } } diff --git a/test/python/visualization/references/pass_manager_style.dot b/test/python/visualization/references/pass_manager_style.dot index 3c779cfde72b..0c80853d00be 100644 --- a/test/python/visualization/references/pass_manager_style.dot +++ b/test/python/visualization/references/pass_manager_style.dot @@ -61,27 +61,29 @@ fontname=helvetica; label="[6] do_while"; labeljust=l; 18 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +19 [color=black, fontname=helvetica, fontsize=10, label=label, shape=ellipse, style=dashed]; +19 -> 18; 14 -> 18; } -subgraph cluster_19 { +subgraph cluster_20 { fontname=helvetica; label="[7] "; labeljust=l; -20 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -21 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -21 -> 20; -22 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -22 -> 20; -18 -> 20; +21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +22 -> 21; +23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +23 -> 21; +18 -> 21; } -subgraph cluster_23 { +subgraph cluster_24 { fontname=helvetica; label="[8] "; labeljust=l; -24 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -20 -> 24; +25 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +21 -> 25; } }