Repository: spikeinterface/spikeinterface
Issue: Crash during export_to_phy when compute_amplitudes=True

When running export_to_phy with compute_amplitudes=True, the spike_amplitudes.py script throws an error when loading the sorting extractor. It appears to happen because the 'sampling_frequency' and 'multisortingcomparison' fields in the extractor dict are blank.

# Error traceback:
Exception in initializer:
Traceback (most recent call last):
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\concurrent\\futures\\process.py", line 230, in _process_worker initializer(*initargs)
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\site-packages\\spikeinterface\\core\\job_tools.py", line 294, in worker_initializer
_worker_ctx = init_func(*init_args)
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\site-packages\\spikeinterface\\toolkit\\postprocessing\\spike_amplitudes.py", line 202, in _init_worker_spike_amplitudes
    sorting = load_extractor(sorting)
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\site-packages\\spikeinterface\\core\\base.py", line 902, in load_extractor
    return BaseExtractor.from_dict(file_or_folder_or_dict, base_folder=base_folder)
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\site-packages\\spikeinterface\\core\\base.py", line 339, in from_dict
    extractor = _load_extractor_from_dict(d)
  File "C:\\Users\\Elissa\\Anaconda3\\envs\\spike_sorting\\lib\\site-packages\\spikeinterface\\core\\base.py", line 850, in _load_extractor_from_dict
    extractor = cls(**kwargs)
TypeError: __init__() missing 2 required positional arguments: 'sampling_frequency' and 'multisortingcomparison'


# Error producing code
```
recording = se.read_spikeglx(recording_path, stream_id='imec0.ap')

recording_cmr = recording
recording_f = si.bandpass_filter(recording, freq_min=300, freq_max=6000)
recording_cmr = si.common_reference(recording_f, reference='local', operator='median',
                                    local_radius=(30, 200))
kwargs = {'n_jobs': n_jobs, 'total_memory': '8G'}
recording = recording_cmr.save(format='binary', `folder=recording_folder,` **kwargs)

sorter_params = {"keep_good_only": True}
ks3_sorter = ss.run_sorter(sorter_name='kilosort3', recording=recording, output_folder=kilosort3_folder,
                           verbose=False, docker_image=use_docker, **sorter_params)
sorter_params = {"keep_good_only": False}
ks2_5_sorter = ss.run_sorter(sorter_name='kilosort2_5', recording=recording,
                             output_folder=kilosort2_5_folder,
                             verbose=False, docker_image=use_docker, **sorter_params)

consensus = sc.compare_multiple_sorters(sorting_list=[ks3_sorter, ks2_5_sorter],
                                        name_list=['kilosort3', 'kilosort2_5'], verbose=False,
                                        delta_time=.2,
                                        match_score=.3,
                                        spiketrain_mode='union')
consensus = consensus.get_agreement_sorting(minimum_agreement_count=2)

waveforms = si.WaveformExtractor.create(recording, consensus, waveforms_folder)
waveforms.set_params(ms_before=3., ms_after=4., max_spikes_per_unit=500)
waveforms.run_extract_waveforms(n_jobs=n_jobs, chunk_size=30000)

sparsity_dict = dict(method="radius", radius_um=50, peak_sign='both')
job_kwargs = {'n_jobs': n_jobs, 'total_memory': '8G'}
export_to_phy(waveforms, phy_folder, compute_pc_features=True, compute_amplitudes=True, copy_binary=True,
              remove_if_exists=True, sparsity_dict=sparsity_dict, max_channels_per_template=None,
              **job_kwargs)
```

Changed files:
spikeinterface/core/tests/test_job_tools.py, spikeinterface/core/core_tools.py, spikeinterface/postprocessing/spike_amplitudes.py, spikeinterface/widgets/_legacy_mpl_widgets/activity.py, spikeinterface/core/waveform_extractor.py, spikeinterface/core/base.py, spikeinterface/core/job_tools.py

diff --git a/spikeinterface/core/base.py b/spikeinterface/core/base.py
index 664f253b..082fe391 100644
--- a/spikeinterface/core/base.py
+++ b/spikeinterface/core/base.py
@@ -281,7 +281,7 @@ class BaseExtractor:
         if self._preferred_mp_context is not None:
             other._preferred_mp_context = self._preferred_mp_context
 
