Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0a0b6b
add check_all_none function
TomDonoghue Nov 26, 2025
cea2599
drop param converts from algorithm
TomDonoghue Nov 26, 2025
f2c82e0
update to do param conv from model obj
TomDonoghue Nov 26, 2025
44f7603
add functionality for converting parameters
TomDonoghue Nov 26, 2025
4bcb27d
drop old convert_params method
TomDonoghue Nov 26, 2025
1851f72
clean ups / udpate docstrings
TomDonoghue Nov 26, 2025
833d5fc
update converter def & allow custom
TomDonoghue Nov 26, 2025
0e36977
update to manage default & specified param conversions
TomDonoghue Nov 26, 2025
beda89a
add get_params to Modes
TomDonoghue Nov 28, 2025
5ed5412
update get_params to take output type
TomDonoghue Nov 28, 2025
a13b183
UPDATERS -> CONVERTERS
TomDonoghue Nov 28, 2025
dc6e2ad
convert peak -> convert periodic
TomDonoghue Nov 29, 2025
85b5a37
move param conversion to own submodule
TomDonoghue Nov 29, 2025
2ca3799
reorg convert submodule
TomDonoghue Nov 29, 2025
03bc59f
cleanups
TomDonoghue Nov 29, 2025
bad2787
add converter object
TomDonoghue Nov 29, 2025
44b90e9
clean up naming
TomDonoghue Nov 29, 2025
4276675
update convert definitions to use converter object
TomDonoghue Nov 29, 2025
018983c
add tests for convert sub-module
TomDonoghue Nov 29, 2025
366fd45
add check_converters & updates
TomDonoghue Nov 29, 2025
23b6220
refactor converter defintions
TomDonoghue Nov 29, 2025
ec6c8b8
udpate call signature of converters (value not label)
TomDonoghue Nov 29, 2025
3adf232
update base object use for converters
TomDonoghue Nov 29, 2025
1e8b05c
add example on customizing conversions
TomDonoghue Nov 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions examples/customize/plot_custom_param_conversions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
"""
Custom Parameter Conversions
============================

This example covers defining and using custom parameter post-fitting conversions.
"""

from specparam import SpectralModel

from specparam.utils.download import load_example_data

# Import the default set of parameter conversions
from specparam.convert.definitions import check_converters, DEFAULT_CONVERTERS

# Import objects to define parameter conversions
from specparam.convert.converter import PeriodicParamConverter, AperiodicParamConverter

###################################################################################################
# Parameter Conversions
# ---------------------
#
# After model fitting, a model object includes the parameters for the model as defined by the
# fit modes and as arrived at by the fit algorithm. These fit parameters define the model fit,
# as visualized, for example, by the 'full model' fit, when plotting the model.
#
# However, these 'fit' parameters are not necessarily defined in a way that we actually
# want to analyzed. For this reason, spectral parameterization supports doing post-fitting
# parameter conversions, whereby after the fitting process, conversions can be applied to
# the fit parameters.
#
# Let's first explore this with an example model fit.
#

###################################################################################################

# Load example spectra
freqs = load_example_data('freqs.npy', folder='data')
powers = load_example_data('spectrum.npy', folder='data')

# Define fitting fit range
freq_range = [2, 40]

# Initialize and fit an example model
fm = SpectralModel()
fm.report(freqs, powers, freq_range)

###################################################################################################
#
# In the above, we see the model fit, and reported parameter values.
#
# Let's further investigate the different versions of the parameters: 'fit' and 'converted'.
#

###################################################################################################

# Check the aperiodic fit & converted parameters
print(fm.results.get_params('aperiodic', version='fit'))
print(fm.results.get_params('aperiodic', version='converted'))

###################################################################################################
#
# In the above, we can see that there are fit parameters, but there is no defined converted
# version of the parameters, indicating that there are no conversions defined for the
# aperiodic parameters.
#

###################################################################################################

# Check the periodic fit & converted parameters, for an example peak
print(fm.results.get_params('periodic', version='fit')[1, :])
print(fm.results.get_params('periodic', version='converted')[1, :])

###################################################################################################
#
# In this case, there are both fit and converted versions of the parameters,
# and they are not the same!
#
# There are defined periodic parameter conversions that are being done. Note also that it is
# the converted versions of the parameters that are printed in the report above.
#

###################################################################################################
# Default Converters
# ------------------
#
# To see what the conversions are that are being defined, we can examine the set of
# DEFAULT_CONVERTERS, which we imported from the module.
#

