Skip to content

Commit 89e37c2

Browse files
committed
add custom modes example
1 parent ebb5e2d commit 89e37c2

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
"""
2+
Custom Modes
3+
============
4+
5+
This example covers defining and using custom fit modes.
6+
"""
7+
8+
# sphinx_gallery_thumbnail_number = 3
9+
10+
import numpy as np
11+
12+
# Import the model object
13+
from specparam import SpectralModel
14+
15+
# Import function to simulate a power spectrum
16+
from specparam.sim import sim_power_spectrum
17+
18+
# Import OrderedDict, used in mode definition
19+
from collections import OrderedDict
20+
21+
# Import objects used to define a custom mode
22+
from specparam.modes.mode import Mode
23+
from specparam.modes.params import ParamDefinition
24+
25+
###################################################################################################
26+
# Defining Custom Fit Modes
27+
# -------------------------
28+
#
29+
# As covered in the tutorials, the specparam module has a set of predefined fit modes, wherein
30+
# `modes` refer to the fit function and associated metadata & description that describes how
31+
# each component is fit.
32+
#
33+
# In this tutorial, we will explore how you can also define your own custom modes.
34+
#
35+
# To do so, we will start by simulating an example power spectrum to use for this example.
36+
#
37+
38+
###################################################################################################
39+
40+
# Define simulation parameters
41+
ap_params = [0, 1]
42+
gauss_params = [10, 0.5, 2, 20, 0.3, 2]
43+
nlv = 0.0025
44+
45+
# Simulate an example power spectrum
46+
freqs, powers = sim_power_spectrum(\
47+
[1, 40], {'fixed' : ap_params}, {'gaussian' : gauss_params}, nlv, freq_res=0.25)
48+
49+
###################################################################################################
50+
# Example: Custom Aperiodic Mode
51+
# ------------------------------
52+
#
53+
# To start, we will define a custom aperiodic fit mode. Specifically, we will define a
54+
# custom aperiodic fit mode that fits only a single exponent.
55+
#
56+
# In theory, such an aperiodic fit mode could be used if one knew that the offset for all
57+
# power spectra of interest had an offset of 0 (for example, if they were all normalized as such),
58+
# but in practice this is not likely to be a useful fit mode, and is used here merely as
59+
# a simplified example of defining a custom fit mode. That is, while this is a functioning
60+
# custom fit mode definition for the sake of an example, this is not necessarily a useful
61+
# (or recommended) fit function.
62+
#
63+
# First, we need to define a fit function that can be applied to fit our component.
64+
#
65+
# To do so, we define a function that defines the fit function. To be able to be used in a
66+
# model object, this function needs to follow a couple key principles:
67+
#
68+
# - the first input should be for x-axis values of the data to be modeled (frequencies)
69+
# - regardless of how many parameters the fit function has, these should be organized as *args
70+
# in the function definition
71+
# - the body of function should unpack the params, fit the function definition to the input
72+
# data, and then return the model result (modeled component)
73+
#
74+
# By following these conventions, the model object is able to use this function to fit the
75+
# specified component, passing in data and parameters appropriately, regardless of the
76+
# set of parameters and function definition.
77+
#
78+
79+
###################################################################################################
80+
81+
# Define a custom aperiodic fit function
82+
def expo_only_function(xs, *params):
83+
"""Exponent only aperiodic fit function
84+
85+
Parameters
86+
----------
87+
xs : 1d array
88+
X-axis values.
89+
*params : float
90+
Parameters that define the component fit.
91+
92+
Returns
93+
-------
94+
ys : 1d array
95+
Output values of the fit.
96+
"""
97+
98+
exp = params
99+
ys = np.log10(xs**exp)
100+
101+
return ys
102+
103+
###################################################################################################
104+
#
105+
# Now that we have the fit function defined, we need to collect some additional information
106+
# to be able to use our fit mode in the model object, starting with the parameter definition.
107+
#
108+
# In order for the model object to have a description of the parameters that define the fit
109+
# mode, we use the :class:`~specparam.modes.params.ParamDefinition` object.
110+
#
111+
# To use this object, we use an OrderedDict to define a parameter description, where each key
112+
# is a parameter name (this being the name that will be used to access the parameter from the
113+
# model object), and a description of the parameter. Note that by using an OrderedDict, we
114+
# ensure that the order of the parameters is consistent. Make sure the order of the parameters
115+
# in the definition matches the order of the parameters in the fit function.
116+
#
117+
118+
###################################################################################################
119+
120+
# Define the parameter definition for the expo only fit mode
121+
expo_only_params = ParamDefinition(OrderedDict({
122+
'expo' : 'Exponent of the aperiodic component.',
123+
}))
124+
125+
###################################################################################################
126+
#
127+
# Now we have the main elements of our new fit mode, we can use the
128+
# :class:`~specparam.modes.mode.Mode` object to define the full mode. To do so, we initialize
129+
# a Mode object passing in metadata about the fit mode, as well our our fit function and
130+
# parameter definition.
131+
#
132+
# Note that there is some meta-data that needs to be defined in the Mode definition, including:
133+
#
134+
# - name, component, description: describes the fit mode
135+
# - jacobian: a function that computes the Jacobian for the fit function,
136+
# which in some cases can speed up fitting.
137+
# - ndim: the dimensionality of the output parameters, which should typically be 1 for
138+
# aperiodic modes (returns a 1d array of parameters per model fit) and 2d for peak parameters
139+
# (returns a 2d array of parameters across potentially multiple detected peaks)
140+
# - freqs_space, powers_space : the expected spacing of the data
141+
#
142+
143+
###################################################################################################
144+
145+
# Define the custom exponent only fit mode
146+
expo_only_mode = Mode(
147+
name='custom_expo_only',
148+
component='aperiodic',
149+
description='Fit an exponent only.',
150+
func=expo_only_function,
151+
jacobian=None,
152+
params=expo_only_params,
153+
ndim=1,
154+
freq_space='linear',
155+
powers_space='log10',
156+
)
157+
158+
###################################################################################################
159+
#
160+
# Our custom fit mode if now defined!
161+
#
162+
# The use this fit mode, we initialize a model object, passing the fit mode in as the specified
163+
# component mode.
164+
#
165+
166+
###################################################################################################
167+
168+
# Initialize model object, passing in custom aperiodic mode definition
169+
fm = SpectralModel(aperiodic_mode=expo_only_mode)
170+
171+
###################################################################################################
172+
#
173+
# Now we can use the model object to fit some data.
174+
#
175+
176+
###################################################################################################
177+
178+
# Fit model and report results
179+
fm.report(freqs, powers)
180+
181+
###################################################################################################
182+
#
183+
# In the above report we can see that under the aperiodic mode section, the results reflect
184+
# our custom fit mode!
185+
#
186+
# Note that the parameter name is taken from what we described in the parameter definition. This
187+
# is also the name you use to access the parameter results in `get_params`.
188+
#
189+
190+
###################################################################################################
191+
192+
# Get the aperiodic parameters, using the custom fit mode parameter name
193+
fm.get_params('aperiodic', 'expo')
194+
195+
###################################################################################################
196+
# Example Custom Periodic Fit Mode
197+
# --------------------------------
198+
#
199+
# Defining a custom fit mode can also be done in the same way for periodic modes.
200+
#
201+
# In this example, we will define and apply a custom periodic fit mode that uses a
202+
# rectangular peak fit function. Note that as we will see, this is not really a good
203+
# peak option for neural data (though it may be useful for other data), but serves here
204+
# as an example of
205+
#
206+
# First, we start by defining a fit function, as before. This should follow the same format
207+
# as introduced previously for the aperiodic fit mode function, with the added consideration
208+
# that peak functions should be flexible for potentially having multiple possible peaks,
209+
# meaning that the fit function needs to be set up in a way that multiple sets of parameters
210+
# for multiple peaks can be passed in together, and the results summed together (e.g., if the
211+
# fit function takes `p` parameters, the function should accept multiples of `p` number of inputs
212+
# and loops across these, summing the resultant peaks).
213+
#
214+
215+
###################################################################################################
216+
217+
# Define a custom peak fit function
218+
def rectangular_function(xs, *params):
219+
"""Custom periodic fit function - rectangular fit.
220+
221+
Parameters
222+
----------
223+
xs : 1d array
224+
X-axis values.
225+
*params : float
226+
Parameters that define the component fit.
227+
228+
Returns
229+
-------
230+
ys : 1d array
231+
Output values of the fit.
232+
"""
233+
234+
ys = np.zeros_like(xs)
235+
236+
for ctr, hgt, wid in zip(*[iter(params)] * 3):
237+
ys[np.abs(xs - ctr) <= wid] += 1 * hgt
238+
239+
return ys
240+
241+
###################################################################################################
242+
#
243+
# Next up, as before, we need to define a parameter definition. Here, we will use the same labels
244+
# as the standard (Gaussian) peak mode, redefined for the rectangle case.
245+
#
246+
247+
###################################################################################################
248+
249+
rectangular_params = ParamDefinition(OrderedDict({
250+
'cf' : 'Center frequency of the rectangle.',
251+
'pw' : 'Power of the rectangle, over and above the aperiodic component.',
252+
'bw' : 'Width of the rectangle.'
253+
}))
254+
255+
###################################################################################################
256+
#
257+
# Finally, as before, we collect together all the required information to define our fit mode.
258+
#
259+
260+
###################################################################################################
261+
262+
# Define the custom rectangular fit mode
263+
rectangular_mode = Mode(
264+
name='rectangular',
265+
component='periodic',
266+
description='Custom rectangular (boxcar) peak fit function.',
267+
func=rectangular_function,
268+
jacobian=None,
269+
params=rectangular_params,
270+
ndim=2,
271+
freq_space='linear',
272+
powers_space='log10',
273+
)
274+
275+
###################################################################################################
276+
#
277+
# Now we can initialize a model object with our custom periodic fit mode, and use it to fit
278+
# some data!
279+
#
280+
281+
###################################################################################################
282+
283+
# Initialize model object, passing in custom periodic mode definition
284+
fm = SpectralModel(periodic_mode=rectangular_mode, max_n_peaks=2)
285+
286+
###################################################################################################
287+
288+
# Fit model and report results
289+
fm.report(freqs, powers)
290+
291+
###################################################################################################
292+
#
293+
# In the above, we can see the results of using the custom periodic mode peak fit function.
294+
#
295+
296+
###################################################################################################
297+
# Relationship Between Fit Modes & Fit Algorithms
298+
# -----------------------------------------------
299+
#
300+
# In these examples, we defined simple fit modes wherein the new functions are similar enough
301+
# to the existing cases that they can be dropped in and still work with the default fit
302+
# algorithm. There are some new fit modes that might not work well with the existing / default
303+
# algorithm - for example, the default algorithm makes some assumptions about the peak fit
304+
# function. This means that major changes to the fit modes may need to be defined
305+
# in tandem with updates to the fitting algorithm to make it all work together. Relatedly,
306+
# you might notice from the above mode definition that there are additional details
307+
# that can be customized, such as the expected spacing of the data, that would also require
308+
# coordination with making sure the fit algorithm is consistent with the defined fit modes.
309+
#
310+
311+
###################################################################################################
312+
# Combining Custom Modes
313+
# ----------------------
314+
#
315+
# As a final example, note that you can also combine custom periodic and aperiodic fit modes
316+
# together.
317+
#
318+
319+
###################################################################################################
320+
321+
# Initialize model object, passing in custom aperiodic & periodic mode definitions
322+
fm = SpectralModel(aperiodic_mode=expo_only_mode, periodic_mode=rectangular_mode, max_n_peaks=2)
323+
324+
# Fit model and report results
325+
fm.report(freqs, powers)
326+
327+
###################################################################################################
328+
# Adding New Modes to the Module
329+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
330+
#
331+
# As a final note, if you look into the set of 'built-in' modes that are available within
332+
# the module, you will see that these are defined in the exact way as done here - the only
333+
# difference is that they are defined within the module and therefore can be accessed via
334+
# their name, as a shortcut, rather than the user having to pass in their own full definitions.
335+
#
336+
# This also means that if you have a custom mode that you think would be of interest to
337+
# other specparam users, once the Mode object is defined it is quite easy to add this
338+
# to the module as a new default option. If you would be interested in suggesting a mode
339+
# be added to the module, feel free to open an issue and/or pull request.
340+
#

0 commit comments

Comments
 (0)