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
GETrequest at the endpointget_config.Post the job to the backend through a
POSTrequest at the endpointpost_job.Verify the job status through a
GETrequest at the endpointget_job_status.Obtain the result through a
GETrequest 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_qubitsgives 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-atomthe entryn_qubitsis 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_gatesis a list of the supported instructions. These are defined via theGateConfigclass whose sepcifications are given as a dictionary under the keygates. Note that in theqasm_def, the actual specification{}remains empty. Thecoupling_mapdefines on which wires (Qubits) these gates can be executed. As therxgate can only be applied to the sodium wire, itscoupling_mapis[[0]].supported_instructionsis 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_pulsetoFalse. This means that the backend will not acceptPulseJobs 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
urlof 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_requiredkeyword 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.pyfile 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.