Skip to content

Commit c2a4687

Browse files
authored
Merge pull request #383 from fooof-tools/peakh
[ENH/WIP] - Add general support for parameter conversions
2 parents f2f771b + b6b96ce commit c2a4687

28 files changed

+1266
-86
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
"""
2+
Custom Parameter Conversions
3+
============================
4+
5+
This example covers defining and using custom parameter post-fitting conversions.
6+
"""
7+
8+
from specparam import SpectralModel
9+
10+
from specparam.utils.download import load_example_data
11+
12+
# Import the default set of parameter conversions
13+
from specparam.convert.definitions import check_converters, DEFAULT_CONVERTERS
14+
15+
# Import objects to define parameter conversions
16+
from specparam.convert.converter import PeriodicParamConverter, AperiodicParamConverter
17+
18+
###################################################################################################
19+
# Parameter Conversions
20+
# ---------------------
21+
#
22+
# After model fitting, a model object includes the parameters for the model as defined by the
23+
# fit modes and as arrived at by the fit algorithm. These fit parameters define the model fit,
24+
# as visualized, for example, by the 'full model' fit, when plotting the model.
25+
#
26+
# However, these 'fit' parameters are not necessarily defined in a way that we actually
27+
# want to analyzed. For this reason, spectral parameterization supports doing post-fitting
28+
# parameter conversions, whereby after the fitting process, conversions can be applied to
29+
# the fit parameters.
30+
#
31+
# Let's first explore this with an example model fit.
32+
#
33+
34+
###################################################################################################
35+
36+
# Load example spectra
37+
freqs = load_example_data('freqs.npy', folder='data')
38+
powers = load_example_data('spectrum.npy', folder='data')
39+
40+
# Define fitting fit range
41+
freq_range = [2, 40]
42+
43+
# Initialize and fit an example model
44+
fm = SpectralModel()
45+
fm.report(freqs, powers, freq_range)
46+
47+
###################################################################################################
48+
#
49+
# In the above, we see the model fit, and reported parameter values.
50+
#
51+
# Let's further investigate the different versions of the parameters: 'fit' and 'converted'.
52+
#
53+
54+
###################################################################################################
55+
56+
# Check the aperiodic fit & converted parameters
57+
print(fm.results.get_params('aperiodic', version='fit'))
58+
print(fm.results.get_params('aperiodic', version='converted'))
59+
60+
###################################################################################################
61+
#
62+
# In the above, we can see that there are fit parameters, but there is no defined converted
63+
# version of the parameters, indicating that there are no conversions defined for the
64+
# aperiodic parameters.
65+
#
66+
67+
###################################################################################################
68+
69+
# Check the periodic fit & converted parameters, for an example peak
70+
print(fm.results.get_params('periodic', version='fit')[1, :])
71+
print(fm.results.get_params('periodic', version='converted')[1, :])
72+
73+
###################################################################################################
74+
#
75+
# In this case, there are both fit and converted versions of the parameters,
76+
# and they are not the same!
77+
#
78+
# There are defined periodic parameter conversions that are being done. Note also that it is
79+
# the converted versions of the parameters that are printed in the report above.
80+
#
81+
82+
###################################################################################################
83+
# Default Converters
84+
# ------------------
85+
#
86+
# To see what the conversions are that are being defined, we can examine the set of
87+
# DEFAULT_CONVERTERS, which we imported from the module.
88+
#
89+
90+
###################################################################################################
91+
92+
# Check the default model fit parameters
93+
DEFAULT_CONVERTERS
94+
95+
###################################################################################################
96+
# Change Default Converters
97+
# ~~~~~~~~~~~~~~~~~~~~~~~~~
98+
#
99+
# Next, we can explore changing which converters we use.
100+
#
101+
# To start with a simple example, let's turn off all parameter conversions.
102+
#
103+
# Note that as a shortcut, we can get a parameter definition from the Modes sub-object that
104+
# is part of the model object, specified to return a dictionary.
105+
#
106+
107+
###################################################################################################
108+
109+
# Get a dictionary representation of current parameters
110+
null_converters = fm.modes.get_params('dict')
111+
null_converters
112+
113+
###################################################################################################
114+
115+
# Initialize & fit a new model with null converters
116+
fm1 = SpectralModel(converters=null_converters)
117+
fm1.report(freqs, powers, freq_range)
118+
119+
###################################################################################################
120+
#
121+
# In the above no parameter conversions were applied!
122+
#
123+
124+
###################################################################################################
125+
126+
# Check that there are no converted parameters - should all be nan
127+
print(fm1.results.get_params('aperiodic', version='converted'))
128+
print(fm1.results.get_params('periodic', version='converted'))
129+
130+
###################################################################################################
131+
#
132+
# Next, we can explore specifying to use different built in parameter conversions.
133+
#
134+
# To do so, we can explore the available options with the
135+
# :func:`~specparam.convert.definitions.check_converters` function.
136+
#
137+
138+
###################################################################################################
139+
140+
# Check the available aperiodic parameter converters
141+
check_converters('aperiodic')
142+
143+
###################################################################################################
144+
145+
# Check the available periodic parameter converters
146+
check_converters('periodic')
147+
148+
###################################################################################################
149+
#
150+
# Now we can select different conversions from these options.
151+
#
152+
153+
###################################################################################################
154+
155+
# Take a copy of the null converters dictionary
156+
selected_converters = null_converters.copy()
157+
158+
# Specify a different
159+
selected_converters['periodic']['pw'] = 'lin_sub'
160+
161+
###################################################################################################
162+
163+
# Initialize & fit a new model with selected converters
164+
fm2 = SpectralModel(converters=selected_converters)
165+
fm2.report(freqs, powers, freq_range)
166+
167+
###################################################################################################
168+
#
169+
# In the above, the converted and reported parameter outputs used the specified conversions!
170+
#
171+
172+
###################################################################################################
173+
# Create Custom Converters
174+
# ------------------------
175+
#
176+
# Finally, let's explore defining some custom parameter conversions.
177+
#
178+
# To do so, for any parameter that we wish to define a conversion for, we can define a
179+
# callable that implements our desired conversion.
180+
#
181+
# In order for specparam to be able to use the callable, they must follow properties:
182+
#
183+
# - for aperiodic component conversions : callable should accept inputs `fit_value` and `model`
184+
# - for periodic component conversions: callable should accept inputs `fit_value`, `model`, and `peak_ind`
185+
#
186+
187+
###################################################################################################
188+
189+
# Take a copy of the null converters dictionary
190+
custom_converters = null_converters.copy()
191+
192+
###################################################################################################
193+
#
194+
# To start with, let's define a simple conversion for the aperiodic exponent to convert the
195+
# fit value into the equivalent spectral slope value (the negative of the exponent value).
196+
#
197+
# To define this simple conversion we can even use a lambda function.
198+
#
199+
200+
###################################################################################################
201+
202+
# Create a custom exponent converter as a lambda function
203+
custom_converters['aperiodic']['exponent'] = lambda param, model : -param
204+
205+
###################################################################################################
206+
#
207+
# Let's also define a conversion for a periodic parameter. As an example, we can define a
208+
# conversion of the fit center frequency value that finds and update to the closest frequency
209+
# value that actually occurs in the frequency definition. For this case, we will implement
210+
# conversion function.
211+
#
212+
213+
###################################################################################################
214+
215+
# Import utility function to find nearest index
216+
from specparam.utils.select import nearest_ind
217+
218+
# Define a function to update the center frequency
219+
def update_cf(fit_value, model, peak_ind):
220+
"""Updates center frequency to be closest existing frequency value."""
221+
222+
f_ind = nearest_ind(model.data.freqs, fit_value)
223+
new_cf = model.data.freqs[f_ind]
224+
225+
return new_cf
226+
227+
###################################################################################################
228+
229+
# Add the custom cf converter function to function collection
230+
custom_converters['periodic']['cf'] = update_cf
231+
232+
###################################################################################################
233+
#
234+
# Now we have defined our custom converters, we can use them in the fitting process!
235+
#
236+
237+
###################################################################################################
238+
239+
# Initialize & fit a new model with custom converters
240+
fm3 = SpectralModel(converters=custom_converters)
241+
fm3.report(freqs, powers, freq_range)
242+
243+
###################################################################################################
244+
#
245+
# In the above report, our custom parameter conversions were used.
246+
#
247+
248+
###################################################################################################
249+
# Parameter Converter Objects
250+
# ---------------------------
251+
#
252+
# In the above, we defined custom parameter converters by directly passing in callables that
253+
# implement our desired conversions. As we've seen above, this works to pass in conversions
254+
#
255+
# However, only passing in the callable is a bit light on details and description. If you
256+
# want to implement parameter conversions using an approach that keeps track of additional
257+
# description of the approach, you can use the
258+
# :class:`~specparam.convert.converter.AperiodicParamConverter` and
259+
# :class:`~specparam.convert.converter.PeriodicParamConverter` objects to
260+
#
261+
262+
###################################################################################################
263+
264+
# Define the exponent to slope conversion as a converter object
265+
exp_slope_converter = AperiodicParamConverter(
266+
parameter='exponent',
267+
name='slope',
268+
description='Convert the fit exponent value to the equivalent spectral slope value.',
269+
function=lambda param, model : -param,
270+
)
271+
272+
# Define the center frequency fixed frequency converter as a converter object
273+
cf_fixed_freq_converter = PeriodicParamConverter(
274+
parameter='cf',
275+
name='fixed_freq',
276+
description='Convert the fit center frequency value to a fixed frequency value.',
277+
function=update_cf,
278+
)
279+
280+
###################################################################################################
281+
282+
# Take a new copy of the null converters dictionary & add
283+
custom_converters2 = null_converters.copy()
284+
custom_converters['aperiodic']['exponent'] = exp_slope_converter
285+
custom_converters2['periodic']['cf'] = cf_fixed_freq_converter
286+
287+
###################################################################################################
288+
#
289+
# Same as before, we can now use our custom converter definitions in the model fitting process.
290+
#
291+
292+
###################################################################################################
293+
294+
# Initialize & fit a new model with custom converters
295+
fm4 = SpectralModel(converters=custom_converters2)
296+
fm4.report(freqs, powers, freq_range)
297+
298+
###################################################################################################
299+
# Adding New Parameter Conversions to the Module
300+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
301+
#
302+
# As a final note, if you look into the set of 'built-in' parameter conversions that are
303+
# available within the module, you will see that these are defined in the same way as done here,
304+
# using the conversion objects introduced above. The only difference is that they are defined
305+
# within the module and therefore can be accessed via their name, as a shortcut,
306+
# rather than the user having to pass in their own full definitions.
307+
#
308+
# This also means that if you have a custom parameter conversion that you think would be of
309+
# interest to other specparam users, once the ParamConverter object is defined it is quite
310+
# easy to add this to the module as a new default option. If you would be interested in
311+
# suggesting a mode be added to the module, feel free to open an issue and/or pull request.
312+
#

examples/customize/plot_sub_objects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def print_public_api(obj):
259259
###################################################################################################
260260

261261
# Initialize a base model, passing in empty mode definitions
262-
base = BaseModel(None, None, False)
262+
base = BaseModel(None, None, None, False)
263263

264264
# Check the API of the object
265265
print_public_api(base)

0 commit comments

Comments
 (0)