def update_calibrations(self, experiment_data: ExperimentData):
        """Update the drag parameter of the pulse in the calibrations."""

        result_index = self.experiment_options.result_index
        group = experiment_data.metadata["cal_group"]
        target_angle = experiment_data.metadata["target_angle"]
        qubits = experiment_data.metadata["physical_qubits"]

        schedule = self._cals.get_schedule(self._sched_name, qubits)

        # Obtain sigma as it is needed for the fine DRAG update rule.
        sigmas = []
        for block in schedule.blocks:
            if isinstance(block, Play) and hasattr(block.pulse, "sigma"):
                sigmas.append(getattr(block.pulse, "sigma"))

        if len(set(sigmas)) != 1:
            raise CalibrationError(
                "Cannot run fine Drag calibration on a schedule with multiple values of sigma."
            )

        if len(sigmas) == 0:
            raise CalibrationError(f"Could not infer sigma from {schedule}.")

        d_theta = BaseUpdater.get_value(experiment_data, "d_theta",
                                        result_index)

        # See the documentation in fine_drag.py for the derivation of this rule.
        d_beta = -np.sqrt(np.pi) * d_theta * sigmas[0] / target_angle**2
        old_beta = experiment_data.metadata["cal_param_value"]
        new_beta = old_beta + d_beta

        BaseUpdater.add_parameter_value(self._cals, experiment_data, new_beta,
                                        self._param_name, schedule, group)
Example #2
0
    def _get_frequencies(
        self,
        element: FrequencyElement,
        group: str = "default",
        cutoff_date: datetime = None,
    ) -> List[float]:
        """Internal helper method."""

        if element == FrequencyElement.READOUT:
            param = self.meas_freq.name
        elif element == FrequencyElement.QUBIT:
            param = self.qubit_freq.name
        else:
            raise CalibrationError(
                f"Frequency element {element} is not supported.")

        freqs = []
        for qubit in self._qubits:
            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]
                elif element == FrequencyElement.QUBIT:
                    freq = self._backend.defaults().qubit_freq_est[qubit]
                else:
                    raise CalibrationError(
                        f"Frequency element {element} is not supported.")

            freqs.append(freq)

        return freqs
Example #3
0
    def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
        """Create the circuits for the Rabi experiment.

        Args:
            backend: A backend object.

        Returns:
            A list of circuits with a rabi gate with an attached schedule. Each schedule
            will have a different value of the scanned amplitude.

        Raises:
            CalibrationError:
                - If the user-provided schedule does not contain a channel with an index
                  that matches the qubit on which to run the Rabi experiment.
                - If the user provided schedule has more than one free parameter.
        """
        schedule = self.experiment_options.get("schedule", None)

        if schedule is None:
            schedule = self._default_gate_schedule(backend=backend)
        else:
            if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels):
                raise CalibrationError(
                    f"User provided schedule {schedule.name} does not contain a channel "
                    "for the qubit on which to run Rabi."
                )

        if len(schedule.parameters) != 1:
            raise CalibrationError("Schedule in Rabi must have exactly one free parameter.")

        param = next(iter(schedule.parameters))

        # Create template circuit
        circuit = self._template_circuit(param)
        circuit.add_calibration(
            self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param]
        )

        # Create the circuits to run
        circs = []
        for amp in self.experiment_options.amplitudes:
            amp = np.round(amp, decimals=6)
            assigned_circ = circuit.assign_parameters({param: amp}, inplace=False)
            assigned_circ.metadata = {
                "experiment_type": self._type,
                "qubits": (self.physical_qubits[0],),
                "xval": amp,
                "unit": "arb. unit",
                "amplitude": amp,
                "schedule": str(schedule),
            }

            if backend:
                assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt", "n.a.")

            circs.append(assigned_circ)

        return circs
    def _validated_value(value: str) -> Union[int, float, complex]:
        """
        Convert the string representation of value to the correct type.

        Args:
            value: The string to convert to either an int, float, or complex.

        Returns:
            value converted to either int, float, or complex.

        Raises:
            CalibrationError: If the conversion fails.
        """
        try:
            return int(value)
        except ValueError:
            pass

        try:
            return float(value)
        except ValueError:
            pass

        try:
            return complex(value)
        except ValueError as val_err:
            raise CalibrationError(
                f"Could not convert {value} to int, float, or complex."
            ) from val_err
Example #5
0
    def __init__(
        self,
        basis_gates: Optional[List[str]] = None,
        default_values: Optional[Dict] = None,
        **extra_kwargs,
    ):
        """Setup the library.

        Args:
            basis_gates: The basis gates to generate.
            default_values: A dictionary to override library default parameter values.
            extra_kwargs: Extra key-word arguments of the subclasses that are saved to be able
                to reconstruct the library using the :meth:`__init__` method.

        Raises:
            CalibrationError: If on of the given basis gates is not supported by the library.
        """
        # Update the default values.
        self._extra_kwargs = extra_kwargs
        self._default_values = self.__default_values__.copy()
        if default_values is not None:
            self._default_values.update(default_values)

        if basis_gates is None:
            basis_gates = list(self.__supported_gates__)

        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._schedules = self._build_schedules(set(basis_gates))
    def get_template(
            self,
            schedule_name: str,
            qubits: Optional[Tuple[int, ...]] = None) -> ScheduleBlock:
        """Get a template schedule.

        Allows the user to get a template schedule that was previously registered.
        A template schedule will typically be fully parametric, i.e. all pulse
        parameters and channel indices are represented by :class:`Parameter`.

        Args:
            schedule_name: The name of the template schedule.
            qubits: The qubits under which the template schedule was registered.

        Returns:
            The registered template schedule.

        Raises:
            CalibrationError: if no template schedule for the given schedule name and qubits
                was registered.
        """
        key = ScheduleKey(schedule_name, self._to_tuple(qubits))

        if key in self._schedules:
            return self._schedules[key]

        if ScheduleKey(schedule_name, ()) in self._schedules:
            return self._schedules[ScheduleKey(schedule_name, ())]

        if qubits:
            msg = f"Could not find schedule {schedule_name} on qubits {qubits}."
        else:
            msg = f"Could not find schedule {schedule_name}."

        raise CalibrationError(msg)
Example #7
0
    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 load(cls, files: List[str]) -> "Calibrations":
     """
     Retrieves the parameterized schedules and pulse parameters from the
     given location.
     """
     raise CalibrationError(
         "Full calibration loading is not implemented yet.")
    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 None:
            self._basis_gates = self.__supported_gates__

        else:
            self._basis_gates = dict()

            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[gate] = self.__supported_gates__[gate]
Example #10
0
    def circuits(self) -> List[QuantumCircuit]:
        """Create the circuits for the fine amplitude calibration experiment.

        Returns:
            A list of circuits with a variable number of gates.

        Raises:
            CalibrationError: If the analysis options do not contain the angle_per_gate.
        """
        # Prepare the circuits.
        repetitions = self.experiment_options.get("repetitions")

        circuits = []

        if self.experiment_options.add_xp_circuit:
            # Note that the rotation error in this xval will be overweighted when calibrating xp
            # because it will be treated as a half pulse instead of a full pulse. However, since
            # the qubit population is first-order insensitive to rotation errors for an xp pulse
            # this point won't contribute much to inferring the angle error.
            angle_per_gate = self.analysis.options.get("angle_per_gate", None)
            phase_offset = self.analysis.options.get("phase_offset")

            if angle_per_gate is None:
                raise CalibrationError(
                    f"Unknown angle_per_gate for {self.__class__.__name__}. "
                    "Please set it in the analysis options.")

            circuit = QuantumCircuit(1)
            circuit.x(0)
            circuit.measure_all()

            circuit.metadata = {
                "experiment_type": self._type,
                "qubits": self.physical_qubits,
                "xval": (np.pi - phase_offset) / angle_per_gate,
                "unit": "gate number",
            }

            circuits.append(circuit)

        for repetition in repetitions:
            circuit = self._pre_circuit()

            for _ in range(repetition):
                circuit.append(self.experiment_options.gate, (0, ))

            circuit.measure_all()

            circuit.metadata = {
                "experiment_type": self._type,
                "qubits": self.physical_qubits,
                "xval": repetition,
                "unit": "gate number",
            }

            circuits.append(circuit)

        return circuits
    def update(
        cls,
        calibrations: Calibrations,
        exp_data: ExperimentData,
        result_index: Optional[int] = -1,
        group: str = "default",
        angles_schedules: List[Tuple[float, str, Union[str,
                                                       ScheduleBlock]]] = None,
        **options,
    ):
        """Update the amplitude of pulses.

        The value of the amplitude must be derived from the fit so the base method cannot be used.

        Args:
            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.
            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.
            options: Trailing options.

        Raises:
            CalibrationError: If the experiment is not of the supported type.
        """
        from qiskit_experiments.library.calibration.rabi import Rabi

        if angles_schedules is None:
            angles_schedules = [(np.pi, "amp", "xp")]

        if isinstance(exp_data.experiment, Rabi):
            rate = 2 * np.pi * BaseUpdater.get_value(exp_data, "rabi_rate",
                                                     result_index)

            for angle, param, schedule in angles_schedules:
                qubits = exp_data.metadata["physical_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)

        else:
            raise CalibrationError(
                f"{cls.__name__} updates from {type(Rabi.__name__)}.")
    def update(
        cls,
        calibrations: Calibrations,
        exp_data: ExperimentData,
        parameter: str,
        schedule: Union[ScheduleBlock, str],
        result_index: Optional[int] = -1,
        group: str = "default",
        target_angle: float = np.pi,
        **options,
    ):
        """Update the value of a drag parameter measured by the FineDrag experiment.

        Args:
            calibrations: The calibrations to update.
            exp_data: The experiment data from which to update.
            parameter: The name of the parameter in the calibrations to update.
            schedule: The ScheduleBlock instance or the name of the instance to which the parameter
                is attached.
            result_index: The result index to use. By default search entry by name.
            group: The calibrations group to update. Defaults to "default."
            target_angle: The target rotation angle of the pulse.
            options: Trailing options.

        Raises:
            CalibrationError: If we cannot get the pulse's standard deviation from the schedule.
        """
        qubits = exp_data.metadata["physical_qubits"]

        if isinstance(schedule, str):
            schedule = calibrations.get_schedule(schedule, qubits)

        # Obtain sigma as it is needed for the fine DRAG update rule.
        sigma = None
        for block in schedule.blocks:
            if isinstance(block, Play) and hasattr(block.pulse, "sigma"):
                sigma = getattr(block.pulse, "sigma")

        if sigma is None:
            raise CalibrationError(f"Could not infer sigma from {schedule}.")

        d_theta = BaseUpdater.get_value(exp_data, "d_theta", result_index)

        # See the documentation in fine_drag.py for the derivation of this rule.
        d_beta = -np.sqrt(np.pi) * d_theta * sigma / target_angle**2

        old_beta = calibrations.get_parameter_value(parameter,
                                                    qubits,
                                                    schedule,
                                                    group=group)
        new_beta = old_beta + d_beta

        cls.add_parameter_value(calibrations, exp_data, new_beta, parameter,
                                schedule, group)
Example #13
0
    def _default_gate_schedule(self, backend: Optional[Backend] = None):
        """Create the default schedule for the EFRabi gate with a frequency shift to the 1-2
        transition."""

        if self.experiment_options.frequency_shift is None:
            try:
                anharm, _ = backend.properties().qubit_property(self.physical_qubits[0])[
                    "anharmonicity"
                ]
                self.set_experiment_options(frequency_shift=anharm)
            except KeyError as key_err:
                raise CalibrationError(
                    f"The backend {backend} does not provide an anharmonicity for qubit "
                    f"{self.physical_qubits[0]}. Use EFRabi.set_experiment_options(frequency_shift="
                    f"anharmonicity) to manually set the correct frequency for the 1-2 transition."
                ) from key_err
            except AttributeError as att_err:
                raise CalibrationError(
                    "When creating the default schedule without passing a backend, the frequency needs "
                    "to be set manually through EFRabi.set_experiment_options(frequency_shift=..)."
                ) from att_err

        amp = Parameter("amp")
        with pulse.build(backend=backend, name=self.__rabi_gate_name__) as default_schedule:
            with pulse.frequency_offset(
                self.experiment_options.frequency_shift,
                pulse.DriveChannel(self.physical_qubits[0]),
            ):
                pulse.play(
                    pulse.Gaussian(
                        duration=self.experiment_options.duration,
                        amp=amp,
                        sigma=self.experiment_options.sigma,
                    ),
                    pulse.DriveChannel(self.physical_qubits[0]),
                )

        return default_schedule
Example #14
0
    def __init__(self):
        """Updaters are not meant to be instantiated.

        Instead of instantiating updaters use them by calling the :meth:`update` class method.
        For example, the :class:`Frequency` updater is called in the following way

         .. code-block:: python

            Frequency.update(calibrations, spectroscopy_data)

        Here, calibrations is an instance of :class:`Calibrations` and spectroscopy_data
        is the result of a :class:`QubitSpectroscopy` experiment.
        """
        raise CalibrationError(
            "Calibration updaters are not meant to be instantiated. The intended usage"
            "is Updater.update(calibrations, exp_data, ...).")
Example #15
0
    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}.")
Example #16
0
    def set_experiment_options(self, reps: Optional[List] = None, **fields):
        """Raise if reps has a length different from three.

        Raises:
            CalibrationError: if the number of repetitions is different from three.
        """

        if reps is None:
            reps = [1, 3, 5]
        else:
            reps = sorted(reps)  # ensure reps 1 is the lowest frequency.

        if len(reps) != 3:
            raise CalibrationError(
                f"{self.__class__.__name__} must use exactly three repetition numbers. "
                f"Received {reps} with length {len(reps)} != 3.")

        super().set_experiment_options(reps=reps, **fields)
    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)}."
            )
Example #18
0
    def _generate_fit_guesses(
        self, user_opt: curve.FitOptions
    ) -> Union[curve.FitOptions, List[curve.FitOptions]]:
        """Compute the initial guesses.

        Args:
            user_opt: Fit options filled with user provided guess and bounds.

        Returns:
            List of fit options that are passed to the fitter function.

        Raises:
            CalibrationError: When ``angle_per_gate`` is missing.
        """
        n_guesses = self._get_option("number_guesses")

        curve_data = self._data()
        max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)
        max_y, min_y = np.max(curve_data.y), np.min(curve_data.y)

        user_opt.bounds.set_if_empty(d_theta=(-np.pi, np.pi),
                                     base=(-max_abs_y, max_abs_y))
        user_opt.p0.set_if_empty(base=(max_y + min_y) / 2)

        if "amp" in user_opt.p0:
            user_opt.p0.set_if_empty(amp=max_y - min_y)
            user_opt.bounds.set_if_empty(amp=(-2 * max_abs_y, 2 * max_abs_y))

        # Base the initial guess on the intended angle_per_gate.
        angle_per_gate = self._get_option("angle_per_gate")

        if angle_per_gate is None:
            raise CalibrationError(
                "The angle_per_gate was not specified in the analysis options."
            )

        guess_range = max(abs(angle_per_gate), np.pi / 2)
        options = []
        for d_theta_guess in np.linspace(-guess_range, guess_range, n_guesses):
            new_opt = user_opt.copy()
            new_opt.p0.set_if_empty(d_theta=d_theta_guess)
            options.append(new_opt)

        return options
    def __post_init__(self):
        """
        Ensure that the variables in self have the proper types. This allows
        us to give strings to self.__init__ as input which is useful when loading
        serialized parameter values.
        """
        if isinstance(self.valid, str):
            if self.valid == "True":
                self.valid = True
            else:
                self.valid = False

        if isinstance(self.value, str):
            self.value = self._validated_value(self.value)

        if isinstance(self.date_time, str):
            base_fmt = "%Y-%m-%d %H:%M:%S.%f"
            zone_fmts = ["%z", "", "Z"]
            for time_zone in zone_fmts:
                date_format = base_fmt + time_zone
                try:
                    self.date_time = datetime.strptime(self.date_time,
                                                       date_format)
                    break
                except ValueError:
                    pass
            else:
                formats = list(base_fmt + zone for zone in zone_fmts)
                raise CalibrationError(
                    f"Cannot parse {self.date_time} in either of {formats} formats."
                )

        self.date_time = self.date_time.astimezone()

        if not isinstance(self.value, (int, float, complex)):
            raise CalibrationError(
                f"Values {self.value} must be int, float or complex.")

        if not isinstance(self.date_time, datetime):
            raise CalibrationError(
                f"Datetime {self.date_time} must be a datetime.")

        if not isinstance(self.valid, bool):
            raise CalibrationError(f"Valid {self.valid} is not a boolean.")

        if self.exp_id and not isinstance(self.exp_id, str):
            raise CalibrationError(
                f"Experiment id {self.exp_id} is not a string.")

        if not isinstance(self.group, str):
            raise CalibrationError(f"Group {self.group} is not a string.")
    def add_parameter_value(
        self,
        value: Union[int, float, complex, ParameterValue],
        param: Union[Parameter, str],
        qubits: Union[int, Tuple[int, ...]] = None,
        schedule: Union[ScheduleBlock, str] = None,
    ):
        """Add a parameter value to the stored parameters.

        This parameter value may be applied to several channels, for instance, all
        DRAG pulses may have the same standard deviation.

        Args:
            value: The value of the parameter to add. If an int, float, or complex is given
                then the timestamp of the parameter value will automatically be generated
                and set to the current local time of the user.
            param: The parameter or its name for which to add the measured value.
            qubits: The qubits to which this parameter applies.
            schedule: The schedule or its name for which to add the measured parameter value.

        Raises:
            CalibrationError: If the schedule name is given but no schedule with that name
                exists.
        """
        qubits = self._to_tuple(qubits)

        if isinstance(value, (int, float, complex)):
            value = ParameterValue(value,
                                   datetime.now(timezone.utc).astimezone())

        param_name = param.name if isinstance(param, Parameter) else param
        sched_name = schedule.name if isinstance(schedule,
                                                 ScheduleBlock) else schedule

        registered_schedules = set(key.schedule for key in self._schedules)

        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)
    def calibration_parameter(
        self,
        parameter_name: str,
        qubits: Union[int, Tuple[int, ...]] = None,
        schedule_name: str = None,
    ) -> Parameter:
        """Return a parameter given its keys.

        Returns a Parameter object given the triplet parameter_name, qubits and schedule_name
        which uniquely determine the context of a parameter.

        Args:
            parameter_name: Name of the parameter to get.
            qubits: The qubits to which this parameter belongs. If qubits is None then
                the default scope is assumed and the key will be an empty tuple.
            schedule_name: The name of the schedule to which this parameter belongs. A
                parameter may not belong to a schedule in which case None is accepted.

        Returns:
             calibration parameter: The parameter that corresponds to the given arguments.

        Raises:
            CalibrationError: If the desired parameter is not found.
        """
        qubits = self._to_tuple(qubits)

        # 1) Check for qubit specific parameters.
        if ParameterKey(parameter_name, qubits,
                        schedule_name) in self._parameter_map:
            return self._parameter_map[ParameterKey(parameter_name, qubits,
                                                    schedule_name)]

        # 2) Check for default parameters.
        elif ParameterKey(parameter_name, (),
                          schedule_name) in self._parameter_map:
            return self._parameter_map[ParameterKey(parameter_name, (),
                                                    schedule_name)]
        else:
            raise CalibrationError(
                f"No parameter for {parameter_name} and schedule {schedule_name} "
                f"and qubits {qubits}. No default value exists.")
Example #22
0
    def set_experiment_options(self, reps: Optional[List] = None, **fields):
        """Raise if reps has a length different from three.

        Raises:
            CalibrationError: if the number of repetitions is different from three.
        """
        if reps is not None:
            if len(reps) != 3:
                raise CalibrationError(
                    f"{self.__class__.__name__} must use exactly three repetition numbers. "
                    f"Received {reps} with length {len(reps)} != 3.")
            reps = sorted(reps)  # ensure reps 1 is the lowest frequency.
            super().set_experiment_options(reps=reps)

            if isinstance(self.analysis, DragCalAnalysis):
                self.analysis.set_options(fixed_parameters={
                    "reps0": reps[0],
                    "reps1": reps[1],
                    "reps2": reps[2]
                })

        super().set_experiment_options(**fields)
    def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]:
        """Ensure that qubits is a tuple of ints.

        Args:
            qubits: An int, a tuple of ints, or a string representing a tuple of ints.

        Returns:
            qubits: A tuple of ints.

        Raises:
            CalibrationError: If the given input does not conform to an int or
                tuple of ints.
        """
        if qubits is None:
            return tuple()

        if isinstance(qubits, str):
            try:
                return tuple(
                    int(qubit) for qubit in qubits.strip("( )").split(",")
                    if qubit != "")
            except ValueError:
                pass

        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

        raise CalibrationError(
            f"{qubits} must be int, tuple of ints, or str  that can be parsed"
            f"to a tuple if ints. Received {qubits}.")
    def add_schedule(
        self,
        schedule: ScheduleBlock,
        qubits: Union[int, Tuple[int, ...]] = None,
        num_qubits: Optional[int] = None,
    ):
        """Add a schedule block and register its parameters.

        Schedules that use Call instructions must register the called schedules separately.

        Args:
            schedule: The :class:`ScheduleBlock` to add.
            qubits: The qubits for which to add the schedules. If None or an empty tuple is
                given then this schedule is the default schedule for all qubits and, in this
                case, the number of qubits that this schedule act on must be given.
            num_qubits: The number of qubits that this schedule will act on when exported to
                a circuit instruction. This argument is optional as long as qubits is either
                not None or not an empty tuple (i.e. default schedule).

        Raises:
            CalibrationError: If schedule is not an instance of :class:`ScheduleBlock`.
            CalibrationError: If the parameterized channel index is not formatted properly.
            CalibrationError: If several parameters in the same schedule have the same name.
            CalibrationError: If a channel is parameterized by more than one parameter.
            CalibrationError: If the schedule name starts with the prefix of ScheduleBlock.
            CalibrationError: If the schedule calls subroutines that have not been registered.
            CalibrationError: If a :class:`Schedule` is Called instead of a :class:`ScheduleBlock`.
            CalibrationError: If a schedule with the same name exists and acts on a different
                number of qubits.

        """
        qubits = self._to_tuple(qubits)

        if len(qubits) == 0 and num_qubits is None:
            raise CalibrationError(
                "Both qubits and num_qubits cannot simultaneously be None.")

        num_qubits = len(qubits) or num_qubits

        if not isinstance(schedule, ScheduleBlock):
            raise CalibrationError(f"{schedule.name} is not a ScheduleBlock.")

        sched_key = ScheduleKey(schedule.name, qubits)

        # Ensure one to one mapping between name and number of qubits.
        if sched_key in self._schedules_qubits and self._schedules_qubits[
                sched_key] != num_qubits:
            raise CalibrationError(
                f"Cannot add schedule {schedule.name} acting on {num_qubits} qubits."
                "self already contains a schedule with the same name acting on "
                f"{self._schedules_qubits[sched_key]} qubits. Remove old schedule first."
            )

        # check that channels, if parameterized, have the proper name format.
        if schedule.name.startswith(ScheduleBlock.prefix):
            raise CalibrationError(
                f"{self.__class__.__name__} uses the `name` property of the schedule as part of a "
                f"database key. Using the automatically generated name {schedule.name} may have "
                f"unintended consequences. Please define a meaningful and unique schedule name."
            )

        param_indices = set()
        for ch in schedule.channels:
            if isinstance(ch.index, Parameter):
                if len(ch.index.parameters) != 1:
                    raise CalibrationError(
                        f"Channel {ch} can only have one parameter.")

                param_indices.add(ch.index)
                if re.compile(self.__channel_pattern__).match(
                        ch.index.name) is None:
                    raise CalibrationError(
                        f"Parameterized channel must correspond to {self.__channel_pattern__}"
                    )

        # Check that subroutines are present.
        for block in schedule.blocks:
            if isinstance(block, Call):
                if isinstance(block.subroutine, Schedule):
                    raise CalibrationError(
                        "Calling a Schedule is forbidden, call ScheduleBlock instead."
                    )

                if (block.subroutine.name, qubits) not in self._schedules:
                    raise CalibrationError(
                        f"Cannot register schedule block {schedule.name} with unregistered "
                        f"subroutine {block.subroutine.name}.")

        # Clean the parameter to schedule mapping. This is needed if we overwrite a schedule.
        self._clean_parameter_map(schedule.name, qubits)

        # Add the schedule.
        self._schedules[sched_key] = schedule
        self._schedules_qubits[sched_key] = num_qubits

        # Register parameters that are not indices.
        # Do not register parameters that are in call instructions.
        params_to_register = set()
        for inst in self._exclude_calls(schedule, []):
            for param in inst.parameters:
                if param not in param_indices:
                    params_to_register.add(param)

        if len(params_to_register) != len(
                set(param.name for param in params_to_register)):
            raise CalibrationError(
                f"Parameter names in {schedule.name} must be unique.")

        for param in params_to_register:
            self._register_parameter(param, qubits, schedule)
    def _get_channel_index(self, qubits: Tuple[int, ...],
                           chan: PulseChannel) -> int:
        """Get the index of the parameterized channel.

        The return index is determined from the given qubits and the name of the parameter
        in the channel index. The name of this parameter for control channels must be written
        as chqubit_index1.qubit_index2... followed by an optional $index. For example, the
        following parameter names are valid: 'ch1', 'ch1.0', 'ch30.12', and 'ch1.0$1'.

        Args:
            qubits: The qubits for which we want to obtain the channel index.
            chan: The channel with a parameterized name.

        Returns:
            index: The index of the channel. For example, if qubits=(10, 32) and
                chan is a control channel with parameterized index name 'ch1.0'
                the method returns the control channel corresponding to
                qubits (qubits[1], qubits[0]) which is here the control channel of
                qubits (32, 10).

        Raises:
            CalibrationError:
                - If the number of qubits is incorrect.
                - If the number of inferred ControlChannels is not correct.
                - If ch is not a DriveChannel, MeasureChannel, or ControlChannel.
        """
        if isinstance(chan.index, Parameter):
            if isinstance(chan, (DriveChannel, MeasureChannel, AcquireChannel,
                                 RegisterSlot, MemorySlot)):
                index = int(chan.index.name[2:].split("$")[0])

                if len(qubits) <= index:
                    raise CalibrationError(
                        f"Not enough qubits given for channel {chan}.")

                return qubits[index]

            # Control channels name example ch1.0$1
            if isinstance(chan, ControlChannel):

                channel_index_parts = chan.index.name[2:].split("$")
                qubit_channels = channel_index_parts[0]

                indices = [
                    int(sub_channel)
                    for sub_channel in qubit_channels.split(".")
                ]
                ch_qubits = tuple(qubits[index] for index in indices)
                chs_ = self._controls_config[ch_qubits]

                control_index = 0
                if len(channel_index_parts) == 2:
                    control_index = int(channel_index_parts[1])

                if len(chs_) <= control_index:
                    raise CalibrationError(
                        f"Control channel index {control_index} not found for qubits {qubits}."
                    )

                return chs_[control_index].index

            raise CalibrationError(
                f"{chan} must be a sub-type of {PulseChannel} or an {AcquireChannel}, "
                f"{RegisterSlot}, or a {MemorySlot}.")

        return chan.index
    def get_parameter_value(
        self,
        param: Union[Parameter, str],
        qubits: Union[int, Tuple[int, ...]],
        schedule: Union[ScheduleBlock, str, None] = None,
        valid_only: bool = True,
        group: str = "default",
        cutoff_date: datetime = None,
    ) -> Union[int, float, complex]:
        """Retrieves the value of a parameter.

        Parameters may be linked. :meth:`get_parameter_value` does the following steps:

        1. Retrieve the parameter object corresponding to (param, qubits, schedule).
        2. The values of this parameter may be stored under another schedule since
           schedules can share parameters. To deal with this, a list of candidate keys
           is created internally based on the current configuration.
        3. Look for candidate parameter values under the candidate keys.
        4. Filter the candidate parameter values according to their date (up until the
           cutoff_date), validity and calibration group.
        5. Return the most recent parameter.

        Args:
            param: The parameter or the name of the parameter for which to get the parameter value.
            qubits: The qubits for which to get the value of the parameter.
            schedule: The schedule or its name for which to get the parameter value.
            valid_only: Use only parameters marked as valid.
            group: The calibration group from which to draw the parameters.
                If not specified this defaults to the 'default' group.
            cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters
                generated after the cutoff date will be ignored. If the cutoff_date is None then
                all parameters are considered. This allows users to discard more recent values that
                may be erroneous.

        Returns:
            value: The value of the parameter.

        Raises:
            CalibrationError: If there is no parameter value for the given parameter name and
                pulse channel.
        """
        qubits = self._to_tuple(qubits)

        # 1) Identify the parameter object.
        param_name = param.name if isinstance(param, Parameter) else param
        sched_name = schedule.name if isinstance(schedule,
                                                 ScheduleBlock) else schedule

        param = self.calibration_parameter(param_name, qubits, sched_name)

        # 2) Get a list of candidate keys restricted to the qubits of interest.
        candidate_keys = []
        for key in self._parameter_map_r[param]:
            candidate_keys.append(
                ParameterKey(key.parameter, qubits, key.schedule))

        # 3) Loop though the candidate keys to candidate values
        candidates = []
        for key in candidate_keys:
            if key in self._params:
                candidates += self._params[key]

        # If no candidate parameter values were found look for default parameters
        # i.e. parameters that do not specify a qubit.
        if len(candidates) == 0:
            for key in candidate_keys:
                if ParameterKey(key.parameter, (),
                                key.schedule) in self._params:
                    candidates += self._params[ParameterKey(
                        key.parameter, (), key.schedule)]

        # 4) Filter candidate parameter values.
        if valid_only:
            candidates = [val for val in candidates if val.valid]

        candidates = [val for val in candidates if val.group == group]

        if cutoff_date:
            candidates = [
                val for val in candidates if val.date_time <= cutoff_date
            ]

        if len(candidates) == 0:
            msg = f"No candidate parameter values for {param_name} in calibration group {group} "

            if qubits:
                msg += f"on qubits {qubits} "

            if sched_name:
                msg += f"in schedule {sched_name} "

            if cutoff_date:
                msg += f"with cutoff date: {cutoff_date}"

            raise CalibrationError(msg)

        # 5) Return the most recent parameter.
        return max(enumerate(candidates), key=lambda x:
                   (x[1].date_time, x[0]))[1].value
    def get_schedule(
        self,
        name: str,
        qubits: Union[int, Tuple[int, ...]],
        assign_params: Dict[Union[str, ParameterKey],
                            ParameterValueType] = None,
        group: Optional[str] = "default",
        cutoff_date: datetime = None,
    ) -> ScheduleBlock:
        """Get the template schedule with parameters assigned to values.

        All the parameters in the template schedule block will be assigned to the values managed
        by the calibrations unless they are specified in assign_params. In this case the value in
        assign_params will override the value stored by the calibrations. A parameter value in
        assign_params may also be a :class:`ParameterExpression`.

        .. code-block:: python

            # Get an xp schedule with a parametric amplitude
            sched = cals.get_schedule("xp", 3, assign_params={"amp": Parameter("amp")})

            # Get an echoed-cross-resonance schedule between qubits (0, 2) where the xp echo gates
            # are Called schedules but leave their amplitudes as parameters.
            assign_dict = {("amp", (0,), "xp"): Parameter("my_amp")}
            sched = cals.get_schedule("cr", (0, 2), assign_params=assign_dict)

        Args:
            name: The name of the schedule to get.
            qubits: The qubits for which to get the schedule.
            assign_params: The parameters to assign manually. Each parameter is specified by a
                ParameterKey which is a named tuple of the form (parameter name, qubits,
                schedule name). Each entry in assign_params can also be a string corresponding
                to the name of the parameter. In this case, the schedule name and qubits of the
                corresponding ParameterKey will be the name and qubits given as arguments to
                get_schedule.
            group: The calibration group from which to draw the parameters. If not specified
                this defaults to the 'default' group.
            cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters
                generated after the cutoff date will be ignored. If the cutoff_date is None then
                all parameters are considered. This allows users to discard more recent values that
                may be erroneous.

        Returns:
            schedule: A copy of the template schedule with all parameters assigned
            except for those specified by assign_params.

        Raises:
            CalibrationError: If the name of the schedule is not known.
            CalibrationError: If a parameter could not be found.
        """
        qubits = self._to_tuple(qubits)

        # Standardize the input in the assignment dictionary
        if assign_params:
            assign_params_ = dict()
            for assign_param, value in assign_params.items():
                if isinstance(assign_param, str):
                    assign_params_[ParameterKey(assign_param, qubits,
                                                name)] = value
                else:
                    assign_params_[ParameterKey(*assign_param)] = value

            assign_params = assign_params_
        else:
            assign_params = dict()

        # Get the template schedule
        if (name, qubits) in self._schedules:
            schedule = self._schedules[ScheduleKey(name, qubits)]
        elif (name, ()) in self._schedules:
            schedule = self._schedules[ScheduleKey(name, ())]
        else:
            raise CalibrationError(
                f"Schedule {name} is not defined for qubits {qubits}.")

        # Retrieve the channel indices based on the qubits and bind them.
        binding_dict = {}
        for ch in schedule.channels:
            if ch.is_parameterized():
                binding_dict[ch.index] = self._get_channel_index(qubits, ch)

        # Binding the channel indices makes it easier to deal with parameters later on
        schedule = schedule.assign_parameters(binding_dict, inplace=False)

        # Now assign the other parameters
        assigned_schedule = self._assign(schedule, qubits, assign_params,
                                         group, cutoff_date)

        free_params = set()
        for param in assign_params.values():
            if isinstance(param, ParameterExpression):
                free_params.add(param)

        if len(assigned_schedule.parameters) != len(free_params):
            raise CalibrationError(
                f"The number of free parameters {len(assigned_schedule.parameters)} in "
                f"the assigned schedule differs from the requested number of free "
                f"parameters {len(free_params)}.")

        return assigned_schedule
