Skip to content

Commit 88b43f8

Browse files
authored
Merge branch 'main' into append-concat
2 parents c5cb501 + 705d194 commit 88b43f8

42 files changed

Lines changed: 1501 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,16 @@ spikeinterface.preprocessing
177177
.. autofunction:: clip
178178
.. autofunction:: common_reference
179179
.. autofunction:: correct_lsb
180+
.. autofunction:: compute_motion
180181
.. autofunction:: correct_motion
181182
.. autofunction:: get_motion_presets
182183
.. autofunction:: get_motion_parameters_preset
183184
.. autofunction:: load_motion_info
184185
.. autofunction:: save_motion_info
185186
.. autofunction:: depth_order
186187
.. autofunction:: detect_bad_channels
188+
.. autofunction:: detect_and_interpolate_bad_channels
189+
.. autofunction:: detect_and_remove_bad_channels
187190
.. autofunction:: directional_derivative
188191
.. autofunction:: filter
189192
.. autofunction:: gaussian_filter

doc/how_to/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ Guides on how to solve specific, short problems in SpikeInterface. Learn how to.
1818
auto_curation_training
1919
auto_curation_prediction
2020
physical_units
21+
unsigned_to_signed
2122
customize_a_plot

doc/how_to/physical_units.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _physical_units:
2+
13
Working with physical units in SpikeInterface recordings
24
========================================================
35

doc/how_to/unsigned_to_signed.rst

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
Unsigned to Signed Data types
2+
=============================
3+
4+
As of version 0.103.0 SpikeInterface has changed one of its defaults for interacting with
5+
:code:`Recording` objects. We no longer autocast unsigned dtypes to signed implicitly. This
6+
means that some users of SpikeInterface will need to add one additional line of code to their scripts
7+
to explicitly handle this conversion.
8+
9+
10+
Why this matters?
11+
-----------------
12+
13+
For those that want a deeper understanding of dtypes `NumPy provides a great explanation <https://numpy.org/doc/stable/reference/arrays.dtypes.html>`_.
14+
For our purposes it is important to know that many pieces of recording equipment opt to store their electrophysiological data as unsigned integers
15+
(e.g., Intan, Maxwell Biosystems, 3Brain Biocam).
16+
Similarly to signed integers, in order to convert to real units these file formats only need to store a :code:`gain`
17+
and an :code:`offset`. Our :code:`RecordingExtractor`'s maintain the dtype that the file format utilizes, which means that some of our
18+
:code:`RecordingExtractor`'s will have unsigned dtypes.
19+
20+
The problem with using unsigned dtypes is that many types of functions (including the ones we use from :code:`SciPy`) perform poorly with unsigned integers.
21+
This is made worse by the fact that these failures are silent (i.e. no error is triggered but the operation leads to nonsensical data). So the
22+
solution required is to convernt unsigned integers into signed integers. Previously we did this under the hood, automatically for users that had
23+
a :code:`Recording` object with an unsigned dtype.
24+
25+
We decided, however, that implicitly performing this action was not the best course of action, since:
26+
27+
1) *explicit* is always better than *implicit*
28+
2) some functions would *magically* change the dtype of the :code:`Recording` object, which can cause confusion
29+
30+
So from version 0.103.0, users will now explicitly have to perform this transformation of their data. This will help users better understand how they are
31+
processing their data during an analysis pipeline as well as better understand the provenance of their pipeline.
32+
33+
34+
Using :code:`unsigned_to_signed`
35+
--------------------------------
36+
37+
For users that receive an error because their :code:`Recording` is unsigned, their is one additional step that must be done:
38+
39+
.. code:: python
40+
41+
import spikeinterface.extractors as se
42+
import spikeinterface.preprocessing as spre
43+
44+
# Intan is an example of unsigned data
45+
recording = se.read_intan('path/to/my/file.rhd', stream_id='0')
46+
# to get a signed version of our Recording we use the following function
47+
recording_signed = spre.unsigned_to_signed(recording)
48+
# we can now apply any preprocessing functions like normal, e.g.
49+
recording_filtered = spre.bandpass_filter(recording_signed)
50+
51+
52+
Now with the signed dtype of the :code:`Recording` one can use a SpikeInterface pipeline as usual.
53+
54+
55+
If you are curious if your :code:`Recording` is unsigned you can simply check the repr or use :code:`get_dtype()`
56+
57+
.. code:: python
58+
59+
# the repr automatically displays the dtype
60+
print(recording)
61+
# use method on the Recording object
62+
print(recording.get_dtype())
63+
64+
In either case, if the dtype displayed has a :code:`u` at the beginning (e.g. :code:`uint16`) then your recording is
65+
unsigned. If it doesn't have the :code:`u` (e.g. :code:`int16`) then it is signed and would not need this preprocessing step.
66+
67+
68+
Bit depth
69+
---------
70+
71+
One final important piece of information for some users is the concept of bit depth, which is the number of bits used to
72+
sample the data. The :code:`bit_depth` argument that can be fed into the :code:`unsigned_to_signed` function.
73+
This should be used in cases where the ADC bit depth does not match the bit depth of the data type (e.g., if the data is
74+
stored as :code:`uint16` but the ADC is 12 bits).
75+
Let's make a concrete example: the Biocam acquisition system from 3Brain uses a 12-bit ADC and stores the data as
76+
:code:`uint16`. This means that the data is stored in a 16-bit unsigned integer format, but the actual data
77+
only covers a 12-bit range. Therefore, that the "zero" of the data is not at 0, nor at half of the :code:`uint16` range (i.e. 2^15),
78+
but rather at 2048 (i.e., 2^12).
79+
In this case, setting the :code:`bit_depth` argument to 12 will allow the :code:`unsigned_to_signed` function to
80+
correctly convert the unsigned data to signed data and offset the data to be centered around 0, by subtracting 2048
81+
while converting the data from unsigned to signed.
82+
83+
.. code:: python
84+
85+
recording_unsigned = se.read_biocam('path/to/my/file.brw')
86+
# we can now convert to signed with the correct bit depth
87+
recording_signed = spre.unsigned_to_signed(recording_unsigned, bit_depth=12)
88+
89+
90+
Additional Notes
91+
----------------
92+
93+
1) Some sorters make use of SpikeInterface preprocessing either
94+
within their wrappers or within their own code base. So remember to use the "signed" version of
95+
your recording for the rest of your pipeline.
96+
97+
2) Using :code:`unsigned_to_signed` in versions less than 0.103.0 does not hurt your scripts. This
98+
option was available previously along with the implicit option. Adding this into scripts with old
99+
versions of SpikeInterface will still work and will "future-proof" your scripts for when you
100+
update to a version greater than or equal to 0.103.0.
101+
102+
3) For additional information on units and scaling in SpikeInterface see :ref:`physical_units`.

