Skip to content

Commit

Permalink
Deprecate FitVal and support saving/loading UFloat (#564)
Browse files Browse the repository at this point in the history
This PR introduces uncertainties package in data analysis chain in the curve analysis and also replaces FitVal with ufloat value that the package provides. The result figure of curve analysis is now updated to have confidence interval plot with 1 and 3 sigma intervals by defaults. The old database entries with FitVal can be still loaded. Once the entry is loaded, the analysis result value is implicitly typecasted into ufloat with FutureWarning. 

Co-authored-by: Daniel J. Egger <[email protected]>
Co-authored-by: Christopher Wood <[email protected]>
  • Loading branch information
3 people authored Feb 9, 2022
1 parent 9553d0b commit 1b57107
Show file tree
Hide file tree
Showing 57 changed files with 693 additions and 377 deletions.
4 changes: 2 additions & 2 deletions qiskit_experiments/calibration_management/update_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ def get_value(exp_data: ExperimentData, param_name: str, index: Optional[int] =
# must be passed to analysis results so we don't block indefinitely
candidates = exp_data.analysis_results(param_name, block=False)
if isinstance(candidates, list):
return candidates[index].value.value
return candidates[index].value.nominal_value
else:
return candidates.value.value
return candidates.value.nominal_value


class Frequency(BaseUpdater):
Expand Down
9 changes: 8 additions & 1 deletion qiskit_experiments/curve_analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,15 @@
plot_curve_fit
plot_errorbar
plot_scatter
Utilities
*********
.. autosummary::
:toctree: ../stubs/
is_error_not_significant
"""
from .curve_analysis import CurveAnalysis
from .curve_analysis import CurveAnalysis, is_error_not_significant
from .curve_data import CurveData, SeriesDef, FitData, ParameterRepr, FitOptions
from .curve_fit import (
curve_fit,
Expand Down
69 changes: 49 additions & 20 deletions qiskit_experiments/curve_analysis/curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
"""
# pylint: disable=invalid-name

import copy
import dataclasses
import functools
import inspect
import warnings
from abc import ABC
from typing import Any, Dict, List, Tuple, Callable, Union, Optional
from uncertainties import unumpy as unp

import numpy as np
from qiskit.providers import Backend
import uncertainties
from uncertainties import unumpy as unp

from qiskit.providers import Backend
from qiskit_experiments.curve_analysis.curve_data import (
CurveData,
SeriesDef,
Expand All @@ -44,7 +46,6 @@
BaseAnalysis,
ExperimentData,
AnalysisResultData,
FitVal,
Options,
)

Expand Down Expand Up @@ -72,8 +73,6 @@ class CurveAnalysis(BaseAnalysis, ABC):
- ``name``: Name of the curve. This is arbitrary data field, but should be unique.
- ``plot_color``: String color representation of this series in the plot.
- ``plot_symbol``: String formatter of the scatter of this series in the plot.
- ``plot_fit_uncertainty``: A Boolean signaling whether to plot fit uncertainty
for this series in the plot.
- ``__fixed_parameters__``: A list of parameter names fixed during the fitting.
These parameters should be provided in some way. For example, you can provide
Expand Down Expand Up @@ -574,22 +573,17 @@ def _is_target_series(datum, **filters):

x_key = self.options.x_key
try:
x_values = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float)
xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float)
except KeyError as ex:
raise DataProcessorError(
f"X value key {x_key} is not defined in circuit metadata."
) from ex

if isinstance(data_processor, DataProcessor):
y_data = data_processor(data)

y_nominals = unp.nominal_values(y_data)
y_stderrs = unp.std_devs(y_data)
ydata = data_processor(data)
else:
y_nominals, y_stderrs = zip(*map(data_processor, data))

y_nominals = np.asarray(y_nominals, dtype=float)
y_stderrs = np.asarray(y_stderrs, dtype=float)
ydata = unp.uarray(y_nominals, y_stderrs)

# Store metadata
metadata = np.asarray([datum["metadata"] for datum in data], dtype=object)
Expand All @@ -598,7 +592,7 @@ def _is_target_series(datum, **filters):
shots = np.asarray([datum.get("shots", np.nan) for datum in data])

