From d63dadbb36ca8690ab39cd367f1e386e616c307c Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 2 Sep 2024 10:42:47 -0400 Subject: [PATCH 01/33] SampledSims -> VarSims --- doc/api.rst | 2 +- neurodsp/sim/multi.py | 8 ++++---- neurodsp/sim/signals.py | 17 ++++++++++------- neurodsp/tests/sim/test_multi.py | 4 ++-- neurodsp/tests/sim/test_signals.py | 22 +++++++++++----------- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index e970ff0d..b0aaf5e8 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -370,7 +370,7 @@ The following objects can be used to manage groups of simulated signals: :toctree: generated/ Simulations - SampledSimulations + VariableSimulations MultiSimulations Utilities diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index f0020866..a02eb6f4 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.signals import Simulations, SampledSimulations, MultiSimulations +from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations ################################################################################################### ################################################################################################### @@ -180,12 +180,12 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): Number of simulations to create per parameter definition. return_type : {'object', 'array'} Specifies the return type of the simulations. - If 'object', returns simulations and metadata in a 'SampledSimulations' object. + If 'object', returns simulations and metadata in a 'VariableSimulations' object. If 'array', returns the simulations (no metadata) in an array. Returns ------- - sigs : SampledSimulations or 2d array + sigs : VariableSimulations or 2d array Simulations, return type depends on `return_type` argument. If array, simulations are organized as [n_sims, sig length]. @@ -208,6 +208,6 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): all_params[ind] = params if return_type == 'object': - return SampledSimulations(sigs, all_params, sim_func) + return VariableSimulations(sigs, all_params, sim_func) else: return sigs diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 5933b7c8..90634f56 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -102,8 +102,8 @@ def add_params(self, params): self._params = drop_base_params(params) -class SampledSimulations(Simulations): - """Data object for a set of simulated signals with sampled (variable) parameter definitions. +class VariableSimulations(Simulations): + """Data object for a set of simulated signals with variable parameter definitions. Parameters ---------- @@ -189,11 +189,14 @@ def add_signal(self, signal, params=None): If current object does include parameters, this input is required. """ - try: - self.signals = np.vstack([self.signals, signal]) - except ValueError as array_value_error: - msg = 'Size of the added signal is not consistent with existing signals.' - raise ValueError(msg) from array_value_error + if not self.signals.size: + self.signals = signal + else: + try: + self.signals = np.vstack([self.signals, signal]) + except ValueError as array_value_error: + msg = 'Size of the added signal is not consistent with existing signals.' + raise ValueError(msg) from array_value_error self.add_params(params) diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index eef94727..537cdea0 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -4,7 +4,7 @@ from neurodsp.sim.aperiodic import sim_powerlaw from neurodsp.sim.update import create_updater, create_sampler, ParamSampler -from neurodsp.sim.signals import Simulations, SampledSimulations, MultiSimulations +from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.multi import * @@ -69,7 +69,7 @@ def test_sim_from_sampler(): psampler = ParamSampler(params, samplers) sims_obj = sim_from_sampler(sim_powerlaw, psampler, n_sims, 'object') - assert isinstance(sims_obj, SampledSimulations) + assert isinstance(sims_obj, VariableSimulations) assert sims_obj.signals.shape[0] == n_sims assert len(sims_obj.params) == n_sims diff --git a/neurodsp/tests/sim/test_signals.py b/neurodsp/tests/sim/test_signals.py index 1cc61758..c68e1147 100644 --- a/neurodsp/tests/sim/test_signals.py +++ b/neurodsp/tests/sim/test_signals.py @@ -44,11 +44,11 @@ def test_simulations(): assert sims_full.has_signals assert sims_full.has_params -def test_sampled_simulations(): +def test_variable_simulations(): # Test empty initialization - sims_empty = SampledSimulations() - assert isinstance(sims_empty, SampledSimulations) + sims_empty = VariableSimulations() + assert isinstance(sims_empty, VariableSimulations) # Demo data n_seconds = 2 @@ -59,7 +59,7 @@ def test_sampled_simulations(): {'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1}] # Test initialization with data only - sims_data = SampledSimulations(sigs) + sims_data = VariableSimulations(sigs) assert sims_data assert len(sims_data) == n_sigs assert sims_data.n_seconds is None @@ -74,24 +74,24 @@ def test_sampled_simulations(): assert np.all(sims_data[0]) # Test initialization with metadata - sims_full = SampledSimulations(sigs, params, 'sim_func') + sims_full = VariableSimulations(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(): +def test_variable_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 = VariableSimulations(sig) sims_data1.add_signal(sig) assert sims_data1.has_signals - sims_data2 = SampledSimulations(sig, params) + sims_data2 = VariableSimulations(sig, params) sims_data2.add_signal(sig, params) assert sims_data2.has_signals assert sims_data2.has_params @@ -100,17 +100,17 @@ def test_sampled_simulations_add(): ## ERROR CHECKS # Adding parameters with different base parameters - sims_data3 = SampledSimulations(sig, params) + sims_data3 = VariableSimulations(sig, params) with raises(ValueError): sims_data3.add_signal(sig2, params2) # Adding parameters without previous parameters - sims_data4 = SampledSimulations(sig) + sims_data4 = VariableSimulations(sig) with raises(ValueError): sims_data4.add_signal(sig, params) # Not adding parameters with previous parameters - sims_data4 = SampledSimulations(sig, params) + sims_data4 = VariableSimulations(sig, params) with raises(ValueError): sims_data4.add_signal(sig) From 2b941316869d12399dea1b0b7ee353833a42c636 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 2 Sep 2024 11:01:33 -0400 Subject: [PATCH 02/33] add separate generators file (circ imps) --- neurodsp/sim/generators.py | 71 +++++++++++++++++++++++++++ neurodsp/sim/multi.py | 68 +------------------------ neurodsp/sim/utils.py | 2 +- neurodsp/tests/sim/test_generators.py | 29 +++++++++++ neurodsp/tests/sim/test_multi.py | 19 ------- 5 files changed, 102 insertions(+), 87 deletions(-) create mode 100644 neurodsp/sim/generators.py create mode 100644 neurodsp/tests/sim/test_generators.py diff --git a/neurodsp/sim/generators.py b/neurodsp/sim/generators.py new file mode 100644 index 00000000..bfd64ca5 --- /dev/null +++ b/neurodsp/sim/generators.py @@ -0,0 +1,71 @@ +"""Generator simulation functions.""" + +from collections.abc import Sized + +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, length is defined by the length of `sim_params`, and could be infinite. + + Yields + ------ + sig : 1d array + Simulated time series. + sample_params : dict + Simulation parameters for the yielded time series. + Only returned if `return_sim_params` is True. + """ + + # If `sim_params` has a size, and `n_sims` is defined, check that they are compatible + # To do so, we first check if the iterable has a __len__ attr, and if so check values + if isinstance(sim_params, Sized) and len(sim_params) and n_sims and n_sims > len(sim_params): + msg = 'Cannot simulate the requested number of sims with the given parameters.' + raise ValueError(msg) + + for ind, sample_params in zip(counter(n_sims), sim_params): + + if return_sim_params: + yield sim_func(**sample_params), sample_params + else: + yield sim_func(**sample_params) + + if n_sims and ind >= n_sims: + break diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index a02eb6f4..1b2ca32b 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -1,79 +1,13 @@ """Simulation functions that return multiple instances.""" -from collections.abc import Sized - import numpy as np -from neurodsp.utils.core import counter from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations +from neurodsp.sim.generators import sig_yielder, sig_sampler ################################################################################################### ################################################################################################### -def sig_yielder(sim_func, sim_params, n_sims): - """Generator to yield simulated signals from a given simulation function and parameters. - - Parameters - ---------- - sim_func : callable - Function to create the simulated time series. - sim_params : dict - The parameters for the simulated signal, passed into `sim_func`. - n_sims : int, optional - Number of simulations to set as the max. - If None, creates an infinite generator. - - Yields - ------ - sig : 1d array - Simulated time series. - """ - - for _ in counter(n_sims): - yield sim_func(**sim_params) - - -def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None): - """Generator to yield simulated signals from a parameter sampler. - - Parameters - ---------- - sim_func : callable - Function to create the simulated time series. - sim_params : iterable - The parameters for the simulated signal, passed into `sim_func`. - return_sim_params : bool, optional, default: False - Whether to yield the simulation parameters as well as the simulated time series. - n_sims : int, optional - Number of simulations to set as the max. - If None, length is defined by the length of `sim_params`, and could be infinite. - - Yields - ------ - sig : 1d array - Simulated time series. - sample_params : dict - Simulation parameters for the yielded time series. - Only returned if `return_sim_params` is True. - """ - - # If `sim_params` has a size, and `n_sims` is defined, check that they are compatible - # To do so, we first check if the iterable has a __len__ attr, and if so check values - if isinstance(sim_params, Sized) and len(sim_params) and n_sims and n_sims > len(sim_params): - msg = 'Cannot simulate the requested number of sims with the given parameters.' - raise ValueError(msg) - - for ind, sample_params in zip(counter(n_sims), sim_params): - - if return_sim_params: - yield sim_func(**sample_params), sample_params - else: - yield sim_func(**sample_params) - - if n_sims and ind >= n_sims: - break - - def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): """Simulate multiple samples of a specified simulation. diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index b11a5c55..6ca5f59a 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -2,8 +2,8 @@ import numpy as np -from neurodsp.sim.info import get_sim_func from neurodsp.utils.data import compute_nseconds +from neurodsp.sim.info import get_sim_func ################################################################################################### ################################################################################################### diff --git a/neurodsp/tests/sim/test_generators.py b/neurodsp/tests/sim/test_generators.py new file mode 100644 index 00000000..df275120 --- /dev/null +++ b/neurodsp/tests/sim/test_generators.py @@ -0,0 +1,29 @@ +"""Tests for neurodsp.sim.generators.""" + +import numpy as np + +from neurodsp.sim.aperiodic import sim_powerlaw + +from neurodsp.sim.generators 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 diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index 537cdea0..ac064057 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -11,25 +11,6 @@ ################################################################################################### ################################################################################################### -def test_sig_yielder(): - - params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - yielder = sig_yielder(sim_powerlaw, params, 2) - - for ind, sig in enumerate(yielder): - assert isinstance(sig, np.ndarray) - assert ind == 1 - -def test_sig_sampler(): - - params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, - {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - sampler = sig_sampler(sim_powerlaw, params) - - for ind, sig in enumerate(sampler): - assert isinstance(sig, np.ndarray) - assert ind == 1 - def test_sim_multiple(): n_sims = 2 From 93e1412a85c8001e24afcf4f041ec342b831b12e Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 2 Sep 2024 11:11:05 -0400 Subject: [PATCH 03/33] add test for sim_across_values with ParamIter --- neurodsp/sim/multi.py | 8 +++++--- neurodsp/tests/sim/test_multi.py | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 1b2ca32b..491e80e9 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -4,6 +4,7 @@ from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler +from neurodsp.sim.update import ParamIter ################################################################################################### ################################################################################################### @@ -39,7 +40,7 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): >>> sigs = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sigs = np.zeros([n_sims, sim_params['n_seconds'] * sim_params['fs']]) + sigs = np.zeros([n_sims, int(sim_params['n_seconds'] * sim_params['fs'])]) for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): sigs[ind, :] = sig @@ -88,8 +89,9 @@ def sim_across_values(sim_func, sim_params, n_sims, output='object'): >>> sigs = sim_across_values(sim_powerlaw, params, n_sims=2) """ - update = sim_params.update if \ - not isinstance(sim_params, dict) and hasattr(sim_params, 'update') else None + update = None + if isinstance(sim_params, ParamIter): + update = sim_params.update sims = MultiSimulations(update=update) for ind, cur_sim_params in enumerate(sim_params): diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index ac064057..ef610ab8 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -25,7 +25,7 @@ def test_sim_multiple(): assert isinstance(sims_arr, np.ndarray) assert sims_arr.shape[0] == n_sims -def test_sim_across_values(): +def test_sim_across_values(tsim_iters): n_sims = 3 params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, @@ -42,6 +42,13 @@ def test_sim_across_values(): assert isinstance(sigs_arr, np.ndarray) assert sigs_arr.shape[0:2] == (len(params), n_sims) + # Test with ParamIter input + siter = tsim_iters['pl_exp'] + sims_iter = sim_across_values(sim_powerlaw, siter, n_sims) + assert isinstance(sims_iter, MultiSimulations) + assert sims_iter.update == siter.update + assert sims_iter.values == siter.values + def test_sim_from_sampler(): n_sims = 2 From 0ffc1de146892a0aeec5385c6e930ff4e326f624 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 5 Sep 2024 23:30:12 -0400 Subject: [PATCH 04/33] add get_param_values helper func --- neurodsp/sim/utils.py | 31 ++++++++++++++++++++++++++++++- neurodsp/tests/sim/test_utils.py | 14 ++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index 6ca5f59a..71386952 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -196,7 +196,36 @@ def drop_base_params(params): Returns ------- params : dict - Parameter definition, exluding base parameters. + Parameter definition, excluding base parameters. """ return {key : value for key, value in params.items() if key not in BASE_PARAMS} + + +def get_param_values(params, extract=None, component=None): + """Get a set of parameter values from a set of parameter definitions. + + Parameters + ---------- + params : list of dict + Parameter definitions for multiple simulations. + update : str + Name of the parameter to extract. + component : str, optional + Which component to extract the parameter from. + Only used if the parameter definition is for a multi-component simulation. + + Returns + ------- + values : list + Extracted parameter values. + """ + + if component: + values =[cparams['components'][component][update] for cparams in params] + elif update: + values = [cparams[update] for cparams in params] + else: + values = None + + return values diff --git a/neurodsp/tests/sim/test_utils.py b/neurodsp/tests/sim/test_utils.py index 4467987f..1da9fc04 100644 --- a/neurodsp/tests/sim/test_utils.py +++ b/neurodsp/tests/sim/test_utils.py @@ -49,3 +49,17 @@ def test_drop_base_params(): for bparam in BASE_PARAMS: assert bparam not in out1 assert 'exponent' in params + +def test_get_param_values(): + + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + assert get_param_values(params, 'exponent') == [-2, -1] + assert get_param_values(params, 'n_seconds') == [2, 2] + + params = [{'n_seconds' : 2, 'fs' : 250, 'components' : \ + {'sim_powerlaw' : {'exponent' : -2}, 'sim_oscillation' : {'freq' : 10}}}, + {'n_seconds' : 2, 'fs' : 250, 'components' : \ + {'sim_powerlaw' : {'exponent' : -1}, 'sim_oscillation' : {'freq' : 10}}}] + assert get_param_values(params, 'exponent', 'sim_powerlaw') == [-2, -1] + assert get_param_values(params, 'freq', 'sim_oscillation') == [10, 10] From 9678efc9783cfb00f9ead69c9e7a65eb4635ac9b Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 5 Sep 2024 23:42:24 -0400 Subject: [PATCH 05/33] update arg name --- neurodsp/sim/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index 71386952..a8dc0316 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -209,7 +209,7 @@ def get_param_values(params, extract=None, component=None): ---------- params : list of dict Parameter definitions for multiple simulations. - update : str + extract : str Name of the parameter to extract. component : str, optional Which component to extract the parameter from. @@ -222,9 +222,9 @@ def get_param_values(params, extract=None, component=None): """ if component: - values =[cparams['components'][component][update] for cparams in params] - elif update: - values = [cparams[update] for cparams in params] + values =[cparams['components'][component][extract] for cparams in params] + elif extract: + values = [cparams[extract] for cparams in params] else: values = None From bc544305a2b26164f9aef3c17119b54179a8ad66 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 5 Sep 2024 23:45:16 -0400 Subject: [PATCH 06/33] interim upd: add udpate & comp to sim objs --- neurodsp/sim/multi.py | 6 +++--- neurodsp/sim/signals.py | 34 +++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 491e80e9..e3e37adc 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -89,11 +89,11 @@ def sim_across_values(sim_func, sim_params, n_sims, output='object'): >>> sigs = sim_across_values(sim_powerlaw, params, n_sims=2) """ - update = None + oparams = {'update' : None, 'component' : None} if isinstance(sim_params, ParamIter): - update = sim_params.update + oparams = {'update' : sim_params.update, 'component' : sim_params.component} - sims = MultiSimulations(update=update) + sims = MultiSimulations(**oparams) for ind, cur_sim_params in enumerate(sim_params): sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object')) diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 90634f56..73fe4df6 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.utils.core import listify -from neurodsp.sim.utils import get_base_params, drop_base_params +from neurodsp.sim.utils import get_base_params, drop_base_params, get_param_values ################################################################################################### ################################################################################################### @@ -113,16 +113,23 @@ class VariableSimulations(Simulations): The simulation parameters for each of the simulations. sim_func : str, optional The simulation function that was used to create the simulations. + update : str + The name of the parameter that is updated across simulations. + component : str + Which component the updated parameter is part of. + Only used if the parameter definition is for a multi-component simulation. Notes ----- This object stores a set of simulations with different parameter definitions per signal. """ - def __init__(self, signals=None, params=None, sim_func=None): + def __init__(self, signals=None, params=None, sim_func=None, update=None, component=None): """Initialize SampledSimulations object.""" Simulations.__init__(self, signals, params, sim_func) + self.update = update + self.component = component @property def n_seconds(self): @@ -147,6 +154,12 @@ def params(self): return params + @property + def values(self): + """Alias in the parameter definition of the parameter that varies across the sets.""" + + return get_param_values(self.params, self.update, self.component) + def add_params(self, params): """Add parameter definition(s) to object. @@ -163,7 +176,7 @@ def add_params(self, params): cparams = [drop_base_params(el) for el in params] if not self.has_params: - if len(self) > len(cparams): + if len(self) > 1 and len(self) > len(cparams): msg = 'Cannot add parameters to object without existing parameter values.' raise ValueError(msg) self._base_params = base_params @@ -190,7 +203,7 @@ def add_signal(self, signal, params=None): """ if not self.signals.size: - self.signals = signal + self.signals = np.atleast_2d(signal) else: try: self.signals = np.vstack([self.signals, signal]) @@ -213,18 +226,22 @@ class MultiSimulations(): 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. + component : str + Which component the updated parameter is part of. + Only used if the parameter definition is for a multi-component simulation. 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): + def __init__(self, signals=None, params=None, sim_func=None, update=None, component=None): """Initialize MultiSimulations object.""" self.signals = [] self.add_signals(signals, params, sim_func) self.update = update + self.component = component def __iter__(self): """Define iteration as stepping across sets of simulated signals.""" @@ -272,12 +289,7 @@ def params(self): 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 + return get_param_values(self.params, self.update, self.component) @property def _base_params(self): From 0b11fdf85b67839c444c75a643623d5514738457 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 01:06:33 -0400 Subject: [PATCH 07/33] add BaseUpdater --- neurodsp/sim/update.py | 55 ++++++++++++++++++++++++------- neurodsp/tests/sim/test_update.py | 7 ++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index c75db627..ccb3a9dc 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -4,12 +4,37 @@ import numpy as np -from neurodsp.sim.multi import sig_yielder +from neurodsp.sim.generators import sig_yielder +from neurodsp.sim.utils import get_base_params from neurodsp.utils.core import counter ################################################################################################### ################################################################################################### +## BASE OBJECTS + +class BaseUpdater(): + """Base object for managing parameter and signal update objects. + + Parameters + ---------- + params : dict + Parameter definition. + """ + + def __init__(self, params): + """Initialize BaseUpdater object.""" + + self.params = deepcopy(params) + + + @property + def base(self): + """Alias in base parameters as property attribute.""" + + return get_base_params(self.params) + + ## PARAM UPDATERS def param_updater(parameter): @@ -107,7 +132,7 @@ def param_iter_yielder(sim_params, updater, values): yield deepcopy(sim_params) -class ParamIter(): +class ParamIter(BaseUpdater): """Object for iterating across parameter updates. Parameters @@ -133,14 +158,13 @@ class ParamIter(): def __init__(self, params, update, values, component=None): """Initialize parameter iteration object.""" - params = deepcopy(params) + BaseUpdater.__init__(self, 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 @@ -254,7 +278,7 @@ def param_sample_yielder(sim_params, samplers, n_samples=None): yield out_params -class ParamSampler(): +class ParamSampler(BaseUpdater): """Object for sampling parameter definitions. Parameters @@ -280,7 +304,8 @@ class ParamSampler(): def __init__(self, params, samplers, n_samples=None): """Initialize parameter sampler object.""" - self.params = deepcopy(params) + BaseUpdater.__init__(self, params) + self.samplers = samplers self.n_samples = n_samples @@ -314,16 +339,23 @@ def _reset_yielder(self): self.yielder = param_sample_yielder(self.params, self.samplers, self.n_samples) + @property + def base(self): + """Alias in base parameters as property attribute.""" + + return get_base_params(self.params) + + ## SIG ITER -class SigIter(): +class SigIter(BaseUpdater): """Object for iterating across sampled simulations. Parameters ---------- sim_func : callable Function to create simulations. - sim_params : dict + params : dict Simulation parameters. n_sims : int, optional Number of simulations to create. @@ -337,11 +369,12 @@ class SigIter(): Generator for sampling the sig iterations. """ - def __init__(self, sim_func, sim_params, n_sims=None): + def __init__(self, sim_func, params, n_sims=None): """Initialize signal iteration object.""" + BaseUpdater.__init__(self, params) + self.sim_func = sim_func - self.sim_params = deepcopy(sim_params) self.n_sims = n_sims self.index = 0 @@ -375,4 +408,4 @@ def _reset_yielder(self): """Reset the object yielder.""" self.index = 0 - self.yielder = sig_yielder(self.sim_func, self.sim_params, self.n_sims) + self.yielder = sig_yielder(self.sim_func, self.params, self.n_sims) diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py index 4f114738..c205af31 100644 --- a/neurodsp/tests/sim/test_update.py +++ b/neurodsp/tests/sim/test_update.py @@ -7,6 +7,13 @@ ################################################################################################### ################################################################################################### +def test_class_base_updater(): + + params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} + obj = BaseUpdater(params) + assert obj + assert obj.base + def test_param_updater(): params = {'n_seconds' : 10, 'fs' : 250, 'exponent' : None} From 9b4145ed10d90779f61430feb318795c08ee99b3 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 10:35:33 -0400 Subject: [PATCH 08/33] move param utils to params --- neurodsp/sim/params.py | 105 +++++++++++++++++++++++++++++- neurodsp/sim/utils.py | 66 ------------------- neurodsp/tests/sim/test_params.py | 48 ++++++++++++++ neurodsp/tests/sim/test_utils.py | 29 --------- 4 files changed, 151 insertions(+), 97 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index c46ebcfc..44f703d3 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -6,11 +6,108 @@ from copy import deepcopy -from neurodsp.sim.update import ParamIter, ParamSampler - ################################################################################################### ################################################################################################### +BASE_PARAMS = ['n_seconds', 'fs'] + +## SIMULATION PARAMETER FUNCTIONS + +def get_base_params(params): + """Get base parameters from a parameter definition. + + Parameters + ---------- + params : dict or list of dict or Object + Parameter definition. + + Returns + ------- + base : dict + Base parameters. + """ + + from neurodsp.sim.update import BaseUpdater + + if isinstance(params, dict): + base = _get_base_params(params) + elif isinstance(params, list): + base = _get_base_params(params[0]) + elif isinstance(params, BaseUpdater): + base = getattr(params, 'base') + else: + raise ValueError('Parameter definition not understood.') + + return base + + +def _get_base_params(params): + """Sub-function to get base parameters from a dictionary of parameters.""" + + return {key : value for key, value in params.items() if key in BASE_PARAMS} + + +def drop_base_params(params): + """Drop base parameters from a parameter definition. + + Parameters + ---------- + params : dict or list of dict + Parameter definition. + + Returns + ------- + params : dict + Parameter definition, excluding base parameters. + """ + + if isinstance(params, dict): + params = _drop_base_params(params) + elif isinstance(params, list): + params = [_get_base_params(cparams) for cparams in params] + else: + raise ValueError('Parameter definition not understood.') + + return params + + +def _drop_base_params(params): + """Sub-function to drop base parameters from a dictionary of parameters.""" + + return {key : value for key, value in params.items() if key not in BASE_PARAMS} + + +def get_param_values(params, extract=None, component=None): + """Get a set of parameter values from a set of parameter definitions. + + Parameters + ---------- + params : list of dict + Parameter definitions for multiple simulations. + extract : str + Name of the parameter to extract. + component : str, optional + Which component to extract the parameter from. + Only used if the parameter definition is for a multi-component simulation. + + Returns + ------- + values : list + Extracted parameter values. + """ + + if component: + values =[cparams['components'][component][extract] for cparams in params] + elif extract: + values = [cparams[extract] for cparams in params] + else: + values = None + + return values + + +## SIMULATION PARAMETER OBJECTS + class SimParams(): """Object for managing simulation parameters. @@ -368,6 +465,8 @@ def make_iter(self, label, update, values, component=None): Generator object for iterating across simulation parameters. """ + from neurodsp.sim.update import ParamIter + assert label in self._params.keys(), "Label for simulation parameters not found." return ParamIter(super().__getitem__(label), update, values, component) @@ -532,6 +631,8 @@ def make_sampler(self, label, samplers, n_samples=None): Generator object for sampling simulation parameters. """ + from neurodsp.sim.update import ParamSampler + return ParamSampler(super().__getitem__(label), samplers, n_samples if n_samples else self.n_samples) diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index a8dc0316..e38d2ca2 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -163,69 +163,3 @@ def modulate_signal(sig, modulation, fs=None, mod_params=None): msig = sig * modulation return msig - -## Utilities for helping with parameter management - -BASE_PARAMS = ['n_seconds', 'fs'] - -def get_base_params(params): - """Get base parameters from a parameter definition. - - Parameters - ---------- - params : dict - Parameter definition. - - Returns - ------- - params : dict - Base parameters. - """ - - return {key : value for key, value in params.items() if key in BASE_PARAMS} - - -def drop_base_params(params): - """Drop base parameters from a parameter definition. - - Parameters - ---------- - params : dict - Parameter definition. - - Returns - ------- - params : dict - Parameter definition, excluding base parameters. - """ - - return {key : value for key, value in params.items() if key not in BASE_PARAMS} - - -def get_param_values(params, extract=None, component=None): - """Get a set of parameter values from a set of parameter definitions. - - Parameters - ---------- - params : list of dict - Parameter definitions for multiple simulations. - extract : str - Name of the parameter to extract. - component : str, optional - Which component to extract the parameter from. - Only used if the parameter definition is for a multi-component simulation. - - Returns - ------- - values : list - Extracted parameter values. - """ - - if component: - values =[cparams['components'][component][extract] for cparams in params] - elif extract: - values = [cparams[extract] for cparams in params] - else: - values = None - - return values diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index c436b6d6..6127f1f1 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -7,6 +7,54 @@ ################################################################################################### ################################################################################################### +## FUNCTION TESTS + +def test_get_base_params(tsim_iters): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + out1 = get_base_params(params) + for bparam in out1: + assert bparam in BASE_PARAMS + + params_lst = [params, params] + out2 = get_base_params(params) + for bparam in out2: + assert bparam in BASE_PARAMS + + out3 = get_base_params(tsim_iters['pl_exp']) + for bparam in out3: + assert bparam in BASE_PARAMS + +def test_drop_base_params(): + + params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} + out1 = drop_base_params(params) + for bparam in BASE_PARAMS: + assert bparam not in out1 + assert 'exponent' in out1 + + params_lst = [params, params] + out2 = drop_base_params(params) + for bparam in BASE_PARAMS: + assert bparam not in out2 + assert 'exponent' in out2 + +def test_get_param_values(): + + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + assert get_param_values(params, 'exponent') == [-2, -1] + assert get_param_values(params, 'n_seconds') == [2, 2] + + params = [{'n_seconds' : 2, 'fs' : 250, 'components' : \ + {'sim_powerlaw' : {'exponent' : -2}, 'sim_oscillation' : {'freq' : 10}}}, + {'n_seconds' : 2, 'fs' : 250, 'components' : \ + {'sim_powerlaw' : {'exponent' : -1}, 'sim_oscillation' : {'freq' : 10}}}] + assert get_param_values(params, 'exponent', 'sim_powerlaw') == [-2, -1] + assert get_param_values(params, 'freq', 'sim_oscillation') == [10, 10] + +## CLASS TESTS + def test_sim_params(): # Test initialization diff --git a/neurodsp/tests/sim/test_utils.py b/neurodsp/tests/sim/test_utils.py index 1da9fc04..da26f5e4 100644 --- a/neurodsp/tests/sim/test_utils.py +++ b/neurodsp/tests/sim/test_utils.py @@ -34,32 +34,3 @@ def test_modulate_signal(tsig): # Check modulation passing in a 1d array directly msig2 = modulate_signal(tsig, tsig) check_sim_output(msig2) - -def test_get_base_params(): - - params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - out1 = get_base_params(params) - for bparam in out1: - assert bparam in BASE_PARAMS - -def test_drop_base_params(): - - params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - out1 = drop_base_params(params) - for bparam in BASE_PARAMS: - assert bparam not in out1 - assert 'exponent' in params - -def test_get_param_values(): - - params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, - {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - assert get_param_values(params, 'exponent') == [-2, -1] - assert get_param_values(params, 'n_seconds') == [2, 2] - - params = [{'n_seconds' : 2, 'fs' : 250, 'components' : \ - {'sim_powerlaw' : {'exponent' : -2}, 'sim_oscillation' : {'freq' : 10}}}, - {'n_seconds' : 2, 'fs' : 250, 'components' : \ - {'sim_powerlaw' : {'exponent' : -1}, 'sim_oscillation' : {'freq' : 10}}}] - assert get_param_values(params, 'exponent', 'sim_powerlaw') == [-2, -1] - assert get_param_values(params, 'freq', 'sim_oscillation') == [10, 10] From dd04ee258c46493641ae2aedc9af8f520fa3dd96 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 10:42:12 -0400 Subject: [PATCH 09/33] move modulate funcs to own file --- neurodsp/sim/modulate.py | 165 +++++++++++++++++ neurodsp/sim/utils.py | 167 +----------------- .../sim/{test_utils.py => test_modulate.py} | 0 3 files changed, 168 insertions(+), 164 deletions(-) create mode 100644 neurodsp/sim/modulate.py rename neurodsp/tests/sim/{test_utils.py => test_modulate.py} (100%) diff --git a/neurodsp/sim/modulate.py b/neurodsp/sim/modulate.py new file mode 100644 index 00000000..6517470f --- /dev/null +++ b/neurodsp/sim/modulate.py @@ -0,0 +1,165 @@ +"""Functionality to modulate simulations.""" + +import numpy as np + +from neurodsp.utils.data import compute_nseconds +from neurodsp.sim.info import get_sim_func + +################################################################################################### +################################################################################################### + +def rotate_timeseries(sig, fs, delta_exp, f_rotation=1): + """Rotate a timeseries of data, changing it's 1/f exponent. + + Parameters + ---------- + sig : 1d array + A time series to rotate. + fs : float + Sampling rate of the signal, in Hz. + delta_exp : float + Change in power law exponent to be applied. + Positive is clockwise rotation (steepen), negative is counter clockwise rotation (flatten). + f_rotation : float, optional, default: 1 + Frequency, in Hz, to rotate the spectrum around, where power is unchanged by the rotation. + + Returns + ------- + sig_rotated : 1d array + The rotated version of the signal. + + Notes + ----- + This function works by taking the FFT and spectrally rotating the input signal. + To return a timeseries, the rotated FFT is then turned back into a time series, with an iFFT. + + Examples + -------- + Rotate a timeseries of simulated data: + + >>> from neurodsp.sim import sim_combined + >>> sig = sim_combined(n_seconds=10, fs=500, + ... components={'sim_powerlaw': {}, 'sim_oscillation' : {'freq': 10}}) + >>> rotated_sig = rotate_timeseries(sig, fs=500, delta_exp=0.5) + """ + + # Compute the FFT + fft_vals = np.fft.fft(sig) + freqs = np.fft.fftfreq(len(sig), 1./fs) + + # Rotate the spectrum to create the exponent change + # Delta exponent is divided by two, as the FFT output is in units of amplitude not power + fft_rotated = rotate_spectrum(freqs, fft_vals, delta_exp/2, f_rotation) + + # Invert back to time series, with a z-score to normalize + sig_rotated = np.real(np.fft.ifft(fft_rotated)) + + return sig_rotated + + +def rotate_spectrum(freqs, spectrum, delta_exponent, f_rotation=1): + """Rotate the power law exponent of a power spectrum. + + Parameters + ---------- + freqs : 1d array + Frequency axis of input spectrum, in Hz. + spectrum : 1d array + Power spectrum to be rotated. + delta_exponent : float + Change in power law exponent to be applied. + Positive is clockwise rotation (steepen), negative is counter clockwise rotation (flatten). + f_rotation : float, optional, default: 1 + Frequency, in Hz, to rotate the spectrum around, where power is unchanged by the rotation. + This only matters if not further normalizing signal variance. + + Returns + ------- + rotated_spectrum : 1d array + Rotated spectrum. + + Notes + ----- + The input power spectrum is multiplied with a mask that applies the specified exponent change. + + Examples + -------- + Rotate a power spectrum, calculated on simulated data: + + >>> from neurodsp.sim import sim_combined + >>> from neurodsp.spectral import compute_spectrum + >>> sig = sim_combined(n_seconds=10, fs=500, + ... components={'sim_powerlaw': {}, 'sim_oscillation' : {'freq': 10}}) + >>> freqs, spectrum = compute_spectrum(sig, fs=500) + >>> rotated_spectrum = rotate_spectrum(freqs, spectrum, -1) + """ + + if freqs[0] == 0: + skipped_zero = True + f_0, p_0 = freqs[0], spectrum[0] + freqs, spectrum = freqs[1:], spectrum[1:] + else: + skipped_zero = False + + mask = (np.abs(freqs) / f_rotation)**-delta_exponent + rotated_spectrum = mask * spectrum + + if skipped_zero: + freqs = np.insert(freqs, 0, f_0) + rotated_spectrum = np.insert(rotated_spectrum, 0, p_0) + + return rotated_spectrum + + +def modulate_signal(sig, modulation, fs=None, mod_params=None): + """Apply amplitude modulation to a signal. + + Parameters + ---------- + sig : 1d array + A signal to modulate. + modulation : 1d array or str + Modulation to apply to the signal. + If array, the modulating signal to apply directly to the signal. + If str, a function name to use to simulate the modulating signal that will be applied. + fs : float, optional + Signal sampling rate, in Hz. + Only needed if `modulation` is a callable. + mod_params : dictionary, optional + Parameters for the modulation function. + Only needed if `modulation` is a callable. + + Returns + ------- + msig : 1d array + Amplitude modulated signal. + + Examples + -------- + Amplitude modulate a sinusoidal signal with a lower frequency, passing in a function label: + + >>> from neurodsp.sim import sim_oscillation + >>> fs = 500 + >>> sig = sim_oscillation(n_seconds=10, fs=fs, freq=10) + >>> msig = modulate_signal(sig, 'sim_oscillation', fs, {'freq' : 1}) + + Amplitude modulate a sinusoidal signal with precomputed 1/f signal: + + >>> from neurodsp.sim import sim_oscillation, sim_powerlaw + >>> n_seconds = 10 + >>> fs = 500 + >>> sig = sim_oscillation(n_seconds, fs, freq=10) + >>> mod = sim_powerlaw(n_seconds, fs, exponent=-1) + >>> msig = modulate_signal(sig, mod) + """ + + if isinstance(modulation, str): + mod_func = get_sim_func(modulation) + modulation = mod_func(compute_nseconds(sig, fs), fs, **mod_params) + + assert len(sig) == len(modulation), \ + 'Lengths of the signal and modulator must match to apply modulation' + + msig = sig * modulation + + return msig diff --git a/neurodsp/sim/utils.py b/neurodsp/sim/utils.py index e38d2ca2..fd77dd07 100644 --- a/neurodsp/sim/utils.py +++ b/neurodsp/sim/utils.py @@ -1,165 +1,4 @@ -"""Utility function for neurodsp.sim.""" +"""Simulation utilities.""" -import numpy as np - -from neurodsp.utils.data import compute_nseconds -from neurodsp.sim.info import get_sim_func - -################################################################################################### -################################################################################################### - -def rotate_timeseries(sig, fs, delta_exp, f_rotation=1): - """Rotate a timeseries of data, changing it's 1/f exponent. - - Parameters - ---------- - sig : 1d array - A time series to rotate. - fs : float - Sampling rate of the signal, in Hz. - delta_exp : float - Change in power law exponent to be applied. - Positive is clockwise rotation (steepen), negative is counter clockwise rotation (flatten). - f_rotation : float, optional, default: 1 - Frequency, in Hz, to rotate the spectrum around, where power is unchanged by the rotation. - - Returns - ------- - sig_rotated : 1d array - The rotated version of the signal. - - Notes - ----- - This function works by taking the FFT and spectrally rotating the input signal. - To return a timeseries, the rotated FFT is then turned back into a time series, with an iFFT. - - Examples - -------- - Rotate a timeseries of simulated data: - - >>> from neurodsp.sim import sim_combined - >>> sig = sim_combined(n_seconds=10, fs=500, - ... components={'sim_powerlaw': {}, 'sim_oscillation' : {'freq': 10}}) - >>> rotated_sig = rotate_timeseries(sig, fs=500, delta_exp=0.5) - """ - - # Compute the FFT - fft_vals = np.fft.fft(sig) - freqs = np.fft.fftfreq(len(sig), 1./fs) - - # Rotate the spectrum to create the exponent change - # Delta exponent is divided by two, as the FFT output is in units of amplitude not power - fft_rotated = rotate_spectrum(freqs, fft_vals, delta_exp/2, f_rotation) - - # Invert back to time series, with a z-score to normalize - sig_rotated = np.real(np.fft.ifft(fft_rotated)) - - return sig_rotated - - -def rotate_spectrum(freqs, spectrum, delta_exponent, f_rotation=1): - """Rotate the power law exponent of a power spectrum. - - Parameters - ---------- - freqs : 1d array - Frequency axis of input spectrum, in Hz. - spectrum : 1d array - Power spectrum to be rotated. - delta_exponent : float - Change in power law exponent to be applied. - Positive is clockwise rotation (steepen), negative is counter clockwise rotation (flatten). - f_rotation : float, optional, default: 1 - Frequency, in Hz, to rotate the spectrum around, where power is unchanged by the rotation. - This only matters if not further normalizing signal variance. - - Returns - ------- - rotated_spectrum : 1d array - Rotated spectrum. - - Notes - ----- - The input power spectrum is multiplied with a mask that applies the specified exponent change. - - Examples - -------- - Rotate a power spectrum, calculated on simulated data: - - >>> from neurodsp.sim import sim_combined - >>> from neurodsp.spectral import compute_spectrum - >>> sig = sim_combined(n_seconds=10, fs=500, - ... components={'sim_powerlaw': {}, 'sim_oscillation' : {'freq': 10}}) - >>> freqs, spectrum = compute_spectrum(sig, fs=500) - >>> rotated_spectrum = rotate_spectrum(freqs, spectrum, -1) - """ - - if freqs[0] == 0: - skipped_zero = True - f_0, p_0 = freqs[0], spectrum[0] - freqs, spectrum = freqs[1:], spectrum[1:] - else: - skipped_zero = False - - mask = (np.abs(freqs) / f_rotation)**-delta_exponent - rotated_spectrum = mask * spectrum - - if skipped_zero: - freqs = np.insert(freqs, 0, f_0) - rotated_spectrum = np.insert(rotated_spectrum, 0, p_0) - - return rotated_spectrum - - -def modulate_signal(sig, modulation, fs=None, mod_params=None): - """Apply amplitude modulation to a signal. - - Parameters - ---------- - sig : 1d array - A signal to modulate. - modulation : 1d array or str - Modulation to apply to the signal. - If array, the modulating signal to apply directly to the signal. - If str, a function name to use to simulate the modulating signal that will be applied. - fs : float, optional - Signal sampling rate, in Hz. - Only needed if `modulation` is a callable. - mod_params : dictionary, optional - Parameters for the modulation function. - Only needed if `modulation` is a callable. - - Returns - ------- - msig : 1d array - Amplitude modulated signal. - - Examples - -------- - Amplitude modulate a sinusoidal signal with a lower frequency, passing in a function label: - - >>> from neurodsp.sim import sim_oscillation - >>> fs = 500 - >>> sig = sim_oscillation(n_seconds=10, fs=fs, freq=10) - >>> msig = modulate_signal(sig, 'sim_oscillation', fs, {'freq' : 1}) - - Amplitude modulate a sinusoidal signal with precomputed 1/f signal: - - >>> from neurodsp.sim import sim_oscillation, sim_powerlaw - >>> n_seconds = 10 - >>> fs = 500 - >>> sig = sim_oscillation(n_seconds, fs, freq=10) - >>> mod = sim_powerlaw(n_seconds, fs, exponent=-1) - >>> msig = modulate_signal(sig, mod) - """ - - if isinstance(modulation, str): - mod_func = get_sim_func(modulation) - modulation = mod_func(compute_nseconds(sig, fs), fs, **mod_params) - - assert len(sig) == len(modulation), \ - 'Lengths of the signal and modulator must match to apply modulation' - - msig = sig * modulation - - return msig +# Alias in function that used to be here for backwards compatibility +from neurodsp.sim.modulate import rotate_timeseries, rotate_spectrum, modulate_signal diff --git a/neurodsp/tests/sim/test_utils.py b/neurodsp/tests/sim/test_modulate.py similarity index 100% rename from neurodsp/tests/sim/test_utils.py rename to neurodsp/tests/sim/test_modulate.py From a362809e483317fe980b868de868cad33cd37e7e Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 10:43:05 -0400 Subject: [PATCH 10/33] update through files for moves etc --- doc/api.rst | 6 +++--- neurodsp/sim/aperiodic.py | 8 ++++---- neurodsp/sim/combined.py | 4 ++-- neurodsp/sim/multi.py | 17 ++++++++--------- neurodsp/sim/signals.py | 2 +- neurodsp/sim/update.py | 2 +- neurodsp/spectral/__init__.py | 2 +- neurodsp/spectral/utils.py | 2 +- neurodsp/tests/sim/test_modulate.py | 4 ++-- 9 files changed, 23 insertions(+), 24 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index b0aaf5e8..fb6a9bd8 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -373,10 +373,10 @@ The following objects can be used to manage groups of simulated signals: VariableSimulations MultiSimulations -Utilities -~~~~~~~~~ +Modulate Signals +~~~~~~~~~~~~~~~~ -.. currentmodule:: neurodsp.sim.utils +.. currentmodule:: neurodsp.sim.modulate .. autosummary:: :toctree: generated/ diff --git a/neurodsp/sim/aperiodic.py b/neurodsp/sim/aperiodic.py index f9c0b83d..7ba4262f 100644 --- a/neurodsp/sim/aperiodic.py +++ b/neurodsp/sim/aperiodic.py @@ -7,12 +7,12 @@ from neurodsp.filt import filter_signal, infer_passtype from neurodsp.filt.fir import compute_filter_length from neurodsp.filt.checks import check_filter_definition -from neurodsp.utils import remove_nans +from neurodsp.utils.norm import normalize_sig +from neurodsp.utils.outliers import remove_nans +from neurodsp.utils.decorators import normalize from neurodsp.utils.checks import check_param_range from neurodsp.utils.data import create_times, compute_nsamples -from neurodsp.utils.decorators import normalize -from neurodsp.utils.norm import normalize_sig -from neurodsp.sim.utils import rotate_timeseries +from neurodsp.sim.modulate import rotate_timeseries from neurodsp.sim.transients import sim_synaptic_kernel ################################################################################################### diff --git a/neurodsp/sim/combined.py b/neurodsp/sim/combined.py index c7501bcb..eb49db34 100644 --- a/neurodsp/sim/combined.py +++ b/neurodsp/sim/combined.py @@ -6,9 +6,9 @@ from scipy.linalg import norm from neurodsp.sim.info import get_sim_func -from neurodsp.sim.utils import modulate_signal -from neurodsp.utils.decorators import normalize +from neurodsp.sim.modulate import modulate_signal from neurodsp.utils.data import create_times +from neurodsp.utils.decorators import normalize ################################################################################################### ################################################################################################### diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index e3e37adc..e08a80d5 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -5,6 +5,7 @@ from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler from neurodsp.sim.update import ParamIter +from neurodsp.utils.data import compute_nsamples ################################################################################################### ################################################################################################### @@ -40,7 +41,7 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): >>> sigs = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sigs = np.zeros([n_sims, int(sim_params['n_seconds'] * sim_params['fs'])]) + sigs = np.zeros([n_sims, compute_nsamples(sim_params['n_seconds'], sim_params['fs'])]) for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): sigs[ind, :] = sig @@ -50,7 +51,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='object'): +def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): """Simulate multiple signals across different parameter values. Parameters @@ -89,15 +90,12 @@ def sim_across_values(sim_func, sim_params, n_sims, output='object'): >>> sigs = sim_across_values(sim_powerlaw, params, n_sims=2) """ - oparams = {'update' : None, 'component' : None} - if isinstance(sim_params, ParamIter): - oparams = {'update' : sim_params.update, 'component' : sim_params.component} - - sims = MultiSimulations(**oparams) + sims = MultiSimulations(update=getattr(sim_params, 'update', None), + component=getattr(sim_params, 'component', None)) for ind, cur_sim_params in enumerate(sim_params): sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object')) - if output == 'array': + if return_type == 'array': sims = np.squeeze(np.array([el.signals for el in sims])) return sims @@ -138,7 +136,8 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): """ all_params = [None] * n_sims - sigs = np.zeros([n_sims, sim_sampler.params['n_seconds'] * sim_sampler.params['fs']]) + n_samples = compute_nsamples(sim_sampler.params['n_seconds'], sim_sampler.params['fs']) + sigs = np.zeros([n_sims, n_samples]) for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): sigs[ind, :] = sig all_params[ind] = params diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 73fe4df6..ad06ecf5 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.utils.core import listify -from neurodsp.sim.utils import get_base_params, drop_base_params, get_param_values +from neurodsp.sim.params import get_base_params, drop_base_params, get_param_values ################################################################################################### ################################################################################################### diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index ccb3a9dc..5b033e0d 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -5,7 +5,7 @@ import numpy as np from neurodsp.sim.generators import sig_yielder -from neurodsp.sim.utils import get_base_params +from neurodsp.sim.params import get_base_params from neurodsp.utils.core import counter ################################################################################################### diff --git a/neurodsp/spectral/__init__.py b/neurodsp/spectral/__init__.py index 050c103a..c63c4e41 100644 --- a/neurodsp/spectral/__init__.py +++ b/neurodsp/spectral/__init__.py @@ -5,4 +5,4 @@ from .measures import compute_absolute_power, compute_relative_power, compute_band_ratio from .variance import compute_scv, compute_scv_rs, compute_spectral_hist from .utils import trim_spectrum, trim_spectrogram -from ..sim.utils import rotate_spectrum as rotate_powerlaw +from ..sim.modulate import rotate_spectrum as rotate_powerlaw diff --git a/neurodsp/spectral/utils.py b/neurodsp/spectral/utils.py index 567417b9..b32614bc 100644 --- a/neurodsp/spectral/utils.py +++ b/neurodsp/spectral/utils.py @@ -4,7 +4,7 @@ from scipy.fft import next_fast_len # Alias a function that has moved, for backwards compatibility -from neurodsp.sim.utils import rotate_spectrum as rotate_powerlaw +from neurodsp.sim.modulate import rotate_spectrum as rotate_powerlaw ################################################################################################### ################################################################################################### diff --git a/neurodsp/tests/sim/test_modulate.py b/neurodsp/tests/sim/test_modulate.py index da26f5e4..9c733838 100644 --- a/neurodsp/tests/sim/test_modulate.py +++ b/neurodsp/tests/sim/test_modulate.py @@ -1,11 +1,11 @@ -"""Tests for neurodsp.sim.utils.""" +"""Tests for neurodsp.sim.modulate.""" import numpy as np from neurodsp.tests.tutils import check_sim_output from neurodsp.tests.settings import FS -from neurodsp.sim.utils import * +from neurodsp.sim.modulate import * ################################################################################################### ################################################################################################### From 40186b7a094d91d6549451a8a16a1b6aa0adfea7 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 10:56:20 -0400 Subject: [PATCH 11/33] lints --- neurodsp/filt/filter.py | 2 -- neurodsp/sim/multi.py | 33 ++++++++++++++++----------------- neurodsp/sim/signals.py | 26 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/neurodsp/filt/filter.py b/neurodsp/filt/filter.py index 74a8d343..d624af95 100644 --- a/neurodsp/filt/filter.py +++ b/neurodsp/filt/filter.py @@ -1,7 +1,5 @@ """Filter time series.""" -from warnings import warn - from neurodsp.filt.fir import filter_signal_fir from neurodsp.filt.iir import filter_signal_iir from neurodsp.utils.checks import check_param_options diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index e08a80d5..9d8eee01 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -4,7 +4,6 @@ from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler -from neurodsp.sim.update import ParamIter from neurodsp.utils.data import compute_nsamples ################################################################################################### @@ -28,7 +27,7 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): Returns ------- - sigs : Simulations or 2d array + sims : Simulations or 2d array Simulations, return type depends on `return_type` argument. Simulated time series are organized as [n_sims, sig length]. @@ -38,17 +37,17 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): >>> from neurodsp.sim.aperiodic import sim_powerlaw >>> params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - >>> sigs = sim_multiple(sim_powerlaw, params, n_sims=3) + >>> sims = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sigs = np.zeros([n_sims, compute_nsamples(sim_params['n_seconds'], sim_params['fs'])]) + sims = np.zeros([n_sims, compute_nsamples(sim_params['n_seconds'], sim_params['fs'])]) for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): - sigs[ind, :] = sig + sims[ind, :] = sig if return_type == 'object': - return Simulations(sigs, sim_params, sim_func) - else: - return sigs + sims = Simulations(sims, sim_params, sim_func) + + return sims def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): @@ -78,7 +77,7 @@ def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): Simulate multiple powerlaw signals using a ParamIter object: >>> from neurodsp.sim.aperiodic import sim_powerlaw - >>> from neurodsp.sim.params import ParamIter + >>> from neurodsp.sim.update 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) @@ -92,7 +91,7 @@ def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): sims = MultiSimulations(update=getattr(sim_params, 'update', None), component=getattr(sim_params, 'component', None)) - for ind, cur_sim_params in enumerate(sim_params): + for cur_sim_params in sim_params: sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object')) if return_type == 'array': @@ -119,7 +118,7 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): Returns ------- - sigs : VariableSimulations or 2d array + sims : VariableSimulations or 2d array Simulations, return type depends on `return_type` argument. If array, simulations are organized as [n_sims, sig length]. @@ -132,17 +131,17 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): >>> 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) + >>> sims = sim_from_sampler(sim_powerlaw, param_sampler, n_sims=2) """ all_params = [None] * n_sims n_samples = compute_nsamples(sim_sampler.params['n_seconds'], sim_sampler.params['fs']) - sigs = np.zeros([n_sims, n_samples]) + sims = np.zeros([n_sims, n_samples]) for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): - sigs[ind, :] = sig + sims[ind, :] = sig all_params[ind] = params if return_type == 'object': - return VariableSimulations(sigs, all_params, sim_func) - else: - return sigs + sims = VariableSimulations(sims, all_params, sim_func) + + return sims diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index ad06ecf5..0f8a43c5 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -37,34 +37,40 @@ def __init__(self, signals=None, params=None, sim_func=None): self.add_params(params) self.sim_func = sim_func.__name__ if callable(sim_func) else sim_func + def __iter__(self): """Define iteration as stepping across individual simulated signals.""" for sig in self.signals: yield sig + def __getitem__(self, ind): """Define indexing as accessing simulated signals.""" return self.signals[ind, :] + def __len__(self): """Define the length of the object as the number of signals.""" return len(self.signals) + @property def n_seconds(self): """Alias n_seconds as a property attribute from base parameters.""" return self._base_params['n_seconds'] if self.has_params else None + @property def fs(self): """Alias fs as a property attribute from base parameters.""" return self._base_params['fs'] if self.has_params else None + @property def params(self): """Define the full set of simulation parameters (base + additional parameters).""" @@ -76,18 +82,21 @@ def params(self): return params + @property def has_params(self): """Indicator for if the object has parameters.""" return bool(self._params) + @property def has_signals(self): """Indicator for if the object has signals.""" return bool(len(self)) + def add_params(self, params): """Add parameter definition to object. @@ -131,18 +140,21 @@ def __init__(self, signals=None, params=None, sim_func=None, update=None, compon self.update = update self.component = component + @property def n_seconds(self): """Alias n_seconds as a property.""" return self.params[0].n_seconds if self.has_params else None + @property def fs(self): """Alias fs as a property.""" return self.params[0].fs if self.has_params else None + @property def params(self): """Define simulation parameters (base + additional parameters) for each simulation.""" @@ -154,12 +166,14 @@ def params(self): return params + @property def values(self): """Alias in the parameter definition of the parameter that varies across the sets.""" return get_param_values(self.params, self.update, self.component) + def add_params(self, params): """Add parameter definition(s) to object. @@ -189,6 +203,7 @@ def add_params(self, params): if self.has_params: raise ValueError('Must add parameters if object already has them.') + def add_signal(self, signal, params=None): """Add a signal to the current object. @@ -243,40 +258,47 @@ def __init__(self, signals=None, params=None, sim_func=None, update=None, compon self.update = update self.component = component + 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.""" @@ -285,24 +307,28 @@ def params(self): return params + @property def values(self): """Alias in the parameter definition of the parameter that varies across the sets.""" return get_param_values(self.params, self.update, self.component) + @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. From be22be1566b2ddef840b4a2566a821c39f161605 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 20:58:22 -0400 Subject: [PATCH 12/33] split sim_across_values --- doc/api.rst | 1 + neurodsp/sim/multi.py | 60 ++++++++++++++++++++++++++++++-- neurodsp/tests/sim/test_multi.py | 41 +++++++++++++++++----- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index fb6a9bd8..dbd1c973 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -326,6 +326,7 @@ Multiple Signals sim_multiple sim_across_values + sim_multi_across_values sim_from_sampler Simulation Parameters diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 9d8eee01..17dab54c 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -4,6 +4,7 @@ from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler +from neurodsp.sim.params import get_base_params from neurodsp.utils.data import compute_nsamples ################################################################################################### @@ -50,7 +51,60 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): return sims -def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): +def sim_across_values(sim_func, sim_params, return_type='object'): + """Simulate signals across different parameter values. + + Parameters + ---------- + sim_func : callable + Function to create the simulated time series. + sim_params : ParamIter or iterable or list of dict + Simulation parameters for `sim_func`. + return_type : {'object', 'array'} + Specifies the return type of the simulations. + If 'object', returns simulations and metadata in a 'VariableSimulations' object. + If 'array', returns the simulations (no metadata) in an array. + + Returns + ------- + sims : VariableSimulations or array + Simulations, return type depends on `return_type` argument. + If array, signals are collected together as [n_sims, sig_length]. + + Examples + -------- + Simulate multiple powerlaw signals using a ParamIter object: + + >>> from neurodsp.sim.aperiodic import sim_powerlaw + >>> from neurodsp.sim.update import ParamIter + >>> base_params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : None} + >>> param_iter = ParamIter(base_params, 'exponent', [-2, 1, 0]) + >>> sims = sim_multi_across_values(sim_powerlaw, param_iter) + + 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}] + >>> sims = sim_multi_across_values(sim_powerlaw, params) + """ + + base = get_base_params(sim_params) + + all_params = [None] * len(sim_params) + sims = np.zeros([len(sim_params), compute_nsamples(base['n_seconds'], base['fs'])]) + for ind, cur_sim_params in enumerate(sim_params): + sims[ind, :] = sim_func(**cur_sim_params) + all_params[ind] = cur_sim_params + + if return_type == 'object': + sims = VariableSimulations(sims, all_params, sim_func, + update=getattr(sim_params, 'update', None), + component=getattr(sim_params, 'component', None)) + + return sims + + +def sim_multi_across_values(sim_func, sim_params, n_sims, return_type='object'): """Simulate multiple signals across different parameter values. Parameters @@ -80,13 +134,13 @@ def sim_across_values(sim_func, sim_params, n_sims, return_type='object'): >>> from neurodsp.sim.update 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) + >>> sims = sim_multi_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) + >>> sims = sim_multi_across_values(sim_powerlaw, params, n_sims=2) """ sims = MultiSimulations(update=getattr(sim_params, 'update', None), diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index ef610ab8..5fbb0b4f 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -27,24 +27,47 @@ def test_sim_multiple(): def test_sim_across_values(tsim_iters): + params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, + {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] + + sims_obj = sim_across_values(sim_powerlaw, params, 'object') + assert isinstance(sims_obj, VariableSimulations) + assert len(sims_obj) == len(params) + for csim, cparams, oparams in zip(sims_obj, sims_obj.params, params): + assert isinstance(csim, np.ndarray) + assert cparams == oparams + + sims_arr = sim_across_values(sim_powerlaw, params, 'array') + assert isinstance(sims_arr, np.ndarray) + assert sims_arr.shape[0] == len(params) + + # Test with ParamIter input + siter = tsim_iters['pl_exp'] + sims_iter = sim_across_values(sim_powerlaw, siter) + assert isinstance(sims_iter, VariableSimulations) + assert sims_iter.update == siter.update + assert sims_iter.values == siter.values + +def test_sim_multi_across_values(tsim_iters): + n_sims = 3 params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - sims_obj = sim_across_values(sim_powerlaw, params, n_sims, 'object') + sims_obj = sim_multi_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 + for sims, cparams in zip(sims_obj, params): + assert isinstance(sims, Simulations) + assert len(sims) == n_sims + assert sims.params == cparams - sigs_arr = sim_across_values(sim_powerlaw, params, n_sims, 'array') - assert isinstance(sigs_arr, np.ndarray) - assert sigs_arr.shape[0:2] == (len(params), n_sims) + sims_arr = sim_multi_across_values(sim_powerlaw, params, n_sims, 'array') + assert isinstance(sims_arr, np.ndarray) + assert sims_arr.shape[0:2] == (len(params), n_sims) # Test with ParamIter input siter = tsim_iters['pl_exp'] - sims_iter = sim_across_values(sim_powerlaw, siter, n_sims) + sims_iter = sim_multi_across_values(sim_powerlaw, siter, n_sims) assert isinstance(sims_iter, MultiSimulations) assert sims_iter.update == siter.update assert sims_iter.values == siter.values From d5c0e96c13d88bdf9db4a6603bad569da8e20197 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Fri, 6 Sep 2024 22:19:56 -0400 Subject: [PATCH 13/33] add pre-initialization to signals objects --- neurodsp/sim/multi.py | 4 +- neurodsp/sim/signals.py | 82 +++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 17dab54c..5c56c292 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -79,13 +79,13 @@ def sim_across_values(sim_func, sim_params, return_type='object'): >>> from neurodsp.sim.update import ParamIter >>> base_params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : None} >>> param_iter = ParamIter(base_params, 'exponent', [-2, 1, 0]) - >>> sims = sim_multi_across_values(sim_powerlaw, param_iter) + >>> sims = sim_across_values(sim_powerlaw, param_iter) 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}] - >>> sims = sim_multi_across_values(sim_powerlaw, params) + >>> sims = sim_across_values(sim_powerlaw, params) """ base = get_base_params(sim_params) diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 0f8a43c5..9d9fbfe4 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -5,6 +5,7 @@ import numpy as np from neurodsp.utils.core import listify +from neurodsp.utils.data import compute_nsamples from neurodsp.sim.params import get_base_params, drop_base_params, get_param_values ################################################################################################### @@ -15,8 +16,9 @@ class Simulations(): Parameters ---------- - signals : 1d or 2nd array, optional - The simulated signals, organized as [n_sims, sig_length]. + signals : 1d or 2nd array or int, optional + If array, the simulated signals, organized as [n_sims, sig_length]. + If int, the number of expected simulations, used to pre-initialize array. params : dict, optional The simulation parameters that were used to create the simulations. sim_func : str or callable, optional @@ -31,10 +33,17 @@ class Simulations(): 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([]) + if signals is None: + signals = np.array([]) + elif isinstance(signals, int): + n_samples = compute_nsamples(params['n_seconds'], params['fs']) + signals = np.zeros((signals, n_samples)) + self.signals = np.atleast_2d(signals) + self._base_params = None self._params = None self.add_params(params) + self.sim_func = sim_func.__name__ if callable(sim_func) else sim_func @@ -102,7 +111,7 @@ def add_params(self, params): Parameters ---------- - params : dict, optional + params : dict or None The simulation parameter definition(s). """ @@ -111,13 +120,38 @@ def add_params(self, params): self._params = drop_base_params(params) + def add_signal(self, signal, index=None): + """Add a signal to the current object. + + Parameters + ---------- + signal : 1d array + A simulated signal to add to the object. + index : int + Index to insert the new signal in the signals attribute. + """ + + if index is not None: + self.signals[index, :] = signal + else: + if not self.signals.size: + self.signals = np.atleast_2d(signal) + else: + try: + self.signals = np.vstack([self.signals, signal]) + except ValueError as array_value_error: + msg = 'Size of the added signal is not consistent with existing signals.' + raise ValueError(msg) from array_value_error + + class VariableSimulations(Simulations): """Data object for a set of simulated signals with variable parameter definitions. Parameters ---------- - signals : 2nd array, optional - The simulated signals, organized as [n_sims, sig_length]. + signals : 2nd array or int, optional + If array, the simulated signals, organized as [n_sims, sig_length]. + If int, the number of expected simulations, used to pre-initialize array. params : list of dict, optional The simulation parameters for each of the simulations. sim_func : str, optional @@ -186,25 +220,30 @@ def add_params(self, params): if params: params = listify(params) - base_params = get_base_params(params[0]) - cparams = [drop_base_params(el) for el in params] - if not self.has_params: - if len(self) > 1 and len(self) > len(cparams): - msg = 'Cannot add parameters to object without existing parameter values.' - raise ValueError(msg) + base_params = get_base_params(params[0]) + if not self._base_params: self._base_params = base_params - self._params = cparams - else: - self._params.extend(cparams) + msg = 'Base params have to match existing parameters.' + assert base_params == self._base_params, msg + + cparams = [drop_base_params(el) for el in params] + if cparams[0]: + if not self.has_params: + if len(self) > 1 and len(self) > len(cparams): + msg = 'Cannot add parameters to object without existing parameter values.' + raise ValueError(msg) + self._params = cparams + else: + self._params.extend(cparams) else: if self.has_params: raise ValueError('Must add parameters if object already has them.') - def add_signal(self, signal, params=None): + def add_signal(self, signal, params=None, index=None): """Add a signal to the current object. Parameters @@ -215,16 +254,11 @@ def add_signal(self, signal, params=None): 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. + index : int + Index to insert the new signal in the signals attribute. """ - if not self.signals.size: - self.signals = np.atleast_2d(signal) - else: - try: - self.signals = np.vstack([self.signals, signal]) - except ValueError as array_value_error: - msg = 'Size of the added signal is not consistent with existing signals.' - raise ValueError(msg) from array_value_error + super().add_signal(signal, index=index) self.add_params(params) From bd9c3ad684be6bd14c9ad25ed7820944b146b71a Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 00:11:34 -0400 Subject: [PATCH 14/33] use pre initialization in sim objects in sim multi --- neurodsp/sim/multi.py | 34 ++++++++++++++-------------------- neurodsp/sim/signals.py | 13 ++++++++++--- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 5c56c292..84e412dd 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -41,12 +41,12 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): >>> sims = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sims = np.zeros([n_sims, compute_nsamples(sim_params['n_seconds'], sim_params['fs'])]) + sims = Simulations(n_sims, sim_params, sim_func) for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): - sims[ind, :] = sig + sims.add_signal(sig, index=ind) - if return_type == 'object': - sims = Simulations(sims, sim_params, sim_func) + if return_type == 'array': + sims = sims.signals return sims @@ -88,18 +88,15 @@ def sim_across_values(sim_func, sim_params, return_type='object'): >>> sims = sim_across_values(sim_powerlaw, params) """ - base = get_base_params(sim_params) + sims = VariableSimulations(len(sim_params), get_base_params(sim_params), sim_func, + update=getattr(sim_params, 'update', None), + component=getattr(sim_params, 'component', None)) - all_params = [None] * len(sim_params) - sims = np.zeros([len(sim_params), compute_nsamples(base['n_seconds'], base['fs'])]) for ind, cur_sim_params in enumerate(sim_params): - sims[ind, :] = sim_func(**cur_sim_params) - all_params[ind] = cur_sim_params + sims.add_signal(sim_func(**cur_sim_params), cur_sim_params, index=ind) - if return_type == 'object': - sims = VariableSimulations(sims, all_params, sim_func, - update=getattr(sim_params, 'update', None), - component=getattr(sim_params, 'component', None)) + if return_type == 'array': + sims = sims.signals return sims @@ -188,14 +185,11 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): >>> sims = sim_from_sampler(sim_powerlaw, param_sampler, n_sims=2) """ - all_params = [None] * n_sims - n_samples = compute_nsamples(sim_sampler.params['n_seconds'], sim_sampler.params['fs']) - sims = np.zeros([n_sims, n_samples]) + sims = VariableSimulations(n_sims, get_base_params(sim_sampler), sim_func) for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): - sims[ind, :] = sig - all_params[ind] = params + sims.add_signal(sim_func(**params), params, index=ind) - if return_type == 'object': - sims = VariableSimulations(sims, all_params, sim_func) + if return_type == 'array': + sims = sims.signals return sims diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 9d9fbfe4..26f6c19e 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -171,6 +171,8 @@ def __init__(self, signals=None, params=None, sim_func=None, update=None, compon """Initialize SampledSimulations object.""" Simulations.__init__(self, signals, params, sim_func) + if isinstance(signals, int): + self._params = [{}] * signals self.update = update self.component = component @@ -208,13 +210,15 @@ def values(self): return get_param_values(self.params, self.update, self.component) - def add_params(self, params): + def add_params(self, params, index=None): """Add parameter definition(s) to object. Parameters ---------- params : dict or list of dict, optional The simulation parameter definition(s). + index : int + Index to insert the new parameter definition. """ if params: @@ -236,7 +240,10 @@ def add_params(self, params): raise ValueError(msg) self._params = cparams else: - self._params.extend(cparams) + if index is not None: + self._params[index] = cparams[0] + else: + self._params.extend(cparams) else: if self.has_params: @@ -259,7 +266,7 @@ def add_signal(self, signal, params=None, index=None): """ super().add_signal(signal, index=index) - self.add_params(params) + self.add_params(params, index=index) class MultiSimulations(): From 7348a50d067a6b723abcecd33791c5bc9d101480 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 00:17:03 -0400 Subject: [PATCH 15/33] drop return type option from sim multi --- neurodsp/sim/multi.py | 62 +++++++++----------------------- neurodsp/tests/sim/test_multi.py | 24 +++---------- 2 files changed, 21 insertions(+), 65 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 84e412dd..21c4fad4 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -10,7 +10,7 @@ ################################################################################################### ################################################################################################### -def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): +def sim_multiple(sim_func, sim_params, n_sims): """Simulate multiple samples of a specified simulation. Parameters @@ -21,16 +21,12 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): 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 ------- - sims : Simulations or 2d array - Simulations, return type depends on `return_type` argument. - Simulated time series are organized as [n_sims, sig length]. + sims : Simulations + Simulations object with simulated time series and metadata. + Simulated signals are in the 'signals' attribute with shape [n_sims, sig_length]. Examples -------- @@ -45,13 +41,10 @@ def sim_multiple(sim_func, sim_params, n_sims, return_type='object'): for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): sims.add_signal(sig, index=ind) - if return_type == 'array': - sims = sims.signals - return sims -def sim_across_values(sim_func, sim_params, return_type='object'): +def sim_across_values(sim_func, sim_params): """Simulate signals across different parameter values. Parameters @@ -60,16 +53,12 @@ def sim_across_values(sim_func, sim_params, return_type='object'): Function to create the simulated time series. sim_params : ParamIter or iterable or list of dict Simulation parameters for `sim_func`. - return_type : {'object', 'array'} - Specifies the return type of the simulations. - If 'object', returns simulations and metadata in a 'VariableSimulations' object. - If 'array', returns the simulations (no metadata) in an array. Returns ------- - sims : VariableSimulations or array - Simulations, return type depends on `return_type` argument. - If array, signals are collected together as [n_sims, sig_length]. + sims : VariableSimulations + Simulations object with simulated time series and metadata. + Simulated signals are in the 'signals' attribute with shape [n_sims, sig_length]. Examples -------- @@ -95,13 +84,10 @@ def sim_across_values(sim_func, sim_params, return_type='object'): for ind, cur_sim_params in enumerate(sim_params): sims.add_signal(sim_func(**cur_sim_params), cur_sim_params, index=ind) - if return_type == 'array': - sims = sims.signals - return sims -def sim_multi_across_values(sim_func, sim_params, n_sims, return_type='object'): +def sim_multi_across_values(sim_func, sim_params, n_sims): """Simulate multiple signals across different parameter values. Parameters @@ -112,16 +98,12 @@ def sim_multi_across_values(sim_func, sim_params, n_sims, return_type='object'): Simulation parameters for `sim_func`. n_sims : int Number of simulations to create per parameter definition. - return_type : {'object', 'array'} - Specifies the return type of the simulations. - If 'object', returns simulations and metadata in a 'MultiSimulations' object. - If 'array', returns the simulations (no metadata) in an array. Returns ------- - sims : MultiSimulations or array - Simulations, return type depends on `return_type` argument. - If array, signals are collected together as [n_sets, n_sims, sig_length]. + sims : MultiSimulations + Simulations object with simulated time series and metadata. + Simulated signals are in the 'signals' attribute with shape [n_sets, n_sims, sig_length]. Examples -------- @@ -143,15 +125,12 @@ def sim_multi_across_values(sim_func, sim_params, n_sims, return_type='object'): sims = MultiSimulations(update=getattr(sim_params, 'update', None), component=getattr(sim_params, 'component', None)) for cur_sim_params in sim_params: - sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims, 'object')) - - if return_type == 'array': - sims = np.squeeze(np.array([el.signals for el in sims])) + sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims)) return sims -def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): +def sim_from_sampler(sim_func, sim_sampler, n_sims): """Simulate a set of signals from a parameter sampler. Parameters @@ -162,16 +141,12 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): Parameter definition to sample from. n_sims : int Number of simulations to create per parameter definition. - return_type : {'object', 'array'} - Specifies the return type of the simulations. - If 'object', returns simulations and metadata in a 'VariableSimulations' object. - If 'array', returns the simulations (no metadata) in an array. Returns ------- - sims : VariableSimulations or 2d array - Simulations, return type depends on `return_type` argument. - If array, simulations are organized as [n_sims, sig length]. + sims : VariableSimulations + Simulations object with simulated time series and metadata. + Simulated signals are in the 'signals' attribute with shape [n_sims, sig_length]. Examples -------- @@ -189,7 +164,4 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims, return_type='object'): for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): sims.add_signal(sim_func(**params), params, index=ind) - if return_type == 'array': - sims = sims.signals - return sims diff --git a/neurodsp/tests/sim/test_multi.py b/neurodsp/tests/sim/test_multi.py index 5fbb0b4f..2809adf1 100644 --- a/neurodsp/tests/sim/test_multi.py +++ b/neurodsp/tests/sim/test_multi.py @@ -16,31 +16,23 @@ def test_sim_multiple(): n_sims = 2 params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} - sims_obj = sim_multiple(sim_powerlaw, params, n_sims, 'object') + sims_obj = sim_multiple(sim_powerlaw, params, n_sims) assert isinstance(sims_obj, Simulations) assert sims_obj.signals.shape[0] == n_sims assert sims_obj.params == params - sims_arr = sim_multiple(sim_powerlaw, params, n_sims, 'array') - assert isinstance(sims_arr, np.ndarray) - assert sims_arr.shape[0] == n_sims - def test_sim_across_values(tsim_iters): params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - sims_obj = sim_across_values(sim_powerlaw, params, 'object') + sims_obj = sim_across_values(sim_powerlaw, params) assert isinstance(sims_obj, VariableSimulations) assert len(sims_obj) == len(params) for csim, cparams, oparams in zip(sims_obj, sims_obj.params, params): assert isinstance(csim, np.ndarray) assert cparams == oparams - sims_arr = sim_across_values(sim_powerlaw, params, 'array') - assert isinstance(sims_arr, np.ndarray) - assert sims_arr.shape[0] == len(params) - # Test with ParamIter input siter = tsim_iters['pl_exp'] sims_iter = sim_across_values(sim_powerlaw, siter) @@ -54,17 +46,13 @@ def test_sim_multi_across_values(tsim_iters): params = [{'n_seconds' : 2, 'fs' : 250, 'exponent' : -2}, {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1}] - sims_obj = sim_multi_across_values(sim_powerlaw, params, n_sims, 'object') + sims_obj = sim_multi_across_values(sim_powerlaw, params, n_sims) assert isinstance(sims_obj, MultiSimulations) for sims, cparams in zip(sims_obj, params): assert isinstance(sims, Simulations) assert len(sims) == n_sims assert sims.params == cparams - sims_arr = sim_multi_across_values(sim_powerlaw, params, n_sims, 'array') - assert isinstance(sims_arr, np.ndarray) - assert sims_arr.shape[0:2] == (len(params), n_sims) - # Test with ParamIter input siter = tsim_iters['pl_exp'] sims_iter = sim_multi_across_values(sim_powerlaw, siter, n_sims) @@ -79,11 +67,7 @@ def test_sim_from_sampler(): samplers = {create_updater('exponent') : create_sampler([-2, -1, 0])} psampler = ParamSampler(params, samplers) - sims_obj = sim_from_sampler(sim_powerlaw, psampler, n_sims, 'object') + sims_obj = sim_from_sampler(sim_powerlaw, psampler, n_sims) assert isinstance(sims_obj, VariableSimulations) 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 589d14e3c08189ceff6b01237d2dc449d89e1a4f Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 00:28:41 -0400 Subject: [PATCH 16/33] add tests for signal object pre initializations --- neurodsp/tests/sim/test_signals.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/neurodsp/tests/sim/test_signals.py b/neurodsp/tests/sim/test_signals.py index c68e1147..e703b90b 100644 --- a/neurodsp/tests/sim/test_signals.py +++ b/neurodsp/tests/sim/test_signals.py @@ -4,6 +4,8 @@ import numpy as np +from neurodsp.sim.params import get_base_params + from neurodsp.sim.signals import * ################################################################################################### @@ -44,6 +46,16 @@ def test_simulations(): assert sims_full.has_signals assert sims_full.has_params + # Test pre-initialization + sims_pre = Simulations(n_sigs, params, 'sim_func') + assert len(sims_pre) == n_sigs + assert np.sum(sims_pre.signals) == 0 + assert sims_pre.has_signals and sims_pre.has_params + for ind, sig in enumerate(sigs): + sims_pre.add_signal(sig, ind) + assert len(sims_pre) == n_sigs + assert np.sum(sims_pre.signals) != 0 + def test_variable_simulations(): # Test empty initialization @@ -80,6 +92,16 @@ def test_variable_simulations(): assert sims_full.has_signals assert sims_full.has_params + # Test pre-initialization + sims_pre = VariableSimulations(n_sigs, get_base_params(params), 'sim_func') + assert len(sims_pre) == n_sigs + assert np.sum(sims_pre.signals) == 0 + assert sims_pre.has_signals and sims_pre.has_params + for ind, (sig, cparams) in enumerate(zip(sigs, params)): + sims_pre.add_signal(sig, cparams, ind) + assert len(sims_pre) == n_sigs + assert np.sum(sims_pre.signals) != 0 + def test_variable_simulations_add(): sig = np.array([1, 2, 3, 4, 5]) From 73aacd8cde0ac788969c6efd80fd9c66f20a215d Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 20:41:24 -0400 Subject: [PATCH 17/33] add sim IO file --- neurodsp/sim/io.py | 207 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 neurodsp/sim/io.py diff --git a/neurodsp/sim/io.py b/neurodsp/sim/io.py new file mode 100644 index 00000000..067b50f4 --- /dev/null +++ b/neurodsp/sim/io.py @@ -0,0 +1,207 @@ +"""Simulation I/O functions that return multiple instances.""" + +import os +import json +from pathlib import Path + +import numpy as np + +################################################################################################### +################################################################################################### + +## BASE I/O UTILITIES + +def fpath(file_path, file_name): + """Check and combine a file name and path into a Path object. + + Parameters + ---------- + file_path : str or None + Name of the directory. + file_name : str + Name of the file. + + Returns + ------- + Path + Path object for the full file path. + """ + + return Path(file_path) / file_name if file_path else Path(file_name) + + +def save_json(file_name, data): + """Save data to a json file. + + Parameters + ---------- + file_name : str + Name of the file to save to. + data : dict + Data to save to file. + """ + + with open(file_name, 'w') as json_file: + json.dump(data, json_file) + + +def save_jsonlines(file_name, data): + """Save data to a jsonlines file. + + Parameters + ---------- + file_name : str + Name of the file to save to. + data : list of dict + Data to save to file. + """ + + with open(file_name, 'w') as jsonlines_file: + for row in data: + json.dump(row, jsonlines_file) + jsonlines_file.write('\n') + + +def load_json(file_name): + """Load data from a json file. + + Parameters + ---------- + file_name : str + Name of the file to load from. + + Returns + ------- + data : dict + Loaded data. + """ + + with open(file_name, 'r') as json_file: + data = json.load(json_file) + + return data + + +def load_jsonlines(file_name): + """Load data from a jsonlines file. + + Parameters + ---------- + file_name : str + Name of the file to load from. + + Returns + ------- + data : list of dict + Loaded data. + """ + + data = [] + with open(file_name, 'r') as json_file: + for row in json_file: + data.append(json.loads(row)) + + return data + + +## SIMULATION OBJECT I/O + +def save_sims(sims, label, file_path=None, replace=False): + """Save simulations. + + Parameters + ---------- + sims : Simulations or VariableSimulations or MultipleSimulations + Simulations to save. + label : str + Label to attach to the simulation name. + file_path : str, optional + Directory to save to. + replace : bool, optional, default: False + Whether to replace any existing saved files with the same name. + """ + + assert '_' not in label, 'Cannot have underscores in simulation label.' + + save_path_items = ['sim_unknown' if not sims.sim_func else sims.sim_func] + if isinstance(sims, (VariableSimulations, MultiSimulations)): + if sims.component: + save_path_items.append(sims.component) + if sims.update: + save_path_items.append(sims.update) + save_path_items.append(label) + save_path = '_'.join(save_path_items) + + save_folder = fpath(file_path, save_path) + + if os.path.exists(save_folder): + if not replace: + raise ValueError('Simulation files already exist.') + else: + os.mkdir(save_folder) + + if isinstance(sims, MultiSimulations): + for ind, csims in enumerate(sims.signals): + save_sims(csims, 'set' + str(ind), file_path=save_folder, replace=True) + + else: + np.save(save_folder / 'signals', sims.signals) + if isinstance(sims, VariableSimulations): + save_jsonlines(save_folder / 'params.jsonlines', sims.params) + elif isinstance(sims, Simulations): + save_json(save_folder / 'params.json', sims.params) + + +def load_sims(load_name, file_path=None): + """Load simulations. + + Parameters + ---------- + load_name : str + The name or label of the simulations to load. + If not the full file name, this string can be the label the simulations were saved with. + file_path : str, optional + Directory to load from. + + Returns + ------- + sims : Simulations or VariableSimulations or MultipleSimulations + Loaded simulations. + """ + + if '_' not in load_name: + matches = [el for el in os.listdir(file_path) if load_name in el] + assert len(matches) > 0, 'No matches found for requested simulation label.' + assert len(matches) == 1, 'Multiple matches found for requested simulation label.' + load_name = matches[0] + + splits = load_name.split('_') + sim_func = '_'.join(splits[0:2]) if splits[1] != 'unknown' else None + + update, component = None, None + if len(splits) > 3: + splits = splits[2:-1] + update = splits.pop() + component = '_'.join(splits) if splits else None + + load_folder = fpath(file_path, load_name) + load_files = os.listdir(load_folder) + + if 'signals.npy' not in load_files: + + msims = [load_sims(str(load_folder / load_file)) for load_file in load_files] + sims = MultiSimulations(msims, None, sim_func, update, component) + + else: + + sigs = np.load(load_folder / 'signals.npy') + + if 'params.json' in load_files: + params = load_json(load_folder / 'params.json') + sims = Simulations(sigs, params, sim_func) + + elif 'params.jsonlines' in load_files: + params = load_jsonlines(load_folder / 'params.jsonlines') + sims = VariableSimulations(sigs, params, sim_func, update, component) + + return sims From 80ced1671cf4368303ee275481d88b1bd3ae9a95 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 20:41:41 -0400 Subject: [PATCH 18/33] update tests for using files folder --- neurodsp/tests/conftest.py | 3 ++- neurodsp/tests/settings.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/neurodsp/tests/conftest.py b/neurodsp/tests/conftest.py index f2b82a83..8fa4f160 100644 --- a/neurodsp/tests/conftest.py +++ b/neurodsp/tests/conftest.py @@ -12,7 +12,7 @@ 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) + BASE_TEST_FILE_PATH, TEST_PLOTS_PATH, TEST_FILES_PATH) ################################################################################################### ################################################################################################### @@ -96,3 +96,4 @@ def check_dir(): # Remake (empty) directories os.mkdir(BASE_TEST_FILE_PATH) os.mkdir(TEST_PLOTS_PATH) + os.mkdir(TEST_FILES_PATH) diff --git a/neurodsp/tests/settings.py b/neurodsp/tests/settings.py index 3d605d75..bf328b41 100644 --- a/neurodsp/tests/settings.py +++ b/neurodsp/tests/settings.py @@ -37,4 +37,5 @@ # Set paths for test files TESTS_PATH = Path(os.path.abspath(os.path.dirname(__file__))) BASE_TEST_FILE_PATH = TESTS_PATH / 'test_files' -TEST_PLOTS_PATH = os.path.join(BASE_TEST_FILE_PATH, 'plots') +TEST_PLOTS_PATH = BASE_TEST_FILE_PATH / 'plots' +TEST_FILES_PATH = BASE_TEST_FILE_PATH / 'files' From 0d42d01f6832e63ccb97ee7feef70aaf2402a6fe Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 20:41:54 -0400 Subject: [PATCH 19/33] add tests for base sim io funcs --- neurodsp/tests/sim/test_io.py | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 neurodsp/tests/sim/test_io.py diff --git a/neurodsp/tests/sim/test_io.py b/neurodsp/tests/sim/test_io.py new file mode 100644 index 00000000..fa5a095e --- /dev/null +++ b/neurodsp/tests/sim/test_io.py @@ -0,0 +1,55 @@ +"""Tests for neurodsp.sim.io.""" + +import os +from pathlib import Path + +from neurodsp.tests.settings import TEST_FILES_PATH + +from neurodsp.sim.io import * + +################################################################################################### +################################################################################################### + +def test_fpath(): + + out1 = fpath(None, 'file_name') + assert isinstance(out1, Path) + assert str(out1) == 'file_name' + + out2 = fpath('path', 'file_name') + assert isinstance(out2, Path) + assert str(out2) == 'path/file_name' + +def test_save_json(): + + data = {'a' : 1, 'b' : 2} + fname = 'test_json_file.json' + save_json(TEST_FILES_PATH / fname, data) + assert os.path.exists(TEST_FILES_PATH / fname) + +def test_save_jsonlines(): + + data = [{'a' : 1, 'b' : 2}, {'a' : 10, 'b' : 20}] + fname = 'test_jsonlines_file.json' + save_jsonlines(TEST_FILES_PATH / fname, data) + assert os.path.exists(TEST_FILES_PATH / fname) + +def test_load_json(): + + data_saved = {'a' : 1, 'b' : 2} + fname = 'test_json_file.json' + data_loaded = load_json(TEST_FILES_PATH / fname) + assert data_loaded == data_saved + +def test_load_jsonlines(): + + data_saved = [{'a' : 1, 'b' : 2}, {'a' : 10, 'b' : 20}] + fname = 'test_jsonlines_file.json' + data_loaded = load_jsonlines(TEST_FILES_PATH / fname) + assert data_loaded == data_saved + +def test_save_sims(): + pass + +def test_load_sims(): + pass From 056e47d3372f8637888a4fd53a48661800b9f9f1 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 23:10:25 -0400 Subject: [PATCH 20/33] add tests for sim save / loads --- neurodsp/sim/io.py | 6 ++-- neurodsp/tests/conftest.py | 26 +++++++++++--- neurodsp/tests/sim/test_io.py | 67 ++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/neurodsp/sim/io.py b/neurodsp/sim/io.py index 067b50f4..2be6fe21 100644 --- a/neurodsp/sim/io.py +++ b/neurodsp/sim/io.py @@ -6,6 +6,8 @@ import numpy as np +from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations + ################################################################################################### ################################################################################################### @@ -185,11 +187,11 @@ def load_sims(load_name, file_path=None): component = '_'.join(splits) if splits else None load_folder = fpath(file_path, load_name) - load_files = os.listdir(load_folder) + load_files = [file for file in os.listdir(load_folder) if file[0] != '.'] if 'signals.npy' not in load_files: - msims = [load_sims(str(load_folder / load_file)) for load_file in load_files] + msims = [load_sims(load_file, load_folder) for load_file in load_files] sims = MultiSimulations(msims, None, sim_func, update, component) else: diff --git a/neurodsp/tests/conftest.py b/neurodsp/tests/conftest.py index 8fa4f160..37338e04 100644 --- a/neurodsp/tests/conftest.py +++ b/neurodsp/tests/conftest.py @@ -9,6 +9,7 @@ from neurodsp.sim import sim_oscillation, sim_powerlaw, sim_combined from neurodsp.sim.update import create_updater, create_sampler from neurodsp.sim.params import SimParams +from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations 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, @@ -62,8 +63,8 @@ def tsim_params(): sim_params = SimParams(N_SECONDS, FS) sim_params.register_group({ - 'pl' : {'sim_powerlaw' : {'exponent' : -1}}, - 'osc' : {'sim_oscillation' : {'freq' : -1}}, + 'pl' : {'exponent' : -1}, + 'osc' : {'freq' : -1}, }) yield sim_params @@ -72,7 +73,7 @@ def tsim_params(): def tsim_iters(tsim_params): sim_iters = tsim_params.to_iters() - sim_iters.register_iter('pl_exp', 'pl', 'exponent', [-2, -1, 0]) + sim_iters.register_iter('pl_exp', 'pl', 'exponent', [-2, -1]) yield sim_iters @@ -81,10 +82,27 @@ 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])}) + 'samp_exp', 'pl', {create_updater('exponent') : create_sampler([-2, -1])}) yield sim_samplers +@pytest.fixture(scope='session') +def tsims(tsig2d, tsim_params): + + yield Simulations(tsig2d, tsim_params['pl'], 'sim_test') + +@pytest.fixture(scope='session') +def tvsims(tsig2d, tsim_iters): + + params = [ps for ps in tsim_iters.iters['pl_exp'].yielder] + yield VariableSimulations(tsig2d, params, 'sim_test', tsim_iters.iters['pl_exp'].update) + +@pytest.fixture(scope='session') +def tmsims(tsig2d, tsim_iters): + + params = [ps for ps in tsim_iters.iters['pl_exp'].yielder] + yield MultiSimulations([tsig2d, tsig2d], params, 'sim_test', tsim_iters.iters['pl_exp'].update) + @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_io.py b/neurodsp/tests/sim/test_io.py index fa5a095e..b8bd257e 100644 --- a/neurodsp/tests/sim/test_io.py +++ b/neurodsp/tests/sim/test_io.py @@ -48,8 +48,65 @@ def test_load_jsonlines(): data_loaded = load_jsonlines(TEST_FILES_PATH / fname) assert data_loaded == data_saved -def test_save_sims(): - pass - -def test_load_sims(): - pass +def test_save_sims_sim(tsims): + + label = 'tsims' + folder = '_'.join([tsims.sim_func, label]) + + save_sims(tsims, label, TEST_FILES_PATH) + assert os.path.exists(TEST_FILES_PATH / folder) + assert os.path.exists(TEST_FILES_PATH / folder / 'params.json') + assert os.path.exists(TEST_FILES_PATH / folder / 'signals.npy') + +def test_load_sims_sim(tsims): + # Loads and tests object saved from `test_save_sims_sim` + + loaded_sims = load_sims('tsims', TEST_FILES_PATH) + assert np.array_equal(loaded_sims.signals, tsims.signals) + assert loaded_sims.sim_func == tsims.sim_func + assert loaded_sims.params == tsims.params + +def test_save_sims_vsim(tvsims): + + label = 'tvsims' + folder = '_'.join([tvsims.sim_func, tvsims.update, label]) + + save_sims(tvsims, label, TEST_FILES_PATH) + assert os.path.exists(TEST_FILES_PATH / folder) + assert os.path.exists(TEST_FILES_PATH / folder / 'params.jsonlines') + assert os.path.exists(TEST_FILES_PATH / folder / 'signals.npy') + +def test_load_sims_vsim(tvsims): + # Loads and tests object saved from `test_save_sims_vsim` + + loaded_sims = load_sims('tvsims', TEST_FILES_PATH) + assert np.array_equal(loaded_sims.signals, tvsims.signals) + assert loaded_sims.sim_func == tvsims.sim_func + assert loaded_sims.params == tvsims.params + assert loaded_sims.update == tvsims.update + assert loaded_sims.component == tvsims.component + +def test_save_sims_msim(tmsims): + + label = 'tmsims' + folder = '_'.join([tmsims.sim_func, tmsims.update, label]) + sub_folder = '_'.join([tmsims.sim_func, 'set']) + + save_sims(tmsims, label, TEST_FILES_PATH) + assert os.path.exists(TEST_FILES_PATH / folder) + for ind in range(len(tmsims)): + assert os.path.exists(TEST_FILES_PATH / folder / (sub_folder + str(ind))) + assert os.path.exists(TEST_FILES_PATH / folder / (sub_folder + str(ind)) / 'params.json') + assert os.path.exists(TEST_FILES_PATH / folder / (sub_folder + str(ind)) / 'signals.npy') + +def test_load_sims_msim(tmsims): + # Loads and tests object saved from `test_save_sims_msim` + + label = 'tmsims' + loaded_sims = load_sims(label, TEST_FILES_PATH) + assert loaded_sims.sim_func == tmsims.sim_func + #assert loaded_sims.params == tmsims.params + assert loaded_sims.update == tmsims.update + assert loaded_sims.component == tmsims.component + for lsig, csig in zip(loaded_sims.signals, tmsims.signals): + assert np.array_equal(lsig.signals, csig.signals) From 2d1be6fdee189ecb0d70a75ec9ec81d85d1db7a6 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sat, 7 Sep 2024 23:11:45 -0400 Subject: [PATCH 21/33] add sim io funcs to api list --- doc/api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/api.rst b/doc/api.rst index dbd1c973..d34b0fe6 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -385,6 +385,16 @@ Modulate Signals rotate_timeseries modulate_signal +I/O +~~~ + +.. currentmodule:: neurodsp.sim.io +.. autosummary:: + :toctree: generated/ + + save_sims + load_sims + Random Seed ~~~~~~~~~~~ From dcf6a98e9d6b23f1d03c721ea90bf8b46d903646 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 10:55:17 -0400 Subject: [PATCH 22/33] lints --- neurodsp/sim/multi.py | 5 +---- neurodsp/utils/checks.py | 5 ++++- neurodsp/utils/core.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 21c4fad4..c42793c6 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -1,11 +1,8 @@ """Simulation functions that return multiple instances.""" -import numpy as np - from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler from neurodsp.sim.params import get_base_params -from neurodsp.utils.data import compute_nsamples ################################################################################################### ################################################################################################### @@ -162,6 +159,6 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims): sims = VariableSimulations(n_sims, get_base_params(sim_sampler), sim_func) for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): - sims.add_signal(sim_func(**params), params, index=ind) + sims.add_signal(sig, params, index=ind) return sims diff --git a/neurodsp/utils/checks.py b/neurodsp/utils/checks.py index 11541b4f..20f00790 100644 --- a/neurodsp/utils/checks.py +++ b/neurodsp/utils/checks.py @@ -30,8 +30,11 @@ def check_param_range(param, label, bounds): "It should be between {:1.1f} and {:1.1f}.".format(*bounds) raise ValueError(msg) + # Alias for non-breaking backwards compatibility -def check_param(param, label, bounds): check_param_range(param, label, bounds) +def check_param(param, label, bounds): + check_param_range(param, label, bounds) + def check_param_options(param, label, options): """Check a parameter value is one of the acceptable options. diff --git a/neurodsp/utils/core.py b/neurodsp/utils/core.py index 33c81230..d159d9d6 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) or isinstance(arg, dict): + if not isinstance(arg, Iterable) or isinstance(arg, (str, 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 99d738995bd604c42e18d392fca7984b9c1d487a Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 11:42:23 -0400 Subject: [PATCH 23/33] test checks & extensions --- neurodsp/sim/params.py | 8 ++-- neurodsp/tests/sim/test_params.py | 63 ++++++++++++++++++++++++++---- neurodsp/tests/sim/test_signals.py | 1 + 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 44f703d3..5fac8ad2 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -53,18 +53,18 @@ def drop_base_params(params): Parameters ---------- params : dict or list of dict - Parameter definition. + Parameter definition(s). Returns ------- - params : dict - Parameter definition, excluding base parameters. + params : dict or list of dict + Parameter definition(s), excluding base parameters. """ if isinstance(params, dict): params = _drop_base_params(params) elif isinstance(params, list): - params = [_get_base_params(cparams) for cparams in params] + params = [_drop_base_params(cparams) for cparams in params] else: raise ValueError('Parameter definition not understood.') diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index 6127f1f1..c03d762a 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -1,5 +1,7 @@ """Tests for neurodsp.sim.params.""" +from pytest import raises + from neurodsp.sim.update import create_updater, create_sampler from neurodsp.sim.params import * @@ -25,6 +27,9 @@ def test_get_base_params(tsim_iters): for bparam in out3: assert bparam in BASE_PARAMS + with raises(ValueError): + get_base_params('parameters') + def test_drop_base_params(): params = {'n_seconds' : 2, 'fs' : 250, 'exponent' : -1} @@ -34,10 +39,14 @@ def test_drop_base_params(): assert 'exponent' in out1 params_lst = [params, params] - out2 = drop_base_params(params) - for bparam in BASE_PARAMS: - assert bparam not in out2 - assert 'exponent' in out2 + out2 = drop_base_params(params_lst) + for cparams in out2: + for bparam in BASE_PARAMS: + assert bparam not in cparams + assert 'exponent' in cparams + + with raises(ValueError): + drop_base_params('parameters') def test_get_param_values(): @@ -75,6 +84,14 @@ def test_sim_params(): assert comp1.items() <= sps2['pl'].items() assert comp2.items() <= sps2['osc'].items() + # Test clearing and re-registering + sps2.register_group({'pl2' : comp1, 'osc2' : comp2}, clear=True) + for old_label in ['pl', 'osc']: + with raises(KeyError): + sps2[old_label] + assert comp1.items() <= sps2['pl2'].items() + assert comp2.items() <= sps2['osc2'].items() + def test_sim_params_props(tsim_params): # Test properties @@ -84,7 +101,9 @@ def test_sim_params_props(tsim_params): # Test copy and clear ntsim = tsim_params.copy() assert ntsim != tsim_params - ntsim.clear() + ntsim.clear(True) + assert ntsim.params == {} + assert ntsim.base == {'n_seconds': None, 'fs': None} def test_sim_params_make_params(tsim_params): # Test the SimParams `make_` methods @@ -154,6 +173,17 @@ def test_sim_iters(): assert sis2['pl_exp'] assert sis2['osc_freq'] + # Test clearing and re-registering group with list inputs + sis2.register_group_iters([ + ['pl_exp2', 'pl', 'exponent', [-2, -1 ,0]], + ['osc_freq2', 'osc', 'freq', [10, 20, 30]], + ], clear=True) + for old_label in ['pl_exp', 'osc_freq']: + with raises(KeyError): + sis2[old_label] + assert sis2['pl_exp2'] is not None + assert sis2['osc_freq2'] is not None + def test_sim_iters_props(tsim_iters): # Test properties @@ -163,7 +193,10 @@ def test_sim_iters_props(tsim_iters): # Test copy and clear ntiter = tsim_iters.copy() assert ntiter != tsim_iters - ntiter.clear() + ntiter.clear(True, True, True) + assert ntiter.iters == {} + assert ntiter.params == {} + assert ntiter.base == {'n_seconds': None, 'fs': None} def test_sim_iters_upd(tsim_iters): @@ -193,16 +226,30 @@ def test_sim_samplers(): assert sss2['samp_exp'] is not None assert sss2['samp_freq'] is not None + # Test clearing and re-registering group with list inputs + sss2.register_group_samplers([ + ['samp_exp2', 'pl', {create_updater('exponent') : create_sampler([-2, -1, 0])}], + ['samp_freq2', 'osc', {create_updater('freq') : create_sampler([10, 20, 30])}], + ], clear=True) + for old_label in ['samp_exp', 'samp_freq']: + with raises(KeyError): + sss2[old_label] + assert sss2['samp_exp2'] is not None + assert sss2['samp_freq2'] 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 + # Can't directly copy object with generator - so regenerate, and test clear ntsim = tsim_params.copy() ntsamp = ntsim.to_samplers() - ntsamp.clear() + ntsamp.clear(True, True, True) + assert ntsamp.samplers == {} + assert ntsamp.params == {} + assert ntsamp.base == {'n_seconds': None, 'fs': None} def test_sim_samplers_upd(tsim_samplers): diff --git a/neurodsp/tests/sim/test_signals.py b/neurodsp/tests/sim/test_signals.py index e703b90b..232e89dc 100644 --- a/neurodsp/tests/sim/test_signals.py +++ b/neurodsp/tests/sim/test_signals.py @@ -177,6 +177,7 @@ def test_multi_simulations(): assert params_obj == params_org assert sims_full.sim_func assert sims_full.values + assert sims_full._base_params def test_multi_simulations_add(): From 7a7dce44eb5f27c432f92ee7ebb6e5f106456dbd Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 11:48:20 -0400 Subject: [PATCH 24/33] add contains dunder to params objects --- neurodsp/sim/params.py | 12 ++++++++++++ neurodsp/tests/sim/test_params.py | 25 +++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 5fac8ad2..6b920aae 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -147,6 +147,18 @@ def __getitem__(self, label): return {**self.base, **self._params[label]} + def __contains__(self, label): + """Define object contents as whether label exists in object. + + Parameters + ---------- + label : str + Label to check whether it exists in object. + """ + + return label in self.labels + + @property def base(self): """Get the base parameters, common across all simulations. diff --git a/neurodsp/tests/sim/test_params.py b/neurodsp/tests/sim/test_params.py index c03d762a..22023108 100644 --- a/neurodsp/tests/sim/test_params.py +++ b/neurodsp/tests/sim/test_params.py @@ -76,11 +76,14 @@ def test_sim_params(): # Test registering new simulation parameter definition sps1.register('pl', comp1) + assert 'pl' in sps1 assert comp1.items() <= sps1['pl'].items() # Test registering a group of new simulation parameter definitions sps2 = SimParams(5, 250) sps2.register_group({'pl' : comp1, 'osc' : comp2}) + for label in ['pl', 'osc']: + assert label in sps2 assert comp1.items() <= sps2['pl'].items() assert comp2.items() <= sps2['osc'].items() @@ -89,6 +92,8 @@ def test_sim_params(): for old_label in ['pl', 'osc']: with raises(KeyError): sps2[old_label] + for label in ['pl2', 'osc2']: + assert label in sps2 assert comp1.items() <= sps2['pl2'].items() assert comp2.items() <= sps2['osc2'].items() @@ -160,7 +165,7 @@ def test_sim_iters(): sis1 = SimIters(5, 250) sis1.register('pl', comp_plw) sis1.register_iter('pl_exp', 'pl', 'exponent', [-2, -1, 0]) - assert sis1['pl_exp'] + assert 'pl_exp' in sis1 assert sis1['pl_exp'].values == [-2, -1, 0] # Test registering a group of new simulation iterator definitions @@ -170,8 +175,8 @@ def test_sim_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'] + for label in ['pl_exp', 'osc_freq']: + assert label in sis2 # Test clearing and re-registering group with list inputs sis2.register_group_iters([ @@ -181,8 +186,8 @@ def test_sim_iters(): for old_label in ['pl_exp', 'osc_freq']: with raises(KeyError): sis2[old_label] - assert sis2['pl_exp2'] is not None - assert sis2['osc_freq2'] is not None + for label in ['pl_exp2', 'osc_freq2']: + assert label in sis2 def test_sim_iters_props(tsim_iters): @@ -209,7 +214,7 @@ def test_sim_samplers(): 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 + assert 'samp_exp' in sss1 # Test registering a group of new simulation sampler definitions sss2 = SimSamplers(5, 250) @@ -223,8 +228,8 @@ def test_sim_samplers(): {'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 + for label in ['samp_exp', 'samp_freq']: + assert label in sss2 # Test clearing and re-registering group with list inputs sss2.register_group_samplers([ @@ -234,8 +239,8 @@ def test_sim_samplers(): for old_label in ['samp_exp', 'samp_freq']: with raises(KeyError): sss2[old_label] - assert sss2['samp_exp2'] is not None - assert sss2['samp_freq2'] is not None + for label in ['samp_exp2', 'samp_freq2']: + assert label in sss2 def test_sim_samplers_props(tsim_samplers, tsim_params): From 7613d39f5c593957cdb81bc6a9114764ccf066a8 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 12:35:32 -0400 Subject: [PATCH 25/33] consitency updates --- neurodsp/sim/__init__.py | 2 +- neurodsp/sim/params.py | 60 ++++++++++++++++++++++++---------------- neurodsp/sim/update.py | 12 ++++---- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/neurodsp/sim/__init__.py b/neurodsp/sim/__init__.py index c60b84d1..47f3bab3 100644 --- a/neurodsp/sim/__init__.py +++ b/neurodsp/sim/__init__.py @@ -9,4 +9,4 @@ 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, sim_combined_peak -from .multi import sim_multiple, sim_across_values, sim_from_sampler +from .multi import sim_multiple, sim_across_values, sim_multi_across_values, sim_from_sampler diff --git a/neurodsp/sim/params.py b/neurodsp/sim/params.py index 6b920aae..b25c1cd3 100644 --- a/neurodsp/sim/params.py +++ b/neurodsp/sim/params.py @@ -200,12 +200,12 @@ def params(self): return {label : {**self.base, **params} for label, params in self._params.items()} - def make_params(self, parameters=None, **kwargs): + def make_params(self, params=None, **kwargs): """Make a simulation parameter definition from given parameters. Parameters ---------- - parameters : dict or list of dict + params : dict or list of dict Parameter definition(s) to create simulation parameter definition. **kwargs Additional keyword arguments to create the simulation definition. @@ -216,23 +216,23 @@ def make_params(self, parameters=None, **kwargs): Parameter definition. """ - return {**self.base, **self._make_params(parameters, **kwargs)} + return {**self.base, **self._make_params(params, **kwargs)} - def register(self, label, parameters=None, **kwargs): + def register(self, label, params=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 + params : 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) + self._params[label] = self._make_params(params, **kwargs) def register_group(self, group, clear=False): @@ -250,8 +250,8 @@ def register_group(self, group, clear=False): if clear: self.clear() - for label, parameters in group.items(): - self.register(label, parameters) + for label, params in group.items(): + self.register(label, params) def update_base(self, n_seconds=False, fs=False): @@ -349,27 +349,39 @@ def copy(self): return deepcopy(self) - def _make_params(self, parameters=None, **kwargs): - """Sub-function for `make_params`.""" + def _make_params(self, params=None, **kwargs): + """Sub-function to make parameter definition. - parameters = {} if not parameters else deepcopy(parameters) + Parameters + ---------- + params : 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. + """ + + params = {} if not params else deepcopy(params) - if isinstance(parameters, list): - comps = [parameters.pop(0)] - kwargs = {**kwargs, **parameters[0]} if parameters else kwargs - params = self._make_combined_params(comps, **kwargs) + if isinstance(params, list): + comps = [params.pop(0)] + kwargs = {**kwargs, **params[0]} if params else kwargs + out_params = self._make_combined_params(comps, **kwargs) else: - params = {**parameters, **kwargs} + out_params = {**params, **kwargs} # If any base parameters were passed in, clear them - for bparam in self.base: - params.pop(bparam, None) + out_params = drop_base_params(out_params) - return params + return out_params def _make_combined_params(self, components, component_variances=None): - """Make parameters for combined simulations, specifying multiple components. + """Sub-function to make parameters for combined simulations, specifying multiple components. Parameters ---------- @@ -384,17 +396,17 @@ def _make_combined_params(self, components, component_variances=None): Parameter definition. """ - parameters = {} + out_params = {} comps = {} for comp in components: comps.update(**deepcopy(comp)) - parameters['components'] = comps + out_params['components'] = comps if component_variances: - parameters['component_variances'] = component_variances + out_params['component_variances'] = component_variances - return parameters + return out_params class SimIters(SimParams): diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index 5b033e0d..0187e0c5 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -37,12 +37,12 @@ def base(self): ## PARAM UPDATERS -def param_updater(parameter): +def param_updater(param): """Create a lambda updater function to update a specified parameter. Parameters ---------- - parameter : str + param : str Name of the parameter to update. Returns @@ -51,15 +51,15 @@ def param_updater(parameter): Updater function which can update specified parameter in simulation parameters. """ - return lambda params, value : params.update({parameter : value}) + return lambda params, value : params.update({param : value}) -def component_updater(parameter, component): +def component_updater(param, component): """Create a lambda updater function to update a parameter within a simulation component. Parameters ---------- - parameter : str + param : str Name of the parameter to update. component : str Name of the component to update the parameter within. @@ -70,7 +70,7 @@ def component_updater(parameter, component): Updater function which can update specified parameter in simulation parameters. """ - return lambda params, value : params['components'][component].update({parameter : value}) + return lambda params, value : params['components'][component].update({param : value}) def create_updater(update, component=None): From 62696032559819c4f2d2e353d68874a1986db75a Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 12:45:45 -0400 Subject: [PATCH 26/33] updates to consistency for params --- neurodsp/sim/generators.py | 24 +++++++++--------- neurodsp/sim/multi.py | 42 +++++++++++++++---------------- neurodsp/sim/update.py | 20 +++++++-------- neurodsp/tests/sim/test_update.py | 8 +++--- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/neurodsp/sim/generators.py b/neurodsp/sim/generators.py index bfd64ca5..85ff06d0 100644 --- a/neurodsp/sim/generators.py +++ b/neurodsp/sim/generators.py @@ -7,14 +7,14 @@ ################################################################################################### ################################################################################################### -def sig_yielder(sim_func, sim_params, n_sims): +def sig_yielder(sim_func, 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 + params : dict The parameters for the simulated signal, passed into `sim_func`. n_sims : int, optional Number of simulations to set as the max. @@ -27,23 +27,23 @@ def sig_yielder(sim_func, sim_params, n_sims): """ for _ in counter(n_sims): - yield sim_func(**sim_params) + yield sim_func(**params) -def sig_sampler(sim_func, sim_params, return_sim_params=False, n_sims=None): +def sig_sampler(sim_func, params, return_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 + params : iterable The parameters for the simulated signal, passed into `sim_func`. - return_sim_params : bool, optional, default: False + return_params : bool, optional, default: False Whether to yield the simulation parameters as well as the simulated time series. n_sims : int, optional Number of simulations to set as the max. - If None, length is defined by the length of `sim_params`, and could be infinite. + If None, length is defined by the length of `params`, and could be infinite. Yields ------ @@ -51,18 +51,18 @@ 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. + Only returned if `return_params` is True. """ - # If `sim_params` has a size, and `n_sims` is defined, check that they are compatible + # If `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): + if isinstance(params, Sized) and len(params) and n_sims and n_sims > len(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): + for ind, sample_params in zip(counter(n_sims), params): - if return_sim_params: + if return_params: yield sim_func(**sample_params), sample_params else: yield sim_func(**sample_params) diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index c42793c6..158e6fe3 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -7,14 +7,14 @@ ################################################################################################### ################################################################################################### -def sim_multiple(sim_func, sim_params, n_sims): +def sim_multiple(sim_func, params, n_sims): """Simulate multiple samples of a specified simulation. Parameters ---------- sim_func : callable Function to create the simulated time series. - sim_params : dict + params : dict The parameters for the simulated signal, passed into `sim_func`. n_sims : int Number of simulations to create. @@ -34,21 +34,21 @@ def sim_multiple(sim_func, sim_params, n_sims): >>> sims = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sims = Simulations(n_sims, sim_params, sim_func) - for ind, sig in enumerate(sig_yielder(sim_func, sim_params, n_sims)): + sims = Simulations(n_sims, params, sim_func) + for ind, sig in enumerate(sig_yielder(sim_func, params, n_sims)): sims.add_signal(sig, index=ind) return sims -def sim_across_values(sim_func, sim_params): +def sim_across_values(sim_func, params): """Simulate signals across different parameter values. Parameters ---------- sim_func : callable Function to create the simulated time series. - sim_params : ParamIter or iterable or list of dict + params : ParamIter or iterable or list of dict Simulation parameters for `sim_func`. Returns @@ -74,24 +74,24 @@ def sim_across_values(sim_func, sim_params): >>> sims = sim_across_values(sim_powerlaw, params) """ - sims = VariableSimulations(len(sim_params), get_base_params(sim_params), sim_func, - update=getattr(sim_params, 'update', None), - component=getattr(sim_params, 'component', None)) + sims = VariableSimulations(len(params), get_base_params(params), sim_func, + update=getattr(params, 'update', None), + component=getattr(params, 'component', None)) - for ind, cur_sim_params in enumerate(sim_params): - sims.add_signal(sim_func(**cur_sim_params), cur_sim_params, index=ind) + for ind, cur_params in enumerate(params): + sims.add_signal(sim_func(**cur_params), cur_params, index=ind) return sims -def sim_multi_across_values(sim_func, sim_params, n_sims): +def sim_multi_across_values(sim_func, params, n_sims): """Simulate multiple signals across different parameter values. Parameters ---------- sim_func : callable Function to create the simulated time series. - sim_params : ParamIter or iterable or list of dict + params : ParamIter or iterable or list of dict Simulation parameters for `sim_func`. n_sims : int Number of simulations to create per parameter definition. @@ -119,22 +119,22 @@ def sim_multi_across_values(sim_func, sim_params, n_sims): >>> sims = sim_multi_across_values(sim_powerlaw, params, n_sims=2) """ - sims = MultiSimulations(update=getattr(sim_params, 'update', None), - component=getattr(sim_params, 'component', None)) - for cur_sim_params in sim_params: - sims.add_signals(sim_multiple(sim_func, cur_sim_params, n_sims)) + sims = MultiSimulations(update=getattr(params, 'update', None), + component=getattr(params, 'component', None)) + for cur_params in params: + sims.add_signals(sim_multiple(sim_func, cur_params, n_sims)) return sims -def sim_from_sampler(sim_func, sim_sampler, n_sims): +def sim_from_sampler(sim_func, sampler, n_sims): """Simulate a set of signals from a parameter sampler. Parameters ---------- sim_func : callable Function to create the simulated time series. - sim_sampler : ParamSampler + sampler : ParamSampler Parameter definition to sample from. n_sims : int Number of simulations to create per parameter definition. @@ -157,8 +157,8 @@ def sim_from_sampler(sim_func, sim_sampler, n_sims): >>> sims = sim_from_sampler(sim_powerlaw, param_sampler, n_sims=2) """ - sims = VariableSimulations(n_sims, get_base_params(sim_sampler), sim_func) - for ind, (sig, params) in enumerate(sig_sampler(sim_func, sim_sampler, True, n_sims)): + sims = VariableSimulations(n_sims, get_base_params(sampler), sim_func) + for ind, (sig, params) in enumerate(sig_sampler(sim_func, sampler, True, n_sims)): sims.add_signal(sig, params, index=ind) return sims diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index 0187e0c5..594e2c4f 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -107,12 +107,12 @@ def create_updater(update, component=None): ## PARAM ITER -def param_iter_yielder(sim_params, updater, values): +def param_iter_yielder(params, updater, values): """Parameter yielder. Parameters ---------- - sim_params : dict + params : dict Parameter definition. updater : callable Updater function to update parameter definition. @@ -121,15 +121,15 @@ def param_iter_yielder(sim_params, updater, values): Yields ------ - sim_params : dict + params : dict Simulation parameter definition. """ - sim_params = deepcopy(sim_params) + params = deepcopy(params) for value in values: - updater(sim_params, value) - yield deepcopy(sim_params) + updater(params, value) + yield deepcopy(params) class ParamIter(BaseUpdater): @@ -249,12 +249,12 @@ def create_sampler(values, probs=None, n_samples=None): yield np.random.choice(values, p=probs) -def param_sample_yielder(sim_params, samplers, n_samples=None): +def param_sample_yielder(params, samplers, n_samples=None): """Generator to yield randomly sampled parameter definitions. Parameters ---------- - sim_params : dict + params : dict The parameters for the simulated signal. samplers : dict Sampler definitions to update parameters with. @@ -266,12 +266,12 @@ def param_sample_yielder(sim_params, samplers, n_samples=None): Yields ------ - sim_params : dict + params : dict Simulation parameter definition. """ for _ in counter(n_samples): - out_params = deepcopy(sim_params) + out_params = deepcopy(params) for updater, sampler in samplers.items(): updater(out_params, next(sampler)) diff --git a/neurodsp/tests/sim/test_update.py b/neurodsp/tests/sim/test_update.py index c205af31..0a89ebc1 100644 --- a/neurodsp/tests/sim/test_update.py +++ b/neurodsp/tests/sim/test_update.py @@ -43,11 +43,11 @@ def test_create_updater(): def test_param_iter_yielder(): - sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + 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) + iter_yielder = param_iter_yielder(params, updater, values) for ind, params in enumerate(iter_yielder): assert isinstance(params, dict) for el in ['n_seconds', 'fs', 'exponent']: @@ -56,11 +56,11 @@ def test_param_iter_yielder(): def test_class_param_iter(): - sim_params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} update = 'exponent' values = [-2, -1, 0] - piter = ParamIter(sim_params, update, values) + piter = ParamIter(params, update, values) assert piter for ind, params in enumerate(piter): assert isinstance(params, dict) From f02869cd7fef4be888f73788b39dde31277653a3 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 12:52:45 -0400 Subject: [PATCH 27/33] generators & sim multi can take str function defs --- neurodsp/sim/combined.py | 3 +-- neurodsp/sim/generators.py | 10 ++++++++-- neurodsp/sim/info.py | 7 ++++++- neurodsp/sim/multi.py | 9 ++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/neurodsp/sim/combined.py b/neurodsp/sim/combined.py index eb49db34..9cee3eb3 100644 --- a/neurodsp/sim/combined.py +++ b/neurodsp/sim/combined.py @@ -63,8 +63,7 @@ def sim_combined(n_seconds, fs, components, component_variances=1): raise ValueError('Signal components and variances lengths do not match.') # Collect the sim function to use, and repeat variance if is single number - components = {(get_sim_func(name) if isinstance(name, str) else name) : params \ - for name, params in components.items()} + components = {get_sim_func(name) : params for name, params in components.items()} variances = repeat(component_variances) if \ isinstance(component_variances, (int, float, np.number)) else iter(component_variances) diff --git a/neurodsp/sim/generators.py b/neurodsp/sim/generators.py index 85ff06d0..2cca205d 100644 --- a/neurodsp/sim/generators.py +++ b/neurodsp/sim/generators.py @@ -2,6 +2,7 @@ from collections.abc import Sized +from neurodsp.sim.info import get_sim_func from neurodsp.utils.core import counter ################################################################################################### @@ -12,8 +13,9 @@ def sig_yielder(sim_func, params, n_sims): Parameters ---------- - sim_func : callable + sim_func : str or callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. params : dict The parameters for the simulated signal, passed into `sim_func`. n_sims : int, optional @@ -26,6 +28,7 @@ def sig_yielder(sim_func, params, n_sims): Simulated time series. """ + sim_func = get_sim_func(sim_func) for _ in counter(n_sims): yield sim_func(**params) @@ -35,8 +38,9 @@ def sig_sampler(sim_func, params, return_params=False, n_sims=None): Parameters ---------- - sim_func : callable + sim_func : str or callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. params : iterable The parameters for the simulated signal, passed into `sim_func`. return_params : bool, optional, default: False @@ -54,6 +58,8 @@ def sig_sampler(sim_func, params, return_params=False, n_sims=None): Only returned if `return_params` is True. """ + sim_func = get_sim_func(sim_func) + # If `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(params, Sized) and len(params) and n_sims and n_sims > len(params): diff --git a/neurodsp/sim/info.py b/neurodsp/sim/info.py index 746db886..70e24e78 100644 --- a/neurodsp/sim/info.py +++ b/neurodsp/sim/info.py @@ -58,8 +58,10 @@ def get_sim_func(function_name, modules=SIM_MODULES): Parameters ---------- - function_name : str + function_name : str or callabe Name of the sim function to retrieve. + If callable, returns input. + If string searches for corresponding callable sim function. modules : list of str, optional Which sim modules to look for the function in. @@ -69,6 +71,9 @@ def get_sim_func(function_name, modules=SIM_MODULES): Requested sim function. """ + if callable(function_name): + return function_name + for module in modules: try: func = get_sim_funcs(module)[function_name] diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index 158e6fe3..b2811c68 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -3,6 +3,7 @@ from neurodsp.sim.signals import Simulations, VariableSimulations, MultiSimulations from neurodsp.sim.generators import sig_yielder, sig_sampler from neurodsp.sim.params import get_base_params +from neurodsp.sim.info import get_sim_func ################################################################################################### ################################################################################################### @@ -14,6 +15,7 @@ def sim_multiple(sim_func, params, n_sims): ---------- sim_func : callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. params : dict The parameters for the simulated signal, passed into `sim_func`. n_sims : int @@ -48,6 +50,7 @@ def sim_across_values(sim_func, params): ---------- sim_func : callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. params : ParamIter or iterable or list of dict Simulation parameters for `sim_func`. @@ -78,6 +81,8 @@ def sim_across_values(sim_func, params): update=getattr(params, 'update', None), component=getattr(params, 'component', None)) + sim_func = get_sim_func(sim_func) + for ind, cur_params in enumerate(params): sims.add_signal(sim_func(**cur_params), cur_params, index=ind) @@ -91,6 +96,7 @@ def sim_multi_across_values(sim_func, params, n_sims): ---------- sim_func : callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. params : ParamIter or iterable or list of dict Simulation parameters for `sim_func`. n_sims : int @@ -132,8 +138,9 @@ def sim_from_sampler(sim_func, sampler, n_sims): Parameters ---------- - sim_func : callable + sim_func : str callable Function to create the simulated time series. + If string, should be the name of the desired simulation function. sampler : ParamSampler Parameter definition to sample from. n_sims : int From a9a393af36305e8e217b31b6cf652aab5aa127db Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 13:15:07 -0400 Subject: [PATCH 28/33] conslidate use of 'function' --- neurodsp/sim/generators.py | 22 ++++++------ neurodsp/sim/info.py | 56 ++++++++++++++++++++---------- neurodsp/sim/io.py | 10 +++--- neurodsp/sim/multi.py | 38 ++++++++++---------- neurodsp/sim/signals.py | 33 +++++++++--------- neurodsp/sim/update.py | 10 +++--- neurodsp/tests/sim/test_info.py | 11 ++++++ neurodsp/tests/sim/test_io.py | 14 ++++---- neurodsp/tests/sim/test_signals.py | 8 ++--- 9 files changed, 118 insertions(+), 84 deletions(-) diff --git a/neurodsp/sim/generators.py b/neurodsp/sim/generators.py index 2cca205d..b7ea4c9e 100644 --- a/neurodsp/sim/generators.py +++ b/neurodsp/sim/generators.py @@ -8,16 +8,16 @@ ################################################################################################### ################################################################################################### -def sig_yielder(sim_func, params, n_sims): +def sig_yielder(function, params, n_sims): """Generator to yield simulated signals from a given simulation function and parameters. Parameters ---------- - sim_func : str or callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. params : dict - The parameters for the simulated signal, passed into `sim_func`. + The parameters for the simulated signal, passed into `function`. n_sims : int, optional Number of simulations to set as the max. If None, creates an infinite generator. @@ -28,21 +28,21 @@ def sig_yielder(sim_func, params, n_sims): Simulated time series. """ - sim_func = get_sim_func(sim_func) + function = get_sim_func(function) for _ in counter(n_sims): - yield sim_func(**params) + yield function(**params) -def sig_sampler(sim_func, params, return_params=False, n_sims=None): +def sig_sampler(function, params, return_params=False, n_sims=None): """Generator to yield simulated signals from a parameter sampler. Parameters ---------- - sim_func : str or callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. params : iterable - The parameters for the simulated signal, passed into `sim_func`. + The parameters for the simulated signal, passed into `function`. return_params : bool, optional, default: False Whether to yield the simulation parameters as well as the simulated time series. n_sims : int, optional @@ -58,7 +58,7 @@ def sig_sampler(sim_func, params, return_params=False, n_sims=None): Only returned if `return_params` is True. """ - sim_func = get_sim_func(sim_func) + function = get_sim_func(function) # If `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 @@ -69,9 +69,9 @@ def sig_sampler(sim_func, params, return_params=False, n_sims=None): for ind, sample_params in zip(counter(n_sims), params): if return_params: - yield sim_func(**sample_params), sample_params + yield function(**sample_params), sample_params else: - yield sim_func(**sample_params) + yield function(**sample_params) if n_sims and ind >= n_sims: break diff --git a/neurodsp/sim/info.py b/neurodsp/sim/info.py index 70e24e78..e917b341 100644 --- a/neurodsp/sim/info.py +++ b/neurodsp/sim/info.py @@ -9,39 +9,40 @@ SIM_MODULES = ['periodic', 'aperiodic', 'cycles', 'transients', 'combined'] -def get_sim_funcs(module_name): +def get_sim_funcs(module): """Get the available sim functions from a specified sub-module. Parameters ---------- - module_name : {'periodic', 'aperiodic', 'cycles', 'transients', 'combined'} + module : {'periodic', 'aperiodic', 'cycles', 'transients', 'combined'} Simulation sub-module to get sim functions from. Returns ------- - funcs : dictionary + functions : dictionary A dictionary containing the available sim functions from the requested sub-module. """ - check_param_options(module_name, 'module_name', SIM_MODULES) + check_param_options(module, 'module', SIM_MODULES) # Note: imports done within function to avoid circular import from neurodsp.sim import periodic, aperiodic, transients, combined, cycles - module = eval(module_name) + module = eval(module) - funcs = {name : func for name, func in getmembers(module, isfunction) \ - if name[0:4] == 'sim_' and func.__module__.split('.')[-1] == module.__name__.split('.')[-1]} + module_name = module.__name__.split('.')[-1] + functions = {name : function for name, function in getmembers(module, isfunction) \ + if name[0:4] == 'sim_' and function.__module__.split('.')[-1] == module_name} - return funcs + return functions -def get_sim_names(module_name): +def get_sim_names(module): """Get the names of the available sim functions from a specified sub-module. Parameters ---------- - module_name : {'periodic', 'aperiodic', 'transients', 'combined'} + module : {'periodic', 'aperiodic', 'transients', 'combined'} Simulation sub-module to get sim functions from. Returns @@ -50,15 +51,15 @@ def get_sim_names(module_name): The names of the available functions in the requested sub-module. """ - return list(get_sim_funcs(module_name).keys()) + return list(get_sim_funcs(module).keys()) -def get_sim_func(function_name, modules=SIM_MODULES): +def get_sim_func(function, modules=SIM_MODULES): """Get a specified sim function. Parameters ---------- - function_name : str or callabe + function : str or callabe Name of the sim function to retrieve. If callable, returns input. If string searches for corresponding callable sim function. @@ -67,16 +68,16 @@ def get_sim_func(function_name, modules=SIM_MODULES): Returns ------- - func : callable + function : callable Requested sim function. """ - if callable(function_name): - return function_name + if callable(function): + return function for module in modules: try: - func = get_sim_funcs(module)[function_name] + function = get_sim_funcs(module)[function] break except KeyError: continue @@ -84,4 +85,23 @@ def get_sim_func(function_name, modules=SIM_MODULES): else: raise ValueError('Requested simulation function not found.') from None - return func + return function + + +def get_sim_func_name(function): + """Get the name of a simulation function. + + Parameters + ---------- + function : str or callabe + Function to get name for. + + Returns + ------- + name : str + Name of the function. + """ + + name = function.__name__ if callable(function) else function + + return name diff --git a/neurodsp/sim/io.py b/neurodsp/sim/io.py index 2be6fe21..19c88c5a 100644 --- a/neurodsp/sim/io.py +++ b/neurodsp/sim/io.py @@ -125,7 +125,7 @@ def save_sims(sims, label, file_path=None, replace=False): assert '_' not in label, 'Cannot have underscores in simulation label.' - save_path_items = ['sim_unknown' if not sims.sim_func else sims.sim_func] + save_path_items = ['sim_unknown' if not sims.function else sims.function] if isinstance(sims, (VariableSimulations, MultiSimulations)): if sims.component: save_path_items.append(sims.component) @@ -178,7 +178,7 @@ def load_sims(load_name, file_path=None): load_name = matches[0] splits = load_name.split('_') - sim_func = '_'.join(splits[0:2]) if splits[1] != 'unknown' else None + function = '_'.join(splits[0:2]) if splits[1] != 'unknown' else None update, component = None, None if len(splits) > 3: @@ -192,7 +192,7 @@ def load_sims(load_name, file_path=None): if 'signals.npy' not in load_files: msims = [load_sims(load_file, load_folder) for load_file in load_files] - sims = MultiSimulations(msims, None, sim_func, update, component) + sims = MultiSimulations(msims, None, function, update, component) else: @@ -200,10 +200,10 @@ def load_sims(load_name, file_path=None): if 'params.json' in load_files: params = load_json(load_folder / 'params.json') - sims = Simulations(sigs, params, sim_func) + sims = Simulations(sigs, params, function) elif 'params.jsonlines' in load_files: params = load_jsonlines(load_folder / 'params.jsonlines') - sims = VariableSimulations(sigs, params, sim_func, update, component) + sims = VariableSimulations(sigs, params, function, update, component) return sims diff --git a/neurodsp/sim/multi.py b/neurodsp/sim/multi.py index b2811c68..b9cad1f4 100644 --- a/neurodsp/sim/multi.py +++ b/neurodsp/sim/multi.py @@ -8,16 +8,16 @@ ################################################################################################### ################################################################################################### -def sim_multiple(sim_func, params, n_sims): +def sim_multiple(function, params, n_sims): """Simulate multiple samples of a specified simulation. Parameters ---------- - sim_func : callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. params : dict - The parameters for the simulated signal, passed into `sim_func`. + The parameters for the simulated signal, passed into `function`. n_sims : int Number of simulations to create. @@ -36,23 +36,23 @@ def sim_multiple(sim_func, params, n_sims): >>> sims = sim_multiple(sim_powerlaw, params, n_sims=3) """ - sims = Simulations(n_sims, params, sim_func) - for ind, sig in enumerate(sig_yielder(sim_func, params, n_sims)): + sims = Simulations(n_sims, params, function) + for ind, sig in enumerate(sig_yielder(function, params, n_sims)): sims.add_signal(sig, index=ind) return sims -def sim_across_values(sim_func, params): +def sim_across_values(function, params): """Simulate signals across different parameter values. Parameters ---------- - sim_func : callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. params : ParamIter or iterable or list of dict - Simulation parameters for `sim_func`. + Simulation parameters for `function`. Returns ------- @@ -77,28 +77,28 @@ def sim_across_values(sim_func, params): >>> sims = sim_across_values(sim_powerlaw, params) """ - sims = VariableSimulations(len(params), get_base_params(params), sim_func, + sims = VariableSimulations(len(params), get_base_params(params), function, update=getattr(params, 'update', None), component=getattr(params, 'component', None)) - sim_func = get_sim_func(sim_func) + function = get_sim_func(function) for ind, cur_params in enumerate(params): - sims.add_signal(sim_func(**cur_params), cur_params, index=ind) + sims.add_signal(function(**cur_params), cur_params, index=ind) return sims -def sim_multi_across_values(sim_func, params, n_sims): +def sim_multi_across_values(function, params, n_sims): """Simulate multiple signals across different parameter values. Parameters ---------- - sim_func : callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. params : ParamIter or iterable or list of dict - Simulation parameters for `sim_func`. + Simulation parameters for `function`. n_sims : int Number of simulations to create per parameter definition. @@ -128,17 +128,17 @@ def sim_multi_across_values(sim_func, params, n_sims): sims = MultiSimulations(update=getattr(params, 'update', None), component=getattr(params, 'component', None)) for cur_params in params: - sims.add_signals(sim_multiple(sim_func, cur_params, n_sims)) + sims.add_signals(sim_multiple(function, cur_params, n_sims)) return sims -def sim_from_sampler(sim_func, sampler, n_sims): +def sim_from_sampler(function, sampler, n_sims): """Simulate a set of signals from a parameter sampler. Parameters ---------- - sim_func : str callable + function : str or callable Function to create the simulated time series. If string, should be the name of the desired simulation function. sampler : ParamSampler @@ -164,8 +164,8 @@ def sim_from_sampler(sim_func, sampler, n_sims): >>> sims = sim_from_sampler(sim_powerlaw, param_sampler, n_sims=2) """ - sims = VariableSimulations(n_sims, get_base_params(sampler), sim_func) - for ind, (sig, params) in enumerate(sig_sampler(sim_func, sampler, True, n_sims)): + sims = VariableSimulations(n_sims, get_base_params(sampler), function) + for ind, (sig, params) in enumerate(sig_sampler(function, sampler, True, n_sims)): sims.add_signal(sig, params, index=ind) return sims diff --git a/neurodsp/sim/signals.py b/neurodsp/sim/signals.py index 26f6c19e..36fcd378 100644 --- a/neurodsp/sim/signals.py +++ b/neurodsp/sim/signals.py @@ -6,6 +6,7 @@ from neurodsp.utils.core import listify from neurodsp.utils.data import compute_nsamples +from neurodsp.sim.info import get_sim_func_name from neurodsp.sim.params import get_base_params, drop_base_params, get_param_values ################################################################################################### @@ -21,7 +22,7 @@ class Simulations(): If int, the number of expected simulations, used to pre-initialize array. params : dict, optional The simulation parameters that were used to create the simulations. - sim_func : str or callable, optional + function : 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. @@ -30,7 +31,7 @@ class Simulations(): This object stores a set of simulations generated from a shared parameter definition. """ - def __init__(self, signals=None, params=None, sim_func=None): + def __init__(self, signals=None, params=None, function=None): """Initialize Simulations object.""" if signals is None: @@ -44,7 +45,7 @@ def __init__(self, signals=None, params=None, sim_func=None): self._params = None self.add_params(params) - self.sim_func = sim_func.__name__ if callable(sim_func) else sim_func + self.function = get_sim_func_name(function) def __iter__(self): @@ -154,7 +155,7 @@ class VariableSimulations(Simulations): If int, the number of expected simulations, used to pre-initialize array. params : list of dict, optional The simulation parameters for each of the simulations. - sim_func : str, optional + function : str, optional The simulation function that was used to create the simulations. update : str The name of the parameter that is updated across simulations. @@ -167,10 +168,10 @@ class VariableSimulations(Simulations): This object stores a set of simulations with different parameter definitions per signal. """ - def __init__(self, signals=None, params=None, sim_func=None, update=None, component=None): + def __init__(self, signals=None, params=None, function=None, update=None, component=None): """Initialize SampledSimulations object.""" - Simulations.__init__(self, signals, params, sim_func) + Simulations.__init__(self, signals, params, function) if isinstance(signals, int): self._params = [{}] * signals self.update = update @@ -278,7 +279,7 @@ class MultiSimulations(): 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 + function : 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. @@ -291,11 +292,11 @@ class MultiSimulations(): 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, component=None): + def __init__(self, signals=None, params=None, function=None, update=None, component=None): """Initialize MultiSimulations object.""" self.signals = [] - self.add_signals(signals, params, sim_func) + self.add_signals(signals, params, function) self.update = update self.component = component @@ -334,10 +335,10 @@ def fs(self): @property - def sim_func(self): + def function(self): """Alias func as property.""" - return self.signals[0].sim_func if self else None + return self.signals[0].function if self else None @property @@ -370,7 +371,7 @@ def has_signals(self): return bool(len(self)) - def add_signals(self, signals, params=None, sim_func=None): + def add_signals(self, signals, params=None, function=None): """Add a set of signals to the current object. Parameters @@ -379,7 +380,7 @@ def add_signals(self, signals, params=None, sim_func=None): 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 + function : str, optional The simulation function that was used to create the set of simulations. """ @@ -396,7 +397,7 @@ def add_signals(self, signals, params=None, sim_func=None): 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) + function = repeat(function) if not isinstance(function, list) else function + for csigs, cparams, cfunc in zip(signals, params, function): + signals = Simulations(csigs, params=cparams, function=cfunc) self.signals.append(signals) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index 594e2c4f..3fc7c12d 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -6,6 +6,7 @@ from neurodsp.sim.generators import sig_yielder from neurodsp.sim.params import get_base_params +from neurodsp.sim.info import get_sim_func from neurodsp.utils.core import counter ################################################################################################### @@ -353,8 +354,9 @@ class SigIter(BaseUpdater): Parameters ---------- - sim_func : callable + function : str or callable Function to create simulations. + If string, should be the name of the desired simulation function. params : dict Simulation parameters. n_sims : int, optional @@ -369,12 +371,12 @@ class SigIter(BaseUpdater): Generator for sampling the sig iterations. """ - def __init__(self, sim_func, params, n_sims=None): + def __init__(self, function, params, n_sims=None): """Initialize signal iteration object.""" BaseUpdater.__init__(self, params) - self.sim_func = sim_func + self.function = get_sim_func(function) self.n_sims = n_sims self.index = 0 @@ -408,4 +410,4 @@ def _reset_yielder(self): """Reset the object yielder.""" self.index = 0 - self.yielder = sig_yielder(self.sim_func, self.params, self.n_sims) + self.yielder = sig_yielder(self.function, self.params, self.n_sims) diff --git a/neurodsp/tests/sim/test_info.py b/neurodsp/tests/sim/test_info.py index 87e3e5df..60a47104 100644 --- a/neurodsp/tests/sim/test_info.py +++ b/neurodsp/tests/sim/test_info.py @@ -2,6 +2,8 @@ from pytest import raises +from neurodsp.sim.aperiodic import sim_powerlaw + from neurodsp.sim.info import * ################################################################################################### @@ -31,3 +33,12 @@ def test_get_sim_func(): # Check the error for requesting non-existing function with raises(ValueError): get_sim_func('bad_func') + +def test_get_sim_func_name(): + + in_name = 'sim_oscillation' + name1 = get_sim_func_name(in_name) + assert name1 == in_name + + name2 = get_sim_func_name(sim_powerlaw) + assert name2 == 'sim_powerlaw' diff --git a/neurodsp/tests/sim/test_io.py b/neurodsp/tests/sim/test_io.py index b8bd257e..4be42249 100644 --- a/neurodsp/tests/sim/test_io.py +++ b/neurodsp/tests/sim/test_io.py @@ -51,7 +51,7 @@ def test_load_jsonlines(): def test_save_sims_sim(tsims): label = 'tsims' - folder = '_'.join([tsims.sim_func, label]) + folder = '_'.join([tsims.function, label]) save_sims(tsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) @@ -63,13 +63,13 @@ def test_load_sims_sim(tsims): loaded_sims = load_sims('tsims', TEST_FILES_PATH) assert np.array_equal(loaded_sims.signals, tsims.signals) - assert loaded_sims.sim_func == tsims.sim_func + assert loaded_sims.function == tsims.function assert loaded_sims.params == tsims.params def test_save_sims_vsim(tvsims): label = 'tvsims' - folder = '_'.join([tvsims.sim_func, tvsims.update, label]) + folder = '_'.join([tvsims.function, tvsims.update, label]) save_sims(tvsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) @@ -81,7 +81,7 @@ def test_load_sims_vsim(tvsims): loaded_sims = load_sims('tvsims', TEST_FILES_PATH) assert np.array_equal(loaded_sims.signals, tvsims.signals) - assert loaded_sims.sim_func == tvsims.sim_func + assert loaded_sims.function == tvsims.function assert loaded_sims.params == tvsims.params assert loaded_sims.update == tvsims.update assert loaded_sims.component == tvsims.component @@ -89,8 +89,8 @@ def test_load_sims_vsim(tvsims): def test_save_sims_msim(tmsims): label = 'tmsims' - folder = '_'.join([tmsims.sim_func, tmsims.update, label]) - sub_folder = '_'.join([tmsims.sim_func, 'set']) + folder = '_'.join([tmsims.function, tmsims.update, label]) + sub_folder = '_'.join([tmsims.function, 'set']) save_sims(tmsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) @@ -104,7 +104,7 @@ def test_load_sims_msim(tmsims): label = 'tmsims' loaded_sims = load_sims(label, TEST_FILES_PATH) - assert loaded_sims.sim_func == tmsims.sim_func + assert loaded_sims.function == tmsims.function #assert loaded_sims.params == tmsims.params assert loaded_sims.update == tmsims.update assert loaded_sims.component == tmsims.component diff --git a/neurodsp/tests/sim/test_signals.py b/neurodsp/tests/sim/test_signals.py index 232e89dc..a1e5f23f 100644 --- a/neurodsp/tests/sim/test_signals.py +++ b/neurodsp/tests/sim/test_signals.py @@ -32,7 +32,7 @@ def test_simulations(): assert sims_data.fs is None assert sims_data.has_signals assert sims_data.params is None - assert sims_data.sim_func is None + assert sims_data.function is None # Test dunders - iter & getitem & indicators for el in sims_data: @@ -78,7 +78,7 @@ def test_variable_simulations(): assert sims_data.fs is None assert sims_data.has_signals assert sims_data.params is None - assert sims_data.sim_func is None + assert sims_data.function is None # Test dunders - iter & getitem for el in sims_data: @@ -160,7 +160,7 @@ def test_multi_simulations(): 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.function is None assert sims_data.values is None # Test dunders - iter & getitem & indicators @@ -175,7 +175,7 @@ def test_multi_simulations(): 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.function assert sims_full.values assert sims_full._base_params From 4d1ae9523dba64cec2a0212d979a44af6a07a18f Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 13:25:01 -0400 Subject: [PATCH 29/33] fix order of multi save / loading --- neurodsp/sim/io.py | 2 +- neurodsp/tests/sim/test_io.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/io.py b/neurodsp/sim/io.py index 19c88c5a..3c4a03db 100644 --- a/neurodsp/sim/io.py +++ b/neurodsp/sim/io.py @@ -187,7 +187,7 @@ def load_sims(load_name, file_path=None): component = '_'.join(splits) if splits else None load_folder = fpath(file_path, load_name) - load_files = [file for file in os.listdir(load_folder) if file[0] != '.'] + load_files = sorted([file for file in os.listdir(load_folder) if file[0] != '.']) if 'signals.npy' not in load_files: diff --git a/neurodsp/tests/sim/test_io.py b/neurodsp/tests/sim/test_io.py index 4be42249..1dd0db29 100644 --- a/neurodsp/tests/sim/test_io.py +++ b/neurodsp/tests/sim/test_io.py @@ -105,7 +105,7 @@ def test_load_sims_msim(tmsims): label = 'tmsims' loaded_sims = load_sims(label, TEST_FILES_PATH) assert loaded_sims.function == tmsims.function - #assert loaded_sims.params == tmsims.params + assert loaded_sims.params == tmsims.params assert loaded_sims.update == tmsims.update assert loaded_sims.component == tmsims.component for lsig, csig in zip(loaded_sims.signals, tmsims.signals): From f0325f34ff602fe791ed438283d75a62ffcf2714 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Sun, 8 Sep 2024 23:21:35 -0400 Subject: [PATCH 30/33] fix up tutorials for updates --- tutorials/sim/plot_06_SimParams.py | 2 +- tutorials/sim/plot_07_SimMulti.py | 123 ++++++++++++++++++----------- 2 files changed, 76 insertions(+), 49 deletions(-) diff --git a/tutorials/sim/plot_06_SimParams.py b/tutorials/sim/plot_06_SimParams.py index 8044bc6a..d34dfa79 100644 --- a/tutorials/sim/plot_06_SimParams.py +++ b/tutorials/sim/plot_06_SimParams.py @@ -298,7 +298,7 @@ # 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] + ['osc_sampler', 'osc', osc_upd_sampler], ]) ################################################################################################### diff --git a/tutorials/sim/plot_07_SimMulti.py b/tutorials/sim/plot_07_SimMulti.py index 21df0f7c..59aab4a1 100644 --- a/tutorials/sim/plot_07_SimMulti.py +++ b/tutorials/sim/plot_07_SimMulti.py @@ -7,7 +7,8 @@ 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.multi import (sim_multiple, sim_from_sampler, + sim_across_values, sim_multi_across_values) from neurodsp.sim.update import create_updater, create_sampler, ParamSampler from neurodsp.plts.time_series import plot_time_series, plot_multi_time_series @@ -29,14 +30,14 @@ ################################################################################################### # -# The output the above function is a :class:~.Simulations object that stores multiple simulated -# signals along with relevant metadata. +# 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) +print(sigs.function, ':', sigs.params) ################################################################################################### @@ -66,14 +67,57 @@ for tsig in sig_iter: plot_time_series(None, tsig) +################################################################################################### +# Simulate From Sampler +# --------------------- +# +# We can also use the :func:`~.sim_from_sampler` function to simulate signals, +# sampling parameter values from a sampler definition. +# + +################################################################################################### + +# Define base set of parameters +params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} + +# Create an updater and sampler to sample from +exp_sampler = {create_updater('exponent') : create_sampler([-2, -1, 0])} + +# Create a ParamSampler object +sampler = ParamSampler(params, exp_sampler) + +################################################################################################### + +# Simulate a set of signals from the defined sampler +sampled_sims = sim_from_sampler(sim_powerlaw, sampler, 3) + +################################################################################################### +# +# The output of the above is a :class:~.VariableSimulations object that stores simulations +# across variable simulation parameters, storing the simulated time series as well as the +# simulation parameters for each simulated signal. +# + +################################################################################################### + +# Check some of the metadata stored in the VariableSimulations object +print(sampled_sims.function) +for paramdef in sampled_sims.params: + print(paramdef) + +################################################################################################### + +# Plot the set of sampled simulations +plot_multi_time_series(None, sampled_sims) + ################################################################################################### # Simulate Across Values # ---------------------- # -# Sometimes we may want to simulate signals across a set of parameter values. +# Sometimes we may want to simulate signals across a set defined range 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. +# To do so, we can use the :func:`~.sim_across_values` function, which takes a definition of +# parameter values to simulate across. # ################################################################################################### @@ -86,72 +130,55 @@ ] # Simulate a set of signals -sims_across_params = sim_across_values(sim_powerlaw, multi_params, 3) +sims_across_params = sim_across_values(sim_powerlaw, multi_params) ################################################################################################### # -# 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 output of the above is a :class:~.VariableSimulations object that stores simulations +# across varying simulation parameters (same as with the sampled simulations). # ################################################################################################### -# The length of the object is the number of parameter sets -print('# of sets of signals:', len(sims_across_params)) +plot_multi_time_series(None, sims_across_params) ################################################################################################### +# Simulate Multiple Instances Across Values +# ----------------------------------------- # -# In the above, we created a set of parameters per definition, which by default are returned -# in a dictionary. +# Finally, we may want to simulate multiple instances across a set of parameter definitions. +# +# To do so, we can use the :func:`~.sim_multi_across_values` function, which takes a set of +# parameter definitions and a number of simulations to create per definition. # ################################################################################################### -# 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 a set of signals +n_sims = 3 +sims_multi_across_params = sim_multi_across_values(sim_powerlaw, multi_params, n_sims) ################################################################################################### -# Simulate From Sampler -# --------------------- # -# Finally, we can use the :func:`~.sim_from_sampler` function to simulate signals, sampling -# parameter values from a sampler definition. +# 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. # ################################################################################################### -# Define base set of parameters -params = {'n_seconds' : 5, 'fs' : 250, 'exponent' : None} - -# Create an updater and sampler to sample from -exp_sampler = {create_updater('exponent') : create_sampler([-2, -1, 0])} - -# Create a ParamSampler object -sampler = ParamSampler(params, exp_sampler) - -################################################################################################### - -# Simulate a set of signals from the defined sampler -sampled_sims = sim_from_sampler(sim_powerlaw, sampler, 3) +# The length of the object is the number of parameter sets +print('# of sets of signals:', len(sims_across_params)) ################################################################################################### # -# 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. +# In the above, we created a set of parameters per definition, which by default are returned +# in a dictionary. # ################################################################################################### -# Check some of the metadata stored in the SampledSimulations object -print(sampled_sims.sim_func) -for paramdef in sampled_sims.params: - print(paramdef) - -################################################################################################### - -# Plot the set of sampled simulations -plot_multi_time_series(None, sampled_sims) +# 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]) From 33a3db6049fffa7b5fa91ff878e68b51242bf81e Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 9 Sep 2024 09:31:51 -0400 Subject: [PATCH 31/33] fix param copying --- neurodsp/sim/update.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neurodsp/sim/update.py b/neurodsp/sim/update.py index 3fc7c12d..a1708a35 100644 --- a/neurodsp/sim/update.py +++ b/neurodsp/sim/update.py @@ -159,13 +159,14 @@ class ParamIter(BaseUpdater): def __init__(self, params, update, values, component=None): """Initialize parameter iteration object.""" - BaseUpdater.__init__(self, params) - + params = deepcopy(params) if component is not None: params['components'][component][update] = None else: params[update] = None + BaseUpdater.__init__(self, params) + self.update = update self.values = values self.component = component From 7d52d12ec772ad0b8d08826325f35b2aaa591e81 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Mon, 9 Sep 2024 11:40:01 -0400 Subject: [PATCH 32/33] fix up name scheme for saved sims --- neurodsp/sim/io.py | 12 ++++++------ neurodsp/tests/sim/test_io.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/neurodsp/sim/io.py b/neurodsp/sim/io.py index 3c4a03db..bc7f7be3 100644 --- a/neurodsp/sim/io.py +++ b/neurodsp/sim/io.py @@ -125,10 +125,10 @@ def save_sims(sims, label, file_path=None, replace=False): assert '_' not in label, 'Cannot have underscores in simulation label.' - save_path_items = ['sim_unknown' if not sims.function else sims.function] + save_path_items = ['sim-unknown' if not sims.function else sims.function.replace('_', '-')] if isinstance(sims, (VariableSimulations, MultiSimulations)): if sims.component: - save_path_items.append(sims.component) + save_path_items.append(sims.component.replace('_', '-')) if sims.update: save_path_items.append(sims.update) save_path_items.append(label) @@ -178,13 +178,13 @@ def load_sims(load_name, file_path=None): load_name = matches[0] splits = load_name.split('_') - function = '_'.join(splits[0:2]) if splits[1] != 'unknown' else None + function = splits[0].replace('-', '_') if 'unknown' not in splits[0] else None update, component = None, None - if len(splits) > 3: - splits = splits[2:-1] + if len(splits) > 2: + splits = splits[1:-1] update = splits.pop() - component = '_'.join(splits) if splits else None + component = splits[0].replace('-', '_') if splits else None load_folder = fpath(file_path, load_name) load_files = sorted([file for file in os.listdir(load_folder) if file[0] != '.']) diff --git a/neurodsp/tests/sim/test_io.py b/neurodsp/tests/sim/test_io.py index 1dd0db29..2d2f4b66 100644 --- a/neurodsp/tests/sim/test_io.py +++ b/neurodsp/tests/sim/test_io.py @@ -51,7 +51,7 @@ def test_load_jsonlines(): def test_save_sims_sim(tsims): label = 'tsims' - folder = '_'.join([tsims.function, label]) + folder = '_'.join([tsims.function.replace('_', '-'), label]) save_sims(tsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) @@ -69,7 +69,7 @@ def test_load_sims_sim(tsims): def test_save_sims_vsim(tvsims): label = 'tvsims' - folder = '_'.join([tvsims.function, tvsims.update, label]) + folder = '_'.join([tvsims.function.replace('_', '-'), tvsims.update, label]) save_sims(tvsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) @@ -89,8 +89,8 @@ def test_load_sims_vsim(tvsims): def test_save_sims_msim(tmsims): label = 'tmsims' - folder = '_'.join([tmsims.function, tmsims.update, label]) - sub_folder = '_'.join([tmsims.function, 'set']) + folder = '_'.join([tmsims.function.replace('_', '-'), tmsims.update, label]) + sub_folder = '_'.join([tmsims.function.replace('_', '-'), 'set']) save_sims(tmsims, label, TEST_FILES_PATH) assert os.path.exists(TEST_FILES_PATH / folder) From e1f186ff64e54df4ccfd4402bd6adf07104b33dc Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Tue, 26 Nov 2024 16:21:37 -0500 Subject: [PATCH 33/33] update multi sims to use times vector --- tutorials/sim/plot_07_SimMulti.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tutorials/sim/plot_07_SimMulti.py b/tutorials/sim/plot_07_SimMulti.py index 59aab4a1..5ffea1a6 100644 --- a/tutorials/sim/plot_07_SimMulti.py +++ b/tutorials/sim/plot_07_SimMulti.py @@ -11,6 +11,7 @@ sim_across_values, sim_multi_across_values) from neurodsp.sim.update import create_updater, create_sampler, ParamSampler from neurodsp.plts.time_series import plot_time_series, plot_multi_time_series +from neurodsp.utils.data import create_times ################################################################################################### # Simulate Multiple Signals Together @@ -41,8 +42,11 @@ ################################################################################################### +# Create a times definition corresponding to the simulations +times = create_times(params['n_seconds'], params['fs']) + # Plot the simulated signals -plot_multi_time_series(None, sigs) +plot_multi_time_series(times, sigs) ################################################################################################### # SigIter @@ -65,7 +69,7 @@ # Iterate with the object to create simulations for tsig in sig_iter: - plot_time_series(None, tsig) + plot_time_series(times, tsig) ################################################################################################### # Simulate From Sampler @@ -108,7 +112,7 @@ ################################################################################################### # Plot the set of sampled simulations -plot_multi_time_series(None, sampled_sims) +plot_multi_time_series(times, sampled_sims) ################################################################################################### # Simulate Across Values @@ -140,7 +144,8 @@ ################################################################################################### -plot_multi_time_series(None, sims_across_params) +# Plot the simulated time series from sampled parameters +plot_multi_time_series(times, sims_across_params) ################################################################################################### # Simulate Multiple Instances Across Values