Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3a4f605
wip migration to curve analysis baseclass
nkanazawa1989 Apr 6, 2022
f9373d4
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into feat…
nkanazawa1989 Apr 21, 2022
220a421
integration into main branch
nkanazawa1989 Apr 21, 2022
9e7ad5b
review comment
nkanazawa1989 Apr 21, 2022
759eaea
developer document in module doc
nkanazawa1989 Apr 22, 2022
3672723
update subclasses
nkanazawa1989 Apr 22, 2022
eb22e39
update curvefit unittests
nkanazawa1989 Apr 22, 2022
62d1609
finalize
nkanazawa1989 Apr 22, 2022
79e8704
remove validation
nkanazawa1989 Apr 22, 2022
3a86a25
review comments
nkanazawa1989 Apr 25, 2022
3d7a5e4
review comments
nkanazawa1989 Apr 25, 2022
ef9e61b
readd method documentation and update reno
nkanazawa1989 Apr 25, 2022
d7e25a4
docs and option name
nkanazawa1989 Apr 25, 2022
72d82e5
update reno with link
nkanazawa1989 Apr 26, 2022
e714fc0
Update releasenotes/notes/cleanup-curve-analysis-96d7ff706cae5b4e.yaml
nkanazawa1989 Apr 26, 2022
6509b4a
Update releasenotes/notes/cleanup-curve-analysis-96d7ff706cae5b4e.yaml
nkanazawa1989 Apr 26, 2022
9a92d1e
Merge branch 'main' into feature/curve_analysis_baseclass
nkanazawa1989 Apr 26, 2022
5f20a7b
Merge branch 'feature/curve_analysis_baseclass' of github.com:nkanaza…
nkanazawa1989 Apr 26, 2022
67c942b
test and lint fix
nkanazawa1989 Apr 26, 2022
0c9cb7c
Merge branch 'main' into feature/curve_analysis_baseclass
nkanazawa1989 Apr 26, 2022
d9013ad
minor typo fix
nkanazawa1989 Apr 26, 2022
8c585b2
Merge branch 'feature/curve_analysis_baseclass' of github.com:nkanaza…
nkanazawa1989 Apr 26, 2022
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
498 changes: 461 additions & 37 deletions qiskit_experiments/curve_analysis/__init__.py

Large diffs are not rendered by default.

550 changes: 550 additions & 0 deletions qiskit_experiments/curve_analysis/base_curve_analysis.py

Large diffs are not rendered by default.

953 changes: 121 additions & 832 deletions qiskit_experiments/curve_analysis/curve_analysis.py

Large diffs are not rendered by default.

59 changes: 46 additions & 13 deletions qiskit_experiments/curve_analysis/curve_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ class SeriesDef:
canvas: Optional[int] = None

# Automatically extracted signature of the fit function
signature: List[str] = dataclasses.field(init=False)
signature: Tuple[str] = dataclasses.field(init=False)

def __post_init__(self):
"""Parse the fit function signature to extract the names of the variables.

Fit functions take arguments F(x, p0, p1, p2, ...) thus the first value should be excluded.
"""
signature = list(inspect.signature(self.fit_func).parameters.keys())
fitparams = signature[1:]
fitparams = tuple(signature[1:])

# Note that this dataclass is frozen
object.__setattr__(self, "signature", fitparams)
Expand All @@ -67,13 +67,10 @@ def __post_init__(self):
class CurveData:
"""Set of extracted experiment data."""

# Name of this data set
label: str

# X data
x: np.ndarray

# Y data (measured data)
# Y data
y: np.ndarray

# Error bar
Expand All @@ -83,10 +80,36 @@ class CurveData:
shots: np.ndarray

# Maping of data index to series index
data_index: Union[np.ndarray, int]
data_allocation: np.ndarray

# List of curve names
labels: List[str]

# Metadata associated with each data point. Generated from the circuit metadata.
metadata: np.ndarray = None
def get_subset_of(self, index: Union[str, int]) -> "CurveData":
"""Filter data by series name or index.

Args:
index: Series index of name.

Returns:
A subset of data corresponding to a particular series.
"""
if isinstance(index, int):
_index = index
_name = self.labels[index]
else:
_index = self.labels.index(index)
_name = index

