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

[WIP] Add changes to facilitate lookup of SpikeEventSeries from Units table #819

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
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
19 changes: 14 additions & 5 deletions src/pynwb/data/nwb.ecephys.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,18 @@ groups:
- num_times
shape:
- null
links:
- name: unit_series
doc: the UnitSeries that holds the unit ids for each waveform
quantity: '?'
target_type: UnitSeries
- neurodata_type_def: ClusterWaveforms
neurodata_type_inc: NWBDataInterface
doc: 'DEPRECATED The mean waveform shape, including standard deviation, of the
different clusters. Ideally, the waveform analysis should be performed on data that
is only high-pass filtered. This is a separate module because it is expected to require
updating. For example, IMEC probes may require different storage requirements to
store/display mean waveforms, requiring a new interface or an extension of this one.'
doc: 'DEPRECATED. The mean waveform shape, including standard deviation, of the different clusters.
Ideally, the waveform analysis should be performed on data that is only high-pass
filtered. This is a separate module because it is expected to require updating.
For example, IMEC probes may require different storage requirements to store/display
mean waveforms, requiring a new interface or an extension of this one.'
attributes:
- name: help
dtype: text
Expand Down Expand Up @@ -334,3 +339,7 @@ groups:
doc: the device that was used to record from this electrode group
quantity: '?'
target_type: Device
- name: spike_event_series
doc: the SpikeEventSeries that holds the recorded spike snippets for this electrode group
quantity: '?'
target_type: SpikeEventSeries
29 changes: 29 additions & 0 deletions src/pynwb/data/nwb.misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,32 @@ groups:
- null
quantity: '?'
default_name: Units
- neurodata_type_def: UnitSeries
neurodata_type_inc: TimeSeries
doc: Unit spike times a stream of IDs of spiking units
attributes:
- name: help
dtype: text
doc: Value is 'Unit spike times a stream of IDs of spiking units'
value: Unit spike times a stream of IDs of spiking units
datasets:
- name: data
dtype: int
doc: the index of the spike unit in the DynamicTableRegion "units"
attributes:
- name: resolution
dtype: float
doc: Value is -1.0. Indices do not have resolution
value: -1.0
- name: unit
dtype: text
doc: Value is 'index'
value: index
dims:
- num_times
shape:
- null
links:
- name: units
doc: The units table that is being indexed
target_type: Units
5 changes: 2 additions & 3 deletions src/pynwb/data/nwb.ophys.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,10 @@ groups:
- name: reference_frame
dtype: text
doc: 'Describes position and reference frame of manifold based on position of
first element in manifold. For example, text description of anotomical location
or vectors needed to rotate to common anotomical axis (eg, AP/DV/ML). COMMENT:
first element in manifold. For example, text description of anatomical location
or vectors needed to rotate to common anatomical axis (eg, AP/DV/ML). COMMENT:
This field is necessary to interpret manifold. If manifold is not present then
this field is not required'
quantity: '?'
groups:
- neurodata_type_def: OpticalChannel
neurodata_type_inc: NWBContainer
Expand Down
37 changes: 20 additions & 17 deletions src/pynwb/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .base import TimeSeries, _default_resolution, _default_conversion
from .core import NWBContainer, NWBDataInterface, MultiContainerInterface, DynamicTableRegion
from .device import Device
from .misc import DecompositionSeries


@register_class('ElectrodeGroup', CORE_NAMESPACE)
Expand All @@ -19,20 +18,25 @@ class ElectrodeGroup(NWBContainer):
__nwbfields__ = ('name',
'description',
'location',
'device')
'device',
{'name': 'spike_event_series', 'child': False, 'doc': 'doc'})
Copy link
Member

Choose a reason for hiding this comment

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

This should be an ElectricalSeries, and doc should be something that makes sense. The docstring will get rendered on the API docs, so this is what users will see if they use the API docs for reference.


