Introduction to Qiskit with Q5

Q5 is a 5 qubit Quantum Computer that is co-developed by VTT and IQM. It uses superconducting transmon qubits in a star shaped topology. Q5’s natives gates consist of the phased-rx and controlled-z gates. This architecture is called Adonis by IQM.

In this tutorial running on Q5 is demonstrated using the Qiskit framework. You can also run on Q5 using Cirq with cirq-on-iqm adapter, and this is described in a separate notebook.

Setup

This notebook uses the following requirements for running on Q5. Always verify you’re running the correct version for the quantum computer you’re using!

qiskit-iqm==15.6
iqm-client==20.17
qiskit[visualization]
pylatexenc
networkx

Using Q5 with Qiskit

First we import qiskit-on-iqm which is needed to run on Q5 with qiskit. You can read the user guide here.

import os

import networkx as nx
from iqm.qiskit_iqm import IQMProvider
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
from qiskit import QuantumCircuit, QuantumRegister, transpile
from qiskit.visualization import plot_histogram

Then connection to the backend is simple! For this we point the IQMProvider at the quantum computer’s URL. In this example we use the Q5’s URL. We also set the access token in the IQM_TOKEN environment variable.

os.environ["IQM_TOKEN"] = "<TOKEN>"
provider = IQMProvider("https://qx.vtt.fi/api/devices/q5")
backend = provider.get_backend()

Now that we have the backend connected to the quantum computer, let’s print out some information about Q5!

print(f"Native operations: {backend.operation_names}")
print(f"Number of qubits: {backend.num_qubits}")
print(f"Coupling map: {backend.coupling_map}")

Visualising the topology with networkx:

G = nx.Graph()
G.add_edges_from(backend.coupling_map)
node_labels = {node: f"QB{node + 1}" for node in G.nodes}
nx.draw(G, labels=node_labels, node_color="skyblue", node_size=500, font_size=10)

Constructing and executing quantum circuits

Circuits are constructed and submitted to Q5 using the same methods as with IBM machines. First we construct a Bell pair circuit between 2 qubits. The circuit is then executed on the backend using the backend.run function.

circuit = QuantumCircuit(2, name="Bell pair circuit")
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
circuit.draw(output="mpl")

Executing the circuit on Q5

We now transpile the circuit to run on Q5. This will rewrite the circuit to take into account it’s topology and native gate set. We can also view the transpiled circuit before running it:

transpiled_circuit = transpile(circuit, backend)
transpiled_circuit.draw(output="mpl")

We can now submit the job to run. When submitting a job to Q5 a unique identifier for your job is returned. This can be used to gather additional information about the circuit you just submitted and the results. You should save your job ids!

backend.set_options(shots=100)
job = backend.run(transpiled_circuit)
job = backend.run(transpiled_circuit)
print(f"Job ID: {job.job_id()}.")

Viewing the results

Results can be printed once the job has completed. If results are queried before the job has completed then an error will be returned.

result = job.result()
print(result.job_id)  # The job id can be queried from the result
print(result.get_counts())
# print(result.get_memory())

plot_histogram(result.get_counts())

You can also specify some backend arguments for running on Q5. These are explained in the qiskit-on-iqm documentation.

The IQM backend always corresponds to a calibration set, so that transpiling using this backend will only use loci defined in the calibration_set_id. The server default calibration set will be used unless one is provided while instantiating the backend.

The Various Circuit Compilation options that can be set while running a circuit can be found here iqm-client documentation

from iqm.iqm_client import CircuitCompilationOptions
from iqm.iqm_client.models import HeraldingMode

backend = provider.get_backend(
    calibration_set_id="35fa13d1-17db-4bd2-9b14-203ce32f0551"
)

transpiled_circuit = transpile(circuit, backend)
transpiled_circuit.draw("mpl")

