From 2a0d0ffa1ddea4980cfbcac78e9f304e47dabca1 Mon Sep 17 00:00:00 2001 From: rly Date: Sat, 20 Apr 2024 19:51:35 -0700 Subject: [PATCH] Add "in_mm", add example usage, update tests --- pyproject.toml | 26 +-- ...ndx-extracellular-channels.extensions.yaml | 8 +- .../{classes.py => utils.py} | 0 src/pynwb/tests/test_classes.py | 54 +++---- src/pynwb/tests/test_example_usage_all.py | 151 ++++++++++++++++++ .../test_example_usage_probeinterface.py | 0 src/spec/create_extension_spec.py | 8 +- 7 files changed, 199 insertions(+), 48 deletions(-) rename src/pynwb/ndx_extracellular_channels/{classes.py => utils.py} (100%) create mode 100644 src/pynwb/tests/test_example_usage_all.py create mode 100644 src/pynwb/tests/test_example_usage_probeinterface.py diff --git a/pyproject.toml b/pyproject.toml index 4ac9070..01bfd0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,17 +17,16 @@ authors = [ ] description = "NWB extension for storing extracellular probe and channels metadata" readme = "README.md" -# requires-python = ">=3.8" +requires-python = ">=3.8" license = {text = "BSD-3"} classifiers = [ - # TODO: add classifiers before release - # "Programming Language :: Python", - # "Programming Language :: Python :: 3.8", - # "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: 3.10", - # "Programming Language :: Python :: 3.11", - # "Programming Language :: Python :: 3.12", - # "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", @@ -44,12 +43,12 @@ dependencies = [ ] # TODO: add URLs before release -# [project.urls] -# "Homepage" = "https://github.com/organization/package" +[project.urls] +"Homepage" = "https://github.com/catalystneuro/ndx-extracellular-channels" # "Documentation" = "https://package.readthedocs.io/" -# "Bug Tracker" = "https://github.com/organization/package/issues" +"Bug Tracker" = "https://github.com/catalystneuro/ndx-extracellular-channels/issues" # "Discussions" = "https://github.com/organization/package/discussions" -# "Changelog" = "https://github.com/organization/package/blob/main/CHANGELOG.md" +"Changelog" = "https://github.com/catalystneuro/ndx-extracellular-channels/blob/main/CHANGELOG.md" [tool.hatch.build] include = [ @@ -116,6 +115,7 @@ line-length = 120 [tool.ruff.per-file-ignores] "src/pynwb/ndx_extracellular_channels/__init__.py" = ["E402", "F401"] "src/spec/create_extension_spec.py" = ["T201"] +"src/pynwb/tests/test_example_usage_all.py" = ["T201"] [tool.ruff.mccabe] max-complexity = 17 diff --git a/spec/ndx-extracellular-channels.extensions.yaml b/spec/ndx-extracellular-channels.extensions.yaml index 102f8f5..aa01632 100644 --- a/spec/ndx-extracellular-channels.extensions.yaml +++ b/spec/ndx-extracellular-channels.extensions.yaml @@ -5,7 +5,7 @@ groups: doc: Metadata about the contacts of a probe, compatible with the ProbeInterface specification. datasets: - - name: relative_position + - name: relative_position_in_mm neurodata_type_inc: VectorData dtype: float dims: @@ -18,7 +18,7 @@ groups: - 2 - - null - 3 - doc: Relative position of the contact + doc: Relative position of the contact in millimeters, relative to `reference`. attributes: - name: reference dtype: text @@ -179,7 +179,7 @@ groups: doc: The filter used on the raw (wideband) voltage data from this contact, including the filter name and frequency cutoffs, e.g., 'High-pass filter at 300 Hz.' quantity: '?' - - name: estimated_position + - name: estimated_position_in_mm neurodata_type_inc: VectorData dtype: - name: ap @@ -207,7 +207,7 @@ groups: dtype: text doc: The brain area of the estimated contact position, e.g., 'CA1'. quantity: '?' - - name: actual_position + - name: actual_position_in_mm neurodata_type_inc: VectorData dtype: - name: ap diff --git a/src/pynwb/ndx_extracellular_channels/classes.py b/src/pynwb/ndx_extracellular_channels/utils.py similarity index 100% rename from src/pynwb/ndx_extracellular_channels/classes.py rename to src/pynwb/ndx_extracellular_channels/utils.py diff --git a/src/pynwb/tests/test_classes.py b/src/pynwb/tests/test_classes.py index d09670e..4bc8448 100644 --- a/src/pynwb/tests/test_classes.py +++ b/src/pynwb/tests/test_classes.py @@ -34,7 +34,7 @@ def test_constructor_add_row(self): # 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], + relative_position_in_mm=[10.0, 10.0], shape="circle", contact_id="C1", shank_id="shank0", @@ -46,7 +46,7 @@ def test_constructor_add_row(self): ) ct.add_row( - relative_position=[20.0, 10.0], + relative_position_in_mm=[20.0, 10.0], shape="square", contact_id="C2", shank_id="shank0", @@ -59,13 +59,13 @@ def test_constructor_add_row(self): # 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" + ct["relative_position_in_mm"].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_in_mm"].reference == "The bottom tip of the probe" - assert ct["relative_position"].data == [[10.0, 10.0], [20.0, 10.0]] + assert ct["relative_position_in_mm"].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"] @@ -89,7 +89,7 @@ def addContainer(self): # 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], + relative_position_in_mm=[10.0, 10.0], shape="circle", contact_id="C1", shank_id="shank0", @@ -101,7 +101,7 @@ def addContainer(self): ) ct.add_row( - relative_position=[20.0, 10.0], + relative_position_in_mm=[20.0, 10.0], shape="square", contact_id="C2", shank_id="shank0", @@ -129,7 +129,7 @@ def test_constructor(self): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -162,7 +162,7 @@ def addContainer(self): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -191,7 +191,7 @@ def test_constructor_minimal(self): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -219,7 +219,7 @@ def test_constructor(self): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -252,7 +252,7 @@ def addContainer(self): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -283,15 +283,15 @@ def _create_test_probe(): description="Test contacts table", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) ct.add_row( - relative_position=[10.0, 10.0], + relative_position_in_mm=[10.0, 10.0], shape="circle", ) @@ -419,9 +419,9 @@ def test_constructor_add_row(self): contact=0, reference_contact=2, filter="High-pass at 300 Hz", - estimated_position=[-1.5, 2.5, -2.5], + estimated_position_in_mm=[-1.5, 2.5, -2.5], estimated_brain_area="CA3", - actual_position=[-1.5, 2.4, -2.4], + actual_position_in_mm=[-1.5, 2.4, -2.4], actual_brain_area="CA3", ) @@ -429,16 +429,16 @@ def test_constructor_add_row(self): contact=1, reference_contact=2, filter="High-pass at 300 Hz", - estimated_position=[-1.5, 2.5, -2.4], + estimated_position_in_mm=[-1.5, 2.5, -2.4], estimated_brain_area="CA3", - actual_position=[-1.5, 2.4, -2.3], + actual_position_in_mm=[-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" + ct["estimated_position_in_mm"].reference = "Bregma at the cortical surface" + ct["actual_position_in_mm"].reference = "Bregma at the cortical surface" # TODO assert ct.name == "Neuropixels1ChannelsTable" @@ -477,9 +477,9 @@ def addContainer(self): contact=0, reference_contact=2, filter="High-pass at 300 Hz", - estimated_position=[-1.5, 2.5, -2.5], + estimated_position_in_mm=[-1.5, 2.5, -2.5], estimated_brain_area="CA3", - actual_position=[-1.5, 2.4, -2.4], + actual_position_in_mm=[-1.5, 2.4, -2.4], actual_brain_area="CA3", ) @@ -487,17 +487,17 @@ def addContainer(self): contact=1, reference_contact=2, filter="High-pass at 300 Hz", - estimated_position=[-1.5, 2.5, -2.4], + estimated_position_in_mm=[-1.5, 2.5, -2.4], estimated_brain_area="CA3", - actual_position=[-1.5, 2.4, -2.3], + actual_position_in_mm=[-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" + ct["estimated_position_in_mm"].reference = "Bregma at the cortical surface" + ct["actual_position_in_mm"].reference = "Bregma at the cortical surface" # put this in nwbfile.acquisition for testing self.nwbfile.add_acquisition(ct) diff --git a/src/pynwb/tests/test_example_usage_all.py b/src/pynwb/tests/test_example_usage_all.py new file mode 100644 index 0000000..0bf51b9 --- /dev/null +++ b/src/pynwb/tests/test_example_usage_all.py @@ -0,0 +1,151 @@ +import datetime +from hdmf.common import DynamicTableRegion +import numpy as np +from pynwb import NWBFile, NWBHDF5IO +import uuid + +from ndx_extracellular_channels import ( + ProbeInsertion, + ContactsTable, + ProbeModel, + Probe, + ChannelsTable, + ExtracellularSeries, +) + + +# initialize an NWBFile object +nwbfile = NWBFile( + session_description="A description of my session", + identifier=str(uuid.uuid4()), + session_start_time=datetime.datetime.now(datetime.timezone.utc), +) + +contacts_table = ContactsTable( + description="Test contacts table", +) +# for demonstration, mix and match different shapes. np.nan means the radius/width/height does not apply +contacts_table.add_row( + relative_position_in_mm=[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? +) +contacts_table.add_row( + relative_position_in_mm=[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 +nwbfile.add_acquisition(contacts_table) + +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=contacts_table, +) +# TODO put this into /general/device_models +nwbfile.add_device(pm) + +probe = Probe( + name="Neuropixels Probe 1", + identifier="28948291", + probe_model=pm, +) +nwbfile.add_device(probe) + +pi = ProbeInsertion( + 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], +) + +channels_table = 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 +) + +# all of the keyword arguments in add_row are optional +channels_table.add_row( + contact=0, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position_in_mm=[-1.5, 2.5, -2.5], + estimated_brain_area="CA3", + actual_position_in_mm=[-1.5, 2.4, -2.4], + actual_brain_area="CA3", +) +channels_table.add_row( + contact=1, + reference_contact=2, + filter="High-pass at 300 Hz", + estimated_position_in_mm=[-1.5, 2.5, -2.4], + estimated_brain_area="CA3", + actual_position_in_mm=[-1.5, 2.4, -2.3], + actual_brain_area="CA3", +) +channels_table["estimated_position_in_mm"].reference = "Bregma at the cortical surface" +channels_table["actual_position_in_mm"].reference = "Bregma at the cortical surface" + +# put this in nwbfile.acquisition for testing +nwbfile.add_acquisition(channels_table) + +channels = DynamicTableRegion( + name="channels", # NOTE: this must be named "channels" when used in ExtracellularSeries + data=[0, 1, 2], + description="All of the channels", + table=channels_table, +) + +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 +) + +nwbfile.add_acquisition(es) + +# write the NWBFile to disk +path = "test_extracellular_channels.nwb" +with NWBHDF5IO(path, mode="w") as io: + io.write(nwbfile) + +# read the NWBFile from disk +with NWBHDF5IO(path, mode="r") as io: + read_nwbfile = io.read() + print(read_nwbfile.acquisition["ExtracellularSeries"]) + print(read_nwbfile.acquisition["Neuropixels1ChannelsTable"]) + print(read_nwbfile.devices["Neuropixels Probe 1"]) + print(read_nwbfile.devices["Neuropixels"]) + print(read_nwbfile.acquisition["contacts_table"]) diff --git a/src/pynwb/tests/test_example_usage_probeinterface.py b/src/pynwb/tests/test_example_usage_probeinterface.py new file mode 100644 index 0000000..e69de29 diff --git a/src/spec/create_extension_spec.py b/src/spec/create_extension_spec.py index 71688d5..3cfa28e 100644 --- a/src/spec/create_extension_spec.py +++ b/src/spec/create_extension_spec.py @@ -49,9 +49,9 @@ def main(): default_name="contacts_table", datasets=[ NWBDatasetSpec( - name="relative_position", + name="relative_position_in_mm", neurodata_type_inc="VectorData", - doc="Relative position of the contact", + doc="Relative position of the contact in millimeters, relative to `reference`.", dtype="float", dims=[["num_contacts", "x, y"], ["num_contacts", "x, y, z"]], shape=[[None, 2], [None, 3]], @@ -327,7 +327,7 @@ def main(): quantity="?", ), NWBDatasetSpec( - name="estimated_position", + name="estimated_position_in_mm", neurodata_type_inc="VectorData", doc=( "Stereotactic coordinates (AP, ML, DV) of the estimated contact position, in millimeters. " @@ -371,7 +371,7 @@ def main(): quantity="?", ), NWBDatasetSpec( - name="actual_position", + name="actual_position_in_mm", neurodata_type_inc="VectorData", doc=( "Stereotactic coordinates (AP, ML, DV) of the the verified actual contact position, such as from "