Getting Started¶
In this tutorial, we will go through a simple example that illustrates the typical flow of Workbench programs. Our example program will prepare a GHZ state on three qubits, measure the qubits, and print the result.
As a reminder, a GHZ state is an equal superposition of basis states $|0...0\rangle$ and $|1...1\rangle$. For example, the three-qubit GHZ state can be written as $$\tfrac{1}{\sqrt2}(|000\rangle + |111\rangle)$$
The typical workflow of Workbench programs¶
Workbench program generally use the following workflow:
- Initialize a QPU.
- Allocate qubits.
- Apply gates to the qubits.
- Measure the qubits.
- (optional) Explore the program using debugging tools.
This tutorial will walk you through each step of this workflow.
Import the necessary classes¶
Workbench programs rely on classes defined in the psiqworkbench package, to use them, import them directly:
from psiqworkbench import QPU, Qubits
Initialize a QPU¶
A Workbench program starts by creating a QPU object - the representation of the quantum processing unit on which this program will run. 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.
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. It is common to initialize the QPU with slightly more qubits than necessary when writing small programs that are easily handled by the simulator.
In our case, we'll only need three qubits:
qpu = QPU(num_qubits=3)
Allocate the qubits¶
The next step is to allocate the qubits for the program to use. In Workbench, the simplest way to represent qubits is the Qubits class. You can allocate them by calling the constructor of this class. Notice that you have to specify not only the number of qubits to allocate but also the name of the qubit register and the QPU on which it is allocated:
reg = Qubits(3, "reg", qpu=qpu)
After this, you can treat the register reg as an array of qubits: reg[0] corresponds to the first qubit in the register, reg[1:] - all qubits in the register except the first one, and so on.
Apply the gates¶
Freshly allocated qubits start in the $|0\rangle$ state. We can prepare the GHZ state using the following sequence of steps:
- Apply the Hadamard gate to the first qubit.
- Apply a series of CNOT gates with the first qubit as the control and each of the remaining qubits as the target.
In Workbench, gates are represented as the methods of the Qubits class. You can apply a Hadamard gate to the first qubit of the register reg by calling the method had() on the value reg[0] that represents this qubit:
reg[0].had()
Calling the single-qubit gate method on a multi-qubit register will apply this gate to each qubit in the register. For example, reg.x() will apply an $X$ gate to each qubit of reg.
To apply a controlled variant of a gate to a register, you can use the method that corresponds to this gate with an additional argument that specifies the control register to use. In our case, you can apply a controlled-$X$ gate with register reg[0] (the first qubit) as the control and register reg[1:] (all qubits except the first one) as the target by calling a method reg[1:].x and passing register reg[0] as the argument cond (the argument cond means "conditioned on"):
reg[1:].x(cond=reg[0])
Print the quantum state¶
At this point, our register should be in the GHZ state, and it would be nice to verify this. We can take advantage of the fact that we're running this program on a simulator and take a look at its quantum state directly. The QPU class has a method print_state_vector that prints the current state of the QPU, in this case the state vector describing the state of the qubits:
qpu.print_state_vector()
Putting these steps together gives us the following script that you can run to see that, indeed, the qubits are in the GHZ state. (Learn more about accessing the quantum state in Workbench.)
Workbench often represents basis states as integers instead of bit strings, using little endian for conversion. In the output of
print_state_vector(), the basis states $|000\rangle$ and $|111\rangle$ are represented as $|0\rangle$ and $|7\rangle$, respectively.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu=qpu)
reg[0].had()
reg[1:].x(cond=reg[0])
_ = qpu.print_state_vector()
|reg> |0> 0.707107+0.000000j |7> 0.707107+0.000000j
Measure the qubits¶
You can measure all qubits in the Qubits object using the read method:
res = reg.read()
The return value of this method is an integer that corresponds to the bit string composed of individual measurement results. (Learn more about measurements in Workbench.)
In our case, running the following code snippet will yield $0$ or $7$ with equal probability.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu=qpu)
reg[0].had()
reg[1:].x(cond=reg[0])
res = reg.read()
print(res)
7
Draw the circuit¶
You can easily draw a circuit representation of your program using the draw() method of the QPU object. In the following code snippet, the script we wrote earlier is augmented with labels (label()) and whitespace between the gates (nop()) to improve the readability of the resulting circuit:
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu=qpu)
qpu.label("H")
qpu.nop()
reg[0].had()
qpu.nop()
qpu.label("CNOTs")
reg[1:].x(cond=reg[0])
qpu.label("Measure")
res = reg.read()
qpu.draw()
Next steps¶
Now that you've written your first Workbench program, keep reading to learn more about each of the tools you've used, as well as plenty of other tools to help you write quantum programs! Check out the documentation map to orient yourself on your learning journey.