From bbac0b4abfb85a2a40c66710c82a5958edc5d926 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 14:22:51 -0400 Subject: [PATCH 01/44] add sim_combined_peak --- doc/api.rst | 1 + neurodsp/sim/combined.py | 32 +++++++++++++++++++++++++++++ neurodsp/tests/sim/test_combined.py | 22 +++++++++++++------- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 503478fe..aebb7d28 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -310,6 +310,7 @@ Combined Signals sim_combined sim_peak_oscillation + sim_combined_peak sim_modulated_signal Utilities 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/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, From 6df1cfa5b5910b10c30ae521d7045e8676f06d0f Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 14:33:00 -0400 Subject: [PATCH 02/44] add counter util --- neurodsp/sim/utils.py | 2 +- neurodsp/tests/utils/test_core.py | 14 ++++++++++++++ neurodsp/utils/core.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index 64ac5b67..e3163148 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 diff --git a/neurodsp/tests/utils/test_core.py b/neurodsp/tests/utils/test_core.py index dc59c553..e4ae4cfd 100644 --- a/neurodsp/tests/utils/test_core.py +++ b/neurodsp/tests/utils/test_core.py @@ -20,3 +20,17 @@ 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 diff --git a/neurodsp/utils/core.py b/neurodsp/utils/core.py index 4288975b..95ea189d 100644 --- a/neurodsp/utils/core.py +++ b/neurodsp/utils/core.py @@ -1,5 +1,7 @@ """Core / internal utility functions.""" +from itertools import count + import numpy as np from neurodsp.utils.checks import check_param_options @@ -31,3 +33,20 @@ 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() From 03fc40b89f4360c1e7b54429e781c394d681b4fb Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 15:18:06 -0400 Subject: [PATCH 03/44] add new udpate funcs --- neurodsp/sim/update.py | 431 ++++++++++++++++++++++++++++++ neurodsp/tests/sim/test_update.py | 164 ++++++++++++ 2 files changed, 595 insertions(+) create mode 100644 neurodsp/sim/update.py create mode 100644 neurodsp/tests/sim/test_update.py diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py new file mode 100644 index 00000000..bfe0b8f1 --- /dev/null +++ b/neurodsp/sim/update.py @@ -0,0 +1,431 @@ +"""Simulation parameter management and updaters.""" + +from copy import deepcopy + +import numpy as np + +from neurodsp.sim.sim 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 + ---------- + 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. + """ + + if component is not None: + updater = component_updater(update, component) + else: + updater = param_updater(update) + + return updater + + +## PARAM YIELDER + +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) + + +## PARAM ITER + +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) + + +def param_iter(params, update, values, component=None): + """Wrapper function for the ParamIter object. + + 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. + + Returns + ------- + ParamIter + Iterable object for iterating across parameter definitions. + """ + + return ParamIter(params, update, values, component) + + +## PARAM SAMPLERS + +def create_sampler(values, probs=None, n_samples=None): + """Create a generator to sample from a parameter range. + + 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. + """ + + # 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 ind 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) + + +def param_sampler(sim_params, samplers, n_samples=None): + """Wrapper function for the ParamSampler object. + + 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. + + Returns + ------- + ParamSampler + Iterable object for sampling parameter definitions. + """ + + return ParamSampler(sim_params, samplers, n_samples) + + +## SIG YIELDER / 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) + + +def sig_iter(sim_func, sim_params, n_sims): + """Wrapper function for the SigIter object. + + Parameters + ---------- + sim_func : callable + Function to create simulations. + sim_params : dict + Simulation parameters. + n_sims : int + Number of simulations to create. + + Returns + ------- + SigIter + Iterable object for sampling simulatons. + """ + + return SigIter(sim_func, sim_params, n_sims) diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py new file mode 100644 index 00000000..5990ff0f --- /dev/null +++ b/neurodsp/tests/sim/test_update.py @@ -0,0 +1,164 @@ +"""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_param_iter(): + + sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + update = 'exponent' + values = [-2, -1, 0] + + # Note: no accuracy checking here (done in `test_class_param_iter`) + piter = param_iter(sim_params, update, values) + assert isinstance(piter, ParamIter) + for params in piter: + assert isinstance(params, dict) + +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_param_sampler(): + + # Note: no accuracy checking here (done in `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 = param_sampler(params, samplers, n_samples=5) + for ind, params in enumerate(psampler): + assert isinstance(params, dict) + +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 + +def test_sig_iter(): + + # Note: no accuracy checking here (done in `test_class_sig_iter`) + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + siter = sig_iter(sim_powerlaw, params, n_sims=5) + for ind, sig in enumerate(siter): + assert isinstance(sig, np.ndarray) + assert ind == 4 From 2168f013a60f1ded61371bb9bd8d556b18804d2b Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 16:17:35 -0400 Subject: [PATCH 04/44] add multi sim funcs --- neurodsp/sim/multi.py | 166 +++++++++ neurodsp/sim/objs.py | 573 +++++++++++++++++++++++++++++++ neurodsp/tests/sim/test_multi.py | 59 ++++ neurodsp/tests/sim/test_objs.py | 69 ++++ 4 files changed, 867 insertions(+) create mode 100644 neurodsp/sim/multi.py create mode 100644 neurodsp/sim/objs.py create mode 100644 neurodsp/tests/sim/test_multi.py create mode 100644 neurodsp/tests/sim/test_objs.py diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py new file mode 100644 index 00000000..bd34118a --- /dev/null +++ b/neurodsp/sim/multi.py @@ -0,0 +1,166 @@ +"""Simulation functions that return multiple instances.""" + +import numpy as np + +from neurodsp.utils.core import counter + +################################################################################################### +################################################################################################### + +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, creates an infinite generator. + + Yields + ------ + sig : 1d array + Simulated time series. + sample_params : dict + Simulation parameters for the yielded time series. + """ + + if 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): + """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. + + Returns + ------- + sigs : 2d array + Simulations, as [n_sims, sig length]. + """ + + 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 + + return sigs + + +def sim_across_values(sim_func, sim_params, n_sims, output='dict'): + """Helper function to create a set of simulations across different parameter values. + + Parameters + ---------- + sim_func : callable + Function to create the simulated time series. + sim_params : iterable or list of dict + Simulation parameters for `sim_func`. + n_sims : int + Number of simulations to create per parameter definition. + output : {'dict', 'array'} + Organization of the output for the sims. + If 'dict', stored in a dictionary, organized by simulation parameter. + If 'array', all sims are organized into a 2D array. + + Returns + ------- + sims : dict of {float : array} or array + If dict, dictionary of simulated signals, where: + Each key is the simulation parameter value for the set of simulations. + Each value is the set of simulations for that value, as [n_sims, sig_length]. + If array, is all signals collected together as [n_sims, sig_length]. + """ + + sims = {} + for ind, cur_sim_params in enumerate(sim_params): + label = sim_params.values[ind] if hasattr(sim_params, 'values') else ind + label = label[-1] if isinstance(label, list) else label + sims[label] = sim_multiple(sim_func, cur_sim_params, n_sims) + if output == 'array': + sims = np.squeeze(np.array(list(sims.values()))) + + return sims + + +def sim_from_sampler(sim_func, sim_sampler, n_sims, return_params=False): + """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_params : bool, default: False + Whether to collect and return the parameters of all the generated simulations. + + Returns + ------- + sigs : 2d array + Simulations, as [n_sims, sig length]. + all_params : list of dict + Simulation parameters for each returned time series. + Only returned if `return_params` is True. + """ + + 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 + + if return_params: + all_params[ind] = params + + if return_params: + return sigs, all_params + else: + return sigs diff --git a/neurodsp/sim/objs.py b/neurodsp/sim/objs.py new file mode 100644 index 00000000..7ed4dd68 --- /dev/null +++ b/neurodsp/sim/objs.py @@ -0,0 +1,573 @@ +"""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 param_iter, param_sampler + +################################################################################################### +################################################################################################### + +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 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 _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} + + 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 + + +# TODO / NOTES: +# - could update initialize (take in initialized sim_param object) +# - could update `register_group_iters` to take in sim_params to initialize together +class SimIters(SimParams): + """Class 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, fs): + """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 param_iter(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, fs, 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 updated function. + Each value should be a generator, to sample updated parameter values from. + + Returns + ------- + ParamSampler + Generator object for sampling simulation parameters. + """ + + return param_sampler(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 updated 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/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py new file mode 100644 index 00000000..38dbb02d --- /dev/null +++ b/neurodsp/tests/sim/test_multi.py @@ -0,0 +1,59 @@ +"""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.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(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + sigs = sim_multiple(sim_powerlaw, params, 2) + assert sigs.shape[0] == 2 + +def test_sim_across_values(): + + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + sigs = sim_across_values(sim_powerlaw, params, 2) + assert isinstance(sigs, dict) + for val in [0, 1]: + assert isinstance(sigs[0], np.ndarray) + assert sigs[0].shape[0] == 2 + sigs_arr = sim_across_values(sim_powerlaw, params, 3, 'array') + assert isinstance(sigs_arr, np.ndarray) + assert sigs_arr.shape[0:2] == (2, 3) + +def test_sim_from_sampler(): + + params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + samplers = {create_updater('exponent') : create_sampler([-2, -1, 0])} + psampler = ParamSampler(params, samplers) + + sigs = sim_from_sampler(sim_powerlaw, psampler, 2) + assert isinstance(sigs, np.ndarray) + assert sigs.shape[0] == 2 diff --git a/neurodsp/tests/sim/test_objs.py b/neurodsp/tests/sim/test_objs.py new file mode 100644 index 00000000..ba5f0449 --- /dev/null +++ b/neurodsp/tests/sim/test_objs.py @@ -0,0 +1,69 @@ +"""Tests for neurodsp.sim.objs.""" + +from neurodsp.sim.update import create_updater, create_sampler + +from neurodsp.sim.objs import * + +################################################################################################### +################################################################################################### + +def test_sim_params(): + + # Test initialization + sps1 = SimParams(5, 250) + assert sps1 + + # Test registering new simulation parameter definition + sps1.register('pl', {'sim_powerlaw' : {'exponent' : -1}}) + assert sps1['pl'] + + # Test registering a group of new simulation parameter definitions + sps2 = SimParams(5, 250) + sps2.register_group({ + 'pl' : {'sim_powerlaw' : {'exponent' : -1}}, + 'osc' : {'sim_oscillation' : {'freq' : -1}}, + }) + assert sps2['pl'] + assert sps2['osc'] + +def test_sim_iters(): + + sis1 = SimIters(5, 250) + sis1.register('pl', {'sim_powerlaw' : {'exponent' : -1}}) + sis1.register_iter('pl_exp', 'pl', 'exponent', [-2, -1, 0]) + assert sis1['pl_exp'] + + # Test registering a group of new simulation iterator definitions + sis2 = SimIters(5, 250) + sis2.register_group({ + 'pl' : {'sim_powerlaw' : {'exponent' : -1}}, + 'osc' : {'sim_oscillation' : {'freq' : -1}}, + }) + 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_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 From 693769c1ab2dd96131fd15d3fbccba9d1480434b Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 22:45:11 -0400 Subject: [PATCH 05/44] cleans / lints --- neurodsp/sim/objs.py | 8 ++++---- neurodsp/sim/update.py | 16 +++++++--------- neurodsp/tests/sim/test_update.py | 12 ++++++------ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/neurodsp/sim/objs.py b/neurodsp/sim/objs.py index 7ed4dd68..7ca56064 100644 --- a/neurodsp/sim/objs.py +++ b/neurodsp/sim/objs.py @@ -6,7 +6,7 @@ from copy import deepcopy -from neurodsp.sim.update import param_iter, param_sampler +from neurodsp.sim.update import create_param_iter, create_param_sampler ################################################################################################### ################################################################################################### @@ -328,7 +328,7 @@ def make_iter(self, label, update, values, component=None): assert label in self._params.keys(), "Label for simulation parameters not found." - return param_iter(super().__getitem__(label), update, values, component) + return create_param_iter(super().__getitem__(label), update, values, component) def register_iter(self, name, label, update, values, component=None): @@ -490,8 +490,8 @@ def make_sampler(self, label, samplers, n_samples=None): Generator object for sampling simulation parameters. """ - return param_sampler(super().__getitem__(label), samplers, - n_samples if n_samples else self.n_samples) + return create_param_sampler(super().__getitem__(label), samplers, + n_samples if n_samples else self.n_samples) def register_sampler(self, name, label, samplers): diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index bfe0b8f1..b1c11ae4 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -4,7 +4,7 @@ import numpy as np -from neurodsp.sim.sim import sig_yielder +from neurodsp.sim.multi import sig_yielder from neurodsp.utils.core import counter ################################################################################################### @@ -72,7 +72,7 @@ def create_updater(update, component=None): return updater -## PARAM YIELDER +## PARAM ITER def param_iter_yielder(sim_params, updater, values): """Parameter yielder. @@ -99,8 +99,6 @@ def param_iter_yielder(sim_params, updater, values): yield deepcopy(sim_params) -## PARAM ITER - class ParamIter(): """Object for iterating across parameter updates. @@ -174,7 +172,7 @@ def _reset_yielder(self): self.yielder = param_iter_yielder(self.params, self._updater, self.values) -def param_iter(params, update, values, component=None): +def create_param_iter(params, update, values, component=None): """Wrapper function for the ParamIter object. Parameters @@ -322,7 +320,7 @@ def _reset_yielder(self): self.yielder = param_sample_yielder(self.params, self.samplers, self.n_samples) -def param_sampler(sim_params, samplers, n_samples=None): +def create_param_sampler(sim_params, samplers, n_samples=None): """Wrapper function for the ParamSampler object. Parameters @@ -346,7 +344,7 @@ def param_sampler(sim_params, samplers, n_samples=None): return ParamSampler(sim_params, samplers, n_samples) -## SIG YIELDER / ITER +## SIG ITER class SigIter(): """Object for iterating across sampled simulations. @@ -410,7 +408,7 @@ def _reset_yielder(self): self.yielder = sig_yielder(self.sim_func, self.sim_params, self.n_sims) -def sig_iter(sim_func, sim_params, n_sims): +def create_sig_iter(sim_func, sim_params, n_sims): """Wrapper function for the SigIter object. Parameters @@ -425,7 +423,7 @@ def sig_iter(sim_func, sim_params, n_sims): Returns ------- SigIter - Iterable object for sampling simulatons. + Iterable object for sampling simulations. """ return SigIter(sim_func, sim_params, n_sims) diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py index 5990ff0f..cb425527 100644 --- a/neurodsp/tests/sim/test_update.py +++ b/neurodsp/tests/sim/test_update.py @@ -61,14 +61,14 @@ def test_class_param_iter(): assert el in params assert params['exponent'] == values[ind] -def test_param_iter(): +def test_create_param_iter(): sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} update = 'exponent' values = [-2, -1, 0] # Note: no accuracy checking here (done in `test_class_param_iter`) - piter = param_iter(sim_params, update, values) + piter = create_param_iter(sim_params, update, values) assert isinstance(piter, ParamIter) for params in piter: assert isinstance(params, dict) @@ -134,14 +134,14 @@ def test_class_param_sampler(): assert params[param] in values assert ind == 4 -def test_param_sampler(): +def test_create_param_sampler(): # Note: no accuracy checking here (done in `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 = param_sampler(params, samplers, n_samples=5) + psampler = create_param_sampler(params, samplers, n_samples=5) for ind, params in enumerate(psampler): assert isinstance(params, dict) @@ -154,11 +154,11 @@ def test_class_sig_iter(): assert isinstance(sig, np.ndarray) assert ind == 4 -def test_sig_iter(): +def test_create_sig_iter(): # Note: no accuracy checking here (done in `test_class_sig_iter`) params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - siter = sig_iter(sim_powerlaw, params, n_sims=5) + siter = create_sig_iter(sim_powerlaw, params, n_sims=5) for ind, sig in enumerate(siter): assert isinstance(sig, np.ndarray) assert ind == 4 From b26598dfd93ef040ba3b82e508c3d859f4ed9924 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 22:51:40 -0400 Subject: [PATCH 06/44] minor tutorial fixes --- tutorials/sim/plot_SimParams.py | 49 +++++++++++++++++++++++++ tutorials/sim/plot_SimulateAperiodic.py | 2 +- tutorials/sim/plot_SimulateModulated.py | 1 - 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tutorials/sim/plot_SimParams.py diff --git a/tutorials/sim/plot_SimParams.py b/tutorials/sim/plot_SimParams.py new file mode 100644 index 00000000..7c160303 --- /dev/null +++ b/tutorials/sim/plot_SimParams.py @@ -0,0 +1,49 @@ +""" +Managing Simulation Parameters +============================== + +Manage, update, and iterate across simulation parameters. +""" + +#from neurodsp.sim.update import create_updater + +from neurodsp.sim.params import SimParams + +################################################################################################### +# Parameter Updaters +# ------------------ +# +# Words, words, words +# + +################################################################################################### + +# Define an example set of parameters +params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : -1} + +################################################################################################### + +# Create a parameter updater +exp_updater = create_updater('exponent') + +# +exp_updater(params, -2) + +################################################################################################### +# Iterating Across Parameters +# --------------------------- +# +# + +################################################################################################### + +ParamIter() + +################################################################################################### + + + +################################################################################################### + + + diff --git a/tutorials/sim/plot_SimulateAperiodic.py b/tutorials/sim/plot_SimulateAperiodic.py index 099f9855..43a6479b 100644 --- a/tutorials/sim/plot_SimulateAperiodic.py +++ b/tutorials/sim/plot_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_SimulateModulated.py b/tutorials/sim/plot_SimulateModulated.py index 2b7a4d8f..a2fbcec6 100644 --- a/tutorials/sim/plot_SimulateModulated.py +++ b/tutorials/sim/plot_SimulateModulated.py @@ -3,7 +3,6 @@ ================== Apply amplitude modulation to simulated signals. - """ ################################################################################################### From 3702f27157bce06c8a94de60bbde9838084688d8 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 22:53:53 -0400 Subject: [PATCH 07/44] sim/objs -> sim/params --- neurodsp/sim/{objs.py => params.py} | 0 neurodsp/tests/sim/{test_objs.py => test_params.py} | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename neurodsp/sim/{objs.py => params.py} (100%) rename neurodsp/tests/sim/{test_objs.py => test_params.py} (97%) diff --git a/neurodsp/sim/objs.py b/neurodsp/sim/params.py similarity index 100% rename from neurodsp/sim/objs.py rename to neurodsp/sim/params.py diff --git a/neurodsp/tests/sim/test_objs.py b/neurodsp/tests/sim/test_params.py similarity index 97% rename from neurodsp/tests/sim/test_objs.py rename to neurodsp/tests/sim/test_params.py index ba5f0449..f4b409db 100644 --- a/neurodsp/tests/sim/test_objs.py +++ b/neurodsp/tests/sim/test_params.py @@ -1,8 +1,8 @@ -"""Tests for neurodsp.sim.objs.""" +"""Tests for neurodsp.sim.params.""" from neurodsp.sim.update import create_updater, create_sampler -from neurodsp.sim.objs import * +from neurodsp.sim.params import * ################################################################################################### ################################################################################################### From 1ccfba7da3b6f3878e1ab488b7fdcd04234f33f5 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 22:59:38 -0400 Subject: [PATCH 08/44] use objs instead of wrappers --- neurodsp/sim/params.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 7ca56064..a4402491 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -6,7 +6,7 @@ from copy import deepcopy -from neurodsp.sim.update import create_param_iter, create_param_sampler +from neurodsp.sim.update import ParamIter, ParamSampler ################################################################################################### ################################################################################################### @@ -47,7 +47,6 @@ def __getitem__(self, label): Label to access simulation parameters from `params`. """ - return {**self.base, **self._params[label]} @@ -328,7 +327,7 @@ def make_iter(self, label, update, values, component=None): assert label in self._params.keys(), "Label for simulation parameters not found." - return create_param_iter(super().__getitem__(label), update, values, component) + return ParamIter(super().__getitem__(label), update, values, component) def register_iter(self, name, label, update, values, component=None): @@ -490,8 +489,8 @@ def make_sampler(self, label, samplers, n_samples=None): Generator object for sampling simulation parameters. """ - return create_param_sampler(super().__getitem__(label), samplers, - n_samples if n_samples else self.n_samples) + return ParamSampler(super().__getitem__(label), samplers, + n_samples if n_samples else self.n_samples) def register_sampler(self, name, label, samplers): From 36ba78543f65a00c9a3a81047c7972c0c6cb1b68 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 13 Apr 2024 23:02:20 -0400 Subject: [PATCH 09/44] drop wrapper funcs (offer no new functionality) --- neurodsp/sim/update.py | 69 ------------------------------- neurodsp/tests/sim/test_update.py | 32 -------------- 2 files changed, 101 deletions(-) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index b1c11ae4..f8ad3ad6 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -172,30 +172,6 @@ def _reset_yielder(self): self.yielder = param_iter_yielder(self.params, self._updater, self.values) -def create_param_iter(params, update, values, component=None): - """Wrapper function for the ParamIter object. - - 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. - - Returns - ------- - ParamIter - Iterable object for iterating across parameter definitions. - """ - - return ParamIter(params, update, values, component) - - ## PARAM SAMPLERS def create_sampler(values, probs=None, n_samples=None): @@ -320,30 +296,6 @@ def _reset_yielder(self): self.yielder = param_sample_yielder(self.params, self.samplers, self.n_samples) -def create_param_sampler(sim_params, samplers, n_samples=None): - """Wrapper function for the ParamSampler object. - - 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. - - Returns - ------- - ParamSampler - Iterable object for sampling parameter definitions. - """ - - return ParamSampler(sim_params, samplers, n_samples) - - ## SIG ITER class SigIter(): @@ -406,24 +358,3 @@ def _reset_yielder(self): self.index = 0 self.yielder = sig_yielder(self.sim_func, self.sim_params, self.n_sims) - - -def create_sig_iter(sim_func, sim_params, n_sims): - """Wrapper function for the SigIter object. - - Parameters - ---------- - sim_func : callable - Function to create simulations. - sim_params : dict - Simulation parameters. - n_sims : int - Number of simulations to create. - - Returns - ------- - SigIter - Iterable object for sampling simulations. - """ - - return SigIter(sim_func, sim_params, n_sims) diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py index cb425527..4f114738 100644 --- a/neurodsp/tests/sim/test_update.py +++ b/neurodsp/tests/sim/test_update.py @@ -61,18 +61,6 @@ def test_class_param_iter(): assert el in params assert params['exponent'] == values[ind] -def test_create_param_iter(): - - sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} - update = 'exponent' - values = [-2, -1, 0] - - # Note: no accuracy checking here (done in `test_class_param_iter`) - piter = create_param_iter(sim_params, update, values) - assert isinstance(piter, ParamIter) - for params in piter: - assert isinstance(params, dict) - def test_create_sampler(): values = [-2, -1, 0] @@ -134,17 +122,6 @@ def test_class_param_sampler(): assert params[param] in values assert ind == 4 -def test_create_param_sampler(): - - # Note: no accuracy checking here (done in `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 = create_param_sampler(params, samplers, n_samples=5) - for ind, params in enumerate(psampler): - assert isinstance(params, dict) - def test_class_sig_iter(): params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} @@ -153,12 +130,3 @@ def test_class_sig_iter(): for ind, sig in enumerate(siter): assert isinstance(sig, np.ndarray) assert ind == 4 - -def test_create_sig_iter(): - - # Note: no accuracy checking here (done in `test_class_sig_iter`) - params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - siter = create_sig_iter(sim_powerlaw, params, n_sims=5) - for ind, sig in enumerate(siter): - assert isinstance(sig, np.ndarray) - assert ind == 4 From a2f2307a33ae1066bd1d10ccd4dde0ed4b770062 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 00:18:21 -0400 Subject: [PATCH 10/44] allow pass through of SimParams in derived objs --- neurodsp/sim/params.py | 37 ++++++++++++++++++++++--------- neurodsp/tests/conftest.py | 12 ++++++++++ neurodsp/tests/sim/test_params.py | 15 +++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index a4402491..66e76a68 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -24,7 +24,7 @@ class SimParams(): Attributes ---------- base : dict - Dictionary of base parameters, common across all definitions. + Dictionary of base parameters, common across all parameter definitions. params : dict Dictionary of created simulation parameter definitions. """ @@ -210,6 +210,10 @@ def _make_params(self, parameters=None, **kwargs): else: params = {**parameters, **kwargs} + # If any base parameters were passed in, clear them + for bparam in self.base.keys(): + params.pop(bparam, None) + return params @@ -242,9 +246,6 @@ def _make_combined_params(self, components, component_variances=None): return parameters -# TODO / NOTES: -# - could update initialize (take in initialized sim_param object) -# - could update `register_group_iters` to take in sim_params to initialize together class SimIters(SimParams): """Class object for managing simulation iterators. @@ -254,12 +255,20 @@ class SimIters(SimParams): Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. + sim_params : SimParams + Predefined SimParams object. + If passed in, overides use of `n_seconds`, and `fs` parameters. + Base parameters and any registered parameter definitions are added to current object. """ - def __init__(self, n_seconds, fs): + def __init__(self, n_seconds=None, fs=None, sim_params=None): """Initialize SimIters objects.""" - SimParams.__init__(self, n_seconds, fs) + if sim_params: + SimParams.__init__(self, **sim_params.base) + self.register_group(sim_params.params) + else: + SimParams.__init__(self, n_seconds, fs) self._iters = {} @@ -426,12 +435,20 @@ class SimSamplers(SimParams): n_samples : int, optional The number of parameter iterations to set as max. If None, samplers are created as infinite generators. + sim_params : SimParams + Predefined SimParams object. + If passed in, overides use of `n_seconds`, and `fs` parameters. + Base parameters and any registered parameter definitions are added to current object. """ - def __init__(self, n_seconds, fs, n_samples=None): + def __init__(self, n_seconds=None, fs=None, n_samples=None, sim_params=None): """Initialize SimSamplers objects.""" - SimParams.__init__(self, n_seconds, fs) + if sim_params: + SimParams.__init__(self, **sim_params.base) + self.register_group(sim_params.params) + else: + SimParams.__init__(self, n_seconds, fs) self.n_samples = n_samples self._samplers = {} @@ -480,7 +497,7 @@ def make_sampler(self, label, samplers, n_samples=None): Label for the simulation parameters. samplers : dict Sampler definitions to update parameters with. - Each key should be a callable, a parameter updated function. + Each key should be a callable, a parameter updater function. Each value should be a generator, to sample updated parameter values from. Returns @@ -504,7 +521,7 @@ def register_sampler(self, name, label, samplers): Label for the simulation parameters. samplers : dict Sampler definitions to update parameters with. - Each key should be a callable, a parameter updated function. + Each key should be a callable, a parameter updater function. Each value should be a generator, to sample updated parameter values from. """ diff --git a/neurodsp/tests/conftest.py b/neurodsp/tests/conftest.py index 17fcc9a7..abdf56a1 100644 --- a/neurodsp/tests/conftest.py +++ b/neurodsp/tests/conftest.py @@ -8,6 +8,7 @@ from neurodsp.sim import sim_oscillation, sim_powerlaw, sim_combined from neurodsp.spectral import compute_spectrum +from neurodsp.sim.params import SimParams from neurodsp.utils.sim import set_random_seed from neurodsp.tests.settings import (N_SECONDS, FS, FREQ_SINE, FREQ1, EXP1, BASE_TEST_FILE_PATH, TEST_PLOTS_PATH) @@ -55,6 +56,17 @@ 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', 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_params.py b/neurodsp/tests/sim/test_params.py index f4b409db..7a5bc07d 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -46,6 +46,13 @@ def test_sim_iters(): assert sis2['pl_exp'] assert sis2['osc_freq'] +def test_sim_iters_params(tsim_params): + + sim_iters = SimIters(sim_params=tsim_params) + assert sim_iters + assert sim_iters.base == tsim_params.base + assert sim_iters.params == tsim_params.params + def test_sim_samplers(): sss1 = SimSamplers(5, 250) @@ -67,3 +74,11 @@ def test_sim_samplers(): ]) assert sss2['samp_exp'] is not None assert sss2['samp_freq'] is not None + + +def test_sim_samplers_params(tsim_params): + + sim_samplers = SimSamplers(sim_params=tsim_params) + assert sim_samplers + assert sim_samplers.base == tsim_params.base + assert sim_samplers.params == tsim_params.params From 3cea869a3559b1752df8a6f7a37502f754677f45 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 02:15:38 -0400 Subject: [PATCH 11/44] add sim params tutorial --- tutorials/sim/plot_SimParams.py | 287 ++++++++++++++++++++++++++++++-- 1 file changed, 275 insertions(+), 12 deletions(-) diff --git a/tutorials/sim/plot_SimParams.py b/tutorials/sim/plot_SimParams.py index 7c160303..1db3fa87 100644 --- a/tutorials/sim/plot_SimParams.py +++ b/tutorials/sim/plot_SimParams.py @@ -5,45 +5,308 @@ Manage, update, and iterate across simulation parameters. """ -#from neurodsp.sim.update import create_updater +from neurodsp.sim.update import create_updater, create_sampler +from neurodsp.sim.params import SimParams, SimIters, SimSamplers -from neurodsp.sim.params import SimParams +################################################################################################### +# 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 ################################################################################################### -# Parameter Updaters -# ------------------ # -# Words, words, words +# 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. # ################################################################################################### -# Define an example set of parameters -params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : -1} +# Create a set of simulation parameters +sim_params.make_params({'exponent' : -1}) ################################################################################################### +# +# The object can also be used to 'register' (:method:`~.SimParams.register`) a set of +# simulation parameters, meaning they can be defined and stored in the object, +# with an associated label to access them. +# -# Create a parameter updater -exp_updater = create_updater('exponent') +################################################################################################### + +# 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 +# (:method:`~.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, inheriting from the SimParams object +sim_iters = SimIters(sim_params) + +################################################################################################### +# +# 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. # -exp_updater(params, -2) +# 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) ################################################################################################### -# Iterating Across Parameters +# 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) + +################################################################################################### +# +# 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. # ################################################################################################### -ParamIter() +# 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 base set of simulation parameters +sim_samplers = SimSamplers(sim_params=sim_params, 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) From 74d65f0868c6b981088d674d9398227d066e435d Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 02:16:29 -0400 Subject: [PATCH 12/44] add sim multi tutorial --- tutorials/sim/plot_SimMulti.py | 120 +++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tutorials/sim/plot_SimMulti.py diff --git a/tutorials/sim/plot_SimMulti.py b/tutorials/sim/plot_SimMulti.py new file mode 100644 index 00000000..a4d2aff6 --- /dev/null +++ b/tutorials/sim/plot_SimMulti.py @@ -0,0 +1,120 @@ +""" +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 simulations +# 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) + +################################################################################################### + +# 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 optional 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) + +################################################################################################### +# +# 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) + +################################################################################################### + +# Plot the set of sampled simulations +plot_multi_time_series(None, sampled_sims) From 5f6c34945f51eb1a1de93545595c5b106aac26c7 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 02:16:41 -0400 Subject: [PATCH 13/44] add new sim things to API list --- doc/api.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/api.rst b/doc/api.rst index aebb7d28..7c295246 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -313,6 +313,28 @@ Combined Signals 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 +~~~~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: neurodsp.sim.params +.. autosummary:: + :toctree: generated/ + + SimParams + SimIters + SimSamplers + Utilities ~~~~~~~~~ From de8e26993820d8f704085ca07dde8e3cb5a3d56e Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 02:17:05 -0400 Subject: [PATCH 14/44] udpate docstrings --- neurodsp/sim/multi.py | 2 +- neurodsp/sim/params.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index bd34118a..df719a59 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -94,7 +94,7 @@ def sim_multiple(sim_func, sim_params, n_sims): def sim_across_values(sim_func, sim_params, n_sims, output='dict'): - """Helper function to create a set of simulations across different parameter values. + """Simulate multiple signals across different parameter values. Parameters ---------- diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 66e76a68..37100b3c 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -247,7 +247,7 @@ def _make_combined_params(self, components, component_variances=None): class SimIters(SimParams): - """Class object for managing simulation iterators. + """Object for managing simulation iterators. Parameters ---------- From 8a8a09a1a7a81d4ef85cd530e3745d3f94ca4ae9 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 14 Apr 2024 02:24:03 -0400 Subject: [PATCH 15/44] fix object initialization --- tutorials/sim/plot_SimParams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/sim/plot_SimParams.py b/tutorials/sim/plot_SimParams.py index 1db3fa87..e2b7976f 100644 --- a/tutorials/sim/plot_SimParams.py +++ b/tutorials/sim/plot_SimParams.py @@ -116,7 +116,7 @@ ################################################################################################### # Re-initialize a SimIters object, inheriting from the SimParams object -sim_iters = SimIters(sim_params) +sim_iters = SimIters(sim_params=sim_params) ################################################################################################### # From d36656cb7b50b670030b11b7a8153246a91d0881 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 19 Jul 2024 14:26:28 -0400 Subject: [PATCH 16/44] add docstring examples --- neurodsp/sim/multi.py | 28 ++++++++++++++++++++++++++++ neurodsp/sim/update.py | 2 +- tutorials/sim/plot_SimMulti.py | 4 ++-- tutorials/sim/plot_SimParams.py | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index df719a59..b6cfd3db 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -84,6 +84,14 @@ def sim_multiple(sim_func, sim_params, n_sims): ------- sigs : 2d array Simulations, 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']]) @@ -116,6 +124,15 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): Each key is the simulation parameter value for the set of simulations. Each value is the set of simulations for that value, as [n_sims, sig_length]. If array, is all signals collected together as [n_sims, sig_length]. + + Examples + -------- + Simulate multiple powerlaw signals across a set of different simulation parameters: + + >>> from neurodsp.sim.aperiodic import sim_powerlaw + >>> 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) """ sims = {} @@ -150,6 +167,17 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_params=False): all_params : list of dict Simulation parameters for each returned time series. Only returned if `return_params` is True. + + 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 diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index f8ad3ad6..e88d42bd 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -53,7 +53,7 @@ def create_updater(update, component=None): Parameters ---------- - parameter : str + update : str Name of the parameter to update. component : str Name of the component to update the parameter within. diff --git a/tutorials/sim/plot_SimMulti.py b/tutorials/sim/plot_SimMulti.py index a4d2aff6..116e0416 100644 --- a/tutorials/sim/plot_SimMulti.py +++ b/tutorials/sim/plot_SimMulti.py @@ -15,7 +15,7 @@ # Simulate Multiple Signals Together # ---------------------------------- # -# The :func:`~.sim_multiple` function can be used to simulate multiple simulations +# The :func:`~.sim_multiple` function can be used to simulate multiple signals # from the same set of parameters. # @@ -40,7 +40,7 @@ # 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 optional a number of simulations to create, and use this object +# set of parameters, and optionally a number of simulations to create, and use this object # to yield simulated signals. # diff --git a/tutorials/sim/plot_SimParams.py b/tutorials/sim/plot_SimParams.py index e2b7976f..810b3427 100644 --- a/tutorials/sim/plot_SimParams.py +++ b/tutorials/sim/plot_SimParams.py @@ -199,7 +199,7 @@ ################################################################################################### # -# Updater can also be used to update parameters defined within specified components. +# An updater can also be used to update parameters defined within specified components. # ################################################################################################### From ec90e541958347d725e97e9fc21c4aff8b3388ef Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 19 Jul 2024 16:05:48 -0400 Subject: [PATCH 17/44] udpate API list for new functionaloity --- .gitignore | 1 + doc/api.rst | 21 +++++++++++++++++++++ neurodsp/timefrequency/__init__.py | 3 ++- tutorials/sim/plot_SimParams.py | 4 ++-- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1c4c6643..f61e9cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ doc/_build/ doc/auto_examples/ doc/auto_tutorials/ doc/generated/ +doc/sg_execution_times.rst diff --git a/doc/api.rst b/doc/api.rst index 26171433..bad927c6 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -328,6 +328,8 @@ Multiple Signals Simulation Parameters ~~~~~~~~~~~~~~~~~~~~~ +The following objects can be used to manage simulation parameters: + .. currentmodule:: neurodsp.sim.params .. autosummary:: :toctree: generated/ @@ -336,6 +338,25 @@ Simulation Parameters 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 + Utilities ~~~~~~~~~ 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/tutorials/sim/plot_SimParams.py b/tutorials/sim/plot_SimParams.py index 810b3427..c504392c 100644 --- a/tutorials/sim/plot_SimParams.py +++ b/tutorials/sim/plot_SimParams.py @@ -206,8 +206,8 @@ # Define another set of parameters, with multiple components params2 = {'n_seconds' : 5, 'fs' : 250, - 'components' : [{'sim_powerlaw' : {'exponent' : None}}, - {'sim_oscillation' : {'freq' : 10}}]} + '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') From 09f137c27656b2f3bc902184b73f766ac3a96b78 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 19 Jul 2024 16:40:10 -0400 Subject: [PATCH 18/44] fix up docs & details of sig_sampler --- neurodsp/sim/multi.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index b6cfd3db..81f91503 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -1,5 +1,7 @@ """Simulation functions that return multiple instances.""" +from collections.abc import Sized + import numpy as np from neurodsp.utils.core import counter @@ -43,7 +45,7 @@ def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None): 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, creates an infinite generator. + If None, length is defined by the length of `sim_params`, and could be infinite. Yields ------ @@ -51,9 +53,12 @@ def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None): Simulated time series. sample_params : dict Simulation parameters for the yielded time series. + Only returned if `return_sim_params` is True. """ - if len(sim_params) and n_sims and n_sims > len(sim_params): + # 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) From 4b559362608602688d9924ab2aa888f9cb66c041 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 20 Jul 2024 10:06:51 -0400 Subject: [PATCH 19/44] add SimParams to_* methods --- neurodsp/sim/params.py | 36 +++++++++++++++++++++++++ neurodsp/tests/sim/test_params.py | 44 +++++++++++++++++++++---------- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 37100b3c..bc6adc21 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -198,6 +198,42 @@ def clear(self, clear_base=False): 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 _make_params(self, parameters=None, **kwargs): """Sub-function for `make_params`.""" diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index 7a5bc07d..79580db6 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -13,32 +13,49 @@ def test_sim_params(): 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', {'sim_powerlaw' : {'exponent' : -1}}) - assert sps1['pl'] + 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' : {'sim_powerlaw' : {'exponent' : -1}}, - 'osc' : {'sim_oscillation' : {'freq' : -1}}, - }) - assert sps2['pl'] - assert sps2['osc'] + sps2.register_group({'pl' : comp1, 'osc' : comp2}) + assert comp1.items() <= sps2['pl'].items() + assert comp2.items() <= sps2['osc'].items() + +def test_sim_params_to(): + # Test the SimParams `to_` extraction functions + + sps = SimParams(5, 250) + comp = {'sim_powerlaw' : {'exponent' : -1}} + sps.register('pl', comp) + + iters = sps.to_iters() + assert iters.base == sps.base + assert comp.items() <= iters.params['pl'].items() + + samplers = sps.to_samplers(n_samples=10) + assert samplers.base == sps.base + assert comp.items() <= samplers.params['pl'].items() def test_sim_iters(): + comp_plw = {'sim_powerlaw' : {'exponent' : -1}} + comp_osc = {'sim_oscillation' : {'freq' : -1}} + sis1 = SimIters(5, 250) - sis1.register('pl', {'sim_powerlaw' : {'exponent' : -1}}) + 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' : {'sim_powerlaw' : {'exponent' : -1}}, - 'osc' : {'sim_oscillation' : {'freq' : -1}}, - }) + 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]}, @@ -75,7 +92,6 @@ def test_sim_samplers(): assert sss2['samp_exp'] is not None assert sss2['samp_freq'] is not None - def test_sim_samplers_params(tsim_params): sim_samplers = SimSamplers(sim_params=tsim_params) From 6cfae41403d9b22f6220142bdae6072e6b4cff2d Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 20 Jul 2024 10:14:28 -0400 Subject: [PATCH 20/44] drop allowing SimParams object into init of derived obks --- neurodsp/sim/params.py | 24 ++++------------------ neurodsp/tests/sim/test_params.py | 34 ++++++++----------------------- 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index bc6adc21..48228967 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -291,20 +291,12 @@ class SimIters(SimParams): Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. - sim_params : SimParams - Predefined SimParams object. - If passed in, overides use of `n_seconds`, and `fs` parameters. - Base parameters and any registered parameter definitions are added to current object. """ - def __init__(self, n_seconds=None, fs=None, sim_params=None): + def __init__(self, n_seconds=None, fs=None): """Initialize SimIters objects.""" - if sim_params: - SimParams.__init__(self, **sim_params.base) - self.register_group(sim_params.params) - else: - SimParams.__init__(self, n_seconds, fs) + SimParams.__init__(self, n_seconds, fs) self._iters = {} @@ -471,20 +463,12 @@ class SimSamplers(SimParams): n_samples : int, optional The number of parameter iterations to set as max. If None, samplers are created as infinite generators. - sim_params : SimParams - Predefined SimParams object. - If passed in, overides use of `n_seconds`, and `fs` parameters. - Base parameters and any registered parameter definitions are added to current object. """ - def __init__(self, n_seconds=None, fs=None, n_samples=None, sim_params=None): + def __init__(self, n_seconds=None, fs=None, n_samples=None): """Initialize SimSamplers objects.""" - if sim_params: - SimParams.__init__(self, **sim_params.base) - self.register_group(sim_params.params) - else: - SimParams.__init__(self, n_seconds, fs) + SimParams.__init__(self, n_seconds, fs) self.n_samples = n_samples self._samplers = {} diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index 79580db6..e8ddbfb8 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -27,20 +27,18 @@ def test_sim_params(): assert comp1.items() <= sps2['pl'].items() assert comp2.items() <= sps2['osc'].items() -def test_sim_params_to(): +def test_sim_params_to(tsim_params): # Test the SimParams `to_` extraction functions - sps = SimParams(5, 250) - comp = {'sim_powerlaw' : {'exponent' : -1}} - sps.register('pl', comp) + 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'] - iters = sps.to_iters() - assert iters.base == sps.base - assert comp.items() <= iters.params['pl'].items() - - samplers = sps.to_samplers(n_samples=10) - assert samplers.base == sps.base - assert comp.items() <= samplers.params['pl'].items() + 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(): @@ -63,13 +61,6 @@ def test_sim_iters(): assert sis2['pl_exp'] assert sis2['osc_freq'] -def test_sim_iters_params(tsim_params): - - sim_iters = SimIters(sim_params=tsim_params) - assert sim_iters - assert sim_iters.base == tsim_params.base - assert sim_iters.params == tsim_params.params - def test_sim_samplers(): sss1 = SimSamplers(5, 250) @@ -91,10 +82,3 @@ def test_sim_samplers(): ]) assert sss2['samp_exp'] is not None assert sss2['samp_freq'] is not None - -def test_sim_samplers_params(tsim_params): - - sim_samplers = SimSamplers(sim_params=tsim_params) - assert sim_samplers - assert sim_samplers.base == tsim_params.base - assert sim_samplers.params == tsim_params.params From e24a0a95d9d1794f0beb5e7a8349068af8bae22b Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 16 Aug 2024 21:42:33 -0400 Subject: [PATCH 21/44] add copy method --- neurodsp/sim/params.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 48228967..9c955ee7 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -234,6 +234,12 @@ def to_samplers(self, n_samples=None): 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`.""" From b27848ddd11ec07b58db52c5e57cab5885f4337f Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 16 Aug 2024 23:22:43 -0400 Subject: [PATCH 22/44] add new conftest objects for sim params --- neurodsp/tests/conftest.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/neurodsp/tests/conftest.py b/neurodsp/tests/conftest.py index abdf56a1..f2b82a83 100644 --- a/neurodsp/tests/conftest.py +++ b/neurodsp/tests/conftest.py @@ -7,8 +7,9 @@ import numpy as np from neurodsp.sim import sim_oscillation, sim_powerlaw, sim_combined -from neurodsp.spectral import compute_spectrum +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, BASE_TEST_FILE_PATH, TEST_PLOTS_PATH) @@ -67,6 +68,23 @@ def tsim_params(): 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.""" From 12d4c6d378768c04e45076560d1f72231fd849c9 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 16 Aug 2024 23:23:03 -0400 Subject: [PATCH 23/44] extend tests for sim params --- neurodsp/tests/sim/test_params.py | 82 ++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index e8ddbfb8..c436b6d6 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -27,8 +27,53 @@ def test_sim_params(): 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 functions + # Test the SimParams `to_` extraction methods iters = tsim_params.to_iters() assert iters.base == tsim_params.base @@ -61,11 +106,28 @@ def test_sim_iters(): 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])}) + 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 @@ -82,3 +144,19 @@ def test_sim_samplers(): ]) 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 From e2770f5e6f4c16ca860386b15be35ef9687bd6bb Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 17 Aug 2024 11:43:31 -0400 Subject: [PATCH 24/44] add docstring examples --- neurodsp/sim/update.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index e88d42bd..5dd47199 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -62,6 +62,14 @@ def create_updater(update, component=None): ------- 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: @@ -175,7 +183,7 @@ def _reset_yielder(self): ## PARAM SAMPLERS def create_sampler(values, probs=None, n_samples=None): - """Create a generator to sample from a parameter range. + """Create a generator to sample from a set of parameters. Parameters ---------- @@ -192,6 +200,16 @@ def create_sampler(values, probs=None, n_samples=None): ------ 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 From ce3b1f84304d135917c881ea268b181881aea0f4 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 17 Aug 2024 11:56:15 -0400 Subject: [PATCH 25/44] minor lints --- neurodsp/sim/params.py | 2 +- neurodsp/sim/update.py | 2 +- neurodsp/spectral/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 9c955ee7..c46ebcfc 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -253,7 +253,7 @@ def _make_params(self, parameters=None, **kwargs): params = {**parameters, **kwargs} # If any base parameters were passed in, clear them - for bparam in self.base.keys(): + for bparam in self.base: params.pop(bparam, None) return params diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index 5dd47199..c75db627 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -246,7 +246,7 @@ def param_sample_yielder(sim_params, samplers, n_samples=None): Simulation parameter definition. """ - for ind in counter(n_samples): + for _ in counter(n_samples): out_params = deepcopy(sim_params) for updater, sampler in samplers.items(): updater(out_params, next(sampler)) 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 From cd45e0f6f55ab77d2140c6bbe635d78581ba2e01 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 17 Aug 2024 13:32:17 -0400 Subject: [PATCH 26/44] add ParamIter as doc'd input to sim_multi --- neurodsp/sim/multi.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 81f91503..01276f57 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -113,7 +113,7 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): ---------- sim_func : callable Function to create the simulated time series. - sim_params : iterable or list of dict + 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. @@ -132,9 +132,16 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): Examples -------- - Simulate multiple powerlaw signals across a set of different simulation parameters: + 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) From 260188f1aadd2aa3b188223a67c12d8cfd929279 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 19 Aug 2024 13:25:42 -0400 Subject: [PATCH 27/44] add drop_base_params helper func --- neurodsp/sim/params.py | 21 +++++++++++++++++++++ neurodsp/tests/sim/test_params.py | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index c46ebcfc..7908b8ca 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -11,6 +11,8 @@ ################################################################################################### ################################################################################################### +BASE_PARAMS = ['n_seconds', 'fs'] + class SimParams(): """Object for managing simulation parameters. @@ -613,3 +615,22 @@ def clear(self, clear_samplers=True, clear_params=False, clear_base=False): if clear_params: super().clear(clear_base=clear_base) + + +## Utilities for helping with parameter management + +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/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index c436b6d6..af202b2b 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -160,3 +160,11 @@ def test_sim_samplers_upd(tsim_samplers): tsim_samplers.update_sampler('samp_exp', 'n_samples', 100) assert tsim_samplers['samp_exp'].n_samples == 100 + +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 From d7b34505e170891f5419b2e2cf2c503eae07e6f6 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 19 Aug 2024 13:35:02 -0400 Subject: [PATCH 28/44] add Simulations object --- neurodsp/sim/sims.py | 72 +++++++++++++++++++++++++++++++++ neurodsp/tests/sim/test_sims.py | 30 ++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 neurodsp/sim/sims.py create mode 100644 neurodsp/tests/sim/test_sims.py diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py new file mode 100644 index 00000000..6c138510 --- /dev/null +++ b/neurodsp/sim/sims.py @@ -0,0 +1,72 @@ +"""Objects for managing groups of simulated signals.""" + +import numpy as np + +from neurodsp.sim.params import drop_base_params + +################################################################################################### +################################################################################################### + +class Simulations(): + """Data object for a set of simulated signals. + + Parameters + ---------- + signals : 1d or 2nd array + The simulated signals, organized as [n_sims, sig_length]. + func : str + The simulation function that was used to create the simulations. + params : dict + The simulation parameters that was used to create the simulations. + + Notes + ----- + This object stores a set of simulations generated from a shared parameter definition. + """ + + def __init__(self, signals=None, func=None, parameters=None): + """Initialize Simulations object.""" + + self.signals = np.atleast_2d(signals) if signals is not None else np.array([]) + self.func = func + + self.n_seconds = None + self.fs = None + self._params = {} + if parameters is not None: + self.add_params(parameters) + + 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 params(self): + """Define the full set of simulation parameters (base + additional parameters).""" + + return {**self._base_params, **self._params} + + @property + def _base_params(self): + """Define the base parameters.""" + + return {'n_seconds' : self.n_seconds, 'fs' : self.fs} + + def add_params(self, parameters): + """Add parameter definition to object.""" + + self.n_seconds = parameters['n_seconds'] + self.fs = parameters['fs'] + self._params = drop_base_params(parameters) diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py new file mode 100644 index 00000000..36a7fd8a --- /dev/null +++ b/neurodsp/tests/sim/test_sims.py @@ -0,0 +1,30 @@ +"""Tests for neurodsp.sim.sims.""" + +import numpy as np + +from neurodsp.sim.sims 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.zeros([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 + + # Test initialization with metadata + sims_full = Simulations(sigs, 'sim_func', params) + assert len(sims_full) == n_sigs + assert sims_full.params == params From 1de2b4c02950c9094a1680bd078642534962a49d Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 19 Aug 2024 13:53:05 -0400 Subject: [PATCH 29/44] move drop_base_params func (circ import) --- neurodsp/sim/params.py | 21 --------------------- neurodsp/sim/utils.py | 20 ++++++++++++++++++++ neurodsp/tests/sim/test_params.py | 8 -------- neurodsp/tests/sim/test_utils.py | 8 ++++++++ 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 7908b8ca..c46ebcfc 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -11,8 +11,6 @@ ################################################################################################### ################################################################################################### -BASE_PARAMS = ['n_seconds', 'fs'] - class SimParams(): """Object for managing simulation parameters. @@ -615,22 +613,3 @@ def clear(self, clear_samplers=True, clear_params=False, clear_base=False): if clear_params: super().clear(clear_base=clear_base) - - -## Utilities for helping with parameter management - -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/sim/utils.py b/neurodsp/sim/utils.py index e3163148..2ca35548 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -163,3 +163,23 @@ 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 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/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index af202b2b..c436b6d6 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -160,11 +160,3 @@ def test_sim_samplers_upd(tsim_samplers): tsim_samplers.update_sampler('samp_exp', 'n_samples', 100) assert tsim_samplers['samp_exp'].n_samples == 100 - -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/sim/test_utils.py b/neurodsp/tests/sim/test_utils.py index da26f5e4..b2235ae7 100644 --- a/neurodsp/tests/sim/test_utils.py +++ b/neurodsp/tests/sim/test_utils.py @@ -34,3 +34,11 @@ def test_modulate_signal(tsig): # Check modulation passing in a 1d array directly msig2 = modulate_signal(tsig, tsig) check_sim_output(msig2) + +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 From cc3d76b25278695db102af71d5d14391eb546674 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 19 Aug 2024 13:53:25 -0400 Subject: [PATCH 30/44] use sims object in sim_multiple --- neurodsp/sim/multi.py | 19 ++++++++++++++----- neurodsp/sim/sims.py | 2 +- neurodsp/tests/sim/test_multi.py | 9 ++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 01276f57..741665d6 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -5,6 +5,7 @@ import numpy as np from neurodsp.utils.core import counter +from neurodsp.sim.sims import Simulations ################################################################################################### ################################################################################################### @@ -73,7 +74,7 @@ def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None): break -def sim_multiple(sim_func, sim_params, n_sims): +def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): """Simulate multiple samples of a specified simulation. Parameters @@ -84,11 +85,16 @@ def sim_multiple(sim_func, sim_params, n_sims): 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 : 2d array - Simulations, as [n_sims, sig length]. + sigs : Simulations or 2d array + Simulations, return type depends on `return_type` argument. + Simulated time series are organized as [n_sims, sig length]. Examples -------- @@ -103,7 +109,10 @@ def sim_multiple(sim_func, sim_params, n_sims): for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): sigs[ind, :] = sig - return sigs + if return_type == 'object': + return Simulations(sigs, sim_func, sim_params) + else: + return sigs def sim_across_values(sim_func, sim_params, n_sims, output='dict'): @@ -151,7 +160,7 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): for ind, cur_sim_params in enumerate(sim_params): label = sim_params.values[ind] if hasattr(sim_params, 'values') else ind label = label[-1] if isinstance(label, list) else label - sims[label] = sim_multiple(sim_func, cur_sim_params, n_sims) + sims[label] = sim_multiple(sim_func, cur_sim_params, n_sims, 'array') if output == 'array': sims = np.squeeze(np.array(list(sims.values()))) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index 6c138510..fb87ba0c 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -2,7 +2,7 @@ import numpy as np -from neurodsp.sim.params import drop_base_params +from neurodsp.sim.utils import drop_base_params ################################################################################################### ################################################################################################### diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index 38dbb02d..0bc34506 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -3,6 +3,7 @@ import numpy as np from neurodsp.sim.aperiodic import sim_powerlaw +from neurodsp.sim.sims import Simulations from neurodsp.sim.update import create_updater, create_sampler, ParamSampler from neurodsp.sim.multi import * @@ -32,7 +33,13 @@ def test_sig_sampler(): def test_sim_multiple(): params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - sigs = sim_multiple(sim_powerlaw, params, 2) + + sims = sim_multiple(sim_powerlaw, params, 2, 'object') + assert isinstance(sims, Simulations) + assert sims.signals.shape[0] == 2 + assert sims.params == params + + sigs = sim_multiple(sim_powerlaw, params, 2, 'array') assert sigs.shape[0] == 2 def test_sim_across_values(): From 9406855a7a30d5814655b5c0648eaaa8e13fa659 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 19 Aug 2024 14:07:07 -0400 Subject: [PATCH 31/44] add SampledSims object --- neurodsp/sim/sims.py | 59 +++++++++++++++++++++++++++++++++ neurodsp/tests/sim/test_sims.py | 25 ++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index fb87ba0c..5e2b68d0 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -70,3 +70,62 @@ def add_params(self, parameters): self.n_seconds = parameters['n_seconds'] self.fs = parameters['fs'] self._params = drop_base_params(parameters) + + +class SampledSimulations(Simulations): + """Data object for a set of simulated signals with sampled (variable) parameter definitions. + + Parameters + ---------- + signals : 2nd array + The simulated signals, organized as [n_sims, sig_length]. + func : str + The simulation function that was used to create the simulations. + params : list dict + The simulation parameters for each of the simulations. + + Notes + ----- + This object stores a set of simulations with different parameter definitions per signal. + """ + + def __init__(self, signals=None, func=None, parameters=None): + """Initialize VSims object.""" + + self._params = [] + Simulations.__init__(self, signals, func, parameters) + + def add_params(self, parameters): + """Add parameter definitions to object.""" + + self.n_seconds = parameters[0]['n_seconds'] + self.fs = parameters[0]['fs'] + + self._params = [] + for cparams in parameters: + self._params.append(drop_base_params(cparams)) + + @property + def params(self): + """Define simulation parameters (base + additional parameters) for each simulation.""" + + return [{**self._base_params, **self._params[ind]} for ind in range(len(self))] + + def add_signal(self, sig, params=None): + """Add a signal to the current object. + + Parameters + ---------- + sig : 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. + """ + + self.signals = np.vstack([self.signals, sig]) + if parameters: + self._params.append(drop_base(parameters)) + else: + assert len(self._params) == 0, 'Must add parameters if object already has them.' diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py index 36a7fd8a..1e8d60bb 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_sims.py @@ -23,8 +23,33 @@ def test_simulations(): # Test initialization with data only sims_data = Simulations(sigs) assert sims_data + assert len(sims_data) == n_sigs # Test initialization with metadata sims_full = Simulations(sigs, 'sim_func', params) assert len(sims_full) == n_sigs assert sims_full.params == 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.zeros([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 + + # Test initialization with metadata + sims_full = SampledSimulations(sigs, 'sim_func', params) + assert len(sims_full) == n_sigs == len(sims_full.params) + assert sims_full.params == params From e9534a16fd0a76828e1c97d2041c0e7ca9111ca4 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 25 Aug 2024 15:35:17 -0400 Subject: [PATCH 32/44] use SampledSims object in sim multi --- neurodsp/sim/multi.py | 19 ++++++++++--------- neurodsp/tests/sim/test_multi.py | 28 ++++++++++++++++++---------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 741665d6..8cfa303f 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.utils.core import counter -from neurodsp.sim.sims import Simulations +from neurodsp.sim.sims import Simulations, SampledSimulations ################################################################################################### ################################################################################################### @@ -167,7 +167,7 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): return sims -def sim_from_sampler(sim_func, sim_sampler, n_sims, return_params=False): +def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object', return_params=False): """Simulate a set of signals from a parameter sampler. Parameters @@ -178,8 +178,11 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_params=False): Parameter definition to sample from. n_sims : int Number of simulations to create per parameter definition. - return_params : bool, default: False - Whether to collect and return the parameters of all the generated simulations. + + return_type : {'object', 'array'} + XX + #return_params : bool, default: False + # Whether to collect and return the parameters of all the generated simulations. Returns ------- @@ -205,11 +208,9 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_params=False): 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_params: - all_params[ind] = params - - if return_params: - return sigs, all_params + if return_type == 'object': + return SampledSimulations(sigs, sim_func, all_params) else: return sigs diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index 0bc34506..d7c63c99 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -3,7 +3,7 @@ import numpy as np from neurodsp.sim.aperiodic import sim_powerlaw -from neurodsp.sim.sims import Simulations +from neurodsp.sim.sims import Simulations, SampledSimulations from neurodsp.sim.update import create_updater, create_sampler, ParamSampler from neurodsp.sim.multi import * @@ -32,15 +32,17 @@ def test_sig_sampler(): def test_sim_multiple(): + n_sims = 2 params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - sims = sim_multiple(sim_powerlaw, params, 2, 'object') - assert isinstance(sims, Simulations) - assert sims.signals.shape[0] == 2 - assert sims.params == params + 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 - sigs = sim_multiple(sim_powerlaw, params, 2, 'array') - assert sigs.shape[0] == 2 + 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(): @@ -57,10 +59,16 @@ def test_sim_across_values(): 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) - sigs = sim_from_sampler(sim_powerlaw, psampler, 2) - assert isinstance(sigs, np.ndarray) - assert sigs.shape[0] == 2 + 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 From fbddfe306cb87099924ab6a05cd0f6fe1ab9d84a Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 15:39:43 -0400 Subject: [PATCH 33/44] add get_base_params helper --- neurodsp/sim/utils.py | 17 +++++++++++++++++ neurodsp/tests/sim/test_utils.py | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index 2ca35548..b11a5c55 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -168,6 +168,23 @@ def modulate_signal(sig, modulation, fs=None, mod_params=None): 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. diff --git a/neurodsp/tests/sim/test_utils.py b/neurodsp/tests/sim/test_utils.py index b2235ae7..4467987f 100644 --- a/neurodsp/tests/sim/test_utils.py +++ b/neurodsp/tests/sim/test_utils.py @@ -35,6 +35,13 @@ def test_modulate_signal(tsig): 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} From eab63013c9460dbfa0d6a46c17080f5f8b4b1de1 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 15:39:54 -0400 Subject: [PATCH 34/44] rework details of sims obejcts --- neurodsp/sim/sims.py | 158 ++++++++++++++++++++++++-------- neurodsp/tests/sim/test_sims.py | 4 + 2 files changed, 126 insertions(+), 36 deletions(-) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index 5e2b68d0..832134e4 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -2,7 +2,7 @@ import numpy as np -from neurodsp.sim.utils import drop_base_params +from neurodsp.sim.utils import get_base_params, drop_base_params ################################################################################################### ################################################################################################### @@ -14,27 +14,25 @@ class Simulations(): ---------- signals : 1d or 2nd array The simulated signals, organized as [n_sims, sig_length]. - func : str + sim_func : str The simulation function that was used to create the simulations. params : dict - The simulation parameters that was used to create the simulations. + The simulation parameters that were used to create the simulations. Notes ----- This object stores a set of simulations generated from a shared parameter definition. """ - def __init__(self, signals=None, func=None, parameters=None): + def __init__(self, signals=None, sim_func=None, params=None): """Initialize Simulations object.""" self.signals = np.atleast_2d(signals) if signals is not None else np.array([]) - self.func = func + self.sim_func = sim_func - self.n_seconds = None - self.fs = None - self._params = {} - if parameters is not None: - self.add_params(parameters) + self._base_params = None + self._params = None + self.add_params(params) def __iter__(self): """Define iteration as stepping across individual simulated signals.""" @@ -52,6 +50,14 @@ def __len__(self): return len(self.signals) + @property + def n_seconds(self): + return self._base_params['n_seconds'] if self.has_params else None + + @property + def fs(self): + 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).""" @@ -59,17 +65,67 @@ def params(self): return {**self._base_params, **self._params} @property - def _base_params(self): - """Define the base parameters.""" + def has_params(self): + """Indicator for if the object has parameters.""" + + return bool(self._params) - return {'n_seconds' : self.n_seconds, 'fs' : self.fs} + @property + def has_signals(self): + """Indicator for if the object has signals.""" - def add_params(self, parameters): - """Add parameter definition to object.""" + return bool(len(self)) - self.n_seconds = parameters['n_seconds'] - self.fs = parameters['fs'] - self._params = drop_base_params(parameters) + 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) + + +## TEMP + + +from collections.abc import Iterable + +def listify(param, index=False): + """Check and embed a parameter into a list, if is not already in a list. + + Parameters + ---------- + param : object + Parameter to check and embed in a list, if it is not already. + index : bool, optional + If True, indexes into `param` to check the 0th element, instead of `param` itself. + This can be used for checking and embedding a list into a list. + + Returns + ------- + list + Parameter embedded in a list. + """ + + check = param[0] if index else param + + # 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(check, Iterable) or isinstance(check, str): + out = [param] + # Deal with special case of multi dimensional numpy arrays - want to embed without flattening + elif isinstance(check, np.ndarray) and np.ndim(check) > 1: + out = [param] + # If is iterable (e.g. tuple or numpy array), typecast to list + else: + out = list(param) + + return out class SampledSimulations(Simulations): @@ -79,9 +135,9 @@ class SampledSimulations(Simulations): ---------- signals : 2nd array The simulated signals, organized as [n_sims, sig_length]. - func : str + sim_func : str The simulation function that was used to create the simulations. - params : list dict + params : list of dict The simulation parameters for each of the simulations. Notes @@ -89,27 +145,60 @@ class SampledSimulations(Simulations): This object stores a set of simulations with different parameter definitions per signal. """ - def __init__(self, signals=None, func=None, parameters=None): - """Initialize VSims object.""" + def __init__(self, signals=None, sim_func=None, params=None): + """Initialize SampledSimulations object.""" - self._params = [] - Simulations.__init__(self, signals, func, parameters) + Simulations.__init__(self, signals, sim_func, params) + + @property + def n_seconds(self): + """Alias n_seconds as a property.""" - def add_params(self, parameters): - """Add parameter definitions to object.""" + return self.params[0].n_seconds if self.has_params else None - self.n_seconds = parameters[0]['n_seconds'] - self.fs = parameters[0]['fs'] + @property + def fs(self): + """Alias fs as a property.""" - self._params = [] - for cparams in parameters: - self._params.append(drop_base_params(cparams)) + return self.params[0].fs if self.has_params else None @property def params(self): """Define simulation parameters (base + additional parameters) for each simulation.""" - return [{**self._base_params, **self._params[ind]} for ind in range(len(self))] + 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: + self._base_params = base_params + self._params = cparams + + else: + assert get_base_params(params[0]) == self._base_params, \ + 'Base parameters must match the existing simulations in the object.' + self._params.extend(cparams) + + else: + assert not self._params, 'Must add parameters if object already has them.' def add_signal(self, sig, params=None): """Add a signal to the current object. @@ -125,7 +214,4 @@ def add_signal(self, sig, params=None): """ self.signals = np.vstack([self.signals, sig]) - if parameters: - self._params.append(drop_base(parameters)) - else: - assert len(self._params) == 0, 'Must add parameters if object already has them.' + self.add_params(params) diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py index 1e8d60bb..2a0d97f1 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_sims.py @@ -24,6 +24,8 @@ def test_simulations(): 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 # Test initialization with metadata sims_full = Simulations(sigs, 'sim_func', params) @@ -48,6 +50,8 @@ def test_sampled_simulations(): 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 # Test initialization with metadata sims_full = SampledSimulations(sigs, 'sim_func', params) From 91e31d8839578f536bb36af9cc517f42096d3ad7 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 15:44:48 -0400 Subject: [PATCH 35/44] add/move listify helper func --- neurodsp/sim/sims.py | 39 +------------------------------ neurodsp/tests/utils/test_core.py | 11 +++++++++ neurodsp/utils/core.py | 29 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index 832134e4..f9ad772e 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -2,6 +2,7 @@ import numpy as np +from neurodsp.utils.core import listify from neurodsp.sim.utils import get_base_params, drop_base_params ################################################################################################### @@ -90,44 +91,6 @@ def add_params(self, params): self._params = drop_base_params(params) -## TEMP - - -from collections.abc import Iterable - -def listify(param, index=False): - """Check and embed a parameter into a list, if is not already in a list. - - Parameters - ---------- - param : object - Parameter to check and embed in a list, if it is not already. - index : bool, optional - If True, indexes into `param` to check the 0th element, instead of `param` itself. - This can be used for checking and embedding a list into a list. - - Returns - ------- - list - Parameter embedded in a list. - """ - - check = param[0] if index else param - - # 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(check, Iterable) or isinstance(check, str): - out = [param] - # Deal with special case of multi dimensional numpy arrays - want to embed without flattening - elif isinstance(check, np.ndarray) and np.ndim(check) > 1: - out = [param] - # If is iterable (e.g. tuple or numpy array), typecast to list - else: - out = list(param) - - return out - - class SampledSimulations(Simulations): """Data object for a set of simulated signals with sampled (variable) parameter definitions. diff --git a/neurodsp/tests/utils/test_core.py b/neurodsp/tests/utils/test_core.py index e4ae4cfd..91566143 100644 --- a/neurodsp/tests/utils/test_core.py +++ b/neurodsp/tests/utils/test_core.py @@ -34,3 +34,14 @@ def test_counter(): if ind == 5: break assert ind == 5 + +def test_listify(): + + a1 = 1 + out1 = listify(a1) + assert isinstance(out1, list) + + a2 = [1] + out2 = listify(a2) + assert isinstance(out2, list) + assert not isinstance(out2[0], list) diff --git a/neurodsp/utils/core.py b/neurodsp/utils/core.py index 95ea189d..839d3882 100644 --- a/neurodsp/utils/core.py +++ b/neurodsp/utils/core.py @@ -1,6 +1,7 @@ """Core / internal utility functions.""" from itertools import count +from collections.abc import Iterable import numpy as np @@ -50,3 +51,31 @@ def counter(value): """ 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): + 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 From 0f6c16bb56667162fdbe11e83cc3046cb52e0d7c Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 17:17:41 -0400 Subject: [PATCH 36/44] extend tests & associated updates / fixes --- neurodsp/sim/multi.py | 4 +-- neurodsp/sim/sims.py | 46 ++++++++++++++---------- neurodsp/tests/sim/test_sims.py | 60 ++++++++++++++++++++++++++++--- neurodsp/tests/utils/test_core.py | 13 ++++--- neurodsp/utils/core.py | 2 +- 5 files changed, 95 insertions(+), 30 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 8cfa303f..17c3cbdb 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -110,7 +110,7 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): sigs[ind, :] = sig if return_type == 'object': - return Simulations(sigs, sim_func, sim_params) + return Simulations(sigs, sim_params, sim_func) else: return sigs @@ -211,6 +211,6 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object', return all_params[ind] = params if return_type == 'object': - return SampledSimulations(sigs, sim_func, all_params) + return SampledSimulations(sigs, all_params, sim_func) else: return sigs diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index f9ad772e..5105d16a 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -13,27 +13,26 @@ class Simulations(): Parameters ---------- - signals : 1d or 2nd array + signals : 1d or 2nd array, optional The simulated signals, organized as [n_sims, sig_length]. - sim_func : str - The simulation function that was used to create the simulations. - params : dict + params : dict, optional The simulation parameters that were used to create the simulations. + sim_func : str, optional + The simulation function that was used to create the simulations. Notes ----- This object stores a set of simulations generated from a shared parameter definition. """ - def __init__(self, signals=None, sim_func=None, params=None): + 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.sim_func = sim_func - self._base_params = None self._params = None self.add_params(params) + self.sim_func = sim_func def __iter__(self): """Define iteration as stepping across individual simulated signals.""" @@ -53,10 +52,14 @@ def __len__(self): @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 @@ -96,22 +99,22 @@ class SampledSimulations(Simulations): Parameters ---------- - signals : 2nd array + signals : 2nd array, optional The simulated signals, organized as [n_sims, sig_length]. - sim_func : str - The simulation function that was used to create the simulations. - params : list of dict + 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, sim_func=None, params=None): + def __init__(self, signals=None, params=None, sim_func=None): """Initialize SampledSimulations object.""" - Simulations.__init__(self, signals, sim_func, params) + Simulations.__init__(self, signals, params, sim_func) @property def n_seconds(self): @@ -152,23 +155,25 @@ def add_params(self, params): 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: - assert get_base_params(params[0]) == self._base_params, \ - 'Base parameters must match the existing simulations in the object.' self._params.extend(cparams) else: - assert not self._params, 'Must add parameters if object already has them.' + if self.has_params: + raise ValueError('Must add parameters if object already has them.') - def add_signal(self, sig, params=None): + def add_signal(self, signal, params=None): """Add a signal to the current object. Parameters ---------- - sig : 1d array + signal : 1d array A simulated signal to add to the object. params : dict, optional Parameter definition for the added signal. @@ -176,5 +181,8 @@ def add_signal(self, sig, params=None): If current object does include parameters, this input is required. """ - self.signals = np.vstack([self.signals, sig]) + try: + self.signals = np.vstack([self.signals, signal]) + except ValueError: + raise ValueError('Size of the added signal is not consistent with existing signals.') self.add_params(params) diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py index 2a0d97f1..32b2974c 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_sims.py @@ -1,5 +1,7 @@ """Tests for neurodsp.sim.sims.""" +from pytest import raises + import numpy as np from neurodsp.sim.sims import * @@ -17,7 +19,7 @@ def test_simulations(): n_seconds = 2 fs = 100 n_sigs = 2 - sigs = np.zeros([2, n_seconds * fs]) + sigs = np.ones([2, n_seconds * fs]) params = {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1} # Test initialization with data only @@ -26,11 +28,19 @@ def test_simulations(): assert len(sims_data) == n_sigs assert sims_data.n_seconds is None assert sims_data.fs is None + assert sims_data.has_signals + + # 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, 'sim_func', params) + 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(): @@ -42,7 +52,7 @@ def test_sampled_simulations(): n_seconds = 2 fs = 100 n_sigs = 2 - sigs = np.zeros([2, n_seconds * fs]) + sigs = np.ones([2, n_seconds * fs]) params = [{'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -2}, {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1}] @@ -52,8 +62,50 @@ def test_sampled_simulations(): assert len(sims_data) == n_sigs assert sims_data.n_seconds is None assert sims_data.fs is None + assert sims_data.has_signals + + # 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, 'sim_func', params) + 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) diff --git a/neurodsp/tests/utils/test_core.py b/neurodsp/tests/utils/test_core.py index 91566143..9aac5243 100644 --- a/neurodsp/tests/utils/test_core.py +++ b/neurodsp/tests/utils/test_core.py @@ -37,11 +37,16 @@ def test_counter(): def test_listify(): - a1 = 1 - out1 = listify(a1) + arg1 = 1 + out1 = listify(arg1) assert isinstance(out1, list) - a2 = [1] - out2 = listify(a2) + 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/utils/core.py b/neurodsp/utils/core.py index 839d3882..33c81230 100644 --- a/neurodsp/utils/core.py +++ b/neurodsp/utils/core.py @@ -69,7 +69,7 @@ def listify(arg): # 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): + 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: From 62e7938e7bcfe4038c340678c604c64f1dee6952 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 18:06:50 -0400 Subject: [PATCH 37/44] add MultiSims object --- neurodsp/sim/sims.py | 136 ++++++++++++++++++++++++++++++-- neurodsp/tests/sim/test_sims.py | 59 ++++++++++++++ 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index 5105d16a..272b59d2 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -1,5 +1,7 @@ """Objects for managing groups of simulated signals.""" +from itertools import repeat + import numpy as np from neurodsp.utils.core import listify @@ -66,7 +68,12 @@ def fs(self): def params(self): """Define the full set of simulation parameters (base + additional parameters).""" - return {**self._base_params, **self._params} + if self.has_params: + params = {**self._base_params, **self._params} + else: + params = None + + return params @property def has_params(self): @@ -156,8 +163,8 @@ def add_params(self, params): if not self.has_params: if len(self) > len(cparams): - msg = 'Cannot add parameters to object without existing parameter values.' - raise ValueError(msg) + msg = 'Cannot add parameters to object without existing parameter values.' + raise ValueError(msg) self._base_params = base_params self._params = cparams @@ -183,6 +190,125 @@ def add_signal(self, signal, params=None): try: self.signals = np.vstack([self.signals, signal]) - except ValueError: - raise ValueError('Size of the added signal is not consistent with existing signals.') + 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 + 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 + + 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): + self._add_simulations(csigs, cparams, cfunc) + + def _add_simulations(self, signals, params, sim_func): + """Sub-function a adding a Simulations object to current object.""" + + self.signals.append(Simulations(signals, params=params, sim_func=sim_func)) diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py index 32b2974c..56d55433 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_sims.py @@ -29,6 +29,8 @@ def test_simulations(): 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: @@ -63,6 +65,8 @@ def test_sampled_simulations(): 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: @@ -109,3 +113,58 @@ def test_sampled_simulations_add(): 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 = 2 + n_sets = 2 + sigs = np.ones([2, 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 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) From 277a569dc44c3fa81fbd300643ef5a129f7f1c39 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 21:51:21 -0400 Subject: [PATCH 38/44] updates to sims objs --- neurodsp/sim/sims.py | 21 +++++++++++++-------- neurodsp/tests/sim/test_sims.py | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/sims.py index 272b59d2..9503076f 100644 --- a/neurodsp/sim/sims.py +++ b/neurodsp/sim/sims.py @@ -292,7 +292,7 @@ def add_signals(self, signals, params=None, sim_func=None): Parameters ---------- - signals : 2d array or list of 2d array + 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. @@ -303,12 +303,17 @@ def add_signals(self, signals, params=None, sim_func=None): if signals is None: return - 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): - self._add_simulations(csigs, cparams, cfunc) + if isinstance(signals, Simulations): + self.signals.append(signals) - def _add_simulations(self, signals, params, sim_func): - """Sub-function a adding a Simulations object to current object.""" + if isinstance(signals, list): - self.signals.append(Simulations(signals, params=params, sim_func=sim_func)) + 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/tests/sim/test_sims.py b/neurodsp/tests/sim/test_sims.py index 56d55433..50606610 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_sims.py @@ -123,9 +123,9 @@ def test_multi_simulations(): # Demo data n_seconds = 2 fs = 100 - n_sigs = 2 + n_sigs = 3 n_sets = 2 - sigs = np.ones([2, n_seconds * fs]) + 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}] @@ -144,6 +144,7 @@ def test_multi_simulations(): # 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 @@ -168,3 +169,12 @@ def test_multi_simulations_add(): 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 From 68a6a6f37e739aff62fc89d788d58ff3a5de32d2 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 21:59:02 -0400 Subject: [PATCH 39/44] use object for sim_multi --- neurodsp/sim/multi.py | 49 +++++++++++++++----------------- neurodsp/tests/sim/test_multi.py | 20 +++++++------ 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 17c3cbdb..2d00c4b9 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.utils.core import counter -from neurodsp.sim.sims import Simulations, SampledSimulations +from neurodsp.sim.sims import Simulations, SampledSimulations, MultiSimulations ################################################################################################### ################################################################################################### @@ -115,7 +115,7 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): return sigs -def sim_across_values(sim_func, sim_params, n_sims, output='dict'): +def sim_across_values(sim_func, sim_params, n_sims, output='object'): """Simulate multiple signals across different parameter values. Parameters @@ -126,18 +126,16 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): Simulation parameters for `sim_func`. n_sims : int Number of simulations to create per parameter definition. - output : {'dict', 'array'} - Organization of the output for the sims. - If 'dict', stored in a dictionary, organized by simulation parameter. - If 'array', all sims are organized into a 2D array. + 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 : dict of {float : array} or array - If dict, dictionary of simulated signals, where: - Each key is the simulation parameter value for the set of simulations. - Each value is the set of simulations for that value, as [n_sims, sig_length]. - If array, is all signals collected together as [n_sims, sig_length]. + 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 -------- @@ -156,18 +154,20 @@ def sim_across_values(sim_func, sim_params, n_sims, output='dict'): >>> sigs = sim_across_values(sim_powerlaw, params, n_sims=2) """ - sims = {} + 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): - label = sim_params.values[ind] if hasattr(sim_params, 'values') else ind - label = label[-1] if isinstance(label, list) else label - sims[label] = sim_multiple(sim_func, cur_sim_params, n_sims, 'array') + sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object')) + if output == 'array': - sims = np.squeeze(np.array(list(sims.values()))) + sims = np.array([el.signals for el in sims]) return sims -def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object', return_params=False): +def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): """Simulate a set of signals from a parameter sampler. Parameters @@ -178,19 +178,16 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object', return Parameter definition to sample from. n_sims : int Number of simulations to create per parameter definition. - return_type : {'object', 'array'} - XX - #return_params : bool, default: False - # Whether to collect and return the parameters of all the generated simulations. + 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 : 2d array - Simulations, as [n_sims, sig length]. - all_params : list of dict - Simulation parameters for each returned time series. - Only returned if `return_params` is True. + sigs : SampledSimulations or 2d array + Simulations, return type depends on `return_type` argument. + If array, simulations are organized as [n_sims, sig length]. Examples -------- diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index d7c63c99..3ceb4fd6 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -3,7 +3,7 @@ import numpy as np from neurodsp.sim.aperiodic import sim_powerlaw -from neurodsp.sim.sims import Simulations, SampledSimulations +from neurodsp.sim.sims import Simulations, SampledSimulations, MultiSimulations from neurodsp.sim.update import create_updater, create_sampler, ParamSampler from neurodsp.sim.multi import * @@ -46,16 +46,20 @@ def test_sim_multiple(): def test_sim_across_values(): + n_sims = 3 params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - sigs = sim_across_values(sim_powerlaw, params, 2) - assert isinstance(sigs, dict) - for val in [0, 1]: - assert isinstance(sigs[0], np.ndarray) - assert sigs[0].shape[0] == 2 - sigs_arr = sim_across_values(sim_powerlaw, params, 3, 'array') + + 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] == (2, 3) + assert sigs_arr.shape[0:2] == (len(params), n_sims) def test_sim_from_sampler(): From 3a629dadc0888dcfe2e3b5f093b0257b3c448a92 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 22:09:19 -0400 Subject: [PATCH 40/44] sim/sims - sim/signals --- doc/api.rst | 13 +++++++++++++ neurodsp/sim/multi.py | 2 +- neurodsp/sim/{sims.py => signals.py} | 0 neurodsp/tests/sim/test_multi.py | 2 +- .../tests/sim/{test_sims.py => test_signals.py} | 4 ++-- 5 files changed, 17 insertions(+), 4 deletions(-) rename neurodsp/sim/{sims.py => signals.py} (100%) rename neurodsp/tests/sim/{test_sims.py => test_signals.py} (98%) diff --git a/doc/api.rst b/doc/api.rst index bad927c6..2f6278c8 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -357,6 +357,19 @@ The following functions can be used to update simulation parameters: 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/multi.py b/neurodsp/sim/multi.py index 2d00c4b9..792da25c 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.utils.core import counter -from neurodsp.sim.sims import Simulations, SampledSimulations, MultiSimulations +from neurodsp.sim.signals import Simulations, SampledSimulations, MultiSimulations ################################################################################################### ################################################################################################### diff --git a/neurodsp/sim/sims.py b/neurodsp/sim/signals.py similarity index 100% rename from neurodsp/sim/sims.py rename to neurodsp/sim/signals.py diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index 3ceb4fd6..eef94727 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -3,8 +3,8 @@ import numpy as np from neurodsp.sim.aperiodic import sim_powerlaw -from neurodsp.sim.sims import Simulations, SampledSimulations, MultiSimulations from neurodsp.sim.update import create_updater, create_sampler, ParamSampler +from neurodsp.sim.signals import Simulations, SampledSimulations, MultiSimulations from neurodsp.sim.multi import * diff --git a/neurodsp/tests/sim/test_sims.py b/neurodsp/tests/sim/test_signals.py similarity index 98% rename from neurodsp/tests/sim/test_sims.py rename to neurodsp/tests/sim/test_signals.py index 50606610..1cc61758 100644 --- a/neurodsp/tests/sim/test_sims.py +++ b/neurodsp/tests/sim/test_signals.py @@ -1,10 +1,10 @@ -"""Tests for neurodsp.sim.sims.""" +"""Tests for neurodsp.sim.signals.""" from pytest import raises import numpy as np -from neurodsp.sim.sims import * +from neurodsp.sim.signals import * ################################################################################################### ################################################################################################### From 7792e7e33a36b2c5b4757fff8a2b6f0f6ec8819d Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 22:18:48 -0400 Subject: [PATCH 41/44] order sim tutorials --- .../sim/{plot_SimulatePeriodic.py => plot_01_SimulatePeriodic.py} | 0 .../{plot_SimulateAperiodic.py => plot_02_SimulateAperiodic.py} | 0 .../sim/{plot_SimulateCombined.py => plot_03_SimulateCombined.py} | 0 .../{plot_SimulateModulated.py => plot_04_SimulateModulated.py} | 0 .../{plot_SimulateTransients.py => plot_05_SimulateTransients.py} | 0 tutorials/sim/{plot_SimParams.py => plot_06_SimParams.py} | 0 tutorials/sim/{plot_SimMulti.py => plot_07_SimMulti.py} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename tutorials/sim/{plot_SimulatePeriodic.py => plot_01_SimulatePeriodic.py} (100%) rename tutorials/sim/{plot_SimulateAperiodic.py => plot_02_SimulateAperiodic.py} (100%) rename tutorials/sim/{plot_SimulateCombined.py => plot_03_SimulateCombined.py} (100%) rename tutorials/sim/{plot_SimulateModulated.py => plot_04_SimulateModulated.py} (100%) rename tutorials/sim/{plot_SimulateTransients.py => plot_05_SimulateTransients.py} (100%) rename tutorials/sim/{plot_SimParams.py => plot_06_SimParams.py} (100%) rename tutorials/sim/{plot_SimMulti.py => plot_07_SimMulti.py} (100%) 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 100% rename from tutorials/sim/plot_SimulateAperiodic.py rename to tutorials/sim/plot_02_SimulateAperiodic.py 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 100% rename from tutorials/sim/plot_SimulateModulated.py rename to tutorials/sim/plot_04_SimulateModulated.py 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_SimParams.py b/tutorials/sim/plot_06_SimParams.py similarity index 100% rename from tutorials/sim/plot_SimParams.py rename to tutorials/sim/plot_06_SimParams.py diff --git a/tutorials/sim/plot_SimMulti.py b/tutorials/sim/plot_07_SimMulti.py similarity index 100% rename from tutorials/sim/plot_SimMulti.py rename to tutorials/sim/plot_07_SimMulti.py From da362f7bd09300eeed7b9f6fd9ddb9716d9925ba Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 23:30:44 -0400 Subject: [PATCH 42/44] update tutorials for sim signal objects --- tutorials/sim/plot_06_SimParams.py | 12 +++++----- tutorials/sim/plot_07_SimMulti.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/tutorials/sim/plot_06_SimParams.py b/tutorials/sim/plot_06_SimParams.py index c504392c..8044bc6a 100644 --- a/tutorials/sim/plot_06_SimParams.py +++ b/tutorials/sim/plot_06_SimParams.py @@ -36,7 +36,7 @@ ################################################################################################### # -# The object can also be used to 'register' (:method:`~.SimParams.register`) a set of +# 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. # @@ -73,7 +73,7 @@ # # To manage multiple parameters, they can all be registered to the object. # For convenience, multiple definitions can be registered together with the -# (:method:`~.SimParams.register_group`) method. +# (:func:`~.SimParams.register_group`) method. # ################################################################################################### @@ -115,8 +115,8 @@ ################################################################################################### -# Re-initialize a SimIters object, inheriting from the SimParams object -sim_iters = SimIters(sim_params=sim_params) +# Re-initialize a SimIters object, exporting from existing SimParams object +sim_iters = sim_params.to_iters() ################################################################################################### # @@ -251,8 +251,8 @@ ################################################################################################### -# Initialize base set of simulation parameters -sim_samplers = SimSamplers(sim_params=sim_params, n_samples=3) +# Initialize simulation samplers, from pre-initialized SimParams object +sim_samplers = sim_params.to_samplers(n_samples=3) ################################################################################################### # diff --git a/tutorials/sim/plot_07_SimMulti.py b/tutorials/sim/plot_07_SimMulti.py index 116e0416..21df0f7c 100644 --- a/tutorials/sim/plot_07_SimMulti.py +++ b/tutorials/sim/plot_07_SimMulti.py @@ -27,6 +27,17 @@ # 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 @@ -77,6 +88,18 @@ # 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 @@ -114,6 +137,20 @@ # 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 From c56a1dc9ae611360ffb319ab6b14dcf620ccbaa8 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 28 Aug 2024 23:31:11 -0400 Subject: [PATCH 43/44] allow for callable to be pased for sim_func --- neurodsp/sim/signals.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 9503076f..5933b7c8 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -19,8 +19,9 @@ class Simulations(): 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, optional + 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 ----- @@ -34,7 +35,7 @@ def __init__(self, signals=None, params=None, sim_func=None): self._base_params = None self._params = None self.add_params(params) - self.sim_func = sim_func + self.sim_func = sim_func.__name__ if callable(sim_func) else sim_func def __iter__(self): """Define iteration as stepping across individual simulated signals.""" From 33c8c47a096424d8f34f1bff369e98d296175789 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 1 Sep 2024 11:14:06 -0400 Subject: [PATCH 44/44] update sim init --- neurodsp/sim/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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