diff --git a/doc/api.rst b/doc/api.rst index 206b21b365..ce850d1291 100755 --- a/doc/api.rst +++ b/doc/api.rst @@ -223,6 +223,7 @@ spikeinterface.preprocessing .. autofunction:: detect_artifact_periods .. autofunction:: detect_artifact_periods_by_envelope .. autofunction:: detect_saturation_periods + .. autofunction:: detect_and_remove_artifacts .. autofunction:: directional_derivative .. autofunction:: filter .. autofunction:: gaussian_filter diff --git a/src/spikeinterface/preprocessing/detect_artifacts.py b/src/spikeinterface/preprocessing/detect_artifacts.py index 49af692996..95f286a85a 100644 --- a/src/spikeinterface/preprocessing/detect_artifacts.py +++ b/src/spikeinterface/preprocessing/detect_artifacts.py @@ -442,14 +442,14 @@ def compute( Cut-off frequency (Hz) for the Gaussian low-pass filter applied to the rectified signal when building the envelope. seed : int | None, default: None - Random seed forwarded to :func:`~spikeinterface.core.get_noise_levels`. - If ``None``, ``get_noise_levels`` uses ``seed=0``. + Random seed forwarded to :func:`~spikeinterface.core.get_random_data_chunks`. job_kwargs : dict | None, default: None Keyword arguments forwarded to :func:`run_node_pipeline` (e.g. ``n_jobs``, ``chunk_duration``). random_slices_kwargs : dict | None, default: None Additional keyword arguments forwarded to the ``random_slices_kwargs`` - argument of :func:`~spikeinterface.core.get_noise_levels`.""" + argument of :func:`~spikeinterface.core.get_random_data_chunks`. Any ``"seed"`` + key in this dictionary is overridden by the ``seed`` argument of this function.""" def detect_artifact_periods_by_envelope( @@ -584,13 +584,15 @@ def _transform_internal_dtype_to_artifact_dtype( sub_thr = artifacts[mask] if len(sub_thr) > 0: if not sub_thr["front"][0]: - local_thr = np.zeros(1, dtype=np.dtype(base_period_dtype + [("front", "bool")])) + local_thr = np.zeros(1, dtype=sub_thr.dtype) local_thr["sample_index"] = 0 + local_thr["segment_index"] = seg_index local_thr["front"] = True sub_thr = np.hstack((local_thr, sub_thr)) if sub_thr["front"][-1]: - local_thr = np.zeros(1, dtype=np.dtype(base_period_dtype + [("front", "bool")])) + local_thr = np.zeros(1, dtype=sub_thr.dtype) local_thr["sample_index"] = recording.get_num_samples(seg_index) + local_thr["segment_index"] = seg_index local_thr["front"] = False sub_thr = np.hstack((sub_thr, local_thr)) @@ -710,9 +712,8 @@ class DetectAndRemoveArtifactsRecording(SilencedPeriodsRecording): Keyword arguments forwarded to :func:`run_node_pipeline` (e.g. ``n_jobs``, ``chunk_duration``). noise_levels_kwargs : dict | None, default: None - Keyword arguments for `spikeinterface.core.get_noise_levels()` function. - - If none, `get_noise_levels` uses `seed=0` and `NoiseGeneratorRecording` generates a random seed using `numpy.random.default_rng`. + Keyword arguments for `spikeinterface.core.get_noise_levels()`, used only + when ``mode="noise"``. mode : "zeros" | "noise" | "apodization", default: "zeros" Determines what periods are replaced by. Can be one of the following: @@ -725,7 +726,9 @@ class DetectAndRemoveArtifactsRecording(SilencedPeriodsRecording): apodization_samples : int, default: 7 The factor used for the cosine taper when mode is "apodization". Higher values create a wider taper. seed : int | None, default: None - Random seed for `get_noise_levels` and `NoiseGeneratorRecording`. + Random seed, if applicable. When ``method="envelope"``, used for chunk + subsampling. When ``mode="noise"``, for the noise used to fill the + silenced periods. artifact_periods : np.ndarray | None, default: None Optionally, pre-computed artifact periods can be passed directly to the constructor to skip the detection step. If ``None``, artifact periods are detected on the fly using the specified method @@ -749,8 +752,14 @@ def __init__( else: if recording_to_detect is None: recording_to_detect = recording + detect_method_kwargs = dict(method_kwargs) if method_kwargs else {} + if method == "envelope": + detect_method_kwargs["seed"] = seed artifact_periods = detect_artifact_periods( - recording_to_detect, method=method, method_kwargs=method_kwargs, job_kwargs=job_kwargs + recording_to_detect, + method=method, + method_kwargs=detect_method_kwargs, + job_kwargs=job_kwargs, ) super().__init__( recording, diff --git a/src/spikeinterface/preprocessing/preprocessing_classes.py b/src/spikeinterface/preprocessing/preprocessing_classes.py index 6ada71de44..406e239380 100644 --- a/src/spikeinterface/preprocessing/preprocessing_classes.py +++ b/src/spikeinterface/preprocessing/preprocessing_classes.py @@ -31,7 +31,10 @@ 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 +from .silence_periods import ( + SilencedPeriodsRecording, + silence_periods, +) from .phase_shift import PhaseShiftRecording, phase_shift from .zero_channel_pad import ZeroChannelPaddedRecording, zero_channel_pad from .deepinterpolation import DeepInterpolatedRecording, deepinterpolate, train_deepinterpolation @@ -50,8 +53,6 @@ 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 - _all_preprocesser_dict = { # filter stuff FilterRecording: filter, @@ -92,7 +93,6 @@ DirectionalDerivativeRecording: directional_derivative, AstypeRecording: astype, UnsignedToSignedRecording: unsigned_to_signed, - # SilencedArtifactsRecording: silence_artifacts, } # we control import in the preprocessing init by setting an __all__ diff --git a/src/spikeinterface/preprocessing/silence_periods.py b/src/spikeinterface/preprocessing/silence_periods.py index 1056488c76..430348bbb3 100644 --- a/src/spikeinterface/preprocessing/silence_periods.py +++ b/src/spikeinterface/preprocessing/silence_periods.py @@ -5,7 +5,6 @@ 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 MockRecording -from spikeinterface.core.job_tools import split_job_kwargs from .basepreprocessor import BasePreprocessor, BasePreprocessorSegment from .normalize_scale import ScaleRecording @@ -41,8 +40,10 @@ class SilencedPeriodsRecording(BasePreprocessor): 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`. + Random seed for the noise fill (only used when ``mode="noise"``): both the + per-channel noise-level estimation (`spikeinterface.core.get_noise_levels`) and + the generated noise. Takes precedence over ``noise_levels_kwargs["random_slices_kwargs"]["seed"]`` + if present. **noise_levels_kwargs : Keyword arguments for `spikeinterface.core.get_noise_levels()` function Returns @@ -249,46 +250,3 @@ def get_traces(self, start_frame, end_frame, channel_indices): silence_periods = define_function_handling_dict_from_class( source_class=SilencedPeriodsRecording, name="silence_periods" ) - - -class DetectArtifactAndSilentPeriodsRecording(SilencedPeriodsRecording): - """ - Class doing artifact detection and lient at the same time. - - See SilencedPeriodsRecording and detect_artifact_periods for details. - """ - - _precomputable_kwarg_names = ["artifacts"] - - def __init__( - self, - recording, - detect_artifact_method="envelope", - detect_artifact_kwargs=dict(), - periods=None, - mode="zeros", - noise_levels=None, - seed=None, - **noise_levels_kwargs, - ): - - if artifacts is None: - from spikeinterface.preprocessing import detect_artifact_periods - - artifacts = detect_artifact_periods( - recording, - method=detect_artifact_method, - method_kwargs=detect_artifact_kwargs, - job_kwargs=None, - ) - - SilencedPeriodsRecording.__init__( - self, recording, periods=artifacts, mode=mode, noise_levels=noise_levels, seed=seed, **noise_levels_kwargs - ) - # note self._kwargs["periods"] is done by SilencedPeriodsRecording and so the computaion is done once - - -# function for API -detect_artifacts_and_silent_periods = define_function_handling_dict_from_class( - source_class=DetectArtifactAndSilentPeriodsRecording, name="silence_artifacts" -) diff --git a/src/spikeinterface/preprocessing/tests/test_pipeline.py b/src/spikeinterface/preprocessing/tests/test_pipeline.py index 6a96d4a66c..37376781b7 100644 --- a/src/spikeinterface/preprocessing/tests/test_pipeline.py +++ b/src/spikeinterface/preprocessing/tests/test_pipeline.py @@ -61,7 +61,16 @@ def test_pipeline_equiv_to_step(): pp_dict = {pp_name: {}} - if pp_name in ["normalize_by_quantile", "center", "zscore", "whiten"]: + if pp_name in [ + "normalize_by_quantile", + "center", + "zscore", + "whiten", + "detect_and_remove_bad_channels", + "detect_and_interpolate_bad_channels", + "detect_and_remove_artifacts", + ]: # These steps aren't deterministic (because they sample random chunks), + # so make sure that both pipeline and class get the same seed. pp_dict[pp_name] = {"seed": 1205} pp_rec_from_class = pp_class(rec, seed=1205) elif pp_name == "blank_saturation":