Quantum Characterisation, Verification and Validation

Open in Colab Launch Binder

To demonstrate how to implement new benchmarking experiments within the Superstaq QCVV framework, consider implementing a naive benchmarking routine where we try to estimate the fidelity of a single qubit Z gate by repeatedly applying the gate to a qubit in the ground state (such that the Z-gate should have no effect) and observing if any observations of the excited state occur. If the excited state is observed this indicates an error has occurred. Assuming that each time the Z-gate is applied the probability of a bit flip error is \(e\) then after \(d\) gates the probability of observing the ground state is

\[p(0) = \frac{1}{2}(1-e)^d + \frac{1}{2}\]

We can create an experiment to measure this as follows

[4]:
%load_ext autoreload
%autoreload 2
from __future__ import annotations
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
[ ]:
try:
    import supermarq
except ImportError:
    print("Installing supermarq...")
    %pip install --quiet supermarq
    print("Installed supermarq.")
    print("You may need to restart the kernel to import newly installed packages.")
[5]:
from supermarq.qcvv import BenchmarkingExperiment, Sample, BenchmarkingResults
from dataclasses import dataclass
from collections.abc import Sequence
from typing import Iterable
from tqdm.contrib.itertools import product
import pandas as pd

import cirq

from scipy.stats import linregress
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt


@dataclass(frozen=True)
class NaiveExperimentResult(BenchmarkingResults):
    gate_fidelity: float
    gate_error: float

    experiment_name = "NaiveExperiment"


class NaiveExperiment(BenchmarkingExperiment[NaiveExperimentResult]):
    def __init__(self):
        super().__init__(num_qubits=1)

    def _build_circuits(self, num_circuits: int, layers: Iterable[int]) -> Sequence[Sample]:
        """Build the circuits by composing multiple Z gates together into circuits. The
        number of gates to compose is given by the `layers` parameter.
        """
        samples = []
        for _, depth in product(range(num_circuits), layers, desc="Building circuits."):
            circuit = cirq.Circuit([cirq.Z(*self.qubits) for _ in range(depth)])
            circuit += cirq.measure(*self.qubits)
            samples.append(Sample(raw_circuit=circuit, data={"depth": depth}))

        return samples

    def _process_probabilities(self, samples) -> None:
        """Copy the data and observed probabilities into a pandas DataFrame."""
        records = []
        for sample in samples:
            records.append({**sample.data, **sample.probabilities})
        return pd.DataFrame(records)

    def analyze_results(self, plot_results: bool = True) -> NaiveExperiment:
        """To analyse the results to fit a simple exponential decay. This can be done easily
        by fitting a linear model to the logarithm of the equation above.
        """

        model = linregress(x=self.raw_data["depth"], y=np.log(2 * self.raw_data["0"] - 1))

        fidelity = np.exp(model.slope)

        self._results = NaiveExperimentResult(
            target="& ".join(self.targets),
            total_circuits=len(self.samples),
            gate_fidelity=fidelity,
            gate_error=1 - fidelity,
        )

        if plot_results:
            self.plot_results()

        return self.results

    def plot_results(self) -> None:
        """Plot the data with the fit superimposed on top."""

        fig, axs = plt.subplots(
            1,
        )

        sns.scatterplot(self.raw_data, x="depth", y="0", ax=axs)

        x = np.linspace(0, max(self.raw_data.depth))
        y = 0.5 * self.results.gate_fidelity**x + 0.5
        axs.plot(x, y)
        axs.set_xlabel("Circuit depth")
        axs.set_ylabel("Probability of ground state")

To test this basic experiment, we use a depolarising noise model and a density matrix simulator. Note that if we use a single qubit depolarising channel with pauli error rate \(p\) this will result in an error with probability of \(4p/3\).

[6]:
noise = cirq.DepolarizingChannel(p=0.01 * 3 / 4)
simulator = cirq.DensityMatrixSimulator(noise=noise)
experiment = NaiveExperiment()
experiment.prepare_experiment(10, [10, 50, 100])
experiment.run_with_simulator(repetitions=2000, simulator=simulator)
[7]:
if experiment.collect_data():
    results = experiment.analyze_results(plot_results=True)
    print(results)
NaiveExperimentResult(target='Local simulator', total_circuits=30, experiment_name='NaiveExperiment', gate_fidelity=0.990025224470535, gate_error=0.009974775529465019)
../../../_images/apps_supermarq_qcvv_qcvv_css_9_1.png

Checking this result we have

[8]:
pauli_error_rate = experiment.results.gate_error
print(pauli_error_rate)
0.009974775529465019

Which agrees very closely with our channel which we set up with \(p=0.01\)

We could also run the same experiment on a device through Superstaq. In this case we use the run_on_device() method instead, although for this example we use the Superstaq simulator device. Note we also use the force=True option to overwrite our existing results.

[9]:
experiment.prepare_experiment(5, [10, 50, 100], overwrite=True)
experiment_job = experiment.run_on_device(
    target="ss_unconstrained_simulator", repetitions=1000, overwrite=True
)
[10]:
if experiment.collect_data():
    results = experiment.analyze_results(plot_results=True)
    print(results)
NaiveExperimentResult(target='ss_unconstrained_simulator', total_circuits=15, experiment_name='NaiveExperiment', gate_fidelity=1.0, gate_error=0.0)
../../../_images/apps_supermarq_qcvv_qcvv_css_15_2.png

As expected, since the Superstaq simulator is exact, we obtain a gate fidelity of 1.0