Repository: SpikeInterface/spikeinterface
Pull request #1764 (2023-07-03 13:43:35)

Add docker container tests


Patch:
diff --git a/examples/modules_gallery/core/plot_1_recording_extractor.py b/examples/modules_gallery/core/plot_1_recording_extractor.py
index f20bf6497..a7c2403c4 100644
--- a/examples/modules_gallery/core/plot_1_recording_extractor.py
+++ b/examples/modules_gallery/core/plot_1_recording_extractor.py
@@ -79,7 +79,7 @@ se.BinaryRecordingExtractor.write_recording(recording, file_paths)
 # Note that this new recording is now "on disk" and not "in memory" as the Numpy recording.
 # This means that the loading is "lazy" and the data are not loaded in memory.
 
-recording2 = se.BinaryRecordingExtractor(file_paths=file_paths, sampling_frequency=sampling_frequency, num_channels=num_channels, dtype=traces0.dtype)
+recording2 = se.BinaryRecordingExtractor(file_paths, sampling_frequency, num_channels, traces0.dtype)
 print(recording2)
 
 ##############################################################################
diff --git a/src/spikeinterface/core/baserecording.py b/src/spikeinterface/core/baserecording.py
index e3baf12e8..d4d0beb5f 100644
--- a/src/spikeinterface/core/baserecording.py
+++ b/src/spikeinterface/core/baserecording.py
@@ -450,7 +450,7 @@ class BaseRecording(BaseRecordingSnippets):
             binary_rec = BinaryRecordingExtractor(
                 file_paths=file_paths,
                 sampling_frequency=self.get_sampling_frequency(),
-                num_channels=self.get_num_channels(),
+                num_chan=self.get_num_channels(),
                 dtype=dtype,
                 t_starts=t_starts,
                 channel_ids=self.get_channel_ids(),
diff --git a/src/spikeinterface/core/binaryrecordingextractor.py b/src/spikeinterface/core/binaryrecordingextractor.py
index f56cc2866..c04a1c6ec 100644
--- a/src/spikeinterface/core/binaryrecordingextractor.py
+++ b/src/spikeinterface/core/binaryrecordingextractor.py
@@ -1,12 +1,13 @@
 from typing import List, Union
 import mmap
-import warnings
+
+import shutil
 from pathlib import Path
 
 import numpy as np
 
 from .baserecording import BaseRecording, BaseRecordingSegment
-from .core_tools import write_binary_recording, define_function_from_class
+from .core_tools import read_binary_recording, write_binary_recording, define_function_from_class
 from .job_tools import _shared_job_kwargs_doc
 
 
@@ -20,9 +21,7 @@ class BinaryRecordingExtractor(BaseRecording):
         Path to the binary file
     sampling_frequency: float
         The sampling frequency
-    num_channels: int
-        Number of channels
-    num_chan: int [deprecated, use num_channels instead, will be removed as early as v0.100.0]
+    num_chan: int
         Number of channels
     dtype: str or dtype
         The dtype of the binary file
@@ -41,10 +40,6 @@ class BinaryRecordingExtractor(BaseRecording):
     is_filtered: bool or None
         If True, the recording is assumed to be filtered. If None, is_filtered is not set.
 
-    Notes
-    -----
-    When both num_channels and num_chan are provided, `num_channels` is used and `num_chan` is ignored.
-
     Returns
     -------
     recording: BinaryRecordingExtractor
@@ -60,8 +55,8 @@ class BinaryRecordingExtractor(BaseRecording):
         self,
         file_paths,
         sampling_frequency,
+        num_chan,
         dtype,
-        num_channels=None,
         t_starts=None,
         channel_ids=None,
         time_axis=0,
@@ -69,41 +64,34 @@ class BinaryRecordingExtractor(BaseRecording):
         gain_to_uV=None,
         offset_to_uV=None,
         is_filtered=None,
-        num_chan=None,
     ):
