QPU Object¶
In this tutorial, we will dive deeper into the QPU object - one of the key classes used by Workbench programs.
QPU object: your interface with the quantum processing unit¶
A QPU represents the quantum processing unit used to execute a Workbench program. It acts as the interface between your program and the quantum backend - whether a quantum simulator or a real quantum device. You need to create a QPU object before your program can do any other quantum operations, such as allocate qubits, apply gates and measurements, and so on.
At the moment, Workbench only supports running quantum simulators on a local machine (or the virtual machine if you work in PsiQDE). In the future, it will also support accessing PsiQuantum quantum hardware.
QPU object performs a lot of tasks to enable execution of Workbench programs. Here is the list of its main duties:
- Configure and run the filter pipeline - a sequence of tools (filters) that process quantum instructions such as gates and measurements. Examples of filters include various quantum simulators, as well as compilation steps that break down complex gates into simpler ones.
- Allocate and release qubits
- Draw circuit representation of the programs
- Set program execution parameters, for example, random seed for simulation
- Support program testing and debugging by enabling setting and accessing the quantum simulator state, such as the state vector, directly
- Get estimates of resources, such as number of qubits and number of magic states, necessary for running the program on a fault-tolerant quantum computer
- Process and track low-level QPU events and instructions
In this tutorial, we'll go over the most commonly used basic APIs of the QPU object: initializing it and using it to draw circuits. Later tutorials will dive deeper into the rest of its functions: configuring the filter pipeline, debugging quantum programs, and getting resource estimates.
Initializing a QPU¶
By default, Workbench uses a full-state simulator that you can create by calling the constructor QPU() with one argument - the number of qubits you're planning to use in the program.
This includes both the qubits the program allocates explicitly and any qubits allocated temporarily by library calls, gate decomposition, and so on.
For example, the following code snippet will initialize a full-state simulator with three qubits:
qpu = QPU(num_qubits=3)
The number of qubits used for QPU initialization doesn't have to be exact, but it has to be an upper bound of the number of qubits needed to run the program. If you try to allocate more qubits than you specified when initializing the QPU, Workbench will throw an error. It is common to initialize the QPU with slightly more qubits than necessary when writing small programs that are easily handled by the simulator.
Alternatively, you can initialize a QPU object with no arguments at all. In this case, you'll need to call the reset method on it to specify the number of qubits for initialization before you can allocate any qubits or apply any gates:
qpu = QPU()
qpu.reset(3)
The reset method resets the QPU object without recreating it from scratch. It can be useful, for example, when you want to run a series of experiments on the same QPU instance and want to start each experiment from a clean slate. In this case, you create the QPU object once at the beginning of your program and just call reset() at the start of each experiment.
Drawing circuits¶
You can draw a circuit representation of your program using the draw() method of the QPU object.
Additionally, there are several methods that allow you to tweak the appearance of the circuit without affecting the behavior of the rest of the program:
nop()inserts extra whitespace in the circuit.label()highlights a part of the circuit and adds a label to it. Alabel()call with a non-empty string name begins the labeled part of the circuit, and the next call tolabel()ends it (and possibly begins the next labeled part if a non-empty string name is provided).box()draws a labeled box. Depending on the arguments, it can be a simple box acting on consecutive qubits ("Box 1" in the example), a box acting on disjoint qubits ("Box 2"), or a controlled box ("Box 3"), but it cannot have any other gates in it.box_open()andbox_close()draw a labeled box, possibly with other gates in it. Same as for thebox()method, it can be a box acting on consecutive qubits ("Box 4"), a box acting on disjoint qubits ("Box 5"), or a controlled box ("Box 6").
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu=qpu)
qpu.label("Label")
qpu.nop()
reg[0].had()
qpu.nop()
qpu.label()
qpu.box("Box 1", target_mask=reg)
qpu.box("Box 2", target_mask=reg[0] | reg[2])
qpu.box("Box 3", condition_mask=reg[0], target_mask=reg[1:])
qpu.box_open("Box 4", target_mask=reg)
reg[1:].x(cond=reg[0])
qpu.box_close("Box 4", target_mask=reg)
qpu.box_open("Box 5", target_mask=reg[0] | reg[2])
reg[2].x(cond=reg[0])
qpu.box_close("Box 5", target_mask=reg[0] | reg[2])
qpu.box_open("Box 6", condition_mask=reg[0], target_mask=reg[1:])
reg[1:].x(cond=reg[0])
qpu.box_close("Box 6", condition_mask=reg[0], target_mask=reg[1:])
qpu.draw()