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

NIGSC changes #318

Merged
merged 22 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
28 changes: 14 additions & 14 deletions runcards/niGSC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ qubits: [0]
format: pickle

actions:
# standardrb:
# nqubits: 1
# depths: [1,3,5,7,10]
# runs: 2
# nshots: 1024
# noise_model: PauliErrorOnUnitary
# noise_params: [0.01, 0.01, 0.01]
standardrb:
nqubits: 1
depths: [1,3,5,7,10,20,30,50]
runs: 15
nshots: 1024
noise_model: PauliErrorOnAll
noise_params: [0.01, 0.01, 0.01]
XIdrb:
nqubits: 1
depths: [1,2,3,4,5,6,7,8,9,10]
runs: 5
nshots: 10
noise_model: PauliErrorOnX
noise_params: [0.05, 0.01, 0.01]
# simulfilteredrb:
# nqubits: 1
# depths: [1,3,5,7,10]
# runs: 2
# nshots: 1024
# noise_model: PauliErrorOnUnitary
# noise_params: [0.01, 0.01, 0.01]
simulfilteredrb:
nqubits: 1
depths: [1,3,5,7,10]
runs: 2
nshots: 1024
noise_model: PauliErrorOnAll
noise_params: [0.01, 0.01, 0.01]
123 changes: 39 additions & 84 deletions src/qibocal/calibrations/niGSC/XIdrb.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from qibocal.calibrations.niGSC.basics.circuitfactory import CircuitFactory
from qibocal.calibrations.niGSC.basics.experiment import Experiment
from qibocal.calibrations.niGSC.basics.plot import Report, scatter_fit_fig
from qibocal.calibrations.niGSC.basics.utils import number_to_str
from qibocal.config import raise_error


Expand All @@ -40,22 +41,16 @@ def __init__(self, nqubits: int, depths: list, qubits: list = []) -> None:
if not len(self.qubits) == 1:
raise_error(
ValueError,
"This class is written for gates acting on only one qubit, not {} qubits.".format(
len(self.qubits)
),
f"This class is written for gates acting on only one qubit, not {len(self.qubits)} qubits.",
)
self.name = "XId"

def build_circuit(self, depth: int):
# Initiate the empty circuit from qibo with 'self.nqubits'
# many qubits.
circuit = Circuit(1, density_matrix=True)
# There are only two gates to choose from for every qubit.
a = [gates.I(0), gates.X(0)]
# Draw sequence length many zeros and ones.
random_ints = np.random.randint(0, 2, size=depth)
# Get the Xs and Ids with random_ints as indices.
gate_lists = np.take(a, random_ints)
circuit = Circuit(1)
# Draw sequence length many I and X gates.
gate_lists = np.random.choice([gates.I(0), gates.X(0)], size=depth)
# Add gates to circuit.
circuit.add(gate_lists)
circuit.add(gates.M(0))
Expand All @@ -72,18 +67,20 @@ def __init__(
noise_model: NoiseModel = None,
) -> None:
super().__init__(circuitfactory, data, nshots, noise_model)
# Make the circuitfactory a list be able to store them using save_circuits method.
self.prebuild()
self.name = "XIdRB"

def execute(self, circuit: Circuit, datarow: dict) -> dict:
datadict = super().execute(circuit, datarow)
datadict["depth"] = circuit.ngates - 1
# TODO change that.
datadict["countX"] = circuit.gate_types["x"]
# TODO change to circuit.gate_types["x"] for next Qibo version
datadict["countX"] = len(circuit.gates_of_type("x"))
return datadict


# Define the result class for this specific module.
class moduleReport(Report):
class ModuleReport(Report):
def __init__(self) -> None:
super().__init__()
self.title = "X-Id Benchmarking"
Expand Down Expand Up @@ -137,13 +134,14 @@ def post_processing_sequential(experiment: Experiment):
# After the row by row execution of tasks comes the aggregational task. Something like calculation
# of means, deviations, fitting data, tasks where the whole data as to be looked at, and not just
# one instance of circuit + other information.
def get_aggregational_data(experiment: Experiment) -> pd.DataFrame:
def get_aggregational_data(experiment: Experiment, ndecays: int = 2) -> pd.DataFrame:
Copy link
Contributor

@wilkensJ wilkensJ May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can the number of decays be changed through a runcard?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter is not used in the runcards for now, but can be used to redo the fitting of the same experiment

