Skip to content

Commit

Permalink
Merge pull request #329 from neurodsp-tools/simp
Browse files Browse the repository at this point in the history
[ENH] - Add functionality for managing sim params and simulating multiple signals together
  • Loading branch information
TomDonoghue authored Sep 2, 2024
2 parents c603f5a + 97ca7b2 commit 9808874
Show file tree
Hide file tree
Showing 26 changed files with 2,818 additions and 13 deletions.
57 changes: 57 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,65 @@ Combined Signals

sim_combined
sim_peak_oscillation
sim_combined_peak
sim_modulated_signal

Multiple Signals
~~~~~~~~~~~~~~~~

.. currentmodule:: neurodsp.sim.multi
.. autosummary::
:toctree: generated/

sim_multiple
sim_across_values
sim_from_sampler

Simulation Parameters
~~~~~~~~~~~~~~~~~~~~~

The following objects can be used to manage simulation parameters:

.. currentmodule:: neurodsp.sim.params
.. autosummary::
:toctree: generated/

SimParams
SimIters
SimSamplers

The following objects sample and iterate across parameters & simulations:

.. currentmodule:: neurodsp.sim.update
.. autosummary::
:toctree: generated/

ParamSampler
ParamIter
SigIter

The following functions can be used to update simulation parameters:

.. currentmodule:: neurodsp.sim.update
.. autosummary::
:toctree: generated/

create_updater
create_sampler

Simulated Signals
~~~~~~~~~~~~~~~~~

The following objects can be used to manage groups of simulated signals:

.. currentmodule:: neurodsp.sim.signals
.. autosummary::
:toctree: generated/

Simulations
SampledSimulations
MultiSimulations

Utilities
~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion neurodsp/sim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
sim_knee, sim_frac_gaussian_noise, sim_frac_brownian_motion)
from .cycles import sim_cycle
from .transients import sim_synaptic_kernel, sim_action_potential
from .combined import sim_combined, sim_peak_oscillation, sim_modulated_signal
from .combined import sim_combined, sim_peak_oscillation, sim_modulated_signal, sim_combined_peak
from .multi import sim_multiple, sim_across_values, sim_from_sampler
32 changes: 32 additions & 0 deletions neurodsp/sim/combined.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,38 @@ def sim_peak_oscillation(sig_ap, fs, freq, bw, height):
return sig


@normalize
def sim_combined_peak(n_seconds, fs, components):
"""Simulate a combined signal with an aperiodic component and a peak.
Parameters
----------
n_seconds : float
Simulation time, in seconds.
fs : float
Sampling rate of simulated signal, in Hz.
components : dict
A dictionary of simulation functions to run, with their desired parameters.
Returns
-------
sig : 1d array
Simulated combined peak signal.
"""

sim_names = list(components.keys())
assert len(sim_names) == 2, 'Expected only 2 components.'
assert sim_names[1] == 'sim_peak_oscillation', \
'Expected `sim_peak_oscillation` as the second key.'

ap_func = get_sim_func(sim_names[0]) if isinstance(sim_names[0], str) else sim_names[0]

sig = sim_peak_oscillation(\
ap_func(n_seconds, fs, **components[sim_names[0]]), fs, **components[sim_names[1]])

return sig


@normalize
def sim_modulated_signal(n_seconds, fs, sig_func, sig_params, mod_func, mod_params):
"""Simulate an amplitude modulated signal.
Expand Down
213 changes: 213 additions & 0 deletions neurodsp/sim/multi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""Simulation functions that return multiple instances."""

from collections.abc import Sized

import numpy as np

from neurodsp.utils.core import counter
from neurodsp.sim.signals import Simulations, SampledSimulations, MultiSimulations

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

def sig_yielder(sim_func, sim_params, n_sims):
"""Generator to yield simulated signals from a given simulation function and parameters.
Parameters
----------
sim_func : callable
Function to create the simulated time series.
sim_params : dict
The parameters for the simulated signal, passed into `sim_func`.
n_sims : int, optional
Number of simulations to set as the max.
If None, creates an infinite generator.
Yields
------
sig : 1d array
Simulated time series.
"""

for _ in counter(n_sims):
yield sim_func(**sim_params)


def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None):
"""Generator to yield simulated signals from a parameter sampler.
Parameters
----------
sim_func : callable
Function to create the simulated time series.
sim_params : iterable
The parameters for the simulated signal, passed into `sim_func`.
return_sim_params : bool, optional, default: False
Whether to yield the simulation parameters as well as the simulated time series.
n_sims : int, optional
Number of simulations to set as the max.
If None, length is defined by the length of `sim_params`, and could be infinite.
Yields
------
sig : 1d array
Simulated time series.
sample_params : dict
Simulation parameters for the yielded time series.
Only returned if `return_sim_params` is True.
"""

