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

[WIP] - Add explicit {aperiodic, periodic} 'Modes' support #298

Open
wants to merge 23 commits into
base: basemodel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
26 changes: 16 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,35 @@
Spectral Parameterization
=========================

|ProjectStatus|_ |Version|_ |BuildStatus|_ |Coverage|_ |License|_ |PythonVersions|_ |Paper|_
|ProjectStatus| |Version| |BuildStatus| |Coverage| |License| |PythonVersions| |Publication|

.. |ProjectStatus| image:: http://www.repostatus.org/badges/latest/active.svg
.. _ProjectStatus: https://www.repostatus.org/#active
:target: https://www.repostatus.org/#active
:alt: project status

.. |Version| image:: https://img.shields.io/pypi/v/fooof.svg
.. _Version: https://pypi.python.org/pypi/fooof/
:target: https://pypi.python.org/pypi/fooof/
:alt: version

.. |BuildStatus| image:: https://github.com/fooof-tools/fooof/actions/workflows/build.yml/badge.svg
.. _BuildStatus: https://github.com/fooof-tools/fooof/actions/workflows/build.yml
:target: https://github.com/fooof-tools/fooof/actions/workflows/build.yml
:alt: build status

.. |Coverage| image:: https://codecov.io/gh/fooof-tools/fooof/branch/main/graph/badge.svg
.. _Coverage: https://codecov.io/gh/fooof-tools/fooof
:target: https://codecov.io/gh/fooof-tools/fooof
:alt: coverage

.. |License| image:: https://img.shields.io/pypi/l/fooof.svg
.. _License: https://opensource.org/licenses/Apache-2.0
:target: https://opensource.org/licenses/Apache-2.0
:alt: license

.. |PythonVersions| image:: https://img.shields.io/pypi/pyversions/fooof.svg
.. _PythonVersions: https://pypi.python.org/pypi/fooof/

.. |Paper| image:: https://img.shields.io/badge/paper-nn10.1038-informational.svg
.. _Paper: https://doi.org/10.1038/s41593-020-00744-x
:target: https://pypi.python.org/pypi/fooof/
:alt: python versions

.. |Publication| image:: https://img.shields.io/badge/paper-nn10.1038-informational.svg
:target: https://doi.org/10.1038/s41593-020-00744-x
:alt: publication

Spectral parameterization (`specparam`, formerly `fooof`) is a fast, efficient, and physiologically-informed tool to parameterize neural power spectra.

Expand Down
74 changes: 66 additions & 8 deletions specparam/core/funcs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
"""Functions that can be used for model fitting.

NOTES
-----
- Model fitting currently (only) uses the exponential and gaussian functions.
- Linear & Quadratic functions are from previous versions.
- They are left available for easy swapping back in, if desired.
"""
"""Functions that can be used for model fitting."""

import numpy as np
from scipy.special import erf

from specparam.core.utils import normalize
from specparam.core.errors import InconsistentDataError

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

## PEAK FUNCTIONS

def gaussian_function(xs, *params):
"""Gaussian fitting function.

Expand All @@ -39,6 +36,37 @@ def gaussian_function(xs, *params):
return ys


def skewnorm_function(xs, *params):
"""Skewed normal distribution fitting function.

Parameters
----------
xs : 1d array
Input x-axis values.
*params : float
Parameters that define the skewed normal distribution function.

Returns
-------
ys : 1d array
Output values for skewed normal distribution function.
"""

ys = np.zeros_like(xs)

for ii in range(0, len(params), 4):

ctr, hgt, wid, skew = params[ii:ii+4]

ts = (xs - ctr) / wid
temp = 2 / wid * (1 / np.sqrt(2 * np.pi) * np.exp(-ts**2 / 2)) * \
((1 + erf(skew * ts / np.sqrt(2))) / 2)
ys = ys + hgt * normalize(temp)

return ys

## APERIODIC FUNCTIONS

def expo_function(xs, *params):
"""Exponential fitting function, for fitting aperiodic component with a 'knee'.

Expand Down Expand Up @@ -89,6 +117,34 @@ def expo_nk_function(xs, *params):
return ys


def double_expo_function(xs, *params):
"""Double exponential fitting function, for fitting aperiodic component with two exponents and a knee.

NOTE: this function requires linear frequency (not log).

Parameters
----------
xs : 1d array
Input x-axis values.
*params : float
Parameters (offset, exp0, knee, exp1) that define the function:
y = 10^offset * (1/((x**exp0) * (knee + x^exp1))

Returns
-------
ys : 1d array
Output values for exponential function.
"""