-    def to_dict(self, include_annotations=False, include_properties=False, include_features=False,
+    def to_dict(self, include_annotations=False, include_properties=False,
                 relative_to=None, folder_metadata=None):
         """
         Make a nested serialized dictionary out of the extractor. The dictionary be used to re-initialize an
@@ -290,13 +290,11 @@ class BaseExtractor:
         Parameters
         ----------
         include_annotations: bool
-            If True, all annotations are added to the dict
+            If True, all annotations are added to the dict, by default False
         include_properties: bool
-            If True, all properties are added to the dict
-        include_features: bool
-            If True, all features are added to the dict
+            If True, all properties are added to the dict, by default False
         relative_to: str, Path, or None
-            If not None, file_paths are serialized relative to this path
+            If not None, file_paths are serialized relative to this path, by default None
 
         Returns
         -------
@@ -407,7 +405,7 @@ class BaseExtractor:
         """
         Clones an existing extractor into a new instance.
         """
-        d = self.to_dict(include_annotations=True, include_properties=True, include_features=True)
+        d = self.to_dict(include_annotations=True, include_properties=True)
         clone = BaseExtractor.from_dict(d)
         return clone
 
@@ -434,10 +432,6 @@ class BaseExtractor:
         -------
         Path
             Path object with file path to the file
-
-        Raises
-        ------
-        NotDumpableExtractorError
         """
         ext = extensions[0]
         file_path = Path(file_path)
@@ -483,7 +477,6 @@ class BaseExtractor:
         assert self.check_if_dumpable()
         dump_dict = self.to_dict(include_annotations=True,
                                  include_properties=False,
-                                 include_features=False,
                                  relative_to=relative_to,
                                  folder_metadata=folder_metadata)
         file_path = self._get_file_path(file_path, ['.json'])
@@ -492,7 +485,7 @@ class BaseExtractor:
             encoding='utf8'
         )
 
