-
Notifications
You must be signed in to change notification settings - Fork 112
Expand file tree
/
Copy pathutils.py
More file actions
227 lines (173 loc) · 7.66 KB
/
utils.py
File metadata and controls
227 lines (173 loc) · 7.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
"""Utility functions for managing and manipulating FOOOF objects."""
import numpy as np
from fooof.sim import gen_freqs
from fooof.data import FOOOFResults
from fooof.objs import FOOOF, FOOOFGroup
from fooof.analysis.periodic import get_band_peak_fg
from fooof.core.errors import NoModelError, IncompatibleSettingsError
###################################################################################################
###################################################################################################
def compare_info(fooof_lst, aspect):
"""Compare a specified aspect of FOOOF objects across instances.
Parameters
----------
fooof_lst : list of FOOOF and / or FOOOFGroup
Objects whose attributes are to be compared.
aspect : {'settings', 'meta_data'}
Which set of attributes to compare the objects across.
Returns
-------
consistent : bool
Whether the settings are consistent across the input list of objects.
"""
# Check specified aspect of the objects are the same across instances
for f_obj_1, f_obj_2 in zip(fooof_lst[:-1], fooof_lst[1:]):
if getattr(f_obj_1, 'get_' + aspect)() != getattr(f_obj_2, 'get_' + aspect)():
consistent = False
break
else:
consistent = True
return consistent
def average_fg(fg, bands, avg_method='mean', regenerate=True):
"""Average across model fits in a FOOOFGroup object.
Parameters
----------
fg : FOOOFGroup
Object with model fit results to average across.
bands : Bands
Bands object that defines the frequency bands to collapse peaks across.
avg : {'mean', 'median'}
Averaging function to use.
regenerate : bool, optional, default: True
Whether to regenerate the model for the averaged parameters.
Returns
-------
fm : FOOOF
Object containing the average model results.
Raises
------
ValueError
If the requested averaging method is not understood.
NoModelError
If there are no model fit results available to average across.
"""
if avg_method not in ['mean', 'median']:
raise ValueError("Requested average method not understood.")
if not fg.has_model:
raise NoModelError("No model fit results are available, can not proceed.")
if avg_method == 'mean':
avg_func = np.nanmean
elif avg_method == 'median':
avg_func = np.nanmedian
# Aperiodic parameters: extract & average
ap_params = avg_func(fg.get_params('aperiodic_params'), 0)
# Periodic parameters: extract & average
peak_params = []
gauss_params = []
for band_def in bands.definitions:
peaks = get_band_peak_fg(fg, band_def, attribute='peak_params')
gauss = get_band_peak_fg(fg, band_def, attribute='gaussian_params')
# Check if there are any extracted peaks - if not, don't add
# Note that we only check peaks, but gauss should be the same
if not np.all(np.isnan(peaks)):
peak_params.append(avg_func(peaks, 0))
gauss_params.append(avg_func(gauss, 0))
peak_params = np.array(peak_params)
gauss_params = np.array(gauss_params)
# Goodness of fit measures: extract & average
r2 = avg_func(fg.get_params('r_squared'))
error = avg_func(fg.get_params('error'))
# Collect all results together, to be added to FOOOF object
results = FOOOFResults(ap_params, peak_params, r2, error, gauss_params)
# Create the new FOOOF object, with settings, data info & results
fm = FOOOF()
fm.add_settings(fg.get_settings())
fm.add_meta_data(fg.get_meta_data())
fm.add_results(results)
# Generate the average model from the parameters
if regenerate:
fm._regenerate_model()
return fm
def combine_fooofs(fooofs):
"""Combine a group of FOOOF and/or FOOOFGroup objects into a single FOOOFGroup object.
Parameters
----------
fooofs : list of FOOOF or FOOOFGroup
Objects to be concatenated into a FOOOFGroup.
Returns
-------
fg : FOOOFGroup
Resultant object from combining inputs.
Raises
------
IncompatibleSettingsError
If the input objects have incompatible settings for combining.
Examples
--------
Combine FOOOF objects together (where `fm1`, `fm2` & `fm3` are assumed to be defined and fit):
>>> fg = combine_fooofs([fm1, fm2, fm3]) # doctest:+SKIP
Combine FOOOFGroup objects together (where `fg1` & `fg2` are assumed to be defined and fit):
>>> fg = combine_fooofs([fg1, fg2]) # doctest:+SKIP
"""
# Compare settings
if not compare_info(fooofs, 'settings') or not compare_info(fooofs, 'meta_data'):
raise IncompatibleSettingsError("These objects have incompatible settings "
"or meta data, and so cannot be combined.")
# Initialize FOOOFGroup object, with settings derived from input objects
fg = FOOOFGroup(*fooofs[0].get_settings(), verbose=fooofs[0].verbose)
# Use a temporary store to collect spectra, as we'll only add it if it is consistently present
# We check how many frequencies by accessing meta data, in case of no frequency vector
meta_data = fooofs[0].get_meta_data()
n_freqs = len(gen_freqs(meta_data.freq_range, meta_data.freq_res))
temp_power_spectra = np.empty([0, n_freqs])
# Add FOOOF results from each FOOOF object to group
for f_obj in fooofs:
# Add FOOOFGroup object
if isinstance(f_obj, FOOOFGroup):
fg.group_results.extend(f_obj.group_results)
if f_obj.power_spectra is not None:
temp_power_spectra = np.vstack([temp_power_spectra, f_obj.power_spectra])
# Add FOOOF object
else:
fg.group_results.append(f_obj.get_results())
if f_obj.power_spectrum is not None:
temp_power_spectra = np.vstack([temp_power_spectra, f_obj.power_spectrum])
# If the number of collected power spectra is consistent, then add them to object
if len(fg) == temp_power_spectra.shape[0]:
fg.power_spectra = temp_power_spectra
# Set the check data mode, as True if any of the inputs have it on, False otherwise
fg.set_check_data_mode(any([getattr(f_obj, '_check_data') for f_obj in fooofs]))
# Add data information information
fg.add_meta_data(fooofs[0].get_meta_data())
return fg
def fit_fooof_3d(fg, freqs, power_spectra, freq_range=None, n_jobs=1):
"""Fit FOOOF models across a 3d array of power spectra.
Parameters
----------
fg : FOOOFGroup
Object to fit with, initialized with desired settings.
freqs : 1d array
Frequency values for the power spectra, in linear space.
power_spectra : 3d array
Power values, in linear space, with shape as: [n_conditions, n_power_spectra, n_freqs].
freq_range : list of [float, float], optional
Desired frequency range to fit. If not provided, fits the entire given range.
n_jobs : int, optional, default: 1
Number of jobs to run in parallel.
1 is no parallelization. -1 uses all available cores.
Returns
-------
fgs : list of FOOOFGroups
Collected FOOOFGroups after fitting across power spectra, length of n_conditions.
Examples
--------
Fit a 3d array of power spectra, assuming `freqs` and `spectra` are already defined:
>>> from fooof import FOOOFGroup
>>> fg = FOOOFGroup(peak_width_limits=[1, 6], min_peak_height=0.1)
>>> fgs = fit_fooof_3d(fg, freqs, power_spectra, freq_range=[3, 30]) # doctest:+SKIP
"""
fgs = []
for cond_spectra in power_spectra:
fg.fit(freqs, cond_spectra, freq_range, n_jobs)
fgs.append(fg.copy())
return fgs