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