From afd109eb16f029186909284d4200f41ee44c2c10 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 14:33:05 -0400 Subject: [PATCH 01/12] minor tweak to looping over gaussian params --- fooof/core/funcs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fooof/core/funcs.py b/fooof/core/funcs.py index e4751c46..f14f52b2 100644 --- a/fooof/core/funcs.py +++ b/fooof/core/funcs.py @@ -32,9 +32,7 @@ def gaussian_function(xs, *params): ys = np.zeros_like(xs) - for ii in range(0, len(params), 3): - - ctr, hgt, wid = params[ii:ii+3] + for ctr, hgt, wid in zip(*[iter(params)] * 3): ys = ys + hgt * np.exp(-(xs-ctr)**2 / (2*wid**2)) From 2bfb14005d07cd34e7310f54798594b25cdf8fe3 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 14:38:35 -0400 Subject: [PATCH 02/12] drop unnecessary ys initialization in AP funcs --- fooof/core/funcs.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/fooof/core/funcs.py b/fooof/core/funcs.py index f14f52b2..d6f2c02c 100644 --- a/fooof/core/funcs.py +++ b/fooof/core/funcs.py @@ -58,11 +58,8 @@ def expo_function(xs, *params): Output values for exponential function. """ - ys = np.zeros_like(xs) - offset, knee, exp = params - - ys = ys + offset - np.log10(knee + xs**exp) + ys = offset - np.log10(knee + xs**exp) return ys @@ -86,11 +83,8 @@ def expo_nk_function(xs, *params): Output values for exponential function, without a knee. """ - ys = np.zeros_like(xs) - offset, exp = params - - ys = ys + offset - np.log10(xs**exp) + ys = offset - np.log10(xs**exp) return ys @@ -111,11 +105,8 @@ def linear_function(xs, *params): Output values for linear function. """ - ys = np.zeros_like(xs) - offset, slope = params - - ys = ys + offset + (xs*slope) + ys = offset + (xs*slope) return ys @@ -136,11 +127,8 @@ def quadratic_function(xs, *params): Output values for quadratic function. """ - ys = np.zeros_like(xs) - offset, slope, curve = params - - ys = ys + offset + (xs*slope) + ((xs**2)*curve) + ys = offset + (xs*slope) + ((xs**2)*curve) return ys From a1e1479485381aff10f6360f6f99313006b6b30e Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 14:52:32 -0400 Subject: [PATCH 03/12] drop finite checks in curve_fit, since we already do this --- fooof/objs/fit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index 48106edf..345c93f8 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -946,7 +946,8 @@ def _simple_ap_fit(self, freqs, power_spectrum): warnings.simplefilter("ignore") aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode), freqs, power_spectrum, p0=guess, - maxfev=self._maxfev, bounds=ap_bounds) + maxfev=self._maxfev, bounds=ap_bounds, + check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding parameters in " "the simple aperiodic component fit.") @@ -1003,7 +1004,8 @@ def _robust_ap_fit(self, freqs, power_spectrum): 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) + maxfev=self._maxfev, bounds=ap_bounds, + check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding " "parameters in the robust aperiodic fit.") @@ -1149,7 +1151,8 @@ def _fit_peak_guess(self, guess): # Fit the peaks try: gaussian_params, _ = curve_fit(gaussian_function, self.freqs, self._spectrum_flat, - p0=guess, maxfev=self._maxfev, bounds=gaus_param_bounds) + p0=guess, maxfev=self._maxfev, bounds=gaus_param_bounds, + check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding " "parameters in the peak component fit.") From e89aa35dce49f12e62c2364b90e08d897c708c3c Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 16:27:43 -0400 Subject: [PATCH 04/12] add option to tweak curve_fit tolerance and set new default --- fooof/objs/fit.py | 11 +++++++++-- fooof/tests/objs/test_fit.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index 345c93f8..e5839c3b 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -192,12 +192,16 @@ def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, min_peak_h self._gauss_overlap_thresh = 0.75 # Parameter bounds for center frequency when fitting gaussians, in terms of +/- std dev self._cf_bound = 1.5 - # The maximum number of calls to the curve fitting function - self._maxfev = 5000 # The error metric to calculate, post model fitting. See `_calc_error` for options # Note: this is for checking error post fitting, not an objective function for fitting self._error_metric = 'MAE' + ## PRIVATE CURVE_FIT SETTINGS + # The maximum number of calls to the curve fitting function + self._maxfev = 5000 + # The tolerance setting for curve fitting (see scipy.curve_fit - ftol / xtol / gtol) + self._tol = 0.00001 + ## RUN MODES # Set default debug mode - controls if an error is raised if model fitting is unsuccessful self._debug = False @@ -947,6 +951,7 @@ def _simple_ap_fit(self, freqs, power_spectrum): aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode), freqs, power_spectrum, p0=guess, maxfev=self._maxfev, bounds=ap_bounds, + ftol=self._tol, xtol=self._tol, gtol=self._tol, check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding parameters in " @@ -1005,6 +1010,7 @@ def _robust_ap_fit(self, freqs, power_spectrum): aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode), freqs_ignore, spectrum_ignore, p0=popt, maxfev=self._maxfev, bounds=ap_bounds, + ftol=self._tol, xtol=self._tol, gtol=self._tol, check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding " @@ -1152,6 +1158,7 @@ def _fit_peak_guess(self, guess): try: gaussian_params, _ = curve_fit(gaussian_function, self.freqs, self._spectrum_flat, p0=guess, maxfev=self._maxfev, bounds=gaus_param_bounds, + ftol=self._tol, xtol=self._tol, gtol=self._tol, check_finite=False) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding " diff --git a/fooof/tests/objs/test_fit.py b/fooof/tests/objs/test_fit.py index 23a49432..ad20ff1c 100644 --- a/fooof/tests/objs/test_fit.py +++ b/fooof/tests/objs/test_fit.py @@ -391,7 +391,7 @@ def test_fooof_fit_failure(): ## Induce a runtime error, and check it runs through tfm = FOOOF(verbose=False) - tfm._maxfev = 5 + tfm._maxfev = 2 tfm.fit(*gen_power_spectrum([3, 50], [50, 2], [10, 0.5, 2, 20, 0.3, 4])) @@ -417,7 +417,7 @@ def test_fooof_debug(): """Test FOOOF in debug mode, including with fit failures.""" tfm = FOOOF(verbose=False) - tfm._maxfev = 5 + tfm._maxfev = 2 tfm.set_debug_mode(True) assert tfm._debug is True From f3f74d867be9ba74609a617b63ad7682b5092cd2 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 21:20:24 -0400 Subject: [PATCH 05/12] add jacobian funcs --- fooof/core/jacobians.py | 51 ++++++++++++++++++++++++++++++ fooof/tests/core/test_jacobians.py | 34 ++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 fooof/core/jacobians.py create mode 100644 fooof/tests/core/test_jacobians.py diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py new file mode 100644 index 00000000..47bd2f9f --- /dev/null +++ b/fooof/core/jacobians.py @@ -0,0 +1,51 @@ +""""Functions for computing Jacobian matrices to be used during fitting.""" + +import numpy as np + +################################################################################################### +################################################################################################### + +## Periodic fit functions + +def jacobian_gauss(xs, *params): + """Create the Jacobian matrix for the Guassian function.""" + + jacobians = [] + for a, b, c in zip(*[iter(params)] * 3): + + sub = b * np.exp((-(((-a + xs)**2) / (2 * c**2)))) + + jacobian = np.hstack([ + (sub * (-a + xs) / c**2).reshape(-1, 1), + np.exp(-(-a + xs)**2 / (2 * c**2)).reshape(-1, 1), + (sub * (-a + xs)**2 / c**3).reshape(-1, 1), + ]) + jacobians.append(jacobian) + + return np.hstack(jacobians) + + +## Aperiodic fit functions + +def jacobian_expo(xs, *params): + """Create the Jacobian matrix for the exponential function.""" + + a, b, c = params + jacobian = np.hstack([ + np.ones([len(xs), 1]), + - (1 / (b + xs**c)).reshape(-1, 1), + -((xs**c * np.log10(xs)) / (b + xs**c)).reshape(-1, 1), + ]) + + return jacobian + + +def jacobian_expo_nk(xs, *params): + """Create the Jacobian matrix for the exponential no-knee function.""" + + jacobian = np.hstack([ + np.ones([len(xs), 1]), + (-np.log10(xs) / np.log10(10)).reshape(-1, 1), + ]) + + return jacobian diff --git a/fooof/tests/core/test_jacobians.py b/fooof/tests/core/test_jacobians.py new file mode 100644 index 00000000..6b593c86 --- /dev/null +++ b/fooof/tests/core/test_jacobians.py @@ -0,0 +1,34 @@ +"""Tests for fooof.core.jacobians.""" + + +from fooof.core.jacobians import * + +################################################################################################### +################################################################################################### + +def test_jacobian_gauss(): + + xs = np.arange(1, 100) + ctr, hgt, wid = 50, 5, 10 + + jacobian = jacobian_gauss(xs, ctr, hgt, wid) + assert isinstance(jacobian, np.ndarray) + assert jacobian.shape == (len(xs), 3) + +def test_jacobian_expo(): + + xs = np.arange(1, 100) + off, knee, exp = 10, 5, 2 + + jacobian = jacobian_expo(xs, off, knee, exp) + assert isinstance(jacobian, np.ndarray) + assert jacobian.shape == (len(xs), 3) + +def test_jacobian_expo_nk(): + + xs = np.arange(1, 100) + off, exp = 10, 2 + + jacobian = jacobian_expo_nk(xs, off, exp) + assert isinstance(jacobian, np.ndarray) + assert jacobian.shape == (len(xs), 2) From 8fe511b8f0044b68e4d7d9aafd058924def2c696 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 21:26:04 -0400 Subject: [PATCH 06/12] update jacobian funcs docs --- fooof/core/jacobians.py | 54 +++++++++++++++++++++++++++--- fooof/tests/core/test_jacobians.py | 1 - 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py index 47bd2f9f..ea109537 100644 --- a/fooof/core/jacobians.py +++ b/fooof/core/jacobians.py @@ -1,4 +1,11 @@ -""""Functions for computing Jacobian matrices to be used during fitting.""" +""""Functions for computing Jacobian matrices to be used during fitting. + +Notes +----- +These functions line up with those in `funcs`. +The parameters in these functions are labelled {a, b, c, ...}, but follow the order in `funcs`. +These functions are designed to be passed into `curve_fit` to provide a computed Jacobian. +""" import numpy as np @@ -8,7 +15,20 @@ ## Periodic fit functions def jacobian_gauss(xs, *params): - """Create the Jacobian matrix for the Guassian function.""" + """Create the Jacobian matrix for the Guassian function. + + Parameters + ---------- + xs : 1d array + Input x-axis values. + *params : float + Parameters for the function. + + Returns + ------- + jacobian : 2d array + Jacobian matrix, with shape [len(xs), n_params]. + """ jacobians = [] for a, b, c in zip(*[iter(params)] * 3): @@ -28,7 +48,20 @@ def jacobian_gauss(xs, *params): ## Aperiodic fit functions def jacobian_expo(xs, *params): - """Create the Jacobian matrix for the exponential function.""" + """Create the Jacobian matrix for the exponential function. + + Parameters + ---------- + xs : 1d array + Input x-axis values. + *params : float + Parameters for the function. + + Returns + ------- + jacobian : 2d array + Jacobian matrix, with shape [len(xs), n_params]. + """ a, b, c = params jacobian = np.hstack([ @@ -41,7 +74,20 @@ def jacobian_expo(xs, *params): def jacobian_expo_nk(xs, *params): - """Create the Jacobian matrix for the exponential no-knee function.""" + """Create the Jacobian matrix for the exponential no-knee function. + + Parameters + ---------- + xs : 1d array + Input x-axis values. + *params : float + Parameters for the function. + + Returns + ------- + jacobian : 2d array + Jacobian matrix, with shape [len(xs), n_params]. + """ jacobian = np.hstack([ np.ones([len(xs), 1]), diff --git a/fooof/tests/core/test_jacobians.py b/fooof/tests/core/test_jacobians.py index 6b593c86..aae25aa7 100644 --- a/fooof/tests/core/test_jacobians.py +++ b/fooof/tests/core/test_jacobians.py @@ -1,6 +1,5 @@ """Tests for fooof.core.jacobians.""" - from fooof.core.jacobians import * ################################################################################################### From ce857ee1aae3e7848d4fbf5b59c0a4b47105f7e5 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 21:27:33 -0400 Subject: [PATCH 07/12] provide computed jacobian for gaussian fit --- fooof/objs/fit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index e5839c3b..d574d407 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -71,6 +71,7 @@ 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.jacobians import jacobian_gauss from fooof.core.errors import (FitError, NoModelError, DataError, NoDataError, InconsistentDataError) from fooof.core.strings import (gen_settings_str, gen_results_fm_str, @@ -1159,7 +1160,7 @@ def _fit_peak_guess(self, guess): gaussian_params, _ = curve_fit(gaussian_function, self.freqs, self._spectrum_flat, p0=guess, maxfev=self._maxfev, bounds=gaus_param_bounds, ftol=self._tol, xtol=self._tol, gtol=self._tol, - check_finite=False) + check_finite=False, jac=jacobian_gauss) except RuntimeError as excp: error_msg = ("Model fitting failed due to not finding " "parameters in the peak component fit.") From 90846d6f40583a8121679e906632599658453359 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 20 Aug 2023 22:45:38 -0400 Subject: [PATCH 08/12] add additional note for setting curve_fit tolerance --- fooof/objs/fit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index d574d407..074fdf23 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -201,6 +201,7 @@ def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, min_peak_h # The maximum number of calls to the curve fitting function self._maxfev = 5000 # The tolerance setting for curve fitting (see scipy.curve_fit - ftol / xtol / gtol) + # Here reduce tolerance to speed fitting. Set value to 1e-8 to match curve_fit default self._tol = 0.00001 ## RUN MODES From 5aa0837c22f30d31787fe5dd5e31083637fa0f11 Mon Sep 17 00:00:00 2001 From: Ryan Hammonds Date: Fri, 8 Sep 2023 11:22:57 -0700 Subject: [PATCH 09/12] optimize gaussian jacobian --- fooof/core/jacobians.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py index ea109537..41db9380 100644 --- a/fooof/core/jacobians.py +++ b/fooof/core/jacobians.py @@ -30,19 +30,25 @@ def jacobian_gauss(xs, *params): Jacobian matrix, with shape [len(xs), n_params]. """ - jacobians = [] - for a, b, c in zip(*[iter(params)] * 3): + jacobians = np.zeros((len(xs), len(params))) - sub = b * np.exp((-(((-a + xs)**2) / (2 * c**2)))) + for i, (a, b, c) in enumerate(zip(*[iter(params)] * 3)): - jacobian = np.hstack([ - (sub * (-a + xs) / c**2).reshape(-1, 1), - np.exp(-(-a + xs)**2 / (2 * c**2)).reshape(-1, 1), - (sub * (-a + xs)**2 / c**3).reshape(-1, 1), - ]) - jacobians.append(jacobian) + ax = -a + xs + ax2 = ax**2 - return np.hstack(jacobians) + c2 = c**2 + c3 = c**3 + + exp = np.exp(-ax2 / (2 * c2)) + exp_b = exp * b + + ii = i * 3 + jacobians[:, ii] = (exp_b * ax) / c2 + jacobians[:, ii+1] = exp + jacobians[:, ii+2] = (exp_b * ax2) / c3 + + return jacobians ## Aperiodic fit functions From 096286d3a3bf61346f07592eb4d86ff4db867006 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 11 Sep 2023 20:20:53 -0400 Subject: [PATCH 10/12] refactor ap jacobians, following ryans approach --- fooof/core/jacobians.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py index 41db9380..27483d69 100644 --- a/fooof/core/jacobians.py +++ b/fooof/core/jacobians.py @@ -70,11 +70,13 @@ def jacobian_expo(xs, *params): """ a, b, c = params - jacobian = np.hstack([ - np.ones([len(xs), 1]), - - (1 / (b + xs**c)).reshape(-1, 1), - -((xs**c * np.log10(xs)) / (b + xs**c)).reshape(-1, 1), - ]) + + xs_c = xs**c + b_xs_c = xs_c + b + + jacobian = np.ones((len(xs), len(params))) + jacobian[:, 1] = -1 / b_xs_c + jacobian[:, 2] = -(xs_c * np.log10(xs)) / b_xs_c return jacobian @@ -95,9 +97,7 @@ def jacobian_expo_nk(xs, *params): Jacobian matrix, with shape [len(xs), n_params]. """ - jacobian = np.hstack([ - np.ones([len(xs), 1]), - (-np.log10(xs) / np.log10(10)).reshape(-1, 1), - ]) + jacobian = np.ones((len(xs), len(params))) + jacobian[:, 1] = -np.log10(xs) return jacobian From aa64fe38407d1d06ee19d5c22a28252bc8b0f794 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 11 Sep 2023 20:33:58 -0400 Subject: [PATCH 11/12] fix typos & var consistency --- fooof/core/jacobians.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py index 27483d69..6e22308d 100644 --- a/fooof/core/jacobians.py +++ b/fooof/core/jacobians.py @@ -3,7 +3,7 @@ Notes ----- These functions line up with those in `funcs`. -The parameters in these functions are labelled {a, b, c, ...}, but follow the order in `funcs`. +The parameters in these functions are labeled {a, b, c, ...}, but follow the order in `funcs`. These functions are designed to be passed into `curve_fit` to provide a computed Jacobian. """ @@ -15,7 +15,7 @@ ## Periodic fit functions def jacobian_gauss(xs, *params): - """Create the Jacobian matrix for the Guassian function. + """Create the Jacobian matrix for the Gaussian function. Parameters ---------- @@ -30,7 +30,7 @@ def jacobian_gauss(xs, *params): Jacobian matrix, with shape [len(xs), n_params]. """ - jacobians = np.zeros((len(xs), len(params))) + jacobian = np.zeros((len(xs), len(params))) for i, (a, b, c) in enumerate(zip(*[iter(params)] * 3)): @@ -44,11 +44,11 @@ def jacobian_gauss(xs, *params): exp_b = exp * b ii = i * 3 - jacobians[:, ii] = (exp_b * ax) / c2 - jacobians[:, ii+1] = exp - jacobians[:, ii+2] = (exp_b * ax2) / c3 + jacobian[:, ii] = (exp_b * ax) / c2 + jacobian[:, ii+1] = exp + jacobian[:, ii+2] = (exp_b * ax2) / c3 - return jacobians + return jacobian ## Aperiodic fit functions From 965f2d1a43eff74838431981bd8797cc9c541936 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 11 Sep 2023 21:07:47 -0400 Subject: [PATCH 12/12] fix code headers --- fooof/core/jacobians.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fooof/core/jacobians.py b/fooof/core/jacobians.py index 6e22308d..4ff4b5e3 100644 --- a/fooof/core/jacobians.py +++ b/fooof/core/jacobians.py @@ -12,7 +12,7 @@ ################################################################################################### ################################################################################################### -## Periodic fit functions +## Periodic Jacobian functions def jacobian_gauss(xs, *params): """Create the Jacobian matrix for the Gaussian function. @@ -51,7 +51,7 @@ def jacobian_gauss(xs, *params): return jacobian -## Aperiodic fit functions +## Aperiodic Jacobian functions def jacobian_expo(xs, *params): """Create the Jacobian matrix for the exponential function.