diff --git a/src/pynwb/data/nwb.ecephys.yaml b/src/pynwb/data/nwb.ecephys.yaml index 355f6807d..ad57180ba 100644 --- a/src/pynwb/data/nwb.ecephys.yaml +++ b/src/pynwb/data/nwb.ecephys.yaml @@ -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 @@ -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 diff --git a/src/pynwb/data/nwb.misc.yaml b/src/pynwb/data/nwb.misc.yaml index 39eac748a..1ad542b64 100644 --- a/src/pynwb/data/nwb.misc.yaml +++ b/src/pynwb/data/nwb.misc.yaml @@ -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 diff --git a/src/pynwb/data/nwb.ophys.yaml b/src/pynwb/data/nwb.ophys.yaml index 206376261..35c6573eb 100644 --- a/src/pynwb/data/nwb.ophys.yaml +++ b/src/pynwb/data/nwb.ophys.yaml @@ -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 diff --git a/src/pynwb/ecephys.py b/src/pynwb/ecephys.py index 79baf38fa..e86672a84 100644 --- a/src/pynwb/ecephys.py +++ b/src/pynwb/ecephys.py @@ -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) @@ -19,20 +18,25 @@ class ElectrodeGroup(NWBContainer): __nwbfields__ = ('name', 'description', 'location', - 'device') + 'device', + {'name': 'spike_event_series', 'child': False, 'doc': 'doc'}) @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 = [ @@ -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, @@ -107,7 +113,7 @@ class SpikeEventSeries(ElectricalSeries): electrode). """ - __nwbfields__ = () + __nwbfields__ = ({'name': 'unit_series', 'child': False, 'doc': 'doc'}, ) __help = "Snapshots of spike events from data." @@ -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, @@ -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)): @@ -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) @@ -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") diff --git a/src/pynwb/file.py b/src/pynwb/file.py index d35f3fbfd..6dce9b2c7 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -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): diff --git a/src/pynwb/misc.py b/src/pynwb/misc.py index 3ecde6610..7fa1bdb0e 100644 --- a/src/pynwb/misc.py +++ b/src/pynwb/misc.py @@ -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) @@ -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}, @@ -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): @@ -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