Creating and using your own gate set in (py)AQASM¶
This notebook is the logical continuation of this one.
Why would I want that?¶
The default gate set used in (py)AQASM is quite expressive but can be lacklusting is some cases:
black boxing routine calls¶
Lets say I know that the target hardware (or simulator) has some efficient low level implementation of some routine, for instance a QFT. In that case, I want to write my algorithm using a "black box" called QFT without necessarily specifying its implementation, and still be able to generate a circuit that will be understood by the hardware.
specifying a hardware level gate set¶
The second case appears when dealing directly with hardware level instructions. For instance, lets assume I want to specify a circuit destined to run on an ion-based hardware platform. Since the native gate set of ion-based platforms has a very unusual gate set, I want to be able to directly build the hardware level circuit in pyAQASM because I don't trust the compilation layer, or I want to produce a very optimized code. In that case I would like to describe this hardware level gate set and still be able to use it to build my circuits.
Declaring and using an abstract gate in pyAQASM¶
from qat.lang.AQASM import *
# We give a name to the constructor (here QFT) and a string specifying the argument types (here 1 char "i")
my_qft_gate = AbstractGate("QFT", [int])
# Now we can play around with this gate
QFT_10 = my_qft_gate(10)
prog = Program()
reg = prog.qalloc(10)
prog.apply(QFT_10, reg)
# If we export this program in circuit format, we will obtain a similar result to the AQASM approach above.
circuit_qft = prog.to_circ()
circuit_qft.display()
The abstract gate checks the type and number of the arguments to avoid invalid gates
try:
QFT_toto = my_qft_gate("foo")
except WrongArgumentType as e:
print(e)
print("Ooops, something went wrong when building QFT_foo")
print()
try:
QFT_10_18 = my_qft_gate(10, 18)
except WrongArgumentsNumber as e:
print(e)
print("Ooops, something went wrong when building QFT_10_18")
foo is not compatible with type <class 'int'> Ooops, something went wrong when building QFT_foo QFT requires 1 argument(s) (2 given). Ooops, something went wrong when building QFT_10_18
And now what do we do with this circuit?¶
We have a circuit containing an abstract gate "QFT".
If my hardware is able to handle this gate, I can send it directly to the proper QPU.
But what if I simply did that to create an intermediate circuit in which I want to inline an implementation of the QFT?
Turning a python module into a pyAQASM library is easy to do using function decorators (see below).
Linking a custom implementation using pyAQASM¶
pyAQASM allows us to attach circuit_generators
to AbstractGates
.
The idea is similar to the process of attaching a matrix_generator
to an AbstractGate
.
One simply needs to define a python function having the same input signature as the gate and returning a QRoutine
object.
# Lets first define a custom implementation of the QFT
# This custom implementation needs to return a QRoutine object
# Here we provide an inexact implementation, for illustration purpose only!
def my_fake_qft(n):
rout = QRoutine()
reg = rout.new_wires(n)
for qb in reg:
rout.apply(H, qb)
return rout
# We can now add this circuit generator to the GateSignature object
my_qft_gate.set_circuit_generator(my_fake_qft)
# Of course, one could have written:
my_qft_gate = AbstractGate("QFT", [int], circuit_generator=my_fake_qft)
Playing around with to_circ¶
Once we use an abstract gate with a specified circuit generator, pyAQASM will automatically:
- call the circuit generator for every occurence of the gate
- inline the resulting routine inside the body of the circuit
It is however possible to bypass these actions if needed.
prog = Program()
qbits = prog.qalloc(4)
prog.apply(my_qft_gate(4), qbits)
# Standard usage: the inline will not take place
circuit_full = prog.to_circ()
# Blocking the linking of the QFT gates
circuit_not_full = prog.to_circ(keep=["QFT"])
# Blocking all inline
circuit_not_full_2 = prog.to_circ(inline=True)
circuit_full.display()
circuit_full.display(depth=1)
circuit_not_full.display(depth=1)
circuit_not_full_2.display()
Variable arities¶
Notice that the following circuit is "valid" (is the sense that pyAQASM is not raising any exception:
my_fake_qft_2 = AbstractGate("QFT", [int])
prog = Program()
qbits = prog.qalloc(3)
prog.apply(my_fake_qft_2(18), qbits)
circ = prog.to_circ()
Indeed, we did not specify any constraints on the arity of my_fake_qft_2
.
It is possible to specify an arity_generator
to the AbstractGate
constructor to give more information to pyAQASM in order to statically detect this kind of potential mistakes during the circuit construction:
my_fake_qft_2 = AbstractGate("QFT", [int], arity=lambda n:n)
prog = Program()
qbits = prog.qalloc(3)
try:
prog.apply(my_fake_qft_2(18), qbits)
except Exception as e:
print(type(e), e)
<class 'qat.lang.AQASM.aqasm_util.InvalidGateArguments'> Gate QFT of arity 18 cannot be applied on [0,1,2]
Abstract gates and build_gate decorator¶
Another way of declaring an AbstractGate
with a particular subcircuit implementation is to use the build_gate
function decorator.
This decorator turns function returning routines into abstract gates at the cost of adding some information in the decorator:
from qat.lang.AQASM.misc import build_gate
# We use the same dummy implementation
@build_gate("QFT", [int])
def my_qft(n):
rout = QRoutine()
for i in range(n):
rout.apply(H, i)
return rout
# And use it directly inside a program
prog = Program()
qbs = prog.qalloc(3)
prog.apply(my_qft(3), qbs)
circuit = prog.to_circ()
circuit.display()
Custom arities can be specified using a lambda function:
from qat.lang.AQASM.misc import build_gate
@build_gate("QFT", [int], lambda n:n)
def my_qft(n):
rout = QRoutine()
for i in range(n):
rout.apply(H, i)
return rout
# And use it directly inside a program
prog = Program()
qbs = prog.qalloc(3)
try:
prog.apply(my_qft(18), qbits)
except Exception as e:
print(type(e), e)
<class 'qat.lang.AQASM.aqasm_util.InvalidGateArguments'> Gate QFT of arity 18 cannot be applied on [0,1,2]
Generating gate sets from decorated functions¶
qat.lang.AQASM.qftarith
is a good example of a python namespace containing (many) decorated functions.
It is possible to generate a gate set containing all the declared gates using the generate_gate_set
function:
from qat.lang.AQASM.misc import generate_gate_set
import qat.lang.AQASM.qftarith
qftarith_gate_set = generate_gate_set(qat.lang.AQASM.qftarith)
print(qftarith_gate_set)
PH : 1 params of types [<class 'float'>], generating a gate of arity 1 QFT : 1 params of types [<class 'int'>], generating a gate of arity None ADD_CONST : 2 params of types [<class 'int'>, <class 'int'>], generating a gate of arity None ADD : 2 params of types [<class 'int'>, <class 'int'>], generating a gate of arity None MULT : 3 params of types [<class 'int'>, <class 'int'>, <class 'int'>], generating a gate of arity None MULT_CONST : 3 params of types [<class 'int'>, <class 'int'>, <class 'int'>], generating a gate of arity None
Linking gate sets during circuit generation¶
The to_circ
method allows to pass gate sets as libraries in order to link implementations to abstract gates present inside the circuit.
In the following cell, we will :
- define a simple circuit with an implementation-less QFT
- link qat.lang.AQASM.qftarith in order to provide an implementation to this QFT
empty_QFT = AbstractGate("QFT", [int], arity=lambda n: n)
prog = Program()
qbits = prog.qalloc(3)
prog.apply(empty_QFT(3), qbits)
circuitA = prog.to_circ()
circuitB = prog.to_circ(link=[qat.lang.AQASM.qftarith])
circuitC = prog.to_circ(link=[qftarith_gate_set["QFT"]])
circuitA.display(depth=1)
circuitB.display(depth=1)
circuitC.display(depth=1)
Specifying an abstract gate in AQASM¶
The AQASM syntax to declare a new abstract gate in the header of an AQASM file is the following:
DEFINE PARAM gate_name list_of_arguments_types : gate_arity
For instance lets write the file abstract_qft.aqasm
%%writefile abstract_qft.aqasm
DEFINE PARAM QFT int : None
BEGIN
qubits 4
QFT[4] q[0], q[1], q[2], q[3]
END
Overwriting abstract_qft.aqasm
This file will safely compile via aqasm2circ :
%%bash
aqasm2circ abstract_qft.aqasm abstract_qft.circ
ls abstract_qft.circ
abstract_qft.circ
Let us look into this circuit:
from qat.core import Circuit
circuit_aqasm = Circuit.load("abstract_qft.circ")
print("Circuit containing",len(circuit_aqasm.ops), "gate(s)")
print(circuit_aqasm.ops[0])
print("Its syntax is", circuit_aqasm.gateDic[circuit_aqasm.ops[0].gate].syntax)
Circuit containing 1 gate(s) Op(gate='_0', qbits=[0, 1, 2, 3], type=0, cbits=None, formula=None, remap=None) Its syntax is GSyntax(name='QFT', parameters=[Param(is_abstract=None, type=0, int_p=4, double_p=None, string_p=None, matrix_p=None, serialized_p=None, complex_p=None)])
This tells us that:
- the circuit contains only one gate
- the syntax of this gate stipulates that its a QFT with a single argument of type int with value 4
Linking an implementation using aqasm2circ¶
The -L option of aqasm2circ allows to link a pyAQASM library to the compiler so that it can inline the proper implementation of the various gates used in a AQSM file.
For example, if I want to link the classic implementation of the QFT (described in the python module qat.lang.AQASM.qftarith):
%%bash
aqasm2circ -L qat.lang.AQASM.qftarith abstract_qft.aqasm qft.circ
qft = Circuit.load("qft.circ")
qft.display()