# Find series (invalid data is labeled as -1)
data_index = np.full(x_values.size, -1, dtype=int)
data_index = np.full(xdata.size, -1, dtype=int)
for idx, series_def in enumerate(self.__series__):
data_matched = np.asarray(
[_is_target_series(datum, **series_def.filter_kwargs) for datum in data], dtype=bool
Expand All @@ -608,9 +602,9 @@ def _is_target_series(datum, **filters):
# Store raw data
raw_data = CurveData(
label="raw_data",
x=x_values,
y=y_nominals,
y_err=y_stderrs,
x=xdata,
y=unp.nominal_values(ydata),
y_err=unp.std_devs(ydata),
shots=shots,
data_index=data_index,
metadata=metadata,
Expand Down Expand Up @@ -878,7 +872,7 @@ def _run_analysis(
analysis_results.append(
AnalysisResultData(
name=PARAMS_ENTRY_PREFIX + self.__class__.__name__,
value=FitVal(fit_result.popt, fit_result.popt_err),
value=[p.nominal_value for p in fit_result.popt],
chisq=fit_result.reduced_chisq,
quality=quality,
extra={
Expand All @@ -903,12 +897,20 @@ def _run_analysis(
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_result.fitval(p_name, unit),
value=fit_val,
chisq=fit_result.reduced_chisq,
quality=quality,
extra=self.options.extra,
extra=metadata,
)
analysis_results.append(result_entry)

Expand Down Expand Up @@ -961,3 +963,30 @@ def _run_analysis(
figures = []

return analysis_results, figures


def is_error_not_significant(
val: Union[float, uncertainties.UFloat],
fraction: float = 1.0,
absolute: Optional[float] = None,
) -> bool:
"""Check if the standard error of given value is not significant.
Args:
val: Input value to evaluate. This is assumed to be float or ufloat.
fraction: Valid fraction of the nominal part to its standard error.
This function returns ``False`` if the nominal part is
smaller than the error by this fraction.
absolute: Use this value as a threshold if given.
Returns:
``True`` if the standard error of given value is not significant.
"""
if isinstance(val, float):
return True

threshold = absolute if absolute is not None else fraction * val.nominal_value
if np.isnan(val.std_dev) or val.std_dev < threshold:
return True

return False
23 changes: 6 additions & 17 deletions qiskit_experiments/curve_analysis/curve_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
from typing import Any, Dict, Callable, Union, List, Tuple, Optional, Iterable

import numpy as np

import uncertainties
from qiskit_experiments.exceptions import AnalysisError
from qiskit_experiments.framework import FitVal


@dataclasses.dataclass(frozen=True)
Expand All @@ -42,9 +41,6 @@ class SeriesDef:
# Symbol to represent data points of this line.
plot_symbol: str = "o"

# Whether to plot fit uncertainty for this line.
plot_fit_uncertainty: bool = False

# Latex description of this fit model
model_description: Optional[str] = None

Expand Down Expand Up @@ -83,14 +79,11 @@ class FitData:
"""Set of data generated by the fit function."""

# Order sensitive fit parameter values
popt: np.ndarray
popt: List[uncertainties.UFloat]

# Order sensitive parameter name list
popt_keys: List[str]

# Order sensitive fit parameter uncertainty
popt_err: np.ndarray

# Covariance matrix
pcov: np.ndarray

Expand All @@ -106,26 +99,22 @@ class FitData:
# Y data range
y_range: Tuple[float, float]

def fitval(self, key: str, unit: Optional[str] = None) -> FitVal:
def fitval(self, key: str) -> uncertainties.UFloat:
"""A helper method to get fit value object from parameter key name.
Args:
key: Name of parameters to extract.
unit: Optional. Unit of this value.
Returns:
FitVal object.
A UFloat object which functions as a standard Python float object
but with automatic error propagation.
Raises:
ValueError: When specified parameter is not defined.
"""
try:
index = self.popt_keys.index(key)
return FitVal(
value=self.popt[index],
stderr=self.popt_err[index],
unit=unit,
)
return self.popt[index]
except ValueError as ex:
raise ValueError(f"Parameter {key} is not defined.") from ex

Expand Down
25 changes: 18 additions & 7 deletions qiskit_experiments/curve_analysis/curve_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import List, Dict, Tuple, Callable, Optional, Union

import numpy as np
import uncertainties
import scipy.optimize as opt
from qiskit_experiments.exceptions import AnalysisError
from qiskit_experiments.curve_analysis.data_processing import filter_data
Expand Down Expand Up @@ -62,8 +63,9 @@ def curve_fit(
``xrange`` the range of xdata values used for fit.
Raises:
AnalysisError: if the number of degrees of freedom of the fit is
less than 1, or the curve fitting fails.
AnalysisError:
When the number of degrees of freedom of the fit is
less than 1, or the curve fitting fails.
.. note::
``sigma`` is assumed to be specified in the same units as ``ydata``
Expand Down Expand Up @@ -92,7 +94,7 @@ def fit_func(x, *params):
return func(x, **dict(zip(param_keys, params)))

else:
param_keys = None
param_keys = [f"p{i}" for i in range(len(p0))]
param_p0 = p0
if bounds:
param_bounds = bounds
Expand Down Expand Up @@ -134,7 +136,17 @@ def fit_func(x, *params):
"scipy.optimize.curve_fit failed with error: {}".format(str(ex))
) from ex

popt_err = np.sqrt(np.diag(pcov))
if np.isfinite(pcov).all():
# Keep parameter correlations in following analysis steps
fit_params = uncertainties.correlated_values(
nom_values=popt, covariance_mat=pcov, tags=param_keys
)
else:
# Ignore correlations, add standard error if finite.
fit_params = [
uncertainties.ufloat(nominal_value=n, std_dev=s if np.isfinite(s) else np.nan)
for n, s in zip(popt, np.sqrt(np.diag(pcov)))
]

# Calculate the reduced chi-squared for fit
yfits = fit_func(xdata, *popt)
Expand All @@ -148,9 +160,8 @@ def fit_func(x, *params):
ydata_range = np.min(ydata), np.max(ydata)

return FitData(
popt=popt,
popt_keys=param_keys,
popt_err=popt_err,
popt=list(fit_params),
popt_keys=list(param_keys),
pcov=pcov,
reduced_chisq=reduced_chisq,
dof=dof,
Expand Down
Loading

0 comments on commit 1b57107

Please sign in to comment.