from dataclasses import dataclass, field
from typing import Iterable, Optional, TypedDict, Union
import numpy as np
import plotly.graph_objects as go
from qibolab.platform import Platform
from qibolab.qubits import QubitId
from qibocal.auto.operation import Parameters, Routine
from ..utils import table_dict, table_html
from .fitting import exp1B_func
from .utils import RBData, StandardRBResult, fit, number_to_str, rb_acquisition
[docs]class Depthsdict(TypedDict):
"""Dictionary used to build a list of depths as ``range(start, stop,
step)``."""
start: int
stop: int
step: int
[docs]@dataclass
class StandardRBParameters(Parameters):
"""Standard Randomized Benchmarking runcard inputs."""
depths: Union[list, Depthsdict]
"""A list of depths/sequence lengths.
If a dictionary is given the list will be build.
"""
niter: int
"""Sets how many iterations over the same depth value."""
uncertainties: Optional[float] = None
"""Method of computing the error bars of the signal and uncertainties of
the fit.
If ``None``,
it computes the standard deviation. Otherwise it computes the corresponding confidence interval. Defaults `None`.
"""
unrolling: bool = False
"""If ``True`` it uses sequence unrolling to deploy multiple circuits in a
single instrument call.
Defaults to ``False``.
"""
seed: Optional[int] = None
"""A fixed seed to initialize ``np.random.Generator``.
If ``None``, uses a random seed.
Defaults is ``None``.
"""
noise_model: Optional[str] = None
"""For simulation purposes, string has to match what is in
:mod:`qibocal.protocols.randomized_benchmarking.noisemodels`"""
noise_params: Optional[list] = field(default_factory=list)
"""With this the noise model will be initialized, if not given random
values will be used."""
nshots: int = 10
"""Just to add the default value."""
def __post_init__(self):
if isinstance(self.depths, dict):
self.depths = list(
range(self.depths["start"], self.depths["stop"], self.depths["step"])
)
[docs]def _acquisition(
params: StandardRBParameters,
platform: Platform,
targets: list[QubitId],
) -> RBData:
"""The data acquisition stage of Standard Randomized Benchmarking.
1. Set up the scan
2. Execute the scan
3. Post process the data and initialize a standard rb data object with it.
Args:
params: All parameters in one object.
platform: Platform the experiment is executed on.
target: list of qubits the experiment is executed on.
Returns:
RBData: The depths, samples and ground state probability of each experiment in the scan.
"""
return rb_acquisition(params, platform, targets)
[docs]def _fit(data: RBData) -> StandardRBResult:
"""Takes a data frame, extracts the depths and the signal and fits it with
an exponential function y = Ap^x+B.
Args:
data (RBData): Data from the data acquisition stage.
Returns:
StandardRBResult: Aggregated and processed data.
"""
return fit(data.qubits, data)
[docs]def _plot(
data: RBData, fit: StandardRBResult, target: QubitId
) -> tuple[list[go.Figure], str]:
"""Builds the table for the qq pipe, calls the plot function of the result
object and returns the figure es list.
Args:
data (RBData): Data object used for the table.
fit (StandardRBResult): Is called for the plot.
target (_type_): Not used yet.
Returns:
tuple[list[go.Figure], str]:
"""
if isinstance(target, list):
target = tuple(target)
qubit = target
fig = go.Figure()
fitting_report = ""
x = data.depths
raw_data = data.extract_probabilities(qubit)
y = np.mean(raw_data, axis=1)
raw_depths = [[depth] * data.niter for depth in data.depths]
fig.add_trace(
go.Scatter(
x=np.hstack(raw_depths),
y=np.hstack(raw_data),
line=dict(color="#6597aa"),
mode="markers",
marker={"opacity": 0.2, "symbol": "square"},
name="iterations",
)
)
fig.add_trace(
go.Scatter(
x=x,
y=y,
line=dict(color="#aa6464"),
mode="markers",
name="average",
)
)
# Create a dictionary for the error bars
error_y_dict = None
if fit is not None:
popt, perr = fit.fit_parameters[qubit], fit.fit_uncertainties[qubit]
label = "Fit: y=Ap^x<br>A: {}<br>p: {}<br>B: {}".format(
number_to_str(popt[0], perr[0]),
number_to_str(popt[1], perr[1]),
number_to_str(popt[2], perr[2]),
)
x_fit = np.linspace(min(x), max(x), len(x) * 20)
y_fit = exp1B_func(x_fit, *popt)
fig.add_trace(
go.Scatter(
x=x_fit,
y=y_fit,
name=label,
line=go.scatter.Line(dash="dot", color="#00cc96"),
)
)
if fit.error_bars is not None:
error_bars = fit.error_bars[qubit]
# Constant error bars
if isinstance(error_bars, Iterable) is False:
error_y_dict = {"type": "constant", "value": error_bars}
# Symmetric error bars
elif isinstance(error_bars[0], Iterable) is False:
error_y_dict = {"type": "data", "array": error_bars}
# Asymmetric error bars
else:
error_y_dict = {
"type": "data",
"symmetric": False,
"array": error_bars[1],
"arrayminus": error_bars[0],
}
fig.add_trace(
go.Scatter(
x=x,
y=y,
error_y=error_y_dict,
line={"color": "#aa6464"},
mode="markers",
name="error bars",
)
)
if fit is not None:
fitting_report = table_html(
table_dict(
str(qubit),
["niter", "nshots", "uncertainties", "fidelity", "pulse_fidelity"],
[
data.niter,
data.nshots,
data.uncertainties,
number_to_str(
fit.fidelity[qubit],
np.array(fit.fit_uncertainties[qubit][1]) / 2,
),
number_to_str(
fit.pulse_fidelity[qubit],
np.array(fit.fit_uncertainties[qubit][1])
/ (2 * data.npulses_per_clifford),
),
],
)
)
fig.update_layout(
showlegend=True,
xaxis_title="Circuit depth",
yaxis_title="Survival Probability",
)
return [fig], fitting_report
standard_rb = Routine(_acquisition, _fit, _plot)