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¶

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

The abstract gate checks the type and number of the arguments to avoid invalid gates

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

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

In [4]:
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)
In [5]:
circuit_full.display()
No description has been provided for this image
In [6]:
circuit_full.display(depth=1)
No description has been provided for this image
In [7]:
circuit_not_full.display(depth=1)
No description has been provided for this image
In [8]:
circuit_not_full_2.display()
No description has been provided for this image

Variable arities¶

Notice that the following circuit is "valid" (is the sense that pyAQASM is not raising any exception:

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

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

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

Custom arities can be specified using a lambda function:

In [12]:
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_setfunction:

In [13]:
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
In [14]:
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"]])
In [15]:
circuitA.display(depth=1)
No description has been provided for this image
In [16]:
circuitB.display(depth=1)
No description has been provided for this image
In [17]:
circuitC.display(depth=1)
No description has been provided for this image

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

In [18]:
%%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 :

In [19]:
%%bash 
aqasm2circ abstract_qft.aqasm abstract_qft.circ
ls abstract_qft.circ
abstract_qft.circ

Let us look into this circuit:

In [20]:
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):

In [21]:
%%bash
aqasm2circ -L qat.lang.AQASM.qftarith abstract_qft.aqasm qft.circ
In [22]:
qft = Circuit.load("qft.circ")
qft.display()
No description has been provided for this image
In [ ]: