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

create Electrodes class to replace electrodes table #1205

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
150 changes: 102 additions & 48 deletions src/pynwb/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from .ophys import ImagingPlane
from .ogen import OptogeneticStimulusSite
from .misc import Units
from .core import NWBContainer, NWBDataInterface, MultiContainerInterface, \
ScratchData, LabelledDict
from .core import NWBContainer, NWBDataInterface, MultiContainerInterface, ScratchData, LabelledDict
from hdmf.common import DynamicTableRegion, DynamicTable


Expand Down Expand Up @@ -74,6 +73,75 @@ def __init__(self, **kwargs):
self.date_of_birth = date_of_birth


# NOTE: the electrodes table is not its own neurodata_type
class Electrodes(DynamicTable):

__defaultname__ = 'electrodes'

__columns__ = (
{'name': 'x', 'description': 'the x coordinate of the channel location (+x is posterior)', 'required': True},
{'name': 'y', 'description': 'the y coordinate of the channel location (+y is inferior)', 'required': True},
{'name': 'z', 'description': 'the z coordinate of the channel location (+z is right)', 'required': True},
{'name': 'imp', 'description': 'the impedance of the channel', 'required': True},
{'name': 'location', 'description': 'the location of channel within the subject e.g. brain region',
'required': True},
{'name': 'filtering', 'description': 'description of hardware filtering', 'required': True},
{'name': 'group', 'description': 'a reference to the ElectrodeGroup this electrode is a part of',
'required': True},
{'name': 'group_name', 'description': 'the name of the ElectrodeGroup this electrode is a part of',
'required': True},
{'name': 'rel_x', 'description': 'the x coordinate within the electrode group'},
{'name': 'rel_y', 'description': 'the y coordinate within the electrode group'},
{'name': 'rel_z', 'description': 'the z coordinate within the electrode group'},
{'name': 'reference', 'description': 'Description of the reference used for this electrode.'}
)

