Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions qiskit/passmanager/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def run(
in_programs: Any | list[Any],
callback: Callable = None,
num_processes: int = None,
*,
property_set: dict[str, object] | None = None,
**kwargs,
) -> Any:
"""Run all the passes on the specified ``in_programs``.
Expand Down Expand Up @@ -211,7 +213,11 @@ def callback_func(**kwargs):
execution is enabled. This argument overrides ``num_processes`` in the user
configuration file, and the ``QISKIT_NUM_PROCS`` environment variable. If set
to ``None`` the system default or local user configuration will be used.

property_set: If given, the initial value to use as the :class:`.PropertySet` for the
pass manager pipeline. This can be used to persist analysis from one run to
another, in cases where you know the analysis is safe to share. Beware that some
analysis will be specific to the input circuit and the particular :class:`.Target`,
so you should take a lot of care when using this argument.
kwargs: Arbitrary arguments passed to the compiler frontend and backend.

Returns:
Expand All @@ -229,7 +235,13 @@ def callback_func(**kwargs):
# ourselves, since that can be quite expensive.
if len(in_programs) == 1 or not should_run_in_parallel(num_processes):
out = [
_run_workflow(program=program, pass_manager=self, callback=callback, **kwargs)
_run_workflow(
program=program,
pass_manager=self,
callback=callback,
initial_property_set=property_set,
**kwargs,
)
for program in in_programs
]
if len(in_programs) == 1 and not is_list:
Expand All @@ -246,7 +258,10 @@ def callback_func(**kwargs):
return parallel_map(
_run_workflow_in_new_process,
values=in_programs,
task_kwargs={"pass_manager_bin": dill.dumps(self)},
task_kwargs={
"pass_manager_bin": dill.dumps(self),
"initial_property_set": property_set,
},
num_processes=num_processes,
)

Expand All @@ -270,6 +285,8 @@ def _flatten_tasks(self, elements: Iterable | Task) -> Iterable:
def _run_workflow(
program: Any,
pass_manager: BasePassManager,
*,
initial_property_set: dict[str, object] | None = None,
**kwargs,
) -> Any:
"""Run single program optimization with a pass manager.
Expand All @@ -289,12 +306,12 @@ def _run_workflow(
input_program=program,
**kwargs,
)
property_set = (
PropertySet() if initial_property_set is None else PropertySet(initial_property_set)
)
passmanager_ir, final_state = flow_controller.execute(
passmanager_ir=passmanager_ir,
state=PassManagerState(
workflow_status=initial_status,
property_set=PropertySet(),
),
state=PassManagerState(workflow_status=initial_status, property_set=property_set),
callback=kwargs.get("callback", None),
)
# The `property_set` has historically been returned as a mutable attribute on `PassManager`
Expand All @@ -317,6 +334,8 @@ def _run_workflow(
def _run_workflow_in_new_process(
program: Any,
pass_manager_bin: bytes,
*,
initial_property_set: dict[str, object] | None,
) -> Any:
"""Run single program optimization in new process.

Expand All @@ -330,4 +349,5 @@ def _run_workflow_in_new_process(
return _run_workflow(
program=program,
pass_manager=dill.loads(pass_manager_bin),
initial_property_set=initial_property_set,
)
66 changes: 19 additions & 47 deletions qiskit/transpiler/basepasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@
from inspect import signature

from qiskit.circuit import QuantumCircuit
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.dagcircuit import DAGCircuit
from qiskit.passmanager.base_tasks import GenericPass, PassManagerIR
from qiskit.passmanager.compilation_status import PropertySet, RunState, PassManagerState

from .exceptions import TranspilerError
from .layout import TranspileLayout


class MetaPass(abc.ABCMeta):
Expand Down Expand Up @@ -126,57 +124,31 @@ def __call__(

Args:
circuit: The dag on which the pass is run.
property_set: Input/output property set. An analysis pass
might change the property set in-place.
property_set: Input/output property set. An analysis pass might change the property set
in-place. If not given, the existing ``property_set`` attribute of the pass will
be used (if set).

Returns:
If on transformation pass, the resulting QuantumCircuit.
If analysis pass, the input circuit.
"""
property_set_ = None
if isinstance(property_set, dict): # this includes (dict, PropertySet)
property_set_ = PropertySet(property_set)

if isinstance(property_set_, PropertySet):
# pylint: disable=attribute-defined-outside-init
self.property_set = property_set_

result = self.run(circuit_to_dag(circuit))

result_circuit = circuit

if isinstance(property_set, dict): # this includes (dict, PropertySet)
from qiskit.transpiler import PassManager # pylint: disable=cyclic-import

pm = PassManager([self])
# Previous versions of the `__call__` function would not construct a `PassManager`, but just
# call `self.run` directly (this caused issues with `requires`). It only overrode
# `self.property_set` if the input was not `None`, which some users might have been relying
# on (as our test suite was).
if property_set is None:
property_set = self.property_set
out = pm.run(circuit, property_set=property_set)
if property_set is not None and property_set is not pm.property_set:
# When this `__call__` was first added, it contained this behaviour of mutating the
# input `property_set` in-place, but didn't use the `PassManager` infrastructure. This
# preserves the output-variable nature of the `property_set` parameter.
property_set.clear()
property_set.update(self.property_set)

if isinstance(result, DAGCircuit):
result_circuit = dag_to_circuit(result, copy_operations=False)
elif result is None:
result_circuit = circuit.copy()

if self.property_set["layout"]:
result_circuit._layout = TranspileLayout(
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
_input_qubit_count=len(circuit.qubits),
_output_qubit_list=result_circuit.qubits,
)
if self.property_set["clbit_write_latency"] is not None:
result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
if self.property_set["conditional_latency"] is not None:
result_circuit._conditional_latency = self.property_set["conditional_latency"]
if self.property_set["node_start_time"]:
# This is dictionary keyed on the DAGOpNode, which is invalidated once
# dag is converted into circuit. So this schedule information is
# also converted into list with the same ordering with circuit.data.
topological_start_times = []
start_times = self.property_set["node_start_time"]
for dag_node in result.topological_op_nodes():
topological_start_times.append(start_times[dag_node])
result_circuit._op_start_times = topological_start_times

return result_circuit
property_set.update(pm.property_set)
return out


class AnalysisPass(BasePass): # pylint: disable=abstract-method
Expand Down
13 changes: 13 additions & 0 deletions qiskit/transpiler/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def run( # pylint:disable=arguments-renamed
output_name: str | None = None,
callback: Callable = None,
num_processes: int = None,
*,
property_set: dict[str, object] | None = None,
) -> _CircuitsT:
"""Run all the passes on the specified ``circuits``.

Expand Down Expand Up @@ -216,6 +218,11 @@ def callback_func(**kwargs):
execution is enabled. This argument overrides ``num_processes`` in the user
configuration file, and the ``QISKIT_NUM_PROCS`` environment variable. If set
to ``None`` the system default or local user configuration will be used.
property_set: If given, the initial value to use as the :class:`.PropertySet` for the
pass manager pipeline. This can be used to persist analysis from one run to
another, in cases where you know the analysis is safe to share. Beware that some
analysis will be specific to the input circuit and the particular :class:`.Target`,
so you should take a lot of care when using this argument.

Returns:
The transformed circuit(s).
Expand All @@ -228,6 +235,7 @@ def callback_func(**kwargs):
callback=callback,
output_name=output_name,
num_processes=num_processes,
property_set=property_set,
)

def draw(self, filename=None, style=None, raw=False):
Expand Down Expand Up @@ -436,6 +444,8 @@ def run(
output_name: str | None = None,
callback: Callable | None = None,
num_processes: int = None,
*,
property_set: dict[str, object] | None = None,
) -> _CircuitsT:
self._update_passmanager()
return super().run(circuits, output_name, callback, num_processes=num_processes)
Expand All @@ -462,6 +472,9 @@ def _replace_error(meth):
def wrapper(*meth_args, **meth_kwargs):
try:
return meth(*meth_args, **meth_kwargs)
except TranspilerError:
# If it's already a `TranspilerError` subclass, don't erase the extra information.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I remember discussing this offline, but TranspilerError being a subclass of PassManagerError seems so weird. But it's hard to change this now so this makes sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I assume it was intended as a backwards-compatibility shim for a move to PassManagerError during the genericisation of PassManager, but I don't really know.

raise
except PassManagerError as ex:
raise TranspilerError(ex.message) from ex

Expand Down
24 changes: 24 additions & 0 deletions releasenotes/notes/pass-call-as-passmanager-7f874917b9b303f0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
features_transpiler:
- |
:meth:`.PassManager.run` now accepts a ``property_set`` argument, which can be set to a
:class:`~collections.abc.Mapping`-like object to provide the initial values of the pipeline's
:class:`.PropertySet`. This can be used to recommence a partially applied compilation, or to
reuse certain analysis from a prior compilation in a new place.
upgrade_transpiler:
- |
The keyword argument ``property_set`` is now reserved in :meth:`.BasePassManager.run`, and
cannot be used as a ``kwarg`` that will be forwarded to the subclass's conversion from the
front-end representation to the internal representation.
fixes:
- |
Calling an :class:`.AnalysisPass` or a :class:`.TransformationPass` like a function (as in
``pass_ = MyPass(); pass_(qc)``) will now respect any requirements that the pass might have.
For example, scheduling passes such as :class:`.ALAPScheduleAnalysis` require that
:class:`.TimeUnitConversion` runs before them. Running the pass via a :class:`.PassManager`
always respected this requirement, but until this note, it was not respected by calling the
pass directly.
- |
When a :exc:`.TranspilerError` subclass is raised by a pass inside a call to
:meth:`.PassManger.run`, the exception will now be propagated through losslessly, rather than
becoming a chained exception with an erased type.
3 changes: 1 addition & 2 deletions test/python/transpiler/test_elide_permutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,7 @@ def test_partial_permutation_in_middle(self):

# Make sure that the transpiled circuit *with* the final permutation
# is equivalent to the original circuit
perm = pass_.property_set["virtual_permutation_layout"].to_permutation(qc.qubits)
res.append(PermutationGate(perm), [0, 1, 2, 3, 4])
res.append(PermutationGate(res.layout.routing_permutation()), [0, 1, 2, 3, 4])
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

PassManager.run explicitly clears out virtual_permutation_layout from a property_set after execution, since it updates the layout fields. This new version of the PermutationGate is more properly the behaviour we want to test, though.

self.assertEqual(Operator(res), Operator(qc))

def test_permutation_at_beginning(self):
Expand Down
31 changes: 30 additions & 1 deletion test/python/transpiler/test_pass_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Test calling passes (passmanager-less)"""

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.transpiler import PropertySet
from qiskit.transpiler import PropertySet, TransformationPass, AnalysisPass
from ._dummy_passes import PassD_TP_NR_NP, PassE_AP_NR_NP, PassN_AP_NR_NP
from test import QiskitTestCase # pylint: disable=wrong-import-order

Expand Down Expand Up @@ -90,3 +90,32 @@ def test_analysis_pass_remove_property(self):
self.assertEqual(property_set, PropertySet({"to none": None}))
self.assertIsInstance(property_set, dict)
self.assertEqual(circuit, result)

def test_pass_requires(self):
"""The 'requires' field of a pass should be respected when called."""
name = "my_property"
value = "hello, world"

assert_equal = self.assertEqual

class Analyse(AnalysisPass):
"""Dummy pass to set a property."""

def run(self, dag):
self.property_set[name] = value
return dag

class Execute(TransformationPass):
"""Dummy pass to assert that its required pass ran."""

def __init__(self):
super().__init__()
self.requires.append(Analyse())

def run(self, dag):
assert_equal(self.property_set[name], value)
return dag

pass_ = Execute()
pass_(QuantumCircuit())
self.assertEqual(pass_.property_set[name], value)
19 changes: 18 additions & 1 deletion test/python/transpiler/test_passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
ConditionalController,
DoWhileController,
)
from qiskit.transpiler import PassManager, PropertySet, TransformationPass
from qiskit.transpiler import PassManager, PropertySet, TransformationPass, AnalysisPass
from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator, ResourceEstimation
from qiskit.circuit.library.standard_gates.equivalence_library import (
StandardEquivalenceLibrary as std_eqlib,
Expand Down Expand Up @@ -191,3 +191,20 @@ def callback(pass_, **_):
"third 4",
]
self.assertEqual(calls, expected)