src/spikeinterface/benchmark/tests/common_benchmark_testing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def compute_gt_templates(recording, gt_sorting, ms_before=2.0, ms_after=3.0, ret
7474
channel_ids=recording.channel_ids,
7575
unit_ids=gt_sorting.unit_ids,
7676
probe=recording.get_probe(),
77-
is_scaled=return_scaled,
77+
is_in_uV=return_scaled,
7878
)
7979
return gt_templates
8080

src/spikeinterface/core/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@
109109
get_chunk_with_margin,
110110
order_channels_by_depth,
111111
)
112-
from .sorting_tools import spike_vector_to_spike_trains, random_spikes_selection, apply_merges_to_sorting
112+
from .sorting_tools import (
113+
spike_vector_to_spike_trains,
114+
random_spikes_selection,
115+
apply_merges_to_sorting,
116+
apply_splits_to_sorting,
117+
)
113118

114119
from .waveform_tools import extract_waveforms_to_buffers, estimate_templates, estimate_templates_with_accumulator
115120
from .snippets_tools import snippets_from_sorting

src/spikeinterface/core/analyzer_extension_core.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* ComputeNoiseLevels which is very convenient to have
1010
"""
1111

12+
import warnings
1213
import numpy as np
1314

1415
from .sortinganalyzer import AnalyzerExtension, register_result_extension
@@ -92,6 +93,11 @@ def _merge_extension_data(
9293
new_data["random_spikes_indices"] = np.flatnonzero(selected_mask[keep_mask])
9394
return new_data
9495

96+
def _split_extension_data(self, split_units, new_unit_ids, new_sorting_analyzer, verbose=False, **job_kwargs):
97+
new_data = dict()
98+
new_data["random_spikes_indices"] = self.data["random_spikes_indices"].copy()
99+
return new_data
100+
95101
def _get_data(self):
96102
return self.data["random_spikes_indices"]
97103

@@ -243,8 +249,6 @@ def _select_extension_data(self, unit_ids):
243249
def _merge_extension_data(
244250
self, merge_unit_groups, new_unit_ids, new_sorting_analyzer, keep_mask=None, verbose=False, **job_kwargs
245251
):
246-
new_data = dict()
247-
248252
waveforms = self.data["waveforms"]
249253
some_spikes = self.sorting_analyzer.get_extension("random_spikes").get_random_spikes()
250254
if keep_mask is not None:
@@ -275,6 +279,11 @@ def _merge_extension_data(
275279

276280
return dict(waveforms=waveforms)
277281

282+
def _split_extension_data(self, split_units, new_unit_ids, new_sorting_analyzer, verbose=False, **job_kwargs):
283+
# splitting only affects random spikes, not waveforms
284+
new_data = dict(waveforms=self.data["waveforms"].copy())
285+
return new_data
286+
278287
def get_waveforms_one_unit(self, unit_id, force_dense: bool = False):
279288
"""
280289
Returns the waveforms of a unit id.
@@ -554,6 +563,49 @@ def _merge_extension_data(
554563

555564
return new_data
556565

566+
def _split_extension_data(self, split_units, new_unit_ids, new_sorting_analyzer, verbose=False, **job_kwargs):
567+
if not new_sorting_analyzer.has_extension("waveforms"):
568+
warnings.warn(
569+
"Splitting templates without the 'waveforms' extension will simply copy the template of the unit that "
570+
"was split to the new split units. This is not recommended and may lead to incorrect results. It is "
571+
"recommended to compute the 'waveforms' extension before splitting, or to use 'hard' splitting mode.",
572+
)
573+
new_data = dict()
574+
for operator, arr in self.data.items():
575+
# we first copy the unsplit units
576+
new_array = np.zeros((len(new_sorting_analyzer.unit_ids), arr.shape[1], arr.shape[2]), dtype=arr.dtype)
577+
new_analyzer_unit_ids = list(new_sorting_analyzer.unit_ids)
578+
unsplit_unit_ids = [unit_id for unit_id in self.sorting_analyzer.unit_ids if unit_id not in split_units]
579+
new_indices = np.array([new_analyzer_unit_ids.index(unit_id) for unit_id in unsplit_unit_ids])
580+
old_indices = self.sorting_analyzer.sorting.ids_to_indices(unsplit_unit_ids)
581+
new_array[new_indices, ...] = arr[old_indices, ...]
582+
583+
for split_unit_id, new_splits in zip(split_units, new_unit_ids):
584+
if new_sorting_analyzer.has_extension("waveforms"):
585+
for new_unit_id in new_splits:
586+
split_unit_index = new_sorting_analyzer.sorting.id_to_index(new_unit_id)
587+
wfs = new_sorting_analyzer.get_extension("waveforms").get_waveforms_one_unit(
588+
new_unit_id, force_dense=True
589+
)
590+
591+
if operator == "average":
592+
arr = np.average(wfs, axis=0)
593+
elif operator == "std":
594+
arr = np.std(wfs, axis=0)
595+
elif operator == "median":
596+
arr = np.median(wfs, axis=0)
597+
elif "percentile" in operator:
598+
_, percentile = operator.splot("_")
599+
arr = np.percentile(wfs, float(percentile), axis=0)
600+
new_array[split_unit_index, ...] = arr
601+
else:
602+
split_unit_index = self.sorting_analyzer.sorting.id_to_index(split_unit_id)
603+
old_template = arr[split_unit_index, ...]
604+
new_indices = new_sorting_analyzer.sorting.ids_to_indices(new_splits)
605+
new_array[new_indices, ...] = np.tile(old_template, (len(new_splits), 1, 1))
606+
new_data[operator] = new_array
607+
return new_data
608+
557609
def _get_data(self, operator="average", percentile=None, outputs="numpy"):
558610
if operator != "percentile":
559611
key = operator
@@ -646,7 +698,7 @@ def get_templates(self, unit_ids=None, operator="average", percentile=None, save
646698
channel_ids=self.sorting_analyzer.channel_ids,
647699
unit_ids=unit_ids,
648700
probe=self.sorting_analyzer.get_probe(),
649-
is_scaled=self.sorting_analyzer.return_in_uV,
701+
is_in_uV=self.sorting_analyzer.return_in_uV,
650702
)
651703
else:
652704
raise ValueError("`outputs` must be 'numpy' or 'Templates'")
@@ -727,6 +779,10 @@ def _merge_extension_data(
727779
# this does not depend on units
728780
return self.data.copy()
729781

782+
def _split_extension_data(self, split_units, new_unit_ids, new_sorting_analyzer, verbose=False, **job_kwargs):
783+
# this does not depend on units
784+
return self.data.copy()
785+
730786
def _run(self, verbose=False, **job_kwargs):
731787
self.data["noise_levels"] = get_noise_levels(
732788
self.sorting_analyzer.recording,

0 commit comments

Comments
 (0)