Repository: SpikeInterface/spikeinterface
Pull request #3773 (2025-03-14 15:52:23)

Allow preprocessing on dicts of recordings (again)
Copy of #3711, before I somehow made a mess of it.

See #3711 for information and discussion.

Edit: read the new docs here: https://spikeinterface--3773.org.readthedocs.build/en/3773/how_to/process_by_channel_group.html#preprocessing-a-recording-by-channel-group


Patch:
diff --git a/.github/workflows/deepinterpolation.yml b/.github/workflows/deepinterpolation.yml
index cdbef0f0e..1b5ff4803 100644
--- a/.github/workflows/deepinterpolation.yml
+++ b/.github/workflows/deepinterpolation.yml
@@ -43,7 +43,12 @@ jobs:
           pip install tensorflow==2.7.0
           pip install deepinterpolation@git+https://github.com/AllenInstitute/deepinterpolation.git
           pip install protobuf==3.20.*
+          pip install numpy==1.26.4
           pip install -e .[full,test_core]
+      - name: Pip list
+        if: ${{ steps.modules-changed.outputs.DEEPINTERPOLATION_CHANGED == 'true' }}
+        run: |
+          pip list
       - name: Test DeepInterpolation with pytest
         if: ${{ steps.modules-changed.outputs.DEEPINTERPOLATION_CHANGED == 'true' }}
         run: |
diff --git a/doc/api.rst b/doc/api.rst
index 21770f209..bc863214a 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -168,7 +168,7 @@ spikeinterface.preprocessing
     .. autofunction:: astype
     .. autofunction:: average_across_direction
     .. autofunction:: bandpass_filter
-    .. autofunction:: blank_staturation
+    .. autofunction:: blank_saturation
     .. autofunction:: center
     .. autofunction:: clip
     .. autofunction:: common_reference
diff --git a/doc/how_to/process_by_channel_group.rst b/doc/how_to/process_by_channel_group.rst
index 08a87ab73..2b9e0e12a 100644
--- a/doc/how_to/process_by_channel_group.rst
+++ b/doc/how_to/process_by_channel_group.rst
@@ -87,40 +87,28 @@ Splitting a recording by channel group returns a dictionary containing separate
 Preprocessing a Recording by Channel Group
 ------------------------------------------
 
-The essence of preprocessing by channel group is to first split the recording
-into separate recordings, perform the preprocessing steps, then aggregate
-the channels back together.
-
-In the below example, we loop over the split recordings, preprocessing each channel group
-individually. At the end, we use the :py:func:`~aggregate_channels` function
-to combine the separate channel group recordings back together.
+If a preprocessing function is given a dictionary of recordings, it will apply the preprocessing
+seperately to each recording in the dict, and return a dictionary of preprocessed recordings.
+Hence we can pass the ``split_recording_dict`` in the same way as we would pass a single recording
+to any preprocessing function.
 
 .. code-block:: python
 
-    preprocessed_recordings = []
-
-   # loop over the recordings contained in the dictionary
-    for chan_group_rec in split_recordings_dict.values():
-
-        # Apply the preprocessing steps to the channel group in isolation
-        shifted_recording = spre.phase_shift(chan_group_rec)
+    shifted_recordings = spre.phase_shift(split_recording_dict)
+    filtered_recording = spre.bandpass_filter(shifted_recording)
+    referenced_recording = spre.common_reference(filtered_recording)
 
-        filtered_recording = spre.bandpass_filter(shifted_recording)
+We can then aggregate the recordings back together using the ``aggregate_channels`` function
 
-        referenced_recording = spre.common_reference(filtered_recording)
-
-        preprocessed_recordings.append(referenced_recording)
+.. code-block:: python
 
-    # Combine our preprocessed channel groups back together
-    combined_preprocessed_recording = aggregate_channels(preprocessed_recordings)
+    combined_preprocessed_recording = aggregate_channels(referenced_recording)
 
