Make your own QPU

In our Framework, a QPU class should inherit from qat.qpus.QPUHandler which defines the following methods:

Raising exception in the code

A QPU can raise QPUException. A QPU can be accessed remotely (using Qaptiva Access or by starting the QPU in server mode). This exception can be serialized and re-raised on the client side

A QPUException can be raised using assert_qpu()

from qat.core.assertion import assert_qpu

# If my_condition() returns False:  raises a QPUException
# If my_condition() returns True: do nothing
assert_qpu(my_condition(), "Error message")

Warning

If your QPU implements its own constructor, please ensure the parent constructor is called

from qat.core.qpu import QPUHandler

class MyQPU(QPUHandler):
    def __init__(self, parameter):
        super().__init__()  # Please ensure parents constructor is called
        self._parameter = parameter

    def submit_job(self, job):
        ...

Method submit_job

The submit_job() method is the only required method. This function takes one or two parameters:

  • a required argument of type Job. This argument defines what to execute and what to return. Attributes of this class are defined on the job page

  • an optional argument of type dict[str, str] (optional means that this argument can be removed, i.e. def submit_job(self, job): ... is a valid method), this argument containing the meta-data of the Batch

    A good practice

    Meta-data could be used to override temporarly the parameter used to instantiate a QPU (the name of the QPU corresponds to the key of this dictionary, and the associated value contains the value to override). Multiple components (like plugins) can interact with this QPU by sending Batch. Implementing this “good practise” can improve the interaction between these components and the QPU.

    For instance, to override the parameter argument defined in the example above, the submit function shall look like:

    import json
    from qat.core.qpu import QPUHandler
    
    class MyQPU(QPUHandler):
        def __init__(self, parameter):
            super().__init__()  # Please ensure parents constructor is called
            self._parameter = parameter
            self._default_options = {"parameter": parameter}
    
        def _override(self, options):
            if "parameter" in options:
                self._parameter = options["parameter"]
    
        def submit_job(self, job, meta_data=None):
            # Override "self._parameter"
            if meta_data:
                options = json.loads(meta_data.get("MyQPU", "{}"))
                self._override(options)
    
            ... # Perform execution
    
            self._override(self._default_options)
            return ...  # Return result
    

This method is often splitted in 3 main steps:

  • Step 1: ensures the quantum job can be executed “as it is”. This steps ensure the number of shot is valid, the processing type (e.g. circuit, schedule, etc.) is valid regarding the QPU, the final measurement is correct, etc.

  • Step 2: exceutes the quantum job nbshots times (0 shots corresponds to the maximal number of shots supported by the QPU - this value is valid)

    • if the quantum job contains a circuit, gates are executed one by one. Method iterate_simple() can be used to list all the gates composing the circuit

    • if the quantum jobs contains a scheduler, the quantum simulation should be executed accordingly

  • Step 3: builds and cleans result. Samples are added using the add_sample() method, the average value of the observable is set by updating the attribute value of Result. In sample mode, the result can be clean-up using function aggregate_data()

The card underneath provides a skeleton for a QPU executing quantum circuits in sample mode

A skeleton for a custom QPU
from qat.core import Result
from qat.core.qpu import QPUHandler
from qat.core.assertion import assert_qpu
from qat.core.wrappers.result import aggregate_data

MAX_NB_SHOTS = 1024


class QPUSkeleton(QPUHandler):
    """
    Skeleton of a custom QPU

    This skeleton execute a circuit, by running gates one by one. This skeleton also returns
    a result
    """
    def submit_job(self, job) -> Result:
        """
        Execute a job
        The job should contain a circuit (neither a analog job, nor a annealing job)

        Args:
            job: the job to execute

        Returns:
            Result: result of the computation
        """
        # Check job
        nb_shots = job.nbshots or MAX_NB_SHOTS

        assert_qpu(job.circuit is not None, "This skeleton can only execute a circuit job")
        assert_qpu(0 < nb_shots <= MAX_NB_SHOTS, "Invalid number of shots")
        assert_qpu(job.type == ProcessingType.SAMPLE, "This QPU does not support OBSERVABLE measurement")

        # Initialize result
        result = Result()

        # Measured qubits: qubits which should be measured at the end
        # The "qubits" attribute is either:
        #   - a list of qubits (list of integer)
        #   - None (all qubits should be measured)
        measured_qubits = job.qubits or list(range(job.circuit.nbqbits))

        # Execute the circuit several time
        for shot in range(nb_shots):
            for gate in job.circuit.iterate_simple():
                ... # TODO: execute gate

            state = ... # TODO: measure qubits listed in "measured_qubits"
            result.add_sample(state)

        # Aggregate data
        # If set to True, the output will be compressed. The list of sample will be caster into a shorter
        # list of tuple [state, probability]
        if job.aggregate_data:
            # The "threshold" parameter is used to remove state having a probability lower than this value
            # are removed
            aggregate_data(result, threshold=job.threshold)

        # Return result
        return result