"""Computes aggregational tasks, fits data and stores the results in a data frame.

No data is manipulated in the ``experiment`` object.

Args:
experiment (Experiment): After sequential postprocessing of the experiment data.
ndecays (int): Number of decays to be fitted. Default is 2.

Returns:
pd.DataFrame: The summarized data.
Expand All @@ -152,39 +150,26 @@ def get_aggregational_data(experiment: Experiment) -> pd.DataFrame:
depths, ydata = experiment.extract("filter", "depth", "mean")
_, ydata_std = experiment.extract("filter", "depth", "std")
# Fit the filtered signal for each depth, there could be two overlaying exponential functions.
popt, perr = fitting_methods.fit_exp2_func(depths, ydata)
popt, perr = fitting_methods.fit_expn_func(depths, ydata, n=ndecays)
# Create dictionaries with fitting parameters and estimated errors
popt_keys = [f"A{k+1}" for k in range(ndecays)]
popt_keys += [f"p{k+1}" for k in range(ndecays)]
popt_dict = dict(zip(popt_keys, popt))
perr_keys = [f"A{k+1}_err" for k in range(ndecays)]
perr_keys += [f"p{k+1}_err" for k in range(ndecays)]
perr_dict = dict(zip(perr_keys, perr))
# Build a list of dictionaries with the aggregational information.
data = [
{
"depth": depths, # The x-axis.
"data": ydata, # The filtred signal.
"2sigma": 2 * ydata_std, # The 2 * standard deviation error for each depth.
"fit_func": "exp2_func", # Which function was used to fit.
"popt_real": {
"A1_real": np.real(
popt[0]
), # The complex prefactors would lead to imaginary data.
"A2_real": np.real(
popt[1]
), # That's why they have to be stored seperatly.
"p1": popt[2],
"p2": popt[3],
}, # The real fitting parameters.
"perr": {
"A1_err": perr[0],
"A2_err": perr[1],
"p1_err": perr[2],
"p2_err": perr[3],
}, # The estimated errors.
"fit_func": "expn_func", # Which function was used to fit.
"popt": popt_dict, # The real fitting parameters.
"perr": perr_dict, # The estimated errors.
}
]
if np.iscomplex(popt[0]) or np.iscomplex(popt[1]):
data[0]["popt_imag"] = {
"A1_imag": np.imag(popt[0]),
"A2_imag": np.imag(popt[1]),
"p1": popt[2],
"p2": popt[3],
} # The imaginary fitting parameters.

df = pd.DataFrame(data, index=["filter"])
return df

Expand All @@ -204,54 +189,24 @@ def build_report(experiment: Experiment, df_aggr: pd.DataFrame) -> Figure:
"""

# Initiate a report object.
report = moduleReport()
fitting_report = ""
# Add general information to the object.
report = ModuleReport()
# Add general information to the table.
report.info_dict["Number of qubits"] = len(experiment.data[0]["samples"][0])
report.info_dict["Number of shots"] = len(experiment.data[0]["samples"])
report.info_dict["runs"] = experiment.extract("samples", "depth", "count")[1][0]
report.info_dict[
"A1_real"
] = f"{df_aggr['popt_real'][0]['A1_real']:.3f} +/- {df_aggr['perr'][0]['A1_err']:.3f}"
report.info_dict[
"p1_real"
] = f"{df_aggr['popt_real'][0]['p1']:.3f} +/- {df_aggr['perr'][0]['p1_err']:.3f}"
report.info_dict[
"A2_real"
] = f"{df_aggr['popt_real'][0]['A2_real']:.3f} +/- {df_aggr['perr'][0]['A2_err']:.3f}"
report.info_dict[
"p2_real"
] = f"{df_aggr['popt_real'][0]['p2']:.3f} +/- {df_aggr['perr'][0]['p2_err']:.3f}"
dfrow = df_aggr.loc["filter"]
popt_pairs = list(dfrow["popt"].items())[::2] + list(dfrow["popt"].items())[1::2]
report.info_dict["Fit"] = "".join(
[f"{key}={number_to_str(value)} " for key, value in popt_pairs]
)
perr_pairs = list(dfrow["perr"].items())[::2] + list(dfrow["perr"].items())[1::2]
report.info_dict["Fitting deviations"] = "".join(
[f"{key}={number_to_str(value)} " for key, value in perr_pairs]
)
# Use the predefined ``scatter_fit_fig`` function from ``basics.utils`` to build the wanted
# plotly figure with the scattered filtered data along with the mean for
# each depth and the exponential fit for the means.
report.all_figures.append(
scatter_fit_fig(
experiment, df_aggr, "depth", "filter", fittingparam_label="popt_real"
)
)
if "popt_imag" in df_aggr:
report.all_figures.append(
scatter_fit_fig(
experiment, df_aggr, "depth", "filter", fittingparam_label="popt_imag"
)
)
report.info_dict[
"A1_imag"
] = f"{df_aggr['popt_imag'][0]['A1_imag']:.3f} +/- {df_aggr['perr'][0]['A1_err']:.3f}"
report.info_dict[
"p1_imag"
] = f"{df_aggr['popt_imag'][0]['p1']:.3f} +/- {df_aggr['perr'][0]['p1_err']:.3f}"
report.info_dict[
"A2_imag"
] = f"{df_aggr['popt_imag'][0]['A2_imag']:.3f} +/- {df_aggr['perr'][0]['A2_err']:.3f}"
report.info_dict[
"p2_imag"
] = f"{df_aggr['popt_imag'][0]['p2']:.3f} +/- {df_aggr['perr'][0]['p2_err']:.3f}"
for key, value in report.info_dict.items():
if isinstance(value, str):
fitting_report += f"q{0}/r{0} | {key}: {value}<br>"
else:
fitting_report += f"q{0}/r{0} | {key}: {value:,.0f}<br>"
# Return the figure the report object builds out of all figures added to the report.
return report.build(), fitting_report
report.all_figures.append(scatter_fit_fig(experiment, df_aggr, "depth", "filter"))

