Measurements¶
In this tutorial, we will discuss performing measurements in Workbench and interpreting their results.
Measuring qubits¶
You can measure all qubits in the Qubits object reg using the read() method:
res = reg.read()
This method measures each qubit in the register separately and returns an integer that corresponds to the bit string composed of individual measurement results in little endian notation (the least significant bit stored first). The measurements are done in the computational basis. After the measurement, the qubits end up in the basis state that corresponds to the measurement outcome.
For example, the following code snippet prepares two qubits in the state $\tfrac1{\sqrt2}(|0\rangle + |1\rangle) \otimes |1\rangle$ and measures them. The measurement outcomes will form a two-bit string, $01$ or $11$, represented as a number $2$ or $3$, respectively (in little endian encoding). You can see that the state vector after the measurement is always a basis state that corresponds to the return value of the read() method.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=2)
reg = Qubits(2, "reg", qpu=qpu)
reg[0].had()
reg[1].x()
res = reg.read()
print(f"{res} = {bin(res)}")
qpu.print_state_vector()
qpu.nop(repeat=10)
qpu.draw()
2 = 0b10 |reg> |2> 1.000000+0.000000j
Measuring a subset of qubits¶
To measure a subset of qubits in a register, you can combine the read() method with slicing. For example, you can measure just the first qubit of the register by using reg[0].read(). If your register encodes an unsigned integer in a little-endian notation, and you want to interpret it as one in a big-endian notation, you can measure the reversed register reg[::-1].read().
The following code snippet prepares two qubits in the state $\tfrac1{\sqrt2} |0\rangle \otimes |0\rangle + \tfrac12 |1\rangle \otimes (|0\rangle + |1\rangle)$ and measures the first qubit. You can see that the state of the system post-measurement changes depending on the measurement outcome.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=2)
reg = Qubits(2, "reg", qpu=qpu)
reg[0].had()
reg[1].had(cond=reg[0])
qpu.print_state_vector()
res = reg[0].read()
print(f"{res} = {bin(res)}")
qpu.print_state_vector()
qpu.nop(repeat=9)
qpu.draw()
|reg> |0> 0.707107+0.000000j |1> 0.500000+0.000000j |3> 0.500000+0.000000j 1 = 0b1 |reg> |1> 0.707107+0.000000j |3> 0.707107+0.000000j
Effects of the Qubits bit mask on measurement outcomes¶
When we introduced the Qubits data type, we mentioned that one of its data fields is the bit mask that specifies the behavior of this object in certain scenarios. The most prominent use case for this bit mask is specifying control patterns when using the Qubits object as a control register (see the Controlled Gates tutorial).
However, this bit mask also affects the measurement outcomes for the Qubits object.
If the bit mask indicates that some qubits in the register are inverted, the measurement results for those qubits will be flipped: measurement result $0$ will be shown as $1$ and vice versa.
The following example illustrates this behavior. As a reminder, ~reg is the negation of register reg that consists of the same qubits as reg and has its bit mask inverted, which means that all qubits in the register are inverted. You can see that the measurement results of ~reg are the opposite of those of reg.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=2)
reg = Qubits(2, "reg", qpu=qpu)
reg[1].x()
qpu.print_state_vector()
print(f"{reg.read()=} = {bin(reg.read())}")
print(f"{(~reg).read()=} = {bin((~reg).read())}")
|reg> |2> 1.000000+0.000000j reg.read()=2 = 0b10 (~reg).read()=1 = 0b1
Mid-circuit measurements¶
There are two main scenarios in which measurements occur in quantum programs:
- Getting the final answer produced by the algorithm at the end of the program execution. In this case, you won't need anything more sophisticated than the
read()method. - Measuring one or several qubits of a larger state mid-circuit to decide which gates, if any, to apply to the remaining qubits. The simplest example of this scenario is the teleportation protocol, in which the results of measuring the sender's two qubits tells the receiver the fixup gates they need to apply to their one qubit.
Workbench has two different ways to handle measurements and make subsequent decisions based on the measurement outcomes.
First, you can use the read() method and analyze its return value using regular Python tools, such as if or match statements.
The following example implements the teleportation protocol using this approach.
from math import atan2
from psiqworkbench import QPU, Qubits, Units
qpu = QPU(num_qubits=3)
message = Qubits(1, "m", qpu=qpu)
b0 = Qubits(1, "b0", qpu=qpu)
b1 = Qubits(1, "b1", qpu=qpu)
# Prepare the message qubit in a superposition state
qpu.label("Message")
message.ry(2 * atan2(0.8, 0.6) * Units.rad)
# Prepare a Bell pair
qpu.label("Bell pair")
b0.had()
b1.x(cond=b0)
# Do a Bell basis measurement on qubits message and b0
qpu.label("Bell meas")
b0.x(cond=message)
message.had()
res0, res1 = b0.read(), message.read()
# Apply fixup gates conditionally
qpu.label("Fix")
if res0:
b1.x()
if res1:
b1.z()
# Print the resulting state and the circuit
print(b1.pull_state())
qpu.draw()
[0.6+0.j 0.8-0.j]
If you run this code several times, you'll notice that the last section of the circuit changes from run to run: it can have one gate, $X$ or $Z$, both gates, or it can be absent from the circuit at all. The circuit also doesn't show the connection between the measurement outcomes and the gates applied. This happens because the logic of applying these gates conditionally is not encoded in Workbench instructions, but is handled by Python flow control logic instead.
Alternatively, you can use the read_async() method and use its return value as an argument for the gates that need to be applied conditionally. The following example rewrites the teleportation protocol using this approach.
from math import atan2
from psiqworkbench import QPU, Qubits, Units
qpu = QPU(num_qubits=3)
message = Qubits(1, "m", qpu=qpu)
b0 = Qubits(1, "b0", qpu=qpu)
b1 = Qubits(1, "b1", qpu=qpu)
# Prepare the message qubit in a superposition state
qpu.label("Message")
message.ry(2 * atan2(0.8, 0.6) * Units.rad)
# Prepare a Bell pair
qpu.label("Bell pair")
b0.had()
b1.x(cond=b0)
# Do a Bell basis measurement on qubits message and b0
qpu.label("Bell meas")
b0.x(cond=message)
message.had()
res0, res1 = b0.read_async(), message.read_async()
# Apply fixup gates conditionally
qpu.label("Fix")
with res0:
b1.x()
with res1:
b1.z()
# Print the resulting state and the circuit
print(b1.pull_state())
qpu.draw()
[0.6+0.j 0.8+0.j]
You can see that now the circuit diagram looks the same every time you run this code, regardless of the measurement outcomes. It also shows that the last two gates are applied conditionally, depending on the measurement outcomes. This approach is more efficient compared to using read() and leads to more expressive circuit diagrams, but it is less flexible:
- The return values of
read_async()can be analyzed using only basic comparisons (< > <= >= == !=) and not arbitrary Python calculations. - The quantum gates within the
withblock have to be conditioned on the result of these comparisons directly, so using complicated computation logic there might not be supported.
The typical scenario in which read_async() is very useful is skipping some gates for certain measurement outcomes, like the teleportation protocol does.
Next steps¶
Now that you've learned how to read quantum information, you're familiar with the basics of qubit manipulation in Workbench! The next tutorial will dive deeper into non-standard quantum gates and measurements. Alternatively, read on to learn how to configure Workbench program execution.