Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3dc5729
Test IBL extractors tests failing for PI update
alejoe91 Dec 29, 2025
d1a0532
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 6, 2026
33c6769
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 16, 2026
2c94bac
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 20, 2026
a412bd8
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 2, 2026
504e19d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 12, 2026
cd09c19
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 19, 2026
a40d073
Merge branch 'main' of github.com:alejoe91/spikeinterface
alejoe91 Feb 24, 2026
a1da327
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 2, 2026
ef19a8e
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 3, 2026
a098b51
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 6, 2026
61c317a
Fix OpenEphys tests
alejoe91 Mar 6, 2026
c9ff247
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 9, 2026
3520138
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 16, 2026
f61329d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 16, 2026
d64ae6a
Merge branch 'main' of github.com:alejoe91/spikeinterface
alejoe91 Mar 16, 2026
aef197d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 17, 2026
e82331b
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 20, 2026
710cb6f
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 23, 2026
c2f8db1
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 23, 2026
161d25b
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 27, 2026
1d09ec6
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
afb7d33
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
fa556ba
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
8e68f16
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Apr 14, 2026
1c80910
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Apr 14, 2026
5eff246
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Apr 17, 2026
b6ee0e8
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Apr 20, 2026
49c51da
feat: implement DetectAndRemoveArtifacts and signed saturation
alejoe91 Apr 20, 2026
da6b401
Merge branch 'main' into saturation-preprocessor
alejoe91 Apr 20, 2026
99afff6
Apply suggestion from @alejoe91
alejoe91 Apr 20, 2026
9c25668
saturation application with apodization
oliche Apr 22, 2026
a689744
Merge pull request #30 from int-brain-lab/olive-saturation
alejoe91 Apr 23, 2026
1636817
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 23, 2026
230507f
Add docstrings for apodization and tests
alejoe91 Apr 23, 2026
1586fa5
conflicts
alejoe91 Apr 23, 2026
cbacc4c
fix: apodization_factor -> apodization_samples and move scipy import
alejoe91 Apr 24, 2026
cfefa2f
Merge branch 'main' into saturation-preprocessor
alejoe91 Apr 24, 2026
9db9cad
feat: add pipeline substitution logic
alejoe91 May 4, 2026
f3b581b
Merge branch 'main' into saturation-preprocessor
alejoe91 May 5, 2026
9b9e87c
Add margin and tests for silence with aopdization
alejoe91 May 5, 2026
361cca6
add test on dump/load and make silence_periods recording not JSON ser…
alejoe91 May 5, 2026
676d09f
allow channel_filters to be passed as a list (JSON-serializable)
alejoe91 May 5, 2026
d0c4e04
add test and autmatixally load saturation levels from probeinterface
alejoe91 May 6, 2026
bb764c9
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
alejoe91 May 6, 2026
5e77552
fix: conflicts
alejoe91 May 14, 2026
005493d
centralize np saturation computation
alejoe91 May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions doc/modules/preprocessing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,37 @@ can also be obtained from the pipeline object directly:
dict_used_to_make_pipeline = preprocessing_pipeline.preprocessor_dict


Some preprocessing steps, such as :code:`detect_and_remove_artifacts`, allow you to specify an input recording
and optionally another recording to perform some computation (e.g., detect artifacts on the output of a previous
preprocessor, but remove them on the the parent preprocessor). In this case, the string "pipeline[preprocessor_name]"
can be used in the dictionary to specify that the recording argument for this step should be the output of a previous
preprocessor in the same pipeline. For example, if we want to use the output of the "bandpass_filter" step as the
recording to detect artifacts, we can specify it as follows:

.. code-block:: python

preprocessing_dict = {
'bandpass_filter': {'freq_min': 250},
'common_reference': {'operator': 'median', 'reference': 'global'},
'detect_and_remove_artifacts': {'recording_to_detect': 'pipeline[bandpass_filter]'},
}

