# Curve Analysis (qiskit_experiments.curve_analysis)¶

Curve analysis provides the analysis base class for a variety of experiments with a single experimental parameter sweep. This analysis subclasses can override several class attributes to customize the behavior from data processing to post-processing, including providing systematic initial guess for parameters tailored to the experiment. Here we describe how code developers can create new analysis inheriting from the base class.

## Curve Analysis Overview¶

The base class CurveAnalysis implements the multi-objective optimization on different sets of experiment results. A single experiment can define sub-experiments consisting of multiple circuits which are tagged with common metadata, and curve analysis sorts the experiment results based on the circuit metadata.

This is an example of showing the abstract data structure of typical curve analysis experiment:

"experiment"
- circuits[0] (x=x1_A, "series_A")
- circuits[1] (x=x1_B, "series_B")
- circuits[2] (x=x2_A, "series_A")
- circuits[3] (x=x2_B, "series_B")
- circuits[4] (x=x3_A, "series_A")
- circuits[5] (x=x3_B, "series_B")
- ...

"experiment data"
- data[0] (y1_A, "series_A")
- data[1] (y1_B, "series_B")
- data[2] (y2_A, "series_A")
- data[3] (y2_B, "series_B")
- data[4] (y3_A, "series_A")
- data[5] (y3_B, "series_B")
- ...

"analysis"
- "series_A": y_A = f_A(x_A; p0, p1, p2)
- "series_B": y_B = f_B(x_B; p0, p1, p2)
- fixed parameters {p1: v}


Here the experiment runs two subset of experiments, namely, series A and series B. The analysis defines corresponding fit models $$f_A(x_A)$$ and $$f_B(x_B)$$. Data extraction function in the analysis creates two datasets, $$(x_A, y_A)$$ for the series A and $$(x_B, y_B)$$ for the series B, from the experiment data. Optionally, the curve analysis can fix certain parameters during the fitting. In this example, $$p_1 = v$$ remains unchanged during the fitting.

The curve analysis aims at solving the following optimization problem:

$\Theta_{\mbox{opt}} = \arg\min_{\Theta_{\rm fit}} \sigma^{-2} (F(X, \Theta)-Y)^2,$

where $$F$$ is the composite objective function defined on the full experiment data $$(X, Y)$$, where $$X = x_A \oplus x_B$$ and $$Y = y_A \oplus y_B$$. This objective function can be described by two fit functions as follows.

$F(X, \Theta) = f_A(x_A, \theta_A) \oplus f_B(x_B, \theta_B).$

The solver conducts the least square curve fitting against this objective function and returns the estimated parameters $$\Theta_{\mbox{opt}}$$ that minimizes the reduced chi-squared value. The parameters to be evaluated are $$\Theta = \Theta_{\rm fit} \cup \Theta_{\rm fix}$$, where $$\Theta_{\rm fit} = \theta_A \cup \theta_B$$. Since series A and B share the parameters in this example, $$\Theta_{\rm fit} = \{p_0, p_2\}$$, and the fixed parameters are $$\Theta_{\rm fix} = \{ p_1 \}$$ as mentioned. Thus, $$\Theta = \{ p_0, p_1, p_2 \}$$.

Experiment for each series can perform individual parameter sweep for $$x_A$$ and $$x_B$$, and experiment data yield outcomes $$y_A$$ and $$y_B$$, which might be different size. Data processing function may also compute $$\sigma_A$$ and $$\sigma_B$$ which are the uncertainty of outcomes arising from the sampling error or measurement error.

More specifically, the curve analysis defines following data model.

• Series: Definition of the single curve. Every series may define unique filter keyword arguments for data sorting, a fit function with parameters, and preferred style for fit outcome visualization.

• Group: List of series. Fit functions defined under the group must share the fit parameters. Fit functions in the group are simultaneously fit to generate a single fit result.

To manage this structure, curve analysis provides a special dataclass SeriesDef that represents a model configuration for a single curve data. Based on this information, the curve analysis automatically builds the optimization routine. Finally, the analysis outputs a set of AnalysisResultData entries for important fit outcomes along with a single Matplotlib figure of the fit curves with the measured data points.

