Repository: SpikeInterface/spikeinterface

Issue:
Unable to open kilosort4 results with phy
Dear,

I'm using `spikeinterface==0.100.5` and `kilosort4== .4.0.3`, on 64 channel 6 shanks probe. The phy output is works perfectly with the  `ironclust ` generated results.

However, I have encountered some issues when open the kilosort4 phy output folder. And I saw there are some silimer reports, and I tried the following things:
1. Before waveform extraction `sorting.remove_empty_units()` and `sc.remove_excess_spikes(sorting_no_empty,rec_corrected1)`
2. Set `sparse=True` or `sparse=False` or `sparse=True, method="by_property",by_property="group"` in the `si.extract_waveforms()`
3. `sparsity=None` or `sparsity=True` in the `sexp.export_to_phy()`
None of above was works, and the phy is giving me the error message as below. I'm wondering if there any solution or any parameter I can adjust?

Error message:
```bash
19:51:55.272 [E] __init__:62          An error has occurred (AssertionError):
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Scripts\phy.exe\__main__.py", line 7, in <module>
    sys.exit(phycli())
             ^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\click\decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\phy\apps\__init__.py", line 159, in cli_template_gui
    template_gui(params_path, **kwargs)
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\phy\apps\template\gui.py", line 209, in template_gui
    model = load_model(params_path)
            ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\phylib\io\model.py", line 1433, in load_model
    return TemplateModel(**get_template_params(params_path))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\phylib\io\model.py", line 339, in __init__
    self._load_data()
  File "C:\Users\sachur\AppData\Local\anaconda3\envs\phy2\Lib\site-packages\phylib\io\model.py", line 358, in _load_data
    assert self.amplitudes.shape == (ns,)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

QWidget: Must construct a QApplication before a QWidget
```

Comments:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e6a67cb80..6b8d2f9b5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@ repos:
     -   id: end-of-file-fixer
     -   id: trailing-whitespace
 -   repo: https://github.com/psf/black
-    rev: 24.3.0
+    rev: 24.4.0
     hooks:
     -   id: black
         files: ^src/
diff --git a/examples/modules_gallery/core/plot_4_sorting_analyzer.py b/examples/modules_gallery/core/plot_4_sorting_analyzer.py
index b41109bf6..f38746bbb 100644
--- a/examples/modules_gallery/core/plot_4_sorting_analyzer.py
+++ b/examples/modules_gallery/core/plot_4_sorting_analyzer.py
@@ -72,7 +72,12 @@ print(analyzer)
 # when using format="binary_folder" or format="zarr"
 
 folder = "analyzer_folder"
-analyzer = create_sorting_analyzer(sorting=sorting, recording=recording, format="binary_folder", folder=folder)
+analyzer = create_sorting_analyzer(sorting=sorting,
+                                   recording=recording,
+                                   format="binary_folder",
+                                   return_scaled=True, # this is the default to attempt to return scaled
+                                   folder=folder
+                                   )
 print(analyzer)
 
 # then it can be loaded back
@@ -90,7 +95,7 @@ analyzer.compute(
     method="uniform",
     max_spikes_per_unit=500,
 )
-analyzer.compute("waveforms", ms_before=1.0, ms_after=2.0, return_scaled=True)
+analyzer.compute("waveforms", ms_before=1.0, ms_after=2.0)
 analyzer.compute("templates", operators=["average", "median", "std"])
 print(analyzer)
 
@@ -100,12 +105,12 @@ print(analyzer)
 # using parallel processing (recommended!). Like this
 
 analyzer.compute(
-    "waveforms", ms_before=1.0, ms_after=2.0, return_scaled=True, n_jobs=8, chunk_duration="1s", progress_bar=True
+    "waveforms", ms_before=1.0, ms_after=2.0, n_jobs=8, chunk_duration="1s", progress_bar=True
 )
 
 # which is equivalent to this:
 job_kwargs = dict(n_jobs=8, chunk_duration="1s", progress_bar=True)
-analyzer.compute("waveforms", ms_before=1.0, ms_after=2.0, return_scaled=True, **job_kwargs)
+analyzer.compute("waveforms", ms_before=1.0, ms_after=2.0, **job_kwargs)
 
 #################################################################################
 # Because certain extensions rely on others (e.g. we need waveforms to calculate