Method get_specs

This method does not take any parameter and returns a description of the hardware. The output of this function is used by compilers to update a quantum jobs, to make it executable by the QPU.

This hardware description defines:

  • the number of qubits composing the QPU

  • the topology of the hardware

  • a gate set

  • if the QPU supports SAMPLE measurements or the OBSERVABLE measurements

  • a description of the hardware

This method is already implemented by QPUHandler but could be overrided. If not implemented, the QPU is assumed to support any size of quantum jobs (in term of qubits), all to all interaction, can execute any gate, support SAMPLE and OBSERVABLE measurements

from qat.core import HardwareSpecs
from qat.core.qpu import QPUHandler

def MyQPU(QPUHandler):
    def get_specs(self):
        """
        Returns a description of the hardware
        """
        return HardwareSpecs(...)

Class HardwareSpecs is detailed on this in the plugin section

Estimate and apply resource

Methods estimate_resources_for_batch() and apply_resource_consumption_limits() are used to reserve resources and get the list of reserved resources. These functions are called before submitting a job.

Note

Function estimate_resources_for_batch() is optional. If not set, or if this method returns None, resource won’t be reserved by the Qaptiva Resource Manager. Not locking resource could lead to resource conflict

The first method takes a batch (i.e. a list of job) and returns a list of ResourceModel. Currently, the resource management only supports a single type of resource, so this list should comprised of a single ResourceModel object. This data-structure defines few attributes:

  • a qpu giving the name of the Python class (required string - used for logging)

  • the nunber of qubits giving the number of qubits used to execute the batch (required integer - used for logging)

  • the number of jobs composing the batch (used for logging)

  • the memory (in MB) required to execute a job (integer)

  • the number of wished threads - resource manager can allocate less threads (integer)

  • the number of nodes to be reserved - must be set to 1 for mono-node simulation, defaults to 1 (integer)

  • the type of nodes to be reserved - defaults to NodeType.QAPTIVA (0) (NodeType)

  • a list of Device - a device describes either a QPU hardware or a GPU. A device can be used by a single job at a time

Reserving a device

A device is defined by 4 parameters:

  • a type (QPU or GPU)

  • an optional manufacturer (e.g. "Eviden")

  • an optional model (e.g. "Qaptiva-QPU")

  • an optional identifier. The identifier is a unique identifier used to differentiate the differente devices. This identifier will be used by apply_resource_consumption_limits() to target the right device.

An attribute is considered to be not defined if the attribute above is not defined.

Requesting a QPU can be done using the following sample of code

from qat.comm.resource.ttypes import Device, DeviceType

# Requesting a QPU device
Device(type=DeviceType.QPU)

Requestion a QPU built by Eviden can be done using the following sample of code. Please note that Eviden does not build QPU, this example is used for illustration purpose only

from qat.comm.resource.ttypes import Device, DeviceType

# Requesting a QPU device built by Eviden
Device(type=DeviceType.QPU, manufacturer="eviden")

Requesting a Qaptiva QPU can be done using the following sample of code. Please note that the Qaptiva QPU does not exist and is used for illustration purpose only

from qat.comm.resource.ttypes import Device, DeviceType

# Requesting the Qaptiva QPU
Device(type=DeviceType.QPU, manufacturer="eviden", model="qaptiva")

Warning

Only QPUs registered to the resource manager are bookable. These QPUs can be defined in the resource manager config file

Once resource granted by the resource manager, method apply_resource_consumption_limits() is called. This method is called if and only if resources have been granted. This function takes a single argument of type AllocationModel, the QPU should look at this object to target the right resource