With this baseclass a developer can avoid writing boilerplate code in various curve analyses subclass and one can quickly write up the analysis code for a particular experiment.

## Defining New Series¶

You can intuitively write the definition of a new series, as shown below:

from qiskit_experiments.curve_analysis import SeriesDef, fit_function

SeriesDef(
fit_func=lambda x, p0, p1, p2: fit_function.exponential_decay(
x, amp=p0, lamb=p1, baseline=p2
),
model_description="p0 * exp(-p1 * x) + p2",
)


The minimum field you must fill with is the fit_func, which is a callback function used with the optimization solver. Here you must call one of the fit functions from the module qiskit_experiments.curve_analysis.fit_function because they implement special logic to compute error propagation. Note that argument name of the fit function is important because the signature of the provided fit function is inspected behind the scenes and used as a parameter name of the analysis result instance. This name may be used to populate your experiment database with the result.

Optionally you can set model_description which is a string representation of your fitting model that will be passed to the analysis result as a part of metadata. This instance should be set to CurveAnalysis.__series__ as a python list.

Here is another example how to implement multi-objective optimization task:

[
SeriesDef(
name="my_experiment1",
fit_func=lambda x, p0, p1, p2, p3: fit_function.exponential_decay(
x, amp=p0, lamb=p1, baseline=p3
),
filter_kwargs={"tag": 1},
plot_color="red",
plot_symbol="^",
),
SeriesDef(
name="my_experiment2",
fit_func=lambda x, p0, p1, p2, p3: fit_function.exponential_decay(
x, amp=p0, lamb=p2, baseline=p3
),
filter_kwargs={"tag": 2},
plot_color="blue",
plot_symbol="o",
),
]


Note that now you also need to provide name and filter_kwargs to distinguish the entries and filter the corresponding dataset from the experiment data. Optionally, you can provide plot_color and plot_symbol to visually separate two curves in the plot. In this model, you have 4 parameters [p0, p1, p2, p3] and the two curves share p0 (p3) for amp (baseline) of the exponential_decay() fit function. Here one should expect the experiment data will have two classes of data with metadata "tag": 1 and "tag": 2 for my_experiment1 and my_experiment2, respectively.

By using this model, one can flexibly set up your fit model. Here is another example:

[
SeriesDef(
name="my_experiment1",
fit_func=lambda x, p0, p1, p2, p3: fit_function.cos(
x, amp=p0, freq=p1, phase=p2, baseline=p3
),
filter_kwargs={"tag": 1},
plot_color="red",
plot_symbol="^",
),
SeriesDef(
name="my_experiment2",
fit_func=lambda x, p0, p1, p2, p3: fit_function.sin(
x, amp=p0, freq=p1, phase=p2, baseline=p3
),
filter_kwargs={"tag": 2},
plot_color="blue",
plot_symbol="o",
),
]


You have the same set of fit parameters for two curves, but now you fit two datasets with different trigonometric functions.

## Fitting with Fixed Parameters¶

You can also remain certain parameters unchanged during the fitting by specifying the parameter names in the analysis option fixed_parameters. This feature is useful especially when you want to define a subclass of a particular analysis class.

class AnalysisA(CurveAnalysis):

__series__ = [
SeriesDef(
fit_func=lambda x, p0, p1, p2: fit_function.exponential_decay(
x, amp=p0, lamb=p1, baseline=p2
),
),
]

class AnalysisB(AnalysisA):

@classmethod
def _default_options(cls) -> Options:
options = super()._default_options()
options.fixed_parameters = {"p0": 3.0}

return options


The parameter specified in fixed_parameters is exluded from the fitting. This code will give you identical fit model to the one defined in the following class:

class AnalysisB(CurveAnalysis):

__series__ = [
SeriesDef(
fit_func=lambda x, p1, p2: fit_function.exponential_decay(
x, amp=3.0, lamb=p1, baseline=p2
),
),
]


However, note that you can also inherit other features, e.g. the algorithm to generate initial guesses for parameters, from the AnalysisA in the first example. On the other hand, in the latter case, you need to manually copy and paste every logic defined in the AnalysisA.

## Cureve Analysis Workflow¶

Typically curve analysis performs fitting as follows. This workflow is defined in the method CurveAnalysis._run_analysis().