diff --git a/src/spikeinterface/core/template.py b/src/spikeinterface/core/template.py
index d85faa751..51688709b 100644
--- a/src/spikeinterface/core/template.py
+++ b/src/spikeinterface/core/template.py
@@ -108,6 +108,23 @@ class Templates:
                 if not self._are_passed_templates_sparse():
                     raise ValueError("Sparsity mask passed but the templates are not sparse")
 
+    def __repr__(self):
+        sampling_frequency_khz = self.sampling_frequency / 1000
+        repr_str = (
+            f"Templates: {self.num_units} units - {self.num_samples} samples - {self.num_channels} channels \n"
+            f"sampling_frequency={sampling_frequency_khz:.2f} kHz - "
+            f"ms_before={self.ms_before:.2f} ms - "
+            f"ms_after={self.ms_after:.2f} ms"
+        )
+
+        if self.probe is not None:
+            repr_str += f"\n{self.probe.__repr__()}"
+
+        if self.sparsity is not None:
+            repr_str += f"\n{self.sparsity.__repr__()}"
+
+        return repr_str
+
     def to_sparse(self, sparsity):
         # Turn a dense representation of templates into a sparse one, given some sparsity.
         # Note that nothing prevent Templates tobe empty after sparsification if the sparse mask have no channels for some units
diff --git a/src/spikeinterface/curation/remove_excess_spikes.py b/src/spikeinterface/curation/remove_excess_spikes.py
index b9f00212e..52653d684 100644
--- a/src/spikeinterface/curation/remove_excess_spikes.py
+++ b/src/spikeinterface/curation/remove_excess_spikes.py
@@ -79,8 +79,9 @@ class RemoveExcessSpikesSortingSegment(BaseSortingSegment):
     ) -> np.ndarray:
         spike_train = self._parent_segment.get_unit_spike_train(unit_id, start_frame=start_frame, end_frame=end_frame)
         max_spike = np.searchsorted(spike_train, self._num_samples, side="left")
+        min_spike = np.searchsorted(spike_train, 0, side="left")
 
-        return spike_train[:max_spike]
+        return spike_train[min_spike:max_spike]
 
 
 def remove_excess_spikes(sorting, recording):


Test patch:
diff --git a/src/spikeinterface/curation/tests/test_remove_excess_spikes.py b/src/spikeinterface/curation/tests/test_remove_excess_spikes.py
index f99c408c2..7175e0a61 100644
--- a/src/spikeinterface/curation/tests/test_remove_excess_spikes.py
+++ b/src/spikeinterface/curation/tests/test_remove_excess_spikes.py
@@ -14,6 +14,7 @@ def test_remove_excess_spikes():
     num_spikes = 100
     num_num_samples_spikes_per_segment = 5
     num_excess_spikes_per_segment = 5
+    num_neg_spike_times_per_segment = 2
     times = []
     labels = []
     for segment_index in range(recording.get_num_segments()):
@@ -21,12 +22,15 @@ def test_remove_excess_spikes():
         times_segment = np.array([], dtype=int)
         labels_segment = np.array([], dtype=int)
         for unit in range(num_units):
+            neg_spike_times = np.random.randint(-50, 0, num_neg_spike_times_per_segment)
             spike_times = np.random.randint(0, num_samples, num_spikes)
             last_samples_spikes = (num_samples - 1) * np.ones(num_num_samples_spikes_per_segment, dtype=int)
             num_samples_spike_times = num_samples * np.ones(num_num_samples_spikes_per_segment, dtype=int)
             excess_spikes = np.random.randint(num_samples, num_samples + 100, num_excess_spikes_per_segment)
             spike_times = np.sort(
-                np.concatenate((spike_times, last_samples_spikes, num_samples_spike_times, excess_spikes))
+                np.concatenate(
+                    (neg_spike_times, spike_times, last_samples_spikes, num_samples_spike_times, excess_spikes)
+                )
             )
             spike_labels = unit * np.ones_like(spike_times)
             times_segment = np.concatenate((times_segment, spike_times))
@@ -47,7 +51,10 @@ def test_remove_excess_spikes():
 
             assert (
                 len(spike_train_corrected)
-                == len(spike_train_excess) - num_num_samples_spikes_per_segment - num_excess_spikes_per_segment
+                == len(spike_train_excess)
+                - num_num_samples_spikes_per_segment
+                - num_excess_spikes_per_segment
+                - num_neg_spike_times_per_segment
             )