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

[MNT] - Optimizations for curve fit #299

Merged
merged 13 commits into from
Sep 13, 2023
24 changes: 5 additions & 19 deletions fooof/core/funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -60,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

Expand All @@ -88,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

Expand All @@ -113,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

Expand All @@ -138,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

Expand Down
97 changes: 97 additions & 0 deletions fooof/core/jacobians.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
""""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

###################################################################################################
###################################################################################################

## Periodic fit functions

def jacobian_gauss(xs, *params):
"""Create the Jacobian matrix for the Guassian function.
TomDonoghue marked this conversation as resolved.
Show resolved Hide resolved

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):

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)
ryanhammonds marked this conversation as resolved.
Show resolved Hide resolved

return np.hstack(jacobians)


## Aperiodic fit functions

def jacobian_expo(xs, *params):
"""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([
np.ones([len(xs), 1]),
- (1 / (b + xs**c)).reshape(-1, 1),
-((xs**c * np.log10(xs)) / (b + xs**c)).reshape(-1, 1),
TomDonoghue marked this conversation as resolved.
Show resolved Hide resolved
])

return jacobian


def jacobian_expo_nk(xs, *params):
"""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]),
(-np.log10(xs) / np.log10(10)).reshape(-1, 1),
TomDonoghue marked this conversation as resolved.
Show resolved Hide resolved
])

return jacobian
22 changes: 17 additions & 5 deletions fooof/objs/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -192,12 +193,17 @@ 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)
# Here reduce tolerance to speed fitting. Set value to 1e-8 to match curve_fit default
self._tol = 0.00001

## RUN MODES
# Set default debug mode - controls if an error is raised if model fitting is unsuccessful
self._debug = False
Expand Down Expand Up @@ -946,7 +952,9 @@ 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,
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 "
"the simple aperiodic component fit.")
Expand Down Expand Up @@ -1003,7 +1011,9 @@ 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,
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 the robust aperiodic fit.")
Expand Down Expand Up @@ -1149,7 +1159,9 @@ 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,
ftol=self._tol, xtol=self._tol, gtol=self._tol,
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.")
Expand Down
33 changes: 33 additions & 0 deletions fooof/tests/core/test_jacobians.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""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)
4 changes: 2 additions & 2 deletions fooof/tests/objs/test_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand All @@ -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
Expand Down