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 19 commits into
base: dev
Choose a base branch
from
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# PyNWB Changelog

## PyNWB 3.0.1 (February 26, 2025)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please bump to PyNWB 3.1.0 as this is a minor change, not a bug fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will also be part of supporting NWB schema 2.9.0


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

## PyNWB 3.0.0 (February 26, 2025)

### Breaking changes
- The validation methods have been updated with multiple breaking changes. @stephprince [#1911](https://github.com/NeurodataWithoutBorders/pynwb/pull/1911)
- The behavior of `pynwb.validate(io=...)` now matches the behavior of `pynwb.validate(path=...)`. In previous pynwb versions, `pynwb.validate(io=...)` did not use the cached namespaces during validation. To obtain the same behavior as in previous versions, you can update the function call to `pynwb.validate(io=..., use_cached_namespaces=False)`
- `pynwb.validate` will return only a list of validation errors instead of a tuple: (list of validation_errors, status code)
- the `pynwb.validate(path=...)` argument has been added as a replacement for `pynwb.validate(paths=[...])`, which will be deprecated in a future major release [#2024](https://github.com/NeurodataWithoutBorders/pynwb/pull/2024)
- The validate module has been renamed to `validation.py`. The validate method can be
- The validate module has been renamed to `validation.py`. The validate method can be
imported using `import pynwb; pynwb.validate` or `from pynwb import validate`

### Deprecations
Expand All @@ -23,7 +28,7 @@
- ``ImageSeries.format`` is fixed to 'external' if an external file is provided.
- ``ImageSeries.bits_per_pixel`` is deprecated.
- ``ImagingPlane.manifold``, ``ImagingPlane.conversion`` and ``ImagingPlane.unit`` are deprecated. Use ``ImagingPlane.origin_coords`` and ``ImagingPlane.grid_spacing`` instead.
- ``IndexSeries.unit`` is fixed to "N\A".
- ``IndexSeries.unit`` is fixed to "N\A".
- ``IndexSeries.indexed_timeseries`` is deprecated. Use ``IndexSeries.indexed_images`` instead.
- The following deprecated methods have been removed:
- ``NWBFile.add_ic_electrode`` is removed. Use ``NWBFile.add_icephys_electrode`` instead.
Expand Down Expand Up @@ -55,7 +60,7 @@
- Updated `SpikeEventSeries`, `DecompositionSeries`, and `FilteredEphys` examples. @stephprince [#2012](https://github.com/NeurodataWithoutBorders/pynwb/pull/2012)
- Replaced deprecated `scipy.misc.face` dataset in the images tutorial with another example. @stephprince [#2016](https://github.com/NeurodataWithoutBorders/pynwb/pull/2016)
- Removed Allen Brain Observatory example which was unnecessary and difficult to maintain. @rly [#2026](https://github.com/NeurodataWithoutBorders/pynwb/pull/2026)

## PyNWB 2.8.3 (November 19, 2024)

### Enhancements and minor changes
Expand Down
56 changes: 55 additions & 1 deletion src/pynwb/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import numpy as np
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, AllowPositional

Expand Down Expand Up @@ -67,6 +67,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 @@ -389,7 +389,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 @@ -615,7 +615,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 @@ -709,7 +709,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 @@ -791,13 +791,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 800 in src/pynwb/file.py

View check run for this annotation

Codecov / codecov/patch

src/pynwb/file.py#L800

Added line #L800 was not covered by tests
raise ValueError(msg)
electrode_table = getargs('electrode_table', kwargs)
self.electrodes = electrode_table
Expand Down Expand Up @@ -1150,16 +1150,6 @@
return t


def ElectrodeTable(name='electrodes',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might break downstream code. Hopefully not because people should not be using this function, but it is not conventionally marked as private with leading underscores, so they might. What do you think about changing this to something like:

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()

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 @@ -181,6 +181,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 195 in src/pynwb/io/file.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L194 - L195 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L197 - L198 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
n_electrodes = data.shape[1] if data is not None else 5
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 @@ -15,7 +15,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
Loading
Loading