Repository: SpikeInterface/spikeinterface (python)

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
```

Pull request description:
Allow for `remove_excess_spikes` to remove negative spike times
Fixes #2710.

Remove negative spikes (a known bug in KS4) and update tests to test removing negative times.

Patch:
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
             )