This will detect artifacts on the output of the "bandpass_filter" step, but the artifacts will be removed on the output
of the "common_reference" step (since the parent recording for "detect_and_remove_artifacts" is by default the output of
the previous step in the pipeline, which is "common_reference" in this case).
To specify the "raw" recording, i.e., the input to the pipeline, we can use "pipeline[raw]".
For example, if we want to detect artifacts on the raw recording, we can specify it as follows:


.. code-block:: python

preprocessing_dict = {
'bandpass_filter': {'freq_min': 250},
'common_reference': {'operator': 'median', 'reference': 'global'},
'detect_and_remove_artifacts': {'recording_to_detect': 'pipeline[raw]'},
}


Impact on recording dtype
-------------------------

Expand Down
356 changes: 270 additions & 86 deletions src/spikeinterface/preprocessing/detect_artifacts.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/spikeinterface/preprocessing/detect_bad_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ def detect_bad_channels(
if channel_filters is None:
channel_filters = allowed_filters

if isinstance(channel_filters, list):
channel_filters = set(channel_filters)

if not isinstance(channel_filters, set):
raise ValueError(f"channel_filters must be None or a set of the following values : {allowed_filters} ")

Expand Down
31 changes: 28 additions & 3 deletions src/spikeinterface/preprocessing/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,29 @@ def _apply(self, recording, apply_precomputed_kwargs=False):
Preprocessed recording

"""

for preprocessor_name, kwargs in self.preprocessor_dict.items():

instantiated_recordings = {"raw": recording}
for preprocessor_name, kwargs_ in self.preprocessor_dict.items():
kwargs = kwargs_.copy()
dont_apply_kwargs = ["recording", "parent_recording"]

for k, v in kwargs.items():
if isinstance(v, str) and "pipeline[" in v:
if "recording" not in k:
raise ValueError(
f"Cannot substitute recording for argument '{k}' of preprocessor '{preprocessor_name}' "
f"because this argument is not meant to be a recording object."
)
if k in dont_apply_kwargs:
raise ValueError(
f"Cannot substitute recording for argument '{k}' of preprocessor '{preprocessor_name}' "
f"because this argument is reserved for the recording to be preprocessed."
)
rec_name = v.split("pipeline[")[-1].split("]")[0]
substituted_recording = instantiated_recordings.get(rec_name)
if substituted_recording is None:
raise ValueError(f"Cannot find recording '{rec_name}' from previous steps in the pipeline.")
kwargs[k] = substituted_recording

if not apply_precomputed_kwargs:
preprocessor_class = pp_names_to_classes[preprocessor_name]
precomputable_kwarg_names = preprocessor_class._precomputable_kwarg_names
Expand All @@ -112,6 +130,7 @@ def _apply(self, recording, apply_precomputed_kwargs=False):
non_rec_kwargs = {key: value for key, value in kwargs.items() if key not in dont_apply_kwargs}
pp_output = pp_names_to_functions[preprocessor_name](recording, **non_rec_kwargs)
recording = pp_output
instantiated_recordings[preprocessor_name] = recording

return recording

Expand Down Expand Up @@ -305,6 +324,12 @@ def _load_pp_from_dict(prov_dict, kwargs_dict):
for name, value in prov_dict["kwargs"].items():
if is_dict_extractor(value):
this_level_kwargs[name] = _load_pp_from_dict(value, kwargs_dict)
elif isinstance(value, BaseRecording):
extractor_as_dict = value.to_dict()
if name in ["recording", "parent_recording"]:
this_level_kwargs[name] = _load_pp_from_dict(extractor_as_dict, kwargs_dict)
else: # this branch takes care of other arguments being a recording, e.g., `recording_to_detect`
this_level_kwargs[name] = value
elif isinstance(value, dict):
this_level_kwargs[name] = {k: prov_dict_to_kwargs_dict(v) for k, v in value.items()}
elif isinstance(value, list):
Expand Down
3 changes: 3 additions & 0 deletions src/spikeinterface/preprocessing/preprocessing_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from .depth_order import DepthOrderRecording, depth_order
from .astype import AstypeRecording, astype
from .unsigned_to_signed import UnsignedToSignedRecording, unsigned_to_signed
from .detect_artifacts import DetectAndRemoveArtifactsRecording, detect_and_remove_artifacts

# from .silence_artifacts import SilencedArtifactsRecording, silence_artifacts

Expand All @@ -72,6 +73,8 @@
# bad channel detection/interpolation
DetectAndRemoveBadChannelsRecording: detect_and_remove_bad_channels,
DetectAndInterpolateBadChannelsRecording: detect_and_interpolate_bad_channels,
# artifact/saturation handling
DetectAndRemoveArtifactsRecording: detect_and_remove_artifacts,
# misc
RectifyRecording: rectify,
ClipRecording: clip,
Expand Down
105 changes: 80 additions & 25 deletions src/spikeinterface/preprocessing/silence_periods.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import numpy as np

from spikeinterface.core.core_tools import define_function_handling_dict_from_class
from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment

from spikeinterface.core import get_noise_levels
from spikeinterface.core.base import base_period_dtype
from spikeinterface.core.core_tools import define_function_handling_dict_from_class
from spikeinterface.core.recording_tools import get_noise_levels, get_chunk_with_margin
from spikeinterface.core.generate import NoiseGeneratorRecording
from spikeinterface.core.job_tools import split_job_kwargs
from spikeinterface.core.base import base_period_dtype

from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment


class SilencedPeriodsRecording(BasePreprocessor):
Expand All @@ -21,21 +22,26 @@ class SilencedPeriodsRecording(BasePreprocessor):
----------
recording : RecordingExtractor
The recording extractor to silance periods
list_periods : list of lists/arrays
One list per segment of tuples (start_frame, end_frame) to silence
noise_levels : array
Noise levels if already computed
seed : int | None, default: None
Random seed for `get_noise_levels` and `NoiseGeneratorRecording`.
If none, `get_noise_levels` uses `seed=0` and `NoiseGeneratorRecording` generates a random seed using `numpy.random.default_rng`.
mode : "zeros" | "noise, default: "zeros"
periods : np.array
A numpy array with dtype `base_period_dtype` and fields
"segment_index", "start_sample_index", "end_sample_index".
Each row corresponds to a period to silence.
mode : "zeros" | "noise" | "apodization", default: "zeros"
Determines what periods are replaced by. Can be one of the following:

- "zeros": Artifacts are replaced by zeros.

- "noise": The periods are filled with a gaussion noise that has the
same variance that the one in the recordings, on a per channel
basis
- "apodization": The periods zeroed, but are apodized with a cosine taper (using `apodization_samples`)
apodization_samples : int, default: 7
The factor used for the cosine taper when mode is "apodization". Higher values create a wider taper.
noise_levels : array
Noise levels if already computed
seed : int | None, default: None
Random seed for `get_noise_levels` and `NoiseGeneratorRecording`.
If none, `get_noise_levels` uses `seed=0` and `NoiseGeneratorRecording` generates a random seed using `numpy.random.default_rng`.
**noise_levels_kwargs : Keyword arguments for `spikeinterface.core.get_noise_levels()` function

Returns
Expand All @@ -48,22 +54,29 @@ def __init__(
self,
recording,
periods=None,
# this is keep for backward compatibility
# this is kept for backward compatibility
list_periods=None,
mode="zeros",
apodization_samples=7,
noise_levels=None,
seed=None,
**noise_levels_kwargs,
):
available_modes = ("zeros", "noise")
available_modes = ("zeros", "noise", "apodization")
num_seg = recording.get_num_segments()

# handle backward compatibility with previous version
if list_periods is not None:
assert periods is None
assert periods is None, (
"You cannot specify both list_periods and periods. "
f"Please specify only periods, which should be a np.array with dtype {base_period_dtype}"
)
periods = _all_period_list_to_periods_vec(list_periods, num_seg)
else:
assert list_periods is None
assert list_periods is None, (
"list_periods is deprecated. Please specify periods, which should be a np.array with "
f"dtype {base_period_dtype}"
)
if not isinstance(periods, np.ndarray):
raise ValueError(f"periods must be a np.array with dtype {base_period_dtype}")

Expand Down Expand Up @@ -108,11 +121,26 @@ def __init__(
i1 = seg_limits[seg_index + 1]
periods_in_seg = periods[i0:i1]
rec_segment = SilencedPeriodsRecordingSegment(
parent_segment, periods_in_seg, mode, noise_generator, seg_index
parent_segment,
periods_in_seg,
mode,
noise_generator,
seg_index,
apodization_samples=apodization_samples,
)
self.add_recording_segment(rec_segment)

self._kwargs = dict(recording=recording, periods=periods, mode=mode, seed=seed, noise_levels=noise_levels)
# the base_period_dtype is a structured dtype, which is not json serializable
self._serializability["json"] = False

self._kwargs = dict(
recording=recording,
periods=periods,
mode=mode,
seed=seed,
noise_levels=noise_levels,
apodization_samples=apodization_samples,
)


def _all_period_list_to_periods_vec(list_periods, num_seg):
Expand Down Expand Up @@ -154,18 +182,28 @@ def _check_periods(periods, num_seg):


class SilencedPeriodsRecordingSegment(BasePreprocessorSegment):
def __init__(self, parent_recording_segment, periods, mode, noise_generator, seg_index):
def __init__(self, parent_recording_segment, periods, mode, noise_generator, seg_index, apodization_samples=7):
BasePreprocessorSegment.__init__(self, parent_recording_segment)
self.periods = periods
self.mode = mode
self.seg_index = seg_index
self.noise_generator = noise_generator
self.apodization_samples = apodization_samples

def get_traces(self, start_frame, end_frame, channel_indices):
traces = self.parent_recording_segment.get_traces(start_frame, end_frame, channel_indices)
if self.mode in ("zeros", "noise"):
margin = 0
elif self.mode == "apodization":
margin = self.apodization_samples
else:
raise ValueError(f"Unknown method {self.mode}")

traces, left_margin, right_margin = get_chunk_with_margin(
self.parent_recording_segment, start_frame, end_frame, channel_indices, margin=margin
)

if self.periods.size > 0:
new_interval = np.array([start_frame, end_frame])
new_interval = np.array([start_frame - margin, end_frame + margin])

lower_index = np.searchsorted(self.periods["end_sample_index"], new_interval[0])
upper_index = np.searchsorted(self.periods["start_sample_index"], new_interval[1])
Expand All @@ -174,9 +212,14 @@ def get_traces(self, start_frame, end_frame, channel_indices):
traces = traces.copy()

periods_in_interval = self.periods[lower_index:upper_index]

# For apodization, we pre-allocate the mute function and cosine window
if self.mode == "apodization":
mute_mask = np.zeros(traces.shape[0], dtype=np.float32)

for period in periods_in_interval:
onset = max(0, period["start_sample_index"] - start_frame)
offset = min(period["end_sample_index"] - start_frame, end_frame)
onset = max(0, period["start_sample_index"] - start_frame - margin)
offset = min(period["end_sample_index"] - start_frame + margin, end_frame + margin)

if self.mode == "zeros":
traces[onset:offset, :] = 0
Expand All @@ -185,8 +228,20 @@ def get_traces(self, start_frame, end_frame, channel_indices):
:, channel_indices
]
traces[onset:offset, :] = noise[onset:offset]

return traces
elif self.mode == "apodization":
# apply a cosine taper to the saturation to create a mute function
mute_mask[onset:offset] = 1

# For apodization, we apply the mute function including all periods to the whole trace,
# so that the edges of the silenced periods are smoothly tapered
if self.mode == "apodization":
import scipy.signal

win = scipy.signal.windows.cosine(self.apodization_samples)
mute = np.maximum(0, 1 - scipy.signal.convolve(mute_mask, win, mode="same"))
traces = (traces.astype(np.float32) * mute[:, np.newaxis]).astype(traces.dtype)
# discard margin
return traces[left_margin : traces.shape[0] - right_margin, :]


# function for API
Expand Down
Loading
Loading