Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Curve analysis with uncertainties package #551

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cd67ca7
add fit function wrapper
nkanazawa1989 Nov 30, 2021
a733d9a
offload error propagation to uncertainties package
nkanazawa1989 Dec 1, 2021
d875ba7
remove FitVal from experiments (replaced with ufloat)
nkanazawa1989 Dec 1, 2021
4050a5f
use better alias
nkanazawa1989 Dec 1, 2021
b204540
replace np function with unp
nkanazawa1989 Dec 1, 2021
2b19a10
keep fit parameter correlation
nkanazawa1989 Dec 1, 2021
2ad7511
cleanup
nkanazawa1989 Dec 1, 2021
394b564
lint
nkanazawa1989 Dec 2, 2021
d5a234a
minor fix
nkanazawa1989 Dec 2, 2021
2d97ba5
fix import
nkanazawa1989 Dec 2, 2021
87ba919
reno
nkanazawa1989 Dec 2, 2021
c1ee1f6
Merge branch 'main' into upgrade/curve_analysis_uncertainties
nkanazawa1989 Dec 6, 2021
9aaeab4
Update qiskit_experiments/curve_analysis/curve_data.py
nkanazawa1989 Dec 6, 2021
47e92ed
Update qiskit_experiments/curve_analysis/fit_function.py
nkanazawa1989 Dec 6, 2021
13724db
Update qiskit_experiments/curve_analysis/fit_function.py
nkanazawa1989 Dec 6, 2021
9405937
Update releasenotes/notes/upgrade-curve-fit-4dc01b1db55ee398.yaml
nkanazawa1989 Dec 6, 2021
a238d97
add error if finite
nkanazawa1989 Dec 6, 2021
602bf79
add safeguard for nan stdev
nkanazawa1989 Dec 6, 2021
9e9715b
update decorator name
nkanazawa1989 Dec 6, 2021
ab23437
Merge branch 'upgrade/curve_analysis_uncertainties' of github.com:nka…
nkanazawa1989 Dec 6, 2021
6e149e9
tag issue to TODO
nkanazawa1989 Dec 6, 2021
707ffec
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into upgr…
nkanazawa1989 Dec 6, 2021
af3f489
black
nkanazawa1989 Dec 6, 2021
3b822b6
update tutorials
nkanazawa1989 Dec 6, 2021
320e444
fix bug
nkanazawa1989 Dec 6, 2021
3066dbc
black
nkanazawa1989 Dec 6, 2021
f8922fc
fix bug
nkanazawa1989 Dec 6, 2021
432529b
add 3 sigma area
nkanazawa1989 Dec 8, 2021
ef79416
replace util function name
nkanazawa1989 Dec 8, 2021
97113d3
update docs
nkanazawa1989 Dec 8, 2021
2a9b9a4
Merge branch 'main' into upgrade/curve_analysis_uncertainties
nkanazawa1989 Dec 8, 2021
670d4be
change until function name to be more precise
nkanazawa1989 Dec 8, 2021
776af49
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into upgr…
nkanazawa1989 Dec 10, 2021
7b4e8e8
rerun tutorials
nkanazawa1989 Dec 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 108 additions & 84 deletions docs/tutorials/randomized_benchmarking.ipynb

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions docs/tutorials/t1.ipynb

Large diffs are not rendered by default.

52 changes: 24 additions & 28 deletions docs/tutorials/t2ramsey_characterization.ipynb

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions qiskit_experiments/curve_analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@
plot_curve_fit
plot_errorbar
plot_scatter

Utilities
*********
.. autosummary::
:toctree: ../stubs/

