Implement a Custom Gate

BQSKit’s claims great portability, and as such, most algorithms in BQSKit can work natively with any gate set. We have included many commonly used gates inside of the bqskit.ir.gates subpackage, but you may want to experiment with your own gates. In this tutorial, we will implement a custom gate in BQSKit. Since BQSKit’s algorithms are built on numerical instantiation, this process is usually as simple as defining a new subclass with a unitary at a high-level.

For example, let’s look at the TGate definition in BQSKit:

...
class TGate(ConstantGate, QubitGate):
    _num_qudits = 1
    _qasm_name = 't'
    _utry = UnitaryMatrix(
        [
            [1, 0],
            [0, cmath.exp(1j * cmath.pi / 4)],
        ],
    )

A gate is defined by subclassing Gate, however, there are some abstract subclasses that can be extended instead to simplify the process. For example, the TGate is a subclass of ConstantGate and QubitGate. The ConstantGate subclass is used for gates that have a fixed unitary matrix, and the QubitGate subclass is used for gates that act only on qubits – rather than qudits. In the following sections, the process of defining a custom gate will be explained in more detail.

Defining a Custom Gate

To define a custom gate, you need to subclass Gate, and define all the required attributes. These attributes can be defined as instance variables, class variables, or through methods. The following attributes are required:

  • _num_qudits: The number of qudits the gate acts on.

  • _num_params: The number of parameters the gate takes.

  • _radixes: The radixes of the qudits this gate acts on. This is a tuple of integers, where each integer is the radix of the corresponding qudit. For example, (2, 2) would be a 2-qubit gate, (3, 3) would be a 2-qutrit gate, and (2, 3, 3) would be a gate that acts on a qubit and two qutrits.

  • _name: The name of the gate. This is used during print operations.

  • _qasm_name: The name of the gate in QASM. (Qubit only gates, should use lowercase, optional)

Additionally, you will need to override the abstract method get_unitary. This method maps the parameters of the gate to a unitary matrix.

Here is an example of a custom gate that acts on a single qubit:

import cmath
from bqskit.ir.gate import Gate
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.qis.unitary.unitary import RealVector

class MyGate(Gate):
    _num_qudits = 1
    _num_params = 1
    _radixes = (2,)
    _name = 'MyGate'
    _qasm_name = 'mygate'

    def get_unitary(self, params: RealVector) -> UnitaryMatrix:
        theta = params[0]
        return UnitaryMatrix(
            [
                [cmath.exp(1j * theta / 2), 0],
                [0, cmath.exp(-1j * theta / 2)],
            ],
        )

Note the params argument is a RealVector object, which is an alias for many types of float arrays. There is a helper method in the Gate class hierarchy called check_parameters that can be used to validate the parameters before using them. This will check for the correct types and lengths of the parameters:

...
    def get_unitary(self, params: RealVector) -> UnitaryMatrix:
        self.check_parameters(params)
        ...
        return UnitaryMatrix(
            ...
        )

As mentioned previously, the required attributes can be defined as class variables, like in the above example, or as instance variables. The following example shows how to define a tensor product of an arbitrary number of MyGates using instance variables:

import cmath
from bqskit.ir.gate import Gate
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.qis.unitary.unitary import RealVector

class MyGateTensor(Gate):
    def __init__(self, num_qudits: int) -> None:
        self._num_qudits = num_qudits
        self._num_params = 1
        self._radixes = tuple([2] * num_qudits)
        self._name = f'MyGateTensor{num_qudits}'

    def get_unitary(self, params: RealVector) -> UnitaryMatrix:
        self.check_parameters(params)
        theta = params[0]
        base = UnitaryMatrix(
            [
                [cmath.exp(1j * theta / 2), 0],
                [0, cmath.exp(-1j * theta / 2)],
            ],
        )
        base.otimes(*[base] * (self._num_qudits - 1)) # base tensor product with itself
        # Note: Since the unitary is diagonal, there are more efficient ways to
        #       compute the tensor product, but this is a simple example meant
        #       to demonstrate the concept. In general, you should always implement
        #       the most efficient method for your gate.

