Skip to content

Commit e4707db

Browse files
authored
Merge pull request #3697 from alejoe91/ks4-dynamic-params
Add BaseSorter._dynamic_params to be to retrieve parameters dynamically
2 parents 8141538 + f74fcdb commit e4707db

5 files changed

Lines changed: 76 additions & 92 deletions

File tree

.github/scripts/test_kilosort4_ci.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import pytest
2222
import copy
23-
from typing import Any
23+
from packaging.version import parse
2424
from inspect import signature
2525

2626
import numpy as np
@@ -84,8 +84,6 @@
8484
"duplicate_spike_ms": 0.3,
8585
}
8686

87-
PARAMS_TO_TEST = list(PARAMS_TO_TEST_DICT.keys())
88-
8987
PARAMETERS_NOT_AFFECTING_RESULTS = [
9088
"artifact_threshold",
9189
"ccg_threshold",
@@ -95,13 +93,24 @@
9593
"duplicate_spike_ms", # this is because ground-truth spikes don't have violations
9694
]
9795

98-
# THIS IS A PLACEHOLDER FOR FUTURE PARAMS TO TEST
99-
# if parse(version("kilosort")) >= parse("4.0.X"):
100-
# PARAMS_TO_TEST_DICT.update(
101-
# [
102-
# {"new_param": new_value},
103-
# ]
104-
# )
96+
97+
# Add/Remove version specific parameters
98+
if parse(kilosort.__version__) >= parse("4.0.22"):
99+
PARAMS_TO_TEST_DICT.update(
100+
{"position_limit": 50}
101+
)
102+
# Position limit only affects computing spike locations after sorting
103+
PARAMETERS_NOT_AFFECTING_RESULTS.append("position_limit")
104+
105+
if parse(kilosort.__version__) >= parse("4.0.24"):
106+
PARAMS_TO_TEST_DICT.update(
107+
{"max_peels": 200},
108+
)
109+
# max_peels is not affecting the results in this short dataset
110+
PARAMETERS_NOT_AFFECTING_RESULTS.append("max_peels")
111+
112+
113+
PARAMS_TO_TEST = list(PARAMS_TO_TEST_DICT.keys())
105114

106115

