diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5c681571..1ad0e7a1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/pycqa/isort diff --git a/doc/source/getting-started/interface.rst b/doc/source/getting-started/interface.rst index fadf174eb..c252d2c6d 100644 --- a/doc/source/getting-started/interface.rst +++ b/doc/source/getting-started/interface.rst @@ -1,3 +1,5 @@ +.. _interface: + How to use Qibocal? =================== diff --git a/doc/source/tutorials/api.rst b/doc/source/tutorials/api.rst new file mode 100644 index 000000000..ac750dfa9 --- /dev/null +++ b/doc/source/tutorials/api.rst @@ -0,0 +1,108 @@ +How to use Qibocal as a library +=============================== + +Qibocal also allows executing protocols without the standard :ref:`interface `. + +In the following tutorial we show how to run a single protocol using Qibocal as a library. +For this particular example we will focus on the `single shot classification protocol +`_. + +.. code-block:: python + + from qibocal.protocols.characterization import Operation + from qibolab import create_platform + + # allocate platform + platform = create_platform("....") + # get qubits from platform + qubits = platform.qubits + + # we select the protocol + protocol = Operation.single_shot_classification.value + +``protocol`` is a `Routine `_ object which contains all the necessary +methods to execute the experiment. + +In order to run a protocol the user needs to specify the parameters. +The user can check which parameters need to be provided either by checking the +documentation of the specific protocol or by simply inspecting ``protocol.parameters_type``. +For ``single_shot_classification`` we can pass just the number of shots +in the following way: + +.. code-block:: python + + parameters = experiment.parameters_type.load(dict(nshots=1024)) + + +After defining the parameters, the user can perform the acquisition using +``experiment.acquisition`` which accepts the following parameters: + +* params (`experiment.parameters_type `_): input parameters for the experiment +* platform (`qibolab.platform.Platform `_): Qibolab platform class +* qubits (dict[`QubitId `_, `QubitPairId `_]) dictionary with qubits where the acquisition will run + +and returns the following: + +* data (`experiment.data_type `_): data acquired +* acquisition_time (float): acquisition time on hardware + +.. code-block:: python + + data, acquisition_time = experiment.acquisition(params=parameters, + platform=platform, + qubits=qubits) + + +The user can now use the raw data acquired by the quantum processor to perform +an arbitrary post-processing analysis. This is one of the main advantages of this API +compared to the cli execution. + +The fitting corresponding to the experiment (``experiment.fit``) can be launched in the +following way: + +.. code-block:: python + + fit, fit_time = experiment.fit(data) + +To be more specific the user should pass as input ``data`` which is of type +``experiment.data_type`` and the outputs are the following: + +* fit: (`experiment.results_type `_) input parameters for the experiment +* fit_time (float): post-processing time + + +It is also possible to access the plots and the tables generated in the +report using ``experiment.report`` which accepts the following parameters: + +* data: (`experiment.data_type `_) data structure used by ``experiment`` +* qubit (dict[`QubitId `_, `QubitPairId `_]): qubit / qubit pair to be plotted +* fit: (`experiment.results_type `_): data structure for post-processing used by ``experiment`` + +.. code-block:: python + + # Plot for qubit 0 + qubit = 0 + figs, html_content = experiment.report(data=data, qubit=0, fit=fit) + +``experiment.report`` returns the following: + +* figs: list of plotly figures +* html_content: raw html with additional information usually in the form of a table + +In our case we get the following figure for qubit 0: + +.. code-block:: python + + figs[0] + + +.. image:: classification_plot.png + +and we can render the html content in the following way: + +.. code-block:: python + + import IPython + IPython.display.HTML(html_content) + +.. image:: classification_table.png diff --git a/doc/source/tutorials/classification_plot.png b/doc/source/tutorials/classification_plot.png new file mode 100644 index 000000000..8a1cc60cd Binary files /dev/null and b/doc/source/tutorials/classification_plot.png differ diff --git a/doc/source/tutorials/classification_table.png b/doc/source/tutorials/classification_table.png new file mode 100644 index 000000000..9af9e280e Binary files /dev/null and b/doc/source/tutorials/classification_table.png differ diff --git a/doc/source/tutorials/index.rst b/doc/source/tutorials/index.rst index 35332a383..167a4514d 100644 --- a/doc/source/tutorials/index.rst +++ b/doc/source/tutorials/index.rst @@ -8,4 +8,5 @@ In this section we present code examples from basic to advanced features impleme .. toctree:: :maxdepth: 2 + api protocol diff --git a/serverscripts/qibocal-index-reports.py b/serverscripts/qibocal-index-reports.py index 8f5064080..3f6eb8989 100644 --- a/serverscripts/qibocal-index-reports.py +++ b/serverscripts/qibocal-index-reports.py @@ -16,8 +16,17 @@ "start-time": "-", "end-time": "-", "tag": "-", + "author": "-", +} +REQUIRED_FILE_METADATA = { + "title", + "date", + "platform", + "start-time", + "end-time", + "tag", + "author", } -REQUIRED_FILE_METADATA = {"title", "date", "platform", "start-time" "end-time", "tag"} def meta_from_path(p): @@ -36,17 +45,18 @@ def meta_from_path(p): def register(p): path_meta = meta_from_path(p) - title, date, platform, start_time, end_time, tag = ( + title, date, platform, start_time, end_time, tag, author = ( path_meta["title"], path_meta["date"], path_meta["platform"], path_meta["start-time"], path_meta["end-time"], path_meta["tag"], + path_meta["author"], ) url = ROOT_URL + p.name titlelink = f'{title}' - return (titlelink, date, platform, start_time, end_time, tag) + return (titlelink, date, platform, start_time, end_time, tag, author) def make_index(): diff --git a/serverscripts/web/index.html b/serverscripts/web/index.html index 316835e15..56b41fa02 100644 --- a/serverscripts/web/index.html +++ b/serverscripts/web/index.html @@ -21,7 +21,7 @@ var data = json['data']; var global_table = $('#global').DataTable({ "data": data, - "order": [[2, "desc"]], + "order": [[1, "desc"]], "dom": "frtipl" }); }); @@ -55,6 +55,7 @@

Uploaded Reports

