diff --git a/qiskit_experiments/calibration_management/backend_calibrations.py b/qiskit_experiments/calibration_management/backend_calibrations.py index 807a250661..062a3da2f8 100644 --- a/qiskit_experiments/calibration_management/backend_calibrations.py +++ b/qiskit_experiments/calibration_management/backend_calibrations.py @@ -21,6 +21,7 @@ from qiskit.circuit import Parameter from qiskit_experiments.calibration_management.calibrations import Calibrations, ParameterKey from qiskit_experiments.exceptions import CalibrationError +from qiskit_experiments.calibration_management.basis_gate_library import BasisGateLibrary class FrequencyElement(Enum): @@ -43,8 +44,33 @@ class BackendCalibrations(Calibrations): __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.""" + def __init__( + self, + backend: Backend, + library: BasisGateLibrary = None, + ): + """Setup an instance to manage the calibrations of a backend. + + BackendCalibrations can be initialized from a basis gate library, i.e. a subclass of + :class:`BasisGateLibrary`. As example consider the following code: + + .. code-block:: python + + cals = BackendCalibrations( + backend, + library=FixedFrequencyTransmon( + basis_gates=["x", "sx"], + default_values={duration: 320} + ) + ) + + Args: + backend: A backend instance from which to extract the qubit and readout frequencies + (which will be added as first guesses for the corresponding parameters) as well + as the coupling map. + library: A library class that will be instantiated with the library options to then + get template schedules to register as well as default parameter values. + """ if hasattr(backend.configuration(), "control_channels"): control_channels = backend.configuration().control_channels else: @@ -67,6 +93,18 @@ def __init__(self, backend: Backend): for meas, freq in enumerate(backend.defaults().meas_freq_est): self.add_parameter_value(freq, self.meas_freq, meas) + if library is not None: + + # Add the basis gates + for gate in library.basis_gates: + self.add_schedule(library[gate]) + + # Add the default values + for param_conf in library.default_values(): + schedule_name = param_conf[-1] + if schedule_name in library.basis_gates: + self.add_parameter_value(*param_conf) + def _get_frequencies( self, element: FrequencyElement, diff --git a/qiskit_experiments/calibration_management/basis_gate_library.py b/qiskit_experiments/calibration_management/basis_gate_library.py new file mode 100644 index 0000000000..8270eba8cb --- /dev/null +++ b/qiskit_experiments/calibration_management/basis_gate_library.py @@ -0,0 +1,207 @@ +# 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 collections of libraries to setup Calibrations. + +Note that the set of available libraries will be extended in future releases. +""" + +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Tuple + +from qiskit.circuit import Parameter +import qiskit.pulse as pulse +from qiskit.pulse import ScheduleBlock + +from qiskit_experiments.calibration_management.calibration_key_types import ParameterValueType +from qiskit_experiments.exceptions import CalibrationError + + +class BasisGateLibrary(ABC): + """A base class for libraries of basis gates to make it easier to setup Calibrations.""" + + # Location where default parameter values are stored. These may be updated at construction. + __default_values__ = dict() + + # The basis gates that this library generates. + __supported_gates__ = None + + def __init__( + self, basis_gates: Optional[List[str]] = None, default_values: Optional[Dict] = None + ): + """Setup the library. + + Args: + basis_gates: The basis gates to generate. + default_values: A dictionary to override library default parameter values. + + Raises: + CalibrationError: If on of the given basis gates is not supported by the library. + """ + self._schedules = dict() + + # Update the default values. + self._default_values = dict(self.__default_values__) + if default_values is not None: + self._default_values.update(default_values) + + if basis_gates is not None: + for gate in basis_gates: + if gate not in self.__supported_gates__: + raise CalibrationError( + f"Gate {gate} is not supported by {self.__class__.__name__}. " + f"Supported gates are: {self.__supported_gates__}." + ) + + self._basis_gates = basis_gates or self.__supported_gates__ + + def __getitem__(self, name: str) -> ScheduleBlock: + """Return the schedule.""" + if name not in self._schedules: + raise CalibrationError(f"Gate {name} is not contained in {self.__class__.__name__}.") + + return self._schedules[name] + + def __contains__(self, name: str) -> bool: + """Check if the basis gate is in the library.""" + return name in self._schedules + + @property + def basis_gates(self) -> List[str]: + """Return the basis gates supported by the library.""" + return list(name for name in self._schedules) + + @abstractmethod + def default_values(self) -> List[Tuple[ParameterValueType, Parameter, Tuple, ScheduleBlock]]: + """Return the default values for the parameters. + + Returns + A list of tuples is returned. These tuples are structured so that instances of + :class:`Calibrations` can call :meth:`add_parameter_value` on the tuples. + """ + + +class FixedFrequencyTransmon(BasisGateLibrary): + """A library of gates for fixed-frequency superconducting qubit architectures. + + Note that for now this library supports single-qubit gates and will be extended + in the future. + """ + + __default_values__ = {"duration": 160, "amp": 0.5, "β": 0.0} + + __supported_gates__ = ["x", "y", "sx", "sy"] + + def __init__( + self, + basis_gates: Optional[List[str]] = None, + default_values: Optional[Dict] = None, + use_drag: bool = True, + link_parameters: bool = True, + ): + """Setup the schedules. + + Args: + basis_gates: The basis gates to generate. + default_values: Default values for the parameters this dictionary can contain + the following keys: "duration", "amp", "β", and "σ". If "σ" is not provided + this library will take one fourth of the pulse duration as default value. + use_drag: If set to False then Gaussian pulses will be used instead of DRAG + pulses. + link_parameters: if set to True then the amplitude and DRAG parameters of the + X and Y gates will be linked as well as those of the SX and SY gates. + """ + super().__init__(basis_gates, default_values) + self._link_parameters = link_parameters + + dur = Parameter("duration") + sigma = Parameter("σ") + + # Generate the pulse parameters + def _beta(use_drag): + return Parameter("β") if use_drag else None + + x_amp, x_beta = Parameter("amp"), _beta(use_drag) + + if self._link_parameters: + y_amp, y_beta = 1.0j * x_amp, x_beta + else: + y_amp, y_beta = Parameter("amp"), _beta(use_drag) + + sx_amp, sx_beta = Parameter("amp"), _beta(use_drag) + + if self._link_parameters: + sy_amp, sy_beta = 1.0j * sx_amp, sx_beta + else: + sy_amp, sy_beta = Parameter("amp"), _beta(use_drag) + + # Create the schedules for the gates + sched_x = self._single_qubit_schedule("x", dur, x_amp, sigma, x_beta) + sched_y = self._single_qubit_schedule("y", dur, y_amp, sigma, y_beta) + sched_sx = self._single_qubit_schedule("sx", dur, sx_amp, sigma, sx_beta) + sched_sy = self._single_qubit_schedule("sy", dur, sy_amp, sigma, sy_beta) + + for sched in [sched_x, sched_y, sched_sx, sched_sy]: + if sched.name in self._basis_gates: + self._schedules[sched.name] = sched + + @staticmethod + def _single_qubit_schedule( + name: str, + dur: Parameter, + amp: Parameter, + sigma: Parameter, + beta: Optional[Parameter] = None, + ) -> ScheduleBlock: + """Build a single qubit pulse.""" + + chan = pulse.DriveChannel(Parameter("ch0")) + + if beta is not None: + with pulse.build(name=name) as sched: + pulse.play(pulse.Drag(duration=dur, amp=amp, sigma=sigma, beta=beta), chan) + else: + with pulse.build(name=name) as sched: + pulse.play(pulse.Gaussian(duration=dur, amp=amp, sigma=sigma), chan) + + return sched + + def default_values(self) -> List[Tuple[ParameterValueType, Parameter, Tuple, ScheduleBlock]]: + """Return the default values for the parameters. + + Returns + A list of tuples is returned. These tuples are structured so that instances of + :class:`Calibrations` can call :meth:`add_parameter_value` on the tuples. + """ + defaults = [] + for name in self.basis_gates: + schedule = self._schedules[name] + for param in schedule.parameters: + if "ch" not in param.name: + if "y" in name and self._link_parameters: + continue + + if param.name == "σ" and "σ" not in self._default_values: + value = self._default_values["duration"] / 4 + else: + value = self._default_values[param.name] + + if name in {"sx", "sy"} and param.name == "amp": + value /= 2.0 + + if "y" in name and param.name == "amp": + value *= 1.0j + + defaults.append((value, param.name, tuple(), name)) + + return defaults diff --git a/qiskit_experiments/calibration_management/update_library.py b/qiskit_experiments/calibration_management/update_library.py index 393106f3fd..0d6abaa559 100644 --- a/qiskit_experiments/calibration_management/update_library.py +++ b/qiskit_experiments/calibration_management/update_library.py @@ -209,7 +209,10 @@ def update( rate = 2 * np.pi * freq for angle, param, schedule in angles_schedules: - value = np.round(angle / rate, decimals=8) + qubits = exp_data.data(0)["metadata"]["qubits"] + prev_amp = calibrations.get_parameter_value(param, qubits, schedule, group=group) + + value = np.round(angle / rate, decimals=8) * np.exp(1.0j * np.angle(prev_amp)) cls._add_parameter_value(calibrations, exp_data, value, param, schedule, group) diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index bf58a4b938..c386b67d95 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -12,9 +12,12 @@ """Class to test the backend calibrations.""" +import qiskit.pulse as pulse from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeArmonk + from qiskit_experiments.calibration_management import BackendCalibrations +from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon class TestBackendCalibrations(QiskitTestCase): @@ -26,3 +29,25 @@ def test_run_options(self): self.assertEqual(cals.get_meas_frequencies(), [6993370669.000001]) self.assertEqual(cals.get_qubit_frequencies(), [4971852852.405576]) + + def test_setup_withLibrary(self): + """Test that we can setup with a library.""" + + cals = BackendCalibrations( + FakeArmonk(), + library=FixedFrequencyTransmon( + basis_gates=["x", "sx"], default_values={"duration": 320} + ), + ) + + # Check the x gate + with pulse.build(name="x") as expected: + pulse.play(pulse.Drag(duration=320, amp=0.5, sigma=80, beta=0), pulse.DriveChannel(0)) + + self.assertEqual(cals.get_schedule("x", (0,)), expected) + + # Check the sx gate + with pulse.build(name="sx") as expected: + pulse.play(pulse.Drag(duration=320, amp=0.25, sigma=80, beta=0), pulse.DriveChannel(0)) + + self.assertEqual(cals.get_schedule("sx", (0,)), expected) diff --git a/test/calibration/test_setup_library.py b/test/calibration/test_setup_library.py new file mode 100644 index 0000000000..370d12e80a --- /dev/null +++ b/test/calibration/test_setup_library.py @@ -0,0 +1,122 @@ +# 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 test the calibrations setup methods.""" + +import qiskit.pulse as pulse +from qiskit.test import QiskitTestCase + +from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon +from qiskit_experiments.exceptions import CalibrationError + + +class TestFixedFrequencyTransmon(QiskitTestCase): + """Test the various setup methods.""" + + def test_standard_single_qubit_gates(self): + """Test the setup of single-qubit gates.""" + + library = FixedFrequencyTransmon(default_values={"duration": 320}) + + for gate in ["x", "sx"]: + sched = library[gate] + self.assertTrue(isinstance(sched, pulse.ScheduleBlock)) + self.assertEqual(len(sched.parameters), 5) + + sched_x = library["x"] + sched_y = library["y"] + sched_sx = library["sx"] + sched_sy = library["sy"] + + self.assertEqual(sched_x.blocks[0].pulse.duration, sched_sx.blocks[0].pulse.duration) + self.assertEqual(sched_x.blocks[0].pulse.sigma, sched_sx.blocks[0].pulse.sigma) + + self.assertEqual(len(sched_x.parameters & sched_y.parameters), 4) + self.assertEqual(len(sched_sx.parameters & sched_sy.parameters), 4) + + expected = [ + (0.5, "amp", (), "x"), + (0.0, "β", (), "x"), + (320, "duration", (), "x"), + (80, "σ", (), "x"), + (320, "duration", (), "sx"), + (0.0, "β", (), "sx"), + (0.25, "amp", (), "sx"), + (80, "σ", (), "sx"), + ] + + for param_conf in library.default_values(): + self.assertTrue(param_conf in expected) + + # Check that an error gets raise if the gate is not in the library. + with self.assertRaises(CalibrationError): + print(library["bswap"]) + + # Test the basis gates of the library. + self.assertListEqual(library.basis_gates, ["x", "y", "sx", "sy"]) + + def test_turn_off_drag(self): + """Test the use_drag parameter.""" + + library = FixedFrequencyTransmon(use_drag=False) + self.assertTrue(isinstance(library["x"].blocks[0].pulse, pulse.Gaussian)) + + library = FixedFrequencyTransmon() + self.assertTrue(isinstance(library["x"].blocks[0].pulse, pulse.Drag)) + + def test_unlinked_parameters(self): + """Test the we get schedules with unlinked parameters.""" + + library = FixedFrequencyTransmon(link_parameters=False) + + sched_x = library["x"] + sched_y = library["y"] + sched_sx = library["sx"] + sched_sy = library["sy"] + + # Test the number of parameters. + self.assertEqual(len(sched_x.parameters & sched_y.parameters), 2) + self.assertEqual(len(sched_sx.parameters & sched_sy.parameters), 2) + + expected = [ + (0.5, "amp", (), "x"), + (0.0, "β", (), "x"), + (160, "duration", (), "x"), + (40, "σ", (), "x"), + (160, "duration", (), "sx"), + (0.0, "β", (), "sx"), + (0.25, "amp", (), "sx"), + (40, "σ", (), "sx"), + (0.5j, "amp", (), "y"), + (0.0, "β", (), "y"), + (160, "duration", (), "y"), + (40, "σ", (), "y"), + (160, "duration", (), "sy"), + (0.0, "β", (), "sy"), + (0.25j, "amp", (), "sy"), + (40, "σ", (), "sy"), + ] + + self.assertSetEqual(set(library.default_values()), set(expected)) + + def test_setup_partial_gates(self): + """Check that we do not setup all gates if not required.""" + + library = FixedFrequencyTransmon(basis_gates=["x", "sy"]) + + self.assertTrue("x" in library) + self.assertTrue("sy" in library) + self.assertTrue("y" not in library) + self.assertTrue("sx" not in library) + + with self.assertRaises(CalibrationError): + FixedFrequencyTransmon(basis_gates=["x", "bswap"]) diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index 60cfcbd576..2235554d59 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -40,20 +40,21 @@ def setUp(self): self.cals = Calibrations() self.qubit = 1 - amp = Parameter("amp") + axp = 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)) + pulse.play(pulse.Gaussian(duration=160, amp=axp, sigma=40), pulse.DriveChannel(chan)) - amp = Parameter("amp") + ax90p = Parameter("amp") with pulse.build(name="x90p") as x90p: - pulse.play(pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(chan)) + pulse.play(pulse.Gaussian(duration=160, amp=ax90p, sigma=40), pulse.DriveChannel(chan)) self.x90p = x90p self.cals.add_schedule(xp) self.cals.add_schedule(x90p) self.cals.add_parameter_value(0.2, "amp", self.qubit, "xp") + self.cals.add_parameter_value(0.1, "amp", self.qubit, "x90p") def test_amplitude(self): """Test amplitude update from Rabi.""" @@ -68,14 +69,14 @@ def test_amplitude(self): to_update = [(np.pi, "amp", "xp"), (np.pi / 2, "amp", self.x90p)] - self.assertEqual(len(self.cals.parameters_table()), 1) + self.assertEqual(len(self.cals.parameters_table()), 2) Amplitude.update(self.cals, exp_data, angles_schedules=to_update) with self.assertRaises(CalibrationError): self.cals.get_schedule("xp", qubits=0) - self.assertEqual(len(self.cals.parameters_table()), 3) + self.assertEqual(len(self.cals.parameters_table()), 4) # Now check the corresponding schedules result = exp_data.analysis_results(-1).data()