diff --git a/doc/api.rst b/doc/api.rst index b6438c76..e970ff0d 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -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 ~~~~~~~~~ diff --git a/neurodsp/sim/__init__.py b/neurodsp/sim/__init__.py index 69b48df1..c60b84d1 100644 --- a/neurodsp/sim/__init__.py +++ b/neurodsp/sim/__init__.py @@ -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 diff --git a/neurodsp/sim/combined.py b/neurodsp/sim/combined.py index f064c776..c7501bcb 100644 --- a/neurodsp/sim/combined.py +++ b/neurodsp/sim/combined.py @@ -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. diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py new file mode 100644 index 00000000..792da25c --- /dev/null +++ b/neurodsp/sim/multi.py @@ -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 diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py new file mode 100644 index 00000000..c46ebcfc --- /dev/null +++ b/neurodsp/sim/params.py @@ -0,0 +1,615 @@ +"""Simulation parameter related objects. + +Notes: +- once we hit minimum python of 3.9, dictionary merging can by updated to use | +""" + +from copy import deepcopy + +from neurodsp.sim.update import ParamIter, ParamSampler + +################################################################################################### +################################################################################################### + +class SimParams(): + """Object for managing simulation parameters. + + Parameters + ---------- + n_seconds : float + Simulation time, in seconds. + fs : float + Sampling rate of simulated signal, in Hz. + + Attributes + ---------- + base : dict + Dictionary of base parameters, common across all parameter definitions. + params : dict + Dictionary of created simulation parameter definitions. + """ + + def __init__(self, n_seconds=None, fs=None): + """Initialize SimParams object.""" + + self.n_seconds = n_seconds + self.fs = fs + + self._params = {} + + + def __getitem__(self, label): + """Make object subscriptable, to access stored simulation parameters. + + Parameters + ---------- + label : str + Label to access simulation parameters from `params`. + """ + + return {**self.base, **self._params[label]} + + + @property + def base(self): + """Get the base parameters, common across all simulations. + + Returns + ------- + base_params : dict + Definition of the current base parameters. + """ + + return {'n_seconds' : self.n_seconds, 'fs' : self.fs} + + + @property + def labels(self): + """Get the set of labels for the defined parameters. + + Returns + ------- + labels : list of str + Labels for all defined simulation parameters. + """ + + return list(self._params.keys()) + + + @property + def params(self): + """Get the set of currently defined simulation parameters. + + Returns + ------- + params : dict + Dictionary of currently defined simulation parameters. + Each key is a label for the parameter definition. + Each value is a dictionary of simulations parameters. + """ + + return {label : {**self.base, **params} for label, params in self._params.items()} + + + def make_params(self, parameters=None, **kwargs): + """Make a simulation parameter definition from given parameters. + + Parameters + ---------- + parameters : dict or list of dict + Parameter definition(s) to create simulation parameter definition. + **kwargs + Additional keyword arguments to create the simulation definition. + + Returns + ------- + params : dict + Parameter definition. + """ + + return {**self.base, **self._make_params(parameters, **kwargs)} + + + def register(self, label, parameters=None, **kwargs): + """Register a new simulation parameter definition. + + Parameters + ---------- + label : str + Label to set simulation parameters under in `params`. + parameters : dict or list of dict + Parameter definition(s) to create simulation parameter definition. + **kwargs + Additional keyword arguments to create the simulation definition. + """ + + self._params[label] = self._make_params(parameters, **kwargs) + + + def register_group(self, group, clear=False): + """Register multiple simulation parameter definitions. + + Parameters + ---------- + group : dict + Dictionary of simulation parameters. + Each key should be a label, and each set of values a dictionary of parameters. + clear : bool, optional, default: False + If True, clears current parameter definitions before adding new group. + """ + + if clear: + self.clear() + + for label, parameters in group.items(): + self.register(label, parameters) + + + def update_base(self, n_seconds=False, fs=False): + """Update base parameters. + + Parameters + ---------- + n_seconds : float, optional + Simulation time, in seconds. + fs : float, optional + Sampling rate of simulated signal, in Hz. + + Notes + ----- + If set as False, value will not be updated. + If set as None, value will be updated to None. + """ + + if n_seconds is not False: + self.n_seconds = n_seconds + if fs is not False: + self.fs = fs + + + def update_param(self, label, attribute, new_value): + """Update a simulation parameter definition. + + Parameters + ---------- + label : str + Label for the parameter definition. + attribute : str + Name of the attribute to update. + new_value : obj + New value to give the attribute. + """ + + self._params[label][attribute] = new_value + + + def clear(self, clear_base=False): + """"Clear parameter definitions. + + Parameters + ---------- + clear_base : bool, optional, default: False + Whether to also clear base parameters. + """ + + self._params = {} + + if clear_base: + self.update_base(None, None) + + + def to_iters(self): + """Convert to a SimIters object. + + Returns + ------- + iters : SimIters + A converted object, initialized with the current base parameters. + """ + + iters = SimIters(**self.base) + iters.register_group(self.params) + + return iters + + + def to_samplers(self, n_samples=None): + """Convert to a SimSamplers object. + + Parameters + ---------- + n_samples : int, optional + The number of parameter iterations to set as max. + If None, samplers are created as infinite generators. + + Returns + ------- + samplers : SimSamplers + A converted object, initialized with the current base parameters. + """ + + samplers = SimSamplers(**self.base, n_samples=n_samples) + samplers.register_group(self.params) + + return samplers + + + def copy(self): + """Return a copy of the current object.""" + + return deepcopy(self) + + + def _make_params(self, parameters=None, **kwargs): + """Sub-function for `make_params`.""" + + parameters = {} if not parameters else deepcopy(parameters) + + if isinstance(parameters, list): + comps = [parameters.pop(0)] + kwargs = {**kwargs, **parameters[0]} if parameters else kwargs + params = self._make_combined_params(comps, **kwargs) + else: + params = {**parameters, **kwargs} + + # If any base parameters were passed in, clear them + for bparam in self.base: + params.pop(bparam, None) + + return params + + + def _make_combined_params(self, components, component_variances=None): + """Make parameters for combined simulations, specifying multiple components. + + Parameters + ---------- + components : list of dict + List of simulation component parameters. + component_variances : list of float + Component variances for the combined simulation. + + Returns + ------- + params : dict + Parameter definition. + """ + + parameters = {} + + comps = {} + for comp in components: + comps.update(**deepcopy(comp)) + parameters['components'] = comps + + if component_variances: + parameters['component_variances'] = component_variances + + return parameters + + +class SimIters(SimParams): + """Object for managing simulation iterators. + + Parameters + ---------- + n_seconds : float + Simulation time, in seconds. + fs : float + Sampling rate of simulated signal, in Hz. + """ + + def __init__(self, n_seconds=None, fs=None): + """Initialize SimIters objects.""" + + SimParams.__init__(self, n_seconds, fs) + + self._iters = {} + + + def __getitem__(self, label): + """Make object subscriptable, to access simulation iterators. + + Parameters + ---------- + label : str + Label to access and create parameter iterator from `_iters`. + """ + + return self.make_iter(**self._iters[label]) + + + @property + def iters(self): + """Get the set of currently defined simulation iterators. + + Returns + ------- + iters : dict + Dictionary of currently defined simulation iterators. + Each key is a label for the parameter iterator. + Each value is a ParamIter object. + """ + + return {label : self.make_iter(**params) for label, params in self._iters.items()} + + + @property + def labels(self): + """Get the set of labels for the defined iterators. + + Returns + ------- + labels : list of str + Labels for all defined iterators. + """ + + return list(self._iters.keys()) + + + def make_iter(self, label, update, values, component=None): + """Create iterator to step across simulation parameter values. + + Parameters + ---------- + label : str + Label for the simulation parameters. + update : str + Name of the parameter to update. + values : 1d array + Values to iterate across. + component : str, optional + Which component to update the parameter in. + Only used if the parameter definition is for a multi-component simulation. + + Returns + ------- + ParamIter + Generator object for iterating across simulation parameters. + """ + + assert label in self._params.keys(), "Label for simulation parameters not found." + + return ParamIter(super().__getitem__(label), update, values, component) + + + def register_iter(self, name, label, update, values, component=None): + """Register an iterator definition. + + Parameters + ---------- + name : str + Name to give the registered iterator. + label : str + Label for the simulation parameters. + update : str + Name of the parameter to update. + values : 1d array + Values to iterate across. + component : str, optional + Which component to update the parameter in. + Only used if the parameter definition is for a multi-component simulation. + """ + + self._iters[name] = { + 'label' : label, + 'update' : update, + 'values' : values, + 'component' : component, + } + + + def register_group_iters(self, group, clear=False): + """Register a group of simulation iterators. + + Parameters + ---------- + group : list of list or list of dict + Set of simulation iterator definitions. + clear : bool, optional, default: False + If True, clears current parameter iterators before adding new group. + """ + + if clear: + self.clear() + + for iterdef in group: + if isinstance(iterdef, list): + self.register_iter(*iterdef) + elif isinstance(iterdef, dict): + self.register_iter(**iterdef) + + + def update_iter(self, label, attribute, new_value): + """Update the definition of an iterator. + + Parameters + ---------- + label : str + Label for the iterator. + attribute : str + Name of the attribute to update. + new_value : obj + New value to give the attribute. + """ + + self._iters[label][attribute] = new_value + + + def clear(self, clear_iters=True, clear_params=False, clear_base=False): + """"Clear iterator and/or parameter definitions. + + Parameters + ---------- + clear_iters : bool, optional, default: True + Whether to clear the currently defined iterators. + clear_params : bool, optional, default: False + Whether to clear the currently defined simulation parameters. + clear_base : bool, optional, default: False + Whether to also clear base parameters. + Only applied if `clear_params` is True. + """ + + if clear_iters: + self._iters = {} + + if clear_params: + super().clear(clear_base=clear_base) + + +class SimSamplers(SimParams): + """Object for sampling simulation parameter definitions. + + Parameters + ---------- + n_seconds : float + Simulation time, in seconds. + fs : float + Sampling rate of simulated signal, in Hz. + n_samples : int, optional + The number of parameter iterations to set as max. + If None, samplers are created as infinite generators. + """ + + def __init__(self, n_seconds=None, fs=None, n_samples=None): + """Initialize SimSamplers objects.""" + + SimParams.__init__(self, n_seconds, fs) + + self.n_samples = n_samples + self._samplers = {} + + + def __getitem__(self, label): + """Make object subscriptable, to access simulation iterators. + + Parameters + ---------- + label : str + Label to access and create parameter sampler from `_samplers`. + """ + + return self.make_sampler(**self._samplers[label]) + + + @property + def labels(self): + """Get the set of labels for the defined iterators.""" + + return list(self._samplers.keys()) + + + @property + def samplers(self): + """Get the set of currently defined simulation samplers. + + Returns + ------- + samplers : dict + Dictionary of currently defined simulation samplers. + Each key is a label for the parameter sampler. + Each value is a ParamSampler object. + """ + + return {label : self.make_sampler(**params) for label, params in self._samplers.items()} + + + def make_sampler(self, label, samplers, n_samples=None): + """Create sampler to sample simulation parameter values. + + Parameters + ---------- + label : str + Label for the simulation parameters. + samplers : dict + Sampler definitions to update parameters with. + Each key should be a callable, a parameter updater function. + Each value should be a generator, to sample updated parameter values from. + + Returns + ------- + ParamSampler + Generator object for sampling simulation parameters. + """ + + return ParamSampler(super().__getitem__(label), samplers, + n_samples if n_samples else self.n_samples) + + + def register_sampler(self, name, label, samplers): + """Register a sampler definition. + + Parameters + ---------- + name : str + Name to give the registered iterator. + label : str + Label for the simulation parameters. + samplers : dict + Sampler definitions to update parameters with. + Each key should be a callable, a parameter updater function. + Each value should be a generator, to sample updated parameter values from. + """ + + self._samplers[name] = { + 'label' : label, + 'samplers' : samplers, + } + + + def register_group_samplers(self, group, clear=False): + """Register a group of simulation samplers. + + Parameters + ---------- + group : list of list or list of dict + Set of simulation iterator definitions. + clear : bool, optional, default: False + If True, clears current parameter samplers before adding new group. + """ + + if clear: + self.clear() + + for samplerdef in group: + if isinstance(samplerdef, list): + self.register_sampler(*samplerdef) + elif isinstance(samplerdef, dict): + self.register_sampler(**samplerdef) + + + def update_sampler(self, label, attribute, new_value): + """Update the definition of a sampler. + + Parameters + ---------- + label : str + Label for the sampler. + attribute : str + Name of the attribute to update. + new_value : obj + New value to give the attribute. + """ + + self._samplers[label][attribute] = new_value + + + def clear(self, clear_samplers=True, clear_params=False, clear_base=False): + """"Clear sampler and/or parameter definitions. + + Parameters + ---------- + clear_samplers : bool, optional, default: True + Whether to clear the currently defined samplers. + clear_params : bool, optional, default: False + Whether to clear the currently defined simulation parameters. + clear_base : bool, optional, default: False + Whether to also clear base parameters. + Only applied if `clear_params` is True. + """ + + if clear_samplers: + self._samplers = {} + + if clear_params: + super().clear(clear_base=clear_base) diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py new file mode 100644 index 00000000..5933b7c8 --- /dev/null +++ b/neurodsp/sim/signals.py @@ -0,0 +1,320 @@ +"""Objects for managing groups of simulated signals.""" + +from itertools import repeat + +import numpy as np + +from neurodsp.utils.core import listify +from neurodsp.sim.utils import get_base_params, drop_base_params + +################################################################################################### +################################################################################################### + +class Simulations(): + """Data object for a set of simulated signals. + + Parameters + ---------- + signals : 1d or 2nd array, optional + The simulated signals, organized as [n_sims, sig_length]. + params : dict, optional + The simulation parameters that were used to create the simulations. + sim_func : str or callable, optional + The simulation function that was used to create the simulations. + If callable, the name of the function is taken to be added to the object. + + Notes + ----- + This object stores a set of simulations generated from a shared parameter definition. + """ + + def __init__(self, signals=None, params=None, sim_func=None): + """Initialize Simulations object.""" + + self.signals = np.atleast_2d(signals) if signals is not None else np.array([]) + self._base_params = None + self._params = None + self.add_params(params) + self.sim_func = sim_func.__name__ if callable(sim_func) else sim_func + + def __iter__(self): + """Define iteration as stepping across individual simulated signals.""" + + for sig in self.signals: + yield sig + + def __getitem__(self, ind): + """Define indexing as accessing simulated signals.""" + + return self.signals[ind, :] + + def __len__(self): + """Define the length of the object as the number of signals.""" + + return len(self.signals) + + @property + def n_seconds(self): + """Alias n_seconds as a property attribute from base parameters.""" + + return self._base_params['n_seconds'] if self.has_params else None + + @property + def fs(self): + """Alias fs as a property attribute from base parameters.""" + + return self._base_params['fs'] if self.has_params else None + + @property + def params(self): + """Define the full set of simulation parameters (base + additional parameters).""" + + if self.has_params: + params = {**self._base_params, **self._params} + else: + params = None + + return params + + @property + def has_params(self): + """Indicator for if the object has parameters.""" + + return bool(self._params) + + @property + def has_signals(self): + """Indicator for if the object has signals.""" + + return bool(len(self)) + + def add_params(self, params): + """Add parameter definition to object. + + Parameters + ---------- + params : dict, optional + The simulation parameter definition(s). + """ + + if params: + self._base_params = get_base_params(params) + self._params = drop_base_params(params) + + +class SampledSimulations(Simulations): + """Data object for a set of simulated signals with sampled (variable) parameter definitions. + + Parameters + ---------- + signals : 2nd array, optional + The simulated signals, organized as [n_sims, sig_length]. + params : list of dict, optional + The simulation parameters for each of the simulations. + sim_func : str, optional + The simulation function that was used to create the simulations. + + Notes + ----- + This object stores a set of simulations with different parameter definitions per signal. + """ + + def __init__(self, signals=None, params=None, sim_func=None): + """Initialize SampledSimulations object.""" + + Simulations.__init__(self, signals, params, sim_func) + + @property + def n_seconds(self): + """Alias n_seconds as a property.""" + + return self.params[0].n_seconds if self.has_params else None + + @property + def fs(self): + """Alias fs as a property.""" + + return self.params[0].fs if self.has_params else None + + @property + def params(self): + """Define simulation parameters (base + additional parameters) for each simulation.""" + + if self.has_params: + params = [{**self._base_params, **self._params[ind]} for ind in range(len(self))] + else: + params = None + + return params + + def add_params(self, params): + """Add parameter definition(s) to object. + + Parameters + ---------- + params : dict or list of dict, optional + The simulation parameter definition(s). + """ + + if params: + + params = listify(params) + base_params = get_base_params(params[0]) + cparams = [drop_base_params(el) for el in params] + + if not self.has_params: + if len(self) > len(cparams): + msg = 'Cannot add parameters to object without existing parameter values.' + raise ValueError(msg) + self._base_params = base_params + self._params = cparams + + else: + self._params.extend(cparams) + + else: + if self.has_params: + raise ValueError('Must add parameters if object already has them.') + + def add_signal(self, signal, params=None): + """Add a signal to the current object. + + Parameters + ---------- + signal : 1d array + A simulated signal to add to the object. + params : dict, optional + Parameter definition for the added signal. + If current object does not include parameters, should be empty. + If current object does include parameters, this input is required. + """ + + try: + self.signals = np.vstack([self.signals, signal]) + except ValueError as array_value_error: + msg = 'Size of the added signal is not consistent with existing signals.' + raise ValueError(msg) from array_value_error + self.add_params(params) + + +class MultiSimulations(): + """Data object for multiple sets of simulated signals. + + Parameters + ---------- + signals : list of 2d array + Sets of simulated signals, with each array organized as [n_sims, sig_length]. + params : list of dict + The simulation parameters that were used to create the simulations. + sim_func : str or list of str + The simulation function(s) that were used to create the simulations. + update : str + The name of the parameter that is updated across sets of simulations. + + Notes + ----- + This object stores a set of simulations with multiple instances per parameter definition. + """ + + def __init__(self, signals=None, params=None, sim_func=None, update=None): + """Initialize MultiSimulations object.""" + + self.signals = [] + self.add_signals(signals, params, sim_func) + self.update = update + + def __iter__(self): + """Define iteration as stepping across sets of simulated signals.""" + + for sigs in self.signals: + yield sigs + + def __getitem__(self, index): + """Define indexing as accessing sets of simulated signals.""" + + return self.signals[index] + + def __len__(self): + """Define the length of the object as the number of sets of signals.""" + + return len(self.signals) + + @property + def n_seconds(self): + """Alias n_seconds as a property.""" + + return self.signals[0].n_seconds if self else None + + @property + def fs(self): + """Alias fs as a property.""" + + return self.signals[0].fs if self else None + + @property + def sim_func(self): + """Alias func as property.""" + + return self.signals[0].sim_func if self else None + + @property + def params(self): + """Alias in the set of parameters across all sets of simulations.""" + + params = [self[ind].params for ind in range(len(self))] + + return params + + @property + def values(self): + """Alias in the parameter definition of the parameter that varies across the sets.""" + + if self.update: + values = [params[self.update] for params in self.params] + else: + values = None + + return values + + @property + def _base_params(self): + """Alias base parameters as property.""" + + return self.signals[0]._base_params if self else None + + @property + def has_signals(self): + """Indicator for if the object has signals.""" + + return bool(len(self)) + + def add_signals(self, signals, params=None, sim_func=None): + """Add a set of signals to the current object. + + Parameters + ---------- + signals : 2d array or list of 2d array or Simulations + A set of simulated signals, organized as [n_sims, sig_length]. + params : dict or list of dict, optional + The simulation parameters that were used to create the set of simulations. + sim_func : str, optional + The simulation function that was used to create the set of simulations. + """ + + if signals is None: + return + + if isinstance(signals, Simulations): + self.signals.append(signals) + + if isinstance(signals, list): + + if isinstance(signals[0], Simulations): + self.signals.extend(signals) + + else: + params = repeat(params) if not isinstance(params, list) else params + sim_func = repeat(sim_func) if not isinstance(sim_func, list) else sim_func + for csigs, cparams, cfunc in zip(signals, params, sim_func): + signals = Simulations(csigs, params=cparams, sim_func=cfunc) + self.signals.append(signals) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py new file mode 100644 index 00000000..c75db627 --- /dev/null +++ b/neurodsp/sim/update.py @@ -0,0 +1,378 @@ +"""Simulation parameter management and updaters.""" + +from copy import deepcopy + +import numpy as np + +from neurodsp.sim.multi import sig_yielder +from neurodsp.utils.core import counter + +################################################################################################### +################################################################################################### + +## PARAM UPDATERS + +def param_updater(parameter): + """Create a lambda updater function to update a specified parameter. + + Parameters + ---------- + parameter : str + Name of the parameter to update. + + Returns + ------- + callable + Updater function which can update specified parameter in simulation parameters. + """ + + return lambda params, value : params.update({parameter : value}) + + +def component_updater(parameter, component): + """Create a lambda updater function to update a parameter within a simulation component. + + Parameters + ---------- + parameter : str + Name of the parameter to update. + component : str + Name of the component to update the parameter within. + + Returns + ------- + callable + Updater function which can update specified parameter in simulation parameters. + """ + + return lambda params, value : params['components'][component].update({parameter : value}) + + +def create_updater(update, component=None): + """Create an updater function for updating simulation parameters. + + Parameters + ---------- + update : str + Name of the parameter to update. + component : str + Name of the component to update the parameter within. + + Returns + ------- + callable + Updater function which can update specified parameter in simulation parameters. + + Examples + -------- + Create an updater callable for a specified parameter: + >>> upd = create_updater('exponent') + + Create an updater callable for a specified parameter within a specified component: + >>> upd = create_updater('exponent', 'sim_powerlaw') + """ + + if component is not None: + updater = component_updater(update, component) + else: + updater = param_updater(update) + + return updater + + +## PARAM ITER + +def param_iter_yielder(sim_params, updater, values): + """Parameter yielder. + + Parameters + ---------- + sim_params : dict + Parameter definition. + updater : callable + Updater function to update parameter definition. + values : 1d array + Values to iterate across. + + Yields + ------ + sim_params : dict + Simulation parameter definition. + """ + + sim_params = deepcopy(sim_params) + + for value in values: + updater(sim_params, value) + yield deepcopy(sim_params) + + +class ParamIter(): + """Object for iterating across parameter updates. + + Parameters + ---------- + params : dict + Parameter definition to create iterator with. + update : str + Name of the parameter to update. + values : 1d array + Values to iterate across. + component : str, optional + Which component to update the parameter in. + Only used if the parameter definition is for a multi-component simulation. + + Attributes + ---------- + index : int + Index of current location through the iteration. + yielder : generator + Generator for sampling the sig iterations. + """ + + def __init__(self, params, update, values, component=None): + """Initialize parameter iteration object.""" + + params = deepcopy(params) + + if component is not None: + params['components'][component][update] = None + else: + params[update] = None + + self.params = params + self.update = update + self.values = values + self.component = component + + self._updater = create_updater(self.update, self.component) + + self.index = 0 + self.yielder = None + self._reset_yielder() + + + def __next__(self): + """Sample the next set of simulation parameters.""" + + self.index += 1 + return next(self.yielder) + + + def __iter__(self): + """Iterate across simulation parameters.""" + + self._reset_yielder() + for _ in counter(len(self)): + yield next(self) + + + def __len__(self): + """Define length of the object as the number of values to step across.""" + + return len(self.values) + + + def _reset_yielder(self): + """Reset the object yielder.""" + + self.index = 0 + self.yielder = param_iter_yielder(self.params, self._updater, self.values) + + +## PARAM SAMPLERS + +def create_sampler(values, probs=None, n_samples=None): + """Create a generator to sample from a set of parameters. + + Parameters + ---------- + values : list or 1d array + Parameter values to create a generator for. + probs : 1d array, optional + Probabilities to sample from values. + If provided, should be the same lengths as `values`. + n_samples : int, optional + The number of parameter iterations to set as max. + If None, creates an infinite generator. + + Yields + ------ + generator + Generator to sample parameter values from. + + Examples + -------- + Create a generator to sample parameter values from, for a specified number of samples: + + >>> sampler = create_sampler([-2, -1, 0], n_samples=5) + + Create a generator to sampler parameter values from, with specified probability: + + >>> sampler = create_sampler([9, 10, 11], probs=[0.25, 0.5, 0.25]) + """ + + # Check that length of values is same as length of probs, if provided + if np.any(probs): + if len(values) != len(probs): + raise ValueError("The number of options must match the number of probabilities.") + + for _ in counter(n_samples): + + if isinstance(values[0], (list, np.ndarray)): + yield values[np.random.choice(len(values), p=probs)] + else: + yield np.random.choice(values, p=probs) + + +def param_sample_yielder(sim_params, samplers, n_samples=None): + """Generator to yield randomly sampled parameter definitions. + + Parameters + ---------- + sim_params : dict + The parameters for the simulated signal. + samplers : dict + Sampler definitions to update parameters with. + Each key should be a callable, a parameter updated function. + Each value should be a generator, to sample updated parameter values from. + n_samples : int, optional + The number of parameter iterations to set as max. + If None, creates an infinite generator. + + Yields + ------ + sim_params : dict + Simulation parameter definition. + """ + + for _ in counter(n_samples): + out_params = deepcopy(sim_params) + for updater, sampler in samplers.items(): + updater(out_params, next(sampler)) + + yield out_params + + +class ParamSampler(): + """Object for sampling parameter definitions. + + Parameters + ---------- + params : dict + Parameter definition to create sampler with. + samplers : dict + Sampler definitions to update parameters with. + Each key should be a callable, a parameter updated function. + Each value should be a generator, to sample updated parameter values from. + n_samples : int, optional + The number of parameter iterations to set as max. + If None, creates an infinite generator. + + Attributes + ---------- + index : int + Index of current number of yielded parameter definitions. + yielder : generator + Generator for sampling the parameter samples. + """ + + def __init__(self, params, samplers, n_samples=None): + """Initialize parameter sampler object.""" + + self.params = deepcopy(params) + self.samplers = samplers + self.n_samples = n_samples + + self.yielder = None + self._reset_yielder() + + + def __next__(self): + """Sample the next set of simulation parameters.""" + + return next(self.yielder) + + + def __iter__(self): + """Iterate across sampled simulation parameters.""" + + self._reset_yielder() + for _ in counter(len(self)): + yield next(self) + + + def __len__(self): + """Define length of the object as the maximum number of parameters to sample.""" + + return self.n_samples if self.n_samples else 0 + + + def _reset_yielder(self): + """Reset the object yielder.""" + + self.yielder = param_sample_yielder(self.params, self.samplers, self.n_samples) + + +## SIG ITER + +class SigIter(): + """Object for iterating across sampled simulations. + + Parameters + ---------- + sim_func : callable + Function to create simulations. + sim_params : dict + Simulation parameters. + n_sims : int, optional + Number of simulations to create. + If None, creates an infinite generator. + + Attributes + ---------- + index : int + Index of current location through the iteration. + yielder : generator + Generator for sampling the sig iterations. + """ + + def __init__(self, sim_func, sim_params, n_sims=None): + """Initialize signal iteration object.""" + + self.sim_func = sim_func + self.sim_params = deepcopy(sim_params) + self.n_sims = n_sims + + self.index = 0 + self.yielder = None + self._reset_yielder() + + + def __next__(self): + """Sample a new simulation.""" + + self.index += 1 + + return next(self.yielder) + + + def __iter__(self): + """Iterate across simulation outputs.""" + + self._reset_yielder() + for _ in counter(len(self)): + yield next(self) + + + def __len__(self): + """Define length of the object as the number of simulations to create.""" + + return self.n_sims if self.n_sims else 0 + + + def _reset_yielder(self): + """Reset the object yielder.""" + + self.index = 0 + self.yielder = sig_yielder(self.sim_func, self.sim_params, self.n_sims) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index 64ac5b67..b11a5c55 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -1,4 +1,4 @@ -"""Utility function for neurodsp.spectral.""" +"""Utility function for neurodsp.sim.""" import numpy as np @@ -163,3 +163,40 @@ def modulate_signal(sig, modulation, fs=None, mod_params=None): msig = sig * modulation return msig + +## Utilities for helping with parameter management + +BASE_PARAMS = ['n_seconds', 'fs'] + +def get_base_params(params): + """Get base parameters from a parameter definition. + + Parameters + ---------- + params : dict + Parameter definition. + + Returns + ------- + params : dict + Base parameters. + """ + + return {key : value for key, value in params.items() if key in BASE_PARAMS} + + +def drop_base_params(params): + """Drop base parameters from a parameter definition. + + Parameters + ---------- + params : dict + Parameter definition. + + Returns + ------- + params : dict + Parameter definition, exluding base parameters. + """ + + return {key : value for key, value in params.items() if key not in BASE_PARAMS} diff --git a/neurodsp/spectral/__init__.py b/neurodsp/spectral/__init__.py index ef81e7cb..050c103a 100644 --- a/neurodsp/spectral/__init__.py +++ b/neurodsp/spectral/__init__.py @@ -1,6 +1,6 @@ """Spectral module, for calculating power spectra, spectral variance, etc.""" -from .power import (compute_spectrum, compute_spectrum_welch, compute_spectrum_wavelet, +from .power import (compute_spectrum, compute_spectrum_welch, compute_spectrum_wavelet, compute_spectrum_medfilt, compute_spectrum_multitaper) from .measures import compute_absolute_power, compute_relative_power, compute_band_ratio from .variance import compute_scv, compute_scv_rs, compute_spectral_hist diff --git a/neurodsp/tests/conftest.py b/neurodsp/tests/conftest.py index 17fcc9a7..f2b82a83 100644 --- a/neurodsp/tests/conftest.py +++ b/neurodsp/tests/conftest.py @@ -7,6 +7,8 @@ import numpy as np from neurodsp.sim import sim_oscillation, sim_powerlaw, sim_combined +from neurodsp.sim.update import create_updater, create_sampler +from neurodsp.sim.params import SimParams from neurodsp.spectral import compute_spectrum from neurodsp.utils.sim import set_random_seed from neurodsp.tests.settings import (N_SECONDS, FS, FREQ_SINE, FREQ1, EXP1, @@ -55,6 +57,34 @@ def tspectrum(tsig_comb): freqs, powers = compute_spectrum(tsig_comb, FS) yield {'freqs' : freqs, 'powers' : powers} +@pytest.fixture(scope='session') +def tsim_params(): + + sim_params = SimParams(N_SECONDS, FS) + sim_params.register_group({ + 'pl' : {'sim_powerlaw' : {'exponent' : -1}}, + 'osc' : {'sim_oscillation' : {'freq' : -1}}, + }) + + yield sim_params + +@pytest.fixture(scope='session') +def tsim_iters(tsim_params): + + sim_iters = tsim_params.to_iters() + sim_iters.register_iter('pl_exp', 'pl', 'exponent', [-2, -1, 0]) + + yield sim_iters + +@pytest.fixture(scope='session') +def tsim_samplers(tsim_params): + + sim_samplers = tsim_params.to_samplers() + sim_samplers.register_sampler(\ + 'samp_exp', 'pl', {create_updater('exponent') : create_sampler([-2, -1, 0])}) + + yield sim_samplers + @pytest.fixture(scope='session', autouse=True) def check_dir(): """Once, prior to session, this will clear and re-initialize the test file directories.""" diff --git a/neurodsp/tests/sim/test_combined.py b/neurodsp/tests/sim/test_combined.py index 65fd52e2..6d7e5de6 100644 --- a/neurodsp/tests/sim/test_combined.py +++ b/neurodsp/tests/sim/test_combined.py @@ -14,28 +14,28 @@ def test_sim_combined(): - simulations = {'sim_oscillation' : {'freq' : FREQ1}, + components = {'sim_oscillation' : {'freq' : FREQ1}, 'sim_powerlaw' : {'exponent' : -2}} - out = sim_combined(N_SECONDS, FS, simulations) + out = sim_combined(N_SECONDS, FS, components) check_sim_output(out) # Test case with multiple uses of same function - simulations = {'sim_oscillation' : [{'freq' : FREQ1}, {'freq' : FREQ2}], - 'sim_powerlaw' : {'exponent' : -2}} + components = {'sim_oscillation' : [{'freq' : FREQ1}, {'freq' : FREQ2}], + 'sim_powerlaw' : {'exponent' : -2}} variances = [0.5, 0.5, 1] - out = sim_combined(N_SECONDS, FS, simulations, variances) + out = sim_combined(N_SECONDS, FS, components, variances) check_sim_output(out) # Check the variance mismatch error variances = [0.5, 1] with raises(ValueError): - out = sim_combined(N_SECONDS, FS, simulations, variances) + out = sim_combined(N_SECONDS, FS, components, variances) def test_sim_peak_oscillation(): sig_ap = sim_powerlaw(N_SECONDS, FS) - sig = sim_peak_oscillation(sig_ap, FS, FREQ1, bw=5, height=10) + sig = sim_peak_oscillation(sig_ap, FS, FREQ1, bw=4, height=2) check_sim_output(sig) @@ -45,6 +45,14 @@ def test_sim_peak_oscillation(): assert abs(np.argmax(powers-powers_ap) - FREQ1) < 5 +def test_sim_combined_peak(): + + components = {'sim_powerlaw' : {'exponent' : -2}, + 'sim_peak_oscillation' : {'freq' : FREQ1, 'bw' : 4, 'height' : 2}} + + out = sim_combined_peak(N_SECONDS, FS, components) + check_sim_output(out) + def test_sim_modulated_signal(): msig1 = sim_modulated_signal(N_SECONDS, FS, diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py new file mode 100644 index 00000000..eef94727 --- /dev/null +++ b/neurodsp/tests/sim/test_multi.py @@ -0,0 +1,78 @@ +"""Tests for neurodsp.sim.multi.""" + +import numpy as np + +from neurodsp.sim.aperiodic import sim_powerlaw +from neurodsp.sim.update import create_updater, create_sampler, ParamSampler +from neurodsp.sim.signals import Simulations, SampledSimulations, MultiSimulations + +from neurodsp.sim.multi import * + +################################################################################################### +################################################################################################### + +def test_sig_yielder(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + yielder = sig_yielder(sim_powerlaw, params, 2) + + for ind, sig in enumerate(yielder): + assert isinstance(sig, np.ndarray) + assert ind == 1 + +def test_sig_sampler(): + + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + sampler = sig_sampler(sim_powerlaw, params) + + for ind, sig in enumerate(sampler): + assert isinstance(sig, np.ndarray) + assert ind == 1 + +def test_sim_multiple(): + + n_sims = 2 + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + + sims_obj = sim_multiple(sim_powerlaw, params, n_sims, 'object') + assert isinstance(sims_obj, Simulations) + assert sims_obj.signals.shape[0] == n_sims + assert sims_obj.params == params + + sims_arr = sim_multiple(sim_powerlaw, params, n_sims, 'array') + assert isinstance(sims_arr, np.ndarray) + assert sims_arr.shape[0] == n_sims + +def test_sim_across_values(): + + n_sims = 3 + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + + sims_obj = sim_across_values(sim_powerlaw, params, n_sims, 'object') + assert isinstance(sims_obj, MultiSimulations) + for sigs, cparams in zip(sims_obj, params): + assert isinstance(sigs, Simulations) + assert len(sigs) == n_sims + assert sigs.params == cparams + + sigs_arr = sim_across_values(sim_powerlaw, params, n_sims, 'array') + assert isinstance(sigs_arr, np.ndarray) + assert sigs_arr.shape[0:2] == (len(params), n_sims) + +def test_sim_from_sampler(): + + n_sims = 2 + params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + samplers = {create_updater('exponent') : create_sampler([-2, -1, 0])} + psampler = ParamSampler(params, samplers) + + sims_obj = sim_from_sampler(sim_powerlaw, psampler, n_sims, 'object') + assert isinstance(sims_obj, SampledSimulations) + assert sims_obj.signals.shape[0] == n_sims + assert len(sims_obj.params) == n_sims + + sims_arr = sim_from_sampler(sim_powerlaw, psampler, n_sims, 'array') + assert isinstance(sims_arr, np.ndarray) + assert sims_arr.shape[0] == n_sims diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py new file mode 100644 index 00000000..c436b6d6 --- /dev/null +++ b/neurodsp/tests/sim/test_params.py @@ -0,0 +1,162 @@ +"""Tests for neurodsp.sim.params.""" + +from neurodsp.sim.update import create_updater, create_sampler + +from neurodsp.sim.params import * + +################################################################################################### +################################################################################################### + +def test_sim_params(): + + # Test initialization + sps1 = SimParams(5, 250) + assert sps1 + + # Define components to add + comp1 = {'sim_powerlaw' : {'exponent' : -1}} + comp2 = {'sim_oscillation' : {'freq' : -1}} + + # Test registering new simulation parameter definition + sps1.register('pl', comp1) + assert comp1.items() <= sps1['pl'].items() + + # Test registering a group of new simulation parameter definitions + sps2 = SimParams(5, 250) + sps2.register_group({'pl' : comp1, 'osc' : comp2}) + assert comp1.items() <= sps2['pl'].items() + assert comp2.items() <= sps2['osc'].items() + +def test_sim_params_props(tsim_params): + + # Test properties + assert tsim_params.labels + assert tsim_params.params + + # Test copy and clear + ntsim = tsim_params.copy() + assert ntsim != tsim_params + ntsim.clear() + +def test_sim_params_make_params(tsim_params): + # Test the SimParams `make_` methods + + # Operate on a copy + ntsim = tsim_params.copy() + ntsim.clear() + + out1 = ntsim.make_params({'exponent' : -1}) + assert isinstance(out1, dict) + assert out1['n_seconds'] == ntsim.n_seconds + assert out1['exponent'] == -1 + + out2 = ntsim.make_params({'exponent' : -1}, f_range=(1, 50)) + assert out2['f_range'] == (1, 50) + + comps = [{'sim_powerlaw' : {'exponent' : -1}, 'sim_oscillation' : {'freq' : 10}}] + out3 = ntsim.make_params(comps) + assert out3['components'] == comps[0] + +def test_sim_params_upd(tsim_params): + # Test the SimParams `update_` methods + + # Operate on a copy + ntsim = tsim_params.copy() + + # Update base + ntsim.update_base(123, 123) + assert ntsim.n_seconds == 123 + assert ntsim.fs == 123 + + # Update param + ntsim.update_param('pl', 'sim_powerlaw', {'exponent' : -3}) + assert ntsim.params['pl']['sim_powerlaw']['exponent'] == -3 + +def test_sim_params_to(tsim_params): + # Test the SimParams `to_` extraction methods + + iters = tsim_params.to_iters() + assert iters.base == tsim_params.base + assert tsim_params['pl'] == iters.params['pl'] + assert tsim_params['osc'] == iters.params['osc'] + + samplers = tsim_params.to_samplers(n_samples=10) + assert samplers.base == tsim_params.base + assert tsim_params['pl'] == samplers.params['pl'] + assert tsim_params['osc'] == samplers.params['osc'] + +def test_sim_iters(): + + comp_plw = {'sim_powerlaw' : {'exponent' : -1}} + comp_osc = {'sim_oscillation' : {'freq' : -1}} + + sis1 = SimIters(5, 250) + sis1.register('pl', comp_plw) + sis1.register_iter('pl_exp', 'pl', 'exponent', [-2, -1, 0]) + assert sis1['pl_exp'] + assert sis1['pl_exp'].values == [-2, -1, 0] + + # Test registering a group of new simulation iterator definitions + sis2 = SimIters(5, 250) + sis2.register_group({'pl' : comp_plw, 'osc' : comp_osc}) + sis2.register_group_iters([ + {'name' : 'pl_exp', 'label' : 'pl', 'update' : 'exponent', 'values' : [-2, -1 ,0]}, + {'name' : 'osc_freq', 'label' : 'osc', 'update' : 'freq', 'values' : [10, 20, 30]}, + ]) + assert sis2['pl_exp'] + assert sis2['osc_freq'] + +def test_sim_iters_props(tsim_iters): + + # Test properties + assert tsim_iters.labels + assert tsim_iters.iters + + # Test copy and clear + ntiter = tsim_iters.copy() + assert ntiter != tsim_iters + ntiter.clear() + +def test_sim_iters_upd(tsim_iters): + + tsim_iters.update_iter('pl_exp', 'values', [-3, -2, -1]) + assert tsim_iters.iters['pl_exp'].values == [-3, -2, -1] + +def test_sim_samplers(): + + sss1 = SimSamplers(5, 250) + sss1.register('pl', {'sim_powerlaw' : {'exponent' : -1}}) + sss1.register_sampler(\ + 'samp_exp', 'pl', {create_updater('exponent') : create_sampler([-2, -1, 0])}) + assert sss1['samp_exp'] is not None + + # Test registering a group of new simulation sampler definitions + sss2 = SimSamplers(5, 250) + sss2.register_group({ + 'pl' : {'sim_powerlaw' : {'exponent' : -1}}, + 'osc' : {'sim_oscillation' : {'freq' : -1}}, + }) + sss2.register_group_samplers([ + {'name' : 'samp_exp', 'label' : 'pl', + 'samplers' : {create_updater('exponent') : create_sampler([-2, -1, 0])}}, + {'name' : 'samp_freq', 'label' : 'osc', + 'samplers' : {create_updater('freq') : create_sampler([10, 20, 30])}}, + ]) + assert sss2['samp_exp'] is not None + assert sss2['samp_freq'] is not None + +def test_sim_samplers_props(tsim_samplers, tsim_params): + + # Test properties + assert tsim_samplers.labels + assert tsim_samplers.samplers + + # Can't directly copy object with generator - so regenerate + ntsim = tsim_params.copy() + ntsamp = ntsim.to_samplers() + ntsamp.clear() + +def test_sim_samplers_upd(tsim_samplers): + + tsim_samplers.update_sampler('samp_exp', 'n_samples', 100) + assert tsim_samplers['samp_exp'].n_samples == 100 diff --git a/neurodsp/tests/sim/test_signals.py b/neurodsp/tests/sim/test_signals.py new file mode 100644 index 00000000..1cc61758 --- /dev/null +++ b/neurodsp/tests/sim/test_signals.py @@ -0,0 +1,180 @@ +"""Tests for neurodsp.sim.signals.""" + +from pytest import raises + +import numpy as np + +from neurodsp.sim.signals import * + +################################################################################################### +################################################################################################### + +def test_simulations(): + + # Test empty initialization + sims_empty = Simulations() + assert isinstance(sims_empty, Simulations) + + # Demo data + n_seconds = 2 + fs = 100 + n_sigs = 2 + sigs = np.ones([2, n_seconds * fs]) + params = {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1} + + # Test initialization with data only + sims_data = Simulations(sigs) + assert sims_data + assert len(sims_data) == n_sigs + assert sims_data.n_seconds is None + assert sims_data.fs is None + assert sims_data.has_signals + assert sims_data.params is None + assert sims_data.sim_func is None + + # Test dunders - iter & getitem & indicators + for el in sims_data: + assert np.all(el) + assert np.all(sims_data[0]) + + # Test initialization with metadata + sims_full = Simulations(sigs, params, 'sim_func') + assert len(sims_full) == n_sigs + assert sims_full.params == params + assert sims_full.has_signals + assert sims_full.has_params + +def test_sampled_simulations(): + + # Test empty initialization + sims_empty = SampledSimulations() + assert isinstance(sims_empty, SampledSimulations) + + # Demo data + n_seconds = 2 + fs = 100 + n_sigs = 2 + sigs = np.ones([2, n_seconds * fs]) + params = [{'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -2}, + {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1}] + + # Test initialization with data only + sims_data = SampledSimulations(sigs) + assert sims_data + assert len(sims_data) == n_sigs + assert sims_data.n_seconds is None + assert sims_data.fs is None + assert sims_data.has_signals + assert sims_data.params is None + assert sims_data.sim_func is None + + # Test dunders - iter & getitem + for el in sims_data: + assert np.all(el) + assert np.all(sims_data[0]) + + # Test initialization with metadata + sims_full = SampledSimulations(sigs, params, 'sim_func') + assert len(sims_full) == n_sigs == len(sims_full.params) + assert sims_full.params == params + assert sims_full.has_signals + assert sims_full.has_params + +def test_sampled_simulations_add(): + + sig = np.array([1, 2, 3, 4, 5]) + params = {'n_seconds' : 1, 'fs' : 100, 'param' : 'value'} + sig2 = np.array([1, 2, 3, 4, 5, 6, 7, 8]) + params2 = {'n_seconds' : 2, 'fs' : 250, 'param' : 'value'} + + sims_data1 = SampledSimulations(sig) + sims_data1.add_signal(sig) + assert sims_data1.has_signals + + sims_data2 = SampledSimulations(sig, params) + sims_data2.add_signal(sig, params) + assert sims_data2.has_signals + assert sims_data2.has_params + assert len(sims_data2) == len(sims_data2.params) + + ## ERROR CHECKS + + # Adding parameters with different base parameters + sims_data3 = SampledSimulations(sig, params) + with raises(ValueError): + sims_data3.add_signal(sig2, params2) + + # Adding parameters without previous parameters + sims_data4 = SampledSimulations(sig) + with raises(ValueError): + sims_data4.add_signal(sig, params) + + # Not adding parameters with previous parameters + sims_data4 = SampledSimulations(sig, params) + with raises(ValueError): + sims_data4.add_signal(sig) + +def test_multi_simulations(): + + # Test empty initialization + sims_empty = MultiSimulations() + assert isinstance(sims_empty, MultiSimulations) + + # Demo data + n_seconds = 2 + fs = 100 + n_sigs = 3 + n_sets = 2 + sigs = np.ones([n_sigs, n_seconds * fs]) + all_sigs = [sigs] * n_sets + params = [{'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -2}, + {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1}] + + # Test initialization with data only + sims_data = MultiSimulations(all_sigs) + assert sims_data + assert len(sims_data) == n_sets + assert sims_data.n_seconds is None + assert sims_data.fs is None + assert sims_data.has_signals + assert sims_data.params == [None] * n_sets + assert sims_data.sim_func is None + assert sims_data.values is None + + # Test dunders - iter & getitem & indicators + for el in sims_data: + assert isinstance(el, Simulations) + assert len(el) == n_sigs + assert isinstance(sims_data[0], Simulations) + + # Test initialization with metadata + sims_full = MultiSimulations(all_sigs, params, 'sim_func', 'exponent') + assert len(sims_full) == n_sets + assert sims_full.has_signals + for params_obj, params_org in zip(sims_full.params, params): + assert params_obj == params_org + assert sims_full.sim_func + assert sims_full.values + +def test_multi_simulations_add(): + + sigs = [np.ones([2, 5]), np.ones([2, 5])] + params = {'n_seconds' : 1, 'fs' : 100, 'param' : 'value'} + + sims_data1 = MultiSimulations(sigs) + sims_data1.add_signals(sigs) + assert sims_data1.has_signals + + sims_data2 = MultiSimulations(sigs, params) + sims_data2.add_signals(sigs, params) + assert sims_data2.has_signals + assert len(sims_data2) == len(sims_data2.params) + + sims_data3 = MultiSimulations(sigs, params) + sims_add = Simulations(sigs, params) + sims_data3.add_signals(sims_add) + assert sims_data3.has_signals + + sims_data4 = MultiSimulations(sigs, params) + sims_data4.add_signals([sims_add, sims_add]) + assert sims_data4.has_signals diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py new file mode 100644 index 00000000..4f114738 --- /dev/null +++ b/neurodsp/tests/sim/test_update.py @@ -0,0 +1,132 @@ +"""Tests for neurodsp.sim.update.""" + +from neurodsp.sim.aperiodic import sim_powerlaw + +from neurodsp.sim.update import * + +################################################################################################### +################################################################################################### + +def test_param_updater(): + + params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + + upd = param_updater('exponent') + assert callable(upd) + upd(params, -2) + assert params['exponent'] == -2 + +def test_component_updater(): + + params = {'n_seconds' : 10, 'fs' : 250, 'components' : {'sim_powerlaw' : {'exponent' : None}}} + + upd = component_updater('exponent', 'sim_powerlaw') + assert callable(upd) + + upd(params, -2) + assert params['components']['sim_powerlaw']['exponent'] == -2 + +def test_create_updater(): + + upd1 = create_updater('exponent') + assert callable(upd1) + + upd2 = create_updater('exponent', 'sim_powerlaw') + assert callable(upd2) + +def test_param_iter_yielder(): + + sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + updater = create_updater('exponent') + values = [-2, -1, 0] + + iter_yielder = param_iter_yielder(sim_params, updater, values) + for ind, params in enumerate(iter_yielder): + assert isinstance(params, dict) + for el in ['n_seconds', 'fs', 'exponent']: + assert el in params + assert params['exponent'] == values[ind] + +def test_class_param_iter(): + + sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + update = 'exponent' + values = [-2, -1, 0] + + piter = ParamIter(sim_params, update, values) + assert piter + for ind, params in enumerate(piter): + assert isinstance(params, dict) + for el in ['n_seconds', 'fs', 'exponent']: + assert el in params + assert params['exponent'] == values[ind] + +def test_create_sampler(): + + values = [-2, -1, 0] + + sampler1 = create_sampler(values, n_samples=5) + for samp in sampler1: + assert samp in values + + sampler2 = create_sampler(values, probs=[0.5, 0.25, 0.25], n_samples=5) + for samp in sampler2: + assert samp in values + +def test_sample_yielder(): + + # Single sampler example + param1 = 'exponent' + values1 = [-2, -1, 0] + + params1 = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + samplers1 = { + create_updater(param1) : create_sampler(values1), + } + + param_sampler1 = param_sample_yielder(params1, samplers1, n_samples=5) + for ind, params in enumerate(param_sampler1): + assert isinstance(params, dict) + assert params['exponent'] in values1 + assert ind == 4 + + # Multiple samplers example + param2 = 'freq' + values2 = [10, 20, 30] + + params2 = {'n_seconds' : 10, 'fs' : 250, + 'components' : {'sim_powerlaw' : {'exponent' : None}, + 'sim_oscillation' : {'freq' : None}}} + samplers2 = { + create_updater(param1, 'sim_powerlaw') : create_sampler(values1), + create_updater(param2, 'sim_oscillation') : create_sampler(values2), + } + + param_sampler2 = param_sample_yielder(params2, samplers2, n_samples=5) + for ind, params in enumerate(param_sampler2): + assert isinstance(params, dict) + assert params['components']['sim_powerlaw'][param1] in values1 + assert params['components']['sim_oscillation'][param2] in values2 + assert ind == 4 + +def test_class_param_sampler(): + + param = 'exponent' + values = [-2, -1, 0] + params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + samplers = {create_updater(param) : create_sampler(values)} + + psampler = ParamSampler(params, samplers, n_samples=5) + for ind, params in enumerate(psampler): + assert isinstance(params, dict) + assert params[param] in values + assert ind == 4 + +def test_class_sig_iter(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + siter = SigIter(sim_powerlaw, params, n_sims=5) + + for ind, sig in enumerate(siter): + assert isinstance(sig, np.ndarray) + assert ind == 4 diff --git a/neurodsp/tests/sim/test_utils.py b/neurodsp/tests/sim/test_utils.py index da26f5e4..4467987f 100644 --- a/neurodsp/tests/sim/test_utils.py +++ b/neurodsp/tests/sim/test_utils.py @@ -34,3 +34,18 @@ def test_modulate_signal(tsig): # Check modulation passing in a 1d array directly msig2 = modulate_signal(tsig, tsig) check_sim_output(msig2) + +def test_get_base_params(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + out1 = get_base_params(params) + for bparam in out1: + assert bparam in BASE_PARAMS + +def test_drop_base_params(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + out1 = drop_base_params(params) + for bparam in BASE_PARAMS: + assert bparam not in out1 + assert 'exponent' in params diff --git a/neurodsp/tests/utils/test_core.py b/neurodsp/tests/utils/test_core.py index dc59c553..9aac5243 100644 --- a/neurodsp/tests/utils/test_core.py +++ b/neurodsp/tests/utils/test_core.py @@ -20,3 +20,33 @@ def test_get_avg_func(): with raises(ValueError): get_avg_func('not_a_thing') + +def test_counter(): + + c1 = counter(5) + for ind in c1: + pass + assert ind == 4 + + c2 = counter(None) + + for ind in c2: + if ind == 5: + break + assert ind == 5 + +def test_listify(): + + arg1 = 1 + out1 = listify(arg1) + assert isinstance(out1, list) + + arg2 = [1] + out2 = listify(arg2) + assert isinstance(out2, list) + assert not isinstance(out2[0], list) + + arg3 = {'a' : 1, 'b' : 2} + out3 = listify(arg3) + assert isinstance(out3, list) + assert out3[0] == arg3 diff --git a/neurodsp/timefrequency/__init__.py b/neurodsp/timefrequency/__init__.py index 1a233662..dd44b416 100644 --- a/neurodsp/timefrequency/__init__.py +++ b/neurodsp/timefrequency/__init__.py @@ -1,4 +1,5 @@ """Time-frequency analyse of neural time series.""" -from .hilbert import robust_hilbert, phase_by_time, amp_by_time, freq_by_time +from .hilbert import (compute_instantaneous_measure, robust_hilbert, + phase_by_time, amp_by_time, freq_by_time) from .wavelets import compute_wavelet_transform, convolve_wavelet diff --git a/neurodsp/utils/core.py b/neurodsp/utils/core.py index 4288975b..33c81230 100644 --- a/neurodsp/utils/core.py +++ b/neurodsp/utils/core.py @@ -1,5 +1,8 @@ """Core / internal utility functions.""" +from itertools import count +from collections.abc import Iterable + import numpy as np from neurodsp.utils.checks import check_param_options @@ -31,3 +34,48 @@ def get_avg_func(avg_type): func = np.sum return func + + +def counter(value): + """Counter that supports both finite and infinite ranges. + + Parameters + ---------- + value : int or None + Upper bound for the counter (if finite) or None (if infinite). + + Returns + ------- + counter : range or count + Counter object for finite (range) or infinite (count) iteration. + """ + + return range(value) if value else count() + + +def listify(arg): + """Check and embed an argument into a list, if is not already in a list. + + Parameters + ---------- + arg : object + Argument to check and embed in a list, if it is not already. + + Returns + ------- + list + Argument embedded in a list. + """ + + # Embed all non-iterable parameters into a list + # Note: deal with str as a special case of iterable that we want to embed + if not isinstance(arg, Iterable) or isinstance(arg, str) or isinstance(arg, dict): + out = [arg] + # Deal with special case of multi dimensional numpy arrays - want to embed without flattening + elif isinstance(arg, np.ndarray) and np.ndim(arg) > 1: + out = [arg] + # If is iterable (e.g. tuple or numpy array), typecast to list + else: + out = list(arg) + + return out diff --git a/tutorials/sim/plot_SimulatePeriodic.py b/tutorials/sim/plot_01_SimulatePeriodic.py similarity index 100% rename from tutorials/sim/plot_SimulatePeriodic.py rename to tutorials/sim/plot_01_SimulatePeriodic.py diff --git a/tutorials/sim/plot_SimulateAperiodic.py b/tutorials/sim/plot_02_SimulateAperiodic.py similarity index 99% rename from tutorials/sim/plot_SimulateAperiodic.py rename to tutorials/sim/plot_02_SimulateAperiodic.py index 099f9855..43a6479b 100644 --- a/tutorials/sim/plot_SimulateAperiodic.py +++ b/tutorials/sim/plot_02_SimulateAperiodic.py @@ -4,7 +4,7 @@ Simulate aperiodic signals. -This tutorial covers the the ``neurodsp.sim.aperiodic`` module. +This tutorial covers the ``neurodsp.sim.aperiodic`` module. """ ################################################################################################### diff --git a/tutorials/sim/plot_SimulateCombined.py b/tutorials/sim/plot_03_SimulateCombined.py similarity index 100% rename from tutorials/sim/plot_SimulateCombined.py rename to tutorials/sim/plot_03_SimulateCombined.py diff --git a/tutorials/sim/plot_SimulateModulated.py b/tutorials/sim/plot_04_SimulateModulated.py similarity index 99% rename from tutorials/sim/plot_SimulateModulated.py rename to tutorials/sim/plot_04_SimulateModulated.py index 2b7a4d8f..a2fbcec6 100644 --- a/tutorials/sim/plot_SimulateModulated.py +++ b/tutorials/sim/plot_04_SimulateModulated.py @@ -3,7 +3,6 @@ ================== Apply amplitude modulation to simulated signals. - """ ################################################################################################### diff --git a/tutorials/sim/plot_SimulateTransients.py b/tutorials/sim/plot_05_SimulateTransients.py similarity index 100% rename from tutorials/sim/plot_SimulateTransients.py rename to tutorials/sim/plot_05_SimulateTransients.py diff --git a/tutorials/sim/plot_06_SimParams.py b/tutorials/sim/plot_06_SimParams.py new file mode 100644 index 00000000..8044bc6a --- /dev/null +++ b/tutorials/sim/plot_06_SimParams.py @@ -0,0 +1,312 @@ +""" +Managing Simulation Parameters +============================== + +Manage, update, and iterate across simulation parameters. +""" + +from neurodsp.sim.update import create_updater, create_sampler +from neurodsp.sim.params import SimParams, SimIters, SimSamplers + +################################################################################################### +# Managing Simulations Parameters +# ------------------------------- +# +# The :class:`~.SimParams` object can be used to manage a set of simulation parameters. +# + +################################################################################################### + +# Initialize object, with base parameters +sim_params = SimParams(n_seconds=5, fs=250) + +# Check the base parameters in the SimParams object +sim_params.base + +################################################################################################### +# +# A defined SimParams object with base parameters can be used to create a full set of simulation +# parameters by specifying additional parameters to add to the base parameters. +# + +################################################################################################### + +# Create a set of simulation parameters +sim_params.make_params({'exponent' : -1}) + +################################################################################################### +# +# The object can also be used to 'register' (:func:`~.SimParams.register`) a set of +# simulation parameters, meaning they can be defined and stored in the object, +# with an associated label to access them. +# + +################################################################################################### + +# Register a set of simulation parameters +sim_params.register('ap', {'exponent' : -1}) + +# Check the registered simulation definition +sim_params['ap'] + +################################################################################################### +# +# The SimParams object can also be updated, for example, clearing previous simulation parameters, +# updating base parameters, and/or updating previously registered simulation definitions. +# + +################################################################################################### + +# Clear the current set of parameter definitions +sim_params.clear() + +# Update the base definition +sim_params.update_base(n_seconds=10) + +# Check the updated base parameters +sim_params.base + +################################################################################################### +# +# The SimParams object can also be used to manage multiple different simulation parameter +# definitions, for example for different functions, which share the same base parameters. +# +# To manage multiple parameters, they can all be registered to the object. +# For convenience, multiple definitions can be registered together with the +# (:func:`~.SimParams.register_group`) method. +# + +################################################################################################### + +# Register a group of parameter definitions +sim_params.register_group( + {'ap' : {'exponent' : -1}, + 'osc' : {'freq' : 10}}) + +################################################################################################### + +# Check the set of labels and parameters defined on the object +print(sim_params.labels) +print(sim_params.params) + +################################################################################################### + +# Check the simulation parameters for the different labels +print(sim_params['ap']) +print(sim_params['osc']) + +################################################################################################### +# Iterating Across Simulations Parameters +# --------------------------------------- +# +# One application of interest for managing simulation parameters may be to iterate +# across parameter values. +# +# To do so, the :class:`~.SimIters` class can be used. +# + +################################################################################################### + +# Initialize base set of simulation parameters +sim_iters = SimIters(n_seconds=5, fs=250) + +# Check the base parameters of the SimIters object +sim_iters.base + +################################################################################################### + +# Re-initialize a SimIters object, exporting from existing SimParams object +sim_iters = sim_params.to_iters() + +################################################################################################### +# +# Similar to the SimParams object, the SimIter object can be used to make simulation iterators. +# + +################################################################################################### + +# Make a parameter iterator from the SimIter object +exp_iter = sim_iters.make_iter('ap', 'exponent', [-2, -1, 0]) + +# Use the iterator to step across parameters +for params in exp_iter: + print(params) + +################################################################################################### +# +# Just as before, we can 'register' an iterator definition on the SimIter object. +# + +################################################################################################### + +# Register an iterator on the SimIter object +sim_iters.register_iter('exp_iter', 'ap', 'exponent', [-2, -1, 0]) + +# Use the iterator from the SimIter object to step across parameters +for params in sim_iters['exp_iter']: + print(params) + +################################################################################################### +# +# Just like the SimParams object, the SimIter object can be cleared, updated, etc. +# +# It can also be used to register a group of iterators, which will share the same base parameters. +# + +################################################################################################### + +# Clear the current object +sim_iters.clear() + +# Register a group of iterators +sim_iters.register_group_iters([ + ['exp_iter', 'ap', 'exponent', [-2, -1, 0]], + ['osc_iter', 'osc', 'freq', [10, 20, 30]]]) + +################################################################################################### + +# Check the labels for the defined iterators, and the iterators +print(sim_iters.labels) +print(sim_iters.iters) + +# Check a set of iterated parameters from the SimIter object +for params in sim_iters['osc_iter']: + print(params) + +################################################################################################### +# Defining Parameters Updates +# --------------------------- +# +# For the next application, we will explore defining sets of parameters to sample from. +# +# To do so, we first need to explore some functionality for defining which parameters to +# update, and how to sample parameter values from a specified set of objects. +# +# To start with, we can use the :func:`~.create_updater` function to create a helper +# function to update parameters. +# + +################################################################################################### + +# Define a set of parameters +params1 = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + +# Create an update object for the exponent parameter +exp_updater = create_updater('exponent') + +# Use the exponent updater +exp_updater(params1, -1) + +################################################################################################### +# +# An updater can also be used to update parameters defined within specified components. +# + +################################################################################################### + +# Define another set of parameters, with multiple components +params2 = {'n_seconds' : 5, 'fs' : 250, + 'components' : {'sim_powerlaw' : {'exponent' : None}, + 'sim_oscillation' : {'freq' : 10}}} + +# Create an updater for the exponent, within the components +exp_comp_updater = create_updater('exponent', 'sim_powerlaw') + +# Use the exponent updater +exp_comp_updater(params2, -1) + +################################################################################################### +# +# Next, we can define a way to sample parameter values. +# +# To do so, we can use the :func:`~.create_sampler` function. +# + +################################################################################################### + +# Create a sampler for a set of exponent values +exp_sampler = create_sampler([-2, -1, 0]) + +# Sample some values from the exponent sampler +for ind in range(3): + print(next(exp_sampler)) + +################################################################################################### +# +# From the above, we can combine updaters and samplers to create definitions of how +# to sample full parameter definitions. +# + +################################################################################################### + +# Define a combined updater and sampler for exponent values +exp_upd_sampler = {create_updater('exponent') : create_sampler([-2, -1, 0])} + +################################################################################################### +# Sampling Simulations Parameters +# ------------------------------- +# +# To manage sampling simulation parameters, we can use the :class:`~.SimSamplers` class. +# + +################################################################################################### + +# Initialize simulation samplers, from pre-initialized SimParams object +sim_samplers = sim_params.to_samplers(n_samples=3) + +################################################################################################### +# +# Just as before, the SimSamplers object can be used to make samplers. +# + +################################################################################################### + +# Make a parameter sampler from the SimSamplers object +exp_sampler = sim_samplers.make_sampler('ap', exp_upd_sampler) + +# Use the exponent sampler to check +for samp_params in exp_sampler: + print(samp_params) + +################################################################################################### +# +# As before, we can also register a sampler on the object. +# + +################################################################################################### + +# Register a sampler definition on the SimSamplers object +sim_samplers.register_sampler('exp_sampler', 'ap', exp_upd_sampler) + +# Check some example sampled parameter values +for samp_params in sim_samplers['exp_sampler']: + print(samp_params) + +################################################################################################### +# +# The object can also be cleared, updated, etc, just as the previous objects. +# + +################################################################################################### + +# Clear the previously defined simulation samplers +sim_samplers.clear() + +# Define a new definition to sample parameter values +osc_upd_sampler = {create_updater('freq') : create_sampler([10, 20, 30])} + +# Register a group of samplers to the object +sim_samplers.register_group_samplers([ + ['exp_sampler', 'ap', exp_upd_sampler], + ['osc_sampler', 'osc', osc_upd_sampler] +]) + +################################################################################################### + +# Check the labels and defined samplers on the object +print(sim_samplers.labels) +print(sim_samplers.samplers) + +# Check example sampled parameter values +for samp_params in sim_samplers['osc_sampler']: + print(samp_params) diff --git a/tutorials/sim/plot_07_SimMulti.py b/tutorials/sim/plot_07_SimMulti.py new file mode 100644 index 00000000..21df0f7c --- /dev/null +++ b/tutorials/sim/plot_07_SimMulti.py @@ -0,0 +1,157 @@ +""" +Simulating Multiple Signals +=========================== + +Simulate multiple signals together. +""" + +from neurodsp.sim.update import SigIter +from neurodsp.sim.aperiodic import sim_powerlaw +from neurodsp.sim.multi import sim_multiple, sim_across_values, sim_from_sampler +from neurodsp.sim.update import create_updater, create_sampler, ParamSampler +from neurodsp.plts.time_series import plot_time_series, plot_multi_time_series + +################################################################################################### +# Simulate Multiple Signals Together +# ---------------------------------- +# +# The :func:`~.sim_multiple` function can be used to simulate multiple signals +# from the same set of parameters. +# + +################################################################################################### + +# Define a set of simulation parameters +params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : -1, 'f_range' : [0.5, None]} + +# Simulate multiple iterations from the same parameter definition +sigs = sim_multiple(sim_powerlaw, params, 3) + +################################################################################################### +# +# The output the above function is a :class:~.Simulations object that stores multiple simulated +# signals along with relevant metadata. +# + +################################################################################################### + +# Check the metadata stored in the simulations object +print(sigs.sim_func, ':', sigs.params) + +################################################################################################### + +# Plot the simulated signals +plot_multi_time_series(None, sigs) + +################################################################################################### +# SigIter +# ------- +# +# In some cases, it may be useful to define a way to sample iterations from the same set of +# simulation parameters. To do so, we can use the :class:`~.SigIter` class. +# +# Using this class, we can define an object that stores the simulation function, the +# set of parameters, and optionally a number of simulations to create, and use this object +# to yield simulated signals. +# + +################################################################################################### + +# Initialize a SigIter object +sig_iter = SigIter(sim_powerlaw, params, 3) + +################################################################################################### + +# Iterate with the object to create simulations +for tsig in sig_iter: + plot_time_series(None, tsig) + +################################################################################################### +# Simulate Across Values +# ---------------------- +# +# Sometimes we may want to simulate signals across a set of parameter values. +# +# To do so, we can use the :func:`~.sim_across_values` function. In doing so, we can define +# a set of parameter definitions and a number of simulations to create per definition. +# + +################################################################################################### + +# Define a set of parameters, stepping across exponent values +multi_params = [ + {'n_seconds' : 5, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 5, 'fs' : 250, 'exponent' : -1}, + {'n_seconds' : 5, 'fs' : 250, 'exponent' : -0}, +] + +# Simulate a set of signals +sims_across_params = sim_across_values(sim_powerlaw, multi_params, 3) + +################################################################################################### +# +# The output of the above is a :class:~.MultiSimulations object that stores sets of simulations +# across different parameters, and relevant metadata. Each set of simulations is stored within +# this object as a :class:~.Simulations object. +# + +################################################################################################### + +# The length of the object is the number of parameter sets +print('# of sets of signals:', len(sims_across_params)) + +################################################################################################### +# +# In the above, we created a set of parameters per definition, which by default are returned +# in a dictionary. +# + +################################################################################################### + +# Plot the simulated signals, accessing signals from each simulation definition +plot_multi_time_series(None, sims_across_params[0]) +plot_multi_time_series(None, sims_across_params[1]) +plot_multi_time_series(None, sims_across_params[2]) + +################################################################################################### +# Simulate From Sampler +# --------------------- +# +# Finally, we can use the :func:`~.sim_from_sampler` function to simulate signals, sampling +# parameter values from a sampler definition. +# + +################################################################################################### + +# Define base set of parameters +params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + +# Create an updater and sampler to sample from +exp_sampler = {create_updater('exponent') : create_sampler([-2, -1, 0])} + +# Create a ParamSampler object +sampler = ParamSampler(params, exp_sampler) + +################################################################################################### + +# Simulate a set of signals from the defined sampler +sampled_sims = sim_from_sampler(sim_powerlaw, sampler, 3) + +################################################################################################### +# +# The output of the above is a :class:~.SampledSimulations object that stores simulations +# created from sampled simulations, as well as the metadata, including the simulation +# parameters for each simulated signal. +# + +################################################################################################### + +# Check some of the metadata stored in the SampledSimulations object +print(sampled_sims.sim_func) +for paramdef in sampled_sims.params: + print(paramdef) + +################################################################################################### + +# Plot the set of sampled simulations +plot_multi_time_series(None, sampled_sims)