-        # This assigns num_channels if num_channels is not None, otherwise num_chan is assigned
-        num_channels = num_channels or num_chan
-        assert num_channels is not None, "You must provide num_channels or num_chan"
-        if num_chan is not None:
-            warnings.warn("`num_chan` is to be deprecated in version 0.100, please use `num_channels` instead")
-
         if channel_ids is None:
-            channel_ids = list(range(num_channels))
+            channel_ids = list(range(num_chan))
         else:
-            assert len(channel_ids) == num_channels, "Provided recording channels have the wrong length"
+            assert len(channel_ids) == num_chan, "Provided recording channels have the wrong length"
 
         BaseRecording.__init__(self, sampling_frequency, channel_ids, dtype)
 
         if isinstance(file_paths, list):
             # several segment
-            file_path_list = [Path(p) for p in file_paths]
+            datfiles = [Path(p) for p in file_paths]
         else:
             # one segment
-            file_path_list = [Path(file_paths)]
+            datfiles = [Path(file_paths)]
 
         if t_starts is not None:
-            assert len(t_starts) == len(file_path_list), "t_starts must be a list of same size than file_paths"
+            assert len(t_starts) == len(datfiles), "t_starts must be a list of same size than file_paths"
             t_starts = [float(t_start) for t_start in t_starts]
 
         dtype = np.dtype(dtype)
 
-        for i, file_path in enumerate(file_path_list):
+        for i, datfile in enumerate(datfiles):
             if t_starts is None:
                 t_start = None
             else:
                 t_start = t_starts[i]
             rec_segment = BinaryRecordingSegment(
-                file_path, sampling_frequency, t_start, num_channels, dtype, time_axis, file_offset
+                datfile, sampling_frequency, t_start, num_chan, dtype, time_axis, file_offset
             )
             self.add_recording_segment(rec_segment)
 
@@ -117,11 +105,10 @@ class BinaryRecordingExtractor(BaseRecording):
             self.set_channel_offsets(offset_to_uV)
 
         self._kwargs = {
-            "file_paths": [str(e.absolute()) for e in file_path_list],
+            "file_paths": [str(e.absolute()) for e in datfiles],
             "sampling_frequency": sampling_frequency,
             "t_starts": t_starts,
-            "num_channels": num_channels,
-            "num_chan": num_channels,  # TODO: This should be here at least till version 0.100.0
+            "num_chan": num_chan,
             "dtype": dtype.str,
             "channel_ids": channel_ids,
             "time_axis": time_axis,
@@ -155,7 +142,7 @@ class BinaryRecordingExtractor(BaseRecording):
         d = dict(
             file_paths=self._kwargs["file_paths"],
             dtype=np.dtype(self._kwargs["dtype"]),
-            num_channels=self._kwargs["num_channels"],
+            num_channels=self._kwargs["num_chan"],
             time_axis=self._kwargs["time_axis"],
             file_offset=self._kwargs["file_offset"],
         )
@@ -168,23 +155,23 @@ BinaryRecordingExtractor.write_recording.__doc__ = BinaryRecordingExtractor.writ
 
 
 class BinaryRecordingSegment(BaseRecordingSegment):
-    def __init__(self, datfile, sampling_frequency, t_start, num_channels, dtype, time_axis, file_offset):
+    def __init__(self, datfile, sampling_frequency, t_start, num_chan, dtype, time_axis, file_offset):
         BaseRecordingSegment.__init__(self, sampling_frequency=sampling_frequency, t_start=t_start)
-        self.num_channels = num_channels
+        self.num_chan = num_chan
         self.dtype = np.dtype(dtype)
         self.file_offset = file_offset
         self.time_axis = time_axis
         self.datfile = datfile
         self.file = open(self.datfile, "r")
-        self.num_samples = (Path(datfile).stat().st_size - file_offset) // (num_channels * np.dtype(dtype).itemsize)
+        self.num_samples = (Path(datfile).stat().st_size - file_offset) // (num_chan * np.dtype(dtype).itemsize)
         if self.time_axis == 0:
-            self.shape = (self.num_samples, self.num_channels)
+            self.shape = (self.num_samples, self.num_chan)
         else:
-            self.shape = (self.num_channels, self.num_samples)
+            self.shape = (self.num_chan, self.num_samples)
 
         byte_offset = self.file_offset
         dtype_size_bytes = self.dtype.itemsize
-        data_size_bytes = dtype_size_bytes * self.num_samples * self.num_channels
+        data_size_bytes = dtype_size_bytes * self.num_samples * self.num_chan
         self.memmap_offset, self.array_offset = divmod(byte_offset, mmap.ALLOCATIONGRANULARITY)
         self.memmap_length = data_size_bytes + self.array_offset
 
diff --git a/src/spikeinterface/extractors/shybridextractors.py b/src/spikeinterface/extractors/shybridextractors.py
index cb48e3d20..434f7f5df 100644
--- a/src/spikeinterface/extractors/shybridextractors.py
+++ b/src/spikeinterface/extractors/shybridextractors.py
@@ -65,7 +65,7 @@ class SHYBRIDRecordingExtractor(BinaryRecordingExtractor):
             self,
             file_paths=bin_file,
             sampling_frequency=float(params["fs"]),
-            num_channels=nb_channels,
+            num_chan=nb_channels,
             dtype=params["dtype"],
             time_axis=time_axis,
         )


