Defining and using a custom gate set in pyAQASM¶

pyAQASM comes with a universal, quite expressive, set of gates. Although this gate is set is convenient for general purpose quantum programmation, one might want to restrict oneself to a particular, potentially less expressive, gate set to, for instance, faithfully describe hardware-level circuits.

In this notebook, we will derive the definition and use of a simple gate set composed of:

  • a single, parametrized, entangling gate acting on 2 qubits: CX(theta)
  • Two 1-qbit gates:
    • a non-parametrized local gate P
    • a parametrized local gate PX(phi, theta) parametrized by two angles

For each of these gates, we will provide a matrix generator in order to be able to populate the matrices in the circuit model, thus allowing us to later simulate these circuits.

Abstract gates¶

Defining custom parametrized gates in pyAQASM is done via the AbstractGate class:

In [1]:
from qat.lang.AQASM import *

# An AbstractGate takes a gate name and a list of parameter types
# Authorized types are int, float, and str

# CX takes a single parameter of type float (an angle)
CX = AbstractGate("CX", [float], arity=2)

# P takes no parameter and is of arity 1
P = AbstractGate("P", [], arity=1)

# Finally PX takes two parameters of types float
PX = AbstractGate("PX", [float, float], arity=1)

Using these gates is as simple as using a native parametrized gate, such as RZ:

In [2]:
prog = Program()
qbits = prog.qalloc(2)
prog.apply(CX(0.33), qbits)
prog.apply(P(), qbits[0]) ## Notice that we have to add parenthesis. P is still parametrized by 0 parameters
prog.apply(PX(1.33, 0.4), qbits[1])
circuit = prog.to_circ()

circuit.display()
No description has been provided for this image

Matrix generators¶

It is possible to attach matrix definitions to abstract gates. A matrix generator will be any python function that has the same signature as the gate and returns a numpy array:

In [3]:
import numpy as np

# Let us define a function that given a value for theta returns a matrix corresponding to CX(theta)
# Our CX(theta) will simply be some kind of parametrized CNOT
def CX_generator(theta):
    return np.array([[1, 0, 0, 0],
                     [0, 1, 0, 0],
                     [0, 0, 0, np.exp(-1j * theta/2)],
                     [0, 0, np.exp(1j * theta/2), 0]])

# We can now attach this generator to our gate
CX.set_matrix_generator(CX_generator)
# Equivalently, we could have defined the gate directly as follows:
CX = AbstractGate("CX", [float], arity=2, matrix_generator=CX_generator)

# We do the same for P 
P.set_matrix_generator(lambda: np.array([[1,0],[0, np.exp(1j*np.pi/2)]]))

# And lets do something tricky for PX
def PX_generator(phi, theta):
    _I = np.eye(2, dtype=np.complex128)
    _X = np.array([[0,1],[1,0]], dtype=np.complex128)
    _Y = np.array([[0,-1j],[1j,0]], dtype=np.complex128)
    return np.cos(theta/2) * _I - 1j * np.sin(theta/2) *(np.cos(phi) * _X + np.sin(phi) * _Y)
PX.set_matrix_generator(PX_generator)

Simulating the gates¶

Since we provided "recipes" to produce matrices from the parameters, we can easily simulate any circuit that contains theses gates:

In [4]:
prog = Program()

qbits = prog.qalloc(2)

# Here, since P is an abstract gate, it still needs requires a list of parameters
# to be used inside a Program, hence the P() and not simply P.
prog.apply(P(), qbits[0])
prog.apply(CX(np.pi/2), qbits[0], qbits[1])
prog.apply(PX(np.pi, np.pi/2), qbits[1])

circuit = prog.to_circ()


from qat.qpus import get_default_qpu
mypylinalgqpu = get_default_qpu()

job = circuit.to_job()

for sample in mypylinalgqpu.submit(job):
    print("State %s amplitude %s" % (sample.state, sample.amplitude))
State |00> amplitude (0.7071067811865476+0j)
State |01> amplitude (8.659560562354932e-17+0.7071067811865475j)
In [ ]: