Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
19f8899
* Added the update method to the calibrations.
eggerdj Jun 7, 2021
a19c821
* Used ParameterValue in update.
eggerdj Jun 7, 2021
71553c7
Update qiskit_experiments/calibration/calibrations.py
eggerdj Jun 10, 2021
4904e42
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 10, 2021
9c6ef7f
* Updated docstring.
eggerdj Jun 10, 2021
299c959
* Added completion times.
eggerdj Jun 10, 2021
43a00cf
* More robust quality check.
eggerdj Jun 10, 2021
383fc08
* Changed default result index to -1.
eggerdj Jun 10, 2021
61c0920
* Updated spectroscopy integration.
eggerdj Jun 10, 2021
c1e29db
* Renamed calibration_types to calibration_key_types.
eggerdj Jun 10, 2021
4f2f40f
* Removed CalibrationExtraction.
eggerdj Jun 10, 2021
c7a4b71
* Removed CalibrationExtraction.
eggerdj Jun 10, 2021
f276c7b
* Black lint
eggerdj Jun 10, 2021
991f8ea
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 14, 2021
9a76125
Update qiskit_experiments/calibration/calibration_key_types.py
eggerdj Jun 15, 2021
f6f2d83
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 15, 2021
3fea5a4
* Added name for readability.
eggerdj Jun 15, 2021
722ba59
* Updated docstring.
eggerdj Jun 15, 2021
4e31847
Update qiskit_experiments/calibration/calibrations.py
eggerdj Jun 15, 2021
ae4a7c9
* Updated default timestamp to now.
eggerdj Jun 15, 2021
f032b5d
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 15, 2021
0d8fc79
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 16, 2021
ff9d254
Update qiskit_experiments/calibration/calibrations.py
eggerdj Jun 16, 2021
9f72426
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 17, 2021
8cd5c89
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 18, 2021
e0e3bfd
* Added an updater library.
eggerdj Jun 18, 2021
db00f3b
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 20, 2021
a9a38b9
* Made update methodology more permissible.
eggerdj Jun 20, 2021
c3f5907
* Black
eggerdj Jun 20, 2021
468af33
* Moved methodology to class methods.
eggerdj Jun 21, 2021
d9d47df
* Bug fixes, black, and lint.
eggerdj Jun 21, 2021
864f3af
* Qubits in Rabi.
eggerdj Jun 21, 2021
744bb17
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into cali…
eggerdj Jun 21, 2021
b818297
Update qiskit_experiments/calibration/update_library.py
eggerdj Jun 22, 2021
8e65eef
* Improved docstring.
eggerdj Jun 22, 2021
23b3a04
Update qiskit_experiments/calibration/update_library.py
eggerdj Jun 22, 2021
d4c792d
* Raise on Update instantiation.
eggerdj Jun 22, 2021
fac68ec
Merge branch 'calibrations_spec_integration_v3' of github.com:eggerdj…
eggerdj Jun 22, 2021
c2b4109
* Moved cals update tests to their own library.
eggerdj Jun 22, 2021
90b5617
* Renamed test.
eggerdj Jun 22, 2021
762ded0
* Made _add_parameter_value a static method.
eggerdj Jun 23, 2021
ce9b9fc
* Switched arguments order in update library.
eggerdj Jun 23, 2021
cac9706
* Added edgecase handling in the calibrations for parameter value add…
eggerdj Jun 23, 2021
e1e5a72
Merge branch 'main' into calibrations_spec_integration_v3
eggerdj Jun 23, 2021
4bb5861
* Changed behaviour of get_parameter_value.
eggerdj Jun 23, 2021
1262246
* Removed the adding of a microsecond.
eggerdj Jun 23, 2021
206baa1
* Test docstring fix.
eggerdj Jun 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions qiskit_experiments/calibration/backend_calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,33 @@ class BackendCalibrations(Calibrations):
any schedule.
"""

__qubit_freq_parameter__ = "qubit_lo_freq"
__readout_freq_parameter__ = "meas_lo_freq"

def __init__(self, backend: Backend):
"""Setup an instance to manage the calibrations of a backend."""
super().__init__(backend.configuration().control_channels)
if hasattr(backend.configuration(), "control_channels"):
control_channels = backend.configuration().control_channels
else:
control_channels = None

super().__init__(control_channels)

# Use the same naming convention as in backend.defaults()
self.qubit_freq = Parameter("qubit_lo_freq")
self.meas_freq = Parameter("meas_lo_freq")
self.qubit_freq = Parameter(self.__qubit_freq_parameter__)
self.meas_freq = Parameter(self.__readout_freq_parameter__)
self._register_parameter(self.qubit_freq, ())
self._register_parameter(self.meas_freq, ())

self._qubits = set(range(backend.configuration().n_qubits))
self._backend = backend

for qubit, freq in enumerate(backend.defaults().qubit_freq_est):
self.add_parameter_value(freq, self.qubit_freq, qubit)

for meas, freq in enumerate(backend.defaults().meas_freq_est):
self.add_parameter_value(freq, self.meas_freq, meas)

def _get_frequencies(
self,
element: FrequencyElement,
Expand All @@ -70,8 +84,9 @@ def _get_frequencies(

freqs = []
for qubit in self._qubits:
if ParameterKey(None, param, (qubit,)) in self._params:
freq = self.get_parameter_value(param, (qubit,), None, True, group, cutoff_date)
schedule = None # A qubit frequency is not attached to a schedule.
if ParameterKey(param, (qubit,), schedule) in self._params:
freq = self.get_parameter_value(param, (qubit,), schedule, True, group, cutoff_date)
else:
if element == FrequencyElement.READOUT:
freq = self._backend.defaults().meas_freq_est[qubit]
Expand Down
23 changes: 23 additions & 0 deletions qiskit_experiments/calibration/calibration_key_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Types used by the calibration module."""