@docval({'name': 'name', 'type': str, 'doc': 'the name of this electrode'},
{'name': 'description', 'type': str, 'doc': 'description of this electrode group'},
{'name': 'location', 'type': str, 'doc': 'description of location of this electrode group'},
{'name': 'device', 'type': Device, 'doc': 'the device that was used to record from this electrode group'},
{'name': 'spike_event_series', 'type': 'SpikeEventSeries',
'doc': 'SpikeEventSeries recorded from this group', 'default': None},
{'name': 'parent', 'type': 'NWBContainer',
'doc': 'The parent NWBContainer for this NWBContainer', 'default': None})
def __init__(self, **kwargs):
call_docval_func(super(ElectrodeGroup, self).__init__, kwargs)
description, location, device = popargs("description", "location", "device", kwargs)
description, location, device, spike_event_series = popargs("description", "location", "device",
'spike_event_series', kwargs)
self.description = description
self.location = location
self.device = device
self.spike_event_series = spike_event_series


_et_docval = [
Expand All @@ -58,14 +62,16 @@ class ElectricalSeries(TimeSeries):
"""

__nwbfields__ = ({'name': 'electrodes', 'required_name': 'electrodes',
'doc': 'the electrodes that generated this electrical series', 'child': True},)
'doc': 'the electrodes that generated this electrical series', 'child': True},
{'name': 'electrode_group',
'doc': 'the electrode group that generated this electrical series', 'child': False})

__help = "Stores acquired voltage data from extracellular recordings."

@docval({'name': 'name', 'type': str, 'doc': 'The name of this TimeSeries dataset'},
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ((None, ), (None, None)),
{'name': 'data', 'type': ('array_data', 'data', TimeSeries),
'shape': ((None,), (None, None), (None, None, None)),
'doc': 'The data this TimeSeries dataset stores. Can also store binary data e.g. image frames'},

{'name': 'electrodes', 'type': DynamicTableRegion,
'doc': 'the table region corresponding to the electrodes from which this series was recorded'},
{'name': 'resolution', 'type': float,
Expand Down Expand Up @@ -107,7 +113,7 @@ class SpikeEventSeries(ElectricalSeries):
electrode).
"""

__nwbfields__ = ()
__nwbfields__ = ({'name': 'unit_series', 'child': False, 'doc': 'doc'}, )
Copy link
Member

Choose a reason for hiding this comment

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

doc needs to be something meaningful... see above for explanation.


__help = "Snapshots of spike events from data."

Expand All @@ -116,13 +122,15 @@ class SpikeEventSeries(ElectricalSeries):
'doc': 'The data this TimeSeries dataset stores. Can also store binary data e.g. image frames'},
{'name': 'timestamps', 'type': ('array_data', 'data', TimeSeries),
'doc': 'Timestamps for samples stored in data'},
{'name': 'unit_series', 'type': 'UnitSeries', 'doc': 'unit times that link waveforms to units table',
'default': None},
{'name': 'electrodes', 'type': DynamicTableRegion,
'doc': 'the table region corresponding to the electrodes from which this series was recorded'},
{'name': 'resolution', 'type': float,
'doc': 'The smallest meaningful difference (in specified unit) between values in data',
'default': _default_resolution},
{'name': 'conversion', 'type': float,
'doc': 'Scalar to multiply each element by to conver to volts', 'default': _default_conversion},
'doc': 'Scalar to multiply each element by to convert to volts', 'default': _default_conversion},
{'name': 'comments', 'type': str,
'doc': 'Human-readable comments about this TimeSeries dataset', 'default': 'no comments'},
{'name': 'description', 'type': str,
Expand All @@ -134,7 +142,7 @@ class SpikeEventSeries(ElectricalSeries):
{'name': 'parent', 'type': 'NWBContainer',
'doc': 'The parent NWBContainer for this NWBContainer', 'default': None})
def __init__(self, **kwargs):
name, data, electrodes = popargs('name', 'data', 'electrodes', kwargs)
name, data, unit_series = popargs('name', 'data', 'unit_series', kwargs)
timestamps = getargs('timestamps', kwargs)
if not (isinstance(data, TimeSeries) and isinstance(timestamps, TimeSeries)):
if not (isinstance(data, DataChunkIterator) and isinstance(timestamps, DataChunkIterator)):
Expand All @@ -143,7 +151,8 @@ def __init__(self, **kwargs):
else:
# TODO: add check when we have DataChunkIterators
pass
super(SpikeEventSeries, self).__init__(name, data, electrodes, **kwargs)
super(SpikeEventSeries, self).__init__(name, data, **kwargs)
self.unit_series = unit_series