This style is helpful when the gate’s attributes are dependent on the constructor arguments.

The last way to define the attributes is through methods. The corresponding property names can be found on the Gate class. The following example computes the gate name of MyGateTensor through the name property:

...
class MyGateTensor(Gate):
    ... # __init__ and get_unitary methods same as before without _name attribute

    @property
    def name(self) -> str:
        return f'MyGateTensor{self._num_qudits}'

Utilizing Helper Classes

BQSKit provides some helper classes to simplify the process of defining gates. In the first example of this guide, we used the ConstantGate and QubitGate helper classes. To use these helper subclasses, we will subclass them instead of Gate. The following are the available helper classes:

  • ConstantGate: A gate that has a fixed unitary matrix with no parameters. This will automatically set _num_params to 0, and swap the get_unitary method for a _utry attribute. Additionally, these gates have the trivial differentiable implementations provided.

  • QubitGate: A gate that acts only on qubits. This defines _radixes to be all 2s.

  • QutritGate: A gate that acts only on qutrits. This defines _radixes to be all 3s.

  • QuditGate: A gate that acts on qudits of the same radix. This swaps the _radixes requirement for a required _radix attribute. This is useful for gates that act on qudits of the same radix, but not necessarily only qubits or qutrits.

  • ComposedGate: A gate that is composed of other gates. This provides methods to dynamically determine if the gate is differentiable or optimizable via other means.

Differentiable Gates

If you are implementing a parameterized gate, you may want to make it differentiable. By making a gate differentiable, you allow it to be used by BQSKit’s instantiation engine. In turn, this allows synthesis and other algorithms to work more easily with these gates. To do this, you will need to additionally subclass DifferentiableUnitary and implement the get_grad method. ConstantGates are trivially differentiable, as they have no parameters.

Most of the time, the get_unitary_and_grad method is called by other parts of BQSKit, since both the unitary and gradient are typically needed at the same time. For most gates, computing them at the same time can allow for greater efficiency, since the unitary and gradient can share some computations.

Let’s make MyGate differentiable:

import cmath
from bqskit.ir.gate import Gate
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.qis.unitary.unitary import RealVector
from bqskit.qis.unitary.differentiableunitary import DifferentiableUnitary

class MyGate(Gate, DifferentiableUnitary):
    _num_qudits = 1
    _num_params = 1
    _radixes = (2,)
    _name = 'MyGate'
    _qasm_name = 'mygate'

    def get_unitary(self, params: RealVector) -> UnitaryMatrix:
        self.check_parameters(params)
        theta = params[0]
        return UnitaryMatrix(
            [
                [cmath.exp(1j * theta / 2), 0],
                [0, cmath.exp(-1j * theta / 2)],
            ],
        )

    def get_grad(self, params: RealVector) -> npt.NDArray[np.complex128]:
        self.check_parameters(params)
        theta = params[0]
        return np.array(
            [
                [
                    [1j / 2 * cmath.exp(1j * theta / 2), 0],
                    [0, -1j / 2 * cmath.exp(-1j * theta / 2)],
                ],
            ],
        )

The get_grad method should return a 3D array, where the first index is the parameter index. get_grad(params)[i] should return the gradient of the unitary with respect to the i-th parameter. The gradient should be a matrix of the same shape as the unitary matrix, where each element is the derivative of the unitary matrix element with respect to the parameter.

Working with QASM

If you want to use your gate in QASM, you will need to define the _qasm_name attribute. This is the name of the gate in QASM. However, some gates require special qasm definitions to be included at the top of a qasm file. This can be achieved by defining the get_qasm_gate_def method. This method returns a string, which will be included as-is at the top of every qasm file that uses the gate.