Visualizing Quality Metrics

A quantum computers performance can change on a regular basis. This can be mitigated to some extent by calibration of the device which generates a calibration set, which is a set of parameters to current operate the quantum computer at. Because of drifts in the environment that the quantum computer operates at, regular calibration is required to account for these drifts and maintain optimal performance.

To test the current performance of the calibration, we also benchmark the device, producing a quality metrics set. These benchmarks reflect the current status of the quantum computer and are essential pieces of information for using the system. For example, performance can often be increased by choosing a particular qubit or tunable coupler when running your quantum circuit.

This notebook demonstrates how one can read the quality metrics set from the API and visualize it.

Software versions

Last verified: 2026-07-25, against:

iqm-client[qiskit]>=34.0.0,<35.0.0  # tested with iqm-client==34.0.4

Importing Libraries

import os

import matplotlib.pyplot as plt
from iqm.iqm_client import IQMClient
from iqm.qiskit_iqm import IQMProvider

Getting the data from the API

The calibration set ID and quality metrics set data are bundled together in JSON format available from the Cortex API. In this example, we make a HTTP request to the particular API endpoint, in this case, calibration/metrics/latest. We use the IQMClient to add the authentication token to the request, which is needed for accessing VTT quantum computers.

First we define a helper function to get the data. The function get_calibration_data takes input IQMClient, calibration_set_id and optionally a filename. The filename option can be pass to save the data to a json file.

def get_calibration_data(client: IQMClient, filename: str = None):
    """
    Return the quality metric set for a calibration set using IQMClient.
    Optionally save the response to a json file, if filename is provided.
    """
    default_cal_set_id = client.get_dynamic_quantum_architecture().calibration_set_id
    quality_metric_set = client.get_quality_metric_set(
        calibration_set_id=default_cal_set_id
    )

    if filename:
        with open(filename, "w") as f:
            f.write(quality_metric_set.model_dump_json(indent=4))
        print(f"Data saved to {filename}")

    return quality_metric_set, default_cal_set_id

Defining the IQM Client

Setup the qiskit IQMProvider using the link to backend.

os.environ["IQM_TOKEN"] = ""
provider = IQMProvider(
    url="https://qx.vtt.fi",
    quantum_computer="q50",  # Replace with 'q5' or 'q50' as needed
)
backend = provider.get_backend()

We can then get the raw calibration data and see what it looks like

calibration_data, cal_set_id = get_calibration_data(backend.client)

We can parse this data to see individual metrics and values. For example:

calibration_data.observations

calibration_data is a QualityMetricSet (see the iqm-client API docs). Its observations field is a flat list of ObservationLite entries, each with a dotted dut_field string that encodes both the metric name and the qubit(s) it was measured on, e.g. metrics.rb.clifford.uz_cz.QB5__QB6.fidelity:par=d2, plus a numeric value.

Plotting the data

To plot the data, we utilise a simple plotting function which can plot a specific metric which we are interested in. The function iterates over the gathered data and extracts the particular keys and values for the input metric. This requires knowing and using the same string as the metric you are interested in.

def filter_observations(
    data, *, startswith: str | None = None, endswith: str | None = None
):
    observations = []
    for obs in data.observations:
        field = obs.dut_field
        if startswith and not field.startswith(startswith):
            continue
        if endswith and not field.endswith(endswith):
            continue
        observations.append(obs)

    return type("FilteredData", (), {"observations": observations})()


