diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de2bee06..6e47d051f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ ## Upcoming ### Breaking changes - - Removed unused functions `prepend_string` and `_not_parent` in `core.py`, `_not_parent` in `file.py`, and `NWBBaseTypeMapper.get_nwb_file` in `io/core.py` @oruebel [#2036](https://github.com/NeurodataWithoutBorders/pynwb/pull/2036) ### Enhancements and minor changes -- Added support for NWB Schema 2.9.0. @rly [#2079](https://github.com/NeurodataWithoutBorders/pynwb/pull/2079) - - 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. +- Added support for NWB Schema 2.9.0. + - 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) - 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) @@ -16,12 +17,13 @@ - Added mask_type option to `mock_PlaneSegmentation`. @pauladkisson [#2067](https://github.com/NeurodataWithoutBorders/pynwb/pull/2067) ### Bug fixes -- Fix `add_data_interface` functionality that was mistakenly removed in PyNWB 3.0. @stephprince [#2052](https://github.com/NeurodataWithoutBorders/pynwb/pull/2052) +- Fixed `add_data_interface` functionality that was mistakenly removed in PyNWB 3.0. @stephprince [#2052](https://github.com/NeurodataWithoutBorders/pynwb/pull/2052) - Fixed bug in `IntracellularRecordingsTable.__init__` were `IntracellularResponsesTable` wasn't created correctly when custom category tables were provided @oruebel. [#2031](https://github.com/NeurodataWithoutBorders/pynwb/pull/2031) - Fixed shape check in `SpikeEventSeries.__init__` to support `AbstractDataChunkIterator` for timestamps/data. @oruebel [#2031](https://github.com/NeurodataWithoutBorders/pynwb/pull/2031) - Added unit tests to enhance coverage of `core.py`, `image.py`, `spec.py`, `icephys.py`, `epoch.py` and others. @oruebel [#2031](https://github.com/NeurodataWithoutBorders/pynwb/pull/2031) -- Fix missing `IndexSeries.indexed_images`. @rly [#2074](https://github.com/NeurodataWithoutBorders/pynwb/pull/2074) - +- Fixed missing `IndexSeries.indexed_images`. @rly [#2074](https://github.com/NeurodataWithoutBorders/pynwb/pull/2074) +- Fixed missing `__nwbfields__` and `_fieldsname` for `NWBData` and its subclasses. @rly [#2082](https://github.com/NeurodataWithoutBorders/pynwb/pull/2082) + ## PyNWB 3.0.0 (February 26, 2025) ### Breaking changes diff --git a/src/pynwb/core.py b/src/pynwb/core.py index 64410b5ba..fc04915ed 100644 --- a/src/pynwb/core.py +++ b/src/pynwb/core.py @@ -28,6 +28,10 @@ class NWBMixin(AbstractContainer): _data_type_attr = 'neurodata_type' + _fieldsname = '__nwbfields__' + + __nwbfields__ = tuple() + @docval({'name': 'neurodata_type', 'type': str, 'doc': 'the data_type to search for', 'default': None}) def get_ancestor(self, **kwargs): """ @@ -72,9 +76,7 @@ def data_type(self): @register_class('NWBContainer', CORE_NAMESPACE) class NWBContainer(NWBMixin, Container): - _fieldsname = '__nwbfields__' - - __nwbfields__ = tuple() + pass @register_class('NWBDataInterface', CORE_NAMESPACE) @@ -110,8 +112,8 @@ def __getitem__(self, args): def append(self, arg): """ Append a single element to the data - - Note: The arg to append should be 1 dimension less than the data. + + Note: The arg to append should be 1 dimension less than the data. For example, if the data is a 2D array, arg should be a 1D array. Appending to scalar data is not supported. To append multiple elements, use extend. @@ -172,7 +174,7 @@ def notes(self): warn(("Use of ScratchData.notes has been deprecated and will be removed in PyNWB 4.0. " "Use ScratchData.description instead."), DeprecationWarning) return self.description - + @notes.setter def notes(self, value): """ diff --git a/src/pynwb/ecephys.py b/src/pynwb/ecephys.py index 9f3c6e4b2..96db7c1a0 100644 --- a/src/pynwb/ecephys.py +++ b/src/pynwb/ecephys.py @@ -2,7 +2,7 @@ import numpy as np from collections.abc import Iterable -from hdmf.common import DynamicTableRegion +from hdmf.common import DynamicTableRegion, DynamicTable from hdmf.data_utils import assertEqualShape from hdmf.utils import docval, popargs, get_docval, popargs_to_dict, get_data_shape, AllowPositional @@ -18,7 +18,8 @@ 'EventDetection', 'LFP', 'FilteredEphys', - 'FeatureExtraction' + 'FeatureExtraction', + 'ElectrodesTable', ] @@ -67,6 +68,42 @@ def __init__(self, **kwargs): setattr(self, key, val) +@register_class('ElectrodesTable', CORE_NAMESPACE) +class ElectrodesTable(DynamicTable): + """A table of all electrodes (i.e. channels) used for recording. Introduced in NWB 3.0.0. Replaces the "electrodes" + table (neurodata_type_inc DynamicTable, no neurodata_type_def) that is part of NWBFile.""" + + __columns__ = ( + {'name': 'location', 'description': 'Location of the electrode (channel).', 'required': True}, + {'name': 'group', 'description': 'Reference to the ElectrodeGroup.', 'required': True}, + {'name': 'group_name', 'description': 'Name of the ElectrodeGroup.', 'required': False}, + {'name': 'x', 'description': 'x coordinate of the channel location in the brain.', 'required': False}, + {'name': 'y', 'description': 'y coordinate of the channel location in the brain.', 'required': False}, + {'name': 'z', 'description': 'z coordinate of the channel location in the brain.', 'required': False}, + {'name': 'imp', 'description': 'Impedance of the channel, in ohms.', 'required': False}, + {'name': 'filtering', 'description': 'Description of hardware filtering.', 'required': False}, + {'name': 'rel_x', 'description': 'x coordinate in electrode group.', 'required': False}, + {'name': 'rel_y', 'description': 'xy coordinate in electrode group.', 'required': False}, + {'name': 'rel_z', 'description': 'z coordinate in electrode group.', 'required': False}, + {'name': 'reference', 'description': ('Description of the reference electrode and/or reference scheme used ' + 'for this electrode.'), 'required': False} + ) + + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames')) + def __init__(self, **kwargs): + kwargs['name'] = 'electrodes' + kwargs['description'] = 'metadata about extracellular electrodes' + super().__init__(**kwargs) + + def copy(self): + """ + Return a copy of this ElectrodesTable. + This is useful for linking. + """ + kwargs = dict(id=self.id, columns=self.columns, colnames=self.colnames) + return self.__class__(**kwargs) + + @register_class('ElectricalSeries', CORE_NAMESPACE) class ElectricalSeries(TimeSeries): """ @@ -155,12 +192,12 @@ def __init__(self, **kwargs): # case where the data is a AbstractDataChunkIterator data_shape = get_data_shape(kwargs['data'], strict_no_data_load=True) timestamps_shape = get_data_shape(kwargs['timestamps'], strict_no_data_load=True) - if (data_shape is not None and - timestamps_shape is not None and - len(data_shape) > 0 and + if (data_shape is not None and + timestamps_shape is not None and + len(data_shape) > 0 and len(timestamps_shape) > 0): - if (data_shape[0] != timestamps_shape[0] and - data_shape[0] is not None and + if (data_shape[0] != timestamps_shape[0] and + data_shape[0] is not None and timestamps_shape[0] is not None): raise ValueError('Must provide the same number of timestamps and spike events') super().__init__(**kwargs) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index dcc9ad458..e91fc3059 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -15,7 +15,7 @@ from .base import TimeSeries, ProcessingModule from .device import Device from .epoch import TimeIntervals -from .ecephys import ElectrodeGroup +from .ecephys import ElectrodeGroup, ElectrodesTable from .icephys import (IntracellularElectrode, SweepTable, PatchClampSeries, IntracellularRecordingsTable, SimultaneousRecordingsTable, SequentialRecordingsTable, RepetitionsTable, ExperimentalConditionsTable) @@ -385,12 +385,12 @@ class NWBFile(MultiContainerInterface, HERDManager): {'name': 'lab_meta_data', 'type': (list, tuple), 'default': None, 'doc': 'an extension that contains lab-specific meta-data'}, {'name': 'electrodes', 'type': DynamicTable, - 'doc': 'the ElectrodeTable that belongs to this NWBFile', 'default': None}, + 'doc': 'the ElectrodesTable that belongs to this NWBFile', 'default': None}, {'name': 'electrode_groups', 'type': Iterable, 'doc': 'the ElectrodeGroups that belong to this NWBFile', 'default': None}, {'name': 'ic_electrodes', 'type': (list, tuple), 'doc': 'DEPRECATED use icephys_electrodes parameter instead. ' - 'IntracellularElectrodes that belong to this NWBFile', 'default': None}, + 'IntracellularElectrodes that belong to this NWBFile', 'default': None}, # TODO remove this arg in PyNWB 4.0 {'name': 'sweep_table', 'type': SweepTable, 'doc': '[DEPRECATED] Use IntracellularRecordingsTable instead. ' @@ -611,7 +611,7 @@ def add_epoch(self, **kwargs): def __check_electrodes(self): if self.electrodes is None: - self.electrodes = ElectrodeTable() + self.electrodes = ElectrodesTable() @docval(*get_docval(DynamicTable.add_column), allow_extra=True) def add_electrode_column(self, **kwargs): @@ -666,31 +666,15 @@ def add_electrode(self, **kwargs): # are not allowed if not d['location']: raise ValueError("The 'location' argument is required when creating an electrode.") - if not kwargs['group']: + if not d['group']: raise ValueError("The 'group' argument is required when creating an electrode.") if d.get('group_name', None) is None: d['group_name'] = d['group'].name - new_cols = [('x', 'the x coordinate of the position (+x is posterior)'), - ('y', 'the y coordinate of the position (+y is inferior)'), - ('z', 'the z coordinate of the position (+z is right)'), - ('imp', 'the impedance of the electrode, in ohms'), - ('filtering', 'description of hardware filtering, including the filter name and frequency cutoffs'), - ('rel_x', 'the x coordinate within the electrode group'), - ('rel_y', 'the y coordinate within the electrode group'), - ('rel_z', 'the z coordinate within the electrode group'), - ('reference', 'Description of the reference electrode and/or reference scheme used for this \ - electrode, e.g.,"stainless steel skull screw" or "online common average referencing".') - ] - - # add column if the arg is supplied and column does not yet exist - # do not pass arg to add_row if arg is not supplied - for col_name, col_doc in new_cols: - if kwargs[col_name] is not None: - if col_name not in self.electrodes: - self.electrodes.add_column(col_name, col_doc) - else: - d.pop(col_name) # remove args from d if not set + # remove keys that are None + for key in list(d.keys()): + if d[key] is None: + d.pop(key) self.electrodes.add_row(**d) @@ -705,7 +689,7 @@ def create_electrode_table_region(self, **kwargs): for idx in region: if idx < 0 or idx >= len(self.electrodes): raise IndexError('The index ' + str(idx) + - ' is out of range for the ElectrodeTable of length ' + ' is out of range for the ElectrodesTable of length ' + str(len(self.electrodes))) desc = getargs('description', kwargs) name = getargs('name', kwargs) @@ -787,13 +771,13 @@ def add_invalid_time_interval(self, **kwargs): self.__check_invalid_times() self.invalid_times.add_interval(**kwargs) - @docval({'name': 'electrode_table', 'type': DynamicTable, 'doc': 'the ElectrodeTable for this file'}) + @docval({'name': 'electrode_table', 'type': ElectrodesTable, 'doc': 'the ElectrodesTable for this file'}) def set_electrode_table(self, **kwargs): """ - Set the electrode table of this NWBFile to an existing ElectrodeTable + Set the electrode table of this NWBFile to an existing ElectrodesTable """ if self.electrodes is not None: - msg = 'ElectrodeTable already exists, cannot overwrite' + msg = 'ElectrodesTable already exists, cannot overwrite' raise ValueError(msg) electrode_table = getargs('electrode_table', kwargs) self.electrodes = electrode_table @@ -804,7 +788,7 @@ def _check_sweep_table(self): """ if self.sweep_table is None: if self._in_construct_mode: - # Construct the SweepTable without triggering errors in construct mode because + # Construct the SweepTable without triggering errors in construct mode because # SweepTable has been deprecated sweep_table = SweepTable.__new__(SweepTable, parent=self, in_construct_mode=True) sweep_table.__init__(name='sweep_table') @@ -1146,19 +1130,16 @@ def _tablefunc(table_name, description, columns): return t -def ElectrodeTable(name='electrodes', - description='metadata about extracellular electrodes'): - return _tablefunc(name, description, - [('location', 'the location of channel within the subject e.g. brain region'), - ('group', 'a reference to the ElectrodeGroup this electrode is a part of'), - ('group_name', 'the name of the ElectrodeGroup this electrode is a part of') - ] - ) - - def TrialTable(name='trials', description='metadata about experimental trials'): return _tablefunc(name, description, ['start_time', 'stop_time']) def InvalidTimesTable(name='invalid_times', description='time intervals to be removed from analysis'): return _tablefunc(name, description, ['start_time', 'stop_time']) + + +def ElectrodeTable(name='electrodes', + description='metadata about extracellular electrodes'): + warn("The ElectrodeTable convenience function is deprecated. Please create a new instance of " + "the ElectrodesTable class instead.", DeprecationWarning) + return ElectrodesTable() \ No newline at end of file diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index 53d257a05..1e48350d3 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -181,6 +181,24 @@ def scratch(self, builder, manager): ret.append(manager.construct(d)) return tuple(ret) if len(ret) > 0 else None + @ObjectMapper.constructor_arg('electrodes') + def electrodes(self, builder, manager): + try: + electrodes_builder = builder['general']['extracellular_ephys']['electrodes'] + except KeyError: + # Note: This is here because the ObjectMapper pulls argname from docval and checks to see + # if there is an override even if the file doesn't have what is looking for. In this case, + # electrodes for NWBFile. + electrodes_builder = None + if (electrodes_builder is not None and electrodes_builder.attributes['neurodata_type'] != 'ElectrodesTable'): + electrodes_builder.attributes['neurodata_type'] = 'ElectrodesTable' + electrodes_builder.attributes['namespace'] = 'core' + manager.clear_cache() + new_container = manager.construct(electrodes_builder) + return new_container + else: + return None + @ObjectMapper.constructor_arg('session_start_time') def dateconversion(self, builder, manager): """Set the constructor arg for 'session_start_time' to a datetime object. diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index 3616ed183..a8127bba9 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit 3616ed183c38d55e9cca3021d65633d6a8656846 +Subproject commit a8127bba98c8df2c4aa10eca0cbbc08c2721b820 diff --git a/src/pynwb/testing/make_test_files.py b/src/pynwb/testing/make_test_files.py index 2311989ca..073cf0372 100644 --- a/src/pynwb/testing/make_test_files.py +++ b/src/pynwb/testing/make_test_files.py @@ -214,6 +214,30 @@ def _make_subject_without_age_reference(): test_name = 'subject_no_age__reference' _write(test_name, nwbfile) +def _make_electrodes_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()) + device = nwbfile.create_device(name="array", description="an array", manufacturer="company") + nwbfile.add_electrode_column(name="label", description="label of electrode") + + for i in range(4): + electrode_group = nwbfile.create_electrode_group( + name=f"shank{i}", + description=f"electrode group for shank {i}", + device=device, + location="brain area", + ) + for j in range(3): + nwbfile.add_electrode( + group=electrode_group, + location="brain area", + label=f"shank{i}electrode{j}", + ) + + test_name = 'electrodes_dynamic_table' + _write(test_name, nwbfile) if __name__ == '__main__': # install these versions of PyNWB and run this script to generate new files @@ -242,3 +266,6 @@ def _make_subject_without_age_reference(): if __version__ == "2.2.0": _make_subject_without_age_reference() + + if __version__ == "3.0.0": + _make_electrodes_dynamic_table() \ No newline at end of file diff --git a/src/pynwb/testing/mock/ecephys.py b/src/pynwb/testing/mock/ecephys.py index 315eb3d9c..5d3399e6d 100644 --- a/src/pynwb/testing/mock/ecephys.py +++ b/src/pynwb/testing/mock/ecephys.py @@ -1,12 +1,13 @@ from typing import Optional +import warnings import numpy as np from hdmf.common.table import DynamicTableRegion, DynamicTable from ...device import Device -from ...file import ElectrodeTable, NWBFile -from ...ecephys import ElectricalSeries, ElectrodeGroup, SpikeEventSeries +from ...file import NWBFile +from ...ecephys import ElectricalSeries, ElectrodeGroup, SpikeEventSeries, ElectrodesTable from .device import mock_Device from .utils import name_generator from ...misc import Units @@ -35,10 +36,10 @@ def mock_ElectrodeGroup( return electrode_group -def mock_ElectrodeTable( +def mock_ElectrodesTable( n_rows: int = 5, group: Optional[ElectrodeGroup] = None, nwbfile: Optional[NWBFile] = None -) -> DynamicTable: - electrodes_table = ElectrodeTable() +) -> ElectrodesTable: + electrodes_table = ElectrodesTable() group = group if group is not None else mock_ElectrodeGroup(nwbfile=nwbfile) for i in range(n_rows): electrodes_table.add_row( @@ -53,11 +54,18 @@ def mock_ElectrodeTable( return electrodes_table +def mock_ElectrodeTable( + n_rows: int = 5, group: Optional[ElectrodeGroup] = None, nwbfile: Optional[NWBFile] = None +) -> ElectrodesTable: + warnings.warn("mock_ElectrodeTable() is deprecated. Use mock_ElectrodesTable() instead.", DeprecationWarning) + return mock_ElectrodesTable(n_rows=n_rows, group=group, nwbfile=nwbfile) + + def mock_electrodes( n_electrodes: int = 5, table: Optional[DynamicTable] = None, nwbfile: Optional[NWBFile] = None ) -> DynamicTableRegion: - table = table or mock_ElectrodeTable(n_rows=5, nwbfile=nwbfile) + table = table or mock_ElectrodesTable(n_rows=5, nwbfile=nwbfile) return DynamicTableRegion( name="electrodes", data=list(range(n_electrodes)), @@ -80,7 +88,7 @@ def mock_ElectricalSeries( conversion: float = 1.0, offset: float = 0., ) -> ElectricalSeries: - + # Set a default rate if timestamps are not provided rate = 30_000.0 if (timestamps is None and rate is None) else rate n_electrodes = data.shape[1] if data is not None else 5 diff --git a/tests/back_compat/3.0.0_electrodes_dynamic_table.nwb b/tests/back_compat/3.0.0_electrodes_dynamic_table.nwb new file mode 100644 index 000000000..f551e9463 Binary files /dev/null and b/tests/back_compat/3.0.0_electrodes_dynamic_table.nwb differ diff --git a/tests/back_compat/test_read.py b/tests/back_compat/test_read.py index 16a119690..4540f4874 100644 --- a/tests/back_compat/test_read.py +++ b/tests/back_compat/test_read.py @@ -3,6 +3,7 @@ import warnings from pynwb import NWBHDF5IO, validate, TimeSeries +from pynwb.ecephys import ElectrodesTable from pynwb.image import ImageSeries from pynwb.testing import TestCase @@ -136,3 +137,10 @@ def test_read_subject_no_age__reference(self): with self.get_io(f) as io: read_nwbfile = io.read() self.assertIsNone(read_nwbfile.subject.age__reference) + + def test_read_electrodes_table_as_dynamic_table(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) diff --git a/tests/integration/hdf5/test_ecephys.py b/tests/integration/hdf5/test_ecephys.py index 9bfdf3086..0ad27cb1b 100644 --- a/tests/integration/hdf5/test_ecephys.py +++ b/tests/integration/hdf5/test_ecephys.py @@ -13,9 +13,9 @@ SpikeEventSeries, EventDetection, FeatureExtraction, + ElectrodesTable, ) from pynwb.device import Device -from pynwb.file import ElectrodeTable as get_electrode_table from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, NWBH5IOFlexMixin, TestCase @@ -43,7 +43,7 @@ def getContainer(self, nwbfile): def setup_electrode_table(): - table = get_electrode_table() + table = ElectrodesTable() dev1 = Device(name='dev1') group = ElectrodeGroup( name='tetrode1', @@ -175,10 +175,10 @@ def setUpContainer(self): # raise error on write error_msg = "The Clustering neurodata type is deprecated. Use pynwb.misc.Units or NWBFile.units instead" kwargs = dict(description="A fake Clustering interface", - num=[0, 1, 2, 0, 1, 2], - peak_over_rms=[100., 101., 102.], + num=[0, 1, 2, 0, 1, 2], + peak_over_rms=[100., 101., 102.], times=[float(i) for i in range(10, 61, 10)]) - + # create object with deprecated argument with self.assertRaisesWith(ValueError, error_msg): Clustering(**kwargs) @@ -187,7 +187,7 @@ def setUpContainer(self): # no warning should be raised obj = Clustering.__new__(Clustering, in_construct_mode=True) obj.__init__(**kwargs) - + return obj def roundtripContainer(self, cache_spec=False): @@ -247,7 +247,7 @@ def setUpContainer(self): with self.assertRaisesWith(ValueError, msg): cw = ClusterWaveforms(self.clustering, 'filtering', means, stdevs) - # create object in construct mode, modeling the behavior of the ObjectMapper on read + # create object in construct mode, modeling the behavior of the ObjectMapper on read # no warning should be raised cw = ClusterWaveforms.__new__(ClusterWaveforms, container_source=None, diff --git a/tests/integration/hdf5/test_misc.py b/tests/integration/hdf5/test_misc.py index fe0ded502..534175386 100644 --- a/tests/integration/hdf5/test_misc.py +++ b/tests/integration/hdf5/test_misc.py @@ -4,9 +4,8 @@ from pynwb import TimeSeries from pynwb.misc import Units, DecompositionSeries from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, TestCase -from pynwb.ecephys import ElectrodeGroup +from pynwb.ecephys import ElectrodeGroup, ElectrodesTable from pynwb.device import Device -from pynwb.file import ElectrodeTable as get_electrode_table class TestUnitsIO(AcquisitionH5IOMixin, TestCase): @@ -158,7 +157,7 @@ class TestDecompositionSeriesWithSourceChannelsIO(AcquisitionH5IOMixin, TestCase @staticmethod def make_electrode_table(self): """ Make an electrode table, electrode group, and device """ - self.table = get_electrode_table() + self.table = ElectrodesTable() self.dev1 = Device(name='dev1') self.group = ElectrodeGroup( name='tetrode1', diff --git a/tests/unit/test_ecephys.py b/tests/unit/test_ecephys.py index 9ed263d05..cffebdb59 100644 --- a/tests/unit/test_ecephys.py +++ b/tests/unit/test_ecephys.py @@ -14,9 +14,10 @@ FilteredEphys, FeatureExtraction, ElectrodeGroup, + ElectrodesTable ) -from pynwb.device import Device from pynwb.file import ElectrodeTable +from pynwb.device import Device from pynwb.testing import TestCase from pynwb.testing.mock.ecephys import mock_ElectricalSeries @@ -25,11 +26,11 @@ def make_electrode_table(): - table = ElectrodeTable() + table = ElectrodesTable() dev1 = Device(name='dev1') - group = ElectrodeGroup(name='tetrode1', - description='tetrode description', - location='tetrode location', + group = ElectrodeGroup(name='tetrode1', + description='tetrode description', + location='tetrode location', device=dev1) table.add_row(location='CA1', group=group, group_name='tetrode1') table.add_row(location='CA1', group=group, group_name='tetrode1') @@ -74,7 +75,7 @@ def test_init(self): def test_link(self): table, region = self._create_table_and_region() - ts1 = ElectricalSeries(name='test_ts1', data=[0, 1, 2, 3, 4, 5], electrodes=region, + ts1 = ElectricalSeries(name='test_ts1', data=[0, 1, 2, 3, 4, 5], electrodes=region, timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5]) ts2 = ElectricalSeries(name='test_ts2', data=ts1, electrodes=region, timestamps=ts1) ts3 = ElectricalSeries(name='test_ts3', data=ts2, electrodes=region, timestamps=ts2) @@ -87,7 +88,7 @@ def test_invalid_data_shape(self): table, region = self._create_table_and_region() with self.assertRaisesWith(ValueError, ("ElectricalSeries.__init__: incorrect shape for 'data' (got '(2, 2, 2, " "2)', expected '((None,), (None, None), (None, None, None))')")): - ElectricalSeries(name='test_ts1', data=np.ones((2, 2, 2, 2)), electrodes=region, + ElectricalSeries(name='test_ts1', data=np.ones((2, 2, 2, 2)), electrodes=region, timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5]) def test_dimensions_warning(self): @@ -211,9 +212,9 @@ def test_init(self): def test_init_position_array(self): position = np.array((1, 2, 3), dtype=np.dtype([('x', float), ('y', float), ('z', float)])) dev1 = Device(name='dev1') - group = ElectrodeGroup(name='elec1', - description='electrode description', - location='electrode location', + group = ElectrodeGroup(name='elec1', + description='electrode description', + location='electrode location', device=dev1, position=position) self.assertEqual(group.name, 'elec1') @@ -279,9 +280,9 @@ def test_init(self): ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] table, region = self._create_table_and_region() eS = ElectricalSeries(name='test_eS', data=data, electrodes=region, timestamps=ts) - eD = EventDetection(detection_method='detection_method', - source_electricalseries=eS, - source_idx=(1, 2, 3), + eD = EventDetection(detection_method='detection_method', + source_electricalseries=eS, + source_idx=(1, 2, 3), times=(0.1, 0.2, 0.3)) self.assertEqual(eD.detection_method, 'detection_method') self.assertEqual(eD.source_electricalseries, eS) @@ -305,14 +306,14 @@ def test_init(self): peak_over_rms = [5.3, 6.3] error_msg = "The Clustering neurodata type is deprecated. Use pynwb.misc.Units or NWBFile.units instead" - kwargs = dict(description='description', + kwargs = dict(description='description', num=num, peak_over_rms=peak_over_rms, times=times) with self.assertRaisesWith(ValueError, error_msg): cc = Clustering(**kwargs) - - # create object in construct mode, modeling the behavior of the ObjectMapper on read + + # create object in construct mode, modeling the behavior of the ObjectMapper on read # no error or warning should be raised cc = Clustering.__new__(Clustering, in_construct_mode=True) cc.__init__(**kwargs) @@ -330,7 +331,7 @@ def test_init(self): num = [3, 4] peak_over_rms = [5.3, 6.3] - # create object in construct mode, modeling the behavior of the ObjectMapper on read + # create object in construct mode, modeling the behavior of the ObjectMapper on read cc = Clustering.__new__(Clustering, container_source=None, parent=None, @@ -343,7 +344,7 @@ def test_init(self): with self.assertRaisesWith(ValueError, error_msg): cw = ClusterWaveforms(cc, 'filtering', means, stdevs) - # create object in construct mode, modeling the behavior of the ObjectMapper on read + # create object in construct mode, modeling the behavior of the ObjectMapper on read # no error or warning should be raised cw = ClusterWaveforms.__new__(ClusterWaveforms, container_source=None, @@ -371,7 +372,7 @@ def _create_table_and_region(self): def test_init(self): _, region = self._create_table_and_region() - eS = ElectricalSeries(name='test_eS', data=[0, 1, 2, 3], + eS = ElectricalSeries(name='test_eS', data=[0, 1, 2, 3], electrodes=region, timestamps=[0.1, 0.2, 0.3, 0.4]) msg = ( "The linked table for DynamicTableRegion 'electrodes' does not share " @@ -385,9 +386,9 @@ def test_init(self): def test_add_electrical_series(self): lfp = LFP() table, region = self._create_table_and_region() - eS = ElectricalSeries(name='test_eS', - data=[0, 1, 2, 3], - electrodes=region, + eS = ElectricalSeries(name='test_eS', + data=[0, 1, 2, 3], + electrodes=region, timestamps=[0.1, 0.2, 0.3, 0.4]) pm = ProcessingModule(name='test_module', description='a test module') pm.add(table) @@ -410,9 +411,9 @@ def _create_table_and_region(self): def test_init(self): _, region = self._create_table_and_region() - eS = ElectricalSeries(name='test_eS', - data=[0, 1, 2, 3], - electrodes=region, + eS = ElectricalSeries(name='test_eS', + data=[0, 1, 2, 3], + electrodes=region, timestamps=[0.1, 0.2, 0.3, 0.4]) msg = ( "The linked table for DynamicTableRegion 'electrodes' does not share " @@ -425,9 +426,9 @@ def test_init(self): def test_add_electrical_series(self): table, region = self._create_table_and_region() - eS = ElectricalSeries(name='test_eS', - data=[0, 1, 2, 3], - electrodes=region, + eS = ElectricalSeries(name='test_eS', + data=[0, 1, 2, 3], + electrodes=region, timestamps=[0.1, 0.2, 0.3, 0.4]) pm = ProcessingModule(name='test_module', description='a test module') fe = FilteredEphys() @@ -455,9 +456,9 @@ def test_init(self): table, region = self._create_table_and_region() description = ['desc1', 'desc2', 'desc3'] features = [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]]] - fe = FeatureExtraction(electrodes=region, - description=description, - times=event_times, + fe = FeatureExtraction(electrodes=region, + description=description, + times=event_times, features=features) self.assertEqual(fe.description, description) self.assertEqual(fe.times, event_times) @@ -469,9 +470,9 @@ def test_invalid_init_mismatched_event_times(self): description = ['desc1', 'desc2', 'desc3'] features = [[[0, 1, 2], [3, 4, 5]]] with self.assertRaises(ValueError): - FeatureExtraction(electrodes=region, - description=description, - times=event_times, + FeatureExtraction(electrodes=region, + description=description, + times=event_times, features=features) def test_invalid_init_mismatched_electrodes(self): @@ -481,9 +482,9 @@ def test_invalid_init_mismatched_electrodes(self): description = ['desc1', 'desc2', 'desc3'] features = [[[0, 1, 2], [3, 4, 5]]] with self.assertRaises(ValueError): - FeatureExtraction(electrodes=region, - description=description, - times=event_times, + FeatureExtraction(electrodes=region, + description=description, + times=event_times, features=features) def test_invalid_init_mismatched_description(self): @@ -492,9 +493,9 @@ def test_invalid_init_mismatched_description(self): description = ['desc1', 'desc2', 'desc3', 'desc4'] # Need 3 descriptions but give 4 features = [[[0, 1, 2], [3, 4, 5]]] with self.assertRaises(ValueError): - FeatureExtraction(electrodes=region, - description=description, - times=event_times, + FeatureExtraction(electrodes=region, + description=description, + times=event_times, features=features) def test_invalid_init_mismatched_description2(self): @@ -503,7 +504,15 @@ def test_invalid_init_mismatched_description2(self): description = ['desc1', 'desc2', 'desc3'] features = [[0, 1, 2], [3, 4, 5]] # Need 3D feature array but give only 2D array with self.assertRaises(ValueError): - FeatureExtraction(electrodes=region, - description=description, - times=event_times, + FeatureExtraction(electrodes=region, + description=description, + times=event_times, features=features) + +class ElectrodesTableConstructor(TestCase): + + def test_backwards_compatibility(self): + warn_msg = ("The ElectrodeTable convenience function is deprecated. Please create a new instance of " + "the ElectrodesTable class instead.") + with self.assertWarnsWith(DeprecationWarning, warn_msg): + ElectrodeTable() diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 7d66926cd..5338f6669 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -9,9 +9,9 @@ from hdmf.utils import docval, get_docval, popargs from pynwb import NWBFile, TimeSeries, NWBHDF5IO from pynwb.base import Image, Images -from pynwb.file import Subject, ElectrodeTable, _add_missing_timezone +from pynwb.file import Subject, _add_missing_timezone from pynwb.epoch import TimeIntervals -from pynwb.ecephys import ElectricalSeries +from pynwb.ecephys import ElectricalSeries, ElectrodesTable from pynwb.testing import TestCase, remove_test_file @@ -261,7 +261,7 @@ def test_add_acquisition_invalid_name(self): self.nwbfile.get_acquisition("TEST_TS") def test_set_electrode_table(self): - table = ElectrodeTable() + table = ElectrodesTable() dev1 = self.nwbfile.create_device('dev1') group = self.nwbfile.create_electrode_group('tetrode1', 'tetrode description', 'tetrode location', dev1) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 173ac9fdf..342a86e98 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -3,9 +3,9 @@ from hdmf.common import DynamicTable, VectorData, DynamicTableRegion from pynwb.misc import AnnotationSeries, AbstractFeatureSeries, IntervalSeries, Units, DecompositionSeries -from pynwb.file import TimeSeries, ElectrodeTable as get_electrode_table +from pynwb.file import TimeSeries from pynwb.device import Device -from pynwb.ecephys import ElectrodeGroup +from pynwb.ecephys import ElectrodeGroup, ElectrodesTable from pynwb.testing import TestCase @@ -18,9 +18,9 @@ def test_init(self): class AbstractFeatureSeriesConstructor(TestCase): def test_init(self): - aFS = AbstractFeatureSeries(name='test_aFS', - feature_units=['feature units'], - features=['features'], + aFS = AbstractFeatureSeries(name='test_aFS', + feature_units=['feature units'], + features=['features'], timestamps=list()) self.assertEqual(aFS.name, 'test_aFS') self.assertEqual(aFS.feature_units, ['feature units']) @@ -88,7 +88,7 @@ def test_init_delayed_bands(self): @staticmethod def make_electrode_table(self): """ Make an electrode table, electrode group, and device """ - self.table = get_electrode_table() + self.table = ElectrodesTable() self.dev1 = Device(name='dev1') self.group = ElectrodeGroup(name='tetrode1', description='tetrode description', @@ -251,9 +251,9 @@ def test_times_and_intervals(self): def test_electrode_group(self): ut = Units() device = Device(name='test_device') - electrode_group = ElectrodeGroup(name='test_electrode_group', - description='description', - location='location', + electrode_group = ElectrodeGroup(name='test_electrode_group', + description='description', + location='location', device=device) ut.add_unit(electrode_group=electrode_group) self.assertEqual(ut['electrode_group'][0], electrode_group) diff --git a/tests/unit/test_mock.py b/tests/unit/test_mock.py index b0adad269..136e28d82 100644 --- a/tests/unit/test_mock.py +++ b/tests/unit/test_mock.py @@ -32,7 +32,7 @@ from pynwb.testing.mock.ecephys import ( mock_ElectrodeGroup, - mock_ElectrodeTable, + mock_ElectrodesTable, mock_ElectricalSeries, mock_SpikeEventSeries, mock_Units, @@ -70,7 +70,7 @@ mock_CompassDirection, mock_SpatialSeries, mock_ElectrodeGroup, - mock_ElectrodeTable, + mock_ElectrodesTable, mock_ElectricalSeries, mock_SpikeEventSeries, mock_Subject, @@ -116,12 +116,11 @@ def test_mock_write(mock_function, tmp_path): def test_name_generator(): - name_generator_registry.clear() # reset registry - assert name_generator("TimeSeries") == "TimeSeries" assert name_generator("TimeSeries") == "TimeSeries2" + @pytest.mark.parametrize("mask_type", ["image_mask", "pixel_mask", "voxel_mask"]) def test_mock_PlaneSegmentation_mask_type(mask_type): plane_segmentation = mock_PlaneSegmentation(mask_type=mask_type)