From a608e904ad6fd75f7f983c1555bea1832f850f8e Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Fri, 15 Nov 2024 12:24:14 -0600 Subject: [PATCH] Propagate `unit_electrode_indices` to `SortingInterface` (#1124) --- CHANGELOG.md | 1 + .../ecephys/basesortingextractorinterface.py | 8 +++++ .../tools/spikeinterface/spikeinterface.py | 19 +++++++--- tests/test_ecephys/test_ecephys_interfaces.py | 35 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f4e6b5f..002aed660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * datetime objects now can be validated as conversion options [#1139](https://github.com/catalystneuro/neuroconv/pull/1126) ## Features +* Propagate the `unit_electrode_indices` argument from the spikeinterface tools to `BaseSortingExtractorInterface`. This allows users to map units to the electrode table when adding sorting data [PR #1124](https://github.com/catalystneuro/neuroconv/pull/1124) * Imaging interfaces have a new conversion option `always_write_timestamps` that can be used to force writing timestamps even if neuroconv's heuristics indicates regular sampling rate [PR #1125](https://github.com/catalystneuro/neuroconv/pull/1125) * Added .csv support to DeepLabCutInterface [PR #1140](https://github.com/catalystneuro/neuroconv/pull/1140) diff --git a/src/neuroconv/datainterfaces/ecephys/basesortingextractorinterface.py b/src/neuroconv/datainterfaces/ecephys/basesortingextractorinterface.py index cd8396154..dca2dea5f 100644 --- a/src/neuroconv/datainterfaces/ecephys/basesortingextractorinterface.py +++ b/src/neuroconv/datainterfaces/ecephys/basesortingextractorinterface.py @@ -288,6 +288,7 @@ def add_to_nwbfile( write_as: Literal["units", "processing"] = "units", units_name: str = "units", units_description: str = "Autogenerated by neuroconv.", + unit_electrode_indices: Optional[list[list[int]]] = None, ): """ Primary function for converting the data in a SortingExtractor to NWB format. @@ -312,9 +313,15 @@ def add_to_nwbfile( units_name : str, default: 'units' The name of the units table. If write_as=='units', then units_name must also be 'units'. units_description : str, default: 'Autogenerated by neuroconv.' + unit_electrode_indices : list of lists of int, optional + A list of lists of integers indicating the indices of the electrodes that each unit is associated with. + The length of the list must match the number of units in the sorting extractor. """ from ...tools.spikeinterface import add_sorting_to_nwbfile + if metadata is None: + metadata = self.get_metadata() + metadata_copy = deepcopy(metadata) if write_ecephys_metadata: self.add_channel_metadata_to_nwb(nwbfile=nwbfile, metadata=metadata_copy) @@ -346,4 +353,5 @@ def add_to_nwbfile( write_as=write_as, units_name=units_name, units_description=units_description, + unit_electrode_indices=unit_electrode_indices, ) diff --git a/src/neuroconv/tools/spikeinterface/spikeinterface.py b/src/neuroconv/tools/spikeinterface/spikeinterface.py index 5aa3c8925..fa00d58ed 100644 --- a/src/neuroconv/tools/spikeinterface/spikeinterface.py +++ b/src/neuroconv/tools/spikeinterface/spikeinterface.py @@ -1368,7 +1368,7 @@ def add_units_table_to_nwbfile( write_in_processing_module: bool = False, waveform_means: Optional[np.ndarray] = None, waveform_sds: Optional[np.ndarray] = None, - unit_electrode_indices=None, + unit_electrode_indices: Optional[list[list[int]]] = None, null_values_for_properties: Optional[dict] = None, ): """ @@ -1405,8 +1405,9 @@ def add_units_table_to_nwbfile( Waveform standard deviation for each unit. Shape: (num_units, num_samples, num_channels). unit_electrode_indices : list of lists of int, optional For each unit, a list of electrode indices corresponding to waveform data. - null_values_for_properties: dict, optional - A dictionary mapping properties to null values to use when the property is not present + unit_electrode_indices : list of lists of int, optional + A list of lists of integers indicating the indices of the electrodes that each unit is associated with. + The length of the list must match the number of units in the sorting extractor. """ unit_table_description = unit_table_description or "Autogenerated by neuroconv." @@ -1414,6 +1415,13 @@ def add_units_table_to_nwbfile( nwbfile, pynwb.NWBFile ), f"'nwbfile' should be of type pynwb.NWBFile but is of type {type(nwbfile)}" + if unit_electrode_indices is not None: + electrodes_table = nwbfile.electrodes + if electrodes_table is None: + raise ValueError( + "Electrodes table is required to map units to electrodes. Add an electrode table to the NWBFile first." + ) + null_values_for_properties = dict() if null_values_for_properties is None else null_values_for_properties if not write_in_processing_module and units_table_name != "units": @@ -1668,7 +1676,7 @@ def add_sorting_to_nwbfile( units_description: str = "Autogenerated by neuroconv.", waveform_means: Optional[np.ndarray] = None, waveform_sds: Optional[np.ndarray] = None, - unit_electrode_indices=None, + unit_electrode_indices: Optional[list[list[int]]] = None, ): """Add sorting data (units and their properties) to an NWBFile. @@ -1703,7 +1711,8 @@ def add_sorting_to_nwbfile( waveform_sds : np.ndarray, optional Waveform standard deviation for each unit. Shape: (num_units, num_samples, num_channels). unit_electrode_indices : list of lists of int, optional - For each unit, a list of electrode indices corresponding to waveform data. + A list of lists of integers indicating the indices of the electrodes that each unit is associated with. + The length of the list must match the number of units in the sorting extractor. """ if skip_features is not None: diff --git a/tests/test_ecephys/test_ecephys_interfaces.py b/tests/test_ecephys/test_ecephys_interfaces.py index 24393923f..e036ccb81 100644 --- a/tests/test_ecephys/test_ecephys_interfaces.py +++ b/tests/test_ecephys/test_ecephys_interfaces.py @@ -54,6 +54,41 @@ def test_propagate_conversion_options(self, setup_interface): assert nwbfile.units is None assert "processed_units" in ecephys.data_interfaces + def test_electrode_indices(self, setup_interface): + + recording_interface = MockRecordingInterface(num_channels=4, durations=[0.100]) + recording_extractor = recording_interface.recording_extractor + recording_extractor = recording_extractor.rename_channels(new_channel_ids=["a", "b", "c", "d"]) + recording_extractor.set_property(key="property", values=["A", "B", "C", "D"]) + recording_interface.recording_extractor = recording_extractor + + nwbfile = recording_interface.create_nwbfile() + + unit_electrode_indices = [[3], [0, 1], [1], [2]] + expected_properties_matching = [["D"], ["A", "B"], ["B"], ["C"]] + self.interface.add_to_nwbfile(nwbfile=nwbfile, unit_electrode_indices=unit_electrode_indices) + + unit_table = nwbfile.units + + for unit_row, electrode_indices, property in zip( + unit_table.to_dataframe().itertuples(index=False), + unit_electrode_indices, + expected_properties_matching, + ): + electrode_table_region = unit_row.electrodes + electrode_table_region_indices = electrode_table_region.index.to_list() + assert electrode_table_region_indices == electrode_indices + + electrode_table_region_properties = electrode_table_region["property"].to_list() + assert electrode_table_region_properties == property + + def test_electrode_indices_assertion_error_when_missing_table(self, setup_interface): + with pytest.raises( + ValueError, + match="Electrodes table is required to map units to electrodes. Add an electrode table to the NWBFile first.", + ): + self.interface.create_nwbfile(unit_electrode_indices=[[0], [1], [2], [3]]) + class TestRecordingInterface(RecordingExtractorInterfaceTestMixin): data_interface_cls = MockRecordingInterface