Configuring Program Execution¶
In this tutorial, we will dive deeper into configuring execution of Workbench programs, going over the ways to choose the compilation and simulation tools used to run the program.
Filter pipeline¶
As mentioned in the earlier tutorial, Workbench programs are executed using the filter pipeline - a sequence of tools ("filters") that each process the quantum instructions in a certain way specific to that filter. You can run the same Workbench program use different filter pipelines to get different results. Importantly, you don't need to modify the quantum logic of your code to do that, only the way you initialize the QPU object (and possibly the way you handle the results.)
Workbench offers a variety of filters in several categories:
- Simulators are classical programs that simulate high-level behavior of quantum systems, allowing you to run your quantum programs and get their results as if they were executed on a quantum computer.
- Compilation filters modify the internal representation of the program, replacing certain instructions with others, allocating additional qubits, and so on. They allow you to transform the high-level Workbench code into lower-level instructions that will ultimately be passed to the Instruction Management System (IMS) of the quantum device.
- Utility filters keep track of the instructions they receive and pass them on without transforming them. The filters of this group enable drawing circuits, performing resource estimation, and so on.
You can also create custom filters for your own needs and add them to the filter pipeline easily. We will cover creation of custom filters in a separate tutorial. In this tutorial, we'll go over configuring the program execution by customizing the filter pipeline and take a look at the most used Workbench filters.
Customizing filter pipeline¶
You can specify the filters you want to use in the pipeline by providing the filters argument to the QPU object constructor. This argument is a list of filter names in the order in which the QPU instructions will pass through them. For example, using the default value filters=['>>witness>>', '>>buffer>>', '>>state-vector-sim>>'] means that
- The instructions are first passed to the
>>witness>>filter which keeps track of the resources required to run the program. - Then, the instructions are passed to the
>>buffer>>filter which just stores them for circuit drawing. - And finally, the instructions end up in the
>>state-vector-sim>>filter which runs state vector simulation of the program.
QPU class constructor has three arguments which allow you to provide filters as lists of filter names:
pre_filters,filters, andpost_filters. When you create a QPU object, these arguments are concatenated to form a single pipeline, so the only thing that matters is the overall order of filters across the arguments, not which argument the filter was included in.filtersis the only argument with a non-empty default value; if not provided explicitly, it is set to['>>witness>>', '>>buffer>>', '>>state-vector-sim>>']. For simplicity, you can stick to using just thefiltersargument in your code.
The following example shows how to print the list of filters from a QPU object in the order in which they are applied.
from psiqworkbench import QPU
qpu = QPU()
qpu.print_stream_setup()
>>witness>>: <class 'psiqworkbench.resource_estimation.witness_counter.witness_counter.WitnessFilter'> ..>>state-vector-sim>>: <class 'psiqworkbench.opfilter.stream_qpu.StreamQPU'> **NATIVE** ....>>buffer>>: <class 'psiqworkbench.opfilter.utility_streams.StreamBuffer'>
QPU filter presets¶
Workbench offers filter presets for accessing several standard filter pipeline setups that are used for most jobs.
You can use these constants, defined in psiqworkbench.filter_presets, instead of listing the filters explicitly.
| Preset | Equivalent filter list | Use |
|---|---|---|
SV_DEFAULT |
['>>witness>>', '>>buffer>>', '>>state-vector-sim>>'] |
The default setup supporting state vector simulation, resource estimation, and circuit drawing. Equivalent to initializing QPU without filters arguments. |
BIT_DEFAULT |
['>>witness>>', '>>buffer>>', '>>bit-sim>>'] |
Simulation and resource estimation of large-scale reversible circuits, such as quantum arithmetic. |
NO_SIM_DEFAULT |
['>>witness>>', '>>buffer>>'] |
Resource estimation of arbitrary programs without simulation (but with circuit drawing). |
NO_SIM_LONG |
['>>witness>>'] |
Resource estimation of arbitrary programs without circuit drawing. |
CUDAQ_STATE_VEC |
['>>witness>>', '>>buffer>>', '>>jump-control>>', '>>batch>>', '>>cudaq-sim>>'] |
Simulate on GPU via NVIDIA CUDA-Q state vector simulator. |
CUDAQ_STATE_VEC_MGPU |
['>>witness>>', '>>buffer>>', '>>jump-control>>', '>>batch>>', '>>cudaq-sim-mgpu>>'] |
Simulate on multiple GPUs via NVIDIA CUDA-Q state vector simulator. See Activating Multiple GPUs. |
CUDAQ_TENSORNET |
['>>witness>>', '>>buffer>>', '>>jump-control>>', '>>batch>>', '>>cudaq-tensornet>>'] |
Simulate on GPU via NVIDIA CUDA-Q tensor network simulator. |
SV_IMS |
['>>clean-ladder-filter>>', '>>single-control-filter>>', '>>pauli-product-filter>>', '>>witness>>', '>>buffer>>', '>>state-vector-sim>>'] |
Compilation into IMS-compatible instructions. Currently runs state vector simulation; in the future, will target the quantum computer. |
from psiqworkbench import QPU
from psiqworkbench.filter_presets import BIT_DEFAULT
qpu = QPU(filters=BIT_DEFAULT)
qpu.print_stream_setup()
>>witness>>: <class 'psiqworkbench.resource_estimation.witness_counter.witness_counter.WitnessFilter'> ..>>buffer>>: <class 'psiqworkbench.opfilter.utility_streams.StreamBuffer'> ....>>bit-sim>>: <class 'psiqworkbench.opfilter.stream_qpu.StreamBitQPU'> **NATIVE**
If you find yourself using a different sequence of filters on a regular basis, you can define your own preset to use.
Notice that if the list of filters doesn't include the
>>witness>>explicitly, it will be added as the last filter before the simulation filter (or, if no simulation filters are included, the last filter).
from psiqworkbench import QPU
custom_preset = ['>>clean-ladder-filter>>', '>>buffer>>', '>>state-vector-sim>>']
qpu = QPU(filters=custom_preset)
qpu.print_stream_setup()
>>clean-ladder-filter>>: <class 'psiqworkbench.compilation.filters.elementary_filters.clean_ladders.CleanLadderFilter'> ..>>buffer>>: <class 'psiqworkbench.opfilter.utility_streams.StreamBuffer'> ....>>witness>>: <class 'psiqworkbench.resource_estimation.witness_counter.witness_counter.WitnessFilter'> ......>>state-vector-sim>>: <class 'psiqworkbench.opfilter.stream_qpu.StreamQPU'> **NATIVE**
Simulation filters¶
Simulators are classical programs that simulate high-level behavior of quantum systems. Using simulators allows you to run your quantum programs and get their results as if they were executed on a quantum computer. Simulators also help you test and debug your programs, providing you insight into code behavior that would be impossible on a real quantum device.
| Filter | Use |
|---|---|
>>state-vector-sim>> |
State vector simulation for small (~30 qubits) programs. |
>>bit-sim>> |
Basis state simulation for large reversible computations (for example, arithmetic). |
>>cudaq-sim>> |
State vector simulation using a CUDA-Q backend running on a GPU. |
>>cudaq-sim-mgpu>> |
State vector simulation using a CUDA-Q backend running on multiple GPUs (Use "mpiexec -np m python file.py" for m GPUs) |
>>cudaq-tensornet>> |
Tensor network simulation using a CUDA-Q backend running on a GPU. |
The use of simulators in Workbench is covered in more detail in the next tutorial.
Compilation filters¶
Compilation filters modify the internal representation of the program, replacing certain instructions with others. They allow you to transform the high-level Workbench code into lower-level instructions that will ultimately be passed to the Instruction Management System (IMS) of the quantum device.
| Filter | Use |
|---|---|
>>clean-ladder-filter>> |
Decompose multi-controlled gates into Toffoli gates and rotations using clean auxiliary qubits. |
>>cond-clean-dirty-ladder-filter>> |
Decompose multi-controlled gates into Toffoli gates and rotations using conditionally clean ladder compilation with dirty auxiliary qubits. |
>>cond-clean-ladder-filter>> |
Decompose multi-controlled gates into Toffoli gates and rotations using conditionally clean ladder compilation with clean auxiliary qubits. |
>>dirty-ladder-filter>> |
Decompose multi-controlled gates into Toffoli gates and rotations using dirty auxiliary qubits. |
>>elbow-reduction-filter>> |
Simplify pairs of elbows conditioned on the same qubits. |
>>jump-control>> |
Perform the actions specified by jump instructions. |
>>no-control-filter>> |
Decompose uncontrolled SWAP and Rx/Ry gates. |
>>pauli-product-filter>> |
Decompose instructions into PPRs and PPMs. |
>>pga-synth-filter>> |
Rotation synthesis via addition into a phase gradient state. |
>>rs-synth-filter>> |
Ross-Selinger approximate rotation synthesis. |
>>single-control-filter>> |
Decompose single-control operations into CNOTs and single-qubit gates. |
>>skip-zeros>> |
Skip instructions conditioned on freshly allocated qubits known to be in the zero state. |
>>toffoli-filter>> |
Decompose Toffoli gates into Clifford + T gates. |
>>tunable-cond-clean-ladder-filter>> |
Decompose multi-controlled gates into Toffoli gates using a mix of clean and conditionally clean auxiliary qubits. |
>>hermitian-window-filter>> |
Remove any hermitian gates that cancel or replace with a cancellation rule if controlled within a set window |
Since these filters act by transforming the program, they typically don't require any special handling beyond adding them to the filter pipeline.
Example: exploring compilation of a Workbench program¶
The following example takes a Workbench program that prepares a W state on four qubits using a sequence of controlled rotation gates. As a reminder,
$$|W_4\rangle = \tfrac12(|1000\rangle + |0100\rangle + |0010\rangle + |0001\rangle)$$
Then, it demonstrates how adding extra compilation filters transforms the circuit by decomposing different gates into sequences of other, simpler gates:
>>clean-ladder-filter>>decomposes each controlled $Ry$ gate with two or more controls into sequences of Toffoli gates (left and right Gidney elbows) with a single-controlled $Ry$ gate between them. This filter allocates additional qubits to be used in the decomposition.>>elbow-reduction-filter>>cancels out the right elbow followed by the left elbow with the same control pattern between the two decomposed $Ry$ gates. This is an optimization filter: it doesn't change the types of gates used in the circuit, but it reduces the resources it requires by reducing the numbers of gates.>>single-control-filter>>decomposes the single-controlled $Ry$ gates into sequences of CNOT gates and single-qubit gates.>>toffoli-filter>>decomposes Toffoli gates: left elbows are replaced with sequences of Clifford and T gates, and right elbows - with Clifford gates and measurements, which is more efficient in terms of T gate count.
Notice that this example doesn't re-create a QPU with a different filter pipeline to get every plot. Instead, it uses a convenient feature of the
drawmethod of the QPU object: you can pass thefiltersargument to this method, and these compilation filters will be applied to the instructions before drawing.
from math import asin, sqrt
from psiqworkbench import QPU, Qubits, Units
n = 4
qpu = QPU(num_qubits=6)
reg = Qubits(n, "reg", qpu)
reg[0].ry(2 * asin(1 / sqrt(n)) * Units.rad)
for j in range(1, n):
reg[j].ry(2 * asin(1 / sqrt(n - j)) * Units.rad, cond=~reg[:j])
qpu.print_state_vector()
print("Initial circuit")
qpu.draw()
print("Step 1: Decompose multi-controlled Ry gates into Toffoli gates and single-controlled Ry gates")
compilation_filters = [">>clean-ladder-filter>>"]
qpu.draw(filters=compilation_filters)
print("Step 2: Cancel pairs of matching left and right elbows")
compilation_filters += [">>elbow-reduction-filter>>"]
qpu.draw(filters=compilation_filters)
print("Step 3: Decompose single-controlled Ry gates into CNOTs and single-qubit gates")
compilation_filters += [">>single-control-filter>>"]
qpu.draw(filters=compilation_filters)
print("Step 4: Decompose Toffoli gates into Clifford and T gates and measurements")
compilation_filters += [">>toffoli-filter>>"]
qpu.draw(filters=compilation_filters)
|reg|?> |1|.> 0.500000+0.000000j |2|.> 0.500000+0.000000j |4|.> 0.500000+0.000000j |8|.> 0.500000+0.000000j Initial circuit
Step 1: Decompose multi-controlled Ry gates into Toffoli gates and single-controlled Ry gates
Step 2: Cancel pairs of matching left and right elbows
Step 3: Decompose single-controlled Ry gates into CNOTs and single-qubit gates
Step 4: Decompose Toffoli gates into Clifford and T gates and measurements
You can modify this code further, for example, swap out the >>clean-ladder-filter>> for a different filter that decomposes multi-controlled gates, add a filter to decompose the uncontrolled $Ry$ gate, or use one of the approximate rotation synthesis filters to approximate the remaining rotations.
Utility filters¶
Utility filters are filters that keep track of the instructions they receive and pass them on without transforming them. The filters of this group enable accessing the list of program instructions, drawing circuits, performing resource estimation, and so on.
| Filter | Use |
|---|---|
>>avg-qubit-estimator>> |
Estimate the upper bound on the average number of qubits used by a circuit. |
>>batch>> |
Batch instructions by collecting them until flush() is called and sending them downstream in larger batches. |
>>buffer>> |
Store list of program instructions. By default, limited to storing 100,000 instructions; use >>capture>> for larger programs. You need either this filter or >>capture>> to be able to draw circuits. |
>>capture>> |
Store list of program instructions. You need either this filter or >>buffer>> to be able to draw circuits. |
>>connect>> |
Connect to a remote target over the network and send instructions to it. |
>>print>> |
Print program instructions as low-level Workbench code. |
>>qasm-export>> |
Export the program as QASM code. |
>>unitary>> |
Construct a unitary matrix of the gates applied. |
>>witness>> |
Collect resource counts for resource estimation. |
We will see most of these filters in action in later tutorials, so we won't go through all of them in detail here. The following example shows how to run the W state preparation code from the earlier example with three of the filters. These filters are a nice illustration of how different filters can have different APIs even if they perform a similar function:
- The
>>buffer>>filter accumulates the program instructions that you can later fetch for drawing, debugging, or just printing. Using it (or the>>capture>>filter) enables QPU methods such asdrawandget_instructions. - The
>>print>>filter prints the program instructions as low-level Workbench code. Using it in the filter pipeline prints the instructions right away, without any additional API calls required. - The
>>qasm-export>>filter converts Workbench code into QASM code. To fetch the generated code, you need to get the instance of this filter using theget_filter_by_namemethod of the QPU and then call theget_qasm_stringmethod of the filter object.
from math import asin, sqrt
from psiqworkbench import QPU, Qubits, Units
n = 4
qpu = QPU(num_qubits=n, filters=['>>buffer>>', '>>qasm-export>>', '>>print>>'])
reg = Qubits(n, "reg", qpu)
reg[0].ry(2 * asin(1 / sqrt(n)) * Units.rad)
for j in range(1, n):
reg[j].ry(2 * asin(1 / sqrt(n - j)) * Units.rad, cond=~reg[:j])
qpu.draw()
print("----- Instructions accumulated by >>buffer>>")
print(qpu.get_instructions(format="asm"))
qasm_filter = qpu.get_filter_by_name('>>qasm-export>>')
qasm_code = qasm_filter.get_qasm_string()
print("----- QASM code generated by >>qasm-export>>")
print(qasm_code)
qc.reset(num_qubits=4) qc.qubits_alloc(target_mask=0xf, label="reg") qc.ry(target_mask=0x1, theta=60.00000000000001) qc.ry(target_mask=0x2, condition_mask=0x1, theta=70.52877936550932, cond_xor_mask=0x1) qc.ry(target_mask=0x4, condition_mask=0x3, theta=89.99999999999999, cond_xor_mask=0x3) qc.ry(target_mask=0x8, condition_mask=0x7, theta=180.0, cond_xor_mask=0x7)
----- Instructions accumulated by >>buffer>> qc.reset(num_qubits=4) qc.qubits_alloc(target_mask=0xf, label="reg") qc.ry(target_mask=0x1, theta=60.00000000000001) qc.ry(target_mask=0x2, condition_mask=0x1, theta=70.52877936550932, cond_xor_mask=0x1) qc.ry(target_mask=0x4, condition_mask=0x3, theta=89.99999999999999, cond_xor_mask=0x3) qc.ry(target_mask=0x8, condition_mask=0x7, theta=180.0, cond_xor_mask=0x7) ----- QASM code generated by >>qasm-export>> OPENQASM 3; include "stdgates.inc"; qubit[4] q; let reg_1 = q[0:4]; ry(1.0471975511965979) q[0]; negctrl @ ry(1.230959417340775) q[0], q[1]; negctrl @ negctrl @ ry(1.5707963267948963) q[0], q[1], q[2]; negctrl @ negctrl @ negctrl @ ry(3.141592653589793) q[0], q[1], q[2], q[3];
Next steps¶
In this tutorial, you've learned about using the filter pipeline to configure compilation and execution of Workbench programs. The next tutorial will focus on the most commonly used type of filters - simulators.