class TestAnalysis(CurveAnalysis):
        """Fake analysis class for unittest."""

        __series__ = [
            SeriesDef(
                name="curve1",
                fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.
                exponential_decay(x, amp=par0, lamb=par1, baseline=par4),
                filter_kwargs={
                    "op1": 1,
                    "op2": True
                },
                model_description=r"p_0 * \exp(p_1 x) + p4",
            ),
            SeriesDef(
                name="curve2",
                fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.
                exponential_decay(x, amp=par0, lamb=par2, baseline=par4),
                filter_kwargs={
                    "op1": 2,
                    "op2": True
                },
                model_description=r"p_0 * \exp(p_2 x) + p4",
            ),
            SeriesDef(
                name="curve3",
                fit_func=lambda x, par0, par1, par2, par3, par4: fit_function.
                exponential_decay(x, amp=par0, lamb=par3, baseline=par4),
                filter_kwargs={
                    "op1": 3,
                    "op2": True
                },
                model_description=r"p_0 * \exp(p_3 x) + p4",
            ),
        ]
    def test_fixed_param_is_missing(self):
        """Test raising an analysis error when fixed parameter is missing."""
        analysis = create_new_analysis(
            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),
                ),
            ],
            fixed_params=["fixed_p2"],
        )

        ref_p0 = 0.1
        ref_p1 = 2
        ref_p2 = -0.3
        ref_p3 = 0.5

        test_data = simulate_output_data(
            func=fit_function.cos,
            xvals=self.xvalues,
            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})
        with self.assertRaises(AnalysisError):
            analysis._run_analysis(test_data)
 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),
                   )
     ]
    def test_run_single_curve_analysis(self):
        """Test analysis for single curve."""
        analysis = create_new_analysis(series=[
            SeriesDef(
                name="curve1",
                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",
            )
        ], )
        ref_p0 = 0.9
        ref_p1 = 2.5
        ref_p2 = 0.0
        ref_p3 = 0.1

        test_data = simulate_output_data(
            func=fit_function.exponential_decay,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "lamb": ref_p1,
                "x0": ref_p2,
                "baseline": ref_p3
            },
        )
        analysis.set_options(
            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)
        result = results[0]

        ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3])

        # 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"],
                             ["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
        result = results[1]
        self.assertEqual(result.name, "parameter_name")
        self.assertEqual(result.extra["unit"], "unit")
        self.assertAlmostEqual(result.value.nominal_value,
                               ref_p1,
                               places=self.err_decimal)
 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),
         ),
         SeriesDef(
             name="fit2",
             fit_func=lambda x, p1: fit_function.exponential_decay(x,
                                                                   amp=p1),
         ),
     ]
     with self.assertRaises(AnalysisError):
         create_new_analysis(
             series=invalid_series)  # fit1 has param p0 while fit2 has p1
    def setUp(self):
        super().setUp()
        self.xvalues = np.linspace(1.0, 5.0, 10)

        # Description of test setting
        #
        # - This model contains three curves, namely, curve1, curve2, curve3
        # - Each curve can be represented by the same function
        # - Parameter amp and baseline are shared among all curves
        # - Each curve has unique lamb
        # - In total 5 parameters in the fit, namely, p0, p1, p2, p3
        #
        self.analysis = create_new_analysis(series=[
            SeriesDef(
                name="curve1",
                fit_func=lambda x, p0, p1, p2, p3, p4: fit_function.
                exponential_decay(x, amp=p0, lamb=p1, baseline=p4),
                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),
                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),
                filter_kwargs={
                    "type": 3,
                    "valid": True
                },
                model_description=r"p_0 * \exp(p_3 x) + p4",
            ),
        ], )
        self.err_decimal = 3
    def test_cannot_create_invalid_series_fit(self):
        """Test we cannot create invalid analysis instance."""
        invalid_series = [
            SeriesDef(
                name="fit1",
                fit_func=lambda x, par0: fit_function.exponential_decay(
                    x, amp=par0),
            ),
            SeriesDef(
                name="fit2",
                fit_func=lambda x, par1: fit_function.exponential_decay(
                    x, amp=par1),
            ),
        ]

        instance = create_new_analysis(series=invalid_series)
        with self.assertRaises(AnalysisError):
            # pylint: disable=pointless-statement
            instance.parameters  # fit1 has param par0 while fit2 has par1
 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),
                   ),
     ]
     with self.assertRaises(AnalysisError):
         create_new_analysis(
             series=valid_series,
             fixed_params=["not_existing_parameter"
                           ],  # this parameter is not defined
         )
    def test_run_fixed_parameters(self):
        """Test analysis when some of parameters are fixed."""
        analysis = create_new_analysis(
            series=[
                SeriesDef(
                    name="curve1",
                    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_par2"],
        )

        ref_p0 = 0.1
        ref_p1 = 2
        ref_p2 = -0.3
        ref_p3 = 0.5

        test_data = simulate_output_data(
            func=fit_function.cos,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "freq": ref_p1,
                "phase": ref_p2,
                "baseline": ref_p3
            },
        )

        analysis.set_options(
            p0={
                "par0": ref_p0,
                "par1": ref_p1,
                "par3": ref_p3
            },
            fixed_parameters={"fixed_par2": ref_p2},
        )

        results, _ = analysis._run_analysis(test_data)
        result = results[0]

        ref_popt = np.asarray([ref_p0, ref_p1, ref_p3])

        # check result data
        np.testing.assert_array_almost_equal(result.value,
                                             ref_popt,
                                             decimal=self.err_decimal)
        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
    def test_run_single_curve_fail(self):
        """Test analysis returns status when it fails."""
        analysis = create_new_analysis(series=[
            SeriesDef(
                name="curve1",
                fit_func=lambda x, par0, par1, par2, par3: fit_function.
                exponential_decay(
                    x, amp=par0, lamb=par1, x0=par2, baseline=par3),
            )
        ], )
        ref_p0 = 0.9
        ref_p1 = 2.5
        ref_p2 = 0.0
        ref_p3 = 0.1

        test_data = simulate_output_data(
            func=fit_function.exponential_decay,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "lamb": ref_p1,
                "x0": ref_p2,
                "baseline": ref_p3
            },
        )
        analysis.set_options(
            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,
        )

        # Try to fit with infeasible parameter boundary. This should fail.
        results, _ = analysis._run_analysis(test_data)

        # This returns only data point entry
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].name, "@Data_TestAnalysis")
    def test_run_two_curves_with_two_fitfuncs(self):
        """Test analysis for two curves. Curves shares fit parameters."""
        analysis = create_new_analysis(series=[
            SeriesDef(
                name="curve1",
                fit_func=lambda x, p0, p1, p2, p3: fit_function.cos(
                    x, amp=p0, freq=p1, phase=p2, baseline=p3),
                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),
                filter_kwargs={"exp": 1},
            ),
        ], )
        ref_p0 = 0.1
        ref_p1 = 2
        ref_p2 = -0.3
        ref_p3 = 0.5

        test_data0 = simulate_output_data(
            func=fit_function.cos,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "freq": ref_p1,
                "phase": ref_p2,
                "baseline": ref_p3
            },
            exp=0,
        )

        test_data1 = simulate_output_data(
            func=fit_function.sin,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "freq": ref_p1,
                "phase": ref_p2,
                "baseline": ref_p3
            },
            exp=1,
        )

        # merge two experiment data
        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
        })
        results, _ = analysis._run_analysis(test_data0)
        result = results[0]

        ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3])

        # check result data
        np.testing.assert_array_almost_equal(result.value.value,
                                             ref_popt,
                                             decimal=self.err_decimal)
    def test_run_two_curves_with_same_fitfunc(self):
        """Test analysis for two curves. Curves shares fit model."""
        analysis = create_new_analysis(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),
                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),
                filter_kwargs={"exp": 1},
            ),
        ], )
        ref_p0 = 0.9
        ref_p1 = 7.0
        ref_p2 = 5.0
        ref_p3 = 0.0
        ref_p4 = 0.1

        test_data0 = simulate_output_data(
            func=fit_function.exponential_decay,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "lamb": ref_p1,
                "x0": ref_p3,
                "baseline": ref_p4
            },
            exp=0,
        )

        test_data1 = simulate_output_data(
            func=fit_function.exponential_decay,
            xvals=self.xvalues,
            param_dict={
                "amp": ref_p0,
                "lamb": ref_p2,
                "x0": ref_p3,
                "baseline": ref_p4
            },
            exp=1,
        )

        # merge two experiment data
        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,
            "p4": ref_p4
        })
        results, _ = analysis._run_analysis(test_data0)
        result = results[0]

        ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3, ref_p4])

        # check result data
        np.testing.assert_array_almost_equal(result.value.value,
                                             ref_popt,
                                             decimal=self.err_decimal)