locs = self.data_allocation == _index
return CurveData(
x=self.x[locs],
y=self.y[locs],
y_err=self.y_err[locs],
shots=self.shots[locs],
data_allocation=np.full(np.count_nonzero(locs), _index),
labels=[_name],
)


@dataclasses.dataclass(frozen=True)
Expand All @@ -108,11 +131,21 @@ class FitData:
# Degree of freedom
dof: int

# X data range
x_range: Tuple[float, float]
# X data
x_data: np.ndarray

# Y data
y_data: np.ndarray

# Y data range
y_range: Tuple[float, float]
@property
def x_range(self) -> Tuple[float, float]:
"""Return range of x values."""
return np.min(self.x_data), np.max(self.x_data)

@property
def y_range(self) -> Tuple[float, float]:
"""Return range of y values."""
return np.min(self.y_data), np.max(self.y_data)

def fitval(self, key: str) -> uncertainties.UFloat:
"""A helper method to get fit value object from parameter key name.
Expand Down
8 changes: 2 additions & 6 deletions qiskit_experiments/curve_analysis/curve_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,14 @@ def fit_func(x, *params):
residues = residues / (sigma**2)
reduced_chisq = np.sum(residues) / dof

# Compute data range for fit
xdata_range = np.min(xdata), np.max(xdata)
ydata_range = np.min(ydata), np.max(ydata)

return FitData(
popt=list(fit_params),
popt_keys=list(param_keys),
pcov=pcov,
reduced_chisq=reduced_chisq,
dof=dof,
x_range=xdata_range,
y_range=ydata_range,
x_data=xdata,
y_data=ydata,
)


Expand Down
16 changes: 3 additions & 13 deletions qiskit_experiments/curve_analysis/standard_analysis/decay.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,10 @@ class DecayAnalysis(curve.CurveAnalysis):
]

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.

Raises:
AnalysisError: When the y data is likely constant.
"""
curve_data = self._data()

user_opt.p0.set_if_empty(base=curve.guess.min_height(curve_data.y)[0])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,13 @@ def _default_options(cls):
return default_options

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.

Raises:
CalibrationError: When ``angle_per_gate`` is missing.
"""
fixed_params = self.options.fixed_parameters

curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)
max_y, min_y = np.max(curve_data.y), np.min(curve_data.y)

Expand Down
24 changes: 7 additions & 17 deletions qiskit_experiments/curve_analysis/standard_analysis/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,11 @@ def _default_options(cls) -> Options:
return options

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.
"""
curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)

user_opt.bounds.set_if_empty(
Expand Down Expand Up @@ -128,22 +122,18 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]:
threshold of two, and
- a standard error on the sigma of the Gaussian that is smaller than the sigma.
"""
curve_data = self._data()

max_freq = np.max(curve_data.x)
min_freq = np.min(curve_data.x)
freq_increment = np.mean(np.diff(curve_data.x))
freq_increment = np.mean(np.diff(fit_data.x_data))

fit_a = fit_data.fitval("a")
fit_b = fit_data.fitval("b")
fit_freq = fit_data.fitval("freq")
fit_sigma = fit_data.fitval("sigma")

snr = abs(fit_a.n) / np.sqrt(abs(np.median(curve_data.y) - fit_b.n))
fit_width_ratio = fit_sigma.n / (max_freq - min_freq)
snr = abs(fit_a.n) / np.sqrt(abs(np.median(fit_data.y_data) - fit_b.n))
fit_width_ratio = fit_sigma.n / np.ptp(fit_data.x_data)

criteria = [
min_freq <= fit_freq.n <= max_freq,
fit_data.x_range[0] <= fit_freq.n <= fit_data.x_range[1],
1.5 * freq_increment < fit_sigma.n,
fit_width_ratio < 0.25,
fit_data.reduced_chisq < 3,
Expand Down
25 changes: 6 additions & 19 deletions qiskit_experiments/curve_analysis/standard_analysis/oscillation.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,11 @@ class OscillationAnalysis(curve.CurveAnalysis):
]

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.
"""
curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)

user_opt.bounds.set_if_empty(
Expand Down Expand Up @@ -182,17 +176,10 @@ class DumpedOscillationAnalysis(curve.CurveAnalysis):
]

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.
"""
curve_data = self._data()

user_opt.p0.set_if_empty(
amp=0.5,
Expand Down
24 changes: 7 additions & 17 deletions qiskit_experiments/curve_analysis/standard_analysis/resonance.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,11 @@ def _default_options(cls) -> Options:
return options

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.
"""
curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)