def plot_metrics(
    metric: str, title: str, ylabel: str, xlabel: str, data, limits: list = []
):
    values_by_qubit: dict[str, list[float]] = {}
    for obs in data.observations:
        if metric not in obs.dut_field:
            continue
        for token in obs.dut_field.split("."):
            for qb in token.split("__"):
                if qb.startswith("QB") and qb[2:].isdigit():
                    values_by_qubit.setdefault(qb, []).append(obs.value)

    if not values_by_qubit:
        return f"{metric} not in quality metrics set!"

    labels = sorted(values_by_qubit, key=lambda qb: int(qb[2:]))
    values = [sum(values_by_qubit[qb]) / len(values_by_qubit[qb]) for qb in labels]

    plt.bar(range(len(values)), values, width=0.4, 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()

Readout Fidelities

The readout fidelity describes how well the quantum computer can measure your quantum state. From the readout fidelity one can see how much error from their quantum circuit they can attribute to incorrectly reading out or measuring the quantum state.

Readout fidelity includes both state preparation and measurement errors here.

ssro_fidelity_data = filter_observations(
    calibration_data,
    startswith="metrics.ssro.measure.constant.",
    endswith=".fidelity",
)

plot_metrics(
    metric="metrics.ssro.measure.constant.QB",
    title="Single Shot Readout Fidelities",
    xlabel="Qubits",
    ylabel="Fidelity",
    data=ssro_fidelity_data,
    limits=[0.85, 1],
)

T_1 & T_2 Times

The T_1 and T_2 times are the coherence times for the system.

The T_1 time is called the longitudinal relaxation rate and describes how quickly the excited state of the qubit returns to its ground state.

The T_2 time is called the transverse relaxation rate and describes loss of coherence of a superposition state.

The T_2-echo time describes the loss of coherence of the superposition state of the qubit. It is more precise than the T_2 Time as it is less susceptible to low-frequency noise.

Based on these values you can approximate how many gates you can run on the quantum computer before decoherence occurs, which is an important quantity when writing quantum circuits.

plot_metrics(
    metric="t1_time",
    title="T1 times",
    xlabel="Qubits",
    ylabel="Time",
    data=calibration_data,
)
plot_metrics(
    metric="t2_time",
    title="T2 times",
    xlabel="Qubits",
    ylabel="Time",
    data=calibration_data,
)
plot_metrics(
    metric="t2_echo_time",
    title="T2 Echo times",
    xlabel="Qubits",
    ylabel="Time",
    data=calibration_data,
)

Single qubit gate fidelities

Single qubit gate fidelities are measured through executing the single qubit randomized benchmarking (RB) experiment.

In this experiment a random sequence of single-qubit clifford gates are sampled and the survival probabilities are measured. An exponential curve is then fit and an estimate for the average gate fidelity across the set of clifford gates is calculated.

It is important to note that single qubit gate fidelities are independent of state preparation and measurement (SPAM) errors.

plot_metrics(
    metric="rb.prx",
    title="Single-qubit Gate Fidelities",
    xlabel="Qubits",
    ylabel="Fidelities",
    data=calibration_data,
    limits=[0.95, 1],
)

Two qubit gate fidelities

The calibration data reports two metrics for the two-qubit gate fidelity. The CZ gate fidelity and the two-qubit average gate fidelity.

The CZ fidelity is estimated using interleaved randomized benchmarking. In this randomized benchmarking sequence is interleaved with the CZ gate, obtaining the average gate fidelity for that specific gate. This is important to know because VTT’s quantum computers have their two-qubit native gate as the CZ gate. The fidelity is usually higher than the average 2QB gate fidelity as a random 2QB Clifford transpiles on average to 8.25 1QB gates and 1.5 2QB CZ gates.

The 2QB average gate fidelity is estimated with randomized benchmarking protocol in a similar fashion to the single-qubit RB protocol.

plot_metrics(
    metric="irb.cz",
    title="CZ Gate Fidelities",
    xlabel="Qubit pairs",
    ylabel="Fidelities",
    data=calibration_data,
    limits=[0.8, 1],
)
plot_metrics(
    metric="rb.clifford.uz_cz",
    title="Two-qubit Gates Cliffords Averaged",
    xlabel="Qubits",
    ylabel="Fidelities",
    data=calibration_data,
    limits=[0.7, 1],
)