Example #28
0
    def __init__(
        self,
        backend: Backend,
        library: BasisGateLibrary = None,
        num_qubits: Optional[int] = 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.
            num_qubits: Number of qubits in case the backend object fails to specify this in its
                configuration.

        Raises:
            CalibrationError: If the backend configuration does not have num_qubits and num_qubits
                is None.
        """
        super().__init__(
            getattr(backend.configuration(), "control_channels", None))

        # Instruction schedule map variables and support variables.
        self._inst_map = InstructionScheduleMap()
        self._operated_qubits = defaultdict(list)
        self._update_inst_map = False  # When True add_parameter_value triggers an inst. map update

        # Use the same naming convention as in backend.defaults()
        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, ())

        num_qubits = getattr(backend.configuration(), "num_qubits", num_qubits)
        if num_qubits is None:
            raise CalibrationError(
                "backend.configuration() does not have 'num_qubits' and None given."
            )

        self._qubits = list(range(num_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)

        if library is not None:

            # Add the basis gates
            for gate in library.basis_gates:
                self.add_schedule(library[gate],
                                  num_qubits=library.num_qubits(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)

        self._update_inst_map = True

        # Push the schedules to the instruction schedule map.
        self.update_inst_map()
    def _assign(
        self,
        schedule: ScheduleBlock,
        qubits: Tuple[int, ...],
        assign_params: Dict[Union[str, ParameterKey], ParameterValueType],
        group: Optional[str] = "default",
        cutoff_date: datetime = None,
    ) -> ScheduleBlock:
        """Recursively assign parameters in a schedule.

        The recursive behaviour is needed to handle Call instructions as the name of
        the called instruction defines the scope of the parameter. Each time a Call
        is found _assign recurses on the channel-assigned subroutine of the Call
        instruction and the qubits that are in said subroutine. This requires a
        careful extraction of the qubits from the subroutine and in the appropriate
        order. Next, the parameters are identified and assigned. This is needed to
        handle situations where the same parameterized schedule is called but on
        different channels. For example,

        .. code-block:: python

            ch0 = Parameter("ch0")
            ch1 = Parameter("ch1")

            with pulse.build(name="xp") as xp:
                pulse.play(Gaussian(duration, amp, sigma), DriveChannel(ch0))

            with pulse.build(name="xt_xp") as xt:
                pulse.call(xp)
                pulse.call(xp, value_dict={ch0: ch1})

        Here, we define the xp :class:`ScheduleBlock` for all qubits as a Gaussian. Next, we define
        a schedule where both xp schedules are called simultaneously on different channels. We now
        explain a subtlety related to manually assigning values in the case above. In the schedule
        above, the parameters of the Gaussian pulses are coupled, e.g. the xp pulse on ch0 and ch1
        share the same instance of :class:`ParameterExpression`. Suppose now that both pulses have
        a duration and sigma of 160 and 40 samples, respectively, and that the amplitudes are 0.5
        and 0.3 for qubits 0 and 2, respectively. These values are stored in self._params. When
        retrieving a schedule without specifying assign_params, i.e.

        .. code-block:: python

            cals.get_schedule("xt_xp", (0, 2))

        we will obtain the expected schedule with amplitudes 0.5 and 0.3. When specifying the
        following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we
        will obtain a schedule where the amplitudes of the xp pulse on qubit 0 is set to
        :code:`Parameter("my_new_amp")`. The amplitude of the xp pulse on qubit 2 is set to
        the value stored by the calibrations, i.e. 0.3.

        .. code-bloc:: python

            cals.get_schedule(
                "xt_xp",
                (0, 2),
                assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}
            )

        Args:
            schedule: The schedule with assigned channel indices for which we wish to
                assign values to non-channel parameters.
            qubits: The qubits for which to get the schedule.
            assign_params: The parameters to manually assign. See get_schedules for details.
            group: The calibration group of the parameters.
            cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters
                generated after the cutoff date will be ignored. If the cutoff_date is None then
                all parameters are considered. This allows users to discard more recent values that
                may be erroneous.

        Returns:
            ret_schedule: The schedule with assigned parameters.

        Raises:
            CalibrationError:
                - If a channel has not been assigned.
                - If there is an ambiguous parameter assignment.
                - If there are inconsistencies between a called schedule and the template
                  schedule registered under the name of the called schedule.
        """
        # 1) Restrict the given qubits to those in the given schedule.
        qubit_set = set()
        for chan in schedule.channels:
            if isinstance(chan.index, ParameterExpression):
                raise (CalibrationError(
                    f"All parametric channels must be assigned before searching for "
                    f"non-channel parameters. {chan} is parametric."))
            if isinstance(chan, (DriveChannel, MeasureChannel)):
                qubit_set.add(chan.index)

            if isinstance(chan, ControlChannel):
                for qubit in self._controls_config_r[chan]:
                    qubit_set.add(qubit)

        qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set)

        # 2) Recursively assign the parameters in the called instructions.
        ret_schedule = ScheduleBlock(
            alignment_context=schedule.alignment_context,
            name=schedule.name,
            metadata=schedule.metadata,
        )

        for inst in schedule.blocks:
            if isinstance(inst, Call):
                # Check that there are no inconsistencies with the called subroutines.
                template_subroutine = self.get_template(
                    inst.subroutine.name, qubits_)
                if inst.subroutine != template_subroutine:
                    raise CalibrationError(
                        f"The subroutine {inst.subroutine.name} called by {inst.name} does not "
                        f"match the template schedule stored under {template_subroutine.name}."
                    )

                inst = inst.assigned_subroutine()

            if isinstance(inst, ScheduleBlock):
                inst = self._assign(inst, qubits_, assign_params, group,
                                    cutoff_date)

            ret_schedule.append(inst, inplace=True)

        # 3) Get the parameter keys of the remaining instructions. At this point in
        #    _assign all parameters in Call instructions that are supposed to be
        #     assigned have been assigned.
        keys = set()

        if ret_schedule.name in set(key.schedule
                                    for key in self._parameter_map):
            for param in ret_schedule.parameters:
                keys.add(ParameterKey(param.name, qubits_, ret_schedule.name))

        # 4) Build the parameter binding dictionary.
        binding_dict = {}
        assignment_table = {}
        for key, value in assign_params.items():
            key_orig = key
            if key.qubits == ():
                key = ParameterKey(key.parameter, qubits_, key.schedule)
                if key in assign_params:
                    # if (param, (1,), sched) and (param, (), sched) are both
                    # in assign_params, skip the default value instead of
                    # possibly triggering an error about conflicting
                    # parameters.
                    continue
            elif key.qubits != qubits_:
                continue
            param = self.calibration_parameter(*key)
            if param in ret_schedule.parameters:
                assign_okay = (
                    param not in binding_dict
                    or key.schedule == ret_schedule.name
                    and assignment_table[param].schedule != ret_schedule.name)
                if assign_okay:
                    binding_dict[param] = value
                    assignment_table[param] = key_orig
                elif (key.schedule == ret_schedule.name
                      or assignment_table[param].schedule != ret_schedule.name
                      ) and binding_dict[param] != value:
                    raise CalibrationError(
                        "Ambiguous assignment: assign_params keys "
                        f"{key_orig} and {assignment_table[param]} "
                        "resolve to the same parameter.")

        for key in keys:
            # Get the parameter object. Since we are dealing with a schedule the name of
            # the schedule is always defined. However, the parameter may be a default
            # parameter for all qubits, i.e. qubits may be an empty tuple.
            param = self.calibration_parameter(*key)

            if param not in binding_dict:
                binding_dict[param] = self.get_parameter_value(
                    key.parameter,
                    key.qubits,
                    key.schedule,
                    group=group,
                    cutoff_date=cutoff_date,
                )

        return ret_schedule.assign_parameters(binding_dict, inplace=False)
    def save(
        self,
        file_type: str = "csv",
        folder: str = None,
        overwrite: bool = False,
        file_prefix: str = "",
    ):
        """Save the parameterized schedules and parameter value.

        The schedules and parameter values can be stored in csv files. This method creates
        three files:

        * parameter_config.csv: This file stores a table of parameters which indicates
          which parameters appear in which schedules.
        * parameter_values.csv: This file stores the values of the calibrated parameters.
        * schedules.csv: This file stores the parameterized schedules.

        Warning:
            Schedule blocks will only be saved in string format and can therefore not be
            reloaded and must instead be rebuilt.

        Args:
            file_type: The type of file to which to save. By default this is a csv.
                Other file types may be supported in the future.
            folder: The folder in which to save the calibrations.
            overwrite: If the files already exist then they will not be overwritten
                unless overwrite is set to True.
            file_prefix: A prefix to add to the name of the files such as a date tag or a
                UUID.

        Raises:
            CalibrationError: if the files exist and overwrite is not set to True.
        """
        warnings.warn(
            "Schedules are only saved in text format. They cannot be re-loaded."
        )

        cwd = os.getcwd()
        if folder:
            os.chdir(folder)

        parameter_config_file = file_prefix + "parameter_config.csv"
        parameter_value_file = file_prefix + "parameter_values.csv"
        schedule_file = file_prefix + "schedules.csv"

        if os.path.isfile(parameter_config_file) and not overwrite:
            raise CalibrationError(
                f"{parameter_config_file} already exists. Set overwrite to True."
            )

        if os.path.isfile(parameter_value_file) and not overwrite:
            raise CalibrationError(
                f"{parameter_value_file} already exists. Set overwrite to True."
            )

        if os.path.isfile(schedule_file) and not overwrite:
            raise CalibrationError(
                f"{schedule_file} already exists. Set overwrite to True.")

        # Write the parameter configuration.
        header_keys = [
            "parameter.name", "parameter unique id", "schedule", "qubits"
        ]
        body = []

        for parameter, keys in self.parameters.items():
            for key in keys:
                body.append({
                    "parameter.name":
                    parameter.name,
                    "parameter unique id":
                    self._hash_to_counter_map[parameter],
                    "schedule":
                    key.schedule,
                    "qubits":
                    key.qubits,
                })

        if file_type == "csv":
            with open(parameter_config_file, "w", newline="",
                      encoding="utf-8") as output_file:
                dict_writer = csv.DictWriter(output_file, header_keys)
                dict_writer.writeheader()
                dict_writer.writerows(body)

            # Write the values of the parameters.
            values = self.parameters_table()["data"]
            if len(values) > 0:
                header_keys = values[0].keys()

                with open(parameter_value_file,
                          "w",
                          newline="",
                          encoding="utf-8") as output_file:
                    dict_writer = csv.DictWriter(output_file, header_keys)
                    dict_writer.writeheader()
                    dict_writer.writerows(values)

            # Serialize the schedules. For now we just print them.
            schedules = []
            header_keys = ["name", "qubits", "schedule"]
            for key, sched in self._schedules.items():
                schedules.append({
                    "name": key.schedule,
                    "qubits": key.qubits,
                    "schedule": str(sched)
                })

            with open(schedule_file, "w", newline="",
                      encoding="utf-8") as output_file:
                dict_writer = csv.DictWriter(output_file, header_keys)
                dict_writer.writeheader()
                dict_writer.writerows(schedules)

        else:
            raise CalibrationError(
                f"Saving to .{file_type} is not yet supported.")

        os.chdir(cwd)