# If `sim_params` has a size, and `n_sims` is defined, check that they are compatible
# To do so, we first check if the iterable has a __len__ attr, and if so check values
if isinstance(sim_params, Sized) and len(sim_params) and n_sims and n_sims > len(sim_params):
msg = 'Cannot simulate the requested number of sims with the given parameters.'
raise ValueError(msg)

for ind, sample_params in zip(counter(n_sims), sim_params):

if return_sim_params:
yield sim_func(**sample_params), sample_params
else:
yield sim_func(**sample_params)

if n_sims and ind >= n_sims:
break


def sim_multiple(sim_func, sim_params, n_sims, return_type='object'):
"""Simulate multiple samples of a specified simulation.
Parameters
----------
sim_func : callable
Function to create the simulated time series.
sim_params : dict
The parameters for the simulated signal, passed into `sim_func`.
n_sims : int
Number of simulations to create.
return_type : {'object', 'array'}
Specifies the return type of the simulations.
If 'object', returns simulations and metadata in a 'Simulations' object.
If 'array', returns the simulations (no metadata) in an array.
Returns
-------
sigs : Simulations or 2d array
Simulations, return type depends on `return_type` argument.
Simulated time series are organized as [n_sims, sig length].
Examples
--------
Simulate multiple samples of a powerlaw signal:
>>> from neurodsp.sim.aperiodic import sim_powerlaw
>>> params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}
>>> sigs = sim_multiple(sim_powerlaw, params, n_sims=3)
"""

sigs = np.zeros([n_sims, sim_params['n_seconds'] * sim_params['fs']])
for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)):
sigs[ind, :] = sig

if return_type == 'object':
return Simulations(sigs, sim_params, sim_func)
else:
return sigs


def sim_across_values(sim_func, sim_params, n_sims, output='object'):
"""Simulate multiple signals across different parameter values.
Parameters
----------
sim_func : callable
Function to create the simulated time series.
sim_params : ParamIter or iterable or list of dict
Simulation parameters for `sim_func`.
n_sims : int
Number of simulations to create per parameter definition.
return_type : {'object', 'array'}
Specifies the return type of the simulations.
If 'object', returns simulations and metadata in a 'MultiSimulations' object.
If 'array', returns the simulations (no metadata) in an array.
Returns
-------
sims : MultiSimulations or array
Simulations, return type depends on `return_type` argument.
If array, signals are collected together as [n_sets, n_sims, sig_length].
Examples
--------
Simulate multiple powerlaw signals using a ParamIter object:
>>> from neurodsp.sim.aperiodic import sim_powerlaw
>>> from neurodsp.sim.params import ParamIter
>>> base_params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : None}
>>> param_iter = ParamIter(base_params, 'exponent', [-2, 1, 0])
>>> sigs = sim_across_values(sim_powerlaw, param_iter, n_sims=2)
Simulate multiple powerlaw signals from manually defined set of simulation parameters:
>>> params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2},
... {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}]
>>> sigs = sim_across_values(sim_powerlaw, params, n_sims=2)
"""

update = sim_params.update if \
not isinstance(sim_params, dict) and hasattr(sim_params, 'update') else None

sims = MultiSimulations(update=update)
for ind, cur_sim_params in enumerate(sim_params):
sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object'))

if output == 'array':
sims = np.array([el.signals for el in sims])

return sims


def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'):
"""Simulate a set of signals from a parameter sampler.
Parameters
----------
sim_func : callable
Function to create the simulated time series.
sim_sampler : ParamSampler
Parameter definition to sample from.
n_sims : int
Number of simulations to create per parameter definition.
return_type : {'object', 'array'}
Specifies the return type of the simulations.
If 'object', returns simulations and metadata in a 'SampledSimulations' object.
If 'array', returns the simulations (no metadata) in an array.
Returns
-------
sigs : SampledSimulations or 2d array
Simulations, return type depends on `return_type` argument.
If array, simulations are organized as [n_sims, sig length].
Examples
--------
Simulate multiple powerlaw signals using a parameter sampler:
>>> from neurodsp.sim.aperiodic import sim_powerlaw
>>> from neurodsp.sim.update import create_updater, create_sampler, ParamSampler
>>> params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None}
>>> samplers = {create_updater('exponent') : create_sampler([-2, -1, 0])}
>>> param_sampler = ParamSampler(params, samplers)
>>> sigs = sim_from_sampler(sim_powerlaw, param_sampler, n_sims=2)
"""

all_params = [None] * n_sims
sigs = np.zeros([n_sims, sim_sampler.params['n_seconds'] * sim_sampler.params['fs']])
for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)):
sigs[ind, :] = sig
all_params[ind] = params

if return_type == 'object':
return SampledSimulations(sigs, all_params, sim_func)
else:
return sigs
Loading

0 comments on commit 9808874

Please sign in to comment.