# Return the figure of the report object and the corresponding table.
return report.build()
17 changes: 10 additions & 7 deletions src/qibocal/calibrations/niGSC/basics/circuitfactory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import abc
from copy import deepcopy

import numpy as np
from qibo import gates
Expand Down Expand Up @@ -49,13 +50,15 @@ def __next__(self) -> Circuit:
# Check if the stop critarion is met.
if self.n >= len(self.depths):
raise StopIteration
else:
circuit = self.build_circuit(self.depths[self.n])
self.n += 1
# Distribute the circuit onto the given support.
bigcircuit = Circuit(self.nqubits)
bigcircuit.add(circuit.on_qubits(*self.qubits))
return bigcircuit

circuit = self.build_circuit(self.depths[self.n])
self.n += 1
# Distribute the circuit onto the given support.
circuit_init_kwargs = deepcopy(circuit.init_kwargs)
circuit_init_kwargs["nqubits"] = self.nqubits
bigcircuit = Circuit(**circuit_init_kwargs)
bigcircuit.add(circuit.on_qubits(*self.qubits))
return bigcircuit

def build_circuit(self, depth: int) -> Circuit:
"""Initiate a ``qibo.models.Circuit`` object and fill it with the wanted gates.
Expand Down
40 changes: 22 additions & 18 deletions src/qibocal/calibrations/niGSC/basics/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,25 @@ def __init__(
if circuitfactory is not None and not isinstance(circuitfactory, Iterable):
raise_error(
TypeError,
"given circuit factory has wrong type {}, must be Iterable | None.".format(
type(circuitfactory)
),
f"Invalid type {type(circuitfactory)} for CircuitFactory, must be Iterable or None.",
)
self.circuitfactory = circuitfactory
if data is not None and not isinstance(data, Iterable):
raise_error(
TypeError,
"given data has wrong type {}, must be Iterable | None ".format(
type(data)
),
f"Invalid data type {type(data)}. Data must be Iterable or None.",
)
self.data = data
if nshots is not None and not isinstance(nshots, int):
raise_error(
TypeError,
"given nshots has wrong type {}, must be int | None".format(
type(nshots)
),
f"Invalid nshots type {type(nshots)}. Nshots must be int or None.",
)
self.nshots = nshots
if noise_model is not None and not isinstance(noise_model, NoiseModel):
raise_error(
TypeError,
"given circuit factory has wrong type {}, must be qibo NoiseModel | None .".format(
type(noise_model)
),
f"NoiseModel has wrong type {type(noise_model)}. NoiseModel must be qibo NoiseModel or None .",
)
self.__noise_model = noise_model
self.name = "Abstract"
Expand Down Expand Up @@ -107,24 +99,36 @@ def load(cls, path: str) -> Experiment:
obj = cls(circuitfactory, data=data, nshots=nshots)
return obj

def save(self, path: str | None = None, force=False) -> str:
"""Creates a path if None given and pickles relevant data from ``self.data``
and if ``self.circuitfactory`` is a list that one too.
def save_circuits(self, path: str | None = None, force: bool = False) -> str:
"""Creates a path if None given and pickles ``self.circuitfactory``
if it is a list.

Returns:
(str): The path of stored experiment.
"""

# Check if path to store is given, if not create one. If yes check if the last character
# is a /, if not add it.
# Check if path to store is given, if not create one.
if path is None:
self.path = generate_output_folder(path, force)
else:
self.path = path
# Only if the circuit factory is a list it will be stored.
if isinstance(self.circuitfactory, list):
with open(f"{self.path}/circuits.pkl", "wb") as f:
pickle.dump(self.circuitfactory, f)
return self.path

def save(self, path: str | None = None, force: bool = False) -> str:
"""Creates a path if None given and pickles relevant data from ``self.data``.

Returns:
(str): The path of stored experiment.
"""

# Check if path to store is given, if not create one.
if path is None:
self.path = generate_output_folder(path, force)
else:
self.path = path
# And only if data is not None the data list (full of dicionaries) will be
# stored.
if self.data is not None:
Expand Down
Loading