is_error_not_significant
"""
from .curve_analysis import CurveAnalysis
from .curve_data import CurveData, SeriesDef, FitData, ParameterRepr, FitOptions
Expand All @@ -110,6 +117,7 @@
process_curve_data,
process_multi_curve_data,
)
from .utils import is_error_not_significant
from .visualization import plot_curve_fit, plot_errorbar, plot_scatter, FitResultPlotters
from . import guess
from . import fit_function
Expand Down
27 changes: 10 additions & 17 deletions qiskit_experiments/curve_analysis/curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
BaseAnalysis,
ExperimentData,
AnalysisResultData,
FitVal,
Options,
)

Expand Down Expand Up @@ -72,8 +71,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 @@ -579,22 +576,17 @@ def _is_target_series(datum, **filters):

x_key = self._get_option("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 @@ -603,7 +595,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 @@ -613,9 +605,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 @@ -961,7 +953,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 @@ -988,7 +980,8 @@ def _run_analysis(
unit = None
result_entry = AnalysisResultData(
name=p_repr,
value=fit_result.fitval(p_name, unit),
value=fit_result.fitval(p_name),
unit=unit,
chisq=fit_result.reduced_chisq,
quality=quality,
extra=self._get_option("extra"),
Expand Down
22 changes: 6 additions & 16 deletions qiskit_experiments/curve_analysis/curve_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from typing import Any, Dict, Callable, Union, List, Tuple, Optional, Iterable

import numpy as np
from uncertainties.core import UFloat

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


@dataclasses.dataclass(frozen=True)
Expand All @@ -42,9 +42,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 +80,11 @@ class FitData:
"""Set of data generated by the fit function."""

# Order sensitive fit parameter values
popt: np.ndarray
popt: List[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 +100,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) -> 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]
eggerdj marked this conversation as resolved.
Show resolved Hide resolved
except ValueError as ex:
raise ValueError(f"Parameter {key} is not defined.") from ex

Expand Down
22 changes: 15 additions & 7 deletions qiskit_experiments/curve_analysis/curve_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# pylint: disable = invalid-name

from typing import List, Dict, Tuple, Callable, Optional, Union
from uncertainties import correlated_values, ufloat

import numpy as np
import scipy.optimize as opt
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))]
eggerdj marked this conversation as resolved.
Show resolved Hide resolved
param_p0 = p0
if bounds:
param_bounds = bounds
Expand Down Expand Up @@ -134,7 +136,14 @@ 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 = correlated_values(nom_values=popt, covariance_mat=pcov, tags=param_keys)
else:
# Ignore correlations, add standard error if finite.
fit_params = [
ufloat(n, 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 +157,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
56 changes: 46 additions & 10 deletions qiskit_experiments/curve_analysis/fit_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,39 @@
"""
A library of fit functions.
"""
# pylint: disable=invalid-name
# pylint: disable=invalid-name, no-member

import functools
from typing import Callable, Union

import numpy as np
from uncertainties import unumpy as unp, UFloat


def typecast_returns(fit_func: Callable) -> Callable:
"""A decorator to typecast y values to a float array if the input parameters have no error.

Args:
fit_func: Fit function that returns a ufloat array or an array of float.

Returns:
Fit function with typecast.
"""

@functools.wraps(fit_func)
def _wrapper(x, *args, **kwargs) -> Union[float, UFloat, np.ndarray]:
yvals = fit_func(x, *args, **kwargs)
try:
if isinstance(x, float):
return float(yvals)
return yvals.astype(float)
except TypeError:
return yvals

return _wrapper


@typecast_returns
def cos(
x: np.ndarray,
amp: float = 1.0,
Expand All @@ -31,9 +59,10 @@ def cos(
y = {\rm amp} \cdot \cos\left(2 \pi {\rm freq} \cdot x
+ {\rm phase}\right) + {\rm baseline}
"""
return amp * np.cos(2 * np.pi * freq * x + phase) + baseline
return amp * unp.cos(2 * np.pi * freq * x + phase) + baseline


