def update_calibrations(self, experiment_data: ExperimentData): """Update the drag parameter of the pulse in the calibrations.""" result_index = self.experiment_options.result_index group = experiment_data.metadata["cal_group"] target_angle = experiment_data.metadata["target_angle"] qubits = experiment_data.metadata["physical_qubits"] schedule = self._cals.get_schedule(self._sched_name, qubits) # Obtain sigma as it is needed for the fine DRAG update rule. sigmas = [] for block in schedule.blocks: if isinstance(block, Play) and hasattr(block.pulse, "sigma"): sigmas.append(getattr(block.pulse, "sigma")) if len(set(sigmas)) != 1: raise CalibrationError( "Cannot run fine Drag calibration on a schedule with multiple values of sigma." ) if len(sigmas) == 0: raise CalibrationError(f"Could not infer sigma from {schedule}.") d_theta = BaseUpdater.get_value(experiment_data, "d_theta", result_index) # See the documentation in fine_drag.py for the derivation of this rule. d_beta = -np.sqrt(np.pi) * d_theta * sigmas[0] / target_angle**2 old_beta = experiment_data.metadata["cal_param_value"] new_beta = old_beta + d_beta BaseUpdater.add_parameter_value(self._cals, experiment_data, new_beta, self._param_name, schedule, group)
def _get_frequencies( self, element: FrequencyElement, group: str = "default", cutoff_date: datetime = None, ) -> List[float]: """Internal helper method.""" if element == FrequencyElement.READOUT: param = self.meas_freq.name elif element == FrequencyElement.QUBIT: param = self.qubit_freq.name else: raise CalibrationError( f"Frequency element {element} is not supported.") freqs = [] for qubit in self._qubits: schedule = None # A qubit frequency is not attached to a schedule. if ParameterKey(param, (qubit, ), schedule) in self._params: freq = self.get_parameter_value(param, (qubit, ), schedule, True, group, cutoff_date) else: if element == FrequencyElement.READOUT: freq = self._backend.defaults().meas_freq_est[qubit] elif element == FrequencyElement.QUBIT: freq = self._backend.defaults().qubit_freq_est[qubit] else: raise CalibrationError( f"Frequency element {element} is not supported.") freqs.append(freq) return freqs
def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """Create the circuits for the Rabi experiment. Args: backend: A backend object. Returns: A list of circuits with a rabi gate with an attached schedule. Each schedule will have a different value of the scanned amplitude. Raises: CalibrationError: - If the user-provided schedule does not contain a channel with an index that matches the qubit on which to run the Rabi experiment. - If the user provided schedule has more than one free parameter. """ schedule = self.experiment_options.get("schedule", None) if schedule is None: schedule = self._default_gate_schedule(backend=backend) else: if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): raise CalibrationError( f"User provided schedule {schedule.name} does not contain a channel " "for the qubit on which to run Rabi." ) if len(schedule.parameters) != 1: raise CalibrationError("Schedule in Rabi must have exactly one free parameter.") param = next(iter(schedule.parameters)) # Create template circuit circuit = self._template_circuit(param) circuit.add_calibration( self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param] ) # Create the circuits to run circs = [] for amp in self.experiment_options.amplitudes: amp = np.round(amp, decimals=6) assigned_circ = circuit.assign_parameters({param: amp}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, "qubits": (self.physical_qubits[0],), "xval": amp, "unit": "arb. unit", "amplitude": amp, "schedule": str(schedule), } if backend: assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt", "n.a.") circs.append(assigned_circ) return circs
def _validated_value(value: str) -> Union[int, float, complex]: """ Convert the string representation of value to the correct type. Args: value: The string to convert to either an int, float, or complex. Returns: value converted to either int, float, or complex. Raises: CalibrationError: If the conversion fails. """ try: return int(value) except ValueError: pass try: return float(value) except ValueError: pass try: return complex(value) except ValueError as val_err: raise CalibrationError( f"Could not convert {value} to int, float, or complex." ) from val_err
def __init__( self, basis_gates: Optional[List[str]] = None, default_values: Optional[Dict] = None, **extra_kwargs, ): """Setup the library. Args: basis_gates: The basis gates to generate. default_values: A dictionary to override library default parameter values. extra_kwargs: Extra key-word arguments of the subclasses that are saved to be able to reconstruct the library using the :meth:`__init__` method. Raises: CalibrationError: If on of the given basis gates is not supported by the library. """ # Update the default values. self._extra_kwargs = extra_kwargs self._default_values = self.__default_values__.copy() if default_values is not None: self._default_values.update(default_values) if basis_gates is None: basis_gates = list(self.__supported_gates__) for gate in basis_gates: if gate not in self.__supported_gates__: raise CalibrationError( f"Gate {gate} is not supported by {self.__class__.__name__}. " f"Supported gates are: {self.__supported_gates__}.") self._schedules = self._build_schedules(set(basis_gates))
def get_template( self, schedule_name: str, qubits: Optional[Tuple[int, ...]] = None) -> ScheduleBlock: """Get a template schedule. Allows the user to get a template schedule that was previously registered. A template schedule will typically be fully parametric, i.e. all pulse parameters and channel indices are represented by :class:`Parameter`. Args: schedule_name: The name of the template schedule. qubits: The qubits under which the template schedule was registered. Returns: The registered template schedule. Raises: CalibrationError: if no template schedule for the given schedule name and qubits was registered. """ key = ScheduleKey(schedule_name, self._to_tuple(qubits)) if key in self._schedules: return self._schedules[key] if ScheduleKey(schedule_name, ()) in self._schedules: return self._schedules[ScheduleKey(schedule_name, ())] if qubits: msg = f"Could not find schedule {schedule_name} on qubits {qubits}." else: msg = f"Could not find schedule {schedule_name}." raise CalibrationError(msg)
def __getitem__(self, name: str) -> ScheduleBlock: """Return the schedule.""" if name not in self._schedules: raise CalibrationError( f"Gate {name} is not contained in {self.__class__.__name__}.") return self._schedules[name]
def load(cls, files: List[str]) -> "Calibrations": """ Retrieves the parameterized schedules and pulse parameters from the given location. """ raise CalibrationError( "Full calibration loading is not implemented yet.")
def __init__( self, basis_gates: Optional[List[str]] = None, default_values: Optional[Dict] = None ): """Setup the library. Args: basis_gates: The basis gates to generate. default_values: A dictionary to override library default parameter values. Raises: CalibrationError: If on of the given basis gates is not supported by the library. """ self._schedules = dict() # Update the default values. self._default_values = dict(self.__default_values__) if default_values is not None: self._default_values.update(default_values) if basis_gates is None: self._basis_gates = self.__supported_gates__ else: self._basis_gates = dict() for gate in basis_gates: if gate not in self.__supported_gates__: raise CalibrationError( f"Gate {gate} is not supported by {self.__class__.__name__}. " f"Supported gates are: {self.__supported_gates__}." ) self._basis_gates[gate] = self.__supported_gates__[gate]
def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the fine amplitude calibration experiment. Returns: A list of circuits with a variable number of gates. Raises: CalibrationError: If the analysis options do not contain the angle_per_gate. """ # Prepare the circuits. repetitions = self.experiment_options.get("repetitions") circuits = [] if self.experiment_options.add_xp_circuit: # Note that the rotation error in this xval will be overweighted when calibrating xp # because it will be treated as a half pulse instead of a full pulse. However, since # the qubit population is first-order insensitive to rotation errors for an xp pulse # this point won't contribute much to inferring the angle error. angle_per_gate = self.analysis.options.get("angle_per_gate", None) phase_offset = self.analysis.options.get("phase_offset") if angle_per_gate is None: raise CalibrationError( f"Unknown angle_per_gate for {self.__class__.__name__}. " "Please set it in the analysis options.") circuit = QuantumCircuit(1) circuit.x(0) circuit.measure_all() circuit.metadata = { "experiment_type": self._type, "qubits": self.physical_qubits, "xval": (np.pi - phase_offset) / angle_per_gate, "unit": "gate number", } circuits.append(circuit) for repetition in repetitions: circuit = self._pre_circuit() for _ in range(repetition): circuit.append(self.experiment_options.gate, (0, )) circuit.measure_all() circuit.metadata = { "experiment_type": self._type, "qubits": self.physical_qubits, "xval": repetition, "unit": "gate number", } circuits.append(circuit) return circuits
def update( cls, calibrations: Calibrations, exp_data: ExperimentData, result_index: Optional[int] = -1, group: str = "default", angles_schedules: List[Tuple[float, str, Union[str, ScheduleBlock]]] = None, **options, ): """Update the amplitude of pulses. The value of the amplitude must be derived from the fit so the base method cannot be used. Args: calibrations: The calibrations to update. exp_data: The experiment data from which to update. result_index: The result index to use which defaults to -1. group: The calibrations group to update. Defaults to "default." angles_schedules: A list of tuples specifying which angle to update for which pulse schedule. Each tuple is of the form: (angle, parameter_name, schedule). Here, angle is the rotation angle for which to extract the amplitude, parameter_name is the name of the parameter whose value is to be updated, and schedule is the schedule or its name that contains the parameter. options: Trailing options. Raises: CalibrationError: If the experiment is not of the supported type. """ from qiskit_experiments.library.calibration.rabi import Rabi if angles_schedules is None: angles_schedules = [(np.pi, "amp", "xp")] if isinstance(exp_data.experiment, Rabi): rate = 2 * np.pi * BaseUpdater.get_value(exp_data, "rabi_rate", result_index) for angle, param, schedule in angles_schedules: qubits = exp_data.metadata["physical_qubits"] prev_amp = calibrations.get_parameter_value(param, qubits, schedule, group=group) value = np.round(angle / rate, decimals=8) * np.exp( 1.0j * np.angle(prev_amp)) cls.add_parameter_value(calibrations, exp_data, value, param, schedule, group) else: raise CalibrationError( f"{cls.__name__} updates from {type(Rabi.__name__)}.")
def update( cls, calibrations: Calibrations, exp_data: ExperimentData, parameter: str, schedule: Union[ScheduleBlock, str], result_index: Optional[int] = -1, group: str = "default", target_angle: float = np.pi, **options, ): """Update the value of a drag parameter measured by the FineDrag experiment. Args: calibrations: The calibrations to update. exp_data: The experiment data from which to update. parameter: The name of the parameter in the calibrations to update. schedule: The ScheduleBlock instance or the name of the instance to which the parameter is attached. result_index: The result index to use. By default search entry by name. group: The calibrations group to update. Defaults to "default." target_angle: The target rotation angle of the pulse. options: Trailing options. Raises: CalibrationError: If we cannot get the pulse's standard deviation from the schedule. """ qubits = exp_data.metadata["physical_qubits"] if isinstance(schedule, str): schedule = calibrations.get_schedule(schedule, qubits) # Obtain sigma as it is needed for the fine DRAG update rule. sigma = None for block in schedule.blocks: if isinstance(block, Play) and hasattr(block.pulse, "sigma"): sigma = getattr(block.pulse, "sigma") if sigma is None: raise CalibrationError(f"Could not infer sigma from {schedule}.") d_theta = BaseUpdater.get_value(exp_data, "d_theta", result_index) # See the documentation in fine_drag.py for the derivation of this rule. d_beta = -np.sqrt(np.pi) * d_theta * sigma / target_angle**2 old_beta = calibrations.get_parameter_value(parameter, qubits, schedule, group=group) new_beta = old_beta + d_beta cls.add_parameter_value(calibrations, exp_data, new_beta, parameter, schedule, group)
def _default_gate_schedule(self, backend: Optional[Backend] = None): """Create the default schedule for the EFRabi gate with a frequency shift to the 1-2 transition.""" if self.experiment_options.frequency_shift is None: try: anharm, _ = backend.properties().qubit_property(self.physical_qubits[0])[ "anharmonicity" ] self.set_experiment_options(frequency_shift=anharm) except KeyError as key_err: raise CalibrationError( f"The backend {backend} does not provide an anharmonicity for qubit " f"{self.physical_qubits[0]}. Use EFRabi.set_experiment_options(frequency_shift=" f"anharmonicity) to manually set the correct frequency for the 1-2 transition." ) from key_err except AttributeError as att_err: raise CalibrationError( "When creating the default schedule without passing a backend, the frequency needs " "to be set manually through EFRabi.set_experiment_options(frequency_shift=..)." ) from att_err amp = Parameter("amp") with pulse.build(backend=backend, name=self.__rabi_gate_name__) as default_schedule: with pulse.frequency_offset( self.experiment_options.frequency_shift, pulse.DriveChannel(self.physical_qubits[0]), ): pulse.play( pulse.Gaussian( duration=self.experiment_options.duration, amp=amp, sigma=self.experiment_options.sigma, ), pulse.DriveChannel(self.physical_qubits[0]), ) return default_schedule
def __init__(self): """Updaters are not meant to be instantiated. Instead of instantiating updaters use them by calling the :meth:`update` class method. For example, the :class:`Frequency` updater is called in the following way .. code-block:: python Frequency.update(calibrations, spectroscopy_data) Here, calibrations is an instance of :class:`Calibrations` and spectroscopy_data is the result of a :class:`QubitSpectroscopy` experiment. """ raise CalibrationError( "Calibration updaters are not meant to be instantiated. The intended usage" "is Updater.update(calibrations, exp_data, ...).")
def _validate_channels(self, schedule: ScheduleBlock): """Check that the physical qubits are contained in the schedule. This is a helper method that experiment developers can call in their implementation of :meth:`validate_schedules` when checking the schedules. Args: schedule: The schedule for which to check the qubits. Raises: CalibrationError: If a physical qubit is not contained in the channels schedule. """ for qubit in self.physical_qubits: if qubit not in set(ch.index for ch in schedule.channels): raise CalibrationError( f"Schedule {schedule.name} does not contain a channel " f"for the physical qubit {qubit}.")
def set_experiment_options(self, reps: Optional[List] = None, **fields): """Raise if reps has a length different from three. Raises: CalibrationError: if the number of repetitions is different from three. """ if reps is None: reps = [1, 3, 5] else: reps = sorted(reps) # ensure reps 1 is the lowest frequency. if len(reps) != 3: raise CalibrationError( f"{self.__class__.__name__} must use exactly three repetition numbers. " f"Received {reps} with length {len(reps)} != 3.") super().set_experiment_options(reps=reps, **fields)
def _validate_parameters(self, schedule: ScheduleBlock, n_expected_parameters: int): """Check that the schedule has the expected number of parameters. This is a helper method that experiment developers can call in their implementation of :meth:`validate_schedules` when checking the schedules. Args: schedule: The schedule for which to check the qubits. n_expected_parameters: The number of free parameters the schedule must have. Raises: CalibrationError: If the schedule does not have n_expected_parameters parameters. """ if len(schedule.parameters) != n_expected_parameters: raise CalibrationError( f"The schedules {schedule.name} for {self.__class__.__name__} must have " f"{n_expected_parameters} parameters. Found {len(schedule.parameters)}." )
def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. Raises: CalibrationError: When ``angle_per_gate`` is missing. """ n_guesses = self._get_option("number_guesses") curve_data = self._data() max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) max_y, min_y = np.max(curve_data.y), np.min(curve_data.y) user_opt.bounds.set_if_empty(d_theta=(-np.pi, np.pi), base=(-max_abs_y, max_abs_y)) user_opt.p0.set_if_empty(base=(max_y + min_y) / 2) if "amp" in user_opt.p0: user_opt.p0.set_if_empty(amp=max_y - min_y) user_opt.bounds.set_if_empty(amp=(-2 * max_abs_y, 2 * max_abs_y)) # Base the initial guess on the intended angle_per_gate. angle_per_gate = self._get_option("angle_per_gate") if angle_per_gate is None: raise CalibrationError( "The angle_per_gate was not specified in the analysis options." ) guess_range = max(abs(angle_per_gate), np.pi / 2) options = [] for d_theta_guess in np.linspace(-guess_range, guess_range, n_guesses): new_opt = user_opt.copy() new_opt.p0.set_if_empty(d_theta=d_theta_guess) options.append(new_opt) return options
def __post_init__(self): """ Ensure that the variables in self have the proper types. This allows us to give strings to self.__init__ as input which is useful when loading serialized parameter values. """ if isinstance(self.valid, str): if self.valid == "True": self.valid = True else: self.valid = False if isinstance(self.value, str): self.value = self._validated_value(self.value) if isinstance(self.date_time, str): base_fmt = "%Y-%m-%d %H:%M:%S.%f" zone_fmts = ["%z", "", "Z"] for time_zone in zone_fmts: date_format = base_fmt + time_zone try: self.date_time = datetime.strptime(self.date_time, date_format) break except ValueError: pass else: formats = list(base_fmt + zone for zone in zone_fmts) raise CalibrationError( f"Cannot parse {self.date_time} in either of {formats} formats." ) self.date_time = self.date_time.astimezone() if not isinstance(self.value, (int, float, complex)): raise CalibrationError( f"Values {self.value} must be int, float or complex.") if not isinstance(self.date_time, datetime): raise CalibrationError( f"Datetime {self.date_time} must be a datetime.") if not isinstance(self.valid, bool): raise CalibrationError(f"Valid {self.valid} is not a boolean.") if self.exp_id and not isinstance(self.exp_id, str): raise CalibrationError( f"Experiment id {self.exp_id} is not a string.") if not isinstance(self.group, str): raise CalibrationError(f"Group {self.group} is not a string.")
def add_parameter_value( self, value: Union[int, float, complex, ParameterValue], param: Union[Parameter, str], qubits: Union[int, Tuple[int, ...]] = None, schedule: Union[ScheduleBlock, str] = None, ): """Add a parameter value to the stored parameters. This parameter value may be applied to several channels, for instance, all DRAG pulses may have the same standard deviation. Args: value: The value of the parameter to add. If an int, float, or complex is given then the timestamp of the parameter value will automatically be generated and set to the current local time of the user. param: The parameter or its name for which to add the measured value. qubits: The qubits to which this parameter applies. schedule: The schedule or its name for which to add the measured parameter value. Raises: CalibrationError: If the schedule name is given but no schedule with that name exists. """ qubits = self._to_tuple(qubits) if isinstance(value, (int, float, complex)): value = ParameterValue(value, datetime.now(timezone.utc).astimezone()) param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule registered_schedules = set(key.schedule for key in self._schedules) if sched_name and sched_name not in registered_schedules: raise CalibrationError( f"Schedule named {sched_name} was never registered.") self._params[ParameterKey(param_name, qubits, sched_name)].append(value)
def calibration_parameter( self, parameter_name: str, qubits: Union[int, Tuple[int, ...]] = None, schedule_name: str = None, ) -> Parameter: """Return a parameter given its keys. Returns a Parameter object given the triplet parameter_name, qubits and schedule_name which uniquely determine the context of a parameter. Args: parameter_name: Name of the parameter to get. qubits: The qubits to which this parameter belongs. If qubits is None then the default scope is assumed and the key will be an empty tuple. schedule_name: The name of the schedule to which this parameter belongs. A parameter may not belong to a schedule in which case None is accepted. Returns: calibration parameter: The parameter that corresponds to the given arguments. Raises: CalibrationError: If the desired parameter is not found. """ qubits = self._to_tuple(qubits) # 1) Check for qubit specific parameters. if ParameterKey(parameter_name, qubits, schedule_name) in self._parameter_map: return self._parameter_map[ParameterKey(parameter_name, qubits, schedule_name)] # 2) Check for default parameters. elif ParameterKey(parameter_name, (), schedule_name) in self._parameter_map: return self._parameter_map[ParameterKey(parameter_name, (), schedule_name)] else: raise CalibrationError( f"No parameter for {parameter_name} and schedule {schedule_name} " f"and qubits {qubits}. No default value exists.")
def set_experiment_options(self, reps: Optional[List] = None, **fields): """Raise if reps has a length different from three. Raises: CalibrationError: if the number of repetitions is different from three. """ if reps is not None: if len(reps) != 3: raise CalibrationError( f"{self.__class__.__name__} must use exactly three repetition numbers. " f"Received {reps} with length {len(reps)} != 3.") reps = sorted(reps) # ensure reps 1 is the lowest frequency. super().set_experiment_options(reps=reps) if isinstance(self.analysis, DragCalAnalysis): self.analysis.set_options(fixed_parameters={ "reps0": reps[0], "reps1": reps[1], "reps2": reps[2] }) super().set_experiment_options(**fields)
def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: """Ensure that qubits is a tuple of ints. Args: qubits: An int, a tuple of ints, or a string representing a tuple of ints. Returns: qubits: A tuple of ints. Raises: CalibrationError: If the given input does not conform to an int or tuple of ints. """ if qubits is None: return tuple() if isinstance(qubits, str): try: return tuple( int(qubit) for qubit in qubits.strip("( )").split(",") if qubit != "") except ValueError: pass if isinstance(qubits, int): return (qubits, ) if isinstance(qubits, list): return tuple(qubits) if isinstance(qubits, tuple): if all(isinstance(n, int) for n in qubits): return qubits raise CalibrationError( f"{qubits} must be int, tuple of ints, or str that can be parsed" f"to a tuple if ints. Received {qubits}.")
def add_schedule( self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None, num_qubits: Optional[int] = None, ): """Add a schedule block and register its parameters. Schedules that use Call instructions must register the called schedules separately. Args: schedule: The :class:`ScheduleBlock` to add. qubits: The qubits for which to add the schedules. If None or an empty tuple is given then this schedule is the default schedule for all qubits and, in this case, the number of qubits that this schedule act on must be given. num_qubits: The number of qubits that this schedule will act on when exported to a circuit instruction. This argument is optional as long as qubits is either not None or not an empty tuple (i.e. default schedule). Raises: CalibrationError: If schedule is not an instance of :class:`ScheduleBlock`. CalibrationError: If the parameterized channel index is not formatted properly. CalibrationError: If several parameters in the same schedule have the same name. CalibrationError: If a channel is parameterized by more than one parameter. CalibrationError: If the schedule name starts with the prefix of ScheduleBlock. CalibrationError: If the schedule calls subroutines that have not been registered. CalibrationError: If a :class:`Schedule` is Called instead of a :class:`ScheduleBlock`. CalibrationError: If a schedule with the same name exists and acts on a different number of qubits. """ qubits = self._to_tuple(qubits) if len(qubits) == 0 and num_qubits is None: raise CalibrationError( "Both qubits and num_qubits cannot simultaneously be None.") num_qubits = len(qubits) or num_qubits if not isinstance(schedule, ScheduleBlock): raise CalibrationError(f"{schedule.name} is not a ScheduleBlock.") sched_key = ScheduleKey(schedule.name, qubits) # Ensure one to one mapping between name and number of qubits. if sched_key in self._schedules_qubits and self._schedules_qubits[ sched_key] != num_qubits: raise CalibrationError( f"Cannot add schedule {schedule.name} acting on {num_qubits} qubits." "self already contains a schedule with the same name acting on " f"{self._schedules_qubits[sched_key]} qubits. Remove old schedule first." ) # check that channels, if parameterized, have the proper name format. if schedule.name.startswith(ScheduleBlock.prefix): raise CalibrationError( f"{self.__class__.__name__} uses the `name` property of the schedule as part of a " f"database key. Using the automatically generated name {schedule.name} may have " f"unintended consequences. Please define a meaningful and unique schedule name." ) param_indices = set() for ch in schedule.channels: if isinstance(ch.index, Parameter): if len(ch.index.parameters) != 1: raise CalibrationError( f"Channel {ch} can only have one parameter.") param_indices.add(ch.index) if re.compile(self.__channel_pattern__).match( ch.index.name) is None: raise CalibrationError( f"Parameterized channel must correspond to {self.__channel_pattern__}" ) # Check that subroutines are present. for block in schedule.blocks: if isinstance(block, Call): if isinstance(block.subroutine, Schedule): raise CalibrationError( "Calling a Schedule is forbidden, call ScheduleBlock instead." ) if (block.subroutine.name, qubits) not in self._schedules: raise CalibrationError( f"Cannot register schedule block {schedule.name} with unregistered " f"subroutine {block.subroutine.name}.") # Clean the parameter to schedule mapping. This is needed if we overwrite a schedule. self._clean_parameter_map(schedule.name, qubits) # Add the schedule. self._schedules[sched_key] = schedule self._schedules_qubits[sched_key] = num_qubits # Register parameters that are not indices. # Do not register parameters that are in call instructions. params_to_register = set() for inst in self._exclude_calls(schedule, []): for param in inst.parameters: if param not in param_indices: params_to_register.add(param) if len(params_to_register) != len( set(param.name for param in params_to_register)): raise CalibrationError( f"Parameter names in {schedule.name} must be unique.") for param in params_to_register: self._register_parameter(param, qubits, schedule)
def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: """Get the index of the parameterized channel. The return index is determined from the given qubits and the name of the parameter in the channel index. The name of this parameter for control channels must be written as chqubit_index1.qubit_index2... followed by an optional $index. For example, the following parameter names are valid: 'ch1', 'ch1.0', 'ch30.12', and 'ch1.0$1'. Args: qubits: The qubits for which we want to obtain the channel index. chan: The channel with a parameterized name. Returns: index: The index of the channel. For example, if qubits=(10, 32) and chan is a control channel with parameterized index name 'ch1.0' the method returns the control channel corresponding to qubits (qubits[1], qubits[0]) which is here the control channel of qubits (32, 10). Raises: CalibrationError: - If the number of qubits is incorrect. - If the number of inferred ControlChannels is not correct. - If ch is not a DriveChannel, MeasureChannel, or ControlChannel. """ if isinstance(chan.index, Parameter): if isinstance(chan, (DriveChannel, MeasureChannel, AcquireChannel, RegisterSlot, MemorySlot)): index = int(chan.index.name[2:].split("$")[0]) if len(qubits) <= index: raise CalibrationError( f"Not enough qubits given for channel {chan}.") return qubits[index] # Control channels name example ch1.0$1 if isinstance(chan, ControlChannel): channel_index_parts = chan.index.name[2:].split("$") qubit_channels = channel_index_parts[0] indices = [ int(sub_channel) for sub_channel in qubit_channels.split(".") ] ch_qubits = tuple(qubits[index] for index in indices) chs_ = self._controls_config[ch_qubits] control_index = 0 if len(channel_index_parts) == 2: control_index = int(channel_index_parts[1]) if len(chs_) <= control_index: raise CalibrationError( f"Control channel index {control_index} not found for qubits {qubits}." ) return chs_[control_index].index raise CalibrationError( f"{chan} must be a sub-type of {PulseChannel} or an {AcquireChannel}, " f"{RegisterSlot}, or a {MemorySlot}.") return chan.index
def get_parameter_value( self, param: Union[Parameter, str], qubits: Union[int, Tuple[int, ...]], schedule: Union[ScheduleBlock, str, None] = None, valid_only: bool = True, group: str = "default", cutoff_date: datetime = None, ) -> Union[int, float, complex]: """Retrieves the value of a parameter. Parameters may be linked. :meth:`get_parameter_value` does the following steps: 1. Retrieve the parameter object corresponding to (param, qubits, schedule). 2. The values of this parameter may be stored under another schedule since schedules can share parameters. To deal with this, a list of candidate keys is created internally based on the current configuration. 3. Look for candidate parameter values under the candidate keys. 4. Filter the candidate parameter values according to their date (up until the cutoff_date), validity and calibration group. 5. Return the most recent parameter. Args: param: The parameter or the name of the parameter for which to get the parameter value. qubits: The qubits for which to get the value of the parameter. schedule: The schedule or its name for which to get the parameter value. valid_only: Use only parameters marked as valid. group: The calibration group from which to draw the parameters. If not specified this defaults to the 'default' group. cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters generated after the cutoff date will be ignored. If the cutoff_date is None then all parameters are considered. This allows users to discard more recent values that may be erroneous. Returns: value: The value of the parameter. Raises: CalibrationError: If there is no parameter value for the given parameter name and pulse channel. """ qubits = self._to_tuple(qubits) # 1) Identify the parameter object. param_name = param.name if isinstance(param, Parameter) else param sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule param = self.calibration_parameter(param_name, qubits, sched_name) # 2) Get a list of candidate keys restricted to the qubits of interest. candidate_keys = [] for key in self._parameter_map_r[param]: candidate_keys.append( ParameterKey(key.parameter, qubits, key.schedule)) # 3) Loop though the candidate keys to candidate values candidates = [] for key in candidate_keys: if key in self._params: candidates += self._params[key] # If no candidate parameter values were found look for default parameters # i.e. parameters that do not specify a qubit. if len(candidates) == 0: for key in candidate_keys: if ParameterKey(key.parameter, (), key.schedule) in self._params: candidates += self._params[ParameterKey( key.parameter, (), key.schedule)] # 4) Filter candidate parameter values. if valid_only: candidates = [val for val in candidates if val.valid] candidates = [val for val in candidates if val.group == group] if cutoff_date: candidates = [ val for val in candidates if val.date_time <= cutoff_date ] if len(candidates) == 0: msg = f"No candidate parameter values for {param_name} in calibration group {group} " if qubits: msg += f"on qubits {qubits} " if sched_name: msg += f"in schedule {sched_name} " if cutoff_date: msg += f"with cutoff date: {cutoff_date}" raise CalibrationError(msg) # 5) Return the most recent parameter. return max(enumerate(candidates), key=lambda x: (x[1].date_time, x[0]))[1].value
def get_schedule( self, name: str, qubits: Union[int, Tuple[int, ...]], assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: """Get the template schedule with parameters assigned to values. All the parameters in the template schedule block will be assigned to the values managed by the calibrations unless they are specified in assign_params. In this case the value in assign_params will override the value stored by the calibrations. A parameter value in assign_params may also be a :class:`ParameterExpression`. .. code-block:: python # Get an xp schedule with a parametric amplitude sched = cals.get_schedule("xp", 3, assign_params={"amp": Parameter("amp")}) # Get an echoed-cross-resonance schedule between qubits (0, 2) where the xp echo gates # are Called schedules but leave their amplitudes as parameters. assign_dict = {("amp", (0,), "xp"): Parameter("my_amp")} sched = cals.get_schedule("cr", (0, 2), assign_params=assign_dict) Args: name: The name of the schedule to get. qubits: The qubits for which to get the schedule. assign_params: The parameters to assign manually. Each parameter is specified by a ParameterKey which is a named tuple of the form (parameter name, qubits, schedule name). Each entry in assign_params can also be a string corresponding to the name of the parameter. In this case, the schedule name and qubits of the corresponding ParameterKey will be the name and qubits given as arguments to get_schedule. group: The calibration group from which to draw the parameters. If not specified this defaults to the 'default' group. cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters generated after the cutoff date will be ignored. If the cutoff_date is None then all parameters are considered. This allows users to discard more recent values that may be erroneous. Returns: schedule: A copy of the template schedule with all parameters assigned except for those specified by assign_params. Raises: CalibrationError: If the name of the schedule is not known. CalibrationError: If a parameter could not be found. """ qubits = self._to_tuple(qubits) # Standardize the input in the assignment dictionary if assign_params: assign_params_ = dict() for assign_param, value in assign_params.items(): if isinstance(assign_param, str): assign_params_[ParameterKey(assign_param, qubits, name)] = value else: assign_params_[ParameterKey(*assign_param)] = value assign_params = assign_params_ else: assign_params = dict() # Get the template schedule if (name, qubits) in self._schedules: schedule = self._schedules[ScheduleKey(name, qubits)] elif (name, ()) in self._schedules: schedule = self._schedules[ScheduleKey(name, ())] else: raise CalibrationError( f"Schedule {name} is not defined for qubits {qubits}.") # Retrieve the channel indices based on the qubits and bind them. binding_dict = {} for ch in schedule.channels: if ch.is_parameterized(): binding_dict[ch.index] = self._get_channel_index(qubits, ch) # Binding the channel indices makes it easier to deal with parameters later on schedule = schedule.assign_parameters(binding_dict, inplace=False) # Now assign the other parameters assigned_schedule = self._assign(schedule, qubits, assign_params, group, cutoff_date) free_params = set() for param in assign_params.values(): if isinstance(param, ParameterExpression): free_params.add(param) if len(assigned_schedule.parameters) != len(free_params): raise CalibrationError( f"The number of free parameters {len(assigned_schedule.parameters)} in " f"the assigned schedule differs from the requested number of free " f"parameters {len(free_params)}.") return assigned_schedule
def __init__( self, backend: Backend, library: BasisGateLibrary = None, num_qubits: Optional[int] = None, ): """Setup an instance to manage the calibrations of a backend. BackendCalibrations can be initialized from a basis gate library, i.e. a subclass of :class:`BasisGateLibrary`. As example consider the following code: .. code-block:: python cals = BackendCalibrations( backend, library=FixedFrequencyTransmon( basis_gates=["x", "sx"], default_values={duration: 320} ) ) Args: backend: A backend instance from which to extract the qubit and readout frequencies (which will be added as first guesses for the corresponding parameters) as well as the coupling map. library: A library class that will be instantiated with the library options to then get template schedules to register as well as default parameter values. num_qubits: Number of qubits in case the backend object fails to specify this in its configuration. Raises: CalibrationError: If the backend configuration does not have num_qubits and num_qubits is None. """ super().__init__( getattr(backend.configuration(), "control_channels", None)) # Instruction schedule map variables and support variables. self._inst_map = InstructionScheduleMap() self._operated_qubits = defaultdict(list) self._update_inst_map = False # When True add_parameter_value triggers an inst. map update # Use the same naming convention as in backend.defaults() self.qubit_freq = Parameter(self.__qubit_freq_parameter__) self.meas_freq = Parameter(self.__readout_freq_parameter__) self._register_parameter(self.qubit_freq, ()) self._register_parameter(self.meas_freq, ()) num_qubits = getattr(backend.configuration(), "num_qubits", num_qubits) if num_qubits is None: raise CalibrationError( "backend.configuration() does not have 'num_qubits' and None given." ) self._qubits = list(range(num_qubits)) self._backend = backend for qubit, freq in enumerate(backend.defaults().qubit_freq_est): self.add_parameter_value(freq, self.qubit_freq, qubit) for meas, freq in enumerate(backend.defaults().meas_freq_est): self.add_parameter_value(freq, self.meas_freq, meas) if library is not None: # Add the basis gates for gate in library.basis_gates: self.add_schedule(library[gate], num_qubits=library.num_qubits(gate)) # Add the default values for param_conf in library.default_values(): schedule_name = param_conf[-1] if schedule_name in library.basis_gates: self.add_parameter_value(*param_conf) self._update_inst_map = True # Push the schedules to the instruction schedule map. self.update_inst_map()
def _assign( self, schedule: ScheduleBlock, qubits: Tuple[int, ...], assign_params: Dict[Union[str, ParameterKey], ParameterValueType], group: Optional[str] = "default", cutoff_date: datetime = None, ) -> ScheduleBlock: """Recursively assign parameters in a schedule. The recursive behaviour is needed to handle Call instructions as the name of the called instruction defines the scope of the parameter. Each time a Call is found _assign recurses on the channel-assigned subroutine of the Call instruction and the qubits that are in said subroutine. This requires a careful extraction of the qubits from the subroutine and in the appropriate order. Next, the parameters are identified and assigned. This is needed to handle situations where the same parameterized schedule is called but on different channels. For example, .. code-block:: python ch0 = Parameter("ch0") ch1 = Parameter("ch1") with pulse.build(name="xp") as xp: pulse.play(Gaussian(duration, amp, sigma), DriveChannel(ch0)) with pulse.build(name="xt_xp") as xt: pulse.call(xp) pulse.call(xp, value_dict={ch0: ch1}) Here, we define the xp :class:`ScheduleBlock` for all qubits as a Gaussian. Next, we define a schedule where both xp schedules are called simultaneously on different channels. We now explain a subtlety related to manually assigning values in the case above. In the schedule above, the parameters of the Gaussian pulses are coupled, e.g. the xp pulse on ch0 and ch1 share the same instance of :class:`ParameterExpression`. Suppose now that both pulses have a duration and sigma of 160 and 40 samples, respectively, and that the amplitudes are 0.5 and 0.3 for qubits 0 and 2, respectively. These values are stored in self._params. When retrieving a schedule without specifying assign_params, i.e. .. code-block:: python cals.get_schedule("xt_xp", (0, 2)) we will obtain the expected schedule with amplitudes 0.5 and 0.3. When specifying the following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we will obtain a schedule where the amplitudes of the xp pulse on qubit 0 is set to :code:`Parameter("my_new_amp")`. The amplitude of the xp pulse on qubit 2 is set to the value stored by the calibrations, i.e. 0.3. .. code-bloc:: python cals.get_schedule( "xt_xp", (0, 2), assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")} ) Args: schedule: The schedule with assigned channel indices for which we wish to assign values to non-channel parameters. qubits: The qubits for which to get the schedule. assign_params: The parameters to manually assign. See get_schedules for details. group: The calibration group of the parameters. cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters generated after the cutoff date will be ignored. If the cutoff_date is None then all parameters are considered. This allows users to discard more recent values that may be erroneous. Returns: ret_schedule: The schedule with assigned parameters. Raises: CalibrationError: - If a channel has not been assigned. - If there is an ambiguous parameter assignment. - If there are inconsistencies between a called schedule and the template schedule registered under the name of the called schedule. """ # 1) Restrict the given qubits to those in the given schedule. qubit_set = set() for chan in schedule.channels: if isinstance(chan.index, ParameterExpression): raise (CalibrationError( f"All parametric channels must be assigned before searching for " f"non-channel parameters. {chan} is parametric.")) if isinstance(chan, (DriveChannel, MeasureChannel)): qubit_set.add(chan.index) if isinstance(chan, ControlChannel): for qubit in self._controls_config_r[chan]: qubit_set.add(qubit) qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) # 2) Recursively assign the parameters in the called instructions. ret_schedule = ScheduleBlock( alignment_context=schedule.alignment_context, name=schedule.name, metadata=schedule.metadata, ) for inst in schedule.blocks: if isinstance(inst, Call): # Check that there are no inconsistencies with the called subroutines. template_subroutine = self.get_template( inst.subroutine.name, qubits_) if inst.subroutine != template_subroutine: raise CalibrationError( f"The subroutine {inst.subroutine.name} called by {inst.name} does not " f"match the template schedule stored under {template_subroutine.name}." ) inst = inst.assigned_subroutine() if isinstance(inst, ScheduleBlock): inst = self._assign(inst, qubits_, assign_params, group, cutoff_date) ret_schedule.append(inst, inplace=True) # 3) Get the parameter keys of the remaining instructions. At this point in # _assign all parameters in Call instructions that are supposed to be # assigned have been assigned. keys = set() if ret_schedule.name in set(key.schedule for key in self._parameter_map): for param in ret_schedule.parameters: keys.add(ParameterKey(param.name, qubits_, ret_schedule.name)) # 4) Build the parameter binding dictionary. binding_dict = {} assignment_table = {} for key, value in assign_params.items(): key_orig = key if key.qubits == (): key = ParameterKey(key.parameter, qubits_, key.schedule) if key in assign_params: # if (param, (1,), sched) and (param, (), sched) are both # in assign_params, skip the default value instead of # possibly triggering an error about conflicting # parameters. continue elif key.qubits != qubits_: continue param = self.calibration_parameter(*key) if param in ret_schedule.parameters: assign_okay = ( param not in binding_dict or key.schedule == ret_schedule.name and assignment_table[param].schedule != ret_schedule.name) if assign_okay: binding_dict[param] = value assignment_table[param] = key_orig elif (key.schedule == ret_schedule.name or assignment_table[param].schedule != ret_schedule.name ) and binding_dict[param] != value: raise CalibrationError( "Ambiguous assignment: assign_params keys " f"{key_orig} and {assignment_table[param]} " "resolve to the same parameter.") for key in keys: # Get the parameter object. Since we are dealing with a schedule the name of # the schedule is always defined. However, the parameter may be a default # parameter for all qubits, i.e. qubits may be an empty tuple. param = self.calibration_parameter(*key) if param not in binding_dict: binding_dict[param] = self.get_parameter_value( key.parameter, key.qubits, key.schedule, group=group, cutoff_date=cutoff_date, ) return ret_schedule.assign_parameters(binding_dict, inplace=False)
def save( self, file_type: str = "csv", folder: str = None, overwrite: bool = False, file_prefix: str = "", ): """Save the parameterized schedules and parameter value. The schedules and parameter values can be stored in csv files. This method creates three files: * parameter_config.csv: This file stores a table of parameters which indicates which parameters appear in which schedules. * parameter_values.csv: This file stores the values of the calibrated parameters. * schedules.csv: This file stores the parameterized schedules. Warning: Schedule blocks will only be saved in string format and can therefore not be reloaded and must instead be rebuilt. Args: file_type: The type of file to which to save. By default this is a csv. Other file types may be supported in the future. folder: The folder in which to save the calibrations. overwrite: If the files already exist then they will not be overwritten unless overwrite is set to True. file_prefix: A prefix to add to the name of the files such as a date tag or a UUID. Raises: CalibrationError: if the files exist and overwrite is not set to True. """ warnings.warn( "Schedules are only saved in text format. They cannot be re-loaded." ) cwd = os.getcwd() if folder: os.chdir(folder) parameter_config_file = file_prefix + "parameter_config.csv" parameter_value_file = file_prefix + "parameter_values.csv" schedule_file = file_prefix + "schedules.csv" if os.path.isfile(parameter_config_file) and not overwrite: raise CalibrationError( f"{parameter_config_file} already exists. Set overwrite to True." ) if os.path.isfile(parameter_value_file) and not overwrite: raise CalibrationError( f"{parameter_value_file} already exists. Set overwrite to True." ) if os.path.isfile(schedule_file) and not overwrite: raise CalibrationError( f"{schedule_file} already exists. Set overwrite to True.") # Write the parameter configuration. header_keys = [ "parameter.name", "parameter unique id", "schedule", "qubits" ] body = [] for parameter, keys in self.parameters.items(): for key in keys: body.append({ "parameter.name": parameter.name, "parameter unique id": self._hash_to_counter_map[parameter], "schedule": key.schedule, "qubits": key.qubits, }) if file_type == "csv": with open(parameter_config_file, "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(body) # Write the values of the parameters. values = self.parameters_table()["data"] if len(values) > 0: header_keys = values[0].keys() with open(parameter_value_file, "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(values) # Serialize the schedules. For now we just print them. schedules = [] header_keys = ["name", "qubits", "schedule"] for key, sched in self._schedules.items(): schedules.append({ "name": key.schedule, "qubits": key.qubits, "schedule": str(sched) }) with open(schedule_file, "w", newline="", encoding="utf-8") as output_file: dict_writer = csv.DictWriter(output_file, header_keys) dict_writer.writeheader() dict_writer.writerows(schedules) else: raise CalibrationError( f"Saving to .{file_type} is not yet supported.") os.chdir(cwd)