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:
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.
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()
.
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.
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.
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.
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))
opt1.add_extra_options(method="lm")
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
def _create_analysis_results(self, fit_data, quality, **metadata):
outcomes = super()._create_analysis_results(fit_data, **metadata)
p0 = fit_data.fitval("p0")
p1 = fit_data.fitval("p1")
extra_entry = AnalysisResultData(
name="p01",
value=p0 * p1,
quality=quality,
extra=metadata,
)
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¶
|
A dataclass to describe the definition of the curve. |
|
A dataclass that manages the multiple arrays comprising the dataset for fitting. |
|
A dataclass to store the outcome of the fitting. |
|
Detailed description of fitting parameter. |
|
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¶
|
Cosine function. |
|
Cosine function with exponential decay. |
|
Exponential function |
|
Gaussian function |
|
Square-root Lorentzian function for spectroscopy. |
|
Sine function. |
|
Sine function with exponential decay. |
|
Bloch oscillation in x basis. |
|
Bloch oscillation in y basis. |
|
Bloch oscillation in z basis. |
Initial Guess Estimators¶
Get constant offset of sinusoidal signal. |
|
|
Get constant offset of spectral baseline. |
|
Get exponential decay parameter from monotonically increasing (decreasing) curve. |
|
Get base of exponential decay function which is assumed to be close to 1. |
|
Get full width half maximum value of the peak. |
|
Get frequency of oscillating signal. |
|
Get maximum value of y curve and its index. |
|
Get minimum value of y curve and its index. |
|
Get exponential decay parameter from oscillating signal. |
Utilities¶
|
Check if the standard error of given value is not significant. |