In [1]:
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.

In [2]:
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()
No description has been provided for this image

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.

In [3]:
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()
No description has been provided for this image

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).

In [4]:
rout_controlled = routine.ctrl(5)

rout_controlled.display()
No description has been provided for this image
In [ ]: