Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 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
83695df
Use hdmf 4.1.0
rly May 28, 2025
c44f2d4
Update src/pynwb/misc.py
rly May 28, 2025
7ec367d
Fix tests
rly May 28, 2025
0b7d140
Fix test, fix classes
rly May 28, 2025
35ceb6d
Fix ruff
rly May 28, 2025
e56075f
Update schema
rly May 28, 2025
9569ec2
Update CHANGELOG.md
rly May 28, 2025
74cc961
Update CHANGELOG.md
rly May 28, 2025
32397c7
Clean up IO classes
rly May 28, 2025
f5cf830
Remove extra file
rly May 28, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
- Added `BaseImage` and `ExternalImage` as new neurodata types. The first so both `Image` and `ExternalImage` can inherit from it. The second to store external images. @rly [#2079](https://github.com/NeurodataWithoutBorders/pynwb/pull/2079)
- Added new `ElectrodesTable` neurodata type. @mavaylon1 [#1890](https://github.com/NeurodataWithoutBorders/pynwb/pull/1890)
- Formally defined and renamed `ElectrodeTable` as the `ElectrodesTable` neurodata type. @mavaylon1 [#1890](https://github.com/NeurodataWithoutBorders/pynwb/pull/1890)
- Formally defined bands within `DecompositionSeries` as the neurodatatype `FrequencyBandsTable`. @mavaylon1 @rly [#2063](https://github.com/NeurodataWithoutBorders/pynwb/pull/2063)
- Automatically add timezone information to timestamps reference time if no timezone information is specified. @stephprince [#2056](https://github.com/NeurodataWithoutBorders/pynwb/pull/2056)
- Added option to disable typemap caching and updated type map cache location. @stephprince [#2057](https://github.com/NeurodataWithoutBorders/pynwb/pull/2057)
- 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)
- Updated minimum HDMF version to 4.1.0. @mavaylon1 @rly [#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
2 changes: 1 addition & 1 deletion environment-ros3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ channels:
dependencies:
- python==3.13
- h5py==3.12.1
- hdmf==3.14.5
- hdmf==4.1.0
- matplotlib==3.9.2
- numpy==2.1.3
- pandas==2.2.3
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ classifiers = [
]
dependencies = [
"h5py>=3.2.0",
"hdmf>=3.14.5,<5",
"hdmf>=4.1.0,<6",
"numpy>=1.24.0",
"pandas>=1.2.0",
"python-dateutil>=2.8.2",
Expand Down
2 changes: 1 addition & 1 deletion requirements-min.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# minimum versions of package dependencies for installing PyNWB
h5py==3.2.0
hdmf==3.14.5
hdmf==4.1.0
numpy==1.24.0
pandas==1.2.0
python-dateutil==2.8.2
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pinned dependencies to reproduce an entire development environment to use PyNWB
h5py==3.12.1
hdmf==3.14.5
hdmf==4.1.0
numpy==2.1.1; python_version > "3.9" # numpy 2.1+ is not compatible with py3.9
numpy==2.0.2; python_version == "3.9"
pandas==2.2.3
Expand Down
5 changes: 0 additions & 5 deletions src/pynwb/io/icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ def __init__(self, spec):

@register_map(IntracellularRecordingsTable)
class IntracellularRecordingsTableMap(AlignedDynamicTableMap):
"""
Customize the mapping for AlignedDynamicTable
"""
def __init__(self, spec):
super().__init__(spec)

@DynamicTableMap.object_attr('electrodes')
def electrodes(self, container, manager):
Expand Down
19 changes: 15 additions & 4 deletions src/pynwb/io/misc.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
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):

@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

Check warning on line 18 in src/pynwb/io/misc.py

View check run for this annotation

Codecov / codecov/patch

src/pynwb/io/misc.py#L14-L18

Added lines #L14 - L18 were not covered by tests


@register_map(Units)
class UnitsMap(DynamicTableMap):

def __init__(self, spec):
super().__init__(spec)

@DynamicTableMap.constructor_arg('resolution')
def resolution_carg(self, builder, manager):
if 'spike_times' in builder:
Expand Down
80 changes: 45 additions & 35 deletions src/pynwb/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,43 @@ 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(*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.'
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': (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 +308,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,39 +332,19 @@ 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,
'doc': 'low and high frequencies of bandpass filter in Hz'},
{'name': 'band_mean', 'type': float, 'doc': 'the mean of Gaussian filters in Hz',
'default': None},
{'name': 'band_stdev', 'type': float, 'doc': 'the standard deviation of Gaussian filters in Hz',
'default': None},
allow_extra=True)
@docval(
{'name': 'band_name', 'type': str, 'doc': 'the name of the frequency band'},
{'name': 'band_limits', 'type': ('array_data', 'data'),
'doc': 'low and high frequencies of bandpass filter in Hz'},
{'name': 'band_mean', 'type': float, 'doc': 'the mean of Gaussian filters in Hz',
'default': None},
{'name': 'band_stdev', 'type': float, 'doc': 'the standard deviation of Gaussian filters in Hz',
'default': None},
allow_extra=True
)
def add_band(self, **kwargs):
"""
Add ROI data to this
"""
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})
"""Add a frequency band to the bands table of this DecompositionSeries."""
self.bands.add_band(**kwargs)
2 changes: 1 addition & 1 deletion src/pynwb/nwb-schema
35 changes: 33 additions & 2 deletions src/pynwb/testing/make_test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pynwb import NWBFile, NWBHDF5IO, __version__, TimeSeries, get_class, load_namespaces
from pynwb.file import Subject
from pynwb.image import ImageSeries
from pynwb.misc import DecompositionSeries
from pynwb.spec import NWBNamespaceBuilder, export_spec, NWBGroupSpec, NWBAttributeSpec


Expand Down Expand Up @@ -235,10 +236,39 @@ def _make_electrodes_dynamic_table():
location="brain area",
label=f"shank{i}electrode{j}",
)

test_name = 'electrodes_dynamic_table'
_write(test_name, nwbfile)

def _make_bands_dynamic_table():
"""Create a test file where electrodes is a dynamic table and not its own type."""
nwbfile = NWBFile(session_description='ADDME',
identifier='ADDME',
session_start_time=datetime.now().astimezone())
mod = nwbfile.create_processing_module(name="test_mod", description="a test module")
ts = TimeSeries(
name='dummy timeseries',
description='desc',
data=np.ones((3, 3)),
unit='Volts',
timestamps=np.ones((3,))
)
ds = DecompositionSeries(
name='LFPSpectralAnalysis',
description='my description',
data=np.ones((3, 3, 3)),
timestamps=[1., 2., 3.],
source_timeseries=ts,
metric='amplitude'
)
for band_name in ['alpha', 'beta', 'gamma']:
ds.add_band(band_name=band_name, band_limits=np.array([1., 1.]), band_mean=1., band_stdev=1.)
mod.add(ts)
mod.add(ds)

test_name = 'decompositionseries_bands_dynamic_table'
_write(test_name, nwbfile)

if __name__ == '__main__':
# install these versions of PyNWB and run this script to generate new files
# python src/pynwb/testing/make_test_files.py
Expand Down Expand Up @@ -268,4 +298,5 @@ def _make_electrodes_dynamic_table():
_make_subject_without_age_reference()

if __version__ == "3.0.0":
_make_electrodes_dynamic_table()
_make_electrodes_dynamic_table()
_make_bands_dynamic_table()
Binary file not shown.
10 changes: 9 additions & 1 deletion tests/back_compat/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pynwb import NWBHDF5IO, validate, TimeSeries
from pynwb.ecephys import ElectrodesTable
from pynwb.image import ImageSeries
from pynwb.misc import FrequencyBandsTable
from pynwb.testing import TestCase


Expand Down Expand Up @@ -138,9 +139,16 @@ def test_read_subject_no_age__reference(self):
read_nwbfile = io.read()
self.assertIsNone(read_nwbfile.subject.age__reference)

def test_read_electrodes_table_as_dynamic_table(self):
def test_read_electrodes_table_as_neurodata_type(self):
"""Test that an "electrodes" table written as a DynamicTable is read as an ElectrodesTable"""
f = Path(__file__).parent / '3.0.0_electrodes_dynamic_table.nwb'
with self.get_io(f) as io:
read_nwbfile = io.read()
assert isinstance(read_nwbfile.electrodes, ElectrodesTable)

def test_read_bands_table_as_neurodata_type(self):
"""Test that a "bands" table written as a DynamicTable is read as an FrequencyBandsTable"""
f = Path(__file__).parent / '3.0.0_decompositionseries_bands_dynamic_table.nwb'
with self.get_io(f) as io:
read_nwbfile = io.read()
assert isinstance(read_nwbfile.processing['test_mod']['LFPSpectralAnalysis'].bands, FrequencyBandsTable)
12 changes: 4 additions & 8 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 hdmf.common import VectorData, DynamicTableRegion
from pynwb import TimeSeries
from pynwb.misc import Units, DecompositionSeries
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 @@ -115,9 +115,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 +177,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
Loading
Loading