from typing import Union
from collections import namedtuple

from qiskit.circuit import ParameterExpression


ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"])
ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"])
ParameterValueType = Union[ParameterExpression, float, int, complex]
Comment thread
eggerdj marked this conversation as resolved.
18 changes: 11 additions & 7 deletions qiskit_experiments/calibration/calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Class to store and manage the results of calibration experiments."""

import os
from collections import namedtuple, defaultdict
from collections import defaultdict
from datetime import datetime
from typing import Any, Dict, Set, Tuple, Union, List, Optional
import csv
Expand All @@ -37,10 +37,11 @@
from qiskit.circuit import Parameter, ParameterExpression
from qiskit_experiments.calibration.exceptions import CalibrationError
from qiskit_experiments.calibration.parameter_value import ParameterValue

ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"])
ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"])
ParameterValueType = Union[ParameterExpression, float, int, complex]
from qiskit_experiments.calibration.calibration_key_types import (
ParameterKey,
ParameterValueType,
ScheduleKey,
)


class Calibrations:
Expand Down Expand Up @@ -545,7 +546,7 @@ def get_parameter_value(
raise CalibrationError(msg)

# 5) Return the most recent parameter.
return max(candidates, key=lambda x: x.date_time).value
return max(enumerate(candidates), key=lambda x: (x[1].date_time, x[0]))[1].value

def get_schedule(
self,
Expand Down Expand Up @@ -1025,7 +1026,7 @@ def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]:
CalibrationError: If the given input does not conform to an int or
tuple of ints.
"""
if not qubits:
if qubits is None:
return tuple()

if isinstance(qubits, str):
Expand All @@ -1037,6 +1038,9 @@ def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]:
if isinstance(qubits, int):
return (qubits,)

if isinstance(qubits, list):
return tuple(qubits)