###################################################################################################

# Check the default model fit parameters
DEFAULT_CONVERTERS

###################################################################################################
# Change Default Converters
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Next, we can explore changing which converters we use.
#
# To start with a simple example, let's turn off all parameter conversions.
#
# Note that as a shortcut, we can get a parameter definition from the Modes sub-object that
# is part of the model object, specified to return a dictionary.
#

###################################################################################################

# Get a dictionary representation of current parameters
null_converters = fm.modes.get_params('dict')
null_converters

###################################################################################################

# Initialize & fit a new model with null converters
fm1 = SpectralModel(converters=null_converters)
fm1.report(freqs, powers, freq_range)

###################################################################################################
#
# In the above no parameter conversions were applied!
#

###################################################################################################

# Check that there are no converted parameters - should all be nan
print(fm1.results.get_params('aperiodic', version='converted'))
print(fm1.results.get_params('periodic', version='converted'))

###################################################################################################
#
# Next, we can explore specifying to use different built in parameter conversions.
#
# To do so, we can explore the available options with the
# :func:`~specparam.convert.definitions.check_converters` function.
#

###################################################################################################

# Check the available aperiodic parameter converters
check_converters('aperiodic')

###################################################################################################

# Check the available periodic parameter converters
check_converters('periodic')

###################################################################################################
#
# Now we can select different conversions from these options.
#

###################################################################################################

# Take a copy of the null converters dictionary
selected_converters = null_converters.copy()

# Specify a different
selected_converters['periodic']['pw'] = 'lin_sub'

###################################################################################################

# Initialize & fit a new model with selected converters
fm2 = SpectralModel(converters=selected_converters)
fm2.report(freqs, powers, freq_range)

###################################################################################################
#
# In the above, the converted and reported parameter outputs used the specified conversions!
#

###################################################################################################
# Create Custom Converters
# ------------------------
#
# Finally, let's explore defining some custom parameter conversions.
#
# To do so, for any parameter that we wish to define a conversion for, we can define a
# callable that implements our desired conversion.
#
# In order for specparam to be able to use the callable, they must follow properties:
#
# - for aperiodic component conversions : callable should accept inputs `fit_value` and `model`
# - for periodic component conversions: callable should accept inputs `fit_value`, `model`, and `peak_ind`
#

###################################################################################################

# Take a copy of the null converters dictionary
custom_converters = null_converters.copy()

###################################################################################################
#
# To start with, let's define a simple conversion for the aperiodic exponent to convert the
# fit value into the equivalent spectral slope value (the negative of the exponent value).
#
# To define this simple conversion we can even use a lambda function.
#

###################################################################################################

# Create a custom exponent converter as a lambda function
custom_converters['aperiodic']['exponent'] = lambda param, model : -param

###################################################################################################
#
# Let's also define a conversion for a periodic parameter. As an example, we can define a
# conversion of the fit center frequency value that finds and update to the closest frequency
# value that actually occurs in the frequency definition. For this case, we will implement
# conversion function.
#

###################################################################################################

# Import utility function to find nearest index
from specparam.utils.select import nearest_ind

# Define a function to update the center frequency
def update_cf(fit_value, model, peak_ind):
"""Updates center frequency to be closest existing frequency value."""

f_ind = nearest_ind(model.data.freqs, fit_value)
new_cf = model.data.freqs[f_ind]

return new_cf

###################################################################################################

# Add the custom cf converter function to function collection
custom_converters['periodic']['cf'] = update_cf

###################################################################################################
#
# Now we have defined our custom converters, we can use them in the fitting process!
#

###################################################################################################

# Initialize & fit a new model with custom converters
fm3 = SpectralModel(converters=custom_converters)
fm3.report(freqs, powers, freq_range)

###################################################################################################
#
# In the above report, our custom parameter conversions were used.
#

###################################################################################################
# Parameter Converter Objects
# ---------------------------
#
# In the above, we defined custom parameter converters by directly passing in callables that
# implement our desired conversions. As we've seen above, this works to pass in conversions
#
# However, only passing in the callable is a bit light on details and description. If you
# want to implement parameter conversions using an approach that keeps track of additional
# description of the approach, you can use the
# :class:`~specparam.convert.converter.AperiodicParamConverter` and
# :class:`~specparam.convert.converter.PeriodicParamConverter` objects to
#

