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)
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