Skip to content
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

ElectrodesTable #1890

Draft
wants to merge 16 commits into
base: dev
Choose a base branch
from
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## PyNWB 2.8.3 (Upcoming)

### Enhancements
- Formally defined and renamed `ElectrodeTable` as the `ElectrodesTable` neurodata_type. @mavaylon1 [#1890](https://github.com/NeurodataWithoutBorders/pynwb/pull/1890)

### Performance
- Cache global type map to speed import 3X. @sneakers-the-rat [#1931](https://github.com/NeurodataWithoutBorders/pynwb/pull/1931)

Expand Down
56 changes: 55 additions & 1 deletion src/pynwb/ecephys.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import warnings
from collections.abc import Iterable

from hdmf.common import DynamicTableRegion
from hdmf.common import DynamicTableRegion, DynamicTable, VectorData
from hdmf.data_utils import DataChunkIterator, assertEqualShape
from hdmf.utils import docval, popargs, get_docval, popargs_to_dict, get_data_shape

Expand Down Expand Up @@ -37,6 +37,60 @@ 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 })

@docval({'name': 'x', 'type': VectorData, 'doc':'x coordinate of the channel location in the brain',
'default': None},
{'name': 'y', 'type': VectorData, 'doc':'y coordinate of the channel location in the brain',
'default': None},
{'name': 'z', 'type': VectorData, 'doc':'z coordinate of the channel location in the brain',
'default': None},
{'name': 'imp', 'type': VectorData, 'doc':'Impedance of the channel, in ohms.', 'default': None},
{'name': 'filtering', 'type': VectorData, 'doc':'Description of hardware filtering.', 'default': None},
{'name': 'rel_x', 'type': VectorData, 'doc':'x coordinate in electrode group', 'default': None},
{'name': 'rel_y', 'type': VectorData, 'doc':'xy coordinate in electrode group', 'default': None},
{'name': 'rel_z', 'type': VectorData, 'doc':'z coordinate in electrode group', 'default': None},
{'name': 'reference', 'type': VectorData, 'default': None,
'doc':'Description of the reference electrode and/or reference scheme used for this electrode'},
*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'))
def __init__(self, **kwargs):
kwargs['name'] = 'electrodes'
kwargs['description'] = 'metadata about extracellular electrodes'

# optional fields
keys_to_set = (
'x',
'y',
'z',
'imp',
'filtering',
'rel_x',
'rel_y',
'rel_z',
'reference')
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)

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):
"""
Expand Down
24 changes: 7 additions & 17 deletions src/pynwb/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -374,7 +374,7 @@
{'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),
Expand Down Expand Up @@ -641,7 +641,7 @@

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):
Expand Down Expand Up @@ -735,7 +735,7 @@
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)
Expand Down Expand Up @@ -817,13 +817,13 @@
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'

Check warning on line 826 in src/pynwb/file.py

View check run for this annotation

Codecov / codecov/patch

src/pynwb/file.py#L826

Added line #L826 was not covered by tests
raise ValueError(msg)
electrode_table = getargs('electrode_table', kwargs)
self.electrodes = electrode_table
Expand Down Expand Up @@ -1176,16 +1176,6 @@
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'])

Expand Down
18 changes: 18 additions & 0 deletions src/pynwb/io/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@
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'

Check warning on line 194 in src/pynwb/io/file.py

View check run for this annotation

Codecov / codecov/patch

src/pynwb/io/file.py#L193-L194

Added lines #L193 - L194 were not covered by tests

new_container = manager.construct(electrodes_builder, True)
return new_container

Check warning on line 197 in src/pynwb/io/file.py

View check run for this annotation

Codecov / codecov/patch

src/pynwb/io/file.py#L196-L197

Added lines #L196 - L197 were not covered by tests
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.
Expand Down
12 changes: 6 additions & 6 deletions src/pynwb/testing/mock/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
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
Expand Down Expand Up @@ -35,10 +35,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()
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(
Expand All @@ -57,7 +57,7 @@ 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)),
Expand All @@ -80,7 +80,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

Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/integration/hdf5/test_ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
FeatureExtraction,
)
from pynwb.device import Device
from pynwb.file import ElectrodeTable as get_electrode_table
from pynwb.ecephys import ElectrodesTable as get_electrode_table
from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, NWBH5IOFlexMixin, TestCase


Expand Down
2 changes: 1 addition & 1 deletion tests/integration/hdf5/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, TestCase
from pynwb.ecephys import ElectrodeGroup
from pynwb.device import Device
from pynwb.file import ElectrodeTable as get_electrode_table
from pynwb.ecephys import ElectrodesTable as get_electrode_table


class TestUnitsIO(AcquisitionH5IOMixin, TestCase):
Expand Down
1 change: 1 addition & 0 deletions tests/integration/hdf5/test_nwbfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ def getContainer(self, nwbfile):

def test_roundtrip(self):
super().test_roundtrip()

# When comparing the pandas dataframes for the row we drop the 'group' column since the
# ElectrodeGroup object after reading will naturally have a different address
pd.testing.assert_frame_equal(self.read_container[0].drop('group', axis=1),
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
FilteredEphys,
FeatureExtraction,
ElectrodeGroup,
ElectrodesTable
)
from pynwb.device import Device
from pynwb.file import ElectrodeTable
from pynwb.testing import TestCase
from pynwb.testing.mock.ecephys import mock_ElectricalSeries

from hdmf.common import DynamicTableRegion


def make_electrode_table():
table = ElectrodeTable()
table = ElectrodesTable()
dev1 = Device('dev1')
group = ElectrodeGroup('tetrode1', 'tetrode description', 'tetrode location', dev1)
table.add_row(location='CA1', group=group, group_name='tetrode1')
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -245,7 +245,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)

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 as get_electrode_table
from pynwb.testing import TestCase


Expand Down
5 changes: 2 additions & 3 deletions tests/unit/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from pynwb.testing.mock.ecephys import (
mock_ElectrodeGroup,
mock_ElectrodeTable,
mock_ElectrodesTable,
mock_ElectricalSeries,
mock_SpikeEventSeries,
mock_Units,
Expand Down Expand Up @@ -70,7 +70,7 @@
mock_CompassDirection,
mock_SpatialSeries,
mock_ElectrodeGroup,
mock_ElectrodeTable,
mock_ElectrodesTable,
mock_ElectricalSeries,
mock_SpikeEventSeries,
mock_Subject,
Expand Down Expand Up @@ -121,4 +121,3 @@ def test_name_generator():

assert name_generator("TimeSeries") == "TimeSeries"
assert name_generator("TimeSeries") == "TimeSeries2"

Loading