Note
Go to the end to download the full example code.
Custom Algorithms¶
This example covers defining and using custom fit algorithms.
from specparam import SpectralModel
# Import function to simulate a power spectrum
from specparam.sim import sim_power_spectrum
# Import elements to define custom fit algorithms
from specparam.algorithms.settings import SettingsDefinition
from specparam.algorithms.algorithm import Algorithm
Defining Custom Fit Algorithms¶
The specparam module includes a standard fitting algorithm that is used to for fitting the selected fit modes to the data. However, you do not have to use this particular algorithm, you can tweak how it works, and/or define your own custom algorithm and plug this in to the model object.
In this tutorial, we will explore how you can also define your own custom fit algorithms.
To do so, we will start by simulating an example power spectrum to use for this example.
# Define simulation parameters
ap_params = [0, 1]
gauss_params = [10, 0.5, 2]
nlv = 0.025
# Simulate an example power spectrum
freqs, powers = sim_power_spectrum(\
[3, 50], {'fixed' : ap_params}, {'gaussian' : gauss_params}, nlv)
Example: Custom Algorithm Object¶
In our first example, we will introduce how to create a custom fit algorithm.
For simplicity, we will start with a ‘dummy’ algorithm - one that functions code wise, but doesn’t actually implement a detailed fitting algorithm, so that we can start with the organization of the code, and build up from there.
Algorithm Settings¶
A fitting algorithm typically has some settings that you want to define and describe so that the user can check their description and provide values for the settings.
For fitting algorithms, these setting descriptions are managed by the
SettingsDefinition object.
For our dummy algorithm, we will initialize a settings definition object, with a placeholder label and description.
# Create a settings definition for our dummy algorithm
DUMMY_ALGO_SETTINGS = SettingsDefinition({'fit_setting' : 'Setting description'})
Algorithm Object¶
Now we can define our custom fitting algorithm. To do so, we will create a custom object
that inherits from the specparam Algorithm object.
Implementing a custom fit object requires following several standards for specparam to be able to use it:
the class should inherit from the specparam Algorithm object
the object needs to accept modes, data, results, and debug input arguments
at initialization, the object should initialize the Algorithm object (‘super()’), including providing a name and description, passing in the algorithm settings object (from above), and passing in the ‘modes’, ‘data’, ‘results’, and ‘debug’ inputs
the object needs to define a _fit function that serves as the main fit function
In the following code, we initialize a custom object following the above to create a fit algorithm object. Note that as a dummy algorithm, the ‘fit’ aspect doesn’t actually implement a step-by-step fitting procedure, but simply instantiates a pre-specified model (to mimic the outputs of a fit algorithm).
import numpy as np
class DummyAlgorithm(Algorithm):
"""Dummy object to mimic a fit algorithm."""
def __init__(self, modes=None, data=None, results=None, debug=False):
"""Initialize DummyAlgorithm instance."""
# Initialize base algorithm object with algorithm metadata
super().__init__(
name='dummy_fit_algo',
description='Dummy fit algorithm.',
public_settings=DUMMY_ALGO_SETTINGS,
modes=modes, data=data, results=results, debug=debug)
def _fit(self):
"""Define the full fitting algorithm."""
self.results.params.aperiodic.add_params('fit', np.array([0, 1]))
self.results.params.periodic.add_params('fit', np.array([10, 0.5, 2], ndmin=2))
self.results._regenerate_model(self.data.freqs)
Expected outcomes of algorithm fitting¶
In order for a custom fitting algorithm to work properly when embedded within a model object, there are some expectations for what the fitting process should do.
The following elements are expected to computed through the fitting procedure:
- Parameter results should be added for each parameter
model.results.params.{component}.add_params(…)
- The model should be computed and added to the object
model.results.model.modeled_spectrum should be populated, as well as model components (model.results.model._ap_fit & model.results.model._peak_fit)
If the above you do the above, the model object can be used as normal, and you can do (fit / print_results / plot / report / as well as save and load results).
There are also some additional procedures / outputs that a custom fit process may do:
Update fit parameters to also have converted versions
Now that our custom fit algorithm is defined, we can use it by passing it into a model object.
Note that in this example, we will use SpectralModel for our example, but
you can also take the same approach to define custom fit algorithms with other model objects.
# Initialize a model object, passing in our custom dummy algorithm
fm = SpectralModel(algorithm=DummyAlgorithm)
# Check the defined fit algorithm
fm.algorithm.print()
==================================================================================================
ALGORITHM: dummy_fit_algo
ALGORITHM SETTINGS
fit_setting : None
==================================================================================================
# Fit and report model, using our custom algorithm
fm.report(freqs, powers)

