Design a Custom Pass

Designing a custom pass in BQSKit is easy and allows you to completely tailor a compilation workflow to your specific needs. As a reminder, we happily accept contributions; if you feel that others could benefit from a pass you have designed, please make a Pull Request on our Github page. We are happy to help get your code out to other users if needed.

Basic Idea

Every pass in BQSKit inherits from BasePass and implements the async run() method. The run method will take as input a circuit (and the PassData, but we will get to that later) and either analyze it, modify it, or both. This method should be entirely self-contained without side-effects to the class, meaning the Pass object (self) should only be read from and not written to. Any configuration for your Pass should be done in an __init__ method, and the Pass should be immutable afterward.

Note about Importability

Since the BQSKit Runtime is built on top of Python’s multiprocessing library, which uses pickle, every object sent through the runtime must be pickle-able. All code that is sent must be accessible and importable from all workers in the Runtime. Therefore, you cannot define a pass in the same Python script (__main__) executed. A simple workaround is to define your pass in another module or file next to your script and import the pass.

PassData

The PassData is an object wrapping a dictionary from strings to anything. During the execution of a compilation workflow, one is created at the very beginning with some default values, and this object is shared between all the passes in the workflow. This allows the passes to communicate with one another by reading from and writing to the PassData object. Some passes will have features that can be accessed by leaving specific keys in the dictionary.

Some important reserved keys exist in the PassData, for example, the PassData.model, which holds the current workflow’s target MachineModel.

Parallelization

Some passes may be very compute intensive and can benefit from parallelizing or distributing their workload across the active Runtime. This can be done by requesting a handle on the runtime and mapping work over it. For example, the following code that performs some work under a loop:

...
class MyPass(BasePass):
    async def run(circuit: Circuit, data: PassData) -> None:
        ...
        results = []
        for i in long_list:
            results.append(do_work(i))
        ...

can be parallelized to:

...
from bqskit.runtime import get_runtime
...
class MyPass(BasePass):
    ...
    async def run(circuit: Circuit, data: PassData) -> None:
        ...
        results = await get_runtime().map(do_work, long_list)
        ...

There are other methods available, such as cancel, next, and submit. See get_runtime for more info.