Test patch:
diff --git a/.github/workflows/test_containers_docker.yml b/.github/workflows/test_containers_docker.yml
new file mode 100644
index 000000000..b9e26fecf
--- /dev/null
+++ b/.github/workflows/test_containers_docker.yml
@@ -0,0 +1,40 @@
+name: Test sorter images in Docker
+
+on: workflow_dispatch
+
+
+jobs:
+  test-images:
+    env:
+      SPIKEINTERFACE_DEV_PATH: ${{ github.workspace }}
+    name: Test on (${{ matrix.os }})
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: ["ubuntu-latest", "windows-latest"]
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: '3.10'
+      - name: Python version
+        run:
+          python --version
+      - name: Docker version
+        run:
+          docker --version
+      - name: Install dependencies
+        run: |
+          python -m pip install -U pip  # Official recommended way
+          pip install pytest
+          pip install -e .[full]
+          pip install docker
+      - name: Test that containers install the local CI version of spikeinterface
+        run: |
+          echo $SPIKEINTERFACE_DEV_PATH
+          python -c "import os; assert os.getenv('SPIKEINTERFACE_DEV_PATH') is not None"
+          ls -l
+      - name: Run test singularity containers
+        run: |
+          pytest -vv --capture=tee-sys -rA src/spikeinterface/sorters/external/tests/test_docker_containers.py
diff --git a/.github/workflows/test_containers_singularity.yml b/.github/workflows/test_containers_singularity.yml
index baa591dc1..be97cd864 100644
--- a/.github/workflows/test_containers_singularity.yml
+++ b/.github/workflows/test_containers_singularity.yml
@@ -28,12 +28,6 @@ jobs:
       - name: Singularity version
         run:
           singularity --version
-      - name: Check Singularity cache
-        run:
-          singularity cache list -v
-      - name: Clear Singularity cache
-        run:
-          singularity cache clean --force
       - name: Install dependencies
         run: |
           python -m pip install -U pip  # Official recommended way
diff --git a/src/spikeinterface/core/tests/test_baserecording.py b/src/spikeinterface/core/tests/test_baserecording.py
index ed9a79d05..230ff40b4 100644
--- a/src/spikeinterface/core/tests/test_baserecording.py
+++ b/src/spikeinterface/core/tests/test_baserecording.py
@@ -34,9 +34,7 @@ def test_BaseRecording():
     for i in range(num_seg):
         a = np.memmap(file_paths[i], dtype=dtype, mode="w+", shape=(num_samples, num_chan))
         a[:] = np.random.randn(*a.shape).astype(dtype)