107116
class TestKilosort4Long:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ test = [
159159
"s3fs",
160160

161161
# tridesclous
162-
"numba",
162+
"numba<0.61.0;python_version<'3.13'",
163+
"numba>=0.61.0;python_version>='3.13'",
163164
"hdbscan>=0.8.33", # Previous version had a broken wheel
164165

165166
# for sortingview backend

src/spikeinterface/preprocessing/tests/test_whiten.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def get_test_recording(self, dtype, means=None):
5656

5757
return means, cov_mat, recording
5858

59-
def get_test_data_with_known_distribution(self, num_samples, dtype, means=None):
59+
def get_test_data_with_known_distribution(self, num_samples, dtype, means=None, seed=0):
6060
"""
6161
Create multivariate normal data with known means and covariance matrixs.
6262
If `dtype` is int16, scale to full range of int16 before cast.
@@ -68,7 +68,8 @@ def get_test_data_with_known_distribution(self, num_samples, dtype, means=None):
6868

6969
cov_mat = np.array([[1, 0.5, 0], [0.5, 1, -0.25], [0, -0.25, 1]])
7070

71-
data = np.random.multivariate_normal(means, cov_mat, num_samples)
71+
rng = np.random.RandomState(seed)
72+
data = rng.multivariate_normal(means, cov_mat, num_samples)
7273

7374
# Set the dtype, if `int16`, first scale to +/- 1 then cast to int16 range.
7475
if dtype == np.float32:

src/spikeinterface/sorters/basesorter.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,25 @@ def initialize_folder(cls, recording, output_folder, verbose, remove_existing_fo
152152

153153
return output_folder
154154

155+
@classmethod
156+
def _dynamic_params(cls):
157+
# optional
158+
# can be implemented in subclass for dynamic default parameters
159+
return cls._default_params, cls._params_description
160+
155161
@classmethod
156162
def default_params(cls):
157-
p = copy.deepcopy(cls._default_params)
163+
default_params, _ = cls._dynamic_params()
164+
p = copy.deepcopy(default_params)
158165
if cls.requires_binary_data:
159166
job_kwargs = get_global_job_kwargs()
160167
p.update(job_kwargs)
161168
return p
162169

163170
@classmethod
164171
def params_description(cls):
165-
p = copy.deepcopy(cls._params_description)
172+
_, default_params_description = cls._dynamic_params()
173+
p = copy.deepcopy(default_params_description)
166174
if cls.requires_binary_data:
167175
p.update(default_job_kwargs_description)
168176
return p

src/spikeinterface/sorters/external/kilosort4.py

Lines changed: 42 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pathlib import Path
44
from typing import Union
5+
import warnings
56
from packaging import version
67

78

@@ -22,95 +23,29 @@ class Kilosort4Sorter(BaseSorter):
2223
gpu_capability = "nvidia-optional"
2324
requires_binary_data = False
2425

25-
_default_params = {
26-
"batch_size": 60000,
27-
"nblocks": 1,
28-
"Th_universal": 9,
29-
"Th_learned": 8,
26+
_si_default_params = {
3027
"do_CAR": True,
3128
"invert_sign": False,
32-
"nt": 61,
33-
"shift": None,
34-
"scale": None,
35-
"artifact_threshold": None,
36-
"nskip": 25,
37-
"whitening_range": 32,
38-
"highpass_cutoff": 300,
39-
"binning_depth": 5,
40-
"sig_interp": 20,
41-
"drift_smoothing": [0.5, 0.5, 0.5],
42-
"nt0min": None,
43-
"dmin": None,
44-
"dminx": 32,
45-
"min_template_size": 10,
46-
"template_sizes": 5,
47-
"nearest_chans": 10,
48-
"nearest_templates": 100,
49-
"max_channel_distance": None,
50-
"templates_from_data": True,
51-
"n_templates": 6,
52-
"n_pcs": 6,
53-
"Th_single_ch": 6,
54-
"acg_threshold": 0.2,
55-
"ccg_threshold": 0.25,
56-
"cluster_downsampling": 20,
57-
"cluster_pcs": 64,
58-
"x_centers": None,
59-
"duplicate_spike_ms": 0.25,
60-
"scaleproc": None,
29+
"save_extra_vars": False,
6130
"save_preprocessed_copy": False,
6231
"torch_device": "auto",
6332
"bad_channels": None,
6433
"clear_cache": False,
65-
"save_extra_vars": False,
6634
"do_correction": True,
67-
"keep_good_only": False,
6835
"skip_kilosort_preprocessing": False,
36+
"keep_good_only": False,
6937
"use_binary_file": True,
7038
"delete_recording_dat": True,
7139
}
7240

73-
_params_description = {
74-
"batch_size": "Number of samples included in each batch of data.",
75-
"nblocks": "Number of non-overlapping blocks for drift correction (additional nblocks-1 blocks are created in the overlaps). Default value: 1.",
76-
"Th_universal": "Spike detection threshold for universal templates. Th(1) in previous versions of Kilosort. Default value: 9.",
77-
"Th_learned": "Spike detection threshold for learned templates. Th(2) in previous versions of Kilosort. Default value: 8.",
78-
"do_CAR": "Whether to perform common average reference. Default value: True.",
79-
"invert_sign": "Invert the sign of the data. Default value: False.",
80-
"nt": "Number of samples per waveform. Also size of symmetric padding for filtering. Default value: 61.",
81-
"shift": "Scalar shift to apply to data before all other operations. Default None.",
82-
"scale": "Scaling factor to apply to data before all other operations. Default None.",
83-
"artifact_threshold": "If a batch contains absolute values above this number, it will be zeroed out under the assumption that a recording artifact is present. By default, the threshold is infinite (so that no zeroing occurs). Default value: None.",
84-
"nskip": "Batch stride for computing whitening matrix. Default value: 25.",
85-
"whitening_range": "Number of nearby channels used to estimate the whitening matrix. Default value: 32.",
86-
"highpass_cutoff": "High-pass filter cutoff frequency in Hz. Default value: 300.",
87-
"binning_depth": "For drift correction, vertical bin size in microns used for 2D histogram. Default value: 5.",
88-
"sig_interp": "For drift correction, sigma for interpolation (spatial standard deviation). Approximate smoothness scale in units of microns. Default value: 20.",
89-
"drift_smoothing": "Amount of gaussian smoothing to apply to the spatiotemporal drift estimation, for x,y,time axes in units of registration blocks (for x,y axes) and batch size (for time axis). The x,y smoothing has no effect for `nblocks = 1`.",
90-
"nt0min": "Sample index for aligning waveforms, so that their minimum or maximum value happens here. Default of 20. Default value: None.",
91-
"dmin": "Vertical spacing of template centers used for spike detection, in microns. Determined automatically by default. Default value: None.",
92-
"dminx": "Horizontal spacing of template centers used for spike detection, in microns. Default value: 32.",
93-
"min_template_size": "Standard deviation of the smallest, spatial envelope Gaussian used for universal templates. Default value: 10.",
94-
"template_sizes": "Number of sizes for universal spike templates (multiples of the min_template_size). Default value: 5.",
95-
"nearest_chans": "Number of nearest channels to consider when finding local maxima during spike detection. Default value: 10.",
96-
"nearest_templates": "Number of nearest spike template locations to consider when finding local maxima during spike detection. Default value: 100.",
97-
"max_channel_distance": "Templates farther away than this from their nearest channel will not be used. Also limits distance between compared channels during clustering. Default value: None.",
98-
"templates_from_data": "Indicates whether spike shapes used in universal templates should be estimated from the data or loaded from the predefined templates. Default value: True.",
99-
"n_templates": "Number of single-channel templates to use for the universal templates (only used if templates_from_data is True). Default value: 6.",
100-
"n_pcs": "Number of single-channel PCs to use for extracting spike features (only used if templates_from_data is True). Default value: 6.",
101-
"Th_single_ch": "For single channel threshold crossings to compute universal- templates. In units of whitened data standard deviations. Default value: 6.",
102-
"acg_threshold": 'Fraction of refractory period violations that are allowed in the ACG compared to baseline; used to assign "good" units. Default value: 0.2.',
103-
"ccg_threshold": "Fraction of refractory period violations that are allowed in the CCG compared to baseline; used to perform splits and merges. Default value: 0.25.",
104-
"cluster_downsampling": "Inverse fraction of nodes used as landmarks during clustering (can be 1, but that slows down the optimization). Default value: 20.",
105-
"cluster_pcs": "Maximum number of spatiotemporal PC features used for clustering. Default value: 64.",
106-
"x_centers": "Number of x-positions to use when determining center points for template groupings. If None, this will be determined automatically by finding peaks in channel density. For 2D array type probes, we recommend specifying this so that centers are placed every few hundred microns.",
107-
"duplicate_spike_bins": "Number of bins for which subsequent spikes from the same cluster are assumed to be artifacts. A value of 0 disables this step. Default value: 7.",
108-
"save_extra_vars": "If True, additional kwargs are saved to the output",
109-
"scaleproc": "int16 scaling of whitened data, if None set to 200.",
110-
"save_preprocessed_copy": "Save a pre-processed copy of the data (including drift correction) to temp_wh.dat in the results directory and format Phy output to use that copy of the data",
111-
"torch_device": "Select the torch device auto/cuda/cpu",
112-
"bad_channels": "A list of channel indices (rows in the binary file) that should not be included in sorting. Listing channels here is equivalent to excluding them from the probe dictionary.",
113-
"clear_cache": "If True, force pytorch to free up memory reserved for its cache in between memory-intensive operations. Note that setting `clear_cache=True` is NOT recommended unless you encounter GPU out-of-memory errors, since this can result in slower sorting.",
41+
_si_params_description = {
42+
"do_CAR": "If True, common average reference is performed. Default is True. (run_kilosrt parameter)",
43+
"invert_sign": "Invert the sign of the data. Default value: False. (run_kilosort parameter)",
44+
"save_extra_vars": "If True, additional kwargs are saved to the output. Default is False. (run_kilosort parameter)",
45+
"save_preprocessed_copy": "Save a pre-processed copy of the data (including drift correction) to temp_wh.dat in the results directory and format Phy output to use that copy of the data. (run_kilosort parameter)",
46+
"torch_device": "Select the torch device auto/cuda/cpu. Default is 'auto'. (run_kilosort parameter)",
47+
"bad_channels": "A list of channel indices (rows in the binary file) that should not be included in sorting. Listing channels here is equivalent to excluding them from the probe dictionary. (run_kilosort parameter)",
48+
"clear_cache": "If True, force pytorch to free up memory reserved for its cache in between memory-intensive operations. Note that setting `clear_cache=True` is NOT recommended unless you encounter GPU out-of-memory errors, since this can result in slower sorting. (run_kilosort parameter)",
11449
"do_correction": "If True, drift correction is performed. Default is True. (spikeinterface parameter)",
11550
"skip_kilosort_preprocessing": "Can optionally skip the internal kilosort preprocessing. (spikeinterface parameter)",
11651
"keep_good_only": "If True, only the units labeled as 'good' by Kilosort are returned in the output. (spikeinterface parameter)",
@@ -151,6 +86,36 @@ def get_sorter_version(cls):
15186
"""kilosort.__version__ <4.0.10 is always '4'"""
15287
return importlib_version("kilosort")
15388

89+
@classmethod
90+
def _dynamic_params(cls):
91+
if cls.is_installed():
92+
import kilosort as ks
93+
94+
# we skip some parameters that are not relevant for the user
95+
# n_chan_bin/sampling_frequency: retrieved from the recording
96+
# tmin/tmax: same ase time/frame_slice in SpikeInterface
97+
skip_main = ["n_chan_bin", "sampling_frequency", "tmin", "tmax"]
98+
default_params = {}
99+
default_params_descriptions = {}
100+
ks_params = ks.parameters.MAIN_PARAMETERS.copy()
101+
ks_params.update(ks.parameters.EXTRA_PARAMETERS)
102+
for param, param_value in ks_params.items():
103+
if param not in skip_main:
104+
default_params[param] = param_value["default"]
105+
desc = param_value.get("description")
106+
if desc is None:
107+
desc = ""
108+
else:
109+
# get rid of escape characters and extra spaces
110+
desc = " ".join(desc.replace("\n", "").split())
111+
default_params_descriptions[param] = desc
112+
default_params.update(cls._si_default_params)
113+
default_params_descriptions.update(cls._si_params_description)
114+
return default_params, default_params_descriptions
115+
else:
116+
warnings.warn("Kilosort4 is not installed. Please install kilosort4 to get the parameters.")
117+
return {}, {}
118+
154119
@classmethod
155120
def initialize_folder(cls, recording, output_folder, verbose, remove_existing_folder):
156121
if not cls.is_installed():

0 commit comments

Comments
 (0)