Skip to content

Commit

Permalink
add MultiSims object
Browse files Browse the repository at this point in the history
  • Loading branch information
TomDonoghue committed Aug 28, 2024
1 parent 0f6c16b commit 62e7938
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 5 deletions.
136 changes: 131 additions & 5 deletions neurodsp/sim/sims.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Objects for managing groups of simulated signals."""

from itertools import repeat

import numpy as np

from neurodsp.utils.core import listify
Expand Down Expand Up @@ -66,7 +68,12 @@ def fs(self):
def params(self):
"""Define the full set of simulation parameters (base + additional parameters)."""

return {**self._base_params, **self._params}
if self.has_params:
params = {**self._base_params, **self._params}
else:
params = None

return params

@property
def has_params(self):
Expand Down Expand Up @@ -156,8 +163,8 @@ def add_params(self, params):

if not self.has_params:
if len(self) > len(cparams):
msg = 'Cannot add parameters to object without existing parameter values.'
raise ValueError(msg)
msg = 'Cannot add parameters to object without existing parameter values.'
raise ValueError(msg)
self._base_params = base_params
self._params = cparams

Expand All @@ -183,6 +190,125 @@ def add_signal(self, signal, params=None):

try:
self.signals = np.vstack([self.signals, signal])
except ValueError:
raise ValueError('Size of the added signal is not consistent with existing signals.')
except ValueError as array_value_error:
msg = 'Size of the added signal is not consistent with existing signals.'
raise ValueError(msg) from array_value_error
self.add_params(params)


class MultiSimulations():
"""Data object for multiple sets of simulated signals.
Parameters
----------
signals : list of 2d array
Sets of simulated signals, with each array organized as [n_sims, sig_length].
params : list of dict
The simulation parameters that were used to create the simulations.
sim_func : str or list of str
The simulation function(s) that were used to create the simulations.
update : str
The name of the parameter that is updated across sets of simulations.
Notes
-----
This object stores a set of simulations with multiple instances per parameter definition.
"""

def __init__(self, signals=None, params=None, sim_func=None, update=None):
"""Initialize MultiSimulations object."""

self.signals = []
self.add_signals(signals, params, sim_func)
self.update = update

def __iter__(self):
"""Define iteration as stepping across sets of simulated signals."""

for sigs in self.signals:
yield sigs

def __getitem__(self, index):
"""Define indexing as accessing sets of simulated signals."""

return self.signals[index]

def __len__(self):
"""Define the length of the object as the number of sets of signals."""

return len(self.signals)

@property
def n_seconds(self):
"""Alias n_seconds as a property."""

return self.signals[0].n_seconds if self else None

@property
def fs(self):
"""Alias fs as a property."""

return self.signals[0].fs if self else None

@property
def sim_func(self):
"""Alias func as property."""

return self.signals[0].sim_func if self else None

@property
def params(self):
"""Alias in the set of parameters across all sets of simulations."""

params = [self[ind].params for ind in range(len(self))]

return params

@property
def values(self):
"""Alias in the parameter definition of the parameter that varies across the sets."""

if self.update:
values = [params[self.update] for params in self.params]
else:
values = None

return values

@property
def _base_params(self):
"""Alias base parameters as property."""

return self.signals[0]._base_params if self else None

Check warning on line 282 in neurodsp/sim/sims.py

View check run for this annotation

Codecov / codecov/patch

neurodsp/sim/sims.py#L282

Added line #L282 was not covered by tests

@property
def has_signals(self):
"""Indicator for if the object has signals."""

return bool(len(self))

def add_signals(self, signals, params=None, sim_func=None):
"""Add a set of signals to the current object.
Parameters
----------
signals : 2d array or list of 2d array
A set of simulated signals, organized as [n_sims, sig_length].
params : dict or list of dict, optional
The simulation parameters that were used to create the set of simulations.
sim_func : str, optional
The simulation function that was used to create the set of simulations.
"""

if signals is None:
return

params = repeat(params) if not isinstance(params, list) else params
sim_func = repeat(sim_func) if not isinstance(sim_func, list) else sim_func
for csigs, cparams, cfunc in zip(signals, params, sim_func):
self._add_simulations(csigs, cparams, cfunc)

def _add_simulations(self, signals, params, sim_func):
"""Sub-function a adding a Simulations object to current object."""

self.signals.append(Simulations(signals, params=params, sim_func=sim_func))
59 changes: 59 additions & 0 deletions neurodsp/tests/sim/test_sims.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def test_simulations():
assert sims_data.n_seconds is None
assert sims_data.fs is None
assert sims_data.has_signals
assert sims_data.params is None
assert sims_data.sim_func is None

# Test dunders - iter & getitem & indicators
for el in sims_data:
Expand Down Expand Up @@ -63,6 +65,8 @@ def test_sampled_simulations():
assert sims_data.n_seconds is None
assert sims_data.fs is None
assert sims_data.has_signals
assert sims_data.params is None
assert sims_data.sim_func is None

# Test dunders - iter & getitem
for el in sims_data:
Expand Down Expand Up @@ -109,3 +113,58 @@ def test_sampled_simulations_add():
sims_data4 = SampledSimulations(sig, params)
with raises(ValueError):
sims_data4.add_signal(sig)

def test_multi_simulations():

# Test empty initialization
sims_empty = MultiSimulations()
assert isinstance(sims_empty, MultiSimulations)

# Demo data
n_seconds = 2
fs = 100
n_sigs = 2
n_sets = 2
sigs = np.ones([2, n_seconds * fs])
all_sigs = [sigs] * n_sets
params = [{'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -2},
{'n_seconds' : n_seconds, 'fs' : fs, 'exponent' : -1}]

# Test initialization with data only
sims_data = MultiSimulations(all_sigs)
assert sims_data
assert len(sims_data) == n_sets
assert sims_data.n_seconds is None
assert sims_data.fs is None
assert sims_data.has_signals
assert sims_data.params == [None] * n_sets
assert sims_data.sim_func is None
assert sims_data.values is None

# Test dunders - iter & getitem & indicators
for el in sims_data:
assert isinstance(el, Simulations)
assert isinstance(sims_data[0], Simulations)

# Test initialization with metadata
sims_full = MultiSimulations(all_sigs, params, 'sim_func', 'exponent')
assert len(sims_full) == n_sets
assert sims_full.has_signals
for params_obj, params_org in zip(sims_full.params, params):
assert params_obj == params_org
assert sims_full.sim_func
assert sims_full.values

def test_multi_simulations_add():

sigs = [np.ones([2, 5]), np.ones([2, 5])]
params = {'n_seconds' : 1, 'fs' : 100, 'param' : 'value'}

sims_data1 = MultiSimulations(sigs)
sims_data1.add_signals(sigs)
assert sims_data1.has_signals

sims_data2 = MultiSimulations(sigs, params)
sims_data2.add_signals(sigs, params)
assert sims_data2.has_signals
assert len(sims_data2) == len(sims_data2.params)

0 comments on commit 62e7938

Please sign in to comment.