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 QRoutine
s, 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()