-Now, when this recording is used in sorting, plotting, or whenever
+Now, when ``combined_preprocessed_recording`` is used in sorting, plotting, or whenever
 calling its :py:func:`~get_traces` method, the data will have been
 preprocessed separately per-channel group (then concatenated
 back together under the hood).
 
-It is strongly recommended to use the above structure to preprocess by channel group.
-
 .. note::
 
     The splitting and aggregation of channels for preprocessing is flexible.
diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst
index 23919cab2..c87b1d270 100644
--- a/doc/modules/preprocessing.rst
+++ b/doc/modules/preprocessing.rst
@@ -196,18 +196,18 @@ The whitened traces are then the dot product between the traces and the :code:`W
 
 * :py:func:`~spikeinterface.preprocessing.whiten()`
 
-clip() / blank_staturation()
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+clip() / blank_saturation()
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 We can limit traces between a user-defined minimum and maximum using :code:`clip()` function.
-The :code:`blank_staturation()` function is similar, but it automatically estimates the limits by using quantiles.
+The :code:`blank_saturation()` function is similar, but it automatically estimates the limits by using quantiles.
 
 .. code-block:: python
 
     rec_w = clip(recording=rec, a_min=-250., a_max=260)
 
 * :py:func:`~spikeinterface.preprocessing.clip()`
-* :py:func:`~spikeinterface.preprocessing.blank_staturation()`
+* :py:func:`~spikeinterface.preprocessing.blank_saturation()`
 
 
 highpass_spatial_filter()
diff --git a/src/spikeinterface/core/core_tools.py b/src/spikeinterface/core/core_tools.py
index 2ee378e87..5a688e486 100644
--- a/src/spikeinterface/core/core_tools.py
+++ b/src/spikeinterface/core/core_tools.py
@@ -9,10 +9,54 @@ from copy import deepcopy
 import importlib
 from math import prod
 from collections import namedtuple
+import inspect
 
 import numpy as np
 
 
+def define_function_handling_dict_from_class(source_class, name):
+    """
+    Depending on whether `source_class` is passed a `Recording` object or a dict of
+    `Recording` objects, this function will return `source_class` or a dict of
+    `source_class` objects to match the input.
+    """
+
+    from spikeinterface.core import BaseRecording
+
+    def source_class_or_dict_of_sources_classes(*args, **kwargs):
+
+        recording_in_kwargs = False
+        if rec_or_dict_of_recs := kwargs.get("recording"):
+            recording_in_kwargs = True
+        else:
+            rec_or_dict_of_recs = args[0]
+
+        if isinstance(rec_or_dict_of_recs, BaseRecording):
+            return source_class(*args, **kwargs)
+        elif isinstance(rec_or_dict_of_recs, dict):
+            # Edit args & kwargs to pass the dict of recordings but _not_ the original recording
+            new_kwargs = {key: kwarg for key, kwarg in kwargs.items() if key != "recording"}
+            if recording_in_kwargs:
+                new_args = args
+            else:
+                new_args = args[1:]
+
+            preprocessed_recordings_dict = {
+                property_id: source_class(recording, *new_args, **new_kwargs)
+                for property_id, recording in rec_or_dict_of_recs.items()
+            }
+
+            return preprocessed_recordings_dict
+        else:
+            raise TypeError(f"The function `{name}` only accepts a recording or a dict of recordings.")
+
+    source_class_or_dict_of_sources_classes.__signature__ = inspect.signature(source_class)
+    source_class_or_dict_of_sources_classes.__doc__ = source_class.__doc__
+    source_class_or_dict_of_sources_classes.__name__ = name
+
+    return source_class_or_dict_of_sources_classes
+
+
 def define_function_from_class(source_class, name):
     "Wrapper to change the name of a class"
 
diff --git a/src/spikeinterface/preprocessing/astype.py b/src/spikeinterface/preprocessing/astype.py
index 1ac46ed0b..2527993db 100644
--- a/src/spikeinterface/preprocessing/astype.py
+++ b/src/spikeinterface/preprocessing/astype.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 from .filter import fix_dtype
 
@@ -80,4 +80,4 @@ class AstypeRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-astype = define_function_from_class(source_class=AstypeRecording, name="astype")
+astype = define_function_handling_dict_from_class(source_class=AstypeRecording, name="astype")
diff --git a/src/spikeinterface/preprocessing/average_across_direction.py b/src/spikeinterface/preprocessing/average_across_direction.py
index 88c5f7301..ce23cb3f4 100644
--- a/src/spikeinterface/preprocessing/average_across_direction.py
+++ b/src/spikeinterface/preprocessing/average_across_direction.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import numpy as np
 from spikeinterface.core import BaseRecording, BaseRecordingSegment
 from .basepreprocessor import BasePreprocessorSegment
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 
 class AverageAcrossDirectionRecording(BaseRecording):
@@ -139,7 +139,7 @@ class AverageAcrossDirectionRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-average_across_direction = define_function_from_class(
+average_across_direction = define_function_handling_dict_from_class(
     source_class=AverageAcrossDirectionRecording,
     name="average_across_direction",
 )
diff --git a/src/spikeinterface/preprocessing/clip.py b/src/spikeinterface/preprocessing/clip.py
index 4987368aa..6692a4775 100644
--- a/src/spikeinterface/preprocessing/clip.py
+++ b/src/spikeinterface/preprocessing/clip.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 from spikeinterface.core import get_random_data_chunks
@@ -169,5 +169,7 @@ class ClipRecordingSegment(BasePreprocessorSegment):
         return traces
 
 
-clip = define_function_from_class(source_class=ClipRecording, name="clip")
-blank_staturation = define_function_from_class(source_class=BlankSaturationRecording, name="blank_staturation")
+clip = define_function_handling_dict_from_class(source_class=ClipRecording, name="clip")
+blank_saturation = define_function_handling_dict_from_class(
+    source_class=BlankSaturationRecording, name="blank_saturation"
+)
diff --git a/src/spikeinterface/preprocessing/common_reference.py b/src/spikeinterface/preprocessing/common_reference.py
index b087f9b76..3ab93fdf1 100644
--- a/src/spikeinterface/preprocessing/common_reference.py
+++ b/src/spikeinterface/preprocessing/common_reference.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 import numpy as np
 from typing import Optional, Literal
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 from spikeinterface.core import get_closest_channels
@@ -259,4 +259,6 @@ class CommonReferenceRecordingSegment(BasePreprocessorSegment):
         return zip(group_indices, selected_channels, group_channels)
 
 
-common_reference = define_function_from_class(source_class=CommonReferenceRecording, name="common_reference")
+common_reference = define_function_handling_dict_from_class(
+    source_class=CommonReferenceRecording, name="common_reference"
+)
diff --git a/src/spikeinterface/preprocessing/decimate.py b/src/spikeinterface/preprocessing/decimate.py
index 7e05fca61..8a2ec1f83 100644
--- a/src/spikeinterface/preprocessing/decimate.py
+++ b/src/spikeinterface/preprocessing/decimate.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 from spikeinterface.core.core_tools import (
-    define_function_from_class,
+    define_function_handling_dict_from_class,
 )
 
 from .basepreprocessor import BasePreprocessor
@@ -136,4 +136,4 @@ class DecimateRecordingSegment(BaseRecordingSegment):
         ].astype(self._dtype)
 
 
-decimate = define_function_from_class(source_class=DecimateRecording, name="decimate")
+decimate = define_function_handling_dict_from_class(source_class=DecimateRecording, name="decimate")
diff --git a/src/spikeinterface/preprocessing/deepinterpolation/deepinterpolation.py b/src/spikeinterface/preprocessing/deepinterpolation/deepinterpolation.py
index e5a734124..aa3f9a34c 100644
--- a/src/spikeinterface/preprocessing/deepinterpolation/deepinterpolation.py
+++ b/src/spikeinterface/preprocessing/deepinterpolation/deepinterpolation.py
@@ -5,7 +5,7 @@ from typing import Optional
 from packaging.version import parse
 
 from .tf_utils import has_tf, import_tf
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from spikeinterface.preprocessing.basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 
@@ -194,4 +194,6 @@ class DeepInterpolatedRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-deepinterpolate = define_function_from_class(source_class=DeepInterpolatedRecording, name="deepinterpolate")
+deepinterpolate = define_function_handling_dict_from_class(
+    source_class=DeepInterpolatedRecording, name="deepinterpolate"
+)
diff --git a/src/spikeinterface/preprocessing/depth_order.py b/src/spikeinterface/preprocessing/depth_order.py
index e8e8f9781..4a5ec7066 100644
--- a/src/spikeinterface/preprocessing/depth_order.py
+++ b/src/spikeinterface/preprocessing/depth_order.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from spikeinterface.core import order_channels_by_depth, ChannelSliceRecording
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 
 class DepthOrderRecording(ChannelSliceRecording):
@@ -43,4 +43,4 @@ class DepthOrderRecording(ChannelSliceRecording):
         )
 
 
-depth_order = define_function_from_class(source_class=DepthOrderRecording, name="depth_order")
+depth_order = define_function_handling_dict_from_class(source_class=DepthOrderRecording, name="depth_order")
diff --git a/src/spikeinterface/preprocessing/directional_derivative.py b/src/spikeinterface/preprocessing/directional_derivative.py
index 3a6a480f5..c945e4e6d 100644
--- a/src/spikeinterface/preprocessing/directional_derivative.py
+++ b/src/spikeinterface/preprocessing/directional_derivative.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import numpy as np
 from spikeinterface.core import BaseRecording, BaseRecordingSegment
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 
 class DirectionalDerivativeRecording(BasePreprocessor):
@@ -136,6 +136,6 @@ class DirectionalDerivativeRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-directional_derivative = define_function_from_class(
+directional_derivative = define_function_handling_dict_from_class(
     source_class=DirectionalDerivativeRecording, name="directional_derivative"
 )
diff --git a/src/spikeinterface/preprocessing/filter.py b/src/spikeinterface/preprocessing/filter.py
index 3ed2502cf..42b7090c0 100644
--- a/src/spikeinterface/preprocessing/filter.py
+++ b/src/spikeinterface/preprocessing/filter.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 from spikeinterface.core import get_chunk_with_margin
@@ -322,10 +322,10 @@ class NotchFilterRecording(BasePreprocessor):
 
 
 # functions for API
-filter = define_function_from_class(source_class=FilterRecording, name="filter")
-bandpass_filter = define_function_from_class(source_class=BandpassFilterRecording, name="bandpass_filter")
-notch_filter = define_function_from_class(source_class=NotchFilterRecording, name="notch_filter")
-highpass_filter = define_function_from_class(source_class=HighpassFilterRecording, name="highpass_filter")
+filter = define_function_handling_dict_from_class(source_class=FilterRecording, name="filter")
+bandpass_filter = define_function_handling_dict_from_class(source_class=BandpassFilterRecording, name="bandpass_filter")
+notch_filter = define_function_handling_dict_from_class(source_class=NotchFilterRecording, name="notch_filter")
+highpass_filter = define_function_handling_dict_from_class(source_class=HighpassFilterRecording, name="highpass_filter")
 
 
 def causal_filter(
diff --git a/src/spikeinterface/preprocessing/filter_gaussian.py b/src/spikeinterface/preprocessing/filter_gaussian.py
index b16df9be6..b053ef653 100644
--- a/src/spikeinterface/preprocessing/filter_gaussian.py
+++ b/src/spikeinterface/preprocessing/filter_gaussian.py
@@ -5,7 +5,7 @@ from typing import Iterable, Union
 import numpy as np
 
 from spikeinterface.core import BaseRecording, BaseRecordingSegment, get_chunk_with_margin, normal_pdf
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 
@@ -136,4 +136,4 @@ class GaussianFilterRecordingSegment(BasePreprocessorSegment):
         return gaussian
 
 
-gaussian_filter = define_function_from_class(source_class=GaussianFilterRecording, name="gaussian_filter")
+gaussian_filter = define_function_handling_dict_from_class(source_class=GaussianFilterRecording, name="gaussian_filter")
diff --git a/src/spikeinterface/preprocessing/highpass_spatial_filter.py b/src/spikeinterface/preprocessing/highpass_spatial_filter.py
index 735db098c..9228f5de1 100644
--- a/src/spikeinterface/preprocessing/highpass_spatial_filter.py
+++ b/src/spikeinterface/preprocessing/highpass_spatial_filter.py
@@ -5,7 +5,7 @@ import numpy as np
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 from .filter import fix_dtype
 from spikeinterface.core import order_channels_by_depth, get_chunk_with_margin
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 
 class HighpassSpatialFilterRecording(BasePreprocessor):
@@ -245,7 +245,7 @@ class HighPassSpatialFilterSegment(BasePreprocessorSegment):
 
 
 # function for API
-highpass_spatial_filter = define_function_from_class(
+highpass_spatial_filter = define_function_handling_dict_from_class(
     source_class=HighpassSpatialFilterRecording, name="highpass_spatial_filter"
 )
 
diff --git a/src/spikeinterface/preprocessing/interpolate_bad_channels.py b/src/spikeinterface/preprocessing/interpolate_bad_channels.py
index 508868e0b..87bb0f936 100644
--- a/src/spikeinterface/preprocessing/interpolate_bad_channels.py
+++ b/src/spikeinterface/preprocessing/interpolate_bad_channels.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import numpy as np
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from spikeinterface.preprocessing import preprocessing_tools
 
 
@@ -113,6 +113,6 @@ def estimate_recommended_sigma_um(recording):
     return scipy.stats.mode(np.diff(np.unique(y_sorted)), keepdims=False)[0]
 
 
-interpolate_bad_channels = define_function_from_class(
+interpolate_bad_channels = define_function_handling_dict_from_class(
     source_class=InterpolateBadChannelsRecording, name="interpolate_bad_channels"
 )
diff --git a/src/spikeinterface/preprocessing/normalize_scale.py b/src/spikeinterface/preprocessing/normalize_scale.py
index 1adcc2000..62d1ea0cf 100644
--- a/src/spikeinterface/preprocessing/normalize_scale.py
+++ b/src/spikeinterface/preprocessing/normalize_scale.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
@@ -326,9 +326,9 @@ class ZScoreRecording(BasePreprocessor):
 
 
 # functions for API
-normalize_by_quantile = define_function_from_class(
+normalize_by_quantile = define_function_handling_dict_from_class(
     source_class=NormalizeByQuantileRecording, name="normalize_by_quantile"
 )
-scale = define_function_from_class(source_class=ScaleRecording, name="scale")
-center = define_function_from_class(source_class=CenterRecording, name="center")
-zscore = define_function_from_class(source_class=ZScoreRecording, name="zscore")
+scale = define_function_handling_dict_from_class(source_class=ScaleRecording, name="scale")
+center = define_function_handling_dict_from_class(source_class=CenterRecording, name="center")
+zscore = define_function_handling_dict_from_class(source_class=ZScoreRecording, name="zscore")
diff --git a/src/spikeinterface/preprocessing/phase_shift.py b/src/spikeinterface/preprocessing/phase_shift.py
index 0de7e6d6c..872793a30 100644
--- a/src/spikeinterface/preprocessing/phase_shift.py
+++ b/src/spikeinterface/preprocessing/phase_shift.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from spikeinterface.core import get_chunk_with_margin
 
@@ -108,7 +108,7 @@ class PhaseShiftRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-phase_shift = define_function_from_class(source_class=PhaseShiftRecording, name="phase_shift")
+phase_shift = define_function_handling_dict_from_class(source_class=PhaseShiftRecording, name="phase_shift")
 
 
 def apply_frequency_shift(signal, shift_samples, axis=0):
diff --git a/src/spikeinterface/preprocessing/preprocessinglist.py b/src/spikeinterface/preprocessing/preprocessinglist.py
index bdf5f2219..37727cef6 100644
--- a/src/spikeinterface/preprocessing/preprocessinglist.py
+++ b/src/spikeinterface/preprocessing/preprocessinglist.py
@@ -29,7 +29,7 @@ from .scale import scale_to_uV
 
 from .whiten import WhitenRecording, whiten, compute_whitening_matrix
 from .rectify import RectifyRecording, rectify
-from .clip import BlankSaturationRecording, blank_staturation, ClipRecording, clip
+from .clip import BlankSaturationRecording, blank_saturation, ClipRecording, clip
 from .common_reference import CommonReferenceRecording, common_reference
 from .remove_artifacts import RemoveArtifactsRecording, remove_artifacts
 from .silence_periods import SilencedPeriodsRecording, silence_periods
diff --git a/src/spikeinterface/preprocessing/rectify.py b/src/spikeinterface/preprocessing/rectify.py
index aea866452..96d68dda9 100644
--- a/src/spikeinterface/preprocessing/rectify.py
+++ b/src/spikeinterface/preprocessing/rectify.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
@@ -27,4 +27,4 @@ class RectifyRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-rectify = define_function_from_class(source_class=RectifyRecording, name="rectify")
+rectify = define_function_handling_dict_from_class(source_class=RectifyRecording, name="rectify")
diff --git a/src/spikeinterface/preprocessing/remove_artifacts.py b/src/spikeinterface/preprocessing/remove_artifacts.py
index 2dbf034b6..034d827c8 100644
--- a/src/spikeinterface/preprocessing/remove_artifacts.py
+++ b/src/spikeinterface/preprocessing/remove_artifacts.py
@@ -4,7 +4,7 @@ import warnings
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 from spikeinterface.core import NumpySorting, estimate_templates
@@ -446,4 +446,6 @@ class RemoveArtifactsRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-remove_artifacts = define_function_from_class(source_class=RemoveArtifactsRecording, name="remove_artifacts")
+remove_artifacts = define_function_handling_dict_from_class(
+    source_class=RemoveArtifactsRecording, name="remove_artifacts"
+)
diff --git a/src/spikeinterface/preprocessing/resample.py b/src/spikeinterface/preprocessing/resample.py
index f9a2f1472..0fbf8e54e 100644
--- a/src/spikeinterface/preprocessing/resample.py
+++ b/src/spikeinterface/preprocessing/resample.py
@@ -4,7 +4,7 @@ import numpy as np
 import warnings
 
 from spikeinterface.core.core_tools import (
-    define_function_from_class,
+    define_function_handling_dict_from_class,
     recursive_key_finder,
 )
 
@@ -154,7 +154,7 @@ class ResampleRecordingSegment(BaseRecordingSegment):
         return resampled_traces.astype(self._dtype)
 
 
-resample = define_function_from_class(source_class=ResampleRecording, name="resample")
+resample = define_function_handling_dict_from_class(source_class=ResampleRecording, name="resample")
 
 
 # Some helpers to do checks
diff --git a/src/spikeinterface/preprocessing/silence_periods.py b/src/spikeinterface/preprocessing/silence_periods.py
index c5a772b9f..db4533b65 100644
--- a/src/spikeinterface/preprocessing/silence_periods.py
+++ b/src/spikeinterface/preprocessing/silence_periods.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 from spikeinterface.core import get_random_data_chunks, get_noise_levels
@@ -137,4 +137,6 @@ class SilencedPeriodsRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-silence_periods = define_function_from_class(source_class=SilencedPeriodsRecording, name="silence_periods")
+silence_periods = define_function_handling_dict_from_class(
+    source_class=SilencedPeriodsRecording, name="silence_periods"
+)
diff --git a/src/spikeinterface/preprocessing/unsigned_to_signed.py b/src/spikeinterface/preprocessing/unsigned_to_signed.py
index 7f5774c17..fdb5a663b 100644
--- a/src/spikeinterface/preprocessing/unsigned_to_signed.py
+++ b/src/spikeinterface/preprocessing/unsigned_to_signed.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import numpy as np
 
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
 
 
@@ -66,4 +66,6 @@ class UnsignedToSignedRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-unsigned_to_signed = define_function_from_class(source_class=UnsignedToSignedRecording, name="unsigned_to_signed")
+unsigned_to_signed = define_function_handling_dict_from_class(
+    source_class=UnsignedToSignedRecording, name="unsigned_to_signed"
+)
diff --git a/src/spikeinterface/preprocessing/whiten.py b/src/spikeinterface/preprocessing/whiten.py
index 41abd98ba..7cb16e82b 100644
--- a/src/spikeinterface/preprocessing/whiten.py
+++ b/src/spikeinterface/preprocessing/whiten.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import numpy as np
 
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 from spikeinterface.core import get_random_data_chunks, get_channel_distances
 from .filter import fix_dtype
@@ -285,4 +285,4 @@ def compute_sklearn_covariance_matrix(data, regularize_kwargs):
 
 
 # function for API
-whiten = define_function_from_class(source_class=WhitenRecording, name="whiten")
+whiten = define_function_handling_dict_from_class(source_class=WhitenRecording, name="whiten")
diff --git a/src/spikeinterface/preprocessing/zero_channel_pad.py b/src/spikeinterface/preprocessing/zero_channel_pad.py
index ab1c90dfd..c06baf525 100644
--- a/src/spikeinterface/preprocessing/zero_channel_pad.py
+++ b/src/spikeinterface/preprocessing/zero_channel_pad.py
@@ -6,7 +6,7 @@ import numpy as np
 
 from spikeinterface.core import BaseRecording, BaseRecordingSegment
 from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment
-from spikeinterface.core.core_tools import define_function_from_class
+from spikeinterface.core.core_tools import define_function_handling_dict_from_class
 
 
 class TracePaddedRecording(BasePreprocessor):
@@ -201,5 +201,7 @@ class ZeroChannelPaddedRecordingSegment(BasePreprocessorSegment):
 
 
 # function for API
-zero_channel_pad = define_function_from_class(source_class=ZeroChannelPaddedRecording, name="zero_channel_pad")
-pad_traces = define_function_from_class(source_class=TracePaddedRecording, name="pad_traces")
+zero_channel_pad = define_function_handling_dict_from_class(
+    source_class=ZeroChannelPaddedRecording, name="zero_channel_pad"
+)
+pad_traces = define_function_handling_dict_from_class(source_class=TracePaddedRecording, name="pad_traces")


Test patch:
diff --git a/src/spikeinterface/preprocessing/tests/test_clip.py b/src/spikeinterface/preprocessing/tests/test_clip.py
index c18c7d37a..cea15722a 100644
--- a/src/spikeinterface/preprocessing/tests/test_clip.py
+++ b/src/spikeinterface/preprocessing/tests/test_clip.py
@@ -1,6 +1,6 @@
 from spikeinterface.core import generate_recording
 
-from spikeinterface.preprocessing import clip, blank_staturation
+from spikeinterface.preprocessing import clip, blank_saturation
 
 import numpy as np
 
@@ -25,13 +25,13 @@ def test_clip():
     assert np.all(-1.5 <= traces1[1])
 
 
-def test_blank_staturation():
+def test_blank_saturation():
     rec = generate_recording()
 
-    rec0 = blank_staturation(rec, abs_threshold=3.0)
+    rec0 = blank_saturation(rec, abs_threshold=3.0)
     rec0.save(verbose=False)
 
-    rec1 = blank_staturation(rec, quantile_threshold=0.01, direction="both", chunk_size=10000)
+    rec1 = blank_saturation(rec, quantile_threshold=0.01, direction="both", chunk_size=10000)
     rec1.save(verbose=False)
 
     traces0 = rec0.get_traces(segment_index=0, channel_ids=["1"])
@@ -47,4 +47,4 @@ def test_blank_staturation():
 
 if __name__ == "__main__":
     test_clip()
-    test_blank_staturation()
+    test_blank_saturation()
diff --git a/src/spikeinterface/preprocessing/tests/test_grouped_preprocessing.py b/src/spikeinterface/preprocessing/tests/test_grouped_preprocessing.py
new file mode 100644
index 000000000..c2cdbeb3d
--- /dev/null
+++ b/src/spikeinterface/preprocessing/tests/test_grouped_preprocessing.py
@@ -0,0 +1,62 @@
+from spikeinterface.core import generate_recording
+from spikeinterface.preprocessing import common_reference, silence_periods
+from spikeinterface.core.testing import check_recordings_equal
+
+
+def test_grouped_preprocessing():
+    """Here we make a dict of two recordings and apply preprocessing steps directly
+    to the dict. This should give the same result as applying preprocessing steps to
+    each recording separately.
+    The arg/kwarg logic in `source_class_or_dict_of_sources_classes` is non-trivial,
+    so we test some arg/kwarg possibilities here.
+    """
+
+    seed = 1205
+
+    recording_1 = generate_recording(seed=seed, durations=[5])
+    recording_2 = generate_recording(seed=seed, durations=[6])
+
+    dict_of_recordings = {"one": recording_1, "two": recording_2}
+
+    # First use dict_of_recordings as an arg
+    operator = "average"
+    dict_of_preprocessed_recordings = common_reference(dict_of_recordings, operator=operator)
+
+    pp_recording_1 = common_reference(recording_1, operator=operator)
+    pp_recording_2 = common_reference(recording_2, operator=operator)
+
+    check_recordings_equal(dict_of_preprocessed_recordings["one"], pp_recording_1)
+    check_recordings_equal(dict_of_preprocessed_recordings["two"], pp_recording_2)
+
+    # Re-try using recording as a kwarg
+    dict_of_preprocessed_recordings = common_reference(recording=dict_of_recordings, operator=operator)
+    check_recordings_equal(dict_of_preprocessed_recordings["one"], pp_recording_1)
+    check_recordings_equal(dict_of_preprocessed_recordings["two"], pp_recording_2)
+
+    # Now try a `silence periods` which has two args
+    list_periods = [[1, 10]]
+    mode = "noise"
+
+    sp_recording_1 = silence_periods(recording_1, list_periods=list_periods, mode=mode, seed=seed)
+    sp_recording_2 = silence_periods(recording_2, list_periods=list_periods, mode=mode, seed=seed)
+
+    dict_of_silence_period_recordings = silence_periods(dict_of_recordings, list_periods, mode=mode, seed=seed)
+    check_recordings_equal(dict_of_silence_period_recordings["one"], sp_recording_1)
+    check_recordings_equal(dict_of_silence_period_recordings["two"], sp_recording_2)
+
+    dict_of_preprocessed_recordings = silence_periods(
+        dict_of_recordings, list_periods=list_periods, mode=mode, seed=seed
+    )
+    check_recordings_equal(dict_of_silence_period_recordings["one"], sp_recording_1)
+    check_recordings_equal(dict_of_silence_period_recordings["two"], sp_recording_2)
+
+    # Now pass dict_of_recordings as a kwarg
+    dict_of_silence_period_recordings = silence_periods(
+        recording=dict_of_recordings, list_periods=list_periods, mode=mode, seed=seed
+    )
+    check_recordings_equal(dict_of_silence_period_recordings["one"], sp_recording_1)
+    check_recordings_equal(dict_of_silence_period_recordings["two"], sp_recording_2)
+
+
+if __name__ == "__main__":
+    test_grouped_preprocessing()