Exemple #14
0
    def _run_analysis(
        self, experiment_data: ExperimentData
    ) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]:
        #
        # 1. Parse arguments
        #

        # 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__
            }

            # 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__}, "
                    "must be provided by the analysis options to run this analysis."
                )

            # Override series definition with assigned fit functions.
            assigned_series = []
            for series_def in self.__series__:
                dict_def = dataclasses.asdict(series_def)
                dict_def["fit_func"] = functools.partial(
                    series_def.fit_func, **assigned_params)
                assigned_series.append(SeriesDef(**dict_def))
            self.__series__ = assigned_series

        # get experiment metadata
        try:
            self.__experiment_metadata = experiment_data.metadata

        except AttributeError:
            pass

        # get backend
        try:
            self.__backend = experiment_data.backend
        except AttributeError:
            pass

        #
        # 2. Setup data processor
        #

        # If no data processor was provided at run-time we infer one from the job
        # metadata and default to the data processor for averaged classified data.
        data_processor = self.options.data_processor

        if not data_processor:
            data_processor = get_processor(experiment_data, self.options)

        if isinstance(data_processor,
                      DataProcessor) and not data_processor.is_trained:
            # Qiskit DataProcessor instance. May need calibration.
            data_processor.train(data=experiment_data.data())

        #
        # 3. Extract curve entries from experiment data
        #
        self._extract_curves(experiment_data=experiment_data,
                             data_processor=data_processor)

        #
        # 4. Run fitting
        #
        formatted_data = self._data(label="fit_ready")

        # Generate algorithmic initial guesses and boundaries
        default_fit_opt = FitOptions(
            parameters=self._fit_params(),
            default_p0=self.options.p0,
            default_bounds=self.options.bounds,
            **self.options.curve_fitter_options,
        )

        fit_options = self._generate_fit_guesses(default_fit_opt)
        if isinstance(fit_options, FitOptions):
            fit_options = [fit_options]

        # Run fit for each configuration
        fit_results = []
        for fit_opt in set(fit_options):
            try:
                fit_result = self.options.curve_fitter(
                    funcs=[
                        series_def.fit_func for series_def in self.__series__
                    ],
                    series=formatted_data.data_index,
                    xdata=formatted_data.x,
                    ydata=formatted_data.y,
                    sigma=formatted_data.y_err,
                    **fit_opt.options,
                )
                fit_results.append(fit_result)
            except AnalysisError:
                # Some guesses might be too far from the true parameters and may thus fail.
                # We ignore initial guesses that fail and continue with the next fit candidate.
                pass

        # Find best value with chi-squared value
        if len(fit_results) == 0:
            warnings.warn(
                "All initial guesses and parameter boundaries failed to fit the data. "
                "Please provide better initial guesses or fit parameter boundaries.",
                UserWarning,
            )
            # at least return raw data points rather than terminating
            fit_result = None
        else:
            fit_result = sorted(fit_results, key=lambda r: r.reduced_chisq)[0]

        #
        # 5. Create database entry
        #
        analysis_results = []
        if fit_result:
            # pylint: disable=assignment-from-none
            quality = self._evaluate_quality(fit_data=fit_result)

            fit_models = {
                series_def.name: series_def.model_description
                or "no description"
                for series_def in self.__series__
            }

            # overview entry
            analysis_results.append(
                AnalysisResultData(
                    name=PARAMS_ENTRY_PREFIX + self.__class__.__name__,
                    value=[p.nominal_value for p in fit_result.popt],
                    chisq=fit_result.reduced_chisq,
                    quality=quality,
                    extra={
                        "popt_keys": fit_result.popt_keys,
                        "dof": fit_result.dof,
                        "covariance_mat": fit_result.pcov,
                        "fit_models": fit_models,
                        **self.options.extra,
                    },
                ))

            # output special parameters
            result_parameters = self.options.result_parameters
            if result_parameters:
                for param_repr in result_parameters:
                    if isinstance(param_repr, ParameterRepr):
                        p_name = param_repr.name
                        p_repr = param_repr.repr or param_repr.name
                        unit = param_repr.unit
                    else:
                        p_name = param_repr
                        p_repr = param_repr
                        unit = None

                    fit_val = fit_result.fitval(p_name)
                    if unit:
                        metadata = copy.copy(self.options.extra)
                        metadata["unit"] = unit
                    else:
                        metadata = self.options.extra

                    result_entry = AnalysisResultData(
                        name=p_repr,
                        value=fit_val,
                        chisq=fit_result.reduced_chisq,
                        quality=quality,
                        extra=metadata,
                    )
                    analysis_results.append(result_entry)

            # add extra database entries
            analysis_results.extend(self._extra_database_entry(fit_result))

        if self.options.return_data_points:
            # save raw data points in the data base if option is set (default to false)
            raw_data_dict = dict()
            for series_def in self.__series__:
                series_data = self._data(series_name=series_def.name,
                                         label="raw_data")
                raw_data_dict[series_def.name] = {
                    "xdata": series_data.x,
                    "ydata": series_data.y,
                    "sigma": series_data.y_err,
                }
            raw_data_entry = AnalysisResultData(
                name=DATA_ENTRY_PREFIX + self.__class__.__name__,
                value=raw_data_dict,
                extra={
                    "x-unit": self.options.xval_unit,
                    "y-unit": self.options.yval_unit,
                },
            )
            analysis_results.append(raw_data_entry)

        #
        # 6. Create figures
        #
        if self.options.plot:
            fit_figure = FitResultPlotters[
                self.options.curve_plotter].value.draw(
                    series_defs=self.__series__,
                    raw_samples=[
                        self._data(ser.name, "raw_data")
                        for ser in self.__series__
                    ],
                    fit_samples=[
                        self._data(ser.name, "fit_ready")
                        for ser in self.__series__
                    ],
                    tick_labels={
                        "xval_unit": self.options.xval_unit,
                        "yval_unit": self.options.yval_unit,
                        "xlabel": self.options.xlabel,
                        "ylabel": self.options.ylabel,
                        "xlim": self.options.xlim,
                        "ylim": self.options.ylim,
                    },
                    fit_data=fit_result,
                    result_entries=analysis_results,
                    style=self.options.style,
                    axis=self.options.axis,
                )
            figures = [fit_figure]
        else:
            figures = []

        return analysis_results, figures