diff --git a/docs/apidocs/passmanager.rst b/docs/apidocs/passmanager.rst new file mode 100644 index 000000000000..f11794df3f3f --- /dev/null +++ b/docs/apidocs/passmanager.rst @@ -0,0 +1,6 @@ +.. _qiskit-passmanager: + +.. automodule:: qiskit.passmanager + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index cde921096f18..33795079383e 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -18,6 +18,7 @@ Qiskit Terra API Reference assembler dagcircuit extensions + passmanager providers_basicaer providers providers_fake_provider diff --git a/qiskit/passmanager/__init__.py b/qiskit/passmanager/__init__.py new file mode 100644 index 000000000000..24f94292bea6 --- /dev/null +++ b/qiskit/passmanager/__init__.py @@ -0,0 +1,106 @@ +# 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. + +""" +======================================= +Passmanager (:mod:`qiskit.passmanager`) +======================================= + +.. currentmodule:: qiskit.passmanager + +Overview +======== + +Qiskit pass manager is somewhat inspired by the `LLVM compiler `_, +but it is designed to take Qiskit object as an input instead of plain source code. + +The pass manager converts the input object into an intermediate representation (IR), +and it can be optimized and get lowered with a variety of transformations over multiple passes. +This representation must be preserved throughout the transformation. +The passes may consume the hardware constraints that Qiskit backend may provide. +Finally, the IR is converted back to some Qiskit object. +Note that the input type and output type don't need to match. + +Execution of passes is managed by the :class:`.FlowController`, +which is initialized with a set of transform and analysis passes and provides an iterator of them. +This iterator can be conditioned on the :class:`.PropertySet`, which is a namespace +storing the intermediate data necessary for the transformation. +A pass has read and write access to the property set namespace, +and the stored data is shared among scheduled passes. + +The :class:`BasePassManager` provides a user interface to build and execute transform passes. +It internally spawns a :class:`BasePassRunner` instance to apply transforms to +the input object. In this sense, the pass manager itself is unaware of the +underlying IR, but it is indirectly tied to a particular IR through the pass runner class. + +The responsibilities of the pass runner are the following: + +* Defining the input type / pass manager IR / output type. +* Converting an input object to a particular pass manager IR. +* Preparing own property set namespace. +* Running scheduled flow controllers to apply a series of transformations to the IR. +* Converting the IR back to an output object. + +A single pass runner always takes a single input object and returns a single output object. +Parallelism for multiple input objects is supported by the :class:`BasePassManager` by +broadcasting the pass runner via the :mod:`qiskit.tools.parallel_map` function. + +The base class :class:`BasePassRunner` doesn't define any associated type by itself, +and a developer needs to implement a subclass for a particular object type to optimize. +This `veil of ignorance` allows us to choose the most efficient data representation +for a particular optimization task, while we can reuse the pass flow control machinery +for different input and output types. + + +Base classes +------------ + +.. autosummary:: + :toctree: ../stubs/ + + BasePassRunner + BasePassManager + +Flow controllers +---------------- + +.. autosummary:: + :toctree: ../stubs/ + + FlowController + ConditionalController + DoWhileController + +PropertySet +----------- + +.. autosummary:: + :toctree: ../stubs/ + + PropertySet + +Exceptions +---------- + +.. autosummary:: + :toctree: ../stubs/ + + PassManagerError + +""" + +from .passrunner import BasePassRunner +from .passmanager import BasePassManager +from .flow_controllers import FlowController, ConditionalController, DoWhileController +from .base_pass import GenericPass +from .propertyset import PropertySet +from .exceptions import PassManagerError diff --git a/qiskit/passmanager/base_pass.py b/qiskit/passmanager/base_pass.py new file mode 100644 index 000000000000..f6cc27fc9b30 --- /dev/null +++ b/qiskit/passmanager/base_pass.py @@ -0,0 +1,80 @@ +# 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. + +"""Metaclass of Qiskit pass manager pass.""" + +from abc import abstractmethod +from collections.abc import Hashable +from inspect import signature +from typing import Any + +from .propertyset import PropertySet + + +class MetaPass(type): + """Metaclass for transpiler passes. + + Enforces the creation of some fields in the pass while allowing passes to + override ``__init__``. + """ + + def __call__(cls, *args, **kwargs): + pass_instance = type.__call__(cls, *args, **kwargs) + pass_instance._hash = hash(MetaPass._freeze_init_parameters(cls, args, kwargs)) + return pass_instance + + @staticmethod + def _freeze_init_parameters(class_, args, kwargs): + self_guard = object() + init_signature = signature(class_.__init__) + bound_signature = init_signature.bind(self_guard, *args, **kwargs) + arguments = [("class_.__name__", class_.__name__)] + for name, value in bound_signature.arguments.items(): + if value == self_guard: + continue + if isinstance(value, Hashable): + arguments.append((name, type(value), value)) + else: + arguments.append((name, type(value), repr(value))) + return frozenset(arguments) + + +class GenericPass(metaclass=MetaPass): + """Generic pass class with IR-agnostic minimal functionality.""" + + def __init__(self): + self.requires = [] # List of passes that requires + self.preserves = [] # List of passes that preserves + self.property_set = PropertySet() # This pass's pointer to the pass manager's property set. + self._hash = hash(None) + + def __hash__(self): + return self._hash + + def __eq__(self, other): + return hash(self) == hash(other) + + def name(self): + """Return the name of the pass.""" + return self.__class__.__name__ + + @abstractmethod + def run(self, passmanager_ir: Any): + """Run a pass on the pass manager IR. This is implemented by the pass developer. + + Args: + passmanager_ir: the dag on which the pass is run. + + Raises: + NotImplementedError: when this is left unimplemented for a pass. + """ + raise NotImplementedError diff --git a/qiskit/passmanager/exceptions.py b/qiskit/passmanager/exceptions.py new file mode 100644 index 000000000000..c1bca0563d7a --- /dev/null +++ b/qiskit/passmanager/exceptions.py @@ -0,0 +1,19 @@ +# 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. + +"""Qiskit pass manager exceptions.""" + +from qiskit.exceptions import QiskitError + + +class PassManagerError(QiskitError): + """Pass manager error.""" diff --git a/qiskit/passmanager/flow_controllers.py b/qiskit/passmanager/flow_controllers.py new file mode 100644 index 000000000000..6fe795222ac8 --- /dev/null +++ b/qiskit/passmanager/flow_controllers.py @@ -0,0 +1,158 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2019, 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. + +"""Pass flow controllers to provide pass iterator conditioned on the property set.""" +from __future__ import annotations +from collections import OrderedDict +from collections.abc import Sequence +from typing import Union, List +import logging + +from .base_pass import GenericPass +from .exceptions import PassManagerError + +logger = logging.getLogger(__name__) + + +class FlowController: + """Base class for multiple types of working list. + + This class is a base class for multiple types of working list. When you iterate on it, it + returns the next pass to run. + """ + + registered_controllers = OrderedDict() + + def __init__(self, passes, options, **partial_controller): + self._passes = passes + self.passes = FlowController.controller_factory(passes, options, **partial_controller) + self.options = options + + def __iter__(self): + yield from self.passes + + def dump_passes(self): + """Fetches the passes added to this flow controller. + + Returns: + dict: {'options': self.options, 'passes': [passes], 'type': type(self)} + """ + # TODO remove + ret = {"options": self.options, "passes": [], "type": type(self)} + for pass_ in self._passes: + if isinstance(pass_, FlowController): + ret["passes"].append(pass_.dump_passes()) + else: + ret["passes"].append(pass_) + return ret + + @classmethod + def add_flow_controller(cls, name, controller): + """Adds a flow controller. + + Args: + name (string): Name of the controller to add. + controller (type(FlowController)): The class implementing a flow controller. + """ + cls.registered_controllers[name] = controller + + @classmethod + def remove_flow_controller(cls, name): + """Removes a flow controller. + + Args: + name (string): Name of the controller to remove. + Raises: + KeyError: If the controller to remove was not registered. + """ + if name not in cls.registered_controllers: + raise KeyError("Flow controller not found: %s" % name) + del cls.registered_controllers[name] + + @classmethod + def controller_factory( + cls, + passes: Sequence[GenericPass | "FlowController"], + options: dict, + **partial_controller, + ): + """Constructs a flow controller based on the partially evaluated controller arguments. + + Args: + passes: passes to add to the flow controller. + options: PassManager options. + **partial_controller: Partially evaluated controller arguments in the form `{name:partial}` + + Raises: + PassManagerError: When partial_controller is not well-formed. + + Returns: + FlowController: A FlowController instance. + """ + if None in partial_controller.values(): + raise PassManagerError("The controller needs a condition.") + + if partial_controller: + for registered_controller in cls.registered_controllers.keys(): + if registered_controller in partial_controller: + return cls.registered_controllers[registered_controller]( + passes, options, **partial_controller + ) + raise PassManagerError("The controllers for %s are not registered" % partial_controller) + + return FlowControllerLinear(passes, options) + + +class FlowControllerLinear(FlowController): + """The basic controller runs the passes one after the other.""" + + def __init__(self, passes, options): # pylint: disable=super-init-not-called + self.passes = self._passes = passes + self.options = options + + +class DoWhileController(FlowController): + """Implements a set of passes in a do-while loop.""" + + def __init__(self, passes, options=None, do_while=None, **partial_controller): + self.do_while = do_while + self.max_iteration = options["max_iteration"] if options else 1000 + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + for _ in range(self.max_iteration): + yield from self.passes + + if not self.do_while(): + return + + raise PassManagerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration) + + +class ConditionalController(FlowController): + """Implements a set of passes under a certain condition.""" + + def __init__(self, passes, options=None, condition=None, **partial_controller): + self.condition = condition + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + if self.condition(): + yield from self.passes + + +# Alias to a sequence of all kind of pass elements +PassSequence = Union[Union[GenericPass, FlowController], List[Union[GenericPass, FlowController]]] + +# Default controllers +FlowController.add_flow_controller("condition", ConditionalController) +FlowController.add_flow_controller("do_while", DoWhileController) diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py new file mode 100644 index 000000000000..935a3afb3918 --- /dev/null +++ b/qiskit/passmanager/passmanager.py @@ -0,0 +1,268 @@ +# 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. + +"""Manager for a set of Passes and their scheduling during transpilation.""" +from __future__ import annotations +from abc import ABC +from collections.abc import Callable, Sequence +from typing import Any +import logging +import dill +from qiskit.tools.parallel import parallel_map + +from .base_pass import GenericPass +from .passrunner import BasePassRunner +from .exceptions import PassManagerError +from .flow_controllers import FlowController, PassSequence + +logger = logging.getLogger(__name__) + + +class BasePassManager(ABC): + """Pass manager base class.""" + + PASS_RUNNER = BasePassRunner + + def __init__( + self, + passes: PassSequence | None = None, + max_iteration: int = 1000, + ): + """Initialize an empty pass manager object. + + Args: + passes: A pass set to be added to the pass manager schedule. + max_iteration: The maximum number of iterations the schedule will be looped if the + condition is not met. + """ + # the pass manager's schedule of passes, including any control-flow. + # Populated via PassManager.append(). + self._pass_sets: list[dict[str, Any]] = [] + self.max_iteration = max_iteration + + if passes is not None: + self.append(passes) + + def append( + self, + passes: PassSequence, + **flow_controller_conditions: Callable, + ) -> None: + """Append a Pass Set to the schedule of passes. + + Args: + passes: A set of passes (a pass set) to be added to schedule. A pass set is a list of + passes that are controlled by the same flow controller. If a single pass is + provided, the pass set will only have that pass a single element. + It is also possible to append a + :class:`~qiskit.transpiler.runningpassmanager.FlowController` instance and the + rest of the parameter will be ignored. + flow_controller_conditions: Dictionary of control flow plugins. Default: + + * do_while (callable property_set -> boolean): The passes repeat until the + callable returns False. + Default: `lambda x: False # i.e. passes run once` + + * condition (callable property_set -> boolean): The passes run only if the + callable returns True. + Default: `lambda x: True # i.e. passes run` + """ + passes = self._normalize_passes(passes) + self._pass_sets.append({"passes": passes, "flow_controllers": flow_controller_conditions}) + + def replace( + self, + index: int, + passes: PassSequence, + **flow_controller_conditions: Any, + ) -> None: + """Replace a particular pass in the scheduler. + + Args: + index: Pass index to replace, based on the position in passes(). + passes: A pass set (as defined in :py:func:`qiskit.transpiler.PassManager.append`) + to be added to the pass manager schedule. + flow_controller_conditions: control flow plugins. + + Raises: + PassManagerError: if a pass in passes is not a proper pass or index not found. + """ + passes = self._normalize_passes(passes) + + try: + self._pass_sets[index] = { + "passes": passes, + "flow_controllers": flow_controller_conditions, + } + except IndexError as ex: + raise PassManagerError(f"Index to replace {index} does not exists") from ex + + def remove(self, index: int) -> None: + """Removes a particular pass in the scheduler. + + Args: + index: Pass index to replace, based on the position in passes(). + + Raises: + PassManagerError: if the index is not found. + """ + try: + del self._pass_sets[index] + except IndexError as ex: + raise PassManagerError(f"Index to replace {index} does not exists") from ex + + def __setitem__(self, index, item): + self.replace(index, item) + + def __len__(self): + return len(self._pass_sets) + + def __getitem__(self, index): + new_passmanager = self.__class__(max_iteration=self.max_iteration) + _pass_sets = self._pass_sets[index] + if isinstance(_pass_sets, dict): + _pass_sets = [_pass_sets] + new_passmanager._pass_sets = _pass_sets + return new_passmanager + + def __add__(self, other): + if isinstance(other, self.__class__): + new_passmanager = self.__class__(max_iteration=self.max_iteration) + new_passmanager._pass_sets = self._pass_sets + other._pass_sets + return new_passmanager + else: + try: + new_passmanager = self.__class__(max_iteration=self.max_iteration) + new_passmanager._pass_sets += self._pass_sets + new_passmanager.append(other) + return new_passmanager + except PassManagerError as ex: + raise TypeError( + "unsupported operand type + for %s and %s" % (self.__class__, other.__class__) + ) from ex + + def _normalize_passes( + self, + passes: PassSequence, + ) -> Sequence[GenericPass | FlowController] | FlowController: + if isinstance(passes, FlowController): + return passes + if isinstance(passes, GenericPass): + passes = [passes] + for pass_ in passes: + if isinstance(pass_, FlowController): + # Normalize passes in nested FlowController. + # TODO: Internal renormalisation should be the responsibility of the + # `FlowController`, but the separation between `FlowController`, + # `RunningPassManager` and `PassManager` is so muddled right now, it would be better + # to do this as part of more top-down refactoring. ---Jake, 2022-10-03. + pass_.passes = self._normalize_passes(pass_.passes) + elif not isinstance(pass_, GenericPass): + raise PassManagerError( + "%s is not a pass or FlowController instance " % pass_.__class__ + ) + return passes + + def run( + self, + in_programs: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + """Run all the passes on the specified ``circuits``. + + Args: + in_programs: Input programs to transform via all the registered passes. + callback: A callback function that will be called after each pass execution. The + function will be called with 5 keyword arguments:: + + pass_ (Pass): the pass being run + passmanager_ir (Any): depending on pass manager subclass + time (float): the time to execute the pass + property_set (PropertySet): the property set + count (int): the index for the pass execution + + The exact arguments pass expose the internals of the pass + manager and are subject to change as the pass manager internals + change. If you intend to reuse a callback function over + multiple releases be sure to check that the arguments being + passed are the same. + + To use the callback feature you define a function that will + take in kwargs dict and access the variables. For example:: + + def callback_func(**kwargs): + pass_ = kwargs['pass_'] + dag = kwargs['dag'] + time = kwargs['time'] + property_set = kwargs['property_set'] + count = kwargs['count'] + ... + + metadata: Metadata which might be attached to output program. + + Returns: + The transformed program(s). + """ + if not self._pass_sets and not metadata and callback is None: + return in_programs + + is_list = True + if isinstance(in_programs, self.PASS_RUNNER.IN_PROGRAM_TYPE): + in_programs = [in_programs] + is_list = False + + if len(in_programs) == 1: + out_program = self._run_single_circuit(in_programs[0], callback, **metadata) + if is_list: + return [out_program] + return out_program + + # TODO support for List(output_name) and List(callback) + del metadata + del callback + + return self._run_several_circuits(in_programs) + + def _create_running_passmanager(self) -> BasePassRunner: + # Must be implemented by followup PR. + # BasePassRunner.append assumes normalized pass input, which is not pass_sets. + raise NotImplementedError + + def _run_single_circuit( + self, + input_program: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + pass_runner = self._create_running_passmanager() + return pass_runner.run(input_program, callback=callback, **metadata) + + def _run_several_circuits( + self, + input_programs: Sequence[Any], + ) -> Any: + # Pass runner may contain callable and we need to serialize through dill rather than pickle. + # See https://github.com/Qiskit/qiskit-terra/pull/3290 + # Note that serialized object is deserialized as a different object. + # Thus, we can resue the same runner without state collision, without building it per thread. + return parallel_map( + self._in_parallel, input_programs, task_kwargs={"pm_dill": dill.dumps(self)} + ) + + @staticmethod + def _in_parallel( + in_program: Any, + pm_dill: bytes = None, + ) -> Any: + pass_runner = dill.loads(pm_dill)._create_running_passmanager() + return pass_runner.run(in_program) diff --git a/qiskit/passmanager/passrunner.py b/qiskit/passmanager/passrunner.py new file mode 100644 index 000000000000..9ce983ce6591 --- /dev/null +++ b/qiskit/passmanager/passrunner.py @@ -0,0 +1,250 @@ +# 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. + +"""Pass runner to apply transformation on passmanager IR.""" +from __future__ import annotations +import logging +import time +from abc import ABC, abstractmethod +from functools import partial +from collections.abc import Callable +from typing import Any + +from .base_pass import GenericPass +from .exceptions import PassManagerError +from .flow_controllers import FlowController, ConditionalController, DoWhileController +from .propertyset import PropertySet + +logger = logging.getLogger(__name__) + +# NoneType is removed from types module in < Python3.10. +NoneType = type(None) + + +class BasePassRunner(ABC): + """Pass runner base class.""" + + IN_PROGRAM_TYPE = NoneType + OUT_PROGRAM_TYPE = NoneType + IR_TYPE = NoneType + + def __init__(self, max_iteration: int): + """Initialize an empty pass runner object. + + Args: + max_iteration: The schedule looping iterates until the condition is met or until + max_iteration is reached. + """ + self.callback = None + self.count = None + self.metadata = None + + # the pass manager's schedule of passes, including any control-flow. + # Populated via PassManager.append(). + self.working_list = [] + + # global property set is the context of the circuit held by the pass manager + # as it runs through its scheduled passes. The flow controller + # have read-only access (via the fenced_property_set). + self.property_set = PropertySet() + + # passes already run that have not been invalidated + self.valid_passes = set() + + # pass manager's overriding options for the passes it runs (for debugging) + self.passmanager_options = {"max_iteration": max_iteration} + + def append( + self, + flow_controller: FlowController, + ): + """Append a flow controller to the schedule of controllers. + + Args: + flow_controller: A normalized flow controller instance. + """ + # We assume flow controller is already normalized. + self.working_list.append(flow_controller) + + @abstractmethod + def _to_passmanager_ir(self, in_program): + """Convert input program into pass manager IR. + + Args: + in_program: Input program. + + Returns: + Pass manager IR. + """ + pass + + @abstractmethod + def _to_target(self, passmanager_ir): + """Convert pass manager IR into output program. + + Args: + passmanager_ir: Pass manager IR after optimization. + + Returns: + Output program. + """ + pass + + @abstractmethod + def _run_base_pass( + self, + pass_: GenericPass, + passmanager_ir: Any, + ) -> Any: + """Do a single base pass. + + Args: + pass_: A base pass to run. + passmanager_ir: Pass manager IR. + + Returns: + Pass manager IR with optimization. + """ + pass + + def _run_pass_generic( + self, + pass_sequence: GenericPass | FlowController, + passmanager_ir: Any, + options: dict[str, Any] | None = None, + ) -> Any: + """Do either base pass or single flow controller. + + Args: + pass_sequence: Base pass or flow controller to run. + passmanager_ir: Pass manager IR. + options: PassManager options. + + Returns: + Pass manager IR with optimization. + + Raises: + PassManagerError: When pass_sequence is not a valid class. + TypeError: When IR type changed during transformation. + """ + if isinstance(pass_sequence, GenericPass): + # First, do the requirements of this pass + for required_pass in pass_sequence.requires: + passmanager_ir = self._run_pass_generic( + pass_sequence=required_pass, + passmanager_ir=passmanager_ir, + options=options, + ) + # Run the pass itself, if not already run + if pass_sequence not in self.valid_passes: + start_time = time.time() + try: + passmanager_ir = self._run_base_pass( + pass_=pass_sequence, + passmanager_ir=passmanager_ir, + ) + finally: + run_time = time.time() - start_time + log_msg = f"Pass: {pass_sequence.name()} - {run_time * 1000:.5f} (ms)" + logger.info(log_msg) + if self.callback: + self.callback( + pass_=pass_sequence, + passmanager_ir=passmanager_ir, + time=run_time, + property_set=self.property_set, + count=self.count, + ) + self.count += 1 + self._update_valid_passes(pass_sequence) + if not isinstance(passmanager_ir, self.IR_TYPE): + raise TypeError( + f"A transformed object {passmanager_ir} is not valid IR in this pass manager. " + "Object representation type must be preserved during transformation. " + f"The pass {pass_sequence.name()} returns invalid object." + ) + return passmanager_ir + + if isinstance(pass_sequence, FlowController): + # This will be removed in followup PR. Code is temporary. + fenced_property_set = getattr(self, "fenced_property_set") + + if isinstance(pass_sequence, ConditionalController) and not isinstance( + pass_sequence.condition, partial + ): + pass_sequence.condition = partial(pass_sequence.condition, fenced_property_set) + if isinstance(pass_sequence, DoWhileController) and not isinstance( + pass_sequence.do_while, partial + ): + pass_sequence.do_while = partial(pass_sequence.do_while, fenced_property_set) + for pass_ in pass_sequence: + passmanager_ir = self._run_pass_generic( + pass_sequence=pass_, + passmanager_ir=passmanager_ir, + options=pass_sequence.options, + ) + return passmanager_ir + + raise PassManagerError( + f"{pass_sequence.__class__} is not a valid base pass nor flow controller." + ) + + def run( + self, + in_program: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + """Run all the passes on an input program. + + Args: + in_program: Input program to compile via all the registered passes. + callback: A callback function that will be called after each pass execution. + **metadata: Metadata attached to the output program. + + Returns: + Compiled or optimized program. + + Raises: + TypeError: When input or output object is unexpected type. + """ + if not isinstance(in_program, self.IN_PROGRAM_TYPE): + raise TypeError( + f"Input object {in_program} is not valid type for this pass manager. " + f"This pass manager accepts {self.IN_PROGRAM_TYPE}." + ) + + if callback: + self.callback = callback + self.count = 0 + self.metadata = metadata + + passmanager_ir = self._to_passmanager_ir(in_program) + del in_program + + for controller in self.working_list: + passmanager_ir = self._run_pass_generic( + pass_sequence=controller, + passmanager_ir=passmanager_ir, + options=self.passmanager_options, + ) + out_program = self._to_target(passmanager_ir) + + if not isinstance(out_program, self.OUT_PROGRAM_TYPE): + raise TypeError( + f"Output object {out_program} is not valid type for this pass manager. " + f"This pass manager must return {self.OUT_PROGRAM_TYPE}." + ) + return out_program + + def _update_valid_passes(self, pass_): + self.valid_passes.add(pass_) diff --git a/qiskit/passmanager/propertyset.py b/qiskit/passmanager/propertyset.py new file mode 100644 index 000000000000..d13eeb7c0032 --- /dev/null +++ b/qiskit/passmanager/propertyset.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018, 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. + +"""A property set is maintained by the pass runner. + + +This is sort of shared memory space among passes. +""" + + +class PropertySet(dict): + """A default dictionary-like object""" + + def __missing__(self, key): + return None diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 76cd6fc3d6f4..3d32c911566f 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -1197,10 +1197,6 @@ StagedPassManager PassManager PassManagerConfig - PropertySet - FlowController - ConditionalController - DoWhileController Layout and Topology ------------------- @@ -1248,11 +1244,16 @@ TranspilerAccessError """ -from .runningpassmanager import FlowController, ConditionalController, DoWhileController -from .passmanager import PassManager +# For backward compatibility +from qiskit.passmanager import ( + FlowController, + ConditionalController, + DoWhileController, +) + +from .passmanager import PassManager, StagedPassManager from .passmanager_config import PassManagerConfig -from .passmanager import StagedPassManager -from .propertyset import PropertySet +from .propertyset import PropertySet # pylint: disable=no-name-in-module from .exceptions import TranspilerError, TranspilerAccessError from .fencedobjs import FencedDAGCircuit, FencedPropertySet from .basepasses import AnalysisPass, TransformationPass diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index f54fb8f27ab9..46b10b6510da 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -13,61 +13,18 @@ """Base transpiler passes.""" from abc import abstractmethod -from collections.abc import Hashable -from inspect import signature -from .propertyset import PropertySet -from .layout import TranspileLayout - - -class MetaPass(type): - """Metaclass for transpiler passes. - Enforces the creation of some fields in the pass while allowing passes to - override ``__init__``. - """ +from qiskit.passmanager.base_pass import GenericPass +from qiskit.passmanager.propertyset import PropertySet - def __call__(cls, *args, **kwargs): - pass_instance = type.__call__(cls, *args, **kwargs) - pass_instance._hash = hash(MetaPass._freeze_init_parameters(cls, args, kwargs)) - return pass_instance - - @staticmethod - def _freeze_init_parameters(class_, args, kwargs): - self_guard = object() - init_signature = signature(class_.__init__) - bound_signature = init_signature.bind(self_guard, *args, **kwargs) - arguments = [("class_.__name__", class_.__name__)] - for name, value in bound_signature.arguments.items(): - if value == self_guard: - continue - if isinstance(value, Hashable): - arguments.append((name, type(value), value)) - else: - arguments.append((name, type(value), repr(value))) - return frozenset(arguments) +from .layout import TranspileLayout -class BasePass(metaclass=MetaPass): +class BasePass(GenericPass): """Base class for transpiler passes.""" - def __init__(self): - self.requires = [] # List of passes that requires - self.preserves = [] # List of passes that preserves - self.property_set = PropertySet() # This pass's pointer to the pass manager's property set. - self._hash = hash(None) - - def __hash__(self): - return self._hash - - def __eq__(self, other): - return hash(self) == hash(other) - - def name(self): - """Return the name of the pass.""" - return self.__class__.__name__ - @abstractmethod - def run(self, dag): + def run(self, dag): # pylint: disable=arguments-differ """Run a pass on the DAGCircuit. This is implemented by the pass developer. Args: diff --git a/qiskit/transpiler/exceptions.py b/qiskit/transpiler/exceptions.py index c30aadedf173..ef79603bfed2 100644 --- a/qiskit/transpiler/exceptions.py +++ b/qiskit/transpiler/exceptions.py @@ -14,9 +14,10 @@ Exception for errors raised by the transpiler. """ from qiskit.exceptions import QiskitError +from qiskit.passmanager.exceptions import PassManagerError -class TranspilerAccessError(QiskitError): +class TranspilerAccessError(PassManagerError): """DEPRECATED: Exception of access error in the transpiler passes.""" diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 03019e872ff4..0221f429cad7 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -12,26 +12,34 @@ """Manager for a set of Passes and their scheduling during transpilation.""" from __future__ import annotations +import inspect import io import re +from functools import wraps from collections.abc import Iterator, Iterable, Callable, Sequence from typing import Union, List, Any -import dill - -from qiskit.tools.parallel import parallel_map from qiskit.circuit import QuantumCircuit +from qiskit.passmanager import BasePassManager +from qiskit.passmanager.flow_controllers import PassSequence, FlowController +from qiskit.passmanager.exceptions import PassManagerError from .basepasses import BasePass from .exceptions import TranspilerError -from .runningpassmanager import RunningPassManager, FlowController +from .runningpassmanager import RunningPassManager _CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] -class PassManager: +class PassManager(BasePassManager): """Manager for a set of Passes and their scheduling during transpilation.""" - def __init__(self, passes: BasePass | list[BasePass] | None = None, max_iteration: int = 1000): + PASS_RUNNER = RunningPassManager + + def __init__( + self, + passes: PassSequence | None = None, + max_iteration: int = 1000, + ): """Initialize an empty `PassManager` object (with no passes scheduled). Args: @@ -40,18 +48,12 @@ def __init__(self, passes: BasePass | list[BasePass] | None = None, max_iteratio max_iteration: The maximum number of iterations the schedule will be looped if the condition is not met. """ - # the pass manager's schedule of passes, including any control-flow. - # Populated via PassManager.append(). - - self._pass_sets: list[dict[str, Any]] = [] - if passes is not None: - self.append(passes) - self.max_iteration = max_iteration + super().__init__(passes, max_iteration) self.property_set = None def append( self, - passes: BasePass | Sequence[BasePass | FlowController], + passes: PassSequence, max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -65,26 +67,28 @@ def append( :class:`~qiskit.transpiler.runningpassmanager.FlowController` instance and the rest of the parameter will be ignored. max_iteration: max number of iterations of passes. - flow_controller_conditions: control flow plugins. + flow_controller_conditions: Dictionary of control flow plugins. Default: + + * do_while (callable property_set -> boolean): The passes repeat until the + callable returns False. + Default: `lambda x: False # i.e. passes run once` + + * condition (callable property_set -> boolean): The passes run only if the + callable returns True. + Default: `lambda x: True # i.e. passes run` Raises: TranspilerError: if a pass in passes is not a proper pass. - - See Also: - ``RunningPassManager.add_flow_controller()`` for more information about the control - flow plugins. """ if max_iteration: # TODO remove this argument from append self.max_iteration = max_iteration - - passes = PassManager._normalize_passes(passes) - self._pass_sets.append({"passes": passes, "flow_controllers": flow_controller_conditions}) + super().append(passes, **flow_controller_conditions) def replace( self, index: int, - passes: BasePass | list[BasePass], + passes: PassSequence, max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -99,91 +103,13 @@ def replace( Raises: TranspilerError: if a pass in passes is not a proper pass or index not found. - - See Also: - ``RunningPassManager.add_flow_controller()`` for more information about the control - flow plugins. """ if max_iteration: # TODO remove this argument from append self.max_iteration = max_iteration + super().replace(index, passes, **flow_controller_conditions) - passes = PassManager._normalize_passes(passes) - - try: - self._pass_sets[index] = { - "passes": passes, - "flow_controllers": flow_controller_conditions, - } - except IndexError as ex: - raise TranspilerError(f"Index to replace {index} does not exists") from ex - - def remove(self, index: int) -> None: - """Removes a particular pass in the scheduler. - - Args: - index: Pass index to replace, based on the position in passes(). - - Raises: - TranspilerError: if the index is not found. - """ - try: - del self._pass_sets[index] - except IndexError as ex: - raise TranspilerError(f"Index to replace {index} does not exists") from ex - - def __setitem__(self, index, item): - self.replace(index, item) - - def __len__(self): - return len(self._pass_sets) - - def __getitem__(self, index): - new_passmanager = PassManager(max_iteration=self.max_iteration) - _pass_sets = self._pass_sets[index] - if isinstance(_pass_sets, dict): - _pass_sets = [_pass_sets] - new_passmanager._pass_sets = _pass_sets - return new_passmanager - - def __add__(self, other): - if isinstance(other, PassManager): - new_passmanager = PassManager(max_iteration=self.max_iteration) - new_passmanager._pass_sets = self._pass_sets + other._pass_sets - return new_passmanager - else: - try: - new_passmanager = PassManager(max_iteration=self.max_iteration) - new_passmanager._pass_sets += self._pass_sets - new_passmanager.append(other) - return new_passmanager - except TranspilerError as ex: - raise TypeError( - f"unsupported operand type + for {self.__class__} and {other.__class__}" - ) from ex - - @staticmethod - def _normalize_passes( - passes: BasePass | Sequence[BasePass | FlowController] | FlowController, - ) -> Sequence[BasePass | FlowController] | FlowController: - if isinstance(passes, FlowController): - return passes - if isinstance(passes, BasePass): - passes = [passes] - for pass_ in passes: - if isinstance(pass_, FlowController): - # Normalize passes in nested FlowController. - # TODO: Internal renormalisation should be the responsibility of the - # `FlowController`, but the separation between `FlowController`, - # `RunningPassManager` and `PassManager` is so muddled right now, it would be better - # to do this as part of more top-down refactoring. ---Jake, 2022-10-03. - pass_.passes = PassManager._normalize_passes(pass_.passes) - elif not isinstance(pass_, BasePass): - raise TranspilerError( - "%s is not a BasePass or FlowController instance " % pass_.__class__ - ) - return passes - + # pylint: disable=arguments-differ def run( self, circuits: _CircuitsT, @@ -225,73 +151,30 @@ def callback_func(**kwargs): Returns: The transformed circuit(s). """ - if not self._pass_sets and output_name is None and callback is None: - return circuits - if isinstance(circuits, QuantumCircuit): - return self._run_single_circuit(circuits, output_name, callback) - if len(circuits) == 1: - return [self._run_single_circuit(circuits[0], output_name, callback)] - return self._run_several_circuits(circuits, output_name, callback) + return super().run( + in_programs=circuits, + callback=callback, + output_name=output_name, + ) def _create_running_passmanager(self) -> RunningPassManager: - running_passmanager = RunningPassManager(self.max_iteration) + running_passmanager = self.PASS_RUNNER(self.max_iteration) for pass_set in self._pass_sets: running_passmanager.append(pass_set["passes"], **pass_set["flow_controllers"]) return running_passmanager - @staticmethod - def _in_parallel(circuit, pm_dill=None) -> QuantumCircuit: - """Task used by the parallel map tools from ``_run_several_circuits``.""" - running_passmanager = dill.loads(pm_dill)._create_running_passmanager() - result = running_passmanager.run(circuit) - return result - - def _run_several_circuits( - self, - circuits: List[QuantumCircuit], - output_name: str | None = None, - callback: Callable | None = None, - ) -> List[QuantumCircuit]: - """Run all the passes on the specified ``circuits``. - - Args: - circuits: Circuits to transform via all the registered passes. - output_name: The output circuit name. If ``None``, it will be set to the same as the - input circuit name. - callback: A callback function that will be called after each pass execution. - - Returns: - The transformed circuits. - """ - # TODO support for List(output_name) and List(callback) - del output_name - del callback - - return parallel_map( - PassManager._in_parallel, circuits, task_kwargs={"pm_dill": dill.dumps(self)} - ) - def _run_single_circuit( self, - circuit: QuantumCircuit, - output_name: str | None = None, + input_program: QuantumCircuit, callback: Callable | None = None, + **metadata, ) -> QuantumCircuit: - """Run all the passes on a ``circuit``. - - Args: - circuit: Circuit to transform via all the registered passes. - output_name: The output circuit name. If ``None``, it will be set to the same as the - input circuit name. - callback: A callback function that will be called after each pass execution. + pass_runner = self._create_running_passmanager() + out_program = pass_runner.run(input_program, callback=callback, **metadata) + # Store property set of pass runner for backward compatibility + self.property_set = pass_runner.property_set - Returns: - The transformed circuit. - """ - running_passmanager = self._create_running_passmanager() - result = running_passmanager.run(circuit, output_name=output_name, callback=callback) - self.property_set = running_passmanager.property_set - return result + return out_program def draw(self, filename=None, style=None, raw=False): """Draw the pass manager. @@ -507,7 +390,15 @@ def remove(self, index: int) -> None: def __getitem__(self, index): self._update_passmanager() - return super().__getitem__(index) + + # Do not inherit from the PassManager, i.e. super() + # It returns instance of self.__class__ which is StagedPassManager. + new_passmanager = PassManager(max_iteration=self.max_iteration) + _pass_sets = self._pass_sets[index] + if isinstance(_pass_sets, dict): + _pass_sets = [_pass_sets] + new_passmanager._pass_sets = _pass_sets + return new_passmanager def __len__(self): self._update_passmanager() @@ -541,3 +432,29 @@ def draw(self, filename=None, style=None, raw=False): from qiskit.visualization import staged_pass_manager_drawer return staged_pass_manager_drawer(self, filename=filename, style=style, raw=raw) + + +# A temporary error handling with slight overhead at class loading. +# This method wraps all class methods to replace PassManagerError with TranspilerError. +# The pass flow controller mechanics raises PassManagerError, as it has been moved to base class. +# PassManagerError is not caught by TranspilerError due to the hierarchy. + + +def _replace_error(meth): + @wraps(meth) + def wrapper(*meth_args, **meth_kwargs): + try: + return meth(*meth_args, **meth_kwargs) + except PassManagerError as ex: + raise TranspilerError(ex.message) from ex + + return wrapper + + +for _name, _method in inspect.getmembers(PassManager, predicate=inspect.isfunction): + if _name.startswith("_"): + # Ignore protected and private. + # User usually doesn't directly execute and catch error from these methods. + continue + _wrapped = _replace_error(_method) + setattr(PassManager, _name, _wrapped) diff --git a/qiskit/transpiler/propertyset.py b/qiskit/transpiler/propertyset.py index 9cba8fc886e3..6244a49d2c28 100644 --- a/qiskit/transpiler/propertyset.py +++ b/qiskit/transpiler/propertyset.py @@ -14,8 +14,9 @@ about the current state of the circuit """ -class PropertySet(dict): - """A default dictionary-like object""" +from qiskit.passmanager import propertyset as passmanager_propertyset - def __missing__(self, key): - return None + +def __getattr__(name): + # Just redirect to new module. This will be deprecated. + return getattr(passmanager_propertyset, name) diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index 6290ed4d52bc..aa574414667a 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -13,57 +13,57 @@ """RunningPassManager class for the transpiler. This object holds the state of a pass manager during running-time.""" from __future__ import annotations -from functools import partial -from collections import OrderedDict import logging -from time import time +import inspect +from functools import partial, wraps +from typing import Callable -from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit import QuantumCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.dagcircuit import DAGCircuit +from qiskit.passmanager import BasePassRunner +from qiskit.passmanager.flow_controllers import ( + PassSequence, + FlowController, + DoWhileController, + ConditionalController, +) +from qiskit.passmanager.exceptions import PassManagerError from qiskit.transpiler.basepasses import BasePass -from .propertyset import PropertySet -from .fencedobjs import FencedPropertySet, FencedDAGCircuit from .exceptions import TranspilerError +from .fencedobjs import FencedPropertySet, FencedDAGCircuit from .layout import TranspileLayout logger = logging.getLogger(__name__) -class RunningPassManager: +class RunningPassManager(BasePassRunner): """A RunningPassManager is a running pass manager.""" - def __init__(self, max_iteration): + IN_PROGRAM_TYPE = QuantumCircuit + OUT_PROGRAM_TYPE = QuantumCircuit + IR_TYPE = DAGCircuit + + def __init__(self, max_iteration: int): """Initialize an empty PassManager object (with no passes scheduled). Args: - max_iteration (int): The schedule looping iterates until the condition is met or until + max_iteration: The schedule looping iterates until the condition is met or until max_iteration is reached. """ - self.callback = None - # the pass manager's schedule of passes, including any control-flow. - # Populated via PassManager.append(). - self.working_list = [] - - # global property set is the context of the circuit held by the pass manager - # as it runs through its scheduled passes. The flow controller - # have read-only access (via the fenced_property_set). - self.property_set = PropertySet() + super().__init__(max_iteration) self.fenced_property_set = FencedPropertySet(self.property_set) - # passes already run that have not been invalidated - self.valid_passes = set() - - # pass manager's overriding options for the passes it runs (for debugging) - self.passmanager_options = {"max_iteration": max_iteration} - - self.count = 0 - - def append(self, passes: list[BasePass], **flow_controller_conditions): - """Append a Pass to the schedule of passes. + def append( + self, + passes: PassSequence, + **flow_controller_conditions, + ): + """Append a passes to the schedule of passes. Args: - passes (list[TBasePass]): passes to be added to schedule - flow_controller_conditions (kwargs): See add_flow_controller(): Dictionary of + passes: passes to be added to schedule + flow_controller_conditions: See add_flow_controller(): Dictionary of control flow plugins. Default: * do_while (callable property_set -> boolean): The passes repeat until the @@ -73,26 +73,19 @@ def append(self, passes: list[BasePass], **flow_controller_conditions): * condition (callable property_set -> boolean): The passes run only if the callable returns True. Default: `lambda x: True # i.e. passes run` - - Raises: - TranspilerError: if a pass in passes is not a proper pass. """ # attaches the property set to the controller so it has access to it. if isinstance(passes, ConditionalController): passes.condition = partial(passes.condition, self.fenced_property_set) - self.working_list.append(passes) - if isinstance(passes, DoWhileController): + elif isinstance(passes, DoWhileController): if not isinstance(passes.do_while, partial): passes.do_while = partial(passes.do_while, self.fenced_property_set) - self.working_list.append(passes) else: flow_controller_conditions = self._normalize_flow_controller(flow_controller_conditions) - self.working_list.append( - FlowController.controller_factory( - passes, self.passmanager_options, **flow_controller_conditions - ) + passes = FlowController.controller_factory( + passes, self.passmanager_options, **flow_controller_conditions ) - pass + super().append(passes) def _normalize_flow_controller(self, flow_controller): for name, param in flow_controller.items(): @@ -102,33 +95,18 @@ def _normalize_flow_controller(self, flow_controller): raise TranspilerError("The flow controller parameter %s is not callable" % name) return flow_controller - def run(self, circuit, output_name=None, callback=None): - """Run all the passes on a QuantumCircuit + def _to_passmanager_ir(self, in_program: QuantumCircuit) -> DAGCircuit: + if not isinstance(in_program, QuantumCircuit): + raise TranspilerError(f"Input {in_program.__class__} is not QuantumCircuit.") + return circuit_to_dag(in_program) - Args: - circuit (QuantumCircuit): circuit to transform via all the registered passes - output_name (str): The output circuit name. If not given, the same as the - input circuit - callback (callable): A callback function that will be called after each pass execution. - Returns: - QuantumCircuit: Transformed circuit. - """ - name = circuit.name - dag = circuit_to_dag(circuit) - del circuit - - if callback: - self.callback = callback + def _to_target(self, passmanager_ir: DAGCircuit) -> QuantumCircuit: + if not isinstance(passmanager_ir, DAGCircuit): + raise TranspilerError(f"Input {passmanager_ir.__class__} is not DAGCircuit.") - for passset in self.working_list: - for pass_ in passset: - dag = self._do_pass(pass_, dag, passset.options) + circuit = dag_to_circuit(passmanager_ir, copy_operations=False) + circuit.name = self.metadata["output_name"] - circuit = dag_to_circuit(dag, copy_operations=False) - if output_name: - circuit.name = output_name - else: - circuit.name = name if self.property_set["layout"] is not None: circuit._layout = TranspileLayout( initial_layout=self.property_set["layout"], @@ -144,237 +122,118 @@ def run(self, circuit, output_name=None, callback=None): # 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 dag.topological_op_nodes(): + for dag_node in passmanager_ir.topological_op_nodes(): topological_start_times.append(start_times[dag_node]) circuit._op_start_times = topological_start_times return circuit - def _do_pass(self, pass_, dag, options): - """Do either a pass and its "requires" or FlowController. + # pylint: disable=arguments-differ + def run( + self, + circuit: QuantumCircuit, + output_name: str = None, + callback: Callable = None, + ) -> QuantumCircuit: + """Run all the passes on a QuantumCircuit Args: - pass_ (BasePass or FlowController): Pass to do. - dag (DAGCircuit): The dag on which the pass is ran. - options (dict): PassManager options. + circuit: Circuit to transform via all the registered passes. + output_name: The output circuit name. If not given, the same as the input circuit. + callback: A callback function that will be called after each pass execution. + Returns: - DAGCircuit: The transformed dag in case of a transformation pass. - The same input dag in case of an analysis pass. - Raises: - TranspilerError: If the pass is not a proper pass instance. + QuantumCircuit: Transformed circuit. """ - if isinstance(pass_, BasePass): - # First, do the requires of pass_ - for required_pass in pass_.requires: - dag = self._do_pass(required_pass, dag, options) - - # Run the pass itself, if not already run - if pass_ not in self.valid_passes: - dag = self._run_this_pass(pass_, dag) - - # update the valid_passes property - self._update_valid_passes(pass_) - - # if provided a nested flow controller - elif isinstance(pass_, FlowController): - - if isinstance(pass_, ConditionalController) and not isinstance( - pass_.condition, partial - ): - pass_.condition = partial(pass_.condition, self.fenced_property_set) + return super().run( + in_program=circuit, + callback=_rename_callback_args(callback), + output_name=output_name or circuit.name, + ) + + def _run_base_pass( + self, + pass_: BasePass, + passmanager_ir: DAGCircuit, + ) -> DAGCircuit: + """Do either a pass and its "requires" or FlowController. - elif isinstance(pass_, DoWhileController) and not isinstance(pass_.do_while, partial): - pass_.do_while = partial(pass_.do_while, self.fenced_property_set) + Args: + pass_: A base pass to run. + passmanager_ir: Pass manager IR, i.e. DAGCircuit for this class. - for _pass in pass_: - dag = self._do_pass(_pass, dag, pass_.options) - else: - raise TranspilerError( - "Expecting type BasePass or FlowController, got %s." % type(pass_) - ) - return dag + Returns: + The transformed dag in case of a transformation pass. + The same input dag in case of an analysis pass. - def _run_this_pass(self, pass_, dag): + Raises: + TranspilerError: When transform pass returns non DAGCircuit. + TranspilerError: When pass is neither transform pass nor analysis pass. + """ pass_.property_set = self.property_set + if pass_.is_transformation_pass: # Measure time if we have a callback or logging set - start_time = time() - new_dag = pass_.run(dag) - end_time = time() - run_time = end_time - start_time - # Execute the callback function if one is set - if self.callback: - self.callback( - pass_=pass_, - dag=new_dag, - time=run_time, - property_set=self.property_set, - count=self.count, - ) - self.count += 1 - self._log_pass(start_time, end_time, pass_.name()) + new_dag = pass_.run(passmanager_ir) if isinstance(new_dag, DAGCircuit): - new_dag.calibrations = dag.calibrations + new_dag.calibrations = passmanager_ir.calibrations else: raise TranspilerError( "Transformation passes should return a transformed dag." "The pass %s is returning a %s" % (type(pass_).__name__, type(new_dag)) ) - dag = new_dag + passmanager_ir = new_dag elif pass_.is_analysis_pass: # Measure time if we have a callback or logging set - start_time = time() - pass_.run(FencedDAGCircuit(dag)) - end_time = time() - run_time = end_time - start_time - # Execute the callback function if one is set - if self.callback: - self.callback( - pass_=pass_, - dag=dag, - time=run_time, - property_set=self.property_set, - count=self.count, - ) - self.count += 1 - self._log_pass(start_time, end_time, pass_.name()) + pass_.run(FencedDAGCircuit(passmanager_ir)) else: raise TranspilerError("I dont know how to handle this type of pass") - return dag - - def _log_pass(self, start_time, end_time, name): - log_msg = f"Pass: {name} - {(end_time - start_time) * 1000:.5f} (ms)" - logger.info(log_msg) + return passmanager_ir def _update_valid_passes(self, pass_): - self.valid_passes.add(pass_) + super()._update_valid_passes(pass_) if not pass_.is_analysis_pass: # Analysis passes preserve all self.valid_passes.intersection_update(set(pass_.preserves)) -class FlowController: - """Base class for multiple types of working list. - - This class is a base class for multiple types of working list. When you iterate on it, it - returns the next pass to run. - """ - - registered_controllers = OrderedDict() - - def __init__(self, passes, options, **partial_controller): - self._passes = passes - self.passes = FlowController.controller_factory(passes, options, **partial_controller) - self.options = options - - def __iter__(self): - yield from self.passes - - def dump_passes(self): - """Fetches the passes added to this flow controller. - - Returns: - dict: {'options': self.options, 'passes': [passes], 'type': type(self)} - """ - # TODO remove - ret = {"options": self.options, "passes": [], "type": type(self)} - for pass_ in self._passes: - if isinstance(pass_, FlowController): - ret["passes"].append(pass_.dump_passes()) - else: - ret["passes"].append(pass_) - return ret - - @classmethod - def add_flow_controller(cls, name, controller): - """Adds a flow controller. - - Args: - name (string): Name of the controller to add. - controller (type(FlowController)): The class implementing a flow controller. - """ - cls.registered_controllers[name] = controller - - @classmethod - def remove_flow_controller(cls, name): - """Removes a flow controller. - - Args: - name (string): Name of the controller to remove. - Raises: - KeyError: If the controller to remove was not registered. - """ - if name not in cls.registered_controllers: - raise KeyError("Flow controller not found: %s" % name) - del cls.registered_controllers[name] - - @classmethod - def controller_factory(cls, passes: list[BasePass], options, **partial_controller): - """Constructs a flow controller based on the partially evaluated controller arguments. - - Args: - passes (list[TBasePass]): passes to add to the flow controller. - options (dict): PassManager options. - **partial_controller (dict): Partially evaluated controller arguments in the form - `{name:partial}` - - Raises: - TranspilerError: When partial_controller is not well-formed. - - Returns: - FlowController: A FlowController instance. - """ - if None in partial_controller.values(): - raise TranspilerError("The controller needs a condition.") - - if partial_controller: - for registered_controller in cls.registered_controllers.keys(): - if registered_controller in partial_controller: - return cls.registered_controllers[registered_controller]( - passes, options, **partial_controller - ) - raise TranspilerError("The controllers for %s are not registered" % partial_controller) - - return FlowControllerLinear(passes, options) - - -class FlowControllerLinear(FlowController): - """The basic controller runs the passes one after the other.""" - - def __init__(self, passes, options): # pylint: disable=super-init-not-called - self.passes = self._passes = passes - self.options = options - - -class DoWhileController(FlowController): - """Implements a set of passes in a do-while loop.""" - - def __init__(self, passes, options=None, do_while=None, **partial_controller): - self.do_while = do_while - self.max_iteration = options["max_iteration"] if options else 1000 - super().__init__(passes, options, **partial_controller) +def _rename_callback_args(callback): + """A helper function to run callback with conventional argument names.""" + if callback is None: + return callback - def __iter__(self): - for _ in range(self.max_iteration): - yield from self.passes + def _call_with_dag(pass_, passmanager_ir, time, property_set, count): + callback( + pass_=pass_, + dag=passmanager_ir, + time=time, + property_set=property_set, + count=count, + ) - if not self.do_while(): - return + return _call_with_dag - raise TranspilerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration) +# A temporary error handling with slight overhead at class loading. +# This method wraps all class methods to replace PassManagerError with TranspilerError. +# The pass flow controller mechanics raises PassManagerError, as it has been moved to base class. +# PassManagerError is not caught by TranspilerError due to the hierarchy. -class ConditionalController(FlowController): - """Implements a set of passes under a certain condition.""" - def __init__(self, passes, options=None, condition=None, **partial_controller): - self.condition = condition - super().__init__(passes, options, **partial_controller) +def _replace_error(meth): + @wraps(meth) + def wrapper(*meth_args, **meth_kwargs): + try: + return meth(*meth_args, **meth_kwargs) + except PassManagerError as ex: + raise TranspilerError(ex.message) from ex - def __iter__(self): - if self.condition(): - yield from self.passes + return wrapper -# Default controllers -FlowController.add_flow_controller("condition", ConditionalController) -FlowController.add_flow_controller("do_while", DoWhileController) +for _name, _method in inspect.getmembers(RunningPassManager, predicate=inspect.isfunction): + if _name.startswith("_"): + # Ignore protected and private. + # User usually doesn't directly execute and catch error from these methods. + continue + _wrapped = _replace_error(_method) + setattr(RunningPassManager, _name, _wrapped) diff --git a/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml b/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml new file mode 100644 index 000000000000..a3df013fcf40 --- /dev/null +++ b/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + A new module :mod:`qiskit.passmanager` is added. + This module implements a generic pass manager and flow controllers, + and provides an infrastructure to manage execution of transform passes. + The pass manager is a baseclass and not aware of the input and output object types, + and you need to create a subclass of the pass manager + for a particular program data to optimize. + The :mod:`qiskit.transpiler` module is also reorganized to rebuild the existing + quantum circuit pass manager based off of new generic pass manager. + See upgrade notes for more details. +upgrade: + - | + :class:`qiskit.transpiler.PassManager` is now a subclass of + :class:`qiskit.passmanager.BasePassManager`. There is no functional modification + due to this class hierarchy change. + - | + New error baseclass :class:`~qiskit.passmanager.PassManagerError` is introduced. + This will replace :class:`~qiskit.transpiler.TranspilerError` raised in the + pass handling machinery. The TranspilerError is now only used for the errors + related to the failure in handling the quantum circuit or DAG circuit object. + Note that the TranspilerError can be caught by the PassManagerError + because of their class hierarchy. For backward compatibility, + :class:`qiskit.transpiler.PassManager` catches PassManagerError and + re-raises the TranspilerError. This error replacement will be dropped in future.