From 31d03a7074281b447dedf5196884b33ab2003a28 Mon Sep 17 00:00:00 2001 From: ryanhammonds Date: Fri, 12 Feb 2021 16:51:13 -0800 Subject: [PATCH 1/3] flexible dtype results class --- fooof/data/__init__.py | 2 +- fooof/data/data.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/fooof/data/__init__.py b/fooof/data/__init__.py index 87fe8c368..17c0a9367 100644 --- a/fooof/data/__init__.py +++ b/fooof/data/__init__.py @@ -1,3 +1,3 @@ """Data sub-module for FOOOF.""" -from .data import FOOOFSettings, FOOOFMetaData, FOOOFResults, SimParams +from .data import FOOOFSettings, FOOOFMetaData, FOOOFResults, SimParams, FitParams diff --git a/fooof/data/data.py b/fooof/data/data.py index aef669f1a..7e8b58f85 100644 --- a/fooof/data/data.py +++ b/fooof/data/data.py @@ -8,7 +8,13 @@ - this means no additional attributes can be defined (which is more memory efficient) """ -from collections import namedtuple +from collections import namedtuple, OrderedDict + +import numpy as np + +from fooof.core.modutils import safe_import + +pd = safe_import('pandas') ################################################################################################### ################################################################################################### @@ -98,3 +104,33 @@ class SimParams(namedtuple('SimParams', ['aperiodic_params', 'periodic_params', This object is a data object, based on a NamedTuple, with immutable data attributes. """ __slots__ = () + + +class FitParams(np.ndarray): + + def __new__(cls, results, labels): + + return np.asarray(results).view(cls) + + def __init__(self, results, labels): + + self.results = results + self.labels = labels + + def to_dict(self): + + results = OrderedDict((k, v) for k, v in \ + zip(self.labels, self.results.transpose())) + + return results + + def to_pandas(self): + + if pd is None: + raise ValueError("Pandas is not installed.") + + results = self.to_dict() + + results = pd.DataFrame(results) + + return results From f767c28c9a025ac1c35b94d04cb5e5fc0a3b7372 Mon Sep 17 00:00:00 2001 From: ryanhammonds Date: Fri, 19 Feb 2021 17:42:06 -0800 Subject: [PATCH 2/3] get fit funcs accept custom funcs --- fooof/core/funcs.py | 8 ++++++-- fooof/objs/fit.py | 28 ++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/fooof/core/funcs.py b/fooof/core/funcs.py index e4751c467..01a2d34ab 100644 --- a/fooof/core/funcs.py +++ b/fooof/core/funcs.py @@ -167,7 +167,9 @@ def get_pe_func(periodic_mode): """ - if periodic_mode == 'gaussian': + if isinstance(periodic_mode, function): + pe_func = periodic_mode + elif periodic_mode == 'gaussian': pe_func = gaussian_function else: raise ValueError("Requested periodic mode not understood.") @@ -194,7 +196,9 @@ def get_ap_func(aperiodic_mode): If the specified aperiodic mode label is not understood. """ - if aperiodic_mode == 'fixed': + if isinstance(aperiodic_mode, function): + ap_func = aperiodic_mode + elif aperiodic_mode == 'fixed': ap_func = expo_nk_function elif aperiodic_mode == 'knee': ap_func = expo_function diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index fba745d27..68b7418fa 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -67,7 +67,7 @@ from fooof.core.reports import save_report_fm from fooof.core.modutils import copy_doc_func_to_method from fooof.core.utils import group_three, check_array_dim -from fooof.core.funcs import gaussian_function, get_ap_func, infer_ap_func +from fooof.core.funcs import get_pe_func, get_ap_func, infer_ap_func from fooof.core.errors import (FitError, NoModelError, DataError, NoDataError, InconsistentDataError) from fooof.core.strings import (gen_settings_str, gen_results_fm_str, @@ -154,8 +154,9 @@ class FOOOF(): """ # pylint: disable=attribute-defined-outside-init - def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, min_peak_height=0.0, - peak_threshold=2.0, aperiodic_mode='fixed', verbose=True): + def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, + min_peak_height=0.0, peak_threshold=2.0, aperiodic_mode='fixed', + periodic_mode='gaussian', verbose=True): """Initialize object with desired settings.""" # Set input settings @@ -164,6 +165,7 @@ def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, min_peak_h self.min_peak_height = min_peak_height self.peak_threshold = peak_threshold self.aperiodic_mode = aperiodic_mode + self.periodic_mode = periodic_mode self.verbose = verbose ## PRIVATE SETTINGS @@ -439,6 +441,9 @@ def fit(self, freqs=None, power_spectrum=None, freq_range=None): if self.verbose: self._check_width_limits() + # Determine the aperiodic and periodic fit funcs + self._set_fit_funcs() + # In rare cases, the model fails to fit, and so uses try / except try: @@ -715,6 +720,11 @@ def set_check_data_mode(self, check_data): self._check_data = check_data + def _set_fit_funcs(self): + """Set the requested aperiodic and periodic fit functions.""" + + self._pe_func = get_pe_func(self.periodic_mode) + self._ap_func = get_ap_func(self.aperiodic_mode) def _check_width_limits(self): """Check and warn about peak width limits / frequency resolution interaction.""" @@ -762,8 +772,7 @@ def _simple_ap_fit(self, freqs, power_spectrum): try: with warnings.catch_warnings(): warnings.simplefilter("ignore") - aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode), - freqs, power_spectrum, p0=guess, + aperiodic_params, _ = curve_fit(self._ap_func, freqs, power_spectrum, p0=guess, maxfev=self._maxfev, bounds=ap_bounds) except RuntimeError: raise FitError("Model fitting failed due to not finding parameters in " @@ -818,9 +827,8 @@ def _robust_ap_fit(self, freqs, power_spectrum): try: with warnings.catch_warnings(): warnings.simplefilter("ignore") - aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode), - freqs_ignore, spectrum_ignore, p0=popt, - maxfev=self._maxfev, bounds=ap_bounds) + aperiodic_params, _ = curve_fit(self._ap_func, freqs_ignore, spectrum_ignore, + p0=popt, maxfev=self._maxfev, bounds=ap_bounds) except RuntimeError: raise FitError("Model fitting failed due to not finding " "parameters in the robust aperiodic fit.") @@ -904,7 +912,7 @@ def _fit_peaks(self, flat_iter): # Collect guess parameters and subtract this guess gaussian from the data guess = np.vstack((guess, (guess_freq, guess_height, guess_std))) - peak_gauss = gaussian_function(self.freqs, guess_freq, guess_height, guess_std) + peak_gauss = self._pe_func(self.freqs, guess_freq, guess_height, guess_std) flat_iter = flat_iter - peak_gauss # Check peaks based on edges, and on overlap, dropping any that violate requirements @@ -963,7 +971,7 @@ def _fit_peak_guess(self, guess): # Fit the peaks try: - gaussian_params, _ = curve_fit(gaussian_function, self.freqs, self._spectrum_flat, + gaussian_params, _ = curve_fit(self._pe_func, self.freqs, self._spectrum_flat, p0=guess, maxfev=self._maxfev, bounds=gaus_param_bounds) except RuntimeError: raise FitError("Model fitting failed due to not finding " From a4a02a20fdf846ca0ff8b31b58138ff621bba3f1 Mon Sep 17 00:00:00 2001 From: ryanhammonds Date: Mon, 22 Feb 2021 20:05:19 -0800 Subject: [PATCH 3/3] to_dict and to_pandas added to FOOOFResults --- fooof/core/funcs.py | 75 +++++++++++++++++++++++++++++---------------- fooof/data/data.py | 42 +++++++++++++++++++------ fooof/objs/fit.py | 12 ++++++-- 3 files changed, 90 insertions(+), 39 deletions(-) diff --git a/fooof/core/funcs.py b/fooof/core/funcs.py index 01a2d34ab..e65350db3 100644 --- a/fooof/core/funcs.py +++ b/fooof/core/funcs.py @@ -7,6 +7,8 @@ - They are left available for easy swapping back in, if desired. """ +from inspect import isfunction + import numpy as np from fooof.core.errors import InconsistentDataError @@ -14,15 +16,21 @@ ################################################################################################### ################################################################################################### -def gaussian_function(xs, *params): +def gaussian_function(xs, cf, pw, bw, *params): """Gaussian fitting function. Parameters ---------- xs : 1d array Input x-axis values. + cf : float + The center of the gaussian. + pw : float + The height of the gaussian. + bw : float + The width of the gaussian. *params : float - Parameters that define gaussian function. + Additional centers, heights, and widths. Returns ------- @@ -32,16 +40,17 @@ def gaussian_function(xs, *params): ys = np.zeros_like(xs) + params = [cf, pw, bw, *params] + for ii in range(0, len(params), 3): ctr, hgt, wid = params[ii:ii+3] - ys = ys + hgt * np.exp(-(xs-ctr)**2 / (2*wid**2)) return ys -def expo_function(xs, *params): +def expo_function(xs, offset, knee, exp): """Exponential fitting function, for fitting aperiodic component with a 'knee'. NOTE: this function requires linear frequency (not log). @@ -50,26 +59,32 @@ def expo_function(xs, *params): ---------- xs : 1d array Input x-axis values. - *params : float - Parameters (offset, knee, exp) that define Lorentzian function: - y = 10^offset * (1/(knee + x^exp)) + offset : float + The y-intercept of the fit. + knee : float + The bend in the fit. + exp : float + The exponential slope of the fit. Returns ------- ys : 1d array Output values for exponential function. + + Notes + ----- + Parameters (offset, knee, exp) that define Lorentzian function: + y = 10^offset * (1/(knee + x^exp)) """ ys = np.zeros_like(xs) - offset, knee, exp = params - ys = ys + offset - np.log10(knee + xs**exp) return ys -def expo_nk_function(xs, *params): +def expo_nk_function(xs, offset, exp): """Exponential fitting function, for fitting aperiodic component without a 'knee'. NOTE: this function requires linear frequency (not log). @@ -78,34 +93,40 @@ def expo_nk_function(xs, *params): ---------- xs : 1d array Input x-axis values. - *params : float - Parameters (offset, exp) that define Lorentzian function: - y = 10^off * (1/(x^exp)) + offset : float + The y-intercept of the fit. + exp : float + The exponential slope of the fit. Returns ------- ys : 1d array Output values for exponential function, without a knee. + + Notes + ----- + Parameters (offset, exp) that define Lorentzian function: + y = 10^off * (1/(x^exp)) """ ys = np.zeros_like(xs) - offset, exp = params - ys = ys + offset - np.log10(xs**exp) return ys -def linear_function(xs, *params): +def linear_function(xs, offset, slope): """Linear fitting function. Parameters ---------- xs : 1d array Input x-axis values. - *params : float - Parameters that define linear function. + offset : float + The y-intercept of the fit. + slope : float + The slope of the fit. Returns ------- @@ -115,22 +136,24 @@ def linear_function(xs, *params): ys = np.zeros_like(xs) - offset, slope = params - ys = ys + offset + (xs*slope) return ys -def quadratic_function(xs, *params): +def quadratic_function(xs, offset, slope, curve): """Quadratic fitting function. Parameters ---------- xs : 1d array Input x-axis values. - *params : float - Parameters that define quadratic function. + offset : float + The y-intercept of the fit. + slope : float + The slope of the fit. + curve : float + The curve of the fit. Returns ------- @@ -140,8 +163,6 @@ def quadratic_function(xs, *params): ys = np.zeros_like(xs) - offset, slope, curve = params - ys = ys + offset + (xs*slope) + ((xs**2)*curve) return ys @@ -167,7 +188,7 @@ def get_pe_func(periodic_mode): """ - if isinstance(periodic_mode, function): + if isfunction(periodic_mode): pe_func = periodic_mode elif periodic_mode == 'gaussian': pe_func = gaussian_function @@ -196,7 +217,7 @@ def get_ap_func(aperiodic_mode): If the specified aperiodic mode label is not understood. """ - if isinstance(aperiodic_mode, function): + if isfunction(aperiodic_mode): ap_func = aperiodic_mode elif aperiodic_mode == 'fixed': ap_func = expo_nk_function diff --git a/fooof/data/data.py b/fooof/data/data.py index 7e8b58f85..e7d44ef78 100644 --- a/fooof/data/data.py +++ b/fooof/data/data.py @@ -84,8 +84,30 @@ class FOOOFResults(namedtuple('FOOOFResults', ['aperiodic_params', 'peak_params' ----- This object is a data object, based on a NamedTuple, with immutable data attributes. """ + __slots__ = () + def to_dict(self): + + # Combined peak, aperiodic, and goodness of fit params + results_dict = OrderedDict() + results_dict.update(self.peak_params.to_dict()) + results_dict.update(self.aperiodic_params.to_dict()) + results_dict.update(OrderedDict(r_squared=self.r_squared, error=self.error)) + + return results_dict + + def to_pandas(self): + + if pd is None: + raise ValueError("Pandas is not installed.") + + results_dict = self.to_dict() + + results_df = pd.DataFrame(results_dict) + + return results_df + class SimParams(namedtuple('SimParams', ['aperiodic_params', 'periodic_params', 'nlv'])): """Parameters that define a simulated power spectrum. @@ -108,29 +130,29 @@ class SimParams(namedtuple('SimParams', ['aperiodic_params', 'periodic_params', class FitParams(np.ndarray): - def __new__(cls, results, labels): + def __new__(cls, params, labels): - return np.asarray(results).view(cls) + return np.asarray(params).view(cls) - def __init__(self, results, labels): + def __init__(self, params, labels): - self.results = results + self.params = params self.labels = labels def to_dict(self): - results = OrderedDict((k, v) for k, v in \ - zip(self.labels, self.results.transpose())) + params = OrderedDict((k, v) for k, v in \ + zip(self.labels, self.params.transpose())) - return results + return params def to_pandas(self): if pd is None: raise ValueError("Pandas is not installed.") - results = self.to_dict() + params = self.to_dict() - results = pd.DataFrame(results) + params = pd.DataFrame(params) - return results + return params diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index 68b7418fa..78418262b 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -56,6 +56,7 @@ import warnings from copy import deepcopy +from inspect import signature import numpy as np from numpy.linalg import LinAlgError @@ -77,7 +78,7 @@ from fooof.plts.style import style_spectrum_plot from fooof.utils.data import trim_spectrum from fooof.utils.params import compute_gauss_std -from fooof.data import FOOOFResults, FOOOFSettings, FOOOFMetaData +from fooof.data import FOOOFResults, FOOOFSettings, FOOOFMetaData, FitParams from fooof.sim.gen import gen_freqs, gen_aperiodic, gen_periodic, gen_model ################################################################################################### @@ -484,6 +485,14 @@ def fit(self, freqs=None, power_spectrum=None, freq_range=None): # Convert gaussian definitions to peak parameters self.peak_params_ = self._create_peak_params(self.gaussian_params_) + # Create a flexible datatype object for ap/pe parameters + pe_labels = list(signature(self._pe_func).parameters.keys())[1:] + ap_labels = list(signature(self._ap_func).parameters.keys())[1:] + + self.peak_params_ = FitParams(self.peak_params_, pe_labels) + self.gaussian_params_ = FitParams(self.gaussian_params_, pe_labels) + self.aperiodic_params_ = FitParams(self.aperiodic_params_, ap_labels) + # Calculate R^2 and error of the model fit self._calc_r_squared() self._calc_error() @@ -636,7 +645,6 @@ def get_results(self): return FOOOFResults(**{key.strip('_') : getattr(self, key) \ for key in OBJ_DESC['results']}) - @copy_doc_func_to_method(plot_fm) def plot(self, plot_peaks=None, plot_aperiodic=True, plt_log=False, add_legend=True, save_fig=False, file_name=None, file_path=None,