job2 = backend.run(
    transpiled_circuit,
    shots=1000,
    circuit_compilation_options=CircuitCompilationOptions(
        max_circuit_duration_over_t2=None, heralding_mode=HeraldingMode.ZEROS
    ),
)

After submitting, the job is now running. The status of the job can be queried using job.status(). Using the job id, you can retrieve previous jobs.

print(job2.job_id())
plot_histogram(job2.result().get_counts())

Additional metadata about the executed job can also be found.

result = job.result()
exp_result = result._get_experiment(transpiled_circuit)
print("Job ID: ", job.job_id())  # Retrieving the submitted job id
print(result.request.circuits)  # Retrieving the circuit request sent
print(
    "Calibration Set ID: ", exp_result.calibration_set_id
)  # Retrieving the current calibration set id.
print(result.request.qubit_mapping)  # Retrieving the qubit mapping
print(result.request.shots)  # Retrieving the number of requested shots.
print(exp_result.header)

Explicit Transpilation

For more control, you can also specify the initial layout in the transpile function. For example, Q%’s topology only allows 2 qubit gates between the central and outer qubits. Therefore we can map the 2 qubit gate to QB3. For this we make use of the QuantumRegister.

qreg = QuantumRegister(2, "QB")
circuit = QuantumCircuit(qreg, name="Bell pair circuit")
circuit.h(qreg[0])
circuit.cx(qreg[0], qreg[1])
circuit.measure_all()

# Qubit numbers start at 0 index whereas the qubit names start at 1 index.
qubit_mapping = {
    qreg[0]: backend.qubit_name_to_index("QB1"),
    qreg[1]: backend.qubit_name_to_index("QB2"),
}

transpiled_circuit = transpile(circuit, backend, initial_layout=qubit_mapping)
job = backend.run(transpiled_circuit)

Qiskit refers to qubits using integer indices, whereas IQM uses strings. The backend class provides utility methods for mapping them to one another. Let’s see on which physical qubits the logical circuit qubits were mapped.

mapping = {}
for qubit in circuit.qubits:
    index = circuit.find_bit(qubit).index
    mapping[index] = backend.index_to_qubit_name(index)

print(mapping)

Optimizing circuits for Q5

Qiskit on IQM provides the option to optimize your transpiled quantum circuits for running on Q5. Currently the optimization uses Qiskit’s transpiler passes to reduce the number of single qubit gates in the quantum circuit, thus reducing the total circuit depth. Further information can be found in the IQM qiskit documentation.

Here we optimize the previous instance of transpiled_circuit as the optimize_single_qubit_gates expected a transpiled circuit as an argument.

circuit_optimized = optimize_single_qubit_gates(transpiled_circuit)
circuit_optimized.draw("mpl")

Simulating circuits locally with noise

Qiskit on IQM provides an IQMFakeBackend with IQMFakeAdonis for simulating Q5.

from iqm.qiskit_iqm import IQMFakeAdonis

fake_backend = IQMFakeAdonis()
transpiled_job = transpile(circuit, fake_backend)
job = fake_backend.run(circuit, shots=1000)
print(job.result().get_counts())
plot_histogram(job.result().get_counts())

The error profile of the noise can be queried and customised by the user following the qiskit-iqm user guide.

print(fake_backend.error_profile)
error_profile = fake_backend.error_profile
error_profile.t1s["QB1"] = 38940.0  # in ns 3.8940
error_profile.t1s["QB2"] = 25127.0  # in ns
error_profile.t1s["QB3"] = 43322.0  # in ns
error_profile.t1s["QB4"] = 38223.0  # in ns
error_profile.t1s["QB5"] = 37365.0  # in ns

error_profile.t2s["QB1"] = 24785.0  # in ns
error_profile.t2s["QB2"] = 20751.0  # in ns
error_profile.t2s["QB3"] = 10050.0  # in ns
error_profile.t2s["QB4"] = 14391.0  # in ns
error_profile.t2s["QB5"] = 29012.0  # in ns

