diff --git a/spec/ndx-extracellular-channels.extensions.yaml b/spec/ndx-extracellular-channels.extensions.yaml index 98b4959..2f06c16 100644 --- a/spec/ndx-extracellular-channels.extensions.yaml +++ b/spec/ndx-extracellular-channels.extensions.yaml @@ -126,6 +126,11 @@ groups: dtype: text doc: Identifier of the probe, usually the serial number. required: false + groups: + - name: probe_insertion + neurodata_type_inc: ProbeInsertion + doc: Information about the insertion of a probe into the brain. + quantity: '?' links: - name: probe_model target_type: ProbeModel @@ -287,11 +292,6 @@ groups: dtype: text doc: The brain area of the actual contact position, e.g., "CA1". quantity: '?' - groups: - - name: probe_insertion - neurodata_type_inc: ProbeInsertion - doc: Information about the insertion of a probe into the brain. - quantity: '?' links: - name: probe target_type: Probe diff --git a/src/pynwb/tests/test_classes.py b/src/pynwb/tests/test_classes.py index 21295c1..575ac6e 100644 --- a/src/pynwb/tests/test_classes.py +++ b/src/pynwb/tests/test_classes.py @@ -201,6 +201,125 @@ def getContainer(self, nwbfile: NWBFile): return nwbfile.devices["Neuropixels 1.0 Probe Model"] +class TestProbeInsertion(TestCase): + """Simple unit test for creating a ProbeInsertion.""" + + def test_constructor_minimal(self): + pi = ProbeInsertion() + assert pi.name == "probe_insertion" + assert pi.position_reference is None + assert pi.hemisphere is None + assert pi.depth_in_mm is None + assert pi.insertion_position_ap_in_mm is None + assert pi.insertion_position_ml_in_mm is None + assert pi.insertion_position_dv_in_mm is None + assert pi.insertion_angle_roll_in_deg is None + assert pi.insertion_angle_pitch_in_deg is None + assert pi.insertion_angle_yaw_in_deg is None + + def test_constructor_with_depth(self): + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + position_reference="Bregma at the cortical surface.", + hemisphere="left", + depth_in_mm=10.0, + insertion_position_ap_in_mm=2.0, + insertion_position_ml_in_mm=-4.0, + insertion_angle_roll_in_deg=-10.0, + insertion_angle_pitch_in_deg=0.0, + insertion_angle_yaw_in_deg=0.0, + ) + + assert pi.name == "ProbeInsertion" + assert pi.position_reference == "Bregma at the cortical surface." + assert pi.hemisphere == "left" + assert pi.depth_in_mm == 10.0 + assert pi.insertion_position_ap_in_mm == 2.0 + assert pi.insertion_position_ml_in_mm == -4.0 + assert pi.insertion_position_dv_in_mm is None + assert pi.insertion_angle_roll_in_deg == -10.0 + assert pi.insertion_angle_pitch_in_deg == 0.0 + assert pi.insertion_angle_yaw_in_deg == 0.0 + + def test_constructor_with_dv(self): + """Test creating a ProbeInsertion with insertion_position_dv_in_mm instead of depth_in_mm""" + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + position_reference="Bregma at the cortical surface.", + hemisphere="left", + insertion_position_ap_in_mm=2.0, + insertion_position_ml_in_mm=-4.0, + insertion_position_dv_in_mm=-10.0, + insertion_angle_roll_in_deg=-10.0, + insertion_angle_pitch_in_deg=0.0, + insertion_angle_yaw_in_deg=0.0, + ) + + assert pi.name == "ProbeInsertion" + assert pi.position_reference == "Bregma at the cortical surface." + assert pi.hemisphere == "left" + assert pi.depth_in_mm is None + assert pi.insertion_position_ap_in_mm == 2.0 + assert pi.insertion_position_ml_in_mm == -4.0 + assert pi.insertion_position_dv_in_mm == -10.0 + assert pi.insertion_angle_roll_in_deg == -10.0 + assert pi.insertion_angle_pitch_in_deg == 0.0 + assert pi.insertion_angle_yaw_in_deg == 0.0 + + +class TestProbeInsertionDepthRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ProbeInsertion.""" + + def getContainerType(self): + return "ProbeInsertion" + + def addContainer(self): + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + position_reference="Bregma at the cortical surface.", + hemisphere="left", + depth_in_mm=10.0, + insertion_position_ap_in_mm=2.0, + insertion_position_ml_in_mm=-4.0, + insertion_angle_roll_in_deg=-10.0, + insertion_angle_pitch_in_deg=0.0, + insertion_angle_yaw_in_deg=0.0, + ) + + # put this in nwbfile.scratch for testing + self.nwbfile.add_scratch(pi) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.scratch["ProbeInsertion"] + + +class TestProbeInsertionDVRoundTrip(NWBH5IOFlexMixin, TestCase): + """Simple roundtrip test for a ProbeInsertion with insertion_position_dv_in_mm instead of depth_in_mm.""" + + def getContainerType(self): + return "ProbeInsertion" + + def addContainer(self): + pi = ProbeInsertion( + name="ProbeInsertion", # test custom name + position_reference="Bregma at the cortical surface.", + hemisphere="left", + depth_in_mm=10.0, + insertion_position_ap_in_mm=2.0, + insertion_position_ml_in_mm=-4.0, + insertion_position_dv_in_mm=-10.0, + insertion_angle_roll_in_deg=-10.0, + insertion_angle_pitch_in_deg=0.0, + insertion_angle_yaw_in_deg=0.0, + ) + + # put this in nwbfile.scratch for testing + self.nwbfile.add_scratch(pi) + + def getContainer(self, nwbfile: NWBFile): + return nwbfile.scratch["ProbeInsertion"] + + class TestProbe(TestCase): """Simple unit test for creating a Probe.""" @@ -230,9 +349,13 @@ def test_constructor_minimal(self): assert probe.name == "Neuropixels Probe 1" assert probe.identifier is None assert probe.probe_model is pm + assert probe.probe_insertion is None def test_constructor(self): """Test that the constructor for ProbeModel sets values as expected.""" + # NOTE: ProbeInsertion must be named "probe_insertion" when used in Probe. this is the default. + pi = ProbeInsertion() + ct = ContactsTable( description="Test contacts table", ) @@ -253,9 +376,12 @@ def test_constructor(self): name="Neuropixels Probe 1", identifier="28948291", probe_model=pm, + probe_insertion=pi, ) assert probe.identifier == "28948291" + assert probe.probe_insertion is pi + assert probe.probe_model is pm class TestProbeRoundTrip(NWBH5IOFlexMixin, TestCase): @@ -265,6 +391,9 @@ def getContainerType(self): return "Probe" def addContainer(self): + # NOTE: ProbeInsertion must be named "probe_insertion" when used in Probe. this is the default. + pi = ProbeInsertion() + ct = ContactsTable( description="Test contacts table", ) @@ -290,6 +419,7 @@ def addContainer(self): name="Neuropixels Probe 1", identifier="28948291", probe_model=pm, + probe_insertion=pi, ) self.nwbfile.add_device(probe) @@ -331,123 +461,6 @@ def _create_test_probe(): 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.position_reference is None - assert pi.hemisphere is None - assert pi.depth_in_mm is None - assert pi.insertion_position_ap_in_mm is None - assert pi.insertion_position_ml_in_mm is None - assert pi.insertion_position_dv_in_mm is None - assert pi.insertion_angle_roll_in_deg is None - assert pi.insertion_angle_pitch_in_deg is None - assert pi.insertion_angle_yaw_in_deg is None - - def test_constructor_with_depth(self): - pi = ProbeInsertion( - name="ProbeInsertion", # test custom name - position_reference="Bregma at the cortical surface.", - hemisphere="left", - depth_in_mm=10.0, - insertion_position_ap_in_mm=2.0, - insertion_position_ml_in_mm=-4.0, - insertion_angle_roll_in_deg=-10.0, - insertion_angle_pitch_in_deg=0.0, - insertion_angle_yaw_in_deg=0.0, - ) - - assert pi.name == "ProbeInsertion" - assert pi.position_reference == "Bregma at the cortical surface." - assert pi.hemisphere == "left" - assert pi.depth_in_mm == 10.0 - assert pi.insertion_position_ap_in_mm == 2.0 - assert pi.insertion_position_ml_in_mm == -4.0 - assert pi.insertion_position_dv_in_mm is None - assert pi.insertion_angle_roll_in_deg == -10.0 - assert pi.insertion_angle_pitch_in_deg == 0.0 - assert pi.insertion_angle_yaw_in_deg == 0.0 - - def test_constructor_with_dv(self): - pi = ProbeInsertion( - name="ProbeInsertion", # test custom name - position_reference="Bregma at the cortical surface.", - hemisphere="left", - insertion_position_ap_in_mm=2.0, - insertion_position_ml_in_mm=-4.0, - insertion_position_dv_in_mm=-10.0, - insertion_angle_roll_in_deg=-10.0, - insertion_angle_pitch_in_deg=0.0, - insertion_angle_yaw_in_deg=0.0, - ) - - assert pi.name == "ProbeInsertion" - assert pi.position_reference == "Bregma at the cortical surface." - assert pi.hemisphere == "left" - assert pi.insertion_position_ap_in_mm == 2.0 - assert pi.insertion_position_ml_in_mm == -4.0 - assert pi.insertion_position_dv_in_mm == -10.0 - assert pi.insertion_angle_roll_in_deg == -10.0 - assert pi.insertion_angle_pitch_in_deg == 0.0 - assert pi.insertion_angle_yaw_in_deg == 0.0 - - -class TestProbeInsertionDepthRoundTrip(NWBH5IOFlexMixin, TestCase): - """Simple roundtrip test for a ProbeInsertion.""" - - def getContainerType(self): - return "ProbeInsertion" - - def addContainer(self): - pi = ProbeInsertion( - name="ProbeInsertion", # test custom name - position_reference="Bregma at the cortical surface.", - hemisphere="left", - depth_in_mm=10.0, - insertion_position_ap_in_mm=2.0, - insertion_position_ml_in_mm=-4.0, - insertion_angle_roll_in_deg=-10.0, - insertion_angle_pitch_in_deg=0.0, - insertion_angle_yaw_in_deg=0.0, - ) - - # put this in nwbfile.scratch for testing - self.nwbfile.add_scratch(pi) - - def getContainer(self, nwbfile: NWBFile): - return nwbfile.scratch["ProbeInsertion"] - - -class TestProbeInsertionDVRoundTrip(NWBH5IOFlexMixin, TestCase): - """Simple roundtrip test for a ProbeInsertion.""" - - def getContainerType(self): - return "ProbeInsertion" - - def addContainer(self): - pi = ProbeInsertion( - name="ProbeInsertion", # test custom name - position_reference="Bregma at the cortical surface.", - hemisphere="left", - depth_in_mm=10.0, - insertion_position_ap_in_mm=2.0, - insertion_position_ml_in_mm=-4.0, - insertion_position_dv_in_mm=-10.0, - insertion_angle_roll_in_deg=-10.0, - insertion_angle_pitch_in_deg=0.0, - insertion_angle_yaw_in_deg=0.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.""" @@ -501,8 +514,6 @@ def test_constructor_add_row_with_reference(self): def test_constructor_add_row(self): """Test that the constructor for ChannelsTable sets values as expected.""" probe = _create_test_probe() - # NOTE: ProbeInsertion must be named "probe_insertion" when used in ChannelsTable. this is the default. - pi = ProbeInsertion() ct = ChannelsTable( name="Neuropixels1ChannelsTable", # test custom name @@ -512,7 +523,6 @@ def test_constructor_add_row(self): position_reference="(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface.", position_confirmation_method="Histology", probe=probe, - probe_insertion=pi, ) ct.add_row( @@ -550,7 +560,6 @@ def test_constructor_add_row(self): assert ct.position_reference == "(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface." assert ct.position_confirmation_method == "Histology" assert ct.probe is probe - assert ct.probe_insertion is pi assert len(ct) == 2 assert ct["contact"].data == [0, 1] assert ct["contact"].table is probe.probe_model.contacts_table @@ -578,10 +587,6 @@ def addContainer(self): self.nwbfile.add_device(probe.probe_model) # TODO change to add_device_model after integration in core self.nwbfile.add_device(probe) - pi = ProbeInsertion( - name="probe_insertion", - ) - ct = ChannelsTable( name="Neuropixels1ChannelsTable", # test custom name description="Test channels table", @@ -590,7 +595,6 @@ def addContainer(self): position_reference="(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface.", position_confirmation_method="Histology", probe=probe, - probe_insertion=pi, ) ct.add_row( diff --git a/src/pynwb/tests/test_example_usage_all.py b/src/pynwb/tests/test_example_usage_all.py index f41d960..631a871 100644 --- a/src/pynwb/tests/test_example_usage_all.py +++ b/src/pynwb/tests/test_example_usage_all.py @@ -64,13 +64,6 @@ def test_all_classes(): # 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( position_reference="(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface.", hemisphere="left", @@ -82,6 +75,14 @@ def test_all_classes(): insertion_angle_yaw_in_deg=0.0, ) + probe = Probe( + name="Neuropixels Probe 1", + identifier="28948291", + probe_model=pm, + probe_insertion=pi, + ) + nwbfile.add_device(probe) + channels_table = ChannelsTable( name="Neuropixels1ChannelsTable", # test custom name description="Test channels table", @@ -90,7 +91,6 @@ def test_all_classes(): position_reference="(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface.", position_confirmation_method="Histology", probe=probe, - probe_insertion=pi, ) # all of the keyword arguments in add_row are optional @@ -193,16 +193,16 @@ def test_all_classes(): npt.assert_array_equal(read_channels_table["confirmed_brain_area"].data[:], ["CA3", "CA3"]) assert ( - read_channels_table.probe_insertion.position_reference + read_channels_table.probe.probe_insertion.position_reference == "(AP, ML, DV) = (0, 0, 0) corresponds to bregma at the cortical surface." ) - assert read_channels_table.probe_insertion.hemisphere == "left" - assert read_channels_table.probe_insertion.depth_in_mm == 10.0 - assert read_channels_table.probe_insertion.insertion_position_ap_in_mm == 2.0 - assert read_channels_table.probe_insertion.insertion_position_ml_in_mm == -4.0 - assert read_channels_table.probe_insertion.insertion_angle_roll_in_deg == -10.0 - assert read_channels_table.probe_insertion.insertion_angle_pitch_in_deg == 0.0 - assert read_channels_table.probe_insertion.insertion_angle_yaw_in_deg == 0.0 + assert read_channels_table.probe.probe_insertion.hemisphere == "left" + assert read_channels_table.probe.probe_insertion.depth_in_mm == 10.0 + assert read_channels_table.probe.probe_insertion.insertion_position_ap_in_mm == 2.0 + assert read_channels_table.probe.probe_insertion.insertion_position_ml_in_mm == -4.0 + assert read_channels_table.probe.probe_insertion.insertion_angle_roll_in_deg == -10.0 + assert read_channels_table.probe.probe_insertion.insertion_angle_pitch_in_deg == 0.0 + assert read_channels_table.probe.probe_insertion.insertion_angle_yaw_in_deg == 0.0 assert read_nwbfile.devices["Neuropixels Probe 1"].name == "Neuropixels Probe 1" assert read_nwbfile.devices["Neuropixels Probe 1"].identifier == "28948291" diff --git a/src/spec/create_extension_spec.py b/src/spec/create_extension_spec.py index e744f51..64ed3f9 100644 --- a/src/spec/create_extension_spec.py +++ b/src/spec/create_extension_spec.py @@ -128,6 +128,14 @@ def main(): neurodata_type_def="Probe", neurodata_type_inc="Device", doc="Specific instance of a neural probe object.", + groups=[ + NWBGroupSpec( + name="probe_insertion", + neurodata_type_inc="ProbeInsertion", + doc="Information about the insertion of a probe into the brain.", + quantity="?", + ), + ], links=[ NWBLinkSpec( name="probe_model", @@ -297,14 +305,6 @@ def main(): 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", - neurodata_type_inc="ProbeInsertion", - doc="Information about the insertion of a probe into the brain.", - quantity="?", - ), - ], datasets=[ NWBDatasetSpec( name="contact",