def test_override_initial_property_set(self):
"""Test that the ``property_set`` argument allows seeding the base analysis."""
input_name = "my_property"
output_name = "output_property"

class Analyse(AnalysisPass):
def run(self, dag):
self.property_set[output_name] = self.property_set[input_name]
return dag

pm = PassManager([Analyse()])
pm.run(QuantumCircuit(), property_set={input_name: "hello, world"})
self.assertEqual(pm.property_set[output_name], "hello, world")

pm.run(QuantumCircuit(), property_set={input_name: "a different string"})
self.assertEqual(pm.property_set[output_name], "a different string")
8 changes: 4 additions & 4 deletions test/python/visualization/test_circuit_text_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3246,8 +3246,8 @@ def test_mixed_layout(self):
circuit.h(qr)

pass_ = ApplyLayout()
pass_.property_set["layout"] = Layout({qr[0]: 0, ancilla[1]: 1, ancilla[0]: 2, qr[1]: 3})
circuit_with_layout = pass_(circuit)
layout = Layout({qr[0]: 0, ancilla[1]: 1, ancilla[0]: 2, qr[1]: 3})
circuit_with_layout = pass_(circuit, property_set={"layout": layout})

self.assertEqual(
str(circuit_drawer(circuit_with_layout, output="text", initial_state=True)), expected
Expand Down Expand Up @@ -3308,8 +3308,8 @@ def test_with_classical_regs(self):
circuit.measure(qr2[1], cr[1])

pass_ = ApplyLayout()
pass_.property_set["layout"] = Layout({qr1[0]: 0, qr1[1]: 1, qr2[0]: 2, qr2[1]: 3})
circuit_with_layout = pass_(circuit)
layout = Layout({qr1[0]: 0, qr1[1]: 1, qr2[0]: 2, qr2[1]: 3})
circuit_with_layout = pass_(circuit, property_set={"layout": layout})

self.assertEqual(
str(circuit_drawer(circuit_with_layout, output="text", initial_state=True)), expected
Expand Down