ys = np.zeros_like(xs)

offset, exp0, knee, exp1 = params

ys = ys + offset - np.log10((xs**exp0) * (knee + xs**exp1))

return ys


def linear_function(xs, *params):
"""Linear fitting function.

Expand Down Expand Up @@ -133,6 +189,8 @@ def quadratic_function(xs, *params):
return ys


## GETTER FUNCTIONS

def get_pe_func(periodic_mode):
"""Select and return specified function for periodic component.

Expand Down
6 changes: 6 additions & 0 deletions specparam/core/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def get_ap_indices(aperiodic_mode):
Mapping of the column labels and indices for the aperiodic parameters.
"""

# TEMP / TEST:
aperiodic_mode = str(aperiodic_mode)

if aperiodic_mode == 'fixed':
labels = ('offset', 'exponent')
elif aperiodic_mode == 'knee':
Expand All @@ -101,6 +104,9 @@ def get_indices(aperiodic_mode):
Mapping of the column labels and indices for all parameters.
"""

# TEMP / TEST:
aperiodic_mode = str(aperiodic_mode)

# Get the periodic indices, and then update dictionary with aperiodic ones
indices = get_peak_indices()
indices.update(get_ap_indices(aperiodic_mode))
Expand Down
7 changes: 4 additions & 3 deletions specparam/core/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def gen_model_results_str(model, concise=False):

# Aperiodic parameters
('Aperiodic Parameters (offset, ' + \
('knee, ' if model.aperiodic_mode == 'knee' else '') + \
('knee, ' if str(model.aperiodic_mode) == 'knee' else '') + \
'exponent): '),
', '.join(['{:2.4f}'] * len(model.aperiodic_params_)).format(*model.aperiodic_params_),
'',
Expand Down Expand Up @@ -357,7 +357,7 @@ def gen_group_results_str(group, concise=False):
errors = group.get_params('error')
exps = group.get_params('aperiodic_params', 'exponent')
kns = group.get_params('aperiodic_params', 'knee') \
if group.aperiodic_mode == 'knee' else np.array([0])
if str(group.aperiodic_mode) == 'knee' else np.array([0])

str_lst = [

Expand All @@ -380,12 +380,13 @@ def gen_group_results_str(group, concise=False):

# Aperiodic parameters - knee fit status, and quick exponent description
'Power spectra were fit {} a knee.'.format(\
'with' if group.aperiodic_mode == 'knee' else 'without'),
'with' if str(group.aperiodic_mode) == 'knee' else 'without'),
'',
'Aperiodic Fit Values:',
*[el for el in [' Knees - Min: {:6.2f}, Max: {:6.2f}, Mean: {:5.2f}'
.format(*compute_arr_desc(kns)),
] if group.aperiodic_mode == 'knee'],

'Exponents - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'
.format(*compute_arr_desc(exps)),
'',
Expand Down
33 changes: 26 additions & 7 deletions specparam/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,32 @@ def unlog(arr, base=10):
return np.power(base, arr)


def group_three(vec):
"""Group an array of values into threes.
def normalize(data):
"""Normalize an array of numerical data (to the range of 0-1).

Parameters
----------
data : np.ndarray
Array of data to normalize.

Returns
-------
np.ndarray
Normalized data.
"""

return (data - np.min(data)) / (np.max(data) - np.min(data))


def groupby(vec, groupby):
"""Group an array of values by a specified number.

Parameters
----------
vec : list or 1d array
List or array of items to group by 3. Length of array must be divisible by three.
num : int
Number to group by.

Returns
-------
Expand All @@ -43,17 +62,17 @@ def group_three(vec):
Raises
------
ValueError
If input data cannot be evenly grouped into threes.
If input data cannot be evenly grouped into specified number.
"""

if len(vec) % 3 != 0:
if len(vec) % groupby != 0:
raise ValueError("Wrong size array to group by three.")

# Reshape, if an array, as it's faster, otherwise asssume lise
# Reshape, if an array, as it's faster, otherwise assume list
if isinstance(vec, np.ndarray):
return np.reshape(vec, (-1, 3))
return np.reshape(vec, (-1, groupby))
else:
return [list(vec[ii:ii+3]) for ii in range(0, len(vec), 3)]
return [list(vec[ii:ii+groupby]) for ii in range(0, len(vec), groupby)]


def nearest_ind(array, value):
Expand Down
Loading