diff --git a/qiskit_experiments/calibration_management/base_calibration_experiment.py b/qiskit_experiments/calibration_management/base_calibration_experiment.py new file mode 100644 index 0000000000..8b3856dcc9 --- /dev/null +++ b/qiskit_experiments/calibration_management/base_calibration_experiment.py @@ -0,0 +1,366 @@ +# 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. + +"""Base class for calibration-type experiments.""" + +from abc import ABC +from typing import Dict, Optional, Tuple, Type +import warnings + +from qiskit.providers.backend import Backend +from qiskit.circuit import Parameter +from qiskit.pulse import ScheduleBlock + +from qiskit_experiments.calibration_management.calibrations import Calibrations +from qiskit_experiments.calibration_management.update_library import BaseUpdater +from qiskit_experiments.framework.base_experiment import BaseExperiment +from qiskit_experiments.framework.experiment_data import ExperimentData +from qiskit_experiments.exceptions import CalibrationError + + +class BaseCalibrationExperiment(BaseExperiment, ABC): + """A mixin class to create calibration experiments. + + This abstract class extends a characterization experiment by turning it into a + calibration experiment. Such experiments allow schedule management and updating of an + instance of :class:`Calibrations`. Furthermore, calibration experiments also specify + an auto_update variable which, by default, is set to True. If this variable, + is True then the run method of the experiment will call :meth:`block_for_results` + and update the calibrations instance once the backend has returned the data. + + This mixin class inherits from the :class:`BaseExperiment` class since calibration + experiments by default call :meth:`block_for_results`. This ensures that the next + calibration experiment cannot proceed before the calibration parameters have been + updated. Developers that wish to create a calibration experiment must subclass this + base class and the characterization experiment. Therefore, developers that use this + mixin class must pay special attention to their class definition. Indeed, the first + class should be this mixin and the second class should be the characterization + experiment since the run method from the mixin must be used. For example, the rough + frequency calibration experiment is defined as + + .. code-block:: python + + RoughFrequency(BaseCalibrationExperiment, QubitSpectroscopy) + + This ensures that the :meth:`run` method of :class:`RoughFrequency` will be the + run method of the :class:`BaseCalibrationExperiment` class. Furthermore, developers + must explicitly call the :meth:`__init__` methods of both parent classes. + + Developers should strive to follow the convention that the first two arguments of + a calibration experiment are the qubit(s) and the :class:`Calibration` instance. + + If the experiment uses custom schedules, which is typically the case, then + developers may chose to use the :meth:`get_schedules` method when creating the + circuits for the experiment. If :meth:`get_schedules` is used then the developer + must override at least one of the following methods used by :meth:`get_schedules` + to set the schedules: + + #. :meth:`_get_schedules_from_options` + + #. :meth:`_get_schedules_from_calibrations` + + #. :meth:`_get_schedules_from_defaults` + + These methods are called by :meth:`get_schedules`. + + The :meth:`update_calibrations` method is responsible for updating the values of the parameters + stored in the instance of :class:`Calibrations`. Here, :class:`BaseCalibrationExperiment` + provides a default update methodology that subclasses can override if a more elaborate behaviour + is needed. At the minimum the developer must set the variable :code:`_updater` which + should have an :code:`update` method and can be chosen from the library + :mod:`qiskit_experiments.calibration_management.update_library`. See also + :class:`qiskit_experiments.calibration_management.update_library.BaseUpdater`. If no updater + is specified the experiment will still run but no update of the calibrations will be performed. + + In addition to the calibration specific requirements, the developer must set the analysis method + with the class variable :code:`__analysis_class__` and any default experiment options. + """ + + def __init_subclass__(cls, **kwargs): + """Warn if BaseCalibrationExperiment is not the first parent.""" + for mro_cls in cls.mro(): + if mro_cls is BaseCalibrationExperiment: + break + if issubclass(mro_cls, BaseExperiment) and not issubclass( + mro_cls, BaseCalibrationExperiment + ): + warnings.warn( + "Calibration experiments must inherit from BaseCalibrationExperiment " + f"before a BaseExperiment subclass: {cls}->{mro_cls}." + ) + break + super().__init_subclass__(**kwargs) + + # pylint: disable=super-init-not-called + def __init__( + self, + calibrations: Calibrations, + *args, + schedule_name: Optional[str] = None, + cal_parameter_name: Optional[str] = None, + updater: Optional[Type[BaseUpdater]] = None, + auto_update: bool = True, + **kwargs, + ): + """Setup the calibration experiment object. + + Args: + calibrations: The calibrations instance with which to initialize the experiment. + args: Arguments for the characterization class. + schedule_name: An optional string which specifies the name of the schedule in + the calibrations that will be updated. + cal_parameter_name: An optional string which specifies the name of the parameter in + the calibrations that will be updated. If None is given then no parameter will + be updated. Subclasses may assign default values in their init. + updater: The updater class that updates the Calibrations instance. Different + calibration experiments will use different updaters. + auto_update: If set to True (the default) then the calibrations will automatically be + updated once the experiment has run and :meth:`block_for_results()` will be called. + kwargs: Key word arguments for the characterization class. + """ + super().__init__(*args, **kwargs) + self._cals = calibrations + self._sched_name = schedule_name + self._param_name = cal_parameter_name + self._updater = updater + self.auto_update = auto_update + + @property + def calibrations(self) -> Calibrations: + """Return the calibrations.""" + return self._cals + + def update_calibrations(self, experiment_data: ExperimentData): + """Update parameter values in the :class:`Calibrations` instance. + + The default behaviour is to call the update method of the class variable + :code:`__updater__` with simplistic options. Subclasses can override this + method to update the instance of :class:`Calibrations` if they require a + more sophisticated behaviour as is the case for the :class:`Rabi` and + :class:`FineAmplitude` calibration experiments. + """ + if self._updater is not None: + self._updater.update( + self._cals, + experiment_data, + parameter=self._param_name, + schedule=self._sched_name, + ) + + def _get_schedule_from_options(self, option_name: str) -> ScheduleBlock: + """Get a schedule from the experiment options. + + Developers can subclass this method if they need a more sophisticated + methodology to get schedules from their experiment options. + + Args: + option_name: The name of the option under which the schedule is stored. + + Returns: + The schedule to use in the calibration experiment. + """ + return self.experiment_options.get(option_name, None) + + def _get_schedule_from_calibrations( + self, + qubits: Optional[Tuple[int, ...]] = None, + sched_name: Optional[str] = None, + assign_params: Optional[Dict[str, Parameter]] = None, + ) -> Optional[ScheduleBlock]: + """Get the schedules from the Calibrations instance. + + This method is called if :meth:`get_schedules_from_options` did not return + any schedules to use. Here, we get a schedule from an instance of + :class:`Calibrations` using the :meth:`get_schedule` method. Subclasses can override + this method if they need a different behaviour. + + Args: + qubits: The qubits for which to fetch the schedules. If None is given this will + default to the physical qubits of the experiment. + sched_name: The name of the schedule to fetch from the calibrations. If None is + gven this will default to :code:`self._sched_name`. + assign_params: A dict to specify parameters in the schedule that are + to be mapped to an unassigned parameter. + + Returns: + A schedule for the corresponding arguments if there exists an instance + :code:`self._cals`. + """ + + if sched_name is None: + sched_name = self._sched_name + + if qubits is None: + qubits = self.physical_qubits + + if self._cals is not None: + return self._cals.get_schedule(sched_name, qubits=qubits, assign_params=assign_params) + + return None + + def _get_schedule_from_defaults(self, **kwargs) -> Optional[ScheduleBlock]: + """Get the schedules based on default experiment options. + + Subclasses can override this method to define and get default schedules based on + default experiment options such as the number of samples in a Gaussian and its + amplitude. This function is called as a last resort in :meth:`get_schedules` + and accommodates cases when the user provides neither calibrations nor schedules. + For example, if the default schedule is a Gaussian then this function may return + the schedule + + .. code-block:: python + + with pulse.build(backend=backend, name="rabi") as default_schedule: + pulse.play( + pulse.Gaussian( + duration=self.experiment_options.duration, + amp=Parameter("amp"), + sigma=self.experiment_options.sigma, + ), + pulse.DriveChannel(self.physical_qubits[0]), + ) + + """ + raise NotImplementedError( + f"{self.__class__.__name__} could not find a schedule in the experiment options " + "or the calibrations and no default schedule method was implemented." + ) + + def _validate_schedule(self, schedule: ScheduleBlock): + """Subclass can implement this method to validate the schedule they use. + + Validating schedules may include checks on the number of parameters and + the channels in the schedule. The functions :meth:`_validate_channels` and + :meth:`_validate_parameters` implement such standard checks for reuse. + """ + + def _validate_channels(self, schedule: ScheduleBlock): + """Check that the physical qubits are contained in the schedule. + + This is a helper method that experiment developers can call in their implementation + of :meth:`validate_schedules` when checking the schedules. + + Args: + schedule: The schedule for which to check the qubits. + + Raises: + CalibrationError: If a physical qubit is not contained in the channels schedule. + """ + for qubit in self.physical_qubits: + if qubit not in set(ch.index for ch in schedule.channels): + raise CalibrationError( + f"Schedule {schedule.name} does not contain a channel " + f"for the physical qubit {qubit}." + ) + + def _validate_parameters(self, schedule: ScheduleBlock, n_expected_parameters: int): + """Check that the schedule has the expected number of parameters. + + This is a helper method that experiment developers can call in their implementation + of :meth:`validate_schedules` when checking the schedules. + + Args: + schedule: The schedule for which to check the qubits. + n_expected_parameters: The number of free parameters the schedule must have. + + Raises: + CalibrationError: If the schedule does not have n_expected_parameters parameters. + """ + if len(schedule.parameters) != n_expected_parameters: + raise CalibrationError( + f"The schedules {schedule.name} for {self.__class__.__name__} must have " + f"{n_expected_parameters} parameters. Found {len(schedule.parameters)}." + ) + + def get_schedule( + self, + qubits: Optional[Tuple[int, ...]] = None, + sched_name: Optional[str] = None, + option_name: str = "schedule", + assign_params: Optional[Dict[str, Parameter]] = None, + **kwargs, + ) -> ScheduleBlock: + """Get the schedules for the circuits. + + This method defines the order in which the schedules are consumed. This order is + + #. Use the schedules directly available in the experiment, i.e. those specified + by experiment users. This is made possible in experiments by implementing the + :meth:`get_schedules_from_options` method. + + #. Use the schedules found in the instance of :class:`Calibrations` attached to the + experiment. This is done by implementing the :meth:`get_schedules_from_calibrations` + method. + + #. Use any default schedules specified by the :meth:`get_schedules_from_defaults`. + + If any one step does not return a schedule then we attempt to get schedules from the next + step. If none of these three steps have returned any schedules then an error is raised. + + Args: + qubits: The qubits for which to get the schedule in the calibrations. If None is given + this will default to the physical qubits of the experiment. + sched_name: The name of the schedule to retrieve from the instance of + :class:`Calibrations` stored as a protected variable. If this is None then + :meth:`get_schedule_from_calibrations` will default to the :code:`self._sched_name`. + option_name: The name of the option under which to get the schedule from the experiment + options. This will default to "schedule" if None is given. + assign_params: A dict that :meth:`get_schedule_from_calibrations` can use to leave + certain parameters in the schedule unassigned. The key is the name of the parameter + and the value should be an instance of :class:`ParameterExpression`. + kwargs: Additional keyword arguments that can be used by implementations of + :meth:`get_schedule_from_defaults`. + + Returns: + schedules: The schedules (possibly with one or more free parameters) as either a + ScheduleBlock or a list of ScheduleBlocks depending on the experiment. + + Raises: + CalibrationError: if none of the methods above returned schedules. + """ + schedules = self._get_schedule_from_options(option_name) + + if schedules is None: + schedules = self._get_schedule_from_calibrations(qubits, sched_name, assign_params) + + if schedules is None: + schedules = self._get_schedule_from_defaults(**kwargs) + + self._validate_schedule(schedules) + + return schedules + + def run( + self, + backend: Backend, + analysis: bool = True, + experiment_data: Optional[ExperimentData] = None, + **run_options, + ) -> ExperimentData: + """Run an experiment, perform analysis, and update any calibrations. + + Args: + backend: The backend to run the experiment on. + analysis: If True run analysis on the experiment data. + experiment_data: Optional, add results to existing experiment data. + If None a new ExperimentData object will be returned. + run_options: backend runtime options used for circuit execution. + + Returns: + The experiment data object. + """ + experiment_data = super().run(backend, analysis, experiment_data, **run_options) + + if self.auto_update and analysis: + experiment_data.add_analysis_callback(self.update_calibrations) + + return experiment_data diff --git a/qiskit_experiments/calibration_management/update_library.py b/qiskit_experiments/calibration_management/update_library.py index fed9bd2704..8c0a932bf2 100644 --- a/qiskit_experiments/calibration_management/update_library.py +++ b/qiskit_experiments/calibration_management/update_library.py @@ -149,6 +149,7 @@ def update( calibrations: BackendCalibrations, exp_data: ExperimentData, result_index: Optional[int] = None, + parameter: str = None, group: str = "default", fit_parameter: Optional[str] = None, **options, @@ -161,16 +162,21 @@ def update( calibrations: The calibrations to update. exp_data: The experiment data from which to update. result_index: The result index to use which defaults to -1. + parameter: The name of the parameter to update. If None is given this will default + to :code:`calibrations.__qubit_freq_parameter__`. group: The calibrations group to update. Defaults to "default." options: Trailing options. fit_parameter: The name of the fit parameter in the analysis result. This will default to the class variable :code:`__fit_parameter__` if not given. """ + if parameter is None: + parameter = calibrations.__qubit_freq_parameter__ + super().update( calibrations=calibrations, exp_data=exp_data, - parameter=calibrations.__qubit_freq_parameter__, + parameter=parameter, schedule=None, result_index=result_index, group=group, diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index b53637af01..1f2c7cb5fc 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -78,6 +78,7 @@ class instance to manage parameters and pulse schedules. :toctree: ../stubs/ :template: autosummary/experiment.rst + ~calibration.RoughFrequencyCal ~calibration.DragCal ~calibration.FineDrag ~calibration.FineXDrag @@ -100,6 +101,7 @@ class instance to manage parameters and pulse schedules. FineAmplitude, FineXAmplitude, FineSXAmplitude, + RoughFrequencyCal, RamseyXY, ) from .characterization import ( diff --git a/qiskit_experiments/library/calibration/__init__.py b/qiskit_experiments/library/calibration/__init__.py index 8fd98b2581..2e8292a8b3 100644 --- a/qiskit_experiments/library/calibration/__init__.py +++ b/qiskit_experiments/library/calibration/__init__.py @@ -39,6 +39,7 @@ :toctree: ../stubs/ :template: autosummary/experiment.rst + RoughFrequencyCal DragCal FineDrag FineXDrag @@ -66,6 +67,7 @@ See :mod:`qiskit_experiments.calibration_management`. """ +from .rough_frequency import RoughFrequencyCal from .drag import DragCal from .fine_drag import FineDrag, FineXDrag, FineSXDrag from .rabi import Rabi, EFRabi diff --git a/qiskit_experiments/library/calibration/rough_frequency.py b/qiskit_experiments/library/calibration/rough_frequency.py new file mode 100644 index 0000000000..5088ba0c7f --- /dev/null +++ b/qiskit_experiments/library/calibration/rough_frequency.py @@ -0,0 +1,109 @@ +# 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. + +"""Calibration version of spectroscopy experiments.""" + +from typing import Iterable + +from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy +from qiskit_experiments.library.characterization.ef_spectroscopy import EFSpectroscopy +from qiskit_experiments.calibration_management.update_library import Frequency +from qiskit_experiments.calibration_management.backend_calibrations import BackendCalibrations +from qiskit_experiments.calibration_management.base_calibration_experiment import ( + BaseCalibrationExperiment, +) + + +class RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy): + """A calibration experiment that runs QubitSpectroscopy.""" + + def __init__( + self, + qubit: int, + calibrations: BackendCalibrations, + frequencies: Iterable[float], + unit: str = "Hz", + auto_update: bool = True, + absolute: bool = True, + ): + """See :class:`QubitSpectroscopy` for detailed documentation. + + Args: + qubit: The qubit on which to run spectroscopy. + calibrations: If calibrations is given then running the experiment may update the values + of the frequencies stored in calibrations. + frequencies: The frequencies to scan in the experiment. + unit: The unit in which the user specifies the frequencies. Can be one of 'Hz', 'kHz', + 'MHz', 'GHz'. Internally, all frequencies will be converted to 'Hz'. + auto_update: If set to True, which is the default, then the experiment will + automatically update the frequency in the calibrations. + absolute: Boolean to specify if the frequencies are absolute or relative to the + qubit frequency in the backend. + + Raises: + QiskitError: if there are less than three frequency shifts or if the unit is not known. + + """ + super().__init__( + calibrations, + qubit, + frequencies, + unit, + absolute, + updater=Frequency, + auto_update=auto_update, + ) + + +class RoughEFFrequencyCal(BaseCalibrationExperiment, EFSpectroscopy): + """A calibration experiment that runs QubitSpectroscopy.""" + + __updater__ = Frequency + + # pylint: disable=super-init-not-called + def __init__( + self, + qubit: int, + calibrations: BackendCalibrations, + frequencies: Iterable[float], + unit: str = "Hz", + auto_update: bool = True, + absolute: bool = True, + ): + """See :class:`QubitSpectroscopy` for detailed documentation. + + Args: + qubit: The qubit on which to run spectroscopy. + calibrations: If calibrations is given then running the experiment may update the values + of the frequencies stored in calibrations. + frequencies: The frequencies to scan in the experiment. + unit: The unit in which the user specifies the frequencies. Can be one of 'Hz', 'kHz', + 'MHz', 'GHz'. Internally, all frequencies will be converted to 'Hz'. + auto_update: If set to True, which is the default, then the experiment will + automatically update the frequency in the calibrations. + absolute: Boolean to specify if the frequencies are absolute or relative to the + qubit frequency in the backend. + + Raises: + QiskitError: if there are less than three frequency shifts or if the unit is not known. + + """ + super().__init__( + calibrations, + qubit, + frequencies, + unit, + absolute, + cal_parameter_name="f12", + updater=Frequency, + auto_update=auto_update, + ) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index a1224d2aba..d44b3038d3 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -12,7 +12,7 @@ """Spectroscopy experiment class.""" -from typing import List, Optional, Tuple, Union +from typing import Iterable, Optional, Tuple import numpy as np import qiskit.pulse as pulse @@ -60,7 +60,16 @@ def _default_run_options(cls) -> Options: @classmethod def _default_experiment_options(cls) -> Options: - """Default option values used for the spectroscopy pulse.""" + """Default option values used for the spectroscopy pulse. + + Experiment Options: + amp (float): The amplitude of the spectroscopy pulse. Defaults to 0.1. + duration (int): The duration of the spectroscopy pulse. Defaults to 1024 samples. + sigma (float): The standard deviation of the flanks of the spectroscopy pulse. + Defaults to 256. + width (int): The width of the flat-top part of the GaussianSquare pulse. + Defaults to 0. + """ options = super()._default_experiment_options() options.amp = 0.1 @@ -85,8 +94,8 @@ def _default_analysis_options(cls) -> Options: def __init__( self, qubit: int, - frequencies: Union[List[float], np.array], - unit: Optional[str] = "Hz", + frequencies: Iterable[float], + unit: str = "Hz", absolute: bool = True, ): """ @@ -101,9 +110,8 @@ def __init__( Args: qubit: The qubit on which to run spectroscopy. frequencies: The frequencies to scan in the experiment. - unit: The unit in which the user specifies the frequencies. Can be one - of 'Hz', 'kHz', 'MHz', 'GHz'. Internally, all frequencies will be converted - to 'Hz'. + unit: The unit in which the user specifies the frequencies. Can be one of 'Hz', 'kHz', + 'MHz', 'GHz'. Internally, all frequencies will be converted to 'Hz'. absolute: Boolean to specify if the frequencies are absolute or relative to the qubit frequency in the backend. @@ -111,6 +119,8 @@ def __init__( QiskitError: if there are less than three frequency shifts or if the unit is not known. """ + super().__init__([qubit]) + if len(frequencies) < 3: raise QiskitError("Spectroscopy requires at least three frequencies.") @@ -119,8 +129,6 @@ def __init__( else: self._frequencies = [apply_prefix(freq, unit) for freq in frequencies] - super().__init__([qubit]) - self._absolute = absolute if not self._absolute: diff --git a/test/calibration/experiments/test_rough_frequency.py b/test/calibration/experiments/test_rough_frequency.py new file mode 100644 index 0000000000..69a013bb81 --- /dev/null +++ b/test/calibration/experiments/test_rough_frequency.py @@ -0,0 +1,67 @@ +# 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. + +"""Rough frequency calibration tests.""" + +from test.test_qubit_spectroscopy import SpectroscopyBackend + +import numpy as np + +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeArmonk + +from qiskit_experiments.library import RoughFrequencyCal +from qiskit_experiments.calibration_management import BackendCalibrations +from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon + + +class TestRoughFrequency(QiskitTestCase): + """Tests for the rough frequency calibration experiment.""" + + def test_init(self): + """Test that initialization.""" + + qubit = 1 + cals = BackendCalibrations(FakeArmonk()) + frequencies = [1, 2, 3] + unit = "kHz" + auto_update = False + absolute = False + + freq = RoughFrequencyCal(qubit, cals, frequencies, unit, auto_update, absolute) + + self.assertEqual(freq.physical_qubits, (qubit,)) + self.assertEqual(freq._frequencies, [1000, 2000, 3000]) + self.assertEqual(freq._absolute, False) + self.assertEqual(freq.auto_update, False) + + def test_update_calibrations(self): + """Test that we can properly update an instance of BackendCalibrations.""" + + freq01 = FakeArmonk().defaults().qubit_freq_est[0] + + backend = SpectroscopyBackend(freq_offset=5e6, line_width=2e6) + backend.defaults().qubit_freq_est = [freq01, freq01] + + library = FixedFrequencyTransmon(basis_gates=["x", "sx"]) + cals = BackendCalibrations(FakeArmonk(), library=library) + + prev_freq = cals.get_parameter_value(cals.__qubit_freq_parameter__, (0,)) + self.assertEqual(prev_freq, freq01) + + frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) + + RoughFrequencyCal(0, cals, frequencies).run(backend).block_for_results() + + # Check the updated frequency which should be shifted by 5MHz. + post_freq = cals.get_parameter_value(cals.__qubit_freq_parameter__, (0,)) + self.assertTrue(abs(post_freq - freq01 - 5e6) < 1e6) diff --git a/test/calibration/test_base_calibration_experiment.py b/test/calibration/test_base_calibration_experiment.py new file mode 100644 index 0000000000..749e4863cd --- /dev/null +++ b/test/calibration/test_base_calibration_experiment.py @@ -0,0 +1,47 @@ +# 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. + +"""Tests for the base class for calibration-type experiments.""" + +from qiskit.test import QiskitTestCase + +from qiskit_experiments.library import QubitSpectroscopy +from qiskit_experiments.calibration_management.calibrations import Calibrations +from qiskit_experiments.calibration_management.base_calibration_experiment import ( + BaseCalibrationExperiment, +) + + +class TestBaseCalibrationClass(QiskitTestCase): + """Tests for base calibration experiment classes.""" + + def test_class_order(self): + """Test warnings when the BaseCalibrationExperiment is not the first parent.""" + + class CorrectOrder(BaseCalibrationExperiment, QubitSpectroscopy): + """A class with the correct order should not produce warnings..""" + + def __init__(self): + """A dummy class for parent order testing.""" + super().__init__(Calibrations(), 0, [0, 1, 2]) + + CorrectOrder() + + with self.assertWarns(Warning): + + # pylint: disable=unused-variable + class WrongOrder(QubitSpectroscopy, BaseCalibrationExperiment): + """Merely defining this class is enough to raise the warning.""" + + def __init__(self): + """A dummy class for parent order testing.""" + super().__init__(Calibrations(), 0, [0, 1, 2])