user_opt.bounds.set_if_empty(
Expand Down Expand Up @@ -128,22 +122,18 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]:
threshold of two, and
- a standard error on the kappa of the Lorentzian that is smaller than the kappa.
"""
curve_data = self._data()

max_freq = np.max(curve_data.x)
min_freq = np.min(curve_data.x)
freq_increment = np.mean(np.diff(curve_data.x))
freq_increment = np.mean(np.diff(fit_data.x_data))

fit_a = fit_data.fitval("a")
fit_b = fit_data.fitval("b")
fit_freq = fit_data.fitval("freq")
fit_kappa = fit_data.fitval("kappa")

snr = abs(fit_a.n) / np.sqrt(abs(np.median(curve_data.y) - fit_b.n))
fit_width_ratio = fit_kappa.n / (max_freq - min_freq)
snr = abs(fit_a.n) / np.sqrt(abs(np.median(fit_data.y_data) - fit_b.n))
fit_width_ratio = fit_kappa.n / np.ptp(fit_data.x_data)

criteria = [
min_freq <= fit_freq.n <= max_freq,
fit_data.x_range[0] <= fit_freq.n <= fit_data.x_range[1],
1.5 * freq_increment < fit_kappa.n,
fit_width_ratio < 0.25,
fit_data.reduced_chisq < 3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import qiskit_experiments.curve_analysis as curve

import qiskit_experiments.data_processing as dp
from qiskit_experiments.database_service.device_component import Qubit
from qiskit_experiments.framework import AnalysisResultData


Expand Down Expand Up @@ -219,24 +218,19 @@ def _default_options(cls):
return default_options

def _generate_fit_guesses(
self, user_opt: curve.FitOptions
self,
user_opt: curve.FitOptions,
curve_data: curve.CurveData,
) -> Union[curve.FitOptions, List[curve.FitOptions]]:
"""Compute the initial guesses.

Args:
user_opt: Fit options filled with user provided guess and bounds.

Returns:
List of fit options that are passed to the fitter function.
"""
user_opt.bounds.set_if_empty(t_off=(0, np.inf), b=(-1, 1))
user_opt.p0.set_if_empty(b=1e-9)

guesses = defaultdict(list)
for control in (0, 1):
x_data = self._data(series_name=f"x|c={control}")
y_data = self._data(series_name=f"y|c={control}")
z_data = self._data(series_name=f"z|c={control}")
x_data = curve_data.get_subset_of(f"x|c={control}")
y_data = curve_data.get_subset_of(f"y|c={control}")
z_data = curve_data.get_subset_of(f"z|c={control}")

omega_xyz = []
for data in (x_data, y_data, z_data):
Expand Down Expand Up @@ -288,20 +282,13 @@ def _generate_fit_guesses(

return fit_options

def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]:
"""Algorithmic criteria for whether the fit is good or bad.

A good fit has:
- If chi-squared value is less than 3.
"""
if fit_data.reduced_chisq < 3:
return "good"

return "bad"

def _extra_database_entry(self, fit_data: curve.FitData) -> List[AnalysisResultData]:
"""Calculate Hamiltonian coefficients from fit values."""
extra_entries = []
def _create_analysis_results(
self,
fit_data: curve.FitData,
quality: str,
**metadata,
) -> List[AnalysisResultData]:
outcomes = super()._create_analysis_results(fit_data, quality, **metadata)

for control in ("z", "i"):
for target in ("x", "y", "z"):
Expand All @@ -313,14 +300,17 @@ def _extra_database_entry(self, fit_data: curve.FitData) -> List[AnalysisResultD
else:
coef_val = 0.5 * (p0_val + p1_val) / (2 * np.pi)

extra_entries.append(
outcomes.append(
AnalysisResultData(
name=f"omega_{control}{target}",
value=coef_val,
chisq=fit_data.reduced_chisq,
device_components=[Qubit(q) for q in self._physical_qubits],
extra={"unit": "Hz"},
quality=quality,
extra={
"unit": "Hz",
**metadata,
},
)
)

return extra_entries
return outcomes
Loading