Auxiliary Qubit Management in Qubricks¶
In this tutorial, we will discuss one of the core features of Qubricks: automatic management of quantum memory.
A lot of quantum computing routines rely on the use of auxiliary qubits - additional qubits that are used only within that routine. These qubits are allocated at the beginning of the routine, used in the computation it performs (and potentially in the uncomputation afterward), and released at the end. Workbench allows you to allocate qubits as you need them and manages their release automatically. This means that the developer of a program that uses routines doesn't need to be aware of the details of how these routines use auxiliary qubits, which makes writing high-level programs easier and less error-prone.
Allocating auxiliary qubits within Qubricks¶
You can allocate a qubit register for temporary use within a Qubrick's _compute method by calling the alloc_temp_qreg method of this Qubrick. This method takes two main arguments: the number of qubits in the register and the register name. The QPU on which the qubits will be allocated is deduced automatically, either from the other qubit registers passed to this Qubrick's methods or from the QPU passed to the Qubrick explicitly during its initialization.
The alloc_temp_qreg method returns a Qubits object that you can then use just like the Qubits objects created via their constructors.
The following code snippet shows an example of defining and using a Qubrick which prepares an equal superposition of all basis states with parity $1$. It does that in several steps:
- Reset the given Qubits register to the $|0...0\rangle$ state and then use Hadamard gates to prepare an equal superposition of all basis states.
- Allocate an auxiliary qubit and use it to compute the parity of each of the basis states. If you print the state vector of the system at this point, you can see that four basis states have the parity qubit in state $|0\rangle$ and four - in state $|1\rangle$.
- Measure the parity qubit and, if the result is $0$, adjust the parity of the resulting state by applying an $X$ gate to one of the qubits.
Notice that the part of the code which uses this Qubrick doesn't have to be aware of any qubits temporarily allocated within it. You can swap this Qubrick for another one which does the same operation without using auxiliary qubits and the code will still work.
from psiqworkbench import QPU, Qubits, Qubrick
class PrepareParityOneSuperposition(Qubrick):
"""Qubrick to prepare an equal superposition of all basis states with parity 1."""
def _compute(self, reg: Qubits) -> None:
reg.write(0)
reg.had()
parity = self.alloc_temp_qreg(1, "parity")
for q in reg:
parity.x(cond=q)
self.get_qc().print_state_vector()
with parity.read_async() == 0:
reg[0].x()
qpu = QPU(num_qubits=4)
reg = Qubits(3, "reg", qpu)
pp1 = PrepareParityOneSuperposition()
pp1.compute(reg)
qpu.print_state_vector()
qpu.nop(repeat=10)
qpu.draw(show_qubricks=True)
|reg|parity> |0|0> 0.353553+0.000000j |1|1> 0.353553+0.000000j |2|1> 0.353553+0.000000j |3|0> 0.353553+0.000000j |4|1> 0.353553+0.000000j |5|0> 0.353553+0.000000j |6|0> 0.353553+0.000000j |7|1> 0.353553+0.000000j |reg|parity> |1|1> 0.500000+0.000000j |2|1> 0.500000+0.000000j |4|1> 0.500000+0.000000j |7|1> 0.500000+0.000000j
From the output of the qpu.print_state_vector() you can see that the parity qubit, allocated within the compute method, is still allocated after the compute call, and is either in $|0\rangle$ or in $|1\rangle$ state depending on the measurement result. The next compute call on this Qubrick will try to allocate a fresh auxiliary qubit. Why does this happen, and how to change this behavior?
By default, qubits allocated in the compute method using alloc_temp_qreg are kept around until the corresponding uncompute method finishes execution, and are released after that. Since we're not calling uncompute of this Qubrick, the parity qubit is never released.
We can change this behavior by passing an additional argument to the alloc_temp_qreg method: release_after_compute=True indicates that the qubits allocated with this call should be released immediately after compute is finished. (The default value of this argument is False.)
The following code snippet shows the same Qubrick, rewritten to use release_after_compute=True when allocating the parity qubit. You can see that the qubit is released after the first compute call. It is then allocated afresh for the next compute call.
from psiqworkbench import QPU, Qubits, Qubrick
class PrepareParityOneSuperposition(Qubrick):
"""Qubrick to prepare an equal superposition of all basis states with parity 1."""
def _compute(self, reg: Qubits) -> None:
reg.write(0)
reg.had()
parity = self.alloc_temp_qreg(1, "parity", release_after_compute=True)
for q in reg:
parity.x(cond=q)
with parity.read_async() == 0:
reg[0].x()
qpu = QPU(num_qubits=4)
reg = Qubits(3, "reg", qpu)
pp1 = PrepareParityOneSuperposition()
pp1.compute(reg)
qpu.print_state_vector()
pp1.compute(reg)
qpu.draw(show_qubricks=True)
|reg|?> |1|.> 0.500000+0.000000j |2|.> 0.500000+0.000000j |4|.> 0.500000+0.000000j |7|.> 0.500000+0.000000j
Marking qubit register as computation result¶
A typical scenario in which it is useful to hold on to the temporarily allocated qubits until uncompute finishes is when these qubits are used to store the result of the computation. For example, a Qubrick that computes the parity of the input register needs a fresh qubit to store the result. Depending on the Qubrick implementation, it can either accept this qubit as an additional argument to the compute method (and require that this qubit is allocated by the user code) or it can take care of allocating this qubit itself.
To mark a Qubits register as a result of the compute method, you can use the set_result_qreg method of the Qubrick with a single argument - the Qubits object that will act as a result. To access this Qubits object outside the Qubrick, you have to get it using the get_result_qreg method of that Qubrick.
You can pass a second argument, the name to associate with a Qubits register, to
set_result_qreg. This way, you can mark multiple Qubits objects as results ofcomputeby registering them with different names, and then using these names to fetch the objects withget_result_qreg. The default name is'result'.
The following example shows an implementation of a Qubrick that computes the parity of the input register using a qubit it allocates and returns this qubit as the result. The second part of the example uses that Qubrick to flip the phases of only the basis states with parity $1$ by getting the parity qubit and applying a $Z$ gate to it. You can see that the parity qubit is released only after the uncompute method finishes.
from psiqworkbench import QPU, Qubits, Qubrick
class Parity(Qubrick):
"""Qubrick to compute parity of the state."""
def _compute(self, reg: Qubits) -> None:
parity = self.alloc_temp_qreg(1, "parity")
self.set_result_qreg(parity)
for q in reg:
parity.x(cond=q)
qpu = QPU(num_qubits=4)
reg = Qubits(3, "reg", qpu)
reg.had()
qpu.print_state_vector()
p = Parity()
p.compute(reg)
qpu.print_state_vector()
parity = p.get_result_qreg()
parity.z()
p.uncompute()
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg|?> |0|.> 0.353553+0.000000j |1|.> 0.353553+0.000000j |2|.> 0.353553+0.000000j |3|.> 0.353553+0.000000j |4|.> 0.353553+0.000000j |5|.> 0.353553+0.000000j |6|.> 0.353553+0.000000j |7|.> 0.353553+0.000000j |reg|parity> |0|0> 0.353553+0.000000j |1|1> 0.353553+0.000000j |2|1> 0.353553+0.000000j |3|0> 0.353553+0.000000j |4|1> 0.353553+0.000000j |5|0> 0.353553+0.000000j |6|0> 0.353553+0.000000j |7|1> 0.353553+0.000000j |reg|?> |0|.> 0.353553+0.000000j |1|.> -0.353553-0.000000j |2|.> -0.353553-0.000000j |3|.> 0.353553+0.000000j |4|.> -0.353553-0.000000j |5|.> 0.353553+0.000000j |6|.> 0.353553+0.000000j |7|.> -0.353553-0.000000j
A Qubrick can mark as a result any kind of qubits: the qubits that were passed as part of the input, the qubits allocated by the Qubrick itself, or a combination of these two types. The Qubrick can even switch between these scenarios based on its internal logic.
For example, consider the USP Qubrick that prepares a uniform superposition of $d$ basis states.
- If its
computemethod is called with two arguments, the integer $d$ and a Qubits register, it prepares the required state on this register. - However, if its
computemethod is called with one argument, just the integer $d$, it allocates the qubits internally and prepares the state on them. - In both scenarios, the result of the
computemethod is the register with the prepared state.
The following code snippet illustrates this behavior by creating a USP Qubrick and calling its compute method twice: first with a pre-allocated register reg1 and then without a register. In the second call, the Qubrick allocates a new register qbk_usp and returns it as a result. The user code then gets this register and creates a reference to it called reg2. Now, both reg1 and reg2 are in the same state and can be used in the same way.
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import USP
qpu = QPU(num_qubits=5)
reg1 = Qubits(2, "reg1", qpu)
usp = USP()
# Prepare state on a register that is already allocated
usp.compute(3, reg1)
qpu.print_state_vector()
# Allocate a new register and prepare state on it
usp.compute(3)
reg2 = Qubits(usp.get_result_qreg(), "reg2")
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg1|?> |0|.> -0.333333+0.471405j |1|.> -0.333333+0.471405j |2|.> -0.333333+0.471405j |reg1|qbk_usp|?> |0|0|.> -0.111111-0.314270j |0|1|.> -0.111111-0.314270j |0|2|.> -0.111111-0.314270j |1|0|.> -0.111111-0.314270j |1|1|.> -0.111111-0.314270j |1|2|.> -0.111111-0.314270j |2|0|.> -0.111111-0.314270j |2|1|.> -0.111111-0.314270j |2|2|.> -0.111111-0.314270j
You can find examples of more complicated code that relies on a Qubrick returning a Qubits register as a result in a later tutorial which discusses applying operations conditioned on results of Qubrick computation.
Next steps¶
In this tutorial, you've learned about automatic quantum memory management within Qubricks. This concludes the introduction to the basics of working with Qubricks. In the next tutorial, we'll move on to discussing quantum arithmetic and related quantum data types.