-    rec = BinaryRecordingExtractor(
-        file_paths=file_paths, sampling_frequency=sampling_frequency, num_channels=num_chan, dtype=dtype
-    )
+    rec = BinaryRecordingExtractor(file_paths, sampling_frequency, num_chan, dtype)
 
     assert rec.get_num_segments() == 2
     assert rec.get_num_channels() == 3
@@ -230,25 +228,14 @@ def test_BaseRecording():
     assert np.dtype(rec_float32.get_traces().dtype) == np.float32
 
     # test with t_start
-    rec = BinaryRecordingExtractor(
-        file_paths=file_paths,
-        sampling_frequency=sampling_frequency,
-        num_channels=num_chan,
-        dtype=dtype,
-        t_starts=np.arange(num_seg) * 10.0,
-    )
+    rec = BinaryRecordingExtractor(file_paths, sampling_frequency, num_chan, dtype, t_starts=np.arange(num_seg) * 10.0)
     times1 = rec.get_times(1)
     folder = cache_folder / "recording_with_t_start"
     rec2 = rec.save(folder=folder)
     assert np.allclose(times1, rec2.get_times(1))
 
     # test with time_vector
-    rec = BinaryRecordingExtractor(
-        file_paths=file_paths,
-        sampling_frequency=sampling_frequency,
-        num_channels=num_chan,
-        dtype=dtype,
-    )
+    rec = BinaryRecordingExtractor(file_paths, sampling_frequency, num_chan, dtype)
     rec.set_times(np.arange(num_samples) / sampling_frequency + 30.0, segment_index=0)
     rec.set_times(np.arange(num_samples) / sampling_frequency + 40.0, segment_index=1)
     times1 = rec.get_times(1)
diff --git a/src/spikeinterface/core/tests/test_binaryrecordingextractor.py b/src/spikeinterface/core/tests/test_binaryrecordingextractor.py
index fb4c3ee3c..1d2c6e4c2 100644
--- a/src/spikeinterface/core/tests/test_binaryrecordingextractor.py
+++ b/src/spikeinterface/core/tests/test_binaryrecordingextractor.py
@@ -13,21 +13,17 @@ else:
 
 def test_BinaryRecordingExtractor():
     num_seg = 2
-    num_channels = 3
+    num_chan = 3
     num_samples = 30
     sampling_frequency = 10000
     dtype = "int16"
 
     file_paths = [cache_folder / f"test_BinaryRecordingExtractor_{i}.raw" for i in range(num_seg)]
     for i in range(num_seg):
-        np.memmap(file_paths[i], dtype=dtype, mode="w+", shape=(num_samples, num_channels))
+        np.memmap(file_paths[i], dtype=dtype, mode="w+", shape=(num_samples, num_chan))
 
-    rec = BinaryRecordingExtractor(
-        file_paths=file_paths,
-        sampling_frequency=sampling_frequency,
-        num_channels=num_channels,
-        dtype=dtype,
-    )
+    rec = BinaryRecordingExtractor(file_paths, sampling_frequency, num_chan, dtype)
+    print(rec)
 
     file_paths = [cache_folder / f"test_BinaryRecordingExtractor_copied_{i}.raw" for i in range(num_seg)]
     BinaryRecordingExtractor.write_recording(rec, file_paths)
@@ -48,12 +44,9 @@ def test_round_trip(tmp_path):
     BinaryRecordingExtractor.write_recording(recording=recording, dtype=dtype, file_paths=file_path)
 
     sampling_frequency = recording.get_sampling_frequency()
-    num_channels = recording.get_num_channels()
+    num_chan = recording.get_num_channels()
     binary_recorder = BinaryRecordingExtractor(
-        file_paths=file_path,
-        sampling_frequency=sampling_frequency,
-        num_channels=num_channels,
-        dtype=dtype,
+        file_paths=file_path, sampling_frequency=sampling_frequency, num_chan=num_chan, dtype=dtype
     )
 
     assert np.allclose(recording.get_traces(), binary_recorder.get_traces())
