Source code for qiskit_experiments.calibration_management.base_calibration_experiment

# 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, abstractmethod
import functools
import logging
from typing import List, Optional, Sequence, Type, Union
import warnings

from qiskit import QuantumCircuit
from qiskit.providers.options import Options
from qiskit.pulse import ScheduleBlock
from qiskit.transpiler import StagedPassManager, PassManager, Layout, CouplingMap
from qiskit.transpiler.passes import (
    EnlargeWithAncilla,
    FullAncillaAllocation,
    ApplyLayout,
    SetLayout,
)

from qiskit_experiments.calibration_management.calibrations import Calibrations
from qiskit_experiments.calibration_management.update_library import BaseUpdater
from qiskit_experiments.framework.base_analysis import BaseAnalysis
from qiskit_experiments.framework.base_experiment import BaseExperiment
from qiskit_experiments.framework.experiment_data import ExperimentData
from qiskit_experiments.exceptions import CalibrationError

LOG = logging.getLogger(__name__)


[docs] 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:`~.ExperimentData.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:`~.ExperimentData.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 RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy) This ensures that the ``run`` method of :class:`.RoughFrequencyCal` 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:`.Calibrations` 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. """ 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: Keyword 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 @property def analysis(self) -> Union[BaseAnalysis, None]: """Return the analysis instance for the experiment. .. note:: Analysis instance set to calibration experiment is implicitly patched to run calibration updater to update the parameters in the calibration table. """ return self._analysis @analysis.setter def analysis(self, analysis: Union[BaseAnalysis, None]) -> None: """Set the analysis instance for the experiment""" if analysis is None: return # Create direct alias to the original run method to avoid infinite recursion. # .run method is overruled by the wrapped method # and thus .run method cannot be called within the wrapper function. analysis_run = getattr(analysis, "run") @functools.wraps(analysis_run) def _wrap_run_analysis(*args, **kwargs): experiment_data = analysis_run(*args, **kwargs) if self.auto_update: experiment_data.add_analysis_callback(self.update_calibrations) return experiment_data # Monkey patch run method. # This calls update_calibrations immediately after standard analysis. # This mechanism allows a composite experiment to invoke updater. # Note that the composite experiment only takes circuits from individual experiment # and the composite analysis calls analysis.run of each experiment. # This is only place the updater function can be called from the composite experiment. analysis.run = _wrap_run_analysis BaseExperiment.analysis.fset(self, analysis) @classmethod def _default_experiment_options(cls) -> Options: """Default values for a calibration experiment. Experiment Options: result_index (int): The index of the result from which to update the calibrations. group (str): The calibration group to which the parameter belongs. This will default to the value "default". """ options = super()._default_experiment_options() options.update_options(result_index=-1, group="default") return options @classmethod def _default_transpile_options(cls) -> Options: """Return empty default transpile options as optimization_level is not used.""" return Options()
[docs] def set_transpile_options(self, **fields): r"""Add a warning message. .. note:: If your experiment has overridden `_transpiled_circuits` and needs transpile options then please also override `set_transpile_options`. """ warnings.warn(f"Transpile options are not used in {self.__class__.__name__ }.")
[docs] 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 _validate_channels(self, schedule: ScheduleBlock, physical_qubits: Sequence[int]): """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. physical_qubits: The qubits that should be included in the schedule. Raises: CalibrationError: If a physical qubit is not contained in the channels schedule. """ for qubit in 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 _metadata(self): """Add standard calibration metadata.""" metadata = super()._metadata() metadata["cal_group"] = self.experiment_options.group metadata["cal_param_name"] = self._param_name metadata["cal_schedule"] = self._sched_name # Store measurement level and meas return if they have been # set for the experiment for run_opt in ["meas_level", "meas_return"]: if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata def _transpiled_circuits(self) -> List[QuantumCircuit]: """Override the transpiled circuits method to bring in the inst_map. The transpilation should do the strict minimum to make the circuits hardware compatible. Indeed, calibration experiments are designed with a specific gate sequence in mind. Any transpiler operation that changes this gate sequence may compromise the validity of the calibration experiment. Sub-classes may override this method to define their own transpilation if need be. Returns: A list of transpiled circuits. """ transpiled = [] for circ in self.circuits(): circ = self._map_to_physical_qubits(circ) self._attach_calibrations(circ) transpiled.append(circ) return transpiled def _map_to_physical_qubits(self, circuit: QuantumCircuit) -> QuantumCircuit: """Map program qubits to physical qubits. Args: circuit: The quantum circuit to map to device qubits. Returns: A quantum circuit that has the same number of qubits as the backend and where the physical qubits of the experiment have been properly mapped. """ initial_layout = Layout.from_intlist(list(self.physical_qubits), *circuit.qregs) coupling_map = self._backend_data.coupling_map if coupling_map is not None: coupling_map = CouplingMap(self._backend_data.coupling_map) layout = PassManager( [ SetLayout(initial_layout), FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout(), ] ) return StagedPassManager(["layout"], layout=layout).run(circuit) @abstractmethod def _attach_calibrations(self, circuit: QuantumCircuit): """Attach the calibrations to the quantum circuit. This method attaches calibrations from the `self._cals` instance to the transpiled quantum circuits. Given how important this method is it is made abstract to force potential calibration experiment developers to implement it and think about how schedules are attached to the circuits. The implementation of this method is delegated to the sub-classes so that they can map gate instructions to the schedules stored in the ``Calibrations`` instance. This method is needed for most calibration experiments. However, some experiments already attach circuits to the logical circuits and do not needed to run ``_attach_calibrations``. In such experiments a simple ``pass`` statement will suffice. """