Repository: SpikeInterface/spikeinterface
Pull request #3504 (2024-10-24 13:22:59)

Add custom tutorials index page.
This PR introduces a potential new way to index sphinx-gallery generated pages rather than using the default sphinx-gallery pages. This follows from discussions such as  #2327 #2800, where it was discussed that having more flexibility to integrate sphinx-generated tutorials and non-sphinx generated `.rst` files in the same index page would be useful. At present, what is a `tutorials` vs. `how to` is arbitrarily fixed by how the documentation is built.

The approach taken here is to
- Add a `custom_tutorial_index.rst` that replicates the current `tutorials/index.rst` page generated by sphinx (both are kept on the docs for now, for comparison).
- This required adding some custom `.css` to replicate some of the sphinx behaviour.

The documentation is available [here](https://spikeinterface--3504.org.readthedocs.build/en/3504/).

**Some benefits of this approach:**
- Flexibility mix both sphinx-generated files and non-sphinx generated `.rst` files in the same index page.
- Easier to reorder the gallery cards, as well as select any desired thumbnail image.
- Can be extended to any index page for consistency across the documentation. For example, we might like such a presentation for the 'Getting Started' page or organise the How To's in the same way.
- Can add custom subtitles that are displayed on hover. I couldn't replicate exactly the sphinx mouse-hover behaviour, which shows the first line of the generated documentation file. In practice this leads to the preview obtained on hover to be kind of irrelevant / unhelpful. Here we can customise the text shown on hover to ensure it is informative.

**Some drawbacks:**
- This is more maintenance in the following ways:
    - We now have a custom `.css` file. This should never need to be changed, but nonetheless it is some added complexity in the docs codebase.
    - It is now necessary to manually add any new tutorials pages to the index, and the process is a little longer than normal (need to point to sphinx gallery file and image). This should however only need to be done once for any new pages. Instructions are at the top of the new index page.
 - I couldn't perfectly replicate the sizing and mouse-hover behaviour of the sphinx gallery. Personally I prefer the bigger cards and targeted subtitles, but this is personal taste so this is a potential downside. I think nonetheless it I should be possible to further adjust the card size using css if desired.

EDITS:
- css / preview removed from final version
- developer documentation added.

JoeZiminski: Thanks @zm711, looking forward to hearing your thoughts!
zm711: <img width="735" alt="Screenshot 2024-10-25 at 8 43 00 AM" src="https://github.com/user-attachments/assets/f4612b8f-fe27-4778-8108-9ee2a1b58411">

Any way to fix this?
JoeZiminski: Hey @zm711, it might be through the sphinx machinery but I am not sure. This is an issue with the current live docs [i.e. here](https://github.com/SpikeInterface/spikeinterface/pull/3499#issuecomment-2434604284) but does not appear for the proposed custom index. I guess another benefit of the flexibility is we can directly troubleshoot such issues rather than having to work around the sphinx-gallery rendering.

Patch:
diff --git a/doc/development/development.rst b/doc/development/development.rst
index a91818a27..1638c4124 100644
--- a/doc/development/development.rst
+++ b/doc/development/development.rst
@@ -213,6 +213,25 @@ We use Sphinx to build the documentation. To build the documentation locally, yo
 
 This will build the documentation in the :code:`doc/_build/html` folder. You can open the :code:`index.html` file in your browser to see the documentation.
 
+Adding new documentation
+------------------------
+
+Documentation can be added as a
+`sphinx-gallery <https://sphinx-gallery.github.io/stable/index.html>`_
+python file ('tutorials')
+or a
+`sphinx rst <https://sphinx-tutorial.readthedocs.io/step-1/>`_
+file (all other sections).
+
+To add a new tutorial, add your ``.py`` file to ``spikeinterface/examples``.
+Then, update the ``spikeinterface/doc/tutorials_custom_index.rst`` file
+to make a new card linking to the page and an optional image. See
+``tutorials_custom_index.rst`` header for more information.
+
+For other sections, write your documentation in ``.rst`` format and add
+the page to the appropriate ``index.rst`` file found in the relevant
+folder (e.g. ``how_to/index.rst``).
+
 How to run code coverage locally
 --------------------------------
 To run code coverage locally, you can use the following command:
diff --git a/doc/index.rst b/doc/index.rst
index ed443e420..e6d8aa3fe 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -51,7 +51,7 @@ SpikeInterface is made of several modules to deal with different aspects of the
 
     overview
     get_started/index
-    tutorials/index
+    tutorials_custom_index
     how_to/index
     modules/index
     api
diff --git a/doc/tutorials_custom_index.rst b/doc/tutorials_custom_index.rst
new file mode 100644
index 000000000..4c7625d81
--- /dev/null
+++ b/doc/tutorials_custom_index.rst
@@ -0,0 +1,196 @@
+.. This page provides a custom index to the 'Tutorials' page, rather than the default sphinx-gallery
+.. generated page. The benefits of this are flexibility in design and inclusion of non-sphinx files in the index.
+..
+.. To update this index with a new documentation page
+.. 1) Copy the grid-item-card and associated ".. raw:: html" section.
+.. 2) change :link: to a link to your page. If this is an `.rst` file, point to the rst file directly.
+..    If it is a sphinx-gallery generated file, format the path as separated by underscore and prefix `sphx_glr`,
+..    pointing to the .py file. e.g. `tutorials/my/page.py` -> `sphx_glr_tutorials_my_page.py
+.. 3) Change :img-top: to point to the thumbnail image of your choosing. You can point to images generated
+..    in the sphinx gallery page if you wish.
+.. 4) In the `html` section, change the `default-title` to your pages title and `hover-content` to the subtitle.
+
+:orphan:
+
+Tutorials
+============
+
+Longer form tutorials about using SpikeInterface. Many of these are downloadable
+as notebooks or Python scripts so that you can "code along" with the tutorials.
+
+If you're new to SpikeInterface, we recommend trying out the
+:ref:`get_started/quickstart:Quickstart tutorial` first.
+
+Updating from legacy
+--------------------
+
+.. toctree::
+   :maxdepth: 1
+
+   tutorials/waveform_extractor_to_sorting_analyzer
+
+Core tutorials
+--------------
+
+These tutorials focus on the :py:mod:`spikeinterface.core` module.
+
+.. grid:: 1 2 2 3
+   :gutter: 2
+
+   .. grid-item-card:: Recording objects
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_1_recording_extractor.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_1_recording_extractor_thumb.png
+      :img-alt: Recording objects
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Sorting objects
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_2_sorting_extractor.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_2_sorting_extractor_thumb.png
+      :img-alt: Sorting objects
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Handling probe information
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_3_handle_probe_info.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_3_handle_probe_info_thumb.png
+      :img-alt: Handling probe information
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: SortingAnalyzer
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_4_sorting_analyzer.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_4_sorting_analyzer_thumb.png
+      :img-alt: SortingAnalyzer
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Append and/or concatenate segments
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_5_append_concatenate_segments.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_5_append_concatenate_segments_thumb.png
+      :img-alt: Append/Concatenate segments
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Handle time information
+      :link-type: ref
+      :link: sphx_glr_tutorials_core_plot_6_handle_times.py
+      :img-top: /tutorials/core/images/thumb/sphx_glr_plot_6_handle_times_thumb.png
+      :img-alt: Handle time information
+      :class-card: gallery-card
+      :text-align: center
+
+Extractors tutorials
+--------------------
+
+The :py:mod:`spikeinterface.extractors` module is designed to load and save recorded and sorted data, and to handle probe information.
+
+.. grid:: 1 2 2 3
+   :gutter: 2
+
+   .. grid-item-card:: Read various formats
+      :link-type: ref
+      :link: sphx_glr_tutorials_extractors_plot_1_read_various_formats.py
+      :img-top: /tutorials/extractors/images/thumb/sphx_glr_plot_1_read_various_formats_thumb.png
+      :img-alt: Read various formats
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Working with unscaled traces
+      :link-type: ref
+      :link: sphx_glr_tutorials_extractors_plot_2_working_with_unscaled_traces.py
+      :img-top: /tutorials/extractors/images/thumb/sphx_glr_plot_2_working_with_unscaled_traces_thumb.png
+      :img-alt: Unscaled traces
+      :class-card: gallery-card
+      :text-align: center
+
+Quality metrics tutorial
+------------------------
+
+The :code:`spikeinterface.qualitymetrics` module allows users to compute various quality metrics to assess the goodness of a spike sorting output.
+
+.. grid:: 1 2 2 3
+   :gutter: 2
+
+   .. grid-item-card:: Quality Metrics
+      :link-type: ref
+      :link: sphx_glr_tutorials_qualitymetrics_plot_3_quality_mertics.py
+      :img-top: /tutorials/qualitymetrics/images/thumb/sphx_glr_plot_3_quality_mertics_thumb.png
+      :img-alt: Quality Metrics
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Curation Tutorial
+      :link-type: ref
+      :link: sphx_glr_tutorials_qualitymetrics_plot_4_curation.py
+      :img-top: /tutorials/qualitymetrics/images/thumb/sphx_glr_plot_4_curation_thumb.png
+      :img-alt: Curation Tutorial
+      :class-card: gallery-card
+      :text-align: center
+
+Comparison tutorial
+-------------------
+
+The :code:`spikeinterface.comparison` module allows you to compare sorter outputs or benchmark against ground truth.
+
+.. grid:: 1 2 2 3
+   :gutter: 2
+
+   .. grid-item-card:: Sorter Comparison
+      :link-type: ref
+      :link: sphx_glr_tutorials_comparison_plot_5_comparison_sorter_weaknesses.py
+      :img-top: /tutorials/comparison/images/thumb/sphx_glr_plot_5_comparison_sorter_weaknesses_thumb.png
+      :img-alt: Sorter Comparison
+      :class-card: gallery-card
+      :text-align: center
+
+Widgets tutorials
+-----------------
+
+The :code:`widgets` module contains several plotting routines (widgets) for visualizing recordings, sorting data, probe layout, and more.
+
+.. grid:: 1 2 2 3
+   :gutter: 2
+
+   .. grid-item-card:: RecordingExtractor Widgets
+      :link-type: ref
+      :link: sphx_glr_tutorials_widgets_plot_1_rec_gallery.py
+      :img-top: /tutorials/widgets/images/thumb/sphx_glr_plot_1_rec_gallery_thumb.png
+      :img-alt: Recording Widgets
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: SortingExtractor Widgets
+      :link-type: ref
+      :link: sphx_glr_tutorials_widgets_plot_2_sort_gallery.py
+      :img-top: /tutorials/widgets/images/thumb/sphx_glr_plot_2_sort_gallery_thumb.png
+      :img-alt: Sorting Widgets
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Waveforms Widgets
+      :link-type: ref
+      :link: sphx_glr_tutorials_widgets_plot_3_waveforms_gallery.py
+      :img-top: /tutorials/widgets/images/thumb/sphx_glr_plot_3_waveforms_gallery_thumb.png
+      :img-alt: Waveforms Widgets
+      :class-card: gallery-card
+      :text-align: center
+
+   .. grid-item-card:: Peaks Widgets
+      :link-type: ref
+      :link: sphx_glr_tutorials_widgets_plot_4_peaks_gallery.py
+      :img-top: /tutorials/widgets/images/thumb/sphx_glr_plot_4_peaks_gallery_thumb.png
+      :img-alt: Peaks Widgets
+      :class-card: gallery-card
+      :text-align: center
+
+Download All Examples
+---------------------
+
+- :download:`Download all examples in Python source code </tutorials/tutorials_python.zip>`
+- :download:`Download all examples in Jupyter notebooks </tutorials/tutorials_jupyter.zip>`
diff --git a/src/spikeinterface/core/analyzer_extension_core.py b/src/spikeinterface/core/analyzer_extension_core.py
index 1d3501c4d..bc5de63d0 100644
--- a/src/spikeinterface/core/analyzer_extension_core.py
+++ b/src/spikeinterface/core/analyzer_extension_core.py
@@ -691,13 +691,12 @@ class ComputeNoiseLevels(AnalyzerExtension):
     need_recording = True
     use_nodepipeline = False
     need_job_kwargs = False
-    need_backward_compatibility_on_load = True
 
     def __init__(self, sorting_analyzer):
         AnalyzerExtension.__init__(self, sorting_analyzer)
 
-    def _set_params(self, **noise_level_params):
-        params = noise_level_params.copy()
+    def _set_params(self, num_chunks_per_segment=20, chunk_size=10000, seed=None):
+        params = dict(num_chunks_per_segment=num_chunks_per_segment, chunk_size=chunk_size, seed=seed)
         return params
 
     def _select_extension_data(self, unit_ids):
@@ -718,15 +717,6 @@ class ComputeNoiseLevels(AnalyzerExtension):
     def _get_data(self):
         return self.data["noise_levels"]
 
-    def _handle_backward_compatibility_on_load(self):
-        # The old parameters used to be params=dict(num_chunks_per_segment=20, chunk_size=10000, seed=None)
-        # now it is handle more explicitly using random_slices_kwargs=dict()
-        for key in ("num_chunks_per_segment", "chunk_size", "seed"):
-            if key in self.params:
-                if "random_slices_kwargs" not in self.params:
-                    self.params["random_slices_kwargs"] = dict()
-                self.params["random_slices_kwargs"][key] = self.params.pop(key)
-
 
 register_result_extension(ComputeNoiseLevels)
 compute_noise_levels = ComputeNoiseLevels.function_factory()
diff --git a/src/spikeinterface/core/job_tools.py b/src/spikeinterface/core/job_tools.py
index 27f05bb36..5240edcee 100644
--- a/src/spikeinterface/core/job_tools.py
+++ b/src/spikeinterface/core/job_tools.py
@@ -185,22 +185,6 @@ def ensure_n_jobs(recording, n_jobs=1):
     return n_jobs
 
 
-def chunk_duration_to_chunk_size(chunk_duration, recording):
-    if isinstance(chunk_duration, float):
-        chunk_size = int(chunk_duration * recording.get_sampling_frequency())
-    elif isinstance(chunk_duration, str):
-        if chunk_duration.endswith("ms"):
-            chunk_duration = float(chunk_duration.replace("ms", "")) / 1000.0
-        elif chunk_duration.endswith("s"):
-            chunk_duration = float(chunk_duration.replace("s", ""))
-        else:
-            raise ValueError("chunk_duration must ends with s or ms")
-        chunk_size = int(chunk_duration * recording.get_sampling_frequency())
-    else:
-        raise ValueError("chunk_duration must be str or float")
-    return chunk_size
-
-
 def ensure_chunk_size(
     recording, total_memory=None, chunk_size=None, chunk_memory=None, chunk_duration=None, n_jobs=1, **other_kwargs
 ):
@@ -247,7 +231,18 @@ def ensure_chunk_size(
         num_channels = recording.get_num_channels()
         chunk_size = int(total_memory / (num_channels * n_bytes * n_jobs))
     elif chunk_duration is not None:
-        chunk_size = chunk_duration_to_chunk_size(chunk_duration, recording)
+        if isinstance(chunk_duration, float):
+            chunk_size = int(chunk_duration * recording.get_sampling_frequency())
+        elif isinstance(chunk_duration, str):
+            if chunk_duration.endswith("ms"):
+                chunk_duration = float(chunk_duration.replace("ms", "")) / 1000.0
+            elif chunk_duration.endswith("s"):
+                chunk_duration = float(chunk_duration.replace("s", ""))
+            else:
+                raise ValueError("chunk_duration must ends with s or ms")
+            chunk_size = int(chunk_duration * recording.get_sampling_frequency())
+        else:
+            raise ValueError("chunk_duration must be str or float")
     else:
         # Edge case to define single chunk per segment for n_jobs=1.
         # All chunking parameters equal None mean single chunk per segment
@@ -387,13 +382,11 @@ class ChunkRecordingExecutor:
                 f"chunk_duration={chunk_duration_str}",
             )
 
-    def run(self, all_chunks=None):
+    def run(self):
         """
         Runs the defined jobs.
         """
-
-        if all_chunks is None:
-            all_chunks = divide_recording_into_chunks(self.recording, self.chunk_size)
+        all_chunks = divide_recording_into_chunks(self.recording, self.chunk_size)
 
         if self.handle_returns:
             returns = []
diff --git a/src/spikeinterface/core/recording_tools.py b/src/spikeinterface/core/recording_tools.py
index 2ab74ce51..77d427bc8 100644
--- a/src/spikeinterface/core/recording_tools.py
+++ b/src/spikeinterface/core/recording_tools.py
@@ -18,8 +18,6 @@ from .job_tools import (
     fix_job_kwargs,
     ChunkRecordingExecutor,
     _shared_job_kwargs_doc,
-    chunk_duration_to_chunk_size,
-    split_job_kwargs,
 )
 
 
@@ -514,38 +512,33 @@ def determine_cast_unsigned(recording, dtype):
     return cast_unsigned
 
 
-def get_random_recording_slices(
+def get_random_data_chunks(
     recording,
-    method="full_random",
+    return_scaled=False,
     num_chunks_per_segment=20,
-    chunk_duration="500ms",
-    chunk_size=None,
+    chunk_size=10000,
+    concatenated=True,
+    seed=0,
     margin_frames=0,
-    seed=None,
 ):
     """
-    Get random slice of a recording across segments.
+    Extract random chunks across segments
 
-    This is used for instance in get_noise_levels() and get_random_data_chunks() to estimate noise on traces.
+    This is used for instance in get_noise_levels() to estimate noise on traces.
 
     Parameters
     ----------
     recording : BaseRecording
         The recording to get random chunks from
-    method : "full_random"
-        The method used to get random slices.
-          * "full_random" : legacy method,  used until version 0.101.0, there is no constrain on slices
-            and they can overlap.
+    return_scaled : bool, default: False
+        If True, returned chunks are scaled to uV
     num_chunks_per_segment : int, default: 20
         Number of chunks per segment
-    chunk_duration : str | float | None, default "500ms"
-        The duration of each chunk in 's' or 'ms'
-    chunk_size : int | None
-        Size of a chunk in number of frames. This is ued only if chunk_duration is None.
-        This is kept for backward compatibility, you should prefer 'chunk_duration=500ms' instead.
+    chunk_size : int, default: 10000
+        Size of a chunk in number of frames
     concatenated : bool, default: True
         If True chunk are concatenated along time axis
-    seed : int, default: None
+    seed : int, default: 0
         Random seed
     margin_frames : int, default: 0
         Margin in number of frames to avoid edge effects
@@ -554,89 +547,42 @@ def get_random_recording_slices(
     -------
     chunk_list : np.array
         Array of concatenate chunks per segment
-
-
     """
     # TODO: if segment have differents length make another sampling that dependant on the length of the segment
     # Should be done by changing kwargs with total_num_chunks=XXX and total_duration=YYYY
     # And randomize the number of chunk per segment weighted by segment duration
 
-    if method == "full_random":
-        if chunk_size is None:
-            if chunk_duration is not None:
-                chunk_size = chunk_duration_to_chunk_size(chunk_duration, recording)
-            else:
-                raise ValueError("get_random_recording_slices need chunk_size or chunk_duration")
-
-        # check chunk size
-        num_segments = recording.get_num_segments()
-        for segment_index in range(num_segments):
-            chunk_size_limit = recording.get_num_frames(segment_index) - 2 * margin_frames
-            if chunk_size > chunk_size_limit:
-                chunk_size = chunk_size_limit - 1
-                warnings.warn(
-                    f"chunk_size is greater than the number "
-                    f"of samples for segment index {segment_index}. "
-                    f"Using {chunk_size}."
-                )
-        rng = np.random.default_rng(seed)
-        recording_slices = []
-        low = margin_frames
-        size = num_chunks_per_segment
-        for segment_index in range(num_segments):
-            num_frames = recording.get_num_frames(segment_index)
-            high = num_frames - chunk_size - margin_frames
-            random_starts = rng.integers(low=low, high=high, size=size)
-            random_starts = np.sort(random_starts)
-            recording_slices += [
-                (segment_index, start_frame, (start_frame + chunk_size)) for start_frame in random_starts
-            ]
-    else:
-        raise ValueError(f"get_random_recording_slices : wrong method {method}")
-
-    return recording_slices
-
-
-def get_random_data_chunks(recording, return_scaled=False, concatenated=True, **random_slices_kwargs):
-    """
-    Extract random chunks across segments.
-
-    Internally, it uses `get_random_recording_slices()` and retrieves the traces chunk as a list
-    or a concatenated unique array.
-
-    Please read `get_random_recording_slices()` for more details on parameters.
-
-
-    Parameters
-    ----------
-    recording : BaseRecording
-        The recording to get random chunks from
-    return_scaled : bool, default: False
-        If True, returned chunks are scaled to uV
-    num_chunks_per_segment : int, default: 20
-        Number of chunks per segment
-    concatenated : bool, default: True
-        If True chunk are concatenated along time axis
-    **random_slices_kwargs : dict
-        Options transmited to  get_random_recording_slices(), please read documentation from this
-        function for more details.
-
-    Returns
-    -------
-    chunk_list : np.array | list of np.array
-        Array of concatenate chunks per segment
-    """
-    recording_slices = get_random_recording_slices(recording, **random_slices_kwargs)
+    # check chunk size
+    num_segments = recording.get_num_segments()
+    for segment_index in range(num_segments):
+        chunk_size_limit = recording.get_num_frames(segment_index) - 2 * margin_frames
+        if chunk_size > chunk_size_limit:
+            chunk_size = chunk_size_limit - 1
+            warnings.warn(
+                f"chunk_size is greater than the number "
+                f"of samples for segment index {segment_index}. "
+                f"Using {chunk_size}."
+            )
 
+    rng = np.random.default_rng(seed)
     chunk_list = []
-    for segment_index, start_frame, end_frame in recording_slices:
-        traces_chunk = recording.get_traces(
-            start_frame=start_frame,
-            end_frame=end_frame,
-            segment_index=segment_index,
-            return_scaled=return_scaled,
-        )
-        chunk_list.append(traces_chunk)
+    low = margin_frames
+    size = num_chunks_per_segment
+    for segment_index in range(num_segments):
+        num_frames = recording.get_num_frames(segment_index)
+        high = num_frames - chunk_size - margin_frames
+        random_starts = rng.integers(low=low, high=high, size=size)
+        segment_trace_chunk = [
+            recording.get_traces(
+                start_frame=start_frame,
+                end_frame=(start_frame + chunk_size),
+                segment_index=segment_index,
+                return_scaled=return_scaled,
+            )
+            for start_frame in random_starts
+        ]
+
+        chunk_list.extend(segment_trace_chunk)
 
     if concatenated:
         return np.concatenate(chunk_list, axis=0)
@@ -691,52 +637,19 @@ def get_closest_channels(recording, channel_ids=None, num_channels=None):
     return np.array(closest_channels_inds), np.array(dists)
 
 
-def _noise_level_chunk(segment_index, start_frame, end_frame, worker_ctx):
-    recording = worker_ctx["recording"]
-
-    one_chunk = recording.get_traces(
-        start_frame=start_frame,
-        end_frame=end_frame,
-        segment_index=segment_index,
-        return_scaled=worker_ctx["return_scaled"],
-    )
-
-    if worker_ctx["method"] == "mad":
-        med = np.median(one_chunk, axis=0, keepdims=True)
-        # hard-coded so that core doesn't depend on scipy
-        noise_levels = np.median(np.abs(one_chunk - med), axis=0) / 0.6744897501960817
-    elif worker_ctx["method"] == "std":
-        noise_levels = np.std(one_chunk, axis=0)
-
-    return noise_levels
-
-
-def _noise_level_chunk_init(recording, return_scaled, method):
-    worker_ctx = {}
-    worker_ctx["recording"] = recording
-    worker_ctx["return_scaled"] = return_scaled
-    worker_ctx["method"] = method
-    return worker_ctx
-
-
 def get_noise_levels(
     recording: "BaseRecording",
     return_scaled: bool = True,
     method: Literal["mad", "std"] = "mad",
     force_recompute: bool = False,
-    random_slices_kwargs: dict = {},
-    **kwargs,
+    **random_chunk_kwargs,
 ) -> np.ndarray:
     """
     Estimate noise for each channel using MAD methods.
     You can use standard deviation with `method="std"`
 
     Internally it samples some chunk across segment.
-    And then, it uses the MAD estimator (more robust than STD) or the STD on each chunk.
-    Finally the average of all MAD/STD values is performed.
-
-    The result is cached in a property of the recording, so that the next call on the same
-    recording will use the cached result unless `force_recompute=True`.
+    And then, it use MAD estimator (more robust than STD)
 
     Parameters
     ----------
@@ -749,11 +662,8 @@ def get_noise_levels(
         The method to use to estimate noise levels
     force_recompute : bool
         If True, noise levels are recomputed even if they are already stored in the recording extractor
-    random_slices_kwargs : dict
-        Options transmited to  get_random_recording_slices(), please read documentation from this
-        function for more details.
-
-    {}
+    random_chunk_kwargs : dict
+        Kwargs for get_random_data_chunks
 
     Returns
     -------
@@ -769,56 +679,19 @@ def get_noise_levels(
     if key in recording.get_property_keys() and not force_recompute:
         noise_levels = recording.get_property(key=key)
     else:
-        # This is to keep backward compatibility
-        # lets keep for a while and remove this maybe in 0.103.0
-        # chunk_size used to be in the signature and now is ambiguous
-        random_slices_kwargs_, job_kwargs = split_job_kwargs(kwargs)
-        if len(random_slices_kwargs_) > 0 or "chunk_size" in job_kwargs:
-            msg = (
-                "get_noise_levels(recording, num_chunks_per_segment=20) is deprecated\n"
-                "Now, you need to use get_noise_levels(recording, random_slices_kwargs=dict(num_chunks_per_segment=20, chunk_size=1000))\n"
-                "Please read get_random_recording_slices() documentation for more options."
-            )
-            # if the user use both the old and the new behavior then an error is raised
-            assert len(random_slices_kwargs) == 0, msg
-            warnings.warn(msg)
-            random_slices_kwargs = random_slices_kwargs_
-            if "chunk_size" in job_kwargs:
-                random_slices_kwargs["chunk_size"] = job_kwargs["chunk_size"]
-
-        recording_slices = get_random_recording_slices(recording, **random_slices_kwargs)
-
-        noise_levels_chunks = []
-
-        def append_noise_chunk(res):
-            noise_levels_chunks.append(res)
-
-        func = _noise_level_chunk
-        init_func = _noise_level_chunk_init
-        init_args = (recording, return_scaled, method)
-        executor = ChunkRecordingExecutor(
-            recording,
-            func,
-            init_func,
-            init_args,
-            job_name="noise_level",
-            verbose=False,
-            gather_func=append_noise_chunk,
-            **job_kwargs,
-        )
-        executor.run(all_chunks=recording_slices)
-        noise_levels_chunks = np.stack(noise_levels_chunks)
-        noise_levels = np.mean(noise_levels_chunks, axis=0)
-
-        # set property
+        random_chunks = get_random_data_chunks(recording, return_scaled=return_scaled, **random_chunk_kwargs)
+
+        if method == "mad":
+            med = np.median(random_chunks, axis=0, keepdims=True)
+            # hard-coded so that core doesn't depend on scipy
+            noise_levels = np.median(np.abs(random_chunks - med), axis=0) / 0.6744897501960817
+        elif method == "std":
+            noise_levels = np.std(random_chunks, axis=0)
         recording.set_property(key, noise_levels)
 
     return noise_levels
 
 
-get_noise_levels.__doc__ = get_noise_levels.__doc__.format(_shared_job_kwargs_doc)
-
-
 def get_chunk_with_margin(
     rec_segment,
     start_frame,
diff --git a/src/spikeinterface/core/template_tools.py b/src/spikeinterface/core/template_tools.py
index 3c8663df7..934b18ed4 100644
--- a/src/spikeinterface/core/template_tools.py
+++ b/src/spikeinterface/core/template_tools.py
@@ -31,12 +31,7 @@ def get_dense_templates_array(one_object: Templates | SortingAnalyzer, return_sc
             )
         ext = one_object.get_extension("templates")
         if ext is not None:
-            if "average" in ext.data:
-                templates_array = ext.data.get("average")
-            elif "median" in ext.data:
-                templates_array = ext.data.get("median")
-            else:
-                raise ValueError("Average or median templates have not been computed.")
+            templates_array = ext.data["average"]
         else:
             raise ValueError("SortingAnalyzer need extension 'templates' to be computed to retrieve templates")
     else:
diff --git a/src/spikeinterface/curation/__init__.py b/src/spikeinterface/curation/__init__.py
index 0302ffe5b..657b936fb 100644
--- a/src/spikeinterface/curation/__init__.py
+++ b/src/spikeinterface/curation/__init__.py
@@ -3,7 +3,7 @@ from .curation_tools import find_duplicated_spikes
 from .remove_redundant import remove_redundant_units, find_redundant_units
 from .remove_duplicated_spikes import remove_duplicated_spikes
 from .remove_excess_spikes import remove_excess_spikes
-from .auto_merge import compute_merge_unit_groups, auto_merge_units, get_potential_auto_merge
+from .auto_merge import get_potential_auto_merge
 
 
 # manual sorting,
diff --git a/src/spikeinterface/curation/auto_merge.py b/src/spikeinterface/curation/auto_merge.py
index 4f4cff144..19336e594 100644
--- a/src/spikeinterface/curation/auto_merge.py
+++ b/src/spikeinterface/curation/auto_merge.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-import warnings
-
 from typing import Tuple
 import numpy as np
 import math
@@ -14,90 +12,49 @@ except ImportError:
     HAVE_NUMBA = False
 
 from ..core import SortingAnalyzer, Templates
+from ..core.template_tools import get_template_extremum_channel
+from ..postprocessing import compute_correlograms
 from ..qualitymetrics import compute_refrac_period_violations, compute_firing_rates
 
 from .mergeunitssorting import MergeUnitsSorting
 from .curation_tools import resolve_merging_graph
 
-_compute_merge_presets = {
-    "similarity_correlograms": [
-        "num_spikes",
-        "remove_contaminated",
-        "unit_locations",
-        "template_similarity",
-        "correlogram",
-        "quality_score",
-    ],
-    "temporal_splits": [
-        "num_spikes",
-        "remove_contaminated",
-        "unit_locations",
-        "template_similarity",
-        "presence_distance",
-        "quality_score",
-    ],
-    "x_contaminations": [
-        "num_spikes",
-        "remove_contaminated",
-        "unit_locations",
-        "template_similarity",
-        "cross_contamination",
-        "quality_score",
-    ],
-    "feature_neighbors": [
-        "num_spikes",
-        "snr",
-        "remove_contaminated",
-        "unit_locations",
-        "knn",
-        "quality_score",
-    ],
-}
+
+_possible_presets = ["similarity_correlograms", "x_contaminations", "temporal_splits", "feature_neighbors"]
 
 _required_extensions = {
-    "unit_locations": ["templates", "unit_locations"],
+    "unit_locations": ["unit_locations"],
     "correlogram": ["correlograms"],
-    "snr": ["templates", "noise_levels"],
-    "template_similarity": ["templates", "template_similarity"],
-    "knn": ["templates", "spike_locations", "spike_amplitudes"],
-}
-
-
-_default_step_params = {
-    "num_spikes": {"min_spikes": 100},
-    "snr": {"min_snr": 2},
-    "remove_contaminated": {"contamination_thresh": 0.2, "refractory_period_ms": 1.0, "censored_period_ms": 0.3},
-    "unit_locations": {"max_distance_um": 150},
-    "correlogram": {
-        "corr_diff_thresh": 0.16,
-        "censor_correlograms_ms": 0.15,
-        "sigma_smooth_ms": 0.6,
-        "adaptative_window_thresh": 0.5,
-    },
-    "template_similarity": {"template_diff_thresh": 0.25},
-    "presence_distance": {"presence_distance_thresh": 100},
-    "knn": {"k_nn": 10},
-    "cross_contamination": {
-        "cc_thresh": 0.1,
-        "p_value": 0.2,
-        "refractory_period_ms": 1.0,
-        "censored_period_ms": 0.3,
-    },
-    "quality_score": {"firing_contamination_balance": 1.5, "refractory_period_ms": 1.0, "censored_period_ms": 0.3},
+    "template_similarity": ["template_similarity"],
+    "knn": ["spike_locations", "spike_amplitudes"],
 }
 
 
-def compute_merge_unit_groups(
+def get_potential_auto_merge(
     sorting_analyzer: SortingAnalyzer,
     preset: str | None = "similarity_correlograms",
-    resolve_graph: bool = True,
-    steps_params: dict = None,
-    compute_needed_extensions: bool = True,
+    resolve_graph: bool = False,
+    min_spikes: int = 100,
+    min_snr: float = 2,
+    max_distance_um: float = 150.0,
+    corr_diff_thresh: float = 0.16,
+    template_diff_thresh: float = 0.25,
+    contamination_thresh: float = 0.2,
+    presence_distance_thresh: float = 100,
+    p_value: float = 0.2,
+    cc_thresh: float = 0.1,
+    censored_period_ms: float = 0.3,
+    refractory_period_ms: float = 1.0,
+    sigma_smooth_ms: float = 0.6,
+    adaptative_window_thresh: float = 0.5,
+    censor_correlograms_ms: float = 0.15,
+    firing_contamination_balance: float = 2.5,
+    k_nn: int = 10,
+    knn_kwargs: dict | None = None,
+    presence_distance_kwargs: dict | None = None,
     extra_outputs: bool = False,
     steps: list[str] | None = None,
-    force_copy: bool = True,
-    **job_kwargs,
-) -> list[tuple[int | str, int | str]] | Tuple[list[tuple[int | str, int | str]], dict]:
+) -> list[tuple[int | str, int | str]] | Tuple[tuple[int | str, int | str], dict]:
     """
     Algorithm to find and check potential merges between units.
 
@@ -121,9 +78,6 @@ def compute_merge_unit_groups(
 
         Q = f(1 - (k + 1)C)
 
-    IMPORTANT: internally, all computations are relying on extensions of the analyzer, that are computed
-    with default parameters if not present (i.e. correlograms, template_similarity, ...) If you want to
-    have a finer control on these values, please precompute the extensions before applying the auto_merge
 
     Parameters
     ----------
@@ -144,11 +98,47 @@ def compute_merge_unit_groups(
         * | "feature_neighbors": focused on finding unit pairs whose spikes are close in the feature space using kNN.
           | It uses the following steps: "num_spikes", "snr", "remove_contaminated", "unit_locations",
           | "knn", "quality_score"
+
         If `preset` is None, you can specify the steps manually with the `steps` parameter.
-    resolve_graph : bool, default: True
+    resolve_graph : bool, default: False
         If True, the function resolves the potential unit pairs to be merged into multiple-unit merges.
-    compute_needed_extensions : bool, default : True
-        Should we force the computation of needed extensions, if not already computed?
+    min_spikes : int, default: 100
+        Minimum number of spikes for each unit to consider a potential merge.
+        Enough spikes are needed to estimate the correlogram
+    min_snr : float, default 2
+        Minimum Signal to Noise ratio for templates to be considered while merging
+    max_distance_um : float, default: 150
+        Maximum distance between units for considering a merge
+    corr_diff_thresh : float, default: 0.16
+        The threshold on the "correlogram distance metric" for considering a merge.
+        It needs to be between 0 and 1
+    template_diff_thresh : float, default: 0.25
+        The threshold on the "template distance metric" for considering a merge.
+        It needs to be between 0 and 1
+    contamination_thresh : float, default: 0.2
+        Threshold for not taking in account a unit when it is too contaminated.
+    presence_distance_thresh : float, default: 100
+        Parameter to control how present two units should be simultaneously.
+    p_value : float, default: 0.2
+        The p-value threshold for the cross-contamination test.
+    cc_thresh : float, default: 0.1
+        The threshold on the cross-contamination for considering a merge.
+    censored_period_ms : float, default: 0.3
+        Used to compute the refractory period violations aka "contamination".
+    refractory_period_ms : float, default: 1
+        Used to compute the refractory period violations aka "contamination".
+    sigma_smooth_ms : float, default: 0.6
+        Parameters to smooth the correlogram estimation.
+    adaptative_window_thresh : float, default: 0.5
+        Parameter to detect the window size in correlogram estimation.
+    censor_correlograms_ms : float, default: 0.15
+        The period to censor on the auto and cross-correlograms.
+    firing_contamination_balance : float, default: 2.5
+        Parameter to control the balance between firing rate and contamination in computing unit "quality score".
+    k_nn : int, default 5
+        The number of neighbors to consider for every spike in the recording.
+    knn_kwargs : dict, default None
+        The dict of extra params to be passed to knn.
     extra_outputs : bool, default: False
         If True, an additional dictionary (`outs`) with processed data is returned.
     steps : None or list of str, default: None
@@ -156,141 +146,157 @@ def compute_merge_unit_groups(
         Pontential steps : "num_spikes", "snr", "remove_contaminated", "unit_locations", "correlogram",
         "template_similarity", "presence_distance", "cross_contamination", "knn", "quality_score"
         Please check steps explanations above!
-    steps_params : dict
-        A dictionary whose keys are the steps, and keys are steps parameters.
-    force_copy : boolean, default: True
-        When new extensions are computed, the default is to make a copy of the analyzer, to avoid overwriting
-        already computed extensions. False if you want to overwrite
+    presence_distance_kwargs : None|dict, default: None
+        A dictionary of kwargs to be passed to compute_presence_distance().
 
     Returns
     -------
-    merge_unit_groups:
-        List of groups that need to be merge.
-        When `resolve_graph` is true (default) a list of tuples of 2+ elements
-        If `resolve_graph` is false then a list of tuple of 2 elements is returned instead.
+    potential_merges:
+        A list of tuples of 2 elements (if `resolve_graph`if false) or 2+ elements (if `resolve_graph` is true).
+        List of pairs that could be merged.
     outs:
         Returned only when extra_outputs=True
         A dictionary that contains data for debugging and plotting.
 
     References
     ----------
-    This function used to be inspired and built upon similar functions from Lussac [Llobet]_,
+    This function is inspired and built upon similar functions from Lussac [Llobet]_,
     done by Aurelien Wyngaard and Victor Llobet.
     https://github.com/BarbourLab/lussac/blob/v1.0.0/postprocessing/merge_units.py
-
-    However, it has been greatly consolidated and refined depending on the presets.
     """
     import scipy
 
     sorting = sorting_analyzer.sorting
     unit_ids = sorting.unit_ids
 
-    if preset is None and steps is None:
-        raise ValueError("You need to specify a preset or steps for the auto-merge function")
-    elif steps is not None:
-        # steps has precedence on presets
-        pass
-    elif preset is not None:
-        if preset not in _compute_merge_presets:
-            raise ValueError(f"preset must be one of {list(_compute_merge_presets.keys())}")
-        steps = _compute_merge_presets[preset]
-
-    # check at least one extension is needed
-    at_least_one_extension_to_compute = False
+    # to get fast computation we will not analyse pairs when:
+    #    * not enough spikes for one of theses
+    #    * auto correlogram is contaminated
+    #    * to far away one from each other
+
+    all_steps = [
+        "num_spikes",
+        "snr",
+        "remove_contaminated",
+        "unit_locations",
+        "correlogram",
+        "template_similarity",
+        "presence_distance",
+        "knn",
+        "cross_contamination",
+        "quality_score",
+    ]
+
+    if preset is not None and preset not in _possible_presets:
+        raise ValueError(f"preset must be one of {_possible_presets}")
+
+    if steps is None:
+        if preset is None:
+            if steps is None:
+                raise ValueError("You need to specify a preset or steps for the auto-merge function")
+        elif preset == "similarity_correlograms":
+            steps = [
+                "num_spikes",
+                "remove_contaminated",
+                "unit_locations",
+                "template_similarity",
+                "correlogram",
+                "quality_score",
+            ]
+        elif preset == "temporal_splits":
+            steps = [
+                "num_spikes",
+                "remove_contaminated",
+                "unit_locations",
+                "template_similarity",
+                "presence_distance",
+                "quality_score",
+            ]
+        elif preset == "x_contaminations":
+            steps = [
+                "num_spikes",
+                "remove_contaminated",
+                "unit_locations",
+                "template_similarity",
+                "cross_contamination",
+                "quality_score",
+            ]
+        elif preset == "feature_neighbors":
+            steps = [
+                "num_spikes",
+                "snr",
+                "remove_contaminated",
+                "unit_locations",
+                "knn",
+                "quality_score",
+            ]
+
     for step in steps:
-        assert step in _default_step_params, f"{step} is not a valid step"
         if step in _required_extensions:
             for ext in _required_extensions[step]:
-                if sorting_analyzer.has_extension(ext):
-                    continue
-                if not compute_needed_extensions:
+                if not sorting_analyzer.has_extension(ext):
                     raise ValueError(f"{step} requires {ext} extension")
-                at_least_one_extension_to_compute = True
-
-    if force_copy and at_least_one_extension_to_compute:
-        # To avoid erasing the extensions of the user
-        sorting_analyzer = sorting_analyzer.copy()
 
     n = unit_ids.size
-    pair_mask = np.triu(np.arange(n), 1) > 0
+    pair_mask = np.triu(np.arange(n)) > 0
     outs = dict()
 
     for step in steps:
 
-        if step in _required_extensions:
-            for ext in _required_extensions[step]:
-                if sorting_analyzer.has_extension(ext):
-                    continue
-
-                # special case for templates
-                if ext == "templates" and not sorting_analyzer.has_extension("random_spikes"):
-                    sorting_analyzer.compute(["random_spikes", "templates"], **job_kwargs)
-                else:
-                    sorting_analyzer.compute(ext, **job_kwargs)
-
-        params = _default_step_params.get(step).copy()
-        if steps_params is not None and step in steps_params:
-            params.update(steps_params[step])
+        assert step in all_steps, f"{step} is not a valid step"
 
         # STEP : remove units with too few spikes
         if step == "num_spikes":
-
             num_spikes = sorting.count_num_spikes_per_unit(outputs="array")
-            to_remove = num_spikes < params["min_spikes"]
+            to_remove = num_spikes < min_spikes
             pair_mask[to_remove, :] = False
             pair_mask[:, to_remove] = False
-            outs["num_spikes"] = to_remove
 
         # STEP : remove units with too small SNR
         elif step == "snr":
             qm_ext = sorting_analyzer.get_extension("quality_metrics")
             if qm_ext is None:
-                sorting_analyzer.compute("quality_metrics", metric_names=["snr"], **job_kwargs)
+                sorting_analyzer.compute("noise_levels")
+                sorting_analyzer.compute("quality_metrics", metric_names=["snr"])
                 qm_ext = sorting_analyzer.get_extension("quality_metrics")
 
             snrs = qm_ext.get_data()["snr"].values
-            to_remove = snrs < params["min_snr"]
+            to_remove = snrs < min_snr
             pair_mask[to_remove, :] = False
             pair_mask[:, to_remove] = False
-            outs["snr"] = to_remove
 
         # STEP : remove contaminated auto corr
         elif step == "remove_contaminated":
             contaminations, nb_violations = compute_refrac_period_violations(
-                sorting_analyzer,
-                refractory_period_ms=params["refractory_period_ms"],
-                censored_period_ms=params["censored_period_ms"],
+                sorting_analyzer, refractory_period_ms=refractory_period_ms, censored_period_ms=censored_period_ms
             )
             nb_violations = np.array(list(nb_violations.values()))
             contaminations = np.array(list(contaminations.values()))
-            to_remove = contaminations > params["contamination_thresh"]
+            to_remove = contaminations > contamination_thresh
             pair_mask[to_remove, :] = False
             pair_mask[:, to_remove] = False
-            outs["remove_contaminated"] = to_remove
 
         # STEP : unit positions are estimated roughly with channel
-        elif step == "unit_locations":
+        elif step == "unit_locations" in steps:
             location_ext = sorting_analyzer.get_extension("unit_locations")
             unit_locations = location_ext.get_data()[:, :2]
 
             unit_distances = scipy.spatial.distance.cdist(unit_locations, unit_locations, metric="euclidean")
-            pair_mask = pair_mask & (unit_distances <= params["max_distance_um"])
+            pair_mask = pair_mask & (unit_distances <= max_distance_um)
             outs["unit_distances"] = unit_distances
 
         # STEP : potential auto merge by correlogram
-        elif step == "correlogram":
+        elif step == "correlogram" in steps:
             correlograms_ext = sorting_analyzer.get_extension("correlograms")
             correlograms, bins = correlograms_ext.get_data()
-            censor_ms = params["censor_correlograms_ms"]
-            sigma_smooth_ms = params["sigma_smooth_ms"]
-            mask = (bins[:-1] >= -censor_ms) & (bins[:-1] < censor_ms)
+            mask = (bins[:-1] >= -censor_correlograms_ms) & (bins[:-1] < censor_correlograms_ms)
             correlograms[:, :, mask] = 0
             correlograms_smoothed = smooth_correlogram(correlograms, bins, sigma_smooth_ms=sigma_smooth_ms)
             # find correlogram window for each units
             win_sizes = np.zeros(n, dtype=int)
             for unit_ind in range(n):
                 auto_corr = correlograms_smoothed[unit_ind, unit_ind, :]
-                thresh = np.max(auto_corr) * params["adaptative_window_thresh"]
+                thresh = np.max(auto_corr) * adaptative_window_thresh
                 win_size = get_unit_adaptive_window(auto_corr, thresh)
                 win_sizes[unit_ind] = win_size
             correlogram_diff = compute_correlogram_diff(
@@ -300,7 +306,7 @@ def compute_merge_unit_groups(
                 pair_mask=pair_mask,
             )
             # print(correlogram_diff)
-            pair_mask = pair_mask & (correlogram_diff < params["corr_diff_thresh"])
+            pair_mask = pair_mask & (correlogram_diff < corr_diff_thresh)
             outs["correlograms"] = correlograms
             outs["bins"] = bins
             outs["correlograms_smoothed"] = correlograms_smoothed
@@ -308,21 +314,22 @@ def compute_merge_unit_groups(
             outs["win_sizes"] = win_sizes
 
         # STEP : check if potential merge with CC also have template similarity
-        elif step == "template_similarity":
+        elif step == "template_similarity" in steps:
             template_similarity_ext = sorting_analyzer.get_extension("template_similarity")
             templates_similarity = template_similarity_ext.get_data()
             templates_diff = 1 - templates_similarity
-            pair_mask = pair_mask & (templates_diff < params["template_diff_thresh"])
+            pair_mask = pair_mask & (templates_diff < template_diff_thresh)
             outs["templates_diff"] = templates_diff
 
         # STEP : check the vicinity of the spikes
-        elif step == "knn":
-            pair_mask = get_pairs_via_nntree(sorting_analyzer, **params, pair_mask=pair_mask)
+        elif step == "knn" in steps:
+            if knn_kwargs is None:
+                knn_kwargs = dict()
+            pair_mask = get_pairs_via_nntree(sorting_analyzer, k_nn, pair_mask, **knn_kwargs)
 
         # STEP : check how the rates overlap in times
-        elif step == "presence_distance":
-            presence_distance_kwargs = params.copy()
-            presence_distance_thresh = presence_distance_kwargs.pop("presence_distance_thresh")
+        elif step == "presence_distance" in steps:
+            presence_distance_kwargs = presence_distance_kwargs or dict()
             num_samples = [
                 sorting_analyzer.get_num_samples(segment_index) for segment_index in range(sorting.get_num_segments())
             ]
@@ -333,243 +340,40 @@ def compute_merge_unit_groups(
             outs["presence_distances"] = presence_distances
 
         # STEP : check if the cross contamination is significant
-        elif step == "cross_contamination":
-            refractory = (
-                params["censored_period_ms"],
-                params["refractory_period_ms"],
-            )
+        elif step == "cross_contamination" in steps:
+            refractory = (censored_period_ms, refractory_period_ms)
             CC, p_values = compute_cross_contaminations(
-                sorting_analyzer, pair_mask, params["cc_thresh"], refractory, contaminations
+                sorting_analyzer, pair_mask, cc_thresh, refractory, contaminations
             )
-            pair_mask = pair_mask & (p_values > params["p_value"])
+            pair_mask = pair_mask & (p_values > p_value)
             outs["cross_contaminations"] = CC, p_values
 
         # STEP : validate the potential merges with CC increase the contamination quality metrics
-        elif step == "quality_score":
+        elif step == "quality_score" in steps:
             pair_mask, pairs_decreased_score = check_improve_contaminations_score(
                 sorting_analyzer,
                 pair_mask,
                 contaminations,
-                params["firing_contamination_balance"],
-                params["refractory_period_ms"],
-                params["censored_period_ms"],
+                firing_contamination_balance,
+                refractory_period_ms,
+                censored_period_ms,
             )
             outs["pairs_decreased_score"] = pairs_decreased_score
 
     # FINAL STEP : create the final list from pair_mask boolean matrix
     ind1, ind2 = np.nonzero(pair_mask)
-    merge_unit_groups = list(zip(unit_ids[ind1], unit_ids[ind2]))
+    potential_merges = list(zip(unit_ids[ind1], unit_ids[ind2]))
+
+    # some methods return identities ie (1,1) which we can cleanup first.
+    potential_merges = [(ids[0], ids[1]) for ids in potential_merges if ids[0] != ids[1]]
 
     if resolve_graph:
-        merge_unit_groups = resolve_merging_graph(sorting, merge_unit_groups)
+        potential_merges = resolve_merging_graph(sorting, potential_merges)
 
     if extra_outputs:
-        return merge_unit_groups, outs
+        return potential_merges, outs
     else:
-        return merge_unit_groups
-
-
-def auto_merge_units(
-    sorting_analyzer: SortingAnalyzer, compute_merge_kwargs: dict = {}, apply_merge_kwargs: dict = {}, **job_kwargs
-) -> SortingAnalyzer:
-    """
-    Compute merge unit groups and apply it on a SortingAnalyzer.
-    Internally uses `compute_merge_unit_groups()`
-    """
-    merge_unit_groups = compute_merge_unit_groups(
-        sorting_analyzer, extra_outputs=False, **compute_merge_kwargs, **job_kwargs
-    )
-
-    merged_analyzer = sorting_analyzer.merge_units(merge_unit_groups, **apply_merge_kwargs, **job_kwargs)
-    return merged_analyzer
-
-
-def get_potential_auto_merge(
-    sorting_analyzer: SortingAnalyzer,
-    preset: str | None = "similarity_correlograms",
-    resolve_graph: bool = False,
-    min_spikes: int = 100,
-    min_snr: float = 2,
-    max_distance_um: float = 150.0,
-    corr_diff_thresh: float = 0.16,
-    template_diff_thresh: float = 0.25,
-    contamination_thresh: float = 0.2,
-    presence_distance_thresh: float = 100,
-    p_value: float = 0.2,
-    cc_thresh: float = 0.1,
-    censored_period_ms: float = 0.3,
-    refractory_period_ms: float = 1.0,
-    sigma_smooth_ms: float = 0.6,
-    adaptative_window_thresh: float = 0.5,
-    censor_correlograms_ms: float = 0.15,
-    firing_contamination_balance: float = 1.5,
-    k_nn: int = 10,
-    knn_kwargs: dict | None = None,
-    presence_distance_kwargs: dict | None = None,
-    extra_outputs: bool = False,
-    steps: list[str] | None = None,
-) -> list[tuple[int | str, int | str]] | Tuple[tuple[int | str, int | str], dict]:
-    """
-    This function is deprecated. Use compute_merge_unit_groups() instead.
-    This will be removed in 0.103.0
-
-    Algorithm to find and check potential merges between units.
-
-    The merges are proposed based on a series of steps with different criteria:
-
-        * "num_spikes": enough spikes are found in each unit for computing the correlogram (`min_spikes`)
-        * "snr": the SNR of the units is above a threshold (`min_snr`)
-        * "remove_contaminated": each unit is not contaminated (by checking auto-correlogram - `contamination_thresh`)
-        * "unit_locations": estimated unit locations are close enough (`max_distance_um`)
-        * "correlogram": the cross-correlograms of the two units are similar to each auto-corrleogram (`corr_diff_thresh`)
-        * "template_similarity": the templates of the two units are similar (`template_diff_thresh`)
-        * "presence_distance": the presence of the units is complementary in time (`presence_distance_thresh`)
-        * "cross_contamination": the cross-contamination is not significant (`cc_thresh` and `p_value`)
-        * "knn": the two units are close in the feature space
-        * "quality_score": the unit "quality score" is increased after the merge
-
-    The "quality score" factors in the increase in firing rate (**f**) due to the merge and a possible increase in
-    contamination (**C**), wheighted by a factor **k** (`firing_contamination_balance`).
-
-    .. math::
-
-        Q = f(1 - (k + 1)C)
-
-    IMPORTANT: internally, all computations are relying on extensions of the analyzer, that are computed
-    with default parameters if not present (i.e. correlograms, template_similarity, ...) If you want to
-    have a finer control on these values, please precompute the extensions before applying the auto_merge
-
-    Parameters
-    ----------
-    sorting_analyzer : SortingAnalyzer
-        The SortingAnalyzer
-    preset : "similarity_correlograms" | "x_contaminations" | "temporal_splits" | "feature_neighbors" | None, default: "similarity_correlograms"
-        The preset to use for the auto-merge. Presets combine different steps into a recipe and focus on:
-
-        * | "similarity_correlograms": mainly focused on template similarity and correlograms.
-          | It uses the following steps: "num_spikes", "remove_contaminated", "unit_locations",
-          | "template_similarity", "correlogram", "quality_score"
-        * | "x_contaminations": similar to "similarity_correlograms", but checks for cross-contamination instead of correlograms.
-          | It uses the following steps: "num_spikes", "remove_contaminated", "unit_locations",
-          | "template_similarity", "cross_contamination", "quality_score"
-        * | "temporal_splits": focused on finding temporal splits using presence distance.
-          | It uses the following steps: "num_spikes", "remove_contaminated", "unit_locations",
-          | "template_similarity", "presence_distance", "quality_score"
-        * | "feature_neighbors": focused on finding unit pairs whose spikes are close in the feature space using kNN.
-          | It uses the following steps: "num_spikes", "snr", "remove_contaminated", "unit_locations",
-          | "knn", "quality_score"
-
-        If `preset` is None, you can specify the steps manually with the `steps` parameter.
-    resolve_graph : bool, default: False
-        If True, the function resolves the potential unit pairs to be merged into multiple-unit merges.
-    min_spikes : int, default: 100
-        Minimum number of spikes for each unit to consider a potential merge.
-        Enough spikes are needed to estimate the correlogram
-    min_snr : float, default 2
-        Minimum Signal to Noise ratio for templates to be considered while merging
-    max_distance_um : float, default: 150
-        Maximum distance between units for considering a merge
-    corr_diff_thresh : float, default: 0.16
-        The threshold on the "correlogram distance metric" for considering a merge.
-        It needs to be between 0 and 1
-    template_diff_thresh : float, default: 0.25
-        The threshold on the "template distance metric" for considering a merge.
-        It needs to be between 0 and 1
-    contamination_thresh : float, default: 0.2
-        Threshold for not taking in account a unit when it is too contaminated.
-    presence_distance_thresh : float, default: 100
-        Parameter to control how present two units should be simultaneously.
-    p_value : float, default: 0.2
-        The p-value threshold for the cross-contamination test.
-    cc_thresh : float, default: 0.1
-        The threshold on the cross-contamination for considering a merge.
-    censored_period_ms : float, default: 0.3
-        Used to compute the refractory period violations aka "contamination".
-    refractory_period_ms : float, default: 1
-        Used to compute the refractory period violations aka "contamination".
-    sigma_smooth_ms : float, default: 0.6
-        Parameters to smooth the correlogram estimation.
-    adaptative_window_thresh : float, default: 0.5
-        Parameter to detect the window size in correlogram estimation.
-    censor_correlograms_ms : float, default: 0.15
-        The period to censor on the auto and cross-correlograms.
-    firing_contamination_balance : float, default: 1.5
-        Parameter to control the balance between firing rate and contamination in computing unit "quality score".
-    k_nn : int, default 5
-        The number of neighbors to consider for every spike in the recording.
-    knn_kwargs : dict, default None
-        The dict of extra params to be passed to knn.
-    extra_outputs : bool, default: False
-        If True, an additional dictionary (`outs`) with processed data is returned.
-    steps : None or list of str, default: None
-        Which steps to run, if no preset is used.
-        Pontential steps : "num_spikes", "snr", "remove_contaminated", "unit_locations", "correlogram",
-        "template_similarity", "presence_distance", "cross_contamination", "knn", "quality_score"
-        Please check steps explanations above!
-    presence_distance_kwargs : None|dict, default: None
-        A dictionary of kwargs to be passed to compute_presence_distance().
-
-    Returns
-    -------
-    potential_merges:
-        A list of tuples of 2 elements (if `resolve_graph`if false) or 2+ elements (if `resolve_graph` is true).
-        List of pairs that could be merged.
-    outs:
-        Returned only when extra_outputs=True
-        A dictionary that contains data for debugging and plotting.
-
-    References
-    ----------
-    This function is inspired and built upon similar functions from Lussac [Llobet]_,
-    done by Aurelien Wyngaard and Victor Llobet.
-    https://github.com/BarbourLab/lussac/blob/v1.0.0/postprocessing/merge_units.py
-    """
-    warnings.warn(
-        "get_potential_auto_merge() is deprecated. Use compute_merge_unit_groups() instead",
-        DeprecationWarning,
-        stacklevel=2,
-    )
-
-    presence_distance_kwargs = presence_distance_kwargs or dict()
-    knn_kwargs = knn_kwargs or dict()
-    return compute_merge_unit_groups(
-        sorting_analyzer,
-        preset,
-        resolve_graph,
-        steps_params={
-            "num_spikes": {"min_spikes": min_spikes},
-            "snr": {"min_snr": min_snr},
-            "remove_contaminated": {
-                "contamination_thresh": contamination_thresh,
-                "refractory_period_ms": refractory_period_ms,
-                "censored_period_ms": censored_period_ms,
-            },
-            "unit_locations": {"max_distance_um": max_distance_um},
-            "correlogram": {
-                "corr_diff_thresh": corr_diff_thresh,
-                "censor_correlograms_ms": censor_correlograms_ms,
-                "sigma_smooth_ms": sigma_smooth_ms,
-                "adaptative_window_thresh": adaptative_window_thresh,
-            },
-            "template_similarity": {"template_diff_thresh": template_diff_thresh},
-            "presence_distance": {"presence_distance_thresh": presence_distance_thresh, **presence_distance_kwargs},
-            "knn": {"k_nn": k_nn, **knn_kwargs},
-            "cross_contamination": {
-                "cc_thresh": cc_thresh,
-                "p_value": p_value,
-                "refractory_period_ms": refractory_period_ms,
-                "censored_period_ms": censored_period_ms,
-            },
-            "quality_score": {
-                "firing_contamination_balance": firing_contamination_balance,
-                "refractory_period_ms": refractory_period_ms,
-                "censored_period_ms": censored_period_ms,
-            },
-        },
-        compute_needed_extensions=True,
-        extra_outputs=extra_outputs,
-        steps=steps,
-    )
+        return potential_merges
 
 
 def get_pairs_via_nntree(sorting_analyzer, k_nn=5, pair_mask=None, **knn_kwargs):
@@ -857,10 +661,10 @@ def check_improve_contaminations_score(
         f_new = compute_firing_rates(sorting_analyzer_new)[unit_id1]
 
         # old and new scores
-        k = 1 + firing_contamination_balance
-        score_1 = f_1 * (1 - k * c_1)
-        score_2 = f_2 * (1 - k * c_2)
-        score_new = f_new * (1 - k * c_new)
+        k = firing_contamination_balance
+        score_1 = f_1 * (1 - (k + 1) * c_1)
+        score_2 = f_2 * (1 - (k + 1) * c_2)
+        score_new = f_new * (1 - (k + 1) * c_new)
 
         if score_new < score_1 or score_new < score_2:
             # the score is not improved
diff --git a/src/spikeinterface/preprocessing/silence_periods.py b/src/spikeinterface/preprocessing/silence_periods.py
index 85169011d..8f38f0146 100644
--- a/src/spikeinterface/preprocessing/silence_periods.py
+++ b/src/spikeinterface/preprocessing/silence_periods.py
@@ -71,10 +71,8 @@ class SilencedPeriodsRecording(BasePreprocessor):
 
         if mode in ["noise"]:
             if noise_levels is None:
-                random_slices_kwargs = random_chunk_kwargs.copy()
-                random_slices_kwargs["seed"] = seed
                 noise_levels = get_noise_levels(
-                    recording, return_scaled=False, random_slices_kwargs=random_slices_kwargs
+                    recording, return_scaled=False, concatenated=True, seed=seed, **random_chunk_kwargs
                 )
             noise_generator = NoiseGeneratorRecording(
                 num_channels=recording.get_num_channels(),


Test patch:
diff --git a/src/spikeinterface/core/tests/test_analyzer_extension_core.py b/src/spikeinterface/core/tests/test_analyzer_extension_core.py
index 6f5bef3c6..626899ab6 100644
--- a/src/spikeinterface/core/tests/test_analyzer_extension_core.py
+++ b/src/spikeinterface/core/tests/test_analyzer_extension_core.py
@@ -2,8 +2,6 @@ import pytest
 
 import shutil
 
-from pathlib import Path
-
 from spikeinterface.core import generate_ground_truth_recording
 from spikeinterface.core import create_sorting_analyzer
 from spikeinterface.core import Templates
@@ -252,17 +250,16 @@ def test_compute_several(create_cache_folder):
 
 
 if __name__ == "__main__":
-    cache_folder = Path(__file__).resolve().parents[4] / "cache_folder" / "core"
-    # test_ComputeWaveforms(format="memory", sparse=True, create_cache_folder=cache_folder)
-    # test_ComputeWaveforms(format="memory", sparse=False, create_cache_folder=cache_folder)
-    # test_ComputeWaveforms(format="binary_folder", sparse=True, create_cache_folder=cache_folder)
-    # test_ComputeWaveforms(format="binary_folder", sparse=False, create_cache_folder=cache_folder)
-    # test_ComputeWaveforms(format="zarr", sparse=True, create_cache_folder=cache_folder)
-    # test_ComputeWaveforms(format="zarr", sparse=False, create_cache_folder=cache_folder)
-    # test_ComputeRandomSpikes(format="memory", sparse=True, create_cache_folder=cache_folder)
-    test_ComputeRandomSpikes(format="binary_folder", sparse=False, create_cache_folder=cache_folder)
-    test_ComputeTemplates(format="memory", sparse=True, create_cache_folder=cache_folder)
-    test_ComputeNoiseLevels(format="memory", sparse=False, create_cache_folder=cache_folder)
-
-    # test_get_children_dependencies()
-    # test_delete_on_recompute(cache_folder)
+
+    test_ComputeWaveforms(format="memory", sparse=True)
+    test_ComputeWaveforms(format="memory", sparse=False)
+    test_ComputeWaveforms(format="binary_folder", sparse=True)
+    test_ComputeWaveforms(format="binary_folder", sparse=False)
+    test_ComputeWaveforms(format="zarr", sparse=True)
+    test_ComputeWaveforms(format="zarr", sparse=False)
+    test_ComputeRandomSpikes(format="memory", sparse=True)
+    test_ComputeTemplates(format="memory", sparse=True)
+    test_ComputeNoiseLevels(format="memory", sparse=False)
+
+    test_get_children_dependencies()
+    test_delete_on_recompute()
diff --git a/src/spikeinterface/core/tests/test_recording_tools.py b/src/spikeinterface/core/tests/test_recording_tools.py
index dad5273f1..23a1574f2 100644
--- a/src/spikeinterface/core/tests/test_recording_tools.py
+++ b/src/spikeinterface/core/tests/test_recording_tools.py
@@ -11,7 +11,6 @@ from spikeinterface.core.generate import NoiseGeneratorRecording
 from spikeinterface.core.recording_tools import (
     write_binary_recording,
     write_memory_recording,
-    get_random_recording_slices,
     get_random_data_chunks,
     get_chunk_with_margin,
     get_closest_channels,
@@ -168,17 +167,6 @@ def test_write_memory_recording():
         shm.unlink()
 
 
-def test_get_random_recording_slices():
-    rec = generate_recording(num_channels=1, sampling_frequency=1000.0, durations=[10.0, 20.0])
-    rec_slices = get_random_recording_slices(
-        rec, method="full_random", num_chunks_per_segment=20, chunk_duration="500ms", margin_frames=0, seed=0
-    )
-    assert len(rec_slices) == 40
-    for seg_ind, start, stop in rec_slices:
-        assert stop - start == 500
-        assert seg_ind in (0, 1)
-
-
 def test_get_random_data_chunks():
     rec = generate_recording(num_channels=1, sampling_frequency=1000.0, durations=[10.0, 20.0])
     chunks = get_random_data_chunks(rec, num_chunks_per_segment=50, chunk_size=500, seed=0)
@@ -194,17 +182,16 @@ def test_get_closest_channels():
 
 
 def test_get_noise_levels():
-    job_kwargs = dict(n_jobs=1, progress_bar=True)
     rec = generate_recording(num_channels=2, sampling_frequency=1000.0, durations=[60.0])
 
-    noise_levels_1 = get_noise_levels(rec, return_scaled=False, **job_kwargs)
-    noise_levels_2 = get_noise_levels(rec, return_scaled=False, **job_kwargs)
+    noise_levels_1 = get_noise_levels(rec, return_scaled=False)
+    noise_levels_2 = get_noise_levels(rec, return_scaled=False)
 
     rec.set_channel_gains(0.1)
     rec.set_channel_offsets(0)
-    noise_levels = get_noise_levels(rec, return_scaled=True, force_recompute=True, **job_kwargs)
+    noise_levels = get_noise_levels(rec, return_scaled=True, force_recompute=True)
 
-    noise_levels = get_noise_levels(rec, return_scaled=True, method="std", **job_kwargs)
+    noise_levels = get_noise_levels(rec, return_scaled=True, method="std")
 
     # Generate a recording following a gaussian distribution to check the result of get_noise.
     std = 6.0
@@ -214,10 +201,8 @@ def test_get_noise_levels():
     recording = NumpyRecording(traces, 30000)
 
     assert np.all(noise_levels_1 == noise_levels_2)
-    assert np.allclose(get_noise_levels(recording, return_scaled=False, **job_kwargs), [std, std], rtol=1e-2, atol=1e-3)
-    assert np.allclose(
-        get_noise_levels(recording, method="std", return_scaled=False, **job_kwargs), [std, std], rtol=1e-2, atol=1e-3
-    )
+    assert np.allclose(get_noise_levels(recording, return_scaled=False), [std, std], rtol=1e-2, atol=1e-3)
+    assert np.allclose(get_noise_levels(recording, method="std", return_scaled=False), [std, std], rtol=1e-2, atol=1e-3)
 
 
 def test_get_noise_levels_output():
@@ -231,21 +216,10 @@ def test_get_noise_levels_output():
     traces = rng.normal(loc=10.0, scale=std, size=(num_samples, num_channels))
     recording = NumpyRecording(traces_list=traces, sampling_frequency=sampling_frequency)
 
-    std_estimated_with_mad = get_noise_levels(
-        recording,
-        method="mad",
-        return_scaled=False,
-        random_slices_kwargs=dict(num_chunks_per_segment=40, chunk_size=1_000, seed=seed),
-    )
-    print(std_estimated_with_mad)
+    std_estimated_with_mad = get_noise_levels(recording, method="mad", return_scaled=False, chunk_size=1_000)
     assert np.allclose(std_estimated_with_mad, [std, std], rtol=1e-2, atol=1e-3)
 
-    std_estimated_with_std = get_noise_levels(
-        recording,
-        method="std",
-        return_scaled=False,
-        random_slices_kwargs=dict(num_chunks_per_segment=40, chunk_size=1_000, seed=seed),
-    )
+    std_estimated_with_std = get_noise_levels(recording, method="std", return_scaled=False, chunk_size=1_000)
     assert np.allclose(std_estimated_with_std, [std, std], rtol=1e-2, atol=1e-3)
 
 
@@ -359,16 +333,14 @@ def test_do_recording_attributes_match():
 
 if __name__ == "__main__":
     # Create a temporary folder using the standard library
-    # import tempfile
-
-    # with tempfile.TemporaryDirectory() as tmpdirname:
-    #     tmp_path = Path(tmpdirname)
-    #     test_write_binary_recording(tmp_path)
-    # test_write_memory_recording()
-
-    test_get_random_recording_slices()
-    # test_get_random_data_chunks()
-    # test_get_closest_channels()
-    # test_get_noise_levels()
-    # test_get_noise_levels_output()
-    # test_order_channels_by_depth()
+    import tempfile
+
+    with tempfile.TemporaryDirectory() as tmpdirname:
+        tmp_path = Path(tmpdirname)
+        test_write_binary_recording(tmp_path)
+    test_write_memory_recording()
+
+    test_get_random_data_chunks()
+    test_get_closest_channels()
+    test_get_noise_levels()
+    test_order_channels_by_depth()
diff --git a/src/spikeinterface/curation/tests/test_auto_merge.py b/src/spikeinterface/curation/tests/test_auto_merge.py
index 4c05f41a4..33fd06d27 100644
--- a/src/spikeinterface/curation/tests/test_auto_merge.py
+++ b/src/spikeinterface/curation/tests/test_auto_merge.py
@@ -3,16 +3,16 @@ import pytest
 
 from spikeinterface.core import create_sorting_analyzer
 from spikeinterface.core.generate import inject_some_split_units
-from spikeinterface.curation import compute_merge_unit_groups, auto_merge
+from spikeinterface.curation import get_potential_auto_merge
 
 
 from spikeinterface.curation.tests.common import make_sorting_analyzer, sorting_analyzer_for_curation
 
 
 @pytest.mark.parametrize(
-    "preset", ["x_contaminations", "feature_neighbors", "temporal_splits", "similarity_correlograms", None]
+    "preset", ["x_contaminations", "feature_neighbors", "temporal_splits", "similarity_correlograms"]
 )
-def test_compute_merge_unit_groups(sorting_analyzer_for_curation, preset):
+def test_get_auto_merge_list(sorting_analyzer_for_curation, preset):
 
     print(sorting_analyzer_for_curation)
     sorting = sorting_analyzer_for_curation.sorting
@@ -47,38 +47,32 @@ def test_compute_merge_unit_groups(sorting_analyzer_for_curation, preset):
     )
 
     if preset is not None:
-        # do not resolve graph for checking true pairs
-        merge_unit_groups, outs = compute_merge_unit_groups(
+        potential_merges, outs = get_potential_auto_merge(
             sorting_analyzer,
             preset=preset,
-            resolve_graph=False,
-            # min_spikes=1000,
-            # max_distance_um=150.0,
-            # contamination_thresh=0.2,
-            # corr_diff_thresh=0.16,
-            # template_diff_thresh=0.25,
-            # censored_period_ms=0.0,
-            # refractory_period_ms=4.0,
-            # sigma_smooth_ms=0.6,
-            # adaptative_window_thresh=0.5,
-            # firing_contamination_balance=1.5,
+            min_spikes=1000,
+            max_distance_um=150.0,
+            contamination_thresh=0.2,
+            corr_diff_thresh=0.16,
+            template_diff_thresh=0.25,
+            censored_period_ms=0.0,
+            refractory_period_ms=4.0,
+            sigma_smooth_ms=0.6,
+            adaptative_window_thresh=0.5,
+            firing_contamination_balance=1.5,
             extra_outputs=True,
-            **job_kwargs,
         )
         if preset == "x_contaminations":
-            assert len(merge_unit_groups) == num_unit_splited
+            assert len(potential_merges) == num_unit_splited
             for true_pair in other_ids.values():
                 true_pair = tuple(true_pair)
-                assert true_pair in merge_unit_groups
+                assert true_pair in potential_merges
     else:
         # when preset is None you have to specify the steps
         with pytest.raises(ValueError):
-            merge_unit_groups = compute_merge_unit_groups(sorting_analyzer, preset=preset)
-        merge_unit_groups = compute_merge_unit_groups(
-            sorting_analyzer,
-            preset=preset,
-            steps=["num_spikes", "snr", "remove_contaminated", "unit_locations"],
-            **job_kwargs,
+            potential_merges = get_potential_auto_merge(sorting_analyzer, preset=preset)
+        potential_merges = get_potential_auto_merge(
+            sorting_analyzer, preset=preset, steps=["min_spikes", "min_snr", "remove_contaminated", "unit_positions"]
         )
 
     # DEBUG
@@ -99,7 +93,7 @@ def test_compute_merge_unit_groups(sorting_analyzer_for_curation, preset):
 
     # m = correlograms.shape[2] // 2
 
-    # for unit_id1, unit_id2 in merge_unit_groups[:5]:
+    # for unit_id1, unit_id2 in potential_merges[:5]:
     #     unit_ind1 = sorting_with_split.id_to_index(unit_id1)
     #     unit_ind2 = sorting_with_split.id_to_index(unit_id2)
 
@@ -135,6 +129,4 @@ def test_compute_merge_unit_groups(sorting_analyzer_for_curation, preset):
 
 if __name__ == "__main__":
     sorting_analyzer = make_sorting_analyzer(sparse=True)
-    # preset = "x_contaminations"
-    preset = None
-    test_compute_merge_unit_groups(sorting_analyzer, preset=preset)
+    test_get_auto_merge_list(sorting_analyzer)
diff --git a/src/spikeinterface/preprocessing/tests/test_scaling.py b/src/spikeinterface/preprocessing/tests/test_scaling.py
index e32d96901..321d7c9df 100644
--- a/src/spikeinterface/preprocessing/tests/test_scaling.py
+++ b/src/spikeinterface/preprocessing/tests/test_scaling.py
@@ -55,11 +55,11 @@ def test_scaling_in_preprocessing_chain():
     recording.set_channel_gains(gains)
     recording.set_channel_offsets(offsets)
 
-    centered_recording = CenterRecording(scale_to_uV(recording=recording), seed=2205)
+    centered_recording = CenterRecording(scale_to_uV(recording=recording))
     traces_scaled_with_argument = centered_recording.get_traces(return_scaled=True)
 
     # Chain preprocessors
-    centered_recording_scaled = CenterRecording(scale_to_uV(recording=recording), seed=2205)
+    centered_recording_scaled = CenterRecording(scale_to_uV(recording=recording))
     traces_scaled_with_preprocessor = centered_recording_scaled.get_traces()
 
     np.testing.assert_allclose(traces_scaled_with_argument, traces_scaled_with_preprocessor)
@@ -68,8 +68,3 @@ def test_scaling_in_preprocessing_chain():
     traces_scaled_with_preprocessor_and_argument = centered_recording_scaled.get_traces(return_scaled=True)
 
     np.testing.assert_allclose(traces_scaled_with_preprocessor, traces_scaled_with_preprocessor_and_argument)
-
-
-if __name__ == "__main__":
-    test_scale_to_uV()
-    test_scaling_in_preprocessing_chain()
diff --git a/src/spikeinterface/preprocessing/tests/test_silence.py b/src/spikeinterface/preprocessing/tests/test_silence.py
index 20d4f6dfc..6c2e8ec8b 100644
--- a/src/spikeinterface/preprocessing/tests/test_silence.py
+++ b/src/spikeinterface/preprocessing/tests/test_silence.py
@@ -9,8 +9,6 @@ from spikeinterface.core import get_noise_levels
 
 import numpy as np
 
-from pathlib import Path
-
 
 def test_silence(create_cache_folder):
 
@@ -48,5 +46,4 @@ def test_silence(create_cache_folder):
 
 
 if __name__ == "__main__":
-    cache_folder = Path(__file__).resolve().parents[4] / "cache_folder"
-    test_silence(cache_folder)
+    test_silence()
diff --git a/src/spikeinterface/preprocessing/tests/test_whiten.py b/src/spikeinterface/preprocessing/tests/test_whiten.py
index b40627d83..04b731de4 100644
--- a/src/spikeinterface/preprocessing/tests/test_whiten.py
+++ b/src/spikeinterface/preprocessing/tests/test_whiten.py
@@ -5,15 +5,13 @@ from spikeinterface.core import generate_recording
 
 from spikeinterface.preprocessing import whiten, scale, compute_whitening_matrix
 
-from pathlib import Path
-
 
 def test_whiten(create_cache_folder):
     cache_folder = create_cache_folder
     rec = generate_recording(num_channels=4, seed=2205)
 
     print(rec.get_channel_locations())
-    random_chunk_kwargs = {"seed": 2205}
+    random_chunk_kwargs = {}
     W1, M = compute_whitening_matrix(rec, "global", random_chunk_kwargs, apply_mean=False, radius_um=None)
     # print(W)
     # print(M)
@@ -49,5 +47,4 @@ def test_whiten(create_cache_folder):
 
 
 if __name__ == "__main__":
-    cache_folder = Path(__file__).resolve().parents[4] / "cache_folder"
-    test_whiten(cache_folder)
+    test_whiten()