diff --git a/spec/ndx-binned-spikes.extensions.yaml b/spec/ndx-binned-spikes.extensions.yaml index fa9a0ff..83fe4e8 100644 --- a/spec/ndx-binned-spikes.extensions.yaml +++ b/spec/ndx-binned-spikes.extensions.yaml @@ -1,8 +1,46 @@ groups: -- neurodata_type_def: TetrodeSeries - neurodata_type_inc: ElectricalSeries - doc: An extension of ElectricalSeries to include the tetrode ID for each time series. +- neurodata_type_def: BinnedAlignedSpikes + neurodata_type_inc: NWBDataInterface + default_name: BinnedAlignedSpikes + doc: A data interface for binned spike data aligned to an event (e.g. a stimuli + or the beginning of a trial). attributes: - - name: trode_id - dtype: int32 - doc: The tetrode ID. + - name: bin_width_in_milliseconds + dtype: float64 + doc: The lenght in milliseconds of the bins + - name: milliseconds_from_event_to_first_bin + dtype: float64 + default_value: 0.0 + doc: The time in milliseconds from the event (e.g. a stimuli or the beginning + of a trial),to the first bin. Note that this is a negative number if the first + bin is before the event. + required: false + - name: units + dtype: + target_type: Units + reftype: object + doc: A link to the units Table that contains the units of the data. + required: false + datasets: + - name: data + dtype: numeric + dims: + - - number_of_event_repetitions + - number_of_bins + - - number_of_event_repetitions + - number_of_bins + - num_units + shape: + - - null + - null + - - null + - null + - null + doc: TODO + - name: event_timestamps + dtype: float64 + dims: + - number_of_event_repetitions + shape: + - null + doc: The timestamps at whic the event occurred. diff --git a/src/pynwb/ndx_binned_spikes/__init__.py b/src/pynwb/ndx_binned_spikes/__init__.py index fe8a96b..c232e06 100644 --- a/src/pynwb/ndx_binned_spikes/__init__.py +++ b/src/pynwb/ndx_binned_spikes/__init__.py @@ -18,11 +18,8 @@ # Load the namespace load_namespaces(str(__spec_path)) -# TODO: Define your classes here to make them accessible at the package level. -# Either have PyNWB generate a class from the spec using `get_class` as shown -# below or write a custom class and register it using the class decorator -# `@register_class("TetrodeSeries", "ndx-binned-spikes")` -TetrodeSeries = get_class("TetrodeSeries", "ndx-binned-spikes") +BinnedAlignedSpikes = get_class("BinnedAlignedSpikes", "ndx-binned-spikes") + # Remove these functions from the package del load_namespaces, get_class diff --git a/src/pynwb/tests/test_binned_aligned_spikes.py b/src/pynwb/tests/test_binned_aligned_spikes.py new file mode 100644 index 0000000..c87a1b1 --- /dev/null +++ b/src/pynwb/tests/test_binned_aligned_spikes.py @@ -0,0 +1,121 @@ +"""Unit and integration tests for the example BinnedAlignedSpikes extension neurodata type. + +TODO: Modify these tests to test your extension neurodata type. +""" + +import numpy as np + +from pynwb import NWBHDF5IO, NWBFile +from pynwb.testing.mock.device import mock_Device +from pynwb.testing.mock.ecephys import mock_ElectrodeGroup, mock_ElectrodeTable +from pynwb.testing.mock.file import mock_NWBFile +from pynwb.testing import TestCase, remove_test_file, NWBH5IOFlexMixin + +from ndx_binned_spikes import BinnedAlignedSpikes + + +class TestBinnedAlignedSpikesConstructor(TestCase): + """Simple unit test for creating a BinnedAlignedSpikes.""" + + def setUp(self): + """Set up an NWB file. Necessary because BinnedAlignedSpikes requires references to electrodes.""" + self.nwbfile = mock_NWBFile() + + def test_constructor(self): + """Test that the constructor for BinnedAlignedSpikes sets values as expected.""" + + number_of_bins = 3 + number_of_event_repetitions = 4 + bin_width_in_milliseconds = 20.0 + milliseconds_from_event_to_first_bin = 1.0 + data_list = [] + for i in range(number_of_event_repetitions): + data_list.append([i] * number_of_bins) + data = np.array(data_list) + event_timestamps = np.arange(number_of_event_repetitions, dtype="float64") + + binned_aligned_spikes = BinnedAlignedSpikes( + bin_width_in_milliseconds=bin_width_in_milliseconds, + milliseconds_from_event_to_first_bin=milliseconds_from_event_to_first_bin, + data=data, + event_timestamps=event_timestamps + ) + + np.testing.assert_array_equal(binned_aligned_spikes.data, data) + np.testing.assert_array_equal(binned_aligned_spikes.event_timestamps, event_timestamps) + self.assertEqual(binned_aligned_spikes.bin_width_in_milliseconds, bin_width_in_milliseconds) + self.assertEqual( + binned_aligned_spikes.milliseconds_from_event_to_first_bin, milliseconds_from_event_to_first_bin + ) + + self.assertEqual(binned_aligned_spikes.data.shape[0], number_of_event_repetitions) + self.assertEqual(binned_aligned_spikes.data.shape[1], number_of_bins) + + +def generate_binned_aligned_spikes( + number_of_bins=3, + number_of_event_repetitions=4, + bin_width_in_milliseconds=20.0, + milliseconds_from_event_to_first_bin=1.0, +): + + data_list = [] + for i in range(number_of_event_repetitions): + data_list.append([i] * number_of_bins) + data = np.array(data_list) + event_timestamps = np.arange(number_of_event_repetitions, dtype="float64") + + binned_aligned_spikes = BinnedAlignedSpikes( + bin_width_in_milliseconds=bin_width_in_milliseconds, + milliseconds_from_event_to_first_bin=milliseconds_from_event_to_first_bin, + data=data, + event_timestamps=event_timestamps, + ) + return binned_aligned_spikes + + +class TestBinnedAlignedSpikesSimpleRoundtrip(TestCase): + """Simple roundtrip test for BinnedAlignedSpikes.""" + + + + def setUp(self): + self.nwbfile = mock_NWBFile() + + self.binned_aligned_spikes = generate_binned_aligned_spikes() + + self.path = "test.nwb" + + def tearDown(self): + remove_test_file(self.path) + + def test_roundtrip_acquisition(self): + """ + Add a BinnedAlignedSpikes to an NWBFile, write it to file, read the file, and test that the BinnedAlignedSpikes from the + file matches the original BinnedAlignedSpikes. + """ + + self.nwbfile.add_acquisition(self.binned_aligned_spikes) + + with NWBHDF5IO(self.path, mode="w") as io: + io.write(self.nwbfile) + + with NWBHDF5IO(self.path, mode="r", load_namespaces=True) as io: + read_nwbfile = io.read() + self.assertContainerEqual(self.binned_aligned_spikes, read_nwbfile.acquisition["BinnedAlignedSpikes"]) + + def test_roundtrip_processing_module(self): + + + ecephys_processinng_module = self.nwbfile.create_processing_module( + name="ecephys", description="a description" + ) + ecephys_processinng_module.add(self.binned_aligned_spikes) + + with NWBHDF5IO(self.path, mode="w") as io: + io.write(self.nwbfile) + + with NWBHDF5IO(self.path, mode="r", load_namespaces=True) as io: + read_nwbfile = io.read() + read_container = read_nwbfile.processing["ecephys"]["BinnedAlignedSpikes"] + self.assertContainerEqual(self.binned_aligned_spikes, read_container) \ No newline at end of file diff --git a/src/pynwb/tests/test_tetrodeseries.py b/src/pynwb/tests/test_tetrodeseries.py deleted file mode 100644 index 564f59d..0000000 --- a/src/pynwb/tests/test_tetrodeseries.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Unit and integration tests for the example TetrodeSeries extension neurodata type. - -TODO: Modify these tests to test your extension neurodata type. -""" - -import numpy as np - -from pynwb import NWBHDF5IO, NWBFile -from pynwb.testing.mock.device import mock_Device -from pynwb.testing.mock.ecephys import mock_ElectrodeGroup, mock_ElectrodeTable -from pynwb.testing.mock.file import mock_NWBFile -from pynwb.testing import TestCase, remove_test_file, NWBH5IOFlexMixin - -from ndx_binned_spikes import TetrodeSeries - - -def set_up_nwbfile(nwbfile: NWBFile = None): - """Create an NWBFile with a Device, ElectrodeGroup, and 10 electrodes in the ElectrodeTable.""" - nwbfile = nwbfile or mock_NWBFile() - device = mock_Device(nwbfile=nwbfile) - electrode_group = mock_ElectrodeGroup(device=device, nwbfile=nwbfile) - _ = mock_ElectrodeTable(n_rows=10, group=electrode_group, nwbfile=nwbfile) - - return nwbfile - - -class TestTetrodeSeriesConstructor(TestCase): - """Simple unit test for creating a TetrodeSeries.""" - - def setUp(self): - """Set up an NWB file. Necessary because TetrodeSeries requires references to electrodes.""" - self.nwbfile = set_up_nwbfile() - - def test_constructor(self): - """Test that the constructor for TetrodeSeries sets values as expected.""" - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="name", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - - self.assertEqual(tetrode_series.name, "name") - self.assertEqual(tetrode_series.description, "description") - np.testing.assert_array_equal(tetrode_series.data, data) - self.assertEqual(tetrode_series.rate, 1000.0) - self.assertEqual(tetrode_series.starting_time, 0) - self.assertEqual(tetrode_series.electrodes, all_electrodes) - self.assertEqual(tetrode_series.trode_id, 1) - - -class TestTetrodeSeriesSimpleRoundtrip(TestCase): - """Simple roundtrip test for TetrodeSeries.""" - - def setUp(self): - self.nwbfile = set_up_nwbfile() - self.path = "test.nwb" - - def tearDown(self): - remove_test_file(self.path) - - def test_roundtrip(self): - """ - Add a TetrodeSeries to an NWBFile, write it to file, read the file, and test that the TetrodeSeries from the - file matches the original TetrodeSeries. - """ - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="TetrodeSeries", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - - self.nwbfile.add_acquisition(tetrode_series) - - with NWBHDF5IO(self.path, mode="w") as io: - io.write(self.nwbfile) - - with NWBHDF5IO(self.path, mode="r", load_namespaces=True) as io: - read_nwbfile = io.read() - self.assertContainerEqual(tetrode_series, read_nwbfile.acquisition["TetrodeSeries"]) - - -class TestTetrodeSeriesRoundtripPyNWB(NWBH5IOFlexMixin, TestCase): - """Complex, more complete roundtrip test for TetrodeSeries using pynwb.testing infrastructure.""" - - def getContainerType(self): - return "TetrodeSeries" - - def addContainer(self): - set_up_nwbfile(self.nwbfile) - - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="TetrodeSeries", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - self.nwbfile.add_acquisition(tetrode_series) - - def getContainer(self, nwbfile: NWBFile): - return nwbfile.acquisition["TetrodeSeries"] diff --git a/src/spec/create_extension_spec.py b/src/spec/create_extension_spec.py index 742f99c..7e12999 100644 --- a/src/spec/create_extension_spec.py +++ b/src/spec/create_extension_spec.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import os.path -from pynwb.spec import NWBNamespaceBuilder, export_spec, NWBGroupSpec, NWBAttributeSpec +from pynwb.spec import NWBNamespaceBuilder, export_spec, NWBGroupSpec, NWBAttributeSpec, NWBRefSpec, NWBDatasetSpec # TODO: import other spec classes as needed # from pynwb.spec import NWBDatasetSpec, NWBLinkSpec, NWBDtypeSpec, NWBRefSpec @@ -14,16 +14,16 @@ def main(): version="""0.1.0""", doc="""to-do""", author=[ - "Ben Dicther", - "Heberto Mayorquin", + "Ben Dicther", + "Heberto Mayorquin", ], contact=[ - "ben.dichter@gmail.com", - "h.mayorquin@gmail.com", + "ben.dichter@gmail.com", + "h.mayorquin@gmail.com", ], ) ns_builder.include_namespace("core") - + # TODO: if your extension builds on another extension, include the namespace # of the other extension below # ns_builder.include_namespace("ndx-other-extension") @@ -31,15 +31,56 @@ def main(): # TODO: define your new data types # see https://pynwb.readthedocs.io/en/stable/tutorials/general/extensions.html # for more information - tetrode_series = NWBGroupSpec( - neurodata_type_def="TetrodeSeries", - neurodata_type_inc="ElectricalSeries", - doc="An extension of ElectricalSeries to include the tetrode ID for each time series.", - attributes=[NWBAttributeSpec(name="trode_id", doc="The tetrode ID.", dtype="int32")], + + + binned_aligned_spikes_data = NWBDatasetSpec( + name="data", + doc="TODO", + dtype="numeric", # TODO should this be a uint64? + shape=[(None, None), (None, None, None)], + dims=[("number_of_event_repetitions", "number_of_bins"), ("number_of_event_repetitions", "number_of_bins", "num_units")], + ) + + event_timestamps = NWBDatasetSpec( + name="event_timestamps", + doc="The timestamps at whic the event occurred.", + dtype="float64", + shape=(None,), + dims=("number_of_event_repetitions",), + ) + + binned_aligned_spikes = NWBGroupSpec( + neurodata_type_def="BinnedAlignedSpikes", + neurodata_type_inc="NWBDataInterface", + default_name="BinnedAlignedSpikes", + doc="A data interface for binned spike data aligned to an event (e.g. a stimuli or the beginning of a trial).", + datasets=[binned_aligned_spikes_data, event_timestamps], + attributes=[ + NWBAttributeSpec( + name="bin_width_in_milliseconds", + doc="The lenght in milliseconds of the bins", + dtype="float64", + ), + NWBAttributeSpec( + name="milliseconds_from_event_to_first_bin", + doc=( + "The time in milliseconds from the event (e.g. a stimuli or the beginning of a trial)," + "to the first bin. Note that this is a negative number if the first bin is before the event." + ), + dtype="float64", + default_value=0.0, + ), + NWBAttributeSpec( + name="units", + doc="A link to the units Table that contains the units of the data.", + required=False, + dtype=NWBRefSpec(target_type="Units", reftype="object"), + ), + ], ) # TODO: add all of your new data types to this list - new_data_types = [tetrode_series] + new_data_types = [binned_aligned_spikes] # export the spec to yaml files in the spec folder output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "spec"))