From 8662e64e4627a9e23247e96d757947b99a0a7efd Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 24 Sep 2026 14:37:39 +0200 Subject: [PATCH 01/15] wip --- src/spikeinterface/core/basesorting.py | 2 + src/spikeinterface/core/binaryfolder.py | 4 +- src/spikeinterface/core/core_tools.py | 4 +- src/spikeinterface/core/sortinganalyzer.py | 77 ++++++++-------------- src/spikeinterface/core/sortingfolder.py | 15 +++-- src/spikeinterface/core/zarrextractors.py | 23 +++++-- 6 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/spikeinterface/core/basesorting.py b/src/spikeinterface/core/basesorting.py index 1e136f2dff..93a249bf93 100644 --- a/src/spikeinterface/core/basesorting.py +++ b/src/spikeinterface/core/basesorting.py @@ -535,6 +535,8 @@ def save(self, format="numpy_folder", **save_kwargs): * "numpy_folder" format: - folder : str or Path The folder where the files will be saved. + - mmap_mode : str or None, default: None + The memory-mapping mode to use when saving numpy files. If None, no memory mapping is used. - overwrite : bool, default: False If True, existing files in the folder will be overwritten. * "zarr" format: diff --git a/src/spikeinterface/core/binaryfolder.py b/src/spikeinterface/core/binaryfolder.py index c0090719ca..c3c4e07cb8 100644 --- a/src/spikeinterface/core/binaryfolder.py +++ b/src/spikeinterface/core/binaryfolder.py @@ -15,7 +15,7 @@ make_paths_absolute, load_properties_from_folder, save_properties_to_folder, - save_extractor_provenance, + save_provenance_to_folder, save_annotations_to_folder, load_annotations_from_folder, ) @@ -160,7 +160,7 @@ def write_recording( ) save_properties_to_folder(folder_path / "properties", recording) - save_extractor_provenance(folder_path, recording) + save_provenance_to_folder(folder_path, recording, relative_to=folder_path) # new in version 0.105.0, before that annotations were handle by "si_folder.json" file save_annotations_to_folder(folder_path, recording) diff --git a/src/spikeinterface/core/core_tools.py b/src/spikeinterface/core/core_tools.py index b5ee518208..25e1da5e93 100644 --- a/src/spikeinterface/core/core_tools.py +++ b/src/spikeinterface/core/core_tools.py @@ -934,11 +934,11 @@ def load_annotations_from_folder(folder: str | Path, extractor: "BaseExtractor") extractor._annotations.update(annotations) -def save_extractor_provenance(folder: str | Path, extractor: "BaseExtractor"): +def save_provenance_to_folder(folder: str | Path, extractor: "BaseExtractor", relative_to: str | Path | None = None): folder = Path(folder) if extractor.check_serializability("json"): provenance_file_path = folder / f"provenance.json" - extractor.dump_to_json(file_path=provenance_file_path, relative_to=folder) + extractor.dump_to_json(file_path=provenance_file_path, relative_to=relative_to) elif extractor.check_serializability("pickle"): provenance_file = folder / f"provenance.pkl" extractor.dump_to_pickle(provenance_file, relative_to=folder) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index 953f75259d..62d84dd465 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -714,7 +714,6 @@ def create_binary_folder( info_file = folder / "spikeinterface_info.json" settings_file = folder / "settings.json" sorting_folder = folder / "sorting" - sorting_provenance_file = folder / "sorting_provenance.json" sparsity_file = folder / "sparsity_mask.npy" recording_provenance_file = folder / "recording" # .json (default) or .pickle (if not json-serializable) rec_attributes_file = recording_info_folder / "recording_attributes.json" @@ -731,17 +730,7 @@ def create_binary_folder( json.dump(check_json(settings), f, indent=4) # Save the sorting output - sorting = sorting.save(folder=sorting_folder) - - # Dump sorting provenance - if sorting.check_serializability("json"): - sorting.dump(sorting_provenance_file, relative_to=folder) - elif sorting.check_serializability("pickle"): - sorting.dump(sorting_provenance_file.with_suffix(".pickle"), relative_to=folder) - else: - warnings.warn( - "The sorting provenance is not serializable! The sorting provenance link will be lost for future load" - ) + _ = sorting.save(folder=sorting_folder, relative_to=folder) # Save sparsity if sparsity is not None: @@ -1071,27 +1060,9 @@ def create_zarr( settings = {"return_in_uV": return_in_uV, "peak_sign": peak_sign, "peak_mode": peak_mode} zarr_root.attrs["settings"] = check_json(settings) - # Save the sorting output - add_sorting_to_zarr_group(sorting, sorting_group, **saving_options) - - # Dump sorting provenance relative_to = None if is_remote else folder - sort_dict = sorting.to_dict(relative_to=relative_to, recursive=True) - if sorting.check_serializability("json"): - _write_object_array(zarr_root, "sorting_provenance", check_json(sort_dict), codec="json") - elif sorting.check_serializability("pickle"): - try: - _write_object_array(zarr_root, "sorting_provenance", sort_dict, codec="pickle") - except: - warnings.warn( - "Failed to serialize sorting provenance with Pickle Codec! " - "The sorting provenance link will be lost for future load" - ) - else: - warnings.warn( - "The sorting provenance is not serializable! " - "The sorting provenance link will be lost for future load" - ) + # Save the sorting output + add_sorting_to_zarr_group(sorting, sorting_group, relative_to=folder, **saving_options) # Save sparsity if sparsity is not None: @@ -1135,7 +1106,7 @@ def create_zarr( # Create SortingAnalyzer sorting_analyzer = SortingAnalyzer( - sorting=NumpySorting.from_sorting(sorting, with_metadata=True, copy_spike_vector=True), + sorting=sorting, recording=recording, rec_attributes={**rec_attributes_to_save, "probegroup": probegroup}, format="zarr", @@ -2006,9 +1977,12 @@ def merge_units( mergeable_unit_groups = [] unmergeable_unit_groups = [] - for merge_unit_group, mergeable in zip(merge_unit_groups, mergeable.values()): + new_unit_ids_mergeable = [] if new_unit_ids is not None else None + for i, (merge_unit_group, mergeable) in enumerate(zip(merge_unit_groups, mergeable.values())): if mergeable: mergeable_unit_groups.append(merge_unit_group) + if new_unit_ids_mergeable is not None: + new_unit_ids_mergeable.append(new_unit_ids[i]) else: unmergeable_unit_groups.append(merge_unit_group) @@ -2021,9 +1995,14 @@ def merge_units( warnings.warn(warning_message) else: mergeable_unit_groups = merge_unit_groups + new_unit_ids_mergeable = new_unit_ids + + if len(mergeable_unit_groups) == 0: + warnings.warn("No mergeable unit groups found.") + return self if not return_new_unit_ids else (self, []) new_unit_ids = generate_unit_ids_for_merge_group( - self.unit_ids, mergeable_unit_groups, new_unit_ids, new_id_strategy + self.unit_ids, mergeable_unit_groups, new_unit_ids_mergeable, new_id_strategy ) all_unit_ids = _get_ids_after_merging(self.unit_ids, mergeable_unit_groups, new_unit_ids=new_unit_ids) @@ -2175,34 +2154,36 @@ def get_sorting_provenance(self): """ from .loading import load + sorting_provenance = None if self.format == "memory": # the original sorting provenance is not kept in that case - sorting_provenance = None - + pass elif self.format == "binary_folder": for type in ("json", "pickle"): - filename = self.folder / f"sorting_provenance.{type}" + filename = self.folder / "sorting" / f"provenance.{type}" sorting_provenance = None + if not filename.is_file(): + # Legacy location + filename = self.folder / "sorting_provenance.json" + # try-except here is because it's not required to be able + # to load the sorting provenance, as the user might have deleted + # the original sorting folder if filename.is_file(): - # try-except here is because it's not required to be able - # to load the sorting provenance, as the user might have deleted - # the original sorting folder try: sorting_provenance = load(filename, base_folder=self.folder) break except: pass - # sorting_provenance = None - elif self.format == "zarr": zarr_root = self._get_zarr_root(mode="r") sorting_provenance = None - if "sorting_provenance" in zarr_root.keys(): - # try-except here is because it's not required to be able - # to load the sorting provenance, as the user might have deleted - # the original sorting folder - try: + sort_dict = zarr_root["sorting"].attrs.get("provenance", None) + if sort_dict is None: + # Legacy + if "sorting_provenance" in zarr_root.keys(): sort_dict = zarr_root["sorting_provenance"][0] + if sort_dict is not None: + try: sorting_provenance = load(sort_dict, base_folder=self.folder) except: pass diff --git a/src/spikeinterface/core/sortingfolder.py b/src/spikeinterface/core/sortingfolder.py index 3a068e0384..fefe5c1229 100644 --- a/src/spikeinterface/core/sortingfolder.py +++ b/src/spikeinterface/core/sortingfolder.py @@ -15,7 +15,7 @@ save_properties_to_folder, save_annotations_to_folder, load_annotations_from_folder, - save_extractor_provenance, + save_provenance_to_folder, ) @@ -61,7 +61,13 @@ def __init__(self, folder_path, mmap_mode: str | None = None): self._kwargs = dict(folder_path=str(folder_path.absolute()), mmap_mode=mmap_mode) @staticmethod - def write_sorting(sorting, folder_path, overwrite: bool = False): + def write_sorting( + sorting, + folder_path, + overwrite: bool = False, + mmap_mode: str | None = None, + relative_to: str | Path | None = None, + ): # the folder can already exists but not contaning numpysorting_info.json folder_path = Path(folder_path) if folder_path.is_dir(): @@ -81,14 +87,15 @@ def write_sorting(sorting, folder_path, overwrite: bool = False): np.save(folder_path / "spikes.npy", sorting.to_spike_vector()) save_properties_to_folder(folder_path / "properties", sorting) - save_extractor_provenance(folder_path, sorting) + relative_to = folder_path if relative_to is None else relative_to + save_provenance_to_folder(folder_path, sorting, relative_to=relative_to) # new in version 0.105.0, before that annotations were handle by "si_folder.json" file save_annotations_to_folder(folder_path, sorting) # Create the si_folder file to make the load() easier until version 0.105.0 # All properties, annotations, and probe information are already saved in the folder, # so we don't need to include them in the si_folder.json - cached = NumpyFolderSorting(folder_path=folder_path) + cached = NumpyFolderSorting(folder_path=folder_path, mmap_mode=mmap_mode) si_folder_path = folder_path / f"si_folder.json" cached.dump_to_json( file_path=si_folder_path, diff --git a/src/spikeinterface/core/zarrextractors.py b/src/spikeinterface/core/zarrextractors.py index 448dbd94a7..27451bda2a 100644 --- a/src/spikeinterface/core/zarrextractors.py +++ b/src/spikeinterface/core/zarrextractors.py @@ -559,6 +559,7 @@ def write_sorting( folder_path: str | Path, overwrite: bool = False, storage_options: dict | None = None, + relative_to: str | Path | None = None, **kwargs, ): """ @@ -567,7 +568,7 @@ def write_sorting( folder_path = create_zarr_path_for_write(folder_path, overwrite=overwrite) zarr_root = zarr.open(str(folder_path), mode="w", storage_options=storage_options) zarr_root.attrs["zarr_class_info"] = retrieve_importing_provenance(ZarrSortingExtractor) - add_sorting_to_zarr_group(sorting, zarr_root, **kwargs) + add_sorting_to_zarr_group(sorting, zarr_root, relative_to=relative_to, **kwargs) return ZarrSortingExtractor(folder_path, storage_options=storage_options) @@ -734,7 +735,9 @@ def add_properties_and_annotations(zarr_group: zarr.Group, recording_or_sorting: zarr_group.attrs["annotations"] = check_json(recording_or_sorting._annotations) -def add_sorting_to_zarr_group(sorting: BaseSorting, zarr_group: zarr.Group, **kwargs): +def add_sorting_to_zarr_group( + sorting: BaseSorting, zarr_group: zarr.Group, relative_to: str | Path | None = None, **kwargs +): """ Add a sorting extractor to a zarr group. @@ -749,6 +752,11 @@ def add_sorting_to_zarr_group(sorting: BaseSorting, zarr_group: zarr.Group, **kw """ from numcodecs import Delta + if sorting.check_serializability("json"): + zarr_group.attrs["provenance"] = check_json(sorting.to_dict(recursive=True, relative_to=relative_to)) + else: + zarr_group.attrs["provenance"] = None + num_segments = sorting.get_num_segments() zarr_group.attrs["sampling_frequency"] = float(sorting.sampling_frequency) zarr_group.attrs["num_segments"] = int(num_segments) @@ -777,11 +785,18 @@ def add_sorting_to_zarr_group(sorting: BaseSorting, zarr_group: zarr.Group, **kw add_properties_and_annotations(zarr_group, sorting) -def add_recording_to_zarr_group(recording: BaseRecording, zarr_group: zarr.Group, verbose=False, dtype=None, **kwargs): +def add_recording_to_zarr_group( + recording: BaseRecording, + zarr_group: zarr.Group, + verbose=False, + dtype=None, + relative_to: str | Path | None = None, + **kwargs, +): zarr_kwargs, job_kwargs = split_job_kwargs(kwargs) if recording.check_serializability("json"): - zarr_group.attrs["provenance"] = check_json(recording.to_dict(recursive=True)) + zarr_group.attrs["provenance"] = check_json(recording.to_dict(recursive=True, relative_to=relative_to)) else: zarr_group.attrs["provenance"] = None From 3962ffbd06d04b37805cde4da953a4f3fbbe6163 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 24 Sep 2026 14:39:07 +0200 Subject: [PATCH 02/15] revert merge changes --- src/spikeinterface/core/sortinganalyzer.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index 62d84dd465..fab0a2451e 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -1977,12 +1977,9 @@ def merge_units( mergeable_unit_groups = [] unmergeable_unit_groups = [] - new_unit_ids_mergeable = [] if new_unit_ids is not None else None - for i, (merge_unit_group, mergeable) in enumerate(zip(merge_unit_groups, mergeable.values())): + for merge_unit_group, mergeable in zip(merge_unit_groups, mergeable.values()): if mergeable: mergeable_unit_groups.append(merge_unit_group) - if new_unit_ids_mergeable is not None: - new_unit_ids_mergeable.append(new_unit_ids[i]) else: unmergeable_unit_groups.append(merge_unit_group) @@ -1995,14 +1992,9 @@ def merge_units( warnings.warn(warning_message) else: mergeable_unit_groups = merge_unit_groups - new_unit_ids_mergeable = new_unit_ids - - if len(mergeable_unit_groups) == 0: - warnings.warn("No mergeable unit groups found.") - return self if not return_new_unit_ids else (self, []) new_unit_ids = generate_unit_ids_for_merge_group( - self.unit_ids, mergeable_unit_groups, new_unit_ids_mergeable, new_id_strategy + self.unit_ids, mergeable_unit_groups, new_unit_ids, new_id_strategy ) all_unit_ids = _get_ids_after_merging(self.unit_ids, mergeable_unit_groups, new_unit_ids=new_unit_ids) From 5be6690562ce61f9baefea21bfbbb973080b6839 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 24 Sep 2026 14:54:58 +0200 Subject: [PATCH 03/15] fix: add warnigns in zarr when provenance is not JSON serializable --- src/spikeinterface/core/zarrextractors.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/spikeinterface/core/zarrextractors.py b/src/spikeinterface/core/zarrextractors.py index 27451bda2a..1ad57db992 100644 --- a/src/spikeinterface/core/zarrextractors.py +++ b/src/spikeinterface/core/zarrextractors.py @@ -755,6 +755,9 @@ def add_sorting_to_zarr_group( if sorting.check_serializability("json"): zarr_group.attrs["provenance"] = check_json(sorting.to_dict(recursive=True, relative_to=relative_to)) else: + warnings.warn( + "The sorting provenance is not serializable! " "The sorting provenance link will be lost for future load" + ) zarr_group.attrs["provenance"] = None num_segments = sorting.get_num_segments() @@ -798,6 +801,10 @@ def add_recording_to_zarr_group( if recording.check_serializability("json"): zarr_group.attrs["provenance"] = check_json(recording.to_dict(recursive=True, relative_to=relative_to)) else: + warnings.warn( + "The recording provenance is not serializable! " + "The recording provenance link will be lost for future load" + ) zarr_group.attrs["provenance"] = None # save data (done the subclass) From df58f4679d75db4467a49892fbbe76a4f7037f5c Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 25 Sep 2026 14:57:24 +0200 Subject: [PATCH 04/15] fix: add copy method to ZarrSpikeVector --- src/spikeinterface/core/zarrextractors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spikeinterface/core/zarrextractors.py b/src/spikeinterface/core/zarrextractors.py index 1ad57db992..ea4d0d2566 100644 --- a/src/spikeinterface/core/zarrextractors.py +++ b/src/spikeinterface/core/zarrextractors.py @@ -453,6 +453,9 @@ def __array__(self, dtype=None): arr["segment_index"][s0:s1] = seg_idx return arr if dtype is None else arr.astype(dtype) + def copy(self): + return np.copy(np.asarray(self)) + class ZarrSortingExtractor(BaseSorting): """ From 87b4ef4b24f648a2fd4c8d981d096cdbe7d31fa7 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 12:45:16 +0200 Subject: [PATCH 05/15] fix: use cached sorting and make NumpySorting not pickle-serializable --- src/spikeinterface/core/numpyextractors.py | 3 +-- src/spikeinterface/core/sortinganalyzer.py | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index b58a98eddb..e2c2df6280 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -276,8 +276,7 @@ def __init__(self, spikes, sampling_frequency, unit_ids): self._serializability["memory"] = True self._serializability["json"] = False - # theorically this should be False but for simplicity make generators simples we still need this. - self._serializability["pickle"] = True + self._serializability["pickle"] = False if spikes.size == 0: nseg = 1 diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index fab0a2451e..59ae9ed7d2 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -730,7 +730,8 @@ def create_binary_folder( json.dump(check_json(settings), f, indent=4) # Save the sorting output - _ = sorting.save(folder=sorting_folder, relative_to=folder) + mmap_mode = "r" if lazy else None + sorting_cached = sorting.save(folder=sorting_folder, relative_to=folder, mmap_mode=mmap_mode) # Save sparsity if sparsity is not None: @@ -765,7 +766,7 @@ def create_binary_folder( # Create SortingAnalyzer sorting_analyzer = SortingAnalyzer( - sorting=sorting, + sorting=sorting_cached, recording=recording, rec_attributes={**rec_attributes_to_save, "probegroup": probegroup}, format="binary_folder", @@ -1031,9 +1032,7 @@ def create_zarr( backend_options: dict | None, lazy: bool = False, ) -> "SortingAnalyzer": - # used by create and save_as - - from .zarrextractors import add_sorting_to_zarr_group + from .zarrextractors import add_sorting_to_zarr_group, ZarrSortingExtractor is_remote = is_path_remote(folder) if not is_remote: @@ -1063,6 +1062,7 @@ def create_zarr( relative_to = None if is_remote else folder # Save the sorting output add_sorting_to_zarr_group(sorting, sorting_group, relative_to=folder, **saving_options) + sorting_cached = ZarrSortingExtractor(folder / "sorting", lazy_spike_vector=lazy) # Save sparsity if sparsity is not None: @@ -1106,7 +1106,7 @@ def create_zarr( # Create SortingAnalyzer sorting_analyzer = SortingAnalyzer( - sorting=sorting, + sorting=sorting_cached, recording=recording, rec_attributes={**rec_attributes_to_save, "probegroup": probegroup}, format="zarr", From 82b894cbc7bd43914cf96b30a4d3b8def6103d3c Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 13:01:09 +0200 Subject: [PATCH 06/15] fix: tests --- src/spikeinterface/core/tests/test_sortinganalyzer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spikeinterface/core/tests/test_sortinganalyzer.py b/src/spikeinterface/core/tests/test_sortinganalyzer.py index 8cc069ce1e..88bbb13c75 100644 --- a/src/spikeinterface/core/tests/test_sortinganalyzer.py +++ b/src/spikeinterface/core/tests/test_sortinganalyzer.py @@ -92,6 +92,7 @@ def test_SortingAnalyzer_memory(tmp_path, dataset): def test_SortingAnalyzer_binary_folder(tmp_path, dataset): recording, sorting = dataset + recording = recording.save(folder=tmp_path / "recording_binary") folder = tmp_path / "test_SortingAnalyzer_binary_folder" if folder.exists(): @@ -138,6 +139,7 @@ def test_SortingAnalyzer_binary_folder(tmp_path, dataset): def test_SortingAnalyzer_zarr(tmp_path, dataset): recording, sorting = dataset + recording = recording.save(folder=tmp_path / "recording_zarr") folder = tmp_path / "test_SortingAnalyzer_zarr.zarr" From a9fd1ba5b803ec0771b2f8a7be5c9b2e93de51cd Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 14:29:37 +0200 Subject: [PATCH 07/15] fix: preprocessing pipeline tests --- src/spikeinterface/preprocessing/tests/test_pipeline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/preprocessing/tests/test_pipeline.py b/src/spikeinterface/preprocessing/tests/test_pipeline.py index 5f5c298430..4a62bc0088 100644 --- a/src/spikeinterface/preprocessing/tests/test_pipeline.py +++ b/src/spikeinterface/preprocessing/tests/test_pipeline.py @@ -169,6 +169,7 @@ def test_loading_provenance(create_cache_folder): cache_folder = create_cache_folder / "preprocessed_rec_for_pipeline" rec, _ = generate_ground_truth_recording(seed=0, num_channels=6) + rec = rec.save(folder=cache_folder / "recording_provenance") pp_rec = detect_and_remove_bad_channels( bandpass_filter(common_reference(rec, operator="average")), noisy_channel_threshold=0.3, @@ -176,9 +177,9 @@ def test_loading_provenance(create_cache_folder): # when several run seed=2205, ) - pp_rec.save(folder=cache_folder) + pp_rec.save(folder=cache_folder / "preprocessed_recording") - loaded_pp_list = get_preprocessing_list_from_file(cache_folder / "provenance.pkl") + loaded_pp_list = get_preprocessing_list_from_file(cache_folder / "preprocessed_recording" / "provenance.json") pipeline_rec_applying_precomputed_kwargs = apply_preprocessing_pipeline( rec, @@ -204,6 +205,7 @@ def test_loading_from_analyzer(create_cache_folder): cache_folder = create_cache_folder recording, sorting = generate_ground_truth_recording() + recording = recording.save(folder=cache_folder / "recording") preprocessing_dict = {"common_reference": {}, "highpass_filter": {"freq_min": 301.0}} pp_recording = apply_preprocessing_pipeline(recording, preprocessing_dict) From 48d829016da88d8f07845592a6990adbb88d6ae3 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 14:54:58 +0200 Subject: [PATCH 08/15] fix: postprocessing tests pickle failures --- .../postprocessing/tests/test_multi_extensions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/spikeinterface/postprocessing/tests/test_multi_extensions.py b/src/spikeinterface/postprocessing/tests/test_multi_extensions.py index 1bb3d383a2..7b53f24f29 100644 --- a/src/spikeinterface/postprocessing/tests/test_multi_extensions.py +++ b/src/spikeinterface/postprocessing/tests/test_multi_extensions.py @@ -69,7 +69,7 @@ } -def get_dataset_to_merge(): +def get_dataset_to_merge(cache_folder): # generate a dataset with some split units to minimize merge errors recording, sorting = generate_ground_truth_recording( durations=[30.0], @@ -81,6 +81,7 @@ def get_dataset_to_merge(): generate_unit_locations_kwargs=dict(margin_um=10.0, minimum_z=2.0, maximum_z=15.0, minimum_distance=20), seed=2205, ) + recording = recording.save(folder=cache_folder / "recording_merge") # since templates are going to be averaged and this might be a problem for amplitude scaling # we select the 3 units with the largest templates to split @@ -96,7 +97,7 @@ def get_dataset_to_merge(): return recording, sorting_with_splits, split_unit_ids -def get_dataset_to_split(): +def get_dataset_to_split(cache_folder): # generate a dataset and return large unit to split to minimize split errors recording, sorting = generate_ground_truth_recording( durations=[30.0], @@ -107,6 +108,7 @@ def get_dataset_to_split(): noise_kwargs=dict(noise_levels=5.0, strategy="tile_pregenerated"), seed=2205, ) + recording = recording.save(folder=cache_folder / "recording_split") # since templates are going to be averaged and this might be a problem for amplitude scaling # we select the 3 units with the largest templates to split @@ -120,13 +122,13 @@ def get_dataset_to_split(): @pytest.fixture(scope="module") -def dataset_to_merge(): - return get_dataset_to_merge() +def dataset_to_merge(create_cache_folder): + return get_dataset_to_merge(create_cache_folder) @pytest.fixture(scope="module") -def dataset_to_split(): - return get_dataset_to_split() +def dataset_to_split(create_cache_folder): + return get_dataset_to_split(create_cache_folder) @pytest.mark.parametrize("lazy", [False, True]) From ae8a94d89d3658afce41a391f36c8ca349bc06cb Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 15:14:15 +0200 Subject: [PATCH 09/15] Apply suggestion from @alejoe91 --- src/spikeinterface/core/zarrextractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spikeinterface/core/zarrextractors.py b/src/spikeinterface/core/zarrextractors.py index ea4d0d2566..4be7c0d3d6 100644 --- a/src/spikeinterface/core/zarrextractors.py +++ b/src/spikeinterface/core/zarrextractors.py @@ -759,7 +759,7 @@ def add_sorting_to_zarr_group( zarr_group.attrs["provenance"] = check_json(sorting.to_dict(recursive=True, relative_to=relative_to)) else: warnings.warn( - "The sorting provenance is not serializable! " "The sorting provenance link will be lost for future load" + "The sorting provenance is not serializable! The sorting provenance link will be lost for future load" ) zarr_group.attrs["provenance"] = None From 9c1f17d8293e06bc814a0ef1c6c2be1d3ebf9111 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 15:59:35 +0200 Subject: [PATCH 10/15] fix: pickling error in metrics --- src/spikeinterface/metrics/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/spikeinterface/metrics/conftest.py b/src/spikeinterface/metrics/conftest.py index aefe2e6cfe..9c495cd36f 100644 --- a/src/spikeinterface/metrics/conftest.py +++ b/src/spikeinterface/metrics/conftest.py @@ -8,12 +8,13 @@ job_kwargs = dict(n_jobs=2, progress_bar=True, chunk_duration="1s") -def make_small_analyzer(): +def make_small_analyzer(folder): recording, sorting = generate_ground_truth_recording( durations=[10.0], num_units=10, seed=1205, ) + recording = recording.save(folder=folder / "rec_for_small_analyzer", format="binary") sorting = sorting.select_units(["2", "7", "0"], ["#3", "#9", "#4"]) @@ -36,12 +37,12 @@ def make_small_analyzer(): @pytest.fixture(scope="module") -def small_sorting_analyzer(): - return make_small_analyzer() +def small_sorting_analyzer(create_cache_folder): + return make_small_analyzer(create_cache_folder) @pytest.fixture(scope="module") -def sorting_analyzer_simple(): +def sorting_analyzer_simple(create_cache_folder): # we need high firing rate for amplitude_cutoff recording, sorting = generate_ground_truth_recording( durations=[ @@ -64,6 +65,7 @@ def sorting_analyzer_simple(): noise_kwargs=dict(noise_levels=5.0, strategy="tile_pregenerated"), seed=1205, ) + recording = recording.save(folder=create_cache_folder / "rec_for_sorting_analyzer_simple", format="binary") sorting_analyzer = create_sorting_analyzer(sorting, recording, format="memory", sparse=True) From 8645dd0cdb6180056bc08beeb6fdcb26fa2d5a0a Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 17:52:12 +0200 Subject: [PATCH 11/15] fix: tests --- src/spikeinterface/sorters/tests/test_runsorter.py | 7 ++++--- .../motion/tests/test_motion_interpolation.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/spikeinterface/sorters/tests/test_runsorter.py b/src/spikeinterface/sorters/tests/test_runsorter.py index 332e6e857e..c07cd94c9b 100644 --- a/src/spikeinterface/sorters/tests/test_runsorter.py +++ b/src/spikeinterface/sorters/tests/test_runsorter.py @@ -13,14 +13,15 @@ ON_GITHUB = bool(os.getenv("GITHUB_ACTIONS")) -def _generate_recording(): +def _generate_recording(folder): recording, _ = generate_ground_truth_recording(num_channels=8, durations=[10.0], seed=2205) + recording = recording.save(folder=folder / "rec_for_generate_recording", format="binary") return recording @pytest.fixture(scope="module") -def generate_recording(): - return _generate_recording() +def generate_recording(create_cache_folder): + return _generate_recording(create_cache_folder) @pytest.mark.xfail( diff --git a/src/spikeinterface/sortingcomponents/motion/tests/test_motion_interpolation.py b/src/spikeinterface/sortingcomponents/motion/tests/test_motion_interpolation.py index 9635bad48b..97558448f4 100644 --- a/src/spikeinterface/sortingcomponents/motion/tests/test_motion_interpolation.py +++ b/src/spikeinterface/sortingcomponents/motion/tests/test_motion_interpolation.py @@ -224,7 +224,7 @@ def test_cross_band_interpolation(): assert np.array_equal(traces_corrected, target) -def test_InterpolateMotionRecording(): +def test_InterpolateMotionRecording(create_cache_folder): # rec, sorting = make_dataset() # 2 segments @@ -244,6 +244,7 @@ def test_InterpolateMotionRecording(): noise_kwargs=dict(noise_levels=5.0, strategy="on_the_fly"), seed=2205, ) + rec = rec.save(folder=create_cache_folder / "rec_for_test_interpolate_motion", format="binary") motion = make_fake_motion(rec) From 790d530f7642510ff2932474e59ef2c5bdb27368 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 28 Sep 2026 19:29:26 +0200 Subject: [PATCH 12/15] fix: sorters tests --- src/spikeinterface/sorters/tests/test_launcher.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/spikeinterface/sorters/tests/test_launcher.py b/src/spikeinterface/sorters/tests/test_launcher.py index 2cd25f119a..96c9b57dcd 100644 --- a/src/spikeinterface/sorters/tests/test_launcher.py +++ b/src/spikeinterface/sorters/tests/test_launcher.py @@ -13,10 +13,11 @@ SORTERS = ["tridesclous2"] -def create_recordings(NUM_RECORDINGS=2, base_seed=42): +def create_recordings(folder, NUM_RECORDINGS=2, base_seed=42): recordings = [] for i in range(NUM_RECORDINGS): recording, _ = generate_ground_truth_recording(num_channels=8, durations=[10.0], seed=base_seed + i) + recording = recording.save(folder=folder / f"rec_for_generate_recording_{i}", format="binary") if i % 2 == 0: recording.set_channel_groups(["0"] * 4 + ["1"] * 4) @@ -28,7 +29,7 @@ def create_recordings(NUM_RECORDINGS=2, base_seed=42): def get_job_list(base_folder): jobs = [] - recordings = create_recordings(NUM_RECORDINGS) + recordings = create_recordings(base_folder, NUM_RECORDINGS) for i, recording in enumerate(recordings): for sorter_name in SORTERS: kwargs = dict( @@ -209,7 +210,7 @@ def test_run_sorter_by_property(create_cache_folder): if working_folder2.is_dir(): shutil.rmtree(working_folder2) - recordings = create_recordings(NUM_RECORDINGS) + recordings = create_recordings(create_cache_folder, NUM_RECORDINGS) rec0 = recordings[0] rec0_by = rec0.split_by("group") From 979698cca07352de14770870500a6f5f57b2ee76 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 29 Sep 2026 09:32:00 +0200 Subject: [PATCH 13/15] fix: sorters tests --- src/spikeinterface/sorters/tests/test_launcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/sorters/tests/test_launcher.py b/src/spikeinterface/sorters/tests/test_launcher.py index 96c9b57dcd..6619431866 100644 --- a/src/spikeinterface/sorters/tests/test_launcher.py +++ b/src/spikeinterface/sorters/tests/test_launcher.py @@ -16,8 +16,9 @@ def create_recordings(folder, NUM_RECORDINGS=2, base_seed=42): recordings = [] for i in range(NUM_RECORDINGS): + recording, _ = generate_ground_truth_recording(num_channels=8, durations=[10.0], seed=base_seed + i) - recording = recording.save(folder=folder / f"rec_for_generate_recording_{i}", format="binary") + recording = recording.save(folder=folder / f"rec_for_generate_recording_{i}", format="binary", overwrite=True) if i % 2 == 0: recording.set_channel_groups(["0"] * 4 + ["1"] * 4) From 53cbb5007e11986cec37efde912148aff6dbec67 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 29 Sep 2026 11:34:27 +0200 Subject: [PATCH 14/15] fix: examples --- examples/tutorials/core/plot_4_sorting_analyzer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/tutorials/core/plot_4_sorting_analyzer.py b/examples/tutorials/core/plot_4_sorting_analyzer.py index 7c89088c09..c52426251c 100644 --- a/examples/tutorials/core/plot_4_sorting_analyzer.py +++ b/examples/tutorials/core/plot_4_sorting_analyzer.py @@ -33,6 +33,8 @@ # First let's generate a simulated recording and sorting recording, sorting = generate_ground_truth_recording() +# Make the recording JSON serializable, so it can be reloaded later +recording = recording.save(folder="recording_folder") print(recording) print(sorting) From dc2a0d6c8e4a5628074c8bd12801338231f0d7a6 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 2 Oct 2026 16:56:34 +0200 Subject: [PATCH 15/15] fix: remove warnings if not JSON serializable Co-authored-by: Alessio Buccino --- src/spikeinterface/core/zarrextractors.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/spikeinterface/core/zarrextractors.py b/src/spikeinterface/core/zarrextractors.py index 4be7c0d3d6..9b88d59b98 100644 --- a/src/spikeinterface/core/zarrextractors.py +++ b/src/spikeinterface/core/zarrextractors.py @@ -758,9 +758,6 @@ def add_sorting_to_zarr_group( if sorting.check_serializability("json"): zarr_group.attrs["provenance"] = check_json(sorting.to_dict(recursive=True, relative_to=relative_to)) else: - warnings.warn( - "The sorting provenance is not serializable! The sorting provenance link will be lost for future load" - ) zarr_group.attrs["provenance"] = None num_segments = sorting.get_num_segments() @@ -804,10 +801,6 @@ def add_recording_to_zarr_group( if recording.check_serializability("json"): zarr_group.attrs["provenance"] = check_json(recording.to_dict(recursive=True, relative_to=relative_to)) else: - warnings.warn( - "The recording provenance is not serializable! " - "The recording provenance link will be lost for future load" - ) zarr_group.attrs["provenance"] = None # save data (done the subclass)