diff --git a/examples/python/using_qiskit_terra_level_2.py b/examples/python/using_qiskit_terra_level_2.py index 4d97e84537a4..7b7baed71e1e 100644 --- a/examples/python/using_qiskit_terra_level_2.py +++ b/examples/python/using_qiskit_terra_level_2.py @@ -52,7 +52,7 @@ # 2. custom compile -- customize PassManager to run specific circuit transformations from qiskit.transpiler.passes import CXCancellation pm = transpiler.PassManager() -pm.add_pass(CXCancellation()) +pm.add_passes(CXCancellation()) qobj_custom = transpiler.compile(circ, backend_device, pass_manager=pm) [compiled_custom] = qobj_to_circuits(qobj_custom) print(compiled_custom.qasm()) diff --git a/qiskit/transpiler/README.md b/qiskit/transpiler/README.md new file mode 100644 index 000000000000..a679b79baf2f --- /dev/null +++ b/qiskit/transpiler/README.md @@ -0,0 +1,142 @@ +## Goals and elements +The main goal of Qiskit Terra's transpiler is to provide an extensible infrastructure of pluggable passes that allows flexibility in customizing the compilation pipeline through the creation and combination of new passes. + +### Passes +- Passes run with the implementation of the abstract method `run`, which takes and returns a DAG (directed acyclic graph) representation of the circuit. +- Passes are instances of either `AnalysisPass` or `TransformationPass`. +- Passes are described not just by their class, but also by their parameters (see Use cases: pass identity) +- Analysis passes analyze the DAG and write conclusions to a common context, a `PropertySet` object. They cannot modify the DAG. +- Transformation passes can alter the DAG, but have read-only access to the property set. + +### Pass Mananger +- A `PassManager` instance determines the schedule for running registered passes. +- The pass manager is in charge of deciding the next pass to run, not the pass itself. +- Registering passes in the pass manager pipeline is done by the `add_passes` method. +- While registering, you can specify basic control primitives over each pass (conditionals and loops). +- Options to control the scheduler: + - Passes can have arguments at init time that can affect their scheduling. If you want to set properties related to how the pass is run, you can do so by accessing these properties (e.g. pass_.max_iteration = 10). + - Options set from the pass manager take more precedence over those set at the time of adding a pass set, and those take more precedence over the options of each individual pass. (see [tests](https://github.com/Qiskit/qiskit-terra/master/test/transpiler/test_pass_scheduler.py)) + + +### Pass dependency control +The transpiler architecture allows passes to declare two kinds of dependency control to the pass manager: +- `requires` are passes that need to have been run before executing the current pass. +- `preserves` are passes that are not invalidated by the current pass. +- Analysis passes preserve all. +- The `requires` and `preserves` lists contain concrete instances of other passes (i.e. with specific pass parameters). + + +### Control Flow Plugins +By default, there are two control flow plugins included in the default pass manager: `do_while` and `conditional` (see **Fixed Point** and **Conditional** sue cases). You might want to add more control flow plugins. For example, a for-loop can be implemented in the following way: + +```Python +class DoXTimesController(FlowController): + def __init__(self, passes, do_x_times, **_): + self.do_x_times = do_x_times() + super().__init__(passes) + + def __iter__(self): + for _ in range(self.do_x_times): + for pass_ in self.working_list: + yield pass_ +``` + +The plugin is added to the pass manager in this way: + +``` +self.passmanager.add_flow_controller('do_x_times', DoXTimesController) +``` + +This allows to use the parameter `do_x_times`, which needs to be a callable. In this case, this is used to parametrized the plugin, so it will for-loop 3 times. + +``` +self.passmanager.add_passes([Pass()], do_x_times=lambda x : 3) +``` + + +## Use cases +### A simple chain with dependencies: +The `CxCancellation` requires and preserves `ToffoliDecompose`. Same for `RotationMerge`. The pass `Mapper` requires extra information for running (the `coupling_map`, in this case). + +``` +pm = PassManager() +pm.add_passes(CxCancellation()) # requires: ToffoliDecompose / preserves: ToffoliDecompose +pm.add_passes(RotationMerge()) # requires: ToffoliDecompose / preserves: ToffoliDecompose +pm.add_passes(Mapper(coupling_map=coupling_map)) # requires: [] / preserves: [] +pm.add_passes(CxCancellation()) +``` + +Given the above, the pass manager executes the following sequence of passes: + +1. `ToffoliDecompose`, because it is required by `CxCancellation`. +2. `CxCancellation` +3. `RotationMerge`, because, even when `RotationMerge` also requires `ToffoliDecompose`, the `CxCancellation` preserved it, so no need to run it again. +4. `Mapper` +5. `ToffoliDecompose`, because `Mapper` did not preserved `ToffoliDecompose` and is require by `CxCancellation` +6. `CxCancellation` + +### Same pass with different parameters (pass identity) +A pass behavior can be heavily influenced by its parameters. For example, unrolling using some basis gates is totally different than unrolling to different gates. And a PassManager might use both. + +``` +pm.add_passes(Unroller(basis_gates=['id','u1','u2','u3','cx'])) +pm.add_passes(...) +pm.add_passes(Unroller(basis_gates=['U','CX'])) +``` + +where (from `qelib1.inc`): + +``` +gate id q { U(0,0,0) q; } +gate u1(lambda) q { U(0,0,lambda) q; } +gate u2(phi,lambda) q { U(pi/2,phi,lambda) q; } +gate u3(theta,phi,lambda) q { U(theta,phi,lambda) q; } +gate cx c,t { CX c,t; } +``` + +For this reason, the identity of a pass is given by its name and parameters. + +### While loop up to fixed point +There are cases when one or more passes have to be run repeatedly, until a condition is fulfilled. + +``` +pm = PassManager() +pm.add_passes([CxCancellation(), RotationMerge(), CalculateDepth()], + do_while=lambda property_set: not property_set['fixed_point']['depth']) +``` +The control argument `do_while` will run these passes until the callable returns `False`. The callable always takes in one argument, the pass manager's property set. In this example, `CalculateDepth` is an analysis pass that updates the property `depth` in the property set. + +### Conditional +The pass manager developer can avoid one or more passes by making them conditional (on a property in the property set): + +``` +pm.add_passes(LayoutMapper(coupling_map)) +pm.add_passes(CheckIfMapped(coupling_map)) +pm.add_passes(SwapMapper(coupling_map), + condition=lambda property_set: not property_set['is_mapped']) +``` + +The `CheckIfMapped` is an analysis pass that updates the property `is_mapped`. If `LayoutMapper` could map the circuit to the coupling map, the `SwapMapper` is unnecessary. + + +### Idempotent passes +If a pass is idempotent, the transpiler can use that property to perform certain optimizations. +A pass is idempotent if `pass.run(pass.run(dag)) == pass.run(dag)`. Analysis passes are idempotent by definition, since they do not modify the DAG. Transformation passes can declare themselves as idempotent by annotating as *self preserve* in the following way (`<-`): + +```Python +class IdempotentPass(TransformationPass): + def __init__(self): + super().__init__() + self.preserves.append(self) # <- +``` + +### Misbehaving passes +To help the pass developer discipline, if an analysis pass attempts to modify the DAG or if a transformation pass tries to set a property in the property set of the pass manager, a `TranspilerAccessError` raises. + +The enforcement of this does not attempt to be strict. + +## Next Steps + +* Support "provides". Different mappers provide the same feature. In this way, a pass can request `mapper` and any mapper that provides `mapper` can be used. +* It might be handy to have property set items in the field `requires` and analysis passes that "provide" that field. +* Move passes to this scheme and, on the way, well-define a DAG API. diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 31b66626a1bb..fa034f1b9e0f 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -7,12 +7,12 @@ """Utils for transpiler.""" import os -from ._passmanager import PassManager -from ._transpilererror import TranspilerError - -# pylint: disable=redefined-builtin -from ._transpiler import compile, transpile - +from ._passmanager import PassManager, FlowController +from ._propertyset import PropertySet +from ._transpilererror import TranspilerError, TranspilerAccessError +from ._fencedobjs import FencedDAGCircuit, FencedPropertySet +from ._basepasses import AnalysisPass, TransformationPass +from ._transpiler import compile, transpile # pylint: disable=redefined-builtin from ._parallel import parallel_map from ._progressbar import TextProgressBar diff --git a/qiskit/transpiler/_basepass.py b/qiskit/transpiler/_basepass.py deleted file mode 100644 index af08fd1b9a9d..000000000000 --- a/qiskit/transpiler/_basepass.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2018, IBM. -# -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. - -"""This module implements the base pass. -""" -from abc import ABC, abstractmethod - - -class BasePass(ABC): - """Base class for transpiler passes.""" - - @abstractmethod - def run(self, dag): - """Run a pass on the DAGCircuit. - """ - return dag diff --git a/qiskit/transpiler/_basepasses.py b/qiskit/transpiler/_basepasses.py new file mode 100644 index 000000000000..f9fd889d7419 --- /dev/null +++ b/qiskit/transpiler/_basepasses.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""This module implements the base pass.""" + +from abc import abstractmethod +from collections import Hashable +from inspect import signature + + +class MetaPass(type): + """ + Enforces the creation of some fields in the pass + while allowing passes to override __init__ + """ + + def __call__(cls, *args, **kwargs): + if '_pass_cache' not in cls.__dict__.keys(): + cls._pass_cache = {} + args, kwargs = cls.normalize_parameters(*args, **kwargs) + hash_ = hash(MetaPass._freeze_init_parameters(cls.__init__, args, kwargs)) + if hash_ not in cls._pass_cache: + new_pass = type.__call__(cls, *args, **kwargs) + cls._pass_cache[hash_] = new_pass + return cls._pass_cache[hash_] + + @staticmethod + def _freeze_init_parameters(init_method, args, kwargs): + self_guard = object() + init_signature = signature(init_method) + bound_signature = init_signature.bind(self_guard, *args, **kwargs) + arguments = [] + 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 BasePass(metaclass=MetaPass): + """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 = {} # This pass's pointer to the pass manager's property set. + + @classmethod + def normalize_parameters(cls, *args, **kwargs): + """ + Because passes with the same args/kwargs are considered the same, this method allows to + modify the args/kargs to respect that identity. + Args: + *args: args to normalize + **kwargs: kwargs to normalize + + Returns: + tuple: normalized (list(args), dict(kwargs)) + """ + return args, kwargs + + def name(self): + """ The name of the pass. """ + return self.__class__.__name__ + + @abstractmethod + def run(self, dag): + """ + Run a pass on the DAGCircuit. This is implemented by the pass developer. + Args: + dag (DAGCircuit): the dag on which the pass is run. + Raises: + NotImplementedError: when this is left unimplemented for a pass. + """ + raise NotImplementedError + + @property + def is_transformation_pass(self): + """ If the pass is a TransformationPass, that means that the pass can manipulate the DAG, + but cannot modify the property set (but it can be read). """ + return isinstance(self, TransformationPass) + + @property + def is_analysis_pass(self): + """ If the pass is an AnalysisPass, that means that the pass can analyze the DAG and write + the results of that analysis in the property set. Modifications on the DAG are not allowed + by this kind of pass. """ + return isinstance(self, AnalysisPass) + + +class AnalysisPass(BasePass): # pylint: disable=abstract-method + """ An analysis pass: change property set, not DAG. """ + pass + + +class TransformationPass(BasePass): # pylint: disable=abstract-method + """ A transformation pass: change DAG, not property set. """ + pass diff --git a/qiskit/transpiler/_fencedobjs.py b/qiskit/transpiler/_fencedobjs.py new file mode 100644 index 000000000000..a65ac242fd6e --- /dev/null +++ b/qiskit/transpiler/_fencedobjs.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" Fenced objects are wraps for raising TranspilerAccessError when they are modified.""" + +from ._transpilererror import TranspilerAccessError + + +class FencedObject(): + """ Given an instance and a list of attributes to fence, raises a TranspilerAccessError when one + of these attributes is accessed.""" + + def __init__(self, instance, attributes_to_fence): + self._wrapped = instance + self._attributes_to_fence = attributes_to_fence + + def __getattribute__(self, name): + object.__getattribute__(self, '_check_if_fenced')(name) + return getattr(object.__getattribute__(self, '_wrapped'), name) + + def __getitem__(self, key): + object.__getattribute__(self, '_check_if_fenced')('__getitem__') + return object.__getattribute__(self, '_wrapped')[key] + + def __setitem__(self, key, value): + object.__getattribute__(self, '_check_if_fenced')('__setitem__') + object.__getattribute__(self, '_wrapped')[key] = value + + def _check_if_fenced(self, name): + """ + Checks if the attribute name is in the list of attributes to protect. If so, raises + TranpilerAccessError. + + Args: + name (string): the attribute name to check + + Raises: + TranspilerAccessError: when name is the list of attributes to protect. + """ + if name in object.__getattribute__(self, '_attributes_to_fence'): + raise TranspilerAccessError("The fenced %s has the property %s protected" % + (type(object.__getattribute__(self, '_wrapped')), name)) + + +class FencedPropertySet(FencedObject): + """ A property set that cannot be written (via __setitem__) """ + def __init__(self, property_set_instance): + super().__init__(property_set_instance, ['__setitem__']) + + +class FencedDAGCircuit(FencedObject): + """ A dag circuit that cannot be modified (via _remove_op_node) """ + # FIXME: add more fenced methods of the dag after dagcircuit rewrite + def __init__(self, dag_circuit_instance): + super().__init__(dag_circuit_instance, ['_remove_op_node']) diff --git a/qiskit/transpiler/_passmanager.py b/qiskit/transpiler/_passmanager.py index 1c5c00d08111..c4ee9ada4369 100644 --- a/qiskit/transpiler/_passmanager.py +++ b/qiskit/transpiler/_passmanager.py @@ -1,33 +1,276 @@ # -*- coding: utf-8 -*- -# pylint: disable=redefined-builtin - # Copyright 2018, IBM. # # This source code is licensed under the Apache License, Version 2.0 found in # the LICENSE.txt file in the root directory of this source tree. -"""This module implements a passmanager -""" +"""PassManager class for the transpiler.""" + +from functools import partial +from collections import OrderedDict +from qiskit.dagcircuit import DAGCircuit +from ._propertyset import PropertySet +from ._basepasses import BasePass +from ._fencedobjs import FencedPropertySet, FencedDAGCircuit +from ._transpilererror import TranspilerError class PassManager(): - """PassManager class for the transpiler. - - A PassManager instance is responsible for launching the requisite - analysis and transformation passes on the quantum circuit, and to - do this correctly & efficiently - (i.e. keep track of dependencies between passes) - """ - def __init__(self): - """Initialize an empty PassManager object - (with no passes scheduled). - """ - self._passes = [] - - def add_pass(self, pass_): - """Schedule a pass in the passmanager.""" - self._passes.append(pass_) - - def passes(self): - """Return list of passes scheduled.""" - return self._passes + """ A PassManager schedules the passes """ + + def __init__(self, ignore_requires=None, ignore_preserves=None, max_iteration=None): + """ + Initialize an empty PassManager object (with no passes scheduled). + + Args: + ignore_requires (bool): The schedule ignores the requires field in the passes. The + default setting in the pass is False. + ignore_preserves (bool): The schedule ignores the preserves field in the passes. The + default setting in the pass is False. + max_iteration (int): The schedule looping iterates until the condition is met or until + max_iteration is reached. + """ + # the pass manager's schedule of passes, including any control-flow. + # Populated via PassManager.add_passes(). + self.working_list = [] + + # global property set is the context of the circuit held by the pass manager + # as it runs through its scheduled passes. Analysis passes may update the property_set, + # but transformation passes have read-only access (via the fenced_property_set). + self.property_set = PropertySet() + 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 = {'ignore_requires': ignore_requires, + 'ignore_preserves': ignore_preserves, + 'max_iteration': max_iteration} + + def _join_options(self, passset_options): + """ Set the options of each passset, based on precedence rules: + passset options (set via ``PassManager.add_passes()``) override + passmanager options (set via ``PassManager.__init__()``), which override Default. + . + """ + default = {'ignore_preserves': False, # Ignore preserves for this pass + 'ignore_requires': False, # Ignore requires for this pass + 'max_iteration': 1000} # Maximum allowed iteration on this pass + + passmanager_level = {k: v for k, v in self.passmanager_options.items() if v is not None} + passset_level = {k: v for k, v in passset_options.items() if v is not None} + return {**default, **passmanager_level, **passset_level} + + def add_passes(self, passes, ignore_requires=None, ignore_preserves=None, max_iteration=None, + **flow_controller_conditions): + """ + Args: + passes (list[BasePass] or BasePass): pass(es) to be added to schedule + ignore_preserves (bool): ignore the preserves claim of passes. Default: False + ignore_requires (bool): ignore the requires need of passes. Default: False + max_iteration (int): max number of iterations of passes. Default: 1000 + flow_controller_conditions (kwargs): See add_flow_controller(): 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. + """ + + passset_options = {'ignore_requires': ignore_requires, + 'ignore_preserves': ignore_preserves, + 'max_iteration': max_iteration} + + options = self._join_options(passset_options) + + if isinstance(passes, BasePass): + passes = [passes] + + for pass_ in passes: + if not isinstance(pass_, BasePass): + raise TranspilerError('%s is not a pass instance' % pass_.__class__) + + for name, param in flow_controller_conditions.items(): + if callable(param): + flow_controller_conditions[name] = partial(param, self.fenced_property_set) + else: + raise TranspilerError('The flow controller parameter %s is not callable' % name) + + self.working_list.append( + FlowController.controller_factory(passes, options, **flow_controller_conditions)) + + def run_passes(self, dag): + """Run all the passes on the dag. + + Args: + dag (DAGCircuit): dag circuit to transform via all the registered passes + """ + for passset in self.working_list: + for pass_ in passset: + dag = self._do_pass(pass_, dag, passset.options) + + def _do_pass(self, pass_, dag, options): + """Do a pass and its "requires". + + Args: + pass_ (BasePass): Pass to do. + dag (DAGCircuit): The dag on which the pass is ran. + options (dict): PassManager options. + 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. + """ + + # First, do the requires of pass_ + if not options["ignore_requires"]: + for required_pass in pass_.requires: + self._do_pass(required_pass, dag, options) + + # Run the pass itself, if not already run + if pass_ not in self.valid_passes: + if pass_.is_transformation_pass: + pass_.property_set = self.fenced_property_set + new_dag = pass_.run(dag) + if not isinstance(new_dag, DAGCircuit): + 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 + elif pass_.is_analysis_pass: + pass_.property_set = self.property_set + pass_.run(FencedDAGCircuit(dag)) + else: + raise TranspilerError("I dont know how to handle this type of pass") + + # update the valid_passes property + self._update_valid_passes(pass_, options['ignore_preserves']) + + return dag + + def _update_valid_passes(self, pass_, ignore_preserves): + self.valid_passes.add(pass_) + if not pass_.is_analysis_pass: # Analysis passes preserve all + if ignore_preserves: + self.valid_passes.clear() + else: + self.valid_passes.intersection_update(set(pass_.preserves)) + + +class FlowController(): + """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 = FlowController.controller_factory(passes, options, **partial_controller) + self.options = options + + def __iter__(self): + for pass_ in self.passes: + yield pass_ + + @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, options, **partial_controller): + """ + Constructs a flow controller based on the partially evaluated controller arguments. + Args: + passes (list[BasePass]): 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) + else: + return FlowControllerLinear(passes, options) + + +class FlowControllerLinear(FlowController): + """ The basic controller run the passes one after the other one. """ + + def __init__(self, passes, options): # pylint: disable=super-init-not-called + self.passes = passes + self.options = options + + +class DoWhileController(FlowController): + """Implements a set of passes in a do while loop. """ + + def __init__(self, passes, options, do_while=None, + **partial_controller): + self.do_while = do_while + self.max_iteration = options['max_iteration'] + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + for _ in range(self.max_iteration): + for pass_ in self.passes: + yield pass_ + + if not self.do_while(): + return + + raise TranspilerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration) + + +class ConditionalController(FlowController): + """Implements a set of passes under certain condition. """ + + def __init__(self, passes, options, condition=None, + **partial_controller): + self.condition = condition + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + if self.condition(): + for pass_ in self.passes: + yield pass_ + + +# Default controllers +FlowController.add_flow_controller('condition', ConditionalController) +FlowController.add_flow_controller('do_while', DoWhileController) diff --git a/qiskit/transpiler/_propertyset.py b/qiskit/transpiler/_propertyset.py new file mode 100644 index 000000000000..84cf69582f4f --- /dev/null +++ b/qiskit/transpiler/_propertyset.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" A property set is maintained by the PassManager to keep information +about the current state of the circuit """ + + +class PropertySet: + """ A dictionary-like object """ + + def __init__(self): + self._properties = {} + + def __getitem__(self, key): + return self._properties.get(key, None) + + def __setitem__(self, key, value): + self._properties[key] = value diff --git a/qiskit/transpiler/_transpiler.py b/qiskit/transpiler/_transpiler.py index 7f6c68392af2..62557e00bc2b 100644 --- a/qiskit/transpiler/_transpiler.py +++ b/qiskit/transpiler/_transpiler.py @@ -336,8 +336,7 @@ def transpile(dag, basis_gates='u1,u2,u3,cx,id', coupling_map=None, if pass_manager: # run the passes specified by the pass manager - for pass_ in pass_manager.passes(): - pass_.run(dag) + pass_manager.run_passes(dag) else: # default set of passes # TODO: move each step here to a pass, and use a default passmanager below diff --git a/qiskit/transpiler/_transpilererror.py b/qiskit/transpiler/_transpilererror.py index 81ec4325adbe..d082e1a17bd3 100644 --- a/qiskit/transpiler/_transpilererror.py +++ b/qiskit/transpiler/_transpilererror.py @@ -13,3 +13,7 @@ class TranspilerError(QISKitError): """Exceptions raised during transpilation""" + + +class TranspilerAccessError(QISKitError): + """ Exception of access error in the transpiler passes. """ diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 6fae875a2278..435d07691c7f 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -8,3 +8,4 @@ """Utils for transpiler.""" from .cx_cancellation import CXCancellation +from .fixed_point import FixedPoint diff --git a/qiskit/transpiler/passes/cx_cancellation.py b/qiskit/transpiler/passes/cx_cancellation.py index 34fec79d512c..3aa882858627 100644 --- a/qiskit/transpiler/passes/cx_cancellation.py +++ b/qiskit/transpiler/passes/cx_cancellation.py @@ -7,21 +7,20 @@ """Pass for peep-hole cancellation of consecutive CX gates. """ -from qiskit.transpiler._basepass import BasePass +from qiskit.transpiler._basepasses import TransformationPass -class CXCancellation(BasePass): +class CXCancellation(TransformationPass): """Cancel back-to-back 'cx' gates in dag.""" - def __init__(self): - pass - def run(self, dag): """ - run one pass of cx cancellation on the circuit + Run one pass of cx cancellation on the circuit Args: - dag (DAGCircuit): the directed acyclic graph to run on + dag (DAGCircuit): the directed acyclic graph to run on. + Returns: + DAGCircuit: Transformed DAG. """ cx_runs = dag.collect_runs(["cx"]) for cx_run in cx_runs: @@ -45,3 +44,4 @@ def run(self, dag): else: for n in chunk[1:]: dag._remove_op_node(n) + return dag diff --git a/qiskit/transpiler/passes/fixed_point.py b/qiskit/transpiler/passes/fixed_point.py new file mode 100644 index 000000000000..d583567560c3 --- /dev/null +++ b/qiskit/transpiler/passes/fixed_point.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" TODO +""" +from collections import defaultdict +from qiskit.transpiler._basepasses import AnalysisPass + + +class FixedPoint(AnalysisPass): + """ A dummy analysis pass that checks if a property reached a fixed point. The results is saved + in property_set['fixed_point'][] as a boolean. + """ + + def __init__(self, property_to_check): + """ + Args: + property_to_check (str): The property to check if a fixed point was reached. + """ + super().__init__() + self._property = property_to_check + self._previous_value = None + + def run(self, dag): + if self.property_set['fixed_point'] is None: + self.property_set['fixed_point'] = defaultdict(lambda: False) + + current_value = self.property_set[self._property] + + if self._previous_value is not None: + self.property_set['fixed_point'][self._property] = self._previous_value == current_value + + self._previous_value = current_value diff --git a/test/python/test_transpiler.py b/test/python/test_transpiler.py index d4531426c3bf..7ba0c7172ebb 100644 --- a/test/python/test_transpiler.py +++ b/test/python/test_transpiler.py @@ -95,7 +95,7 @@ def test_pass_cx_cancellation(self): dag_circuit = DAGCircuit.fromQuantumCircuit(circuit) pass_manager = PassManager() - pass_manager.add_pass(CXCancellation()) + pass_manager.add_passes(CXCancellation()) dag_circuit = transpile(dag_circuit, pass_manager=pass_manager) resources_after = dag_circuit.count_ops() diff --git a/test/python/transpiler/__init__.py b/test/python/transpiler/__init__.py new file mode 100644 index 000000000000..c35aabd3f9e6 --- /dev/null +++ b/test/python/transpiler/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""QISKit transpiler unit tests.""" diff --git a/test/python/transpiler/_dummy_passes.py b/test/python/transpiler/_dummy_passes.py new file mode 100644 index 000000000000..6f685312b575 --- /dev/null +++ b/test/python/transpiler/_dummy_passes.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# pylint: disable=invalid-name,super-init-not-called + +"""Dummy passes used by Tranpiler testing""" + +import logging +from qiskit.transpiler.passes import FixedPoint + +from qiskit.transpiler import TransformationPass, AnalysisPass + +logger = "LocalLogger" + + +class DummyTP(TransformationPass): + """ A dummy transformation pass.""" + + def run(self, dag): + logging.getLogger(logger).info('run transformation pass %s', self.name()) + return dag + + +class DummyAP(AnalysisPass): + """ A dummy analysis pass.""" + + def run(self, dag): + logging.getLogger(logger).info('run analysis pass %s', self.name()) + + +class PassA_TP_NR_NP(DummyTP): + """ A dummy pass without any requires/preserves. + TP: Transformation Pass + NR: No requires + NP: No preserves + """ + + def __init__(self): + super().__init__() + self.preserves.append(self) # preserves itself (idempotence) + + +class PassB_TP_RA_PA(DummyTP): + """ A dummy pass that requires PassA_TP_NR_NP and preserves it. + TP: Transformation Pass + RA: Requires PassA + PA: Preserves PassA + """ + + def __init__(self): + super().__init__() + self.requires.append(PassA_TP_NR_NP()) + self.preserves.append(PassA_TP_NR_NP()) + self.preserves.append(self) # preserves itself (idempotence) + + +class PassC_TP_RA_PA(DummyTP): + """ A dummy pass that requires PassA_TP_NR_NP and preserves it. + TP: Transformation Pass + RA: Requires PassA + PA: Preserves PassA + """ + + def __init__(self): + super().__init__() + self.requires.append(PassA_TP_NR_NP()) + self.preserves.append(PassA_TP_NR_NP()) + self.preserves.append(self) # preserves itself (idempotence) + + +class PassD_TP_NR_NP(DummyTP): + """ A dummy transfomation pass that takes an argument. + TP: Transformation Pass + NR: No Requires + NP: No Preserves + """ + + def __init__(self, argument1=None, argument2=None): + super().__init__() + self.argument1 = argument1 + self.argument2 = argument2 + self.preserves.append(self) # preserves itself (idempotence) + + def run(self, dag): + super().run(dag) + logging.getLogger(logger).info('argument %s', self.argument1) + return dag + + +class PassE_AP_NR_NP(DummyAP): + """ A dummy analysis pass that takes an argument. + AP: Analysis Pass + NR: No Requires + NP: No Preserves + """ + + def __init__(self, argument1): + super().__init__() + self.argument1 = argument1 + + def run(self, dag): + super().run(dag) + self.property_set['property'] = self.argument1 + logging.getLogger(logger).info('set property as %s', self.property_set['property']) + + +class PassF_reduce_dag_property(DummyTP): + """ A dummy transformation pass that (sets and) reduces a property in the DAG. + NI: Non-idempotent transformation pass + NR: No Requires + NP: No Preserves + """ + + def run(self, dag): + super().run(dag) + if not hasattr(dag, 'property'): + dag.property = 8 + dag.property = round(dag.property * 0.8) + logging.getLogger(logger).info('dag property = %i', dag.property) + return dag + + +class PassG_calculates_dag_property(DummyAP): + """ A dummy transformation pass that "calculates" property in the DAG. + AP: Analysis Pass + NR: No Requires + NP: No Preserves + """ + + def run(self, dag): + super().run(dag) + if hasattr(dag, 'property'): + self.property_set['property'] = dag.property + else: + self.property_set['property'] = 8 + logging.getLogger(logger).info('set property as %s (from dag.property)', + self.property_set['property']) + + +class PassH_Bad_TP(DummyTP): + """ A dummy transformation pass tries to modify the property set. + NR: No Requires + NP: No Preserves + """ + + def run(self, dag): + super().run(dag) + self.property_set['property'] = "value" + logging.getLogger(logger).info('set property as %s', self.property_set['property']) + return dag + + +class PassI_Bad_AP(DummyAP): + """ A dummy analysis pass tries to modify the dag. + NR: No Requires + NP: No Preserves + """ + + def run(self, dag): + super().run(dag) + cx_runs = dag.collect_runs(["cx"]) + logging.getLogger(logger).info('cx_runs: %s', cx_runs) + dag._remove_op_node(cx_runs.pop()[0]) + logging.getLogger(logger).info('done removing') + + +class PassJ_Bad_NoReturn(DummyTP): + """ A bad dummy transformation pass that does not return a DAG. + NR: No Requires + NP: No Preserves + """ + + def run(self, dag): + super().run(dag) + return "Something else than DAG" + + +class PassK_check_fixed_point_property(DummyAP, FixedPoint): + """ A dummy analysis pass that checks if a property reached a fixed point. The results is saved + in property_set['fixed_point'][] as a boolean + AP: Analysis Pass + R: PassG_calculates_dag_property() + """ + + def __init__(self): + FixedPoint.__init__(self, 'property') + self.requires.append(PassG_calculates_dag_property()) + + def run(self, dag): + for base in PassK_check_fixed_point_property.__bases__: + base.run(self, dag) diff --git a/test/python/transpiler/test_fixed_point_pass.py b/test/python/transpiler/test_fixed_point_pass.py new file mode 100644 index 000000000000..6461091bc0c8 --- /dev/null +++ b/test/python/transpiler/test_fixed_point_pass.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""FixedPoint pass testing""" + +import unittest +from qiskit.transpiler import PropertySet +from qiskit.transpiler.passes import FixedPoint +from ..common import QiskitTestCase + + +class TestFixedPointPass(QiskitTestCase): + """ Tests for PropertySet methods. """ + + def setUp(self): + self.pass_ = FixedPoint('property') + self.pset = self.pass_.property_set = PropertySet() + self.dag = None # The pass do not read the DAG. + + def test_requires_field_none(self): + """ When pass_that_updates_the_property is not passed, there are no requirements. """ + self.assertEqual(len(self.pass_.requires), 0) + + def test_fixed_point_property_is_created(self): + """ The property set does not have a property called "fixed_point" and it is created after + the first run of the pass. """ + self.assertIsNone(self.pset['fixed_point']) + self.pass_.run(self.dag) + self.assertIsNotNone(self.pset['fixed_point']) + + def test_fixed_point_setting_to_none(self): + """ Setting a property to None twice does not create a fixed-point. """ + self.pass_.property_set['property'] = None + self.pass_.run(self.dag) + self.pass_.property_set['property'] = None + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + + def test_fixed_point_reached(self): + """ Setting a property to the same value twice creates a fixed-point. """ + self.pset['property'] = 1 + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + self.pset['property'] = 1 + self.pass_.run(self.dag) + self.assertTrue(self.pset['fixed_point']['property']) + + def test_fixed_point_not_reached(self): + """ Setting a property with different values does not create a fixed-point. """ + self.pset['property'] = 1 + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + self.pset['property'] = 2 + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + + def test_fixed_point_left(self): + """ A fixed-point is not permanent. """ + self.pset['property'] = 1 + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + self.pset['property'] = 1 + self.pass_.run(self.dag) + self.assertTrue(self.pset['fixed_point']['property']) + self.pset['property'] = 2 + self.pass_.run(self.dag) + self.assertFalse(self.pset['fixed_point']['property']) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/transpiler/test_generic_pass.py b/test/python/transpiler/test_generic_pass.py new file mode 100644 index 000000000000..4c46d76594f8 --- /dev/null +++ b/test/python/transpiler/test_generic_pass.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# pylint: disable=invalid-name + +"""BasePass and generic pass testing""" + +import unittest.mock +from ._dummy_passes import DummyAP, DummyTP, PassA_TP_NR_NP, PassD_TP_NR_NP, PassE_AP_NR_NP +from ..common import QiskitTestCase + + +class TestGenericPass(QiskitTestCase): + """ Passes have common characteristics defined in BasePass.""" + + def test_is_TP_or_AP(self): + """ Passes have is_transformation_pass and is_analysis_pass properties.""" + tp_pass = DummyTP() + self.assertTrue(tp_pass.is_transformation_pass) + self.assertFalse(tp_pass.is_analysis_pass) + ap_pass = DummyAP() + self.assertFalse(ap_pass.is_transformation_pass) + self.assertTrue(ap_pass.is_analysis_pass) + + def test_pass_diff_TP_AP(self): + """ Different passes are different """ + pass1 = DummyAP() + pass2 = DummyTP() + self.assertNotEqual(pass1, pass2) + + def test_pass_diff_parent_child(self): + """ Parents are different from their children """ + pass2 = DummyTP() + pass1 = PassD_TP_NR_NP() + self.assertNotEqual(pass1, pass2) + + def test_pass_diff_args(self): + """ Same pass with different arguments are different """ + pass1 = PassD_TP_NR_NP(argument1=[1, 2]) + pass2 = PassD_TP_NR_NP(argument1=[2, 1]) + self.assertNotEqual(pass1, pass2) + + def test_pass_kwargs_out_of_order(self): + """ Passes instances with same arguments (independently of the order) are the same""" + pass1 = PassD_TP_NR_NP(argument1=1, argument2=2) + pass2 = PassD_TP_NR_NP(argument2=2, argument1=1) + self.assertEqual(pass1, pass2) + + def test_pass_kwargs_and_args(self): + """ Passes instances with same arguments (independently if they are named or not) are the + same""" + pass1 = PassD_TP_NR_NP(1, 2) + pass2 = PassD_TP_NR_NP(argument2=2, argument1=1) + self.assertEqual(pass1, pass2) + + def test_set_identity(self): + """ Two "instances" of the same pass in a set are counted as one.""" + a_set = set() + a_set.add(PassA_TP_NR_NP()) + a_set.add(PassA_TP_NR_NP()) + self.assertEqual(len(a_set), 1) + + def test_identity_params_same_hash(self): + """ True is 1. They are not the same parameter.""" + self.assertNotEqual(PassE_AP_NR_NP(True), PassE_AP_NR_NP(1)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/transpiler/test_pass_scheduler.py b/test/python/transpiler/test_pass_scheduler.py new file mode 100644 index 000000000000..60fd5e6b2b12 --- /dev/null +++ b/test/python/transpiler/test_pass_scheduler.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# pylint: disable=invalid-name + +"""Tranpiler testing""" + +import unittest.mock + +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler import PassManager, transpile, TranspilerAccessError, TranspilerError, \ + FlowController +from qiskit.transpiler._passmanager import DoWhileController, ConditionalController +from ._dummy_passes import PassA_TP_NR_NP, PassB_TP_RA_PA, PassC_TP_RA_PA, \ + PassD_TP_NR_NP, PassE_AP_NR_NP, PassF_reduce_dag_property, \ + PassH_Bad_TP, PassI_Bad_AP, PassJ_Bad_NoReturn, PassK_check_fixed_point_property +from ..common import QiskitTestCase + +logger = "LocalLogger" + + +class SchedulerTestCase(QiskitTestCase): + """ Asserts for the scheduler. """ + + def assertScheduler(self, dag, passmanager, expected): + """ + Runs transpiler(dag, passmanager) and checks if the passes run as expected. + Args: + dag (DAGCircuit): DAG circuit to transform via transpilation. + passmanager (PassManager): pass manager instance for the tranpilation process + expected (list): List of things the passes are logging + """ + with self.assertLogs(logger, level='INFO') as cm: + transpile(dag, pass_manager=passmanager) + self.assertEqual([record.message for record in cm.records], expected) + + def assertSchedulerRaises(self, dag, passmanager, expected, exception_type): + """ + Runs transpiler(dag, passmanager) and checks if the passes run as expected until + expcetion_type is raised. + Args: + dag (DAGCircuit): DAG circuit to transform via transpilation + passmanager (PassManager): pass manager instance for the tranpilation process + expected (list): List of things the passes are logging + exception_type (Exception): Exception that is expected to be raised. + """ + with self.assertLogs(logger, level='INFO') as cm: + self.assertRaises(exception_type, transpile, dag, pass_manager=passmanager) + self.assertEqual([record.message for record in cm.records], expected) + + +class TestUseCases(SchedulerTestCase): + """ The pass manager schedules passes in, sometimes, tricky ways. These tests combine passes in + many ways, and checks that passes are ran in the right order. """ + + def setUp(self): + self.dag = DAGCircuit.fromQuantumCircuit(QuantumCircuit(QuantumRegister(1))) + self.passmanager = PassManager() + + def test_chain(self): + """ A single chain of passes, with Requests and Preserves.""" + self.passmanager.add_passes(PassC_TP_RA_PA()) # Request: PassA / Preserves: PassA + self.passmanager.add_passes(PassB_TP_RA_PA()) # Request: PassA / Preserves: PassA + self.passmanager.add_passes(PassD_TP_NR_NP(argument1=[1, 2])) # Requires: {}/ Preserves: {} + self.passmanager.add_passes(PassB_TP_RA_PA()) + self.assertScheduler(self.dag, self.passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassD_TP_NR_NP', + 'argument [1, 2]', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA']) + + def test_conditional_passes_true(self): + """ A pass set with a conditional parameter. The callable is True. """ + self.passmanager.add_passes(PassE_AP_NR_NP(True)) + self.passmanager.add_passes(PassA_TP_NR_NP(), + condition=lambda property_set: property_set['property']) + self.assertScheduler(self.dag, self.passmanager, ['run analysis pass PassE_AP_NR_NP', + 'set property as True', + 'run transformation pass PassA_TP_NR_NP']) + + def test_conditional_passes_false(self): + """ A pass set with a conditional parameter. The callable is False. """ + self.passmanager.add_passes(PassE_AP_NR_NP(False)) + self.passmanager.add_passes(PassA_TP_NR_NP(), + condition=lambda property_set: property_set['property']) + self.assertScheduler(self.dag, self.passmanager, ['run analysis pass PassE_AP_NR_NP', + 'set property as False']) + + def test_conditional_and_loop(self): + """ Run a conditional first, then a loop""" + self.passmanager.add_passes(PassE_AP_NR_NP(True)) + self.passmanager.add_passes( + [PassK_check_fixed_point_property(), + PassA_TP_NR_NP(), + PassF_reduce_dag_property()], + do_while=lambda property_set: not property_set['fixed_point']['property'], + condition=lambda property_set: property_set['property']) + self.assertScheduler(self.dag, self.passmanager, + ['run analysis pass PassE_AP_NR_NP', + 'set property as True', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 8 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 6', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 6 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 5', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 5 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 4', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 4 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 3', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 3 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2']) + + def test_loop_and_conditional(self): + """ Run a loop first, then a conditional""" + FlowController.remove_flow_controller('condition') + FlowController.add_flow_controller('condition', ConditionalController) + + self.passmanager.add_passes(PassK_check_fixed_point_property()) + self.passmanager.add_passes( + [PassK_check_fixed_point_property(), + PassA_TP_NR_NP(), + PassF_reduce_dag_property()], + do_while=lambda property_set: not property_set['fixed_point']['property'], + condition=lambda property_set: not property_set['fixed_point']['property']) + self.assertScheduler(self.dag, self.passmanager, + ['run analysis pass PassG_calculates_dag_property', + 'set property as 8 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 6', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 6 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 5', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 5 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 4', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 4 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 3', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 3 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2']) + + def test_do_not_repeat_based_on_preservation(self): + """ When a pass is still a valid pass (because following passes preserved it), it should not + run again""" + self.passmanager.add_passes([PassB_TP_RA_PA(), PassA_TP_NR_NP(), PassB_TP_RA_PA()]) + self.assertScheduler(self.dag, self.passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA']) + + def test_do_not_repeat_based_on_idempotence(self): + """ Repetition can be optimized to a single execution when the pass is idempotent""" + self.passmanager.add_passes(PassA_TP_NR_NP()) + self.passmanager.add_passes([PassA_TP_NR_NP(), PassA_TP_NR_NP()]) + self.passmanager.add_passes(PassA_TP_NR_NP()) + self.assertScheduler(self.dag, self.passmanager, ['run transformation pass PassA_TP_NR_NP']) + + def test_non_idempotent_pass(self): + """ Two or more runs of a non-idempotent pass cannot be optimized. """ + self.passmanager.add_passes(PassF_reduce_dag_property()) + self.passmanager.add_passes([PassF_reduce_dag_property(), PassF_reduce_dag_property()]) + self.passmanager.add_passes(PassF_reduce_dag_property()) + self.assertScheduler(self.dag, self.passmanager, + ['run transformation pass PassF_reduce_dag_property', + 'dag property = 6', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 5', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 4', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 3']) + + def test_fenced_property_set(self): + """ Transformation passes are not allowed to modified the property set. """ + self.passmanager.add_passes(PassH_Bad_TP()) + self.assertSchedulerRaises(self.dag, self.passmanager, + ['run transformation pass PassH_Bad_TP'], + TranspilerAccessError) + + def test_fenced_dag(self): + """ Analysis passes are not allowed to modified the DAG. """ + qr = QuantumRegister(2) + circ = QuantumCircuit(qr) + # pylint: disable=no-member + circ.cx(qr[0], qr[1]) + circ.cx(qr[0], qr[1]) + circ.cx(qr[1], qr[0]) + circ.cx(qr[1], qr[0]) + dag = DAGCircuit.fromQuantumCircuit(circ) + + self.passmanager.add_passes(PassI_Bad_AP()) + self.assertSchedulerRaises(dag, self.passmanager, + ['run analysis pass PassI_Bad_AP', + 'cx_runs: {(5, 6, 7, 8)}'], + TranspilerAccessError) + + def test_ignore_request_pm(self): + """ A pass manager that ignores requests does not run the passes decleared in the 'requests' + field of the passes.""" + passmanager = PassManager(ignore_requires=True) + passmanager.add_passes(PassC_TP_RA_PA()) # Request: PassA / Preserves: PassA + passmanager.add_passes(PassB_TP_RA_PA()) # Request: PassA / Preserves: PassA + passmanager.add_passes(PassD_TP_NR_NP(argument1=[1, 2])) # Requires: {} / Preserves: {} + passmanager.add_passes(PassB_TP_RA_PA()) + self.assertScheduler(self.dag, passmanager, ['run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassD_TP_NR_NP', + 'argument [1, 2]', + 'run transformation pass PassB_TP_RA_PA']) + + def test_ignore_preserves_pm(self): + """ A pass manager that ignores preserves does not record the passes decleared in the + 'preserves' field of the passes as valid passes.""" + passmanager = PassManager(ignore_preserves=True) + passmanager.add_passes(PassC_TP_RA_PA()) # Request: PassA / Preserves: PassA + passmanager.add_passes(PassB_TP_RA_PA()) # Request: PassA / Preserves: PassA + passmanager.add_passes(PassD_TP_NR_NP(argument1=[1, 2])) # Requires: {} / Preserves: {} + passmanager.add_passes(PassB_TP_RA_PA()) + self.assertScheduler(self.dag, passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassD_TP_NR_NP', + 'argument [1, 2]', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA']) + + def test_pass_non_idempotence_pm(self): + """ A pass manager that considers every pass as not idempotent, allows the immediate + repetition of a pass""" + passmanager = PassManager(ignore_preserves=True) + passmanager.add_passes(PassA_TP_NR_NP()) + passmanager.add_passes(PassA_TP_NR_NP()) # Normally removed for optimization, not here. + passmanager.add_passes(PassB_TP_RA_PA()) # Normally requiered is ignored for optimization, + # not here + self.assertScheduler(self.dag, passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA']) + + def test_pass_non_idempotence_passset(self): + """ A pass set that is not idempotent. """ + passmanager = PassManager() + passmanager.add_passes([PassA_TP_NR_NP(), PassB_TP_RA_PA()], ignore_preserves=True) + self.assertScheduler(self.dag, passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA']) + + def test_analysis_pass_is_idempotent(self): + """ Analysis passes are idempotent. """ + passmanager = PassManager() + passmanager.add_passes(PassE_AP_NR_NP(argument1=1)) + passmanager.add_passes(PassE_AP_NR_NP(argument1=1)) + self.assertScheduler(self.dag, passmanager, ['run analysis pass PassE_AP_NR_NP', + 'set property as 1']) + + def test_ap_before_and_after_a_tp(self): + """ A default transformation does not preserves anything and analysis passes + need to be re-run""" + passmanager = PassManager() + passmanager.add_passes(PassE_AP_NR_NP(argument1=1)) + passmanager.add_passes(PassA_TP_NR_NP()) + passmanager.add_passes(PassE_AP_NR_NP(argument1=1)) + self.assertScheduler(self.dag, passmanager, ['run analysis pass PassE_AP_NR_NP', + 'set property as 1', + 'run transformation pass PassA_TP_NR_NP', + 'run analysis pass PassE_AP_NR_NP', + 'set property as 1']) + + def test_pass_option_precedence(self): + """ The precedence of options is, in order of priority: + - The passset option + - The Pass Manager option + - Default + """ + passmanager = PassManager(ignore_preserves=False, ignore_requires=True) + tp_pass = PassA_TP_NR_NP() + passmanager.add_passes(tp_pass, ignore_preserves=True) + the_pass_in_the_workinglist = next(iter(passmanager.working_list)) + self.assertTrue(the_pass_in_the_workinglist.options['ignore_preserves']) + self.assertTrue(the_pass_in_the_workinglist.options['ignore_requires']) + + def test_pass_no_return_a_dag(self): + """ Passes instances with same arguments (independently of the order) are the same. """ + self.passmanager.add_passes(PassJ_Bad_NoReturn()) + self.assertSchedulerRaises(self.dag, self.passmanager, + ['run transformation pass PassJ_Bad_NoReturn'], TranspilerError) + + def test_fixed_point_pass(self): + """ A pass set with a do_while parameter that checks for a fixed point. """ + self.passmanager.add_passes( + [PassK_check_fixed_point_property(), + PassA_TP_NR_NP(), + PassF_reduce_dag_property()], + do_while=lambda property_set: not property_set['fixed_point']['property']) + self.assertScheduler(self.dag, self.passmanager, + ['run analysis pass PassG_calculates_dag_property', + 'set property as 8 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 6', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 6 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 5', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 5 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 4', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 4 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 3', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 3 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 2 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 2']) + + def test_fixed_point_pass_max_iteration(self): + """ A pass set with a do_while parameter that checks that the max_iteration is raised. """ + self.passmanager.add_passes( + [PassK_check_fixed_point_property(), + PassA_TP_NR_NP(), + PassF_reduce_dag_property()], + do_while=lambda property_set: not property_set['fixed_point']['property'], + max_iteration=2) + self.assertSchedulerRaises(self.dag, self.passmanager, + ['run analysis pass PassG_calculates_dag_property', + 'set property as 8 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 6', + 'run analysis pass PassG_calculates_dag_property', + 'set property as 6 (from dag.property)', + 'run analysis pass PassK_check_fixed_point_property', + 'run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassF_reduce_dag_property', + 'dag property = 5'], TranspilerError) + + +class DoXTimesController(FlowController): + """ A control-flow plugin for running a set of passes an X amount of times.""" + + def __init__(self, passes, options, do_x_times=0, **_): # pylint: disable=super-init-not-called + self.do_x_times = do_x_times() + super().__init__(passes, options) + + def __iter__(self): + for _ in range(self.do_x_times): + for pass_ in self.passes: + yield pass_ + + +class TestControlFlowPlugin(SchedulerTestCase): + """ Testing the control flow plugin system. """ + + def setUp(self): + self.passmanager = PassManager() + self.dag = DAGCircuit.fromQuantumCircuit(QuantumCircuit(QuantumRegister(1))) + + def test_control_flow_plugin(self): + """ Adds a control flow plugin with a single parameter and runs it. """ + FlowController.add_flow_controller('do_x_times', DoXTimesController) + self.passmanager.add_passes([PassB_TP_RA_PA(), PassC_TP_RA_PA()], do_x_times=lambda x: 3) + self.assertScheduler(self.dag, self.passmanager, ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassC_TP_RA_PA']) + + def test_callable_control_flow_plugin(self): + """ Removes do_while, then adds it back. Checks max_iteration still working. """ + controllers_length = len(FlowController.registered_controllers) + FlowController.remove_flow_controller('do_while') + self.assertEqual(controllers_length - 1, len(FlowController.registered_controllers)) + FlowController.add_flow_controller('do_while', DoWhileController) + self.assertEqual(controllers_length, len(FlowController.registered_controllers)) + self.passmanager.add_passes([PassB_TP_RA_PA(), PassC_TP_RA_PA()], + do_while=lambda property_set: True, max_iteration=2) + self.assertSchedulerRaises(self.dag, self.passmanager, + ['run transformation pass PassA_TP_NR_NP', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassC_TP_RA_PA', + 'run transformation pass PassB_TP_RA_PA', + 'run transformation pass PassC_TP_RA_PA'], TranspilerError) + + def test_remove_nonexistent_plugin(self): + """ Tries to remove a plugin that does not exist. """ + self.assertRaises(KeyError, FlowController.remove_flow_controller, "foo") + + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/transpiler/test_property_set.py b/test/python/transpiler/test_property_set.py new file mode 100644 index 000000000000..fa86e7acaeb7 --- /dev/null +++ b/test/python/transpiler/test_property_set.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""Transpiler PropertySet testing""" + +import unittest +from qiskit.transpiler import PropertySet +from ..common import QiskitTestCase + + +class TestPropertySet(QiskitTestCase): + """ Tests for PropertySet methods. """ + + def setUp(self): + self.pset = PropertySet() + + def test_get_non_existent(self): + """ Getting non-existent property should return None. """ + self.assertIsNone(self.pset['does_not_exists']) + + def test_get_set_and_retrive(self): + """ Setting and retrieving.""" + self.pset['property'] = 'value' + self.assertEqual(self.pset['property'], 'value') + + +if __name__ == '__main__': + unittest.main()