Quantum Characterisation, Verification and Validation
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
We can create an experiment to measure this as follows
[1]:
from __future__ import annotations
[ ]:
try:
import supermarq # noqa: F401
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.")
[ ]:
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import cirq
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from scipy.stats import linregress
from supermarq.qcvv import QCVVExperiment, QCVVResults, Sample
from tqdm.contrib.itertools import product
if TYPE_CHECKING:
from typing_extensions import Self
@dataclass
class NaiveExperimentResult(QCVVResults):
gate_fidelity: float = 1
gate_error: float = 0
def _analyze(self) -> None:
"""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.data["depth"], y=np.log(2 * self.data["0"] - 1))
self.gate_fidelity = np.exp(model.slope)
self.gate_error = 1 - np.exp(model.slope)
def plot_results(self, filename: str | None = None) -> None:
"""Plot the data with the fit superimposed on top."""
fig, axs = plt.subplots(
1,
)
sns.scatterplot(self.data, x="depth", y="0", ax=axs)
x = np.linspace(0, max(self.data.depth))
y = 0.5 * self.gate_fidelity**x + 0.5
axs.plot(x, y)
axs.set_xlabel("Circuit depth")
axs.set_ylabel("Probability of ground state")
if filename is not None:
fig.savefig(filename)
def print_results(self) -> None:
print(f"Estimated gate error: {self.gate_error}")
class NaiveExperiment(QCVVExperiment[NaiveExperimentResult]):
def __init__(
self,
num_circuits: int,
cycle_depths: Iterable[int],
*,
random_seed: int | np.random.Generator | None = None,
_samples: list[Sample] | None = None,
) -> None:
super().__init__(
qubits=1,
num_circuits=num_circuits,
cycle_depths=cycle_depths,
results_cls=NaiveExperimentResult,
random_seed=random_seed,
_samples=_samples,
)
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 index, 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(circuit_realization=index, circuit=circuit, data={"depth": depth})
)
return samples
def _json_dict_(self) -> dict[str, Any]:
return super()._json_dict_()
@classmethod
def _from_json_dict_(
cls,
samples: list[Sample],
qubits: int,
num_circuits: int,
cycle_depths: list[int],
**kwargs: Any,
) -> Self:
return cls(
num_circuits=num_circuits,
qubits=qubits,
cycle_depths=cycle_depths,
_samples=samples**kwargs,
)
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\).
[4]:
noise = cirq.DepolarizingChannel(p=0.01 * 3 / 4)
simulator = cirq.DensityMatrixSimulator(noise=noise, seed=0)
experiment = NaiveExperiment(10, [10, 50, 100], random_seed=0)
results = experiment.run_with_simulator(simulator=simulator)
[5]:
results.analyze()
Estimated gate error: 0.0100782074092588
Checking this result we have
[6]:
pauli_error_rate = results.gate_error
print(pauli_error_rate)
0.0100782074092588
Which agrees very closely with our channel which we set up with \(p=0.01\)
[7]:
ss_results = experiment.run_on_device(target="ss_unconstrained_simulator", repetitions=1000)
[8]:
ss_results.analyze()
Estimated gate error: 0.0
As expected, since the Superstaq simulator is exact, we obtain a gate fidelity of 1.0