Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/implement_IRB_routine #994

Merged
merged 40 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0de866f
Feature: add base qcvv framework
cdbf1 Jul 16, 2024
0aa0d41
nit: add blank line
cdbf1 Jul 16, 2024
41bcc9b
Move qcvv to Supermarq
cdbf1 Jul 19, 2024
17b585b
Fix imports and tests
cdbf1 Jul 19, 2024
6dbdfa2
Revised results processing
cdbf1 Jul 19, 2024
a8ba683
Fix tests
cdbf1 Jul 19, 2024
246c58d
Remove qcvv from cirq docs
cdbf1 Jul 19, 2024
3eac549
Patch css Service in tests
cdbf1 Jul 19, 2024
9d8e844
Fix import css
cdbf1 Jul 19, 2024
26af253
fix: fix tests and notebook
cdbf1 Jul 22, 2024
d107276
fix: add seaborn to requirements
cdbf1 Jul 22, 2024
5859d69
fix: add future annotations to notebook
cdbf1 Jul 22, 2024
215b806
Merge branch 'main' into feature/base_qcvv_framework
cdbf1 Jul 23, 2024
48aa81d
Fixes following review
cdbf1 Jul 31, 2024
cc881ad
Merge branch 'main' into feature/base_qcvv_framework
cdbf1 Jul 31, 2024
f58dcd0
minor fix to tests and docs
cdbf1 Jul 31, 2024
3cde95e
Reduce circuit count in example
cdbf1 Jul 31, 2024
f361b14
Further fixes from code review
cdbf1 Aug 1, 2024
16cf125
Remove kw_only data classes as it doesn't work with python 3.8
cdbf1 Aug 1, 2024
2a0092c
Fix: add functionality for multiple subjobs
cdbf1 Aug 2, 2024
f3ea316
Merge branch 'main' into feature/base_qcvv_framework
cdbf1 Aug 2, 2024
43c7879
Merge branch 'main' into feature/base_qcvv_framework
cdbf1 Aug 5, 2024
c3e40d4
Feature: Implement IRB routine
cdbf1 Jul 16, 2024
a66304f
feature: start building notebook
cdbf1 Jul 16, 2024
08f2aac
Finish adding example notebook
cdbf1 Jul 16, 2024
4aad5be
nit: fix format
cdbf1 Jul 16, 2024
5da72d6
nit: init return none
cdbf1 Jul 16, 2024
08c7af7
tie up lose ends
cdbf1 Aug 5, 2024
0753367
Nit: formatting and mock css.service in irb test
cdbf1 Aug 5, 2024
52d95b6
Fix: Add generic type to base qcvv framework to facilitate subclassin…
cdbf1 Aug 5, 2024
6b2f808
Patch init
cdbf1 Aug 5, 2024
57b4a65
Merge branch 'main' into feature/implement_IRB_routine
cdbf1 Aug 14, 2024
b4a3845
Align with updated qcvv framework
cdbf1 Aug 14, 2024
7e54f99
Merge branch 'main' into feature/implement_IRB_routine
cdbf1 Aug 19, 2024
98303b4
Nit: fix formatting
cdbf1 Aug 19, 2024
24b9880
Merge branch 'main' into feature/implement_IRB_routine
cdbf1 Aug 27, 2024
c3fd266
Updates following DO review
cdbf1 Aug 27, 2024
8e5af55
Changes following DO review
cdbf1 Sep 6, 2024
59199d2
Merge branch 'main' into feature/implement_IRB_routine
cdbf1 Sep 6, 2024
3a37d13
Nit: formatting
cdbf1 Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/source/apps/supermarq/qcvv/qcvv.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ For a demonstration of how to implement a new experiment take a look at the foll

qcvv_css


Alternatively for pre-build experiments that can be used out of the box see

.. toctree::
:maxdepth: 1


qcvv_irb_css
qcvv_xeb_css

.. note::

