from qat.lang.AQASM import *
Compute, uncompute, and computation scopes¶
The following set of operations constitute a recurrent scheme in reversible computation/programmation:
- Apply some routine
A - Apply some other computation
B - Undo routine
A
Even though the QRoutine class allows us to define A and B inside two distinct QRoutines, and apply A, B, A.dag(), having to separate both set of instructions into two distinct routines in order to define a third routine feels clunky and unnecessary.
For that reason, QRoutine provides a way to implement this scheme efficiently using what we call computation scopes.
The main flow is the following:
- Open a fresh computation scope
- Apply all the instructions of
A - Close the computation scope
- Apply all the instructions of
B - Uncompute the last closed scope
Simple computation scope usage¶
In practice, this scope definition/management is hidden inside the QRoutine class and can be manipulated via a with statement.
from qat.lang.AQASM import *
routine = QRoutine()
with routine.compute(): # Here we open a fresh "computation scope"
routine.apply(X, 0) # This gate will be stored in the scope
routine.apply(CNOT, 0, 1) # Here we leave the scope and apply another gate
routine.uncompute() # And finally, we uncompute the last scope
routine.display()
Nested scopes manipulation¶
Scopes can also be nested for even more powerful schemes. In practice, closing a scope stores it on top of a stack. Uncomputing simply pops the latest scope of the stack and uncomputes it.
routine = QRoutine()
w0, w1, w2 = routine.new_wires(3)
with routine.compute(): # Opening scope #1
routine.apply(X, w0)
with routine.compute(): # Opening scope #2 inside scope #1
routine.apply(CNOT, w0, w1)
routine.apply(CNOT, w1, w2) # We left scope #2, we are still inside scope #1
routine.uncompute() # We uncompute scope #2 inside scope #1
routine.apply(CNOT, w2, w1)
routine.apply(RZ(0.3), w1)
# We left scope #1
routine.apply(RZ(0.5), w1)
routine.uncompute() # Uncomputing scope #1
routine.display()
Computation/uncomputation and controls¶
A nice thing about having a built-in computation/uncomputation scheme is to save up on controls when controling the resulting routine.
Indeed, when controlling the sequence A; B; A.dag(), one simply needs to control B, thus saving up on the overhead of having to control A and A.dag().
Our built-in compute/uncompute scheme tags all the operations in the scope to "protect" them against controls (similar to changing all the routine.apply calls into routine.protected_apply calls inside a computation scope).
rout_controlled = routine.ctrl(5)
rout_controlled.display()