Skip to content

Commit

Permalink
Add pass to filter ops and label inserted barriers (#10323)
Browse files Browse the repository at this point in the history
* Add pass to remove labeled ops and label inserted barriers

This commit adds a new transpiler pass RemoveLabeledOps which is used to
remove any op nodes that match a given label. This is paired with a new
label option for BarrierBeforeFinalMeasurements. These are combined in
the preset pass managers to ensure we're not always adding a barrier
before output measurements in the output of the transpiler.

Fixes #10321

* Fix rebase issues

* Handle pre-existing adjacent barriers

In the case when a label is set to trigger the removal of the labelled
barrier the merge step would lose the context around which barrier
instruction was transpiler inserted and which was user provided. To
address this issue this commit skips the MergeAdjacentBarrier step so
that the transpiler barrier is kept separate from any user inserted
barriers which need to be preserved.

* Generalize RemoveLabeledOps to FilterOpNodes

This commit generalizes the new RemoveLabeledOps pass to be a generic
node filtering pass that given a filter function it will remove any
matching op nodes in the circuit.

* Add release note

* Add tests for new pass

* Add release note for new barrier pass kwarg

* Fix type hint

* Fix docs typo

* Invert predicate usage

This commit inverts the predicate usage to be consistent with Python's
built in filter() function. Now if the predicate returns True the dag node
is retained and if it returns false it is removed. This is also
explicitly documented to make it clear how the pass is to be used.

* Update pass manager drawer reference images
  • Loading branch information
mtreinish authored Nov 22, 2023
1 parent 98e1cc6 commit 8a58b3a
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 30 deletions.
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
GatesInBasis
ConvertConditionsToIfOps
UnrollForLoops
FilterOpNodes
"""

# layout selection (placement)
Expand Down Expand Up @@ -292,3 +293,4 @@
from .utils import GatesInBasis
from .utils import ConvertConditionsToIfOps
from .utils import UnrollForLoops
from .utils import FilterOpNodes
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
65 changes: 65 additions & 0 deletions qiskit/transpiler/passes/utils/filter_op_nodes.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 28 additions & 4 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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}")
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 18 additions & 1 deletion qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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


Expand Down
16 changes: 16 additions & 0 deletions releasenotes/notes/add-filter-op-nodes-aa024a0f1058e4b7.yaml
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
78 changes: 78 additions & 0 deletions test/python/transpiler/test_filter_op_nodes.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion test/python/transpiler/test_sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)


Expand Down
Loading

0 comments on commit 8a58b3a

Please sign in to comment.