@docval({'name': 'name', 'type': str, 'doc': 'name of this Electrodes table', 'default': 'electrodes'},
{'name': 'description', 'type': str, 'doc': 'description of this Electrodes table',
'default': "metadata about extracellular electrodes"},
*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'))
def __init__(self, **kwargs):
call_docval_func(super().__init__, kwargs)

@docval({'name': 'x', 'type': 'float', 'doc': 'the x coordinate of the position (+x is posterior)',
'default': None},
{'name': 'y', 'type': 'float', 'doc': 'the y coordinate of the position (+y is inferior)',
'default': None},
{'name': 'z', 'type': 'float', 'doc': 'the z coordinate of the position (+z is right)',
'default': None},
{'name': 'imp', 'type': 'float', 'doc': 'the impedance of the electrode',
'default': None},
{'name': 'location', 'type': str, 'doc': 'the location of electrode within the subject e.g. brain region',
'default': None},
{'name': 'filtering', 'type': str, 'doc': 'description of hardware filtering',
'default': None},
{'name': 'group', 'type': ElectrodeGroup, 'doc': 'the ElectrodeGroup object to add to this NWBFile',
'default': None},
{'name': 'id', 'type': int, 'doc': 'a unique identifier for the electrode', 'default': None},
{'name': 'rel_x', 'type': 'float', 'doc': 'the x coordinate within the electrode group', 'default': None},
{'name': 'rel_y', 'type': 'float', 'doc': 'the y coordinate within the electrode group', 'default': None},
{'name': 'rel_z', 'type': 'float', 'doc': 'the z coordinate within the electrode group', 'default': None},
{'name': 'reference', 'type': str, 'doc': 'Description of the reference used for this electrode.',
'default': None},
allow_extra=True)
def add_electrode(self, **kwargs):
d = _copy.copy(kwargs['data']) if kwargs.get('data') is not None else kwargs
if d.get('group_name', None) is None:
d['group_name'] = d['group'].name

super().add_row(**d)

@classmethod
@docval({'name': 'table', 'type': DynamicTable, 'doc': 'the DynamicTable object to cast into an Electrodes object'})
def cast(cls, table):
"""Cast a DynamicTable object into an Electrodes object. The input table is cast (no copy) and returned.
Class columns defined in Electrodes.__columns__ are added to the table.
"""
table.__class__ = cls
table._init_class_columns() # create columns and attrs for columns defined in Electrodes.__columns__
return table


@register_class('NWBFile', CORE_NAMESPACE)
class NWBFile(MultiContainerInterface):
"""
Expand Down Expand Up @@ -322,7 +390,6 @@ def __init__(self, **kwargs):
'keywords',
'processing',
'epoch_tags',
'electrodes',
'electrode_groups',
'devices',
'imaging_planes',
Expand Down Expand Up @@ -354,6 +421,11 @@ def __init__(self, **kwargs):
for attr in fieldnames:
setattr(self, attr, kwargs.get(attr, None))

electrodes = kwargs.get('electrodes', None)
if electrodes is not None and not isinstance(electrodes, Electrodes):
electrodes = Electrodes.cast(electrodes)
setattr(self, 'electrodes', electrodes)

# backwards-compatibility code for ic_electrodes / icephys_electrodes
ic_elec_val = kwargs.get('icephys_electrodes', None)
if ic_elec_val is None and kwargs.get('ic_electrodes', None) is not None:
Expand Down Expand Up @@ -482,9 +554,9 @@ def add_epoch(self, **kwargs):

def __check_electrodes(self):
if self.electrodes is None:
self.electrodes = ElectrodeTable()
self.electrodes = Electrodes()

@docval(*get_docval(DynamicTable.add_column))
@docval(*get_docval(Electrodes.add_column))
def add_electrode_column(self, **kwargs):
"""
Add a column to the electrode table.
Expand All @@ -493,45 +565,18 @@ def add_electrode_column(self, **kwargs):
self.__check_electrodes()
call_docval_func(self.electrodes.add_column, kwargs)

@docval({'name': 'x', 'type': 'float', 'doc': 'the x coordinate of the position (+x is posterior)'},
{'name': 'y', 'type': 'float', 'doc': 'the y coordinate of the position (+y is inferior)'},
{'name': 'z', 'type': 'float', 'doc': 'the z coordinate of the position (+z is right)'},
{'name': 'imp', 'type': 'float', 'doc': 'the impedance of the electrode'},
{'name': 'location', 'type': str, 'doc': 'the location of electrode within the subject e.g. brain region'},
{'name': 'filtering', 'type': str, 'doc': 'description of hardware filtering'},
{'name': 'group', 'type': ElectrodeGroup, 'doc': 'the ElectrodeGroup object to add to this NWBFile'},
{'name': 'id', 'type': int, 'doc': 'a unique identifier for the electrode', 'default': None},
{'name': 'rel_x', 'type': 'float', 'doc': 'the x coordinate within the electrode group', 'default': None},
{'name': 'rel_y', 'type': 'float', 'doc': 'the y coordinate within the electrode group', 'default': None},
{'name': 'rel_z', 'type': 'float', 'doc': 'the z coordinate within the electrode group', 'default': None},
{'name': 'reference', 'type': str, 'doc': 'Description of the reference used for this electrode.',
'default': None},
allow_extra=True)
@docval(*get_docval(Electrodes.add_electrode), allow_extra=True)
def add_electrode(self, **kwargs):
"""
Add a unit to the unit table.
Add an electrode to the electrodes table.
See :py:meth:`~hdmf.common.DynamicTable.add_row` for more details.

Required fields are *x*, *y*, *z*, *imp*, *location*, *filtering*,
*group* and any columns that have been added
(through calls to `add_electrode_columns`).
"""
self.__check_electrodes()
d = _copy.copy(kwargs['data']) if kwargs.get('data') is not None else kwargs
if d.get('group_name', None) is None:
d['group_name'] = d['group'].name

new_cols = [('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 used for this electrode.')]
for col_name, col_doc in new_cols:
if kwargs[col_name] is not None and 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

call_docval_func(self.electrodes.add_row, d)
call_docval_func(self.electrodes.add_electrode, kwargs)

@docval({'name': 'region', 'type': (slice, list, tuple), 'doc': 'the indices of the table'},
{'name': 'description', 'type': str, 'doc': 'a brief description of what this electrode is'},
Expand Down Expand Up @@ -763,24 +808,33 @@ def _tablefunc(table_name, description, columns):
return t


def ElectrodeTable(name='electrodes',
description='metadata about extracellular electrodes'):
return _tablefunc(name, description,
[('x', 'the x coordinate of the channel location'),
('y', 'the y coordinate of the channel location'),
('z', 'the z coordinate of the channel location'),
('imp', 'the impedance of the channel'),
('location', 'the location of channel within the subject e.g. brain region'),
('filtering', 'description of hardware filtering'),
('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 ElectrodeTable(name='electrodes', description='metadata about extracellular electrodes'):
"""DEPRECATED. Initialize the electrodes table."""
warn("Use of the ElectrodeTable method is deprecated. "
"Use Electrodes to initialize the electrodes table instead. The optional columns 'rel_x', 'rel_y', 'rel_z', "
"and 'reference' will not be initialized in the tabke returned from this function", DeprecationWarning)
columns = [('x', 'the x coordinate of the channel location'),
('y', 'the y coordinate of the channel location'),
('z', 'the z coordinate of the channel location'),
('imp', 'the impedance of the channel'),
('location', 'the location of channel within the subject e.g. brain region'),
('filtering', 'description of hardware filtering'),
('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')]
t = DynamicTable(name, description)
for c in columns:
t.add_column(c[0], c[1])
return t


def TrialTable(name='trials', description='metadata about experimental trials'):
warn("Use of the TrialTable method is deprecated. "
"Use add_trial or add_trial_column to initialize the trials table instead.", DeprecationWarning)
return _tablefunc(name, description, ['start_time', 'stop_time'])


def InvalidTimesTable(name='invalid_times', description='time intervals to be removed from analysis'):
warn("Use of the InvalidTimesTable method is deprecated. "
"Use add_invalid_time_interval or add_invalid_times_column to initialize the "
"invalid times table instead.", DeprecationWarning)
return _tablefunc(name, description, ['start_time', 'stop_time'])
25 changes: 23 additions & 2 deletions src/pynwb/io/file.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from dateutil.parser import parse as dateutil_parse
from hdmf.build import ObjectMapper
from .. import register_map
from ..file import NWBFile, Subject
from ..core import ScratchData
from ..file import NWBFile, Subject, Electrodes
from ..core import ScratchData, DynamicTable


@register_map(NWBFile)
Expand Down Expand Up @@ -196,6 +196,27 @@ def publication_obj_attr(self, container, manager):
ret = (container.related_publications,)
return ret

@ObjectMapper.constructor_arg('electrodes')
def electrodes_carg(self, builder, manager):
# change typing of constructed electrodes table from DynamicTable to Electrodes
if ('extracellular_ephys' not in builder['general'] or
'electrodes' not in builder['general']['extracellular_ephys']):
return None
electrodes_builder = builder['general']['extracellular_ephys']['electrodes']
# construct the electrodes table from the spec (as a DynamicTable)
# this has happened earlier in the construct process but this function does not have access to the previously
# constructed object, so we do it again here
constructed = manager.construct(electrodes_builder)
ret = Electrodes.cast(constructed)
return ret

@ObjectMapper.object_attr('electrodes')
def electrodes_obj_attr(self, container, manager):
ret = None
if not isinstance(container.electrodes, Electrodes) and isinstance(container.electrodes, DynamicTable):
ret = Electrodes.cast(container.electrodes)
return ret


@register_map(Subject)
class SubjectMap(ObjectMapper):
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/hdf5/test_ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pynwb.ecephys import ElectrodeGroup, ElectricalSeries, FilteredEphys, LFP, Clustering, ClusterWaveforms,\
SpikeEventSeries, EventWaveform, EventDetection, FeatureExtraction
from pynwb.device import Device
from pynwb.file import ElectrodeTable as get_electrode_table
from pynwb.file import Electrodes
from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, TestCase


Expand All @@ -30,7 +30,7 @@ class TestElectricalSeriesIO(AcquisitionH5IOMixin, TestCase):
@staticmethod
def make_electrode_table(self):
""" Make an electrode table, electrode group, and device """
self.table = get_electrode_table()
self.table = Electrodes()
self.dev1 = Device('dev1')
self.group = ElectrodeGroup('tetrode1',
'tetrode description', 'tetrode location', self.dev1)
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/hdf5/test_nwbfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,13 @@ def addContainer(self, nwbfile):
self.group = nwbfile.create_electrode_group('tetrode1', 'tetrode description', 'tetrode location', self.dev1)

nwbfile.add_electrode(id=1, x=1.0, y=2.0, z=3.0, imp=-1.0, location='CA1', filtering='none', group=self.group,
group_name='tetrode1')
group_name='tetrode1', rel_x=1.0)
nwbfile.add_electrode(id=2, x=1.0, y=2.0, z=3.0, imp=-2.0, location='CA1', filtering='none', group=self.group,
group_name='tetrode1')
group_name='tetrode1', rel_x=1.0)
nwbfile.add_electrode(id=3, x=1.0, y=2.0, z=3.0, imp=-3.0, location='CA1', filtering='none', group=self.group,
group_name='tetrode1')
group_name='tetrode1', rel_x=1.0)
nwbfile.add_electrode(id=4, x=1.0, y=2.0, z=3.0, imp=-4.0, location='CA1', filtering='none', group=self.group,
group_name='tetrode1')
group_name='tetrode1', rel_x=1.0)

self.container = nwbfile.electrodes # override self.container which has the placeholder

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 @@ -3,14 +3,14 @@
from pynwb.ecephys import ElectricalSeries, SpikeEventSeries, EventDetection, Clustering, EventWaveform,\
ClusterWaveforms, LFP, FilteredEphys, FeatureExtraction, ElectrodeGroup
from pynwb.device import Device
from pynwb.file import ElectrodeTable
from pynwb.file import Electrodes
from pynwb.testing import TestCase

from hdmf.common import DynamicTableRegion


def make_electrode_table():
table = ElectrodeTable()
table = Electrodes()
dev1 = Device('dev1')
group = ElectrodeGroup('tetrode1', 'tetrode description', 'tetrode location', dev1)
table.add_row(id=1, x=1.0, y=2.0, z=3.0, imp=-1.0, location='CA1', filtering='none',
Expand Down
Loading