-    def dump_to_pickle(self, file_path=None, include_properties=True, include_features=True,
+    def dump_to_pickle(self, file_path=None, include_properties=True,
                        relative_to=None, folder_metadata=None):
         """
         Dump recording extractor to a pickle file.
@@ -504,15 +497,12 @@ class BaseExtractor:
             Path of the json file
         include_properties: bool
             If True, all properties are dumped
-        include_features: bool
-            If True, all features are dumped
         relative_to: str, Path, or None
             If not None, file_paths are serialized relative to this path
         """
         assert self.check_if_dumpable()
         dump_dict = self.to_dict(include_annotations=True,
                                  include_properties=include_properties,
-                                 include_features=include_features,
                                  relative_to=relative_to,
                                  folder_metadata=folder_metadata)
         file_path = self._get_file_path(file_path, ['.pkl', '.pickle'])
diff --git a/spikeinterface/core/core_tools.py b/spikeinterface/core/core_tools.py
index b9d71a71..ec22e77d 100644
--- a/spikeinterface/core/core_tools.py
+++ b/spikeinterface/core/core_tools.py
@@ -11,7 +11,7 @@ import inspect
 
 from .job_tools import ensure_chunk_size, ensure_n_jobs, divide_segment_into_chunks, ChunkRecordingExecutor, \\
     _shared_job_kwargs_doc
-    
+
     
 def copy_signature(source_fct):
     def copy(target_fct):
diff --git a/spikeinterface/core/job_tools.py b/spikeinterface/core/job_tools.py
index d8519c6f..8629cdb6 100644
--- a/spikeinterface/core/job_tools.py
+++ b/spikeinterface/core/job_tools.py
@@ -12,9 +12,9 @@ from tqdm.auto import tqdm
 
 from concurrent.futures import ProcessPoolExecutor
 import multiprocessing as mp
-
 from threadpoolctl import threadpool_limits
 
+
 _shared_job_kwargs_doc = """**job_kwargs: keyword arguments for parallel processing:
             * chunk_duration or chunk_size or chunk_memory or total_memory
                 - chunk_size: int
@@ -125,10 +125,12 @@ def ensure_n_jobs(recording, n_jobs=1):
         print(f"Python {sys.version} does not support parallel processing")
         n_jobs = 1
 
-    if not recording.is_dumpable:
-        if n_jobs > 1:
-            n_jobs = 1
-            print("RecordingExtractor is not dumpable and can't be processed in parallel")
+    if not recording.check_if_dumpable():
+        if n_jobs != 1:
+            raise RuntimeError(
+                "Recording is not dumpable and can't be processed in parallel. "
+                "You can use the `recording.save()` function to make it dumpable or set 'n_jobs' to 1."
+            )
 
     return n_jobs
 
@@ -278,7 +280,6 @@ class ChunkRecordingExecutor:
                                             chunk_memory=chunk_memory, chunk_duration=chunk_duration,
                                             n_jobs=self.n_jobs)
         self.job_name = job_name
-        
         self.max_threads_per_process = max_threads_per_process
         
         if verbose:
diff --git a/spikeinterface/core/tests/test_job_tools.py b/spikeinterface/core/tests/test_job_tools.py
index 10a76e86..bb773ffe 100644
--- a/spikeinterface/core/tests/test_job_tools.py
+++ b/spikeinterface/core/tests/test_job_tools.py
@@ -1,3 +1,5 @@
+import pytest
+
 from spikeinterface.core import generate_recording
 
 from spikeinterface.core.job_tools import divide_segment_into_chunks, ensure_n_jobs, ensure_chunk_size, \\
@@ -26,9 +28,9 @@ def test_ensure_n_jobs():
     n_jobs = ensure_n_jobs(recording, n_jobs=1)
     assert n_jobs == 1
 
-    # not dumpable force n_jobs=1
-    n_jobs = ensure_n_jobs(recording, n_jobs=-1)
-    assert n_jobs == 1
+    # not dumpable fails
+    with pytest.raises(RuntimeError):
+        n_jobs = ensure_n_jobs(recording, n_jobs=-1)
 
     # dumpable
     n_jobs = ensure_n_jobs(recording.save(), n_jobs=-1)
diff --git a/spikeinterface/core/waveform_extractor.py b/spikeinterface/core/waveform_extractor.py
index 89470d55..c7e0612e 100644
--- a/spikeinterface/core/waveform_extractor.py
+++ b/spikeinterface/core/waveform_extractor.py
@@ -5,13 +5,14 @@ import json
 
 import numpy as np
 from copy import deepcopy
+from warnings import warn
 
-from .base import load_extractor
+from spikeinterface.core.waveform_tools import extract_waveforms_to_buffers
+import probeinterface
 
+from .base import load_extractor
 from .core_tools import check_json
 from .job_tools import _shared_job_kwargs_doc
-from spikeinterface.core.waveform_tools import extract_waveforms_to_buffers
-import probeinterface
 from .recording_tools import check_probe_do_not_overlap
 
 _possible_template_modes = ('average', 'std', 'median')
@@ -238,6 +239,9 @@ class WaveformExtractor:
                 recording.dump(folder / 'recording.json', relative_to=relative_to)
             if sorting.is_dumpable:
                 sorting.dump(folder / 'sorting.json', relative_to=relative_to)
+            else:
+                warn("Sorting object is not dumpable, which might result in downstream errors for "
+                     "parallel processing. To make the sorting dumpable, use the `sorting.save()` function.")
             
             # dump some attributes of the recording for the mode with_recording=False at next load
             rec_attributes_file = folder / 'recording_info' / 'recording_attributes.json'
@@ -663,6 +667,9 @@ class WaveformExtractor:
                     self.recording.dump(folder / 'recording.json', relative_to=relative_to)
             if self.sorting.is_dumpable:
                 self.sorting.dump(folder / 'sorting.json', relative_to=relative_to)
+            else:
+                warn("Sorting object is not dumpable, which might result in downstream errors for "
+                     "parallel processing. To make the sorting dumpable, use the `sorting.save()` function.")
 
             # dump some attributes of the recording for the mode with_recording=False at next load
             rec_attributes_file = folder / 'recording_info' / 'recording_attributes.json'
@@ -712,6 +719,9 @@ class WaveformExtractor:
             if self.sorting.is_dumpable:
                 sort_dict = self.sorting.to_dict(relative_to=relative_to)
                 zarr_root.attrs['sorting'] = check_json(sort_dict)
+            else:
+                warn("Sorting object is not dumpable, which might result in downstream errors for "
+                     "parallel processing. To make the sorting dumpable, use the `sorting.save()` function.")
             recording_info = zarr_root.create_group('recording_info')
             recording_info.attrs['recording_attributes'] = check_json(rec_attributes)
             if probegroup is not None:
diff --git a/spikeinterface/postprocessing/spike_amplitudes.py b/spikeinterface/postprocessing/spike_amplitudes.py
index b2371f09..b3bf33ca 100644
--- a/spikeinterface/postprocessing/spike_amplitudes.py
+++ b/spikeinterface/postprocessing/spike_amplitudes.py
@@ -69,6 +69,11 @@ class SpikeAmplitudesCalculator(BaseWaveformExtractorExtension):
         if n_jobs == 1:
             init_args = (recording, sorting)
         else:
+            # TODO: avoid dumping sorting and use spike vector and peak pipeline instead
+            assert sorting.check_if_dumpable(), (
+                "The soring object is not dumpable and cannot be processed in parallel. You can use the "
+                "`sorting.save()` function to make it dumpable"
+            )
             init_args = (recording.to_dict(), sorting.to_dict())
         init_args = init_args + (extremum_channels_index, peak_shifts, return_scaled)
         processor = ChunkRecordingExecutor(recording, func, init_func, init_args,
diff --git a/spikeinterface/widgets/_legacy_mpl_widgets/activity.py b/spikeinterface/widgets/_legacy_mpl_widgets/activity.py
index c4cc1a88..6c885871 100644
--- a/spikeinterface/widgets/_legacy_mpl_widgets/activity.py
+++ b/spikeinterface/widgets/_legacy_mpl_widgets/activity.py
@@ -67,7 +67,6 @@ class PeakActivityMapWidget(BaseWidget):
         peaks = self.peaks
         if peaks is None:
             from spikeinterface.sortingcomponents.peak_detection import detect_peaks
-            self.detect_peaks_kwargs['outputs'] = 'numpy_compact'
             peaks = detect_peaks(rec, **self.detect_peaks_kwargs)
 
         fs = rec.get_sampling_frequency()