Writing and linking libraries¶
PyAQASM provides all the administrative tools to write and link subcircuit libraries. This notebook demonstrates these tools via an advanced example.
Our use case is the following. Lets say we want to construct a complicated oracle for some Grover like algorithm. Our oracle will require generalized Toffoli gates (i.e Toffoli gates with an arbitrary number of controls) and some arithmetic subroutine (an addition for instance).
In this situation, one might want to write a skeleton that makes no assumption on the underlying implementation of our Toffoli gates and our addition. This has two advantages:
- We will focus solely on the structure of our oracle, instead of dealing with annoying ancillae management or qubit counting
- Later, we will be able to link different implementations. For instance, in case I want to simulate my oracle, I might want to link a low qubit count implementation of the Toffoli. However, if I want to estimate the number of proper Toffoli gates in my circuit, I might want to link realistic implementations of the addition/Toffoli.
The skeleton¶
Place holders¶
In order to write our oracle, we will create two abstract gates that will play the role of place holders for the proper subroutine calls.
from qat.lang.AQASM import AbstractGate
# Place holder for a generalized Toffoli gate. It takes a single integer parameter: the number of qubits
# it is applied on
toffoli = AbstractGate("TOFF", [int], arity=lambda n: n)
print(toffoli)
# Place holder for an addition. We give it the same signature as the adder implementation of the
# qat.lang.AQASM.arithmetic.add gate, in order to be able to link qftarith or classarith implementations
# of the adder.
# We will assume that the second register is added into the first one.
add = AbstractGate("ADD", [int, int], arity=lambda n1, n2: n1 + n2)
print(add)
TOFF : 1 params of types [<class 'int'>], generating a gate of arity None ADD : 2 params of types [<class 'int'>, <class 'int'>], generating a gate of arity None
The skeleton's body¶
Lets write our oracle.
Lets say we want our oracle to take 2 numbers as input $|a\rangle$ and $|b\rangle$ and checks if $a + b = 7$.
We will pack our oracle using a function that, given the number of qubits used to represent $|a\rangle$ and $|b\rangle$, returns a QRoutine implementing the oracle.
from qat.lang.AQASM import QRoutine, X, Program
def oracle(n1, n2):
rout = QRoutine()
# Our two registers carrying a and b
a = rout.new_wires(n1)
b = rout.new_wires(n2)
# the output
result = rout.new_wires(1)
# First we open a fresh computation scope that will be uncomputed later one
with rout.compute():
# We start by adding b into a
rout.apply(add(n1, n2), a, b)
# We then flip all the bits of a, but the first 3 (we want to check if a == 7)
for wire in a[3:]:
rout.apply(X, wire)
# Now we check that all the bits of a are 1
rout.apply(toffoli(n1 + 1), a, result)
# And we uncompute the scope
rout.uncompute()
return rout
oracle(4, 3).display()
Linking a subroutine¶
Lets first link a proper implementation of a Toffoli to our "TOFF" gate.
We will define two implementations of a generalized Toffoli:
- a $X$ gate controlled $(n-1)$ times
- a recursive divide-and-conquer implementation that uses ancillae. Even though this is not the most efficient way of implementing a Toffoli, it is simple enough to be written in a notebook.
We will pack each of these implementations in functions returning QRoutines.
Each function will be lifted into an AbstractGate using the build_gate
decorator.
from qat.lang.AQASM.misc import build_gate
@build_gate("TOFF", [int], lambda n: n)
def standard_toffoli(n):
rout = QRoutine()
wires = rout.new_wires(n)
rout.apply(X.ctrl(n - 1), wires)
return rout
@build_gate("TOFF", [int], lambda n: n)
def dac_toffoli(n):
rout = QRoutine()
controls = rout.new_wires(n - 1)
target = rout.new_wires(1)
if n == 3:
rout.apply(X.ctrl(2), controls, target)
return rout
first_half = (n - 1) // 2 + ((n - 1) % 2)
second_half = (n - 1) // 2
with rout.compute():
first_toffoli = dac_toffoli(first_half + 1)
first_anc = rout.new_wires(1)
rout.apply(first_toffoli, controls[0:first_half], first_anc)
rout.set_ancillae(first_anc)
if second_half > 1:
second_toffoli = dac_toffoli(second_half + 1)
second_anc = rout.new_wires(1)
rout.apply(second_toffoli, controls[first_half:], second_anc)
rout.set_ancillae(second_anc)
else:
second_anc = controls[-1]
rout.apply(X.ctrl(2), first_anc, second_anc, target)
rout.uncompute()
return rout
(~standard_toffoli)(4).display(depth=1)
(~dac_toffoli)(4).display(depth=2)
We can now link any of these implementations to our oracle using the link
option in the to_circ
method.
my_oracle = oracle(4, 5)
prog = Program()
qbits = prog.qalloc(my_oracle.arity)
prog.apply(my_oracle, qbits)
# The linking will happen here.
# We set inline to True, just so we can introspect the circuit after
# In practice, inline should stay at False for performance reasons
circuit_standard = prog.to_circ(link=[standard_toffoli])
circuit_standard.display(depth=1)
circuit_dac = prog.to_circ(link=[dac_toffoli])
circuit_dac.display(depth=3)
There are two things to notice:
- First, the two circuit correspond to what we expected: our oracle where the "TOFF" gate has been replaced with one of the two implementation we defined above
- Second, the second circuit has more qubits than the first one. This is due to the fact that our second implementation
dac_toffoli
uses ancillae. These ancillae have been allocated during the call to theto_circ
method.
Ok! Now lets finish the job and link a proper implementation of an adder. PyAQASM comes with preprogramed arithmetic libraries using either QFT based arithmetic or carry based.
For the sake of demonstration, we will try and link each one of these libraries.
import qat.lang.AQASM.qftarith
import qat.lang.AQASM.classarith
circuit_standard_qft = prog.to_circ(link=[qat.lang.AQASM.qftarith, standard_toffoli])
circuit_standard_qft.display(depth=3)
circuit_standard_carry = prog.to_circ(link=[qat.lang.AQASM.classarith, standard_toffoli])
circuit_standard_carry.display(depth=3)
This second circuit demonstrate how ancillae are reused: the first addition allocates some ancilla, and this same ancilla is then used by the dagger of the addition.
We can link our other implementation of the Toffoli to check that the Toffoli will also use this same ancilla:
circuit_dac_carry = prog.to_circ(link=[qat.lang.AQASM.classarith, dac_toffoli])
circuit_dac_carry.display(depth=4)
Writing proper libraries¶
In the example above, we linked a full python package (qat.lang.AQASM.qftarith/classarith) to the circuit extraction routine.
In practice, the link
keyword accepts:
AbstractGate
s that have subcircuit implementations (such as@build_gate
decorated functions)- python packages (in that case, the package is imported, crawled, and all
AbstractGates
are extracted and linked) - gate sets (dictionaries of
AbstractGates
)
Hence, any python file containing many functions decorated using @build_gate
can be linked in one go. This is the case of qat.lang.AQASM.qftarith
, qat.lang.AQASM.classarith
, and qat.lang.AQASM.arithmetic
.
To push our example to its limits, one can think of writing two different files:
- simulation_lib.py that will contain:
standard_toffoli
qat.lang.AQASM.qftarith.add
- realistic_lib.py that will contain:
dac_toffoli
qat.lang.AQASM.classarith.cuccaro_add
Then we can link any of the two package, depending on if we want to simulate the circuit or obtain a ressource estimation out of it.
import simulation_lib, realistic_lib
circuit_simulation = prog.to_circ(link=[simulation_lib])
circuit_simulation.display(depth=2)
circuit_estimation = prog.to_circ(link=[realistic_lib])
circuit_estimation.display(depth=2)