At present the QCVV library is only available in :code:`cirq-superstaq`.
At present the QCVV library is only available in :code:`cirq-superstaq`.
190 changes: 190 additions & 0 deletions docs/source/apps/supermarq/qcvv/qcvv_irb_css.ipynb

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions supermarq-benchmarks/supermarq/qcvv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""A toolkit of QCVV routines."""

from .base_experiment import BenchmarkingExperiment, BenchmarkingResults, Sample
from .irb import IRB, IRBResults
from .xeb import XEB, XEBResults, XEBSample

__all__ = [
"BenchmarkingExperiment",
"BenchmarkingResults",
"Sample",
"IRB",
"IRBResults",
"XEB",
"XEBResults",
"XEBSample",
Expand Down
8 changes: 4 additions & 4 deletions supermarq-benchmarks/supermarq/qcvv/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def _retrieve_jobs(self) -> dict[str, str]:

return statuses

def _run_check(self) -> None:
def _has_raw_data(self) -> None:
"""Checks if any of the samples already have probabilities stored. If so raises a runtime
error to prevent them from being overwritten.

Expand Down Expand Up @@ -523,7 +523,7 @@ def run_on_device(
The superstaq job containing all the circuits submitted as part of the experiment.
"""
if not overwrite:
self._run_check()
self._has_raw_data()

experiment_job = self._service.create_job(
[sample.circuit for sample in self.samples],
Expand Down Expand Up @@ -556,7 +556,7 @@ def run_with_simulator(
be over written in the process. Defaults to False.
"""
if not overwrite:
self._run_check()
self._has_raw_data()

if simulator is None:
simulator = cirq.Simulator()
Expand Down Expand Up @@ -593,7 +593,7 @@ def run_with_callable(
RuntimeError: If the returned probabilities dictionary values do not sum to 1.0.
"""
if not overwrite:
self._run_check()
self._has_raw_data()
for sample in tqdm(self.samples, desc="Running circuits"):
probability = circuit_eval_func(sample.circuit, **kwargs)
if not all(len(key) == self.num_qubits for key in probability.keys()):
Expand Down
292 changes: 292 additions & 0 deletions supermarq-benchmarks/supermarq/qcvv/irb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# Copyright 2021 The Cirq Developers
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tooling for interleaved randomised benchmarking
"""

from __future__ import annotations

import random
from collections.abc import Iterable, Sequence
from dataclasses import dataclass

import cirq
import numpy as np
import pandas as pd
import seaborn as sns
from scipy.stats import linregress
from tqdm.contrib.itertools import product

from supermarq.qcvv.base_experiment import BenchmarkingExperiment, BenchmarkingResults, Sample


@dataclass(frozen=True)
class IRBResults(BenchmarkingResults):
"""Data structure for the IRB experiment results."""

rb_layer_fidelity: float
"""Layer fidelity estimate without the interleaving gate."""
rb_layer_fidelity_std: float
"""Standard deviation of the layer fidelity estimate without the interleaving gate."""
irb_layer_fidelity: float
"""Layer fidelity estimate with the interleaving gate."""
irb_layer_fidelity_std: float
"""Standard deviation of the layer fidelity estimate with the interleaving gate."""
average_interleaved_gate_error: float
"""Estimate of the interleaving gate error."""
average_interleaved_gate_error_std: float
"""Standard deviation of the estimate for the interleaving gate error."""

experiment_name = "IRB"


class IRB(BenchmarkingExperiment[IRBResults]):
r"""Interleaved random benchmarking (IRB) experiment.

IRB estimates the gate error of specified Clifford gate, :math:`\mathcal{C}^*`.
This is achieved by first choosing a random sequence, :math:`\{\mathcal{C_i}\}_m`
of :math:`m` Clifford gates and then using this to generate two circuits. The first
is generated by appending to this sequence the single gate that corresponds to the
inverse of the original sequence. The second circuit it obtained by inserting the
interleaving gate, :math:`\mathcal{C}^*` after each gate in the sequence and then
again appending the corresponding inverse element of the new circuit. Thus both
circuits correspond to the identity operation.

We run both circuits on the specified target and calculate the probability of measuring
the resulting state in the ground state, :math:`p(0...0)`. This gives the circuit fidelity

.. math::

f(m) = 2p(0...0) - 1

We can then fit and exponential decay :math:`\log(f) \sim m` to this circuit fidelity
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved
for each circuit, with decay rates :math:`\alpha` and :math:`\tilde{\alpha}` for the circuit
without and with interleaving respectively. Finally the gate error of the
specified gate, :math:`\mathcal{C}^*` is estimated as

.. math::

e_{\mathcal{C}^*} = \frac{1}{2} \left(1 - \frac{\tilde{\alpha}}{\alpha}\right)

For more details see: https://arxiv.org/abs/1203.4550
"""

def __init__(
self,
interleaved_gate: cirq.ops.SingleQubitCliffordGate = cirq.ops.SingleQubitCliffordGate.Z,
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved
num_qubits: int = 1,
) -> None:
"""Args:
interleaved_gate: The Clifford gate to measure the gate error of.
num_qubits: The number of qubits to experiment on
"""
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved
if num_qubits != 1:
raise NotImplementedError(
"IRB experiment is currently only implemented for single qubit use"
)
super().__init__(num_qubits=1)

self.interleaved_gate = interleaved_gate
"""The gate being interleaved"""

@staticmethod
def _reduce_clifford_seq(
gate_seq: list[cirq.ops.SingleQubitCliffordGate],
) -> cirq.ops.SingleQubitCliffordGate:
"""Reduces a list of single qubit clifford gates to a single gate.

Args:
gate_seq: The list of gates.

Returns:
The single reduced gate
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved
"""
cur = gate_seq[0]
for gate in gate_seq[1:]:
cur = cur.merged_with(gate)
return cur

@classmethod
def _random_single_qubit_clifford(cls) -> cirq.ops.SingleQubitCliffordGate:
"""Choose a random singe qubit clifford gate.
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved

Returns:
The random clifford gate.
"""
Id = cirq.ops.SingleQubitCliffordGate.I
H = cirq.ops.SingleQubitCliffordGate.H
S = cirq.ops.SingleQubitCliffordGate.Z_sqrt
X = cirq.ops.SingleQubitCliffordGate.X
Y = cirq.ops.SingleQubitCliffordGate.Y
Z = cirq.ops.SingleQubitCliffordGate.Z

set_A = [
Id,
S,
H,
cls._reduce_clifford_seq([H, S]),
cls._reduce_clifford_seq([S, H]),
cls._reduce_clifford_seq([H, S, H]),
]

set_B = [Id, X, Y, Z]

return cls._reduce_clifford_seq([random.choice(set_A), random.choice(set_B)])

def _invert_clifford_circuit(self, circuit: cirq.Circuit) -> cirq.Circuit:
"""Given a Clifford circuit find and append the corresponding inverse Clifford gate
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved

Args:
circuit: The Clifford circuit to invert.

Returns:
A copy of the original Clifford circuit with the inverse element appended.
"""
clifford_gates = [op.gate for op in circuit.all_operations()]
inv_element = self._reduce_clifford_seq(
cirq.inverse(clifford_gates) # type: ignore[arg-type]
)
return circuit + inv_element(*self.qubits)

def _build_circuits(self, num_circuits: int, cycle_depths: Iterable[int]) -> Sequence[Sample]:
"""Build a list of randomised circuits required for the IRB experiment.

Args:
num_circuits: Number of circuits to generate.
cycle_depths: An iterable of the different cycle depths to use during the experiment.

Returns:
The list of experiment samples.
"""
samples = []
for _, depth in product(range(num_circuits), cycle_depths, desc="Building circuits"):
base_circuit = cirq.Circuit(
*[self._random_single_qubit_clifford()(*self.qubits) for _ in range(depth)]
)
rb_circuit = self._invert_clifford_circuit(base_circuit)
irb_circuit = self._invert_clifford_circuit(
self._interleave_op(
base_circuit, self.interleaved_gate(*self.qubits), include_final=True
)
)
samples += [
Sample(
raw_circuit=rb_circuit + cirq.measure(sorted(rb_circuit.all_qubits())),
data={
"num_cycles": depth,
"circuit_depth": len(rb_circuit),
"experiment": "RB",
},
),
Sample(
raw_circuit=irb_circuit + cirq.measure(sorted(irb_circuit.all_qubits())),
data={
"num_cycles": depth,
"circuit_depth": len(irb_circuit),
"experiment": "IRB",
},
),
]

return samples

def _process_probabilities(self, samples: Sequence[Sample]) -> pd.DataFrame:
"""Processes the probabilities generated by sampling the circuits into the data structures
needed for analyzing the results.

Args:
samples: The list of samples to process the results from.

Returns:
A data frame of the full results needed to analyse the experiment.
"""

records = []
for sample in samples:
records.append(
{
"clifford_depth": sample.data["num_cycles"],
"circuit_depth": sample.data["circuit_depth"],
"experiment": sample.data["experiment"],
**sample.probabilities,
}
)

return pd.DataFrame(records)

def plot_results(self) -> None:
"""Plot the exponential decay of the circuit fidelity with
cycle depth.
"""
cdbf1 marked this conversation as resolved.
Show resolved Hide resolved
plot = sns.lmplot(
data=self.raw_data,
x="clifford_depth",
y="log_fidelity",
hue="experiment",
)
ax = plot.axes.item()
plot.tight_layout()
ax.set_xlabel(r"Cycle depth", fontsize=15)
ax.set_ylabel(r"Log Circuit fidelity", fontsize=15)
ax.set_title(r"Exponential decay of circuit fidelity", fontsize=15)

def analyze_results(self, plot_results: bool = True) -> IRBResults:
"""Analyse the experiment results and estimate the interleaved gate error.

Args:
plot_results: Whether to generate plots of the results. Defaults to False.

Returns:
A named tuple of the final results from the experiment.
"""

self.raw_data["fidelity"] = 2 * self.raw_data["0"] - 1
self.raw_data["log_fidelity"] = np.log(self.raw_data["fidelity"])
self.raw_data.dropna(axis=0, inplace=True) # Remove any NaNs coming from the P(0) < 0.5

rb_model = linregress(
self.raw_data.query("experiment == 'RB'")["clifford_depth"],
np.log(self.raw_data.query("experiment == 'RB'")["fidelity"]),
)
irb_model = linregress(
self.raw_data.query("experiment == 'IRB'")["clifford_depth"],
np.log(self.raw_data.query("experiment == 'IRB'")["fidelity"]),
)

# Extract fit values.
rb_layer_fidelity = np.exp(rb_model.slope)
rb_layer_fidelity_std = rb_model.stderr * rb_layer_fidelity
irb_layer_fidelity = np.exp(irb_model.slope)
irb_layer_fidelity_std = irb_model.stderr * irb_layer_fidelity

interleaved_gate_error = (1 - irb_layer_fidelity / rb_layer_fidelity) / 2

interleaved_gate_error_std = np.sqrt(
(irb_layer_fidelity_std / (2 * rb_layer_fidelity)) ** 2
+ ((irb_layer_fidelity * rb_layer_fidelity_std) / (2 * rb_layer_fidelity**2)) ** 2
)

self._results = IRBResults(
target="& ".join(self.targets),
total_circuits=len(self.samples),
rb_layer_fidelity=rb_layer_fidelity,
rb_layer_fidelity_std=rb_layer_fidelity_std,
irb_layer_fidelity=irb_layer_fidelity,
irb_layer_fidelity_std=irb_layer_fidelity_std,
average_interleaved_gate_error=interleaved_gate_error,
average_interleaved_gate_error_std=interleaved_gate_error_std,
)

if plot_results:
self.plot_results()

return self.results
Loading