###################################################################################################

# Define the exponent to slope conversion as a converter object
exp_slope_converter = AperiodicParamConverter(
parameter='exponent',
name='slope',
description='Convert the fit exponent value to the equivalent spectral slope value.',
function=lambda param, model : -param,
)

# Define the center frequency fixed frequency converter as a converter object
cf_fixed_freq_converter = PeriodicParamConverter(
parameter='cf',
name='fixed_freq',
description='Convert the fit center frequency value to a fixed frequency value.',
function=update_cf,
)

###################################################################################################

# Take a new copy of the null converters dictionary & add
custom_converters2 = null_converters.copy()
custom_converters['aperiodic']['exponent'] = exp_slope_converter
custom_converters2['periodic']['cf'] = cf_fixed_freq_converter

###################################################################################################
#
# Same as before, we can now use our custom converter definitions in the model fitting process.
#

###################################################################################################

# Initialize & fit a new model with custom converters
fm4 = SpectralModel(converters=custom_converters2)
fm4.report(freqs, powers, freq_range)

###################################################################################################
# Adding New Parameter Conversions to the Module
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# As a final note, if you look into the set of 'built-in' parameter conversions that are
# available within the module, you will see that these are defined in the same way as done here,
# using the conversion objects introduced above. The only difference is that they are defined
# within the module and therefore can be accessed via their name, as a shortcut,
# rather than the user having to pass in their own full definitions.
#
# This also means that if you have a custom parameter conversion that you think would be of
# interest to other specparam users, once the ParamConverter object is defined it is quite
# easy to add this to the module as a new default option. If you would be interested in
# suggesting a mode be added to the module, feel free to open an issue and/or pull request.
#
2 changes: 1 addition & 1 deletion examples/customize/plot_sub_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def print_public_api(obj):
###################################################################################################

# Initialize a base model, passing in empty mode definitions
base = BaseModel(None, None, False)
base = BaseModel(None, None, None, False)

# Check the API of the object
print_public_api(base)
Expand Down
55 changes: 0 additions & 55 deletions specparam/algorithms/spectral_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,6 @@ def _fit(self):
self.results.model.modeled_spectrum = \
self.results.model._peak_fit + self.results.model._ap_fit

## PARAMETER UPDATES

# Convert fit peak parameters to updated values
self.results.params.periodic.add_params('converted', \
self._create_peak_params(self.results.params.periodic.get_params('fit')))


def _get_ap_guess(self, freqs, power_spectrum):
"""Get the guess parameters for the aperiodic fit.
Expand Down Expand Up @@ -603,52 +597,3 @@ def _drop_peak_overlap(self, guess):
guess = np.array([gu for (gu, keep) in zip(guess, keep_peak) if keep])

return guess


def _create_peak_params(self, fit_peak_params):
"""Copies over the fit peak parameters output parameters, updating as appropriate.

Parameters
----------
fit_peak_params : 2d array
Parameters that define the peak parameters directly fit to the spectrum.

Returns
-------
peak_params : 2d array
Updated parameter values for the peaks.

Notes
-----
The center frequency estimate is unchanged as the peak center frequency.

The peak height is updated to reflect the height of the peak above
the aperiodic fit. This is returned instead of the fit peak height, as
the fit height is harder to interpret, due to peak overlaps.

The peak bandwidth is updated to be 'both-sided', to reflect the overal width
of the peak, as opposed to the fit parameter, which is 1-sided standard deviation.

Performing this conversion requires that the model has been run,
with `freqs`, `modeled_spectrum` and `_ap_fit` all required to be available.
"""

inds = self.modes.periodic.params.indices

peak_params = np.empty((len(fit_peak_params), self.modes.periodic.n_params))

for ii, peak in enumerate(fit_peak_params):

cpeak = peak.copy()

# Gets the index of the power_spectrum at the frequency closest to the CF of the peak
cf_ind = np.argmin(np.abs(self.data.freqs - peak[inds['cf']]))
cpeak[inds['pw']] = \
self.results.model.modeled_spectrum[cf_ind] - self.results.model._ap_fit[cf_ind]

# Bandwidth is updated to be 'two-sided' (as opposed to one-sided std dev)
cpeak[inds['bw']] = peak[inds['bw']] * 2

peak_params[ii] = cpeak

return peak_params
1 change: 1 addition & 0 deletions specparam/convert/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Sub-module for functionality related to parameter conversions."""
Loading