Access systems with your account

This is an overview of how to use your IBM Quantum account to access the systems and simulators available in IBM Quantum. Additionally, it will detail how to interact with, and retrieve, jobs associated with executing circuits on these backends.

In this tutorial, we will review the core components of Qiskit’s base backend framework, using the IBM Quantum account as an example.

The interface has four main components: the account, providers, backends, and jobs:

  • account: Gives access to one or more ‘providers’ based on the account’s permissions.

  • provider: Provides access to quantum devices and simulators, collectively called ‘backends’, and additional services tailored to a specific backend instance.

  • backend: A quantum device or simulator capable of running quantum circuits or pulse schedules.

  • job: A local reference to a collection of quantum circuits or pulse schedules submitted to a given backend.

The Account

The Qiskit IBMQ account object is the local reference for accessing your IBM Quantum account, and all of the providers, backends, etc., that are available to you.

The IBM Quantum account has functions for handling administrative tasks. The credentials can be saved to disk, or used in a session and never saved.

  • enable_account(TOKEN, HUB, GROUP, PROJECT): Enable your account in the current session and optionally specify a default provider to return.

  • save_account(TOKEN, HUB, GROUP, PROJECT): Save your account to disk for future use, and optionally specify a default provider to return when loading your account.

  • load_account(): Load account using stored credentials.

  • disable_account(): Disable your account in the current session.

  • stored_account(): List the account stored to disk.

  • active_account(): List the account currently in the session.

  • delete_account(): Delete the saved account from disk.

A provider offers access to quantum systems, simulators, and additional services. To see all the providers available with your IBM Quantum account:

from qiskit import IBMQ

# IBMQ.save_account(TOKEN)
IBMQ.load_account() # Load account from disk
IBMQ.providers()    # List all available providers
from qiskit import IBMQ

# IBMQ.save_account(TOKEN)
IBMQ.load_account() # Load account from disk
IBMQ.providers()    # List all available providers

where we have assumed that the user has stored their IBM Quantum account information locally ahead of time using IBMQ.save_account(TOKEN). TOKEN here is the API token you obtain from your IBM Quantum account.

Note: The use of provider instances is the default way of retrieving backends from Qiskit 0.11 onwards - if you have been using earlier versions of Qiskit, check the “Updating from previous versions” section for more detailed instructions on updating and using the different options.

The above example shows two different providers. All IBMQ providers are specified by a hub, group, and project. The provider given by hub='ibm-q', group='open', project='main' is the provider that gives access to the public IBM Quantum devices available to all IBM Quantum users. The second is an example of a provider that is only unlocked for a specific set of users. Members of the IBM Quantum Network may see one or more providers (with names different than those shown above) depending on the access level granted to them.

To access a given provider one should use the get_provider() method of the IBMQ account, filtering by hub, group, or project:

IBMQ.get_provider(hub='ibm-q')
IBMQ.get_provider(group='open')

Finally, as a convenience, calling IBMQ.load_account() or IBMQ.enable_account() will return the default public provider instance <AccountProvider for IBMQ(hub='ibm-q', group='open', project='main')>.

The Provider

All providers inherit from qiskit.providers.BaseProvider and implement the methods:

  • backends(): Returns all backend objects known to the provider.

  • get_backend(NAME): Returns the named backend.

Providers associated via your IBM Quantum account provide access to a collection of different backends, such as the open-access offerings, or the premium access offered through the IBM Quantum Network.

Using the public provider instance from above:

provider = IBMQ.get_provider(hub='ibm-q')
provider.backends()

Selecting a backend is done by name using the get_backend(NAME) method:

backend = provider.get_backend('ibmq_vigo')
backend

Filtering the Backends

You may also optionally filter the set of returned backends, by passing arguments that query the backend’s configuration, status, or properties. The filters are passed by conditions and, for more general filters, you can make advanced functions using a lambda function.

As a first example, let’s return only those backends that are real quantum devices, and that are currently operational:

provider.backends(simulator=False, operational=True)

Or, only those backends that are real devices, have more than 10 qubits, and are operational:

provider.backends(filters=lambda x: x.configuration().n_qubits >= 10
                                    and not x.configuration().simulator
                                    and x.status().operational==True)

Lastly, show the least-busy five-qubit device (in terms of the number of jobs pending in the queue):

from qiskit.providers.ibmq import least_busy

small_devices = provider.backends(filters=lambda x: x.configuration().n_qubits == 5
                                   and not x.configuration().simulator)
least_busy(small_devices)

The above filters can be combined as desired.

