Skip to content

FrequencyBandsTable #2063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 60 commits into
base: nwb-schema-2.9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
2be97c9
ElectrodesTable
mavaylon1 Apr 18, 2024
ea7d60f
foo
mavaylon1 Apr 18, 2024
fe28745
check
mavaylon1 Apr 18, 2024
5ac7321
file
mavaylon1 Apr 18, 2024
b24d47d
checkpoint
mavaylon1 Apr 25, 2024
45dbe9e
Merge branch 'dev' into electrode
mavaylon1 May 2, 2024
6caa774
Merge branch 'dev' into electrode
mavaylon1 Aug 1, 2024
f00b134
not sure clean up
mavaylon1 Aug 1, 2024
dbfb805
Merge branch 'dev' into electrode
mavaylon1 Sep 25, 2024
be0614c
checkpoint repr works as well as able to write
mavaylon1 Sep 29, 2024
c97a0df
backwards compat seems to work
mavaylon1 Sep 30, 2024
64ad906
draft complete
mavaylon1 Oct 1, 2024
dc1f7b8
cleanup 1
mavaylon1 Oct 1, 2024
c02886b
cleanup 1
mavaylon1 Oct 1, 2024
0cd8edc
clean up 2
mavaylon1 Oct 2, 2024
06cf465
Add note
mavaylon1 Oct 2, 2024
81fa0e6
rebase:
mavaylon1 Feb 27, 2025
6cb8577
Update test_nwbfile.py
mavaylon1 Feb 27, 2025
5785f75
clean up
mavaylon1 Feb 27, 2025
083e7fe
Bands
mavaylon1 Mar 2, 2025
a193491
checkpoint unclean
mavaylon1 Mar 3, 2025
6559b67
poc checkpoint
mavaylon1 Mar 27, 2025
a39b9d1
tbd
mavaylon1 Mar 27, 2025
6cc4196
work in progress checkpoint:it passes
mavaylon1 Apr 3, 2025
ec3a476
back compat
mavaylon1 Apr 24, 2025
e93b368
tests
mavaylon1 Apr 29, 2025
c94561e
clena up
mavaylon1 Apr 29, 2025
cde5c3e
clean up
mavaylon1 Apr 29, 2025
7be7a80
rename
mavaylon1 Apr 30, 2025
85566b7
Update CHANGELOG.md
mavaylon1 Apr 30, 2025
aadf787
Merge branch 'dev' into bands
mavaylon1 Apr 30, 2025
134be1d
ruff
mavaylon1 Apr 30, 2025
967207c
Merge branch 'bands' of https://github.com/NeurodataWithoutBorders/py…
mavaylon1 Apr 30, 2025
856f9ec
weird rebase
mavaylon1 Apr 30, 2025
4cfd7a7
Merge branch 'dev' into bands
mavaylon1 Apr 30, 2025
a172361
changelog
mavaylon1 May 1, 2025
d3c5b3c
Add BaseImage and ExternalImage
rly May 10, 2025
57d7be7
Update changelog
rly May 10, 2025
4509172
Remove unnecessary import
rly May 10, 2025
012e4be
Merge branch 'nwb-schema-2.9.0' into external-image
rly May 10, 2025
db04219
Merge remote-tracking branch 'origin/nwb-schema-2.9.0' into electrode
rly May 10, 2025
c51e7d5
Merge branch 'external-image' into electrode
rly May 10, 2025
1ed1184
Re-add ElectrodeTable class, fix style
rly May 10, 2025
844266a
Add back mock_ElectrodeTable
rly May 10, 2025
608b916
Update schema to bands branch
rly May 10, 2025
ef718f4
Merge branch 'electrode' into bands
rly May 11, 2025
2bd7ee4
Update schema
rly May 11, 2025
370650e
Fix merge
rly May 11, 2025
17378f7
Fix add_electrode and ElectrodesTable.__init__
rly May 11, 2025
eed1aee
Fix ruff
rly May 11, 2025
c803fb9
Remove accidental test file
rly May 11, 2025
872d369
Merge branch 'external-image' into electrode
rly May 11, 2025
2718016
Merge branch 'electrode' into bands
rly May 11, 2025
de2fead
Clean up tests
rly May 11, 2025
89a647f
Merge branch 'electrode' into bands
rly May 11, 2025
4e0ccdb
Merge branch 'nwb-schema-2.9.0' into bands
rly May 17, 2025
5dc2bb6
Remove old test file
rly May 17, 2025
6af039d
Discard changes to src/pynwb/validation.py
rly May 17, 2025
e7a3ce9
Discard changes to src/pynwb/testing/testh5io.py
rly May 17, 2025
d2e16aa
Discard changes to tests/unit/test_ecephys.py
rly May 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Added dictionary-like operations directly on `ProcessingModule` objects (e.g., `len(processing_module)`). @bendichter [#2020](https://github.com/NeurodataWithoutBorders/pynwb/pull/2020)
- When an external file is detected when initializing an ImageSeries and no format is provided, automatically set format to "external" instead of raising an error. @stephprince [#2060](https://github.com/NeurodataWithoutBorders/pynwb/pull/2060)
- Added mask_type option to `mock_PlaneSegmentation`. @pauladkisson [#2067](https://github.com/NeurodataWithoutBorders/pynwb/pull/2067)
- Formally defined bands within `DecompositionSeries` as the neurodatatype `FrequencyBandsTable`. @mavaylon1 [#2063](https://github.com/NeurodataWithoutBorders/pynwb/pull/2063)

### Bug fixes
- Fixed `add_data_interface` functionality that was mistakenly removed in PyNWB 3.0. @stephprince [#2052](https://github.com/NeurodataWithoutBorders/pynwb/pull/2052)
Expand Down
18 changes: 17 additions & 1 deletion src/pynwb/io/misc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
from hdmf.common.io.table import DynamicTableMap
from .base import TimeSeriesMap

from .. import register_map
from pynwb.misc import Units
from pynwb.misc import Units, DecompositionSeries


@register_map(DecompositionSeries)
class DecompositionSeriesMap(TimeSeriesMap):
def __init__(self, spec):
super().__init__(spec)

@TimeSeriesMap.constructor_arg('bands')
def bands(self, builder, manager):
if builder.groups['bands'].attributes['neurodata_type'] != 'FrequencyBandsTable':
builder.groups['bands'].attributes['neurodata_type'] = 'FrequencyBandsTable'
builder.groups['bands'].attributes['namespace'] = 'core'
manager.clear_cache()
new_container = manager.construct(builder.groups['bands'])
return new_container


@register_map(Units)
Expand Down
68 changes: 46 additions & 22 deletions src/pynwb/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from . import register_class, CORE_NAMESPACE
from .base import TimeSeries
from .ecephys import ElectrodeGroup
from hdmf.common import DynamicTable, DynamicTableRegion
from hdmf.common import DynamicTable, DynamicTableRegion, VectorData

__all__ = [
'AnnotationSeries',
Expand Down Expand Up @@ -253,13 +253,50 @@ def get_unit_obs_intervals(self, **kwargs):
index = getargs('index', kwargs)
return np.asarray(self['obs_intervals'][index])

@register_class('FrequencyBandsTable', CORE_NAMESPACE)
class FrequencyBandsTable(DynamicTable):
"""
Table for describing the bands that DecompositionSeries was generated from.
"""
__columns__ = (
{'name': 'band_name', 'description': 'Name of the band, e.g. theta.', 'required': True},
{'name': 'band_limits', 'description': 'Low and high limit of each band in Hz.', 'required': True},
{'name': 'band_mean', 'description': 'The mean Gaussian filters, in Hz.', 'required': False},
{'name': 'band_stdev', 'description': 'The standard deviation Gaussian filters, in Hz.', 'required': False})

@docval({'name': 'band_mean', 'type': VectorData, 'doc': 'The mean Gaussian filters, in Hz.', 'default': None},
{'name': 'band_stdev', 'type': VectorData, 'doc': 'The standard deviation Gaussian filters, in Hz.',
'default': None},
*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'))
def __init__(self, **kwargs):
kwargs['name'] = 'bands'
kwargs['description'] = 'Table for describing the bands that DecompositionSeries was generated from.'

# optional fields
keys_to_set = ('band_mean', 'band_stdev')
args_to_set = popargs_to_dict(keys_to_set, kwargs)
for key, val in args_to_set.items():
setattr(self, key, val)

super().__init__(**kwargs)

@docval({'name': 'band_name', 'type': str, 'doc': 'Name of the band, e.g. theta.'},
{'name': 'band_limits', 'type': ('array_data', 'data'), 'shape': [None, 2],
'doc': 'Low and high limit of each band in Hz.'},
{'name': 'band_mean', 'type': float, 'doc': 'The mean Gaussian filters, in Hz.',
'default': None},
{'name': 'band_stdev', 'type': float, 'doc': 'The standard deviation Gaussian filters, in Hz.',
'default': None},
allow_extra=True)
def add_band(self, **kwargs):
super().add_row(**kwargs)


@register_class('DecompositionSeries', CORE_NAMESPACE)
class DecompositionSeries(TimeSeries):
"""
Stores product of spectral analysis
"""

__nwbfields__ = ('metric',
{'name': 'source_timeseries', 'child': False, 'doc': 'the input TimeSeries from this analysis'},
{'name': 'source_channels', 'child': True, 'doc': 'the channels that provided the source data'},
Expand All @@ -278,7 +315,7 @@ class DecompositionSeries(TimeSeries):
{'name': 'metric', 'type': str, # required
'doc': "metric of analysis. recommended - 'phase', 'amplitude', 'power'"},
{'name': 'unit', 'type': str, 'doc': 'SI unit of measurement', 'default': 'no unit'},
{'name': 'bands', 'type': DynamicTable,
{'name': 'bands', 'type': FrequencyBandsTable,
'doc': 'a table for describing the frequency bands that the signal was decomposed into', 'default': None},
{'name': 'source_timeseries', 'type': TimeSeries,
'doc': 'the input TimeSeries from this analysis', 'default': None},
Expand All @@ -302,16 +339,9 @@ def __init__(self, **kwargs):
"corresponding source_channels. (Optional)")
self.metric = metric
if bands is None:
bands = DynamicTable(
name="bands",
description="data about the frequency bands that the signal was decomposed into"
)
bands = FrequencyBandsTable()
self.bands = bands

def __check_column(self, name, desc):
if name not in self.bands.colnames:
self.bands.add_column(name, desc)

@docval({'name': 'band_name', 'type': str, 'doc': 'the name of the frequency band',
'default': None},
{'name': 'band_limits', 'type': ('array_data', 'data'), 'default': None,
Expand All @@ -327,14 +357,8 @@ def add_band(self, **kwargs):
"""
band_name, band_limits, band_mean, band_stdev = getargs('band_name', 'band_limits', 'band_mean', 'band_stdev',
kwargs)
if band_name is not None:
self.__check_column('band_name', "the name of the frequency band (recommended: 'alpha', 'beta', 'gamma', "
"'delta', 'high gamma'")
if band_name is not None:
self.__check_column('band_limits', 'low and high frequencies of bandpass filter in Hz')
if band_mean is not None:
self.__check_column('band_mean', 'the mean of Gaussian filters in Hz')
if band_stdev is not None:
self.__check_column('band_stdev', 'the standard deviation of Gaussian filters in Hz')

self.bands.add_row({k: v for k, v in kwargs.items() if v is not None})

self.bands.add_band(band_name=band_name,
band_limits=band_limits,
band_mean=band_mean,
band_stdev=band_stdev)
2 changes: 1 addition & 1 deletion src/pynwb/nwb-schema
Binary file added tests/back_compat/3.0.0_DecompositionSeries.nwb
Binary file not shown.
26 changes: 17 additions & 9 deletions tests/integration/hdf5/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import numpy as np

from hdmf.common import DynamicTable, VectorData, DynamicTableRegion
from pynwb import TimeSeries
from pynwb.misc import Units, DecompositionSeries
from hdmf.common import VectorData, DynamicTableRegion
from pynwb import TimeSeries, NWBHDF5IO
from pynwb.misc import Units, DecompositionSeries, FrequencyBandsTable
from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, TestCase
from pynwb.ecephys import ElectrodeGroup, ElectrodesTable
from pynwb.device import Device
Expand Down Expand Up @@ -104,6 +104,18 @@ def test_to_dataframe(self):
units.to_dataframe()


class TestBackCompatDecompositionSeries(TestCase):
def test_read_nwbfile(self):
"""
Test that reads an NWBFile with a DecompositionSeries that has a DynamicTable for bands.
"""
io = NWBHDF5IO("tests/back_compat/3.0.0_DecompositionSeries.nwb", mode="r")
nwbfile = io.read()
# Read FrequencyBandsTable to ensure it uses the schema type
bands = nwbfile.processing['test_mod']['LFPSpectralAnalysis'].bands
self.assertTrue(isinstance(bands, FrequencyBandsTable))


class TestDecompositionSeriesIO(NWBH5IOMixin, TestCase):

def setUpContainer(self):
Expand All @@ -115,9 +127,7 @@ def setUpContainer(self):
unit='flibs',
timestamps=np.ones((3,)),
)
bands = DynamicTable(
name='bands',
description='band info for LFPSpectralAnalysis',
bands = FrequencyBandsTable(
columns=[
VectorData(name='band_name', description='name of bands', data=['alpha', 'beta', 'gamma']),
VectorData(name='band_limits', description='low and high cutoffs in Hz', data=np.ones((3, 2))),
Expand Down Expand Up @@ -179,9 +189,7 @@ def setUpContainer(self):
)
data = np.random.randn(100, 2, 30)
timestamps = np.arange(100)/100
bands = DynamicTable(
name='bands',
description='band info for LFPSpectralAnalysis',
bands = FrequencyBandsTable(
columns=[
VectorData(name='band_name', description='name of bands', data=['alpha', 'beta', 'gamma']),
VectorData(name='band_limits', description='low and high cutoffs in Hz', data=np.ones((3, 2))),
Expand Down
63 changes: 47 additions & 16 deletions tests/unit/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import numpy as np

from hdmf.common import DynamicTable, VectorData, DynamicTableRegion
from hdmf.common import VectorData, DynamicTableRegion

from pynwb.misc import AnnotationSeries, AbstractFeatureSeries, IntervalSeries, Units, DecompositionSeries
from pynwb.misc import (
AnnotationSeries, AbstractFeatureSeries, IntervalSeries, Units, DecompositionSeries, FrequencyBandsTable
)
from pynwb.file import TimeSeries
from pynwb.device import Device
from pynwb.ecephys import ElectrodeGroup, ElectrodesTable
Expand All @@ -29,21 +31,51 @@ def test_init(self):
aFS.add_features(2.0, [1.])


class FrequencyBandsTableConstructor(TestCase):
def setUp(self):
self.bands = FrequencyBandsTable(
columns=[
VectorData(name='band_name', description='name of bands', data=['alpha', 'beta', 'gamma']),
VectorData(name='band_limits', description='low and high cutoffs in Hz', data=np.ones((3, 2))),
VectorData(name='band_mean', description='mean gaussian filters in Hz', data=np.ones((3,))),
VectorData(
name='band_stdev',
description='standard deviation of gaussian filters in Hz',
data=np.ones((3,))
),
],
)

def test_init(self):
self.assertEqual(self.bands['band_name'].data, ['alpha', 'beta', 'gamma'])
np.testing.assert_equal(self.bands['band_limits'].data, np.ones((3, 2)))
np.testing.assert_equal(self.bands['band_mean'].data, np.ones((3,)))
np.testing.assert_equal(self.bands['band_stdev'].data, np.ones((3,)))

def test_add_row(self):
self.bands.add_band(band_name='band_name', band_limits=np.array([[1., 1.]]), band_mean=1., band_stdev=1.)
np.testing.assert_equal(self.bands['band_limits'].data, np.ones((4, 2)))
np.testing.assert_equal(self.bands['band_mean'].data, np.ones((4,)))
np.testing.assert_equal(self.bands['band_stdev'].data, np.ones((4,)))


class DecompositionSeriesConstructor(TestCase):
def test_init(self):
timeseries = TimeSeries(name='dummy timeseries', description='desc',
data=np.ones((3, 3)), unit='Volts',
timestamps=[1., 2., 3.])
bands = DynamicTable(name='bands', description='band info for LFPSpectralAnalysis', columns=[
VectorData(name='band_name', description='name of bands', data=['alpha', 'beta', 'gamma']),
VectorData(name='band_limits', description='low and high cutoffs in Hz', data=np.ones((3, 2))),
VectorData(name='band_mean', description='mean gaussian filters in Hz', data=np.ones((3,))),
VectorData(
name='band_stdev',
description='standard deviation of gaussian filters in Hz',
data=np.ones((3,))
),
])
bands = FrequencyBandsTable(
columns=[
VectorData(name='band_name', description='name of bands', data=['alpha', 'beta', 'gamma']),
VectorData(name='band_limits', description='low and high cutoffs in Hz', data=np.ones((3, 2))),
VectorData(name='band_mean', description='mean gaussian filters in Hz', data=np.ones((3,))),
VectorData(
name='band_stdev',
description='standard deviation of gaussian filters in Hz',
data=np.ones((3,))
),
],
)
spec_anal = DecompositionSeries(name='LFPSpectralAnalysis',
description='my description',
data=np.ones((3, 3, 3)),
Expand Down Expand Up @@ -74,16 +106,15 @@ def test_init_delayed_bands(self):
source_timeseries=timeseries,
metric='amplitude')
for band_name in ['alpha', 'beta', 'gamma']:
spec_anal.add_band(band_name=band_name, band_limits=(1., 1.), band_mean=1., band_stdev=1.)

spec_anal.add_band(band_name=band_name, band_limits=np.array([[1., 1.]]), band_mean=1., band_stdev=1.)
self.assertEqual(spec_anal.name, 'LFPSpectralAnalysis')
self.assertEqual(spec_anal.description, 'my description')
np.testing.assert_equal(spec_anal.data, np.ones((3, 3, 3)))
np.testing.assert_equal(spec_anal.timestamps, [1., 2., 3.])
self.assertEqual(spec_anal.bands['band_name'].data, ['alpha', 'beta', 'gamma'])
np.testing.assert_equal(spec_anal.bands['band_limits'].data, np.ones((3, 2)))
self.assertEqual(spec_anal.source_timeseries, timeseries)
self.assertEqual(spec_anal.metric, 'amplitude')
self.assertEqual(spec_anal.bands['band_name'].data, ['alpha', 'beta', 'gamma'])
np.testing.assert_equal(spec_anal.bands['band_limits'].data, [np.array([[1., 1.]]) for _ in range(3)])

@staticmethod
def make_electrode_table(self):
Expand Down
Loading