From 19f88992e7bf5857734d361f4eca71d10b024cd2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Jun 2021 17:25:03 +0200 Subject: [PATCH 01/36] * Added the update method to the calibrations. * Added required functionality to update spectroscopy. --- .../calibration/backend_calibrations.py | 15 +++- .../calibration/calibration_extraction.py | 86 +++++++++++++++++++ .../calibration/calibration_types.py | 23 +++++ .../calibration/calibrations.py | 79 +++++++++++++++-- .../characterization/qubit_spectroscopy.py | 13 +++ test/test_qubit_spectroscopy.py | 16 +++- 6 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 qiskit_experiments/calibration/calibration_extraction.py create mode 100644 qiskit_experiments/calibration/calibration_types.py diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 99414a8a19..90543a68e5 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -40,19 +40,28 @@ class BackendCalibrations(Calibrations): any schedule. """ + __qubit_freq_parameter__ = "qubit_lo_freq" + __readout_freq_parameter__ = "meas_lo_freq" + def __init__(self, backend: Backend): """Setup an instance to manage the calibrations of a backend.""" super().__init__(backend.configuration().control_channels) # Use the same naming convention as in backend.defaults() - self.qubit_freq = Parameter("qubit_lo_freq") - self.meas_freq = Parameter("meas_lo_freq") + self.qubit_freq = Parameter(self.__qubit_freq_parameter__) + self.meas_freq = Parameter(self.__readout_freq_parameter__) self._register_parameter(self.qubit_freq, ()) self._register_parameter(self.meas_freq, ()) self._qubits = set(range(backend.configuration().n_qubits)) self._backend = backend + for qubit, freq in enumerate(backend.defaults().qubit_freq_est): + self.add_parameter_value(freq, self.qubit_freq, qubit) + + for meas, freq in enumerate(backend.defaults().meas_freq_est): + self.add_parameter_value(freq, self.meas_freq, meas) + def _get_frequencies( self, element: FrequencyElement, @@ -70,7 +79,7 @@ def _get_frequencies( freqs = [] for qubit in self._qubits: - if ParameterKey(None, param, (qubit,)) in self._params: + if ParameterKey(param, (qubit,), None) in self._params: freq = self.get_parameter_value(param, (qubit,), None, True, group, cutoff_date) else: if element == FrequencyElement.READOUT: diff --git a/qiskit_experiments/calibration/calibration_extraction.py b/qiskit_experiments/calibration/calibration_extraction.py new file mode 100644 index 0000000000..7b647745f4 --- /dev/null +++ b/qiskit_experiments/calibration/calibration_extraction.py @@ -0,0 +1,86 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class to extract arbitrary data from a result object.""" + +from abc import abstractmethod +from typing import List, Tuple, Union + +from qiskit.circuit import Parameter + +from qiskit_experiments.base_analysis import AnalysisResult +from qiskit_experiments.calibration.calibration_types import ParameterValueType + + +class CalibrationExtraction: + """Performs non-trivial calibration parameter value extraction from analysis results. + + Most analysis results will contain the value of the calibration parameter under the + "value" key. However, in some instance more complex parameter value extraction is + required. For example, from a single Rabi experiment we may update the pulse amplitude + of the xp pulse as well as the x90p pulse. Both of these amplitudes must be extracted + from the same result object which can be done by subclassing :class:`CalibrationExtraction`. + """ + + def __init__(self, parameters: List[Union[str, Parameter]], schedule_names: List[str]): + """ + Args: + parameters: The parameters to update. + schedule_names: The names of the schedules that will be updated. + """ + self._parameters = parameters + self._schedules = schedule_names + + @abstractmethod + def __call__( + self, + result: AnalysisResult + ) -> List[Tuple[ParameterValueType, str, Tuple[int, ...], str]]: + """Method to extract calibration data from a result instance. + + Args: + result: The result instance from which to extract the parameters. + + Returns: + A list of tuples. Each tuple is a parameter value, the name of the parameter to + update, the qubits to update, and the name of the schedule to which the parameter + belongs. + """ + +class RabiExtraction(CalibrationExtraction): + """Extract rotation angles from a cosine fit.""" + + def __init__(self, params_angles_schedules: List[Tuple[str, float, str]]): + """Class to extract amplitudes for different rotation angles. + + Args: + params_angles_schedules: A list of tuples. Each tuple corresponds to + the parameter name to update, the corresponding rotation angle, and the + name of the schedule to which the parameter belongs. + """ + + parameters, schedules = [], [] + self._angles = [] + for parameter, angle, schedule in params_angles_schedules: + parameters.append(parameter) + schedules.append(schedule) + self._angles.append(angle) + + super().__init__(parameters, schedules) + + def __call__( + self, + result: AnalysisResult + ) -> List[Tuple[ParameterValueType, str, Tuple[int, ...], str]]: + """TODO""" + + diff --git a/qiskit_experiments/calibration/calibration_types.py b/qiskit_experiments/calibration/calibration_types.py new file mode 100644 index 0000000000..a1bdcccbc5 --- /dev/null +++ b/qiskit_experiments/calibration/calibration_types.py @@ -0,0 +1,23 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Types used by the calibration module.""" + +from typing import Union +from collections import namedtuple + +from qiskit.circuit import ParameterExpression + + +ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) +ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) +ParameterValueType = Union[ParameterExpression, float, int, complex] diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 228ff93808..a73524cdef 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -13,9 +13,9 @@ """Class to store and manage the results of calibration experiments.""" import os -from collections import namedtuple, defaultdict +from collections import defaultdict from datetime import datetime -from typing import Any, Dict, Set, Tuple, Union, List, Optional +from typing import Any, Callable, Dict, Set, Tuple, Union, List, Optional import csv import dataclasses import warnings @@ -37,10 +37,13 @@ from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue - -ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) -ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) -ParameterValueType = Union[ParameterExpression, float, int, complex] +from qiskit_experiments.experiment_data import ExperimentData +from qiskit_experiments.calibration.calibration_extraction import CalibrationExtraction +from qiskit_experiments.calibration.calibration_types import ( + ParameterKey, + ParameterValueType, + ScheduleKey +) class Calibrations: @@ -897,6 +900,68 @@ def parameters_table( return data + def update( + self, + exp_data: ExperimentData, + calibration_extraction: Optional[Union[Callable, CalibrationExtraction]] = None, + result_index: int = 0, + force_update: bool = False, + group: str = "default", + ): + """Update the calibrations form a result in the given experiment data. + + This function allows users to update their calibrations from experiment data. Typically, + the value of the parameter to update is directly stored as the result of the fit. However, + for more complex cases, such as Rabi, the value of the parameter is extracted from the + fit result using the calibration extraction callable. + + Args: + exp_data: An analysis result which contains either the value to update under the + key value or the information required by the calibration_extraction function + which will build the values to update. + calibration_extraction: A callable that must return + List[Tuple[ParameterValueType, str, Tuple[int, ...], str]] where each tuple + is a parameter value, the name of the parameter to update, the qubits to update, + and the name of the schedule to which the parameter belongs. + result_index: The index of the result which defaults to 0. + force_update: If set to True then the calibrations will be updated even if the + quality of the result is "computer_bad". + group: The calibration group from which to draw the parameters. + If not specified this defaults to the 'default' group. + + Raises: + CalibrationError: If the result does not have the required calibration keys. These + keys are "calibration_parameter", "qubits", "calibration_schedule", and "value". + """ + result = exp_data.analysis_result(result_index) + + if result["quality"] == "computer_bad" and not force_update: + return + + if calibration_extraction is None: + for key in ["calibration_parameter", "qubits", "calibration_schedule", "value"]: + if key not in result: + raise CalibrationError( + f"Cannot update calibrations from a result without a {key} key." + ) + + value = ParameterValue( + value=result["value"], + date_time=datetime.now(), + group=group, + exp_id=exp_data.experiment_id + ) + + schedule = result["calibration_schedule"] + param = result["calibration_parameter"] + qubits = self._to_tuple(result["qubits"]) + + self.add_parameter_value(value, param, qubits, schedule) + + else: + for value, param, qubits, schedule in calibration_extraction(result): + self.add_parameter_value(value, param, qubits, schedule) + def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): """Save the parameterized schedules and parameter value. @@ -1025,7 +1090,7 @@ def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: CalibrationError: If the given input does not conform to an int or tuple of ints. """ - if not qubits: + if qubits is None: return tuple() if isinstance(qubits, str): diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 5aaf3f2480..6876c74f5a 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -30,6 +30,7 @@ from qiskit_experiments import ExperimentData from qiskit_experiments.data_processing.processor_library import get_to_signal_processor from qiskit_experiments.analysis import plotting +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class SpectroscopyAnalysis(BaseAnalysis): @@ -81,6 +82,8 @@ def _default_options(cls): sigma_bounds=None, freq_bounds=None, offset_bounds=(-2, 2), + calibration_parameter=BackendCalibrations.__qubit_freq_parameter__, + calibration_schedule=None, ) # pylint: disable=arguments-differ, unused-argument @@ -98,6 +101,8 @@ def _run_analysis( offset_bounds: Tuple[float, float] = (-2, 2), plot: bool = True, ax: Optional["AxesSubplot"] = None, + calibration_parameter: str = None, + calibration_schedule = None, **kwargs, ) -> Tuple[AnalysisResult, None]: """Analyze the given data by fitting it to a Gaussian. @@ -210,6 +215,9 @@ def fit_fun(x, a, sigma, freq, b): best_fit["xdata"] = xdata best_fit["ydata"] = ydata best_fit["ydata_err"] = sigmas + best_fit["qubits"] = (experiment_data.data(0)["metadata"]["qubit"], ) + best_fit["calibration_parameter"] = calibration_parameter + best_fit["calibration_schedule"] = calibration_schedule best_fit["quality"] = self._fit_quality( best_fit["popt"][0], best_fit["popt"][1], @@ -377,6 +385,11 @@ def __init__( super().__init__([qubit]) + self.set_analysis_options( + calibration_parameter=BackendCalibrations.__qubit_freq_parameter__, + calibration_schedule=None, + ) + def circuits(self, backend: Optional[Backend] = None): """Create the circuit for the spectroscopy experiment. diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 73e1c2ee45..c345e5c14c 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -17,9 +17,11 @@ import numpy as np from qiskit.qobj.utils import MeasLevel from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeAthens from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy from qiskit_experiments.test.mock_iq_backend import TestJob, IQTestBackend +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class SpectroscopyBackend(IQTestBackend): @@ -93,6 +95,12 @@ def run( class TestQubitSpectroscopy(QiskitTestCase): """Test spectroscopy experiment.""" + def setUp(self): + """Setup the test.""" + super().setUp() + + self._cals = BackendCalibrations(FakeAthens()) + def test_spectroscopy_end2end_classified(self): """End to end test of the spectroscopy experiment.""" @@ -111,12 +119,18 @@ def test_spectroscopy_end2end_classified(self): spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - result = spec.run(backend).analysis_result(0) + exp_data = spec.run(backend) + result = exp_data.analysis_result(0) self.assertTrue(result["value"] < 5.1e6) self.assertTrue(result["value"] > 4.9e6) self.assertEqual(result["quality"], "computer_good") + # Test the integration with the BackendCalibrations + self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["value"]) + self._cals.update(exp_data) + self.assertEqual(self._cals.get_qubit_frequencies()[3], result["value"]) + def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" From a19c821444801d6ea225938beccd24c8b5ebb2f6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Jun 2021 17:34:05 +0200 Subject: [PATCH 02/36] * Used ParameterValue in update. --- qiskit_experiments/calibration/calibrations.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a73524cdef..d5c93317c9 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -960,7 +960,15 @@ def update( else: for value, param, qubits, schedule in calibration_extraction(result): - self.add_parameter_value(value, param, qubits, schedule) + + param_value = ParameterValue( + value=value, + date_time=datetime.now(), + group=group, + exp_id=exp_data.experiment_id + ) + + self.add_parameter_value(param_value, param, qubits, schedule) def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): """Save the parameterized schedules and parameter value. From 71553c750b07717c316d9b84661da55076a99207 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Thu, 10 Jun 2021 13:43:17 +0200 Subject: [PATCH 03/36] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/calibrations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d5c93317c9..7edbd36b5c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -939,8 +939,7 @@ def update( return if calibration_extraction is None: - for key in ["calibration_parameter", "qubits", "calibration_schedule", "value"]: - if key not in result: + if not all(key in result for key in ["calibration_parameter", "qubits", "calibration_schedule", "value"]): raise CalibrationError( f"Cannot update calibrations from a result without a {key} key." ) From 9c6ef7f9f0896ba02cd13f68d886a449896d6085 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 13:52:54 +0200 Subject: [PATCH 04/36] * Updated docstring. --- qiskit_experiments/calibration/calibrations.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7edbd36b5c..47664ae4c2 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -913,7 +913,16 @@ def update( This function allows users to update their calibrations from experiment data. Typically, the value of the parameter to update is directly stored as the result of the fit. However, for more complex cases, such as Rabi, the value of the parameter is extracted from the - fit result using the calibration extraction callable. + fit result using the calibration extraction callable. Importantly, this method requires + the following keys to be present in the analysis result if no custom calibration extraction + function is given + - calibration_parameter: The name of the calibration parameter to update. + - qubits: The qubits to which the parameter belongs. + - calibration_schedule: The name of the schedule which is updated, this can be None (e.g. + for qubit frequencies). + - value: The value of the parameter which will enter the calibrations. + The ParameterKey formed by the values under (calibration_parameter, qubits, + calibration_schedule) must therefore be valid. Args: exp_data: An analysis result which contains either the value to update under the From 299c9596093a2f3430ae816a6305d1f6ce95b0a2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 16:16:15 +0200 Subject: [PATCH 05/36] * Added completion times. --- qiskit_experiments/calibration/calibrations.py | 7 ++++++- qiskit_experiments/experiment_data.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 47664ae4c2..76c8e93168 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -953,9 +953,14 @@ def update( f"Cannot update calibrations from a result without a {key} key." ) + timestamp = None + all_times = exp_data.completion_times.values() + if all_times: + timestamp = max(all_times) + value = ParameterValue( value=result["value"], - date_time=datetime.now(), + date_time=timestamp, group=group, exp_id=exp_data.experiment_id ) diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index c6569894b0..d2a9c21b56 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -17,6 +17,7 @@ import os import uuid from collections import OrderedDict +from datetime import datetime from qiskit.result import Result from qiskit.providers import Backend @@ -108,6 +109,16 @@ def job_ids(self) -> List[str]: """ return list(self._jobs.keys()) + @property + def completion_times(self) -> Dict[str, datetime]: + """Returns the completion times of the jobs.""" + job_times = {} + for job_id, job in self._jobs.items(): + if job is not None and "COMPLETED" in job.time_per_step(): + job_times[job_id] = job.time_per_step().get("COMPLETED") + + return job_times + @property def backend(self) -> Backend: """Return backend. From 43a00cf1e42729c5328b5251b889777d09eddc9a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 16:21:16 +0200 Subject: [PATCH 06/36] * More robust quality check. --- qiskit_experiments/calibration/calibrations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 76c8e93168..8476530100 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -944,7 +944,8 @@ def update( """ result = exp_data.analysis_result(result_index) - if result["quality"] == "computer_bad" and not force_update: + quality = result.get("quality", "computer_good") + if quality == "computer_bad" and not force_update: return if calibration_extraction is None: From 383fc081221ffcc7c385a5271da2b14c4fb1aa17 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 16:23:51 +0200 Subject: [PATCH 07/36] * Changed default result index to -1. --- qiskit_experiments/calibration/calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 8476530100..bae0b3ff64 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -904,7 +904,7 @@ def update( self, exp_data: ExperimentData, calibration_extraction: Optional[Union[Callable, CalibrationExtraction]] = None, - result_index: int = 0, + result_index: int = -1, force_update: bool = False, group: str = "default", ): @@ -948,8 +948,9 @@ def update( if quality == "computer_bad" and not force_update: return + required_keys = ["calibration_parameter", "qubits", "calibration_schedule", "value"] if calibration_extraction is None: - if not all(key in result for key in ["calibration_parameter", "qubits", "calibration_schedule", "value"]): + if not all(key in result for key in required_keys): raise CalibrationError( f"Cannot update calibrations from a result without a {key} key." ) From 61c0920eb1a38f2d8262adc66f5e3873a86a2a3f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 16:57:56 +0200 Subject: [PATCH 08/36] * Updated spectroscopy integration. --- .../calibration/backend_calibrations.py | 7 +++++- .../calibration/calibrations.py | 23 +++++++++++-------- .../characterization/qubit_spectroscopy.py | 13 ++++++----- qiskit_experiments/test/mock_iq_backend.py | 6 +++++ test/test_qubit_spectroscopy.py | 5 ++-- 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 90543a68e5..ef656b8dde 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -45,7 +45,12 @@ class BackendCalibrations(Calibrations): def __init__(self, backend: Backend): """Setup an instance to manage the calibrations of a backend.""" - super().__init__(backend.configuration().control_channels) + if hasattr(backend.configuration(), "control_channels"): + control_channels = backend.configuration().control_channels + else: + control_channels = None + + super().__init__(control_channels) # Use the same naming convention as in backend.defaults() self.qubit_freq = Parameter(self.__qubit_freq_parameter__) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index bae0b3ff64..0bdb445e47 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -940,7 +940,7 @@ def update( Raises: CalibrationError: If the result does not have the required calibration keys. These - keys are "calibration_parameter", "qubits", "calibration_schedule", and "value". + keys are "cal_parameter", "qubits", "cal_schedule", and "cal_value". """ result = exp_data.analysis_result(result_index) @@ -948,12 +948,12 @@ def update( if quality == "computer_bad" and not force_update: return - required_keys = ["calibration_parameter", "qubits", "calibration_schedule", "value"] + required_keys = ["cal_parameter", "cal_schedule", "cal_value"] if calibration_extraction is None: if not all(key in result for key in required_keys): - raise CalibrationError( - f"Cannot update calibrations from a result without a {key} key." - ) + raise CalibrationError( + f"Cannot update calibrations from result. One of {required_keys} is missing." + ) timestamp = None all_times = exp_data.completion_times.values() @@ -961,15 +961,20 @@ def update( timestamp = max(all_times) value = ParameterValue( - value=result["value"], + value=result["cal_value"], date_time=timestamp, group=group, exp_id=exp_data.experiment_id ) - schedule = result["calibration_schedule"] - param = result["calibration_parameter"] - qubits = self._to_tuple(result["qubits"]) + schedule = result["cal_schedule"] + param = result["cal_parameter"] + + # TODO update this with experiment metadata PR #67 + try: + qubits = exp_data.data(0)["metadata"]["qubits"] + except KeyError as error: + raise CalibrationError("Cannot find qubit information in metadata.") from error self.add_parameter_value(value, param, qubits, schedule) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 11208c3e71..495b710d3f 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -33,6 +33,7 @@ ) from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class SpectroscopyAnalysis(CurveAnalysis): @@ -169,6 +170,11 @@ def _post_processing(self, analysis_result: CurveAnalysisResult) -> CurveAnalysi else: analysis_result["quality"] = "computer_bad" + # Add calibration information + analysis_result["cal_parameter"] = BackendCalibrations.__qubit_freq_parameter__ + analysis_result["cal_schedule"] = None + analysis_result["cal_value"] = fit_freq + return analysis_result @@ -254,11 +260,6 @@ def __init__( self._absolute = absolute self.set_analysis_options(xlabel=f"Frequency [{unit}]", ylabel="Signal [arb. unit]") - self.set_analysis_options( - calibration_parameter=BackendCalibrations.__qubit_freq_parameter__, - calibration_schedule=None, - ) - def circuits(self, backend: Optional[Backend] = None): """Create the circuit for the spectroscopy experiment. @@ -320,7 +321,7 @@ def circuits(self, backend: Optional[Backend] = None): assigned_circ = circuit.assign_parameters({freq_param: freq}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, - "qubit": self.physical_qubits[0], + "qubits": (self.physical_qubits[0], ), "xval": freq, "unit": "Hz", "amplitude": self.experiment_options.amp, diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index 35138428d1..3088514697 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -13,6 +13,7 @@ """An mock IQ backend for testing.""" from typing import Dict, List, Tuple +from datetime import datetime import numpy as np from qiskit.providers.backend import BackendV1 as Backend @@ -33,6 +34,11 @@ def result(self) -> Result: """Return a result.""" return Result.from_dict(self._result) + @staticmethod + def time_per_step() -> Dict[str, datetime]: + """Return the completion time.""" + return {"COMPLETED": datetime.now()} + def submit(self): pass diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 22c7be1618..fdb3a9ccc9 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -22,6 +22,7 @@ from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy from qiskit_experiments.test.mock_iq_backend import TestJob, IQTestBackend from qiskit_experiments.analysis import get_opt_value +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class SpectroscopyBackend(IQTestBackend): @@ -131,9 +132,9 @@ def test_spectroscopy_end2end_classified(self): self.assertEqual(result["quality"], "computer_good") # Test the integration with the BackendCalibrations - self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["value"]) + self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["cal_value"]) self._cals.update(exp_data) - self.assertEqual(self._cals.get_qubit_frequencies()[3], result["value"]) + self.assertEqual(self._cals.get_qubit_frequencies()[3], result["cal_value"]) def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" From c1e29dbd332df42c2414088389b719f01c407e78 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 17:01:13 +0200 Subject: [PATCH 09/36] * Renamed calibration_types to calibration_key_types. --- qiskit_experiments/calibration/calibration_extraction.py | 2 +- .../{calibration_types.py => calibration_key_types.py} | 0 qiskit_experiments/calibration/calibrations.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename qiskit_experiments/calibration/{calibration_types.py => calibration_key_types.py} (100%) diff --git a/qiskit_experiments/calibration/calibration_extraction.py b/qiskit_experiments/calibration/calibration_extraction.py index 7b647745f4..2ce38557f8 100644 --- a/qiskit_experiments/calibration/calibration_extraction.py +++ b/qiskit_experiments/calibration/calibration_extraction.py @@ -18,7 +18,7 @@ from qiskit.circuit import Parameter from qiskit_experiments.base_analysis import AnalysisResult -from qiskit_experiments.calibration.calibration_types import ParameterValueType +from qiskit_experiments.calibration.calibration_key_types import ParameterValueType class CalibrationExtraction: diff --git a/qiskit_experiments/calibration/calibration_types.py b/qiskit_experiments/calibration/calibration_key_types.py similarity index 100% rename from qiskit_experiments/calibration/calibration_types.py rename to qiskit_experiments/calibration/calibration_key_types.py diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 0bdb445e47..d6900f5271 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -39,7 +39,7 @@ from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.calibration.calibration_extraction import CalibrationExtraction -from qiskit_experiments.calibration.calibration_types import ( +from qiskit_experiments.calibration.calibration_key_types import ( ParameterKey, ParameterValueType, ScheduleKey From 4f2f40f81ec9c823d8d7be164bbf5e4fe83555bf Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 17:04:15 +0200 Subject: [PATCH 10/36] * Removed CalibrationExtraction. --- .../calibration/calibrations.py | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d6900f5271..939c7f400c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -903,7 +903,6 @@ def parameters_table( def update( self, exp_data: ExperimentData, - calibration_extraction: Optional[Union[Callable, CalibrationExtraction]] = None, result_index: int = -1, force_update: bool = False, group: str = "default", @@ -928,10 +927,6 @@ def update( exp_data: An analysis result which contains either the value to update under the key value or the information required by the calibration_extraction function which will build the values to update. - calibration_extraction: A callable that must return - List[Tuple[ParameterValueType, str, Tuple[int, ...], str]] where each tuple - is a parameter value, the name of the parameter to update, the qubits to update, - and the name of the schedule to which the parameter belongs. result_index: The index of the result which defaults to 0. force_update: If set to True then the calibrations will be updated even if the quality of the result is "computer_bad". @@ -949,46 +944,33 @@ def update( return required_keys = ["cal_parameter", "cal_schedule", "cal_value"] - if calibration_extraction is None: - if not all(key in result for key in required_keys): - raise CalibrationError( - f"Cannot update calibrations from result. One of {required_keys} is missing." - ) - - timestamp = None - all_times = exp_data.completion_times.values() - if all_times: - timestamp = max(all_times) - - value = ParameterValue( - value=result["cal_value"], - date_time=timestamp, - group=group, - exp_id=exp_data.experiment_id + if not all(key in result for key in required_keys): + raise CalibrationError( + f"Cannot update calibrations from result. One of {required_keys} is missing." ) - schedule = result["cal_schedule"] - param = result["cal_parameter"] + timestamp = None + all_times = exp_data.completion_times.values() + if all_times: + timestamp = max(all_times) - # TODO update this with experiment metadata PR #67 - try: - qubits = exp_data.data(0)["metadata"]["qubits"] - except KeyError as error: - raise CalibrationError("Cannot find qubit information in metadata.") from error + value = ParameterValue( + value=result["cal_value"], + date_time=timestamp, + group=group, + exp_id=exp_data.experiment_id + ) - self.add_parameter_value(value, param, qubits, schedule) + schedule = result["cal_schedule"] + param = result["cal_parameter"] - else: - for value, param, qubits, schedule in calibration_extraction(result): + # TODO update this with experiment metadata PR #67 + try: + qubits = exp_data.data(0)["metadata"]["qubits"] + except KeyError as error: + raise CalibrationError("Cannot find qubit information in metadata.") from error - param_value = ParameterValue( - value=value, - date_time=datetime.now(), - group=group, - exp_id=exp_data.experiment_id - ) - - self.add_parameter_value(param_value, param, qubits, schedule) + self.add_parameter_value(value, param, qubits, schedule) def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): """Save the parameterized schedules and parameter value. From c7a4b715f5db0a10d301fb5b1acf68b87e8a5787 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 17:10:34 +0200 Subject: [PATCH 11/36] * Removed CalibrationExtraction. --- .../calibration/calibration_extraction.py | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 qiskit_experiments/calibration/calibration_extraction.py diff --git a/qiskit_experiments/calibration/calibration_extraction.py b/qiskit_experiments/calibration/calibration_extraction.py deleted file mode 100644 index 2ce38557f8..0000000000 --- a/qiskit_experiments/calibration/calibration_extraction.py +++ /dev/null @@ -1,86 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Class to extract arbitrary data from a result object.""" - -from abc import abstractmethod -from typing import List, Tuple, Union - -from qiskit.circuit import Parameter - -from qiskit_experiments.base_analysis import AnalysisResult -from qiskit_experiments.calibration.calibration_key_types import ParameterValueType - - -class CalibrationExtraction: - """Performs non-trivial calibration parameter value extraction from analysis results. - - Most analysis results will contain the value of the calibration parameter under the - "value" key. However, in some instance more complex parameter value extraction is - required. For example, from a single Rabi experiment we may update the pulse amplitude - of the xp pulse as well as the x90p pulse. Both of these amplitudes must be extracted - from the same result object which can be done by subclassing :class:`CalibrationExtraction`. - """ - - def __init__(self, parameters: List[Union[str, Parameter]], schedule_names: List[str]): - """ - Args: - parameters: The parameters to update. - schedule_names: The names of the schedules that will be updated. - """ - self._parameters = parameters - self._schedules = schedule_names - - @abstractmethod - def __call__( - self, - result: AnalysisResult - ) -> List[Tuple[ParameterValueType, str, Tuple[int, ...], str]]: - """Method to extract calibration data from a result instance. - - Args: - result: The result instance from which to extract the parameters. - - Returns: - A list of tuples. Each tuple is a parameter value, the name of the parameter to - update, the qubits to update, and the name of the schedule to which the parameter - belongs. - """ - -class RabiExtraction(CalibrationExtraction): - """Extract rotation angles from a cosine fit.""" - - def __init__(self, params_angles_schedules: List[Tuple[str, float, str]]): - """Class to extract amplitudes for different rotation angles. - - Args: - params_angles_schedules: A list of tuples. Each tuple corresponds to - the parameter name to update, the corresponding rotation angle, and the - name of the schedule to which the parameter belongs. - """ - - parameters, schedules = [], [] - self._angles = [] - for parameter, angle, schedule in params_angles_schedules: - parameters.append(parameter) - schedules.append(schedule) - self._angles.append(angle) - - super().__init__(parameters, schedules) - - def __call__( - self, - result: AnalysisResult - ) -> List[Tuple[ParameterValueType, str, Tuple[int, ...], str]]: - """TODO""" - - From f276c7b6c2e62c7b06a4fea05831009ff3d837c5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 10 Jun 2021 17:14:21 +0200 Subject: [PATCH 12/36] * Black lint --- qiskit_experiments/calibration/calibrations.py | 7 +++---- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- qiskit_experiments/characterization/t1_experiment.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 939c7f400c..6ac8468a4e 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -15,7 +15,7 @@ import os from collections import defaultdict from datetime import datetime -from typing import Any, Callable, Dict, Set, Tuple, Union, List, Optional +from typing import Any, Dict, Set, Tuple, Union, List, Optional import csv import dataclasses import warnings @@ -38,11 +38,10 @@ from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.calibration.calibration_extraction import CalibrationExtraction from qiskit_experiments.calibration.calibration_key_types import ( ParameterKey, ParameterValueType, - ScheduleKey + ScheduleKey, ) @@ -958,7 +957,7 @@ def update( value=result["cal_value"], date_time=timestamp, group=group, - exp_id=exp_data.experiment_id + exp_id=exp_data.experiment_id, ) schedule = result["cal_schedule"] diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 495b710d3f..93e55afb69 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -321,7 +321,7 @@ def circuits(self, backend: Optional[Backend] = None): assigned_circ = circuit.assign_parameters({freq_param: freq}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, - "qubits": (self.physical_qubits[0], ), + "qubits": (self.physical_qubits[0],), "xval": freq, "unit": "Hz", "amplitude": self.experiment_options.amp, diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 2dd2738c57..e79c09aa2b 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -26,7 +26,7 @@ from qiskit_experiments.analysis.curve_fitting import process_curve_data, curve_fit from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments import AnalysisResult +from qiskit_experiments.experiment_data import AnalysisResult class T1Analysis(BaseAnalysis): From 9a76125bfe718629004e8e6773e823a34bc6e400 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Tue, 15 Jun 2021 09:26:36 +0200 Subject: [PATCH 13/36] Update qiskit_experiments/calibration/calibration_key_types.py Co-authored-by: Naoki Kanazawa From 3fea5a49619db16c9208ae18916372c2944765f2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 15 Jun 2021 10:10:12 +0200 Subject: [PATCH 14/36] * Added name for readability. --- qiskit_experiments/calibration/backend_calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index ef656b8dde..eee4d3150b 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -84,8 +84,9 @@ def _get_frequencies( freqs = [] for qubit in self._qubits: - if ParameterKey(param, (qubit,), None) in self._params: - freq = self.get_parameter_value(param, (qubit,), None, True, group, cutoff_date) + schedule = None # A qubit frequency is not attached to a schedule. + if ParameterKey(param, (qubit,), schedule) in self._params: + freq = self.get_parameter_value(param, (qubit,), schedule, True, group, cutoff_date) else: if element == FrequencyElement.READOUT: freq = self._backend.defaults().meas_freq_est[qubit] From 722ba590362dd9413ceaba8af2ac6955a5b1131c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 15 Jun 2021 10:14:05 +0200 Subject: [PATCH 15/36] * Updated docstring. --- qiskit_experiments/calibration/calibrations.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 6ac8468a4e..cb1a38cc49 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -909,16 +909,13 @@ def update( """Update the calibrations form a result in the given experiment data. This function allows users to update their calibrations from experiment data. Typically, - the value of the parameter to update is directly stored as the result of the fit. However, - for more complex cases, such as Rabi, the value of the parameter is extracted from the - fit result using the calibration extraction callable. Importantly, this method requires - the following keys to be present in the analysis result if no custom calibration extraction - function is given - - calibration_parameter: The name of the calibration parameter to update. + the value of the parameter to update is directly stored as the result of the fit. + Importantly, this method requires the following keys to be present in the analysis result + - cal_parameter: The name of the calibration parameter to update. - qubits: The qubits to which the parameter belongs. - - calibration_schedule: The name of the schedule which is updated, this can be None (e.g. - for qubit frequencies). - - value: The value of the parameter which will enter the calibrations. + - cal_schedule: The name of the schedule which is updated, this can be None (e.g. for + qubit frequencies). + - cal_value: The value of the parameter which will enter the calibrations. The ParameterKey formed by the values under (calibration_parameter, qubits, calibration_schedule) must therefore be valid. From 4e318479e7b582a75907d9dab833a96603fec3ab Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Tue, 15 Jun 2021 10:15:56 +0200 Subject: [PATCH 16/36] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/calibrations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index cb1a38cc49..563977cc17 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -935,8 +935,7 @@ def update( """ result = exp_data.analysis_result(result_index) - quality = result.get("quality", "computer_good") - if quality == "computer_bad" and not force_update: + if "quality" in result and result["quality"] == "computer_bad" and not force_update: return required_keys = ["cal_parameter", "cal_schedule", "cal_value"] From ae4a7c996cce0e630b43693737307fa2ea1cf06a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 15 Jun 2021 10:17:24 +0200 Subject: [PATCH 17/36] * Updated default timestamp to now. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 563977cc17..a306105d6c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -944,7 +944,7 @@ def update( f"Cannot update calibrations from result. One of {required_keys} is missing." ) - timestamp = None + timestamp = datetime.now() all_times = exp_data.completion_times.values() if all_times: timestamp = max(all_times) From ff9d254bae8bfc34172779af040c7a32268bf311 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:31:59 +0200 Subject: [PATCH 18/36] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a306105d6c..7b90d6bf59 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -906,7 +906,7 @@ def update( force_update: bool = False, group: str = "default", ): - """Update the calibrations form a result in the given experiment data. + """Update the calibrations from a result in the given experiment data. This function allows users to update their calibrations from experiment data. Typically, the value of the parameter to update is directly stored as the result of the fit. From e0e3bfd10ce93bfe62aea48d7493bfbf9880c6c4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 18 Jun 2021 13:09:38 +0200 Subject: [PATCH 19/36] * Added an updater library. --- .../calibration/update_library.py | 149 ++++++++++++++++++ .../characterization/qubit_spectroscopy.py | 2 + 2 files changed, 151 insertions(+) create mode 100644 qiskit_experiments/calibration/update_library.py diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py new file mode 100644 index 0000000000..8e7c9cff96 --- /dev/null +++ b/qiskit_experiments/calibration/update_library.py @@ -0,0 +1,149 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A library of experiment calibrations.""" + +from abc import ABC, abstractmethod +from datetime import datetime +from typing import List, Tuple, Union +import numpy as np + +from qiskit.pulse import ScheduleBlock + +from qiskit_experiments.experiment_data import ExperimentData +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations +from qiskit_experiments.calibration.calibrations import Calibrations +from qiskit_experiments.calibration.parameter_value import ParameterValue +from qiskit_experiments.calibration.exceptions import CalibrationError + + +class BaseUpdater(ABC): + """A base class to update calibrations.""" + + def __init__(self): + """Initialize the class.""" + self.qubits = None + self.param = None + self.value = None + self.schedule = None + + @staticmethod + def _time_stamp(exp_data: ExperimentData) -> datetime: + """Helper method to extract the datetime.""" + all_times = exp_data.completion_times.values() + if all_times: + return max(all_times) + + return datetime.now() + + def _update(self, exp_data: ExperimentData, cal: Calibrations, group: str = "default"): + """Update the calibrations with the values.""" + value = ParameterValue( + value=self.value, + date_time=BaseUpdater._time_stamp(exp_data), + group=group, + exp_id=exp_data.experiment_id, + ) + + cal.add_parameter_value(value, self.param, self.qubits, self.schedule) + + @abstractmethod + def update( + self, + exp_data: ExperimentData, + calibrations: BackendCalibrations, + **options + ): + """Update the calibrations based on the data. + + Child update classes must implement this function. + """ + + +class Frequency(BaseUpdater): + """Update frequencies.""" + + def update( + self, + exp_data: ExperimentData, + calibrations: BackendCalibrations, + result_index: int = -1, + group: str = "default", + ): + """Update a qubit frequency from QubitSpectroscopy. + + Args: + exp_data: The experiment data from which to update. + calibrations: The calibrations to update. + result_index: The result index to use, defaults to -1. + group: The calibrations group to update. Defaults to "default." + + Raises: + CalibrationError: If the experiment is not of the supported type. + """ + + from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy + + if isinstance(exp_data.experiment, QubitSpectroscopy): + self.qubits = exp_data.data(0)["metadata"]["qubits"] + self.param = BackendCalibrations.__qubit_freq_parameter__ + self.value = exp_data.analysis_result(result_index)["popt"][2] + else: + raise CalibrationError( + f"{self.__class__.__name__} updates from {type(QubitSpectroscopy.__name__)}." + ) + + self._update(exp_data, calibrations, group) + + +class Amplitude(BaseUpdater): + + def update( + self, + exp_data: ExperimentData, + calibrations: BackendCalibrations, + result_index: int = -1, + group: str = "default", + angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None, + ): + """ + Args: + exp_data: The experiment data from which to update. + calibrations: The calibrations to update. + result_index: The result index to use, defaults to -1. + group: The calibrations group to update. Defaults to "default." + angles_schedules: A list of tuples specifying which angle to update for which + pulse schedule. Each tuple is of the form: (angle, parameter_name, + schedule). Here, angle is the rotation angle for which to extract the amplitude, + parameter_name is the name of the parameter whose value is to be updated, and + schedule is the schedule or its name that contains the parameter. + + Raises: + CalibrationError: If the experiment is not of the supported type. + """ + from qiskit_experiments.calibration.experiments.rabi import Rabi + + if angles_schedules is None: + angles_schedules = [(np.pi, "amp", "xp")] + + self.qubits = exp_data.data(0)["metadata"]["qubits"] + + if isinstance(exp_data.experiment, Rabi): + rate = 2*np.pi*exp_data.analysis_result(result_index)["popt"][1] + for angle, param, schedule in angles_schedules: + self.value = angle / rate + self.schedule = schedule + self.param = param + + self._update(exp_data, calibrations, group) + else: + raise CalibrationError(f"{self.__class__.__name__} updates from {type(Rabi.__name__)}.") diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 4c1361b1ef..5166823d43 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -322,6 +322,8 @@ def circuits(self, backend: Optional[Backend] = None): if not self._absolute: freq += center_freq + freq = np.round(freq, decimals=3) + assigned_circ = circuit.assign_parameters({freq_param: freq}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, From a9a38b9142a6c07a606c883ad29ac8ed85016692 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 20 Jun 2021 17:47:46 +0200 Subject: [PATCH 20/36] * Made update methodology more permissible. * Reoved the previous update methodology. --- .../calibration/calibrations.py | 68 ------------------- .../calibration/experiments/rabi.py | 2 +- .../calibration/update_library.py | 42 +++++++----- .../characterization/qubit_spectroscopy.py | 6 -- test/calibration/experiments/test_rabi.py | 56 +++++++++++++++ test/test_qubit_spectroscopy.py | 7 +- 6 files changed, 86 insertions(+), 95 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7b90d6bf59..c8c44d3585 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -899,74 +899,6 @@ def parameters_table( return data - def update( - self, - exp_data: ExperimentData, - result_index: int = -1, - force_update: bool = False, - group: str = "default", - ): - """Update the calibrations from a result in the given experiment data. - - This function allows users to update their calibrations from experiment data. Typically, - the value of the parameter to update is directly stored as the result of the fit. - Importantly, this method requires the following keys to be present in the analysis result - - cal_parameter: The name of the calibration parameter to update. - - qubits: The qubits to which the parameter belongs. - - cal_schedule: The name of the schedule which is updated, this can be None (e.g. for - qubit frequencies). - - cal_value: The value of the parameter which will enter the calibrations. - The ParameterKey formed by the values under (calibration_parameter, qubits, - calibration_schedule) must therefore be valid. - - Args: - exp_data: An analysis result which contains either the value to update under the - key value or the information required by the calibration_extraction function - which will build the values to update. - result_index: The index of the result which defaults to 0. - force_update: If set to True then the calibrations will be updated even if the - quality of the result is "computer_bad". - group: The calibration group from which to draw the parameters. - If not specified this defaults to the 'default' group. - - Raises: - CalibrationError: If the result does not have the required calibration keys. These - keys are "cal_parameter", "qubits", "cal_schedule", and "cal_value". - """ - result = exp_data.analysis_result(result_index) - - if "quality" in result and result["quality"] == "computer_bad" and not force_update: - return - - required_keys = ["cal_parameter", "cal_schedule", "cal_value"] - if not all(key in result for key in required_keys): - raise CalibrationError( - f"Cannot update calibrations from result. One of {required_keys} is missing." - ) - - timestamp = datetime.now() - all_times = exp_data.completion_times.values() - if all_times: - timestamp = max(all_times) - - value = ParameterValue( - value=result["cal_value"], - date_time=timestamp, - group=group, - exp_id=exp_data.experiment_id, - ) - - schedule = result["cal_schedule"] - param = result["cal_parameter"] - - # TODO update this with experiment metadata PR #67 - try: - qubits = exp_data.data(0)["metadata"]["qubits"] - except KeyError as error: - raise CalibrationError("Cannot find qubit information in metadata.") from error - - self.add_parameter_value(value, param, qubits, schedule) - def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): """Save the parameterized schedules and parameter value. diff --git a/qiskit_experiments/calibration/experiments/rabi.py b/qiskit_experiments/calibration/experiments/rabi.py index 43c1abdf78..5348c06e0c 100644 --- a/qiskit_experiments/calibration/experiments/rabi.py +++ b/qiskit_experiments/calibration/experiments/rabi.py @@ -272,7 +272,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: assigned_circ = circuit.assign_parameters({param: amp}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, - "qubit": self.physical_qubits[0], + "qubits": self.physical_qubits[0], "xval": amp, "unit": "arb. unit", "amplitude": amp, diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index 8e7c9cff96..b1aa5cbc38 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -57,12 +57,7 @@ def _update(self, exp_data: ExperimentData, cal: Calibrations, group: str = "def cal.add_parameter_value(value, self.param, self.qubits, self.schedule) @abstractmethod - def update( - self, - exp_data: ExperimentData, - calibrations: BackendCalibrations, - **options - ): + def update(self, exp_data: ExperimentData, calibrations: BackendCalibrations, **options): """Update the calibrations based on the data. Child update classes must implement this function. @@ -72,12 +67,14 @@ def update( class Frequency(BaseUpdater): """Update frequencies.""" + # pylint: disable=arguments-differ def update( self, exp_data: ExperimentData, calibrations: BackendCalibrations, result_index: int = -1, group: str = "default", + parameter: str = BackendCalibrations.__qubit_freq_parameter__, ): """Update a qubit frequency from QubitSpectroscopy. @@ -86,31 +83,38 @@ def update( calibrations: The calibrations to update. result_index: The result index to use, defaults to -1. group: The calibrations group to update. Defaults to "default." + parameter: The name of the parameter to update. If it is not specified + this will default to the qubit frequency. Raises: - CalibrationError: If the experiment is not of the supported type. + CalibrationError: If the analysis result does not contain a frequency variable. """ - from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy + from qiskit_experiments.characterization.qubit_spectroscopy import SpectroscopyAnalysis - if isinstance(exp_data.experiment, QubitSpectroscopy): - self.qubits = exp_data.data(0)["metadata"]["qubits"] - self.param = BackendCalibrations.__qubit_freq_parameter__ - self.value = exp_data.analysis_result(result_index)["popt"][2] - else: + result = exp_data.analysis_result(result_index) + + if "freq" not in result["popt_keys"]: raise CalibrationError( - f"{self.__class__.__name__} updates from {type(QubitSpectroscopy.__name__)}." + f"{self.__class__.__name__} updates from analysis classes such as " + f'{type(SpectroscopyAnalysis.__name__)} which report "freq" in popt.' ) + self.qubits = exp_data.data(0)["metadata"]["qubits"] + self.param = parameter + self.value = result["popt"][result["popt_keys"].index("freq")] + self._update(exp_data, calibrations, group) class Amplitude(BaseUpdater): + """Update pulse amplitudes.""" + #pylint: disable=arguments-differ def update( self, exp_data: ExperimentData, - calibrations: BackendCalibrations, + calibrations: Calibrations, result_index: int = -1, group: str = "default", angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None, @@ -138,9 +142,13 @@ def update( self.qubits = exp_data.data(0)["metadata"]["qubits"] if isinstance(exp_data.experiment, Rabi): - rate = 2*np.pi*exp_data.analysis_result(result_index)["popt"][1] + result = exp_data.analysis_result(result_index) + + freq = result["popt"][result["popt_keys"].index("freq")] + + rate = 2 * np.pi * freq for angle, param, schedule in angles_schedules: - self.value = angle / rate + self.value = np.round(angle / rate, decimals=8) self.schedule = schedule self.param = param diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 5166823d43..d22f9dc38b 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -33,7 +33,6 @@ ) from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor -from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class SpectroscopyAnalysis(CurveAnalysis): @@ -174,11 +173,6 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR else: analysis_result["quality"] = "computer_bad" - # Add calibration information - analysis_result["cal_parameter"] = BackendCalibrations.__qubit_freq_parameter__ - analysis_result["cal_schedule"] = None - analysis_result["cal_value"] = fit_freq - return analysis_result diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 89305a1ba5..2db9dfd798 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -24,9 +24,12 @@ from qiskit_experiments import ExperimentData from qiskit_experiments.calibration.experiments.rabi import RabiAnalysis, Rabi +from qiskit_experiments.calibration.calibrations import Calibrations +from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.nodes import Probability from qiskit_experiments.test.mock_iq_backend import IQTestBackend +from qiskit_experiments.calibration.update_library import Amplitude class RabiBackend(IQTestBackend): @@ -87,6 +90,59 @@ def test_rabi_end_to_end(self): self.assertEqual(result["quality"], "computer_good") self.assertTrue(abs(result["popt"][1] - backend.rabi_rate) < test_tol) + def test_calibrations_integration(self): + """Test that we can update the value in calibrations.""" + + cals = Calibrations() + + amp = Parameter("amp") + chan = Parameter("ch0") + with pulse.build(name="xp") as xp: + pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) + + amp = Parameter("amp") + with pulse.build(name="x90p") as x90p: + pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) + + cals.add_schedule(xp) + cals.add_schedule(x90p) + + backend = RabiBackend() + + rabi = Rabi(3) + rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) + exp_data = rabi.run(backend) + + for qubit in [0, 3]: + with self.assertRaises(CalibrationError): + cals.get_schedule("xp", qubits=qubit) + + to_update = [(np.pi, "amp", "xp"), (np.pi / 2, "amp", x90p)] + + self.assertEqual(len(cals.parameters_table()), 0) + + Amplitude().update(exp_data, cals, angles_schedules=to_update) + + with self.assertRaises(CalibrationError): + cals.get_schedule("xp", qubits=0) + + self.assertEqual(len(cals.parameters_table()), 2) + + # Now check the corresponding schedules + result = exp_data.analysis_result(-1) + rate = 2 * np.pi * result["popt"][1] + amp = np.round(np.pi / rate, decimals=8) + with pulse.build(name="xp") as expected: + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + + self.assertEqual(cals.get_schedule("xp", qubits=3), expected) + + amp = np.round(0.5 * np.pi / rate, decimals=8) + with pulse.build(name="xp") as expected: + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + + self.assertEqual(cals.get_schedule("x90p", qubits=3), expected) + class TestRabiCircuits(QiskitTestCase): """Test the circuits generated by the experiment and the options.""" diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index f4018e9503..840c37c98c 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -24,6 +24,7 @@ from qiskit_experiments.test.mock_iq_backend import IQTestBackend from qiskit_experiments.analysis import get_opt_value from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations +from qiskit_experiments.calibration.update_library import Frequency class SpectroscopyBackend(IQTestBackend): @@ -91,9 +92,9 @@ def test_spectroscopy_end2end_classified(self): self.assertEqual(result["quality"], "computer_good") # Test the integration with the BackendCalibrations - self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["cal_value"]) - self._cals.update(exp_data) - self.assertEqual(self._cals.get_qubit_frequencies()[3], result["cal_value"]) + self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) + Frequency().update(exp_data, self._cals) + self.assertEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" From c3f59079e9c2aeaad5b6289d50ce88876d8406db Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 20 Jun 2021 17:56:09 +0200 Subject: [PATCH 21/36] * Black --- qiskit_experiments/calibration/update_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index b1aa5cbc38..c2c5d9232a 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -110,7 +110,7 @@ def update( class Amplitude(BaseUpdater): """Update pulse amplitudes.""" - #pylint: disable=arguments-differ + # pylint: disable=arguments-differ def update( self, exp_data: ExperimentData, From 468af330c3965f953bf477f50fc4d2fcafe19707 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 21 Jun 2021 10:27:59 +0200 Subject: [PATCH 22/36] * Moved methodology to class methods. --- .../calibration/update_library.py | 75 ++++++++++++------- test/calibration/experiments/test_rabi.py | 2 +- test/test_qubit_spectroscopy.py | 2 +- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index c2c5d9232a..b49c0052ab 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -17,6 +17,7 @@ from typing import List, Tuple, Union import numpy as np +from qiskit.circuit import Parameter from qiskit.pulse import ScheduleBlock from qiskit_experiments.experiment_data import ExperimentData @@ -24,18 +25,12 @@ from qiskit_experiments.calibration.calibrations import Calibrations from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.calibration.exceptions import CalibrationError +from qiskit_experiments.calibration.calibration_key_types import ParameterValueType class BaseUpdater(ABC): """A base class to update calibrations.""" - def __init__(self): - """Initialize the class.""" - self.qubits = None - self.param = None - self.value = None - self.schedule = None - @staticmethod def _time_stamp(exp_data: ExperimentData) -> datetime: """Helper method to extract the datetime.""" @@ -45,19 +40,43 @@ def _time_stamp(exp_data: ExperimentData) -> datetime: return datetime.now() - def _update(self, exp_data: ExperimentData, cal: Calibrations, group: str = "default"): - """Update the calibrations with the values.""" - value = ParameterValue( - value=self.value, + @classmethod + def _update( + cls, + exp_data: ExperimentData, + cal: Calibrations, + value: ParameterValueType, + param: Union[Parameter, str], + schedule: Union[ScheduleBlock, str] = None, + group: str = "default" + ): + """Update the calibrations with the given value. + + Args: + exp_data: The ExperimentData instance that contains the result and the experiment data. + cal: The Calibrations instance to update. + value: The value extracted by the subclasses in the :meth:`update` method. + param: The name of the parameter, or the parameter instance, which will receive an + updated value. + schedule: The ScheduleBlock instance or the name of the instance to which the parameter + is attached. + group: The calibrations group to update. + """ + + qubits = exp_data.data(0)["metadata"]["qubits"] + + param_value = ParameterValue( + value=value, date_time=BaseUpdater._time_stamp(exp_data), group=group, exp_id=exp_data.experiment_id, ) - cal.add_parameter_value(value, self.param, self.qubits, self.schedule) + cal.add_parameter_value(param_value, param, qubits, schedule) + @classmethod @abstractmethod - def update(self, exp_data: ExperimentData, calibrations: BackendCalibrations, **options): + def update(cls, exp_data: ExperimentData, calibrations: BackendCalibrations, **options): """Update the calibrations based on the data. Child update classes must implement this function. @@ -68,8 +87,9 @@ class Frequency(BaseUpdater): """Update frequencies.""" # pylint: disable=arguments-differ + @classmethod def update( - self, + cls, exp_data: ExperimentData, calibrations: BackendCalibrations, result_index: int = -1, @@ -96,30 +116,31 @@ def update( if "freq" not in result["popt_keys"]: raise CalibrationError( - f"{self.__class__.__name__} updates from analysis classes such as " + f"{cls.__name__} updates from analysis classes such as " f'{type(SpectroscopyAnalysis.__name__)} which report "freq" in popt.' ) - self.qubits = exp_data.data(0)["metadata"]["qubits"] - self.param = parameter - self.value = result["popt"][result["popt_keys"].index("freq")] + param = parameter + value = result["popt"][result["popt_keys"].index("freq")] - self._update(exp_data, calibrations, group) + BaseUpdater._update(exp_data, calibrations, value, param, group) class Amplitude(BaseUpdater): """Update pulse amplitudes.""" # pylint: disable=arguments-differ + @classmethod def update( - self, + cls, exp_data: ExperimentData, calibrations: Calibrations, result_index: int = -1, group: str = "default", angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None, ): - """ + """Update the amplitude of pulses. + Args: exp_data: The experiment data from which to update. calibrations: The calibrations to update. @@ -139,19 +160,15 @@ def update( if angles_schedules is None: angles_schedules = [(np.pi, "amp", "xp")] - self.qubits = exp_data.data(0)["metadata"]["qubits"] - if isinstance(exp_data.experiment, Rabi): result = exp_data.analysis_result(result_index) freq = result["popt"][result["popt_keys"].index("freq")] - rate = 2 * np.pi * freq + for angle, param, schedule in angles_schedules: - self.value = np.round(angle / rate, decimals=8) - self.schedule = schedule - self.param = param + value = np.round(angle / rate, decimals=8) - self._update(exp_data, calibrations, group) + BaseUpdater._update(exp_data, calibrations, value, param, group) else: - raise CalibrationError(f"{self.__class__.__name__} updates from {type(Rabi.__name__)}.") + raise CalibrationError(f"{cls.__name__} updates from {type(Rabi.__name__)}.") diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 2db9dfd798..8e32f9554a 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -121,7 +121,7 @@ def test_calibrations_integration(self): self.assertEqual(len(cals.parameters_table()), 0) - Amplitude().update(exp_data, cals, angles_schedules=to_update) + Amplitude.update(exp_data, cals, angles_schedules=to_update) with self.assertRaises(CalibrationError): cals.get_schedule("xp", qubits=0) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 840c37c98c..2d12921324 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -93,7 +93,7 @@ def test_spectroscopy_end2end_classified(self): # Test the integration with the BackendCalibrations self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) - Frequency().update(exp_data, self._cals) + Frequency.update(exp_data, self._cals) self.assertEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) def test_spectroscopy_end2end_kerneled(self): From d9d47df1c4c3b29ac57cbe2c12e6c574d46b8368 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 21 Jun 2021 17:47:34 +0200 Subject: [PATCH 23/36] * Bug fixes, black, and lint. --- qiskit_experiments/calibration/calibrations.py | 1 - qiskit_experiments/calibration/update_library.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index c8c44d3585..a0285582fe 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -37,7 +37,6 @@ from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue -from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.calibration.calibration_key_types import ( ParameterKey, ParameterValueType, diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index b49c0052ab..2216c7623b 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -48,7 +48,7 @@ def _update( value: ParameterValueType, param: Union[Parameter, str], schedule: Union[ScheduleBlock, str] = None, - group: str = "default" + group: str = "default", ): """Update the calibrations with the given value. @@ -123,7 +123,7 @@ def update( param = parameter value = result["popt"][result["popt_keys"].index("freq")] - BaseUpdater._update(exp_data, calibrations, value, param, group) + BaseUpdater._update(exp_data, calibrations, value, param, schedule=None, group=group) class Amplitude(BaseUpdater): @@ -169,6 +169,6 @@ def update( for angle, param, schedule in angles_schedules: value = np.round(angle / rate, decimals=8) - BaseUpdater._update(exp_data, calibrations, value, param, group) + BaseUpdater._update(exp_data, calibrations, value, param, schedule, group) else: raise CalibrationError(f"{cls.__name__} updates from {type(Rabi.__name__)}.") From 864f3affcefe9bcdd578fb5a389ef092a636e610 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 21 Jun 2021 18:02:10 +0200 Subject: [PATCH 24/36] * Qubits in Rabi. --- qiskit_experiments/calibration/experiments/rabi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/experiments/rabi.py b/qiskit_experiments/calibration/experiments/rabi.py index 5348c06e0c..cdaf85a946 100644 --- a/qiskit_experiments/calibration/experiments/rabi.py +++ b/qiskit_experiments/calibration/experiments/rabi.py @@ -272,7 +272,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: assigned_circ = circuit.assign_parameters({param: amp}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, - "qubits": self.physical_qubits[0], + "qubits": (self.physical_qubits[0],), "xval": amp, "unit": "arb. unit", "amplitude": amp, From b8182972d44b822ae0b7d6f26622538732350450 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Tue, 22 Jun 2021 08:43:49 +0200 Subject: [PATCH 25/36] Update qiskit_experiments/calibration/update_library.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/update_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index 2216c7623b..e12b79db39 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -123,7 +123,7 @@ def update( param = parameter value = result["popt"][result["popt_keys"].index("freq")] - BaseUpdater._update(exp_data, calibrations, value, param, schedule=None, group=group) + cls._update(exp_data, calibrations, value, param, schedule=None, group=group) class Amplitude(BaseUpdater): From 8e65eefabeae10f4d02b26444fa178a6e908ada0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 22 Jun 2021 08:58:03 +0200 Subject: [PATCH 26/36] * Improved docstring. * Changed import order. * Renamed _update to _add_parameter_value. --- .../calibration/update_library.py | 22 ++++++++++--------- test/calibration/experiments/test_rabi.py | 2 +- test/test_qubit_spectroscopy.py | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index e12b79db39..7423ac7a1d 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -41,7 +41,7 @@ def _time_stamp(exp_data: ExperimentData) -> datetime: return datetime.now() @classmethod - def _update( + def _add_parameter_value( cls, exp_data: ExperimentData, cal: Calibrations, @@ -76,10 +76,12 @@ def _update( @classmethod @abstractmethod - def update(cls, exp_data: ExperimentData, calibrations: BackendCalibrations, **options): + def update(cls, calibrations: BackendCalibrations, exp_data: ExperimentData, **options): """Update the calibrations based on the data. - Child update classes must implement this function. + Child update classes must implement this function. This function defines how the data + is extracted from an experiment and then used to update the values of one or more + parameters in the calibrations. """ @@ -90,17 +92,17 @@ class Frequency(BaseUpdater): @classmethod def update( cls, - exp_data: ExperimentData, calibrations: BackendCalibrations, + exp_data: ExperimentData, result_index: int = -1, group: str = "default", parameter: str = BackendCalibrations.__qubit_freq_parameter__, ): - """Update a qubit frequency from QubitSpectroscopy. + """Update a qubit frequency from, e.g., QubitSpectroscopy. Args: - exp_data: The experiment data from which to update. calibrations: The calibrations to update. + exp_data: The experiment data from which to update. result_index: The result index to use, defaults to -1. group: The calibrations group to update. Defaults to "default." parameter: The name of the parameter to update. If it is not specified @@ -123,7 +125,7 @@ def update( param = parameter value = result["popt"][result["popt_keys"].index("freq")] - cls._update(exp_data, calibrations, value, param, schedule=None, group=group) + cls._add_parameter_value(exp_data, calibrations, value, param, schedule=None, group=group) class Amplitude(BaseUpdater): @@ -133,8 +135,8 @@ class Amplitude(BaseUpdater): @classmethod def update( cls, - exp_data: ExperimentData, calibrations: Calibrations, + exp_data: ExperimentData, result_index: int = -1, group: str = "default", angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None, @@ -142,8 +144,8 @@ def update( """Update the amplitude of pulses. Args: - exp_data: The experiment data from which to update. calibrations: The calibrations to update. + exp_data: The experiment data from which to update. result_index: The result index to use, defaults to -1. group: The calibrations group to update. Defaults to "default." angles_schedules: A list of tuples specifying which angle to update for which @@ -169,6 +171,6 @@ def update( for angle, param, schedule in angles_schedules: value = np.round(angle / rate, decimals=8) - BaseUpdater._update(exp_data, calibrations, value, param, schedule, group) + cls._add_parameter_value(exp_data, calibrations, value, param, schedule, group) else: raise CalibrationError(f"{cls.__name__} updates from {type(Rabi.__name__)}.") diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 70682994c2..590c586ec7 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -121,7 +121,7 @@ def test_calibrations_integration(self): self.assertEqual(len(cals.parameters_table()), 0) - Amplitude.update(exp_data, cals, angles_schedules=to_update) + Amplitude.update(cals, exp_data, angles_schedules=to_update) with self.assertRaises(CalibrationError): cals.get_schedule("xp", qubits=0) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 2d204f2557..1af13759a4 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -93,7 +93,7 @@ def test_spectroscopy_end2end_classified(self): # Test the integration with the BackendCalibrations self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) - Frequency.update(exp_data, self._cals) + Frequency.update(self._cals, exp_data) self.assertEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) def test_spectroscopy_end2end_kerneled(self): From 23b3a0448f9dab831cdd7d642265832a3f1deb78 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Tue, 22 Jun 2021 10:55:01 +0200 Subject: [PATCH 27/36] Update qiskit_experiments/calibration/update_library.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/update_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index 7423ac7a1d..600098f838 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -67,7 +67,7 @@ def _add_parameter_value( param_value = ParameterValue( value=value, - date_time=BaseUpdater._time_stamp(exp_data), + date_time=cls._time_stamp(exp_data), group=group, exp_id=exp_data.experiment_id, ) From d4c792d1d629e036988b067937159c520ec6a241 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 22 Jun 2021 17:42:23 +0200 Subject: [PATCH 28/36] * Raise on Update instantiation. --- qiskit_experiments/calibration/update_library.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index 7423ac7a1d..fb2b645c4f 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -31,6 +31,13 @@ class BaseUpdater(ABC): """A base class to update calibrations.""" + def __init__(self): + """Updaters are not meant to be instantiated.""" + raise CalibrationError( + "Calibration updaters are not meant to be instantiated. The intended usage" + "is Updater.update(calibrations, exp_data, ...)." + ) + @staticmethod def _time_stamp(exp_data: ExperimentData) -> datetime: """Helper method to extract the datetime.""" From c2b4109238ceef6c34dcfff73e21930b0d809c1d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 22 Jun 2021 17:55:56 +0200 Subject: [PATCH 29/36] * Moved cals update tests to their own library. --- test/calibration/experiments/test_rabi.py | 56 ----------- test/calibration/test_update_library.py | 108 ++++++++++++++++++++++ test/test_qubit_spectroscopy.py | 14 --- 3 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 test/calibration/test_update_library.py diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 590c586ec7..3f993b1d1c 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -25,11 +25,8 @@ from qiskit_experiments import ExperimentData from qiskit_experiments.calibration.experiments.rabi import RabiAnalysis, Rabi -from qiskit_experiments.calibration.calibrations import Calibrations -from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.nodes import Probability -from qiskit_experiments.calibration.update_library import Amplitude class RabiBackend(MockIQBackend): @@ -90,59 +87,6 @@ def test_rabi_end_to_end(self): self.assertEqual(result["quality"], "computer_good") self.assertTrue(abs(result["popt"][1] - backend.rabi_rate) < test_tol) - def test_calibrations_integration(self): - """Test that we can update the value in calibrations.""" - - cals = Calibrations() - - amp = Parameter("amp") - chan = Parameter("ch0") - with pulse.build(name="xp") as xp: - pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) - - amp = Parameter("amp") - with pulse.build(name="x90p") as x90p: - pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) - - cals.add_schedule(xp) - cals.add_schedule(x90p) - - backend = RabiBackend() - - rabi = Rabi(3) - rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) - exp_data = rabi.run(backend) - - for qubit in [0, 3]: - with self.assertRaises(CalibrationError): - cals.get_schedule("xp", qubits=qubit) - - to_update = [(np.pi, "amp", "xp"), (np.pi / 2, "amp", x90p)] - - self.assertEqual(len(cals.parameters_table()), 0) - - Amplitude.update(cals, exp_data, angles_schedules=to_update) - - with self.assertRaises(CalibrationError): - cals.get_schedule("xp", qubits=0) - - self.assertEqual(len(cals.parameters_table()), 2) - - # Now check the corresponding schedules - result = exp_data.analysis_result(-1) - rate = 2 * np.pi * result["popt"][1] - amp = np.round(np.pi / rate, decimals=8) - with pulse.build(name="xp") as expected: - pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) - - self.assertEqual(cals.get_schedule("xp", qubits=3), expected) - - amp = np.round(0.5 * np.pi / rate, decimals=8) - with pulse.build(name="xp") as expected: - pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) - - self.assertEqual(cals.get_schedule("x90p", qubits=3), expected) - class TestRabiCircuits(QiskitTestCase): """Test the circuits generated by the experiment and the options.""" diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py new file mode 100644 index 0000000000..c4f2d1bc07 --- /dev/null +++ b/test/calibration/test_update_library.py @@ -0,0 +1,108 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test the calibration update library.""" + +from test.calibration.experiments.test_rabi import RabiBackend +from test.test_qubit_spectroscopy import SpectroscopyBackend +import numpy as np + +from qiskit.circuit import Parameter +from qiskit.test import QiskitTestCase +from qiskit.qobj.utils import MeasLevel +import qiskit.pulse as pulse +from qiskit.test.mock import FakeAthens + +from qiskit_experiments.calibration.experiments.rabi import Rabi +from qiskit_experiments.calibration.calibrations import Calibrations +from qiskit_experiments.calibration.exceptions import CalibrationError +from qiskit_experiments.calibration.update_library import Frequency, Amplitude +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations +from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy +from qiskit_experiments.analysis import get_opt_value + + +class TestCalibrationsUpdate(QiskitTestCase): + """Test the update functions in the update library.""" + + def test_amplitude(self): + """Test amplitude update from Rabi.""" + + cals = Calibrations() + + amp = Parameter("amp") + chan = Parameter("ch0") + with pulse.build(name="xp") as xp: + pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) + + amp = Parameter("amp") + with pulse.build(name="x90p") as x90p: + pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) + + cals.add_schedule(xp) + cals.add_schedule(x90p) + + rabi = Rabi(3) + rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) + exp_data = rabi.run(RabiBackend()) + + for qubit in [0, 3]: + with self.assertRaises(CalibrationError): + cals.get_schedule("xp", qubits=qubit) + + to_update = [(np.pi, "amp", "xp"), (np.pi / 2, "amp", x90p)] + + self.assertEqual(len(cals.parameters_table()), 0) + + Amplitude.update(cals, exp_data, angles_schedules=to_update) + + with self.assertRaises(CalibrationError): + cals.get_schedule("xp", qubits=0) + + self.assertEqual(len(cals.parameters_table()), 2) + + # Now check the corresponding schedules + result = exp_data.analysis_result(-1) + rate = 2 * np.pi * result["popt"][1] + amp = np.round(np.pi / rate, decimals=8) + with pulse.build(name="xp") as expected: + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + + self.assertEqual(cals.get_schedule("xp", qubits=3), expected) + + amp = np.round(0.5 * np.pi / rate, decimals=8) + with pulse.build(name="xp") as expected: + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + + self.assertEqual(cals.get_schedule("x90p", qubits=3), expected) + + def test_spectroscopy_end2end_classified(self): + """Test calibrations update from spectroscopy.""" + + backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) + + spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) + exp_data = spec.run(backend) + result = exp_data.analysis_result(0) + + value = get_opt_value(result, "freq") + + self.assertTrue(value < 5.1e6) + self.assertTrue(value > 4.9e6) + self.assertEqual(result["quality"], "computer_good") + + # Test the integration with the BackendCalibrations + cals = BackendCalibrations(FakeAthens()) + self.assertNotEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) + Frequency.update(cals, exp_data) + self.assertEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 1af13759a4..223f02c79f 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -19,12 +19,9 @@ from qiskit import QuantumCircuit from qiskit.qobj.utils import MeasLevel from qiskit.test import QiskitTestCase -from qiskit.test.mock import FakeAthens from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy from qiskit_experiments.analysis import get_opt_value -from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations -from qiskit_experiments.calibration.update_library import Frequency class SpectroscopyBackend(MockIQBackend): @@ -56,12 +53,6 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: class TestQubitSpectroscopy(QiskitTestCase): """Test spectroscopy experiment.""" - def setUp(self): - """Setup the test.""" - super().setUp() - - self._cals = BackendCalibrations(FakeAthens()) - def test_spectroscopy_end2end_classified(self): """End to end test of the spectroscopy experiment.""" @@ -91,11 +82,6 @@ def test_spectroscopy_end2end_classified(self): self.assertTrue(value > 4.9e6) self.assertEqual(result["quality"], "computer_good") - # Test the integration with the BackendCalibrations - self.assertNotEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) - Frequency.update(self._cals, exp_data) - self.assertEqual(self._cals.get_qubit_frequencies()[3], result["popt"][2]) - def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" From 90b5617a38b3ba9dd2a0f4d81c8c031ddb42fc96 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 22 Jun 2021 19:16:29 +0200 Subject: [PATCH 30/36] * Renamed test. --- test/calibration/test_update_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index c4f2d1bc07..40c3efde07 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -85,7 +85,7 @@ def test_amplitude(self): self.assertEqual(cals.get_schedule("x90p", qubits=3), expected) - def test_spectroscopy_end2end_classified(self): + def test_frequency(self): """Test calibrations update from spectroscopy.""" backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) From 762ded0dbd942b62ec76323cc1a9932e18061cd0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 11:23:02 +0200 Subject: [PATCH 31/36] * Made _add_parameter_value a static method. --- qiskit_experiments/calibration/update_library.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index eaaa2334a1..d74412df34 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -47,9 +47,8 @@ def _time_stamp(exp_data: ExperimentData) -> datetime: return datetime.now() - @classmethod + @staticmethod def _add_parameter_value( - cls, exp_data: ExperimentData, cal: Calibrations, value: ParameterValueType, @@ -74,7 +73,7 @@ def _add_parameter_value( param_value = ParameterValue( value=value, - date_time=cls._time_stamp(exp_data), + date_time=BaseUpdater._time_stamp(exp_data), group=group, exp_id=exp_data.experiment_id, ) From ce9b9fc0f0fb5a8482e2c5c3ecd2aad32e4122dd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 12:15:35 +0200 Subject: [PATCH 32/36] * Switched arguments order in update library. --- qiskit_experiments/calibration/update_library.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index d74412df34..a1cbfaec0c 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -49,8 +49,8 @@ def _time_stamp(exp_data: ExperimentData) -> datetime: @staticmethod def _add_parameter_value( - exp_data: ExperimentData, cal: Calibrations, + exp_data: ExperimentData, value: ParameterValueType, param: Union[Parameter, str], schedule: Union[ScheduleBlock, str] = None, @@ -59,8 +59,8 @@ def _add_parameter_value( """Update the calibrations with the given value. Args: - exp_data: The ExperimentData instance that contains the result and the experiment data. cal: The Calibrations instance to update. + exp_data: The ExperimentData instance that contains the result and the experiment data. value: The value extracted by the subclasses in the :meth:`update` method. param: The name of the parameter, or the parameter instance, which will receive an updated value. @@ -131,7 +131,7 @@ def update( param = parameter value = result["popt"][result["popt_keys"].index("freq")] - cls._add_parameter_value(exp_data, calibrations, value, param, schedule=None, group=group) + cls._add_parameter_value(calibrations, exp_data, value, param, schedule=None, group=group) class Amplitude(BaseUpdater): @@ -177,6 +177,6 @@ def update( for angle, param, schedule in angles_schedules: value = np.round(angle / rate, decimals=8) - cls._add_parameter_value(exp_data, calibrations, value, param, schedule, group) + cls._add_parameter_value(calibrations, exp_data, value, param, schedule, group) else: raise CalibrationError(f"{cls.__name__} updates from {type(Rabi.__name__)}.") From cac97064b703a669c9cb651e22a4031a56333b30 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 16:56:19 +0200 Subject: [PATCH 33/36] * Added edgecase handling in the calibrations for parameter value adding. * _add_parameter_value is a classmethod again. --- .../calibration/calibrations.py | 14 +++++++++-- .../calibration/update_library.py | 5 ++-- test/calibration/test_calibrations.py | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a0285582fe..ba53bfa332 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -14,7 +14,7 @@ import os from collections import defaultdict -from datetime import datetime +from datetime import datetime, timedelta from typing import Any, Dict, Set, Tuple, Union, List, Optional import csv import dataclasses @@ -390,7 +390,17 @@ def add_parameter_value( if sched_name and sched_name not in registered_schedules: raise CalibrationError(f"Schedule named {sched_name} was never registered.") - self._params[ParameterKey(param_name, qubits, sched_name)].append(value) + # Edge case handling if the new value has the exact same time as the existing ones + # for the same key. + key = ParameterKey(param_name, qubits, sched_name) + + if self._params[key]: + max_datetime = max(self._params[key], key=lambda x: x.date_time).date_time + + if value.date_time == max_datetime: + value.date_time = value.date_time + timedelta(microseconds=1) + + self._params[key].append(value) def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: """Get the index of the parameterized channel. diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index a1cbfaec0c..aeb45df7c5 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -47,8 +47,9 @@ def _time_stamp(exp_data: ExperimentData) -> datetime: return datetime.now() - @staticmethod + @classmethod def _add_parameter_value( + cls, cal: Calibrations, exp_data: ExperimentData, value: ParameterValueType, @@ -73,7 +74,7 @@ def _add_parameter_value( param_value = ParameterValue( value=value, - date_time=BaseUpdater._time_stamp(exp_data), + date_time=cls._time_stamp(exp_data), group=group, exp_id=exp_data.experiment_id, ) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index da7e085a57..54ee57ab8c 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -441,6 +441,29 @@ def test_parameter_filtering(self): self.assertEqual(len(amp_values), 2) +class TestConcurrentParameters(QiskitTestCase): + """Test a particular edge case with the time in the parameter values.""" + + def test_concurrent_values(self): + """Ensure that parameter values have a unique maximum time.""" + + cals = Calibrations() + + amp = Parameter("amp") + ch0 = Parameter("ch0") + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, amp, 40), DriveChannel(ch0)) + + cals.add_schedule(xp) + + date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + cals.add_parameter_value(ParameterValue(0.25, date_time), "amp", (3,), "xp") + cals.add_parameter_value(ParameterValue(0.35, date_time), "amp", (3,), "xp") + + self.assertEqual(cals.get_parameter_value("amp", 3, "xp"), 0.35) + + class TestMeasurements(QiskitTestCase): """Test that schedules on measure channels are handled properly.""" From 4bb58613f9dee2303a36a71f6fc5eb804bba516e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 18:41:59 +0200 Subject: [PATCH 34/36] * Changed behaviour of get_parameter_value. --- qiskit_experiments/calibration/calibrations.py | 5 ++++- test/calibration/test_calibrations.py | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index ba53bfa332..558dee38a9 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -556,7 +556,7 @@ def get_parameter_value( raise CalibrationError(msg) # 5) Return the most recent parameter. - return max(candidates, key=lambda x: x.date_time).value + return max(enumerate(candidates), key=lambda x: (x[1].date_time, x[0]))[1].value def get_schedule( self, @@ -1048,6 +1048,9 @@ def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: if isinstance(qubits, int): return (qubits,) + if isinstance(qubits, list): + return tuple(qubits) + if isinstance(qubits, tuple): if all(isinstance(n, int) for n in qubits): return qubits diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 54ee57ab8c..12eb2ba2da 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -253,9 +253,6 @@ def test_qubit_input(self): with self.assertRaises(CalibrationError): self.cals.get_parameter_value("amp", "(1, a)", "xp") - with self.assertRaises(CalibrationError): - self.cals.get_parameter_value("amp", [3], "xp") - class TestOverrideDefaults(QiskitTestCase): """ From 1262246ccc14bad797a9e747004e120c08fde1d8 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 18:59:12 +0200 Subject: [PATCH 35/36] * Removed the adding of a microsecond. --- qiskit_experiments/calibration/calibrations.py | 14 ++------------ test/calibration/test_calibrations.py | 3 ++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 558dee38a9..015dc78f12 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -14,7 +14,7 @@ import os from collections import defaultdict -from datetime import datetime, timedelta +from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional import csv import dataclasses @@ -390,17 +390,7 @@ def add_parameter_value( if sched_name and sched_name not in registered_schedules: raise CalibrationError(f"Schedule named {sched_name} was never registered.") - # Edge case handling if the new value has the exact same time as the existing ones - # for the same key. - key = ParameterKey(param_name, qubits, sched_name) - - if self._params[key]: - max_datetime = max(self._params[key], key=lambda x: x.date_time).date_time - - if value.date_time == max_datetime: - value.date_time = value.date_time + timedelta(microseconds=1) - - self._params[key].append(value) + self._params[ParameterKey(param_name, qubits, sched_name)].append(value) def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: """Get the index of the parameterized channel. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 12eb2ba2da..36e77dbd67 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -457,8 +457,9 @@ def test_concurrent_values(self): cals.add_parameter_value(ParameterValue(0.25, date_time), "amp", (3,), "xp") cals.add_parameter_value(ParameterValue(0.35, date_time), "amp", (3,), "xp") + cals.add_parameter_value(ParameterValue(0.45, date_time), "amp", (3,), "xp") - self.assertEqual(cals.get_parameter_value("amp", 3, "xp"), 0.35) + self.assertEqual(cals.get_parameter_value("amp", 3, "xp"), 0.45) class TestMeasurements(QiskitTestCase): From 206baa1ca9886d9247de7afbeaf4339c98265fcc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Jun 2021 19:04:12 +0200 Subject: [PATCH 36/36] * Test docstring fix. --- test/calibration/test_calibrations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 36e77dbd67..0e9d14def1 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -442,7 +442,9 @@ class TestConcurrentParameters(QiskitTestCase): """Test a particular edge case with the time in the parameter values.""" def test_concurrent_values(self): - """Ensure that parameter values have a unique maximum time.""" + """ + Ensure that if the max time has multiple entries we take the most recent appended one. + """ cals = Calibrations()