Backends

Backends represent either a simulator or a real quantum computer, and are responsible for running quantum circuits and/or pulse schedules and returning results. They have a run method which takes in a qobj as input, the Qiskit API serialization format, and returns a BaseJob object. This object allows asynchronous running of jobs for retrieving results from a backend when the job is completed.

At a minimum, backends use the following methods, inherited from qiskit.providers.BaseBackend:

  • provider(): Returns the provider of the backend.

  • name(): Returns the name of the backend.

  • status(): Returns the current status of the backend.

  • configuration(): Returns the backend configuration.

  • properties(): Returns the backend properties.

  • run(QOBJ, **kwargs): Runs a qobj on the backend.

The qiskit-ibmq-provider’s implementation of BaseBackend is IBMQBackend. It accepts additional parameters to the run() method:

  • job_name: Custom name to be assigned to the job.

  • job_share_level: Allows sharing the job at different level.

  • job_tags: Tags to be assigned to the job.

And supports additional methods:

  • jobs(): Returns a list of previous jobs executed on this backend through the current provider instance.

  • retrieve_job(JOB_ID): Returns a job by its job ID.

  • defaults(): Gives a data structure of typical default parameters, if applicable.

  • job_limit(): Returns the job limit for the backend.

  • remaining_jobs_count(): Returns the number of remaining jobs that could be submitted to the backend.

  • active_jobs(): Returns a list of unfinished jobs.

Refer to the IBMQBackend documentation for a complete list of methods.

Let’s load up the least-busy backend from the small_devices filtered above:

backend = least_busy(small_devices)

Some examples using the different methods:

backend.provider()
backend.name()
backend.status()

Here we see the name of the backend and the software version it is running, along with its operational status, number of jobs pending in the backends queue, and a more detailed status message.

Next we look at the backend configuration and properties:

backend.configuration()

The backend configuration provides some useful information via its attributes, such as basis_gates, coupling_map, max_experiments, max_shots, quantum_volume, and simulator.

The backend properties contain data that was measured and reported. Let’s see what kind of information is reported for qubit 0.

props = backend.properties()

def describe_qubit(qubit, properties):
    """Print a string describing some of reported properties of the given qubit."""

    # Conversion factors from standard SI units
    us = 1e6
    ns = 1e9
    GHz = 1e-9

    print("Qubit {0} has a \n"
          "  - T1 time of {1} microseconds\n"
          "  - T2 time of {2} microseconds\n"
          "  - SX gate error of {3}\n"
          "  - SX gate duration of {4} nanoseconds\n"
          "  - resonant frequency of {5} GHz".format(
              qubit,
              properties.t1(qubit) * us,
              properties.t2(qubit) * us,
              properties.gate_error('sx', qubit),
              properties.gate_length('sx', qubit) * ns,
              properties.frequency(qubit) * GHz))

describe_qubit(0, props)

To see the last five jobs run on this backend:

ran_job = None
for ran_job in backend.jobs(limit=5):
    print(str(ran_job.job_id()) + " " + str(ran_job.status()))

A job can be retrieved using the retrieve_job(JOB_ID) method:

if ran_job is not None:
    job = backend.retrieve_job(ran_job.job_id())

Backend Service

qiskit-ibmq-provider version 0.4 (Qiskit version 0.14) introduced a new class IBMQBackendService. It provides generic backend related services for a provider without requiring a particular backend as input. The main methods it supports are:

  • jobs(): Returns a list of previously submitted jobs through the current provider instance.

  • retrieve_job(JOB_ID): Returns a job by its job ID.

The backend service is defined as the backends attribute of a provider. All of the backends available to this provider are also attributes of the backend service, allowing the backend names to be autocompleted:

provider.backends.ibmq_vigo

To see the last five jobs submitted through this provider, regardless of which backend they ran on:

for ran_job in provider.backends.jobs(limit=5):
    print(str(ran_job.job_id()) + " " + str(ran_job.status()))

To retrieve a particular job:

if ran_job is not None:
    job = provider.backends.retrieve_job(ran_job.job_id())

Jobs

Job instances can be thought of as the “ticket” for a submitted job. They find out the execution state at a given point in time (for example, if the job is queued, running, or has failed), and also allow control over the job. They have the following methods:

  • status(): Returns the status of the job.

  • backend(): Returns the backend the job was run on.

  • job_id(): Gets the job_id.

  • cancel(): Cancels the job.

  • result(): Gets the results from the circuit run.