1. Initialization

Curve analysis calls _initialization() method where it initializes some internal states and optionally populate analysis options with the input experiment data. In some case it may train the data processor with fresh outcomes. A developer can override this method to perform initialization of analysis-specific variables.

1. Data processing

Curve analysis calls _run_data_processing() method where the data processor in the analysis option is internally called. This consumes input experiment results and creates CurveData dataclass. Then _format_data() method is called with the processed dataset to format it. By default, the formatter takes average of the outcomes in the processed dataset over the same x values, followed by the sorting in the ascending order of x values. This allows the analysis to easily estimate the slope of the curves to create algorithmic initial guess of fit parameters. A developer can inject extra data processing, for example, filtering, smoothing, or elimination of outliers for better fitting.

1. Fitting

Curve analysis calls _run_curve_fit() method which is the core functionality of the fitting. The another method _generate_fit_guesses() is internally called to prepare the initial guess and parameter boundary with respect to the formatted data. A developer usually override this method to provide better initial guess tailored to the defined fit model or type of the associated experiment. See Providing Initial Guesses for more details. A developer can also override the entire _run_curve_fit() method to apply custom fitting algorithms. This method must return FitData dataclass.

1. Post processing

Curve analysis runs several postprocessing against to the fit outcome. It calls _create_analysis_results() to create AnalysisResultData class for the fitting parameters of interest. A developer can inject a custom code to compute custom quantities based on the raw fit parameters. See Curve Analysis Results for details. Afterwards, the analysis draws several curves in the Matplotlib figure. User can set custom drawer to the option curve_drawer. The drawer defaults to the MplCurveDrawer. Finally, it returns the list of created analysis results and Matplotlib figure.

## Providing Initial Guesses¶

When fit is performed without any prior information of parameters, it usually falls into unsatisfactory result. User can provide initial guesses and boundaries for the fit parameters through analysis options p0 and bounds. These values are the dictionary keyed on the parameter name, and one can get the list of parameters with the CurveAnalysis.parameters. Each boundary value can be a tuple of float representing min and max value.

Apart from user provided guesses, the analysis can systematically generate those values with the method _generate_fit_guesses() which is called with CurveData dataclass. If the analysis contains multiple series definitions, we can get the subset of curve data with CurveData.get_subset_of() with the name of the series. A developer can implement the algorithm to generate initial guesses and boundaries by using this curve data object, which will be provided to the fitter. Note that there are several common initial guess estimators available in qiskit_experiments.curve_analysis.guess.

The _generate_fit_guesses() also receives FitOptions instance user_opt, which contains user provided guesses and boundaries. This is dictionary-like object consisting of sub-dictionaries for initial guess .p0, boundary .bounds, and extra options for the fitter. Note that CurveAnalysis uses SciPy curve_fit as the least square solver. See the API documentation for available options.

The FitOptions class implements convenient method set_if_empty() to manage conflict with user provided values, i.e. user provided values have higher priority, thus systematically generated values cannot override user values.

def _generate_fit_guesses(self, user_opt, curve_data):

opt1 = user_opt.copy()
opt1.p0.set_if_empty(p1=3)
opt1.bounds = set_if_empty(p1=(0, 10))

opt2 = user_opt.copy()
opt2.p0.set_if_empty(p1=4)

return [opt1, opt2]


Here you created two options with different p1 values. If multiple options are returned like this, the _run_curve_fit() method attempts to fit with all provided options and finds the best outcome with the minimum reduced chi-square value. When the fit model contains some parameter that cannot be easily estimated from the curve data, you can create multiple options with varying the initial guess to let the fitter find the most reasonable parameters to explain the model. This allows you to avoid analysis failure with the poor initial guesses.

## Evaluate Fit Quality¶

A subclass can override _evaluate_quality() method to provide an algorithm to evaluate quality of the fitting. This method is called with the FitData object which contains fit parameters and the reduced chi-squared value. Qiskit Experiments often uses the empirical criterion chi-squared < 3 as a good fitting.

## Curve Analysis Results¶

Once the best fit parameters are found, the _create_analysis_results() method is called with the same FitData object. By default CurveAnalysis only creates a single entry @Parameters_<name_of_analysis>. This entry consists of fit parameter values with statistical information of the fitting.

