Submitting and managing tasks

Once authenticated, you can submit a task to the system to add your quantum program to the queue of your chosen Quantum Processing Unit (QPU). You can see a list of QPUs available to you at https://cloud.oqc.app/qpu. You will need the QPU ID for your chose QPU to submit tasks to.

Language specifications

Programs can be submitted in any of the following languages:

Task creation

A quantum program must be transformed into a QPUTask object before you submit it to the cloud. See the following example for the quantum Hello World written in OPENQasm2.0. You can find more about this circuit and the family of circuits it belongs to in the Qiskit textbook.

from qcaas_client.client import OQCClient, QPUTask

client = OQCClient(
url = <oqc_cloud_url>,
authentication_token = <access_token>
)

hello_world = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q;
cx q[0], q[1];
measure q->c;
"""

task = QPUTask(program=hello_world)

task_results = client.schedule_tasks(task,qpu_id=<qpu_id>)

Compiler and runtime configuration

You can include configuration settings when generating the QPUTask object. This allows you to tailor the processing of your program on the QPU. We encourage you to experiment with the different compiler and runtime configuration settings as introduced below (for more information, see https://github.com/oqc-community/qat).

The task configuration is represented by the CompilerConfig object, which contains the following fields:

  • repeats. The number of times the circuit is run during this task, sometimes referred to as shots. The hardware can only store 10000 shots at a time, but you can submit many more than that with OQC Cloud then automatically batching the shots. The default value is 1000.

  • repetition_period. The time that the hardware waits between shots. Our QPU does not have an active reset so it is necessary to provide time to allow the system to relax to reinitialise to a simple fiducial state. The default value is 200e-6 seconds. Note: we strongly recommended that you do not provide a custom value unless you have a good understanding of the potential impact.

  • results_format. The format of the returned results. This can be passed in as a QuantumResultsFormat object. There are two settings: Raw or Binary.

    • Binary. This discriminates the raw values into a 0 or a 1. This can be set using QuantumResultsFormat().binary_count().

    • Raw. This will output the absolute magnitude of the averaged IQ values as measured at readout. This can be set using res_format = QuantumResultsFormat().raw().

  • metrics. The type of metrics that you would like to be recorded (e.g. optimised circuit and its instruction count, different processing times). The default is that all metrics are returned. This can be set using the MetricsType object such as MetricsType.OptimizedCircuit to return the optimised circuit. Default value is MetricsType.Default (i.e. all metrics are returned)

  • optimisations. The optimizations you want to be performed on the input program. The compiler carries out a number of optimisation passes on the program using Qiskit or Tket, with the tKet optimisation compiler integrated into our own compiler pipeline. There are three levels of optimisation offered:

    • 0 (minimum in code): Routes the qubits for the QPU architecture and converts all gates to TK1 and CNOT.

    • 1: Applies Synthesise Tket, a computationally efficient optimisation

    • 2: Applies Full peephole optimise, a more extensive optimisation

    Note that:

    • Levels 1 and 2 squash one qubit rotations (e.g. the gate sequence XZZX would evaluate to the identity operator I).

    The default values are:

    • For QASM2.0 programs, TketOptimizations.One is applied.

    • For QIR programs, no optimisations are applied.

Here, we provide a summary of the different optimisations which you may want to utilise, along with code examples.

# Some standard optimisations:

# In the case you only want to map the qubits onto the hardware
# and convert the gates into native gates
optimisations = Tket(TketOptimizations.One)

# To attain maximum optimisations without consideration for
# hardware specifications
optimisations = Tket(TketOptimizations.Two)

# Optimisation settings are switched off by default, but this can
# be done explicitly
optimisations = Tket()
optimisations.disable()

# The following examples are some notable settings:

# Do nothing
optimisations = Tket(TketOptimizations.Empty)

# Map your circuit onto the physical topology of our system
optimisations = Tket(TketOptimizations.DefaultMappingPass)

# Maximum optimisation, classically inspired by full peephole
# compiler pass
optimisations = Tket(TketOptimizations.FullPeepholeOptimise)

# Perform some context specific simplifications (e.g. we know
# we start in the 0 state, so Z gate has no effect)
optimisations = Tket(TketOptimizations.ContextSimp)

# Compiles the two qubit gates to conform to the directions
# allowed by our hardware
optimisations = Tket(TketOptimizations.DirectionalCXGates)

User-defined configuration

Here is a complete example of a user-defined CompilerConfig object and its inclusion in the generation of a QPUTask object.

# Setup a customised compiler configuration

shot_count = 1000
rep_period = 90e-6
res_format = QuantumResultsFormat().binary_count()
allowed_metrics = MetricsType.OptimizedInstructionCount
optim = Tket()
optim.tket_optimizations = TketOptimizations.DefaultMappingPass

custom_config = CompilerConfig(repeats = shot_count,
                        repetition_period = rep_period,
                        results_format = res_format,
                        metrics = allowed_metrics,
                        optimizations = optim)

Experimental compiler configurations

As part of experimental features, we have two different readout mitigations, namely matrix mitigation and linear mitigation.

For a deep dive into the theory of matrix mitigation the qiskit textbook includes a notebook going through this:

Note:
  • MatrixMitigation has an exponential calibration and runtime overhead and is only potentially available for the Lucy generation of QPU.

  • Only LinearMitigation is enabled now for Toshiko devices. We will update when other options are available.

Matrix mitigation makes use of C_output the counts obtained from the hardware, C_perfect the ideal noiseless result, and M a noisy transformation matrix associated with readout errors. Then Matrix mitigation starts by noting C_output = M*C_perfect, where M is the transfer matrix that takes us away from the theoretical perfect results to C_output. Multiplying both sides by the inverse of M, M^{-1}, gets us, M^{-1}*C_output = C_perfect. We can compute the elements of M_{i,j} = p(i|j), where i is the measured bit string from the input bitstring, j. Note that this is modelled as a purely classical process that we can undo with post-processing

LinearMitigation is a simplification of the above that requires constant number of circuits for calibration. We measure the “all zero” state and “all one” state. From this, for each qubit we are able to extract p(0/1|0/1). We can then for each measured bitstring on each qubit invert the measured bitstring up to a first order correction. This is a much simplified version of the full MatrixMitigation and will only correct for uncorrelated readout errors. Here is a complete example of a user-customised CompilerConfig object and its inclusion in the generation of a QPUTask object.

# To use readout mitigation,
# setup a customised compiler configuration with below results_format and error_mitigation
custom_config = CompilerConfig(
                        results_format=QuantumResultsFormat().binary_count(),
                        error_mitigation=ErrorMitigationConfig.LinearMitigation)

Task submission

When the QPUTask object has been created, you can then be submit it to the system. A list of QPUTasks can also be submitted. For this, there are two modes of execution available using the Client.

  • Scheduling. This submits a task and then returns a list of the QPUTask that have been submitted, including their task ids. The task id of a task can then be used to monitor the task, and to fetch the results upon completion.

task = QPUTask(program = hello_world, config = custom_config)

# schedule_tasks function returns a list of QPUTasks. The following
# code shows how to access the task_id for the first task for
# monitoring purposes (refer to the Task monitoring section)

task_id = client.schedule_tasks(task,qpu_id=<qpu_id>)[0].task_id
task_results = client.get_task_results(task_id,qpu_id=<qpu_id>)
  • Execution. This will submit a task and then continue to monitor the state of the task until a result is returned. Typically, you would only use this feature when you know that a job will be processed promptly (i.e. the job is submitted to an active window) or that a long wait time for the task to complete is acceptable to you.

task = QPUTask(program = hello_world, config = custom_config)
task_results = client.execute_tasks(task, qpu_id=<qpu_id>)

Task cancellation

If a task has not completed or is currently running, you can cancel it. Either a single task or a list of tasks can be cancelled at once. Note, a task cannot be cancelled once it has started.

client.cancel_task(task_id,qpu_id=<qpu_id>) # To cancel one task
client.cancel_task([task1_id, task2_id, task3_id, ...],qpu_id=<qpu_id>) # To cancel many tasks

Task monitoring

You can monitor the status of your submitted tasks at any time. The following examples show how you can track the progress of tasks, and the form of expected responses.

Task timings

The get_task_timings method returns the timings of events a task undergoes as it progresses through OQC Cloud. The events are described in Table 1.

Table 1: Event descriptions

Event

Description

SERVER_RECEIVED

The server has received the task

SERVER_ENQUEUED

The server has pushed the task to our task queue system

COMPILER_DEQUEUED

The compiler, which generates QPU specific instructions for the submitted program using the Quantum Assembly Toolchain (QAT), has pulled the task from the task queue system

COMPILER_ENQUEUED

The compiler has pushed the task to our task queue system with the compiled instructions.

EXECUTOR_DEQUEUED

The executor, which executes the task on the QPU and performs post-porcessing using QAT, has pulled the task from the task queue system

EXECUTOR_ENQUEUED

The executor has pushed the completed task to the task queue system

SERVER_DEQUEUED

The server has pulled from the task queue system queue and stores the results

The command and expected output is presented in the following code.

command: client.get_task_timings(task_id,qpu_id=<qpu_id>)
example response:
{'COMPILER_DEQUEUED': '2024-07-11 09:56:08.074425+00:00',
 'COMPILER_ENQUEUED': '2024-07-11 09:56:09.099431+00:00',
 'EXECUTOR_DEQUEUED': '2024-07-11 09:56:09.574102+00:00',
 'EXECUTOR_ENQUEUED': '2024-07-11 09:56:10.829953+00:00',
 'SERVER_DEQUEUED': '2024-07-11 09:56:11.590405+00:00',
 'SERVER_ENQUEUED': '2024-07-11 09:55:58.327529+00:00',
 'SERVER_RECEIVED': '2024-07-11 09:55:58.268346+00:00'}

Note: all times are in UTC

Task status

The get_task_status function allows you to obtain the status of your task. The possible statuses are described in Table 2.

Table 2: Status descriptions

Status

Description

CREATED

The task has been created and has entered our system

SUBMITTED

The task has been submitted to our task queue system

RUNNING

The task is undergoing compilation and execution on the QPU

COMPLETED

The task has been successfully completed

FAILED

The task has failed to complete. In this event, error information will be available (refer to Task error for more information on how to handle failed tasks)

CANCELLED

The task has been cancelled (refer to the Task cancellation for more details on how to cancel tasks)

UNKNOWN

In the event that a task has this status, you should resubmit the task. If this is repeated, contact OQC Cloud support

EXPIRED

The task is older than 7 days and has been purged from the system

The command and expected output is presented in the following code

command: client.get_task_status(task_id,qpu_id=<qpu_id>)
example response: COMPLETED

Task metrics

The metrics recorded for a given task can be defined in the compiler configuration. In the following, the command and expected output is presented for the case the optimised circuit and instruction count are recorded.

command: client.get_task_metrics(task_id,qpu_id=<qpu_id>)
example response: {'optimized_circuit': '\nOPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\nh q;\ncreg
c[2];\nmeasure q->c;\n', 'optimized_instruction_count': 50}

Task metadata

This function will return the task metadata which includes the compiler configuration and the task creation time.

command: client.get_task_metadata(task_id,qpu_id=<qpu_id>)
example response: {'config': '{"$type": "<class \'qcaas_client.compiler_config.CompilerConfig\'>", "$data": {"repeats": 1000,
"repetition_period": 2e-05, "results_format": {"$type": "<class \'qcaas_client.compiler_config.QuantumResultsFormat\'>",
"$data": {"format": {"$type": "<enum \'qcaas_client.compiler_config.InlineResultsProcessing\'>", "$value": 1}, "transforms":
{"$type": "<enum \'qcaas_client.compiler_config.ResultsFormatting\'>", "$value": 3}}}, "metrics": {"$type":
"<enum \'qcaas_client.compiler_config.MetricsType\'>", "$value": 4}, "active_calibrations": [], "optimizations": {"$type":
"<enum \'qcaas_client.compiler_config.TketOptimizations\'>", "$value": 2}}}', 'created_at': 'Tue, 17 Oct 2023 11:24:35 GMT',
'id': '0f41e535-222c-4ce2-ade0-1a4c57ad53d8'}

Task results

If the task has completed, you can obtain the results of their task using the following code. In this example, the results are in attribute ‘c’ as defined in your output measure q->c;

command: client.get_task_results(task_id,qpu_id=<qpu_id>).result
example response: {'c': {'00': 1000}}

Task error

If the task has failed, you can obtain information about the error using the following code.

command: client.get_task_errors(task_id,qpu_id=<qpu_id>).error_code
example response: 101
command: client.get_task_errors(task_id,qpu_id=<qpu_id>).error_message
example response: local variable 'decoded_program' referenced before assignment

Execution estimation

You can obtain execution estimates for tasks already submitted or for tasks you intend to submit.

Task execution estimates

If you want to know when your submitted tasks are likely to be executed, you can supply a list of the task ids you would like to query. For each valid task id, you will receive an estimated start time, queue position, and a list of upcoming windows.

command: client.get_task_execution_estimates([task_id_1],qpu_id=<qpu_id>)
example response:
{
    'task_wait_times':
        [
            {
                'estimated_start_time': '2023-08-16T11:18:01.680Z',
                'position_in_queue': 10,
                'task_id': '3fa85f64-5717-4562-b3fc-2c963f66afa6',
                'timestamp': '2023-08-16T11:18:01.680Z',
                'windows':
                    [
                        {
                            'end_time': '2023-08-16T11:18:01.680Z',
                            'start_time': '2023-08-16T11:18:01.680Z',
                            'window_description': 'CURRENT'
                        }
                    ]
            }
        ]
}