From d27fe41cec995072fa1db02a14d8437028d81a4e Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 8 Mar 2022 01:08:42 -0500 Subject: [PATCH 1/3] Fix QuantumVolumeAnalysis (#711) Fixes QuantumVolumeAnalysis to remove unnecessary dependence on the QuantumVolume experiment instance being stored in the ExperimentData, since this is not saved to the result DB and will be missing from loaded data. --- .../library/quantum_volume/qv_analysis.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/quantum_volume/qv_analysis.py b/qiskit_experiments/library/quantum_volume/qv_analysis.py index 0fd1357e58..258062a804 100644 --- a/qiskit_experiments/library/quantum_volume/qv_analysis.py +++ b/qiskit_experiments/library/quantum_volume/qv_analysis.py @@ -19,6 +19,7 @@ import numpy as np import uncertainties +from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.curve_analysis import plot_scatter, plot_errorbar from qiskit_experiments.framework import ( BaseAnalysis, @@ -55,14 +56,19 @@ def _default_options(cls) -> Options: return options def _run_analysis(self, experiment_data): - depth = experiment_data.experiment.num_qubits data = experiment_data.data() num_trials = len(data) + depth = None heavy_output_prob_exp = [] for data_trial in data: + trial_depth = data_trial["metadata"]["depth"] + if depth is None: + depth = trial_depth + elif trial_depth != depth: + raise AnalysisError("QuantumVolume circuits do not all have the same depth.") heavy_output = self._calc_ideal_heavy_output( - data_trial["metadata"]["ideal_probabilities"], data_trial["metadata"]["depth"] + data_trial["metadata"]["ideal_probabilities"], trial_depth ) heavy_output_prob_exp.append( self._calc_exp_heavy_output_probability(data_trial, heavy_output) From e1270318ef0cb686cc7f1977399086ed44cfe486 Mon Sep 17 00:00:00 2001 From: Caroline Tornow <79633854+catornow@users.noreply.github.com> Date: Mon, 14 Mar 2022 11:12:50 +0100 Subject: [PATCH 2/3] Restless Experiments (#689) * This PR adds a restless Mixin class that allows experiments to run in the restless operation mode where qubits are not reset between two shots. The mix-in makes it easy for users to enable restless measurements and adds functionality for developers to specify restless data processor. Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- .../framework/restless_mixin.py | 175 ++++++++++++++++++ .../characterization/fine_amplitude.py | 3 +- qiskit_experiments/test/mock_iq_backend.py | 141 ++++++++++++++ .../test_restless_experiment.py | 95 ++++++++++ 4 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 qiskit_experiments/framework/restless_mixin.py create mode 100644 test/data_processing/test_restless_experiment.py diff --git a/qiskit_experiments/framework/restless_mixin.py b/qiskit_experiments/framework/restless_mixin.py new file mode 100644 index 0000000000..82814cafa9 --- /dev/null +++ b/qiskit_experiments/framework/restless_mixin.py @@ -0,0 +1,175 @@ +# 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. + +"""Restless mixin class.""" + +from typing import Callable, Sequence, Optional + +from qiskit.providers import Backend +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.exceptions import DataProcessorError +from qiskit_experiments.data_processing import nodes +from qiskit_experiments.framework.base_analysis import BaseAnalysis + + +class RestlessMixin: + """A mixin to facilitate restless experiments. + + This class defines the following methods + + - :meth:`enable_restless` + - :meth:`_get_restless_processor` + - :meth:`_t1_check` + + A restless enabled experiment is an experiment that can be run in a restless + measurement setting. In restless measurements, the qubit is not reset after + each measurement. Instead, the outcome of the previous quantum non-demolition + measurement is the initial state for the current circuit. Restless measurements + therefore require special data processing which is provided by sub-classes of + the :code:`RestlessNode`. Restless experiments are a fast alternative for + several calibration and characterization tasks, for details see + https://arxiv.org/pdf/2202.06981.pdf. + This class makes it possible for users to enter a restless run-mode without having + to manually set all the required run options and the data processor. The required options + are ``rep_delay``, ``init_qubits``, ``memory``, and ``meas_level``. Furthermore, + subclasses can override the :meth:`_get_restless_processor` method if they require more + complex restless data processing such as two-qubit calibrations. In addition, this + class makes it easy to determine if restless measurements are supported for a given + experiments. + """ + + analysis: BaseAnalysis + set_run_options: Callable + backend: Backend + _physical_qubits: Sequence[int] + _num_qubits: int + + def enable_restless( + self, rep_delay: Optional[float] = None, override_processor_by_restless: bool = True + ): + """Enables a restless experiment by setting the restless run options and the + restless data processor. + + Args: + rep_delay: The repetition delay. This is the delay between a measurement + and the subsequent quantum circuit. Since the backends have + dynamic repetition rates, the repetition delay can be set to a small + value which is required for restless experiments. Typical values are + 1 us or less. + override_processor_by_restless: If False, a data processor that is specified in the + analysis options of the experiment is not overridden by the restless data + processor. The default is True. + + Raises: + DataProcessorError: if the attribute rep_delay_range is not defined for the backend. + DataProcessorError: if a data processor has already been set but + override_processor_by_restless is True. + DataProcessorError: if the experiment analysis does not have the data_processor + option. + DataProcessorError: if the rep_delay is equal to or greater than the + T1 time of one of the physical qubits in the experiment. + """ + try: + if not rep_delay: + rep_delay = self.backend.configuration().rep_delay_range[0] + except AttributeError as error: + raise DataProcessorError( + "The restless experiment can not be enabled because " + "the attribute rep_delay_range is not defined for this backend " + "and a minimum rep_delay can not be set." + ) from error + + # The excited state promotion readout analysis option is set to + # False because it is not compatible with a restless experiment. + if self._t1_check(rep_delay): + if not self.analysis.options.get("data_processor", None): + self.set_run_options( + rep_delay=rep_delay, + init_qubits=False, + memory=True, + meas_level=2, + use_measure_esp=False, + ) + if hasattr(self.analysis.options, "data_processor"): + self.analysis.set_options(data_processor=self._get_restless_processor()) + else: + raise DataProcessorError( + "The restless data processor can not be set since the experiment analysis" + "does not have the data_processor option." + ) + else: + if not override_processor_by_restless: + self.set_run_options( + rep_delay=rep_delay, + init_qubits=False, + memory=True, + meas_level=2, + use_measure_esp=False, + ) + else: + raise DataProcessorError( + "Cannot enable restless. Data processor has already been set and " + "override_processor_by_restless is True." + ) + else: + raise DataProcessorError( + f"The specified repetition delay {rep_delay} is equal to or greater " + f"than the T1 time of one of the physical qubits" + f"{self._physical_qubits} in the experiment. Consider choosing " + f"a smaller repetition delay for the restless experiment." + ) + + def _get_restless_processor(self) -> DataProcessor: + """Returns the restless experiments data processor. + + Notes: + Sub-classes can override this method if they need more complex data processing. + """ + outcome = self.analysis.options.get("outcome", "1" * self._num_qubits) + return DataProcessor( + "memory", + [ + nodes.RestlessToCounts(self._num_qubits), + nodes.Probability(outcome), + ], + ) + + def _t1_check(self, rep_delay: float) -> bool: + """Check that repetition delay < T1 of the physical qubits in the experiment. + + Args: + rep_delay: The repetition delay. This is the delay between a measurement + and the subsequent quantum circuit. + + Returns: + True if the repetition delay is smaller than the qubit T1 times. + + Raises: + DataProcessorError: if the T1 values are not defined for the qubits of + the used backend. + """ + + try: + t1_values = [ + self.backend.properties().qubit_property(physical_qubit)["T1"][0] + for physical_qubit in self._physical_qubits + ] + + if all(rep_delay / t1_value < 1.0 for t1_value in t1_values): + return True + except AttributeError as error: + raise DataProcessorError( + "The restless experiment can not be enabled since " + "T1 values are not defined for the qubits of the used backend." + ) from error + + return False diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index e474b3b18b..429ce100b9 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -20,10 +20,11 @@ from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework.restless_mixin import RestlessMixin from qiskit_experiments.library.characterization.analysis import FineAmplitudeAnalysis -class FineAmplitude(BaseExperiment): +class FineAmplitude(BaseExperiment, RestlessMixin): r"""Error amplifying fine amplitude calibration experiment. # section: overview diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index c1017ab6be..b9fc60eaff 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -18,12 +18,153 @@ from qiskit import QuantumCircuit from qiskit.result import Result + from qiskit.providers.aer import AerSimulator from qiskit.test.mock import FakeOpenPulse2Q from qiskit.qobj.utils import MeasLevel from qiskit_experiments.framework import Options from qiskit_experiments.test.utils import FakeJob +from qiskit_experiments.data_processing.exceptions import DataProcessorError + + +class MockRestlessBackend(FakeOpenPulse2Q): + """An abstract backend for testing that can mock restless data.""" + + def __init__(self, rng_seed: int = 0): + """ + Initialize the backend. + """ + self._rng = np.random.default_rng(rng_seed) + self._precomputed_probabilities = None + super().__init__() + + def _default_options(self): + """Default options of the test backend.""" + return Options( + shots=1024, + meas_level=MeasLevel.CLASSIFIED, + meas_return="single", + ) + + @staticmethod + def _get_state_strings(n_qubits: int) -> List[str]: + """Generate all state strings for the system.""" + format_str = "{0:0" + str(n_qubits) + "b}" + return list(format_str.format(state_num) for state_num in range(2**n_qubits)) + + @abstractmethod + def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]): + """Compute the probabilities of measuring 0 or 1 for each of the given + circuits based on the previous measurement shot. + + This methods computes the dictionary self._precomputed_probabilities where + the keys are a tuple consisting of the circuit index and the previous outcome, + e.g. "0" or "1" for a single qubit. The values are the corresponding probabilities. + + Args: + circuits: The circuits from which to compute the probabilities. + """ + + def run(self, run_input, **options): + """Run the restless backend.""" + + self.options.update_options(**options) + shots = self.options.get("shots") + meas_level = self.options.get("meas_level") + + result = { + "backend_name": f"{self.__class__.__name__}", + "backend_version": "0", + "qobj_id": 0, + "job_id": 0, + "success": True, + "results": [], + } + + self._compute_outcome_probabilities(run_input) + + if run_input[0].num_qubits != 2: + raise DataProcessorError(f"{self.__class__.__name__} is a two qubit mock device.") + + prev_outcome, state_strings = "00", self._get_state_strings(2) + + # Setup the list of dicts where each dict corresponds to a circuit. + sorted_memory = [{"memory": [], "metadata": circ.metadata} for circ in run_input] + + for _ in range(shots): + for circ_idx, _ in enumerate(run_input): + probs = self._precomputed_probabilities[(circ_idx, prev_outcome)] + # Generate the next shot dependent on the pre-computed probabilities. + outcome = self._rng.choice(state_strings, p=probs) + # Append the single shot to the memory of the corresponding circuit. + sorted_memory[circ_idx]["memory"].append(hex(int(outcome, 2))) + + prev_outcome = outcome + + for idx, circ in enumerate(run_input): + counts = {} + for key1, key2 in zip(["00", "01", "10", "11"], ["0x0", "0x1", "0x2", "0x3"]): + counts[key1] = sorted_memory[idx]["memory"].count(key2) + run_result = { + "shots": shots, + "success": True, + "header": {"metadata": circ.metadata}, + "meas_level": meas_level, + "data": { + "counts": counts, + "memory": sorted_memory[idx]["memory"], + }, + } + + result["results"].append(run_result) + + return FakeJob(self, Result.from_dict(result)) + + +class MockRestlessFineAmp(MockRestlessBackend): + """A mock backend for restless single-qubit fine amplitude experiments.""" + + def __init__( + self, angle_error: float, angle_per_gate: float, gate_name: str, rng_seed: int = 0 + ): + """Setup a mock backend to test the restless fine amplitude calibration. + + Args: + angle_error: The rotation error per gate. + angle_per_gate: The angle per gate. + gate_name: The name of the gate to find in the circuit. + rng_seed: The random bit generator seed. + """ + self.angle_error = angle_error + self._gate_name = gate_name + self._angle_per_gate = angle_per_gate + super().__init__(rng_seed=rng_seed) + + self.configuration().basis_gates.extend(["sx", "x"]) + + def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]): + """Compute the probabilities of being in the excited state or + ground state for all circuits.""" + + self._precomputed_probabilities = {} + + for idx, circuit in enumerate(circuits): + + n_ops = circuit.count_ops().get(self._gate_name, 0) + angle = n_ops * (self._angle_per_gate + self.angle_error) + + if self._gate_name != "sx": + angle += np.pi / 2 * circuit.count_ops().get("sx", 0) + + if self._gate_name != "x": + angle += np.pi * circuit.count_ops().get("x", 0) + + prob_1 = np.sin(angle / 2) ** 2 + prob_0 = 1 - prob_1 + + self._precomputed_probabilities[(idx, "00")] = [prob_0, prob_1, 0, 0] + self._precomputed_probabilities[(idx, "01")] = [prob_1, prob_0, 0, 0] class MockIQBackend(FakeOpenPulse2Q): diff --git a/test/data_processing/test_restless_experiment.py b/test/data_processing/test_restless_experiment.py new file mode 100644 index 0000000000..4165f87f01 --- /dev/null +++ b/test/data_processing/test_restless_experiment.py @@ -0,0 +1,95 @@ +# 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. + +"""Test restless fine amplitude characterization and calibration experiments.""" +from test.base import QiskitExperimentsTestCase + +import numpy as np +from ddt import ddt, data + +from qiskit_experiments.library import ( + FineXAmplitude, +) + +from qiskit_experiments.test.mock_iq_backend import MockRestlessFineAmp + +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.nodes import Probability +from qiskit_experiments.framework import Options + + +@ddt +class TestFineAmpEndToEndRestless(QiskitExperimentsTestCase): + """Test the fine amplitude experiment in a restless measurement setting.""" + + def test_enable_restless(self): + """Test the enable_restless method.""" + + error = -np.pi * 0.01 + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + amp_exp.enable_restless(rep_delay=2e-6) + + self.assertTrue( + amp_exp.run_options, + Options( + meas_level=2, rep_delay=2e-6, init_qubits=False, memory=True, use_measure_esp=False + ), + ) + + @data(-0.03, -0.01, 0.02, 0.04) + def test_end_to_end_restless(self, pi_ratio): + """Test the restless experiment end to end.""" + + error = -np.pi * pi_ratio + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + amp_exp.enable_restless(rep_delay=1e-6) + + expdata = amp_exp.run(backend) + self.assertExperimentDone(expdata) + result = expdata.analysis_results(1) + d_theta = result.value.n + + self.assertAlmostEqual(d_theta, error, delta=0.01) + self.assertEqual(result.quality, "good") + + # check that the fit amplitude is almost 1 as expected. + amp_fit = expdata.analysis_results(0).value[0] + self.assertAlmostEqual(amp_fit, 1.0, delta=0.02) + + @data(-0.02, 0.04) + def test_end_to_end_restless_standard_processor(self, pi_ratio): + """Test the restless experiment with a standard processor end to end.""" + + error = -np.pi * pi_ratio + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + # standard data processor. + standard_processor = DataProcessor("counts", [Probability("01")]) + amp_exp.analysis.set_options(data_processor=standard_processor) + # enable a restless measurement setting. + amp_exp.enable_restless(rep_delay=1e-6, override_processor_by_restless=False) + + expdata = amp_exp.run(backend) + self.assertExperimentDone(expdata) + result = expdata.analysis_results(1) + d_theta = result.value.n + + self.assertTrue(abs(d_theta - error) > 0.01) + + # check that the fit amplitude is much smaller than 1. + amp_fit = expdata.analysis_results(0).value[0] + self.assertTrue(amp_fit < 0.05) From aa716c19bbd5d4ea6c33d8475391e1000d667f04 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 14 Mar 2022 19:41:22 +0900 Subject: [PATCH 3/3] Replace `__fixed_parameters__` with analysis option (#734) Add fixed parameters to the curve analysis fit option and remove class attribute __fixed_parameters__ --- .../curve_analysis/curve_analysis.py | 88 ++++-- .../error_amplification_analysis.py | 22 +- .../library/calibration/fine_amplitude.py | 13 +- .../analysis/fine_amplitude_analysis.py | 2 - .../analysis/fine_drag_analysis.py | 12 + .../analysis/fine_frequency_analysis.py | 12 + .../analysis/fine_half_angle_analysis.py | 12 + .../characterization/fine_amplitude.py | 20 +- .../library/characterization/fine_drag.py | 18 +- .../characterization/fine_frequency.py | 15 +- .../library/characterization/half_angle.py | 21 +- ...sis-fixed-parameters-5915a29db1e2628b.yaml | 23 ++ .../experiments/test_fine_amplitude.py | 12 +- test/curve_analysis/test_curve_fit.py | 282 +++++++++++------- test/curve_analysis/test_standard_analysis.py | 10 +- 15 files changed, 376 insertions(+), 186 deletions(-) create mode 100644 releasenotes/notes/curve-analysis-fixed-parameters-5915a29db1e2628b.yaml diff --git a/qiskit_experiments/curve_analysis/curve_analysis.py b/qiskit_experiments/curve_analysis/curve_analysis.py index 53ccb1b116..2e15010783 100644 --- a/qiskit_experiments/curve_analysis/curve_analysis.py +++ b/qiskit_experiments/curve_analysis/curve_analysis.py @@ -47,6 +47,7 @@ ExperimentData, AnalysisResultData, Options, + AnalysisConfig, ) PARAMS_ENTRY_PREFIX = "@Parameters_" @@ -233,13 +234,24 @@ class AnalysisExample(CurveAnalysis): #: List[SeriesDef]: List of mapping representing a data series __series__ = list() - #: List[str]: Fixed parameter in fit function. Value should be set to the analysis options. - __fixed_parameters__ = list() - def __init__(self): """Initialize data fields that are privately accessed by methods.""" super().__init__() + if hasattr(self, "__fixed_parameters__"): + warnings.warn( + "The class attribute __fixed_parameters__ has been deprecated and will be removed. " + "Now this attribute is absorbed in analysis options as fixed_parameters. " + "This warning will be dropped in v0.4 along with " + "the support for the deprecated attribute.", + DeprecationWarning, + stacklevel=2, + ) + # pylint: disable=no-member + self._options.fixed_parameters = { + p: self.options.get(p, None) for p in self.__fixed_parameters__ + } + #: Dict[str, Any]: Experiment metadata self.__experiment_metadata = None @@ -271,21 +283,12 @@ def _fit_params(cls) -> List[str]: ) # remove the first function argument. this is usually x, i.e. not a fit parameter. - fit_params = list(list(fsigs)[0].parameters.keys())[1:] - - # remove fixed parameters - if cls.__fixed_parameters__ is not None: - for fixed_param in cls.__fixed_parameters__: - try: - fit_params.remove(fixed_param) - except ValueError as ex: - raise AnalysisError( - f"Defined fixed parameter {fixed_param} is not a fit function argument." - "Update series definition to ensure the parameter name is defined with " - f"fit functions. Currently available parameters are {fit_params}." - ) from ex - - return fit_params + return list(list(fsigs)[0].parameters.keys())[1:] + + @property + def parameters(self) -> List[str]: + """Return parameters of this curve analysis.""" + return [s for s in self._fit_params() if s not in self.options.fixed_parameters] @classmethod def _default_options(cls) -> Options: @@ -339,6 +342,9 @@ def _default_options(cls) -> Options: as extra information. curve_fitter_options (Dict[str, Any]) Options that are passed to the specified curve fitting function. + fixed_parameters (Dict[str, Any]): Fitting model parameters that are fixed + during the curve fitting. This should be provided with default value + keyed on one of the parameter names in the series definition. """ options = super()._default_options() @@ -360,11 +366,9 @@ def _default_options(cls) -> Options: options.style = PlotterStyle() options.extra = dict() options.curve_fitter_options = dict() - - # automatically populate initial guess and boundary - fit_params = cls._fit_params() - options.p0 = {par_name: None for par_name in fit_params} - options.bounds = {par_name: None for par_name in fit_params} + options.p0 = {} + options.bounds = {} + options.fixed_parameters = {} return options @@ -754,16 +758,15 @@ def _run_analysis( # # Update all fit functions in the series definitions if fixed parameter is defined. - # Fixed parameters should be provided by the analysis options. - if self.__fixed_parameters__: - assigned_params = {k: self.options.get(k, None) for k in self.__fixed_parameters__} + assigned_params = self.options.fixed_parameters + if assigned_params: # Check if all parameters are assigned. if any(v is None for v in assigned_params.values()): raise AnalysisError( f"Unassigned fixed-value parameters for the fit " f"function {self.__class__.__name__}." - f"All values of fixed-parameters, i.e. {self.__fixed_parameters__}, " + f"All values of fixed-parameters, i.e. {assigned_params}, " "must be provided by the analysis options to run this analysis." ) @@ -815,7 +818,7 @@ def _run_analysis( # Generate algorithmic initial guesses and boundaries default_fit_opt = FitOptions( - parameters=self._fit_params(), + parameters=self.parameters, default_p0=self.options.p0, default_bounds=self.options.bounds, **self.options.curve_fitter_options, @@ -964,6 +967,35 @@ def _run_analysis( return analysis_results, figures + @classmethod + def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "CurveAnalysis": + # For backward compatibility. This will be removed in v0.4. + + instance = super().from_config(config) + + # When fixed param value is hard-coded as options. This is deprecated data structure. + loaded_opts = instance.options.__dict__ + + # pylint: disable=no-member + deprecated_fixed_params = { + p: loaded_opts[p] for p in instance.parameters if p in loaded_opts + } + if any(deprecated_fixed_params): + warnings.warn( + "Fixed parameter value should be defined in options.fixed_parameters as " + "a dictionary values, rather than a standalone analysis option. " + "Please re-save this experiment to be loaded after deprecation period. " + "This warning will be dropped in v0.4 along with " + "the support for the deprecated fixed parameter options.", + DeprecationWarning, + stacklevel=2, + ) + new_fixed_params = instance.options.fixed_parameters + new_fixed_params.update(deprecated_fixed_params) + instance.set_options(fixed_parameters=new_fixed_params) + + return instance + def is_error_not_significant( val: Union[float, uncertainties.UFloat], diff --git a/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py b/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py index 498b4d30ab..a863fe3998 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py @@ -105,12 +105,6 @@ def _default_options(cls): descriptions of analysis options. Analysis Options: - angle_per_gate (float): The ideal angle per repeated gate. - The user must set this option as it defaults to None. - phase_offset (float): A phase offset for the analysis. This phase offset will be - :math:`\pi/2` if the square-root of X gate is added before the repeated gates. - This is decided for the user in :meth:`set_schedule` depending on whether the - sx gate is included in the experiment. max_good_angle_error (float): The maximum angle error for which the fit is considered as good. Defaults to :math:`\pi/2`. """ @@ -118,11 +112,8 @@ def _default_options(cls): default_options.result_parameters = ["d_theta"] default_options.xlabel = "Number of gates (n)" default_options.ylabel = "Population" - default_options.angle_per_gate = None - default_options.phase_offset = 0.0 - default_options.max_good_angle_error = np.pi / 2 - default_options.amp = 1.0 default_options.ylim = [0, 1.0] + default_options.max_good_angle_error = np.pi / 2 return default_options @@ -140,6 +131,8 @@ def _generate_fit_guesses( Raises: CalibrationError: When ``angle_per_gate`` is missing. """ + fixed_params = self.options.fixed_parameters + 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) @@ -152,16 +145,19 @@ def _generate_fit_guesses( if "amp" in user_opt.p0: user_opt.p0.set_if_empty(amp=max_y - min_y) user_opt.bounds.set_if_empty(amp=(0, 2 * max_abs_y)) + amp = user_opt.p0["amp"] + else: + # Fixed parameter + amp = fixed_params.get("amp", 1.0) # Base the initial guess on the intended angle_per_gate and phase offset. - apg = self.options.angle_per_gate - phi = self.options.phase_offset + apg = user_opt.p0.get("angle_per_gate", fixed_params.get("angle_per_gate", 0.0)) + phi = user_opt.p0.get("phase_offset", fixed_params.get("phase_offset", 0.0)) # Prepare logical guess for specific condition (often satisfied) d_theta_guesses = [] offsets = apg * curve_data.x + phi - amp = user_opt.p0.get("amp", self.options.amp) for i in range(curve_data.x.size): xi = curve_data.x[i] yi = curve_data.y[i] diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index 170fa577dc..939e595b18 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -171,9 +171,10 @@ def __init__( auto_update=auto_update, ) self.analysis.set_options( - angle_per_gate=np.pi, - phase_offset=np.pi / 2, - amp=1, + fixed_parameters={ + "angle_per_gate": np.pi, + "phase_offset": np.pi / 2, + } ) @classmethod @@ -222,8 +223,10 @@ def __init__( auto_update=auto_update, ) self.analysis.set_options( - angle_per_gate=np.pi / 2, - phase_offset=np.pi, + fixed_parameters={ + "angle_per_gate": np.pi / 2, + "phase_offset": np.pi, + } ) @classmethod diff --git a/qiskit_experiments/library/characterization/analysis/fine_amplitude_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_amplitude_analysis.py index c256fdf0ee..d64dfd027e 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_amplitude_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_amplitude_analysis.py @@ -58,5 +58,3 @@ class FineAmplitudeAnalysis(ErrorAmplificationAnalysis): filter_kwargs={"series": 1}, ), ] - - __fixed_parameters__ = ["angle_per_gate", "phase_offset"] diff --git a/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py index 4b23620959..5687ac47dd 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py @@ -12,6 +12,8 @@ """Fine DRAG calibration analysis.""" +import warnings + import numpy as np from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis from qiskit_experiments.framework import Options @@ -32,6 +34,16 @@ class FineDragAnalysis(ErrorAmplificationAnalysis): __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] + def __init__(self): + super().__init__() + + warnings.warn( + f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis " + "instance with the analysis options involving the fixed_parameters.", + DeprecationWarning, + stacklevel=2, + ) + @classmethod def _default_options(cls) -> Options: """Default analysis options.""" diff --git a/qiskit_experiments/library/characterization/analysis/fine_frequency_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_frequency_analysis.py index f816d882ae..1bb90a57b2 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_frequency_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_frequency_analysis.py @@ -12,6 +12,8 @@ """Fine frequency experiment analysis.""" +import warnings + import numpy as np from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis @@ -33,6 +35,16 @@ class FineFrequencyAnalysis(ErrorAmplificationAnalysis): __fixed_parameters__ = ["angle_per_gate", "phase_offset"] + def __init__(self): + super().__init__() + + warnings.warn( + f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis " + "instance with the analysis options involving the fixed_parameters.", + DeprecationWarning, + stacklevel=2, + ) + @classmethod def _default_options(cls) -> Options: """Default analysis options.""" diff --git a/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py index 2f57511731..94efc1237c 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py @@ -12,6 +12,8 @@ """Fine half angle calibration analysis.""" +import warnings + import numpy as np from qiskit_experiments.framework import Options from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis, ParameterRepr @@ -31,6 +33,16 @@ class FineHalfAngleAnalysis(ErrorAmplificationAnalysis): __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] + def __init__(self): + super().__init__() + + warnings.warn( + f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis " + "instance with the analysis options involving the fixed_parameters.", + DeprecationWarning, + stacklevel=2, + ) + @classmethod def _default_options(cls) -> Options: r"""Default analysis options. diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index 429ce100b9..d3b7902217 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -254,9 +254,10 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None): super().__init__([qubit], XGate(), backend=backend) # Set default analysis options self.analysis.set_options( - angle_per_gate=np.pi, - phase_offset=np.pi / 2, - amp=1, + fixed_parameters={ + "angle_per_gate": np.pi, + "phase_offset": np.pi / 2, + } ) @classmethod @@ -291,8 +292,10 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None): super().__init__([qubit], SXGate(), backend=backend) # Set default analysis options self.analysis.set_options( - angle_per_gate=np.pi / 2, - phase_offset=np.pi, + fixed_parameters={ + "angle_per_gate": np.pi / 2, + "phase_offset": np.pi, + } ) @classmethod @@ -354,9 +357,10 @@ def __init__(self, qubits: Sequence[int], backend: Optional[Backend] = None): super().__init__(qubits, gate, backend=backend, measurement_qubits=[qubits[1]]) # Set default analysis options self.analysis.set_options( - angle_per_gate=np.pi / 2, - phase_offset=np.pi, - amp=1, + fixed_parameters={ + "angle_per_gate": np.pi / 2, + "phase_offset": np.pi, + }, outcome="1", ) diff --git a/qiskit_experiments/library/characterization/fine_drag.py b/qiskit_experiments/library/characterization/fine_drag.py index fa95a4ca41..485a33d4ea 100644 --- a/qiskit_experiments/library/characterization/fine_drag.py +++ b/qiskit_experiments/library/characterization/fine_drag.py @@ -20,9 +20,7 @@ from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis import ( - FineDragAnalysis, -) +from qiskit_experiments.curve_analysis.standard_analysis import ErrorAmplificationAnalysis class FineDrag(BaseExperiment): @@ -126,7 +124,7 @@ class FineDrag(BaseExperiment): This is the correction formula in the FineDRAG Updater. # section: analysis_ref - :py:class:`FineDragAnalysis` + :py:class:`~qiskit_experiments.curve_analysis.ErrorAmplificationAnalysis` # section: see_also qiskit_experiments.library.calibration.drag.DragCal @@ -161,7 +159,17 @@ def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None): gate: The gate that will be repeated. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], analysis=FineDragAnalysis(), backend=backend) + analysis = ErrorAmplificationAnalysis() + analysis.set_options( + normalization=True, + fixed_parameters={ + "angle_per_gate": 0.0, + "phase_offset": np.pi / 2, + "amp": 1.0, + }, + ) + + super().__init__([qubit], analysis=analysis, backend=backend) self.set_experiment_options(gate=gate) @staticmethod diff --git a/qiskit_experiments/library/characterization/fine_frequency.py b/qiskit_experiments/library/characterization/fine_frequency.py index fa9b63b36b..739e3edf48 100644 --- a/qiskit_experiments/library/characterization/fine_frequency.py +++ b/qiskit_experiments/library/characterization/fine_frequency.py @@ -19,7 +19,7 @@ from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis import FineFrequencyAnalysis +from qiskit_experiments.curve_analysis.standard_analysis import ErrorAmplificationAnalysis class FineFrequency(BaseExperiment): @@ -47,7 +47,7 @@ class FineFrequency(BaseExperiment): meas: 1/══════════════════════════════════════════════╩═ 0 # section: analysis_ref - :py:class:`FineFrequencyAnalysis` + :py:class:`~qiskit_experiments.curve_analysis.ErrorAmplificationAnalysis` """ def __init__( @@ -66,7 +66,16 @@ def __init__( repetitions: The number of repetitions, if not given then the default value from the experiment default options will be used. """ - super().__init__([qubit], analysis=FineFrequencyAnalysis(), backend=backend) + analysis = ErrorAmplificationAnalysis() + analysis.set_options( + normalization=True, + fixed_parameters={ + "angle_per_gate": np.pi / 2, + "phase_offset": 0.0, + }, + ) + + super().__init__([qubit], analysis=analysis, backend=backend) if repetitions is not None: self.set_experiment_options(repetitions=repetitions) diff --git a/qiskit_experiments/library/characterization/half_angle.py b/qiskit_experiments/library/characterization/half_angle.py index c5c0c5b665..6b720cf4d8 100644 --- a/qiskit_experiments/library/characterization/half_angle.py +++ b/qiskit_experiments/library/characterization/half_angle.py @@ -19,7 +19,8 @@ from qiskit.providers import Backend from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis import FineHalfAngleAnalysis +from qiskit_experiments.curve_analysis.standard_analysis import ErrorAmplificationAnalysis +from qiskit_experiments.curve_analysis import ParameterRepr class HalfAngle(BaseExperiment): @@ -84,7 +85,23 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None): qubit: The qubit on which to run the fine amplitude calibration experiment. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], analysis=FineHalfAngleAnalysis(), backend=backend) + analysis = ErrorAmplificationAnalysis() + + default_bounds = analysis.options.bounds + default_bounds.update({"d_theta": (-np.pi / 2, np.pi / 2)}) + + analysis.set_options( + fixed_parameters={ + "angle_per_gate": np.pi, + "phase_offset": -np.pi / 2, + "amp": 1.0, + }, + result_parameters=[ParameterRepr("d_theta", "d_hac", "rad")], + normalization=True, + bounds=default_bounds, + ) + + super().__init__([qubit], analysis=analysis, backend=backend) @staticmethod def _pre_circuit() -> QuantumCircuit: diff --git a/releasenotes/notes/curve-analysis-fixed-parameters-5915a29db1e2628b.yaml b/releasenotes/notes/curve-analysis-fixed-parameters-5915a29db1e2628b.yaml new file mode 100644 index 0000000000..f0701c689d --- /dev/null +++ b/releasenotes/notes/curve-analysis-fixed-parameters-5915a29db1e2628b.yaml @@ -0,0 +1,23 @@ +--- +upgrade: + - | + New default :class:`CurveAnalysis` analysis option ``fixed_parameters`` + has been added. We can directly exclude parameters from the fit model + of the particular analysis instance, rather than defining a new class to define + the class attribute :attr:`CurveAnalysis.__fixed_parameters__`. +deprecations: + - | + Class attribute :attr:`CurveAnalysis.__fixed_parameters__` has been deprecated + and support for the instantiation of the class with this attribute will be dropped soon. + In addition, the fixed parameter value defined as a standalone analysis option + has been deprecated. Please set `fixed_parameters` option instead. + This is a python dictionary of fixed parameter values keyed on the fit parameter names. + - | + Analysis class :class:`FineDragAnalysis` has been deprecated. Now you can directly + set fixed parameters to the :class:`ErrorAmplificationAnalysis` instance as an analysis option. + - | + Analysis class :class:`FineFrequencyAnalysis` has been deprecated. Now you can directly + set fixed parameters to the :class:`ErrorAmplificationAnalysis` instance as an analysis option. + - | + Analysis class :class:`FineHalfAngleAnalysis` has been deprecated. Now you can directly + set fixed parameters to the :class:`ErrorAmplificationAnalysis` instance as an analysis option. diff --git a/test/calibration/experiments/test_fine_amplitude.py b/test/calibration/experiments/test_fine_amplitude.py index 5c25c1c93d..9d62667bde 100644 --- a/test/calibration/experiments/test_fine_amplitude.py +++ b/test/calibration/experiments/test_fine_amplitude.py @@ -158,8 +158,10 @@ def test_fine_x_amp(self): exp = FineXAmplitude(0) self.assertTrue(exp.experiment_options.add_cal_circuits) - self.assertEqual(exp.analysis.options.angle_per_gate, np.pi) - self.assertEqual(exp.analysis.options.phase_offset, np.pi / 2) + self.assertDictEqual( + exp.analysis.options.fixed_parameters, + {"angle_per_gate": np.pi, "phase_offset": np.pi / 2}, + ) self.assertEqual(exp.experiment_options.gate, XGate()) def test_fine_sx_amp(self): @@ -171,8 +173,10 @@ def test_fine_sx_amp(self): expected = [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 21, 23, 25] self.assertEqual(exp.experiment_options.repetitions, expected) - self.assertEqual(exp.analysis.options.angle_per_gate, np.pi / 2) - self.assertEqual(exp.analysis.options.phase_offset, np.pi) + self.assertDictEqual( + exp.analysis.options.fixed_parameters, + {"angle_per_gate": np.pi / 2, "phase_offset": np.pi}, + ) self.assertEqual(exp.experiment_options.gate, SXGate()) @data((2, 3), (3, 1), (0, 1)) diff --git a/test/curve_analysis/test_curve_fit.py b/test/curve_analysis/test_curve_fit.py index 2dddda850e..ef5051b206 100644 --- a/test/curve_analysis/test_curve_fit.py +++ b/test/curve_analysis/test_curve_fit.py @@ -64,7 +64,14 @@ class TestAnalysis(CurveAnalysis): """A mock analysis class to test.""" __series__ = series - __fixed_parameters__ = fixed_params or list() + + @classmethod + def _default_options(cls): + opts = super()._default_options() + if fixed_params: + opts.fixed_parameters = {p: None for p in fixed_params} + + return opts return TestAnalysis() @@ -117,24 +124,24 @@ def setUp(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.exponential_decay( - x, amp=p0, lamb=p1, baseline=p4 + fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.exponential_decay( + x, amp=par0, lamb=par1, baseline=par4 ), filter_kwargs={"type": 1, "valid": True}, model_description=r"p_0 * \exp(p_1 x) + p4", ), SeriesDef( name="curve2", - fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.exponential_decay( - x, amp=p0, lamb=p2, baseline=p4 + fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.exponential_decay( + x, amp=par0, lamb=par2, baseline=par4 ), filter_kwargs={"type": 2, "valid": True}, model_description=r"p_0 * \exp(p_2 x) + p4", ), SeriesDef( name="curve3", - fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.exponential_decay( - x, amp=p0, lamb=p3, baseline=p4 + fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.exponential_decay( + x, amp=par0, lamb=par3, baseline=par4 ), filter_kwargs={"type": 3, "valid": True}, model_description=r"p_0 * \exp(p_3 x) + p4", @@ -145,43 +152,27 @@ def setUp(self): def test_parsed_fit_params(self): """Test parsed fit params.""" - self.assertSetEqual(set(self.analysis._fit_params()), {"p0", "p1", "p2", "p3", "p4"}) - - def test_parsed_init_guess(self): - """Test parsed initial guess and boundaries.""" - default_p0 = self.analysis._default_options().p0 - default_bounds = self.analysis._default_options().bounds - ref = {"p0": None, "p1": None, "p2": None, "p3": None, "p4": None} - self.assertDictEqual(default_p0, ref) - self.assertDictEqual(default_bounds, ref) + self.assertSetEqual( + set(self.analysis._fit_params()), {"par0", "par1", "par2", "par3", "par4"} + ) def test_cannot_create_invalid_series_fit(self): """Test we cannot create invalid analysis instance.""" invalid_series = [ SeriesDef( name="fit1", - fit_func=lambda x, p0: fit_function.exponential_decay(x, amp=p0), + fit_func=lambda x, par0: fit_function.exponential_decay(x, amp=par0), ), SeriesDef( name="fit2", - fit_func=lambda x, p1: fit_function.exponential_decay(x, amp=p1), + fit_func=lambda x, par1: fit_function.exponential_decay(x, amp=par1), ), ] - with self.assertRaises(AnalysisError): - create_new_analysis(series=invalid_series) # fit1 has param p0 while fit2 has p1 - def test_cannot_create_invalid_fixed_parameter(self): - """Test we cannot create invalid analysis instance with wrong fixed value name.""" - valid_series = [ - SeriesDef( - fit_func=lambda x, p0, p1: fit_function.exponential_decay(x, amp=p0, lamb=p1), - ), - ] + instance = create_new_analysis(series=invalid_series) with self.assertRaises(AnalysisError): - create_new_analysis( - series=valid_series, - fixed_params=["not_existing_parameter"], # this parameter is not defined - ) + # pylint: disable=pointless-statement + instance.parameters # fit1 has param par0 while fit2 has par1 def test_data_extraction(self): """Test data extraction method.""" @@ -295,8 +286,8 @@ def test_run_single_curve_analysis(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3: fit_function.exponential_decay( - x, amp=p0, lamb=p1, x0=p2, baseline=p3 + fit_func=lambda x, par0, par1, par2, par3: fit_function.exponential_decay( + x, amp=par0, lamb=par1, x0=par2, baseline=par3 ), model_description=r"p_0 \exp(p_1 x + p_2) + p_3", ) @@ -313,8 +304,8 @@ def test_run_single_curve_analysis(self): param_dict={"amp": ref_p0, "lamb": ref_p1, "x0": ref_p2, "baseline": ref_p3}, ) analysis.set_options( - p0={"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3}, - result_parameters=[ParameterRepr("p1", "parameter_name", "unit")], + p0={"par0": ref_p0, "par1": ref_p1, "par2": ref_p2, "par3": ref_p3}, + result_parameters=[ParameterRepr("par1", "parameter_name", "unit")], ) results, _ = analysis._run_analysis(test_data) @@ -325,7 +316,7 @@ def test_run_single_curve_analysis(self): # check result data np.testing.assert_array_almost_equal(result.value, ref_popt, decimal=self.err_decimal) self.assertEqual(result.extra["dof"], 46) - self.assertListEqual(result.extra["popt_keys"], ["p0", "p1", "p2", "p3"]) + self.assertListEqual(result.extra["popt_keys"], ["par0", "par1", "par2", "par3"]) self.assertDictEqual(result.extra["fit_models"], {"curve1": r"p_0 \exp(p_1 x + p_2) + p_3"}) # special entry formatted for database @@ -340,8 +331,8 @@ def test_run_single_curve_fail(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3: fit_function.exponential_decay( - x, amp=p0, lamb=p1, x0=p2, baseline=p3 + fit_func=lambda x, par0, par1, par2, par3: fit_function.exponential_decay( + x, amp=par0, lamb=par1, x0=par2, baseline=par3 ), ) ], @@ -357,8 +348,8 @@ def test_run_single_curve_fail(self): param_dict={"amp": ref_p0, "lamb": ref_p1, "x0": ref_p2, "baseline": ref_p3}, ) analysis.set_options( - p0={"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3}, - bounds={"p0": [-10, 0], "p1": [-10, 0], "p2": [-10, 0], "p3": [-10, 0]}, + p0={"par0": ref_p0, "par1": ref_p1, "par2": ref_p2, "par3": ref_p3}, + bounds={"par0": [-10, 0], "par1": [-10, 0], "par2": [-10, 0], "par3": [-10, 0]}, return_data_points=True, ) @@ -375,15 +366,15 @@ def test_run_two_curves_with_same_fitfunc(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.exponential_decay( - x, amp=p0, lamb=p1, x0=p3, baseline=p4 + fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.exponential_decay( + x, amp=par0, lamb=par1, x0=par3, baseline=par4 ), filter_kwargs={"exp": 0}, ), SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.exponential_decay( - x, amp=p0, lamb=p2, x0=p3, baseline=p4 + fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.exponential_decay( + x, amp=par0, lamb=par2, x0=par3, baseline=par4 ), filter_kwargs={"exp": 1}, ), @@ -414,7 +405,7 @@ def test_run_two_curves_with_same_fitfunc(self): test_data0.add_data(datum) analysis.set_options( - p0={"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3, "p4": ref_p4} + p0={"par0": ref_p0, "par1": ref_p1, "par2": ref_p2, "par3": ref_p3, "par4": ref_p4} ) results, _ = analysis._run_analysis(test_data0) result = results[0] @@ -430,15 +421,15 @@ def test_run_two_curves_with_two_fitfuncs(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, p2, p3: fit_function.cos( - x, amp=p0, freq=p1, phase=p2, baseline=p3 + fit_func=lambda x, par0, par1, par2, par3: fit_function.cos( + x, amp=par0, freq=par1, phase=par2, baseline=par3 ), filter_kwargs={"exp": 0}, ), SeriesDef( name="curve2", - fit_func=lambda x, p0, p1, p2, p3: fit_function.sin( - x, amp=p0, freq=p1, phase=p2, baseline=p3 + fit_func=lambda x, par0, par1, par2, par3: fit_function.sin( + x, amp=par0, freq=par1, phase=par2, baseline=par3 ), filter_kwargs={"exp": 1}, ), @@ -467,7 +458,7 @@ def test_run_two_curves_with_two_fitfuncs(self): for datum in test_data1.data(): test_data0.add_data(datum) - analysis.set_options(p0={"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3}) + analysis.set_options(p0={"par0": ref_p0, "par1": ref_p1, "par2": ref_p2, "par3": ref_p3}) results, _ = analysis._run_analysis(test_data0) result = results[0] @@ -482,12 +473,12 @@ def test_run_fixed_parameters(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, fixed_p2, p3: fit_function.cos( - x, amp=p0, freq=p1, phase=fixed_p2, baseline=p3 + fit_func=lambda x, par0, par1, fixed_par2, par3: fit_function.cos( + x, amp=par0, freq=par1, phase=fixed_par2, baseline=par3 ), ), ], - fixed_params=["fixed_p2"], + fixed_params=["fixed_par2"], ) ref_p0 = 0.1 @@ -502,8 +493,8 @@ def test_run_fixed_parameters(self): ) analysis.set_options( - p0={"p0": ref_p0, "p1": ref_p1, "p3": ref_p3}, - fixed_p2=ref_p2, + p0={"par0": ref_p0, "par1": ref_p1, "par3": ref_p3}, + fixed_parameters={"fixed_par2": ref_p2}, ) results, _ = analysis._run_analysis(test_data) @@ -520,8 +511,8 @@ def test_fixed_param_is_missing(self): series=[ SeriesDef( name="curve1", - fit_func=lambda x, p0, p1, fixed_p2, p3: fit_function.cos( - x, amp=p0, freq=p1, phase=fixed_p2, baseline=p3 + fit_func=lambda x, par0, par1, fixed_par2, par3: fit_function.cos( + x, amp=par0, freq=par1, phase=fixed_par2, baseline=par3 ), ), ], @@ -539,7 +530,7 @@ def test_fixed_param_is_missing(self): param_dict={"amp": ref_p0, "freq": ref_p1, "phase": ref_p2, "baseline": ref_p3}, ) # do not define fixed_p2 here - analysis.set_options(p0={"p0": ref_p0, "p1": ref_p1, "p3": ref_p3}) + analysis.set_options(p0={"par0": ref_p0, "par1": ref_p1, "par3": ref_p3}) with self.assertRaises(AnalysisError): analysis._run_analysis(test_data) @@ -549,12 +540,16 @@ class TestFitOptions(QiskitExperimentsTestCase): def test_empty(self): """Test if default value is automatically filled.""" - opt = FitOptions(["p0", "p1", "p2"]) + opt = FitOptions(["par0", "par1", "par2"]) # bounds should be default to inf tuple. otherwise crashes the scipy fitter. ref_opts = { - "p0": {"p0": None, "p1": None, "p2": None}, - "bounds": {"p0": (-np.inf, np.inf), "p1": (-np.inf, np.inf), "p2": (-np.inf, np.inf)}, + "p0": {"par0": None, "par1": None, "par2": None}, + "bounds": { + "par0": (-np.inf, np.inf), + "par1": (-np.inf, np.inf), + "par2": (-np.inf, np.inf), + }, } self.assertDictEqual(opt.options, ref_opts) @@ -562,14 +557,14 @@ def test_empty(self): def test_create_option_with_dict(self): """Create option and fill with dictionary.""" opt = FitOptions( - ["p0", "p1", "p2"], - default_p0={"p0": 0, "p1": 1, "p2": 2}, - default_bounds={"p0": (0, 1), "p1": (1, 2), "p2": (2, 3)}, + ["par0", "par1", "par2"], + default_p0={"par0": 0, "par1": 1, "par2": 2}, + default_bounds={"par0": (0, 1), "par1": (1, 2), "par2": (2, 3)}, ) ref_opts = { - "p0": {"p0": 0.0, "p1": 1.0, "p2": 2.0}, - "bounds": {"p0": (0.0, 1.0), "p1": (1.0, 2.0), "p2": (2.0, 3.0)}, + "p0": {"par0": 0.0, "par1": 1.0, "par2": 2.0}, + "bounds": {"par0": (0.0, 1.0), "par1": (1.0, 2.0), "par2": (2.0, 3.0)}, } self.assertDictEqual(opt.options, ref_opts) @@ -577,73 +572,89 @@ def test_create_option_with_dict(self): def test_create_option_with_array(self): """Create option and fill with array.""" opt = FitOptions( - ["p0", "p1", "p2"], + ["par0", "par1", "par2"], default_p0=[0, 1, 2], default_bounds=[(0, 1), (1, 2), (2, 3)], ) ref_opts = { - "p0": {"p0": 0.0, "p1": 1.0, "p2": 2.0}, - "bounds": {"p0": (0.0, 1.0), "p1": (1.0, 2.0), "p2": (2.0, 3.0)}, + "p0": {"par0": 0.0, "par1": 1.0, "par2": 2.0}, + "bounds": {"par0": (0.0, 1.0), "par1": (1.0, 2.0), "par2": (2.0, 3.0)}, } self.assertDictEqual(opt.options, ref_opts) def test_override_partial_dict(self): """Create option and override value with partial dictionary.""" - opt = FitOptions(["p0", "p1", "p2"]) - opt.p0.set_if_empty(p1=3) + opt = FitOptions(["par0", "par1", "par2"]) + opt.p0.set_if_empty(par1=3) ref_opts = { - "p0": {"p0": None, "p1": 3.0, "p2": None}, - "bounds": {"p0": (-np.inf, np.inf), "p1": (-np.inf, np.inf), "p2": (-np.inf, np.inf)}, + "p0": {"par0": None, "par1": 3.0, "par2": None}, + "bounds": { + "par0": (-np.inf, np.inf), + "par1": (-np.inf, np.inf), + "par2": (-np.inf, np.inf), + }, } self.assertDictEqual(opt.options, ref_opts) def test_cannot_override_assigned_value(self): """Test cannot override already assigned value.""" - opt = FitOptions(["p0", "p1", "p2"]) - opt.p0.set_if_empty(p1=3) - opt.p0.set_if_empty(p1=5) + opt = FitOptions(["par0", "par1", "par2"]) + opt.p0.set_if_empty(par1=3) + opt.p0.set_if_empty(par1=5) ref_opts = { - "p0": {"p0": None, "p1": 3.0, "p2": None}, - "bounds": {"p0": (-np.inf, np.inf), "p1": (-np.inf, np.inf), "p2": (-np.inf, np.inf)}, + "p0": {"par0": None, "par1": 3.0, "par2": None}, + "bounds": { + "par0": (-np.inf, np.inf), + "par1": (-np.inf, np.inf), + "par2": (-np.inf, np.inf), + }, } self.assertDictEqual(opt.options, ref_opts) def test_can_override_assigned_value_with_dict_access(self): """Test override already assigned value with direct dict access.""" - opt = FitOptions(["p0", "p1", "p2"]) - opt.p0["p1"] = 3 - opt.p0["p1"] = 5 + opt = FitOptions(["par0", "par1", "par2"]) + opt.p0["par1"] = 3 + opt.p0["par1"] = 5 ref_opts = { - "p0": {"p0": None, "p1": 5.0, "p2": None}, - "bounds": {"p0": (-np.inf, np.inf), "p1": (-np.inf, np.inf), "p2": (-np.inf, np.inf)}, + "p0": {"par0": None, "par1": 5.0, "par2": None}, + "bounds": { + "par0": (-np.inf, np.inf), + "par1": (-np.inf, np.inf), + "par2": (-np.inf, np.inf), + }, } self.assertDictEqual(opt.options, ref_opts) def test_cannot_override_user_option(self): """Test cannot override already assigned value.""" - opt = FitOptions(["p0", "p1", "p2"], default_p0={"p1": 3}) - opt.p0.set_if_empty(p1=5) + opt = FitOptions(["par0", "par1", "par2"], default_p0={"par1": 3}) + opt.p0.set_if_empty(par1=5) ref_opts = { - "p0": {"p0": None, "p1": 3, "p2": None}, - "bounds": {"p0": (-np.inf, np.inf), "p1": (-np.inf, np.inf), "p2": (-np.inf, np.inf)}, + "p0": {"par0": None, "par1": 3, "par2": None}, + "bounds": { + "par0": (-np.inf, np.inf), + "par1": (-np.inf, np.inf), + "par2": (-np.inf, np.inf), + }, } self.assertDictEqual(opt.options, ref_opts) def test_set_operation(self): """Test if set works and duplicated entry is removed.""" - opt1 = FitOptions(["p0", "p1"], default_p0=[0, 1]) - opt2 = FitOptions(["p0", "p1"], default_p0=[0, 1]) - opt3 = FitOptions(["p0", "p1"], default_p0=[0, 2]) + opt1 = FitOptions(["par0", "par1"], default_p0=[0, 1]) + opt2 = FitOptions(["par0", "par1"], default_p0=[0, 1]) + opt3 = FitOptions(["par0", "par1"], default_p0=[0, 2]) opts = set() opts.add(opt1) @@ -656,39 +667,39 @@ def test_detect_invalid_p0(self): """Test if invalid p0 raises Error.""" with self.assertRaises(AnalysisError): # less element - FitOptions(["p0", "p1", "p2"], default_p0=[0, 1]) + FitOptions(["par0", "par1", "par2"], default_p0=[0, 1]) def test_detect_invalid_bounds(self): """Test if invalid bounds raises Error.""" with self.assertRaises(AnalysisError): # less element - FitOptions(["p0", "p1", "p2"], default_bounds=[(0, 1), (1, 2)]) + FitOptions(["par0", "par1", "par2"], default_bounds=[(0, 1), (1, 2)]) with self.assertRaises(AnalysisError): # not min-max tuple - FitOptions(["p0", "p1", "p2"], default_bounds=[0, 1, 2]) + FitOptions(["par0", "par1", "par2"], default_bounds=[0, 1, 2]) with self.assertRaises(AnalysisError): # max-min tuple - FitOptions(["p0", "p1", "p2"], default_bounds=[(1, 0), (2, 1), (3, 2)]) + FitOptions(["par0", "par1", "par2"], default_bounds=[(1, 0), (2, 1), (3, 2)]) def test_detect_invalid_key(self): """Test if invalid key raises Error.""" - opt = FitOptions(["p0", "p1", "p2"]) + opt = FitOptions(["par0", "par1", "par2"]) with self.assertRaises(AnalysisError): - opt.p0.set_if_empty(p3=3) + opt.p0.set_if_empty(par3=3) def test_set_extra_options(self): """Add extra fitter options.""" opt = FitOptions( - ["p0", "p1", "p2"], default_p0=[0, 1, 2], default_bounds=[(0, 1), (1, 2), (2, 3)] + ["par0", "par1", "par2"], default_p0=[0, 1, 2], default_bounds=[(0, 1), (1, 2), (2, 3)] ) opt.add_extra_options(ex1=0, ex2=1) ref_opts = { - "p0": {"p0": 0.0, "p1": 1.0, "p2": 2.0}, - "bounds": {"p0": (0.0, 1.0), "p1": (1.0, 2.0), "p2": (2.0, 3.0)}, + "p0": {"par0": 0.0, "par1": 1.0, "par2": 2.0}, + "bounds": {"par0": (0.0, 1.0), "par1": (1.0, 2.0), "par2": (2.0, 3.0)}, "ex1": 0, "ex2": 1, } @@ -697,40 +708,89 @@ def test_set_extra_options(self): def test_complicated(self): """Test for realistic operations for algorithmic guess with user options.""" - user_p0 = {"p0": 1, "p1": None} - user_bounds = {"p0": None, "p1": (-100, 100)} + user_p0 = {"par0": 1, "par1": None} + user_bounds = {"par0": None, "par1": (-100, 100)} opt = FitOptions( - ["p0", "p1", "p2"], + ["par0", "par1", "par2"], default_p0=user_p0, default_bounds=user_bounds, ) # similar computation in algorithmic guess - opt.p0.set_if_empty(p0=5) # this is ignored because user already provided initial guess - opt.p0.set_if_empty(p1=opt.p0["p0"] * 2 + 3) # user provided guess propagates + opt.p0.set_if_empty(par0=5) # this is ignored because user already provided initial guess + opt.p0.set_if_empty(par1=opt.p0["par0"] * 2 + 3) # user provided guess propagates - opt.bounds.set_if_empty(p0=(0, 10)) # this will be set + opt.bounds.set_if_empty(par0=(0, 10)) # this will be set opt.add_extra_options(fitter="algo1") opt1 = opt.copy() # copy options while keeping previous values - opt1.p0.set_if_empty(p2=opt1.p0["p0"] + opt1.p0["p1"]) + opt1.p0.set_if_empty(par2=opt1.p0["par0"] + opt1.p0["par1"]) opt2 = opt.copy() - opt2.p0.set_if_empty(p2=opt2.p0["p0"] * 2) # add another p2 value + opt2.p0.set_if_empty(par2=opt2.p0["par0"] * 2) # add another p2 value ref_opt1 = { - "p0": {"p0": 1.0, "p1": 5.0, "p2": 6.0}, - "bounds": {"p0": (0.0, 10.0), "p1": (-100.0, 100.0), "p2": (-np.inf, np.inf)}, + "p0": {"par0": 1.0, "par1": 5.0, "par2": 6.0}, + "bounds": {"par0": (0.0, 10.0), "par1": (-100.0, 100.0), "par2": (-np.inf, np.inf)}, "fitter": "algo1", } ref_opt2 = { - "p0": {"p0": 1.0, "p1": 5.0, "p2": 2.0}, - "bounds": {"p0": (0.0, 10.0), "p1": (-100.0, 100.0), "p2": (-np.inf, np.inf)}, + "p0": {"par0": 1.0, "par1": 5.0, "par2": 2.0}, + "bounds": {"par0": (0.0, 10.0), "par1": (-100.0, 100.0), "par2": (-np.inf, np.inf)}, "fitter": "algo1", } self.assertDictEqual(opt1.options, ref_opt1) self.assertDictEqual(opt2.options, ref_opt2) + + +class TestBackwardCompatibility(QiskitExperimentsTestCase): + """Test case for backward compatibility.""" + + def test_old_fixed_param_attributes(self): + """Test if old class structure for fixed param is still supported.""" + + class _DeprecatedAnalysis(CurveAnalysis): + __series__ = [ + SeriesDef( + fit_func=lambda x, par0, par1, par2, par3: fit_function.exponential_decay( + x, amp=par0, lamb=par1, x0=par2, baseline=par3 + ), + ) + ] + + __fixed_parameters__ = ["par1"] + + @classmethod + def _default_options(cls): + opts = super()._default_options() + opts.par1 = 2 + + return opts + + with self.assertWarns(DeprecationWarning): + instance = _DeprecatedAnalysis() + + self.assertDictEqual(instance.options.fixed_parameters, {"par1": 2}) + + def test_loading_data_with_deprecated_fixed_param(self): + """Test loading old data with fixed parameters as standalone options.""" + + class _DeprecatedAnalysis(CurveAnalysis): + __series__ = [ + SeriesDef( + fit_func=lambda x, par0, par1, par2, par3: fit_function.exponential_decay( + x, amp=par0, lamb=par1, x0=par2, baseline=par3 + ), + ) + ] + + with self.assertWarns(DeprecationWarning): + # old option data structure, i.e. fixed param as a standalone option + # the analysis instance fixed parameters might be set via the experiment instance + instance = _DeprecatedAnalysis.from_config({"options": {"par1": 2}}) + + self.assertDictEqual(instance.options.fixed_parameters, {"par1": 2}) diff --git a/test/curve_analysis/test_standard_analysis.py b/test/curve_analysis/test_standard_analysis.py index c54f115b78..b12c543e38 100644 --- a/test/curve_analysis/test_standard_analysis.py +++ b/test/curve_analysis/test_standard_analysis.py @@ -50,15 +50,15 @@ def test_fit_vals(self, d_theta_targ): class FakeAmpAnalysis(ErrorAmplificationAnalysis): """Analysis class for testing.""" - __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] - @classmethod def _default_options(cls) -> Options: """Default analysis options.""" options = super()._default_options() - options.angle_per_gate = np.pi - options.phase_offset = np.pi / 2 - options.amp = 1.0 + options.fixed_parameters = { + "angle_per_gate": np.pi, + "phase_offset": np.pi / 2, + "amp": 1.0, + } return options