@typecast_returns
def sin(
x: np.ndarray,
amp: float = 1.0,
Expand All @@ -47,9 +76,10 @@ def sin(
y = {\rm amp} \cdot \sin\left(2 \pi {\rm freq} \cdot x
+ {\rm phase}\right) + {\rm baseline}
"""
return amp * np.sin(2 * np.pi * freq * x + phase) + baseline
return amp * unp.sin(2 * np.pi * freq * x + phase) + baseline


@typecast_returns
def exponential_decay(
x: np.ndarray,
amp: float = 1.0,
Expand All @@ -66,6 +96,7 @@ def exponential_decay(
return amp * base ** (-lamb * x + x0) + baseline


@typecast_returns
def gaussian(
x: np.ndarray, amp: float = 1.0, sigma: float = 1.0, x0: float = 0.0, baseline: float = 0.0
) -> np.ndarray:
Expand All @@ -74,9 +105,10 @@ def gaussian(
.. math::
y = {\rm amp} \cdot \exp \left( - (x - x0)^2 / 2 \sigma^2 \right) + {\rm baseline}
"""
return amp * np.exp(-((x - x0) ** 2) / (2 * sigma ** 2)) + baseline
return amp * unp.exp(-((x - x0) ** 2) / (2 * sigma ** 2)) + baseline


@typecast_returns
def cos_decay(
x: np.ndarray,
amp: float = 1.0,
Expand All @@ -94,6 +126,7 @@ def cos_decay(
return exponential_decay(x, lamb=1 / tau) * cos(x, amp=amp, freq=freq, phase=phase) + baseline


@typecast_returns
def sin_decay(
x: np.ndarray,
amp: float = 1.0,
Expand All @@ -111,6 +144,7 @@ def sin_decay(
return exponential_decay(x, lamb=1 / tau) * sin(x, amp=amp, freq=freq, phase=phase) + baseline


@typecast_returns
def bloch_oscillation_x(
x: np.ndarray, px: float = 0.0, py: float = 0.0, pz: float = 0.0, baseline: float = 0.0
):
Expand All @@ -123,11 +157,12 @@ def bloch_oscillation_x(
where :math:`\omega = \sqrt{p_x^2 + p_y^2 + p_z^2}`. The `p_i` stands for the
measured probability in :math:`i \in \left\{ X, Y, Z \right\}` basis.
"""
w = np.sqrt(px ** 2 + py ** 2 + pz ** 2)
w = unp.sqrt(px ** 2 + py ** 2 + pz ** 2)

return (-pz * px + pz * px * np.cos(w * x) + w * py * np.sin(w * x)) / (w ** 2) + baseline
return (-pz * px + pz * px * unp.cos(w * x) + w * py * unp.sin(w * x)) / (w ** 2) + baseline


@typecast_returns
def bloch_oscillation_y(
x: np.ndarray, px: float = 0.0, py: float = 0.0, pz: float = 0.0, baseline: float = 0.0
):
Expand All @@ -140,11 +175,12 @@ def bloch_oscillation_y(
where :math:`\omega = \sqrt{p_x^2 + p_y^2 + p_z^2}`. The `p_i` stands for the
measured probability in :math:`i \in \left\{ X, Y, Z \right\}` basis.
"""
w = np.sqrt(px ** 2 + py ** 2 + pz ** 2)
w = unp.sqrt(px ** 2 + py ** 2 + pz ** 2)

return (pz * py - pz * py * np.cos(w * x) - w * px * np.sin(w * x)) / (w ** 2) + baseline
return (pz * py - pz * py * unp.cos(w * x) - w * px * unp.sin(w * x)) / (w ** 2) + baseline


@typecast_returns
def bloch_oscillation_z(
x: np.ndarray, px: float = 0.0, py: float = 0.0, pz: float = 0.0, baseline: float = 0.0
):
Expand All @@ -157,6 +193,6 @@ def bloch_oscillation_z(
where :math:`\omega = \sqrt{p_x^2 + p_y^2 + p_z^2}`. The `p_i` stands for the
measured probability in :math:`i \in \left\{ X, Y, Z \right\}` basis.
"""
w = np.sqrt(px ** 2 + py ** 2 + pz ** 2)
w = unp.sqrt(px ** 2 + py ** 2 + pz ** 2)

return (pz ** 2 + (px ** 2 + py ** 2) * np.cos(w * x)) / (w ** 2) + baseline
return (pz ** 2 + (px ** 2 + py ** 2) * unp.cos(w * x)) / (w ** 2) + baseline
3 changes: 1 addition & 2 deletions qiskit_experiments/curve_analysis/standard_analysis/decay.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class DecayAnalysis(curve.CurveAnalysis):
),
plot_color="blue",
model_description=r"amp \exp(-x/tau) + base",
plot_fit_uncertainty=True,
)
]

Expand Down Expand Up @@ -106,7 +105,7 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]:

criteria = [
fit_data.reduced_chisq < 3,
tau.stderr is None or tau.stderr < tau.value,
curve.is_error_not_significant(tau),
]

if all(criteria):
Expand Down
Loading