Skip to content

Commit 344383b

Browse files
alejoe91olichepre-commit-ci[bot]
authored
Extend artifact detection: DetectAndRemoveArtifacts preprocessor and signed saturation (#4539)
Co-authored-by: Olivier Winter <olivier.winter@hotmail.fr> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0f8a07b commit 344383b

13 files changed

Lines changed: 690 additions & 123 deletions

doc/modules/preprocessing.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,37 @@ can also be obtained from the pipeline object directly:
149149
dict_used_to_make_pipeline = preprocessing_pipeline.preprocessor_dict
150150
151151
152+
Some preprocessing steps, such as :code:`detect_and_remove_artifacts`, allow you to specify an input recording
153+
and optionally another recording to perform some computation (e.g., detect artifacts on the output of a previous
154+
preprocessor, but remove them on the the parent preprocessor). In this case, the string "pipeline[preprocessor_name]"
155+
can be used in the dictionary to specify that the recording argument for this step should be the output of a previous
156+
preprocessor in the same pipeline. For example, if we want to use the output of the "bandpass_filter" step as the
157+
recording to detect artifacts, we can specify it as follows:
158+
159+
.. code-block:: python
160+
161+
preprocessing_dict = {
162+
'bandpass_filter': {'freq_min': 250},
163+
'common_reference': {'operator': 'median', 'reference': 'global'},
164+
'detect_and_remove_artifacts': {'recording_to_detect': 'pipeline[bandpass_filter]'},
165+
}
166+
167+
This will detect artifacts on the output of the "bandpass_filter" step, but the artifacts will be removed on the output
168+
of the "common_reference" step (since the parent recording for "detect_and_remove_artifacts" is by default the output of
169+
the previous step in the pipeline, which is "common_reference" in this case).
170+
To specify the "raw" recording, i.e., the input to the pipeline, we can use "pipeline[raw]".
171+
For example, if we want to detect artifacts on the raw recording, we can specify it as follows:
172+
173+
174+
.. code-block:: python
175+
176+
preprocessing_dict = {
177+
'bandpass_filter': {'freq_min': 250},
178+
'common_reference': {'operator': 'median', 'reference': 'global'},
179+
'detect_and_remove_artifacts': {'recording_to_detect': 'pipeline[raw]'},
180+
}
181+
182+
152183
Impact on recording dtype
153184
-------------------------
154185

src/spikeinterface/extractors/neoextractors/openephys.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
import probeinterface
1717

18-
from spikeinterface.extractors.neuropixels_utils import get_neuropixels_sample_shifts_from_probe
18+
from spikeinterface.extractors.neuropixels_utils import (
19+
get_neuropixels_sample_shifts_from_probe,
20+
compute_saturation_threshold_from_probe,
21+
)
1922
from spikeinterface.extractors.neoextractors.neobaseextractor import NeoBaseRecordingExtractor, NeoBaseEventExtractor
2023

2124

@@ -338,6 +341,11 @@ def __init__(
338341
if sample_shifts is not None:
339342
self.set_property("inter_sample_shift", sample_shifts)
340343

344+
# add saturation levels if available
345+
saturation_threshold_uV = compute_saturation_threshold_from_probe(probe, oe_stream_name)
346+
if saturation_threshold_uV is not None:
347+
self.annotate(saturation_threshold_uV=saturation_threshold_uV)
348+
341349
# folder_path can point to different levels of the OE folder structure
342350
# (root, record node, experiment, or recording). We need to find the root folder
343351
# in order to load the sync timestamps and set them as times to the recording.

src/spikeinterface/extractors/neoextractors/spikegadgets.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ def __init__(
5656
)
5757
self._kwargs.update(dict(file_path=str(Path(file_path).absolute()), stream_id=stream_id))
5858

59-
probegroup = None # TODO remove once probeinterface is updated to 0.2.22 in the pyproject.toml
60-
if packaging.version.parse(probeinterface.__version__) > packaging.version.parse("0.2.21"):
61-
probegroup = probeinterface.read_spikegadgets(file_path, raise_error=False)
59+
probegroup = probeinterface.read_spikegadgets(file_path, raise_error=False)
60+
# TODO: add adc sample shifts and saturation levels if available in the probe metadata
6261

6362
if probegroup is not None:
6463
self.set_probegroup(probegroup, in_place=True)

src/spikeinterface/extractors/neoextractors/spikeglx.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import probeinterface
55

66
from spikeinterface.core.core_tools import define_function_from_class
7-
from spikeinterface.extractors.neuropixels_utils import get_neuropixels_sample_shifts_from_probe
7+
from spikeinterface.extractors.neuropixels_utils import (
8+
get_neuropixels_sample_shifts_from_probe,
9+
compute_saturation_threshold_from_probe,
10+
)
811
from spikeinterface.extractors.neoextractors.neobaseextractor import NeoBaseRecordingExtractor, NeoBaseEventExtractor
912

1013

@@ -91,6 +94,11 @@ def __init__(
9194
sample_shifts = get_neuropixels_sample_shifts_from_probe(probe)
9295
if sample_shifts is not None:
9396
self.set_property("inter_sample_shift", sample_shifts)
97+
98+
# add saturation levels if available
99+
saturation_threshold_uV = compute_saturation_threshold_from_probe(probe, self.stream_id)
100+
if saturation_threshold_uV is not None:
101+
self.annotate(saturation_threshold_uV=saturation_threshold_uV)
94102
else:
95103
warning_message = (
96104
"Unable to find a corresponding metadata file for the recording. "

src/spikeinterface/extractors/neuropixels_utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,43 @@ def get_neuropixels_sample_shifts_from_probe(probe: Probe) -> np.ndarray:
5353
return sample_shifts
5454

5555

56+
def compute_saturation_threshold_from_probe(probe: Probe, stream_name: str) -> float:
57+
"""
58+
Compute the saturation threshold in microvolts for a Neuropixels probe based on the probe information.
59+
60+
Parameters
61+
----------
62+
probe : Probe
63+
The probe object containing channel and ADC information.
64+
stream_name : str
65+
The name of the stream. If it contains "lf" (or "LF"), the function will look for the
66+
lf gain and saturation in the probe annotations.
67+
68+
Returns
69+
-------
70+
saturation_threshold_uV : float
71+
The saturation threshold in microvolts, or None if it cannot be computed.
72+
"""
73+
adc_range_vpp = probe.annotations.get("adc_range_vpp", None)
74+
gain = None
75+
if "lf" in stream_name.lower():
76+
gain = probe.annotations.get("lf_gain", None)
77+
else:
78+
gain = probe.annotations.get("ap_gain", None)
79+
80+
if adc_range_vpp is not None and gain is not None:
81+
saturation_threshold_uV = (adc_range_vpp / 2) / gain * 1e6
82+
return saturation_threshold_uV
83+
84+
warnings.warn(
85+
"Unable to compute saturation threshold from the Neuropixels probe metadata. "
86+
"The saturation threshold will not be loaded. ",
87+
UserWarning,
88+
stacklevel=2,
89+
)
90+
return None
91+
92+
5693
def synchronize_neuropixel_streams(recording_ref, recording_other):
5794
"""
5895
Use the last "sync" channel from spikeglx or openephys neuropixels to synchronize

0 commit comments

Comments
 (0)