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
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.
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.
"IQM_TOKEN"] = "<TOKEN>"
os.environ[= IQMProvider("https://qx.vtt.fi/api/devices/q5")
provider = provider.get_backend() 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
:
= nx.Graph()
G
G.add_edges_from(backend.coupling_map)= {node: f"QB{node + 1}" for node in G.nodes}
node_labels =node_labels, node_color="skyblue", node_size=500, font_size=10) nx.draw(G, labels
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.
= QuantumCircuit(2, name="Bell pair circuit")
circuit 0)
circuit.h(0, 1)
circuit.cx(
circuit.measure_all()="mpl") circuit.draw(output
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:
= transpile(circuit, backend)
transpiled_circuit ="mpl") transpiled_circuit.draw(output
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!
=100)
backend.set_options(shots= backend.run(transpiled_circuit) job
= backend.run(transpiled_circuit)
job 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.
= job.result()
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
= provider.get_backend(
backend ="35fa13d1-17db-4bd2-9b14-203ce32f0551"
calibration_set_id
)
= transpile(circuit, backend)
transpiled_circuit "mpl")
transpiled_circuit.draw(
= backend.run(
job2
transpiled_circuit,=1000,
shots=CircuitCompilationOptions(
circuit_compilation_options=None, heralding_mode=HeraldingMode.ZEROS
max_circuit_duration_over_t2
), )
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.
= job.result()
result = result._get_experiment(transpiled_circuit)
exp_result 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
.
= QuantumRegister(2, "QB")
qreg = QuantumCircuit(qreg, name="Bell pair circuit")
circuit 0])
circuit.h(qreg[0], qreg[1])
circuit.cx(qreg[
circuit.measure_all()
# Qubit numbers start at 0 index whereas the qubit names start at 1 index.
= {
qubit_mapping 0]: backend.qubit_name_to_index("QB1"),
qreg[1]: backend.qubit_name_to_index("QB2"),
qreg[
}
= transpile(circuit, backend, initial_layout=qubit_mapping)
transpiled_circuit = backend.run(transpiled_circuit) job
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:
= circuit.find_bit(qubit).index
index = backend.index_to_qubit_name(index)
mapping[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.
= optimize_single_qubit_gates(transpiled_circuit)
circuit_optimized "mpl") circuit_optimized.draw(
Simulating circuits locally with noise
Qiskit on IQM provides an IQMFakeBackend
with IQMFakeAdonis
for simulating Q5.
from iqm.qiskit_iqm import IQMFakeAdonis
= IQMFakeAdonis()
fake_backend = transpile(circuit, fake_backend)
transpiled_job = fake_backend.run(circuit, shots=1000)
job 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)
= fake_backend.error_profile
error_profile "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.t1s[
"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.t2s[
"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.single_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.two_qubit_gate_depolarizing_error_parameters[
"prx"] = 120 # in ns
error_profile.single_qubit_gate_durations["cz"] = 120 # in ns
error_profile.two_qubit_gate_durations[
"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.readout_errors[
= "fake_q5"
error_profile.name
= fake_backend.copy_with_error_profile(error_profile) q5_fake_backend
= transpile(circuit, q5_fake_backend)
transpiled_job = q5_fake_backend.run(circuit, shots=1000)
job 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
= QuantumCircuit(2, name="Bell pair circuit")
circuit_1 0)
circuit_1.h(0, 1)
circuit_1.cx(
circuit_1.measure_all()= transpile(circuit_1, backend)
transpiled_c1
circuits_list.append(transpiled_c1)
="mpl") circuit_1.draw(output
= QuantumCircuit(2, name="Reverse Bell pair circuit")
circuit_2 1)
circuit_2.h(1, 0)
circuit_2.cx(
circuit_2.measure_all()= transpile(circuit_2, backend)
transpiled_c2
circuits_list.append(transpiled_c2)
="mpl") circuit_2.draw(output
# Execute and monitor job
= q5_fake_backend.run(
job =1000, optimization_level=3, initial_layout=[0, 2]
circuits_list, shots
)# Get results
= job.result()
result
# Plot histograms
=["Circuit 1", "Circuit 2"]) plot_histogram(result.get_counts(), legend
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.