If you want to create an analysis result entry for the particular parameter, you can override the analysis options result_parameters. By using ParameterRepr representation, you can rename the parameter in the entry.

from qiskit_experiments.curve_analysis import ParameterRepr

def _default_options(cls) -> Options:
options = super()._default_options()
options.result_parameters = [ParameterRepr("p0", "amp", "Hz")]

return options


Here the first argument p0 is the target parameter defined in the series definition, amp is the representation of p0 in the result entry, and Hz is the optional string for the unit of the value if available.

Not only returning the fit parameters, you can also compute new quantities by combining multiple fit parameters. This can be done by overriding the _create_analysis_results() method.

from qiskit_experiments.framework import AnalysisResultData

p0 = fit_data.fitval("p0")
p1 = fit_data.fitval("p1")

extra_entry = AnalysisResultData(
name="p01",
value=p0 * p1,
quality=quality,
)
outcomes.append(extra_entry)

return outcomes


Note that both p0 and p1 are ufloat object consisting of a nominal value and an error value which assumes the standard deviation. Since this object natively supports error propagation, you don’t need to manually recompute the error of new value.

If there is any missing feature, you can write a feature request as an issue in our GitHub.

## Base Classes¶

 Abstract superclass of curve analysis base classes. Base class for curve analysis with single curve group.

## Data Classes¶

 SeriesDef(fit_func[, filter_kwargs, name, ...]) A dataclass to describe the definition of the curve. CurveData(x, y, y_err, shots, ...) A dataclass that manages the multiple arrays comprising the dataset for fitting. FitData(popt, popt_keys, pcov, ...) A dataclass to store the outcome of the fitting. ParameterRepr(name[, repr, unit]) Detailed description of fitting parameter. FitOptions(parameters[, default_p0, ...]) Collection of fitting options.

## Visualization¶

 Abstract class for the serializable Qiskit Experiments curve drawer. Curve drawer for MatplotLib backend.

## Standard Analysis Library¶

 A class to analyze general exponential decay curve. A class to analyze general exponential decay curve with sinusoidal oscillation. Oscillation analysis class based on a fit of the data to a cosine function. A class to analyze a resonance peak with a square rooted Lorentzian function. A class to analyze a resonance, typically seen as a peak. Error amplification analysis class based on a fit to a cosine function.

### Fit Functions¶

 fit_function.cos(x[, amp, freq, phase, baseline]) Cosine function. fit_function.cos_decay(x[, amp, tau, freq, ...]) Cosine function with exponential decay. fit_function.exponential_decay(x[, amp, ...]) Exponential function fit_function.gaussian(x[, amp, sigma, x0, ...]) Gaussian function fit_function.sqrt_lorentzian(x[, amp, ...]) Square-root Lorentzian function for spectroscopy. fit_function.sin(x[, amp, freq, phase, baseline]) Sine function. fit_function.sin_decay(x[, amp, tau, freq, ...]) Sine function with exponential decay. fit_function.bloch_oscillation_x(x[, px, ...]) Bloch oscillation in x basis. fit_function.bloch_oscillation_y(x[, px, ...]) Bloch oscillation in y basis. fit_function.bloch_oscillation_z(x[, px, ...]) Bloch oscillation in z basis.

### Initial Guess Estimators¶

 Get constant offset of sinusoidal signal. guess.constant_spectral_offset(y[, ...]) Get constant offset of spectral baseline. Get exponential decay parameter from monotonically increasing (decreasing) curve. guess.rb_decay(x, y[, b, a]) Get base of exponential decay function which is assumed to be close to 1. guess.full_width_half_max(x, y, peak_index) Get full width half maximum value of the peak. guess.frequency(x, y[, filter_window, ...]) Get frequency of oscillating signal. guess.max_height(y[, percentile, absolute]) Get maximum value of y curve and its index. guess.min_height(y[, percentile, absolute]) Get minimum value of y curve and its index. guess.oscillation_exp_decay(x, y[, ...]) Get exponential decay parameter from oscillating signal.

### Utilities¶

 is_error_not_significant(val[, fraction, ...]) Check if the standard error of given value is not significant.