Uncomputation Context Manager¶
In this tutorial, we will discuss a convenient tool for handling uncomputation - a context manager that manages uncomputation automatically and concisely.
A common pattern of using uncomputation in quantum algorithms looks as follows:
- Compute a subroutine.
- Perform intermediate computation using results of the first step as part of the inputs.
- Uncompute the first subroutine.
Both common scenarios that we saw in the uncomputation tutorial, undoing changes to the state of the input qubits and unentangling auxiliary qubits from the "main" qubits, follow this pattern.
Workbench provides a convenient context manager for expressing this pattern in your programs, as long as the subroutine that is first computed and then uncomputed is implemented as a Qubrick (it really should be!). In this tutorial, we will explore this context manager and its use in different scenarios, from doing sequences of operations to applying gates conditioned on comparison results.
Computed context manager¶
To get the context manager for uncomputation of a specific Qubrick, you use the method called computed. This method is generated automatically based on the Qubrick's implementation of _compute and _uncompute methods, same as compute and uncompute methods. The computed method provides a context manager that ensures proper handling of computation, capturing its results, and subsequent uncomputation.
The simplest scenario of invoking the computed method is using it with a Qubrick that modifies the quantum registers it acts on in-place, without allocating new qubits to store the computation results. In this case, the method call will look like this:
qbk = MyQubrick()
with qbk.computed(input_register):
# Perform further computation on input_register modified by qbk
...
# Uncompute is called implicitly at the end of this block
This is equivalent to the following code:
qbk = MyQubrick()
qbk.compute(input_register)
# Perform further computation on input_register modified by qbk
...
# Call uncompute explicitly
qbk.uncompute()
The following code snippet shows the example of reflection about a quantum state from the uncomputation tutorial rewritten using the computed context manager. Notice that you pass any arguments you would normally pass to the compute method of a Qubrick to the computed method instead.
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import USP
qpu = QPU(num_qubits=3)
reg = Qubits(2, "reg", qpu)
reg.had()
qpu.print_state_vector()
usp = USP()
# Reflect about the state |0⟩ + |1⟩ + |2⟩
with usp.computed(3, reg, dagger=True):
(~reg).reflect()
# Equivalent to the following lines:
# usp.compute(3, reg, dagger=True)
# (~reg).reflect()
# usp.uncompute()
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg|?> |0|.> 0.500000+0.000000j |1|.> 0.500000+0.000000j |2|.> 0.500000+0.000000j |3|.> 0.500000+0.000000j |reg|?> |0|.> -0.500000-0.000000j |1|.> -0.500000-0.000000j |2|.> -0.500000-0.000000j |3|.> 0.500000+0.000000j
Capturing compute result using context manager¶
In a more general scenario, the subroutine's compute method returns a quantum register that stores the result of the computation. It can be one of the quantum registers passed to it as arguments, a register allocated within the subroutine, or a combination of these. Importantly, the result register has to be marked as such using the get_result_qreg method of the subroutine Qubrick (see the auxiliary qubit management tutorial for a refresher).
In this case, using the computed method will look like this:
qbk = MyQubrick()
with qbk.computed(input_register) as result_register:
# Perform further computation using result_register
...
# Uncompute is called implicitly at the end of this block
This is equivalent to the following code:
qbk = MyQubrick()
qbk.compute(input_register)
result_register = qbk.get_result_qreg()
# Perform further computation using result_register
...
# Call uncompute explicitly
qbk.uncompute()
The following code snippet shows the example from the auxiliary qubit management tutorial rewritten using the computed context manager.
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()
with p.computed(reg) as parity:
qpu.print_state_vector()
parity.z()
# Equivalent to the following lines:
# p.compute(reg)
# parity = p.get_result_qreg()
# qpu.print_state_vector()
# 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
Using comparisons with context manager¶
Comparison Qubricks we saw in the quantum arithmetic tutorial benefit from the use of context manager twice. First, these Qubricks allocate a single-qubit register that stores the result of the comparison, which can be conveniently captured using the with qbk.computed() as cond: syntax. Second, Workbench offers syntactic sugar that allows you to replace Qubrick instantiation and the compute call with a simple comparison expression such as a > b; this syntactic sugar also covers the use of the computed method.
For example, consider using a greater than comparison, implemented by the CompareGT Qubrick. You can perform it using the computed method (with syntactic sugar) as follows:
# Check whether reg1 is greater than reg2
with reg1 < reg2 as cond:
# Perform further computation using comparison result cond
...
# Uncompute is called implicitly at the end of this block
In contrast, here is what the same comparison without the computed method looks like:
# Check whether reg1 is greater than reg2
gt = CompareGT()
gt.compute(reg1, reg2)
cond = gt.get_result_qreg()
# Perform further computation using comparison result cond
...
# Call uncompute explicitly
gt.uncompute()
Notice how much more concise and readable the first code snippet is!
You can use the same syntax for all comparisons, both equality and less than/greater than, and for all types of inputs.
The following code snippet shows how to compare a quantum register with a classical constant and to apply a gate conditioned on the comparison result to another register. You can use the comparison result in any other quantum code as well!
Notice that code fragments that apply a gate controlled on the result of an equality check end up compiled into a simple multi-controlled gate without allocating an extra qubit for the comparison result or uncomputing the comparison itself. The rest of comparisons, though, are implemented as sequences of controlled gates with an extra qubit serving to store the result of the comparison.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=6)
reg = Qubits(3, "reg", qpu)
target = Qubits(1, "target", qpu)
qpu.label("reg == 3")
with reg == 3 as cond:
target.x(cond)
# Same as a multi-controlled gate
# target.x(cond=reg == 3)
qpu.label("~reg")
with ~reg as cond:
target.x(cond)
qpu.label("")
# Same as a multi-controlled gate
# target.x(cond=~reg)
# or
# target.x(cond=reg == 0)
with reg != 3 as cond:
target.x(cond)
with reg < 3 as cond:
target.x(cond)
# >, <=, and >= act similarly to the < comparison
qpu.draw(show_qubricks=True)
The next code snippet shows how to compare two quantum registers and to apply a gate conditioned on the comparison result to another register. In this case, none of the code fragments can be compiled into a single multi-controlled gate.
from psiqworkbench import QPU, Qubits
qpu = QPU(num_qubits=7)
reg1 = Qubits(2, "reg", qpu)
reg2 = Qubits(2, "reg", qpu)
target = Qubits(1, "target", qpu)
with reg1 == reg2 as cond:
target.x(cond)
with reg1 != reg2 as cond:
target.x(cond)
with reg1 < reg2 as cond:
target.x(cond)
# >, <=, and >= act similarly to the < comparison
qpu.draw(show_qubricks=True)
Next steps¶
In this tutorial, you've learned to use the computed context manager to make your code more readable and concise.