diff --git a/src/spikeinterface/core/tests/test_channelslicerecording.py b/src/spikeinterface/core/tests/test_channelslicerecording.py
index 08bb22a2c..c5ba89bd8 100644
--- a/src/spikeinterface/core/tests/test_channelslicerecording.py
+++ b/src/spikeinterface/core/tests/test_channelslicerecording.py
@@ -25,12 +25,7 @@ def test_ChannelSliceRecording():
     for i in range(num_seg):
         traces = np.memmap(file_paths[i], dtype=dtype, mode="w+", shape=(num_samples, num_chan))
         traces[:] = np.arange(3)[None, :]
-    rec = BinaryRecordingExtractor(
-        file_paths=file_paths,
-        sampling_frequency=sampling_frequency,
-        num_channels=num_chan,
-        dtype=dtype,
-    )
+    rec = BinaryRecordingExtractor(file_paths, sampling_frequency, num_chan, dtype)
 
     # keep original ids
     rec_sliced = ChannelSliceRecording(rec, channel_ids=[0, 2])
diff --git a/src/spikeinterface/sorters/external/tests/test_docker_containers.py b/src/spikeinterface/sorters/external/tests/test_docker_containers.py
new file mode 100644
index 000000000..03f7c8d4c
--- /dev/null
+++ b/src/spikeinterface/sorters/external/tests/test_docker_containers.py
@@ -0,0 +1,106 @@
+import os
+import shutil
+
+import pytest
+from pathlib import Path
+
+import spikeinterface.extractors as se
+import spikeinterface.sorters as ss
+
+if hasattr(pytest, "global_test_folder"):
+    cache_folder = pytest.global_test_folder / "sorters"
+else:
+    cache_folder = Path("cache_folder") / "sorters"
+
+
+ON_GITHUB = os.getenv("CI")
+
+
+def check_gh_settings():
+    if ON_GITHUB:
+        si_dev_path = os.getenv("SPIKEINTERFACE_DEV_PATH")
+        assert si_dev_path is not None, "Tests on GITHUB CI must run with the SPIKEINTERFACE_DEV_PATH"
+
+
+def generate_run_kwargs():
+    test_recording, _ = se.toy_example(duration=30, seed=0, num_channels=64, num_segments=1)
+    test_recording = test_recording.save(name="toy")
+    test_recording.set_channel_gains(1)
+    test_recording.set_channel_offsets(1)
+    run_kwargs = dict(recording=test_recording, verbose=True)
+    run_kwargs["docker_image"] = True
+    return run_kwargs
+
+
+@pytest.fixture(scope="module")
+def run_kwargs():
+    check_gh_settings()
+    return generate_run_kwargs()
+
+
+def test_spykingcircus(run_kwargs):
+    sorting = ss.run_sorter("spykingcircus", output_folder=cache_folder / "spykingcircus", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_mountainsort4(run_kwargs):
+    sorting = ss.run_sorter("mountainsort4", output_folder=cache_folder / "mountainsort4", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_mountainsort5(run_kwargs):
+    sorting = ss.run_sorter("mountainsort5", output_folder=cache_folder / "mountainsort5", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_tridesclous(run_kwargs):
+    sorting = ss.run_sorter("tridesclous", output_folder=cache_folder / "tridesclous", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_ironclust(run_kwargs):
+    sorting = ss.run_sorter("ironclust", output_folder=cache_folder / "ironclust", fGpu=False, **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_waveclus(run_kwargs):
+    sorting = ss.run_sorter(sorter_name="waveclus", output_folder=cache_folder / "waveclus", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_hdsort(run_kwargs):
+    sorting = ss.run_sorter(sorter_name="hdsort", output_folder=cache_folder / "hdsort", **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_kilosort1(run_kwargs):
+    sorting = ss.run_sorter(sorter_name="kilosort", output_folder=cache_folder / "kilosort", useGPU=False, **run_kwargs)
+    print("resulting sorting")
+    print(sorting)
+
+
+def test_combinato(run_kwargs):
+    rec = run_kwargs["recording"]
+    channels = rec.get_channel_ids()[0:1]
+    rec_one_channel = rec.channel_slice(channels)
+    run_kwargs["recording"] = rec_one_channel
+    sorting = ss.run_sorter(sorter_name="combinato", output_folder=cache_folder / "combinato", **run_kwargs)
+    print(sorting)
+
+
+@pytest.mark.skip("Klusta is not supported anymore for Python>=3.8")
+def test_klusta(run_kwargs):
+    sorting = ss.run_sorter("klusta", output_folder=cache_folder / "klusta", **run_kwargs)
+    print(sorting)
+
+
+if __name__ == "__main__":
+    kwargs = generate_run_kwargs()
+    test_combinato(kwargs)
diff --git a/src/spikeinterface/sorters/external/tests/test_singularity_containers.py b/src/spikeinterface/sorters/external/tests/test_singularity_containers.py
index 685cf694c..637b6b223 100644
--- a/src/spikeinterface/sorters/external/tests/test_singularity_containers.py
+++ b/src/spikeinterface/sorters/external/tests/test_singularity_containers.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 import pytest
 from pathlib import Path
@@ -15,12 +14,12 @@ else:
 
 os.environ["SINGULARITY_DISABLE_CACHE"] = "true"
 
-# This can be used locally to test singularity or docker
-CONTAINER_MODE = "singularity"  # "singularity" | "docker"
-
 ON_GITHUB = os.getenv("CI")
-if ON_GITHUB:
-    CONTAINER_MODE = "singularity"
+
+
+def clean_singularity_cache():
+    print("Cleaning singularity cache")
+    os.system("singularity cache clean --force")
 
 
 def check_gh_settings():
@@ -35,12 +34,7 @@ def generate_run_kwargs():
     test_recording.set_channel_gains(1)
     test_recording.set_channel_offsets(1)
     run_kwargs = dict(recording=test_recording, verbose=True)
-    if CONTAINER_MODE == "singularity":
-        run_kwargs["singularity_image"] = True
-    elif CONTAINER_MODE == "docker":
-        run_kwargs["docker_image"] = True
-    else:
-        raise Exception("CONTAINER_MODE can be 'docker' or 'singularity'")
+    run_kwargs["singularity_image"] = True
     return run_kwargs
 
 
@@ -51,54 +45,63 @@ def run_kwargs():
 
 
 def test_spykingcircus(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("spykingcircus", output_folder=cache_folder / "spykingcircus", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_mountainsort4(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("mountainsort4", output_folder=cache_folder / "mountainsort4", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_mountainsort5(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("mountainsort5", output_folder=cache_folder / "mountainsort5", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_tridesclous(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("tridesclous", output_folder=cache_folder / "tridesclous", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_ironclust(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("ironclust", output_folder=cache_folder / "ironclust", fGpu=False, **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_waveclus(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter(sorter_name="waveclus", output_folder=cache_folder / "waveclus", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_hdsort(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter(sorter_name="hdsort", output_folder=cache_folder / "hdsort", **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_kilosort1(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter(sorter_name="kilosort", output_folder=cache_folder / "kilosort", useGPU=False, **run_kwargs)
     print("resulting sorting")
     print(sorting)
 
 
 def test_combinato(run_kwargs):
+    clean_singularity_cache()
     rec = run_kwargs["recording"]
     channels = rec.get_channel_ids()[0:1]
     rec_one_channel = rec.channel_slice(channels)
@@ -109,6 +112,7 @@ def test_combinato(run_kwargs):
 
 @pytest.mark.skip("Klusta is not supported anymore for Python>=3.8")
 def test_klusta(run_kwargs):
+    clean_singularity_cache()
     sorting = ss.run_sorter("klusta", output_folder=cache_folder / "klusta", **run_kwargs)
     print(sorting)