From 768599e67dd3dd168d7d4939c937e3b08fa93f06 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 6 Apr 2021 12:46:55 +0200 Subject: [PATCH 001/178] * Added initial draft of cal def. --- .../calibration/calibration_definitions.py | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 qiskit_experiments/calibration/calibration_definitions.py diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibration_definitions.py new file mode 100644 index 0000000000..c8e883cd22 --- /dev/null +++ b/qiskit_experiments/calibration/calibration_definitions.py @@ -0,0 +1,384 @@ +# 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 store the results of a calibration experiments.""" + +import copy +from datetime import datetime +import dataclasses +from typing import Tuple, Union, List, Optional, Type +import pandas as pd + +from qiskit.circuit import Gate +from qiskit import QuantumCircuit +from qiskit.pulse import Schedule, DriveChannel, ControlChannel, MeasureChannel +from qiskit.pulse.channels import PulseChannel +from qiskit.circuit import Parameter +from .exceptions import CalibrationError +from .parameter_value import ParameterValue + + +class CalibrationsDefinition: + """ + A class to manage schedules with calibrated parameter values. + Schedules are stored in a dict and are intended to be fully parameterized, + including the index of the channels. The parameters are therefore stored in + the schedules. The names of the parameters must be unique. The calibrated + values of the parameters are stored in a dictionary. + """ + + def __init__(self, backend): + """ + Initialize the instructions from a given backend. + + Args: + backend: The backend from which to get the configuration. + """ + + self._n_qubits = backend.configuration().num_qubits + self._n_uchannels = backend.configuration().n_uchannels + self._properties = backend.properties() + self._config = backend.configuration() + self._params = {'qubit_freq': {}} + self._schedules = {} + + # Populate the qubit frequency estimates + for qubit, freq in enumerate(backend.defaults().qubit_freq_est): + timestamp = backend.properties().qubit_property(qubit)['frequency'][1] + val = ParameterValue(freq, timestamp) + self.add_parameter_value('qubit_freq', val, DriveChannel(qubit)) + + def schedules(self) -> pd.DataFrame: + """ + Return the schedules in self in a data frame to help + users manage their schedules. + + Returns: + data: A pandas data frame with all the schedules in it. + """ + data = [] + for name, schedule in self._schedules.items(): + data.append({'name': name, + 'schedule': schedule, + 'parameters': schedule.parameters}) + + return pd.DataFrame(data) + + def parameters(self, names: Optional[List[str]] = None, + chs: Optional[List[PulseChannel]] = None) -> pd.DataFrame: + """ + Returns the parameters as a pandas data frame. + This function is here to help users manage their parameters. + + Args: + names: The parameter names that should be included in the returned + table. If None is given then all names are included. + chs: The channels that should be included in the returned table. + If None is given then all channels are returned. + + Returns: + data: A data frame of parameter values. + """ + + data = [] + + if names is None: + names = self._params.keys() + + for name in names: + params_name = self._params.get(name, {}) + + if chs is None: + chs = params_name.keys() + + for ch in chs: + for value in params_name.get(ch, {}): + value_dict = dataclasses.asdict(value) + value_dict['channel'] = ch.name + value_dict['parameter'] = name + + data.append(value_dict) + + return pd.DataFrame(data) + + def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): + """ + Add a schedule and register the parameters. + + Args: + schedules: The schedule to add. + + Raises: + CalibrationError: If the parameterized channel index is not formatted + following index1.index2... + """ + if isinstance(schedules, Schedule): + schedules = [schedules] + + for schedule in schedules: + + # check that channels, if parameterized, have the proper name format. + #pylint: disable = raise-missing-from + for ch in schedule.channels: + if isinstance(ch.index, Parameter): + try: + [int(index) for index in ch.index.name.split('.')] + except ValueError: + raise CalibrationError('Parameterized channel must have a name ' + 'formatted following index1.index2...') + + self._schedules[schedule.name] = schedule + + for param in schedule.parameters: + if param.name not in self._params: + self._params[param.name] = {} + + def add_parameter_value(self, param: Union[Parameter, str], + value: ParameterValue, + chs: Optional[Union[PulseChannel, List[PulseChannel]]] = None, + ch_type: Type[PulseChannel] = 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. The parameters are stored and identified by name. + + Args: + param: The parameter or its name for which to add the measured value. + value: The value of the parameter to add. + chs: The channel(s) to which the parameter applies. If None is given + then the type of channels must by specified. + ch_type: This parameter is only used if chs is None. In this case the + value of the parameter will be set for all channels of the + specified type. + + Raises: + CalibrationError: if ch_type is not given when chs are None, if the + channel type is not a ControlChannel, DriveChannel, or MeasureChannel, or + if the parameter name is not already in self. + """ + if isinstance(param, Parameter): + name = param.name + else: + name = param + + if chs is None: + if ch_type is None: + raise CalibrationError('Channel type must be given when chs are None.') + + if issubclass(ch_type, ControlChannel): + chs = [ch_type(_) for _ in range(self._n_uchannels)] + elif issubclass(ch_type, (DriveChannel, MeasureChannel)): + chs = [ch_type(_) for _ in range(self._n_qubits)] + else: + raise CalibrationError('Unrecognised channel type {}.'.format(ch_type)) + + try: + chs = list(chs) + except TypeError: + chs = [chs] + + if name not in self._params: + raise CalibrationError('Cannot add unknown parameter %s.' % name) + + for ch in chs: + if ch not in self._params[name]: + self._params[name][ch] = [value] + else: + self._params[name][ch].append(value) + + def get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: + """ + Get the index of the parameterized channel based on the given qubits + and the name of the parameter in the channel index. The name of this + parameter must be written as qubit_index1.qubit_index2... . For example, + the following parameter names are valid: '1', '1.0', '3.10.0'. + + 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=(int, int) and + the channel is a u channel with parameterized index name 'x.y' + where x and y the method returns the u_channel corresponding to + qubits (qubits[1], qubits[0]). + + Raises: + CalibrationError: if the number of qubits is incorrect, if the + number of inferred ControlChannels is not correct, or if ch is not + a DriveChannel, MeasureChannel, or ControlChannel. + """ + + if isinstance(chan.index, Parameter): + indices = [int(_) for _ in chan.index.name.split('.')] + ch_qubits = tuple(qubits[_] for _ in indices) + + if isinstance(chan, DriveChannel): + if len(ch_qubits) != 1: + raise CalibrationError('Too many qubits for drive channel: ' + 'got %i expecting 1.' % len(ch_qubits)) + + ch_ = self._config.drive(ch_qubits[0]) + + elif isinstance(chan, MeasureChannel): + if len(ch_qubits) != 1: + raise CalibrationError('Too many qubits for drive channel: ' + 'got %i expecting 1.' % len(ch_qubits)) + + ch_ = self._config.measure(ch_qubits[0]) + + elif isinstance(chan, ControlChannel): + chs_ = self._config.control(ch_qubits) + + if len(chs_) != 1: + raise CalibrationError('Ambiguous number of control channels for ' + 'qubits {} and {}.'.format(qubits, chan.name)) + + ch_ = chs_[0] + + else: + chs = tuple(_.__name__ for _ in [DriveChannel, ControlChannel, MeasureChannel]) + raise CalibrationError('Channel must be of type {}.'.format(chs)) + + return ch_.index + else: + return chan.index + + def parameter_value(self, name: str, chan: PulseChannel, valid_only: bool = True, + group: str = 'default', + cutoff_date: datetime = None) -> Union[int, float, complex]: + """ + Retrieve the value of a calibrated parameter from those stored. + + Args: + name: The name of the parameter to get. + chan: The channel for which we want the value of the parameter. + valid_only: Use only parameters marked as valid. + group: The calibration group from which to draw the + parameters. If not specifies 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. + + Returns: + value: The value of the parameter. + + Raises: + CalibrationError: if there is no parameter value for the given parameter name + and pulse channel. + """ + #pylint: disable = raise-missing-from + try: + if valid_only: + candidates = [p for p in self._params[name][chan] if p.valid] + else: + candidates = self._params[name][chan] + + candidates = [_ for _ in candidates if _.group == group] + + if cutoff_date: + candidates = [_ for _ in candidates if _ <= cutoff_date] + + candidates.sort(key=lambda x: x.date_time) + + return candidates[-1].value + except KeyError: + raise CalibrationError('No parameter value for %s and channel %s' % (name, chan.name)) + + def get_schedule(self, name: str, qubits: Tuple[int, ...], + free_params: List[str] = None, group: Optional[str] = 'default') -> Schedule: + """ + Args: + name: The name of the schedule to get. + qubits: The qubits for which to get the schedule. + free_params: The parameters that should remain unassigned. + group: The calibration group from which to draw the + parameters. If not specifies this defaults to the 'default' group. + + Returns: + schedule: A deep copy of the template schedule with all parameters assigned + except for those specified by free_params. + + Raises: + CalibrationError: if the name of the schedule is not known. + """ + + # Get the schedule and deepcopy it to prevent binding from removing + # the parametric nature of the schedule. + if name not in self._schedules: + raise CalibrationError('Schedule %s is not defined.' % name) + + sched = copy.deepcopy(self._schedules[name]) + + # Retrieve the channel indices based on the qubits and bind them. + binding_dict = {} + for ch in sched.channels: + if ch.is_parameterized(): + index = self.get_channel_index(qubits, ch) + binding_dict[ch.index] = index + + sched.assign_parameters(binding_dict) + + # Loop through the remaining parameters in the schedule, get their values and bind. + if free_params is None: + free_params = [] + + binding_dict = {} + for inst in sched.instructions: + ch = inst[1].channel + for param in inst[1].parameters: + if param.name not in free_params: + binding_dict[param] = self.parameter_value(param.name, ch, group=group) + + sched.assign_parameters(binding_dict) + + return sched + + def get_circuit(self, name: str, qubits: Tuple, free_params: List[str] = None, + group: Optional[str] = 'default', schedule: Schedule = None) -> QuantumCircuit: + """ + Args: + name: The name of the gate to retrieve. + qubits: The qubits for which to generate the Gate. + free_params: Names of the parameters that will remain unassigned. + group: The calibration group from which to retrieve the calibrated values. + If unspecified this default to 'default'. + schedule: The schedule to add to the gate if the internally stored one is + not going to be used. + + Returns: + A quantum circuit in which the parameter values have been assigned aside from + those explicitly specified in free_params. + """ + if schedule is None: + schedule = self.get_schedule(name, qubits, free_params, group) + + gate = Gate(name=name, num_qubits=len(qubits), params=list(schedule.parameters)) + circ = QuantumCircuit(len(qubits), len(qubits)) + circ.append(gate, list(range(len(qubits)))) + circ.add_calibration(gate, qubits, schedule, params=schedule.parameters) + + return circ + + def to_db(self): + """ + Serializes the parameterized schedules and parameter values so + that they can be sent and stored in an external DB. + """ + raise NotImplementedError + + def from_db(self): + """ + Retrieves the parameterized schedules and pulse parameters from an + external DB. + """ + raise NotImplementedError From 75214af0aceec4dc88df7e720649d4ba51415a52 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 11:25:31 +0200 Subject: [PATCH 002/178] * Added exception and PArameterValue. * refactor CalibrationsDefinition to allow non-unique parameter names in different schedules. --- .../calibration/calibration_definitions.py | 233 ++++++++++-------- qiskit_experiments/calibration/exceptions.py | 28 +++ .../calibration/parameter_value.py | 37 +++ 3 files changed, 197 insertions(+), 101 deletions(-) create mode 100644 qiskit_experiments/calibration/exceptions.py create mode 100644 qiskit_experiments/calibration/parameter_value.py diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibration_definitions.py index c8e883cd22..0ba30284fe 100644 --- a/qiskit_experiments/calibration/calibration_definitions.py +++ b/qiskit_experiments/calibration/calibration_definitions.py @@ -15,8 +15,9 @@ import copy from datetime import datetime import dataclasses -from typing import Tuple, Union, List, Optional, Type +from typing import Tuple, Union, List, Optional import pandas as pd +from collections import namedtuple from qiskit.circuit import Gate from qiskit import QuantumCircuit @@ -26,6 +27,8 @@ from .exceptions import CalibrationError from .parameter_value import ParameterValue +ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter"]) + class CalibrationsDefinition: """ @@ -48,14 +51,62 @@ def __init__(self, backend): self._n_uchannels = backend.configuration().n_uchannels self._properties = backend.properties() self._config = backend.configuration() - self._params = {'qubit_freq': {}} + self._params = {} + + # Required because copying a template schedule creates new paramters with new IDs. + self._parameter_map = {} self._schedules = {} - # Populate the qubit frequency estimates - for qubit, freq in enumerate(backend.defaults().qubit_freq_est): - timestamp = backend.properties().qubit_property(qubit)['frequency'][1] - val = ParameterValue(freq, timestamp) - self.add_parameter_value('qubit_freq', val, DriveChannel(qubit)) + def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): + """ + Add a schedule and register the parameters. + + Args: + schedules: The schedule to add. + + Raises: + CalibrationError: If the parameterized channel index is not formatted + following index1.index2... + """ + if isinstance(schedules, Schedule): + schedules = [schedules] + + for schedule in schedules: + + # check that channels, if parameterized, have the proper name format. + #pylint: disable = raise-missing-from + for ch in schedule.channels: + if isinstance(ch.index, Parameter): + try: + [int(index) for index in ch.index.name.split('.')] + except ValueError: + raise CalibrationError('Parameterized channel must have a name ' + 'formatted following index1.index2...') + + self._schedules[schedule.name] = schedule + + for param in schedule.parameters: + self._parameter_map[ParameterKey(schedule.name, param.name)] = hash(param) + if hash(param) not in self._params: + self._params[hash(param)] = {} + + @property + def parameters(self): + """ + Returns a dictionary of parameters managed by the calibrations definition. The value + of the dict is the schedule in which the parameter appears. Parameters that are not + attached to a schedule will have None as a key. + """ + parameters = {} + for key in self._parameter_map.keys(): + schedule_name = key[0] + parameter_name = key[1] + if parameter_name not in parameters: + parameters[parameter_name] = [schedule_name] + else: + parameters[parameter_name].append(schedule_name) + + return parameters def schedules(self) -> pd.DataFrame: """ @@ -73,16 +124,18 @@ def schedules(self) -> pd.DataFrame: return pd.DataFrame(data) - def parameters(self, names: Optional[List[str]] = None, - chs: Optional[List[PulseChannel]] = None) -> pd.DataFrame: + def parameters_table(self, parameters: List[str] = None, + schedules: Union[Schedule, str] = None, + qubit_list: Optional[Tuple[int, ...]] = None) -> pd.DataFrame: """ Returns the parameters as a pandas data frame. This function is here to help users manage their parameters. Args: - names: The parameter names that should be included in the returned + parameters: The parameter names that should be included in the returned table. If None is given then all names are included. - chs: The channels that should be included in the returned table. + schedules: + qubit_list: The qubits that should be included in the returned table. If None is given then all channels are returned. Returns: @@ -91,111 +144,80 @@ def parameters(self, names: Optional[List[str]] = None, data = [] - if names is None: - names = self._params.keys() + # Convert inputs to lists of strings + if parameters is not None: + parameters = [prm.name if isinstance(prm, Parameter) else prm for prm in parameters] + parameters = set(parameters) + + if schedules is not None: + schedules = set([sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules]) - for name in names: - params_name = self._params.get(name, {}) + hash_keys = [] + for key, hash_ in self._parameter_map.items(): + if parameters and key.parameter in parameters: + hash_keys.append((hash_, key)) + if schedules and key.schedule in schedules: + hash_keys.append((hash_, key)) + if parameters is None and schedules is None: + hash_keys.append((hash_, key)) - if chs is None: - chs = params_name.keys() + for hash_key in hash_keys: + param_vals = self._params[hash_key[0]] - for ch in chs: - for value in params_name.get(ch, {}): + for qubits, values in param_vals.items(): + if qubit_list and qubits not in qubit_list: + continue + + for value in values: value_dict = dataclasses.asdict(value) - value_dict['channel'] = ch.name - value_dict['parameter'] = name + value_dict['qubits'] = qubits + value_dict['parameter'] = hash_key[1].parameter + value_dict['schedule'] = hash_key[1].schedule data.append(value_dict) return pd.DataFrame(data) - def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): - """ - Add a schedule and register the parameters. - - Args: - schedules: The schedule to add. - - Raises: - CalibrationError: If the parameterized channel index is not formatted - following index1.index2... - """ - if isinstance(schedules, Schedule): - schedules = [schedules] - - for schedule in schedules: - - # check that channels, if parameterized, have the proper name format. - #pylint: disable = raise-missing-from - for ch in schedule.channels: - if isinstance(ch.index, Parameter): - try: - [int(index) for index in ch.index.name.split('.')] - except ValueError: - raise CalibrationError('Parameterized channel must have a name ' - 'formatted following index1.index2...') - - self._schedules[schedule.name] = schedule - - for param in schedule.parameters: - if param.name not in self._params: - self._params[param.name] = {} - - def add_parameter_value(self, param: Union[Parameter, str], + def add_parameter_value(self, value: ParameterValue, - chs: Optional[Union[PulseChannel, List[PulseChannel]]] = None, - ch_type: Type[PulseChannel] = None): + param: Union[Parameter, str], + qubits: Tuple[int, ...], + schedule: Union[Schedule, 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. The parameters are stored and identified by name. Args: - param: The parameter or its name for which to add the measured value. value: The value of the parameter to add. - chs: The channel(s) to which the parameter applies. If None is given - then the type of channels must by specified. - ch_type: This parameter is only used if chs is None. In this case the - value of the parameter will be set for all channels of the - specified type. + 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 ch_type is not given when chs are None, if the channel type is not a ControlChannel, DriveChannel, or MeasureChannel, or if the parameter name is not already in self. """ - if isinstance(param, Parameter): - name = param.name - else: - name = param - if chs is None: - if ch_type is None: - raise CalibrationError('Channel type must be given when chs are None.') + param_name = param.name if isinstance(param, Parameter) else param + sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - if issubclass(ch_type, ControlChannel): - chs = [ch_type(_) for _ in range(self._n_uchannels)] - elif issubclass(ch_type, (DriveChannel, MeasureChannel)): - chs = [ch_type(_) for _ in range(self._n_qubits)] + if (sched_name, param_name) not in self._parameter_map: + if sched_name is not None: + raise CalibrationError(f'Unknown parameter {param_name}.') else: - raise CalibrationError('Unrecognised channel type {}.'.format(ch_type)) + raise CalibrationError(f'Unknown parameter {param_name} in schedule {sched_name}.') - try: - chs = list(chs) - except TypeError: - chs = [chs] + param_hash = self._parameter_map[(sched_name, param_name)] - if name not in self._params: - raise CalibrationError('Cannot add unknown parameter %s.' % name) - - for ch in chs: - if ch not in self._params[name]: - self._params[name][ch] = [value] - else: - self._params[name][ch].append(value) + if qubits not in self._params[param_hash]: + self._params[param_hash][qubits] = [value] + else: + self._params[param_hash][qubits].append(value) - def get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: + def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ Get the index of the parameterized channel based on the given qubits and the name of the parameter in the channel index. The name of this @@ -253,15 +275,20 @@ def get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: else: return chan.index - def parameter_value(self, name: str, chan: PulseChannel, valid_only: bool = True, + def parameter_value(self, + param: Union[Parameter, str], + qubits: Tuple[int, ...], + schedule: Union[Schedule, str] = None, + valid_only: bool = True, group: str = 'default', cutoff_date: datetime = None) -> Union[int, float, complex]: """ Retrieve the value of a calibrated parameter from those stored. Args: - name: The name of the parameter to get. - chan: The channel for which we want the value of the parameter. + 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 the name of the schedule 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 specifies this defaults to the 'default' group. @@ -276,14 +303,18 @@ def parameter_value(self, name: str, chan: PulseChannel, valid_only: bool = True CalibrationError: if there is no parameter value for the given parameter name and pulse channel. """ - #pylint: disable = raise-missing-from + param_name = param.name if isinstance(param, Parameter) else param + sched_name = schedule.name if isinstance(schedule, Schedule) else schedule + try: + hash_ = self._parameter_map[(sched_name, param_name)] + if valid_only: - candidates = [p for p in self._params[name][chan] if p.valid] + candidates = [p for p in self._params[hash_][qubits] if p.valid] else: - candidates = self._params[name][chan] + candidates = self._params[hash_][qubits] - candidates = [_ for _ in candidates if _.group == group] + candidates = [candidate for candidate in candidates if candidate.group == group] if cutoff_date: candidates = [_ for _ in candidates if _ <= cutoff_date] @@ -291,8 +322,9 @@ def parameter_value(self, name: str, chan: PulseChannel, valid_only: bool = True candidates.sort(key=lambda x: x.date_time) return candidates[-1].value + except KeyError: - raise CalibrationError('No parameter value for %s and channel %s' % (name, chan.name)) + raise CalibrationError(f'No parameter value for {param_name} and qubits {qubits}.') def get_schedule(self, name: str, qubits: Tuple[int, ...], free_params: List[str] = None, group: Optional[str] = 'default') -> Schedule: @@ -323,8 +355,7 @@ def get_schedule(self, name: str, qubits: Tuple[int, ...], binding_dict = {} for ch in sched.channels: if ch.is_parameterized(): - index = self.get_channel_index(qubits, ch) - binding_dict[ch.index] = index + binding_dict[ch.index] = self._get_channel_index(qubits, ch) sched.assign_parameters(binding_dict) @@ -334,10 +365,10 @@ def get_schedule(self, name: str, qubits: Tuple[int, ...], binding_dict = {} for inst in sched.instructions: - ch = inst[1].channel - for param in inst[1].parameters: - if param.name not in free_params: - binding_dict[param] = self.parameter_value(param.name, ch, group=group) + for param in inst[1].operands[0].parameters.values(): + if isinstance(param, Parameter): + if param.name not in free_params: + binding_dict[param] = self.parameter_value(param.name, qubits, name, group=group) sched.assign_parameters(binding_dict) diff --git a/qiskit_experiments/calibration/exceptions.py b/qiskit_experiments/calibration/exceptions.py new file mode 100644 index 0000000000..4d9f45bf3c --- /dev/null +++ b/qiskit_experiments/calibration/exceptions.py @@ -0,0 +1,28 @@ +# 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. + +"""Exceptions for calibration.""" + +from qiskit.exceptions import QiskitError + + +class CalibrationError(QiskitError): + """Errors raised by the calibration module.""" + + def __init__(self, *message): + """Set the error message.""" + super().__init__(*message) + self.message = ' '.join(message) + + def __str__(self): + """Return the message.""" + return repr(self.message) diff --git a/qiskit_experiments/calibration/parameter_value.py b/qiskit_experiments/calibration/parameter_value.py new file mode 100644 index 0000000000..0f257f0325 --- /dev/null +++ b/qiskit_experiments/calibration/parameter_value.py @@ -0,0 +1,37 @@ +# 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. + +"""Data class for parameter values.""" + +from dataclasses import dataclass +from datetime import datetime +from typing import Union + + +@dataclass +class ParameterValue: + """A data class to store parameter values.""" + + # Value assumed by the parameter + value: Union[int, float] = None + + # Data time when the value of the parameter was generated + date_time: datetime = datetime.fromtimestamp(0) + + # A bool indicating if the parameter is valid + valid: bool = True + + # The experiment from which the value of this parameter was generated. + exp_id: str = None + + # The group of calibrations to which this parameter belongs + group: str = 'default' From 40bf8bdf68c98d4076538d598c973f83f821a257 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 11:29:49 +0200 Subject: [PATCH 003/178] * Black and fstrings. --- .../calibration/calibration_definitions.py | 111 +++++++++++------- qiskit_experiments/calibration/exceptions.py | 2 +- .../calibration/parameter_value.py | 2 +- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibration_definitions.py index 0ba30284fe..3b3d83ea96 100644 --- a/qiskit_experiments/calibration/calibration_definitions.py +++ b/qiskit_experiments/calibration/calibration_definitions.py @@ -74,14 +74,16 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): for schedule in schedules: # check that channels, if parameterized, have the proper name format. - #pylint: disable = raise-missing-from + # pylint: disable = raise-missing-from for ch in schedule.channels: if isinstance(ch.index, Parameter): try: - [int(index) for index in ch.index.name.split('.')] + [int(index) for index in ch.index.name.split(".")] except ValueError: - raise CalibrationError('Parameterized channel must have a name ' - 'formatted following index1.index2...') + raise CalibrationError( + "Parameterized channel must have a name " + "formatted following index1.index2..." + ) self._schedules[schedule.name] = schedule @@ -118,15 +120,16 @@ def schedules(self) -> pd.DataFrame: """ data = [] for name, schedule in self._schedules.items(): - data.append({'name': name, - 'schedule': schedule, - 'parameters': schedule.parameters}) + data.append({"name": name, "schedule": schedule, "parameters": schedule.parameters}) return pd.DataFrame(data) - def parameters_table(self, parameters: List[str] = None, - schedules: Union[Schedule, str] = None, - qubit_list: Optional[Tuple[int, ...]] = None) -> pd.DataFrame: + def parameters_table( + self, + parameters: List[str] = None, + schedules: Union[Schedule, str] = None, + qubit_list: Optional[Tuple[int, ...]] = None, + ) -> pd.DataFrame: """ Returns the parameters as a pandas data frame. This function is here to help users manage their parameters. @@ -170,20 +173,21 @@ def parameters_table(self, parameters: List[str] = None, for value in values: value_dict = dataclasses.asdict(value) - value_dict['qubits'] = qubits - value_dict['parameter'] = hash_key[1].parameter - value_dict['schedule'] = hash_key[1].schedule + value_dict["qubits"] = qubits + value_dict["parameter"] = hash_key[1].parameter + value_dict["schedule"] = hash_key[1].schedule data.append(value_dict) return pd.DataFrame(data) - def add_parameter_value(self, - value: ParameterValue, - param: Union[Parameter, str], - qubits: Tuple[int, ...], - schedule: Union[Schedule, str] = None, - ): + def add_parameter_value( + self, + value: ParameterValue, + param: Union[Parameter, str], + qubits: Tuple[int, ...], + schedule: Union[Schedule, 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 @@ -206,9 +210,9 @@ def add_parameter_value(self, if (sched_name, param_name) not in self._parameter_map: if sched_name is not None: - raise CalibrationError(f'Unknown parameter {param_name}.') + raise CalibrationError(f"Unknown parameter {param_name}.") else: - raise CalibrationError(f'Unknown parameter {param_name} in schedule {sched_name}.') + raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") param_hash = self._parameter_map[(sched_name, param_name)] @@ -241,20 +245,22 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ if isinstance(chan.index, Parameter): - indices = [int(_) for _ in chan.index.name.split('.')] + indices = [int(_) for _ in chan.index.name.split(".")] ch_qubits = tuple(qubits[_] for _ in indices) if isinstance(chan, DriveChannel): if len(ch_qubits) != 1: - raise CalibrationError('Too many qubits for drive channel: ' - 'got %i expecting 1.' % len(ch_qubits)) + raise CalibrationError( + f"Too many qubits for drive channel: got {len(ch_qubits)} expecting 1." + ) ch_ = self._config.drive(ch_qubits[0]) elif isinstance(chan, MeasureChannel): if len(ch_qubits) != 1: - raise CalibrationError('Too many qubits for drive channel: ' - 'got %i expecting 1.' % len(ch_qubits)) + raise CalibrationError( + f"Too many qubits for measure channel: got {len(ch_qubits)} expecting 1." + ) ch_ = self._config.measure(ch_qubits[0]) @@ -262,26 +268,30 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: chs_ = self._config.control(ch_qubits) if len(chs_) != 1: - raise CalibrationError('Ambiguous number of control channels for ' - 'qubits {} and {}.'.format(qubits, chan.name)) + raise CalibrationError( + "Ambiguous number of control channels for " + f"qubits {qubits} and {chan.name}." + ) ch_ = chs_[0] else: chs = tuple(_.__name__ for _ in [DriveChannel, ControlChannel, MeasureChannel]) - raise CalibrationError('Channel must be of type {}.'.format(chs)) + raise CalibrationError(f"Channel must be of type {chs}.") return ch_.index else: return chan.index - def parameter_value(self, - param: Union[Parameter, str], - qubits: Tuple[int, ...], - schedule: Union[Schedule, str] = None, - valid_only: bool = True, - group: str = 'default', - cutoff_date: datetime = None) -> Union[int, float, complex]: + def parameter_value( + self, + param: Union[Parameter, str], + qubits: Tuple[int, ...], + schedule: Union[Schedule, str] = None, + valid_only: bool = True, + group: str = "default", + cutoff_date: datetime = None, + ) -> Union[int, float, complex]: """ Retrieve the value of a calibrated parameter from those stored. @@ -324,10 +334,15 @@ def parameter_value(self, return candidates[-1].value except KeyError: - raise CalibrationError(f'No parameter value for {param_name} and qubits {qubits}.') - - def get_schedule(self, name: str, qubits: Tuple[int, ...], - free_params: List[str] = None, group: Optional[str] = 'default') -> Schedule: + raise CalibrationError(f"No parameter value for {param_name} and qubits {qubits}.") + + def get_schedule( + self, + name: str, + qubits: Tuple[int, ...], + free_params: List[str] = None, + group: Optional[str] = "default", + ) -> Schedule: """ Args: name: The name of the schedule to get. @@ -347,7 +362,7 @@ def get_schedule(self, name: str, qubits: Tuple[int, ...], # Get the schedule and deepcopy it to prevent binding from removing # the parametric nature of the schedule. if name not in self._schedules: - raise CalibrationError('Schedule %s is not defined.' % name) + raise CalibrationError("Schedule %s is not defined." % name) sched = copy.deepcopy(self._schedules[name]) @@ -368,14 +383,22 @@ def get_schedule(self, name: str, qubits: Tuple[int, ...], for param in inst[1].operands[0].parameters.values(): if isinstance(param, Parameter): if param.name not in free_params: - binding_dict[param] = self.parameter_value(param.name, qubits, name, group=group) + binding_dict[param] = self.parameter_value( + param.name, qubits, name, group=group + ) sched.assign_parameters(binding_dict) return sched - def get_circuit(self, name: str, qubits: Tuple, free_params: List[str] = None, - group: Optional[str] = 'default', schedule: Schedule = None) -> QuantumCircuit: + def get_circuit( + self, + name: str, + qubits: Tuple, + free_params: List[str] = None, + group: Optional[str] = "default", + schedule: Schedule = None, + ) -> QuantumCircuit: """ Args: name: The name of the gate to retrieve. diff --git a/qiskit_experiments/calibration/exceptions.py b/qiskit_experiments/calibration/exceptions.py index 4d9f45bf3c..26a27ac4b9 100644 --- a/qiskit_experiments/calibration/exceptions.py +++ b/qiskit_experiments/calibration/exceptions.py @@ -21,7 +21,7 @@ class CalibrationError(QiskitError): def __init__(self, *message): """Set the error message.""" super().__init__(*message) - self.message = ' '.join(message) + self.message = " ".join(message) def __str__(self): """Return the message.""" diff --git a/qiskit_experiments/calibration/parameter_value.py b/qiskit_experiments/calibration/parameter_value.py index 0f257f0325..5e732066bf 100644 --- a/qiskit_experiments/calibration/parameter_value.py +++ b/qiskit_experiments/calibration/parameter_value.py @@ -34,4 +34,4 @@ class ParameterValue: exp_id: str = None # The group of calibrations to which this parameter belongs - group: str = 'default' + group: str = "default" From 2639b0d755b7f4f4bb42c25871bbb9fbe39b4160 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 13:16:58 +0200 Subject: [PATCH 004/178] * Removed unnecessary hashing. --- .../calibration/calibration_definitions.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibration_definitions.py index 3b3d83ea96..c30c6b0d29 100644 --- a/qiskit_experiments/calibration/calibration_definitions.py +++ b/qiskit_experiments/calibration/calibration_definitions.py @@ -88,9 +88,9 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): self._schedules[schedule.name] = schedule for param in schedule.parameters: - self._parameter_map[ParameterKey(schedule.name, param.name)] = hash(param) - if hash(param) not in self._params: - self._params[hash(param)] = {} + self._parameter_map[ParameterKey(schedule.name, param.name)] = param + if param not in self._params: + self._params[param] = {} @property def parameters(self): @@ -100,13 +100,13 @@ def parameters(self): attached to a schedule will have None as a key. """ parameters = {} - for key in self._parameter_map.keys(): - schedule_name = key[0] - parameter_name = key[1] - if parameter_name not in parameters: - parameters[parameter_name] = [schedule_name] + for key, param in self._parameter_map.items(): + schedule_name = key.schedule + + if param not in parameters: + parameters[param] = [schedule_name] else: - parameters[parameter_name].append(schedule_name) + parameters[param].append(schedule_name) return parameters @@ -137,7 +137,7 @@ def parameters_table( Args: parameters: The parameter names that should be included in the returned table. If None is given then all names are included. - schedules: + schedules: The schedules to which to restrict the output. qubit_list: The qubits that should be included in the returned table. If None is given then all channels are returned. @@ -155,17 +155,17 @@ def parameters_table( if schedules is not None: schedules = set([sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules]) - hash_keys = [] - for key, hash_ in self._parameter_map.items(): + keys = [] + for key, param in self._parameter_map.items(): if parameters and key.parameter in parameters: - hash_keys.append((hash_, key)) + keys.append((param, key)) if schedules and key.schedule in schedules: - hash_keys.append((hash_, key)) + keys.append((param, key)) if parameters is None and schedules is None: - hash_keys.append((hash_, key)) + keys.append((param, key)) - for hash_key in hash_keys: - param_vals = self._params[hash_key[0]] + for key in keys: + param_vals = self._params[key[0]] for qubits, values in param_vals.items(): if qubit_list and qubits not in qubit_list: @@ -174,8 +174,8 @@ def parameters_table( for value in values: value_dict = dataclasses.asdict(value) value_dict["qubits"] = qubits - value_dict["parameter"] = hash_key[1].parameter - value_dict["schedule"] = hash_key[1].schedule + value_dict["parameter"] = key[1].parameter + value_dict["schedule"] = key[1].schedule data.append(value_dict) @@ -214,12 +214,12 @@ def add_parameter_value( else: raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") - param_hash = self._parameter_map[(sched_name, param_name)] + param = self._parameter_map[(sched_name, param_name)] - if qubits not in self._params[param_hash]: - self._params[param_hash][qubits] = [value] + if qubits not in self._params[param]: + self._params[param][qubits] = [value] else: - self._params[param_hash][qubits].append(value) + self._params[param][qubits].append(value) def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ @@ -317,12 +317,12 @@ def parameter_value( sched_name = schedule.name if isinstance(schedule, Schedule) else schedule try: - hash_ = self._parameter_map[(sched_name, param_name)] + param = self._parameter_map[(sched_name, param_name)] if valid_only: - candidates = [p for p in self._params[hash_][qubits] if p.valid] + candidates = [p for p in self._params[param][qubits] if p.valid] else: - candidates = self._params[hash_][qubits] + candidates = self._params[param][qubits] candidates = [candidate for candidate in candidates if candidate.group == group] From bd0529ac534779faf6688d7948abbaeb8f33db02 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 13:26:34 +0200 Subject: [PATCH 005/178] * Added init. * Fixed lint. --- qiskit_experiments/calibration/__init__.py | 17 +++++++ .../calibration/calibration_definitions.py | 45 ++++++++++--------- 2 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 qiskit_experiments/calibration/__init__.py diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py new file mode 100644 index 0000000000..454cabb285 --- /dev/null +++ b/qiskit_experiments/calibration/__init__.py @@ -0,0 +1,17 @@ +# 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. + +"""Qiskit Experiments Calibration Root.""" + +from .calibration_definitions import CalibrationsDefinition +from .exceptions import CalibrationError +from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibration_definitions.py index c30c6b0d29..d7cd18c7db 100644 --- a/qiskit_experiments/calibration/calibration_definitions.py +++ b/qiskit_experiments/calibration/calibration_definitions.py @@ -13,19 +13,19 @@ """Class to store the results of a calibration experiments.""" import copy -from datetime import datetime import dataclasses +from collections import namedtuple +from datetime import datetime from typing import Tuple, Union, List, Optional import pandas as pd -from collections import namedtuple from qiskit.circuit import Gate from qiskit import QuantumCircuit from qiskit.pulse import Schedule, DriveChannel, ControlChannel, MeasureChannel from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter -from .exceptions import CalibrationError -from .parameter_value import ParameterValue +from qiskit_experiments.calibration.exceptions import CalibrationError +from qiskit_experiments.calibration.parameter_value import ParameterValue ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter"]) @@ -149,11 +149,10 @@ def parameters_table( # Convert inputs to lists of strings if parameters is not None: - parameters = [prm.name if isinstance(prm, Parameter) else prm for prm in parameters] - parameters = set(parameters) + parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} if schedules is not None: - schedules = set([sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules]) + schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} keys = [] for key, param in self._parameter_map.items(): @@ -211,8 +210,8 @@ def add_parameter_value( if (sched_name, param_name) not in self._parameter_map: if sched_name is not None: raise CalibrationError(f"Unknown parameter {param_name}.") - else: - raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") + + raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") param = self._parameter_map[(sched_name, param_name)] @@ -316,25 +315,27 @@ def parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - try: - param = self._parameter_map[(sched_name, param_name)] + if (sched_name, param_name) not in self._parameter_map: + raise CalibrationError(f"No parameter for {param_name} and schedule {sched_name}.") - if valid_only: - candidates = [p for p in self._params[param][qubits] if p.valid] - else: - candidates = self._params[param][qubits] + param = self._parameter_map[(sched_name, param_name)] - candidates = [candidate for candidate in candidates if candidate.group == group] + if qubits not in self._params[param]: + raise CalibrationError(f"No parameter value for {param} and qubits {qubits}.") + + if valid_only: + candidates = [p for p in self._params[param][qubits] if p.valid] + else: + candidates = self._params[param][qubits] - if cutoff_date: - candidates = [_ for _ in candidates if _ <= cutoff_date] + candidates = [candidate for candidate in candidates if candidate.group == group] - candidates.sort(key=lambda x: x.date_time) + if cutoff_date: + candidates = [_ for _ in candidates if _ <= cutoff_date] - return candidates[-1].value + candidates.sort(key=lambda x: x.date_time) - except KeyError: - raise CalibrationError(f"No parameter value for {param_name} and qubits {qubits}.") + return candidates[-1].value def get_schedule( self, From c992ccc6931adc514396802c1b0d338380f1b1e2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 14:38:13 +0200 Subject: [PATCH 006/178] * Renamed CalibrationsDefinition to Calibrations. * Added pandas to requireements. --- qiskit_experiments/calibration/__init__.py | 2 +- .../calibration/{calibration_definitions.py => calibrations.py} | 2 +- requirements.txt | 1 + test/calibration/test_calibrations.py | 0 4 files changed, 3 insertions(+), 2 deletions(-) rename qiskit_experiments/calibration/{calibration_definitions.py => calibrations.py} (99%) create mode 100644 test/calibration/test_calibrations.py diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 454cabb285..d8c12e8864 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -12,6 +12,6 @@ """Qiskit Experiments Calibration Root.""" -from .calibration_definitions import CalibrationsDefinition +from .calibrations import Calibrations from .exceptions import CalibrationError from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/calibration_definitions.py b/qiskit_experiments/calibration/calibrations.py similarity index 99% rename from qiskit_experiments/calibration/calibration_definitions.py rename to qiskit_experiments/calibration/calibrations.py index d7cd18c7db..e8bf90de6c 100644 --- a/qiskit_experiments/calibration/calibration_definitions.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -30,7 +30,7 @@ ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter"]) -class CalibrationsDefinition: +class Calibrations: """ A class to manage schedules with calibrated parameter values. Schedules are stored in a dict and are intended to be fully parameterized, diff --git a/requirements.txt b/requirements.txt index fb0a5a9e9a..21ef5177bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ numpy>=1.17 qiskit-terra>=0.16.0 +pandas>=1.0.0 diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py new file mode 100644 index 0000000000..e69de29bb2 From 9092094af96d7425b89db31c34133723f8604fae Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 7 Apr 2021 17:19:16 +0200 Subject: [PATCH 007/178] * Fixed issue with parameters in the Schedule. * Added tests. --- .../calibration/calibrations.py | 18 ++- test/calibration/test_calibrations.py | 104 ++++++++++++++++++ 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index e8bf90de6c..d017e2deeb 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -16,7 +16,7 @@ import dataclasses from collections import namedtuple from datetime import datetime -from typing import Tuple, Union, List, Optional +from typing import Dict, Set, Tuple, Union, List, Optional import pandas as pd from qiskit.circuit import Gate @@ -93,7 +93,7 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): self._params[param] = {} @property - def parameters(self): + def parameters(self) -> Dict[Parameter, Set]: """ Returns a dictionary of parameters managed by the calibrations definition. The value of the dict is the schedule in which the parameter appears. Parameters that are not @@ -104,9 +104,9 @@ def parameters(self): schedule_name = key.schedule if param not in parameters: - parameters[param] = [schedule_name] + parameters[param] = {schedule_name} else: - parameters[param].append(schedule_name) + parameters[param].add(schedule_name) return parameters @@ -380,13 +380,9 @@ def get_schedule( free_params = [] binding_dict = {} - for inst in sched.instructions: - for param in inst[1].operands[0].parameters.values(): - if isinstance(param, Parameter): - if param.name not in free_params: - binding_dict[param] = self.parameter_value( - param.name, qubits, name, group=group - ) + for param in sched.parameters: + if param.name not in free_params: + binding_dict[param] = self.parameter_value(param.name, qubits, name, group=group) sched.assign_parameters(binding_dict) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index e69de29bb2..30003984af 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -0,0 +1,104 @@ +# 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.""" + +from datetime import datetime +from qiskit.circuit import Parameter +from qiskit.pulse import Drag, DriveChannel +from qiskit.test.mock import FakeAlmaden +import qiskit.pulse as pulse +from qiskit.test import QiskitTestCase +from qiskit_experiments.calibration.calibrations import Calibrations +from qiskit_experiments.calibration.parameter_value import ParameterValue + + +class TestCalibrationsBasic(QiskitTestCase): + """Class to test the management of schedules and parameters for calibrations.""" + + def setUp(self): + """Setup a test environment.""" + backend = FakeAlmaden() + self.cals = Calibrations(backend) + + self.sigma = Parameter("σ") + self.amp_xp = Parameter("amp") + self.amp_x90p = Parameter("amp") + self.amp_y90p = Parameter("amp") + self.beta = Parameter("β") + drive = DriveChannel(Parameter("0")) + + # Define and add template schedules. + with pulse.build(name="xp") as xp: + pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), drive) + + with pulse.build(name="xm") as xm: + pulse.play(Drag(160, -self.amp_xp, self.sigma, self.beta), drive) + + with pulse.build(name="x90p") as x90p: + pulse.play(Drag(160, self.amp_x90p, self.sigma, self.beta), drive) + + with pulse.build(name="y90p") as y90p: + pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), drive) + + self.cals.add_schedules([xp, x90p, y90p, xm]) + + # Add some parameter values. + now = datetime.now + self.cals.add_parameter_value(ParameterValue(40, now()), "σ", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.2, now()), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.1, now()), "amp", (3,), "x90p") + self.cals.add_parameter_value(ParameterValue(0.08, now()), "amp", (3,), "y90p") + self.cals.add_parameter_value(ParameterValue(40, now()), "β", (3,), "xp") + + def test_setup(self): + """Test that the initial setup behaves as expected.""" + self.assertEqual(self.cals.parameters[self.amp_xp], {"xp", "xm"}) + self.assertEqual(self.cals.parameters[self.amp_x90p], {"x90p"}) + self.assertEqual(self.cals.parameters[self.amp_y90p], {"y90p"}) + self.assertEqual(self.cals.parameters[self.beta], {"xp", "xm", "x90p", "y90p"}) + self.assertEqual(self.cals.parameters[self.sigma], {"xp", "xm", "x90p", "y90p"}) + + self.assertEqual(self.cals.parameter_value("amp", (3,), "xp"), 0.2) + self.assertEqual(self.cals.parameter_value("amp", (3,), "xm"), 0.2) + self.assertEqual(self.cals.parameter_value("amp", (3,), "x90p"), 0.1) + self.assertEqual(self.cals.parameter_value("amp", (3,), "y90p"), 0.08) + + def test_parameter_dependency(self): + """Check that two schedules that share the same parameter are simultaneously updated.""" + + xp = self.cals.get_schedule("xp", (3,)) + self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) + + xm = self.cals.get_schedule("xm", (3,)) + self.assertEqual(xm.instructions[0][1].operands[0].amp, -0.2) + + self.cals.add_parameter_value(ParameterValue(0.25, datetime.now()), "amp", (3,), "xp") + + xp = self.cals.get_schedule("xp", (3,)) + self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.25) + + xm = self.cals.get_schedule("xm", (3,)) + self.assertEqual(xm.instructions[0][1].operands[0].amp, -0.25) + + def test_get_value(self): + """Test the retrieve of parameter values.""" + + self.assertEqual(self.cals.parameter_value("amp", (3,), "xp"), 0.2) + self.assertEqual(self.cals.parameter_value("amp", (3,), "x90p"), 0.1) + + self.assertEqual(self.cals.parameter_value("σ", (3,), "x90p"), 40) + self.assertEqual(self.cals.parameter_value("σ", (3,), "xp"), 40) + + self.cals.add_parameter_value(ParameterValue(50, datetime.now()), "σ", (3,), "xp") + self.assertEqual(self.cals.parameter_value("σ", (3,), "x90p"), 50) + self.assertEqual(self.cals.parameter_value("σ", (3,), "xp"), 50) From 8c4c21ec2e16ba5528583e894dad9c0526bdb234 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 16:37:32 +0200 Subject: [PATCH 008/178] * Removed pnadas. --- .../calibration/calibrations.py | 151 +++++++++--------- requirements.txt | 1 - test/calibration/test_calibrations.py | 20 +-- 3 files changed, 85 insertions(+), 87 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d017e2deeb..69b5373633 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -16,7 +16,7 @@ import dataclasses from collections import namedtuple from datetime import datetime -from typing import Dict, Set, Tuple, Union, List, Optional +from typing import Any, Dict, Set, Tuple, Union, List, Optional import pandas as pd from qiskit.circuit import Gate @@ -34,9 +34,8 @@ class Calibrations: """ A class to manage schedules with calibrated parameter values. Schedules are stored in a dict and are intended to be fully parameterized, - including the index of the channels. The parameters are therefore stored in - the schedules. The names of the parameters must be unique. The calibrated - values of the parameters are stored in a dictionary. + including the index of the channels. The parameter values are stored in a + dict where parameters are keys. """ def __init__(self, backend): @@ -110,76 +109,6 @@ def parameters(self) -> Dict[Parameter, Set]: return parameters - def schedules(self) -> pd.DataFrame: - """ - Return the schedules in self in a data frame to help - users manage their schedules. - - Returns: - data: A pandas data frame with all the schedules in it. - """ - data = [] - for name, schedule in self._schedules.items(): - data.append({"name": name, "schedule": schedule, "parameters": schedule.parameters}) - - return pd.DataFrame(data) - - def parameters_table( - self, - parameters: List[str] = None, - schedules: Union[Schedule, str] = None, - qubit_list: Optional[Tuple[int, ...]] = None, - ) -> pd.DataFrame: - """ - Returns the parameters as a pandas data frame. - This function is here to help users manage their parameters. - - Args: - parameters: The parameter names that should be included in the returned - table. If None is given then all names are included. - schedules: The schedules to which to restrict the output. - qubit_list: The qubits that should be included in the returned table. - If None is given then all channels are returned. - - Returns: - data: A data frame of parameter values. - """ - - data = [] - - # Convert inputs to lists of strings - if parameters is not None: - parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} - - if schedules is not None: - schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} - - keys = [] - for key, param in self._parameter_map.items(): - if parameters and key.parameter in parameters: - keys.append((param, key)) - if schedules and key.schedule in schedules: - keys.append((param, key)) - if parameters is None and schedules is None: - keys.append((param, key)) - - for key in keys: - param_vals = self._params[key[0]] - - for qubits, values in param_vals.items(): - if qubit_list and qubits not in qubit_list: - continue - - for value in values: - value_dict = dataclasses.asdict(value) - value_dict["qubits"] = qubits - value_dict["parameter"] = key[1].parameter - value_dict["schedule"] = key[1].schedule - - data.append(value_dict) - - return pd.DataFrame(data) - def add_parameter_value( self, value: ParameterValue, @@ -282,7 +211,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: else: return chan.index - def parameter_value( + def get_parameter_value( self, param: Union[Parameter, str], qubits: Tuple[int, ...], @@ -382,7 +311,7 @@ def get_schedule( binding_dict = {} for param in sched.parameters: if param.name not in free_params: - binding_dict[param] = self.parameter_value(param.name, qubits, name, group=group) + binding_dict[param] = self.get_parameter_value(param.name, qubits, name, group=group) sched.assign_parameters(binding_dict) @@ -420,6 +349,76 @@ def get_circuit( return circ + def schedules(self) -> List[Dict[str, Any]]: + """ + Return the schedules in self in a data frame to help + users manage their schedules. + + Returns: + data: A pandas data frame with all the schedules in it. + """ + data = [] + for name, schedule in self._schedules.items(): + data.append({"name": name, "schedule": schedule, "parameters": schedule.parameters}) + + return data + + def parameters_table( + self, + parameters: List[str] = None, + schedules: Union[Schedule, str] = None, + qubit_list: Optional[Tuple[int, ...]] = None, + ) -> List[Dict[str, Any]]: + """ + Returns the parameters as a pandas data frame. + This function is here to help users manage their parameters. + + Args: + parameters: The parameter names that should be included in the returned + table. If None is given then all names are included. + schedules: The schedules to which to restrict the output. + qubit_list: The qubits that should be included in the returned table. + If None is given then all channels are returned. + + Returns: + data: A data frame of parameter values. + """ + + data = [] + + # Convert inputs to lists of strings + if parameters is not None: + parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} + + if schedules is not None: + schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} + + keys = [] + for key, param in self._parameter_map.items(): + if parameters and key.parameter in parameters: + keys.append((param, key)) + if schedules and key.schedule in schedules: + keys.append((param, key)) + if parameters is None and schedules is None: + keys.append((param, key)) + + for key in keys: + param_vals = self._params[key[0]] + + for qubits, values in param_vals.items(): + if qubit_list and qubits not in qubit_list: + continue + + for value in values: + value_dict = dataclasses.asdict(value) + value_dict["qubits"] = qubits + value_dict["parameter"] = key[1].parameter + value_dict["schedule"] = key[1].schedule + + data.append(value_dict) + + return data + def to_db(self): """ Serializes the parameterized schedules and parameter values so diff --git a/requirements.txt b/requirements.txt index 21ef5177bf..fb0a5a9e9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ numpy>=1.17 qiskit-terra>=0.16.0 -pandas>=1.0.0 diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 30003984af..b5fe7713b0 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -68,10 +68,10 @@ def test_setup(self): self.assertEqual(self.cals.parameters[self.beta], {"xp", "xm", "x90p", "y90p"}) self.assertEqual(self.cals.parameters[self.sigma], {"xp", "xm", "x90p", "y90p"}) - self.assertEqual(self.cals.parameter_value("amp", (3,), "xp"), 0.2) - self.assertEqual(self.cals.parameter_value("amp", (3,), "xm"), 0.2) - self.assertEqual(self.cals.parameter_value("amp", (3,), "x90p"), 0.1) - self.assertEqual(self.cals.parameter_value("amp", (3,), "y90p"), 0.08) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "y90p"), 0.08) def test_parameter_dependency(self): """Check that two schedules that share the same parameter are simultaneously updated.""" @@ -93,12 +93,12 @@ def test_parameter_dependency(self): def test_get_value(self): """Test the retrieve of parameter values.""" - self.assertEqual(self.cals.parameter_value("amp", (3,), "xp"), 0.2) - self.assertEqual(self.cals.parameter_value("amp", (3,), "x90p"), 0.1) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) - self.assertEqual(self.cals.parameter_value("σ", (3,), "x90p"), 40) - self.assertEqual(self.cals.parameter_value("σ", (3,), "xp"), 40) + self.assertEqual(self.cals.get_parameter_value("σ", (3,), "x90p"), 40) + self.assertEqual(self.cals.get_parameter_value("σ", (3,), "xp"), 40) self.cals.add_parameter_value(ParameterValue(50, datetime.now()), "σ", (3,), "xp") - self.assertEqual(self.cals.parameter_value("σ", (3,), "x90p"), 50) - self.assertEqual(self.cals.parameter_value("σ", (3,), "xp"), 50) + self.assertEqual(self.cals.get_parameter_value("σ", (3,), "x90p"), 50) + self.assertEqual(self.cals.get_parameter_value("σ", (3,), "xp"), 50) From fbc2dc709892cd1fbae80e99bfa006434f178d81 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 16:56:06 +0200 Subject: [PATCH 009/178] * Cleaned up _get_Channel_index. --- .../calibration/calibrations.py | 48 +++++++------------ .../calibration/parameter_value.py | 2 +- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 69b5373633..4a96fe0051 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -153,18 +153,19 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ Get the index of the parameterized channel based on the given qubits and the name of the parameter in the channel index. The name of this - parameter must be written as qubit_index1.qubit_index2... . For example, - the following parameter names are valid: '1', '1.0', '3.10.0'. + parameter for control channels must be written as qubit_index1.qubit_index2... . + For example, the following parameter names are valid: '1', '1.0', '30.12'. 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=(int, int) and - the channel is a u channel with parameterized index name 'x.y' - where x and y the method returns the u_channel corresponding to - qubits (qubits[1], qubits[0]). + index: The index of the channel. For example, if qubits=(10, 32) and + chan is a control channel with parameterized index name '1.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 @@ -173,26 +174,15 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ if isinstance(chan.index, Parameter): - indices = [int(_) for _ in chan.index.name.split(".")] - ch_qubits = tuple(qubits[_] for _ in indices) + if isinstance(chan, (DriveChannel, MeasureChannel)): + if len(qubits) != 1: + raise CalibrationError(f"Too many qubits given for {chan.__class__.__name__}.") - if isinstance(chan, DriveChannel): - if len(ch_qubits) != 1: - raise CalibrationError( - f"Too many qubits for drive channel: got {len(ch_qubits)} expecting 1." - ) - - ch_ = self._config.drive(ch_qubits[0]) + return qubits[0] - elif isinstance(chan, MeasureChannel): - if len(ch_qubits) != 1: - raise CalibrationError( - f"Too many qubits for measure channel: got {len(ch_qubits)} expecting 1." - ) - - ch_ = self._config.measure(ch_qubits[0]) - - elif isinstance(chan, ControlChannel): + if isinstance(chan, ControlChannel): + indices = [int(sub_channel) for sub_channel in chan.index.name.split(".")] + ch_qubits = tuple(qubits[index] for index in indices) chs_ = self._config.control(ch_qubits) if len(chs_) != 1: @@ -201,15 +191,11 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: f"qubits {qubits} and {chan.name}." ) - ch_ = chs_[0] + return chs_[0].index - else: - chs = tuple(_.__name__ for _ in [DriveChannel, ControlChannel, MeasureChannel]) - raise CalibrationError(f"Channel must be of type {chs}.") + raise CalibrationError(f"{chan} must be a sub-type of {PulseChannel}.") - return ch_.index - else: - return chan.index + return chan.index def get_parameter_value( self, diff --git a/qiskit_experiments/calibration/parameter_value.py b/qiskit_experiments/calibration/parameter_value.py index 5e732066bf..8fbcd7d931 100644 --- a/qiskit_experiments/calibration/parameter_value.py +++ b/qiskit_experiments/calibration/parameter_value.py @@ -22,7 +22,7 @@ class ParameterValue: """A data class to store parameter values.""" # Value assumed by the parameter - value: Union[int, float] = None + value: Union[int, float, complex] = None # Data time when the value of the parameter was generated date_time: datetime = datetime.fromtimestamp(0) From 47ab364f9d81e2638987fb167ff78f9cd504ec12 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 16:56:44 +0200 Subject: [PATCH 010/178] * Removed pandas import. --- qiskit_experiments/calibration/calibrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4a96fe0051..c4a1e6d3ea 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -17,7 +17,6 @@ from collections import namedtuple from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional -import pandas as pd from qiskit.circuit import Gate from qiskit import QuantumCircuit From a9bc2803df138a33818a246d6b1b1a2a3f560454 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 22:44:36 +0200 Subject: [PATCH 011/178] * Added description to get_circuit. --- qiskit_experiments/calibration/calibrations.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index c4a1e6d3ea..33f3ebbf98 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -304,30 +304,34 @@ def get_schedule( def get_circuit( self, - name: str, + schedule_name: str, qubits: Tuple, free_params: List[str] = None, group: Optional[str] = "default", schedule: Schedule = None, ) -> QuantumCircuit: """ + Queries a schedule by name for the given set of qubits. The parameters given + under the list free_params are left unassigned. The queried schedule is then + embedded in a gate with a calibration and returned as a quantum circuit. + Args: - name: The name of the gate to retrieve. - qubits: The qubits for which to generate the Gate. + schedule_name: The name of the schedule to retrieve. + qubits: The qubits for which to generate the gate with the schedule in it. free_params: Names of the parameters that will remain unassigned. group: The calibration group from which to retrieve the calibrated values. - If unspecified this default to 'default'. + If unspecified this defaults to 'default'. schedule: The schedule to add to the gate if the internally stored one is - not going to be used. + not used. Returns: A quantum circuit in which the parameter values have been assigned aside from those explicitly specified in free_params. """ if schedule is None: - schedule = self.get_schedule(name, qubits, free_params, group) + schedule = self.get_schedule(schedule_name, qubits, free_params, group) - gate = Gate(name=name, num_qubits=len(qubits), params=list(schedule.parameters)) + gate = Gate(name=schedule_name, num_qubits=len(qubits), params=list(schedule.parameters)) circ = QuantumCircuit(len(qubits), len(qubits)) circ.append(gate, list(range(len(qubits)))) circ.add_calibration(gate, qubits, schedule, params=schedule.parameters) From f952975cf2a93a2172b9e0c7a844b6c81aecdec4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 22:49:50 +0200 Subject: [PATCH 012/178] * Added type to backend in __init__. --- qiskit_experiments/calibration/calibrations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 33f3ebbf98..de3aa1b790 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -18,6 +18,7 @@ from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional +from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Gate from qiskit import QuantumCircuit from qiskit.pulse import Schedule, DriveChannel, ControlChannel, MeasureChannel @@ -37,7 +38,7 @@ class Calibrations: dict where parameters are keys. """ - def __init__(self, backend): + def __init__(self, backend: Backend): """ Initialize the instructions from a given backend. From ec956ae45017a8ee55f35c4104ed754133e2403e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 22:56:30 +0200 Subject: [PATCH 013/178] * Added raise error to get_parameter_value. * Fixed issue with date_time. --- qiskit_experiments/calibration/calibrations.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index de3aa1b790..9117d9bdac 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -246,7 +246,16 @@ def get_parameter_value( candidates = [candidate for candidate in candidates if candidate.group == group] if cutoff_date: - candidates = [_ for _ in candidates 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 " \ + f"{group} on qubits {qubits} in schedule {sched_name} " + + if cutoff_date: + msg += f" Cutoff date: {cutoff_date}" + + raise CalibrationError(msg) candidates.sort(key=lambda x: x.date_time) From ae24a11dba097a0c4f0b7e8cd7d43fa6db34c322 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 23:10:09 +0200 Subject: [PATCH 014/178] * Added __init__ in tests for lint. * Added argument control_index to deal with multiple control channels per qubit pair. --- .../calibration/calibrations.py | 21 ++++++++++++------- test/calibration/__init__.py | 0 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 test/calibration/__init__.py diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9117d9bdac..6d31ef229f 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -149,7 +149,7 @@ def add_parameter_value( else: self._params[param][qubits].append(value) - def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: + def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: int = 0) -> int: """ Get the index of the parameterized channel based on the given qubits and the name of the parameter in the channel index. The name of this @@ -159,6 +159,8 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: Args: qubits: The qubits for which we want to obtain the channel index. chan: The channel with a parameterized name. + control_index: An index used to specify which control channel to use if a given + pair of qubits has more than one control channel. Returns: index: The index of the channel. For example, if qubits=(10, 32) and @@ -185,13 +187,12 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: ch_qubits = tuple(qubits[index] for index in indices) chs_ = self._config.control(ch_qubits) - if len(chs_) != 1: + if len(chs_) < control_index: raise CalibrationError( - "Ambiguous number of control channels for " - f"qubits {qubits} and {chan.name}." + f"Control channel index {control_index} not found for qubits {qubits}." ) - return chs_[0].index + return chs_[control_index].index raise CalibrationError(f"{chan} must be a sub-type of {PulseChannel}.") @@ -249,8 +250,10 @@ def get_parameter_value( 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 " \ - f"{group} on qubits {qubits} in schedule {sched_name} " + msg = ( + f"No candidate parameter values for {param_name} in calibration group " + f"{group} on qubits {qubits} in schedule {sched_name} " + ) if cutoff_date: msg += f" Cutoff date: {cutoff_date}" @@ -306,7 +309,9 @@ def get_schedule( binding_dict = {} for param in sched.parameters: if param.name not in free_params: - binding_dict[param] = self.get_parameter_value(param.name, qubits, name, group=group) + binding_dict[param] = self.get_parameter_value( + param.name, qubits, name, group=group + ) sched.assign_parameters(binding_dict) diff --git a/test/calibration/__init__.py b/test/calibration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 7dd1bedf0fa6d3540b18667c830e41a45db9b77d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 08:24:55 +0200 Subject: [PATCH 015/178] * Removed erronous comment. --- qiskit_experiments/calibration/calibrations.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 6d31ef229f..9f963555f7 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -51,8 +51,6 @@ def __init__(self, backend: Backend): self._properties = backend.properties() self._config = backend.configuration() self._params = {} - - # Required because copying a template schedule creates new paramters with new IDs. self._parameter_map = {} self._schedules = {} From edc402d608f37e8ceb9966bfc37696113db76986 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 14 Apr 2021 08:26:22 +0200 Subject: [PATCH 016/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9f963555f7..d4f88924db 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -59,7 +59,7 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): Add a schedule and register the parameters. Args: - schedules: The schedule to add. + schedules: The schedule(s) to add. Raises: CalibrationError: If the parameterized channel index is not formatted From ffe4d871e6fe1ef592c41a0df30fc3079c3c840c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 08:34:55 +0200 Subject: [PATCH 017/178] * implified CalibrationError. * Added unittest for channel names. --- qiskit_experiments/calibration/exceptions.py | 9 ------- test/calibration/test_calibrations.py | 25 +++++++++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/calibration/exceptions.py b/qiskit_experiments/calibration/exceptions.py index 26a27ac4b9..96e00667a8 100644 --- a/qiskit_experiments/calibration/exceptions.py +++ b/qiskit_experiments/calibration/exceptions.py @@ -17,12 +17,3 @@ class CalibrationError(QiskitError): """Errors raised by the calibration module.""" - - def __init__(self, *message): - """Set the error message.""" - super().__init__(*message) - self.message = " ".join(message) - - def __str__(self): - """Return the message.""" - return repr(self.message) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index b5fe7713b0..696e35c7b1 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -14,12 +14,13 @@ from datetime import datetime from qiskit.circuit import Parameter -from qiskit.pulse import Drag, DriveChannel +from qiskit.pulse import Drag, DriveChannel, ControlChannel from qiskit.test.mock import FakeAlmaden import qiskit.pulse as pulse from qiskit.test import QiskitTestCase from qiskit_experiments.calibration.calibrations import Calibrations from qiskit_experiments.calibration.parameter_value import ParameterValue +from qiskit_experiments.calibration.exceptions import CalibrationError class TestCalibrationsBasic(QiskitTestCase): @@ -102,3 +103,25 @@ def test_get_value(self): self.cals.add_parameter_value(ParameterValue(50, datetime.now()), "σ", (3,), "xp") self.assertEqual(self.cals.get_parameter_value("σ", (3,), "x90p"), 50) self.assertEqual(self.cals.get_parameter_value("σ", (3,), "xp"), 50) + + def test_channel_names(self): + """Check the naming of parametric control channels index1.index2.index3...""" + drive_0 = DriveChannel(Parameter('0')) + drive_1 = DriveChannel(Parameter('1')) + control_bad = ControlChannel(Parameter('u_chan')) + control_good = ControlChannel(Parameter('1.0')) + + with pulse.build() as sched_good: + pulse.play(Drag(160, 0.1, 40, 2), drive_0) + pulse.play(Drag(160, 0.1, 40, 2), drive_1) + pulse.play(Drag(160, 0.1, 40, 2), control_good) + + with pulse.build() as sched_bad: + pulse.play(Drag(160, 0.1, 40, 2), drive_0) + pulse.play(Drag(160, 0.1, 40, 2), drive_1) + pulse.play(Drag(160, 0.1, 40, 2), control_bad) + + self.cals.add_schedules(sched_good) + + with self.assertRaises(CalibrationError): + self.cals.add_schedules(sched_bad) From 14cc37888d0b7ec3cbf4c3ba1e9af6574a9581a3 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 14 Apr 2021 08:50:27 +0200 Subject: [PATCH 018/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d4f88924db..9f65ca86a8 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -92,9 +92,10 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): @property def parameters(self) -> Dict[Parameter, Set]: """ - Returns a dictionary of parameters managed by the calibrations definition. The value - of the dict is the schedule in which the parameter appears. Parameters that are not - attached to a schedule will have None as a key. + Returns a dictionary mapping parameters managed by the calibrations definition to schedules + using the parameters. The values of the dict are sets containing the names of the schedules in + which the parameter appears. Parameters that are not attached to a schedule will have None + in place of a schedule name. """ parameters = {} for key, param in self._parameter_map.items(): From d70acce6dc54eb6a7fe73141fc73e6cf9fc3464c Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 14 Apr 2021 08:52:05 +0200 Subject: [PATCH 019/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9f65ca86a8..efd70db4d5 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -201,7 +201,7 @@ def get_parameter_value( self, param: Union[Parameter, str], qubits: Tuple[int, ...], - schedule: Union[Schedule, str] = None, + schedule: Union[Schedule, str, None] = None, valid_only: bool = True, group: str = "default", cutoff_date: datetime = None, From b919f67299c9d99624205bd34719ed6a6d9f5449 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 08:58:54 +0200 Subject: [PATCH 020/178] * Simplified parameter assignment in the Calibrations. --- qiskit_experiments/calibration/calibrations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d4f88924db..0ce07788c6 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -298,15 +298,12 @@ def get_schedule( if ch.is_parameterized(): binding_dict[ch.index] = self._get_channel_index(qubits, ch) - sched.assign_parameters(binding_dict) - # Loop through the remaining parameters in the schedule, get their values and bind. if free_params is None: free_params = [] - binding_dict = {} for param in sched.parameters: - if param.name not in free_params: + if param.name not in free_params and param not in binding_dict: binding_dict[param] = self.get_parameter_value( param.name, qubits, name, group=group ) From b58d3cdfdef57886e7afedf45f7202989826985f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 11:19:44 +0200 Subject: [PATCH 021/178] * Removed pandas from the docstrings. --- qiskit_experiments/calibration/calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index f05ecbb169..5a9176aa00 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -355,7 +355,7 @@ def schedules(self) -> List[Dict[str, Any]]: users manage their schedules. Returns: - data: A pandas data frame with all the schedules in it. + data: A list of dictionaries with all the schedules in it. """ data = [] for name, schedule in self._schedules.items(): @@ -370,7 +370,7 @@ def parameters_table( qubit_list: Optional[Tuple[int, ...]] = None, ) -> List[Dict[str, Any]]: """ - Returns the parameters as a pandas data frame. + Returns the parameters as a list of dictionaries. This function is here to help users manage their parameters. Args: From b08c481078bbc52c81fff8ec704441a409ce5be4 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 14 Apr 2021 11:22:46 +0200 Subject: [PATCH 022/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5a9176aa00..b6375554cf 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -366,7 +366,7 @@ def schedules(self) -> List[Dict[str, Any]]: def parameters_table( self, parameters: List[str] = None, - schedules: Union[Schedule, str] = None, + schedules: List[Union[Schedule, str]] = None, qubit_list: Optional[Tuple[int, ...]] = None, ) -> List[Dict[str, Any]]: """ From a73d9e3e0f687f0f2818951ab33a37d4024c8647 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 11:33:56 +0200 Subject: [PATCH 023/178] * Fixed the keys in parameters_table. --- qiskit_experiments/calibration/calibrations.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b6375554cf..517e2e860d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -395,11 +395,14 @@ def parameters_table( keys = [] for key, param in self._parameter_map.items(): - if parameters and key.parameter in parameters: + if parameters and schedules: + if key.parameter in parameters and key.schedule in schedules: + keys.append((param, key)) + elif schedules and key.schedule in schedules: keys.append((param, key)) - if schedules and key.schedule in schedules: + elif parameters and key.parameter in parameters: keys.append((param, key)) - if parameters is None and schedules is None: + else: keys.append((param, key)) for key in keys: From 1d4923fded671b3a5b193717b4f5334343c9b4f9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 11:55:48 +0200 Subject: [PATCH 024/178] * Made parameter registration more transparent and added an error for duplicate names. --- .../calibration/calibrations.py | 27 ++++++++++++++++--- test/calibration/test_calibrations.py | 16 +++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 517e2e860d..267e5360e4 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -85,9 +85,30 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): self._schedules[schedule.name] = schedule for param in schedule.parameters: - self._parameter_map[ParameterKey(schedule.name, param.name)] = param - if param not in self._params: - self._params[param] = {} + self.register_parameter(param, schedule) + + def register_parameter(self, parameter: Parameter, schedule: Schedule = None): + """ + Registers a parameter for the given schedule. + + Args: + parameter: The parameter to register. + schedule: The Schedule to which this parameter belongs. The schedule can + be None which implies a global parameter. + + Raises: + CalibrationError: if a parameter with the same name was already registered + for the given schedule. + """ + sched_name = schedule.name if schedule else None + if (sched_name, parameter.name) in self._parameter_map: + raise CalibrationError( + f"Parameter with name {parameter.name} is not unique in schedule {sched_name}." + ) + + self._parameter_map[ParameterKey(sched_name, parameter.name)] = parameter + if parameter not in self._params: + self._params[parameter] = {} @property def parameters(self) -> Dict[Parameter, Set]: diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 696e35c7b1..c2245d2e04 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -125,3 +125,19 @@ def test_channel_names(self): with self.assertRaises(CalibrationError): self.cals.add_schedules(sched_bad) + + def test_unique_parameter_names(self): + """Test that we cannot insert schedules in which parameter names are duplicates.""" + with pulse.build() as sched: + pulse.play(Drag(160, Parameter('a'), Parameter('a'), Parameter('a')), DriveChannel(0)) + + with self.assertRaises(CalibrationError): + self.cals.add_schedules(sched) + + def test_parameter_without_schedule(self): + """Test that we can manage parameters that are not bound to a schedule.""" + self.cals.register_parameter(Parameter('a')) + + # Check that we cannot register the same parameter twice. + with self.assertRaises(CalibrationError): + self.cals.register_parameter(Parameter('a')) From f74534eee5e4ef3be7cd46e04994a02fbf79f5cf Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 19:55:53 +0200 Subject: [PATCH 025/178] * Improved docstring. --- qiskit_experiments/calibration/calibrations.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 267e5360e4..4b714fa15d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -233,13 +233,14 @@ def get_parameter_value( 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 the name of the schedule for which to get the parameter value. + 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 specifies 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. + 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. From 72ce1c092e01c70fb55a3c1e1145bd41f5775057 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 20:23:10 +0200 Subject: [PATCH 026/178] * Added check to ensure that all parameter names in a schedule are unique. * Removed the erroneous check in register_parameter. --- qiskit_experiments/calibration/calibrations.py | 17 +++++++---------- test/calibration/test_calibrations.py | 4 ---- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4b714fa15d..341d9e7dbb 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -63,7 +63,8 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): Raises: CalibrationError: If the parameterized channel index is not formatted - following index1.index2... + following index1.index2... or if several parameters in the same schedule + have the same name. """ if isinstance(schedules, Schedule): schedules = [schedules] @@ -84,6 +85,11 @@ def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): self._schedules[schedule.name] = schedule + param_names = [param.name for param in schedule.parameters] + + if len(param_names) != len(set(param_names)): + raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") + for param in schedule.parameters: self.register_parameter(param, schedule) @@ -95,17 +101,8 @@ def register_parameter(self, parameter: Parameter, schedule: Schedule = None): parameter: The parameter to register. schedule: The Schedule to which this parameter belongs. The schedule can be None which implies a global parameter. - - Raises: - CalibrationError: if a parameter with the same name was already registered - for the given schedule. """ sched_name = schedule.name if schedule else None - if (sched_name, parameter.name) in self._parameter_map: - raise CalibrationError( - f"Parameter with name {parameter.name} is not unique in schedule {sched_name}." - ) - self._parameter_map[ParameterKey(sched_name, parameter.name)] = parameter if parameter not in self._params: self._params[parameter] = {} diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index c2245d2e04..689114f6a6 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -137,7 +137,3 @@ def test_unique_parameter_names(self): def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" self.cals.register_parameter(Parameter('a')) - - # Check that we cannot register the same parameter twice. - with self.assertRaises(CalibrationError): - self.cals.register_parameter(Parameter('a')) From 2e2205975f3be086e4bfae758ca28b3ddd98c835 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 20:28:32 +0200 Subject: [PATCH 027/178] * Removed properties from Calibrations. --- qiskit_experiments/calibration/calibrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 341d9e7dbb..adb64472bc 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -48,7 +48,6 @@ def __init__(self, backend: Backend): self._n_qubits = backend.configuration().num_qubits self._n_uchannels = backend.configuration().n_uchannels - self._properties = backend.properties() self._config = backend.configuration() self._params = {} self._parameter_map = {} From 8053c5c9a8456a42f06cf8bce2ce2b909f6e94e1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 21:06:17 +0200 Subject: [PATCH 028/178] * Made self._params a defaultdict. --- qiskit_experiments/calibration/calibrations.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index adb64472bc..974b19e7af 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -14,7 +14,7 @@ import copy import dataclasses -from collections import namedtuple +from collections import namedtuple, defaultdict from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional @@ -49,7 +49,7 @@ def __init__(self, backend: Backend): self._n_qubits = backend.configuration().num_qubits self._n_uchannels = backend.configuration().n_uchannels self._config = backend.configuration() - self._params = {} + self._params = defaultdict(dict) self._parameter_map = {} self._schedules = {} @@ -103,8 +103,6 @@ def register_parameter(self, parameter: Parameter, schedule: Schedule = None): """ sched_name = schedule.name if schedule else None self._parameter_map[ParameterKey(sched_name, parameter.name)] = parameter - if parameter not in self._params: - self._params[parameter] = {} @property def parameters(self) -> Dict[Parameter, Set]: From 75a5cfbb172e9a1dd74888b8c172f1bd74a540ec Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 21:12:42 +0200 Subject: [PATCH 029/178] * Fixed order of raise in add_parameter_value. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 974b19e7af..5ff39cc4f2 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -151,10 +151,10 @@ def add_parameter_value( sched_name = schedule.name if isinstance(schedule, Schedule) else schedule if (sched_name, param_name) not in self._parameter_map: - if sched_name is not None: - raise CalibrationError(f"Unknown parameter {param_name}.") + if sched_name: + raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") - raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") + raise CalibrationError(f"Unknown parameter {param_name}.") param = self._parameter_map[(sched_name, param_name)] From 0862722a4219d733fab24a40451ee50c6d07ed97 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Apr 2021 21:18:19 +0200 Subject: [PATCH 030/178] * Removed the deepcopy of schedule. --- qiskit_experiments/calibration/calibrations.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5ff39cc4f2..8dab4c9d09 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -12,7 +12,6 @@ """Class to store the results of a calibration experiments.""" -import copy import dataclasses from collections import namedtuple, defaultdict from datetime import datetime @@ -287,6 +286,8 @@ def get_schedule( group: Optional[str] = "default", ) -> Schedule: """ + Get the schedule with the non-free parameters assigned to their values. + Args: name: The name of the schedule to get. qubits: The qubits for which to get the schedule. @@ -295,23 +296,18 @@ def get_schedule( parameters. If not specifies this defaults to the 'default' group. Returns: - schedule: A deep copy of the template schedule with all parameters assigned + schedule: A copy of the template schedule with all parameters assigned except for those specified by free_params. Raises: CalibrationError: if the name of the schedule is not known. """ - - # Get the schedule and deepcopy it to prevent binding from removing - # the parametric nature of the schedule. if name not in self._schedules: raise CalibrationError("Schedule %s is not defined." % name) - sched = copy.deepcopy(self._schedules[name]) - # Retrieve the channel indices based on the qubits and bind them. binding_dict = {} - for ch in sched.channels: + for ch in self._schedules[name].channels: if ch.is_parameterized(): binding_dict[ch.index] = self._get_channel_index(qubits, ch) @@ -319,15 +315,13 @@ def get_schedule( if free_params is None: free_params = [] - for param in sched.parameters: + for param in self._schedules[name].parameters: if param.name not in free_params and param not in binding_dict: binding_dict[param] = self.get_parameter_value( param.name, qubits, name, group=group ) - sched.assign_parameters(binding_dict) - - return sched + return self._schedules[name].assign_parameters(binding_dict, inplace=False) def get_circuit( self, From d35b0ed33ec7b3843d40033a14fb86ae3a18c243 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 18 Apr 2021 14:57:49 +0200 Subject: [PATCH 031/178] * Added qubits to keys in self._schedules and self._parameter_map, and self._params. --- .../calibration/calibrations.py | 130 ++++++++++-------- test/calibration/test_calibrations.py | 5 +- 2 files changed, 76 insertions(+), 59 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 8dab4c9d09..f4dc3b5918 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -26,7 +26,7 @@ from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue -ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter"]) +ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter", "qubits"]) class Calibrations: @@ -48,50 +48,47 @@ def __init__(self, backend: Backend): self._n_qubits = backend.configuration().num_qubits self._n_uchannels = backend.configuration().n_uchannels self._config = backend.configuration() - self._params = defaultdict(dict) + self._params = defaultdict(list) self._parameter_map = {} self._schedules = {} - def add_schedules(self, schedules: Union[Schedule, List[Schedule]]): + def add_schedules(self, schedule: Schedule, qubits: Tuple = None): """ Add a schedule and register the parameters. Args: - schedules: The schedule(s) to add. + schedule: The schedule(s) to add. + qubits: The qubits for which to add the schedules. If None is given then this + schedule is the default schedule for all qubits. Raises: CalibrationError: If the parameterized channel index is not formatted following index1.index2... or if several parameters in the same schedule have the same name. """ - if isinstance(schedules, Schedule): - schedules = [schedules] - - for schedule in schedules: - - # check that channels, if parameterized, have the proper name format. - # pylint: disable = raise-missing-from - for ch in schedule.channels: - if isinstance(ch.index, Parameter): - try: - [int(index) for index in ch.index.name.split(".")] - except ValueError: - raise CalibrationError( - "Parameterized channel must have a name " - "formatted following index1.index2..." - ) + # check that channels, if parameterized, have the proper name format. + # pylint: disable = raise-missing-from + for ch in schedule.channels: + if isinstance(ch.index, Parameter): + try: + [int(index) for index in ch.index.name.split(".")] + except ValueError: + raise CalibrationError( + "Parameterized channel must have a name " + "formatted following index1.index2..." + ) - self._schedules[schedule.name] = schedule + self._schedules[(schedule.name, qubits)] = schedule - param_names = [param.name for param in schedule.parameters] + param_names = [param.name for param in schedule.parameters] - if len(param_names) != len(set(param_names)): - raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") + if len(param_names) != len(set(param_names)): + raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") - for param in schedule.parameters: - self.register_parameter(param, schedule) + for param in schedule.parameters: + self.register_parameter(param, schedule) - def register_parameter(self, parameter: Parameter, schedule: Schedule = None): + def register_parameter(self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None): """ Registers a parameter for the given schedule. @@ -99,9 +96,10 @@ def register_parameter(self, parameter: Parameter, schedule: Schedule = None): parameter: The parameter to register. schedule: The Schedule to which this parameter belongs. The schedule can be None which implies a global parameter. + qubits: The qubits for which to register the schedule. """ sched_name = schedule.name if schedule else None - self._parameter_map[ParameterKey(sched_name, parameter.name)] = parameter + self._parameter_map[ParameterKey(sched_name, parameter.name, qubits)] = parameter @property def parameters(self) -> Dict[Parameter, Set]: @@ -126,7 +124,7 @@ def add_parameter_value( self, value: ParameterValue, param: Union[Parameter, str], - qubits: Tuple[int, ...], + qubits: Tuple[int, ...] = None, schedule: Union[Schedule, str] = None, ): """ @@ -149,18 +147,26 @@ def add_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - if (sched_name, param_name) not in self._parameter_map: - if sched_name: - raise CalibrationError(f"Unknown parameter {param_name} in schedule {sched_name}.") + # First look for a parameter that matches the given qubits. + if (sched_name, param_name, qubits) in self._parameter_map: + param = self._parameter_map[(sched_name, param_name, qubits)] + + # If no parameter was found look for a default parameter + else: + param = self._parameter_map[(sched_name, param_name, None)] - raise CalibrationError(f"Unknown parameter {param_name}.") + if param is None: + raise CalibrationError(f"No parameter found for parameter {param_name} in " + f"schedule {sched_name} and qubits {qubits}.") - param = self._parameter_map[(sched_name, param_name)] + # Find all schedules that share this parameter + common_schedules = [(sched_name, param_name, qubits)] + for key in self._parameter_map.keys(): + if self._parameter_map[key] == param: + common_schedules.append(key) - if qubits not in self._params[param]: - self._params[param][qubits] = [value] - else: - self._params[param][qubits].append(value) + for key in common_schedules: + self._params[key].append(value) def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: int = 0) -> int: """ @@ -221,7 +227,8 @@ def get_parameter_value( cutoff_date: datetime = None, ) -> Union[int, float, complex]: """ - Retrieve the value of a calibrated parameter from those stored. + 1) Check if the given qubits have their own Parameter. + 2) If they do not check to see if a parameter global to all qubits exists. Args: param: The parameter or the name of the parameter for which to get the parameter value. @@ -245,30 +252,32 @@ def get_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - if (sched_name, param_name) not in self._parameter_map: - raise CalibrationError(f"No parameter for {param_name} and schedule {sched_name}.") - - param = self._parameter_map[(sched_name, param_name)] - - if qubits not in self._params[param]: - raise CalibrationError(f"No parameter value for {param} and qubits {qubits}.") + if (sched_name, param_name, qubits) in self._params: + candidates = self._params[(sched_name, param_name, qubits)] + elif (sched_name, param_name, None) in self._params: + candidates = self._params[(sched_name, param_name, None)] + else: + raise CalibrationError(f"No parameter for {param_name} and schedule {sched_name} " + f"and qubits {qubits}. No default value exists.") if valid_only: - candidates = [p for p in self._params[param][qubits] if p.valid] - else: - candidates = self._params[param][qubits] + candidates = [val for val in candidates if val.valid] - candidates = [candidate for candidate in candidates if candidate.group == group] + 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 " - f"{group} on qubits {qubits} in schedule {sched_name} " + f"No candidate parameter values for {param_name} in calibration group {group} " ) + if qubits: + msg += f"on qubits {qubits} " + + msg += f"in schedule {sched_name}" + if cutoff_date: msg += f" Cutoff date: {cutoff_date}" @@ -302,12 +311,18 @@ def get_schedule( Raises: CalibrationError: if the name of the schedule is not known. """ - if name not in self._schedules: - raise CalibrationError("Schedule %s is not defined." % name) + print(self._schedules) + + if (name, qubits) in self._schedules: + schedule = self._schedules[(name, qubits)] + elif (name, None) in self._schedules: + schedule = self._schedules[(name, None)] + 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 self._schedules[name].channels: + for ch in schedule.channels: if ch.is_parameterized(): binding_dict[ch.index] = self._get_channel_index(qubits, ch) @@ -315,13 +330,13 @@ def get_schedule( if free_params is None: free_params = [] - for param in self._schedules[name].parameters: + for param in schedule.parameters: if param.name not in free_params and param not in binding_dict: binding_dict[param] = self.get_parameter_value( param.name, qubits, name, group=group ) - return self._schedules[name].assign_parameters(binding_dict, inplace=False) + return schedule.assign_parameters(binding_dict, inplace=False) def get_circuit( self, @@ -397,6 +412,7 @@ def parameters_table( data = [] # Convert inputs to lists of strings + # TODO Align to param, sched, qubits if parameters is not None: parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 689114f6a6..2042abdd7d 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -51,11 +51,12 @@ def setUp(self): with pulse.build(name="y90p") as y90p: pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), drive) - self.cals.add_schedules([xp, x90p, y90p, xm]) + for sched in [xp, x90p, y90p, xm]: + self.cals.add_schedules(sched) # Add some parameter values. now = datetime.now - self.cals.add_parameter_value(ParameterValue(40, now()), "σ", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(40, now()), "σ", None, "xp") self.cals.add_parameter_value(ParameterValue(0.2, now()), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.1, now()), "amp", (3,), "x90p") self.cals.add_parameter_value(ParameterValue(0.08, now()), "amp", (3,), "y90p") From dfc2788ab1759ac36b92e0a0765b7bdb6d662bb2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 19 Apr 2021 07:51:50 +0200 Subject: [PATCH 032/178] * Added test where default schedule is overridden. --- .../calibration/calibrations.py | 22 ++-- test/calibration/test_calibrations.py | 111 +++++++++++++++--- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index f4dc3b5918..a0c79bca7f 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -88,7 +88,9 @@ def add_schedules(self, schedule: Schedule, qubits: Tuple = None): for param in schedule.parameters: self.register_parameter(param, schedule) - def register_parameter(self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None): + def register_parameter( + self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None + ): """ Registers a parameter for the given schedule. @@ -156,8 +158,10 @@ def add_parameter_value( param = self._parameter_map[(sched_name, param_name, None)] if param is None: - raise CalibrationError(f"No parameter found for parameter {param_name} in " - f"schedule {sched_name} and qubits {qubits}.") + raise CalibrationError( + f"No parameter found for parameter {param_name} in " + f"schedule {sched_name} and qubits {qubits}." + ) # Find all schedules that share this parameter common_schedules = [(sched_name, param_name, qubits)] @@ -257,8 +261,10 @@ def get_parameter_value( elif (sched_name, param_name, None) in self._params: candidates = self._params[(sched_name, param_name, None)] else: - raise CalibrationError(f"No parameter for {param_name} and schedule {sched_name} " - f"and qubits {qubits}. No default value exists.") + raise CalibrationError( + f"No parameter for {param_name} and schedule {sched_name} " + f"and qubits {qubits}. No default value exists." + ) if valid_only: candidates = [val for val in candidates if val.valid] @@ -269,9 +275,7 @@ def get_parameter_value( 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} " - ) + msg = f"No candidate parameter values for {param_name} in calibration group {group} " if qubits: msg += f"on qubits {qubits} " @@ -311,8 +315,6 @@ def get_schedule( Raises: CalibrationError: if the name of the schedule is not known. """ - print(self._schedules) - if (name, qubits) in self._schedules: schedule = self._schedules[(name, qubits)] elif (name, None) in self._schedules: diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 2042abdd7d..1f17da13fe 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -14,7 +14,7 @@ from datetime import datetime from qiskit.circuit import Parameter -from qiskit.pulse import Drag, DriveChannel, ControlChannel +from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian from qiskit.test.mock import FakeAlmaden import qiskit.pulse as pulse from qiskit.test import QiskitTestCase @@ -36,31 +36,32 @@ def setUp(self): self.amp_x90p = Parameter("amp") self.amp_y90p = Parameter("amp") self.beta = Parameter("β") - drive = DriveChannel(Parameter("0")) + self.drive = DriveChannel(Parameter("0")) # Define and add template schedules. with pulse.build(name="xp") as xp: - pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), drive) + pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), self.drive) with pulse.build(name="xm") as xm: - pulse.play(Drag(160, -self.amp_xp, self.sigma, self.beta), drive) + pulse.play(Drag(160, -self.amp_xp, self.sigma, self.beta), self.drive) with pulse.build(name="x90p") as x90p: - pulse.play(Drag(160, self.amp_x90p, self.sigma, self.beta), drive) + pulse.play(Drag(160, self.amp_x90p, self.sigma, self.beta), self.drive) with pulse.build(name="y90p") as y90p: - pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), drive) + pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), self.drive) for sched in [xp, x90p, y90p, xm]: self.cals.add_schedules(sched) # Add some parameter values. - now = datetime.now - self.cals.add_parameter_value(ParameterValue(40, now()), "σ", None, "xp") - self.cals.add_parameter_value(ParameterValue(0.2, now()), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(0.1, now()), "amp", (3,), "x90p") - self.cals.add_parameter_value(ParameterValue(0.08, now()), "amp", (3,), "y90p") - self.cals.add_parameter_value(ParameterValue(40, now()), "β", (3,), "xp") + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "x90p") + self.cals.add_parameter_value(ParameterValue(0.08, self.date_time), "amp", (3,), "y90p") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "β", (3,), "xp") def test_setup(self): """Test that the initial setup behaves as expected.""" @@ -107,10 +108,10 @@ def test_get_value(self): def test_channel_names(self): """Check the naming of parametric control channels index1.index2.index3...""" - drive_0 = DriveChannel(Parameter('0')) - drive_1 = DriveChannel(Parameter('1')) - control_bad = ControlChannel(Parameter('u_chan')) - control_good = ControlChannel(Parameter('1.0')) + drive_0 = DriveChannel(Parameter("0")) + drive_1 = DriveChannel(Parameter("1")) + control_bad = ControlChannel(Parameter("u_chan")) + control_good = ControlChannel(Parameter("1.0")) with pulse.build() as sched_good: pulse.play(Drag(160, 0.1, 40, 2), drive_0) @@ -130,11 +131,85 @@ def test_channel_names(self): def test_unique_parameter_names(self): """Test that we cannot insert schedules in which parameter names are duplicates.""" with pulse.build() as sched: - pulse.play(Drag(160, Parameter('a'), Parameter('a'), Parameter('a')), DriveChannel(0)) + pulse.play(Drag(160, Parameter("a"), Parameter("a"), Parameter("a")), DriveChannel(0)) with self.assertRaises(CalibrationError): self.cals.add_schedules(sched) def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" - self.cals.register_parameter(Parameter('a')) + self.cals.register_parameter(Parameter("a")) + + +class TestCalibrationDefaults(QiskitTestCase): + """Test that we can override defaults.""" + + def setUp(self): + """Setup a few parameters.""" + backend = FakeAlmaden() + self.cals = Calibrations(backend) + + self.sigma = Parameter("σ") + self.amp_xp = Parameter("amp") + self.amp = Parameter("amp") + self.beta = Parameter("β") + self.drive = DriveChannel(Parameter("0")) + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + def test_default_schedules(self): + """ + In this test we create two xp schedules. A default schedules with a + Gaussian pulse for all qubits and a Drag schedule for qubit three which + should override the default schedule. We also test to see that updating + a common parameter affects both schedules. + """ + + # Template schedule for qubit 3 + with pulse.build(name="xp") as xp_drag: + pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), self.drive) + + # Default template schedule for all qubits + amp = Parameter("amp") # Same name as self.amp_xp + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) + + # Add the schedules + self.cals.add_schedules(xp) + self.cals.add_schedules(xp_drag, (3,)) + + # Add the minimum number of parameter values + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") + self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") + + xp0 = self.cals.get_schedule("xp", (0,)) + xp3 = self.cals.get_schedule("xp", (3,)) + + # Check that xp0 is Play(Gaussian(160, 0.15, 40), 0) + self.assertTrue(isinstance(xp0.instructions[0][1].pulse, Gaussian)) + self.assertEqual(xp0.instructions[0][1].channel, DriveChannel(0)) + self.assertEqual(xp0.instructions[0][1].pulse.amp, 0.15) + self.assertEqual(xp0.instructions[0][1].pulse.sigma, 40) + self.assertEqual(xp0.instructions[0][1].pulse.duration, 160) + + # Check that xp3 is Play(Drag(160, 0.25, 40, 10), 3) + self.assertTrue(isinstance(xp3.instructions[0][1].pulse, Drag)) + self.assertEqual(xp3.instructions[0][1].channel, DriveChannel(3)) + self.assertEqual(xp3.instructions[0][1].pulse.amp, 0.25) + self.assertEqual(xp3.instructions[0][1].pulse.sigma, 40) + self.assertEqual(xp3.instructions[0][1].pulse.duration, 160) + self.assertEqual(xp3.instructions[0][1].pulse.beta, 10) + + later_date_time = datetime.strptime("16/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + self.cals.add_parameter_value(ParameterValue(50, later_date_time), "σ", None, "xp") + + xp0 = self.cals.get_schedule("xp", (0,)) + xp3 = self.cals.get_schedule("xp", (3,)) + + self.assertEqual(xp0.instructions[0][1].pulse.sigma, 50) + self.assertEqual(xp3.instructions[0][1].pulse.sigma, 50) + + # Check that we have the expected parameters in the calibrations. + expected = {self.amp_xp, self.amp, self.sigma, self.beta} + self.assertEqual(len(set(self.cals.parameters.keys())), len(expected)) From 70f4616b6224fdadd931d1b367c58b77c1e18e19 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 19 Apr 2021 12:55:51 +0200 Subject: [PATCH 033/178] * Updated parameter management to - parameter_map as (schedule.name, parameter.name, qubits): Parameter - _params as (schedule.name, parameter.name, qubits): [ParameterValue, ...] --- .../calibration/calibrations.py | 78 ++++++++-------- test/calibration/test_calibrations.py | 92 ++++++++++++++++--- 2 files changed, 116 insertions(+), 54 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a0c79bca7f..f41a0524f5 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -48,8 +48,13 @@ def __init__(self, backend: Backend): self._n_qubits = backend.configuration().num_qubits self._n_uchannels = backend.configuration().n_uchannels self._config = backend.configuration() - self._params = defaultdict(list) + + # Dict of the form: (schedule.name, parameter.name, qubits): Parameter self._parameter_map = {} + + # Default dict of the form: (schedule.name, parameter.name, qubits): [ParameterValue, ...] + self._params = defaultdict(list) + self._schedules = {} def add_schedules(self, schedule: Schedule, qubits: Tuple = None): @@ -68,8 +73,10 @@ def add_schedules(self, schedule: Schedule, qubits: Tuple = None): """ # check that channels, if parameterized, have the proper name format. # pylint: disable = raise-missing-from + param_indices = set() for ch in schedule.channels: if isinstance(ch.index, Parameter): + param_indices.add(ch.index) try: [int(index) for index in ch.index.name.split(".")] except ValueError: @@ -85,8 +92,10 @@ def add_schedules(self, schedule: Schedule, qubits: Tuple = None): if len(param_names) != len(set(param_names)): raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") + # Register parameters that are not indices. for param in schedule.parameters: - self.register_parameter(param, schedule) + if param not in param_indices: + self.register_parameter(param, schedule, qubits) def register_parameter( self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None @@ -111,14 +120,14 @@ def parameters(self) -> Dict[Parameter, Set]: which the parameter appears. Parameters that are not attached to a schedule will have None in place of a schedule name. """ - parameters = {} + parameters = defaultdict(set) for key, param in self._parameter_map.items(): schedule_name = key.schedule - if param not in parameters: - parameters[param] = {schedule_name} + if key.qubits: + parameters[param].add((schedule_name, key.qubits)) else: - parameters[param].add(schedule_name) + parameters[param].add((schedule_name,)) return parameters @@ -145,17 +154,16 @@ def add_parameter_value( channel type is not a ControlChannel, DriveChannel, or MeasureChannel, or if the parameter name is not already in self. """ - param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule # First look for a parameter that matches the given qubits. if (sched_name, param_name, qubits) in self._parameter_map: - param = self._parameter_map[(sched_name, param_name, qubits)] + param = self._parameter_map[ParameterKey(sched_name, param_name, qubits)] # If no parameter was found look for a default parameter else: - param = self._parameter_map[(sched_name, param_name, None)] + param = self._parameter_map[ParameterKey(sched_name, param_name, None)] if param is None: raise CalibrationError( @@ -164,10 +172,10 @@ def add_parameter_value( ) # Find all schedules that share this parameter - common_schedules = [(sched_name, param_name, qubits)] + common_schedules = {ParameterKey(sched_name, param_name, qubits)} for key in self._parameter_map.keys(): if self._parameter_map[key] == param: - common_schedules.append(key) + common_schedules.add(key) for key in common_schedules: self._params[key].append(value) @@ -394,11 +402,9 @@ def parameters_table( self, parameters: List[str] = None, schedules: List[Union[Schedule, str]] = None, - qubit_list: Optional[Tuple[int, ...]] = None, + qubit_list: List[Tuple[int, ...]] = None, ) -> List[Dict[str, Any]]: """ - Returns the parameters as a list of dictionaries. - This function is here to help users manage their parameters. Args: parameters: The parameter names that should be included in the returned @@ -408,45 +414,39 @@ def parameters_table( If None is given then all channels are returned. Returns: - data: A data frame of parameter values. + data: A dictionary of parameter values which can easily be converted to a + data frame. """ data = [] # Convert inputs to lists of strings - # TODO Align to param, sched, qubits if parameters is not None: parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} if schedules is not None: schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} - keys = [] - for key, param in self._parameter_map.items(): - if parameters and schedules: - if key.parameter in parameters and key.schedule in schedules: - keys.append((param, key)) - elif schedules and key.schedule in schedules: - keys.append((param, key)) - elif parameters and key.parameter in parameters: - keys.append((param, key)) - else: - keys.append((param, key)) + # Look for exact matches. Default values will be ignored. + keys = set() + for key, param in self._params.items(): + if parameters and key.parameter not in parameters: + continue + if schedules and key.schedule not in schedules: + continue + if qubit_list and key.qubits not in qubit_list: + continue - for key in keys: - param_vals = self._params[key[0]] + keys.add(key) - for qubits, values in param_vals.items(): - if qubit_list and qubits not in qubit_list: - continue - - for value in values: - value_dict = dataclasses.asdict(value) - value_dict["qubits"] = qubits - value_dict["parameter"] = key[1].parameter - value_dict["schedule"] = key[1].schedule + for key in keys: + for value in self._params[key]: + value_dict = dataclasses.asdict(value) + value_dict["qubits"] = key.qubits + value_dict["parameter"] = key.parameter + value_dict["schedule"] = key.schedule - data.append(value_dict) + data.append(value_dict) return data diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 1f17da13fe..6eabdd88d8 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -65,11 +65,13 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" - self.assertEqual(self.cals.parameters[self.amp_xp], {"xp", "xm"}) - self.assertEqual(self.cals.parameters[self.amp_x90p], {"x90p"}) - self.assertEqual(self.cals.parameters[self.amp_y90p], {"y90p"}) - self.assertEqual(self.cals.parameters[self.beta], {"xp", "xm", "x90p", "y90p"}) - self.assertEqual(self.cals.parameters[self.sigma], {"xp", "xm", "x90p", "y90p"}) + self.assertEqual(self.cals.parameters[self.amp_xp], {("xp",), ("xm",)}) + self.assertEqual(self.cals.parameters[self.amp_x90p], {("x90p",)}) + self.assertEqual(self.cals.parameters[self.amp_y90p], {("y90p",)}) + + expected = {("xp",), ("xm",), ("x90p",), ("y90p",)} + self.assertEqual(self.cals.parameters[self.beta], expected) + self.assertEqual(self.cals.parameters[self.sigma], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) @@ -156,20 +158,11 @@ def setUp(self): self.drive = DriveChannel(Parameter("0")) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - def test_default_schedules(self): - """ - In this test we create two xp schedules. A default schedules with a - Gaussian pulse for all qubits and a Drag schedule for qubit three which - should override the default schedule. We also test to see that updating - a common parameter affects both schedules. - """ - # Template schedule for qubit 3 with pulse.build(name="xp") as xp_drag: pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), self.drive) # Default template schedule for all qubits - amp = Parameter("amp") # Same name as self.amp_xp with pulse.build(name="xp") as xp: pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) @@ -177,12 +170,56 @@ def test_default_schedules(self): self.cals.add_schedules(xp) self.cals.add_schedules(xp_drag, (3,)) - # Add the minimum number of parameter values + def test_parameter_value_adding_and_filtering(self): + """Test that adding parameter values behaves in the expected way.""" + + # Ensure that no parameter values are present when none have been added. + params = self.cals.parameters_table() + self.assertEqual(params, []) + + # Add a default parameter common to all qubits. + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.assertEqual(len(self.cals.parameters_table()), 1) + + # Check that we can get a default parameter in the parameter table + self.assertEqual(len(self.cals.parameters_table(parameters=["σ"])), 2) + self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xp"])), 2) + self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xm"])), 0) + + # Test behaviour of qubit-specific parameter + self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") + + # Check the value for qubit 0 + params = self.cals.parameters_table(parameters=["amp"], qubit_list=[(0,)]) + self.assertEqual(len(params), 1) + self.assertEqual(params[0]["value"], 0.15) + self.assertEqual(params[0]["qubits"], (0,)) + + # Check the value for qubit 3 + params = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,)]) + self.assertEqual(len(params), 1) + self.assertEqual(params[0]["value"], 0.25) + self.assertEqual(params[0]["qubits"], (3,)) + + def _add_parameters(self): + """Helper function.""" + + # Add the minimum number of parameter values. Sigma is shared across both schedules. self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") + def test_default_schedules(self): + """ + In this test we create two xp schedules. A default schedules with a + Gaussian pulse for all qubits and a Drag schedule for qubit three which + should override the default schedule. We also test to see that updating + a common parameter affects both schedules. + """ + self._add_parameters() + xp0 = self.cals.get_schedule("xp", (0,)) xp3 = self.cals.get_schedule("xp", (3,)) @@ -201,6 +238,7 @@ def test_default_schedules(self): self.assertEqual(xp3.instructions[0][1].pulse.duration, 160) self.assertEqual(xp3.instructions[0][1].pulse.beta, 10) + # Check that updating sigma updates both schedules. later_date_time = datetime.strptime("16/09/19 10:21:35", "%d/%m/%y %H:%M:%S") self.cals.add_parameter_value(ParameterValue(50, later_date_time), "σ", None, "xp") @@ -213,3 +251,27 @@ def test_default_schedules(self): # Check that we have the expected parameters in the calibrations. expected = {self.amp_xp, self.amp, self.sigma, self.beta} self.assertEqual(len(set(self.cals.parameters.keys())), len(expected)) + + def test_parameter_filtering(self): + """Test that we can properly filter parameter values.""" + + self._add_parameters() + + # Check that these values are split between the qubits. + amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(0,)]) + self.assertEqual(len(amp_values), 1) + + # Check that we have one value for sigma. + sigma_values = self.cals.parameters_table(parameters=["σ"]) + self.assertEqual(len(sigma_values), 1) + + # Check that we have two values for amp. + amp_values = self.cals.parameters_table(parameters=["amp"]) + self.assertEqual(len(amp_values), 2) + + amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,)]) + self.assertEqual(len(amp_values), 1) + + # Check to see if we get back the two qubits when explicitly specifying them. + amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,), (0,)]) + self.assertEqual(len(amp_values), 2) From 50af0e3cf2b946779866bbd381eb96f167342c6d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 19 Apr 2021 13:08:36 +0200 Subject: [PATCH 034/178] * Improved docstrings. --- .../calibration/calibrations.py | 43 ++++++++++++------- test/calibration/test_calibrations.py | 12 +++--- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index f41a0524f5..34fac35dfd 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class to store the results of a calibration experiments.""" +"""Class to store and manage the results of a calibration experiments.""" import dataclasses from collections import namedtuple, defaultdict @@ -34,7 +34,9 @@ class Calibrations: A class to manage schedules with calibrated parameter values. Schedules are stored in a dict and are intended to be fully parameterized, including the index of the channels. The parameter values are stored in a - dict where parameters are keys. + dict where parameters are keys. This class supports: + - having different schedules share parameters + - allows default schedules for qubits that can be overridden of specific qubits. """ def __init__(self, backend: Backend): @@ -57,12 +59,12 @@ def __init__(self, backend: Backend): self._schedules = {} - def add_schedules(self, schedule: Schedule, qubits: Tuple = None): + def add_schedule(self, schedule: Schedule, qubits: Tuple = None): """ - Add a schedule and register the parameters. + Add a schedule and register its parameters. Args: - schedule: The schedule(s) to add. + schedule: The schedule to add. qubits: The qubits for which to add the schedules. If None is given then this schedule is the default schedule for all qubits. @@ -101,13 +103,15 @@ def register_parameter( self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None ): """ - Registers a parameter for the given schedule. + Registers a parameter for the given schedule. This allows self to determine the + parameter instance that corresponds to the given schedule name, parameter name + and qubits. Args: parameter: The parameter to register. schedule: The Schedule to which this parameter belongs. The schedule can - be None which implies a global parameter. - qubits: The qubits for which to register the schedule. + be None which allows the calibration to accommodate, e.g. qubit frequencies. + qubits: The qubits for which to register the parameter. """ sched_name = schedule.name if schedule else None self._parameter_map[ParameterKey(sched_name, parameter.name, qubits)] = parameter @@ -115,10 +119,10 @@ def register_parameter( @property def parameters(self) -> Dict[Parameter, Set]: """ - Returns a dictionary mapping parameters managed by the calibrations definition to schedules - using the parameters. The values of the dict are sets containing the names of the schedules in - which the parameter appears. Parameters that are not attached to a schedule will have None - in place of a schedule name. + Returns a dictionary mapping parameters managed by the calibrations to the schedules and + qubits using the parameters. The values of the dict are sets containing the names of the + schedules and qubits in which the parameter appears. Parameters that are not attached to + a schedule will have None in place of a schedule name. """ parameters = defaultdict(set) for key, param in self._parameter_map.items(): @@ -141,7 +145,7 @@ def add_parameter_value( """ 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. The parameters are stored and identified by name. + standard deviation. Args: value: The value of the parameter to add. @@ -241,6 +245,8 @@ def get_parameter_value( """ 1) Check if the given qubits have their own Parameter. 2) If they do not check to see if a parameter global to all qubits exists. + 3) Filter candidate parameter values. + 4) Return the most recent parameter. Args: param: The parameter or the name of the parameter for which to get the parameter value. @@ -264,8 +270,11 @@ def get_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule + # 1) Check for qubit specific parameters. if (sched_name, param_name, qubits) in self._params: candidates = self._params[(sched_name, param_name, qubits)] + + # 2) Check for default values. elif (sched_name, param_name, None) in self._params: candidates = self._params[(sched_name, param_name, None)] else: @@ -274,6 +283,7 @@ def get_parameter_value( f"and qubits {qubits}. No default value exists." ) + # 3) Filter candidate parameter values. if valid_only: candidates = [val for val in candidates if val.valid] @@ -295,6 +305,7 @@ def get_parameter_value( raise CalibrationError(msg) + # 4) Return the most recent parameter. candidates.sort(key=lambda x: x.date_time) return candidates[-1].value @@ -386,15 +397,15 @@ def get_circuit( def schedules(self) -> List[Dict[str, Any]]: """ - Return the schedules in self in a data frame to help + Return the schedules in self in a list of dictionaries to help users manage their schedules. Returns: data: A list of dictionaries with all the schedules in it. """ data = [] - for name, schedule in self._schedules.items(): - data.append({"name": name, "schedule": schedule, "parameters": schedule.parameters}) + for context, sched in self._schedules.items(): + data.append({"context": context, "schedule": sched, "parameters": sched.parameters}) return data diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 6eabdd88d8..88fb426023 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -52,7 +52,7 @@ def setUp(self): pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), self.drive) for sched in [xp, x90p, y90p, xm]: - self.cals.add_schedules(sched) + self.cals.add_schedule(sched) # Add some parameter values. self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -125,10 +125,10 @@ def test_channel_names(self): pulse.play(Drag(160, 0.1, 40, 2), drive_1) pulse.play(Drag(160, 0.1, 40, 2), control_bad) - self.cals.add_schedules(sched_good) + self.cals.add_schedule(sched_good) with self.assertRaises(CalibrationError): - self.cals.add_schedules(sched_bad) + self.cals.add_schedule(sched_bad) def test_unique_parameter_names(self): """Test that we cannot insert schedules in which parameter names are duplicates.""" @@ -136,7 +136,7 @@ def test_unique_parameter_names(self): pulse.play(Drag(160, Parameter("a"), Parameter("a"), Parameter("a")), DriveChannel(0)) with self.assertRaises(CalibrationError): - self.cals.add_schedules(sched) + self.cals.add_schedule(sched) def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" @@ -167,8 +167,8 @@ def setUp(self): pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) # Add the schedules - self.cals.add_schedules(xp) - self.cals.add_schedules(xp_drag, (3,)) + self.cals.add_schedule(xp) + self.cals.add_schedule(xp_drag, (3,)) def test_parameter_value_adding_and_filtering(self): """Test that adding parameter values behaves in the expected way.""" From 800d1d070adaac0750b74a077bd070b8e6bceba9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 19:04:44 +0200 Subject: [PATCH 035/178] * Improved methodology to get the values of parameters. --- .../calibration/calibrations.py | 76 ++++++++++++------- test/calibration/test_calibrations.py | 4 +- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 34fac35dfd..05db275851 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -54,6 +54,9 @@ def __init__(self, backend: Backend): # Dict of the form: (schedule.name, parameter.name, qubits): Parameter self._parameter_map = {} + # Reverse mapping of _parameter_map + self._parameter_map_r = defaultdict(set) + # Default dict of the form: (schedule.name, parameter.name, qubits): [ParameterValue, ...] self._params = defaultdict(list) @@ -114,7 +117,9 @@ def register_parameter( qubits: The qubits for which to register the parameter. """ sched_name = schedule.name if schedule else None - self._parameter_map[ParameterKey(sched_name, parameter.name, qubits)] = parameter + key = ParameterKey(sched_name, parameter.name, qubits) + self._parameter_map[key] = parameter + self._parameter_map_r[parameter].add(key) @property def parameters(self) -> Dict[Parameter, Set]: @@ -161,28 +166,7 @@ def add_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - # First look for a parameter that matches the given qubits. - if (sched_name, param_name, qubits) in self._parameter_map: - param = self._parameter_map[ParameterKey(sched_name, param_name, qubits)] - - # If no parameter was found look for a default parameter - else: - param = self._parameter_map[ParameterKey(sched_name, param_name, None)] - - if param is None: - raise CalibrationError( - f"No parameter found for parameter {param_name} in " - f"schedule {sched_name} and qubits {qubits}." - ) - - # Find all schedules that share this parameter - common_schedules = {ParameterKey(sched_name, param_name, qubits)} - for key in self._parameter_map.keys(): - if self._parameter_map[key] == param: - common_schedules.add(key) - - for key in common_schedules: - self._params[key].append(value) + self._params[ParameterKey(sched_name, param_name, qubits)].append(value) def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: int = 0) -> int: """ @@ -271,19 +255,53 @@ def get_parameter_value( sched_name = schedule.name if isinstance(schedule, Schedule) else schedule # 1) Check for qubit specific parameters. - if (sched_name, param_name, qubits) in self._params: - candidates = self._params[(sched_name, param_name, qubits)] + if (sched_name, param_name, qubits) in self._parameter_map: + param = self._parameter_map[(sched_name, param_name, qubits)] - # 2) Check for default values. - elif (sched_name, param_name, None) in self._params: - candidates = self._params[(sched_name, param_name, None)] + # 2) Check for default parameters. + elif (sched_name, param_name, None) in self._parameter_map: + param = self._parameter_map[(sched_name, param_name, None)] else: raise CalibrationError( f"No parameter for {param_name} and schedule {sched_name} " f"and qubits {qubits}. No default value exists." ) - # 3) Filter candidate parameter values. + # 3) 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.schedule, key.parameter, qubits)) + + # 4) Loop though the candidate keys + candidates = [] + parameter_not_found = True + for key in candidate_keys: + if key in self._params: + if parameter_not_found: + candidates = self._params[key] + parameter_not_found = False + else: + raise CalibrationError(f"Duplicate parameters.") + + # 5) If not candidate parameter values were found look for default parameters + # i.e. parameters that do not specify a qubit. + if len(candidates) == 0: + candidate_default_keys = [] + + for key in candidate_keys: + candidate_default_keys.append(ParameterKey(key.schedule, key.parameter, None)) + + candidate_default_keys = set(candidate_default_keys) + + for key in set(candidate_default_keys): + if key in self._params: + if parameter_not_found: + candidates = self._params[key] + parameter_not_found = False + else: + raise CalibrationError(f"Duplicate parameters.") + + # 6) Filter candidate parameter values. if valid_only: candidates = [val for val in candidates if val.valid] diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 88fb426023..06bf51a5ed 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -182,8 +182,8 @@ def test_parameter_value_adding_and_filtering(self): self.assertEqual(len(self.cals.parameters_table()), 1) # Check that we can get a default parameter in the parameter table - self.assertEqual(len(self.cals.parameters_table(parameters=["σ"])), 2) - self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xp"])), 2) + self.assertEqual(len(self.cals.parameters_table(parameters=["σ"])), 1) + self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xp"])), 1) self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xm"])), 0) # Test behaviour of qubit-specific parameter From 2af8cb0f9ef8f2d497da87a859b051f4168f3964 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 21 Apr 2021 12:54:41 +0200 Subject: [PATCH 036/178] * Fixed lint. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 05db275851..94a473032a 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -281,7 +281,7 @@ def get_parameter_value( candidates = self._params[key] parameter_not_found = False else: - raise CalibrationError(f"Duplicate parameters.") + raise CalibrationError("Duplicate parameters.") # 5) If not candidate parameter values were found look for default parameters # i.e. parameters that do not specify a qubit. @@ -299,7 +299,7 @@ def get_parameter_value( candidates = self._params[key] parameter_not_found = False else: - raise CalibrationError(f"Duplicate parameters.") + raise CalibrationError("Duplicate parameters.") # 6) Filter candidate parameter values. if valid_only: @@ -458,7 +458,7 @@ def parameters_table( # Look for exact matches. Default values will be ignored. keys = set() - for key, param in self._params.items(): + for key in self._params.keys(): if parameters and key.parameter not in parameters: continue if schedules and key.schedule not in schedules: From e75df849083852c292e6b7778fad3e9bd2cdbe40 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 21 Apr 2021 13:05:18 +0200 Subject: [PATCH 037/178] * Removed the backend object and added a config for the controls. --- qiskit_experiments/calibration/calibrations.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 94a473032a..3d635b3797 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -17,7 +17,6 @@ from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional -from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Gate from qiskit import QuantumCircuit from qiskit.pulse import Schedule, DriveChannel, ControlChannel, MeasureChannel @@ -39,17 +38,17 @@ class Calibrations: - allows default schedules for qubits that can be overridden of specific qubits. """ - def __init__(self, backend: Backend): + def __init__(self, control_config: Dict[Tuple[int], List[ControlChannel]] = None): """ Initialize the instructions from a given backend. Args: - backend: The backend from which to get the configuration. + control_config: A configuration dictionary of any control channels. The + keys are tuples of qubits and the values are a list of ControlChannels + that correspond to the qubits in the keys. """ - self._n_qubits = backend.configuration().num_qubits - self._n_uchannels = backend.configuration().n_uchannels - self._config = backend.configuration() + self._controls_config = control_config if control_config else {} # Dict of the form: (schedule.name, parameter.name, qubits): Parameter self._parameter_map = {} @@ -204,7 +203,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: i if isinstance(chan, ControlChannel): indices = [int(sub_channel) for sub_channel in chan.index.name.split(".")] ch_qubits = tuple(qubits[index] for index in indices) - chs_ = self._config.control(ch_qubits) + chs_ = self._controls_config[ch_qubits] if len(chs_) < control_index: raise CalibrationError( From 4461153e5fb4da56d11504b082621b62b6a1b331 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 21 Apr 2021 14:46:01 +0200 Subject: [PATCH 038/178] * Extended the functionality of the channel naming convention. * Start a cross-resonance pulse schedule test. --- .../calibration/calibrations.py | 51 +++++++++---- test/calibration/test_calibrations.py | 71 ++++++++++++++++--- 2 files changed, 97 insertions(+), 25 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 3d635b3797..f07bb238ac 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -13,6 +13,7 @@ """Class to store and manage the results of a calibration experiments.""" import dataclasses +import regex as re from collections import namedtuple, defaultdict from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional @@ -36,9 +37,20 @@ class Calibrations: dict where parameters are keys. This class supports: - having different schedules share parameters - allows default schedules for qubits that can be overridden of specific qubits. + + Parametric channel naming convention. Channels must be name according to a predefined + pattern so that self can resolve the channels and control channels when assigning + values to the parametric channel indices. This is pattern is "^ch\d[.\d]*\${0,1}[\d]*$", + examples of which include "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". + The "." delimiter is used to specify the different qubits when looking for control + channels. The optional $ delimiter is used to specify which control channel to use + if several control channels work together on the same qubits. For example, if the + control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} + then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while + "ch1.0$0" will resolve to ControlChannel(3). """ - def __init__(self, control_config: Dict[Tuple[int], List[ControlChannel]] = None): + def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = None): """ Initialize the instructions from a given backend. @@ -61,6 +73,8 @@ def __init__(self, control_config: Dict[Tuple[int], List[ControlChannel]] = None self._schedules = {} + self._channel_pattern = "^ch\d[.\d]*\${0,1}[\d]*$" + def add_schedule(self, schedule: Schedule, qubits: Tuple = None): """ Add a schedule and register its parameters. @@ -81,12 +95,9 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): for ch in schedule.channels: if isinstance(ch.index, Parameter): param_indices.add(ch.index) - try: - [int(index) for index in ch.index.name.split(".")] - except ValueError: + if re.compile(self._channel_pattern).match(ch.index.name) is None: raise CalibrationError( - "Parameterized channel must have a name " - "formatted following index1.index2..." + f"Parameterized channel must correspond to {self._channel_pattern}" ) self._schedules[(schedule.name, qubits)] = schedule @@ -167,22 +178,21 @@ def add_parameter_value( self._params[ParameterKey(sched_name, param_name, qubits)].append(value) - def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: int = 0) -> int: + def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ Get the index of the parameterized channel based on 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 qubit_index1.qubit_index2... . - For example, the following parameter names are valid: '1', '1.0', '30.12'. + For example, the following parameter names are valid: 'ch1', 'ch1.0', 'ch30.12'. Args: qubits: The qubits for which we want to obtain the channel index. chan: The channel with a parameterized name. - control_index: An index used to specify which control channel to use if a given pair of qubits has more than one control channel. Returns: index: The index of the channel. For example, if qubits=(10, 32) and - chan is a control channel with parameterized index name '1.0' + 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). @@ -193,18 +203,31 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel, control_index: i a DriveChannel, MeasureChannel, or ControlChannel. """ + print(chan.index.name, qubits, chan) + if isinstance(chan.index, Parameter): if isinstance(chan, (DriveChannel, MeasureChannel)): - if len(qubits) != 1: - raise CalibrationError(f"Too many qubits given for {chan.__class__.__name__}.") + index = int(chan.index.name[2:].replace("ch", "").split("$")[0]) + + if len(qubits) < index: + raise CalibrationError(f"Not enough qubits given for channel {chan}.") - return qubits[0] + return qubits[index] + # Control channels name example ch1.0$1 if isinstance(chan, ControlChannel): - indices = [int(sub_channel) for sub_channel in chan.index.name.split(".")] + + 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}." diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 06bf51a5ed..9fc3b0cec9 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -14,8 +14,7 @@ from datetime import datetime from qiskit.circuit import Parameter -from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian -from qiskit.test.mock import FakeAlmaden +from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare import qiskit.pulse as pulse from qiskit.test import QiskitTestCase from qiskit_experiments.calibration.calibrations import Calibrations @@ -28,15 +27,14 @@ class TestCalibrationsBasic(QiskitTestCase): def setUp(self): """Setup a test environment.""" - backend = FakeAlmaden() - self.cals = Calibrations(backend) + self.cals = Calibrations() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") self.amp_x90p = Parameter("amp") self.amp_y90p = Parameter("amp") self.beta = Parameter("β") - self.drive = DriveChannel(Parameter("0")) + self.drive = DriveChannel(Parameter("ch0")) # Define and add template schedules. with pulse.build(name="xp") as xp: @@ -110,10 +108,10 @@ def test_get_value(self): def test_channel_names(self): """Check the naming of parametric control channels index1.index2.index3...""" - drive_0 = DriveChannel(Parameter("0")) - drive_1 = DriveChannel(Parameter("1")) + drive_0 = DriveChannel(Parameter("ch0")) + drive_1 = DriveChannel(Parameter("ch1")) control_bad = ControlChannel(Parameter("u_chan")) - control_good = ControlChannel(Parameter("1.0")) + control_good = ControlChannel(Parameter("ch1.0")) with pulse.build() as sched_good: pulse.play(Drag(160, 0.1, 40, 2), drive_0) @@ -148,14 +146,13 @@ class TestCalibrationDefaults(QiskitTestCase): def setUp(self): """Setup a few parameters.""" - backend = FakeAlmaden() - self.cals = Calibrations(backend) + self.cals = Calibrations() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") self.amp = Parameter("amp") self.beta = Parameter("β") - self.drive = DriveChannel(Parameter("0")) + self.drive = DriveChannel(Parameter("ch0")) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") # Template schedule for qubit 3 @@ -275,3 +272,55 @@ def test_parameter_filtering(self): # Check to see if we get back the two qubits when explicitly specifying them. amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,), (0,)]) self.assertEqual(len(amp_values), 2) + + +class TestControlChannels(QiskitTestCase): + """Test more complex schedules such as an echoed cross-resonance.""" + + def setUp(self): + """Create the setup we will deal with.""" + controls = {(3, 2): [ControlChannel(10), ControlChannel(123)], + (2, 3): [ControlChannel(15), ControlChannel(23)]} + self.cals = Calibrations(control_config=controls) + + self.amp_cr = Parameter("amp_cr") + self.amp_rot = Parameter("amp_rot") + self.amp = Parameter("amp") + self.d0_ = DriveChannel(Parameter('ch0')) + self.d1_ = DriveChannel(Parameter('ch1')) + self.c1_ = ControlChannel(Parameter('ch1.0')) + self.sigma = Parameter("σ") + self.width = Parameter("w") + + gaus_square = GaussianSquare(640, self.amp, self.sigma, self.width) + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, self.amp, self.sigma), self.d0_) + + with pulse.build(name="cr") as cr: + with pulse.align_sequential(): + with pulse.align_left(): + pulse.play(gaus_square, self.d1_) # Rotary tone + pulse.play(gaus_square, self.c1_) # CR tone. + with pulse.align_sequential(): + pulse.call(xp) + with pulse.align_left(): + pulse.play(gaus_square, self.d1_) + pulse.play(gaus_square, self.c1_) + with pulse.align_sequential(): + pulse.call(xp) + + self.cals.add_schedule(xp) + self.cals.add_schedule(cr) + + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3, ), "xp") + self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp_cr", (3, 2), "cr") + self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") + + def test_get_schedule(self): + """Check that we can get a CR schedule.""" + + self.cals.get_schedule("cr", (3, 2)) From f2d680ec4c19deb46485fac482dfc70ddecb7f43 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 22 Apr 2021 19:30:39 +0200 Subject: [PATCH 039/178] * Added methodology to deal with Call schedule and cross-resonance schedules. Note that frames are not supported here yet. --- .../calibration/calibrations.py | 124 ++++++++++++++++-- test/calibration/test_calibrations.py | 55 ++++++-- 2 files changed, 155 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index f07bb238ac..ff452fc26e 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -20,9 +20,21 @@ from qiskit.circuit import Gate from qiskit import QuantumCircuit -from qiskit.pulse import Schedule, DriveChannel, ControlChannel, MeasureChannel +from qiskit.pulse import ( + Schedule, + DriveChannel, + ControlChannel, + MeasureChannel, + Call, + Play, + ShiftPhase, + SetPhase, + ShiftFrequency, + SetFrequency, +) from qiskit.pulse.channels import PulseChannel -from qiskit.circuit import Parameter +from qiskit.pulse.transforms import inline_subroutines +from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue @@ -202,9 +214,6 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: number of inferred ControlChannels is not correct, or if ch is not a DriveChannel, MeasureChannel, or ControlChannel. """ - - print(chan.index.name, qubits, chan) - if isinstance(chan.index, Parameter): if isinstance(chan, (DriveChannel, MeasureChannel)): index = int(chan.index.name[2:].replace("ch", "").split("$")[0]) @@ -372,7 +381,8 @@ def get_schedule( except for those specified by free_params. Raises: - CalibrationError: if the name of the schedule is not known. + CalibrationError: if the name of the schedule is not known or if a parameter could + not be found. """ if (name, qubits) in self._schedules: schedule = self._schedules[(name, qubits)] @@ -387,18 +397,104 @@ def get_schedule( if ch.is_parameterized(): binding_dict[ch.index] = self._get_channel_index(qubits, ch) - # Loop through the remaining parameters in the schedule, get their values and bind. - if free_params is None: - free_params = [] + # Binding the channel indices makes it easier to deal with parameters later on + schedule = schedule.assign_parameters(binding_dict, inplace=False) - for param in schedule.parameters: - if param.name not in free_params and param not in binding_dict: - binding_dict[param] = self.get_parameter_value( - param.name, qubits, name, group=group - ) + # The following code allows us to get the keys when the schedule has call instructions. + # We cannot inline the subroutines yet because we would lose the name of the subroutines. + parameter_keys = Calibrations.get_parameter_keys(schedule, set(), binding_dict, qubits) + + # Now that we have the parameter keys we must inline all call subroutines. + schedule = inline_subroutines(schedule) + + # Build the parameter binding dictionary. + free_params = free_params if free_params else [] + + for key in parameter_keys: + if key.parameter not in free_params: + # 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 None. + if key in self._parameter_map: + param = self._parameter_map[key] + elif (key.schedule, key.parameter, None) in self._parameter_map: + param = self._parameter_map[(key.schedule, key.parameter, None)] + else: + raise CalibrationError( + f"Ill configured calibrations {key} is not present and has not default value." + ) + + if param not in binding_dict: + binding_dict[param] = self.get_parameter_value( + key.parameter, key.qubits, key.schedule, group=group + ) return schedule.assign_parameters(binding_dict, inplace=False) + @staticmethod + def get_parameter_keys(schedule: Schedule, keys: Set, binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...]): + """ + Recursive function to extract parameter keys from a schedule. The recursive + behaviour is needed to handle Call instructions. Each time a Call is found + get_parameter_keys is call on the subroutine of the Call instruction and the + qubits that are in the subroutine. This also implies carefully extracting the + qubits from the subroutine and in the appropriate order. + + Args: + schedule: A schedule from which to extract parameters. + keys: A set of keys that will be populated. + binding_dict: A binding dictionary intended only for channels. This is needed + because calling assign_parameters on a schedule with a Call instruction will + not assign the parameters in the subroutine of the Call instruction. + qubits: The qubits for which we want to have the schedule. + + Returns: + keys: The set of keys populated with schedule name, parameter name, qubits. + """ + + # schedule.channels may give the qubits in any order. This order matters. For example, + # the parameter ('cr', 'amp', (2, 3)) is not the same as ('cr', 'amp', (3, 2)). + # Furthermore, as we call subroutines the list of qubits involved might shrink. For + # example, part of a cross-resonance schedule might involve. + # + # pulse.call(xp) + # ... + # pulse.play(GaussianSquare(...), ControlChannel(X)) + # + # Here, the call instruction might, e.g., only involve qubit 2 while the play instruction + # will apply to qubits (2, 3). + + qubit_set = set() + for chan in schedule.channels: + if isinstance(chan, DriveChannel): + qubit_set.add(chan.index) + + qubits_ = tuple([qubit for qubit in qubits if qubit in qubit_set]) + + for _, inst in schedule.instructions: + + if isinstance(inst, Play): + for params in inst.pulse.parameters.values(): + if isinstance(params, ParameterExpression): + for param in params.parameters: + keys.add(ParameterKey(schedule.name, param.name, qubits_)) + + if isinstance(inst, (ShiftPhase, SetPhase)): + if isinstance(inst.phase, ParameterExpression): + for param in inst.phase.parameters: + keys.add(ParameterKey(schedule.name, param.name, (inst.channel.index,))) + + if isinstance(inst, (ShiftFrequency, SetFrequency)): + if isinstance(inst.frequency, ParameterExpression): + for param in inst.frequency.parameters: + keys.add(ParameterKey(schedule.name, param.name, (inst.channel.index,))) + + if isinstance(inst, Call): + sched_ = inst.subroutine.assign_parameters(binding_dict, inplace=False) + keys = Calibrations.get_parameter_keys(sched_, keys, binding_dict, qubits_) + + return keys + def get_circuit( self, schedule_name: str, diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9fc3b0cec9..11264d27a1 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -288,11 +288,13 @@ def setUp(self): self.amp = Parameter("amp") self.d0_ = DriveChannel(Parameter('ch0')) self.d1_ = DriveChannel(Parameter('ch1')) - self.c1_ = ControlChannel(Parameter('ch1.0')) + self.c1_ = ControlChannel(Parameter('ch0.1')) self.sigma = Parameter("σ") self.width = Parameter("w") + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - gaus_square = GaussianSquare(640, self.amp, self.sigma, self.width) + cr_tone = GaussianSquare(640, self.amp_cr, self.sigma, self.width) + rotary = GaussianSquare(640, self.amp_rot, self.sigma, self.width) with pulse.build(name="xp") as xp: pulse.play(Gaussian(160, self.amp, self.sigma), self.d0_) @@ -300,27 +302,60 @@ def setUp(self): with pulse.build(name="cr") as cr: with pulse.align_sequential(): with pulse.align_left(): - pulse.play(gaus_square, self.d1_) # Rotary tone - pulse.play(gaus_square, self.c1_) # CR tone. + pulse.play(rotary, self.d1_) # Rotary tone + pulse.play(cr_tone, self.c1_) # CR tone. with pulse.align_sequential(): pulse.call(xp) with pulse.align_left(): - pulse.play(gaus_square, self.d1_) - pulse.play(gaus_square, self.c1_) + pulse.play(rotary, self.d1_) + pulse.play(cr_tone, self.c1_) with pulse.align_sequential(): pulse.call(xp) self.cals.add_schedule(xp) self.cals.add_schedule(cr) - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3, ), "xp") self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp_cr", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") + self.cals.add_parameter_value(ParameterValue(20, self.date_time), "w", (3, 2), "cr") + + # Reverse gate parameters + self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (2,), "xp") + self.cals.add_parameter_value(ParameterValue(0.5, self.date_time), "amp_cr", (2, 3), "cr") + self.cals.add_parameter_value(ParameterValue(0.4, self.date_time), "amp_rot", (2, 3), "cr") + self.cals.add_parameter_value(ParameterValue(30, self.date_time), "w", (2, 3), "cr") def test_get_schedule(self): - """Check that we can get a CR schedule.""" + """Check that we can get a CR schedule with a built in Call.""" + + with pulse.build(name="cr") as cr_32: + with pulse.align_sequential(): + with pulse.align_left(): + pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone + pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. + with pulse.align_sequential(): + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + with pulse.align_left(): + pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone + pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. + with pulse.align_sequential(): + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + + self.assertTrue(self.cals.get_schedule("cr", (3, 2)) == cr_32) + + with pulse.build(name="cr") as cr_23: + with pulse.align_sequential(): + with pulse.align_left(): + pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone + pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. + with pulse.align_sequential(): + pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) + with pulse.align_left(): + pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone + pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. + with pulse.align_sequential(): + pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) - self.cals.get_schedule("cr", (3, 2)) + self.assertTrue(self.cals.get_schedule("cr", (2, 3)) == cr_23) From 6019b78cd6ac5344dc3fe37741f7b678b28b56b0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 22 Apr 2021 20:56:22 +0200 Subject: [PATCH 040/178] * Added first stab of the method to export the calibrations to a csv. * Calibrations.parameters now returns self._parameter_map_r. --- .../calibration/calibrations.py | 71 ++++++++++++++----- test/calibration/test_calibrations.py | 23 ++++-- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index ff452fc26e..e1a4ab7b22 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -12,6 +12,7 @@ """Class to store and manage the results of a calibration experiments.""" +import csv import dataclasses import regex as re from collections import namedtuple, defaultdict @@ -144,23 +145,14 @@ def register_parameter( self._parameter_map_r[parameter].add(key) @property - def parameters(self) -> Dict[Parameter, Set]: + def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: """ Returns a dictionary mapping parameters managed by the calibrations to the schedules and - qubits using the parameters. The values of the dict are sets containing the names of the - schedules and qubits in which the parameter appears. Parameters that are not attached to - a schedule will have None in place of a schedule name. + qubits and parameter names using the parameters. The values of the dict are sets containing + the parameter keys. Parameters that are not attached to a schedule will have None in place + of a schedule name. """ - parameters = defaultdict(set) - for key, param in self._parameter_map.items(): - schedule_name = key.schedule - - if key.qubits: - parameters[param].add((schedule_name, key.qubits)) - else: - parameters[param].add((schedule_name,)) - - return parameters + return self._parameter_map_r def add_parameter_value( self, @@ -597,14 +589,57 @@ def parameters_table( return data - def to_db(self): + def to_csv(self): """ Serializes the parameterized schedules and parameter values so - that they can be sent and stored in an external DB. + that they can be stored in csv file. """ - raise NotImplementedError - def from_db(self): + # Write the parameter configuration. + header_keys = ["parameter.name", "hash(parameter)", "schedule", "qubits"] + body = [] + + for parameter, keys in self.parameters.items(): + for key in keys: + body.append({ + "parameter.name": parameter.name, + "hash(parameter)": hash(parameter), + "schedule": key.schedule, + "qubits": key.qubits + }) + + with open('parameter_config.csv', 'w', newline='') 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() + if len(values) > 0: + header_keys = values[0].keys() + + with open('parameter_values.csv', 'w', newline='') 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[0], + "qubits": key[1], + "schedule": str(sched) + }) + + with open('schedules.csv', 'w', newline='') as output_file: + dict_writer = csv.DictWriter(output_file, header_keys) + dict_writer.writeheader() + dict_writer.writerows(schedules) + + @classmethod + def from_csv(cls): """ Retrieves the parameterized schedules and pulse parameters from an external DB. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 11264d27a1..8ee691eb41 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -17,7 +17,7 @@ from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare import qiskit.pulse as pulse from qiskit.test import QiskitTestCase -from qiskit_experiments.calibration.calibrations import Calibrations +from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.calibration.exceptions import CalibrationError @@ -63,12 +63,25 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" - self.assertEqual(self.cals.parameters[self.amp_xp], {("xp",), ("xm",)}) - self.assertEqual(self.cals.parameters[self.amp_x90p], {("x90p",)}) - self.assertEqual(self.cals.parameters[self.amp_y90p], {("y90p",)}) + expected = {ParameterKey("xp", "amp", None), ParameterKey("xm", "amp", None)} + self.assertEqual(self.cals.parameters[self.amp_xp], expected) - expected = {("xp",), ("xm",), ("x90p",), ("y90p",)} + expected = {ParameterKey("x90p", "amp", None)} + self.assertEqual(self.cals.parameters[self.amp_x90p], expected) + + expected = {ParameterKey("y90p", "amp", None)} + self.assertEqual(self.cals.parameters[self.amp_y90p], expected) + + expected = {ParameterKey("xp", "β", None), + ParameterKey("xm", "β", None), + ParameterKey("x90p", "β", None), + ParameterKey("y90p", "β", None)} self.assertEqual(self.cals.parameters[self.beta], expected) + + expected = {ParameterKey("xp", "σ", None), + ParameterKey("xm", "σ", None), + ParameterKey("x90p", "σ", None), + ParameterKey("y90p", "σ", None)} self.assertEqual(self.cals.parameters[self.sigma], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) From 0a01a7a2697b14163b798d9cd12cb3efb78ba0f2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 14:53:57 +0200 Subject: [PATCH 041/178] * Added the class BackendCalibrations to manage configurations in the context of a backend. * Lint and Black. --- .../calibration/BackendCalibrations.py | 140 ++++++++++++++++++ .../calibration/calibrations.py | 44 +++--- 2 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 qiskit_experiments/calibration/BackendCalibrations.py diff --git a/qiskit_experiments/calibration/BackendCalibrations.py b/qiskit_experiments/calibration/BackendCalibrations.py new file mode 100644 index 0000000000..e08cde2a65 --- /dev/null +++ b/qiskit_experiments/calibration/BackendCalibrations.py @@ -0,0 +1,140 @@ +# 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. + +"""Store and manage the results of a calibration experiments in the context of a backend.""" + +from datetime import datetime +from typing import Any, Dict, List +import copy + +from qiskit.providers.ibmq.ibmqbackend import IBMQBackend as Backend +from qiskit.circuit import Parameter +from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey + + +class BackendCalibrations(Calibrations): + """ + A Calibrations class to enable a seamless interplay with backend objects. + This class enables users to export their calibrations into a backend object. + Additionally, it creates frequency parameters for qubits and readout resonators. + The parameters are named `qubit_freq_est` and `meas_freq_est` to be consistent + with the naming in backend.defaults(). These two parameters are not attached to + any schedule. + """ + + def __init__(self, backend: Backend): + """Setup an instance to manage the calibrations of a backend.""" + super().__init__(backend.configuration()._control_channels) + + # Use the same naming convention as in backend.defaults() + self.qubit_freq = Parameter("qubit_freq_est") + self.meas_freq = Parameter("meas_freq_est") + self.register_parameter(self.qubit_freq) + self.register_parameter(self.meas_freq) + + self._qubits = set(range(backend.configuration().n_qubits)) + self._backend = backend + + def _get_frequencies( + self, + meas_freq: bool, + valid_only: bool = True, + group: str = "default", + cutoff_date: datetime = None, + ) -> List[float]: + """ + Get the most recent qubit or measurement frequencies. These frequencies can be + passed to the run-time options of :class:`BaseExperiment`. If no calibrated value + for the frequency of a qubit is found then the default value from the backend + defaults is used. + + Args: + meas_freq: If True return the measurement frequencies otherwise return the qubit + frequencies. + valid_only: Use only valid parameter values. + group: The calibration group from which to draw the + parameters. If not specifies 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: + A List of qubit or measurement frequencies for all qubits of the backend. + """ + + param = self.meas_freq.name if meas_freq else self.qubit_freq.name + + freqs = [] + for qubit in self._qubits: + if ParameterKey(None, param, (qubit,)) in self._params: + freq = self.get_parameter_value(param, qubit, None, valid_only, group, cutoff_date) + else: + if meas_freq: + freq = self._backend.defaults().meas_freq_est[qubit] + else: + freq = self._backend.defaults().qubit_freq_est[qubit] + + freqs.append(freq) + + return freqs + + def run_options( + self, + valid_only: bool = True, + group: str = "default", + cutoff_date: datetime = None, + ) -> Dict[str, Any]: + """ + Retrieve all run-options to be used as kwargs when calling + :meth:`BaseExperiment.run`. This gives us the means to communicate the most recent + measured values of the qubit and measurement frequencies of the backend. + + Args: + valid_only: Use only valid parameter values. + group: The calibration group from which to draw the + parameters. If not specifies 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: + key word arguments containing: `qubit_lo_freq` and `meas_lo_freq` intended to be + passed as arguments to assemble. + """ + + return { + "qubit_lo_freq": self._get_frequencies(False, valid_only, group, cutoff_date), + "meas_lo_freq": self._get_frequencies(True, valid_only, group, cutoff_date), + } + + def export_backend(self) -> Backend: + """ + Exports the calibrations in the backend object that can be used. + + Returns: + calibrated backend: A backend with the calibrations in it. + """ + backend = copy.deepcopy(self._backend) + + backend.defaults().qubit_freq_est = self._get_frequencies(False) + backend.defaults().meas_freq_est = self._get_frequencies(True) + + # TODO: build the instruction schedule map using the stored calibrations + + return backend + + @classmethod + def from_csv(cls): + """Create an instance from csv files""" + raise NotImplementedError diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index e1a4ab7b22..fb978e826d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -12,12 +12,12 @@ """Class to store and manage the results of a calibration experiments.""" -import csv -import dataclasses -import regex as re from collections import namedtuple, defaultdict from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional +import csv +import dataclasses +import regex as re from qiskit.circuit import Gate from qiskit import QuantumCircuit @@ -43,7 +43,7 @@ class Calibrations: - """ + r""" A class to manage schedules with calibrated parameter values. Schedules are stored in a dict and are intended to be fully parameterized, including the index of the channels. The parameter values are stored in a @@ -86,7 +86,7 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._schedules = {} - self._channel_pattern = "^ch\d[.\d]*\${0,1}[\d]*$" + self._channel_pattern = r"^ch\d[.\d]*\${0,1}[\d]*$" def add_schedule(self, schedule: Schedule, qubits: Tuple = None): """ @@ -424,7 +424,9 @@ def get_schedule( return schedule.assign_parameters(binding_dict, inplace=False) @staticmethod - def get_parameter_keys(schedule: Schedule, keys: Set, binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...]): + def get_parameter_keys( + schedule: Schedule, keys: Set, binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...] + ): """ Recursive function to extract parameter keys from a schedule. The recursive behaviour is needed to handle Call instructions. Each time a Call is found @@ -461,7 +463,7 @@ def get_parameter_keys(schedule: Schedule, keys: Set, binding_dict: Dict[Paramet if isinstance(chan, DriveChannel): qubit_set.add(chan.index) - qubits_ = tuple([qubit for qubit in qubits if qubit in qubit_set]) + qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) for _, inst in schedule.instructions: @@ -601,14 +603,16 @@ def to_csv(self): for parameter, keys in self.parameters.items(): for key in keys: - body.append({ - "parameter.name": parameter.name, - "hash(parameter)": hash(parameter), - "schedule": key.schedule, - "qubits": key.qubits - }) - - with open('parameter_config.csv', 'w', newline='') as output_file: + body.append( + { + "parameter.name": parameter.name, + "hash(parameter)": hash(parameter), + "schedule": key.schedule, + "qubits": key.qubits, + } + ) + + with open("parameter_config.csv", "w", newline="") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(body) @@ -618,7 +622,7 @@ def to_csv(self): if len(values) > 0: header_keys = values[0].keys() - with open('parameter_values.csv', 'w', newline='') as output_file: + with open("parameter_values.csv", "w", newline="") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(values) @@ -627,13 +631,9 @@ def to_csv(self): schedules = [] header_keys = ["name", "qubits", "schedule"] for key, sched in self._schedules.items(): - schedules.append({ - "name": key[0], - "qubits": key[1], - "schedule": str(sched) - }) + schedules.append({"name": key[0], "qubits": key[1], "schedule": str(sched)}) - with open('schedules.csv', 'w', newline='') as output_file: + with open("schedules.csv", "w", newline="") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(schedules) From 509aa44db587c01e529dbcfc1befba8630eabc3b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 14:56:26 +0200 Subject: [PATCH 042/178] * Renamed file and added BackendCalibrations to __init__ --- qiskit_experiments/calibration/__init__.py | 1 + .../{BackendCalibrations.py => backend_calibrations.py} | 0 2 files changed, 1 insertion(+) rename qiskit_experiments/calibration/{BackendCalibrations.py => backend_calibrations.py} (100%) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index d8c12e8864..8efc0d6331 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -13,5 +13,6 @@ """Qiskit Experiments Calibration Root.""" from .calibrations import Calibrations +from .backend_calibrations import BackendCalibrations from .exceptions import CalibrationError from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/BackendCalibrations.py b/qiskit_experiments/calibration/backend_calibrations.py similarity index 100% rename from qiskit_experiments/calibration/BackendCalibrations.py rename to qiskit_experiments/calibration/backend_calibrations.py From 908501883a32b86f5c27b159bcc2660ba58869bd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 15:06:01 +0200 Subject: [PATCH 043/178] * Added test for BackendCalibrations. --- .../calibration/backend_calibrations.py | 5 ++-- test/calibration/test_backend_calibrations.py | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 test/calibration/test_backend_calibrations.py diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index e08cde2a65..56dbbb844f 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -13,10 +13,11 @@ """Store and manage the results of a calibration experiments in the context of a backend.""" from datetime import datetime -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import copy from qiskit.providers.ibmq.ibmqbackend import IBMQBackend as Backend +from qiskit.providers import BaseBackend from qiskit.circuit import Parameter from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey @@ -31,7 +32,7 @@ class BackendCalibrations(Calibrations): any schedule. """ - def __init__(self, backend: Backend): + def __init__(self, backend: Union[Backend, BaseBackend]): """Setup an instance to manage the calibrations of a backend.""" super().__init__(backend.configuration()._control_channels) diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py new file mode 100644 index 0000000000..eaf1887e1d --- /dev/null +++ b/test/calibration/test_backend_calibrations.py @@ -0,0 +1,28 @@ +# 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.""" + +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeArmonk +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations, ParameterKey + + +class TestBackendCalibrations(QiskitTestCase): + """Class to test the functionality of a BackendCalibrations""" + + def test_run_options(self): + """Test that we can get run options.""" + cals = BackendCalibrations(FakeArmonk()) + + expected = {'qubit_lo_freq': [4971852852.405576], 'meas_lo_freq': [6993370669.000001]} + self.assertEqual(cals.run_options(), expected) From d014b1747fd61f81c6f74304b0eb887e86db227f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 15:15:13 +0200 Subject: [PATCH 044/178] * Added a check when adding parameter values to ensure that the schedule exists. --- qiskit_experiments/calibration/calibrations.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index fb978e826d..fd1b540ef0 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -173,13 +173,17 @@ def add_parameter_value( schedule: The schedule or its name for which to add the measured parameter value. Raises: - CalibrationError: if ch_type is not given when chs are None, if the - channel type is not a ControlChannel, DriveChannel, or MeasureChannel, or - if the parameter name is not already in self. + CalibrationError: If the schedule name is given but no schedule with that name + exists. """ param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule + registered_schedules = set(key[0] for key in self._schedules.keys()) + + if sched_name and sched_name not in registered_schedules: + raise CalibrationError(f"Schedule named {sched_name} was never registered.") + self._params[ParameterKey(sched_name, param_name, qubits)].append(value) def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: From 490ad6339d6c29368dab57e70a672ba6084c7de8 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 17:36:27 +0200 Subject: [PATCH 045/178] * Fixed small issue with _get_frequencies. --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 56dbbb844f..f52d7808f7 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -78,7 +78,7 @@ 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, valid_only, group, cutoff_date) + freq = self.get_parameter_value(param, (qubit, ), None, valid_only, group, cutoff_date) else: if meas_freq: freq = self._backend.defaults().meas_freq_est[qubit] From fa27ef508b0a8d989df8307cd7de30ffb5b78c63 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 17:37:53 +0200 Subject: [PATCH 046/178] * Made register_parameter private. --- qiskit_experiments/calibration/backend_calibrations.py | 4 ++-- qiskit_experiments/calibration/calibrations.py | 4 ++-- test/calibration/test_calibrations.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index f52d7808f7..ddcd7bc0d2 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -39,8 +39,8 @@ def __init__(self, backend: Union[Backend, BaseBackend]): # Use the same naming convention as in backend.defaults() self.qubit_freq = Parameter("qubit_freq_est") self.meas_freq = Parameter("meas_freq_est") - self.register_parameter(self.qubit_freq) - self.register_parameter(self.meas_freq) + self._register_parameter(self.qubit_freq) + self._register_parameter(self.meas_freq) self._qubits = set(range(backend.configuration().n_qubits)) self._backend = backend diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index fd1b540ef0..98fa9651da 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -123,9 +123,9 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): # Register parameters that are not indices. for param in schedule.parameters: if param not in param_indices: - self.register_parameter(param, schedule, qubits) + self._register_parameter(param, schedule, qubits) - def register_parameter( + def _register_parameter( self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None ): """ diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 8ee691eb41..b2bc9d8bc0 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -151,7 +151,7 @@ def test_unique_parameter_names(self): def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" - self.cals.register_parameter(Parameter("a")) + self.cals._register_parameter(Parameter("a")) class TestCalibrationDefaults(QiskitTestCase): From b31429490e8198cc80732b94b661bffd9dcd33e0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 17:45:03 +0200 Subject: [PATCH 047/178] * Made add_parameter_value accept Union[int, float, complex, ParameterValue]. --- qiskit_experiments/calibration/calibrations.py | 9 +++++++-- test/calibration/test_calibrations.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 98fa9651da..7d0058b9d8 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -156,7 +156,7 @@ def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: def add_parameter_value( self, - value: ParameterValue, + value: Union[int, float, complex, ParameterValue], param: Union[Parameter, str], qubits: Tuple[int, ...] = None, schedule: Union[Schedule, str] = None, @@ -167,7 +167,9 @@ def add_parameter_value( standard deviation. Args: - value: The value of the parameter to add. + value: The value of the parameter to add. If an int, float, or complex is given + then the timestamp of the parameter will automatically be generated to + correspond to the current time. 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. @@ -176,6 +178,9 @@ def add_parameter_value( CalibrationError: If the schedule name is given but no schedule with that name exists. """ + if isinstance(value, (int, float, complex)): + value = ParameterValue(value, datetime.now()) + param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index b2bc9d8bc0..dd002de55a 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -196,9 +196,9 @@ def test_parameter_value_adding_and_filtering(self): self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xp"])), 1) self.assertEqual(len(self.cals.parameters_table(parameters=["σ"], schedules=["xm"])), 0) - # Test behaviour of qubit-specific parameter - self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") + # Test behaviour of qubit-specific parameter and without ParameterValue. + self.cals.add_parameter_value(0.25, "amp", (3,), "xp") + self.cals.add_parameter_value(0.15, "amp", (0,), "xp") # Check the value for qubit 0 params = self.cals.parameters_table(parameters=["amp"], qubit_list=[(0,)]) From 0f60d1f67f6255c73d39dbd98b7ad0df516d3b34 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 17:50:29 +0200 Subject: [PATCH 048/178] * channel pattern is now a class variable. --- qiskit_experiments/calibration/calibrations.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7d0058b9d8..0252d83a43 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -63,6 +63,9 @@ class Calibrations: "ch1.0$0" will resolve to ControlChannel(3). """ + # The channel indices need to be parameterized following this regex. + __channel_pattern__ = r"^ch\d[.\d]*\${0,1}[\d]*$" + def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = None): """ Initialize the instructions from a given backend. @@ -86,8 +89,6 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._schedules = {} - self._channel_pattern = r"^ch\d[.\d]*\${0,1}[\d]*$" - def add_schedule(self, schedule: Schedule, qubits: Tuple = None): """ Add a schedule and register its parameters. @@ -108,9 +109,9 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): for ch in schedule.channels: if isinstance(ch.index, Parameter): param_indices.add(ch.index) - if re.compile(self._channel_pattern).match(ch.index.name) is None: + if re.compile(self.__channel_pattern__).match(ch.index.name) is None: raise CalibrationError( - f"Parameterized channel must correspond to {self._channel_pattern}" + f"Parameterized channel must correspond to {self.__channel_pattern__}" ) self._schedules[(schedule.name, qubits)] = schedule From 9de4086ea915357b293d0e79efcd6beee322ebcd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Apr 2021 17:54:43 +0200 Subject: [PATCH 049/178] * Made _get_circuit private. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 0252d83a43..144049a49c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -499,7 +499,7 @@ def get_parameter_keys( return keys - def get_circuit( + def _get_circuit( self, schedule_name: str, qubits: Tuple, From 7bac46765fd5a00cae9ac23e3ca0a30eb7a17e00 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 12:54:11 +0200 Subject: [PATCH 050/178] * Added method calibration_parameter. * Improved docstrings. --- .../calibration/calibrations.py | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 144049a49c..309ca308e1 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -155,6 +155,39 @@ def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: """ return self._parameter_map_r + def calibration_parameter( + self, parameter_name: str, qubits: Tuple[int, ...] = None, schedule_name: str = None + ) -> Parameter: + """ + 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. + 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. + """ + # 1) Check for qubit specific parameters. + if (schedule_name, parameter_name, qubits) in self._parameter_map: + return self._parameter_map[(schedule_name, parameter_name, qubits)] + + # 2) Check for default parameters. + elif (schedule_name, parameter_name, None) in self._parameter_map: + return self._parameter_map[(schedule_name, parameter_name, None)] + else: + raise CalibrationError( + f"No parameter for {parameter_name} and schedule {schedule_name} " + f"and qubits {qubits}. No default value exists." + ) + def add_parameter_value( self, value: Union[int, float, complex, ParameterValue], @@ -185,7 +218,7 @@ def add_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - registered_schedules = set(key[0] for key in self._schedules.keys()) + registered_schedules = set(key[0] 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.") @@ -260,10 +293,16 @@ def get_parameter_value( cutoff_date: datetime = None, ) -> Union[int, float, complex]: """ - 1) Check if the given qubits have their own Parameter. - 2) If they do not check to see if a parameter global to all qubits exists. - 3) Filter candidate parameter values. - 4) Return the most recent parameter. + Retrieves the value of a parameter. Parameters may be linked. 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 we 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. @@ -284,28 +323,19 @@ def get_parameter_value( CalibrationError: if there is no parameter value for the given parameter name and pulse channel. """ + + # 1) Identify the parameter object. param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - # 1) Check for qubit specific parameters. - if (sched_name, param_name, qubits) in self._parameter_map: - param = self._parameter_map[(sched_name, param_name, qubits)] - - # 2) Check for default parameters. - elif (sched_name, param_name, None) in self._parameter_map: - param = self._parameter_map[(sched_name, param_name, None)] - else: - raise CalibrationError( - f"No parameter for {param_name} and schedule {sched_name} " - f"and qubits {qubits}. No default value exists." - ) + param = self.calibration_parameter(param_name, qubits, sched_name) - # 3) Get a list of candidate keys restricted to the qubits of interest. + # 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.schedule, key.parameter, qubits)) - # 4) Loop though the candidate keys + # 3) Loop though the candidate keys to candidate values candidates = [] parameter_not_found = True for key in candidate_keys: @@ -316,7 +346,7 @@ def get_parameter_value( else: raise CalibrationError("Duplicate parameters.") - # 5) If not candidate parameter values were found look for default parameters + # If no candidate parameter values were found look for default parameters # i.e. parameters that do not specify a qubit. if len(candidates) == 0: candidate_default_keys = [] @@ -334,7 +364,7 @@ def get_parameter_value( else: raise CalibrationError("Duplicate parameters.") - # 6) Filter candidate parameter values. + # 4) Filter candidate parameter values. if valid_only: candidates = [val for val in candidates if val.valid] @@ -356,7 +386,7 @@ def get_parameter_value( raise CalibrationError(msg) - # 4) Return the most recent parameter. + # 5) Return the most recent parameter. candidates.sort(key=lambda x: x.date_time) return candidates[-1].value From 619fea33ca623279faa4981d809713ff9a7d325e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 20:52:24 +0200 Subject: [PATCH 051/178] * Added the hash to Calibrations.parameters --- qiskit_experiments/calibration/calibrations.py | 10 +++++++--- test/calibration/test_calibrations.py | 10 +++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 309ca308e1..3e34eb304d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -153,7 +153,11 @@ def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: the parameter keys. Parameters that are not attached to a schedule will have None in place of a schedule name. """ - return self._parameter_map_r + parameters = {} + for param, key in self._parameter_map_r.items(): + parameters[(param, hash(param))] = key + + return parameters def calibration_parameter( self, parameter_name: str, qubits: Tuple[int, ...] = None, schedule_name: str = None @@ -645,8 +649,8 @@ def to_csv(self): for key in keys: body.append( { - "parameter.name": parameter.name, - "hash(parameter)": hash(parameter), + "parameter.name": parameter[0].name, + "hash(parameter)": parameter[1], "schedule": key.schedule, "qubits": key.qubits, } diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index dd002de55a..bd6baef047 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -64,25 +64,25 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" expected = {ParameterKey("xp", "amp", None), ParameterKey("xm", "amp", None)} - self.assertEqual(self.cals.parameters[self.amp_xp], expected) + self.assertEqual(self.cals.parameters[(self.amp_xp, hash(self.amp_xp))], expected) expected = {ParameterKey("x90p", "amp", None)} - self.assertEqual(self.cals.parameters[self.amp_x90p], expected) + self.assertEqual(self.cals.parameters[(self.amp_x90p, hash(self.amp_x90p))], expected) expected = {ParameterKey("y90p", "amp", None)} - self.assertEqual(self.cals.parameters[self.amp_y90p], expected) + self.assertEqual(self.cals.parameters[(self.amp_y90p, hash(self.amp_y90p))], expected) expected = {ParameterKey("xp", "β", None), ParameterKey("xm", "β", None), ParameterKey("x90p", "β", None), ParameterKey("y90p", "β", None)} - self.assertEqual(self.cals.parameters[self.beta], expected) + self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], expected) expected = {ParameterKey("xp", "σ", None), ParameterKey("xm", "σ", None), ParameterKey("x90p", "σ", None), ParameterKey("y90p", "σ", None)} - self.assertEqual(self.cals.parameters[self.sigma], expected) + self.assertEqual(self.cals.parameters[(self.sigma, hash(self.sigma))], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) From 373cb380f1b2268a6e33afc7a00552601be7ecf5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 21:10:32 +0200 Subject: [PATCH 052/178] * Imprved the docstrings. --- .../calibration/calibrations.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 3e34eb304d..b3b07b67ed 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -51,16 +51,23 @@ class Calibrations: - having different schedules share parameters - allows default schedules for qubits that can be overridden of specific qubits. - Parametric channel naming convention. Channels must be name according to a predefined - pattern so that self can resolve the channels and control channels when assigning - values to the parametric channel indices. This is pattern is "^ch\d[.\d]*\${0,1}[\d]*$", - examples of which include "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". - The "." delimiter is used to specify the different qubits when looking for control - channels. The optional $ delimiter is used to specify which control channel to use + Parametric channel naming convention. + Channels must be name according to a predefined pattern so that self can resolve + the channels and control channels when assigning values to the parametric channel + indices. This pattern is "^ch\d[.\d]*\${0,1}[\d]*$", examples of which include "ch0", + "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". The "." delimiter is used to + specify the different qubits when looking for control channels. + The optional $ delimiter is used to specify which control channel to use if several control channels work together on the same qubits. For example, if the control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while "ch1.0$0" will resolve to ControlChannel(3). + + Parameter naming restriction. + Each parameter must have a unique name within each schedule. For example, it is + acceptable to have a parameter named 'amp' in the schedule 'xp' and a different + parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable + to have two parameters named 'amp' in the same schedule. """ # The channel indices need to be parameterized following this regex. @@ -68,7 +75,7 @@ class Calibrations: def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = None): """ - Initialize the instructions from a given backend. + Initialize the calibrations. Args: control_config: A configuration dictionary of any control channels. The @@ -104,7 +111,6 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): have the same name. """ # check that channels, if parameterized, have the proper name format. - # pylint: disable = raise-missing-from param_indices = set() for ch in schedule.channels: if isinstance(ch.index, Parameter): @@ -206,7 +212,7 @@ def add_parameter_value( Args: value: The value of the parameter to add. If an int, float, or complex is given - then the timestamp of the parameter will automatically be generated to + then the timestamp of the parameter values will automatically be generated to correspond to the current time. param: The parameter or its name for which to add the measured value. qubits: The qubits to which this parameter applies. @@ -325,7 +331,7 @@ def get_parameter_value( Raises: CalibrationError: if there is no parameter value for the given parameter name - and pulse channel. + and pulse channel or if there is an inconsistency in the stored parameters. """ # 1) Identify the parameter object. @@ -590,6 +596,7 @@ def parameters_table( qubit_list: List[Tuple[int, ...]] = None, ) -> List[Dict[str, Any]]: """ + A convenience function to help users visualize the values of their parameter. Args: parameters: The parameter names that should be included in the returned From 38deb524c1390cd22cf4b9fe5fd3512360767c6c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 21:13:59 +0200 Subject: [PATCH 053/178] * black --- test/calibration/test_backend_calibrations.py | 2 +- test/calibration/test_calibrations.py | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index eaf1887e1d..752d4375f7 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -24,5 +24,5 @@ def test_run_options(self): """Test that we can get run options.""" cals = BackendCalibrations(FakeArmonk()) - expected = {'qubit_lo_freq': [4971852852.405576], 'meas_lo_freq': [6993370669.000001]} + expected = {"qubit_lo_freq": [4971852852.405576], "meas_lo_freq": [6993370669.000001]} self.assertEqual(cals.run_options(), expected) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index bd6baef047..2e3701e79a 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -72,16 +72,20 @@ def test_setup(self): expected = {ParameterKey("y90p", "amp", None)} self.assertEqual(self.cals.parameters[(self.amp_y90p, hash(self.amp_y90p))], expected) - expected = {ParameterKey("xp", "β", None), - ParameterKey("xm", "β", None), - ParameterKey("x90p", "β", None), - ParameterKey("y90p", "β", None)} + expected = { + ParameterKey("xp", "β", None), + ParameterKey("xm", "β", None), + ParameterKey("x90p", "β", None), + ParameterKey("y90p", "β", None), + } self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], expected) - expected = {ParameterKey("xp", "σ", None), - ParameterKey("xm", "σ", None), - ParameterKey("x90p", "σ", None), - ParameterKey("y90p", "σ", None)} + expected = { + ParameterKey("xp", "σ", None), + ParameterKey("xm", "σ", None), + ParameterKey("x90p", "σ", None), + ParameterKey("y90p", "σ", None), + } self.assertEqual(self.cals.parameters[(self.sigma, hash(self.sigma))], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) @@ -292,16 +296,18 @@ class TestControlChannels(QiskitTestCase): def setUp(self): """Create the setup we will deal with.""" - controls = {(3, 2): [ControlChannel(10), ControlChannel(123)], - (2, 3): [ControlChannel(15), ControlChannel(23)]} + controls = { + (3, 2): [ControlChannel(10), ControlChannel(123)], + (2, 3): [ControlChannel(15), ControlChannel(23)], + } self.cals = Calibrations(control_config=controls) self.amp_cr = Parameter("amp_cr") self.amp_rot = Parameter("amp_rot") self.amp = Parameter("amp") - self.d0_ = DriveChannel(Parameter('ch0')) - self.d1_ = DriveChannel(Parameter('ch1')) - self.c1_ = ControlChannel(Parameter('ch0.1')) + self.d0_ = DriveChannel(Parameter("ch0")) + self.d1_ = DriveChannel(Parameter("ch1")) + self.c1_ = ControlChannel(Parameter("ch0.1")) self.sigma = Parameter("σ") self.width = Parameter("w") self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -329,7 +335,7 @@ def setUp(self): self.cals.add_schedule(cr) self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3, ), "xp") + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp_cr", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(20, self.date_time), "w", (3, 2), "cr") From 602f0011068939a4a22ae9c1be04454d45edf705 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 21:18:46 +0200 Subject: [PATCH 054/178] * Black --- qiskit_experiments/calibration/backend_calibrations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index ddcd7bc0d2..2d8d4a69c6 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -78,7 +78,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, valid_only, group, cutoff_date) + freq = self.get_parameter_value( + param, (qubit,), None, valid_only, group, cutoff_date + ) else: if meas_freq: freq = self._backend.defaults().meas_freq_est[qubit] From 18ff956d25e90e063cf607ff26bbd9f66a4bf762 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Apr 2021 21:31:26 +0200 Subject: [PATCH 055/178] * Lint --- test/calibration/test_backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index 752d4375f7..57fbef8ebb 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -14,7 +14,7 @@ from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeArmonk -from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations, ParameterKey +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class TestBackendCalibrations(QiskitTestCase): From fe07b6e0a4a9120f91b2c70c59330113922d53d7 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 27 Apr 2021 20:15:46 +0200 Subject: [PATCH 056/178] * Renamed qubit_ref_est -> qubit_lo_freq. --- qiskit_experiments/calibration/backend_calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 2d8d4a69c6..c7b187ae64 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -37,8 +37,8 @@ def __init__(self, backend: Union[Backend, BaseBackend]): super().__init__(backend.configuration()._control_channels) # Use the same naming convention as in backend.defaults() - self.qubit_freq = Parameter("qubit_freq_est") - self.meas_freq = Parameter("meas_freq_est") + self.qubit_freq = Parameter("qubit_lo_freq") + self.meas_freq = Parameter("meas_lo_freq") self._register_parameter(self.qubit_freq) self._register_parameter(self.meas_freq) From 09f1cc368fe7b2b6667163cfa2f20c0d817da333 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 27 Apr 2021 20:21:45 +0200 Subject: [PATCH 057/178] * _get_frequencies only returns valid frequencies. --- .../calibration/backend_calibrations.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index c7b187ae64..9c86548777 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -48,7 +48,6 @@ def __init__(self, backend: Union[Backend, BaseBackend]): def _get_frequencies( self, meas_freq: bool, - valid_only: bool = True, group: str = "default", cutoff_date: datetime = None, ) -> List[float]: @@ -56,12 +55,11 @@ def _get_frequencies( Get the most recent qubit or measurement frequencies. These frequencies can be passed to the run-time options of :class:`BaseExperiment`. If no calibrated value for the frequency of a qubit is found then the default value from the backend - defaults is used. + defaults is used. Only valid parameter values are returned. Args: meas_freq: If True return the measurement frequencies otherwise return the qubit frequencies. - valid_only: Use only valid parameter values. group: The calibration group from which to draw the parameters. If not specifies this defaults to the 'default' group. cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters @@ -78,9 +76,7 @@ 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, valid_only, group, cutoff_date - ) + freq = self.get_parameter_value(param, (qubit,), None, True, group, cutoff_date) else: if meas_freq: freq = self._backend.defaults().meas_freq_est[qubit] @@ -91,19 +87,13 @@ def _get_frequencies( return freqs - def run_options( - self, - valid_only: bool = True, - group: str = "default", - cutoff_date: datetime = None, - ) -> Dict[str, Any]: + def run_options(self, group: str = "default", cutoff_date: datetime = None) -> Dict[str, Any]: """ Retrieve all run-options to be used as kwargs when calling :meth:`BaseExperiment.run`. This gives us the means to communicate the most recent measured values of the qubit and measurement frequencies of the backend. Args: - valid_only: Use only valid parameter values. group: The calibration group from which to draw the parameters. If not specifies this defaults to the 'default' group. cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters @@ -117,8 +107,8 @@ def run_options( """ return { - "qubit_lo_freq": self._get_frequencies(False, valid_only, group, cutoff_date), - "meas_lo_freq": self._get_frequencies(True, valid_only, group, cutoff_date), + "qubit_lo_freq": self._get_frequencies(False, group, cutoff_date), + "meas_lo_freq": self._get_frequencies(True, group, cutoff_date), } def export_backend(self) -> Backend: From 93448345bbc45a84d762f363f5856987efcd35d4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 27 Apr 2021 20:25:06 +0200 Subject: [PATCH 058/178] * Fixed regex. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b3b07b67ed..5018b17f15 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -71,7 +71,7 @@ class Calibrations: """ # The channel indices need to be parameterized following this regex. - __channel_pattern__ = r"^ch\d[.\d]*\${0,1}[\d]*$" + __channel_pattern__ = r"^ch\d[\.\d]*\${0,1}[\d]*$" def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = None): """ From d8dff58c4ff9d995b6a4a4afea303ad9bf3ca4c0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 27 Apr 2021 20:29:20 +0200 Subject: [PATCH 059/178] * Docstring fix. --- qiskit_experiments/calibration/calibrations.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5018b17f15..c467b030a6 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -44,12 +44,11 @@ class Calibrations: r""" - A class to manage schedules with calibrated parameter values. - Schedules are stored in a dict and are intended to be fully parameterized, - including the index of the channels. The parameter values are stored in a - dict where parameters are keys. This class supports: - - having different schedules share parameters - - allows default schedules for qubits that can be overridden of specific qubits. + A class to manage schedules with calibrated parameter values. Schedules are + intended to be fully parameterized, including the index of the channels. + This class: + - supports having different schedules share parameters + - allows default schedules for qubits that can be overridden for specific qubits. Parametric channel naming convention. Channels must be name according to a predefined pattern so that self can resolve From 8f8a569cb7a7ad5c36da1a1cf30f0b9b7e7705ef Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 09:23:30 +0200 Subject: [PATCH 060/178] * Added ScheduleKey. --- qiskit_experiments/calibration/calibrations.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index c467b030a6..191a4d9000 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -40,6 +40,7 @@ from qiskit_experiments.calibration.parameter_value import ParameterValue ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter", "qubits"]) +ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) class Calibrations: @@ -119,7 +120,7 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): f"Parameterized channel must correspond to {self.__channel_pattern__}" ) - self._schedules[(schedule.name, qubits)] = schedule + self._schedules[ScheduleKey(schedule.name, qubits)] = schedule param_names = [param.name for param in schedule.parameters] @@ -227,7 +228,7 @@ def add_parameter_value( param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, Schedule) else schedule - registered_schedules = set(key[0] for key in self._schedules) + 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.") @@ -426,9 +427,9 @@ def get_schedule( not be found. """ if (name, qubits) in self._schedules: - schedule = self._schedules[(name, qubits)] + schedule = self._schedules[ScheduleKey(name, qubits)] elif (name, None) in self._schedules: - schedule = self._schedules[(name, None)] + schedule = self._schedules[ScheduleKey(name, None)] else: raise CalibrationError(f"Schedule {name} is not defined for qubits {qubits}.") @@ -681,7 +682,7 @@ def to_csv(self): schedules = [] header_keys = ["name", "qubits", "schedule"] for key, sched in self._schedules.items(): - schedules.append({"name": key[0], "qubits": key[1], "schedule": str(sched)}) + schedules.append({"name": key.schedule, "qubits": key.qubits, "schedule": str(sched)}) with open("schedules.csv", "w", newline="") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) From 174c4ece84e71a22a680b4ea03c821da79ab6cee Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 09:25:17 +0200 Subject: [PATCH 061/178] * Fixed String trimming. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 191a4d9000..6faec417bb 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -261,7 +261,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ if isinstance(chan.index, Parameter): if isinstance(chan, (DriveChannel, MeasureChannel)): - index = int(chan.index.name[2:].replace("ch", "").split("$")[0]) + index = int(chan.index.name[2:].split("$")[0]) if len(qubits) < index: raise CalibrationError(f"Not enough qubits given for channel {chan}.") From 1546e48f677fa644f281869941f357aeed7c010d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 09:30:48 +0200 Subject: [PATCH 062/178] * Fixed channel checking length issue. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 6faec417bb..6c186880d6 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -282,7 +282,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: if len(channel_index_parts) == 2: control_index = int(channel_index_parts[1]) - if len(chs_) < control_index: + if len(chs_) <= control_index: raise CalibrationError( f"Control channel index {control_index} not found for qubits {qubits}." ) From f4bf40d8451aebb351ea74ac6ec5d16df80235aa Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 09:39:53 +0200 Subject: [PATCH 063/178] * Added the max function for efficiency. --- qiskit_experiments/calibration/calibrations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 6c186880d6..83c2a22f00 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -397,9 +397,7 @@ def get_parameter_value( raise CalibrationError(msg) # 5) Return the most recent parameter. - candidates.sort(key=lambda x: x.date_time) - - return candidates[-1].value + return max(candidates, key=lambda x: x.date_time).value def get_schedule( self, From f9e42ed220bc82d94c15fae2e30af10a2afb9750 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 09:54:53 +0200 Subject: [PATCH 064/178] * Added error message in get_parameter_keys. --- qiskit_experiments/calibration/calibrations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 83c2a22f00..f5c45175e0 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -492,6 +492,9 @@ def get_parameter_keys( Returns: keys: The set of keys populated with schedule name, parameter name, qubits. + + Raises: + CalibrationError: If a channel index is parameterized. """ # schedule.channels may give the qubits in any order. This order matters. For example, @@ -508,6 +511,13 @@ def get_parameter_keys( 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): qubit_set.add(chan.index) From 05d61eb8e6eb820c7b00d9e439a3c0e4671a034e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 12:23:28 +0200 Subject: [PATCH 065/178] * Simplified get_parameter_keys. * Added an extra test. --- .../calibration/calibrations.py | 29 +++++------ test/calibration/test_calibrations.py | 48 ++++++++++++++++++- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index f5c45175e0..01d2b17eef 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -23,6 +23,7 @@ from qiskit import QuantumCircuit from qiskit.pulse import ( Schedule, + ScheduleBlock, DriveChannel, ControlChannel, MeasureChannel, @@ -96,7 +97,7 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._schedules = {} - def add_schedule(self, schedule: Schedule, qubits: Tuple = None): + def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): """ Add a schedule and register its parameters. @@ -122,8 +123,14 @@ def add_schedule(self, schedule: Schedule, qubits: Tuple = None): self._schedules[ScheduleKey(schedule.name, qubits)] = schedule + # Register the schedule param_names = [param.name for param in schedule.parameters] + # Register the subroutines in call instructions + for _, inst in schedule.instructions: + if isinstance(inst, Call): + self.add_schedule(inst.subroutine, qubits) + if len(param_names) != len(set(param_names)): raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") @@ -525,26 +532,14 @@ def get_parameter_keys( for _, inst in schedule.instructions: - if isinstance(inst, Play): - for params in inst.pulse.parameters.values(): - if isinstance(params, ParameterExpression): - for param in params.parameters: - keys.add(ParameterKey(schedule.name, param.name, qubits_)) - - if isinstance(inst, (ShiftPhase, SetPhase)): - if isinstance(inst.phase, ParameterExpression): - for param in inst.phase.parameters: - keys.add(ParameterKey(schedule.name, param.name, (inst.channel.index,))) - - if isinstance(inst, (ShiftFrequency, SetFrequency)): - if isinstance(inst.frequency, ParameterExpression): - for param in inst.frequency.parameters: - keys.add(ParameterKey(schedule.name, param.name, (inst.channel.index,))) - if isinstance(inst, Call): sched_ = inst.subroutine.assign_parameters(binding_dict, inplace=False) keys = Calibrations.get_parameter_keys(sched_, keys, binding_dict, qubits_) + else: + for param in inst.parameters: + keys.add(ParameterKey(schedule.name, param.name, qubits_)) + return keys def _get_circuit( diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 2e3701e79a..7d1873ada7 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -55,7 +55,7 @@ def setUp(self): # Add some parameter values. self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "x90p") self.cals.add_parameter_value(ParameterValue(0.08, self.date_time), "amp", (3,), "y90p") @@ -254,7 +254,7 @@ def test_default_schedules(self): # Check that updating sigma updates both schedules. later_date_time = datetime.strptime("16/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.cals.add_parameter_value(ParameterValue(50, later_date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(50, later_date_time), "σ", schedule="xp") xp0 = self.cals.get_schedule("xp", (0,)) xp3 = self.cals.get_schedule("xp", (3,)) @@ -291,6 +291,50 @@ def test_parameter_filtering(self): self.assertEqual(len(amp_values), 2) +class TestInstructions(QiskitTestCase): + """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" + + def setUp(self): + """Create the setting to test.""" + self.phase = Parameter("φ") + self.freq = Parameter("ν") + self.d0_ = DriveChannel(Parameter("ch0")) + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, 0.5, 40), self.d0_) + + with pulse.build(name="xp12") as xp12: + pulse.shift_phase(self.phase, self.d0_) + pulse.set_frequency(self.freq, self.d0_) + pulse.play(Gaussian(160, 0.5, 40), self.d0_) + + # To make things more interesting we will use a call. + with pulse.build(name="xp02") as xp02: + pulse.call(xp) + pulse.call(xp12) + + self.cals = Calibrations() + self.cals.add_schedule(xp02) + + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + self.cals.add_parameter_value(ParameterValue(1.57, self.date_time), "φ", (3,), "xp12") + self.cals.add_parameter_value(ParameterValue(200, self.date_time), "ν", (3,), "xp12") + + def test_call_registration(self): + """Check that by registering the call we registered three schedules.""" + + self.assertEqual(len(self.cals.schedules()), 3) + + def test_instructions(self): + """Check that we get a properly assigned schedule.""" + + sched = self.cals.get_schedule("xp02", (3, )) + + self.assertTrue(isinstance(sched.instructions[0][1], pulse.Play)) + self.assertEqual(sched.instructions[1][1].phase, 1.57) + self.assertEqual(sched.instructions[2][1].frequency, 200) + class TestControlChannels(QiskitTestCase): """Test more complex schedules such as an echoed cross-resonance.""" From 919403107e9c53d44f49b3366e79c7603f3efdd7 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 12:58:43 +0200 Subject: [PATCH 066/178] * Changed method schedules. --- qiskit_experiments/calibration/calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 01d2b17eef..bf4dd8d54a 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -587,8 +587,8 @@ def schedules(self) -> List[Dict[str, Any]]: data: A list of dictionaries with all the schedules in it. """ data = [] - for context, sched in self._schedules.items(): - data.append({"context": context, "schedule": sched, "parameters": sched.parameters}) + for key, sched in self._schedules.items(): + data.append({"qubits": key.qubits, "schedule": sched, "parameters": sched.parameters}) return data From afaba9bfaac45c9c8c99eafe4ee1899b42f36820 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 13:09:13 +0200 Subject: [PATCH 067/178] * Improved to_csv docstring. --- qiskit_experiments/calibration/calibrations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index bf4dd8d54a..b875be3e6f 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -648,7 +648,11 @@ def parameters_table( def to_csv(self): """ Serializes the parameterized schedules and parameter values so - that they can be stored in csv file. + that they can be stored in csv files. This method will create 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. """ # Write the parameter configuration. From 52650306745524e22ee3974381a97e423a331885 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 14:08:18 +0200 Subject: [PATCH 068/178] * Added method to clean the mapping which is need when overwritting schedule. --- .../calibration/calibrations.py | 24 +++++++++++ test/calibration/test_calibrations.py | 43 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b875be3e6f..8394f4189d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -121,6 +121,10 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = f"Parameterized channel must correspond to {self.__channel_pattern__}" ) + # 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[ScheduleKey(schedule.name, qubits)] = schedule # Register the schedule @@ -139,6 +143,26 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = if param not in param_indices: self._register_parameter(param, schedule, qubits) + def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = None): + """Clean the parameter to schedule mapping for the given schedule, parameter and qubits. + + Args: + schedule_name: The name of the schedule. + qubits: The qubits to which this schedule applies. + + """ + keys_to_remove = [] + for key in self._parameter_map.keys(): + if key.schedule == schedule_name and key.qubits == qubits: + keys_to_remove.append(key) + + for key in keys_to_remove: + del self._parameter_map[key] + + for param, key_set in self._parameter_map_r.items(): + if key in key_set: + key_set.remove(key) + def _register_parameter( self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None ): diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 7d1873ada7..d339a7d9d5 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -220,7 +220,7 @@ def _add_parameters(self): """Helper function.""" # Add the minimum number of parameter values. Sigma is shared across both schedules. - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") @@ -266,6 +266,47 @@ def test_default_schedules(self): expected = {self.amp_xp, self.amp, self.sigma, self.beta} self.assertEqual(len(set(self.cals.parameters.keys())), len(expected)) + def test_replace_schedule(self): + """Test that schedule replacement works as expected.""" + + self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") + self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") + + # Let's replace the schedule for qubit 3 with a double Drag pulse. + with pulse.build(name="xp") as sched: + pulse.play(Drag(160, self.amp_xp/2, self.sigma, self.beta), self.drive) + pulse.play(Drag(160, self.amp_xp/2, self.sigma, self.beta), self.drive) + + expected = self.cals.parameters + + # Adding this new schedule should not change the parameter mapping + self.cals.add_schedule(sched, (3, )) + + self.assertEqual(self.cals.parameters, expected) + + # For completeness we check that schedule that comes out. + sched_cal = self.cals.get_schedule("xp", (3, )) + + self.assertTrue(isinstance(sched_cal.instructions[0][1].pulse, Drag)) + self.assertTrue(isinstance(sched_cal.instructions[1][1].pulse, Drag)) + self.assertEqual(sched_cal.instructions[0][1].pulse.amp, 0.125) + self.assertEqual(sched_cal.instructions[1][1].pulse.amp, 0.125) + + # Let's replace the schedule for qubit 3 with a Gaussian pulse. + # This should change the parameter mapping + with pulse.build(name="xp") as sched2: + pulse.play(Gaussian(160, self.amp_xp/2, self.sigma), self.drive) + + # Check that beta is in the mapping + self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], + {ParameterKey(schedule='xp', parameter='β', qubits=(3,))}) + + self.cals.add_schedule(sched2, (3,)) + + # Check that beta no longer maps to a schedule + self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], set()) + def test_parameter_filtering(self): """Test that we can properly filter parameter values.""" From f817f5cd2c083c60c710e2957667414d42dad4e0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 14:33:35 +0200 Subject: [PATCH 069/178] * Added an instance to make parameter hashes nice and counting up from 0. --- .../calibration/calibrations.py | 21 ++++++++++++------- test/calibration/test_calibrations.py | 14 ++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 8394f4189d..be5927ce3c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -97,6 +97,11 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._schedules = {} + # A variable to store all parameter hashes encountered and present them as ordered + # indices to the user. + self._hash_map = {} + self._parameter_counter = 0 + def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): """ Add a schedule and register its parameters. @@ -177,6 +182,10 @@ def _register_parameter( be None which allows the calibration to accommodate, e.g. qubit frequencies. qubits: The qubits for which to register the parameter. """ + if parameter not in self._hash_map: + self._hash_map[parameter] = self._parameter_counter + self._parameter_counter += 1 + sched_name = schedule.name if schedule else None key = ParameterKey(sched_name, parameter.name, qubits) self._parameter_map[key] = parameter @@ -190,11 +199,7 @@ def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: the parameter keys. Parameters that are not attached to a schedule will have None in place of a schedule name. """ - parameters = {} - for param, key in self._parameter_map_r.items(): - parameters[(param, hash(param))] = key - - return parameters + return self._parameter_map_r def calibration_parameter( self, parameter_name: str, qubits: Tuple[int, ...] = None, schedule_name: str = None @@ -680,15 +685,15 @@ def to_csv(self): """ # Write the parameter configuration. - header_keys = ["parameter.name", "hash(parameter)", "schedule", "qubits"] + 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[0].name, - "hash(parameter)": parameter[1], + "parameter.name": parameter.name, + "parameter unique id": self._hash_map[parameter], "schedule": key.schedule, "qubits": key.qubits, } diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index d339a7d9d5..0465ba0ce3 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -64,13 +64,13 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" expected = {ParameterKey("xp", "amp", None), ParameterKey("xm", "amp", None)} - self.assertEqual(self.cals.parameters[(self.amp_xp, hash(self.amp_xp))], expected) + self.assertEqual(self.cals.parameters[self.amp_xp], expected) expected = {ParameterKey("x90p", "amp", None)} - self.assertEqual(self.cals.parameters[(self.amp_x90p, hash(self.amp_x90p))], expected) + self.assertEqual(self.cals.parameters[self.amp_x90p], expected) expected = {ParameterKey("y90p", "amp", None)} - self.assertEqual(self.cals.parameters[(self.amp_y90p, hash(self.amp_y90p))], expected) + self.assertEqual(self.cals.parameters[self.amp_y90p], expected) expected = { ParameterKey("xp", "β", None), @@ -78,7 +78,7 @@ def test_setup(self): ParameterKey("x90p", "β", None), ParameterKey("y90p", "β", None), } - self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], expected) + self.assertEqual(self.cals.parameters[self.beta], expected) expected = { ParameterKey("xp", "σ", None), @@ -86,7 +86,7 @@ def test_setup(self): ParameterKey("x90p", "σ", None), ParameterKey("y90p", "σ", None), } - self.assertEqual(self.cals.parameters[(self.sigma, hash(self.sigma))], expected) + self.assertEqual(self.cals.parameters[self.sigma], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) @@ -299,13 +299,13 @@ def test_replace_schedule(self): pulse.play(Gaussian(160, self.amp_xp/2, self.sigma), self.drive) # Check that beta is in the mapping - self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], + self.assertEqual(self.cals.parameters[self.beta], {ParameterKey(schedule='xp', parameter='β', qubits=(3,))}) self.cals.add_schedule(sched2, (3,)) # Check that beta no longer maps to a schedule - self.assertEqual(self.cals.parameters[(self.beta, hash(self.beta))], set()) + self.assertEqual(self.cals.parameters[self.beta], set()) def test_parameter_filtering(self): """Test that we can properly filter parameter values.""" From 290e2ef320701a7cb3619d4ba899fc7de0f70780 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 14:54:30 +0200 Subject: [PATCH 070/178] * Do not register parameters in a call but recursively register the subroutine of the call. --- qiskit_experiments/calibration/calibrations.py | 18 +++++++++++++----- test/calibration/test_calibrations.py | 8 ++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index be5927ce3c..d36ec14fd4 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -140,13 +140,21 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = if isinstance(inst, Call): self.add_schedule(inst.subroutine, qubits) - if len(param_names) != len(set(param_names)): + # Register parameters that are not indices. + # Do not register parameters that are in call instructions. These parameters + # will have been registered above. + params_to_register = set() + for _, inst in schedule.instructions: + if not isinstance(inst, Call): + 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.") - # Register parameters that are not indices. - for param in schedule.parameters: - if param not in param_indices: - self._register_parameter(param, schedule, qubits) + for param in params_to_register: + self._register_parameter(param, schedule, qubits) def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = None): """Clean the parameter to schedule mapping for the given schedule, parameter and qubits. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 0465ba0ce3..9785eaae1e 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -387,7 +387,7 @@ def setUp(self): } self.cals = Calibrations(control_config=controls) - self.amp_cr = Parameter("amp_cr") + self.amp_cr = Parameter("amp") self.amp_rot = Parameter("amp_rot") self.amp = Parameter("amp") self.d0_ = DriveChannel(Parameter("ch0")) @@ -419,15 +419,15 @@ def setUp(self): self.cals.add_schedule(xp) self.cals.add_schedule(cr) - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp_cr", (3, 2), "cr") + self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(20, self.date_time), "w", (3, 2), "cr") # Reverse gate parameters self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (2,), "xp") - self.cals.add_parameter_value(ParameterValue(0.5, self.date_time), "amp_cr", (2, 3), "cr") + self.cals.add_parameter_value(ParameterValue(0.5, self.date_time), "amp", (2, 3), "cr") self.cals.add_parameter_value(ParameterValue(0.4, self.date_time), "amp_rot", (2, 3), "cr") self.cals.add_parameter_value(ParameterValue(30, self.date_time), "w", (2, 3), "cr") From e56953fa2dfc9a7dd75589df954c539b22d5d503 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 17:18:38 +0200 Subject: [PATCH 071/178] * Removed regex from docstring and improved it. --- qiskit_experiments/calibration/calibrations.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index d36ec14fd4..75e2b22931 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -53,12 +53,15 @@ class Calibrations: - allows default schedules for qubits that can be overridden for specific qubits. Parametric channel naming convention. - Channels must be name according to a predefined pattern so that self can resolve - the channels and control channels when assigning values to the parametric channel - indices. This pattern is "^ch\d[.\d]*\${0,1}[\d]*$", examples of which include "ch0", - "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". The "." delimiter is used to - specify the different qubits when looking for control channels. - The optional $ delimiter is used to specify which control channel to use + Parametrized channel indices must be named according to a predefined pattern so that + self can resolve the channels and control channels when assigning values to the parametric + channel indices. A channel must have a name that starts with `ch` followed by an integer. + For control channels this integer can be followed by a sequence `.integer`. + Optionally, the name can end with `$integer` to specify the index of a control + for the case when a set of qubits share multiple control channels. Example of + valid channel names includes "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". + The "." delimiter is used to specify the different qubits when looking for control + channels. The optional $ delimiter is used to specify which control channel to use if several control channels work together on the same qubits. For example, if the control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while From 26462276908801242fd8ecbdfb9ef4fe693197e4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 17:34:12 +0200 Subject: [PATCH 072/178] * Added code example in class docstring. * Removed unused variable. --- .../calibration/calibrations.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 75e2b22931..eb87793a9e 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -72,6 +72,30 @@ class Calibrations: acceptable to have a parameter named 'amp' in the schedule 'xp' and a different parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable to have two parameters named 'amp' in the same schedule. + + The code block below illustrates the creation of a template schedule for a cross- + resonance gate. + + .. code-block:: python + + amp_cr = Parameter("amp") + amp = Parameter("amp") + d0 = DriveChannel(Parameter("ch0")) + c1 = ControlChannel(Parameter("ch0.1")) + sigma = Parameter("σ") + width = Parameter("w") + dur1 = Parameter("duration") + dur2 = Parameter("duration") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(dur1, amp, sigma), d0) + + with pulse.build(name="cr") as cr: + with pulse.align_sequential(): + pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.call(xp) + pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.call(xp) """ # The channel indices need to be parameterized following this regex. @@ -135,9 +159,6 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = # Add the schedule. self._schedules[ScheduleKey(schedule.name, qubits)] = schedule - # Register the schedule - param_names = [param.name for param in schedule.parameters] - # Register the subroutines in call instructions for _, inst in schedule.instructions: if isinstance(inst, Call): From 3dfcb3d526431882d71b78e456463e931fc4a4e5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 17:45:27 +0200 Subject: [PATCH 073/178] * Raise error if a channel has more than one parameter. --- qiskit_experiments/calibration/calibrations.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index eb87793a9e..c2c4c6a81f 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -141,12 +141,15 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = Raises: CalibrationError: If the parameterized channel index is not formatted following index1.index2... or if several parameters in the same schedule - have the same name. + have the same name or if a channel is parameterized by more than one parameter. """ # check that channels, if parameterized, have the proper name format. 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( From 91aa4cde5bf8871fac92b9dc494fcf4eb57bfb99 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 17:46:16 +0200 Subject: [PATCH 074/178] * Amended class docstring. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index c2c4c6a81f..0e03deb5d0 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -65,7 +65,7 @@ class Calibrations: if several control channels work together on the same qubits. For example, if the control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while - "ch1.0$0" will resolve to ControlChannel(3). + "ch1.0$0" will resolve to ControlChannel(3). A channel can only have one parameter. Parameter naming restriction. Each parameter must have a unique name within each schedule. For example, it is From 6833d1febb2559567a5402af919f7545712928c5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 20:48:47 +0200 Subject: [PATCH 075/178] * Added remove_schedule method. --- .../calibration/calibrations.py | 28 ++++++++++++++++- test/calibration/test_calibrations.py | 30 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 0e03deb5d0..5ae3ce3e56 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -183,6 +183,22 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = for param in params_to_register: self._register_parameter(param, schedule, qubits) + def remove_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): + """ + Allows users to remove a schedule from the calibrations. The history of the parameters + will remain in the calibrations. + + Args: + schedule: The schedule to remove. + qubits: The qubits for which to remove the schedules. If None is given then this + schedule is the default schedule for all qubits. + """ + if ScheduleKey(schedule.name, qubits) in self._schedules: + del self._schedules[ScheduleKey(schedule.name, qubits)] + + # Clean the parameter to schedule mapping. + self._clean_parameter_map(schedule.name, qubits) + def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = None): """Clean the parameter to schedule mapping for the given schedule, parameter and qubits. @@ -191,7 +207,7 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non qubits: The qubits to which this schedule applies. """ - keys_to_remove = [] + keys_to_remove = [] # of the form (schedule.name, parameter.name, qubits) for key in self._parameter_map.keys(): if key.schedule == schedule_name and key.qubits == qubits: keys_to_remove.append(key) @@ -199,10 +215,20 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non for key in keys_to_remove: del self._parameter_map[key] + # Key set is a set of tuples (schedule.name, parameter.name, qubits) for param, key_set in self._parameter_map_r.items(): if key in key_set: key_set.remove(key) + # Remove entries that do not point to at least one (schedule.name, parameter.name, qubits) + keys_to_delete = [] + for param, key_set in self._parameter_map_r.items(): + if not key_set: + keys_to_delete.append(param) + + for key in keys_to_delete: + del self._parameter_map_r[key] + def _register_parameter( self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None ): diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9785eaae1e..a76235ca67 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -52,6 +52,8 @@ def setUp(self): for sched in [xp, x90p, y90p, xm]: self.cals.add_schedule(sched) + self.xm = xm + # Add some parameter values. self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -93,6 +95,34 @@ def test_setup(self): self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "y90p"), 0.08) + def test_remove_schedule(self): + """Test that we can easily remove a schedule.""" + + self.assertEqual(len(self.cals.schedules()), 4) + + self.cals.remove_schedule(self.xm) + + # Removing xm should remove the schedule but not the parameters as they are shared. + self.assertEqual(len(self.cals.schedules()), 3) + for param in [self.sigma, self.amp_xp, self.amp_x90p, self.amp_y90p, self.beta]: + self.assertTrue(param in self.cals.parameters) + + # Add a schedule with a different parameter and then remove it + with pulse.build(name="error") as sched: + pulse.play(Gaussian(160, Parameter("xyz"), 40), DriveChannel(Parameter("ch0"))) + + self.cals.add_schedule(sched) + + self.assertEqual(len(self.cals.schedules()), 4) + self.assertEqual(len(self.cals.parameters), 6) + + self.cals.remove_schedule(sched) + + self.assertEqual(len(self.cals.schedules()), 3) + self.assertEqual(len(self.cals.parameters), 5) + for param in [self.sigma, self.amp_xp, self.amp_x90p, self.amp_y90p, self.beta]: + self.assertTrue(param in self.cals.parameters) + def test_parameter_dependency(self): """Check that two schedules that share the same parameter are simultaneously updated.""" From 0c1ede5bb2a94c507fd25ffb6ec52ba72a5b0eac Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:54:20 +0200 Subject: [PATCH 076/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/calibrations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5ae3ce3e56..4aa84f741a 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -400,7 +400,9 @@ def get_parameter_value( cutoff_date: datetime = None, ) -> Union[int, float, complex]: """ - Retrieves the value of a parameter. Parameters may be linked. get_parameter_value does the + Retrieves the value of a parameter. + + Parameters may be linked. 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 From 5426c68583fb4dc8c4e2d0bdc8812b6dec814b18 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 21:07:08 +0200 Subject: [PATCH 077/178] * Added the cutoff_date to schedule. --- .../calibration/calibrations.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4aa84f741a..888f87859d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -400,10 +400,9 @@ def get_parameter_value( cutoff_date: datetime = None, ) -> Union[int, float, complex]: """ - Retrieves the value of a parameter. - - Parameters may be linked. get_parameter_value does the - following steps: + Retrieves the value of a parameter. + + Parameters may be linked. 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 we this a list of candidate keys @@ -504,6 +503,7 @@ def get_schedule( qubits: Tuple[int, ...], free_params: List[str] = None, group: Optional[str] = "default", + cutoff_date: datetime = None, ) -> Schedule: """ Get the schedule with the non-free parameters assigned to their values. @@ -514,6 +514,10 @@ def get_schedule( free_params: The parameters that should remain unassigned. group: The calibration group from which to draw the parameters. If not specifies 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 @@ -560,12 +564,16 @@ def get_schedule( param = self._parameter_map[(key.schedule, key.parameter, None)] else: raise CalibrationError( - f"Ill configured calibrations {key} is not present and has not default value." + f"Bad calibrations {key} is not present and has no default value." ) if param not in binding_dict: binding_dict[param] = self.get_parameter_value( - key.parameter, key.qubits, key.schedule, group=group + key.parameter, + key.qubits, + key.schedule, + group=group, + cutoff_date=cutoff_date, ) return schedule.assign_parameters(binding_dict, inplace=False) From 54db71c1cc5caa256c0d664f5187df00558202f2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 21:09:10 +0200 Subject: [PATCH 078/178] * Made get_parameter_keys private. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 888f87859d..755ed529fe 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -545,7 +545,7 @@ def get_schedule( # The following code allows us to get the keys when the schedule has call instructions. # We cannot inline the subroutines yet because we would lose the name of the subroutines. - parameter_keys = Calibrations.get_parameter_keys(schedule, set(), binding_dict, qubits) + parameter_keys = Calibrations._get_parameter_keys(schedule, set(), binding_dict, qubits) # Now that we have the parameter keys we must inline all call subroutines. schedule = inline_subroutines(schedule) @@ -579,7 +579,7 @@ def get_schedule( return schedule.assign_parameters(binding_dict, inplace=False) @staticmethod - def get_parameter_keys( + def _get_parameter_keys( schedule: Schedule, keys: Set, binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...] ): """ @@ -634,7 +634,7 @@ def get_parameter_keys( if isinstance(inst, Call): sched_ = inst.subroutine.assign_parameters(binding_dict, inplace=False) - keys = Calibrations.get_parameter_keys(sched_, keys, binding_dict, qubits_) + keys = Calibrations._get_parameter_keys(sched_, keys, binding_dict, qubits_) else: for param in inst.parameters: From c8f4fb5e7bba9bb7989c67bb10085816dd6fe376 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 21:12:33 +0200 Subject: [PATCH 079/178] * Added missing type hint. --- qiskit_experiments/calibration/calibrations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 755ed529fe..9bd8221a93 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -580,8 +580,11 @@ def get_schedule( @staticmethod def _get_parameter_keys( - schedule: Schedule, keys: Set, binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...] - ): + schedule: Schedule, + keys: Set[ParameterKey], + binding_dict: Dict[Parameter, int], + qubits: Tuple[int, ...], + ) -> Set[ParameterKey]: """ Recursive function to extract parameter keys from a schedule. The recursive behaviour is needed to handle Call instructions. Each time a Call is found From 37a2eafe2e2124a868b73185c98508fed12f282a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 22:43:59 +0200 Subject: [PATCH 080/178] * removed get circuit. --- .../calibration/calibrations.py | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9bd8221a93..2a892df4b7 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -645,42 +645,6 @@ def _get_parameter_keys( return keys - def _get_circuit( - self, - schedule_name: str, - qubits: Tuple, - free_params: List[str] = None, - group: Optional[str] = "default", - schedule: Schedule = None, - ) -> QuantumCircuit: - """ - Queries a schedule by name for the given set of qubits. The parameters given - under the list free_params are left unassigned. The queried schedule is then - embedded in a gate with a calibration and returned as a quantum circuit. - - Args: - schedule_name: The name of the schedule to retrieve. - qubits: The qubits for which to generate the gate with the schedule in it. - free_params: Names of the parameters that will remain unassigned. - group: The calibration group from which to retrieve the calibrated values. - If unspecified this defaults to 'default'. - schedule: The schedule to add to the gate if the internally stored one is - not used. - - Returns: - A quantum circuit in which the parameter values have been assigned aside from - those explicitly specified in free_params. - """ - if schedule is None: - schedule = self.get_schedule(schedule_name, qubits, free_params, group) - - gate = Gate(name=schedule_name, num_qubits=len(qubits), params=list(schedule.parameters)) - circ = QuantumCircuit(len(qubits), len(qubits)) - circ.append(gate, list(range(len(qubits)))) - circ.add_calibration(gate, qubits, schedule, params=schedule.parameters) - - return circ - def schedules(self) -> List[Dict[str, Any]]: """ Return the schedules in self in a list of dictionaries to help From 113150a197d211de6fc248fd43ce5745c32f99dc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 28 Apr 2021 22:49:27 +0200 Subject: [PATCH 081/178] * Fixed inconsistency with in parameters_table. --- qiskit_experiments/calibration/calibrations.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 2a892df4b7..3c09c95c81 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -683,9 +683,6 @@ def parameters_table( data = [] # Convert inputs to lists of strings - if parameters is not None: - parameters = {prm.name if isinstance(prm, Parameter) else prm for prm in parameters} - if schedules is not None: schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} From 30066951a313d3e191f0531243eebfdd7144bd23 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 08:50:38 +0200 Subject: [PATCH 082/178] * Switched to assigned_subroutine. --- .../calibration/calibrations.py | 32 ++++++------------- test/calibration/test_calibrations.py | 6 ++++ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 3c09c95c81..3e442ece35 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -35,7 +35,6 @@ SetFrequency, ) from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.transforms import inline_subroutines from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue @@ -543,12 +542,8 @@ def get_schedule( # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) - # The following code allows us to get the keys when the schedule has call instructions. - # We cannot inline the subroutines yet because we would lose the name of the subroutines. - parameter_keys = Calibrations._get_parameter_keys(schedule, set(), binding_dict, qubits) - - # Now that we have the parameter keys we must inline all call subroutines. - schedule = inline_subroutines(schedule) + # Get the parameter keys by descending into the call instructions. + parameter_keys = Calibrations._get_parameter_keys(schedule, set(), qubits) # Build the parameter binding dictionary. free_params = free_params if free_params else [] @@ -582,23 +577,19 @@ def get_schedule( def _get_parameter_keys( schedule: Schedule, keys: Set[ParameterKey], - binding_dict: Dict[Parameter, int], qubits: Tuple[int, ...], ) -> Set[ParameterKey]: """ Recursive function to extract parameter keys from a schedule. The recursive behaviour is needed to handle Call instructions. Each time a Call is found - get_parameter_keys is call on the subroutine of the Call instruction and the - qubits that are in the subroutine. This also implies carefully extracting the - qubits from the subroutine and in the appropriate order. + get_parameter_keys is call on the assigned subroutine of the Call instruction + and the qubits that are in said subroutine. This requires carefully + extracting the qubits from the subroutine and in the appropriate order. Args: schedule: A schedule from which to extract parameters. - keys: A set of keys that will be populated. - binding_dict: A binding dictionary intended only for channels. This is needed - because calling assign_parameters on a schedule with a Call instruction will - not assign the parameters in the subroutine of the Call instruction. - qubits: The qubits for which we want to have the schedule. + keys: A set of keys recursively populated. + qubits: The qubits for which we want the schedule. Returns: keys: The set of keys populated with schedule name, parameter name, qubits. @@ -609,8 +600,8 @@ def _get_parameter_keys( # schedule.channels may give the qubits in any order. This order matters. For example, # the parameter ('cr', 'amp', (2, 3)) is not the same as ('cr', 'amp', (3, 2)). - # Furthermore, as we call subroutines the list of qubits involved might shrink. For - # example, part of a cross-resonance schedule might involve. + # Furthermore, as we call subroutines the list of qubits involved shrinks. For + # example, a cross-resonance schedule could be # # pulse.call(xp) # ... @@ -634,11 +625,8 @@ def _get_parameter_keys( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) for _, inst in schedule.instructions: - if isinstance(inst, Call): - sched_ = inst.subroutine.assign_parameters(binding_dict, inplace=False) - keys = Calibrations._get_parameter_keys(sched_, keys, binding_dict, qubits_) - + keys = Calibrations._get_parameter_keys(inst.assigned_subroutine(), keys, qubits_) else: for param in inst.parameters: keys.add(ParameterKey(schedule.name, param.name, qubits_)) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index a76235ca67..9b3be30ecc 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -15,6 +15,7 @@ from datetime import datetime from qiskit.circuit import Parameter from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare +from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey @@ -402,10 +403,15 @@ def test_instructions(self): sched = self.cals.get_schedule("xp02", (3, )) + self.assertEqual(sched.parameters, set()) + + sched = inline_subroutines(sched) # inline makes the check more transparent. + self.assertTrue(isinstance(sched.instructions[0][1], pulse.Play)) self.assertEqual(sched.instructions[1][1].phase, 1.57) self.assertEqual(sched.instructions[2][1].frequency, 200) + class TestControlChannels(QiskitTestCase): """Test more complex schedules such as an echoed cross-resonance.""" From 5d40e8c8927d463ca01d738315907de5e67b93e1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 10:24:27 +0200 Subject: [PATCH 083/178] * Cahnged to BackendV1. --- qiskit_experiments/calibration/backend_calibrations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 9c86548777..d37ea0ec99 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -16,8 +16,7 @@ from typing import Any, Dict, List, Union import copy -from qiskit.providers.ibmq.ibmqbackend import IBMQBackend as Backend -from qiskit.providers import BaseBackend +from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Parameter from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey @@ -32,7 +31,7 @@ class BackendCalibrations(Calibrations): any schedule. """ - def __init__(self, backend: Union[Backend, BaseBackend]): + def __init__(self, backend: Backend): """Setup an instance to manage the calibrations of a backend.""" super().__init__(backend.configuration()._control_channels) From 91c239d84cb1b62e934bc79f4039a2e2686586b8 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 10:25:05 +0200 Subject: [PATCH 084/178] * Removed unused import. --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index d37ea0ec99..9df15be717 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -13,7 +13,7 @@ """Store and manage the results of a calibration experiments in the context of a backend.""" from datetime import datetime -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import copy from qiskit.providers.backend import BackendV1 as Backend From c4b7260ca63c39948b07e701f1dda4123e563bde Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 11:51:29 +0200 Subject: [PATCH 085/178] * Fixed lint * Allowed duplicate parameters. --- .../calibration/calibrations.py | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 3e442ece35..a1851b01cb 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -19,20 +19,13 @@ import dataclasses import regex as re -from qiskit.circuit import Gate -from qiskit import QuantumCircuit from qiskit.pulse import ( Schedule, ScheduleBlock, DriveChannel, ControlChannel, MeasureChannel, - Call, - Play, - ShiftPhase, - SetPhase, - ShiftFrequency, - SetFrequency, + Call ) from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter, ParameterExpression @@ -176,7 +169,7 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = 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])): + 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: @@ -207,7 +200,7 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non """ keys_to_remove = [] # of the form (schedule.name, parameter.name, qubits) - for key in self._parameter_map.keys(): + for key in self._parameter_map: if key.schedule == schedule_name and key.qubits == qubits: keys_to_remove.append(key) @@ -444,14 +437,9 @@ def get_parameter_value( # 3) Loop though the candidate keys to candidate values candidates = [] - parameter_not_found = True for key in candidate_keys: if key in self._params: - if parameter_not_found: - candidates = self._params[key] - parameter_not_found = False - else: - raise CalibrationError("Duplicate parameters.") + candidates += self._params[key] # If no candidate parameter values were found look for default parameters # i.e. parameters that do not specify a qubit. @@ -465,11 +453,7 @@ def get_parameter_value( for key in set(candidate_default_keys): if key in self._params: - if parameter_not_found: - candidates = self._params[key] - parameter_not_found = False - else: - raise CalibrationError("Duplicate parameters.") + candidates += self._params[key] # 4) Filter candidate parameter values. if valid_only: From 6ebdc483a906f8267b3ab1e13794a83a7eeddd7c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 11:52:58 +0200 Subject: [PATCH 086/178] * Aligned docstring. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a1851b01cb..767448c58c 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -409,8 +409,8 @@ def get_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 specifies this defaults to the 'default' group. + group: The calibration group from which to draw the parameters. + If not specifies 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 @@ -421,7 +421,7 @@ def get_parameter_value( Raises: CalibrationError: if there is no parameter value for the given parameter name - and pulse channel or if there is an inconsistency in the stored parameters. + and pulse channel. """ # 1) Identify the parameter object. From b4a0f747d329198e4a0d6a4b1166704ec089a882 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 29 Apr 2021 12:02:00 +0200 Subject: [PATCH 087/178] * Made schedules in test more concis. --- test/calibration/test_calibrations.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9b3be30ecc..7d658c02d8 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -444,13 +444,11 @@ def setUp(self): with pulse.align_left(): pulse.play(rotary, self.d1_) # Rotary tone pulse.play(cr_tone, self.c1_) # CR tone. - with pulse.align_sequential(): - pulse.call(xp) + pulse.call(xp) with pulse.align_left(): pulse.play(rotary, self.d1_) pulse.play(cr_tone, self.c1_) - with pulse.align_sequential(): - pulse.call(xp) + pulse.call(xp) self.cals.add_schedule(xp) self.cals.add_schedule(cr) @@ -475,27 +473,23 @@ def test_get_schedule(self): with pulse.align_left(): pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. - with pulse.align_sequential(): - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) with pulse.align_left(): pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. - with pulse.align_sequential(): - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) - self.assertTrue(self.cals.get_schedule("cr", (3, 2)) == cr_32) + self.assertTrue(inline_subroutines(self.cals.get_schedule("cr", (3, 2))) == cr_32) with pulse.build(name="cr") as cr_23: with pulse.align_sequential(): with pulse.align_left(): pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. - with pulse.align_sequential(): - pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) + pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) with pulse.align_left(): pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. - with pulse.align_sequential(): - pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) + pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) self.assertTrue(self.cals.get_schedule("cr", (2, 3)) == cr_23) From d06bb4fedd78d10a55115c16649aa2f647068818 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 30 Apr 2021 11:37:57 +0200 Subject: [PATCH 088/178] * Improved the CR test. This passes but requires terra bug fix #6322. --- test/calibration/test_calibrations.py | 34 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 7d658c02d8..abd90d4b31 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -306,18 +306,18 @@ def test_replace_schedule(self): # Let's replace the schedule for qubit 3 with a double Drag pulse. with pulse.build(name="xp") as sched: - pulse.play(Drag(160, self.amp_xp/2, self.sigma, self.beta), self.drive) - pulse.play(Drag(160, self.amp_xp/2, self.sigma, self.beta), self.drive) + pulse.play(Drag(160, self.amp_xp / 2, self.sigma, self.beta), self.drive) + pulse.play(Drag(160, self.amp_xp / 2, self.sigma, self.beta), self.drive) expected = self.cals.parameters # Adding this new schedule should not change the parameter mapping - self.cals.add_schedule(sched, (3, )) + self.cals.add_schedule(sched, (3,)) self.assertEqual(self.cals.parameters, expected) # For completeness we check that schedule that comes out. - sched_cal = self.cals.get_schedule("xp", (3, )) + sched_cal = self.cals.get_schedule("xp", (3,)) self.assertTrue(isinstance(sched_cal.instructions[0][1].pulse, Drag)) self.assertTrue(isinstance(sched_cal.instructions[1][1].pulse, Drag)) @@ -327,11 +327,13 @@ def test_replace_schedule(self): # Let's replace the schedule for qubit 3 with a Gaussian pulse. # This should change the parameter mapping with pulse.build(name="xp") as sched2: - pulse.play(Gaussian(160, self.amp_xp/2, self.sigma), self.drive) + pulse.play(Gaussian(160, self.amp_xp / 2, self.sigma), self.drive) # Check that beta is in the mapping - self.assertEqual(self.cals.parameters[self.beta], - {ParameterKey(schedule='xp', parameter='β', qubits=(3,))}) + self.assertEqual( + self.cals.parameters[self.beta], + {ParameterKey(schedule="xp", parameter="β", qubits=(3,))}, + ) self.cals.add_schedule(sched2, (3,)) @@ -401,7 +403,7 @@ def test_call_registration(self): def test_instructions(self): """Check that we get a properly assigned schedule.""" - sched = self.cals.get_schedule("xp02", (3, )) + sched = self.cals.get_schedule("xp02", (3,)) self.assertEqual(sched.parameters, set()) @@ -479,8 +481,15 @@ def test_get_schedule(self): pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) - self.assertTrue(inline_subroutines(self.cals.get_schedule("cr", (3, 2))) == cr_32) + # We inline to make the schedules comparable with the construction directly above. + schedule = self.cals.get_schedule("cr", (3, 2)) + inline_schedule = inline_subroutines(schedule) + for idx, inst in enumerate(inline_schedule.instructions): + self.assertTrue(inst == cr_32.instructions[idx]) + self.assertEqual(schedule.parameters, set()) + + # Do the CR in the other direction with pulse.build(name="cr") as cr_23: with pulse.align_sequential(): with pulse.align_left(): @@ -492,4 +501,9 @@ def test_get_schedule(self): pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) - self.assertTrue(self.cals.get_schedule("cr", (2, 3)) == cr_23) + schedule = self.cals.get_schedule("cr", (2, 3)) + inline_schedule = inline_subroutines(schedule) + for idx, inst in enumerate(inline_schedule.instructions): + self.assertTrue(inst == cr_23.instructions[idx]) + + self.assertEqual(schedule.parameters, set()) From 7c403689e83b2990fbecc01844c0dd6699ab3f1a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 30 Apr 2021 12:27:07 +0200 Subject: [PATCH 089/178] * Improved docstrings. * pylint and Black. --- qiskit_experiments/calibration/__init__.py | 4 +- .../calibration/backend_calibrations.py | 40 ++-------- ...calibrations.py => calibrationsmanager.py} | 73 ++++++++++--------- test/calibration/test_backend_calibrations.py | 10 +-- test/calibration/test_calibrations.py | 16 ++-- 5 files changed, 62 insertions(+), 81 deletions(-) rename qiskit_experiments/calibration/{calibrations.py => calibrationsmanager.py} (93%) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 8efc0d6331..1ad27af071 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -12,7 +12,7 @@ """Qiskit Experiments Calibration Root.""" -from .calibrations import Calibrations -from .backend_calibrations import BackendCalibrations +from .calibrationsmanager import CalibrationsManager +from .backend_calibrations import BackendCalibrationsManager from .exceptions import CalibrationError from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 9df15be717..7253f48d3f 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -13,15 +13,15 @@ """Store and manage the results of a calibration experiments in the context of a backend.""" from datetime import datetime -from typing import Any, Dict, List +from typing import List import copy from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Parameter -from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey +from qiskit_experiments.calibration.calibrationsmanager import CalibrationsManager, ParameterKey -class BackendCalibrations(Calibrations): +class BackendCalibrationsManager(CalibrationsManager): """ A Calibrations class to enable a seamless interplay with backend objects. This class enables users to export their calibrations into a backend object. @@ -33,7 +33,7 @@ class BackendCalibrations(Calibrations): def __init__(self, backend: Backend): """Setup an instance to manage the calibrations of a backend.""" - super().__init__(backend.configuration()._control_channels) + super().__init__(backend.configuration().control_channels) # Use the same naming convention as in backend.defaults() self.qubit_freq = Parameter("qubit_lo_freq") @@ -44,7 +44,7 @@ def __init__(self, backend: Backend): self._qubits = set(range(backend.configuration().n_qubits)) self._backend = backend - def _get_frequencies( + def get_frequencies( self, meas_freq: bool, group: str = "default", @@ -86,41 +86,17 @@ def _get_frequencies( return freqs - def run_options(self, group: str = "default", cutoff_date: datetime = None) -> Dict[str, Any]: - """ - Retrieve all run-options to be used as kwargs when calling - :meth:`BaseExperiment.run`. This gives us the means to communicate the most recent - measured values of the qubit and measurement frequencies of the backend. - - Args: - group: The calibration group from which to draw the - parameters. If not specifies 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: - key word arguments containing: `qubit_lo_freq` and `meas_lo_freq` intended to be - passed as arguments to assemble. - """ - - return { - "qubit_lo_freq": self._get_frequencies(False, group, cutoff_date), - "meas_lo_freq": self._get_frequencies(True, group, cutoff_date), - } - def export_backend(self) -> Backend: """ - Exports the calibrations in the backend object that can be used. + Exports the calibrations in a backend object that can be used. Returns: calibrated backend: A backend with the calibrations in it. """ backend = copy.deepcopy(self._backend) - backend.defaults().qubit_freq_est = self._get_frequencies(False) - backend.defaults().meas_freq_est = self._get_frequencies(True) + backend.defaults().qubit_freq_est = self.get_frequencies(False) + backend.defaults().meas_freq_est = self.get_frequencies(True) # TODO: build the instruction schedule map using the stored calibrations diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrationsmanager.py similarity index 93% rename from qiskit_experiments/calibration/calibrations.py rename to qiskit_experiments/calibration/calibrationsmanager.py index 767448c58c..c6c88dcbc3 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrationsmanager.py @@ -19,14 +19,7 @@ import dataclasses import regex as re -from qiskit.pulse import ( - Schedule, - ScheduleBlock, - DriveChannel, - ControlChannel, - MeasureChannel, - Call -) +from qiskit.pulse import Schedule, ScheduleBlock, DriveChannel, ControlChannel, MeasureChannel, Call from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError @@ -36,7 +29,7 @@ ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) -class Calibrations: +class CalibrationsManager: r""" A class to manage schedules with calibrated parameter values. Schedules are intended to be fully parameterized, including the index of the channels. @@ -45,13 +38,13 @@ class Calibrations: - allows default schedules for qubits that can be overridden for specific qubits. Parametric channel naming convention. - Parametrized channel indices must be named according to a predefined pattern so that - self can resolve the channels and control channels when assigning values to the parametric + Parametrized channel indices must be named according to a predefined pattern to properly + identify the channels and control channels when assigning values to the parametric channel indices. A channel must have a name that starts with `ch` followed by an integer. For control channels this integer can be followed by a sequence `.integer`. - Optionally, the name can end with `$integer` to specify the index of a control - for the case when a set of qubits share multiple control channels. Example of - valid channel names includes "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". + Optionally, the name can end with `$integer` to specify the index of a control channel + for the case when a set of qubits share multiple control channels. For example, + valid channel names include "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". The "." delimiter is used to specify the different qubits when looking for control channels. The optional $ delimiter is used to specify which control channel to use if several control channels work together on the same qubits. For example, if the @@ -63,7 +56,9 @@ class Calibrations: Each parameter must have a unique name within each schedule. For example, it is acceptable to have a parameter named 'amp' in the schedule 'xp' and a different parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable - to have two parameters named 'amp' in the same schedule. + to have two parameters named 'amp' in the same schedule. The Call instruction + allows parameters with the same name within the same schedule, see the example + below. The code block below illustrates the creation of a template schedule for a cross- resonance gate. @@ -131,9 +126,10 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = schedule is the default schedule for all qubits. Raises: - CalibrationError: If the parameterized channel index is not formatted - following index1.index2... or if several parameters in the same schedule - have the same name or if a channel is parameterized by more than one parameter. + CalibrationError: + - If the parameterized channel index is not formatted properly. + - If several parameters in the same schedule have the same name. + - If a channel is parameterized by more than one parameter. """ # check that channels, if parameterized, have the proper name format. param_indices = set() @@ -301,8 +297,8 @@ def add_parameter_value( Args: value: The value of the parameter to add. If an int, float, or complex is given - then the timestamp of the parameter values will automatically be generated to - correspond to the current time. + then the timestamp of the parameter values will automatically be generated + and set to the current time. 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. @@ -328,13 +324,14 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ Get the index of the parameterized channel based on 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 qubit_index1.qubit_index2... . - For example, the following parameter names are valid: 'ch1', 'ch1.0', 'ch30.12'. + 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. - pair of qubits has more than one control channel. Returns: index: The index of the channel. For example, if qubits=(10, 32) and @@ -344,9 +341,10 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: qubits (32, 10). Raises: - CalibrationError: if the number of qubits is incorrect, if the - number of inferred ControlChannels is not correct, or if ch is not - a DriveChannel, MeasureChannel, or ControlChannel. + 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)): @@ -410,7 +408,7 @@ def get_parameter_value( 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 specifies this defaults to the 'default' group. + 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 @@ -420,8 +418,8 @@ def get_parameter_value( value: The value of the parameter. Raises: - CalibrationError: if there is no parameter value for the given parameter name - and pulse channel. + CalibrationError: + - If there is no parameter value for the given parameter name and pulse channel. """ # 1) Identify the parameter object. @@ -496,7 +494,7 @@ def get_schedule( qubits: The qubits for which to get the schedule. free_params: The parameters that should remain unassigned. group: The calibration group from which to draw the - parameters. If not specifies this defaults to the 'default' group. + 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 @@ -507,8 +505,9 @@ def get_schedule( except for those specified by free_params. Raises: - CalibrationError: if the name of the schedule is not known or if a parameter could - not be found. + CalibrationError: + - If the name of the schedule is not known. + - If a parameter could not be found. """ if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] @@ -527,7 +526,7 @@ def get_schedule( schedule = schedule.assign_parameters(binding_dict, inplace=False) # Get the parameter keys by descending into the call instructions. - parameter_keys = Calibrations._get_parameter_keys(schedule, set(), qubits) + parameter_keys = CalibrationsManager._get_parameter_keys(schedule, set(), qubits) # Build the parameter binding dictionary. free_params = free_params if free_params else [] @@ -566,7 +565,7 @@ def _get_parameter_keys( """ Recursive function to extract parameter keys from a schedule. The recursive behaviour is needed to handle Call instructions. Each time a Call is found - get_parameter_keys is call on the assigned subroutine of the Call instruction + get_parameter_keys is called on the assigned subroutine of the Call instruction and the qubits that are in said subroutine. This requires carefully extracting the qubits from the subroutine and in the appropriate order. @@ -610,7 +609,9 @@ def _get_parameter_keys( for _, inst in schedule.instructions: if isinstance(inst, Call): - keys = Calibrations._get_parameter_keys(inst.assigned_subroutine(), keys, qubits_) + keys = CalibrationsManager._get_parameter_keys( + inst.assigned_subroutine(), keys, qubits_ + ) else: for param in inst.parameters: keys.add(ParameterKey(schedule.name, param.name, qubits_)) @@ -619,7 +620,7 @@ def _get_parameter_keys( def schedules(self) -> List[Dict[str, Any]]: """ - Return the schedules in self in a list of dictionaries to help + Return the managed schedules in a list of dictionaries to help users manage their schedules. Returns: diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index 57fbef8ebb..db958d1631 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -10,11 +10,11 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class to test the calibrations.""" +"""Class to test the backend calibrations.""" from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeArmonk -from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrationsManager class TestBackendCalibrations(QiskitTestCase): @@ -22,7 +22,7 @@ class TestBackendCalibrations(QiskitTestCase): def test_run_options(self): """Test that we can get run options.""" - cals = BackendCalibrations(FakeArmonk()) + cals = BackendCalibrationsManager(FakeArmonk()) - expected = {"qubit_lo_freq": [4971852852.405576], "meas_lo_freq": [6993370669.000001]} - self.assertEqual(cals.run_options(), expected) + self.assertEqual(cals.get_frequencies(False), [6993370669.000001]) + self.assertEqual(cals.get_frequencies(True), [4971852852.405576]) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index abd90d4b31..1781dfac39 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -18,7 +18,7 @@ from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase -from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey +from qiskit_experiments.calibration.calibrationsmanager import CalibrationsManager, ParameterKey from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.calibration.exceptions import CalibrationError @@ -28,7 +28,7 @@ class TestCalibrationsBasic(QiskitTestCase): def setUp(self): """Setup a test environment.""" - self.cals = Calibrations() + self.cals = CalibrationsManager() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") @@ -194,7 +194,7 @@ class TestCalibrationDefaults(QiskitTestCase): def setUp(self): """Setup a few parameters.""" - self.cals = Calibrations() + self.cals = CalibrationsManager() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") @@ -387,7 +387,7 @@ def setUp(self): pulse.call(xp) pulse.call(xp12) - self.cals = Calibrations() + self.cals = CalibrationsManager() self.cals.add_schedule(xp02) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -415,7 +415,11 @@ def test_instructions(self): class TestControlChannels(QiskitTestCase): - """Test more complex schedules such as an echoed cross-resonance.""" + """ + Test the echoed cross-resonance schedule which is more complex than single-qubit + schedules. The example also shows that a schedule with call instructions can + support parameters with the same names. + """ def setUp(self): """Create the setup we will deal with.""" @@ -423,7 +427,7 @@ def setUp(self): (3, 2): [ControlChannel(10), ControlChannel(123)], (2, 3): [ControlChannel(15), ControlChannel(23)], } - self.cals = Calibrations(control_config=controls) + self.cals = CalibrationsManager(control_config=controls) self.amp_cr = Parameter("amp") self.amp_rot = Parameter("amp_rot") From 1b6bf143c1569607e7b7fefd90e068ffd2700a05 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 30 Apr 2021 12:28:30 +0200 Subject: [PATCH 090/178] * Renamed files. --- qiskit_experiments/calibration/__init__.py | 2 +- qiskit_experiments/calibration/backend_calibrations.py | 2 +- .../{calibrationsmanager.py => calibrations_manager.py} | 0 test/calibration/test_calibrations.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename qiskit_experiments/calibration/{calibrationsmanager.py => calibrations_manager.py} (100%) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 1ad27af071..5922d57616 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -12,7 +12,7 @@ """Qiskit Experiments Calibration Root.""" -from .calibrationsmanager import CalibrationsManager +from .calibrations_manager import CalibrationsManager from .backend_calibrations import BackendCalibrationsManager from .exceptions import CalibrationError from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 7253f48d3f..a67aa4490d 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -18,7 +18,7 @@ from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Parameter -from qiskit_experiments.calibration.calibrationsmanager import CalibrationsManager, ParameterKey +from qiskit_experiments.calibration.calibrations_manager import CalibrationsManager, ParameterKey class BackendCalibrationsManager(CalibrationsManager): diff --git a/qiskit_experiments/calibration/calibrationsmanager.py b/qiskit_experiments/calibration/calibrations_manager.py similarity index 100% rename from qiskit_experiments/calibration/calibrationsmanager.py rename to qiskit_experiments/calibration/calibrations_manager.py diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 1781dfac39..6db2b8b6cf 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -18,7 +18,7 @@ from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase -from qiskit_experiments.calibration.calibrationsmanager import CalibrationsManager, ParameterKey +from qiskit_experiments.calibration.calibrations_manager import CalibrationsManager, ParameterKey from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.calibration.exceptions import CalibrationError From e9d38ab0a2dbcd5d21cd65f9dd50c5b8fea7f30c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 30 Apr 2021 12:42:34 +0200 Subject: [PATCH 091/178] * Lint --- test/calibration/test_calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 6db2b8b6cf..51b1952328 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -53,7 +53,7 @@ def setUp(self): for sched in [xp, x90p, y90p, xm]: self.cals.add_schedule(sched) - self.xm = xm + self.xm_pulse = xm # Add some parameter values. self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -101,7 +101,7 @@ def test_remove_schedule(self): self.assertEqual(len(self.cals.schedules()), 4) - self.cals.remove_schedule(self.xm) + self.cals.remove_schedule(self.xm_pulse) # Removing xm should remove the schedule but not the parameters as they are shared. self.assertEqual(len(self.cals.schedules()), 3) From 9e4ea3b64fe95414d8dc1d0fab43cd4f4119a65e Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 30 Apr 2021 19:52:13 +0200 Subject: [PATCH 092/178] Update qiskit_experiments/calibration/backend_calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index a67aa4490d..47dad7cfa1 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Store and manage the results of a calibration experiments in the context of a backend.""" +"""Store and manage the results of calibration experiments in the context of a backend.""" from datetime import datetime from typing import List From 0ab664a701e40d6a317a46b3fb97f1ac89feafa2 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:04:55 +0200 Subject: [PATCH 093/178] Update qiskit_experiments/calibration/backend_calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 47dad7cfa1..5c2b3bd28b 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -26,7 +26,7 @@ class BackendCalibrationsManager(CalibrationsManager): A Calibrations class to enable a seamless interplay with backend objects. This class enables users to export their calibrations into a backend object. Additionally, it creates frequency parameters for qubits and readout resonators. - The parameters are named `qubit_freq_est` and `meas_freq_est` to be consistent + The parameters are named `qubit_lo_freq` and `meas_lo_freq` to be consistent with the naming in backend.defaults(). These two parameters are not attached to any schedule. """ From 1803285b5f7a43f5780942c9a5bb5c11c6db5f8c Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:05:15 +0200 Subject: [PATCH 094/178] Update qiskit_experiments/calibration/backend_calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 5c2b3bd28b..3921822723 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -60,7 +60,7 @@ def get_frequencies( meas_freq: If True return the measurement frequencies otherwise return the qubit frequencies. group: The calibration group from which to draw the - parameters. If not specifies this defaults to the 'default' group. + 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 From f7ee6b9a6fec2fbce5f819519aa7e14a13a808be Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:05:27 +0200 Subject: [PATCH 095/178] Update qiskit_experiments/calibration/backend_calibrations.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/backend_calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 3921822723..457098a625 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -88,7 +88,7 @@ def get_frequencies( def export_backend(self) -> Backend: """ - Exports the calibrations in a backend object that can be used. + Exports the calibrations to a backend object that can be used. Returns: calibrated backend: A backend with the calibrations in it. From 32892d528896956bc60841cc00a60d8faae458ec Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:05:43 +0200 Subject: [PATCH 096/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index c6c88dcbc3..269e93b093 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class to store and manage the results of a calibration experiments.""" +"""Class to store and manage the results of calibration experiments.""" from collections import namedtuple, defaultdict from datetime import datetime From 3d2a052dfc85def7a7f32a9df5f36d671220d971 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:06:22 +0200 Subject: [PATCH 097/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index 269e93b093..08eda2b0ee 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -297,7 +297,7 @@ def add_parameter_value( Args: value: The value of the parameter to add. If an int, float, or complex is given - then the timestamp of the parameter values will automatically be generated + then the timestamp of the parameter value will automatically be generated and set to the current time. param: The parameter or its name for which to add the measured value. qubits: The qubits to which this parameter applies. From 8148d0ef6d1924883937a70632dd26e46916c931 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:07:36 +0200 Subject: [PATCH 098/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index 08eda2b0ee..0ec7999631 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -291,9 +291,10 @@ def add_parameter_value( schedule: Union[Schedule, 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. + 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 From 13ed686a5b2309cc1d8063733d9c74974af0299a Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:08:28 +0200 Subject: [PATCH 099/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index 0ec7999631..442d1186ec 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -351,7 +351,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: if isinstance(chan, (DriveChannel, MeasureChannel)): index = int(chan.index.name[2:].split("$")[0]) - if len(qubits) < index: + if len(qubits) <= index: raise CalibrationError(f"Not enough qubits given for channel {chan}.") return qubits[index] From 83f82a7f109149b00d3b472fed1926f780fda608 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:09:00 +0200 Subject: [PATCH 100/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index 442d1186ec..b3deeaae8a 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -396,7 +396,7 @@ def get_parameter_value( Parameters may be linked. 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 we this a list of candidate keys + 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 From b83adf3c7200d3693307b2b8cd1ce79d89a97d49 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Sat, 1 May 2021 08:10:33 +0200 Subject: [PATCH 101/178] Update qiskit_experiments/calibration/calibrations_manager.py Co-authored-by: Will Shanks --- qiskit_experiments/calibration/calibrations_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations_manager.py index b3deeaae8a..6650397bbe 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations_manager.py @@ -650,7 +650,7 @@ def parameters_table( If None is given then all channels are returned. Returns: - data: A dictionary of parameter values which can easily be converted to a + data: A list of dictionaries with parameter values and metadata which can easily be converted to a data frame. """ From 1c6d1e3df4105cb8b29415db8c23e9c602e48835 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:19:05 +0200 Subject: [PATCH 102/178] * Reverted name to Calibrations. --- qiskit_experiments/calibration/__init__.py | 4 ++-- qiskit_experiments/calibration/backend_calibrations.py | 4 ++-- .../{calibrations_manager.py => calibrations.py} | 6 +++--- test/calibration/test_backend_calibrations.py | 4 ++-- test/calibration/test_calibrations.py | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) rename qiskit_experiments/calibration/{calibrations_manager.py => calibrations.py} (99%) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 5922d57616..8efc0d6331 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -12,7 +12,7 @@ """Qiskit Experiments Calibration Root.""" -from .calibrations_manager import CalibrationsManager -from .backend_calibrations import BackendCalibrationsManager +from .calibrations import Calibrations +from .backend_calibrations import BackendCalibrations from .exceptions import CalibrationError from .parameter_value import ParameterValue diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 457098a625..22b989ed65 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -18,10 +18,10 @@ from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Parameter -from qiskit_experiments.calibration.calibrations_manager import CalibrationsManager, ParameterKey +from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey -class BackendCalibrationsManager(CalibrationsManager): +class BackendCalibrations(Calibrations): """ A Calibrations class to enable a seamless interplay with backend objects. This class enables users to export their calibrations into a backend object. diff --git a/qiskit_experiments/calibration/calibrations_manager.py b/qiskit_experiments/calibration/calibrations.py similarity index 99% rename from qiskit_experiments/calibration/calibrations_manager.py rename to qiskit_experiments/calibration/calibrations.py index 6650397bbe..1c03173311 100644 --- a/qiskit_experiments/calibration/calibrations_manager.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -29,7 +29,7 @@ ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) -class CalibrationsManager: +class Calibrations: r""" A class to manage schedules with calibrated parameter values. Schedules are intended to be fully parameterized, including the index of the channels. @@ -527,7 +527,7 @@ def get_schedule( schedule = schedule.assign_parameters(binding_dict, inplace=False) # Get the parameter keys by descending into the call instructions. - parameter_keys = CalibrationsManager._get_parameter_keys(schedule, set(), qubits) + parameter_keys = Calibrations._get_parameter_keys(schedule, set(), qubits) # Build the parameter binding dictionary. free_params = free_params if free_params else [] @@ -610,7 +610,7 @@ def _get_parameter_keys( for _, inst in schedule.instructions: if isinstance(inst, Call): - keys = CalibrationsManager._get_parameter_keys( + keys = Calibrations._get_parameter_keys( inst.assigned_subroutine(), keys, qubits_ ) else: diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index db958d1631..b0a5c4dd03 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -14,7 +14,7 @@ from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeArmonk -from qiskit_experiments.calibration.backend_calibrations import BackendCalibrationsManager +from qiskit_experiments.calibration.backend_calibrations import BackendCalibrations class TestBackendCalibrations(QiskitTestCase): @@ -22,7 +22,7 @@ class TestBackendCalibrations(QiskitTestCase): def test_run_options(self): """Test that we can get run options.""" - cals = BackendCalibrationsManager(FakeArmonk()) + cals = BackendCalibrations(FakeArmonk()) self.assertEqual(cals.get_frequencies(False), [6993370669.000001]) self.assertEqual(cals.get_frequencies(True), [4971852852.405576]) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 51b1952328..811c301594 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -18,7 +18,7 @@ from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase -from qiskit_experiments.calibration.calibrations_manager import CalibrationsManager, ParameterKey +from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey from qiskit_experiments.calibration.parameter_value import ParameterValue from qiskit_experiments.calibration.exceptions import CalibrationError @@ -28,7 +28,7 @@ class TestCalibrationsBasic(QiskitTestCase): def setUp(self): """Setup a test environment.""" - self.cals = CalibrationsManager() + self.cals = Calibrations() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") @@ -194,7 +194,7 @@ class TestCalibrationDefaults(QiskitTestCase): def setUp(self): """Setup a few parameters.""" - self.cals = CalibrationsManager() + self.cals = Calibrations() self.sigma = Parameter("σ") self.amp_xp = Parameter("amp") @@ -387,7 +387,7 @@ def setUp(self): pulse.call(xp) pulse.call(xp12) - self.cals = CalibrationsManager() + self.cals = Calibrations() self.cals.add_schedule(xp02) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -427,7 +427,7 @@ def setUp(self): (3, 2): [ControlChannel(10), ControlChannel(123)], (2, 3): [ControlChannel(15), ControlChannel(23)], } - self.cals = CalibrationsManager(control_config=controls) + self.cals = Calibrations(control_config=controls) self.amp_cr = Parameter("amp") self.amp_rot = Parameter("amp_rot") From 62d172815ac5f630b20ce3bb559ff0f1fb97af0d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:21:47 +0200 Subject: [PATCH 103/178] * Improved docstring. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 1c03173311..412aca8969 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -56,9 +56,9 @@ class Calibrations: Each parameter must have a unique name within each schedule. For example, it is acceptable to have a parameter named 'amp' in the schedule 'xp' and a different parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable - to have two parameters named 'amp' in the same schedule. The Call instruction - allows parameters with the same name within the same schedule, see the example - below. + to have two parameters named 'amp' in the same schedule. The naming restriction + only applies to parameters used in the immediate scope of the schedule. Schedules + called by Call instructions have their own scope for Parameter names. The code block below illustrates the creation of a template schedule for a cross- resonance gate. From d19dfe8d32693d681ece7da159d4a00bf2a88b14 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:32:17 +0200 Subject: [PATCH 104/178] * Added functions get_qubit_frequencies and get_meas_frequencies. --- .../calibration/backend_calibrations.py | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 22b989ed65..9a7a75dfac 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -44,31 +44,13 @@ def __init__(self, backend: Backend): self._qubits = set(range(backend.configuration().n_qubits)) self._backend = backend - def get_frequencies( + def _get_frequencies( self, meas_freq: bool, group: str = "default", cutoff_date: datetime = None, ) -> List[float]: - """ - Get the most recent qubit or measurement frequencies. These frequencies can be - passed to the run-time options of :class:`BaseExperiment`. If no calibrated value - for the frequency of a qubit is found then the default value from the backend - defaults is used. Only valid parameter values are returned. - - Args: - meas_freq: If True return the measurement frequencies otherwise return the qubit - frequencies. - 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: - A List of qubit or measurement frequencies for all qubits of the backend. - """ + """Internal helper method.""" param = self.meas_freq.name if meas_freq else self.qubit_freq.name @@ -86,6 +68,54 @@ def get_frequencies( return freqs + def get_qubit_frequencies( + self, + group: str = "default", + cutoff_date: datetime = None, + ) -> List[float]: + """ + Get the most recent qubit frequencies. They can be passed to the run-time + options of :class:`BaseExperiment`. If no calibrated frequency value of a + qubit is found then the default value from the backend defaults is used. + Only valid parameter values are returned. + + Args: + 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: + A List of qubit frequencies for all qubits of the backend. + """ + return self._get_frequencies(False, group, cutoff_date) + + def get_meas_frequencies( + self, + group: str = "default", + cutoff_date: datetime = None, + ) -> List[float]: + """ + Get the most recent measurement frequencies. They can be passed to the run-time + options of :class:`BaseExperiment`. If no calibrated frequency value of a + measurement is found then the default value from the backend defaults is used. + Only valid parameter values are returned. + + Args: + 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: + A List of measurement frequencies for all qubits of the backend. + """ + return self._get_frequencies(True, group, cutoff_date) + def export_backend(self) -> Backend: """ Exports the calibrations to a backend object that can be used. @@ -95,8 +125,8 @@ def export_backend(self) -> Backend: """ backend = copy.deepcopy(self._backend) - backend.defaults().qubit_freq_est = self.get_frequencies(False) - backend.defaults().meas_freq_est = self.get_frequencies(True) + backend.defaults().qubit_freq_est = self.get_qubit_frequencies() + backend.defaults().meas_freq_est = self.get_meas_frequencies() # TODO: build the instruction schedule map using the stored calibrations From fa5edc17b1238a26b637e8f05227dbf0ba80fc0b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:40:49 +0200 Subject: [PATCH 105/178] * Added ScheduleBlock. --- .../calibration/calibrations.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 412aca8969..756de96c18 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -218,7 +218,10 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non del self._parameter_map_r[key] def _register_parameter( - self, parameter: Parameter, schedule: Schedule = None, qubits: Tuple = None + self, + parameter: Parameter, + schedule: Union[Schedule, ScheduleBlock] = None, + qubits: Tuple = None, ): """ Registers a parameter for the given schedule. This allows self to determine the @@ -227,7 +230,7 @@ def _register_parameter( Args: parameter: The parameter to register. - schedule: The Schedule to which this parameter belongs. The schedule can + schedule: The schedule to which this parameter belongs. The schedule can be None which allows the calibration to accommodate, e.g. qubit frequencies. qubits: The qubits for which to register the parameter. """ @@ -288,12 +291,12 @@ def add_parameter_value( value: Union[int, float, complex, ParameterValue], param: Union[Parameter, str], qubits: Tuple[int, ...] = None, - schedule: Union[Schedule, str] = None, + schedule: Union[Schedule, ScheduleBlock, str] = None, ): """ Add a parameter value to the stored parameters. - - This parameter value may be applied to several channels, for instance, all + + This parameter value may be applied to several channels, for instance, all DRAG pulses may have the same standard deviation. Args: @@ -312,7 +315,7 @@ def add_parameter_value( value = ParameterValue(value, datetime.now()) param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, Schedule) else schedule + sched_name = schedule.name if isinstance(schedule, (Schedule, ScheduleBlock)) else schedule registered_schedules = set(key.schedule for key in self._schedules) @@ -385,7 +388,7 @@ def get_parameter_value( self, param: Union[Parameter, str], qubits: Tuple[int, ...], - schedule: Union[Schedule, str, None] = None, + schedule: Union[Schedule, ScheduleBlock, str, None] = None, valid_only: bool = True, group: str = "default", cutoff_date: datetime = None, @@ -425,7 +428,7 @@ def get_parameter_value( # 1) Identify the parameter object. param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, Schedule) else schedule + sched_name = schedule.name if isinstance(schedule, (Schedule, ScheduleBlock)) else schedule param = self.calibration_parameter(param_name, qubits, sched_name) @@ -486,7 +489,7 @@ def get_schedule( free_params: List[str] = None, group: Optional[str] = "default", cutoff_date: datetime = None, - ) -> Schedule: + ) -> Union[Schedule, ScheduleBlock]: """ Get the schedule with the non-free parameters assigned to their values. @@ -559,7 +562,7 @@ def get_schedule( @staticmethod def _get_parameter_keys( - schedule: Schedule, + schedule: Union[Schedule, ScheduleBlock], keys: Set[ParameterKey], qubits: Tuple[int, ...], ) -> Set[ParameterKey]: @@ -610,9 +613,7 @@ def _get_parameter_keys( for _, inst in schedule.instructions: if isinstance(inst, Call): - keys = Calibrations._get_parameter_keys( - inst.assigned_subroutine(), keys, qubits_ - ) + keys = Calibrations._get_parameter_keys(inst.assigned_subroutine(), keys, qubits_) else: for param in inst.parameters: keys.add(ParameterKey(schedule.name, param.name, qubits_)) @@ -636,7 +637,7 @@ def schedules(self) -> List[Dict[str, Any]]: def parameters_table( self, parameters: List[str] = None, - schedules: List[Union[Schedule, str]] = None, + schedules: List[Union[Schedule, ScheduleBlock, str]] = None, qubit_list: List[Tuple[int, ...]] = None, ) -> List[Dict[str, Any]]: """ @@ -658,7 +659,9 @@ def parameters_table( # Convert inputs to lists of strings if schedules is not None: - schedules = {sdl.name if isinstance(sdl, Schedule) else sdl for sdl in schedules} + schedules = { + sdl.name if isinstance(sdl, (Schedule, ScheduleBlock)) else sdl for sdl in schedules + } # Look for exact matches. Default values will be ignored. keys = set() From 5033b4824311807917f638352db55870ace062ae Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:45:15 +0200 Subject: [PATCH 106/178] * Removed candidate_default_keys. --- qiskit_experiments/calibration/calibrations.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 756de96c18..5ea365a733 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -446,16 +446,9 @@ def get_parameter_value( # If no candidate parameter values were found look for default parameters # i.e. parameters that do not specify a qubit. if len(candidates) == 0: - candidate_default_keys = [] - for key in candidate_keys: - candidate_default_keys.append(ParameterKey(key.schedule, key.parameter, None)) - - candidate_default_keys = set(candidate_default_keys) - - for key in set(candidate_default_keys): - if key in self._params: - candidates += self._params[key] + if ParameterKey(key.schedule, key.parameter, None) in self._params: + candidates += self._params[ParameterKey(key.schedule, key.parameter, None)] # 4) Filter candidate parameter values. if valid_only: From f067c7d6de0bbefcbd0a5b103030e653b4b00760 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 08:48:00 +0200 Subject: [PATCH 107/178] * Improved the error message. --- qiskit_experiments/calibration/calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5ea365a733..ab3811cf5b 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -465,10 +465,11 @@ def get_parameter_value( if qubits: msg += f"on qubits {qubits} " - msg += f"in schedule {sched_name}" + if sched_name: + msg += f"in schedule {sched_name} " if cutoff_date: - msg += f" Cutoff date: {cutoff_date}" + msg += f"with cutoff date: {cutoff_date}" raise CalibrationError(msg) From 23402a713b157b7983901bbf670c3136f0a65a50 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 1 May 2021 09:03:55 +0200 Subject: [PATCH 108/178] * Improved docstring. --- qiskit_experiments/calibration/calibrations.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index ab3811cf5b..214b8e1e87 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -600,7 +600,7 @@ def _get_parameter_keys( f"non-channel parameters. {chan} is parametric." ) ) - if isinstance(chan, DriveChannel): + if isinstance(chan, (DriveChannel, MeasureChannel)): qubit_set.add(chan.index) qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) @@ -620,7 +620,12 @@ def schedules(self) -> List[Dict[str, Any]]: users manage their schedules. Returns: - data: A list of dictionaries with all the schedules in it. + data: A list of dictionaries with all the schedules in it. The key-value pairs are + - 'qubits': the qubits to which this schedule applies. This may be None if the + schedule is the default for all qubits. + - 'schedule': The schedule (either a Schedule or a ScheduleBlock). + - 'parameters': The parameters in the schedule exposed for convenience. + This list of dictionaries can easily be converted to a data frame. """ data = [] for key, sched in self._schedules.items(): From 4160914e03b843e543df2add11042a240a99983b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 2 May 2021 20:40:04 +0200 Subject: [PATCH 109/178] * Added measurement schedule tests. --- test/calibration/test_calibrations.py | 57 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 811c301594..a12cef022b 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -14,7 +14,7 @@ from datetime import datetime from qiskit.circuit import Parameter -from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare +from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare, MeasureChannel, Play from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase @@ -365,6 +365,61 @@ def test_parameter_filtering(self): self.assertEqual(len(amp_values), 2) +class TestMeasurements(QiskitTestCase): + """Test that schedules on measure channels are handled properly.""" + + def setUp(self): + """Create the setting to test.""" + self.amp = Parameter("amp") + self.amp_xp = Parameter("amp") + self.sigma = Parameter("σ") + self.sigma_xp = Parameter("σ") + self.width = Parameter("w") + self.duration = 8000 + self.duration_xp = 160 + ch0 = Parameter("ch0") + self.m0_ = MeasureChannel(ch0) + self.d0_ = DriveChannel(ch0) + + with pulse.build(name="meas") as meas: + pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) + + with pulse.build(name="xp_meas") as xp_meas: + pulse.play(Gaussian(self.duration_xp, self.amp_xp, self.sigma_xp), self.d0_) + pulse.call(meas) + + self.cals = Calibrations() + self.cals.add_schedule(meas) + self.cals.add_schedule(xp_meas) + + #self.cals.add_parameter_value(8000, self.duration, schedule="meas") + self.cals.add_parameter_value(0.5, self.amp, (0, ), "meas") + self.cals.add_parameter_value(0.3, self.amp, (2,), "meas") + self.cals.add_parameter_value(160, self.sigma, schedule="meas") + self.cals.add_parameter_value(7000, self.width, schedule="meas") + + self.cals.add_parameter_value(0.9, self.amp_xp, (0, ), "xp_meas") + self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp_meas") + + def test_meas_schedule(self): + """Test that we get a properly assigned measure schedule.""" + sched = self.cals.get_schedule("meas", (0, )) + meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) + self.assertTrue(sched.instructions[0][1], meas) + + sched = self.cals.get_schedule("meas", (2, )) + meas = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(0)) + self.assertTrue(sched.instructions[0][1], meas) + + def test_call_meas(self): + """Test that we can call a measurement pulse.""" + sched = self.cals.get_schedule("xp_meas", (0, )) + xp = Play(Gaussian(160, 0.9, 40), DriveChannel(0)) + meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) + + self.assertTrue(sched.instructions[0][1], xp) + self.assertTrue(sched.instructions[1][1], meas) + class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" From 84b49c91d597f299fcd87c3a0d05dff0ce1ff92c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 2 May 2021 21:04:25 +0200 Subject: [PATCH 110/178] * Added double call test. --- test/calibration/test_calibrations.py | 39 ++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index a12cef022b..9618b98d9a 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -378,19 +378,32 @@ def setUp(self): self.duration = 8000 self.duration_xp = 160 ch0 = Parameter("ch0") + ch1 = Parameter("ch1") self.m0_ = MeasureChannel(ch0) self.d0_ = DriveChannel(ch0) with pulse.build(name="meas") as meas: pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) - with pulse.build(name="xp_meas") as xp_meas: + with pulse.build(name="xp") as xp: pulse.play(Gaussian(self.duration_xp, self.amp_xp, self.sigma_xp), self.d0_) + + with pulse.build(name="xp_meas") as xp_meas: + pulse.call(xp) pulse.call(meas) + with pulse.build(name="xt_meas") as xt_meas: + with pulse.align_sequential(): + pulse.call(xp) + pulse.call(meas) + with pulse.align_sequential(): + pulse.call(xp, value_dict={ch0: ch1}) + pulse.call(meas, value_dict={ch0: ch1}) + self.cals = Calibrations() self.cals.add_schedule(meas) self.cals.add_schedule(xp_meas) + self.cals.add_schedule(xt_meas) #self.cals.add_parameter_value(8000, self.duration, schedule="meas") self.cals.add_parameter_value(0.5, self.amp, (0, ), "meas") @@ -398,8 +411,9 @@ def setUp(self): self.cals.add_parameter_value(160, self.sigma, schedule="meas") self.cals.add_parameter_value(7000, self.width, schedule="meas") - self.cals.add_parameter_value(0.9, self.amp_xp, (0, ), "xp_meas") - self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp_meas") + self.cals.add_parameter_value(0.9, self.amp_xp, (0, ), "xp") + self.cals.add_parameter_value(0.7, self.amp_xp, (2,), "xp") + self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp") def test_meas_schedule(self): """Test that we get a properly assigned measure schedule.""" @@ -420,6 +434,25 @@ def test_call_meas(self): self.assertTrue(sched.instructions[0][1], xp) self.assertTrue(sched.instructions[1][1], meas) + def test_xt_meas(self): + """Test that creating multi-qubit schedules out of calls works.""" + + sched = self.cals.get_schedule("xt_meas", (0, 2)) + + xp0 = Play(Gaussian(160, 0.9, 40), DriveChannel(0)) + xp2 = Play(Gaussian(160, 0.7, 40), DriveChannel(2)) + + meas0 = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) + meas2 = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) + + sched = inline_subroutines(sched) + + self.assertEqual(sched.instructions[0][1], xp0) + self.assertEqual(sched.instructions[1][1], xp2) + self.assertEqual(sched.instructions[2][1], meas0) + self.assertEqual(sched.instructions[3][1], meas2) + + class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" From 4e9f26611df961ffe7f139b2598a8f2c999cf42b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 3 May 2021 21:30:05 +0200 Subject: [PATCH 111/178] * Added recursive assign to handle multiple Calls of the same schedule on different channels. --- .../calibration/calibrations.py | 147 ++++++++++-------- test/calibration/test_calibrations.py | 47 +++--- 2 files changed, 110 insertions(+), 84 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 214b8e1e87..b0115f7a97 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -523,74 +523,55 @@ def get_schedule( # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) - # Get the parameter keys by descending into the call instructions. - parameter_keys = Calibrations._get_parameter_keys(schedule, set(), qubits) + return self._assign(schedule, qubits, free_params, group, cutoff_date) - # Build the parameter binding dictionary. - free_params = free_params if free_params else [] - - for key in parameter_keys: - if key.parameter not in free_params: - # 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 None. - if key in self._parameter_map: - param = self._parameter_map[key] - elif (key.schedule, key.parameter, None) in self._parameter_map: - param = self._parameter_map[(key.schedule, key.parameter, None)] - else: - raise CalibrationError( - f"Bad calibrations {key} is not present and has no default value." - ) + def _assign( + self, + schedule, + qubits: Tuple[int, ...], + free_params: List[str] = None, + group: Optional[str] = "default", + cutoff_date: datetime = None, + ) -> Union[Schedule, ScheduleBlock]: + """ + Recursive function to extract and assign parameters from 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, - 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, - ) + .. code-block:: python - return schedule.assign_parameters(binding_dict, inplace=False) + ch0 = Parameter("ch0") + ch1 = Parameter("ch1") - @staticmethod - def _get_parameter_keys( - schedule: Union[Schedule, ScheduleBlock], - keys: Set[ParameterKey], - qubits: Tuple[int, ...], - ) -> Set[ParameterKey]: - """ - Recursive function to extract parameter keys from a schedule. The recursive - behaviour is needed to handle Call instructions. Each time a Call is found - get_parameter_keys is called on the assigned subroutine of the Call instruction - and the qubits that are in said subroutine. This requires carefully - extracting the qubits from the subroutine and in the appropriate order. + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(duration, amp, sigma), DriveChannel(ch0)) - Args: - schedule: A schedule from which to extract parameters. - keys: A set of keys recursively populated. - qubits: The qubits for which we want the schedule. + with pulse.build(name="xt_xp") as xt: + pulse.call(xp) + pulse.call(xp, value_dict={ch0: ch1}) - Returns: - keys: The set of keys populated with schedule name, parameter name, qubits. + Here, we define the xp schedule for all qubits as a Gaussian. Next, we define a + schedule where both xp schedules are called simultaneously on different channels. - Raises: - CalibrationError: If a channel index is parameterized. + 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. + free_params: The parameters that are to be left free. + 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. """ - # schedule.channels may give the qubits in any order. This order matters. For example, - # the parameter ('cr', 'amp', (2, 3)) is not the same as ('cr', 'amp', (3, 2)). - # Furthermore, as we call subroutines the list of qubits involved shrinks. For - # example, a cross-resonance schedule could be - # - # pulse.call(xp) - # ... - # pulse.play(GaussianSquare(...), ControlChannel(X)) - # - # Here, the call instruction might, e.g., only involve qubit 2 while the play instruction - # will apply to qubits (2, 3). - + # 1) Restrict the given qubits to those in the given schedule. qubit_set = set() for chan in schedule.channels: if isinstance(chan.index, ParameterExpression): @@ -605,14 +586,50 @@ def _get_parameter_keys( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) - for _, inst in schedule.instructions: + # 2) Recursively assign the parameters in the called instructions. + ret_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) + for t0, inst in schedule.instructions: if isinstance(inst, Call): - keys = Calibrations._get_parameter_keys(inst.assigned_subroutine(), keys, qubits_) - else: - for param in inst.parameters: - keys.add(ParameterKey(schedule.name, param.name, qubits_)) + inst = self._assign( + inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date + ) + + ret_schedule.insert(t0, inst, inplace=True) + + # 3) Get the parameter keys of the remaining instructions. + keys = set() + for _, inst in ret_schedule.instructions: + for param in inst.parameters: + keys.add(ParameterKey(ret_schedule.name, param.name, qubits_)) + + # 4) Build the parameter binding dictionary. + free_params = free_params if free_params else [] + + binding_dict = {} + for key in keys: + if key.parameter not in free_params: + # 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 None. + if key in self._parameter_map: + param = self._parameter_map[key] + elif (key.schedule, key.parameter, None) in self._parameter_map: + param = self._parameter_map[(key.schedule, key.parameter, None)] + else: + raise CalibrationError( + f"Bad calibrations {key} is not present and has no default value." + ) + + 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 keys + return ret_schedule.assign_parameters(binding_dict, inplace=False) def schedules(self) -> List[Dict[str, Any]]: """ diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9618b98d9a..5130a91514 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -14,7 +14,15 @@ from datetime import datetime from qiskit.circuit import Parameter -from qiskit.pulse import Drag, DriveChannel, ControlChannel, Gaussian, GaussianSquare, MeasureChannel, Play +from qiskit.pulse import ( + Drag, + DriveChannel, + ControlChannel, + Gaussian, + GaussianSquare, + MeasureChannel, + Play, +) from qiskit.pulse.transforms import inline_subroutines import qiskit.pulse as pulse from qiskit.test import QiskitTestCase @@ -405,29 +413,29 @@ def setUp(self): self.cals.add_schedule(xp_meas) self.cals.add_schedule(xt_meas) - #self.cals.add_parameter_value(8000, self.duration, schedule="meas") - self.cals.add_parameter_value(0.5, self.amp, (0, ), "meas") + # self.cals.add_parameter_value(8000, self.duration, schedule="meas") + self.cals.add_parameter_value(0.5, self.amp, (0,), "meas") self.cals.add_parameter_value(0.3, self.amp, (2,), "meas") self.cals.add_parameter_value(160, self.sigma, schedule="meas") self.cals.add_parameter_value(7000, self.width, schedule="meas") - self.cals.add_parameter_value(0.9, self.amp_xp, (0, ), "xp") + self.cals.add_parameter_value(0.9, self.amp_xp, (0,), "xp") self.cals.add_parameter_value(0.7, self.amp_xp, (2,), "xp") self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp") def test_meas_schedule(self): """Test that we get a properly assigned measure schedule.""" - sched = self.cals.get_schedule("meas", (0, )) + sched = self.cals.get_schedule("meas", (0,)) meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) self.assertTrue(sched.instructions[0][1], meas) - sched = self.cals.get_schedule("meas", (2, )) + sched = self.cals.get_schedule("meas", (2,)) meas = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(0)) self.assertTrue(sched.instructions[0][1], meas) def test_call_meas(self): """Test that we can call a measurement pulse.""" - sched = self.cals.get_schedule("xp_meas", (0, )) + sched = self.cals.get_schedule("xp_meas", (0,)) xp = Play(Gaussian(160, 0.9, 40), DriveChannel(0)) meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) @@ -445,8 +453,6 @@ def test_xt_meas(self): meas0 = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) meas2 = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) - sched = inline_subroutines(sched) - self.assertEqual(sched.instructions[0][1], xp0) self.assertEqual(sched.instructions[1][1], xp2) self.assertEqual(sched.instructions[2][1], meas0) @@ -527,8 +533,11 @@ def setUp(self): self.width = Parameter("w") self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - cr_tone = GaussianSquare(640, self.amp_cr, self.sigma, self.width) - rotary = GaussianSquare(640, self.amp_rot, self.sigma, self.width) + cr_tone_p = GaussianSquare(640, self.amp_cr, self.sigma, self.width) + rotary_p = GaussianSquare(640, self.amp_rot, self.sigma, self.width) + + cr_tone_m = GaussianSquare(640, -self.amp_cr, self.sigma, self.width) + rotary_m = GaussianSquare(640, -self.amp_rot, self.sigma, self.width) with pulse.build(name="xp") as xp: pulse.play(Gaussian(160, self.amp, self.sigma), self.d0_) @@ -536,12 +545,12 @@ def setUp(self): with pulse.build(name="cr") as cr: with pulse.align_sequential(): with pulse.align_left(): - pulse.play(rotary, self.d1_) # Rotary tone - pulse.play(cr_tone, self.c1_) # CR tone. + pulse.play(rotary_p, self.d1_) # Rotary tone + pulse.play(cr_tone_p, self.c1_) # CR tone. pulse.call(xp) with pulse.align_left(): - pulse.play(rotary, self.d1_) - pulse.play(cr_tone, self.c1_) + pulse.play(rotary_m, self.d1_) + pulse.play(cr_tone_m, self.c1_) pulse.call(xp) self.cals.add_schedule(xp) @@ -569,8 +578,8 @@ def test_get_schedule(self): pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) with pulse.align_left(): - pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone - pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. + pulse.play(GaussianSquare(640, -0.2, 40, 20), DriveChannel(2)) # Rotary tone + pulse.play(GaussianSquare(640, -0.3, 40, 20), ControlChannel(10)) # CR tone. pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) # We inline to make the schedules comparable with the construction directly above. @@ -589,8 +598,8 @@ def test_get_schedule(self): pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) with pulse.align_left(): - pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone - pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. + pulse.play(GaussianSquare(640, -0.4, 40, 30), DriveChannel(3)) # Rotary tone + pulse.play(GaussianSquare(640, -0.5, 40, 30), ControlChannel(15)) # CR tone. pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) schedule = self.cals.get_schedule("cr", (2, 3)) From d99c429b51687d2d332e81893c893b61ec546ded Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 4 May 2021 19:21:00 +0200 Subject: [PATCH 112/178] * Added support for parametric durations: - This required handling Schedule and ScheduleBlock seprately in _assign. - Called instructions are now registered separately too. --- .../calibration/calibrations.py | 123 ++++++++++++++---- test/calibration/test_calibrations.py | 72 +++++++--- 2 files changed, 156 insertions(+), 39 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b0115f7a97..7f3ce59868 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -19,7 +19,15 @@ import dataclasses import regex as re -from qiskit.pulse import Schedule, ScheduleBlock, DriveChannel, ControlChannel, MeasureChannel, Call +from qiskit.pulse import ( + Schedule, + ScheduleBlock, + DriveChannel, + ControlChannel, + MeasureChannel, + Call, + Instruction, +) from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter, ParameterExpression from qiskit_experiments.calibration.exceptions import CalibrationError @@ -120,6 +128,8 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = """ Add a schedule and register its parameters. + Schedules that use Call instructions must register the called schedules separately. + Args: schedule: The schedule to add. qubits: The qubits for which to add the schedules. If None is given then this @@ -130,8 +140,15 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = - If the parameterized channel index is not formatted properly. - If several parameters in the same schedule have the same name. - If a channel is parameterized by more than one parameter. + - If the schedule name starts with the prefix of ScheduleBlock. """ # check that channels, if parameterized, have the proper name format. + if schedule.name.startswith(ScheduleBlock.prefix): + raise CalibrationError( + f"A registered schedule name cannot start with {ScheduleBlock.prefix} " + f"received {schedule.name}." + ) + param_indices = set() for ch in schedule.channels: if isinstance(ch.index, Parameter): @@ -150,16 +167,10 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = # Add the schedule. self._schedules[ScheduleKey(schedule.name, qubits)] = schedule - # Register the subroutines in call instructions - for _, inst in schedule.instructions: - if isinstance(inst, Call): - self.add_schedule(inst.subroutine, qubits) - # Register parameters that are not indices. - # Do not register parameters that are in call instructions. These parameters - # will have been registered above. + # Do not register parameters that are in call instructions. params_to_register = set() - for _, inst in schedule.instructions: + for inst in self._exclude_calls(schedule, []): if not isinstance(inst, Call): for param in inst.parameters: if param not in param_indices: @@ -171,6 +182,37 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = for param in params_to_register: self._register_parameter(param, schedule, qubits) + def _exclude_calls( + self, schedule: Union[Schedule, ScheduleBlock], instructions: List[Instruction] + ) -> List[Instruction]: + """ + Recursive function to get all non-Call instructions. This will flatten all blocks + in a ScheduleBlock and return the instructions of the ScheduleBlock leaving out + any Call instructions. For a Schedule this is done by a simple loop of the + instructions of the schedule. + + Args: + schedule: A Schedule or ScheduleBlock from which to extract the instructions. + instructions: The list of instructions that is recursively populated. + + Returns: + The list of instructions to which all non-Call instructions have been added. + """ + if isinstance(schedule, Schedule): + for _, inst in schedule.instructions: + if not isinstance(inst, Call): + instructions.append(inst) + + if isinstance(schedule, ScheduleBlock): + for block in schedule.blocks: + if isinstance(block, ScheduleBlock): + instructions = self._exclude_calls(block, instructions) + else: + if not isinstance(block, Call): + instructions.append(block) + + return instructions + def remove_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): """ Allows users to remove a schedule from the calibrations. The history of the parameters @@ -480,7 +522,7 @@ def get_schedule( self, name: str, qubits: Tuple[int, ...], - free_params: List[str] = None, + free_params: List[Tuple[str, str, Tuple]] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> Union[Schedule, ScheduleBlock]: @@ -527,7 +569,7 @@ def get_schedule( def _assign( self, - schedule, + schedule: Union[Schedule, ScheduleBlock], qubits: Tuple[int, ...], free_params: List[str] = None, group: Optional[str] = "default", @@ -569,7 +611,18 @@ def _assign( 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 schedule is not a Schedule or ScheduleBlock. + - If a channel has not been assigned. + - If a parameter that is needed does not have a value. """ + if not isinstance(schedule, (Schedule, ScheduleBlock)): + raise CalibrationError(f"{schedule.name} is not a Schedule or a ScheduleBlock.") # 1) Restrict the given qubits to those in the given schedule. qubit_set = set() @@ -587,19 +640,41 @@ def _assign( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) # 2) Recursively assign the parameters in the called instructions. - ret_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) - for t0, inst in schedule.instructions: - if isinstance(inst, Call): - inst = self._assign( - inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date - ) + if isinstance(schedule, Schedule): + ret_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) + + for t0, inst in schedule.instructions: + if isinstance(inst, Call): + inst = self._assign( + inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date + ) - ret_schedule.insert(t0, inst, inplace=True) + ret_schedule.insert(t0, inst, inplace=True) - # 3) Get the parameter keys of the remaining instructions. + else: + ret_schedule = ScheduleBlock( + alignment_context=schedule.alignment_context, + name=schedule.name, + metadata=schedule.metadata, + ) + + for inst in schedule.blocks: + if isinstance(inst, Call): + inst = self._assign( + inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date + ) + elif isinstance(inst, ScheduleBlock): + inst = self._assign(inst, qubits_, free_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() - for _, inst in ret_schedule.instructions: - for param in inst.parameters: + + if ret_schedule.name in set(key.schedule for key in self._parameter_map): + for param in ret_schedule.parameters: keys.add(ParameterKey(ret_schedule.name, param.name, qubits_)) # 4) Build the parameter binding dictionary. @@ -607,7 +682,7 @@ def _assign( binding_dict = {} for key in keys: - if key.parameter not in free_params: + if key not in free_params: # 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 None. @@ -667,8 +742,8 @@ def parameters_table( If None is given then all channels are returned. Returns: - data: A list of dictionaries with parameter values and metadata which can easily be converted to a - data frame. + data: A list of dictionaries with parameter values and metadata which can + easily be converted to a data frame. """ data = [] diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 5130a91514..e726a34f63 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -23,7 +23,7 @@ MeasureChannel, Play, ) -from qiskit.pulse.transforms import inline_subroutines +from qiskit.pulse.transforms import inline_subroutines, block_to_schedule import qiskit.pulse as pulse from qiskit.test import QiskitTestCase from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey @@ -44,19 +44,20 @@ def setUp(self): self.amp_y90p = Parameter("amp") self.beta = Parameter("β") self.drive = DriveChannel(Parameter("ch0")) + self.duration = Parameter("dur") # Define and add template schedules. with pulse.build(name="xp") as xp: - pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), self.drive) + pulse.play(Drag(self.duration, self.amp_xp, self.sigma, self.beta), self.drive) with pulse.build(name="xm") as xm: - pulse.play(Drag(160, -self.amp_xp, self.sigma, self.beta), self.drive) + pulse.play(Drag(self.duration, -self.amp_xp, self.sigma, self.beta), self.drive) with pulse.build(name="x90p") as x90p: - pulse.play(Drag(160, self.amp_x90p, self.sigma, self.beta), self.drive) + pulse.play(Drag(self.duration, self.amp_x90p, self.sigma, self.beta), self.drive) with pulse.build(name="y90p") as y90p: - pulse.play(Drag(160, self.amp_y90p, self.sigma, self.beta), self.drive) + pulse.play(Drag(self.duration, self.amp_y90p, self.sigma, self.beta), self.drive) for sched in [xp, x90p, y90p, xm]: self.cals.add_schedule(sched) @@ -67,6 +68,7 @@ def setUp(self): self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") + self.cals.add_parameter_value(ParameterValue(160, self.date_time), "dur", schedule="xp") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "x90p") self.cals.add_parameter_value(ParameterValue(0.08, self.date_time), "amp", (3,), "y90p") @@ -123,12 +125,12 @@ def test_remove_schedule(self): self.cals.add_schedule(sched) self.assertEqual(len(self.cals.schedules()), 4) - self.assertEqual(len(self.cals.parameters), 6) + self.assertEqual(len(self.cals.parameters), 7) self.cals.remove_schedule(sched) self.assertEqual(len(self.cals.schedules()), 3) - self.assertEqual(len(self.cals.parameters), 5) + self.assertEqual(len(self.cals.parameters), 6) for param in [self.sigma, self.amp_xp, self.amp_x90p, self.amp_y90p, self.beta]: self.assertTrue(param in self.cals.parameters) @@ -169,12 +171,12 @@ def test_channel_names(self): control_bad = ControlChannel(Parameter("u_chan")) control_good = ControlChannel(Parameter("ch1.0")) - with pulse.build() as sched_good: + with pulse.build(name="good_sched") as sched_good: pulse.play(Drag(160, 0.1, 40, 2), drive_0) pulse.play(Drag(160, 0.1, 40, 2), drive_1) pulse.play(Drag(160, 0.1, 40, 2), control_good) - with pulse.build() as sched_bad: + with pulse.build(name="bad_sched") as sched_bad: pulse.play(Drag(160, 0.1, 40, 2), drive_0) pulse.play(Drag(160, 0.1, 40, 2), drive_1) pulse.play(Drag(160, 0.1, 40, 2), control_bad) @@ -196,6 +198,13 @@ def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" self.cals._register_parameter(Parameter("a")) + def test_free_parameters(self): + """Test that we can get a schedule with a free parameter.""" + xp = self.cals.get_schedule("xp", (3, ), free_params=[("xp","amp", (3,))]) + self.assertEqual(xp.parameters, {self.amp_xp}) + + xp = self.cals.get_schedule("xp", (3, ), free_params=[("xp", "amp", (3,)), ("xp", "σ", (3,))]) + self.assertEqual(xp.parameters, {self.amp_xp, self.sigma}) class TestCalibrationDefaults(QiskitTestCase): """Test that we can override defaults.""" @@ -210,14 +219,15 @@ def setUp(self): self.beta = Parameter("β") self.drive = DriveChannel(Parameter("ch0")) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + self.duration = Parameter("dur") # Template schedule for qubit 3 with pulse.build(name="xp") as xp_drag: - pulse.play(Drag(160, self.amp_xp, self.sigma, self.beta), self.drive) + pulse.play(Drag(self.duration, self.amp_xp, self.sigma, self.beta), self.drive) # Default template schedule for all qubits with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) + pulse.play(Gaussian(self.duration, self.amp, self.sigma), self.drive) # Add the schedules self.cals.add_schedule(xp) @@ -263,6 +273,7 @@ def _add_parameters(self): self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") + self.cals.add_parameter_value(160, "dur", schedule="xp") def test_default_schedules(self): """ @@ -302,7 +313,7 @@ def test_default_schedules(self): self.assertEqual(xp3.instructions[0][1].pulse.sigma, 50) # Check that we have the expected parameters in the calibrations. - expected = {self.amp_xp, self.amp, self.sigma, self.beta} + expected = {self.amp_xp, self.amp, self.sigma, self.beta, self.duration} self.assertEqual(len(set(self.cals.parameters.keys())), len(expected)) def test_replace_schedule(self): @@ -383,8 +394,8 @@ def setUp(self): self.sigma = Parameter("σ") self.sigma_xp = Parameter("σ") self.width = Parameter("w") - self.duration = 8000 - self.duration_xp = 160 + self.duration = Parameter("dur") + self.duration_xp = Parameter("dur") ch0 = Parameter("ch0") ch1 = Parameter("ch1") self.m0_ = MeasureChannel(ch0) @@ -410,6 +421,7 @@ def setUp(self): self.cals = Calibrations() self.cals.add_schedule(meas) + self.cals.add_schedule(xp) self.cals.add_schedule(xp_meas) self.cals.add_schedule(xt_meas) @@ -418,13 +430,15 @@ def setUp(self): self.cals.add_parameter_value(0.3, self.amp, (2,), "meas") self.cals.add_parameter_value(160, self.sigma, schedule="meas") self.cals.add_parameter_value(7000, self.width, schedule="meas") + self.cals.add_parameter_value(8000, self.duration, schedule="meas") self.cals.add_parameter_value(0.9, self.amp_xp, (0,), "xp") self.cals.add_parameter_value(0.7, self.amp_xp, (2,), "xp") self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp") + self.cals.add_parameter_value(160, self.duration_xp, schedule="xp") def test_meas_schedule(self): - """Test that we get a properly assigned measure schedule.""" + """Test that we get a properly assigned measure schedule without drive channels.""" sched = self.cals.get_schedule("meas", (0,)) meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) self.assertTrue(sched.instructions[0][1], meas) @@ -458,6 +472,25 @@ def test_xt_meas(self): self.assertEqual(sched.instructions[2][1], meas0) self.assertEqual(sched.instructions[3][1], meas2) + def test_free_parameters(self): + """Test that we can get a schedule with free parameters.""" + + schedule = self.cals.get_schedule("xt_meas", (0, 2), free_params=[("xp", "amp", (0,))]) + schedule = block_to_schedule(schedule) + + with pulse.build(name="xt_meas") as expected: + with pulse.align_sequential(): + pulse.play(Gaussian(160, self.amp_xp, 40), DriveChannel(0)) + pulse.play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) + with pulse.align_sequential(): + pulse.play(Gaussian(160, 0.7, 40), DriveChannel(2)) + pulse.play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) + + expected = block_to_schedule(expected) + + self.assertEqual(schedule.parameters, {self.amp_xp}) + self.assertEqual(schedule, expected) + class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" @@ -482,6 +515,8 @@ def setUp(self): pulse.call(xp12) self.cals = Calibrations() + self.cals.add_schedule(xp) + self.cals.add_schedule(xp12) self.cals.add_schedule(xp02) self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") @@ -608,3 +643,10 @@ def test_get_schedule(self): self.assertTrue(inst == cr_23.instructions[idx]) self.assertEqual(schedule.parameters, set()) + + def test_free_parameters(self): + """Test that we can get a schedule with free parameters.""" + + schedule = self.cals.get_schedule("cr", (3, 2), free_params=[("cr", "amp", (3, 2))]) + + self.assertEqual(schedule.parameters, {self.amp_cr}) From a743a9a03b776bfec9ce2967828911dc5947902e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:10:41 +0200 Subject: [PATCH 113/178] * Added tests on the parameter filtering. --- test/calibration/test_calibrations.py | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index e726a34f63..1bc19b143b 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -36,6 +36,8 @@ class TestCalibrationsBasic(QiskitTestCase): def setUp(self): """Setup a test environment.""" + super().setUp() + self.cals = Calibrations() self.sigma = Parameter("σ") @@ -211,6 +213,8 @@ class TestCalibrationDefaults(QiskitTestCase): def setUp(self): """Setup a few parameters.""" + super().setUp() + self.cals = Calibrations() self.sigma = Parameter("σ") @@ -389,6 +393,8 @@ class TestMeasurements(QiskitTestCase): def setUp(self): """Create the setting to test.""" + super().setUp() + self.amp = Parameter("amp") self.amp_xp = Parameter("amp") self.sigma = Parameter("σ") @@ -497,6 +503,8 @@ class TestInstructions(QiskitTestCase): def setUp(self): """Create the setting to test.""" + super().setUp() + self.phase = Parameter("φ") self.freq = Parameter("ν") self.d0_ = DriveChannel(Parameter("ch0")) @@ -552,6 +560,8 @@ class TestControlChannels(QiskitTestCase): def setUp(self): """Create the setup we will deal with.""" + super().setUp() + controls = { (3, 2): [ControlChannel(10), ControlChannel(123)], (2, 3): [ControlChannel(15), ControlChannel(23)], @@ -650,3 +660,51 @@ def test_free_parameters(self): schedule = self.cals.get_schedule("cr", (3, 2), free_params=[("cr", "amp", (3, 2))]) self.assertEqual(schedule.parameters, {self.amp_cr}) + + +class TestFiltering(QiskitTestCase): + """Test that the filtering works as expected.""" + + def setUp(self): + """Setup a calibration.""" + super().setUp() + + self.cals = Calibrations() + + self.sigma = Parameter("σ") + self.amp = Parameter("amp") + self.drive = DriveChannel(Parameter("ch0")) + + # Define and add template schedules. + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) + + self.cals.add_schedule(xp) + + self.date_time1 = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + self.date_time2 = datetime.strptime("15/09/19 11:21:35", "%d/%m/%y %H:%M:%S") + + self.cals.add_parameter_value(ParameterValue(40, self.date_time1), "σ", schedule="xp") + self.cals.add_parameter_value(ParameterValue(45, self.date_time2, False), "σ", schedule="xp") + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time1), "amp", (0, ), "xp") + self.cals.add_parameter_value(ParameterValue(0.2, self.date_time2), "amp", (0, ),"xp") + self.cals.add_parameter_value(ParameterValue(0.4, self.date_time2, group="super_cal"), "amp", (0,), "xp") + + def test_get_parameter_value(self): + """Test that getting parameter values funcions properly.""" + + amp = self.cals.get_parameter_value(self.amp, (0, ), "xp") + self.assertEqual(amp, 0.2) + + amp = self.cals.get_parameter_value(self.amp, (0, ), "xp", group="super_cal") + self.assertEqual(amp, 0.4) + + cutoff_date = datetime.strptime("15/09/19 11:21:34", "%d/%m/%y %H:%M:%S") + amp = self.cals.get_parameter_value(self.amp, (0, ), "xp", cutoff_date=cutoff_date) + self.assertEqual(amp, 0.1) + + sigma = self.cals.get_parameter_value(self.sigma, (0, ), "xp") + self.assertEqual(sigma, 40) + + sigma = self.cals.get_parameter_value(self.sigma, (0, ), "xp", valid_only=False) + self.assertEqual(sigma, 45) From b6d9decca8d05df4d25274b948bb6e5d5974c884 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:11:24 +0200 Subject: [PATCH 114/178] * Black. --- test/calibration/test_calibrations.py | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 1bc19b143b..9bfb3e77cc 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -202,12 +202,15 @@ def test_parameter_without_schedule(self): def test_free_parameters(self): """Test that we can get a schedule with a free parameter.""" - xp = self.cals.get_schedule("xp", (3, ), free_params=[("xp","amp", (3,))]) + xp = self.cals.get_schedule("xp", (3,), free_params=[("xp", "amp", (3,))]) self.assertEqual(xp.parameters, {self.amp_xp}) - xp = self.cals.get_schedule("xp", (3, ), free_params=[("xp", "amp", (3,)), ("xp", "σ", (3,))]) + xp = self.cals.get_schedule( + "xp", (3,), free_params=[("xp", "amp", (3,)), ("xp", "σ", (3,))] + ) self.assertEqual(xp.parameters, {self.amp_xp, self.sigma}) + class TestCalibrationDefaults(QiskitTestCase): """Test that we can override defaults.""" @@ -685,26 +688,30 @@ def setUp(self): self.date_time2 = datetime.strptime("15/09/19 11:21:35", "%d/%m/%y %H:%M:%S") self.cals.add_parameter_value(ParameterValue(40, self.date_time1), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(45, self.date_time2, False), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time1), "amp", (0, ), "xp") - self.cals.add_parameter_value(ParameterValue(0.2, self.date_time2), "amp", (0, ),"xp") - self.cals.add_parameter_value(ParameterValue(0.4, self.date_time2, group="super_cal"), "amp", (0,), "xp") + self.cals.add_parameter_value( + ParameterValue(45, self.date_time2, False), "σ", schedule="xp" + ) + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time1), "amp", (0,), "xp") + self.cals.add_parameter_value(ParameterValue(0.2, self.date_time2), "amp", (0,), "xp") + self.cals.add_parameter_value( + ParameterValue(0.4, self.date_time2, group="super_cal"), "amp", (0,), "xp" + ) def test_get_parameter_value(self): """Test that getting parameter values funcions properly.""" - amp = self.cals.get_parameter_value(self.amp, (0, ), "xp") + amp = self.cals.get_parameter_value(self.amp, (0,), "xp") self.assertEqual(amp, 0.2) - amp = self.cals.get_parameter_value(self.amp, (0, ), "xp", group="super_cal") + amp = self.cals.get_parameter_value(self.amp, (0,), "xp", group="super_cal") self.assertEqual(amp, 0.4) cutoff_date = datetime.strptime("15/09/19 11:21:34", "%d/%m/%y %H:%M:%S") - amp = self.cals.get_parameter_value(self.amp, (0, ), "xp", cutoff_date=cutoff_date) + amp = self.cals.get_parameter_value(self.amp, (0,), "xp", cutoff_date=cutoff_date) self.assertEqual(amp, 0.1) - sigma = self.cals.get_parameter_value(self.sigma, (0, ), "xp") + sigma = self.cals.get_parameter_value(self.sigma, (0,), "xp") self.assertEqual(sigma, 40) - sigma = self.cals.get_parameter_value(self.sigma, (0, ), "xp", valid_only=False) + sigma = self.cals.get_parameter_value(self.sigma, (0,), "xp", valid_only=False) self.assertEqual(sigma, 45) From bc11c3827a92eef1782080468ea28098db881231 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:41:21 +0200 Subject: [PATCH 115/178] * Added test to check that a template is preserved after getting a schedule. --- test/calibration/test_calibrations.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9bfb3e77cc..f73d654425 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -45,7 +45,8 @@ def setUp(self): self.amp_x90p = Parameter("amp") self.amp_y90p = Parameter("amp") self.beta = Parameter("β") - self.drive = DriveChannel(Parameter("ch0")) + self.chan = Parameter("ch0") + self.drive = DriveChannel(self.chan) self.duration = Parameter("dur") # Define and add template schedules. @@ -108,6 +109,25 @@ def test_setup(self): self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "y90p"), 0.08) + def test_preserve_template(self): + """Test that the template schedule is still fully parametric after we get a schedule.""" + + # First get a schedule + xp = self.cals.get_schedule("xp", (3,)) + self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) + + # Find the template schedule for xp and test it. + schedule = pulse.Schedule() + for sched_dict in self.cals.schedules(): + if sched_dict["schedule"].name == "xp": + schedule = sched_dict["schedule"] + + for param in {self.amp_xp, self.sigma, self.beta, self.duration, self.chan}: + self.assertTrue(param in schedule.parameters) + + self.assertEqual(len(schedule.parameters), 5) + self.assertEqual(len(schedule.blocks), 1) + def test_remove_schedule(self): """Test that we can easily remove a schedule.""" From 62b42b677524d081a944d3ae086f77c603e8c561 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:45:46 +0200 Subject: [PATCH 116/178] * Moved docstring to module level. --- qiskit_experiments/calibration/__init__.py | 56 ++++++++++++++++++- .../calibration/calibrations.py | 55 +----------------- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 8efc0d6331..0b5416ee3d 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -10,7 +10,61 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Qiskit Experiments Calibration Root.""" +r""" +Qiskit Experiments Calibration Root. + +Calibrations are managed by the Calibrations class. This class stores schedules which are +intended to be fully parameterized, including the index of the channels. This class: +- supports having different schedules share parameters +- allows default schedules for qubits that can be overridden for specific qubits. + +Parametric channel naming convention. +Parametrized channel indices must be named according to a predefined pattern to properly +identify the channels and control channels when assigning values to the parametric +channel indices. A channel must have a name that starts with `ch` followed by an integer. +For control channels this integer can be followed by a sequence `.integer`. +Optionally, the name can end with `$integer` to specify the index of a control channel +for the case when a set of qubits share multiple control channels. For example, +valid channel names include "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". +The "." delimiter is used to specify the different qubits when looking for control +channels. The optional $ delimiter is used to specify which control channel to use +if several control channels work together on the same qubits. For example, if the +control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} +then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while +"ch1.0$0" will resolve to ControlChannel(3). A channel can only have one parameter. + +Parameter naming restriction. +Each parameter must have a unique name within each schedule. For example, it is +acceptable to have a parameter named 'amp' in the schedule 'xp' and a different +parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable +to have two parameters named 'amp' in the same schedule. The naming restriction +only applies to parameters used in the immediate scope of the schedule. Schedules +called by Call instructions have their own scope for Parameter names. + +The code block below illustrates the creation of a template schedule for a cross- +resonance gate. + +.. code-block:: python + + amp_cr = Parameter("amp") + amp = Parameter("amp") + d0 = DriveChannel(Parameter("ch0")) + c1 = ControlChannel(Parameter("ch0.1")) + sigma = Parameter("σ") + width = Parameter("w") + dur1 = Parameter("duration") + dur2 = Parameter("duration") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(dur1, amp, sigma), d0) + + with pulse.build(name="cr") as cr: + with pulse.align_sequential(): + pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.call(xp) + pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.call(xp) +""" from .calibrations import Calibrations from .backend_calibrations import BackendCalibrations diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7f3ce59868..857c6650be 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -38,59 +38,10 @@ class Calibrations: - r""" + """ A class to manage schedules with calibrated parameter values. Schedules are - intended to be fully parameterized, including the index of the channels. - This class: - - supports having different schedules share parameters - - allows default schedules for qubits that can be overridden for specific qubits. - - Parametric channel naming convention. - Parametrized channel indices must be named according to a predefined pattern to properly - identify the channels and control channels when assigning values to the parametric - channel indices. A channel must have a name that starts with `ch` followed by an integer. - For control channels this integer can be followed by a sequence `.integer`. - Optionally, the name can end with `$integer` to specify the index of a control channel - for the case when a set of qubits share multiple control channels. For example, - valid channel names include "ch0", "ch1", "ch0.1", "ch0$", "ch2$3", and "ch1.0.3$2". - The "." delimiter is used to specify the different qubits when looking for control - channels. The optional $ delimiter is used to specify which control channel to use - if several control channels work together on the same qubits. For example, if the - control channel configuration is {(3,2): [ControlChannel(3), ControlChannel(12)]} - then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while - "ch1.0$0" will resolve to ControlChannel(3). A channel can only have one parameter. - - Parameter naming restriction. - Each parameter must have a unique name within each schedule. For example, it is - acceptable to have a parameter named 'amp' in the schedule 'xp' and a different - parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable - to have two parameters named 'amp' in the same schedule. The naming restriction - only applies to parameters used in the immediate scope of the schedule. Schedules - called by Call instructions have their own scope for Parameter names. - - The code block below illustrates the creation of a template schedule for a cross- - resonance gate. - - .. code-block:: python - - amp_cr = Parameter("amp") - amp = Parameter("amp") - d0 = DriveChannel(Parameter("ch0")) - c1 = ControlChannel(Parameter("ch0.1")) - sigma = Parameter("σ") - width = Parameter("w") - dur1 = Parameter("duration") - dur2 = Parameter("duration") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(dur1, amp, sigma), d0) - - with pulse.build(name="cr") as cr: - with pulse.align_sequential(): - pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) - pulse.call(xp) - pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) - pulse.call(xp) + intended to be fully parameterized, including the index of the channels. See + the module-level documentation for extra details. """ # The channel indices need to be parameterized following this regex. From f8132bbe0cb1e1f70134cb2baa744056e0b26fc4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:47:57 +0200 Subject: [PATCH 117/178] * Removed if isinstance on Call in add_schedule. --- qiskit_experiments/calibration/calibrations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 857c6650be..c7e272b9a6 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -122,10 +122,9 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = # Do not register parameters that are in call instructions. params_to_register = set() for inst in self._exclude_calls(schedule, []): - if not isinstance(inst, Call): - for param in inst.parameters: - if param not in param_indices: - params_to_register.add(param) + 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.") From 0d267c27b9cc21974191ae9cfb8cf96f1a84f740 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 14:55:40 +0200 Subject: [PATCH 118/178] * Moved the Schedule and ScheduleBlock test to add_schedule. --- qiskit_experiments/calibration/calibrations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index c7e272b9a6..40f387ee36 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -88,11 +88,15 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = Raises: CalibrationError: + - If schedule is not a Schedule or a ScheduleBlock. - If the parameterized channel index is not formatted properly. - If several parameters in the same schedule have the same name. - If a channel is parameterized by more than one parameter. - If the schedule name starts with the prefix of ScheduleBlock. """ + if not isinstance(schedule, (Schedule, ScheduleBlock)): + raise CalibrationError(f"{schedule.name} is not a Schedule or a ScheduleBlock.") + # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): raise CalibrationError( @@ -567,13 +571,9 @@ def _assign( Raises: CalibrationError: - - If schedule is not a Schedule or ScheduleBlock. - If a channel has not been assigned. - If a parameter that is needed does not have a value. """ - if not isinstance(schedule, (Schedule, ScheduleBlock)): - raise CalibrationError(f"{schedule.name} is not a Schedule or a ScheduleBlock.") - # 1) Restrict the given qubits to those in the given schedule. qubit_set = set() for chan in schedule.channels: From f6f4b8a6d5f5a9f14c50605631adcc2d8e328cac Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 17:37:40 +0200 Subject: [PATCH 119/178] * Added reverse look-up to deal with pure control channel cases. --- qiskit_experiments/calibration/calibrations.py | 11 +++++++++++ test/calibration/test_calibrations.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 40f387ee36..3b35459e97 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -57,8 +57,15 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = that correspond to the qubits in the keys. """ + # Mapping between qubits and their control channels. self._controls_config = control_config if control_config else {} + # Store the reverse mapping between control channels and qubits for ease of look-up. + self._controls_config_r = {} + for qubits, channels in self._controls_config.items(): + for channel in channels: + self._controls_config_r[channel] = qubits + # Dict of the form: (schedule.name, parameter.name, qubits): Parameter self._parameter_map = {} @@ -587,6 +594,10 @@ def _assign( 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. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index f73d654425..2ac4786f11 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -594,6 +594,7 @@ def setUp(self): self.amp_cr = Parameter("amp") self.amp_rot = Parameter("amp_rot") self.amp = Parameter("amp") + self.amp_tcp = Parameter("amp") self.d0_ = DriveChannel(Parameter("ch0")) self.d1_ = DriveChannel(Parameter("ch1")) self.c1_ = ControlChannel(Parameter("ch0.1")) @@ -621,13 +622,19 @@ def setUp(self): pulse.play(cr_tone_m, self.c1_) pulse.call(xp) + # Mimic a tunable coupler pulse that is just a pulse on a control channel. + with pulse.build(name="tcp") as tcp: + pulse.play(GaussianSquare(640, self.amp_tcp, self.sigma, self.width), self.c1_) + self.cals.add_schedule(xp) self.cals.add_schedule(cr) + self.cals.add_schedule(tcp) self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") + self.cals.add_parameter_value(ParameterValue(0.8, self.date_time), "amp", (3, 2), "tcp") self.cals.add_parameter_value(ParameterValue(20, self.date_time), "w", (3, 2), "cr") # Reverse gate parameters @@ -684,6 +691,14 @@ def test_free_parameters(self): self.assertEqual(schedule.parameters, {self.amp_cr}) + def test_single_control_channel(self): + """Test that getting a correct pulse on a control channel only works.""" + + with pulse.build(name="tcp") as expected: + pulse.play(GaussianSquare(640, 0.8, 40, 20), ControlChannel(10)) + + self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) + class TestFiltering(QiskitTestCase): """Test that the filtering works as expected.""" From f94b34ec0441fdae6610c626e81e1a0cb248d9b1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 17:59:17 +0200 Subject: [PATCH 120/178] * Added a pure Schedule test. --- test/calibration/test_calibrations.py | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 2ac4786f11..2da5ecb5b4 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -22,6 +22,7 @@ GaussianSquare, MeasureChannel, Play, + Schedule, ) from qiskit.pulse.transforms import inline_subroutines, block_to_schedule import qiskit.pulse as pulse @@ -700,6 +701,44 @@ def test_single_control_channel(self): self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) +class TestPureSchedules(QiskitTestCase): + """Test that parameter assignments works when dealing with Schedules.""" + + def setUp(self): + """Setup a calibration.""" + super().setUp() + + self.cals = Calibrations() + + self.sigma = Parameter("σ") + self.amp = Parameter("amp") + self.chan = Parameter("ch0") + self.drive = DriveChannel(self.chan) + self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") + + # Define and add template schedules. + xp = Schedule(name="xp") + xp.insert(0, Play(Gaussian(160, self.amp, self.sigma), self.drive), inplace=True) + + call_xp = Schedule(name="call_xp") + call_xp.insert(0, pulse.Call(xp), inplace=True) + + self.cals.add_schedule(xp) + self.cals.add_schedule(call_xp) + + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") + self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") + + def test_get_schedule(self): + """Test that getting schedules works with Schedule.""" + + expected = Schedule(name="xp") + expected.insert(0, Play(Gaussian(160, 0.1, 40), DriveChannel(3)), inplace=True) + + self.assertEqual(self.cals.get_schedule("xp", (3,)), expected) + self.assertEqual(self.cals.get_schedule("call_xp", (3,)), expected) + + class TestFiltering(QiskitTestCase): """Test that the filtering works as expected.""" From a7f8a367a4f00f137e8b56e04681f407daa4cac1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 18:08:49 +0200 Subject: [PATCH 121/178] * Fixed test_backend_calibrations. --- test/calibration/test_backend_calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index b0a5c4dd03..7f16cd0cd9 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -24,5 +24,5 @@ def test_run_options(self): """Test that we can get run options.""" cals = BackendCalibrations(FakeArmonk()) - self.assertEqual(cals.get_frequencies(False), [6993370669.000001]) - self.assertEqual(cals.get_frequencies(True), [4971852852.405576]) + self.assertEqual(cals.get_meas_frequencies(False), [6993370669.000001]) + self.assertEqual(cals.get_qubit_frequencies(True), [4971852852.405576]) From 980f2c2dc1c982fe814606cd6d13d0eb4b7996e0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 21:06:22 +0200 Subject: [PATCH 122/178] * Refactored order of ParameterKey. * Allowed free_params to be a list of strings or tuples. --- .../calibration/calibrations.py | 38 ++++++++++++------- test/calibration/test_backend_calibrations.py | 4 +- test/calibration/test_calibrations.py | 34 ++++++++--------- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 3b35459e97..31f8eed8ab 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -33,7 +33,7 @@ from qiskit_experiments.calibration.exceptions import CalibrationError from qiskit_experiments.calibration.parameter_value import ParameterValue -ParameterKey = namedtuple("ParameterKey", ["schedule", "parameter", "qubits"]) +ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) @@ -242,7 +242,7 @@ def _register_parameter( self._parameter_counter += 1 sched_name = schedule.name if schedule else None - key = ParameterKey(sched_name, parameter.name, qubits) + key = ParameterKey(parameter.name, qubits, sched_name) self._parameter_map[key] = parameter self._parameter_map_r[parameter].add(key) @@ -277,12 +277,12 @@ def calibration_parameter( CalibrationError: If the desired parameter is not found. """ # 1) Check for qubit specific parameters. - if (schedule_name, parameter_name, qubits) in self._parameter_map: - return self._parameter_map[(schedule_name, parameter_name, qubits)] + 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 (schedule_name, parameter_name, None) in self._parameter_map: - return self._parameter_map[(schedule_name, parameter_name, None)] + elif ParameterKey(parameter_name, None, schedule_name) in self._parameter_map: + return self._parameter_map[ParameterKey(parameter_name, None, schedule_name)] else: raise CalibrationError( f"No parameter for {parameter_name} and schedule {schedule_name} " @@ -325,7 +325,7 @@ def add_parameter_value( if sched_name and sched_name not in registered_schedules: raise CalibrationError(f"Schedule named {sched_name} was never registered.") - self._params[ParameterKey(sched_name, param_name, qubits)].append(value) + self._params[ParameterKey(param_name, qubits, sched_name)].append(value) def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: """ @@ -438,7 +438,7 @@ def get_parameter_value( # 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.schedule, key.parameter, qubits)) + candidate_keys.append(ParameterKey(key.parameter, qubits, key.schedule)) # 3) Loop though the candidate keys to candidate values candidates = [] @@ -450,8 +450,8 @@ def get_parameter_value( # i.e. parameters that do not specify a qubit. if len(candidates) == 0: for key in candidate_keys: - if ParameterKey(key.schedule, key.parameter, None) in self._params: - candidates += self._params[ParameterKey(key.schedule, key.parameter, None)] + if ParameterKey(key.parameter, None, key.schedule) in self._params: + candidates += self._params[ParameterKey(key.parameter, None, key.schedule)] # 4) Filter candidate parameter values. if valid_only: @@ -483,7 +483,7 @@ def get_schedule( self, name: str, qubits: Tuple[int, ...], - free_params: List[Tuple[str, str, Tuple]] = None, + free_params: List[Union[str, Tuple[str, Tuple, str]]] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> Union[Schedule, ScheduleBlock]: @@ -510,6 +510,16 @@ def get_schedule( - If the name of the schedule is not known. - If a parameter could not be found. """ + if free_params: + free_params_ = [] + for free_param in free_params: + if isinstance(free_param, str): + free_params_.append((free_param, qubits, name)) + else: + free_params_.append(free_param) + + free_params = free_params_ + if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] elif (name, None) in self._schedules: @@ -636,7 +646,7 @@ def _assign( if ret_schedule.name in set(key.schedule for key in self._parameter_map): for param in ret_schedule.parameters: - keys.add(ParameterKey(ret_schedule.name, param.name, qubits_)) + keys.add(ParameterKey(param.name, qubits_, ret_schedule.name)) # 4) Build the parameter binding dictionary. free_params = free_params if free_params else [] @@ -649,8 +659,8 @@ def _assign( # parameter for all qubits, i.e. qubits may be None. if key in self._parameter_map: param = self._parameter_map[key] - elif (key.schedule, key.parameter, None) in self._parameter_map: - param = self._parameter_map[(key.schedule, key.parameter, None)] + elif ParameterKey(key.parameter, None, key.schedule) in self._parameter_map: + param = self._parameter_map[ParameterKey(key.parameter, None, key.schedule)] else: raise CalibrationError( f"Bad calibrations {key} is not present and has no default value." diff --git a/test/calibration/test_backend_calibrations.py b/test/calibration/test_backend_calibrations.py index 7f16cd0cd9..08c820877e 100644 --- a/test/calibration/test_backend_calibrations.py +++ b/test/calibration/test_backend_calibrations.py @@ -24,5 +24,5 @@ def test_run_options(self): """Test that we can get run options.""" cals = BackendCalibrations(FakeArmonk()) - self.assertEqual(cals.get_meas_frequencies(False), [6993370669.000001]) - self.assertEqual(cals.get_qubit_frequencies(True), [4971852852.405576]) + self.assertEqual(cals.get_meas_frequencies(), [6993370669.000001]) + self.assertEqual(cals.get_qubit_frequencies(), [4971852852.405576]) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 2da5ecb5b4..977b540b8f 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -80,28 +80,28 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" - expected = {ParameterKey("xp", "amp", None), ParameterKey("xm", "amp", None)} + expected = {ParameterKey("amp", None, "xp"), ParameterKey("amp", None, "xm")} self.assertEqual(self.cals.parameters[self.amp_xp], expected) - expected = {ParameterKey("x90p", "amp", None)} + expected = {ParameterKey("amp", None, "x90p")} self.assertEqual(self.cals.parameters[self.amp_x90p], expected) - expected = {ParameterKey("y90p", "amp", None)} + expected = {ParameterKey("amp", None, "y90p")} self.assertEqual(self.cals.parameters[self.amp_y90p], expected) expected = { - ParameterKey("xp", "β", None), - ParameterKey("xm", "β", None), - ParameterKey("x90p", "β", None), - ParameterKey("y90p", "β", None), + ParameterKey("β", None, "xp"), + ParameterKey("β", None, "xm"), + ParameterKey("β", None, "x90p"), + ParameterKey("β", None, "y90p"), } self.assertEqual(self.cals.parameters[self.beta], expected) expected = { - ParameterKey("xp", "σ", None), - ParameterKey("xm", "σ", None), - ParameterKey("x90p", "σ", None), - ParameterKey("y90p", "σ", None), + ParameterKey("σ", None, "xp"), + ParameterKey("σ", None, "xm"), + ParameterKey("σ", None, "x90p"), + ParameterKey("σ", None, "y90p"), } self.assertEqual(self.cals.parameters[self.sigma], expected) @@ -223,12 +223,10 @@ def test_parameter_without_schedule(self): def test_free_parameters(self): """Test that we can get a schedule with a free parameter.""" - xp = self.cals.get_schedule("xp", (3,), free_params=[("xp", "amp", (3,))]) + xp = self.cals.get_schedule("xp", (3,), free_params=["amp"]) self.assertEqual(xp.parameters, {self.amp_xp}) - xp = self.cals.get_schedule( - "xp", (3,), free_params=[("xp", "amp", (3,)), ("xp", "σ", (3,))] - ) + xp = self.cals.get_schedule("xp", (3,), free_params=["amp", "σ"]) self.assertEqual(xp.parameters, {self.amp_xp, self.sigma}) @@ -379,7 +377,7 @@ def test_replace_schedule(self): # Check that beta is in the mapping self.assertEqual( self.cals.parameters[self.beta], - {ParameterKey(schedule="xp", parameter="β", qubits=(3,))}, + {ParameterKey("β", (3,), "xp")}, ) self.cals.add_schedule(sched2, (3,)) @@ -505,7 +503,7 @@ def test_xt_meas(self): def test_free_parameters(self): """Test that we can get a schedule with free parameters.""" - schedule = self.cals.get_schedule("xt_meas", (0, 2), free_params=[("xp", "amp", (0,))]) + schedule = self.cals.get_schedule("xt_meas", (0, 2), free_params=[("amp", (0,), "xp")]) schedule = block_to_schedule(schedule) with pulse.build(name="xt_meas") as expected: @@ -688,7 +686,7 @@ def test_get_schedule(self): def test_free_parameters(self): """Test that we can get a schedule with free parameters.""" - schedule = self.cals.get_schedule("cr", (3, 2), free_params=[("cr", "amp", (3, 2))]) + schedule = self.cals.get_schedule("cr", (3, 2), free_params=["amp"]) self.assertEqual(schedule.parameters, {self.amp_cr}) From 98530d4d5f6eb3ef07c4ceedde5dad32ba152717 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 22:06:32 +0200 Subject: [PATCH 123/178] * Amended docstring for free_params in get_schedule. --- qiskit_experiments/calibration/calibrations.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 31f8eed8ab..7f3352b3b3 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -483,7 +483,7 @@ def get_schedule( self, name: str, qubits: Tuple[int, ...], - free_params: List[Union[str, Tuple[str, Tuple, str]]] = None, + free_params: List[Union[str, ParameterKey]] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> Union[Schedule, ScheduleBlock]: @@ -493,7 +493,12 @@ def get_schedule( Args: name: The name of the schedule to get. qubits: The qubits for which to get the schedule. - free_params: The parameters that should remain unassigned. + free_params: The parameters that should remain unassigned. Each free parameter is + specified by a ParameterKey a named tuple of the form (parameter name, qubits, + schedule name). Each entry in free_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 @@ -514,7 +519,7 @@ def get_schedule( free_params_ = [] for free_param in free_params: if isinstance(free_param, str): - free_params_.append((free_param, qubits, name)) + free_params_.append(ParameterKey(free_param, qubits, name)) else: free_params_.append(free_param) @@ -542,7 +547,7 @@ def _assign( self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple[int, ...], - free_params: List[str] = None, + free_params: List[ParameterKey] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> Union[Schedule, ScheduleBlock]: @@ -576,7 +581,7 @@ def _assign( 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. - free_params: The parameters that are to be left free. + free_params: The parameters that are to be left free. 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 From d85f5f6d2869affaa1247a81b74f5f304d92750d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 22:12:08 +0200 Subject: [PATCH 124/178] * Improved consistency of argument ordering. --- qiskit_experiments/calibration/calibrations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7f3352b3b3..b900f8dcac 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -141,7 +141,7 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") for param in params_to_register: - self._register_parameter(param, schedule, qubits) + self._register_parameter(param, qubits, schedule) def _exclude_calls( self, schedule: Union[Schedule, ScheduleBlock], instructions: List[Instruction] @@ -223,8 +223,8 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non def _register_parameter( self, parameter: Parameter, - schedule: Union[Schedule, ScheduleBlock] = None, qubits: Tuple = None, + schedule: Union[Schedule, ScheduleBlock] = None, ): """ Registers a parameter for the given schedule. This allows self to determine the @@ -233,9 +233,9 @@ def _register_parameter( Args: parameter: The parameter to register. + qubits: The qubits for which to register the parameter. schedule: The schedule to which this parameter belongs. The schedule can be None which allows the calibration to accommodate, e.g. qubit frequencies. - qubits: The qubits for which to register the parameter. """ if parameter not in self._hash_map: self._hash_map[parameter] = self._parameter_counter @@ -704,8 +704,8 @@ def schedules(self) -> List[Dict[str, Any]]: def parameters_table( self, parameters: List[str] = None, - schedules: List[Union[Schedule, ScheduleBlock, str]] = None, qubit_list: List[Tuple[int, ...]] = None, + schedules: List[Union[Schedule, ScheduleBlock, str]] = None, ) -> List[Dict[str, Any]]: """ A convenience function to help users visualize the values of their parameter. @@ -713,9 +713,9 @@ def parameters_table( Args: parameters: The parameter names that should be included in the returned table. If None is given then all names are included. - schedules: The schedules to which to restrict the output. qubit_list: The qubits that should be included in the returned table. If None is given then all channels are returned. + schedules: The schedules to which to restrict the output. Returns: data: A list of dictionaries with parameter values and metadata which can From 3900b82aa0e38386f7055efd6f5a61bdba7562e2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 22:27:10 +0200 Subject: [PATCH 125/178] * Added a check on the number of free parameters and the assigned schedule. * Added corresponding unit test. --- qiskit_experiments/calibration/calibrations.py | 13 ++++++++++++- test/calibration/test_calibrations.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b900f8dcac..da6b1aa977 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -524,6 +524,8 @@ def get_schedule( free_params_.append(free_param) free_params = free_params_ + else: + free_params = [] if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] @@ -541,7 +543,16 @@ def get_schedule( # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) - return self._assign(schedule, qubits, free_params, group, cutoff_date) + assigned_schedule = self._assign(schedule, qubits, free_params, group, cutoff_date) + + 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 def _assign( self, diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 977b540b8f..5917a45591 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -519,6 +519,16 @@ def test_free_parameters(self): self.assertEqual(schedule.parameters, {self.amp_xp}) self.assertEqual(schedule, expected) + def test_free_parameters_check(self): + """ + Test that get_schedule raises an error if the number of parameters does not match. + This test ensures that we forbid ambiguity in free parameters in schedules with + calls that share parameters. + """ + + with self.assertRaises(CalibrationError): + self.cals.get_schedule("xt_meas", (0, 2), free_params=[("amp", (0,), "xp"), + ("amp", (2,), "xp")]) class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" From 0c519b5d945b987623d1b3fbad94a594c1730691 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 May 2021 22:32:18 +0200 Subject: [PATCH 126/178] * Black --- test/calibration/test_calibrations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 5917a45591..58a9bcd5a8 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -527,8 +527,10 @@ def test_free_parameters_check(self): """ with self.assertRaises(CalibrationError): - self.cals.get_schedule("xt_meas", (0, 2), free_params=[("amp", (0,), "xp"), - ("amp", (2,), "xp")]) + self.cals.get_schedule( + "xt_meas", (0, 2), free_params=[("amp", (0,), "xp"), ("amp", (2,), "xp")] + ) + class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" From 987bc814b7fe816b4dc038b72cb27d39f01a96f6 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 7 May 2021 09:16:55 +0200 Subject: [PATCH 127/178] Update qiskit_experiments/calibration/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 0b5416ee3d..0c1206fe5e 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -18,7 +18,9 @@ - supports having different schedules share parameters - allows default schedules for qubits that can be overridden for specific qubits. -Parametric channel naming convention. +Parametric channel naming convention +========================= + Parametrized channel indices must be named according to a predefined pattern to properly identify the channels and control channels when assigning values to the parametric channel indices. A channel must have a name that starts with `ch` followed by an integer. From 129db5bd282c17837ac30bfecf2d3a858732cca4 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 7 May 2021 09:17:04 +0200 Subject: [PATCH 128/178] Update qiskit_experiments/calibration/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 0c1206fe5e..e6510f2149 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -35,7 +35,9 @@ then given qubits (2, 3) the name "ch1.0$1" will resolve to ControlChannel(12) while "ch1.0$0" will resolve to ControlChannel(3). A channel can only have one parameter. -Parameter naming restriction. +Parameter naming restriction +=================== + Each parameter must have a unique name within each schedule. For example, it is acceptable to have a parameter named 'amp' in the schedule 'xp' and a different parameter instance named 'amp' in the schedule named 'xm'. It is not acceptable From 47c0a44a5e74d6cf5510c6c39e1356a7bb2b4d19 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 7 May 2021 09:17:22 +0200 Subject: [PATCH 129/178] Update qiskit_experiments/calibration/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index e6510f2149..b7b7e41111 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -45,7 +45,7 @@ only applies to parameters used in the immediate scope of the schedule. Schedules called by Call instructions have their own scope for Parameter names. -The code block below illustrates the creation of a template schedule for a cross- +The code block below illustrates the creation of a template schedule for a echoed cross- resonance gate. .. code-block:: python From 80bc8206f7efaf0583aab19bcf748c08c732e373 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 7 May 2021 09:18:29 +0200 Subject: [PATCH 130/178] Update qiskit_experiments/calibration/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index b7b7e41111..48ea3fdc24 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -66,7 +66,7 @@ with pulse.align_sequential(): pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) pulse.call(xp) - pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.play(GaussianSquare(dur2, -amp_cr, sigma, width), c1) pulse.call(xp) """ From f7747c6500f401f172ebeafaace63d32b1b5112a Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 7 May 2021 09:18:50 +0200 Subject: [PATCH 131/178] Update qiskit_experiments/calibration/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 48ea3fdc24..b6b11fa4fb 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -56,8 +56,8 @@ c1 = ControlChannel(Parameter("ch0.1")) sigma = Parameter("σ") width = Parameter("w") - dur1 = Parameter("duration") - dur2 = Parameter("duration") + dur_xp = Parameter("duration") + dur_cr = Parameter("duration") with pulse.build(name="xp") as xp: pulse.play(Gaussian(dur1, amp, sigma), d0) From aaf1479ade0e19bc42ceaf95d3a5b81a08c16621 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 09:21:23 +0200 Subject: [PATCH 132/178] * made docstring example consistent. --- qiskit_experiments/calibration/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index b6b11fa4fb..677a2d2928 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -60,13 +60,13 @@ dur_cr = Parameter("duration") with pulse.build(name="xp") as xp: - pulse.play(Gaussian(dur1, amp, sigma), d0) + pulse.play(Gaussian(dur_xp, amp, sigma), d0) with pulse.build(name="cr") as cr: with pulse.align_sequential(): - pulse.play(GaussianSquare(dur2, amp_cr, sigma, width), c1) + pulse.play(GaussianSquare(dur_cr, amp_cr, sigma, width), c1) pulse.call(xp) - pulse.play(GaussianSquare(dur2, -amp_cr, sigma, width), c1) + pulse.play(GaussianSquare(dur_cr, -amp_cr, sigma, width), c1) pulse.call(xp) """ From 0a49a3cf6a1bf701109626cbaec36b7251403970 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 09:27:15 +0200 Subject: [PATCH 133/178] * Made error message clearer --- qiskit_experiments/calibration/calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index da6b1aa977..9db3aff993 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -107,8 +107,9 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): raise CalibrationError( - f"A registered schedule name cannot start with {ScheduleBlock.prefix} " - f"received {schedule.name}." + f"A registered schedule name cannot start with {ScheduleBlock.prefix}, " + f"received {schedule.name}. " + f"Use a name that does not start with {ScheduleBlock.prefix}." ) param_indices = set() From 320b3772ec4a1b057e12f9650b0f90021299876f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 17:07:48 +0200 Subject: [PATCH 134/178] * Added more robustnes to user qubit input: - Add _to_tuple - Default to all qubits if qubit tuple is empty instead of None. --- .../calibration/calibrations.py | 81 +++++++++++++++---- test/calibration/test_calibrations.py | 55 +++++++++---- 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9db3aff993..9f035a706a 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -82,7 +82,9 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._hash_map = {} self._parameter_counter = 0 - def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): + def add_schedule( + self, schedule: Union[Schedule, ScheduleBlock], qubits: Union[int, Tuple[int, ...]] = None + ): """ Add a schedule and register its parameters. @@ -101,6 +103,8 @@ def add_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = - If a channel is parameterized by more than one parameter. - If the schedule name starts with the prefix of ScheduleBlock. """ + qubits = self._to_tuple(qubits) + if not isinstance(schedule, (Schedule, ScheduleBlock)): raise CalibrationError(f"{schedule.name} is not a Schedule or a ScheduleBlock.") @@ -175,7 +179,9 @@ def _exclude_calls( return instructions - def remove_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tuple = None): + def remove_schedule( + self, schedule: Union[Schedule, ScheduleBlock], qubits: Union[int, Tuple[int, ...]] = None + ): """ Allows users to remove a schedule from the calibrations. The history of the parameters will remain in the calibrations. @@ -185,13 +191,15 @@ def remove_schedule(self, schedule: Union[Schedule, ScheduleBlock], qubits: Tupl qubits: The qubits for which to remove the schedules. If None is given then this schedule is the default schedule for all qubits. """ + qubits = self._to_tuple(qubits) + if ScheduleKey(schedule.name, qubits) in self._schedules: del self._schedules[ScheduleKey(schedule.name, qubits)] # Clean the parameter to schedule mapping. self._clean_parameter_map(schedule.name, qubits) - def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = None): + def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...]): """Clean the parameter to schedule mapping for the given schedule, parameter and qubits. Args: @@ -224,7 +232,7 @@ def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...] = Non def _register_parameter( self, parameter: Parameter, - qubits: Tuple = None, + qubits: Tuple[int, ...], schedule: Union[Schedule, ScheduleBlock] = None, ): """ @@ -258,7 +266,10 @@ def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: return self._parameter_map_r def calibration_parameter( - self, parameter_name: str, qubits: Tuple[int, ...] = None, schedule_name: str = None + self, + parameter_name: str, + qubits: Union[int, Tuple[int, ...]] = None, + schedule_name: str = None, ) -> Parameter: """ Returns a Parameter object given the triplet parameter_name, qubits and schedule_name @@ -277,13 +288,15 @@ def calibration_parameter( 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, None, schedule_name) in self._parameter_map: - return self._parameter_map[ParameterKey(parameter_name, None, schedule_name)] + 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} " @@ -294,7 +307,7 @@ def add_parameter_value( self, value: Union[int, float, complex, ParameterValue], param: Union[Parameter, str], - qubits: Tuple[int, ...] = None, + qubits: Union[int, Tuple[int, ...]] = None, schedule: Union[Schedule, ScheduleBlock, str] = None, ): """ @@ -315,6 +328,8 @@ def add_parameter_value( 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()) @@ -328,7 +343,7 @@ def add_parameter_value( self._params[ParameterKey(param_name, qubits, sched_name)].append(value) - def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: + def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: """ Get the index of the parameterized channel based on the given qubits and the name of the parameter in the channel index. The name of this @@ -391,7 +406,7 @@ def _get_channel_index(self, qubits: Tuple, chan: PulseChannel) -> int: def get_parameter_value( self, param: Union[Parameter, str], - qubits: Tuple[int, ...], + qubits: Union[int, Tuple[int, ...]], schedule: Union[Schedule, ScheduleBlock, str, None] = None, valid_only: bool = True, group: str = "default", @@ -429,6 +444,7 @@ def get_parameter_value( 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 @@ -451,8 +467,8 @@ def get_parameter_value( # i.e. parameters that do not specify a qubit. if len(candidates) == 0: for key in candidate_keys: - if ParameterKey(key.parameter, None, key.schedule) in self._params: - candidates += self._params[ParameterKey(key.parameter, None, key.schedule)] + 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: @@ -483,7 +499,7 @@ def get_parameter_value( def get_schedule( self, name: str, - qubits: Tuple[int, ...], + qubits: Union[int, Tuple[int, ...]], free_params: List[Union[str, ParameterKey]] = None, group: Optional[str] = "default", cutoff_date: datetime = None, @@ -516,6 +532,8 @@ def get_schedule( - If the name of the schedule is not known. - If a parameter could not be found. """ + qubits = self._to_tuple(qubits) + if free_params: free_params_ = [] for free_param in free_params: @@ -530,8 +548,8 @@ def get_schedule( if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] - elif (name, None) in self._schedules: - schedule = self._schedules[ScheduleKey(name, None)] + elif (name, ()) in self._schedules: + schedule = self._schedules[ScheduleKey(name, ())] else: raise CalibrationError(f"Schedule {name} is not defined for qubits {qubits}.") @@ -676,8 +694,8 @@ def _assign( # parameter for all qubits, i.e. qubits may be None. if key in self._parameter_map: param = self._parameter_map[key] - elif ParameterKey(key.parameter, None, key.schedule) in self._parameter_map: - param = self._parameter_map[ParameterKey(key.parameter, None, key.schedule)] + elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: + param = self._parameter_map[ParameterKey(key.parameter, (), key.schedule)] else: raise CalibrationError( f"Bad calibrations {key} is not present and has no default value." @@ -733,6 +751,8 @@ def parameters_table( data: A list of dictionaries with parameter values and metadata which can easily be converted to a data frame. """ + if qubit_list: + qubit_list = [self._to_tuple(qubits) for qubits in qubit_list] data = [] @@ -823,3 +843,30 @@ def from_csv(cls): external DB. """ raise NotImplementedError + + @staticmethod + def _to_tuple(qubits: Union[int, Tuple[int, ...]]) -> Tuple[int, ...]: + """ + Ensure that qubits is a tuple of ints. + + Args: + qubits: An int or 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 not qubits: + return tuple() + + if isinstance(qubits, int): + return (qubits,) + + if isinstance(qubits, tuple): + if all(isinstance(n, int) for n in qubits): + return qubits + + raise CalibrationError(f"{qubits} must be int or tuple of ints.") diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 58a9bcd5a8..543d13a8fd 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -80,35 +80,35 @@ def setUp(self): def test_setup(self): """Test that the initial setup behaves as expected.""" - expected = {ParameterKey("amp", None, "xp"), ParameterKey("amp", None, "xm")} + expected = {ParameterKey("amp", (), "xp"), ParameterKey("amp", (), "xm")} self.assertEqual(self.cals.parameters[self.amp_xp], expected) - expected = {ParameterKey("amp", None, "x90p")} + expected = {ParameterKey("amp", (), "x90p")} self.assertEqual(self.cals.parameters[self.amp_x90p], expected) - expected = {ParameterKey("amp", None, "y90p")} + expected = {ParameterKey("amp", (), "y90p")} self.assertEqual(self.cals.parameters[self.amp_y90p], expected) expected = { - ParameterKey("β", None, "xp"), - ParameterKey("β", None, "xm"), - ParameterKey("β", None, "x90p"), - ParameterKey("β", None, "y90p"), + ParameterKey("β", (), "xp"), + ParameterKey("β", (), "xm"), + ParameterKey("β", (), "x90p"), + ParameterKey("β", (), "y90p"), } self.assertEqual(self.cals.parameters[self.beta], expected) expected = { - ParameterKey("σ", None, "xp"), - ParameterKey("σ", None, "xm"), - ParameterKey("σ", None, "x90p"), - ParameterKey("σ", None, "y90p"), + ParameterKey("σ", (), "xp"), + ParameterKey("σ", (), "xm"), + ParameterKey("σ", (), "x90p"), + ParameterKey("σ", (), "y90p"), } self.assertEqual(self.cals.parameters[self.sigma], expected) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "y90p"), 0.08) + self.assertEqual(self.cals.get_parameter_value("amp", 3, "x90p"), 0.1) + self.assertEqual(self.cals.get_parameter_value("amp", 3, "y90p"), 0.08) def test_preserve_template(self): """Test that the template schedule is still fully parametric after we get a schedule.""" @@ -219,16 +219,37 @@ def test_unique_parameter_names(self): def test_parameter_without_schedule(self): """Test that we can manage parameters that are not bound to a schedule.""" - self.cals._register_parameter(Parameter("a")) + self.cals._register_parameter(Parameter("a"), ()) def test_free_parameters(self): """Test that we can get a schedule with a free parameter.""" - xp = self.cals.get_schedule("xp", (3,), free_params=["amp"]) + xp = self.cals.get_schedule("xp", 3, free_params=["amp"]) self.assertEqual(xp.parameters, {self.amp_xp}) - xp = self.cals.get_schedule("xp", (3,), free_params=["amp", "σ"]) + xp = self.cals.get_schedule("xp", 3, free_params=["amp", "σ"]) self.assertEqual(xp.parameters, {self.amp_xp, self.sigma}) + def test_qubit_input(self): + """Test the qubit input.""" + + xp = self.cals.get_schedule("xp", 3) + self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) + + val = self.cals.get_parameter_value("amp", 3, "xp") + self.assertEqual(val, 0.2) + + val = self.cals.get_parameter_value("amp", (3,), "xp") + self.assertEqual(val, 0.2) + + with self.assertRaises(CalibrationError): + self.cals.get_parameter_value("amp", ("3",), "xp") + + with self.assertRaises(CalibrationError): + self.cals.get_parameter_value("amp", "3", "xp") + + with self.assertRaises(CalibrationError): + self.cals.get_parameter_value("amp", [3], "xp") + class TestCalibrationDefaults(QiskitTestCase): """Test that we can override defaults.""" @@ -267,7 +288,7 @@ def test_parameter_value_adding_and_filtering(self): self.assertEqual(params, []) # Add a default parameter common to all qubits. - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", None, "xp") + self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.assertEqual(len(self.cals.parameters_table()), 1) # Check that we can get a default parameter in the parameter table From f9c0490a1cb23aeba8b9f498c41a2c252a551f43 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 17:10:59 +0200 Subject: [PATCH 135/178] * Amended docstring. --- qiskit_experiments/calibration/calibrations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9f035a706a..34e4fcbe2a 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -92,8 +92,8 @@ def add_schedule( Args: schedule: The schedule to add. - qubits: The qubits for which to add the schedules. If None is given then this - schedule is the default schedule for all qubits. + 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. Raises: CalibrationError: @@ -278,7 +278,7 @@ def calibration_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. + 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. @@ -691,7 +691,7 @@ def _assign( if key not in free_params: # 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 None. + # parameter for all qubits, i.e. qubits may be an empty tuple. if key in self._parameter_map: param = self._parameter_map[key] elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: @@ -719,7 +719,7 @@ def schedules(self) -> List[Dict[str, Any]]: Returns: data: A list of dictionaries with all the schedules in it. The key-value pairs are - - 'qubits': the qubits to which this schedule applies. This may be None if the + - 'qubits': the qubits to which this schedule applies. This may be () if the schedule is the default for all qubits. - 'schedule': The schedule (either a Schedule or a ScheduleBlock). - 'parameters': The parameters in the schedule exposed for convenience. From 3c56906ed1a28fb550b4d280385300de6a85738b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 18:08:22 +0200 Subject: [PATCH 136/178] * Added a test for an acquire channel. --- .../calibration/calibrations.py | 12 ++++++++-- test/calibration/test_calibrations.py | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 34e4fcbe2a..426b851b73 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -27,6 +27,9 @@ MeasureChannel, Call, Instruction, + AcquireChannel, + RegisterSlot, + MemorySlot, ) from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter, ParameterExpression @@ -370,7 +373,9 @@ def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int - If ch is not a DriveChannel, MeasureChannel, or ControlChannel. """ if isinstance(chan.index, Parameter): - if isinstance(chan, (DriveChannel, MeasureChannel)): + if isinstance( + chan, (DriveChannel, MeasureChannel, AcquireChannel, RegisterSlot, MemorySlot) + ): index = int(chan.index.name[2:].split("$")[0]) if len(qubits) <= index: @@ -399,7 +404,10 @@ def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int return chs_[control_index].index - raise CalibrationError(f"{chan} must be a sub-type of {PulseChannel}.") + raise CalibrationError( + f"{chan} must be a sub-type of {PulseChannel} or an {AcquireChannel}, " + f"{RegisterSlot}, or a {MemorySlot}." + ) return chan.index diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 543d13a8fd..aa156ae583 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -18,9 +18,11 @@ Drag, DriveChannel, ControlChannel, + AcquireChannel, Gaussian, GaussianSquare, MeasureChannel, + RegisterSlot, Play, Schedule, ) @@ -449,10 +451,16 @@ def setUp(self): ch1 = Parameter("ch1") self.m0_ = MeasureChannel(ch0) self.d0_ = DriveChannel(ch0) + self.delay = Parameter("delay") with pulse.build(name="meas") as meas: pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) + with pulse.build(name="meas_acquire") as meas_acq: + pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) + pulse.delay(self.delay, pulse.AcquireChannel(ch0)) + pulse.acquire(self.duration, pulse.AcquireChannel(ch0), pulse.RegisterSlot(ch0)) + with pulse.build(name="xp") as xp: pulse.play(Gaussian(self.duration_xp, self.amp_xp, self.sigma_xp), self.d0_) @@ -473,13 +481,16 @@ def setUp(self): self.cals.add_schedule(xp) self.cals.add_schedule(xp_meas) self.cals.add_schedule(xt_meas) + self.cals.add_schedule(meas_acq) # self.cals.add_parameter_value(8000, self.duration, schedule="meas") self.cals.add_parameter_value(0.5, self.amp, (0,), "meas") + self.cals.add_parameter_value(0.56, self.amp, (123,), "meas") self.cals.add_parameter_value(0.3, self.amp, (2,), "meas") self.cals.add_parameter_value(160, self.sigma, schedule="meas") self.cals.add_parameter_value(7000, self.width, schedule="meas") self.cals.add_parameter_value(8000, self.duration, schedule="meas") + self.cals.add_parameter_value(100, self.delay, schedule="meas_acquire") self.cals.add_parameter_value(0.9, self.amp_xp, (0,), "xp") self.cals.add_parameter_value(0.7, self.amp_xp, (2,), "xp") @@ -552,6 +563,18 @@ def test_free_parameters_check(self): "xt_meas", (0, 2), free_params=[("amp", (0,), "xp"), ("amp", (2,), "xp")] ) + def test_measure_and_acquire(self): + """Test that we can get a measurement schedule with an acquire instruction.""" + + sched = self.cals.get_schedule("meas_acquire", (123,)) + + with pulse.build(name="meas_acquire") as expected: + pulse.play(GaussianSquare(8000, 0.56, 160, 7000), MeasureChannel(123)) + pulse.delay(100, AcquireChannel(123)) + pulse.acquire(8000, AcquireChannel(123), RegisterSlot(123)) + + self.assertEqual(sched, expected) + class TestInstructions(QiskitTestCase): """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" From 07d5ee14576e132d6f6e95575704e9ff9b373b7d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 18:12:52 +0200 Subject: [PATCH 137/178] * Renamed _hash_map to _hash_to_counter_map. --- qiskit_experiments/calibration/calibrations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 426b851b73..2a68277542 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -82,7 +82,7 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = # A variable to store all parameter hashes encountered and present them as ordered # indices to the user. - self._hash_map = {} + self._hash_to_counter_map = {} self._parameter_counter = 0 def add_schedule( @@ -249,8 +249,8 @@ def _register_parameter( schedule: The schedule to which this parameter belongs. The schedule can be None which allows the calibration to accommodate, e.g. qubit frequencies. """ - if parameter not in self._hash_map: - self._hash_map[parameter] = self._parameter_counter + if parameter not in self._hash_to_counter_map: + self._hash_to_counter_map[parameter] = self._parameter_counter self._parameter_counter += 1 sched_name = schedule.name if schedule else None @@ -812,7 +812,7 @@ def to_csv(self): body.append( { "parameter.name": parameter.name, - "parameter unique id": self._hash_map[parameter], + "parameter unique id": self._hash_to_counter_map[parameter], "schedule": key.schedule, "qubits": key.qubits, } From 2009af40f961479cde997929f18847cc2d44d87c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 7 May 2021 18:33:50 +0200 Subject: [PATCH 138/178] * Removed Schedule from Calibrations. --- .../calibration/calibrations.py | 100 +++++++----------- test/calibration/test_calibrations.py | 39 ------- 2 files changed, 39 insertions(+), 100 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 2a68277542..98920b32d7 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -20,7 +20,6 @@ import regex as re from qiskit.pulse import ( - Schedule, ScheduleBlock, DriveChannel, ControlChannel, @@ -86,7 +85,7 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._parameter_counter = 0 def add_schedule( - self, schedule: Union[Schedule, ScheduleBlock], qubits: Union[int, Tuple[int, ...]] = None + self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None ): """ Add a schedule and register its parameters. @@ -100,7 +99,7 @@ def add_schedule( Raises: CalibrationError: - - If schedule is not a Schedule or a ScheduleBlock. + - If schedule is not a ScheduleBlock. - If the parameterized channel index is not formatted properly. - If several parameters in the same schedule have the same name. - If a channel is parameterized by more than one parameter. @@ -108,8 +107,8 @@ def add_schedule( """ qubits = self._to_tuple(qubits) - if not isinstance(schedule, (Schedule, ScheduleBlock)): - raise CalibrationError(f"{schedule.name} is not a Schedule or a ScheduleBlock.") + if not isinstance(schedule, ScheduleBlock): + raise CalibrationError(f"{schedule.name} is not a ScheduleBlock.") # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): @@ -152,39 +151,30 @@ def add_schedule( self._register_parameter(param, qubits, schedule) def _exclude_calls( - self, schedule: Union[Schedule, ScheduleBlock], instructions: List[Instruction] + self, schedule: ScheduleBlock, instructions: List[Instruction] ) -> List[Instruction]: """ Recursive function to get all non-Call instructions. This will flatten all blocks in a ScheduleBlock and return the instructions of the ScheduleBlock leaving out - any Call instructions. For a Schedule this is done by a simple loop of the - instructions of the schedule. + any Call instructions. Args: - schedule: A Schedule or ScheduleBlock from which to extract the instructions. + schedule: A ScheduleBlock from which to extract the instructions. instructions: The list of instructions that is recursively populated. Returns: The list of instructions to which all non-Call instructions have been added. """ - if isinstance(schedule, Schedule): - for _, inst in schedule.instructions: - if not isinstance(inst, Call): - instructions.append(inst) - - if isinstance(schedule, ScheduleBlock): - for block in schedule.blocks: - if isinstance(block, ScheduleBlock): - instructions = self._exclude_calls(block, instructions) - else: - if not isinstance(block, Call): - instructions.append(block) + for block in schedule.blocks: + if isinstance(block, ScheduleBlock): + instructions = self._exclude_calls(block, instructions) + else: + if not isinstance(block, Call): + instructions.append(block) return instructions - def remove_schedule( - self, schedule: Union[Schedule, ScheduleBlock], qubits: Union[int, Tuple[int, ...]] = None - ): + def remove_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): """ Allows users to remove a schedule from the calibrations. The history of the parameters will remain in the calibrations. @@ -236,7 +226,7 @@ def _register_parameter( self, parameter: Parameter, qubits: Tuple[int, ...], - schedule: Union[Schedule, ScheduleBlock] = None, + schedule: ScheduleBlock = None, ): """ Registers a parameter for the given schedule. This allows self to determine the @@ -311,7 +301,7 @@ def add_parameter_value( value: Union[int, float, complex, ParameterValue], param: Union[Parameter, str], qubits: Union[int, Tuple[int, ...]] = None, - schedule: Union[Schedule, ScheduleBlock, str] = None, + schedule: Union[ScheduleBlock, str] = None, ): """ Add a parameter value to the stored parameters. @@ -337,7 +327,7 @@ def add_parameter_value( value = ParameterValue(value, datetime.now()) param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, (Schedule, ScheduleBlock)) else schedule + sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule registered_schedules = set(key.schedule for key in self._schedules) @@ -415,7 +405,7 @@ def get_parameter_value( self, param: Union[Parameter, str], qubits: Union[int, Tuple[int, ...]], - schedule: Union[Schedule, ScheduleBlock, str, None] = None, + schedule: Union[ScheduleBlock, str, None] = None, valid_only: bool = True, group: str = "default", cutoff_date: datetime = None, @@ -456,7 +446,7 @@ def get_parameter_value( # 1) Identify the parameter object. param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, (Schedule, ScheduleBlock)) else schedule + sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule param = self.calibration_parameter(param_name, qubits, sched_name) @@ -511,7 +501,7 @@ def get_schedule( free_params: List[Union[str, ParameterKey]] = None, group: Optional[str] = "default", cutoff_date: datetime = None, - ) -> Union[Schedule, ScheduleBlock]: + ) -> ScheduleBlock: """ Get the schedule with the non-free parameters assigned to their values. @@ -583,12 +573,12 @@ def get_schedule( def _assign( self, - schedule: Union[Schedule, ScheduleBlock], + schedule: ScheduleBlock, qubits: Tuple[int, ...], free_params: List[ParameterKey] = None, group: Optional[str] = "default", cutoff_date: datetime = None, - ) -> Union[Schedule, ScheduleBlock]: + ) -> ScheduleBlock: """ Recursive function to extract and assign parameters from a schedule. The recursive behaviour is needed to handle Call instructions as the name of @@ -654,33 +644,21 @@ def _assign( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) # 2) Recursively assign the parameters in the called instructions. - if isinstance(schedule, Schedule): - ret_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) - - for t0, inst in schedule.instructions: - if isinstance(inst, Call): - inst = self._assign( - inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date - ) - - ret_schedule.insert(t0, inst, inplace=True) - - else: - ret_schedule = ScheduleBlock( - alignment_context=schedule.alignment_context, - name=schedule.name, - metadata=schedule.metadata, - ) - - for inst in schedule.blocks: - if isinstance(inst, Call): - inst = self._assign( - inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date - ) - elif isinstance(inst, ScheduleBlock): - inst = self._assign(inst, qubits_, free_params, group, cutoff_date) + ret_schedule = ScheduleBlock( + alignment_context=schedule.alignment_context, + name=schedule.name, + metadata=schedule.metadata, + ) + + for inst in schedule.blocks: + if isinstance(inst, Call): + inst = self._assign( + inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date + ) + elif isinstance(inst, ScheduleBlock): + inst = self._assign(inst, qubits_, free_params, group, cutoff_date) - ret_schedule.append(inst, inplace=True) + 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 @@ -729,7 +707,7 @@ def schedules(self) -> List[Dict[str, Any]]: data: A list of dictionaries with all the schedules in it. The key-value pairs are - 'qubits': the qubits to which this schedule applies. This may be () if the schedule is the default for all qubits. - - 'schedule': The schedule (either a Schedule or a ScheduleBlock). + - 'schedule': The schedule. - 'parameters': The parameters in the schedule exposed for convenience. This list of dictionaries can easily be converted to a data frame. """ @@ -743,7 +721,7 @@ def parameters_table( self, parameters: List[str] = None, qubit_list: List[Tuple[int, ...]] = None, - schedules: List[Union[Schedule, ScheduleBlock, str]] = None, + schedules: List[Union[ScheduleBlock, str]] = None, ) -> List[Dict[str, Any]]: """ A convenience function to help users visualize the values of their parameter. @@ -767,7 +745,7 @@ def parameters_table( # Convert inputs to lists of strings if schedules is not None: schedules = { - sdl.name if isinstance(sdl, (Schedule, ScheduleBlock)) else sdl for sdl in schedules + sdl.name if isinstance(sdl, ScheduleBlock) else sdl for sdl in schedules } # Look for exact matches. Default values will be ignored. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index aa156ae583..d29629270b 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -24,7 +24,6 @@ MeasureChannel, RegisterSlot, Play, - Schedule, ) from qiskit.pulse.transforms import inline_subroutines, block_to_schedule import qiskit.pulse as pulse @@ -755,44 +754,6 @@ def test_single_control_channel(self): self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) -class TestPureSchedules(QiskitTestCase): - """Test that parameter assignments works when dealing with Schedules.""" - - def setUp(self): - """Setup a calibration.""" - super().setUp() - - self.cals = Calibrations() - - self.sigma = Parameter("σ") - self.amp = Parameter("amp") - self.chan = Parameter("ch0") - self.drive = DriveChannel(self.chan) - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - - # Define and add template schedules. - xp = Schedule(name="xp") - xp.insert(0, Play(Gaussian(160, self.amp, self.sigma), self.drive), inplace=True) - - call_xp = Schedule(name="call_xp") - call_xp.insert(0, pulse.Call(xp), inplace=True) - - self.cals.add_schedule(xp) - self.cals.add_schedule(call_xp) - - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") - - def test_get_schedule(self): - """Test that getting schedules works with Schedule.""" - - expected = Schedule(name="xp") - expected.insert(0, Play(Gaussian(160, 0.1, 40), DriveChannel(3)), inplace=True) - - self.assertEqual(self.cals.get_schedule("xp", (3,)), expected) - self.assertEqual(self.cals.get_schedule("call_xp", (3,)), expected) - - class TestFiltering(QiskitTestCase): """Test that the filtering works as expected.""" From e9b44e7dbde8fa8fe15514346259406463ed0446 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 8 May 2021 20:08:58 +0200 Subject: [PATCH 139/178] * Added saving and loading to csv. * Added tests for saving and loading. * Made functions to ensure ParameterValue types are obeyed. --- .../calibration/backend_calibrations.py | 9 +- .../calibration/calibrations.py | 124 +++++++++++++----- .../calibration/parameter_value.py | 66 ++++++++++ test/calibration/test_calibrations.py | 72 ++++++++-- 4 files changed, 219 insertions(+), 52 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 9a7a75dfac..5319b1e5d7 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -38,8 +38,8 @@ def __init__(self, backend: Backend): # Use the same naming convention as in backend.defaults() self.qubit_freq = Parameter("qubit_lo_freq") self.meas_freq = Parameter("meas_lo_freq") - self._register_parameter(self.qubit_freq) - self._register_parameter(self.meas_freq) + self._register_parameter(self.qubit_freq, ()) + self._register_parameter(self.meas_freq, ()) self._qubits = set(range(backend.configuration().n_qubits)) self._backend = backend @@ -131,8 +131,3 @@ def export_backend(self) -> Backend: # TODO: build the instruction schedule map using the stored calibrations return backend - - @classmethod - def from_csv(cls): - """Create an instance from csv files""" - raise NotImplementedError diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 98920b32d7..cea389a891 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -12,6 +12,7 @@ """Class to store and manage the results of calibration experiments.""" +import os from collections import namedtuple, defaultdict from datetime import datetime from typing import Any, Dict, Set, Tuple, Union, List, Optional @@ -84,9 +85,7 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._hash_to_counter_map = {} self._parameter_counter = 0 - def add_schedule( - self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None - ): + def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): """ Add a schedule and register its parameters. @@ -744,9 +743,7 @@ def parameters_table( # Convert inputs to lists of strings if schedules is not None: - schedules = { - sdl.name if isinstance(sdl, ScheduleBlock) else sdl for sdl in schedules - } + schedules = {sdl.name if isinstance(sdl, ScheduleBlock) else sdl for sdl in schedules} # Look for exact matches. Default values will be ignored. keys = set() @@ -771,15 +768,37 @@ def parameters_table( return data - def to_csv(self): + def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): """ - Serializes the parameterized schedules and parameter values so + Saves the parameterized schedules and parameter values so that they can be stored in csv files. This method will create 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. + + 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. + + Raises: + CalibrationError: if the files exist and overwrite is not set to True. """ + cwd = os.getcwd() + if folder: + os.chdir(folder) + + if os.path.isfile("parameter_config.csv") and not overwrite: + raise CalibrationError("parameter_config.csv already exists. Set overwrite to True.") + + if os.path.isfile("parameter_values.csv") and not overwrite: + raise CalibrationError("parameter_values.csv already exists. Set overwrite to True.") + + if os.path.isfile("parameter_values.csv") and not overwrite: + raise CalibrationError("schedules.csv already exists. Set overwrite to True.") # Write the parameter configuration. header_keys = ["parameter.name", "parameter unique id", "schedule", "qubits"] @@ -796,47 +815,73 @@ def to_csv(self): } ) - with open("parameter_config.csv", "w", newline="") 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() - if len(values) > 0: - header_keys = values[0].keys() + if file_type == "csv": + with open("parameter_config.csv", "w", newline="") 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() + if len(values) > 0: + header_keys = values[0].keys() + + with open("parameter_values.csv", "w", newline="") 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("parameter_values.csv", "w", newline="") as output_file: + with open("schedules.csv", "w", newline="") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() - dict_writer.writerows(values) + dict_writer.writerows(schedules) - # 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)}) + else: + raise CalibrationError(f"Saving to .{file_type} is not yet supported.") + + os.chdir(cwd) - with open("schedules.csv", "w", newline="") as output_file: - dict_writer = csv.DictWriter(output_file, header_keys) - dict_writer.writeheader() - dict_writer.writerows(schedules) + def load_parameter_values(self, file_name: str = "parameter_values.csv"): + """ + Load parameter values from a given file into self._params. + + Args: + file_name: The name of the file that stores the parameters. Will default to + parameter_values.csv. + """ + with open(file_name) as fp: + reader = csv.DictReader(fp, delimiter=",", quotechar='"') + + for row in reader: + param_val = ParameterValue( + row["value"], row["date_time"], row["valid"], row["exp_id"], row["group"] + ) + key = ParameterKey(row["parameter"], self._to_tuple(row["qubits"]), row["schedule"]) + self.add_parameter_value(param_val, *key) @classmethod - def from_csv(cls): + def load(cls, files: List[str]) -> "Calibrations": """ - Retrieves the parameterized schedules and pulse parameters from an - external DB. + Retrieves the parameterized schedules and pulse parameters from the + given location. """ - raise NotImplementedError + raise CalibrationError("Full calibration loading is not implemented yet.") @staticmethod - def _to_tuple(qubits: Union[int, Tuple[int, ...]]) -> Tuple[int, ...]: + def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: """ Ensure that qubits is a tuple of ints. Args: - qubits: An int or a tuple of ints. + qubits: An int, a tuple of ints, or a string representing a tuple of ints. Returns: qubits: A tuple of ints. @@ -848,6 +893,12 @@ def _to_tuple(qubits: Union[int, Tuple[int, ...]]) -> Tuple[int, ...]: if not qubits: 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,) @@ -855,4 +906,7 @@ def _to_tuple(qubits: Union[int, Tuple[int, ...]]) -> Tuple[int, ...]: if all(isinstance(n, int) for n in qubits): return qubits - raise CalibrationError(f"{qubits} must be int or tuple of ints.") + raise CalibrationError( + f"{qubits} must be int, tuple of ints, or str that can be parsed" + f"to a tuple if ints. Received {qubits}." + ) diff --git a/qiskit_experiments/calibration/parameter_value.py b/qiskit_experiments/calibration/parameter_value.py index 8fbcd7d931..baa470f606 100644 --- a/qiskit_experiments/calibration/parameter_value.py +++ b/qiskit_experiments/calibration/parameter_value.py @@ -16,6 +16,8 @@ from datetime import datetime from typing import Union +from qiskit_experiments.calibration.exceptions import CalibrationError + @dataclass class ParameterValue: @@ -35,3 +37,67 @@ class ParameterValue: # The group of calibrations to which this parameter belongs group: str = "default" + + 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): + self.date_time = datetime.strptime(self.date_time, "%Y-%m-%d %H:%M:%S") + + 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.") + + @staticmethod + 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 diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index d29629270b..817242e91d 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -12,6 +12,8 @@ """Class to test the calibrations.""" +import os +from collections import defaultdict from datetime import datetime from qiskit.circuit import Parameter from qiskit.pulse import ( @@ -245,8 +247,11 @@ def test_qubit_input(self): with self.assertRaises(CalibrationError): self.cals.get_parameter_value("amp", ("3",), "xp") + val = self.cals.get_parameter_value("amp", "3", "xp") + self.assertEqual(val, 0.2) + with self.assertRaises(CalibrationError): - self.cals.get_parameter_value("amp", "3", "xp") + self.cals.get_parameter_value("amp", "(1, a)", "xp") with self.assertRaises(CalibrationError): self.cals.get_parameter_value("amp", [3], "xp") @@ -628,12 +633,8 @@ def test_instructions(self): self.assertEqual(sched.instructions[2][1].frequency, 200) -class TestControlChannels(QiskitTestCase): - """ - Test the echoed cross-resonance schedule which is more complex than single-qubit - schedules. The example also shows that a schedule with call instructions can - support parameters with the same names. - """ +class CrossResonanceTest(QiskitTestCase): + """Setup class for an echoed cross-resonance calibration.""" def setUp(self): """Create the setup we will deal with.""" @@ -685,7 +686,9 @@ def setUp(self): self.cals.add_schedule(tcp) self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value( + ParameterValue(0.1 + 0.01j, self.date_time), "amp", (3,), "xp" + ) self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") self.cals.add_parameter_value(ParameterValue(0.8, self.date_time), "amp", (3, 2), "tcp") @@ -697,6 +700,14 @@ def setUp(self): self.cals.add_parameter_value(ParameterValue(0.4, self.date_time), "amp_rot", (2, 3), "cr") self.cals.add_parameter_value(ParameterValue(30, self.date_time), "w", (2, 3), "cr") + +class TestControlChannels(CrossResonanceTest): + """ + Test the echoed cross-resonance schedule which is more complex than single-qubit + schedules. The example also shows that a schedule with call instructions can + support parameters with the same names. + """ + def test_get_schedule(self): """Check that we can get a CR schedule with a built in Call.""" @@ -705,11 +716,11 @@ def test_get_schedule(self): with pulse.align_left(): pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + pulse.play(Gaussian(160, 0.1 + 0.01j, 40), DriveChannel(3)) with pulse.align_left(): pulse.play(GaussianSquare(640, -0.2, 40, 20), DriveChannel(2)) # Rotary tone pulse.play(GaussianSquare(640, -0.3, 40, 20), ControlChannel(10)) # CR tone. - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) + pulse.play(Gaussian(160, 0.1 + 0.01j, 40), DriveChannel(3)) # We inline to make the schedules comparable with the construction directly above. schedule = self.cals.get_schedule("cr", (3, 2)) @@ -804,3 +815,44 @@ def test_get_parameter_value(self): sigma = self.cals.get_parameter_value(self.sigma, (0,), "xp", valid_only=False) self.assertEqual(sigma, 45) + + +class TestSavingAndLoading(CrossResonanceTest): + """Test that calibrations can be saved and loaded to and from files.""" + + def test_save_load_parameter_values(self): + """Test that we can save and load parameter values.""" + + self.cals.save("csv", overwrite=True) + self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.1 + 0.01j) + + self.cals._params = defaultdict(list) + + with self.assertRaises(CalibrationError): + self.cals.get_parameter_value("amp", (3,), "xp") + + # Load the parameters, check value and type. + self.cals.load_parameter_values("parameter_values.csv") + + val = self.cals.get_parameter_value("amp", (3,), "xp") + self.assertEqual(val, 0.1 + 0.01j) + self.assertTrue(isinstance(val, complex)) + + val = self.cals.get_parameter_value("σ", (3,), "xp") + self.assertEqual(val, 40) + self.assertTrue(isinstance(val, int)) + + val = self.cals.get_parameter_value("amp", (3, 2), "cr") + self.assertEqual(val, 0.3) + self.assertTrue(isinstance(val, float)) + + # Check that we cannot rewrite files as they already exist. + with self.assertRaises(CalibrationError): + self.cals.save("csv") + + self.cals.save("csv", overwrite=True) + + # Clean-up + os.remove("parameter_values.csv") + os.remove("parameter_config.csv") + os.remove("schedules.csv") From 75fadc7d169a09b64f2962a5bb30dedde77d110f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 10 May 2021 09:26:37 +0200 Subject: [PATCH 140/178] * Improved docstring in __init__ * Changed a test to cover the case when qubit in add_parameter_value is an int. --- qiskit_experiments/calibration/__init__.py | 30 ++++++++++++++++++++++ test/calibration/test_calibrations.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 677a2d2928..3480cce5b8 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -18,6 +18,36 @@ - supports having different schedules share parameters - allows default schedules for qubits that can be overridden for specific qubits. +The following code illustrates how a user can create a parameterized schedule, add +values to the parameters and query a schedule. + +.. code-block:: python + + dur = Parameter("dur") + amp = Parameter("amp") + sigma = Parameter("σ") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(dur, amp, sigma), DriveChannel(Parameter("ch0"))) + + cals = Calibrations() + cals.add_schedule(xp) + + # add duration and sigma parameter values for all qubits. + cals.add_parameter_value(160, "dur", schedule="xp") + cals.add_parameter_value(35.5, "σ", schedule="xp") + + # Add an amplitude for qubit 3. + cals.add_parameter_value(0.2+0.05j, "amp", (3, ), "xp") + + # Retrieve an xp pulse with all parameters assigned + cals.get_schedule("xp", (3, )) + + # Retrieve an xp pulse with unassigned amplitude + cals.get_schedule("xp", (3, ), free_params=["amp"]) + +The Calibrations make a couple of assumptions which are discussed below. + Parametric channel naming convention ========================= diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 817242e91d..ef1d2bb8fe 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -76,7 +76,7 @@ def setUp(self): self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") self.cals.add_parameter_value(ParameterValue(160, self.date_time), "dur", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", (3,), "xp") + self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", 3, "xp") self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "x90p") self.cals.add_parameter_value(ParameterValue(0.08, self.date_time), "amp", (3,), "y90p") self.cals.add_parameter_value(ParameterValue(40, self.date_time), "β", (3,), "xp") From 54c3e812c2c1d0306dbf39b3f9c50f229bc7a269 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 11:12:17 +0200 Subject: [PATCH 141/178] * Added warning in save. --- qiskit_experiments/calibration/calibrations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index cea389a891..b4188ac1cb 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -18,6 +18,7 @@ from typing import Any, Dict, Set, Tuple, Union, List, Optional import csv import dataclasses +import warnings import regex as re from qiskit.pulse import ( @@ -777,6 +778,10 @@ def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = Fal - 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. @@ -787,6 +792,8 @@ def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = Fal 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) From db33772d94f445a0f1dfd7b3dece59fac3cdab8f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 11:15:38 +0200 Subject: [PATCH 142/178] * Added development warning to the __init__ --- qiskit_experiments/calibration/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 3480cce5b8..8bb2b8579a 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -13,6 +13,11 @@ r""" Qiskit Experiments Calibration Root. +.. warning:: + The calibrations interface is still in active development. It may have + breaking API changes without deprecation warnings in future releases until + otherwise indicated. + Calibrations are managed by the Calibrations class. This class stores schedules which are intended to be fully parameterized, including the index of the channels. This class: - supports having different schedules share parameters From 4f04ae4454744d900e837c702314711ac16570c0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 11:46:15 +0200 Subject: [PATCH 143/178] * Added check for unregistered subroutines and correspoinding test. --- .../calibration/calibrations.py | 10 ++++++ test/calibration/test_calibrations.py | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b4188ac1cb..0ce120a671 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -104,6 +104,7 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. - If several parameters in the same schedule have the same name. - If a channel is parameterized by more than one parameter. - If the schedule name starts with the prefix of ScheduleBlock. + - If the schedule calls subroutines that have not been registered. """ qubits = self._to_tuple(qubits) @@ -130,6 +131,15 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. f"Parameterized channel must correspond to {self.__channel_pattern__}" ) + # Check that subroutines are present. + for block in schedule.blocks: + if isinstance(block, Call): + 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) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index ef1d2bb8fe..c22e52210d 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -39,7 +39,7 @@ class TestCalibrationsBasic(QiskitTestCase): """Class to test the management of schedules and parameters for calibrations.""" def setUp(self): - """Setup a test environment.""" + """Create the setting to test.""" super().setUp() self.cals = Calibrations() @@ -261,7 +261,7 @@ class TestCalibrationDefaults(QiskitTestCase): """Test that we can override defaults.""" def setUp(self): - """Setup a few parameters.""" + """Create the setting to test.""" super().setUp() self.cals = Calibrations() @@ -633,11 +633,38 @@ def test_instructions(self): self.assertEqual(sched.instructions[2][1].frequency, 200) +class TestUnregisteredCall(QiskitTestCase): + """Class to test registering of subroutines with calls.""" + + def setUp(self): + """Create the setting to test.""" + super().setUp() + + self.cals = Calibrations() + self.d0_ = DriveChannel(Parameter("ch0")) + + def test_call_registering(self): + """Test registering of schedules with call.""" + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, 0.5, 40), self.d0_) + + with pulse.build(name="call_xp") as call_xp: + pulse.call(xp) + + with self.assertRaises(CalibrationError): + self.cals.add_schedule(call_xp) + + self.cals.add_schedule(xp) + self.cals.add_schedule(call_xp) + + self.assertTrue(isinstance(self.cals.get_schedule("call_xp", 2), pulse.ScheduleBlock)) + + class CrossResonanceTest(QiskitTestCase): """Setup class for an echoed cross-resonance calibration.""" def setUp(self): - """Create the setup we will deal with.""" + """Create the setting to test.""" super().setUp() controls = { From 61a0ba191f69b5541e8624271a5a9343e91fcd62 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 12:13:49 +0200 Subject: [PATCH 144/178] * Added method get_template and corresponding tests. --- qiskit_experiments/calibration/__init__.py | 12 +++++++ .../calibration/calibrations.py | 30 +++++++++++++++++ test/calibration/test_calibrations.py | 33 ++++++++++++++++++- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/__init__.py b/qiskit_experiments/calibration/__init__.py index 8bb2b8579a..ecbf6c6a9a 100644 --- a/qiskit_experiments/calibration/__init__.py +++ b/qiskit_experiments/calibration/__init__.py @@ -103,6 +103,18 @@ pulse.call(xp) pulse.play(GaussianSquare(dur_cr, -amp_cr, sigma, width), c1) pulse.call(xp) + + cals = Calibrations() + cals.add_schedule(xp) + cals.add_schedule(cr) + +Note that a registered template schedule can be retrieve by doing + +.. code-block:: python + + xp = cals.get_template("xp") + +which would return the default xp schedule block template for all qubits. """ from .calibrations import Calibrations diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 0ce120a671..cd7b4afd92 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -184,6 +184,36 @@ def _exclude_calls( return instructions + 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. + + 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 np template schedule for the given schedule name and qubits + was registered. + """ + qubits = self._to_tuple(qubits) + + if ScheduleKey(schedule_name, qubits) not in self._schedules: + 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) + + return self._schedules[ScheduleKey(schedule_name, qubits)] + def remove_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): """ Allows users to remove a schedule from the calibrations. The history of the parameters diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index c22e52210d..1f5a4432c2 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -633,7 +633,7 @@ def test_instructions(self): self.assertEqual(sched.instructions[2][1].frequency, 200) -class TestUnregisteredCall(QiskitTestCase): +class TestRegistering(QiskitTestCase): """Class to test registering of subroutines with calls.""" def setUp(self): @@ -659,6 +659,37 @@ def test_call_registering(self): self.assertTrue(isinstance(self.cals.get_schedule("call_xp", 2), pulse.ScheduleBlock)) + def test_get_template(self): + """Test that we can get a registered template and use it.""" + amp = Parameter("amp") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, amp, 40), self.d0_) + + self.cals.add_schedule(xp) + + registered_xp = self.cals.get_template("xp") + + self.assertEqual(registered_xp, xp) + + with pulse.build(name="dxp") as dxp: + pulse.call(registered_xp) + pulse.play(Gaussian(160, amp, 40), self.d0_) + + self.cals.add_schedule(dxp) + self.cals.add_parameter_value(0.5, "amp", 3, "xp") + + sched = block_to_schedule(self.cals.get_schedule("dxp", 3)) + + self.assertEqual(sched.instructions[0][1], Play(Gaussian(160, 0.5, 40), DriveChannel(3))) + self.assertEqual(sched.instructions[1][1], Play(Gaussian(160, 0.5, 40), DriveChannel(3))) + + with self.assertRaises(CalibrationError): + self.cals.get_template("not registered") + + with self.assertRaises(CalibrationError): + self.cals.get_template("xp", (3, )) + class CrossResonanceTest(QiskitTestCase): """Setup class for an echoed cross-resonance calibration.""" From fd09136c3662c1f48083e0d55492ea24fe675c2d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 12:27:57 +0200 Subject: [PATCH 145/178] * Errors are now raised if a schedule is called. --- qiskit_experiments/calibration/calibrations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index cd7b4afd92..e5bae32d77 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -31,6 +31,7 @@ AcquireChannel, RegisterSlot, MemorySlot, + Schedule, ) from qiskit.pulse.channels import PulseChannel from qiskit.circuit import Parameter, ParameterExpression @@ -105,6 +106,7 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. - If a channel is parameterized by more than one parameter. - If the schedule name starts with the prefix of ScheduleBlock. - If the schedule calls subroutines that have not been registered. + - If a Schedule is Called. """ qubits = self._to_tuple(qubits) @@ -134,6 +136,11 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. # Check that subroutines are present. for block in schedule.blocks: if isinstance(block, Call): + if isinstance(block.subroutine, Schedule): + raise CalibrationError( + "Calling a Schedule if forbidden, call ScheduleBlock instead." + ) + if (block.subroutine.name, qubits) not in self._schedules: raise CalibrationError( f"Cannot register schedule block {schedule.name} with unregistered " From 1b143665099db3f0cb4ce5767c5ad03df7936794 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 12:33:19 +0200 Subject: [PATCH 146/178] * Simplified _assign. --- qiskit_experiments/calibration/calibrations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index e5bae32d77..9dfb293dee 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -699,10 +699,9 @@ def _assign( for inst in schedule.blocks: if isinstance(inst, Call): - inst = self._assign( - inst.assigned_subroutine(), qubits_, free_params, group, cutoff_date - ) - elif isinstance(inst, ScheduleBlock): + inst = inst.assigned_subroutine() + + if isinstance(inst, ScheduleBlock): inst = self._assign(inst, qubits_, free_params, group, cutoff_date) ret_schedule.append(inst, inplace=True) From 27af03c9235ddadda6b926b2f304c820d5d78394 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 14:03:20 +0200 Subject: [PATCH 147/178] * Refactored free_params methodology to assign_params. * Added main of terra to tox.ini --- .../calibration/calibrations.py | 85 +++++++++++-------- test/calibration/test_calibrations.py | 32 ++++--- tox.ini | 4 +- 3 files changed, 73 insertions(+), 48 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 9dfb293dee..4fbaf1f0b5 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -40,6 +40,7 @@ ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) +ParameterValueType = Union["ParameterExpression", float, int, complex] class Calibrations: @@ -192,7 +193,7 @@ def _exclude_calls( return instructions def get_template( - self, schedule_name: str, qubits: Optional[Tuple[int, ...]] = None + self, schedule_name: str, qubits: Optional[Tuple[int, ...]] = None ) -> ScheduleBlock: """Get a template schedule. @@ -206,7 +207,7 @@ def get_template( The registered template schedule. Raises: - CalibrationError if np template schedule for the given schedule name and qubits + CalibrationError: if np template schedule for the given schedule name and qubits was registered. """ qubits = self._to_tuple(qubits) @@ -545,24 +546,28 @@ def get_schedule( self, name: str, qubits: Union[int, Tuple[int, ...]], - free_params: List[Union[str, ParameterKey]] = None, + assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: - """ - Get the schedule with the non-free parameters assigned to their values. + """Get the schedule and assign values to parameters. + + 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`. Args: name: The name of the schedule to get. qubits: The qubits for which to get the schedule. - free_params: The parameters that should remain unassigned. Each free parameter is - specified by a ParameterKey a named tuple of the form (parameter name, qubits, - schedule name). Each entry in free_params can also be a string corresponding + 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. + 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 @@ -570,7 +575,7 @@ def get_schedule( Returns: schedule: A copy of the template schedule with all parameters assigned - except for those specified by free_params. + except for those specified by assign_params. Raises: CalibrationError: @@ -579,17 +584,17 @@ def get_schedule( """ qubits = self._to_tuple(qubits) - if free_params: - free_params_ = [] - for free_param in free_params: - if isinstance(free_param, str): - free_params_.append(ParameterKey(free_param, qubits, name)) + 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: - free_params_.append(free_param) + assign_params_[assign_param] = value - free_params = free_params_ + assign_params = assign_params_ else: - free_params = [] + assign_params = dict() if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] @@ -607,7 +612,12 @@ def get_schedule( # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) - assigned_schedule = self._assign(schedule, qubits, free_params, group, cutoff_date) + 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( @@ -622,7 +632,7 @@ def _assign( self, schedule: ScheduleBlock, qubits: Tuple[int, ...], - free_params: List[ParameterKey] = None, + assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: @@ -656,7 +666,7 @@ def _assign( 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. - free_params: The parameters that are to be left free. See get_schedules for details. + 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 @@ -702,7 +712,7 @@ def _assign( inst = inst.assigned_subroutine() if isinstance(inst, ScheduleBlock): - inst = self._assign(inst, qubits_, free_params, group, cutoff_date) + inst = self._assign(inst, qubits_, assign_params, group, cutoff_date) ret_schedule.append(inst, inplace=True) @@ -716,22 +726,23 @@ def _assign( keys.add(ParameterKey(param.name, qubits_, ret_schedule.name)) # 4) Build the parameter binding dictionary. - free_params = free_params if free_params else [] + assign_params = assign_params if assign_params else dict() binding_dict = {} for key in keys: - if key not in free_params: - # 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. - if key in self._parameter_map: - param = self._parameter_map[key] - elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: - param = self._parameter_map[ParameterKey(key.parameter, (), key.schedule)] - else: - raise CalibrationError( - f"Bad calibrations {key} is not present and has no default value." - ) + # 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. + if key in self._parameter_map: + param = self._parameter_map[key] + elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: + param = self._parameter_map[ParameterKey(key.parameter, (), key.schedule)] + else: + raise CalibrationError( + f"Bad calibrations {key} is not present and has no default value." + ) + + if key not in assign_params: if param not in binding_dict: binding_dict[param] = self.get_parameter_value( @@ -741,6 +752,8 @@ def _assign( group=group, cutoff_date=cutoff_date, ) + else: + binding_dict[param] = assign_params[key] return ret_schedule.assign_parameters(binding_dict, inplace=False) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 1f5a4432c2..103dabbae2 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -226,10 +226,10 @@ def test_parameter_without_schedule(self): def test_free_parameters(self): """Test that we can get a schedule with a free parameter.""" - xp = self.cals.get_schedule("xp", 3, free_params=["amp"]) + xp = self.cals.get_schedule("xp", 3, assign_params={"amp": self.amp_xp}) self.assertEqual(xp.parameters, {self.amp_xp}) - xp = self.cals.get_schedule("xp", 3, free_params=["amp", "σ"]) + xp = self.cals.get_schedule("xp", 3, assign_params={"amp": self.amp_xp, "σ": self.sigma}) self.assertEqual(xp.parameters, {self.amp_xp, self.sigma}) def test_qubit_input(self): @@ -539,12 +539,14 @@ def test_xt_meas(self): def test_free_parameters(self): """Test that we can get a schedule with free parameters.""" - schedule = self.cals.get_schedule("xt_meas", (0, 2), free_params=[("amp", (0,), "xp")]) + my_amp = Parameter("my_amp") + assign_dict = {("amp", (0,), "xp"): my_amp} + schedule = self.cals.get_schedule("xt_meas", (0, 2), assign_params=assign_dict) schedule = block_to_schedule(schedule) with pulse.build(name="xt_meas") as expected: with pulse.align_sequential(): - pulse.play(Gaussian(160, self.amp_xp, 40), DriveChannel(0)) + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(0)) pulse.play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) with pulse.align_sequential(): pulse.play(Gaussian(160, 0.7, 40), DriveChannel(2)) @@ -552,7 +554,7 @@ def test_free_parameters(self): expected = block_to_schedule(expected) - self.assertEqual(schedule.parameters, {self.amp_xp}) + self.assertEqual(schedule.parameters, {my_amp}) self.assertEqual(schedule, expected) def test_free_parameters_check(self): @@ -562,10 +564,18 @@ def test_free_parameters_check(self): calls that share parameters. """ - with self.assertRaises(CalibrationError): - self.cals.get_schedule( - "xt_meas", (0, 2), free_params=[("amp", (0,), "xp"), ("amp", (2,), "xp")] - ) + amp1 = Parameter("amp1") + amp2 = Parameter("amp2") + assign_dict = {("amp", (0,), "xp"): amp1, ("amp", (2,), "xp"): amp2} + + sched = self.cals.get_schedule("xt_meas", (0, 2), assign_params=assign_dict) + + self.assertEqual(sched.parameters, {amp1, amp2}) + + sched = block_to_schedule(sched) + + self.assertEqual(sched.instructions[0][1].parameters, {amp1}) + self.assertEqual(sched.instructions[1][1].parameters, {amp2}) def test_measure_and_acquire(self): """Test that we can get a measurement schedule with an acquire instruction.""" @@ -688,7 +698,7 @@ def test_get_template(self): self.cals.get_template("not registered") with self.assertRaises(CalibrationError): - self.cals.get_template("xp", (3, )) + self.cals.get_template("xp", (3,)) class CrossResonanceTest(QiskitTestCase): @@ -810,7 +820,7 @@ def test_get_schedule(self): def test_free_parameters(self): """Test that we can get a schedule with free parameters.""" - schedule = self.cals.get_schedule("cr", (3, 2), free_params=["amp"]) + schedule = self.cals.get_schedule("cr", (3, 2), assign_params={"amp": self.amp_cr}) self.assertEqual(schedule.parameters, {self.amp_cr}) diff --git a/tox.ini b/tox.ini index 61d3da7976..aa69e4a514 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,9 @@ install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} QISKIT_SUPPRESS_PACKAGING_WARNINGS=Y -deps = -r{toxinidir}/requirements-dev.txt +deps = + -r{toxinidir}/requirements-dev.txt + git+https://github.com/Qiskit/qiskit-terra commands = stestr run {posargs} [testenv:lint] From ced5333a9da4406f87ba88e363dddf1e5dc7d91f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 14:10:03 +0200 Subject: [PATCH 148/178] * Improved error message. --- qiskit_experiments/calibration/calibrations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4fbaf1f0b5..e6ff1ca846 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -738,12 +738,9 @@ def _assign( elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: param = self._parameter_map[ParameterKey(key.parameter, (), key.schedule)] else: - raise CalibrationError( - f"Bad calibrations {key} is not present and has no default value." - ) + raise CalibrationError(f"Parameter key {key} has no parameter.") if key not in assign_params: - if param not in binding_dict: binding_dict[param] = self.get_parameter_value( key.parameter, From 7031ec1575a34c102f414c154a21db6b851b749b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 14:27:11 +0200 Subject: [PATCH 149/178] * Improved docstrings. --- .../calibration/calibrations.py | 87 +++++++++---------- test/calibration/test_calibrations.py | 16 ++++ 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index e6ff1ca846..ecda27ce5b 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -47,15 +47,15 @@ class Calibrations: """ A class to manage schedules with calibrated parameter values. Schedules are intended to be fully parameterized, including the index of the channels. See - the module-level documentation for extra details. + the module-level documentation for extra details. Note that only instances of + ScheduleBlock are supported. """ # The channel indices need to be parameterized following this regex. __channel_pattern__ = r"^ch\d[\.\d]*\${0,1}[\d]*$" def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = None): - """ - Initialize the calibrations. + """Initialize the calibrations. Args: control_config: A configuration dictionary of any control channels. The @@ -89,19 +89,18 @@ def __init__(self, control_config: Dict[Tuple[int, ...], List[ControlChannel]] = self._parameter_counter = 0 def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): - """ - Add a schedule and register its parameters. + """Add a schedule block and register its parameters. Schedules that use Call instructions must register the called schedules separately. Args: - schedule: The schedule to add. + 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. Raises: CalibrationError: - - If schedule is not a ScheduleBlock. + - If schedule is not an instance of :class:`ScheduleBlock`. - If the parameterized channel index is not formatted properly. - If several parameters in the same schedule have the same name. - If a channel is parameterized by more than one parameter. @@ -139,7 +138,7 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. if isinstance(block, Call): if isinstance(block.subroutine, Schedule): raise CalibrationError( - "Calling a Schedule if forbidden, call ScheduleBlock instead." + "Calling a Schedule is forbidden, call ScheduleBlock instead." ) if (block.subroutine.name, qubits) not in self._schedules: @@ -171,13 +170,14 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. def _exclude_calls( self, schedule: ScheduleBlock, instructions: List[Instruction] ) -> List[Instruction]: - """ + """Return the non-Call instructions. + Recursive function to get all non-Call instructions. This will flatten all blocks - in a ScheduleBlock and return the instructions of the ScheduleBlock leaving out - any Call instructions. + in a :class:`ScheduleBlock` and return the instructions of the ScheduleBlock leaving + out any Call instructions. Args: - schedule: A ScheduleBlock from which to extract the instructions. + schedule: A :class:`ScheduleBlock` from which to extract the instructions. instructions: The list of instructions that is recursively populated. Returns: @@ -223,7 +223,8 @@ def get_template( return self._schedules[ScheduleKey(schedule_name, qubits)] def remove_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): - """ + """Remove a schedule that was previously registered. + Allows users to remove a schedule from the calibrations. The history of the parameters will remain in the calibrations. @@ -276,10 +277,10 @@ def _register_parameter( qubits: Tuple[int, ...], schedule: ScheduleBlock = None, ): - """ - Registers a parameter for the given schedule. This allows self to determine the - parameter instance that corresponds to the given schedule name, parameter name - and qubits. + """Registers a parameter for the given schedule. + + This method allows self to determine the parameter instance that corresponds to the given + schedule name, parameter name and qubits. Args: parameter: The parameter to register. @@ -298,7 +299,8 @@ def _register_parameter( @property def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: - """ + """Return a mapping between parameters and parameter keys. + Returns a dictionary mapping parameters managed by the calibrations to the schedules and qubits and parameter names using the parameters. The values of the dict are sets containing the parameter keys. Parameters that are not attached to a schedule will have None in place @@ -312,7 +314,8 @@ def calibration_parameter( 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. @@ -351,8 +354,7 @@ def add_parameter_value( qubits: Union[int, Tuple[int, ...]] = None, schedule: Union[ScheduleBlock, str] = None, ): - """ - Add a parameter value to the stored parameters. + """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. @@ -385,13 +387,12 @@ def add_parameter_value( self._params[ParameterKey(param_name, qubits, sched_name)].append(value) def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: - """ - Get the index of the parameterized channel based on 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'. + """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. @@ -458,8 +459,7 @@ def get_parameter_value( group: str = "default", cutoff_date: datetime = None, ) -> Union[int, float, complex]: - """ - Retrieves the value of a parameter. + """Retrieves the value of a parameter. Parameters may be linked. get_parameter_value does the following steps: 1) Retrieve the parameter object corresponding to (param, qubits, schedule) @@ -636,9 +636,9 @@ def _assign( group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: - """ - Recursive function to extract and assign parameters from a schedule. The - recursive behaviour is needed to handle Call instructions as the name of + """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 @@ -659,8 +659,8 @@ def _assign( pulse.call(xp) pulse.call(xp, value_dict={ch0: ch1}) - Here, we define the xp schedule for all qubits as a Gaussian. Next, we define a - schedule where both xp schedules are called simultaneously on different channels. + 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. Args: schedule: The schedule with assigned channel indices for which we wish to @@ -755,9 +755,7 @@ def _assign( return ret_schedule.assign_parameters(binding_dict, inplace=False) def schedules(self) -> List[Dict[str, Any]]: - """ - Return the managed schedules in a list of dictionaries to help - users manage their schedules. + """Return the managed schedules in a list of dictionaries. Returns: data: A list of dictionaries with all the schedules in it. The key-value pairs are @@ -779,8 +777,7 @@ def parameters_table( qubit_list: List[Tuple[int, ...]] = None, schedules: List[Union[ScheduleBlock, str]] = None, ) -> List[Dict[str, Any]]: - """ - A convenience function to help users visualize the values of their parameter. + """A convenience function to help users visualize the values of their parameter. Args: parameters: The parameter names that should be included in the returned @@ -826,9 +823,10 @@ def parameters_table( return data def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = False): - """ - Saves the parameterized schedules and parameter values so - that they can be stored in csv files. This method will create three files: + """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. @@ -940,8 +938,7 @@ def load(cls, files: List[str]) -> "Calibrations": @staticmethod def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: - """ - Ensure that qubits is a tuple of ints. + """Ensure that qubits is a tuple of ints. Args: qubits: An int, a tuple of ints, or a string representing a tuple of ints. diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 103dabbae2..1c0c43c956 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -700,6 +700,22 @@ def test_get_template(self): with self.assertRaises(CalibrationError): self.cals.get_template("xp", (3,)) + def test_register_schedule(self): + """Test that we cannot register a schedule in a call.""" + + xp = pulse.Schedule(name="xp") + xp.insert(0, pulse.Play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)), inplace=True) + + with pulse.build(name="call_xp") as call_xp: + pulse.call(xp) + + try: + self.cals.add_schedule(call_xp) + except CalibrationError as error: + self.assertEqual( + error.message, "Calling a Schedule is forbidden, call ScheduleBlock instead." + ) + class CrossResonanceTest(QiskitTestCase): """Setup class for an echoed cross-resonance calibration.""" From 6ada9ce6fcbc6571bfff4fb1c4db433f63c07eda Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 14:35:03 +0200 Subject: [PATCH 150/178] * Added utf-8 encoding. --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index ecda27ce5b..7cbd52a404 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -877,7 +877,7 @@ def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = Fal ) if file_type == "csv": - with open("parameter_config.csv", "w", newline="") as output_file: + with open("parameter_config.csv", "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(body) @@ -887,7 +887,7 @@ def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = Fal if len(values) > 0: header_keys = values[0].keys() - with open("parameter_values.csv", "w", newline="") as output_file: + with open("parameter_values.csv", "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(values) @@ -900,7 +900,7 @@ def save(self, file_type: str = "csv", folder: str = None, overwrite: bool = Fal {"name": key.schedule, "qubits": key.qubits, "schedule": str(sched)} ) - with open("schedules.csv", "w", newline="") as output_file: + with open("schedules.csv", "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(schedules) From bb4fb815056b04f88e937f6f63af25c8c8888065 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 May 2021 14:51:46 +0200 Subject: [PATCH 151/178] * Added a missing utf-8 in loading. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7cbd52a404..5e618bf8a2 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -918,7 +918,7 @@ def load_parameter_values(self, file_name: str = "parameter_values.csv"): file_name: The name of the file that stores the parameters. Will default to parameter_values.csv. """ - with open(file_name) as fp: + with open(file_name, encoding="utf-8") as fp: reader = csv.DictReader(fp, delimiter=",", quotechar='"') for row in reader: From bab9b9af3ed9aeb89a9c23c7299c71ac36530567 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 12 May 2021 20:51:13 +0200 Subject: [PATCH 152/178] * Improved docstrings. --- qiskit_experiments/calibration/calibrations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 5e618bf8a2..77ea48562e 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -198,6 +198,8 @@ def get_template( """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. @@ -550,9 +552,9 @@ def get_schedule( group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: - """Get the schedule and assign values to parameters. + """Get the template schedule with parameters assigned to values. - The parameters in the template schedule block will be assigned to the values managed + 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`. From ba2bc1baa6bb71aa27fe348a62e62affc548810a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 12 May 2021 21:00:13 +0200 Subject: [PATCH 153/178] * Added example of parameter assignment. --- qiskit_experiments/calibration/calibrations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 77ea48562e..70ff4d7dbc 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -559,6 +559,16 @@ def get_schedule( 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. From 3166ae24d97f64fe6baa02531de67d84dd94f14d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 May 2021 11:31:39 +0200 Subject: [PATCH 154/178] * refined the parameter management for coupled parameters. --- .../calibration/calibrations.py | 59 ++++++++- test/calibration/test_calibrations.py | 116 +++++++++++++++++- 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 70ff4d7dbc..75dff97603 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -551,6 +551,7 @@ def get_schedule( assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, + break_parameter_coupling: bool = False, ) -> ScheduleBlock: """Get the template schedule with parameters assigned to values. @@ -584,6 +585,9 @@ def get_schedule( 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. + break_parameter_coupling: Boolean which when True will cause parameter couplings to + be ignored when assigning values to parameters. This only applies to values given + in the assign_params dict. Returns: schedule: A copy of the template schedule with all parameters assigned @@ -596,6 +600,7 @@ def get_schedule( """ 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(): @@ -608,6 +613,7 @@ def get_schedule( 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: @@ -624,7 +630,10 @@ def get_schedule( # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) - assigned_schedule = self._assign(schedule, qubits, assign_params, group, cutoff_date) + # Now assign the other parameters + assigned_schedule = self._assign( + schedule, qubits, assign_params, group, cutoff_date, break_parameter_coupling + ) free_params = set() for param in assign_params.values(): @@ -647,6 +656,7 @@ def _assign( assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, + break_parameter_coupling: bool = False, ) -> ScheduleBlock: """Recursively assign parameters in a schedule. @@ -672,7 +682,33 @@ def _assign( 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. + 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. However, when specifying + the following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we + will obtain a schedule where the amplitudes of both xp pulse are set to + :code:`Parameter("my_new_amp")` due to the parameter coupling. To set the amplitude of + the xp pulse on qubit 2 to the value stored by the calibrations, i.e. 0.3, we need to break + the coupling + + .. code-bloc:: python + + cals.get_schedule( + "xt_xp", + (0, 2), + assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}, + break_parameter_coupling = True + ) Args: schedule: The schedule with assigned channel indices for which we wish to @@ -684,6 +720,9 @@ def _assign( 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. + break_parameter_coupling: Boolean which when True will cause parameter couplings to + be ignored when assigning values to parameters. This only applies to values given + in the assign_params dict. Returns: ret_schedule: The schedule with assigned parameters. @@ -712,6 +751,18 @@ def _assign( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) + # Complete the assignment dictionary with any missing parameter couplings + if not break_parameter_coupling: + assign_params_ = dict(assign_params) + for param_key, value in assign_params.items(): + # Iterate over all keys that point to the parameter pointed to by the + # the key param_key. + for key in self._parameter_map_r[self.calibration_parameter(*param_key)]: + if ParameterKey(key.parameter, qubits_, key.schedule) not in assign_params: + assign_params_[ParameterKey(key.parameter, qubits_, key.schedule)] = value + + assign_params = assign_params_ + # 2) Recursively assign the parameters in the called instructions. ret_schedule = ScheduleBlock( alignment_context=schedule.alignment_context, @@ -724,7 +775,9 @@ def _assign( inst = inst.assigned_subroutine() if isinstance(inst, ScheduleBlock): - inst = self._assign(inst, qubits_, assign_params, group, cutoff_date) + inst = self._assign( + inst, qubits_, assign_params, group, cutoff_date, break_parameter_coupling + ) ret_schedule.append(inst, inplace=True) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 1c0c43c956..6b33fb70c9 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -539,9 +539,15 @@ def test_xt_meas(self): def test_free_parameters(self): """Test that we can get a schedule with free parameters.""" + # Test coupling breaking my_amp = Parameter("my_amp") - assign_dict = {("amp", (0,), "xp"): my_amp} - schedule = self.cals.get_schedule("xt_meas", (0, 2), assign_params=assign_dict) + schedule = self.cals.get_schedule( + "xt_meas", + (0, 2), + assign_params={("amp", (0,), "xp"): my_amp}, + break_parameter_coupling=True, + ) + schedule = block_to_schedule(schedule) with pulse.build(name="xt_meas") as expected: @@ -557,6 +563,29 @@ def test_free_parameters(self): self.assertEqual(schedule.parameters, {my_amp}) self.assertEqual(schedule, expected) + # Test when coupling is preserved. + schedule = self.cals.get_schedule( + "xt_meas", + (0, 2), + assign_params={("amp", (0,), "xp"): my_amp}, + break_parameter_coupling=False, + ) + + schedule = block_to_schedule(schedule) + + with pulse.build(name="xt_meas") as expected: + with pulse.align_sequential(): + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(0)) + pulse.play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) + with pulse.align_sequential(): + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) + pulse.play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) + + expected = block_to_schedule(expected) + + self.assertEqual(schedule.parameters, {my_amp}) + self.assertEqual(schedule, expected) + def test_free_parameters_check(self): """ Test that get_schedule raises an error if the number of parameters does not match. @@ -849,6 +878,89 @@ def test_single_control_channel(self): self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) +class TestCoupledAssigning(QiskitTestCase): + """Test that assigning parameters works when they are coupled in calls.""" + + def setUp(self): + """Create the setting to test.""" + super().setUp() + + controls = {(3, 2): [ControlChannel(10)]} + + self.cals = Calibrations(control_config=controls) + + self.amp_cr = Parameter("amp") + self.amp_xp = Parameter("amp") + self.d0_ = DriveChannel(Parameter("ch0")) + self.c1_ = ControlChannel(Parameter("ch0.1")) + self.sigma = Parameter("σ") + self.width = Parameter("w") + self.dur = Parameter("duration") + + with pulse.build(name="cr_p") as cr_p: + pulse.play(GaussianSquare(self.dur, self.amp_cr, self.sigma, self.width), self.c1_) + + with pulse.build(name="cr_m") as cr_m: + pulse.play(GaussianSquare(self.dur, -self.amp_cr, self.sigma, self.width), self.c1_) + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) + + with pulse.build(name="ecr") as ecr: + with pulse.align_sequential(): + pulse.call(cr_p) + pulse.call(xp) + pulse.call(cr_m) + + self.cals.add_schedule(cr_p) + self.cals.add_schedule(cr_m) + self.cals.add_schedule(xp) + self.cals.add_schedule(ecr) + + self.cals.add_parameter_value(0.3, "amp", (3, 2), "cr_p") + self.cals.add_parameter_value(0.2, "amp", (3,), "xp") + self.cals.add_parameter_value(40, "σ", (), "xp") + self.cals.add_parameter_value(640, "w", (3, 2), "cr_p") + self.cals.add_parameter_value(800, "duration", (3, 2), "cr_p") + + def test_assign_coupled(self): + """Test that we get the proper schedules when they are coupled.""" + + # Test that we can preserve the coupling + my_amp = Parameter("my_amp") + assign_params = {("amp", (3, 2), "cr_p"): my_amp} + sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) + sched = block_to_schedule(sched) + + with pulse.build(name="ecr") as expected: + with pulse.align_sequential(): + pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) + pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + # Test that we can break the coupling + my_amp = Parameter("my_amp") + assign_params = {("amp", (3, 2), "cr_p"): my_amp} + sched = self.cals.get_schedule( + "ecr", (3, 2), assign_params=assign_params, break_parameter_coupling=True + ) + sched = block_to_schedule(sched) + + with pulse.build(name="ecr") as expected: + with pulse.align_sequential(): + pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) + pulse.play(GaussianSquare(800, -0.3, 40, 640), ControlChannel(10)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + class TestFiltering(QiskitTestCase): """Test that the filtering works as expected.""" From 1dd07e843866b1986da886467e482492806fd8de Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 19 May 2021 18:04:21 +0200 Subject: [PATCH 155/178] * Removed break coupling. --- .../calibration/calibrations.py | 44 +++++++------------ test/calibration/test_calibrations.py | 30 +------------ 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 75dff97603..044307ea8d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -551,7 +551,6 @@ def get_schedule( assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, - break_parameter_coupling: bool = False, ) -> ScheduleBlock: """Get the template schedule with parameters assigned to values. @@ -585,9 +584,6 @@ def get_schedule( 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. - break_parameter_coupling: Boolean which when True will cause parameter couplings to - be ignored when assigning values to parameters. This only applies to values given - in the assign_params dict. Returns: schedule: A copy of the template schedule with all parameters assigned @@ -631,9 +627,7 @@ def get_schedule( schedule = schedule.assign_parameters(binding_dict, inplace=False) # Now assign the other parameters - assigned_schedule = self._assign( - schedule, qubits, assign_params, group, cutoff_date, break_parameter_coupling - ) + assigned_schedule = self._assign(schedule, qubits, assign_params, group, cutoff_date) free_params = set() for param in assign_params.values(): @@ -656,7 +650,6 @@ def _assign( assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, - break_parameter_coupling: bool = False, ) -> ScheduleBlock: """Recursively assign parameters in a schedule. @@ -698,16 +691,14 @@ def _assign( the following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we will obtain a schedule where the amplitudes of both xp pulse are set to :code:`Parameter("my_new_amp")` due to the parameter coupling. To set the amplitude of - the xp pulse on qubit 2 to the value stored by the calibrations, i.e. 0.3, we need to break - the coupling + the xp pulse on qubit 2 to the value stored by the calibrations, i.e. 0.3, we do .. code-bloc:: python cals.get_schedule( "xt_xp", (0, 2), - assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}, - break_parameter_coupling = True + assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")} ) Args: @@ -720,9 +711,6 @@ def _assign( 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. - break_parameter_coupling: Boolean which when True will cause parameter couplings to - be ignored when assigning values to parameters. This only applies to values given - in the assign_params dict. Returns: ret_schedule: The schedule with assigned parameters. @@ -752,16 +740,18 @@ def _assign( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) # Complete the assignment dictionary with any missing parameter couplings - if not break_parameter_coupling: - assign_params_ = dict(assign_params) - for param_key, value in assign_params.items(): - # Iterate over all keys that point to the parameter pointed to by the - # the key param_key. - for key in self._parameter_map_r[self.calibration_parameter(*param_key)]: - if ParameterKey(key.parameter, qubits_, key.schedule) not in assign_params: - assign_params_[ParameterKey(key.parameter, qubits_, key.schedule)] = value - - assign_params = assign_params_ + assign_params_ = dict(assign_params) + for param_key, value in assign_params.items(): + # Iterate over all keys that point to the parameter pointed to by the + # the key param_key. + for key in self._parameter_map_r[self.calibration_parameter(*param_key)]: + if ( + ParameterKey(key.parameter, qubits_, key.schedule) not in assign_params + and key.qubits == qubits_ + ): + assign_params_[ParameterKey(key.parameter, qubits_, key.schedule)] = value + + assign_params = assign_params_ # 2) Recursively assign the parameters in the called instructions. ret_schedule = ScheduleBlock( @@ -775,9 +765,7 @@ def _assign( inst = inst.assigned_subroutine() if isinstance(inst, ScheduleBlock): - inst = self._assign( - inst, qubits_, assign_params, group, cutoff_date, break_parameter_coupling - ) + inst = self._assign(inst, qubits_, assign_params, group, cutoff_date) ret_schedule.append(inst, inplace=True) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 6b33fb70c9..9781b50c62 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -545,7 +545,6 @@ def test_free_parameters(self): "xt_meas", (0, 2), assign_params={("amp", (0,), "xp"): my_amp}, - break_parameter_coupling=True, ) schedule = block_to_schedule(schedule) @@ -563,29 +562,6 @@ def test_free_parameters(self): self.assertEqual(schedule.parameters, {my_amp}) self.assertEqual(schedule, expected) - # Test when coupling is preserved. - schedule = self.cals.get_schedule( - "xt_meas", - (0, 2), - assign_params={("amp", (0,), "xp"): my_amp}, - break_parameter_coupling=False, - ) - - schedule = block_to_schedule(schedule) - - with pulse.build(name="xt_meas") as expected: - with pulse.align_sequential(): - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(0)) - pulse.play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) - with pulse.align_sequential(): - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) - pulse.play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) - - expected = block_to_schedule(expected) - - self.assertEqual(schedule.parameters, {my_amp}) - self.assertEqual(schedule, expected) - def test_free_parameters_check(self): """ Test that get_schedule raises an error if the number of parameters does not match. @@ -928,7 +904,7 @@ def test_assign_coupled(self): # Test that we can preserve the coupling my_amp = Parameter("my_amp") - assign_params = {("amp", (3, 2), "cr_p"): my_amp} + assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp} sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) @@ -945,9 +921,7 @@ def test_assign_coupled(self): # Test that we can break the coupling my_amp = Parameter("my_amp") assign_params = {("amp", (3, 2), "cr_p"): my_amp} - sched = self.cals.get_schedule( - "ecr", (3, 2), assign_params=assign_params, break_parameter_coupling=True - ) + sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) with pulse.build(name="ecr") as expected: From da84b8a0bcd3fd71dfb85d752c26d1b9926b13ff Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 19 May 2021 19:00:07 +0200 Subject: [PATCH 156/178] * Corrected docstring. --- qiskit_experiments/calibration/calibrations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 044307ea8d..4d3b564aa9 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -687,11 +687,11 @@ def _assign( cals.get_schedule("xt_xp", (0, 2)) - we will obtain the expected schedule with amplitudes 0.5 and 0.3. However, when specifying - the following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we - will obtain a schedule where the amplitudes of both xp pulse are set to - :code:`Parameter("my_new_amp")` due to the parameter coupling. To set the amplitude of - the xp pulse on qubit 2 to the value stored by the calibrations, i.e. 0.3, we do + 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 From 7209231f8483c81864d2d58d21743d66e1cf7422 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Wed, 19 May 2021 16:28:08 -0400 Subject: [PATCH 157/178] Scope assign_params by qubits and calibration_parameter --- .../calibration/calibrations.py | 67 ++++---- test/calibration/test_calibrations.py | 149 +++++++++++++++++- 2 files changed, 183 insertions(+), 33 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4d3b564aa9..a68e8321ef 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -603,7 +603,7 @@ def get_schedule( if isinstance(assign_param, str): assign_params_[ParameterKey(assign_param, qubits, name)] = value else: - assign_params_[assign_param] = value + assign_params_[ParameterKey(*assign_param)] = value assign_params = assign_params_ else: @@ -647,7 +647,7 @@ def _assign( self, schedule: ScheduleBlock, qubits: Tuple[int, ...], - assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, + assign_params: Dict[Union[str, ParameterKey], ParameterValueType], group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: @@ -739,20 +739,6 @@ def _assign( qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) - # Complete the assignment dictionary with any missing parameter couplings - assign_params_ = dict(assign_params) - for param_key, value in assign_params.items(): - # Iterate over all keys that point to the parameter pointed to by the - # the key param_key. - for key in self._parameter_map_r[self.calibration_parameter(*param_key)]: - if ( - ParameterKey(key.parameter, qubits_, key.schedule) not in assign_params - and key.qubits == qubits_ - ): - assign_params_[ParameterKey(key.parameter, qubits_, key.schedule)] = value - - assign_params = assign_params_ - # 2) Recursively assign the parameters in the called instructions. ret_schedule = ScheduleBlock( alignment_context=schedule.alignment_context, @@ -779,9 +765,35 @@ def _assign( keys.add(ParameterKey(param.name, qubits_, ret_schedule.name)) # 4) Build the parameter binding dictionary. - assign_params = assign_params if assign_params else dict() - binding_dict = {} + assignment_table = {} + for key, value in assign_params.items(): + key_orig = key + if key.qubits == (): + key = ParameterKey(key.parameter, qubits_, key.schedule) + 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 + else: + if ( + key.schedule == ret_schedule.name or + assignment_table[param].schedule != ret_schedule.name + ): + 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 @@ -793,17 +805,14 @@ def _assign( else: raise CalibrationError(f"Parameter key {key} has no parameter.") - if key not in assign_params: - 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, - ) - else: - binding_dict[param] = assign_params[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) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9781b50c62..d371fda02f 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -13,6 +13,7 @@ """Class to test the calibrations.""" import os +import unittest from collections import defaultdict from datetime import datetime from qiskit.circuit import Parameter @@ -854,6 +855,91 @@ def test_single_control_channel(self): self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) +class TestAssignment(QiskitTestCase): + """Test simple assignment""" + def setUp(self): + """Create the setting to test.""" + super().setUp() + + controls = {(3, 2): [ControlChannel(10)]} + + self.cals = Calibrations(control_config=controls) + + self.amp_xp = Parameter("amp") + self.ch0 = Parameter("ch0") + self.d0_ = DriveChannel(self.ch0) + self.ch1 = Parameter("ch1") + self.d1_ = DriveChannel(self.ch1) + self.sigma = Parameter("σ") + self.width = Parameter("w") + self.dur = Parameter("duration") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) + + with pulse.build(name="xpxp") as xpxp: + with pulse.align_left(): + pulse.call(xp) + pulse.call(xp, value_dict={self.ch0: self.ch1}) + + self.cals.add_schedule(xp) + self.cals.add_schedule(xpxp) + + self.cals.add_parameter_value(0.2, "amp", (2,), "xp") + self.cals.add_parameter_value(0.3, "amp", (3,), "xp") + self.cals.add_parameter_value(40, "σ", (), "xp") + + def test_short_key(self): + """Test simple value assignment""" + sched = self.cals.get_schedule("xp", (2,), assign_params={"amp": 0.1}) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) + + self.assertEqual(sched, expected) + + def test_assign_parameter(self): + """Test assigning to a Parameter instance""" + my_amp = Parameter("my_amp") + sched = self.cals.get_schedule("xp", (2,), assign_params={"amp": my_amp}) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) + + self.assertEqual(sched, expected) + + def test_full_key(self): + """Test value assignment with full key""" + sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (2,), "xp"): 0.1}) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) + + self.assertEqual(sched, expected) + + def test_default_qubit(self): + """Test value assignment with default qubit""" + sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (), "xp"): 0.1}) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) + + self.assertEqual(sched, expected) + + def test_default_across_qubits(self): + """Test assigning to multiple schedules through default parameter""" + sched = self.cals.get_schedule("xpxp", (2, 3), assign_params={("amp", (), "xp"): 0.4}) + sched = block_to_schedule(sched) + + with pulse.build(name="xpxp") as expected: + with pulse.align_left(): + pulse.play(Gaussian(160, 0.4, 40), DriveChannel(2)) + pulse.play(Gaussian(160, 0.4, 40), DriveChannel(3)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + class TestCoupledAssigning(QiskitTestCase): """Test that assigning parameters works when they are coupled in calls.""" @@ -867,7 +953,10 @@ def setUp(self): self.amp_cr = Parameter("amp") self.amp_xp = Parameter("amp") - self.d0_ = DriveChannel(Parameter("ch0")) + self.ch0 = Parameter("ch0") + self.d0_ = DriveChannel(self.ch0) + self.ch1 = Parameter("ch1") + self.d1_ = DriveChannel(self.ch1) self.c1_ = ControlChannel(Parameter("ch0.1")) self.sigma = Parameter("σ") self.width = Parameter("w") @@ -888,18 +977,28 @@ def setUp(self): pulse.call(xp) pulse.call(cr_m) + with pulse.build(name="cr_echo_both") as cr_echo_both: + with pulse.align_sequential(): + pulse.call(cr_p) + with pulse.align_left(): + pulse.call(xp) + pulse.call(xp, value_dict={self.ch0: self.ch1}) + pulse.call(cr_m) + self.cals.add_schedule(cr_p) self.cals.add_schedule(cr_m) self.cals.add_schedule(xp) self.cals.add_schedule(ecr) + self.cals.add_schedule(cr_echo_both) self.cals.add_parameter_value(0.3, "amp", (3, 2), "cr_p") self.cals.add_parameter_value(0.2, "amp", (3,), "xp") + self.cals.add_parameter_value(0.4, "amp", (2,), "xp") self.cals.add_parameter_value(40, "σ", (), "xp") self.cals.add_parameter_value(640, "w", (3, 2), "cr_p") self.cals.add_parameter_value(800, "duration", (3, 2), "cr_p") - def test_assign_coupled(self): + def test_assign_coupled_explicitly(self): """Test that we get the proper schedules when they are coupled.""" # Test that we can preserve the coupling @@ -918,9 +1017,29 @@ def test_assign_coupled(self): self.assertEqual(sched, expected) - # Test that we can break the coupling + def test_assign_coupled_implicitly(self): + """Test that we get the proper schedules when they are coupled.""" + my_amp = Parameter("my_amp") + my_amp2 = Parameter("my_amp2") + assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp2} + sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) + sched = block_to_schedule(sched) + + with pulse.build(name="ecr") as expected: + with pulse.align_sequential(): + pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) + pulse.play(GaussianSquare(800, -my_amp2, 40, 640), ControlChannel(10)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + def test_break_coupled(self): + """Test that we get the proper schedules when they are coupled.""" my_amp = Parameter("my_amp") - assign_params = {("amp", (3, 2), "cr_p"): my_amp} + my_amp2 = Parameter("my_amp2") + assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp2} sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) @@ -928,6 +1047,28 @@ def test_assign_coupled(self): with pulse.align_sequential(): pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) + pulse.play(GaussianSquare(800, -my_amp2, 40, 640), ControlChannel(10)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + def test_assign_coupled_explicitly_two_channel(self): + """Test that we get the proper schedules when they are coupled.""" + + # Test that we can preserve the coupling + my_amp = Parameter("my_amp") + my_amp2 = Parameter("my_amp2") + assign_params = {("amp", (3,), "xp"): my_amp, ("amp", (2,), "xp"): my_amp2} + sched = self.cals.get_schedule("cr_echo_both", (3, 2), assign_params=assign_params) + sched = block_to_schedule(sched) + + with pulse.build(name="cr_echo_both") as expected: + with pulse.align_sequential(): + pulse.play(GaussianSquare(800, 0.3, 40, 640), ControlChannel(10)) + with pulse.align_left(): + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(3)) + pulse.play(Gaussian(160, my_amp2, 40), DriveChannel(2)) pulse.play(GaussianSquare(800, -0.3, 40, 640), ControlChannel(10)) expected = block_to_schedule(expected) From 86815234e1a4e9d37200591ba7a7a68a9fb30611 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 May 2021 10:44:21 +0200 Subject: [PATCH 158/178] * Added a test and changed test_assign_coupled_implicitly. --- test/calibration/test_calibrations.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index d371fda02f..3dd99b461e 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -1020,8 +1020,7 @@ def test_assign_coupled_explicitly(self): def test_assign_coupled_implicitly(self): """Test that we get the proper schedules when they are coupled.""" my_amp = Parameter("my_amp") - my_amp2 = Parameter("my_amp2") - assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp2} + assign_params = {("amp", (3, 2), "cr_p"): my_amp} sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) @@ -1029,7 +1028,23 @@ def test_assign_coupled_implicitly(self): with pulse.align_sequential(): pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -my_amp2, 40, 640), ControlChannel(10)) + pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + def test_assign_coupled_implicitly_float(self): + """Test that we get the proper schedules when they are coupled.""" + assign_params = {("amp", (3, 2), "cr_m"): 0.8} + sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) + sched = block_to_schedule(sched) + + with pulse.build(name="ecr") as expected: + with pulse.align_sequential(): + pulse.play(GaussianSquare(800, 0.8, 40, 640), ControlChannel(10)) + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) + pulse.play(GaussianSquare(800, -0.8, 40, 640), ControlChannel(10)) expected = block_to_schedule(expected) From 860196aede39911ed12c58dd81bc0eda30e1d2a1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 May 2021 10:47:14 +0200 Subject: [PATCH 159/178] * Changed order of tests for readability. --- test/calibration/test_calibrations.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 3dd99b461e..b7a5665a14 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -1017,34 +1017,34 @@ def test_assign_coupled_explicitly(self): self.assertEqual(sched, expected) - def test_assign_coupled_implicitly(self): + def test_assign_coupled_implicitly_float(self): """Test that we get the proper schedules when they are coupled.""" - my_amp = Parameter("my_amp") - assign_params = {("amp", (3, 2), "cr_p"): my_amp} + assign_params = {("amp", (3, 2), "cr_m"): 0.8} sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) with pulse.build(name="ecr") as expected: with pulse.align_sequential(): - pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) + pulse.play(GaussianSquare(800, 0.8, 40, 640), ControlChannel(10)) pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) + pulse.play(GaussianSquare(800, -0.8, 40, 640), ControlChannel(10)) expected = block_to_schedule(expected) self.assertEqual(sched, expected) - def test_assign_coupled_implicitly_float(self): + def test_assign_coupled_implicitly(self): """Test that we get the proper schedules when they are coupled.""" - assign_params = {("amp", (3, 2), "cr_m"): 0.8} + my_amp = Parameter("my_amp") + assign_params = {("amp", (3, 2), "cr_p"): my_amp} sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) sched = block_to_schedule(sched) with pulse.build(name="ecr") as expected: with pulse.align_sequential(): - pulse.play(GaussianSquare(800, 0.8, 40, 640), ControlChannel(10)) + pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -0.8, 40, 640), ControlChannel(10)) + pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) expected = block_to_schedule(expected) From 3e844bc82c0c1ade88a2537ac7e09371d89bbc05 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 09:43:12 -0400 Subject: [PATCH 160/178] no error for default and specific qubit assign_params --- qiskit_experiments/calibration/calibrations.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a68e8321ef..1e6b18e506 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -771,6 +771,11 @@ def _assign( 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, don't trigger an ambiguous assignment + # error for qubit 1. + continue elif key.qubits != qubits_: continue param = self.calibration_parameter(*key) From dee2f58b35cfe0bcf592ac53a33f15b1a9a99d39 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 09:53:34 -0400 Subject: [PATCH 161/178] Clean up conditional --- .../calibration/calibrations.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 1e6b18e506..e7f7d7a4e6 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -788,16 +788,15 @@ def _assign( if assign_okay: binding_dict[param] = value assignment_table[param] = key_orig - else: - if ( - key.schedule == ret_schedule.name or - assignment_table[param].schedule != ret_schedule.name - ): - raise CalibrationError( - "Ambiguous assignment: assign_params keys " - f"{key_orig} and {assignment_table[param]} " - "resolve to the same parameter." - ) + elif ( + key.schedule == ret_schedule.name or + assignment_table[param].schedule != ret_schedule.name + ): + 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 From 0f3907c0ce9a9e11590310ce05adde32bcc79295 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 09:58:34 -0400 Subject: [PATCH 162/178] Multiple assign_params keys for one param okay if assinging same value --- qiskit_experiments/calibration/calibrations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index e7f7d7a4e6..89183a01d0 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -789,8 +789,10 @@ def _assign( binding_dict[param] = value assignment_table[param] = key_orig elif ( - key.schedule == ret_schedule.name or - assignment_table[param].schedule != ret_schedule.name + ( + 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 " From dae34e13c791576959b2226f259e0f9ccfd0c96e Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 10:04:27 -0400 Subject: [PATCH 163/178] Clarify assign_params comment --- qiskit_experiments/calibration/calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 89183a01d0..7e18722855 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -773,8 +773,9 @@ def _assign( key = ParameterKey(key.parameter, qubits_, key.schedule) if key in assign_params: # if (param, (1,), sched) and (param, (), sched) are both - # in assign_params, don't trigger an ambiguous assignment - # error for qubit 1. + # in assign_params, skip the default value instead of + # possibly triggering an error about conflicting + # parameters. continue elif key.qubits != qubits_: continue From 145c190c53e3422645ca35568c2a9b19b1a992d0 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 13:46:20 -0400 Subject: [PATCH 164/178] Test assign parameter in call --- test/calibration/test_calibrations.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index b7a5665a14..da6c49b977 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -882,6 +882,7 @@ def setUp(self): pulse.call(xp) pulse.call(xp, value_dict={self.ch0: self.ch1}) + self.xp = xp self.cals.add_schedule(xp) self.cals.add_schedule(xpxp) @@ -908,6 +909,22 @@ def test_assign_parameter(self): self.assertEqual(sched, expected) + def test_assign_parameter_in_call(self): + """Test assigning to a Parameter instance in a call""" + with pulse.build(name="call_xp") as call_xp: + pulse.call(self.xp) + self.cals.add_schedule(call_xp) + + my_amp = Parameter("my_amp") + sched = self.cals.get_schedule("call_xp", (2,), assign_params={("amp", (2,), "xp"): my_amp}) + sched = block_to_schedule(sched) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + def test_full_key(self): """Test value assignment with full key""" sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (2,), "xp"): 0.1}) From 90b0dd8977293e6c8356cb11b0a11fc7429a580a Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 20 May 2021 14:11:48 -0400 Subject: [PATCH 165/178] Test assigning to same param name in two levels of subroutines --- test/calibration/test_calibrations.py | 29 +++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index da6c49b977..541e901718 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -899,7 +899,7 @@ def test_short_key(self): self.assertEqual(sched, expected) - def test_assign_parameter(self): + def test_assign_to_parameter(self): """Test assigning to a Parameter instance""" my_amp = Parameter("my_amp") sched = self.cals.get_schedule("xp", (2,), assign_params={"amp": my_amp}) @@ -909,7 +909,7 @@ def test_assign_parameter(self): self.assertEqual(sched, expected) - def test_assign_parameter_in_call(self): + def test_assign_to_parameter_in_call(self): """Test assigning to a Parameter instance in a call""" with pulse.build(name="call_xp") as call_xp: pulse.call(self.xp) @@ -925,6 +925,31 @@ def test_assign_parameter_in_call(self): self.assertEqual(sched, expected) + def test_assign_to_parameter_in_call_and_caller(self): + """Test assigning to a Parameter instance in a call""" + with pulse.build(name="call_xp_xp") as call_xp_xp: + pulse.call(self.xp) + pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) + self.cals.add_schedule(call_xp_xp) + + my_amp = Parameter("amp") + sched = self.cals.get_schedule( + "call_xp_xp", + (2,), + assign_params={ + ("amp", (2,), "xp"): my_amp, + ("amp", (2,), "call_xp_xp"): 0.2, + }, + ) + sched = block_to_schedule(sched) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(2)) + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + def test_full_key(self): """Test value assignment with full key""" sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (2,), "xp"): 0.1}) From c597e524118a699a58d60dff1e91f7bb74651402 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 09:08:44 +0200 Subject: [PATCH 166/178] * Made use of self.calibration_parameter. --- qiskit_experiments/calibration/calibrations.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 4d3b564aa9..348c6f9b0f 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -786,12 +786,7 @@ def _assign( # 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. - if key in self._parameter_map: - param = self._parameter_map[key] - elif ParameterKey(key.parameter, (), key.schedule) in self._parameter_map: - param = self._parameter_map[ParameterKey(key.parameter, (), key.schedule)] - else: - raise CalibrationError(f"Parameter key {key} has no parameter.") + param = self.calibration_parameter(*key) if key not in assign_params: if param not in binding_dict: From a15b6a93ff20d22a49cf0de2a60e718faf61e318 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 09:17:58 +0200 Subject: [PATCH 167/178] * Docstring update. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 348c6f9b0f..7a274480d2 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -106,7 +106,7 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. - If a channel is parameterized by more than one parameter. - If the schedule name starts with the prefix of ScheduleBlock. - If the schedule calls subroutines that have not been registered. - - If a Schedule is Called. + - If a :class:`Schedule` is Called instead of a :class:`ScheduleBlock`. """ qubits = self._to_tuple(qubits) From fd7db9d9af5223cc203d0f6e7a84869db9cfe308 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 09:51:04 +0200 Subject: [PATCH 168/178] * Added check on parameter consistency across subroutines. * get_template can now handle default schedules. --- .../calibration/calibrations.py | 31 +++++++++++++------ test/calibration/test_calibrations.py | 3 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 7a274480d2..b42de0ff34 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -209,20 +209,23 @@ def get_template( The registered template schedule. Raises: - CalibrationError: if np template schedule for the given schedule name and qubits + CalibrationError: if no template schedule for the given schedule name and qubits was registered. """ - qubits = self._to_tuple(qubits) + key = ScheduleKey(schedule_name, self._to_tuple(qubits)) - if ScheduleKey(schedule_name, qubits) not in self._schedules: - if qubits: - msg = f"Could not find schedule {schedule_name} on qubits {qubits}." - else: - msg = f"Could not find schedule {schedule_name}." + if key in self._schedules: + return self._schedules[key] - raise CalibrationError(msg) + if ScheduleKey(schedule_name, ()) in self._schedules: + return self._schedules[ScheduleKey(schedule_name, ())] - return self._schedules[ScheduleKey(schedule_name, qubits)] + 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) def remove_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): """Remove a schedule that was previously registered. @@ -762,6 +765,16 @@ def _assign( for inst in schedule.blocks: if isinstance(inst, Call): + # Check that there are no parameter inconsistencies. + template_subroutine = self.get_template(inst.subroutine.name, qubits_) + for param_ in inst.subroutine.parameters: + if param_ not in template_subroutine.parameters: + raise CalibrationError( + f"The parameters in the called sub-routine {inst.subroutine.name} " + "do not match those in the registered ScheduleBlock " + f"{template_subroutine.name} with the same name." + ) + inst = inst.assigned_subroutine() if isinstance(inst, ScheduleBlock): diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 9781b50c62..58e584485d 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -702,8 +702,7 @@ def test_get_template(self): with self.assertRaises(CalibrationError): self.cals.get_template("not registered") - with self.assertRaises(CalibrationError): - self.cals.get_template("xp", (3,)) + self.cals.get_template("xp", (3,)) def test_register_schedule(self): """Test that we cannot register a schedule in a call.""" From abbe21675c7125205bd352ef6410c01364142be1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 09:57:16 +0200 Subject: [PATCH 169/178] * Renamed a Test. --- test/calibration/test_calibrations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 58e584485d..035360a2e6 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -257,8 +257,12 @@ def test_qubit_input(self): self.cals.get_parameter_value("amp", [3], "xp") -class TestCalibrationDefaults(QiskitTestCase): - """Test that we can override defaults.""" +class TestOverrideDefaults(QiskitTestCase): + """ + Test that we can override defaults. For example, this means that all qubits may have a + Gaussian as xp pulse but a specific qubit may have a Drag pulse which overrides the + default Gaussian. + """ def setUp(self): """Create the setting to test.""" From 03f2d7b03cda676fcdd25c97723344713fb56087 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 10:50:58 +0200 Subject: [PATCH 170/178] * ParameterValueType refactor. --- qiskit_experiments/calibration/calibrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index b42de0ff34..d9b0fd251d 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -40,7 +40,7 @@ ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) -ParameterValueType = Union["ParameterExpression", float, int, complex] +ParameterValueType = Union[ParameterExpression, float, int, complex] class Calibrations: From 4fe70a1ed7745c90632d88eba1421b63ccae04cd Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Fri, 21 May 2021 11:01:51 -0400 Subject: [PATCH 171/178] Test nested assignment to Parameter edge case --- test/calibration/test_calibrations.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 541e901718..2edaac325b 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -926,7 +926,7 @@ def test_assign_to_parameter_in_call(self): self.assertEqual(sched, expected) def test_assign_to_parameter_in_call_and_caller(self): - """Test assigning to a Parameter instance in a call""" + """Test assigning to a Parameter instances in a call and caller""" with pulse.build(name="call_xp_xp") as call_xp_xp: pulse.call(self.xp) pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) @@ -950,6 +950,30 @@ def test_assign_to_parameter_in_call_and_caller(self): self.assertEqual(sched, expected) + def test_assign_to_parameter_in_call_and_to_value_in_caller(self): + """ + Test assigning to a Parameter in a call and reassigning in caller raises + + Check that it is not allowed to leave a parameter in a subschedule free + by assigning it to a Parameter that is also used in the calling + schedule as that will re-bind the Parameter in the subschedule as well. + """ + with pulse.build(name="call_xp_xp") as call_xp_xp: + pulse.call(self.xp) + pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) + self.cals.add_schedule(call_xp_xp) + + my_amp = Parameter("amp") + with self.assertRaises(CalibrationError): + self.cals.get_schedule( + "call_xp_xp", + (2,), + assign_params={ + ("amp", (2,), "xp"): self.amp_xp, + ("amp", (2,), "call_xp_xp"): my_amp, + }, + ) + def test_full_key(self): """Test value assignment with full key""" sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (2,), "xp"): 0.1}) From dd773a1f9c620c03dce47391f3de614bd503724c Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Fri, 21 May 2021 11:03:57 -0400 Subject: [PATCH 172/178] Remvoe unused import --- test/calibration/test_calibrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 2edaac325b..82f312d3e2 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -13,7 +13,6 @@ """Class to test the calibrations.""" import os -import unittest from collections import defaultdict from datetime import datetime from qiskit.circuit import Parameter From 7c43741337ea19d9b36bbbc63c05fbb1fd51dc3d Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Fri, 21 May 2021 11:28:15 -0400 Subject: [PATCH 173/178] Fix test names --- test/calibration/test_calibrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 82f312d3e2..fb65d4214f 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -924,7 +924,7 @@ def test_assign_to_parameter_in_call(self): self.assertEqual(sched, expected) - def test_assign_to_parameter_in_call_and_caller(self): + def test_assign_to_parameter_in_call_and_to_value_in_caller(self): """Test assigning to a Parameter instances in a call and caller""" with pulse.build(name="call_xp_xp") as call_xp_xp: pulse.call(self.xp) @@ -949,7 +949,7 @@ def test_assign_to_parameter_in_call_and_caller(self): self.assertEqual(sched, expected) - def test_assign_to_parameter_in_call_and_to_value_in_caller(self): + def test_assign_to_same_parameter_in_call_and_caller(self): """ Test assigning to a Parameter in a call and reassigning in caller raises From 05b9582aeae725cd772ceab5f0fbebd9ed43400e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 17:51:56 +0200 Subject: [PATCH 174/178] * Black and lint. --- qiskit_experiments/calibration/calibrations.py | 14 ++++++-------- test/calibration/test_calibrations.py | 10 ++++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 57539ceff8..a41c584824 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -795,19 +795,17 @@ def _assign( 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 + 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 - ): + 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]} " diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 4ad1e18c3d..225d74c8d1 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -859,6 +859,7 @@ def test_single_control_channel(self): class TestAssignment(QiskitTestCase): """Test simple assignment""" + def setUp(self): """Create the setting to test.""" super().setUp() @@ -884,7 +885,7 @@ def setUp(self): pulse.call(xp) pulse.call(xp, value_dict={self.ch0: self.ch1}) - self.xp = xp + self.xp_ = xp self.cals.add_schedule(xp) self.cals.add_schedule(xpxp) @@ -914,7 +915,7 @@ def test_assign_to_parameter(self): def test_assign_to_parameter_in_call(self): """Test assigning to a Parameter instance in a call""" with pulse.build(name="call_xp") as call_xp: - pulse.call(self.xp) + pulse.call(self.xp_) self.cals.add_schedule(call_xp) my_amp = Parameter("my_amp") @@ -930,7 +931,7 @@ def test_assign_to_parameter_in_call(self): def test_assign_to_parameter_in_call_and_to_value_in_caller(self): """Test assigning to a Parameter instances in a call and caller""" with pulse.build(name="call_xp_xp") as call_xp_xp: - pulse.call(self.xp) + pulse.call(self.xp_) pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) self.cals.add_schedule(call_xp_xp) @@ -961,7 +962,7 @@ def test_assign_to_same_parameter_in_call_and_caller(self): schedule as that will re-bind the Parameter in the subschedule as well. """ with pulse.build(name="call_xp_xp") as call_xp_xp: - pulse.call(self.xp) + pulse.call(self.xp_) pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) self.cals.add_schedule(call_xp_xp) @@ -1008,6 +1009,7 @@ def test_default_across_qubits(self): self.assertEqual(sched, expected) + class TestCoupledAssigning(QiskitTestCase): """Test that assigning parameters works when they are coupled in calls.""" From e0c6b7f745645e3c9bf47d65c59735c7fa592fce Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 21:19:24 +0200 Subject: [PATCH 175/178] * Added check for subroutine inconsistencies and corresponding tests. --- .../calibration/calibrations.py | 6 +++ test/calibration/test_calibrations.py | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index a41c584824..078f4f6302 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -761,6 +761,12 @@ def _assign( f"{template_subroutine.name} with the same name." ) + 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): diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py index 225d74c8d1..da7e085a57 100644 --- a/test/calibration/test_calibrations.py +++ b/test/calibration/test_calibrations.py @@ -1010,6 +1010,59 @@ def test_default_across_qubits(self): self.assertEqual(sched, expected) +class TestReplaceScheduleAndCall(QiskitTestCase): + """A test to ensure that inconsistencies are picked up when a schedule is reassigned.""" + + def setUp(self): + """Create the setting to test.""" + super().setUp() + + self.cals = Calibrations() + + self.amp = Parameter("amp") + self.dur = Parameter("duration") + self.sigma = Parameter("σ") + self.beta = Parameter("β") + self.ch0 = Parameter("ch0") + + with pulse.build(name="xp") as xp: + pulse.play(Gaussian(self.dur, self.amp, self.sigma), DriveChannel(self.ch0)) + + with pulse.build(name="call_xp") as call_xp: + pulse.call(xp) + + self.cals.add_schedule(xp) + self.cals.add_schedule(call_xp) + + self.cals.add_parameter_value(0.2, "amp", (4,), "xp") + self.cals.add_parameter_value(160, "duration", (4,), "xp") + self.cals.add_parameter_value(40, "σ", (), "xp") + + def test_call_replaced(self): + """Test that we get an error when there is an inconsistency in subroutines.""" + + sched = self.cals.get_schedule("call_xp", (4,)) + sched = block_to_schedule(sched) + + with pulse.build(name="xp") as expected: + pulse.play(Gaussian(160, 0.2, 40), DriveChannel(4)) + + expected = block_to_schedule(expected) + + self.assertEqual(sched, expected) + + # Now update the xp pulse without updating the call_xp schedule and ensure that + # an error is raised. + with pulse.build(name="xp") as drag: + pulse.play(Drag(self.dur, self.amp, self.sigma, self.beta), DriveChannel(self.ch0)) + + self.cals.add_schedule(drag) + self.cals.add_parameter_value(10.0, "β", (4,), "xp") + + with self.assertRaises(CalibrationError): + self.cals.get_schedule("call_xp", (4,)) + + class TestCoupledAssigning(QiskitTestCase): """Test that assigning parameters works when they are coupled in calls.""" From 31a11e75e43f13ebbf76e51adc64eb0eb2427cb8 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 May 2021 22:25:00 +0200 Subject: [PATCH 176/178] * Removed the check on parameters in _assign as the schedule check covers that. --- qiskit_experiments/calibration/calibrations.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 078f4f6302..cb2fa71aee 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -721,7 +721,9 @@ def _assign( Raises: CalibrationError: - If a channel has not been assigned. - - If a parameter that is needed does not have a value. + - 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() @@ -751,16 +753,8 @@ def _assign( for inst in schedule.blocks: if isinstance(inst, Call): - # Check that there are no parameter inconsistencies. + # Check that there are no inconsistencies with the called subroutines. template_subroutine = self.get_template(inst.subroutine.name, qubits_) - for param_ in inst.subroutine.parameters: - if param_ not in template_subroutine.parameters: - raise CalibrationError( - f"The parameters in the called sub-routine {inst.subroutine.name} " - "do not match those in the registered ScheduleBlock " - f"{template_subroutine.name} with the same name." - ) - if inst.subroutine != template_subroutine: raise CalibrationError( f"The subroutine {inst.subroutine.name} called by {inst.name} does not " From c492b35d2aeea808f1e1a365207be6b6d110e7af Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:47:26 +0200 Subject: [PATCH 177/178] Update qiskit_experiments/calibration/calibrations.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/calibration/calibrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index cb2fa71aee..641060dbe3 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -116,9 +116,9 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): raise CalibrationError( - f"A registered schedule name cannot start with {ScheduleBlock.prefix}, " - f"received {schedule.name}. " - f"Use a name that does not start with {ScheduleBlock.prefix}." + f"{self.__class__.__name__} uses `name` property of the schedule as a part of database key. " + f"Using automatically generated name {schedule.name} may hurt handiness of calibration. " + "Please define meaningful and unique name to represent this schedule. " ) param_indices = set() From 048684dc3c93f363fe82c60058e53083556073ab Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Jun 2021 18:00:38 +0200 Subject: [PATCH 178/178] * Added an Enum type for the frequency elements. * Reworded error message. --- .../calibration/backend_calibrations.py | 28 +++++++++++++++---- .../calibration/calibrations.py | 6 ++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/calibration/backend_calibrations.py b/qiskit_experiments/calibration/backend_calibrations.py index 5319b1e5d7..99414a8a19 100644 --- a/qiskit_experiments/calibration/backend_calibrations.py +++ b/qiskit_experiments/calibration/backend_calibrations.py @@ -13,12 +13,21 @@ """Store and manage the results of calibration experiments in the context of a backend.""" from datetime import datetime +from enum import Enum from typing import List import copy from qiskit.providers.backend import BackendV1 as Backend from qiskit.circuit import Parameter from qiskit_experiments.calibration.calibrations import Calibrations, ParameterKey +from qiskit_experiments.calibration.exceptions import CalibrationError + + +class FrequencyElement(Enum): + """An extendable enum for components that have a frequency.""" + + QUBIT = "Qubit" + READOUT = "Readout" class BackendCalibrations(Calibrations): @@ -46,23 +55,30 @@ def __init__(self, backend: Backend): def _get_frequencies( self, - meas_freq: bool, + element: FrequencyElement, group: str = "default", cutoff_date: datetime = None, ) -> List[float]: """Internal helper method.""" - param = self.meas_freq.name if meas_freq else self.qubit_freq.name + 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: if ParameterKey(None, param, (qubit,)) in self._params: freq = self.get_parameter_value(param, (qubit,), None, True, group, cutoff_date) else: - if meas_freq: + if element == FrequencyElement.READOUT: freq = self._backend.defaults().meas_freq_est[qubit] - else: + 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) @@ -90,7 +106,7 @@ def get_qubit_frequencies( Returns: A List of qubit frequencies for all qubits of the backend. """ - return self._get_frequencies(False, group, cutoff_date) + return self._get_frequencies(FrequencyElement.QUBIT, group, cutoff_date) def get_meas_frequencies( self, @@ -114,7 +130,7 @@ def get_meas_frequencies( Returns: A List of measurement frequencies for all qubits of the backend. """ - return self._get_frequencies(True, group, cutoff_date) + return self._get_frequencies(FrequencyElement.READOUT, group, cutoff_date) def export_backend(self) -> Backend: """ diff --git a/qiskit_experiments/calibration/calibrations.py b/qiskit_experiments/calibration/calibrations.py index 641060dbe3..228ff93808 100644 --- a/qiskit_experiments/calibration/calibrations.py +++ b/qiskit_experiments/calibration/calibrations.py @@ -116,9 +116,9 @@ def add_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, .. # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): raise CalibrationError( - f"{self.__class__.__name__} uses `name` property of the schedule as a part of database key. " - f"Using automatically generated name {schedule.name} may hurt handiness of calibration. " - "Please define meaningful and unique name to represent this schedule. " + 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()