Some of the methods that are only available to “IBM Q Job” (IBMQJob) include:

  • creation_date(): Gives the date at which the job was created.

  • queue_info(): Returns queue information for this job, including queue position, estimated start and end time, and dynamic priorities for the hub, group, and project.

  • error_message(): The error message of failed jobs, if any.

  • name(): Returns the name assigned to this job.

  • properties(): Returns the backend properties for this job.

  • time_per_step(): Returns the time spent for each step (job creation, validation, etc).

Refer to the IBMQJob documentation for a complete list of methods.

Now some examples. Let’s start with submitting a job:

from qiskit import *
from qiskit.compiler import transpile, assemble
backend = provider.get_backend('ibmq_qasm_simulator')
qr = QuantumRegister(3)
cr = ClassicalRegister(3)
circuit = QuantumCircuit(qr, cr)
circuit.x(qr[0])
circuit.x(qr[1])
circuit.ccx(qr[0], qr[1], qr[2])
circuit.cx(qr[0], qr[1])
circuit.measure(qr, cr)

To pass this circuit to the backend, we must first map it onto the backend, package it, and send to the device. This is all done for you by the execute function:

#job = execute(circuit, backend)

Alternatively, you can map the circuit yourself using the transpile function, package it using assemble, and then send it from the backend instance itself:

mapped_circuit = transpile(circuit, backend=backend)
qobj = assemble(mapped_circuit, backend=backend, shots=1024)
job = backend.run(qobj)

The status() method returns the job status and a message:

job.status()

To get a backend object from the job, use the backend() method:

backend_temp = job.backend()
backend_temp

To get the job_id use the job_id() method:

job.job_id()

To get the result from the job, use the result() method:

result = job.result()
counts = result.get_counts()
print(counts)

If you want to check the creation date, use creation_date():

job.creation_date()

Job Manager

The Job Manager is another convenience function provided by qiskit-ibmq-provider, available in version 0.4 (Qiskit version 0.14) and up. It splits experiments into multiple jobs based on backend restrictions. When the jobs are finished, it collects and presents the results in a unified view.

IBMQJobManager has the following methods: - run(): Execute a set of circuits or pulse schedules. - report(): Return a report on the statuses of all jobs managed by this manager. - job_sets(): Return a list of managed job sets matching the specified filtering.

You can run multiple sets of experiments by invoking job_manager.run() multiple times, and each call will return a ManagedJobSet instance. The ManagedJobSet methods allow you to apply operations on this set of jobs as a whole and are similiar to those of the Job class: - statuses(): Return the status of each job. - cancel(): Cancel all jobs in this set. - results(): Return the results of the jobs. - error_messages(): Provide details about job failures. - jobs(): Return a list of submitted jobs. - report(): Return a report on current job statuses.

Now let’s see some examples. Say you want to submit a bunch of circuits to a backend:

from qiskit.providers.ibmq.managed import IBMQJobManager

sim_backend = provider.get_backend('ibmq_qasm_simulator')
circs = transpile([circuit]*20, backend=sim_backend)

# Submit them all to the backend
job_manager = IBMQJobManager()
job_set = job_manager.run(circs, backend=sim_backend, name='foo')

You can use the report() method to inquire the statuses of the jobs:

print(job_set.report())

job_set above is not very interesting, as it only contains a single job. To force multiple jobs without making this tutorial too slow, we can use the max_experiments_per_job parameter:

# Restrict 2 experiments per job.
job_set_multiple = job_manager.run(circs, backend=sim_backend, name='bar', max_experiments_per_job=2)
print(job_set_multiple.report())

As you can see, job_set_multiple contains multiple jobs. To collect the results of all jobs, you can use the results() method, which returns a ManagedResults instance. ManagedResults supports the same methods as Result, which can be used to retrieve the ouputs of individual experiments:

results = job_set_multiple.results()
print("This is the histogram data for experiment 1: {}".format(results.get_counts(1)))
print("This is the histogram data for experiment 2: {}".format(results.get_counts(2)))

Similar to a job, each job set is assigned a unique ID. The job set ID along with the provider object can be used to retrieve a previously submitted job set:

job_set_id = job_set_multiple.job_set_id()
retrieved_job_set = job_manager.retrieve_job_set(job_set_id=job_set_id, provider=provider)
print(retrieved_job_set.report())

If you want to manipulate the jobs within a job set directly, you can use the jobs() method to retrive them. Note that an entry is None if that particular job could not be submitted:

jobs = job_set_multiple.jobs()  # Get a list of all jobs in the set.
job0 = jobs[0]
if job0 is None:
    print("Job0 submit failed!")