diff --git a/README.md b/README.md index bc88ea1..44f8c65 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ classDiagram <> name : str - manufactuer : str + manufacturer : str model : str contour : List[Tuple[float, float], Tuple[float, float, float]] contact_table : ContactsTable @@ -151,7 +151,7 @@ classDiagram -------------------------------------- name : str description : str - + -------------------------------------- columns -------------------------------------- @@ -163,7 +163,7 @@ classDiagram } } - + ExtracellularSeries ..> ChannelsTable : links with channels ProbeModel *--> ContactTable : contains diff --git a/spec/ndx-extracellular-channels.extensions.yaml b/spec/ndx-extracellular-channels.extensions.yaml index c876da9..102f8f5 100644 --- a/spec/ndx-extracellular-channels.extensions.yaml +++ b/spec/ndx-extracellular-channels.extensions.yaml @@ -1,27 +1,7 @@ groups: -- neurodata_type_def: ProbeInsertion - neurodata_type_inc: NWBContainer - doc: Metadata about the insertion of a probe into the brain, which can be used to - determine the location of the probe in the brain. - attributes: - - name: reference - dtype: text - doc: Reference point for `insertion_position_in_mm` coordinates, e.g., "bregma - at the cortical surface". - required: false - - name: hemisphere - dtype: text - doc: The hemisphere ("left" or "right") of the targeted location of the optogenetic - stimulus site. Should be consistent with `insertion_position_in_mm.ml` coordinate - (left = ml < 0, right = ml > 0). - required: false - - name: depth_in_mm - dtype: float - doc: Depth that the probe was driven along `insertion_angle` starting from `insertion_position_in_mm`, - in millimeters. - required: false - neurodata_type_def: ContactsTable neurodata_type_inc: DynamicTable + default_name: contacts_table doc: Metadata about the contacts of a probe, compatible with the ProbeInterface specification. datasets: @@ -79,20 +59,20 @@ groups: doc: The axes defining the contact plane. See https://probeinterface.readthedocs.io/en/main/format_spec.html for more details. quantity: '?' - - name: radius + - name: radius_in_um neurodata_type_inc: VectorData dtype: float - doc: Radius of a circular contact. + doc: Radius of a circular contact, in micrometers. quantity: '?' - - name: width + - name: width_in_um neurodata_type_inc: VectorData dtype: float - doc: Width of a rectangular or square contact. + doc: Width of a rectangular or square contact, in micrometers. quantity: '?' - - name: height + - name: height_in_um neurodata_type_inc: VectorData dtype: float - doc: Height of a rectangular contact. + doc: Height of a rectangular contact, in micrometers. quantity: '?' - name: device_channel_index_pi neurodata_type_inc: VectorData @@ -151,8 +131,31 @@ groups: - name: probe_model target_type: ProbeModel doc: The model of the probe used to record the data. +- neurodata_type_def: ProbeInsertion + neurodata_type_inc: NWBContainer + default_name: probe_insertion + doc: Metadata about the insertion of a probe into the brain, which can be used to + determine the location of the probe in the brain. + attributes: + - name: reference + dtype: text + doc: Reference point for `insertion_position_in_mm` coordinates, e.g., "bregma + at the cortical surface". + required: false + - name: hemisphere + dtype: text + doc: The hemisphere ("left" or "right") of the targeted location of the optogenetic + stimulus site. Should be consistent with `insertion_position_in_mm.ml` coordinate + (left = ml < 0, right = ml > 0). + required: false + - name: depth_in_mm + dtype: float + doc: Depth that the probe was driven along `insertion_angle` starting from `insertion_position_in_mm`, + in millimeters. + required: false - neurodata_type_def: ChannelsTable neurodata_type_inc: DynamicTable + default_name: ChannelsTable doc: Metadata about the channels used in an extracellular recording from a single probe. attributes: @@ -263,9 +266,9 @@ groups: to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'. - - name: contacts + - name: channels neurodata_type_inc: DynamicTableRegion - doc: DynamicTableRegion pointer to rows in a ContactsTable that represent the + doc: DynamicTableRegion pointer to rows in a ChannelsTable that represent the channels used to collect the data in this recording. - name: channel_conversion dtype: float diff --git a/src/pynwb/ndx_extracellular_channels/classes.py b/src/pynwb/ndx_extracellular_channels/classes.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pynwb/tests/test_classes.py b/src/pynwb/tests/test_classes.py new file mode 100644 index 0000000..d09670e --- /dev/null +++ b/src/pynwb/tests/test_classes.py @@ -0,0 +1,593 @@ +"""Unit and integration tests for the ndx_extracellular_channels types.""" + +from hdmf.common import DynamicTableRegion +import numpy as np +from pynwb import NWBFile +from pynwb.testing import TestCase, NWBH5IOFlexMixin + +from ndx_extracellular_channels import ( + ProbeInsertion, + ContactsTable, + ProbeModel, + Probe, + ChannelsTable, + ExtracellularSeries, +) + + +class TestContactsTable(TestCase): + """Simple unit test for creating a ContactsTable.""" + + def test_constructor_minimal(self): + ct = ContactsTable( + description="Test contacts table", + ) + assert ct.name == "contacts_table" + assert len(ct) == 0 + + def test_constructor_add_row(self): + """Test that the constructor for ContactsTable sets values as expected.""" + ct = ContactsTable( + name="ContactsTable", # test custom name + description="Test contacts table", + ) + + # for testing, mix and match different shapes. np.nan means the radius/width/height does not apply + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + contact_id="C1", + shank_id="shank0", + contact_plane_axes=[[0.0, 1.0], [1.0, 0.0]], # TODO make realistic + radius_in_um=10.0, + width_in_um=np.nan, + height_in_um=np.nan, + device_channel_index_pi=1, # TODO what is this for? + ) + + ct.add_row( + relative_position=[20.0, 10.0], + shape="square", + contact_id="C2", + shank_id="shank0", + contact_plane_axes=[[0.0, 1.0], [1.0, 0.0]], # TODO make realistic + radius_in_um=np.nan, + width_in_um=10.0, + height_in_um=10.0, + device_channel_index_pi=2, # TODO what is this for? + ) + + # TODO might be nice to put this on the constructor of ContactsTable as relative_position__reference + # without using a custom mapper + ct["relative_position"].reference = "The bottom tip of the probe" + + assert ct.name == "ContactsTable" + assert ct.description == "Test contacts table" + assert ct["relative_position"].reference == "The bottom tip of the probe" + + assert ct["relative_position"].data == [[10.0, 10.0], [20.0, 10.0]] + assert ct["shape"].data == ["circle", "square"] + assert ct["contact_id"].data == ["C1", "C2"] + assert ct["shank_id"].data == ["shank0", "shank0"] + assert ct["contact_plane_axes"].data == [[[0.0, 1.0], [1.0, 0.0]], [[0.0, 1.0], [1.0, 0.0]]] + assert ct["radius_in_um"].data == [10.0, np.nan] + assert ct["width_in_um"].data == [np.nan, 10.0] + assert ct["device_channel_index_pi"].data == [1, 2] + + +class TestContactsTableRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ContactsTable.""" + + def getContainerType(self): + return "ContactsTable" + + def addContainer(self): + ct = ContactsTable( + name="ContactsTable", # test custom name + description="Test contacts table", + ) + + # for testing, mix and match different shapes. np.nan means the radius/width/height does not apply + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + contact_id="C1", + shank_id="shank0", + contact_plane_axes=[[0.0, 1.0], [1.0, 0.0]], # TODO make realistic + radius_in_um=10.0, + width_in_um=np.nan, + height_in_um=np.nan, + device_channel_index_pi=1, # TODO what is this for? + ) + + ct.add_row( + relative_position=[20.0, 10.0], + shape="square", + contact_id="C2", + shank_id="shank0", + contact_plane_axes=[[0.0, 1.0], [1.0, 0.0]], # TODO make realistic + radius_in_um=np.nan, + width_in_um=10.0, + height_in_um=10.0, + device_channel_index_pi=2, # TODO what is this for? + ) + + # add the object into nwbfile.acquisition for testing + # TODO after integration, put this into /general/extracellular_ephys + self.nwbfile.add_acquisition(ct) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.acquisition["ContactsTable"] + + +class TestProbeModel(TestCase): + """Simple unit test for creating a ProbeModel.""" + + def test_constructor(self): + """Test that the constructor for ProbeModel sets values as expected.""" + ct = ContactsTable( # NOTE: this must be named "contacts_table" when used in ProbeModel + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], # TODO make this optional + contacts_table=ct, + ) + + assert pm.name == "Neuropixels" + assert pm.description == "A neuropixels probe" + assert pm.manufacturer == "IMEC" + assert pm.model_name == "Neuropixels 1.0" + assert pm.planar_contour == [[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]] + assert pm.contacts_table is ct + assert pm.ndim == 2 + + +class TestProbeModelRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ProbeModel.""" + + def getContainerType(self): + return "ProbeModel" + + def addContainer(self): + ct = ContactsTable( + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], # TODO make this optional + contacts_table=ct, + ) + + # TODO put this into /general/device_models + self.nwbfile.add_device(pm) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.devices["Neuropixels"] + + +class TestProbe(TestCase): + """Simple unit test for creating a Probe.""" + + def test_constructor_minimal(self): + """Test that the constructor for ProbeModel sets values as expected.""" + ct = ContactsTable( + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], # TODO make this optional + contacts_table=ct, + ) + + probe = Probe( + name="Neuropixels Probe 1", + probe_model=pm, + ) + + assert probe.name == "Neuropixels Probe 1" + assert probe.identifier is None + assert probe.probe_model is pm + + def test_constructor(self): + """Test that the constructor for ProbeModel sets values as expected.""" + ct = ContactsTable( + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], # TODO make this optional + contacts_table=ct, + ) + + probe = Probe( + name="Neuropixels Probe 1", + identifier="28948291", + probe_model=pm, + ) + + assert probe.identifier == "28948291" + + +class TestProbeRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a Probe.""" + + def getContainerType(self): + return "Probe" + + def addContainer(self): + ct = ContactsTable( + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], + contacts_table=ct, + ) + # TODO put this into /general/device_models + self.nwbfile.add_device(pm) + + probe = Probe( + name="Neuropixels Probe 1", + identifier="28948291", + probe_model=pm, + ) + self.nwbfile.add_device(probe) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.devices["Neuropixels Probe 1"] + + +def _create_test_probe(): + ct = ContactsTable( + description="Test contacts table", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + ct.add_row( + relative_position=[10.0, 10.0], + shape="circle", + ) + + pm = ProbeModel( + name="Neuropixels", + description="A neuropixels probe", + manufacturer="IMEC", + model_name="Neuropixels 1.0", + planar_contour=[[-10.0, -10.0], [10.0, -10.0], [10.0, 10.0], [-10.0, 10.0]], + contacts_table=ct, + ) + + probe = Probe( + name="Neuropixels Probe 1", + identifier="28948291", + probe_model=pm, # TODO rename as model? + ) + return probe + + +class TestProbeInsertion(TestCase): + """Simple unit test for creating a ProbeInsertion.""" + + def test_constructor_minimal(self): + pi = ProbeInsertion() + assert pi.name == "probe_insertion" + assert pi.reference is None + assert pi.hemisphere is None + assert pi.depth_in_mm is None + # assert pi.insertion_position_in_mm is None + # assert pi.insertion_angle_in_deg is None + + def test_constructor(self): + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + reference="Bregma at the cortical surface.", + hemisphere="left", + depth_in_mm=10.0, + # insertion_position_in_mm=[2.0, -4.0, 0.0], # TODO waiting on schema + # insertion_angle_in_deg=[0.0, 0.0, -10.0], + ) + + assert pi.name == "ProbeInsertion" + assert pi.reference == "Bregma at the cortical surface." + assert pi.hemisphere == "left" + assert pi.depth_in_mm == 10.0 + # assert pi.insertion_position_in_mm == [2.0, -4.0, 0.0] + # assert pi.insertion_angle_in_deg == [0.0, 0.0, -10.0] + + +class TestProbeInsertionRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ProbeInsertion.""" + + def getContainerType(self): + return "ProbeInsertion" + + def addContainer(self): + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + reference="Bregma at the cortical surface.", + hemisphere="left", + depth_in_mm=10.0, + # insertion_position_in_mm=[2.0, -4.0, 0.0], # TODO waiting on schema + # insertion_angle_in_deg=[0.0, 0.0, -10.0], + ) + + # put this in nwbfile.scratch for testing + self.nwbfile.add_scratch(pi) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.scratch["ProbeInsertion"] + + +class TestChannelsTable(TestCase): + """Simple unit test for creating a ChannelsTable.""" + + def test_constructor_minimal(self): + """Test that the constructor for ChannelsTable sets values as expected.""" + probe = _create_test_probe() + + ct = ChannelsTable( + description="Test channels table", + probe=probe, + ) + + assert ct.name == "ChannelsTable" + assert ct.description == "Test channels table" + assert ct.reference_mode is None + assert ct.probe is probe + assert len(ct) == 0 + + def test_constructor_add_row_minimal(self): + """Test that the constructor for ChannelsTable sets values as expected.""" + probe = _create_test_probe() + + ct = ChannelsTable( + description="Test channels table", + probe=probe, + ) + ct.add_row() + ct.add_row() + + assert len(ct) == 2 + assert ct.id.data == [0, 1] + + def test_constructor_add_row(self): + """Test that the constructor for ChannelsTable sets values as expected.""" + probe = _create_test_probe() + pi = ProbeInsertion() # NOTE: this must be named "probe_insertion" when used in ChannelsTable + + ct = ChannelsTable( + name="Neuropixels1ChannelsTable", # test custom name + description="Test channels table", + reference_mode="Reference to channel 2", + probe=probe, + probe_insertion=pi, + target_tables={ + "contact": probe.probe_model.contacts_table, + "reference_contact": probe.probe_model.contacts_table, + }, + # TODO should not need to specify the above + ) + + ct.add_row( + contact=0, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position=[-1.5, 2.5, -2.5], + estimated_brain_area="CA3", + actual_position=[-1.5, 2.4, -2.4], + actual_brain_area="CA3", + ) + + ct.add_row( + contact=1, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position=[-1.5, 2.5, -2.4], + estimated_brain_area="CA3", + actual_position=[-1.5, 2.4, -2.3], + actual_brain_area="CA3", + ) + + # TODO might be nice to put this on the constructor of ContactsTable as relative_position__reference + # without using a custom mapper + ct["estimated_position"].reference = "Bregma at the cortical surface" + ct["actual_position"].reference = "Bregma at the cortical surface" + + # TODO + assert ct.name == "Neuropixels1ChannelsTable" + # assert ... + + +class TestChannelsTableRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ChannelsTable.""" + + def getContainerType(self): + return "ChannelsTable" + + def addContainer(self): + probe = _create_test_probe() + self.nwbfile.add_device(probe.probe_model) # TODO change to add_device_model + self.nwbfile.add_device(probe) + + pi = ProbeInsertion( + name="probe_insertion", + ) + + ct = ChannelsTable( + name="Neuropixels1ChannelsTable", # test custom name + description="Test channels table", + reference_mode="Reference to channel 2", + probe=probe, + probe_insertion=pi, + target_tables={ + "contact": probe.probe_model.contacts_table, + "reference_contact": probe.probe_model.contacts_table, + }, + # TODO should not need to specify the above + ) + + ct.add_row( + contact=0, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position=[-1.5, 2.5, -2.5], + estimated_brain_area="CA3", + actual_position=[-1.5, 2.4, -2.4], + actual_brain_area="CA3", + ) + + ct.add_row( + contact=1, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position=[-1.5, 2.5, -2.4], + estimated_brain_area="CA3", + actual_position=[-1.5, 2.4, -2.3], + actual_brain_area="CA3", + ) + + # TODO might be nice to put this on the constructor of ContactsTable as relative_position__reference + # without using a custom mapper + # TODO does matching this happen in the container equals roundtrip test? + ct["estimated_position"].reference = "Bregma at the cortical surface" + ct["actual_position"].reference = "Bregma at the cortical surface" + + # put this in nwbfile.acquisition for testing + self.nwbfile.add_acquisition(ct) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.acquisition["Neuropixels1ChannelsTable"] + + +class TestExtracellularSeries(TestCase): + """Simple unit test for creating an ExtracellularSeries.""" + + def test_constructor(self): + probe = _create_test_probe() + + ct = ChannelsTable( + name="Neuropixels1ChannelsTable", + description="Test channels table", + probe=probe, + ) + ct.add_row() + ct.add_row() + ct.add_row() + + channels = DynamicTableRegion( + name="channels", # NOTE: this must be named "channels" when used in ExtracellularSeries + data=[0, 1, 2], + description="All of the channels", + table=ct, + ) + + es = ExtracellularSeries( + name="ExtracellularSeries", + data=[0.0, 1.0, 2.0], + timestamps=[0.0, 0.001, 0.0002], + channels=channels, + channel_conversion=[1.0, 1.1, 1.2], + conversion=1e5, + offset=0.001, + unit="volts", # TODO should not have to specify this in init + ) + + # NOTE: the TimeSeries mapper maps spec "ExtracellularSeries/data/unit" to "ExtracellularSeries.unit" + assert es.unit == "volts" + assert es.timestamps_unit == "seconds" + + # TODO + # assert ... + + +class TestExtracellularSeriesRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ExtracellularSeries.""" + + def getContainerType(self): + return "ExtracellularSeries" + + def addContainer(self): + probe = _create_test_probe() + self.nwbfile.add_device(probe.probe_model) # TODO change to add_device_model + self.nwbfile.add_device(probe) + + ct = ChannelsTable( + name="Neuropixels1ChannelsTable", + description="Test channels table", + probe=probe, + ) + ct.add_row() + ct.add_row() + ct.add_row() + + # put this in nwbfile.acquisition for testing + self.nwbfile.add_acquisition(ct) + + channels = DynamicTableRegion( + name="channels", # TODO I think this HAS to be named "channels" + data=[0, 1, 2], + description="All of the channels", + table=ct, + ) + + es = ExtracellularSeries( + name="ExtracellularSeries", + data=[[0.0, 1.0, 2.0], [1.0, 2.0, 3.0]], + timestamps=[0.0, 0.001], + channels=channels, + channel_conversion=[1.0, 1.1, 1.2], + conversion=1e5, + offset=0.001, + unit="volts", # TODO should not have to specify this in init + ) + self.nwbfile.add_acquisition(es) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.acquisition["ExtracellularSeries"] diff --git a/src/pynwb/tests/test_tetrodeseries.py b/src/pynwb/tests/test_tetrodeseries.py deleted file mode 100644 index bea677f..0000000 --- a/src/pynwb/tests/test_tetrodeseries.py +++ /dev/null @@ -1,133 +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_extracellular_channels import ( - ProbeInsertion, - ContactsTable, - ProbeModel, - Probe, - ChannelsTable, - ExtracellularSeries, -) - - -# 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 e0713e7..71688d5 100644 --- a/src/spec/create_extension_spec.py +++ b/src/spec/create_extension_spec.py @@ -42,76 +42,11 @@ def main(): ) ns_builder.include_namespace("core") - probe_insertion = NWBGroupSpec( - neurodata_type_def="ProbeInsertion", - neurodata_type_inc="NWBContainer", - doc=( - "Metadata about the insertion of a probe into the brain, which can be used to determine the location of " - "the probe in the brain." - ), - attributes=[ - # TODO waiting on https://github.com/hdmf-dev/hdmf/issues/1099 to add these attributes - # NWBAttributeSpec( - # name="insertion_position_in_mm", - # doc=("Stereotactic coordinates (AP, ML, DV) of where the probe was inserted, in millimeters. " - # "AP = anteroposterior coordinate in mm (+ is anterior). " - # "ML = mediolateral coordinate in mm (+ is right). " - # "DV = dorsoventral coordinate in mm (+ is up)." - # "Coordinates are relative to `reference`"), - # dtype=[ - # NWBDtypeSpec(name="ap", dtype="float", doc="Anteroposterior coordinate in mm, relative to `reference` (+ is anterior).",), - # NWBDtypeSpec(name="ml", dtype="float", doc="Mediolateral coordinate in mm, relative to `reference` (+ is right).",), - # NWBDtypeSpec(name="dv", dtype="float", doc="Dorsoventral coordinate in mm, relative to `reference` (+ is up).",), - # ], - # required=False, - # ), - NWBAttributeSpec( - name="reference", - doc=( - "Reference point for `insertion_position_in_mm` coordinates, e.g., " - '"bregma at the cortical surface".' - ), - dtype="text", - required=False, - ), - NWBAttributeSpec( - name="hemisphere", # TODO this is useful to cache but could be done at the API level - doc=( - 'The hemisphere ("left" or "right") of the targeted location of the optogenetic stimulus site. ' - "Should be consistent with `insertion_position_in_mm.ml` coordinate (left = ml < 0, right = ml > 0)." - ), - dtype="text", - required=False, - ), - # NWBAttributeSpec( - # name="insertion_angle_in_deg", - # doc=("The angles (pitch, yaw, roll) of the probe at the time of insertion, in degrees. " - # "Pitch = rotation around left-right axis, like nodding (+ is rotating the nose upward). " - # "Yaw = rotation around dorsal-ventral axis, like shaking (+ is rotating the nose rightward). " - # "Roll = rotation around anterior-posterior axis, like tilting (+ is rotating the right side downward). "), - # dtype=[ - # NWBDtypeSpec(name="pitch", dtype="float", doc="Rotation around left-right axis, like nodding (+ is rotating the nose upward)."), - # NWBDtypeSpec(name="yaw", dtype="float", doc="Rotation around dorsal-ventral axis, like shaking (+ is rotating the nose rightward)."), - # NWBDtypeSpec(name="roll", dtype="float", doc="Rotation around anterior-posterior axis, like tilting (+ is rotating the right side downward)."), - # ], - # required=False, - # ), - NWBAttributeSpec( - name="depth_in_mm", - doc=( - "Depth that the probe was driven along `insertion_angle` starting from " - "`insertion_position_in_mm`, in millimeters." - ), - dtype="float", - required=False, - ), - ], - ) - contacts_table = NWBGroupSpec( neurodata_type_def="ContactsTable", neurodata_type_inc="DynamicTable", doc="Metadata about the contacts of a probe, compatible with the ProbeInterface specification.", + default_name="contacts_table", datasets=[ NWBDatasetSpec( name="relative_position", @@ -124,7 +59,8 @@ def main(): NWBAttributeSpec( name="reference", doc=( - "Reference point for the relative position coordinates and information about the coordinate system used." + "Reference point for the relative position coordinates and information about the " + "coordinate system used." ), dtype="text", required=False, # TODO should this be required? @@ -155,7 +91,8 @@ def main(): name="contact_plane_axes", neurodata_type_inc="VectorData", doc=( - "The axes defining the contact plane. See https://probeinterface.readthedocs.io/en/main/format_spec.html for more details." + "The axes defining the contact plane. " + "See https://probeinterface.readthedocs.io/en/main/format_spec.html for more details." ), dtype="float", dims=[["num_contacts", "v1, v2", "x, y"], ["num_contacts", "v1, v2", "x, y, z"]], @@ -163,23 +100,23 @@ def main(): quantity="?", ), NWBDatasetSpec( - name="radius", + name="radius_in_um", neurodata_type_inc="VectorData", - doc="Radius of a circular contact.", + doc="Radius of a circular contact, in micrometers.", dtype="float", quantity="?", ), NWBDatasetSpec( - name="width", + name="width_in_um", neurodata_type_inc="VectorData", - doc="Width of a rectangular or square contact.", + doc="Width of a rectangular or square contact, in micrometers.", dtype="float", quantity="?", ), NWBDatasetSpec( - name="height", + name="height_in_um", neurodata_type_inc="VectorData", - doc="Height of a rectangular contact.", + doc="Height of a rectangular contact, in micrometers.", dtype="float", quantity="?", ), @@ -249,6 +186,106 @@ def main(): name="model_name", doc="model of the probe; e.g. 'Neuropixels 1.0'", dtype="text", + ), # TODO is this redundant? There should not be more than 1 ProbeModel object got a given model_name + ], + ) + + probe_insertion = NWBGroupSpec( + neurodata_type_def="ProbeInsertion", + neurodata_type_inc="NWBContainer", + doc=( + "Metadata about the insertion of a probe into the brain, which can be used to determine the location of " + "the probe in the brain." + ), + default_name="probe_insertion", + attributes=[ + # TODO waiting on https://github.com/hdmf-dev/hdmf/issues/1099 to add these attributes + # NWBAttributeSpec( + # name="insertion_position_in_mm", + # doc=( + # "Stereotactic coordinates (AP, ML, DV) of where the probe was inserted, in millimeters. " + # "AP = anteroposterior coordinate in mm (+ is anterior). " + # "ML = mediolateral coordinate in mm (+ is right). " + # "DV = dorsoventral coordinate in mm (+ is up)." + # "Coordinates are relative to `reference`" + # ), + # dtype=[ + # NWBDtypeSpec( + # name="ap", + # dtype="float", + # doc="Anteroposterior coordinate in mm, relative to `reference` (+ is anterior).", + # ), + # NWBDtypeSpec( + # name="ml", + # dtype="float", + # doc="Mediolateral coordinate in mm, relative to `reference` (+ is right).", + # ), + # NWBDtypeSpec( + # name="dv", + # dtype="float", + # doc="Dorsoventral coordinate in mm, relative to `reference` (+ is up).", + # ), + # ], + # required=False, + # ), + NWBAttributeSpec( + name="reference", + doc=( + "Reference point for `insertion_position_in_mm` coordinates, e.g., " + '"bregma at the cortical surface".' + ), + dtype="text", + required=False, + ), + NWBAttributeSpec( + name="hemisphere", # TODO this is useful to cache but could be done at the API level + doc=( + 'The hemisphere ("left" or "right") of the targeted location of the optogenetic stimulus site. ' + "Should be consistent with `insertion_position_in_mm.ml` coordinate (left = ml < 0, " + "right = ml > 0)." + ), + dtype="text", + required=False, + ), + # NWBAttributeSpec( + # name="insertion_angle_in_deg", + # doc=( + # "The angles (pitch, yaw, roll) of the probe at the time of insertion, in degrees. " + # "Pitch = rotation around left-right axis, like nodding (+ is rotating the nose upward). " + # "Yaw = rotation around dorsal-ventral axis, like shaking (+ is rotating the nose rightward). " + # "Roll = rotation around anterior-posterior axis, like tilting (+ is rotating the right side " + # "downward). " + # ), + # dtype=[ + # NWBDtypeSpec( + # name="pitch", + # dtype="float", + # doc="Rotation around left-right axis, like nodding (+ is rotating the nose upward).", + # ), + # NWBDtypeSpec( + # name="yaw", + # dtype="float", + # doc="Rotation around dorsal-ventral axis, like shaking (+ is rotating the nose rightward).", + # ), + # NWBDtypeSpec( + # name="roll", + # dtype="float", + # doc=( + # "Rotation around anterior-posterior axis, like tilting (+ is rotating the right side " + # "downward)." + # ), + # ), + # ], + # required=False, + # ), + NWBAttributeSpec( + name="depth_in_mm", + doc=( + "Depth that the probe was driven along `insertion_angle` starting from " + "`insertion_position_in_mm`, in millimeters." + ), + dtype="float", + required=False, ), ], ) @@ -257,6 +294,7 @@ def main(): neurodata_type_def="ChannelsTable", neurodata_type_inc="DynamicTable", doc="Metadata about the channels used in an extracellular recording from a single probe.", + default_name="ChannelsTable", groups=[ NWBGroupSpec( name="probe_insertion", @@ -425,10 +463,10 @@ def main(): ], ), NWBDatasetSpec( - name="contacts", + name="channels", neurodata_type_inc="DynamicTableRegion", doc=( - "DynamicTableRegion pointer to rows in a ContactsTable that represent the channels used to " + "DynamicTableRegion pointer to rows in a ChannelsTable that represent the channels used to " "collect the data in this recording." ), ), @@ -463,7 +501,7 @@ def main(): ], ) - new_data_types = [probe_insertion, contacts_table, probe_model, probe, channels_table, extracellular_series] + new_data_types = [contacts_table, probe_model, probe, probe_insertion, channels_table, extracellular_series] # export the spec to yaml files in the spec folder output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "spec"))