Start-time (UTC) End-time (UTC) Tag + Author diff --git a/src/qibocal/cli/_base.py b/src/qibocal/cli/_base.py index 49fb67693..bad9c2e32 100644 --- a/src/qibocal/cli/_base.py +++ b/src/qibocal/cli/_base.py @@ -1,4 +1,5 @@ """Adds global CLI options.""" +import getpass import pathlib import click @@ -126,11 +127,17 @@ def fit(folder: pathlib.Path, update): type=str, help="Optional tag.", ) -def upload(path, tag): +@click.option( + "--author", + default=getpass.getuser(), + type=str, + help="Default is UID username.", +) +def upload(path, tag, author): """Uploads output folder to server Arguments: - FOLDER: input folder. """ - upload_report(path, tag) + upload_report(path, tag, author) diff --git a/src/qibocal/cli/upload.py b/src/qibocal/cli/upload.py index 0d53ed6db..d0bb3f4de 100644 --- a/src/qibocal/cli/upload.py +++ b/src/qibocal/cli/upload.py @@ -23,12 +23,13 @@ ROOT_URL = "http://login.qrccluster.com:9000/" -def upload_report(path: pathlib.Path, tag: str): +def upload_report(path: pathlib.Path, tag: str, author: str): # load meta and update tag + meta = yaml.safe_load((path / META).read_text()) + meta["author"] = author if tag is not None: - meta = yaml.safe_load((path / META).read_text()) meta["tag"] = tag - (path / META).write_text(json.dumps(meta, indent=4)) + (path / META).write_text(json.dumps(meta, indent=4)) # check the rsync command exists. if not shutil.which("rsync"): diff --git a/src/qibocal/fitting/classifier/data.py b/src/qibocal/fitting/classifier/data.py index 7f5628fbf..34ab33cef 100644 --- a/src/qibocal/fitting/classifier/data.py +++ b/src/qibocal/fitting/classifier/data.py @@ -17,11 +17,10 @@ def generate_models(data, qubit, test_size=0.25): - x_test: Test inputs. - y_test: Test outputs. """ - data0 = data.data[qubit, 0].tolist() - data1 = data.data[qubit, 1].tolist() + qubit_data = data.data[qubit] return train_test_split( - np.array(np.concatenate((data0, data1))), - np.array([0] * len(data0) + [1] * len(data1)), + np.array(qubit_data[["i", "q"]].tolist())[:, :], + np.array(qubit_data[["state"]].tolist())[:, 0], test_size=test_size, random_state=0, shuffle=True, diff --git a/src/qibocal/fitting/classifier/decision_tree.py b/src/qibocal/fitting/classifier/decision_tree.py new file mode 100644 index 000000000..41b80e46b --- /dev/null +++ b/src/qibocal/fitting/classifier/decision_tree.py @@ -0,0 +1,40 @@ +from sklearn.model_selection import GridSearchCV, RepeatedStratifiedKFold +from sklearn.tree import DecisionTreeClassifier + +from . import scikit_utils + + +def constructor(hyperpars): + r"""Return the model class. + + Args: + hyperparams: Model hyperparameters. + """ + return DecisionTreeClassifier().set_params(**hyperpars) + + +def hyperopt(x_train, y_train, _path): + r"""Perform an hyperparameter optimization and return the hyperparameters. + + Args: + x_train: Training inputs. + y_train: Training outputs. + _path (path): Model save path. + + Returns: + Dictionary with model's hyperparameters. + """ + clf = DecisionTreeClassifier() + cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1) + space = {} + space["criterion"] = ["gini", "entropy", "log_loss"] + space["splitter"] = ["best", "random"] + search = GridSearchCV(clf, space, scoring="accuracy", n_jobs=-1, cv=cv) + _ = search.fit(x_train, y_train) + + return search.best_params_ + + +normalize = scikit_utils.scikit_normalize +dump = scikit_utils.scikit_dump +predict_from_file = scikit_utils.scikit_predict diff --git a/src/qibocal/fitting/classifier/qubit_fit.py b/src/qibocal/fitting/classifier/qubit_fit.py index 1525cc6c8..78b436d76 100644 --- a/src/qibocal/fitting/classifier/qubit_fit.py +++ b/src/qibocal/fitting/classifier/qubit_fit.py @@ -4,7 +4,6 @@ import numpy as np import numpy.typing as npt -import skops.io as sio from qibocal.protocols.characterization.utils import cumulative @@ -37,6 +36,9 @@ def hyperopt(_x_train, _y_train, _path): def dump(model, save_path: Path): r"""Dumps the `model` in `save_path`""" + # relative import to reduce overhead when importing qibocal + import skops.io as sio + sio.dump(model, save_path.with_suffix(".skops")) @@ -44,6 +46,9 @@ def predict_from_file(loading_path: Path, input: np.typing.NDArray): r"""This function loads the model saved in `loading_path` and returns the predictions of `input`. """ + # relative import to reduce overhead when importing qibocal + import skops.io as sio + model = sio.load(loading_path, trusted=True) return model.predict(input) diff --git a/src/qibocal/fitting/classifier/run.py b/src/qibocal/fitting/classifier/run.py index c309b83b7..5431d1c49 100644 --- a/src/qibocal/fitting/classifier/run.py +++ b/src/qibocal/fitting/classifier/run.py @@ -23,6 +23,7 @@ "random_forest", "rbf_svm", "qblox_fit", + "decision_tree", ] PRETTY_NAME = [ @@ -35,6 +36,7 @@ "Random Forest", "RBF SVM", "Qblox Fit", + "Decision Tree", ] @@ -63,10 +65,10 @@ def pretty_name(classifier_name: str): class Classifier: - r"""Classs to define the different classifiers used in the benchmarking. + r"""Class to define the different classifiers used in the benchmarking. Args: - mod: Classsification model. + mod: Classification model. base_dir (Path): Where to store the classification results. """ @@ -249,12 +251,9 @@ def train_qubit( classifier = Classifier(mod, qubit_dir) classifier.savedir.mkdir(exist_ok=True) logging.info(f"Training quibt with classifier: {pretty_name(classifier.name)}") - if classifier.name not in cls_data.classifiers_hpars[qubit]: - hyperpars = classifier.hyperopt( - x_train, y_train.astype(np.int64), classifier.savedir - ) - else: - hyperpars = cls_data.classifiers_hpars[qubit][classifier.name] + hyperpars = classifier.hyperopt( + x_train, y_train.astype(np.int64), classifier.savedir + ) hpars_list.append(hyperpars) classifier.dump_hyper(hyperpars) model = classifier.create_model(hyperpars) diff --git a/src/qibocal/fitting/classifier/scikit_utils.py b/src/qibocal/fitting/classifier/scikit_utils.py index 5a7de87d7..60d207f1c 100644 --- a/src/qibocal/fitting/classifier/scikit_utils.py +++ b/src/qibocal/fitting/classifier/scikit_utils.py @@ -30,7 +30,7 @@ def scikit_normalize(constructor): def scikit_dump(model, path: Path): r"""Dumps scikit `model` in `path`""" - initial_type = [("float_input", FloatTensorType([1, 2]))] + initial_type = [("float_input", FloatTensorType([None, 2]))] onx = to_onnx(model, initial_types=initial_type) with open(path.with_suffix(".onnx"), "wb") as f: f.write(onx.SerializeToString()) diff --git a/src/qibocal/protocols/characterization/__init__.py b/src/qibocal/protocols/characterization/__init__.py index 3269d82ce..4db55b857 100644 --- a/src/qibocal/protocols/characterization/__init__.py +++ b/src/qibocal/protocols/characterization/__init__.py @@ -25,9 +25,12 @@ ) from .qubit_spectroscopy import qubit_spectroscopy from .qubit_spectroscopy_ef import qubit_spectroscopy_ef +from .qutrit_classification import qutrit_classification from .rabi.amplitude import rabi_amplitude +from .rabi.amplitude_msr import rabi_amplitude_msr from .rabi.ef import rabi_amplitude_ef from .rabi.length import rabi_length +from .rabi.length_msr import rabi_length_msr from .rabi.length_sequences import rabi_length_sequences from .ramsey import ramsey from .ramsey_msr import ramsey_msr @@ -60,6 +63,8 @@ class Operation(Enum): rabi_amplitude = rabi_amplitude rabi_length = rabi_length rabi_length_sequences = rabi_length_sequences + rabi_amplitude_msr = rabi_amplitude_msr + rabi_length_msr = rabi_length_msr ramsey = ramsey ramsey_msr = ramsey_msr ramsey_sequences = ramsey_sequences @@ -93,5 +98,6 @@ class Operation(Enum): twpa_power = twpa_power rabi_amplitude_ef = rabi_amplitude_ef qubit_spectroscopy_ef = qubit_spectroscopy_ef + qutrit_classification = qutrit_classification resonator_amplitude = resonator_amplitude dispersive_shift_qutrit = dispersive_shift_qutrit diff --git a/src/qibocal/protocols/characterization/classification.py b/src/qibocal/protocols/characterization/classification.py index c2ef639eb..39c15a3ab 100644 --- a/src/qibocal/protocols/characterization/classification.py +++ b/src/qibocal/protocols/characterization/classification.py @@ -7,7 +7,6 @@ import numpy.typing as npt import pandas as pd import plotly.graph_objects as go -from plotly.subplots import make_subplots from qibolab import AcquisitionType, ExecutionParameters from qibolab.platform import Platform from qibolab.pulses import PulseSequence @@ -26,33 +25,34 @@ from qibocal.auto.serialize import serialize from qibocal.fitting.classifier import run from qibocal.protocols.characterization.utils import ( + LEGEND_FONT_SIZE, + MESH_SIZE, + TITLE_SIZE, + evaluate_grid, get_color_state0, - get_color_state1, + plot_results, table_dict, table_html, ) -MESH_SIZE = 50 -MARGIN = 0 -COLUMNWIDTH = 600 ROC_LENGHT = 800 ROC_WIDTH = 800 -LEGEND_FONT_SIZE = 20 -TITLE_SIZE = 25 -SPACING = 0.1 +DEFAULT_CLASSIFIER = "qubit_fit" @dataclass class SingleShotClassificationParameters(Parameters): """SingleShotClassification runcard inputs.""" - classifiers_list: Optional[list[str]] = field(default_factory=lambda: ["qubit_fit"]) + classifiers_list: Optional[list[str]] = field( + default_factory=lambda: [DEFAULT_CLASSIFIER] + ) """List of models to classify the qubit states""" savedir: Optional[str] = " " """Dumping folder of the classification results""" -ClassificationType = np.dtype([("i", np.float64), ("q", np.float64)]) +ClassificationType = np.dtype([("i", np.float64), ("q", np.float64), ("state", int)]) """Custom dtype for rabi amplitude.""" @@ -60,13 +60,13 @@ class SingleShotClassificationParameters(Parameters): class SingleShotClassificationData(Data): nshots: int """Number of shots.""" - classifiers_hpars: dict[QubitId, dict] - """Models' hyperparameters""" savedir: str """Dumping folder of the classification results""" data: dict[QubitId, npt.NDArray] = field(default_factory=dict) """Raw data acquired.""" - classifiers_list: Optional[list[str]] = field(default_factory=lambda: ["qubit_fit"]) + classifiers_list: Optional[list[str]] = field( + default_factory=lambda: [DEFAULT_CLASSIFIER] + ) """List of models to classify the qubit states""" @@ -74,36 +74,36 @@ class SingleShotClassificationData(Data): class SingleShotClassificationResults(Results): """SingleShotClassification outputs.""" - y_tests: dict[QubitId, list] - """States of the testing set.""" - x_tests: dict[QubitId, list] - """I,Q couples to evaluate accuracy and test time.""" names: list """List of models name.""" - threshold: dict[QubitId, float] - """Threshold for classification.""" - rotation_angle: dict[QubitId, float] - """Threshold for classification.""" - mean_gnd_states: dict[QubitId, list[float]] - """Centroid of the ground state blob.""" - mean_exc_states: dict[QubitId, list[float]] - """Centroid of the excited state blob.""" - fidelity: dict[QubitId, float] - """Fidelity evaluated only with the `qubit_fit` model.""" - assignment_fidelity: dict[QubitId, float] - """Assignment fidelity evaluated only with the `qubit_fit` model.""" savedir: str """Dumping folder of the classification results.""" y_preds: dict[QubitId, list] """Models' predictions of the test set.""" grid_preds: dict[QubitId, list] """Models' prediction of the contour grid.""" + threshold: dict[QubitId, float] = field(default_factory=dict) + """Threshold for classification.""" + rotation_angle: dict[QubitId, float] = field(default_factory=dict) + """Threshold for classification.""" + mean_gnd_states: dict[QubitId, list[float]] = field(default_factory=dict) + """Centroid of the ground state blob.""" + mean_exc_states: dict[QubitId, list[float]] = field(default_factory=dict) + """Centroid of the excited state blob.""" + fidelity: dict[QubitId, float] = field(default_factory=dict) + """Fidelity evaluated only with the `qubit_fit` model.""" + assignment_fidelity: dict[QubitId, float] = field(default_factory=dict) + """Assignment fidelity evaluated only with the `qubit_fit` model.""" models: dict[QubitId, list] = field(default_factory=list) """List of trained classification models.""" benchmark_table: Optional[dict[QubitId, pd.DataFrame]] = field(default_factory=dict) """Benchmark tables.""" classifiers_hpars: Optional[dict[QubitId, dict]] = field(default_factory=dict) """Classifiers hyperparameters.""" + x_tests: dict[QubitId, list] = field(default_factory=dict) + """Test set.""" + y_tests: dict[QubitId, list] = field(default_factory=dict) + """Test set.""" def save(self, path): classifiers = run.import_classifiers(self.names) @@ -171,7 +171,6 @@ def _acquisition( RX_pulses = {} ro_pulses = {} - hpars = {} for qubit in qubits: RX_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) ro_pulses[qubit] = platform.create_qubit_readout_pulse( @@ -181,12 +180,10 @@ def _acquisition( state0_sequence.add(ro_pulses[qubit]) state1_sequence.add(RX_pulses[qubit]) state1_sequence.add(ro_pulses[qubit]) - hpars[qubit] = qubits[qubit].classifiers_hpars - # create a DataUnits object to store the results + data = SingleShotClassificationData( nshots=params.nshots, classifiers_list=params.classifiers_list, - classifiers_hpars=hpars, savedir=params.savedir, ) @@ -204,7 +201,9 @@ def _acquisition( for qubit in qubits: result = state0_results[ro_pulses[qubit].serial] data.register_qubit( - ClassificationType, (qubit, 0), dict(i=result.voltage_i, q=result.voltage_q) + ClassificationType, + (qubit), + dict(i=result.voltage_i, q=result.voltage_q, state=[0] * params.nshots), ) # execute the second pulse sequence state1_results = platform.execute_pulse_sequence( @@ -219,7 +218,9 @@ def _acquisition( for qubit in qubits: result = state1_results[ro_pulses[qubit].serial] data.register_qubit( - ClassificationType, (qubit, 1), dict(i=result.voltage_i, q=result.voltage_q) + ClassificationType, + (qubit), + dict(i=result.voltage_i, q=result.voltage_q, state=[1] * params.nshots), ) return data @@ -242,6 +243,7 @@ def _fit(data: SingleShotClassificationData) -> SingleShotClassificationResults: y_test_predict = {} grid_preds_dict = {} for qubit in qubits: + qubit_data = data.data[qubit] benchmark_table, y_test, x_test, models, names, hpars_list = run.train_qubit( data, qubit ) @@ -252,10 +254,8 @@ def _fit(data: SingleShotClassificationData) -> SingleShotClassificationResults: hpars[qubit] = {} y_preds = [] grid_preds = [] - state0_data = data.data[qubit, 0] - state1_data = data.data[qubit, 1] - grid = evaluate_grid(state0_data, state1_data) + grid = evaluate_grid(qubit_data) for i, model_name in enumerate(names): hpars[qubit][model_name] = hpars_list[i] try: @@ -298,143 +298,21 @@ def _fit(data: SingleShotClassificationData) -> SingleShotClassificationResults: def _plot( data: SingleShotClassificationData, qubit, fit: SingleShotClassificationResults ): - figures = [] fitting_report = "" models_name = data.classifiers_list - state0_data = data.data[qubit, 0] - state1_data = data.data[qubit, 1] - grid = evaluate_grid(state0_data, state1_data) - - fig = make_subplots( - rows=1, - cols=len(models_name), - horizontal_spacing=SPACING * 3 / len(models_name) * 3, - vertical_spacing=SPACING, - subplot_titles=[run.pretty_name(model) for model in models_name], - column_width=[COLUMNWIDTH] * len(models_name), - ) - - if len(models_name) != 1 and fit is not None: - fig_roc = go.Figure() - fig_roc.add_shape( - type="line", line=dict(dash="dash"), x0=0.0, x1=1.0, y0=0.0, y1=1.0 - ) - fig_benchmarks = make_subplots( - rows=1, - cols=3, - horizontal_spacing=SPACING, - vertical_spacing=SPACING, - subplot_titles=("accuracy", "training time (s)", "testing time (s)"), - # pylint: disable=E1101 - ) - + figures = plot_results(data, qubit, 2, fit) if fit is not None: y_test = fit.y_tests[qubit] - x_test = fit.x_tests[qubit] - - for i, model in enumerate(models_name): - if fit is not None: - y_pred = fit.y_preds[qubit][i] - predictions = fit.grid_preds[qubit][i] - fig.add_trace( - go.Contour( - x=grid[:, 0], - y=grid[:, 1], - z=np.array(predictions).flatten(), - showscale=False, - colorscale=[get_color_state0(i), get_color_state1(i)], - opacity=0.2, - name="Score", - hoverinfo="skip", - showlegend=True, - ), - row=1, - col=i + 1, - ) - - model = run.pretty_name(model) - max_x = max(grid[:, 0]) - max_y = max(grid[:, 1]) - min_x = min(grid[:, 0]) - min_y = min(grid[:, 1]) - - fig.add_trace( - go.Scatter( - x=state0_data["i"], - y=state0_data["q"], - name=f"{model}: state 0", - legendgroup=f"{model}: state 0", - mode="markers", - showlegend=True, - opacity=0.7, - marker=dict(size=3, color=get_color_state0(i)), - ), - row=1, - col=i + 1, - ) - - fig.add_trace( - go.Scatter( - x=state1_data["i"], - y=state1_data["q"], - name=f"{model}: state 1", - legendgroup=f"{model}: state 1", - mode="markers", - showlegend=True, - opacity=0.7, - marker=dict(size=3, color=get_color_state1(i)), - ), - row=1, - col=i + 1, - ) + y_pred = fit.y_preds[qubit] - fig.add_trace( - go.Scatter( - x=[np.average(state0_data["i"])], - y=[np.average(state0_data["q"])], - name=f"{model}: state 0", - legendgroup=f"{model}: state 0", - showlegend=False, - mode="markers", - marker=dict(size=10, color=get_color_state0(i)), - ), - row=1, - col=i + 1, - ) - - fig.add_trace( - go.Scatter( - x=[np.average(state1_data["i"])], - y=[np.average(state1_data["q"])], - name=f"{model}: state 1", - legendgroup=f"{model}: state 1", - showlegend=False, - mode="markers", - marker=dict(size=10, color=get_color_state1(i)), - ), - row=1, - col=i + 1, - ) - fig.update_xaxes( - title_text=f"i (V)", - range=[min_x, max_x], - row=1, - col=i + 1, - autorange=False, - rangeslider=dict(visible=False), - ) - fig.update_yaxes( - title_text="q (V)", - range=[min_y, max_y], - scaleanchor="x", - scaleratio=1, - row=1, - col=i + 1, - ) - - if fit is not None: - if len(models_name) != 1: - # Evaluate the ROC curve + if len(models_name) != 1: + # Evaluate the ROC curve + fig_roc = go.Figure() + fig_roc.add_shape( + type="line", line=dict(dash="dash"), x0=0.0, x1=1.0, y0=0.0, y1=1.0 + ) + for i, model in enumerate(models_name): + y_pred = fit.y_preds[qubit][i] fpr, tpr, _ = roc_curve(y_test, y_pred) auc_score = roc_auc_score(y_test, y_pred) name = f"{model} (AUC={auc_score:.2f})" @@ -447,109 +325,45 @@ def _plot( marker=dict(size=3, color=get_color_state0(i)), ) ) - fig_benchmarks.add_trace( - go.Scatter( - x=[model], - y=[fit.benchmark_table[qubit][i][0]], - mode="markers", - showlegend=False, - marker=dict(size=10, color=get_color_state1(i)), - ), - row=1, - col=1, - ) - - fig_benchmarks.add_trace( - go.Scatter( - x=[model], - y=[fit.benchmark_table[qubit][i][2]], - mode="markers", - showlegend=False, - marker=dict(size=10, color=get_color_state1(i)), - ), - row=1, - col=2, - ) - - fig_benchmarks.add_trace( - go.Scatter( - x=[model], - y=[fit.benchmark_table[qubit][i][1]], - mode="markers", - showlegend=False, - marker=dict(size=10, color=get_color_state1(i)), - ), - row=1, - col=3, - ) - - fig_benchmarks.update_yaxes(type="log", row=1, col=2) - fig_benchmarks.update_yaxes(type="log", row=1, col=3) - fig_benchmarks.update_layout( - autosize=False, - height=COLUMNWIDTH, - width=COLUMNWIDTH * 3, - title=dict(text="Benchmarks", font=dict(size=TITLE_SIZE)), - ) - fig_roc.update_layout( - width=ROC_WIDTH, - height=ROC_LENGHT, - title=dict(text="ROC curves", font=dict(size=TITLE_SIZE)), - legend=dict(font=dict(size=LEGEND_FONT_SIZE)), - ) - fig_roc.update_xaxes( - title_text=f"False Positive Rate", - range=[0, 1], - ) - fig_roc.update_yaxes( - title_text="True Positive Rate", - range=[0, 1], - ) - - if models_name[i] == "qubit_fit": - fitting_report = table_html( - table_dict( - qubit, - [ - "Average State 0", - "Average State 1", - "Rotational Angle", - "Threshold", - "Readout Fidelity", - "Assignment Fidelity", - ], - [ - np.round(fit.mean_gnd_states[qubit], 3), - np.round(fit.mean_exc_states[qubit], 3), - np.round(fit.rotation_angle[qubit], 3), - np.round(fit.threshold[qubit], 6), - np.round(fit.fidelity[qubit], 3), - np.round(fit.assignment_fidelity[qubit], 3), - ], - ) + fig_roc.update_layout( + width=ROC_WIDTH, + height=ROC_LENGHT, + title=dict(text="ROC curves", font=dict(size=TITLE_SIZE)), + legend=dict(font=dict(size=LEGEND_FONT_SIZE)), + ) + fig_roc.update_xaxes( + title_text=f"False Positive Rate", + range=[0, 1], + ) + fig_roc.update_yaxes( + title_text="True Positive Rate", + range=[0, 1], + ) + figures.append(fig_roc) + + if "qubit_fit" in models_name: + fitting_report = table_html( + table_dict( + qubit, + [ + "Average State 0", + "Average State 1", + "Rotational Angle", + "Threshold", + "Readout Fidelity", + "Assignment Fidelity", + ], + [ + np.round(fit.mean_gnd_states[qubit], 3), + np.round(fit.mean_exc_states[qubit], 3), + np.round(fit.rotation_angle[qubit], 3), + np.round(fit.threshold[qubit], 6), + np.round(fit.fidelity[qubit], 3), + np.round(fit.assignment_fidelity[qubit], 3), + ], ) + ) - fig.update_layout( - uirevision="0", # ``uirevision`` allows zooming while live plotting - autosize=False, - height=COLUMNWIDTH, - width=COLUMNWIDTH * len(models_name), - title=dict(text="Results", font=dict(size=TITLE_SIZE)), - legend=dict( - orientation="h", - yanchor="bottom", - xanchor="left", - y=-0.3, - x=0, - itemsizing="constant", - font=dict(size=LEGEND_FONT_SIZE), - ), - ) - figures.append(fig) - - if len(models_name) != 1 and fit is not None: - figures.append(fig_roc) - figures.append(fig_benchmarks) return figures, fitting_report @@ -560,56 +374,9 @@ def _update( update.threshold(results.threshold[qubit], platform, qubit) update.mean_gnd_states(results.mean_gnd_states[qubit], platform, qubit) update.mean_exc_states(results.mean_exc_states[qubit], platform, qubit) - update.classifiers_hpars(results.classifiers_hpars[qubit], platform, qubit) update.readout_fidelity(results.fidelity[qubit], platform, qubit) update.assignment_fidelity(results.assignment_fidelity[qubit], platform, qubit) single_shot_classification = Routine(_acquisition, _fit, _plot, _update) - - -def evaluate_grid( - state0_data: npt.NDArray, - state1_data: npt.NDArray, -): - """ - This function returns a matrix grid evaluated from - the datapoints `state0_data` and `state1_data`. - """ - max_x = ( - max( - 0, - state0_data["i"].max(), - state1_data["i"].max(), - ) - + MARGIN - ) - max_y = ( - max( - 0, - state0_data["q"].max(), - state1_data["q"].max(), - ) - + MARGIN - ) - min_x = ( - min( - 0, - state0_data["i"].min(), - state1_data["i"].min(), - ) - - MARGIN - ) - min_y = ( - min( - 0, - state0_data["q"].min(), - state1_data["q"].min(), - ) - - MARGIN - ) - i_values, q_values = np.meshgrid( - np.linspace(min_x, max_x, num=MESH_SIZE), - np.linspace(min_y, max_y, num=MESH_SIZE), - ) - return np.vstack([i_values.ravel(), q_values.ravel()]).T +"""Qubit classification routine object.""" diff --git a/src/qibocal/protocols/characterization/qutrit_classification.py b/src/qibocal/protocols/characterization/qutrit_classification.py new file mode 100644 index 000000000..071b21ba5 --- /dev/null +++ b/src/qibocal/protocols/characterization/qutrit_classification.py @@ -0,0 +1,175 @@ +from dataclasses import dataclass, field +from typing import Optional + +import numpy as np +from qibolab import AcquisitionType, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence + +from qibocal.auto.operation import Qubits, Routine +from qibocal.fitting.classifier import run +from qibocal.protocols.characterization.classification import ( + ClassificationType, + SingleShotClassificationData, + SingleShotClassificationParameters, + SingleShotClassificationResults, +) +from qibocal.protocols.characterization.utils import ( + MESH_SIZE, + evaluate_grid, + plot_results, +) + +COLUMNWIDTH = 600 +LEGEND_FONT_SIZE = 20 +TITLE_SIZE = 25 +SPACING = 0.1 +DEFAULT_CLASSIFIER = "naive_bayes" + + +@dataclass +class QutritClassificationParameters(SingleShotClassificationParameters): + """SingleShotClassification runcard inputs.""" + + classifiers_list: Optional[list[str]] = field( + default_factory=lambda: [DEFAULT_CLASSIFIER] + ) + """List of models to classify the qubit states""" + + +@dataclass +class QutritClassificationData(SingleShotClassificationData): + classifiers_list: Optional[list[str]] = field( + default_factory=lambda: [DEFAULT_CLASSIFIER] + ) + """List of models to classify the qubit states""" + + +def _acquisition( + params: QutritClassificationParameters, + platform: Platform, + qubits: Qubits, +) -> QutritClassificationData: + """ + This Routine prepares the qubits in 0,1 and 2 states and measures their + respective I, Q values. + + Args: + nshots (int): number of times the pulse sequence will be repeated. + classifiers (list): list of classifiers, the available ones are: + - naive_bayes + - nn + - random_forest + - decision_tree + The default value is `["naive_bayes"]`. + savedir (str): Dumping folder of the classification results. + If not given, the dumping folder will be the report one. + relaxation_time (float): Relaxation time. + """ + + # taking advantage of multiplexing, apply the same set of gates to all qubits in parallel + states_sequences = [PulseSequence() for _ in range(3)] + ro_pulses = {} + for qubit in qubits: + rx_pulse = platform.create_RX_pulse(qubit, start=0) + rx12_pulse = platform.create_RX12_pulse(qubit, start=rx_pulse.finish) + drive_pulses = [rx_pulse, rx12_pulse] + ro_pulses[qubit] = [] + for i, sequence in enumerate(states_sequences): + sequence.add(*drive_pulses[:i]) + start = drive_pulses[i - 1].finish if i != 0 else 0 + ro_pulses[qubit].append( + platform.create_qubit_readout_pulse(qubit, start=start) + ) + sequence.add(ro_pulses[qubit][-1]) + + data = QutritClassificationData( + nshots=params.nshots, + classifiers_list=params.classifiers_list, + savedir=params.savedir, + ) + states_results = [] + for sequence in states_sequences: + states_results.append( + platform.execute_pulse_sequence( + sequence, + ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + ), + ) + ) + + for qubit in qubits: + for state, state_result in enumerate(states_results): + result = state_result[ro_pulses[qubit][state].serial] + data.register_qubit( + ClassificationType, + (qubit), + dict( + state=[state] * params.nshots, + i=result.voltage_i, + q=result.voltage_q, + ), + ) + + return data + + +def _fit(data: QutritClassificationData) -> SingleShotClassificationResults: + qubits = data.qubits + + benchmark_tables = {} + models_dict = {} + y_tests = {} + x_tests = {} + hpars = {} + y_test_predict = {} + grid_preds_dict = {} + for qubit in qubits: + qubit_data = data.data[qubit] + benchmark_table, y_test, x_test, models, names, hpars_list = run.train_qubit( + data, qubit + ) + benchmark_tables[qubit] = benchmark_table.values.tolist() + models_dict[qubit] = models + y_tests[qubit] = y_test.tolist() + x_tests[qubit] = x_test.tolist() + hpars[qubit] = {} + y_preds = [] + grid_preds = [] + + grid = evaluate_grid(qubit_data) + for i, model_name in enumerate(names): + hpars[qubit][model_name] = hpars_list[i] + try: + y_preds.append(models[i].predict_proba(x_test)[:, 1].tolist()) + except AttributeError: + y_preds.append(models[i].predict(x_test).tolist()) + grid_preds.append( + np.round(np.reshape(models[i].predict(grid), (MESH_SIZE, MESH_SIZE))) + .astype(np.int64) + .tolist() + ) + y_test_predict[qubit] = y_preds + grid_preds_dict[qubit] = grid_preds + return SingleShotClassificationResults( + benchmark_table=benchmark_tables, + names=names, + classifiers_hpars=hpars, + models=models_dict, + savedir=data.savedir, + y_preds=y_test_predict, + grid_preds=grid_preds_dict, + ) + + +def _plot(data: QutritClassificationData, qubit, fit: SingleShotClassificationResults): + figures = plot_results(data, qubit, 3, fit) + fitting_report = "" + return figures, fitting_report + + +qutrit_classification = Routine(_acquisition, _fit, _plot) +"""Qutrit classification Routine object.""" diff --git a/src/qibocal/protocols/characterization/rabi/amplitude.py b/src/qibocal/protocols/characterization/rabi/amplitude.py index c833935b0..15a7ebac6 100644 --- a/src/qibocal/protocols/characterization/rabi/amplitude.py +++ b/src/qibocal/protocols/characterization/rabi/amplitude.py @@ -15,6 +15,7 @@ from qibocal.auto.operation import Data, Parameters, Qubits, Results, Routine from qibocal.config import log +from ..utils import chi2_reduced from . import utils @@ -36,16 +37,17 @@ class RabiAmplitudeParameters(Parameters): class RabiAmplitudeResults(Results): """RabiAmplitude outputs.""" - amplitude: dict[QubitId, float] = field(metadata=dict(update="drive_amplitude")) + amplitude: dict[QubitId, tuple[float, Optional[float]]] """Drive amplitude for each qubit.""" - length: dict[QubitId, float] = field(metadata=dict(update="drive_length")) + length: dict[QubitId, tuple[float, Optional[float]]] """Drive pulse duration. Same for all qubits.""" fitted_parameters: dict[QubitId, dict[str, float]] """Raw fitted parameters.""" + chi2: dict[QubitId, tuple[float, Optional[float]]] = field(default_factory=dict) RabiAmpType = np.dtype( - [("amp", np.float64), ("msr", np.float64), ("phase", np.float64)] + [("amp", np.float64), ("prob", np.float64), ("error", np.float64)] ) """Custom dtype for rabi amplitude.""" @@ -100,9 +102,6 @@ def _acquisition( type=SweeperType.FACTOR, ) - # create a DataUnits object to store the results, - # DataUnits stores by default MSR, phase, i, q - # additionally include qubit drive pulse amplitude data = RabiAmplitudeData(durations=durations) # sweep the parameter @@ -111,21 +110,20 @@ def _acquisition( ExecutionParameters( nshots=params.nshots, relaxation_time=params.relaxation_time, - acquisition_type=AcquisitionType.INTEGRATION, - averaging_mode=AveragingMode.CYCLIC, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, ), sweeper, ) for qubit in qubits: - # average msr, phase, i and q over the number of shots defined in the runcard - result = results[ro_pulses[qubit].serial] + prob = results[qubit].probability(state=1) data.register_qubit( RabiAmpType, (qubit), dict( amp=qd_pulses[qubit].amplitude * qd_pulse_amplitude_range, - msr=result.magnitude, - phase=result.phase, + prob=prob.tolist(), + error=np.sqrt(prob * (1 - prob) / params.nshots).tolist(), ), ) return data @@ -137,19 +135,13 @@ def _fit(data: RabiAmplitudeData) -> RabiAmplitudeResults: pi_pulse_amplitudes = {} fitted_parameters = {} + chi2 = {} for qubit in qubits: qubit_data = data[qubit] - rabi_parameter = qubit_data.amp - voltages = qubit_data.msr - - y_min = np.min(voltages) - y_max = np.max(voltages) - x_min = np.min(rabi_parameter) - x_max = np.max(rabi_parameter) - x = (rabi_parameter - x_min) / (x_max - x_min) - y = (voltages - y_min) / (y_max - y_min) + x = qubit_data.amp + y = qubit_data.prob # Guessing period using fourier transform ft = np.fft.rfft(y) @@ -158,9 +150,9 @@ def _fit(data: RabiAmplitudeData) -> RabiAmplitudeResults: index = local_maxima[0] if len(local_maxima) > 0 else None # 0.5 hardcoded guess for less than one oscillation f = x[index] / (x[1] - x[0]) if index is not None else 0.5 - pguess = [0.5, 1, f, np.pi / 2] + pguess = [0.5, 0.5, 1 / f, np.pi / 2] try: - popt, _ = curve_fit( + popt, perr = curve_fit( utils.rabi_amplitude_fit, x, y, @@ -170,29 +162,34 @@ def _fit(data: RabiAmplitudeData) -> RabiAmplitudeResults: [0, 0, 0, -np.pi], [1, 1, np.inf, np.pi], ), + sigma=qubit_data.error, ) - translated_popt = [ - y_min + (y_max - y_min) * popt[0], - (y_max - y_min) * popt[1], - popt[2] / (x_max - x_min), - popt[3] - 2 * np.pi * x_min / (x_max - x_min) * popt[2], - ] - pi_pulse_parameter = np.abs((1.0 / translated_popt[2]) / 2) + perr = np.sqrt(np.diag(perr)) + pi_pulse_parameter = np.abs(popt[2] / 2) except: log.warning("rabi_fit: the fitting was not succesful") pi_pulse_parameter = 0 - fitted_parameters = [0] * 4 - - pi_pulse_amplitudes[qubit] = pi_pulse_parameter - fitted_parameters[qubit] = translated_popt - - return RabiAmplitudeResults(pi_pulse_amplitudes, data.durations, fitted_parameters) + popt = [0] * 4 + perr = [1] * 4 + + pi_pulse_amplitudes[qubit] = (pi_pulse_parameter, perr[2] / 2) + fitted_parameters[qubit] = popt.tolist() + durations = {key: (value, 0) for key, value in data.durations.items()} + chi2[qubit] = ( + chi2_reduced( + y, + utils.rabi_amplitude_fit(x, *popt), + qubit_data.error, + ), + np.sqrt(2 / len(y)), + ) + return RabiAmplitudeResults(pi_pulse_amplitudes, durations, fitted_parameters, chi2) def _plot(data: RabiAmplitudeData, qubit, fit: RabiAmplitudeResults = None): """Plotting function for RabiAmplitude.""" - return utils.plot(data, qubit, fit) + return utils.plot_probabilities(data, qubit, fit) def _update(results: RabiAmplitudeResults, platform: Platform, qubit: QubitId): diff --git a/src/qibocal/protocols/characterization/rabi/amplitude_msr.py b/src/qibocal/protocols/characterization/rabi/amplitude_msr.py new file mode 100644 index 000000000..ce0b1ca9c --- /dev/null +++ b/src/qibocal/protocols/characterization/rabi/amplitude_msr.py @@ -0,0 +1,183 @@ +from dataclasses import dataclass + +import numpy as np +from qibolab import AcquisitionType, AveragingMode, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence +from qibolab.qubits import QubitId +from qibolab.sweeper import Parameter, Sweeper, SweeperType +from scipy.optimize import curve_fit +from scipy.signal import find_peaks + +from qibocal import update +from qibocal.auto.operation import Qubits, Routine +from qibocal.config import log +from qibocal.protocols.characterization.rabi.amplitude import ( + RabiAmplitudeData, + RabiAmplitudeParameters, + RabiAmplitudeResults, +) + +from . import utils + + +@dataclass +class RabiAmplitudeVoltParameters(RabiAmplitudeParameters): + """RabiAmplitude runcard inputs.""" + + +@dataclass +class RabiAmplitudeVoltResults(RabiAmplitudeResults): + """RabiAmplitude outputs.""" + + +RabiAmpVoltType = np.dtype( + [("amp", np.float64), ("msr", np.float64), ("phase", np.float64)] +) +"""Custom dtype for rabi amplitude.""" + + +@dataclass +class RabiAmplitudeVoltData(RabiAmplitudeData): + """RabiAmplitude data acquisition.""" + + +def _acquisition( + params: RabiAmplitudeVoltParameters, platform: Platform, qubits: Qubits +) -> RabiAmplitudeVoltData: + r""" + Data acquisition for Rabi experiment sweeping amplitude. + In the Rabi experiment we apply a pulse at the frequency of the qubit and scan the drive pulse amplitude + to find the drive pulse amplitude that creates a rotation of a desired angle. + """ + + # create a sequence of pulses for the experiment + sequence = PulseSequence() + qd_pulses = {} + ro_pulses = {} + durations = {} + for qubit in qubits: + qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) + if params.pulse_length is not None: + qd_pulses[qubit].duration = params.pulse_length + + durations[qubit] = qd_pulses[qubit].duration + ro_pulses[qubit] = platform.create_qubit_readout_pulse( + qubit, start=qd_pulses[qubit].finish + ) + sequence.add(qd_pulses[qubit]) + sequence.add(ro_pulses[qubit]) + + # define the parameter to sweep and its range: + # qubit drive pulse amplitude + qd_pulse_amplitude_range = np.arange( + params.min_amp_factor, + params.max_amp_factor, + params.step_amp_factor, + ) + sweeper = Sweeper( + Parameter.amplitude, + qd_pulse_amplitude_range, + [qd_pulses[qubit] for qubit in qubits], + type=SweeperType.FACTOR, + ) + + data = RabiAmplitudeVoltData(durations=durations) + + # sweep the parameter + results = platform.sweep( + sequence, + ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ), + sweeper, + ) + for qubit in qubits: + result = results[ro_pulses[qubit].serial] + data.register_qubit( + RabiAmpVoltType, + (qubit), + dict( + amp=qd_pulses[qubit].amplitude * qd_pulse_amplitude_range, + msr=result.magnitude, + phase=result.phase, + ), + ) + return data + + +def _fit(data: RabiAmplitudeVoltData) -> RabiAmplitudeVoltResults: + """Post-processing for RabiAmplitude.""" + qubits = data.qubits + + pi_pulse_amplitudes = {} + fitted_parameters = {} + + for qubit in qubits: + qubit_data = data[qubit] + + rabi_parameter = qubit_data.amp + voltages = qubit_data.msr + + y_min = np.min(voltages) + y_max = np.max(voltages) + x_min = np.min(rabi_parameter) + x_max = np.max(rabi_parameter) + x = (rabi_parameter - x_min) / (x_max - x_min) + y = (voltages - y_min) / (y_max - y_min) + + # Guessing period using fourier transform + ft = np.fft.rfft(y) + mags = abs(ft) + local_maxima = find_peaks(mags, threshold=10)[0] + index = local_maxima[0] if len(local_maxima) > 0 else None + # 0.5 hardcoded guess for less than one oscillation + f = x[index] / (x[1] - x[0]) if index is not None else 0.5 + pguess = [0.5, 1, 1 / f, np.pi / 2] + try: + popt, _ = curve_fit( + utils.rabi_amplitude_fit, + x, + y, + p0=pguess, + maxfev=100000, + bounds=( + [0, 0, 0, -np.pi], + [1, 1, np.inf, np.pi], + ), + ) + translated_popt = [ # Change it according to fit function changes + y_min + (y_max - y_min) * popt[0], + (y_max - y_min) * popt[1], + popt[2] * (x_max - x_min), + popt[3] - 2 * np.pi * x_min / (x_max - x_min) / popt[2], + ] + pi_pulse_parameter = np.abs((translated_popt[2]) / 2) + + except: + log.warning("rabi_fit: the fitting was not succesful") + pi_pulse_parameter = 0 + fitted_parameters = [0] * 4 + + pi_pulse_amplitudes[qubit] = pi_pulse_parameter + fitted_parameters[qubit] = translated_popt + + return RabiAmplitudeVoltResults( + pi_pulse_amplitudes, data.durations, fitted_parameters + ) + + +def _plot(data: RabiAmplitudeVoltData, qubit, fit: RabiAmplitudeVoltResults = None): + """Plotting function for RabiAmplitude.""" + return utils.plot(data, qubit, fit) + + +def _update(results: RabiAmplitudeVoltResults, platform: Platform, qubit: QubitId): + update.drive_amplitude(results.amplitude[qubit], platform, qubit) + + +rabi_amplitude_msr = Routine(_acquisition, _fit, _plot, _update) +"""RabiAmplitude Routine object.""" diff --git a/src/qibocal/protocols/characterization/rabi/ef.py b/src/qibocal/protocols/characterization/rabi/ef.py index bff13b562..ac632be4a 100644 --- a/src/qibocal/protocols/characterization/rabi/ef.py +++ b/src/qibocal/protocols/characterization/rabi/ef.py @@ -10,21 +10,21 @@ from qibocal import update from qibocal.auto.operation import Qubits, Routine -from . import amplitude, utils +from . import amplitude_msr, utils @dataclass -class RabiAmplitudeEFParameters(amplitude.RabiAmplitudeParameters): +class RabiAmplitudeEFParameters(amplitude_msr.RabiAmplitudeVoltParameters): """RabiAmplitudeEF runcard inputs.""" @dataclass -class RabiAmplitudeEFResults(amplitude.RabiAmplitudeResults): +class RabiAmplitudeEFResults(amplitude_msr.RabiAmplitudeVoltResults): """RabiAmplitudeEF outputs.""" @dataclass -class RabiAmplitudeEFData(amplitude.RabiAmplitudeData): +class RabiAmplitudeEFData(amplitude_msr.RabiAmplitudeVoltData): """RabiAmplitude data acquisition.""" @@ -76,9 +76,6 @@ def _acquisition( type=SweeperType.FACTOR, ) - # create a DataUnits object to store the results, - # DataUnits stores by default MSR, phase, i, q - # additionally include qubit drive pulse amplitude data = RabiAmplitudeEFData(durations=durations) # sweep the parameter @@ -93,10 +90,9 @@ def _acquisition( sweeper, ) for qubit in qubits: - # average msr, phase, i and q over the number of shots defined in the runcard result = results[ro_pulses[qubit].serial] data.register_qubit( - amplitude.RabiAmpType, + amplitude_msr.RabiAmpVoltType, (qubit), dict( amp=qd_pulses[qubit].amplitude * qd_pulse_amplitude_range, @@ -116,9 +112,9 @@ def _plot(data: RabiAmplitudeEFData, qubit, fit: RabiAmplitudeEFResults = None): def _update(results: RabiAmplitudeEFResults, platform: Platform, qubit: QubitId): - """Update RX2 amplitude""" + """Update RX2 amplitude_msr""" update.drive_12_amplitude(results.amplitude[qubit], platform, qubit) -rabi_amplitude_ef = Routine(_acquisition, amplitude._fit, _plot, _update) +rabi_amplitude_ef = Routine(_acquisition, amplitude_msr._fit, _plot, _update) """RabiAmplitudeEF Routine object.""" diff --git a/src/qibocal/protocols/characterization/rabi/length.py b/src/qibocal/protocols/characterization/rabi/length.py index 3ce29eaac..83bb332be 100644 --- a/src/qibocal/protocols/characterization/rabi/length.py +++ b/src/qibocal/protocols/characterization/rabi/length.py @@ -15,6 +15,7 @@ from qibocal.auto.operation import Data, Parameters, Qubits, Results, Routine from qibocal.config import log +from ..utils import chi2_reduced from . import utils @@ -36,16 +37,17 @@ class RabiLengthParameters(Parameters): class RabiLengthResults(Results): """RabiLength outputs.""" - length: dict[QubitId, int] = field(metadata=dict(update="drive_length")) + length: dict[QubitId, tuple[int, Optional[float]]] """Pi pulse duration for each qubit.""" - amplitude: dict[QubitId, float] = field(metadata=dict(update="drive_amplitude")) + amplitude: dict[QubitId, tuple[float, Optional[float]]] """Pi pulse amplitude. Same for all qubits.""" fitted_parameters: dict[QubitId, dict[str, float]] """Raw fitting output.""" + chi2: dict[QubitId, tuple[float, Optional[float]]] = field(default_factory=dict) RabiLenType = np.dtype( - [("length", np.float64), ("msr", np.float64), ("phase", np.float64)] + [("length", np.float64), ("prob", np.float64), ("error", np.float64)] ) """Custom dtype for rabi amplitude.""" @@ -104,9 +106,6 @@ def _acquisition( type=SweeperType.ABSOLUTE, ) - # create a DataUnits object to store the results, - # DataUnits stores by default MSR, phase, i, q - # additionally include qubit drive pulse length data = RabiLengthData(amplitudes=amplitudes) # execute the sweep @@ -115,22 +114,21 @@ def _acquisition( ExecutionParameters( nshots=params.nshots, relaxation_time=params.relaxation_time, - acquisition_type=AcquisitionType.INTEGRATION, - averaging_mode=AveragingMode.CYCLIC, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, ), sweeper, ) for qubit in qubits: - # average msr, phase, i and q over the number of shots defined in the runcard - result = results[ro_pulses[qubit].serial] + prob = results[qubit].probability(state=1) data.register_qubit( RabiLenType, (qubit), dict( length=qd_pulse_duration_range, - msr=result.magnitude, - phase=result.phase, + prob=prob, + error=np.sqrt(prob * (1 - prob) / params.nshots).tolist(), ), ) return data @@ -142,18 +140,12 @@ def _fit(data: RabiLengthData) -> RabiLengthResults: qubits = data.qubits fitted_parameters = {} durations = {} + chi2 = {} for qubit in qubits: qubit_data = data[qubit] - rabi_parameter = qubit_data.length - voltages = qubit_data.msr - - y_min = np.min(voltages) - y_max = np.max(voltages) - x_min = np.min(rabi_parameter) - x_max = np.max(rabi_parameter) - x = (rabi_parameter - x_min) / (x_max - x_min) - y = (voltages - y_min) / (y_max - y_min) + x = qubit_data.length + y = qubit_data.prob # Guessing period using fourier transform ft = np.fft.rfft(y) @@ -162,10 +154,9 @@ def _fit(data: RabiLengthData) -> RabiLengthResults: index = local_maxima[0] if len(local_maxima) > 0 else None # 0.5 hardcoded guess for less than one oscillation f = x[index] / (x[1] - x[0]) if index is not None else 0.5 - - pguess = [1, 1, f, np.pi / 2, 0] + pguess = [0.5, 0.5, np.max(x) / f, np.pi / 2, 0] try: - popt, pcov = curve_fit( + popt, perr = curve_fit( utils.rabi_length_fit, x, y, @@ -175,24 +166,26 @@ def _fit(data: RabiLengthData) -> RabiLengthResults: [0, 0, 0, -np.pi, 0], [1, 1, np.inf, np.pi, np.inf], ), + sigma=qubit_data.error, ) - translated_popt = [ - (y_max - y_min) * popt[0] + y_min, - (y_max - y_min) * popt[1] * np.exp(x_min * popt[4] / (x_max - x_min)), - popt[2] / (x_max - x_min), - popt[3] - 2 * np.pi * x_min * popt[2] / (x_max - x_min), - popt[4] / (x_max - x_min), - ] - pi_pulse_parameter = np.abs((1.0 / translated_popt[2]) / 2) + perr = np.sqrt(np.diag(perr)) + pi_pulse_parameter = np.abs(popt[2] / 2) except: log.warning("rabi_fit: the fitting was not succesful") pi_pulse_parameter = 0 - translated_popt = [0] * 5 - - durations[qubit] = pi_pulse_parameter - fitted_parameters[qubit] = translated_popt - - return RabiLengthResults(durations, data.amplitudes, fitted_parameters) + popt = [0] * 4 + [1] + durations[qubit] = (pi_pulse_parameter, perr[2] / 2) + fitted_parameters[qubit] = popt.tolist() + amplitudes = {key: (value, 0) for key, value in data.amplitudes.items()} + chi2[qubit] = ( + chi2_reduced( + y, + utils.rabi_length_fit(x, *popt), + qubit_data.error, + ), + np.sqrt(2 / len(y)), + ) + return RabiLengthResults(durations, amplitudes, fitted_parameters, chi2) def _update(results: RabiLengthResults, platform: Platform, qubit: QubitId): @@ -201,7 +194,7 @@ def _update(results: RabiLengthResults, platform: Platform, qubit: QubitId): def _plot(data: RabiLengthData, fit: RabiLengthResults, qubit): """Plotting function for RabiLength experiment.""" - return utils.plot(data, qubit, fit) + return utils.plot_probabilities(data, qubit, fit) rabi_length = Routine(_acquisition, _fit, _plot, _update) diff --git a/src/qibocal/protocols/characterization/rabi/length_msr.py b/src/qibocal/protocols/characterization/rabi/length_msr.py new file mode 100644 index 000000000..72b30c6ee --- /dev/null +++ b/src/qibocal/protocols/characterization/rabi/length_msr.py @@ -0,0 +1,186 @@ +from dataclasses import dataclass + +import numpy as np +from qibolab import AcquisitionType, AveragingMode, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence +from qibolab.qubits import QubitId +from qibolab.sweeper import Parameter, Sweeper, SweeperType +from scipy.optimize import curve_fit +from scipy.signal import find_peaks + +from qibocal import update +from qibocal.auto.operation import Qubits, Routine +from qibocal.config import log +from qibocal.protocols.characterization.rabi.length import ( + RabiLengthData, + RabiLengthParameters, + RabiLengthResults, +) + +from . import utils + + +@dataclass +class RabiLengthVoltParameters(RabiLengthParameters): + """RabiLength runcard inputs.""" + + +@dataclass +class RabiLengthVoltResults(RabiLengthResults): + """RabiLength outputs.""" + + +RabiLenVoltType = np.dtype( + [("length", np.float64), ("msr", np.float64), ("phase", np.float64)] +) +"""Custom dtype for rabi amplitude.""" + + +@dataclass +class RabiLengthVoltData(RabiLengthData): + """RabiLength acquisition outputs.""" + + +def _acquisition( + params: RabiLengthVoltParameters, platform: Platform, qubits: Qubits +) -> RabiLengthVoltData: + r""" + Data acquisition for RabiLength Experiment. + In the Rabi experiment we apply a pulse at the frequency of the qubit and scan the drive pulse length + to find the drive pulse length that creates a rotation of a desired angle. + """ + + # create a sequence of pulses for the experiment + sequence = PulseSequence() + qd_pulses = {} + ro_pulses = {} + amplitudes = {} + for qubit in qubits: + # TODO: made duration optional for qd pulse? + qd_pulses[qubit] = platform.create_qubit_drive_pulse( + qubit, start=0, duration=params.pulse_duration_start + ) + if params.pulse_amplitude is not None: + qd_pulses[qubit].amplitude = params.pulse_amplitude + amplitudes[qubit] = qd_pulses[qubit].amplitude + + ro_pulses[qubit] = platform.create_qubit_readout_pulse( + qubit, start=qd_pulses[qubit].finish + ) + sequence.add(qd_pulses[qubit]) + sequence.add(ro_pulses[qubit]) + + # define the parameter to sweep and its range: + # qubit drive pulse duration time + qd_pulse_duration_range = np.arange( + params.pulse_duration_start, + params.pulse_duration_end, + params.pulse_duration_step, + ) + + sweeper = Sweeper( + Parameter.duration, + qd_pulse_duration_range, + [qd_pulses[qubit] for qubit in qubits], + type=SweeperType.ABSOLUTE, + ) + + data = RabiLengthVoltData(amplitudes=amplitudes) + + # execute the sweep + results = platform.sweep( + sequence, + ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ), + sweeper, + ) + + for qubit in qubits: + result = results[ro_pulses[qubit].serial] + data.register_qubit( + RabiLenVoltType, + (qubit), + dict( + length=qd_pulse_duration_range, + msr=result.magnitude, + phase=result.phase, + ), + ) + return data + + +def _fit(data: RabiLengthVoltData) -> RabiLengthVoltResults: + """Post-processing for RabiLength experiment.""" + + qubits = data.qubits + fitted_parameters = {} + durations = {} + + for qubit in qubits: + qubit_data = data[qubit] + rabi_parameter = qubit_data.length + voltages = qubit_data.msr + + y_min = np.min(voltages) + y_max = np.max(voltages) + x_min = np.min(rabi_parameter) + x_max = np.max(rabi_parameter) + x = (rabi_parameter - x_min) / (x_max - x_min) + y = (voltages - y_min) / (y_max - y_min) + + # Guessing period using fourier transform + ft = np.fft.rfft(y) + mags = abs(ft) + local_maxima = find_peaks(mags, threshold=1)[0] + index = local_maxima[0] if len(local_maxima) > 0 else None + # 0.5 hardcoded guess for less than one oscillation + f = x[index] / (x[1] - x[0]) if index is not None else 0.5 + + pguess = [0.5, 0.5, 1 / f, np.pi / 2, 0] + try: + popt, _ = curve_fit( + utils.rabi_length_fit, + x, + y, + p0=pguess, + maxfev=100000, + bounds=( + [0, 0, 0, -np.pi, 0], + [1, 1, np.inf, np.pi, np.inf], + ), + ) + translated_popt = [ # change it according to the fit function + (y_max - y_min) * popt[0] + y_min, + (y_max - y_min) * popt[1] * np.exp(x_min * popt[4] / (x_max - x_min)), + popt[2] * (x_max - x_min), + popt[3] - 2 * np.pi * x_min / popt[2] / (x_max - x_min), + popt[4] / (x_max - x_min), + ] + pi_pulse_parameter = np.abs(translated_popt[2] / 2) + except: + log.warning("rabi_fit: the fitting was not succesful") + pi_pulse_parameter = 0 + translated_popt = [0, 0, 1, 0, 0] + + durations[qubit] = pi_pulse_parameter + fitted_parameters[qubit] = translated_popt + + return RabiLengthVoltResults(durations, data.amplitudes, fitted_parameters) + + +def _update(results: RabiLengthVoltResults, platform: Platform, qubit: QubitId): + update.drive_duration(results.length[qubit], platform, qubit) + + +def _plot(data: RabiLengthVoltData, fit: RabiLengthVoltResults, qubit): + """Plotting function for RabiLength experiment.""" + return utils.plot(data, qubit, fit) + + +rabi_length_msr = Routine(_acquisition, _fit, _plot, _update) +"""RabiLength Routine object.""" diff --git a/src/qibocal/protocols/characterization/rabi/length_sequences.py b/src/qibocal/protocols/characterization/rabi/length_sequences.py index da57e989b..b10399834 100644 --- a/src/qibocal/protocols/characterization/rabi/length_sequences.py +++ b/src/qibocal/protocols/characterization/rabi/length_sequences.py @@ -5,10 +5,10 @@ from qibocal.auto.operation import Qubits, Routine -from .length import ( - RabiLengthData, - RabiLengthParameters, - RabiLenType, +from .length_msr import ( + RabiLengthVoltData, + RabiLengthVoltParameters, + RabiLenVoltType, _fit, _plot, _update, @@ -16,8 +16,8 @@ def _acquisition( - params: RabiLengthParameters, platform: Platform, qubits: Qubits -) -> RabiLengthData: + params: RabiLengthVoltParameters, platform: Platform, qubits: Qubits +) -> RabiLengthVoltData: r""" Data acquisition for RabiLength Experiment. In the Rabi experiment we apply a pulse at the frequency of the qubit and scan the drive pulse length @@ -50,10 +50,7 @@ def _acquisition( params.pulse_duration_step, ) - # create a DataUnits object to store the results, - # DataUnits stores by default MSR, phase, i, q - # additionally include qubit drive pulse length - data = RabiLengthData(amplitudes=amplitudes) + data = RabiLengthVoltData(amplitudes=amplitudes) # sweep the parameter for duration in qd_pulse_duration_range: @@ -73,10 +70,9 @@ def _acquisition( ) for qubit in qubits: - # average msr, phase, i and q over the number of shots defined in the runcard result = results[ro_pulses[qubit].serial] data.register_qubit( - RabiLenType, + RabiLenVoltType, (qubit), dict( length=np.array([duration]), diff --git a/src/qibocal/protocols/characterization/rabi/utils.py b/src/qibocal/protocols/characterization/rabi/utils.py index 84b1bc6ca..0efda1405 100644 --- a/src/qibocal/protocols/characterization/rabi/utils.py +++ b/src/qibocal/protocols/characterization/rabi/utils.py @@ -2,7 +2,7 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots -from ..utils import V_TO_UV, table_dict, table_html +from ..utils import COLORBAND, COLORBAND_LINE, V_TO_UV, table_dict, table_html def rabi_amplitude_fit(x, p0, p1, p2, p3): @@ -12,7 +12,7 @@ def rabi_amplitude_fit(x, p0, p1, p2, p3): # Period T : 1/p[2] # Phase : p[3] # Arbitrary parameter T_2 : 1/p[4] - return p0 + p1 * np.sin(2 * np.pi * x * p2 + p3) + return p0 + p1 * np.sin(2 * np.pi * x / p2 + p3) def rabi_length_fit(x, p0, p1, p2, p3, p4): @@ -22,7 +22,7 @@ def rabi_length_fit(x, p0, p1, p2, p3, p4): # Period T : 1/p[2] # Phase : p[3] # Arbitrary parameter T_2 : 1/p[4] - return p0 + p1 * np.sin(2 * np.pi * x * p2 + p3) * np.exp(-x * p4) + return p0 + p1 * np.sin(2 * np.pi * x / p2 + p3) * np.exp(-x * p4) def plot(data, qubit, fit): @@ -30,7 +30,7 @@ def plot(data, qubit, fit): quantity = "amp" title = "Amplitude (dimensionless)" fitting = rabi_amplitude_fit - elif data.__class__.__name__ == "RabiLengthData": + elif data.__class__.__name__ == "RabiLengthVoltData": quantity = "length" title = "Time (ns)" fitting = rabi_length_fit @@ -116,3 +116,82 @@ def plot(data, qubit, fit): figures.append(fig) return figures, fitting_report + + +def plot_probabilities(data, qubit, fit): + if data.__class__.__name__ == "RabiAmplitudeData": + quantity = "amp" + title = "Amplitude (dimensionless)" + fitting = rabi_amplitude_fit + elif data.__class__.__name__ == "RabiLengthData": + quantity = "length" + title = "Time (ns)" + fitting = rabi_length_fit + + figures = [] + fitting_report = "" + + qubit_data = data[qubit] + probs = qubit_data.prob + error_bars = qubit_data.error + + rabi_parameters = getattr(qubit_data, quantity) + fig = go.Figure( + [ + go.Scatter( + x=rabi_parameters, + y=qubit_data.prob, + opacity=1, + name="Probability", + showlegend=True, + legendgroup="Probability", + mode="lines", + ), + go.Scatter( + x=np.concatenate((rabi_parameters, rabi_parameters[::-1])), + y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), + fill="toself", + fillcolor=COLORBAND, + line=dict(color=COLORBAND_LINE), + showlegend=True, + name="Errors", + ), + ] + ) + + if fit is not None: + rabi_parameter_range = np.linspace( + min(rabi_parameters), + max(rabi_parameters), + 2 * len(rabi_parameters), + ) + params = fit.fitted_parameters[qubit] + fig.add_trace( + go.Scatter( + x=rabi_parameter_range, + y=fitting(rabi_parameter_range, *params), + name="Fit", + line=go.scatter.Line(dash="dot"), + marker_color="rgb(255, 130, 67)", + ), + ) + + fitting_report = table_html( + table_dict( + qubit, + ["Pi pulse amplitude", "Pi pulse length", "chi2 reduced"], + [fit.amplitude[qubit], fit.length[qubit], fit.chi2[qubit]], + display_error=True, + ) + ) + + fig.update_layout( + showlegend=True, + uirevision="0", # ``uirevision`` allows zooming while live plotting + xaxis_title=title, + yaxis_title="Excited state probability", + ) + + figures.append(fig) + + return figures, fitting_report diff --git a/src/qibocal/protocols/characterization/two_qubit_interaction/chevron.py b/src/qibocal/protocols/characterization/two_qubit_interaction/chevron.py index daba102aa..1a2c9e8b5 100644 --- a/src/qibocal/protocols/characterization/two_qubit_interaction/chevron.py +++ b/src/qibocal/protocols/characterization/two_qubit_interaction/chevron.py @@ -124,9 +124,12 @@ def _aquisition( sequence.add(cz.get_qubit_pulses(ordered_pair[1])) # Patch to get the coupler until the routines use QubitPair - sequence.add( - cz.coupler_pulses(platform.pairs[tuple(sorted(ordered_pair))].coupler.name) - ) + if platform.couplers: + sequence.add( + cz.coupler_pulses( + platform.pairs[tuple(sorted(ordered_pair))].coupler.name + ) + ) if params.parking: for pulse in cz: diff --git a/src/qibocal/protocols/characterization/utils.py b/src/qibocal/protocols/characterization/utils.py index a5507dcbf..39f898bff 100644 --- a/src/qibocal/protocols/characterization/utils.py +++ b/src/qibocal/protocols/characterization/utils.py @@ -14,13 +14,22 @@ from qibocal.auto.operation import Data, Results from qibocal.config import log +from qibocal.fitting.classifier import run GHZ_TO_HZ = 1e9 HZ_TO_GHZ = 1e-9 V_TO_UV = 1e6 S_TO_NS = 1e9 +MESH_SIZE = 50 +MARGIN = 0 +SPACING = 0.1 +COLUMNWIDTH = 600 +LEGEND_FONT_SIZE = 20 +TITLE_SIZE = 25 EXTREME_CHI = 1e4 """Chi2 output when errors list contains zero elements""" +COLORBAND = "rgba(0,100,80,0.2)" +COLORBAND_LINE = "rgba(255,255,255,0)" def calculate_frequencies(results, qubit_list): @@ -375,6 +384,204 @@ def significant_digit(number: float): return int(position) +def evaluate_grid( + data: npt.NDArray, +): + """ + This function returns a matrix grid evaluated from + the datapoints `data`. + """ + max_x = ( + max( + 0, + data["i"].max(), + ) + + MARGIN + ) + max_y = ( + max( + 0, + data["q"].max(), + ) + + MARGIN + ) + min_x = ( + min( + 0, + data["i"].min(), + ) + - MARGIN + ) + min_y = ( + min( + 0, + data["q"].min(), + ) + - MARGIN + ) + i_values, q_values = np.meshgrid( + np.linspace(min_x, max_x, num=MESH_SIZE), + np.linspace(min_y, max_y, num=MESH_SIZE), + ) + return np.vstack([i_values.ravel(), q_values.ravel()]).T + + +def plot_results(data: Data, qubit: QubitId, qubit_states: list, fit: Results): + """ + Plots for the qubit and qutrit classification. + + Args: + data (Data): acquisition data + qubit (QubitID): qubit + qubit_states (list): list of qubit states available. + fit (Results): fit results + """ + figures = [] + models_name = data.classifiers_list + qubit_data = data.data[qubit] + grid = evaluate_grid(qubit_data) + + fig = make_subplots( + rows=1, + cols=len(models_name), + horizontal_spacing=SPACING * 3 / len(models_name) * 3, + vertical_spacing=SPACING, + subplot_titles=[run.pretty_name(model) for model in models_name], + column_width=[COLUMNWIDTH] * len(models_name), + ) + + for i, model in enumerate(models_name): + if fit is not None: + predictions = fit.grid_preds[qubit][i] + fig.add_trace( + go.Contour( + x=grid[:, 0], + y=grid[:, 1], + z=np.array(predictions).flatten(), + showscale=False, + colorscale=[get_color_state0(i), get_color_state1(i)], + opacity=0.2, + name="Score", + hoverinfo="skip", + showlegend=True, + ), + row=1, + col=i + 1, + ) + + model = run.pretty_name(model) + max_x = max(grid[:, 0]) + max_y = max(grid[:, 1]) + min_x = min(grid[:, 0]) + min_y = min(grid[:, 1]) + + for state in range(qubit_states): + state_data = qubit_data[qubit_data["state"] == state] + + fig.add_trace( + go.Scatter( + x=state_data["i"], + y=state_data["q"], + name=f"{model}: state {state}", + legendgroup=f"{model}: state {state}", + mode="markers", + showlegend=True, + opacity=0.7, + marker=dict(size=3), + ), + row=1, + col=i + 1, + ) + + fig.add_trace( + go.Scatter( + x=[np.average(state_data["i"])], + y=[np.average(state_data["q"])], + name=f"{model}: state {state}", + legendgroup=f"{model}: state {state}", + showlegend=False, + mode="markers", + marker=dict(size=10), + ), + row=1, + col=i + 1, + ) + + fig.update_xaxes( + title_text=f"i (V)", + range=[min_x, max_x], + row=1, + col=i + 1, + autorange=False, + rangeslider=dict(visible=False), + ) + fig.update_yaxes( + title_text="q (V)", + range=[min_y, max_y], + scaleanchor="x", + scaleratio=1, + row=1, + col=i + 1, + ) + + fig.update_layout( + uirevision="0", # ``uirevision`` allows zooming while live plotting + autosize=False, + height=COLUMNWIDTH, + width=COLUMNWIDTH * len(models_name), + title=dict(text="Results", font=dict(size=TITLE_SIZE)), + legend=dict( + orientation="h", + yanchor="bottom", + xanchor="left", + y=-0.3, + x=0, + itemsizing="constant", + font=dict(size=LEGEND_FONT_SIZE), + ), + ) + figures.append(fig) + + if fit is not None and len(models_name) != 1: + fig_benchmarks = make_subplots( + rows=1, + cols=3, + horizontal_spacing=SPACING, + vertical_spacing=SPACING, + subplot_titles=( + "accuracy", + "testing time (s)", + "training time (s)", + ) + # pylint: disable=E1101 + ) + for i, model in enumerate(models_name): + for plot in range(3): + fig_benchmarks.add_trace( + go.Scatter( + x=[model], + y=[fit.benchmark_table[qubit][i][plot]], + mode="markers", + showlegend=False, + marker=dict(size=10, color=get_color_state1(i)), + ), + row=1, + col=plot + 1, + ) + + fig_benchmarks.update_yaxes(type="log", row=1, col=2) + fig_benchmarks.update_yaxes(type="log", row=1, col=3) + fig_benchmarks.update_layout( + autosize=False, + height=COLUMNWIDTH, + width=COLUMNWIDTH * 3, + title=dict(text="Benchmarks", font=dict(size=TITLE_SIZE)), + ) + + figures.append(fig_benchmarks) + return figures + + def table_dict( qubit: Union[list[QubitId], QubitId], names: list[str], diff --git a/src/qibocal/update.py b/src/qibocal/update.py index aae29d3ef..93b69c5c6 100644 --- a/src/qibocal/update.py +++ b/src/qibocal/update.py @@ -44,23 +44,24 @@ def readout_attenuation(att: int, platform: Platform, qubit: QubitId): def drive_frequency(freq: Union[float, tuple], platform: Platform, qubit: QubitId): """Update drive frequency value in platform for specific qubit.""" - if isinstance( - freq, tuple - ): # TODO: remove this branching after error bars propagation - freq = int(freq[0] * GHZ_TO_HZ) - else: - freq = int(freq * GHZ_TO_HZ) + if isinstance(freq, tuple): + freq = freq[0] + freq = int(freq * GHZ_TO_HZ) platform.qubits[qubit].native_gates.RX.frequency = int(freq) platform.qubits[qubit].drive_frequency = int(freq) -def drive_amplitude(amp: float, platform: Platform, qubit: QubitId): +def drive_amplitude(amp: Union[float, tuple], platform: Platform, qubit: QubitId): """Update drive frequency value in platform for specific qubit.""" + if isinstance(amp, tuple): + amp = amp[0] platform.qubits[qubit].native_gates.RX.amplitude = float(amp) -def drive_duration(duration: int, platform: Platform, qubit: QubitId): +def drive_duration(duration: Union[int, tuple], platform: Platform, qubit: QubitId): """Update drive duration value in platform for specific qubit.""" + if isinstance(duration, tuple): + duration = duration[0] platform.qubits[qubit].native_gates.RX.duration = int(duration) @@ -93,11 +94,6 @@ def assignment_fidelity(fidelity: float, platform: Platform, qubit: QubitId): platform.qubits[qubit].assignment_fidelity = float(fidelity) -def classifiers_hpars(hpars: list, platform: Platform, qubit: QubitId): - """Update classifier hyperparameters in platform for specific qubit.""" - platform.qubits[qubit].classifiers_hpars = hpars - - def virtual_phases(phases: dict[QubitId, float], platform: Platform, pair: QubitPairId): """Update virtual phases for given qubits in pair in results.""" virtual_z_pulses = { diff --git a/tests/runcards/protocols.yml b/tests/runcards/protocols.yml index 50b6c83f4..6125e2d83 100644 --- a/tests/runcards/protocols.yml +++ b/tests/runcards/protocols.yml @@ -186,6 +186,17 @@ actions: pulse_length: 30 nshots: 1024 + - id: rabi msr + priority: 0 + operation: rabi_amplitude_msr + parameters: + min_amp_factor: 0.0 + max_amp_factor: 4.0 + step_amp_factor: 0.1 + pulse_length: 30 + nshots: 1024 + + - id: rabi_ef priority: 0 operation: rabi_amplitude_ef @@ -208,6 +219,16 @@ actions: pulse_amplitude: 0.5 nshots: 1024 + - id: rabi length msr + priority: 0 + operation: rabi_length_msr + parameters: + pulse_duration_start: 4 + pulse_duration_end: 84 + pulse_duration_step: 8 + pulse_amplitude: 0.5 + nshots: 1024 + - id: rabi length sequences priority: 0 operation: rabi_length_sequences @@ -574,3 +595,11 @@ actions: parameters: amplitude_step: 0.1 amplitude_stop: 0.5 + + - id: qutrit + priority: 0 + qubits: [0,1] + operation: qutrit_classification + parameters: + nshots: 100 + classifiers_list: ["naive_bayes", "decision_tree"] diff --git a/tests/test_auto.py b/tests/test_auto.py index a04cb121e..6ace2b6da 100644 --- a/tests/test_auto.py +++ b/tests/test_auto.py @@ -1,6 +1,5 @@ """Test graph execution.""" import pathlib -import tempfile import pytest import yaml @@ -29,7 +28,7 @@ class TestCard: @pytest.mark.parametrize("card", cards.glob("*.yaml")) -def test_execution(card: pathlib.Path): +def test_execution(card: pathlib.Path, tmp_path): """Execute a set of example runcards. The declared result is asserted to be the expected one. @@ -38,7 +37,7 @@ def test_execution(card: pathlib.Path): testcard = TestCard(**yaml.safe_load(card.read_text(encoding="utf-8"))) executor = Executor.load( testcard.runcard, - output=pathlib.Path(tempfile.mkdtemp()), + output=tmp_path, qubits=testcard.runcard.qubits, ) list(executor.run(mode=ExecutionMode.acquire)) diff --git a/tests/test_classifiers.py b/tests/test_classifiers.py index 3f4c4a5d2..403654606 100644 --- a/tests/test_classifiers.py +++ b/tests/test_classifiers.py @@ -1,5 +1,10 @@ +import numpy as np + from qibocal.fitting.classifier import run +MODEL_FILE = "model.skops" +"""Filename for storing the model.""" + def test_load_model(tmp_path): classifier = run.Classifier(run.import_classifiers(["qubit_fit"])[0], tmp_path) @@ -7,3 +12,14 @@ def test_load_model(tmp_path): classifier.dump_hyper(tmp_path) new_classifier = run.Classifier.model_from_dir(tmp_path / "qubit_fit") assert new_classifier == classifier.trainable_model + + +def test_predict_from_file(tmp_path): + """Testing predict_from_file method.""" + classifier = run.Classifier(run.import_classifiers(["qubit_fit"])[0], tmp_path) + model = classifier.create_model({"par1": 1}) + iqs = np.random.rand(10, 2) + classifier.mod.dump(model, classifier.base_dir / MODEL_FILE) + target_predictions = model.predict(iqs) + predictions = classifier.mod.predict_from_file(tmp_path / MODEL_FILE, iqs) + assert np.array_equal(target_predictions, predictions) diff --git a/tests/test_protocols.py b/tests/test_protocols.py index b1bee8a77..beb73fe76 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -1,6 +1,5 @@ """Test routines' acquisition method using dummy platform""" import pathlib -import tempfile import pytest import yaml @@ -29,28 +28,25 @@ def idfn(val): @pytest.mark.parametrize("update", [True, False]) @pytest.mark.parametrize("runcard", generate_runcard_single_protocol(), ids=idfn) -def test_action_builder(runcard, update): +def test_action_builder(runcard, update, tmp_path): """Test ActionBuilder for all protocols.""" - path = pathlib.Path(tempfile.mkdtemp()) - autocalibrate(runcard, path, force=True, update=update) - report(path) + autocalibrate(runcard, tmp_path, force=True, update=update) + report(tmp_path) @pytest.mark.parametrize("runcard", generate_runcard_single_protocol(), ids=idfn) -def test_acquisition_builder(runcard): +def test_acquisition_builder(runcard, tmp_path): """Test AcquisitionBuilder for all protocols.""" - path = pathlib.Path(tempfile.mkdtemp()) - acquire(runcard, path, force=True) - report(path) + acquire(runcard, tmp_path, force=True) + report(tmp_path) @pytest.mark.parametrize("runcard", generate_runcard_single_protocol(), ids=idfn) -def test_fit_builder(runcard): +def test_fit_builder(runcard, tmp_path): """Test FitBuilder.""" - output_folder = pathlib.Path(tempfile.mkdtemp()) - acquire(runcard, output_folder, force=True) - fit(output_folder, update=False) - report(output_folder) + acquire(runcard, tmp_path, force=True) + fit(tmp_path, update=False) + report(tmp_path) # TODO: compare report by calling qq report diff --git a/tests/test_task_options.py b/tests/test_task_options.py index 37dd26f71..b8e88d647 100644 --- a/tests/test_task_options.py +++ b/tests/test_task_options.py @@ -1,6 +1,4 @@ """Test routines' acquisition method using dummy platform""" -import pathlib -import tempfile from copy import deepcopy import pytest @@ -93,7 +91,7 @@ def test_qubits_argument(platform, local_qubits): @pytest.mark.parametrize("global_update", [True, False]) @pytest.mark.parametrize("local_update", [True, False]) -def test_update_argument(global_update, local_update): +def test_update_argument(global_update, local_update, tmp_path): """Test possible update combinations between global and local.""" platform = deepcopy(create_platform("dummy")) old_readout_frequency = platform.qubits[0].readout_frequency @@ -101,7 +99,7 @@ def test_update_argument(global_update, local_update): NEW_CARD = modify_card(deepcopy(UPDATE_CARD), update=local_update) executor = Executor.load( Runcard.load(NEW_CARD), - pathlib.Path(tempfile.mkdtemp()), + tmp_path, platform, platform.qubits, global_update, diff --git a/tests/test_update.py b/tests/test_update.py index d3701045d..591175e38 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -71,13 +71,11 @@ def test_classification_update(qubit): # generate random lists mean_gnd_state = generate_update_list(2) mean_exc_state = generate_update_list(2) - classifiers_hpars = generate_update_list(4) # perform update update.iq_angle(RANDOM_FLOAT, PLATFORM, qubit.name) update.threshold(RANDOM_FLOAT, PLATFORM, qubit.name) update.mean_gnd_states(mean_gnd_state, PLATFORM, qubit.name) update.mean_exc_states(mean_exc_state, PLATFORM, qubit.name) - update.classifiers_hpars(classifiers_hpars, PLATFORM, qubit.name) update.readout_fidelity(RANDOM_FLOAT, PLATFORM, qubit.name) update.assignment_fidelity(RANDOM_FLOAT, PLATFORM, qubit.name) @@ -86,7 +84,6 @@ def test_classification_update(qubit): assert qubit.threshold == RANDOM_FLOAT assert qubit.mean_gnd_states == mean_gnd_state assert qubit.mean_exc_states == mean_exc_state - assert qubit.classifiers_hpars == classifiers_hpars assert qubit.readout_fidelity == RANDOM_FLOAT assert qubit.assignment_fidelity == RANDOM_FLOAT