error_profile.single_qubit_gate_depolarizing_error_parameters["prx"]["QB1"] = 0.0043
error_profile.single_qubit_gate_depolarizing_error_parameters["prx"]["QB2"] = 0.0018
error_profile.single_qubit_gate_depolarizing_error_parameters["prx"]["QB3"] = 0.0022
error_profile.single_qubit_gate_depolarizing_error_parameters["prx"]["QB4"] = 0.0037
error_profile.single_qubit_gate_depolarizing_error_parameters["prx"]["QB5"] = 0.0024

error_profile.two_qubit_gate_depolarizing_error_parameters["cz"][("QB1", "QB3")] = 0.018
error_profile.two_qubit_gate_depolarizing_error_parameters["cz"][("QB2", "QB3")] = 0.033
error_profile.two_qubit_gate_depolarizing_error_parameters["cz"][("QB3", "QB4")] = 0.030
error_profile.two_qubit_gate_depolarizing_error_parameters["cz"][("QB3", "QB5")] = 0.017

error_profile.single_qubit_gate_durations["prx"] = 120  # in ns
error_profile.two_qubit_gate_durations["cz"] = 120  # in ns

error_profile.readout_errors["QB1"]["0"] = 0.03375
error_profile.readout_errors["QB1"]["1"] = 0.03865
error_profile.readout_errors["QB2"]["0"] = 0.032
error_profile.readout_errors["QB2"]["1"] = 0.0520
error_profile.readout_errors["QB3"]["0"] = 0.0365
error_profile.readout_errors["QB3"]["1"] = 0.05885
error_profile.readout_errors["QB4"]["0"] = 0.03735
error_profile.readout_errors["QB4"]["1"] = 0.06225
error_profile.readout_errors["QB5"]["0"] = 0.04375
error_profile.readout_errors["QB5"]["1"] = 0.05689

error_profile.name = "fake_q5"


q5_fake_backend = fake_backend.copy_with_error_profile(error_profile)
transpiled_job = transpile(circuit, q5_fake_backend)
job = q5_fake_backend.run(circuit, shots=1000)
print(job.result().get_counts())
plot_histogram(job.result().get_counts())

Batch execution

Q5 also allows for batches of circuits to be submitted with 1 call to the quantum computer. A batch is simply a list of QuantumCircuits. This is often faster than executing circuits individually, however, circuits will still be executed sequentially. On Q5 currently you can only place a maximum of 20 circuits in one batch. All circuits in a batch are executed with the same number of shots. The maximum number of shots per circuit is 100,000.

All circuits in a batch must measure the same qubits. Adding the initial_layout argument when submitting ensures that you always measure the same qubits.

Batch submission of circuits allows parameterized circuits to be executed using the qiskit.circuit.Parameter class.

circuits_list = []

circuit_1 = QuantumCircuit(2, name="Bell pair circuit")
circuit_1.h(0)
circuit_1.cx(0, 1)
circuit_1.measure_all()
transpiled_c1 = transpile(circuit_1, backend)
circuits_list.append(transpiled_c1)

circuit_1.draw(output="mpl")
circuit_2 = QuantumCircuit(2, name="Reverse Bell pair circuit")
circuit_2.h(1)
circuit_2.cx(1, 0)
circuit_2.measure_all()
transpiled_c2 = transpile(circuit_2, backend)
circuits_list.append(transpiled_c2)

circuit_2.draw(output="mpl")
# Execute and monitor job
job = q5_fake_backend.run(
    circuits_list, shots=1000, optimization_level=3, initial_layout=[0, 2]
)
# Get results
result = job.result()

# Plot histograms
plot_histogram(result.get_counts(), legend=["Circuit 1", "Circuit 2"])

Summary

In this notebook we have demonstrated how to connect and run circuits on Q5 with Qiskit and qiskit-on-iqm. Executing on Q50 can be done in a similiar way just by adjusting the connection URL.