if isinstance(qubits, tuple):
if all(isinstance(n, int) for n in qubits):
return qubits
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/calibration/experiments/rabi.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
assigned_circ = circuit.assign_parameters({param: amp}, inplace=False)
assigned_circ.metadata = {
"experiment_type": self._type,
"qubit": self.physical_qubits[0],
"qubits": (self.physical_qubits[0],),
"xval": amp,
"unit": "arb. unit",
"amplitude": amp,
Expand Down
183 changes: 183 additions & 0 deletions qiskit_experiments/calibration/update_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""A library of experiment calibrations."""

from abc import ABC, abstractmethod
from datetime import datetime
from typing import List, Tuple, Union
import numpy as np

from qiskit.circuit import Parameter
from qiskit.pulse import ScheduleBlock

from qiskit_experiments.experiment_data import ExperimentData
from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations
from qiskit_experiments.calibration.calibrations import Calibrations
from qiskit_experiments.calibration.parameter_value import ParameterValue
from qiskit_experiments.calibration.exceptions import CalibrationError
from qiskit_experiments.calibration.calibration_key_types import ParameterValueType


class BaseUpdater(ABC):
"""A base class to update calibrations."""

def __init__(self):
"""Updaters are not meant to be instantiated."""
raise CalibrationError(
"Calibration updaters are not meant to be instantiated. The intended usage"
"is Updater.update(calibrations, exp_data, ...)."
)

@staticmethod
def _time_stamp(exp_data: ExperimentData) -> datetime:
"""Helper method to extract the datetime."""
all_times = exp_data.completion_times.values()
if all_times:
return max(all_times)

return datetime.now()

@classmethod
def _add_parameter_value(
Comment thread
eggerdj marked this conversation as resolved.
cls,
cal: Calibrations,
exp_data: ExperimentData,
value: ParameterValueType,
param: Union[Parameter, str],
schedule: Union[ScheduleBlock, str] = None,
group: str = "default",
):
"""Update the calibrations with the given value.

Args:
cal: The Calibrations instance to update.
exp_data: The ExperimentData instance that contains the result and the experiment data.
value: The value extracted by the subclasses in the :meth:`update` method.
param: The name of the parameter, or the parameter instance, which will receive an
updated value.
schedule: The ScheduleBlock instance or the name of the instance to which the parameter
is attached.
group: The calibrations group to update.
"""

qubits = exp_data.data(0)["metadata"]["qubits"]

param_value = ParameterValue(
value=value,
date_time=cls._time_stamp(exp_data),
group=group,
exp_id=exp_data.experiment_id,
)

cal.add_parameter_value(param_value, param, qubits, schedule)

@classmethod
@abstractmethod
def update(cls, calibrations: BackendCalibrations, exp_data: ExperimentData, **options):
"""Update the calibrations based on the data.

Child update classes must implement this function. This function defines how the data
is extracted from an experiment and then used to update the values of one or more
parameters in the calibrations.
"""


class Frequency(BaseUpdater):
"""Update frequencies."""

# pylint: disable=arguments-differ
@classmethod
def update(
cls,
calibrations: BackendCalibrations,
exp_data: ExperimentData,
result_index: int = -1,
group: str = "default",
parameter: str = BackendCalibrations.__qubit_freq_parameter__,
):
"""Update a qubit frequency from, e.g., QubitSpectroscopy.

Args:
calibrations: The calibrations to update.
exp_data: The experiment data from which to update.
result_index: The result index to use, defaults to -1.
group: The calibrations group to update. Defaults to "default."
parameter: The name of the parameter to update. If it is not specified
this will default to the qubit frequency.

Raises:
CalibrationError: If the analysis result does not contain a frequency variable.
"""

from qiskit_experiments.characterization.qubit_spectroscopy import SpectroscopyAnalysis

result = exp_data.analysis_result(result_index)

if "freq" not in result["popt_keys"]:
raise CalibrationError(
f"{cls.__name__} updates from analysis classes such as "
f'{type(SpectroscopyAnalysis.__name__)} which report "freq" in popt.'
)

param = parameter
value = result["popt"][result["popt_keys"].index("freq")]

cls._add_parameter_value(calibrations, exp_data, value, param, schedule=None, group=group)


class Amplitude(BaseUpdater):
"""Update pulse amplitudes."""

# pylint: disable=arguments-differ
@classmethod
def update(
cls,
calibrations: Calibrations,
exp_data: ExperimentData,
result_index: int = -1,
group: str = "default",
angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None,
):
"""Update the amplitude of pulses.

Args:
calibrations: The calibrations to update.
exp_data: The experiment data from which to update.
result_index: The result index to use, defaults to -1.
group: The calibrations group to update. Defaults to "default."
angles_schedules: A list of tuples specifying which angle to update for which
pulse schedule. Each tuple is of the form: (angle, parameter_name,
schedule). Here, angle is the rotation angle for which to extract the amplitude,
parameter_name is the name of the parameter whose value is to be updated, and
schedule is the schedule or its name that contains the parameter.

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

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

if isinstance(exp_data.experiment, Rabi):
result = exp_data.analysis_result(result_index)

freq = result["popt"][result["popt_keys"].index("freq")]
rate = 2 * np.pi * freq

for angle, param, schedule in angles_schedules:
value = np.round(angle / rate, decimals=8)

cls._add_parameter_value(calibrations, exp_data, value, param, schedule, group)
else:
raise CalibrationError(f"{cls.__name__} updates from {type(Rabi.__name__)}.")
4 changes: 3 additions & 1 deletion qiskit_experiments/characterization/qubit_spectroscopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,12 @@ def circuits(self, backend: Optional[Backend] = None):
if not self._absolute:
freq += center_freq

freq = np.round(freq, decimals=3)

assigned_circ = circuit.assign_parameters({freq_param: freq}, inplace=False)
assigned_circ.metadata = {
"experiment_type": self._type,
"qubit": self.physical_qubits[0],
"qubits": (self.physical_qubits[0],),
"xval": freq,
"unit": "Hz",
"amplitude": self.experiment_options.amp,
Expand Down
11 changes: 11 additions & 0 deletions qiskit_experiments/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os
import uuid
from collections import OrderedDict
from datetime import datetime

from qiskit.result import Result
from qiskit.providers import Backend
Expand Down Expand Up @@ -116,6 +117,16 @@ def job_ids(self) -> List[str]:
"""
return list(self._jobs.keys())

@property
def completion_times(self) -> Dict[str, datetime]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once StoredData is merged, this will be the proper location of the completion_times property

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out @yaelbh . Which PR is StoredData?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#113 and its continuation #115

"""Returns the completion times of the jobs."""
job_times = {}
for job_id, job in self._jobs.items():
if job is not None and "COMPLETED" in job.time_per_step():
job_times[job_id] = job.time_per_step().get("COMPLETED")

return job_times

@property
def backend(self) -> Backend:
"""Return backend.
Expand Down
Loading