Skip to content

Commit

Permalink
Propagate unit_electrode_indices to SortingInterface (#1124)
Browse files Browse the repository at this point in the history
  • Loading branch information
h-mayorquin authored Nov 15, 2024
1 parent 64fb9e0 commit a608e90
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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,
)
19 changes: 14 additions & 5 deletions src/neuroconv/tools/spikeinterface/spikeinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
"""
Expand Down Expand Up @@ -1405,15 +1405,23 @@ 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."

assert isinstance(
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":
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions tests/test_ecephys/test_ecephys_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a608e90

Please sign in to comment.