Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pass to filter ops and label inserted barriers #10323

Merged
merged 14 commits into from
Nov 22, 2023
Merged
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
FilterOpNode
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
"""

# 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
63 changes: 63 additions & 0 deletions qiskit/transpiler/passes/utils/filter_op_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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.

"""Remove all babeled ops from a circuit"""
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

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`.
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

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


Comment on lines 254 to 260
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know why this changed? The actual barriers inserted before routing should have been the same, right, so it shouldn't have affected StochasticSwap? It's not really a problem, I just don't understand why it's changed.

Copy link
Member Author

@mtreinish mtreinish Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumption was because the input circuit has barriers in it that this was a side effect of not running merge adjacent barriers part of the barrier insertion. (this only changed with 29c1217 ). So, now we have multiple barriers followed by an all qubit barrier instead of it all getting merged into a single barrier. But I didn't look too closely at the internals of what was causing sabre to diverge internally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up I did get to the bottom of this, it was basically what I thought. I pushed up #11295 to fix the behavior of Sabre with regards to barriers and had to revert this change in that PR: 93a3241

Expand Down
Loading