Backend communication schemas¶
qiskit-cold-atom
and the backends communicate by exchanging data over a REST API.
This tutorial outlines the schemas that are sent to, and received from the backend.
They have the following general steps, which we describe in more detail below.
Obtain the backend configuration through a
GET
request at the endpointget_config
.Post the job to the backend through a
POST
request at the endpointpost_job
.Verify the job status through a
GET
request at the endpointget_job_status
.Obtain the result through a
GET
request at the endpointget_job_result
.
We now discuss each step in more detail.
Backend configuration¶
qiskit-cold-atom
must have some information from the backend to be able to prepare the
quantum circuits that will be run on the backend.
Each backend has a unique URL assigned to it from which the configuration is retrieved.
The cold atom backend is initialized by making a GET
request to the endpoint get_config
, using the python requests
package,
to this URL which retrieves the configuration data of the backend provided as a Json file.
The configuration is created by calling BackendConfiguration.from_dict(r.json())
where
r
is the result from the GET
request.
The experimental setup must therefore return a JSon file that complies with the Qiskit schemas
found in backend_configuration_schema.json
.
The configuration schemas comprise all relevant information needed for working with the backend.
As an instructive example for the structure of this Json file, we show below what a configuration
could look like for the atomic mixture experimental backend.
{
'backend_name': 'atomic_mixtures',
'backend_version': '0.0.1',
'n_qubits': 2, # number of wires
'atomic_species': ['Na', 'Li'],
'basis_gates': ['delay', 'rx'],
'gates': [
{
'name': 'delay',
'parameters': ['tau', 'delta'],
'qasm_def': 'gate delay(tau, delta) {}',
'coupling_map': [[0, 1]],
'description': 'evolution under SCC Hamiltonian for time tau'
},
{
'name': 'rx',
'parameters': ['theta'],
'qasm_def': 'gate rx(theta) {}',
'coupling_map': [[0]],
'description': 'Rotation of the sodium spin'
}
],
'supported_instructions': ['delay', 'rx', 'measure', 'barrier'],
'local': False, # backend is local or remote (as seen from user)
'simulator': False, # backend is a simulator
'conditional': False, # backend supports conditional operations
'open_pulse': False, # backend supports open pulse
'memory': True, # backend supports memory
'max_shots': 60,
'coupling_map': [[0, 1]],
'max_experiments': 3,
'description': 'Setup of an atomic mixtures experiment with one trapping site and two atomic species, namely Na and Li.',
'url': 'http://url_of_the_remote_server',
'credits_required': False,
'online_date': datetime.pyi,
'display_name': str = None
}
We now discuss some of the entries in this JSon file in more detail.
n_qubits
gives the maximum amount of wires (which are Qubit objects in Qiskit, hence the name) that an incoming circuit can have. In this case, we describe the experiment with one wire per atomic species, son_qubits = 2
. In the case ofqiskit-cold-atom
the entryn_qubits
is a misnomer but we use it here to comply to the Qiskit schemas. The fact that it is a misnomer does not impactqiskit-cold-atom
.basis_gates
is a list of the supported instructions. These are defined via theGateConfig
class whose sepcifications are given as a dictionary under the keygates
. Note that in theqasm_def
, the actual specification{}
remains empty. Thecoupling_map
defines on which wires (Qubit
s) these gates can be executed. As therx
gate can only be applied to the sodium wire, itscoupling_map
is[[0]]
.supported_instructions
is a list of the names of all instructions that can be applied to a circuit to be run on this backend. This includes the names of all the gates but also non-unitary instructions such as measurements and barriers.For now we set
open_pulse
toFalse
. This means that the backend will not acceptPulseJob
s which are defined inOpenPulse
. This functionality may be added later to give users pulse-level access to the hardware.The maximum amount of shots for a single quantum circuit in one job is given by
max_shots
. A job can consist of multiple quantum circuits that a user wants to execute. The maximum number of different circuits per job is thus bounded bymax_experiments
.The
url
of the backend gives the network address of the remote server to which the user communicates their job requests.Some backends may wish to manage how many jobs a user can run. This can be done by requesting that users have sufficient credits to run jobs. If credits are required then the
credits_required
keyword is set toTrue
.
Job payload¶
This section describes the information that is sent to the backends to run quantum circuits.
If you want to expose a cold-atomic setup as a Qiskit backend then it must be capable of accepting
these payloads.
The run
method of a backend converts the circuits into a Json serializable dict
and sends it
to the backend url via a POST
request to the post_job
endpoint.
Additional parameters are sent along in the json
dict of the request.
The circuits are converted into the dict
by the circuit_to_cold_atom()
function.
This dict
has the structure shown below.
Each circuit has a unique identifier experiment_id
.
Each circuit is then specified by its data
where inst_name
is a reserved name and takes on
the value of one of the basis instructions supported by the hardware as communicated by its configuration file.
wires
indicates the wires in the circuit that the instruction is applied to.
wires
therefore indicates the trapping site and atomic species to which the instruction is applied.
Finally, params
is the list of parameter values, e.g. a rotation angle, that the instruction takes.
shots
defines the number of times the circuit is repeated.
{
experiment_id(str): {
'instructions': [
(inst_name(str), wires(List[int]), params(List[float])),
],
'shots': int,
'num_wires': int
}
}
As example consider the circuit data below which could be received by the NaLi device backend as a Json file.
The instructions in data show that this circuit is to be run with one trapping site.
An rlx
rotation with angle 0.7 radians is applied to the Na atoms followed by a 20 ms delay.
Finally the Na atom is measured.
{
'experiment_0': {
'instructions': [
('rlx', [0], [0.7]),
('delay', [0, 1], [20]),
('measure', [0], []),
('measure', [1], [])
],
'num_wires': 2,
'shots': 10
}
}
The POST
method of the web API will then handle this request and process it further.
In the case of the atomic mixtures backend the backend should perform the following tasks.
Verify the provided
access_token
. Users will most likely only be allowed to run jobs on the backend if they are registered and therefore have a valid access token.Assigning a unique job ID and placing the job in a job management system. Note that this job management is not done by
qiskit-cold-atom
.Processing the circuit. This includes validation which determines if the input data corresponds to the outlined format and that all parameter values, including wire numbers, are within acceptable ranges. The input JSon data should be processed further. For instance, by converting it into a suitable
experiment.py
file for the control setup and running the experiment. The actual implementation of this is left to the backend’s discretion. An example of such an implementation is the qlued framework.
The response
of this POST
request is sent back to the user as a Json that includes a job_id
.
This unique identification number is created by the backend for each submitted data
file.
The job_id
is subsequently used to define a Job
object which is the central object in Qiskit
created to manage and handle the submitted task.
Result payload¶
To describe job results, Qiskit provides the Result
class which we use without further modifications.
The Json dictionary that is returned when a user queries the backend for the result of his job can be
turned into a qiskit.Result
object via the Result.from_dict()
method.
A minimal configuration of the data returned by the backend is shown below.
# configuration of result dictionary returned by the backend as a Json dictionary.
{
"backend_name": str,
"backend_version": str,
"job_id": str,
"qobj_id": str,
"success": bool,
"header": dict, # must be JSon serializable
"results": list[
{
"header": dict, # must be JSon serializable
"shots": int,
"success": bool,
"meas_return": str,
"meas_level": int, # most likely always 1 or perhaps 0
"data": {
"counts": dict, # must be JSon serializable
"memory": list
}
}
]
}
The actual information about the results of the (possibly multiple) QuantumCircuit
s is
given as dictionaries themselves, which are provided as a list under the results
key.
Each element in this list corresponds to one experiment (i.e. QuantumCircuit
).
For each individual circuit, there are two main ways the results are stored.
The default way is to store the measurement results as a dictionary under the key counts
.
This dictionary groups the different shots by their different measurement outcomes and
simply counts the occurrences of each outcome.
For example for a two qubit circuit with 10 shots this may look like:
"counts": {"00": 3, "01": 1, "10": 4, "11": 2}
This count dictionary is accessible via the Result.get_counts()
method.
For the atomic mixtures we have many more degrees of freedom in the observables.
If the backend supports memory, i.e. "memory":True
in the backend config
,
then the individual measurement outcomes of each shot are returned as a list under
the "memory"
key and can be accessed through Result.get_memory
.
This is more appropriate for the cold atom experiments.
The get_memory
function implemented in Qiskit Result
can return the memory
for a specific experiment if given an index or experiment name as argument.
The format of the list returned by memory depends on the meas_return
type.
If single-shots are returned the dimension of the memory is the number of shots
times the number of memory slots.
Each wire has one memory slot.
Each entry in the memory is specified as a list of two numbers.
For the cold atom backend the first number will represent the number of atoms in the
spin-up state while the second number will be the number of atoms in the spin down state.
When result.get_memory()
is called these two numbers are returned as a single complex number.
This formatting is a result of the IQ plane description of superconducting qubits.
An example of a result is shown below.
If averaged results are returned the memory has one dimension less as the shots are averaged.
# Example of a result returned by the NaLi device backend as a Json dictionary.
# The result has one experiment (namely experiment_0 which matches the name above)
# with three shots and two wires (one for Na and one for Li).
# In the first shot there are 90012 Na atoms in the spin-up state and 9988 in the spin-down state.
{
"backend_name": "atomic_mixtures_device",
"backend_version": "0.0.1",
"job_id": "dae51c52-5caa-11eb-b265-080027f905c2",
"qobj_id": None,
"success": True,
"header": {},
"results": list[
{
"header": {"name": "experiment_0", "extra metadata": "text"},
"shots": 3,
"success": True,
"meas_return": "single",
"meas_level": 1,
"data": { # slot 1 (Na) # slot 2 (Li)
"memory": [[[90012., 9988.], [5100., 4900.]], # Shot 1
[[89900., 10100.], [5000., 5000.]], # Shot 2
[[90000., 10000.], [5050., 4950.]]] # Shot 3
}
}
]
}
# Part of the data above modified to the
# case where average results are returned to the user.
"shots": 3,
"meas_return": "avg",
"meas_level": 1,
"data": { # slot 1 (Na) # slot 2 (Li)
"memory": [[89971., 10029.], [5050., 4950]] # Average of three shots
}
The meas_level
and meas_return
(optional) keys indicate what kind of data is returned.
Finally, depending on the backend, instead of counts
or memory
, the dictionary of
the "data"
key can also include statevector
, unitary
or snapshot
keys,
which add further flexibility to the datatypes that can be returned in a result object.