@register_class('EventDetection', CORE_NAMESPACE)
Expand Down Expand Up @@ -295,13 +304,7 @@ class LFP(MultiContainerInterface):
'type': ElectricalSeries,
'add': 'add_electrical_series',
'get': 'get_electrical_series',
'create': 'create_electrical_series'},

{'attr': 'decomposition_series',
'type': DecompositionSeries,
'add': 'add_decomposition_series',
'get': 'get_decomposition_series',
'create': 'create_decomposition_series'}]
'create': 'create_electrical_series'}]

__help = ("LFP data from one or more channels. Filter properties "
"should be noted in the ElectricalSeries")
Expand Down
2 changes: 1 addition & 1 deletion src/pynwb/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ def add_electrode(self, **kwargs):
d['group_name'] = d['group'].name
call_docval_func(self.electrodes.add_row, d)

@docval({'name': 'region', 'type': (slice, list, tuple), 'doc': 'the indices of the table'},
@docval({'name': 'region', 'type': 'array_data', 'doc': 'the indices of the table'},
{'name': 'description', 'type': str, 'doc': 'a brief description of what this electrode is'},
{'name': 'name', 'type': str, 'doc': 'the name of this container', 'default': 'electrodes'})
def create_electrode_table_region(self, **kwargs):
Expand Down
37 changes: 36 additions & 1 deletion src/pynwb/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from . import register_class, CORE_NAMESPACE
from .base import TimeSeries, _default_conversion, _default_resolution
from .core import NWBContainer, ElementIdentifiers, DynamicTable
from .ecephys import ElectrodeGroup


@register_class('AnnotationSeries', CORE_NAMESPACE)
Expand Down Expand Up @@ -212,7 +213,7 @@ def __init__(self, **kwargs):
'default': None, 'shape': (None, 2)},
{'name': 'electrodes', 'type': 'array_data', 'doc': 'the electrodes that each unit came from',
'default': None},
{'name': 'electrode_group', 'type': 'array_data', 'default': None,
{'name': 'electrode_group', 'type': ElectrodeGroup, 'default': None,
'doc': 'the electrode group that each unit came from'},
{'name': 'waveform_mean', 'type': 'array_data', 'doc': 'the spike waveform mean for each unit',
'default': None},
Expand Down Expand Up @@ -247,6 +248,14 @@ def get_unit_obs_intervals(self, **kwargs):
index = getargs('index', kwargs)
return np.asarray(self['obs_intervals'][index])

def get_spike_waveforms(self, row, spike_number=None):
ses = self['electrode_group'].data[row].spike_event_series
if spike_number is None:
return ses.data[ses.unit_series.data == row, ...]
else:
inds = np.where(ses.unit_series.data == row)[0]
return ses.data[inds[spike_number], ...]


@register_class('DecompositionSeries', CORE_NAMESPACE)
class DecompositionSeries(TimeSeries):
Expand Down Expand Up @@ -327,3 +336,29 @@ def add_band(self, **kwargs):
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})


class UnitSeries(TimeSeries):

__nwbfields__ = ({'name': 'units', 'child': False, 'doc': 'link to Units table'},)

@docval({'name': 'name', 'type': str, 'doc': 'The name of this UnitSeries dataset'},
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': (None,),
'doc': 'Indices of Units table (0-indexed)'},
{'name': 'timestamps', 'type': ('array_data', 'data', TimeSeries), 'shape': (None,),
'doc': 'Timestamps for samples stored in data', 'default': None},
{'name': 'units', 'type': Units, 'doc': 'Units table', 'default': None},
{'name': 'description', 'type': str,
'doc': 'Description of this TimeSeries dataset', 'default': 'no description'},
{'name': 'control', 'type': Iterable,
'doc': 'Numerical labels that apply to each element in data', 'default': None},
{'name': 'control_description', 'type': Iterable,
'doc': 'Description of each control value', 'default': None},
{'name': 'parent', 'type': NWBContainer,
'doc': 'The parent NWBContainer for this NWBContainer', 'default': None})
def __init__(self, **kwargs):
kwargs.update(conversion=np.nan, resolution=np.nan)

name, data, units = popargs('name', 'data', 'units', kwargs)
super(UnitSeries, self).__init__(name, data, **kwargs)
self.units = units