==================================================================================================
POWER SPECTRUM MODEL
The model was fit with the 'dummy_fit_algo' algorithm
Model was fit to the 3-50 Hz frequency range with 0.50 Hz resolution
Aperiodic Parameters ('fixed' mode)
(offset, exponent)
0.0000, 1.0000
Peak Parameters ('gaussian' mode) 1 peaks found
CF: 10.00, PW: 0.50, BW: 2.00
Model metrics:
error (mae) is 0.0214
gof (rsquared) is 0.9953
==================================================================================================
In this case, with our dummy algorithm, we cheated a bit - the model was pre-specified to initialize a model that happened to match the simulated data, and no real fitting took place.
The point of this example is to show the outline of how a custom fit algorithm can be developed, since the _fit method can implement any arbitrarily defined procedure to fit a model,
Example with Custom Fitting¶
Having sketched out the basic outline with the dummy algorithm above, lets now define a custom fit algorithm that actually does some fitting.
For simplicity, this algorithm will be a simple fit that starts with an aperiodic fit, and then fits a single peak to the flattened (aperiodic removed) spectrum. To do so, it will take in an algorithm setting that defines a guess center-frequency for this peak.
# Define the algorithm settings for our custom fit
CUSTOM_ALGO_SETTINGS = SettingsDefinition(\
{'guess_cf' : 'Initial guess center frequency for peak.'})
Now we need to define our fit approach! To do so, we will mimic the approach we used above to define a custom algorithm object, this time making the _fit method implement an actual fitting procedure. Note that while the _fit function should be the main method that runs the fitting process, it can also call additional methods. In this implementation, we define additional fit methods to fit each component.
To fit the data components, we will use the curve_fit function from scipy.
from scipy.optimize import curve_fit
class CustomAlgorithm(Algorithm):
"""Custom fitting algorithm."""
def __init__(self, guess_cf, modes=None, data=None, results=None, debug=False):
"""Initialize DummyAlgorithm instance."""
# Initialize base algorithm object with algorithm metadata
super().__init__(
name='custom_fit_algo',
description='Example custom algorithm.',
public_settings=CUSTOM_ALGO_SETTINGS,
modes=modes, data=data, results=results, debug=debug)
## Public settings
self.settings.guess_cf = guess_cf
def _fit(self):
"""Define the full fitting algorithm."""
# Fit each individual component
self._fit_aperiodic()
self._fit_peak()
# Create full model from the individual components
self.results.model.modeled_spectrum = \
self.results.model._peak_fit + self.results.model._ap_fit
def _fit_aperiodic(self):
"""Fit aperiodic - direct fit to full spectrum."""
# Fit aperiodic component directly to data & collect parameter results
ap_params, _ = curve_fit(\
self.modes.aperiodic.func, self.data.freqs, self.data.power_spectrum,
p0=np.array([0] * self.modes.aperiodic.n_params))
self.results.params.aperiodic.add_params('fit', ap_params)
# Construct & collect aperiodic component
self.results.model._ap_fit = self.modes.aperiodic.func(freqs, *ap_params)
def _fit_peak(self):
"""Fit peak - single peak, with initial guess CF, to flattened spectrum."""
# Fit peak
self.results.model._spectrum_flat = self.data.power_spectrum - self.results.model._ap_fit
pe_params, _ = curve_fit(\
self.modes.periodic.func, self.data.freqs, self.results.model._spectrum_flat,
p0=np.array([self.settings.guess_cf] + [1] * (self.modes.periodic.n_params - 1)))
self.results.params.periodic.add_params('fit', np.atleast_2d(pe_params))
# Construct periodic component
self.results.model._peak_fit = self.modes.periodic.func(freqs, *pe_params)
# Initialize a model object, passing in a custom fit algorithm and settings for this algorithm
fm = SpectralModel(algorithm=CustomAlgorithm, guess_cf=10)
# Check the defined fit algorithm
fm.algorithm.print()
==================================================================================================
ALGORITHM: custom_fit_algo
ALGORITHM SETTINGS
guess_cf : 10
==================================================================================================
# Fit model with custom algorithm and report results
fm.report(freqs, powers)

==================================================================================================
POWER SPECTRUM MODEL
The model was fit with the 'custom_fit_algo' algorithm
Model was fit to the 3-50 Hz frequency range with 0.50 Hz resolution
Aperiodic Parameters ('fixed' mode)
(offset, exponent)
0.3250, 1.2015
Peak Parameters ('gaussian' mode) 1 peaks found
CF: 10.13, PW: 0.39, BW: 1.45
Model metrics:
error (mae) is 0.0447
gof (rsquared) is 0.9838
==================================================================================================
In the above we fit a model with our custom fit algorithm, and can see the results.
Notes on Defining Custom Algorithms¶
In these examples, we have made quite simple algorithms. This may be a desired use case - creating bespoke fit approaches for specific kinds of data.
In cases where generalizability is more desired, the fit algorithm is likely going to need to be significantly more detailed to address
To see, for example, the details of the original / default fit algorithm, check the definition of the spectral_fit algorithm in the codebase, which is also defined in the same way as here.
Additional notes to consider when creating custom algorithms:
In the above, we didn’t consider different fit modes, and used the defaults. Depending on your use case, the fit algorithm may or not want to make assumptions about the fit modes. To make it generalize, the algorithm needs to be written in a way that is flexible for applying different fit functions that may have different numbers of parameters
As well as the public settings we defined here, you may want to additional specify a set of private settings (additional settings that are defined for the algorithm, which are not expected to be changed in most use cases, but which can be accessed)
Algorithms that use curve_fit¶
A common approach for fitting functions to data is to use the scipy curve_fit function to estimate parameters to fit a specified function to some data, as we did in an above example.
When doing so, you may also want to manage and allow inputs for settings that to the
curve_fit function to manage the fitting process. As a shortcut for this case, you can use the
AlgorithmCF object which pre-initializes a set
of curve_fit settings.
In addition, when using curve_fit you are likely going to want to
Total running time of the script: (0 minutes 0.606 seconds)