Advanced Helmi Usage¶
This notebooks covers some "advanced" usage of Helmi. In particular we look at:
- Improving results by utilising calibration data
- Improving results by taking into account the topology
- Improving results via readout error mitigation
Setup¶
import json
import os
import matplotlib.pyplot as plt
import requests
from iqm.iqm_client import IQMClient
from iqm.qiskit_iqm import IQMFakeAdonis, IQMProvider
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
from qiskit import QuantumCircuit, QuantumRegister, execute, transpile
from qiskit.tools.monitor import job_monitor
from qiskit.visualization import plot_histogram
from qiskit_aer import Aer
from qiskit_experiments.library import LocalReadoutError
provider = IQMProvider("https://qc.vtt.fi/helmi/cocos")
backend_helmi = provider.get_backend()
backend_sim = Aer.get_backend("aer_simulator")
fake_backend = IQMFakeAdonis()
Creating Bell Pair circuit¶
circuit = QuantumCircuit(2, name="Bell pair circuit")
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
circuit.draw(output="mpl")
job = execute(circuit, backend_helmi, shots=100)
print(f"Job ID: {job.job_id()}.")
print("Tracking execution of job:")
job_monitor(job)
Viewing the results¶
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())
Additional Metadata¶
exp_result = result._get_experiment(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)
Improving results by utilising calibration data¶
Using the execute
function and passing the quantum circuit is a naive implementation. We don't know which qubits we ran on and some qubits may be performing differently to others. To extract the best results from our algorithm we should look at the calibration data and pick the best qubits.
First we use some utility functions to get the calibration data and plot a particular metric.
def get_calibration_data(
client: IQMClient, calibration_set_id=None, filename: str = None
):
"""
Return the calibration data and figures of merit using IQMClient.
Optionally you can input a calibration set id (UUID) to query historical results
Optionally save the response to a json file, if filename is provided
"""
headers = {"User-Agent": client._signature}
bearer_token = client._get_bearer_token()
headers["Authorization"] = bearer_token
if calibration_set_id:
url = os.path.join(client._base_url, "calibration/metrics/", calibration_set_id)
else:
url = os.path.join(client._base_url, "calibration/metrics/latest")
response = requests.get(url, headers=headers)
response.raise_for_status() # will raise an HTTPError if the response was not ok
data = response.json()
data_str = json.dumps(data, indent=4)
if filename:
with open(filename, "w") as f:
f.write(data_str)
print(f"Data saved to {filename}")
return data
def plot_metrics(
metric: str, title: str, ylabel: str, xlabel: str, data: dict, limits: list = []
):
# Initialize lists to store the values and labels
values = []
labels = []
# Iterate over the calibration data and collect values and labels based on the metric
for key, metric_data in data["metrics"].items():
if key.endswith(metric):
values.append(float(metric_data["value"]))
# Extract the qubit label from the key
labels.append(key.split(".")[0])
# Check if values were found for the given metric
if not values:
return f"{metric} not in quality metrics set!"
# Set the width and gap between the bars
bar_width = 0.4
# Calculate the positions of the bars
positions = range(len(values))
# Plot the values with labels
plt.bar(positions, values, width=bar_width, tick_label=labels)
if len(limits) == 2:
plt.ylim(limits)
plt.grid(axis="y")
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.title(title)
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()
calibration_data = get_calibration_data(
backend_helmi.client, calibration_set_id="27fecf23-c289-4fde-93e5-912fbd5f5b66"
)
plot_metrics(
metric="fidelity_2qb_cliffords_averaged",
title="Two-qubit Gates Cliffords Averaged",
xlabel="Qubits",
ylabel="Fidelities",
data=calibration_data,
limits=[0.7, 1],
)
Now let's use transpilation
to map our quantum circuit to a chosen set of qubits.
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]: 4, # Map the first qubit to QB5
qreg[1]: 2, # Map the second qubit to QB3
}
transpiled_circuit = transpile(
circuit,
backend=backend_helmi,
layout_method="sabre",
optimization_level=3,
initial_layout=qubit_mapping,
)
transpiled_circuit.draw("mpl")
job = execute(transpiled_circuit, backend_helmi, shots=100)
print(f"Job ID: {job.job_id()}.")
print("Tracking execution of job:")
job_monitor(job)
result = job.result()
print(result.request.qubit_mapping) # Retrieving the qubit mapping
plot_histogram(result.get_counts())
Improving results by taking into account topology¶
Here a 5 qubit Greenberger-Horne-Zeilinger (GHZ) State quantum circuit is created. This creates an n-qubit entangled state. Running a GHZ experiment is useful for assessing the multi-qubit interactions in a quantum computer.
In this advanced example, a textbook implementation of a GHZ circuit is written and run on Helmi. Then, the circuit is transpiled taking into account the topology of Helmi. The results are compared.
# create quantum circuit
shots = 1000
qreg = QuantumRegister(5, "QB")
qc = QuantumCircuit(qreg, name="GHZ circuit")
qc.h(0)
qc.cx(0, 1) # apply CNOT, control=0, target=1
qc.cx(1, 2)
qc.cx(2, 3)
qc.cx(3, 4)
qc.measure_all()
qc.draw("mpl")
Running this on the ideal simulator gives the following histogram
job = execute(qc, backend_sim, shots=1000)
job_monitor(job)
counts = job.result().get_counts()
plot_histogram(counts)
In this approach, the circuit is created in a 'textbook' fashion. Due to the topology of Helmi, after transpiling the circuit it becomes much longer because SWAP gates are needed.
transpiled_circuit = transpile(
qc, backend=backend_helmi, layout_method="sabre", optimization_level=3
)
transpiled_circuit.draw("mpl")
This can be shown by only displaying the routed circuit, without decomposition into native gates.
transpiled_circuit_simple = transpile(
qc,
coupling_map=backend_helmi.coupling_map,
layout_method="sabre",
optimization_level=3,
)
transpiled_circuit_simple.draw("mpl")
Let's run this on Helmi!
job = execute(transpiled_circuit, backend_helmi, shots=1000)
job_monitor(job)
counts = job.result().get_counts()
plot_histogram(counts)
In this case we have an additional swap gates due to the central qubit (QB3) being the only available qubit to make 2 qubit gates.
We can reduce the number of swap gates needed and improve our GHZ 5 result by placing the Hadamard gate on the central qubit and CNOTs on all the neighbours.
# create quantum circuit
qreg = QuantumRegister(5, "QB")
qc = QuantumCircuit(qreg, name="GHZ circuit")
qc.h(2)
qc.cx(2, 0)
qc.cx(2, 1)
qc.cx(2, 3)
qc.cx(2, 4)
qc.measure_all()
qc.draw("mpl")
transpiled_circuit = transpile(
qc, backend=backend_helmi, layout_method="sabre", optimization_level=3
)
transpiled_circuit.draw("mpl")
Now we run the code on Helmi and look at the histogram.
job = execute(transpiled_circuit, backend_helmi, shots=1000)
job_monitor(job)
counts = job.result().get_counts()
plot_histogram(counts)
We can decrease the depth of the circuit even further by merging adjacent single qubit gates
circuit_optimized = optimize_single_qubit_gates(transpiled_circuit)
circuit_optimized.draw("mpl")
job = execute(circuit_optimized, backend_helmi, shots=1000)
job_monitor(job)
counts = job.result().get_counts()
plot_histogram(counts)
Readout Mitigation Helmi¶
Error mitigation is a class of techniques aimed at reducing the error from submitting to the current generation of noisy devices. This exercise demonstrates how to apply simple readout error mitigation to improve the results from our GHZ circuit.
This follows Qiskit's tutorial: Readout Mitigation, however alternatives such as Mitiq can be used. Mitiq provides an open-source library to learn about and implement error mitigation.
For this brief example, readout error mitigation is applied using the LocalReadoutError
mitigator from qiskit.experiments
. Readout error mitigation refers to errors related to "reading out" of the quantum state into classical information which occurs during measurement.
With the LocalReadoutError
, a $2^n \times 2^n$ assignment matrix $A$ is created, containing the probabilities to observe $y$, given $x$. That is to say that the individual elements of the matrix will contain the probabilities that a qubit prepared in state $|0 \rangle$ or $|1 \rangle$ and was measured in either state $|0 \rangle$ or $|1 \rangle$.
Here we demonstrate the LocalReadoutMitigator
example, which assumes the readout errors of the qubits are uncorrelated. In this case $n 2 \times 2$ mitigation matrices are generated, 1 for each qubit.
First we generate 2 circuits for all of Helmi's qubits. The first circuit has no gates applied with the ideal outcome of all zeros: 00000
, the second circuit applied an $X$ gate to our circuit with the ideal outcome of all ones: 11111
. After running the experiment we get the Mitigator which returns the mitigated qasi-probabilities.
To exaggerate the effect a 5 qubit GHZ circuit is created.
# create quantum circuit
shots = 1000
qreg = QuantumRegister(3, "QB")
qc = QuantumCircuit(qreg, name="GHZ circuit")
qc.h(2)
qc.cx(2, 0)
qc.cx(2, 1)
qc.measure_all()
qc.draw("mpl")
With an ideal result run with the simulator
from qiskit import Aer
simulator = Aer.get_backend("aer_simulator")
result = simulator.run(qc, shots=shots).result()
counts = result.get_counts() # extract statistics from results
print(counts)
plot_histogram(counts)
And then Helmi
transpiled_circuit_simple = transpile(
qc,
coupling_map=[[3, 2], [2, 3], [4, 2], [2, 4]],
layout_method="sabre",
optimization_level=3,
)
transpiled_circuit_simple.draw("mpl")
transpiled_circuit = transpile(
qc,
backend=backend_helmi,
coupling_map=[[3, 2], [2, 3], [4, 2], [2, 4]],
optimization_level=3,
initial_layout=[2, 3, 4],
)
transpiled_circuit.draw("mpl")
job = execute(transpiled_circuit, backend_helmi, shots=1000)
job_monitor(job)
counts = job.result().get_counts()
plot_histogram(counts)
qubits = [2, 3, 4]
# The qiskit experiment class generates the "Calibration Circuits"
# based off the experiment and the qubits input.
exp = LocalReadoutError(qubits)
for c in exp.circuits():
print(c)
exp.analysis.set_options(plot=True)
result = exp.run(backend_helmi)
mitigator = result.analysis_results(0).value
result.figure(0)
The experiment can simple be run. Qiskit's experiments library takes take of the circuit transpilation and execution in addition to analysis. In this case the above circuits are run and then analysed.
exp.analysis.set_options(plot=True)
result = exp.run(fake_backend)
mitigator = result.analysis_results(0).value
result.figure(0)
Here $A$ is the assignment matrix, with $I$ being the identity matrix. The individual components of the assignment matrix represent the probabilities to, for example prepare a $|0 \rangle$ state and get a $|1 \rangle$ state or $|1\rangle$ state and get a $|0\rangle$ state. This is compared against the identity matrix because in the ideal case we would expect $P(X|X) = 1$ and $P(X|Y) = 0$ ($P(X|X)$ means the probability of $X$ given $X$) The plot shows the absolute value of these two matrices.
The automatic scale given by Qiskit experiments can be slightly misleading, as demonstrated when you run this with the simulator.
The assignment matrix can be printed.
mitigator.assignment_matrix()
print(len(mitigator.assignment_matrix()))
If, for example we used the simulator here the assignment matrix would look like the following:
array([[1., 0., 0., ..., 0., 0., 0.],
[0., 1., 0., ..., 0., 0., 0.],
[0., 0., 1., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 1., 0., 0.],
[0., 0., 0., ..., 0., 1., 0.],
[0., 0., 0., ..., 0., 0., 1.]])
With the simulator the $n$ mitigation matrices will look like:
[1. 0.]
[0. 1.]
When using the Qiskit experiment library the analysis is hidden from the user.
You can look at the code from Qiskit experiments LocalReadoutErrorAnalysis
if you wish to see what it's doing under the hood.
for m in mitigator._mitigation_mats:
print(m)
print()
print(len(mitigator._mitigation_mats))
Then a circuit can be run on Helmi and our error mitigation applied! In this case we apply the readout error mitigation to the GHZ circuit.
First, the circuit is run without error mitigation.
shots = 10000
counts = backend_helmi.run(transpiled_circuit, shots=shots).result().get_counts()
unmitigated_probs = {label: count / shots for label, count in counts.items()}
And then with error mitigation
mitigated_quasi_probs = mitigator.quasi_probabilities(counts)
mitigated_probs = (
mitigated_quasi_probs.nearest_probability_distribution().binary_probabilities()
)
legend = ["Mitigated Probabilities", "Unmitigated Probabilities"]
plot_histogram(
[mitigated_probs, unmitigated_probs],
legend=legend,
sort="value_desc",
bar_labels=False,
)