Skip to content

Commit b1a7706

Browse files
authored
Improve Split PCA
1 parent d26744e commit b1a7706

6 files changed

Lines changed: 95 additions & 48 deletions

File tree

src/spikeinterface/sorters/internal/spyking_circus2.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Spykingcircus2Sorter(ComponentsBasedSorter):
3232
"sparsity": {"method": "snr", "amplitude_mode": "peak_to_peak", "threshold": 0.25},
3333
"filtering": {"freq_min": 150, "freq_max": 7000, "ftype": "bessel", "filter_order": 2, "margin_ms": 10},
3434
"whitening": {"mode": "local", "regularize": False},
35-
"detection": {"peak_sign": "neg", "detect_threshold": 4},
35+
"detection": {"peak_sign": "neg", "detect_threshold": 5},
3636
"selection": {
3737
"method": "uniform",
3838
"n_peaks_per_channel": 5000,
@@ -134,7 +134,7 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose):
134134
ms_before = params["general"].get("ms_before", 2)
135135
ms_after = params["general"].get("ms_after", 2)
136136
radius_um = params["general"].get("radius_um", 75)
137-
exclude_sweep_ms = params["detection"].get("exclude_sweep_ms", max(ms_before, ms_after) / 2)
137+
exclude_sweep_ms = params["detection"].get("exclude_sweep_ms", max(ms_before, ms_after))
138138

139139
## First, we are filtering the data
140140
filtering_params = params["filtering"].copy()
@@ -265,6 +265,7 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose):
265265
clustering_params["ms_after"] = ms_after
266266
clustering_params["verbose"] = verbose
267267
clustering_params["tmp_folder"] = sorter_output_folder / "clustering"
268+
clustering_params["debug"] = params["debug"]
268269
clustering_params["noise_threshold"] = detection_params.get("detect_threshold", 4)
269270

270271
legacy = clustering_params.get("legacy", True)

src/spikeinterface/sortingcomponents/clustering/circus.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ class CircusClustering:
4141
"""
4242

4343
_default_params = {
44-
"hdbscan_kwargs": {"min_cluster_size": 10, "allow_single_cluster": True, "min_samples": 5},
44+
"hdbscan_kwargs": {
45+
"min_cluster_size": 20,
46+
"cluster_selection_epsilon": 0.5,
47+
"cluster_selection_method": "leaf",
48+
"allow_single_cluster": True,
49+
},
4550
"cleaning_kwargs": {},
4651
"waveforms": {"ms_before": 2, "ms_after": 2},
4752
"sparsity": {"method": "snr", "amplitude_mode": "peak_to_peak", "threshold": 0.25},
@@ -51,7 +56,7 @@ class CircusClustering:
5156
"returns_split_count": True,
5257
},
5358
"radius_um": 100,
54-
"n_svd": [5, 2],
59+
"n_svd": 5,
5560
"few_waveforms": None,
5661
"ms_before": 0.5,
5762
"ms_after": 0.5,
@@ -60,6 +65,7 @@ class CircusClustering:
6065
"noise_levels": None,
6166
"tmp_folder": None,
6267
"verbose": True,
68+
"debug": False,
6369
}
6470

6571
@classmethod
@@ -110,7 +116,7 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
110116

111117
from sklearn.decomposition import TruncatedSVD
112118

113-
tsvd = TruncatedSVD(params["n_svd"][0])
119+
tsvd = TruncatedSVD(params["n_svd"])
114120
tsvd.fit(wfs)
115121

116122
model_folder = tmp_folder / "tsvd_model"
@@ -166,8 +172,8 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
166172
sub_data = all_pc_data[mask]
167173
sub_data = sub_data.reshape(len(sub_data), -1)
168174

169-
if all_pc_data.shape[1] > params["n_svd"][1]:
170-
tsvd = PCA(params["n_svd"][1], whiten=True)
175+
if all_pc_data.shape[1] > params["n_svd"]:
176+
tsvd = PCA(params["n_svd"], whiten=True)
171177
else:
172178
tsvd = PCA(all_pc_data.shape[1], whiten=True)
173179

@@ -207,7 +213,12 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
207213
original_labels = peaks["channel_index"]
208214
from spikeinterface.sortingcomponents.clustering.split import split_clusters
209215

210-
min_size = 2 * params["hdbscan_kwargs"].get("min_cluster_size", 10)
216+
min_size = 2 * params["hdbscan_kwargs"].get("min_cluster_size", 20)
217+
218+
if params["debug"]:
219+
debug_folder = tmp_folder / "split"
220+
else:
221+
debug_folder = None
211222

212223
peak_labels, _ = split_clusters(
213224
original_labels,
@@ -221,9 +232,9 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
221232
waveforms_sparse_mask=sparse_mask,
222233
min_size_split=min_size,
223234
clusterer_kwargs=d["hdbscan_kwargs"],
224-
n_pca_features=params["n_svd"][1],
225-
scale_n_pca_by_depth=True,
235+
n_pca_features=5,
226236
),
237+
debug_folder=debug_folder,
227238
**params["recursive_kwargs"],
228239
**job_kwargs,
229240
)

src/spikeinterface/sortingcomponents/clustering/method_list.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from .dummy import DummyClustering
44
from .position import PositionClustering
55
from .sliding_hdbscan import SlidingHdbscanClustering
6-
from .sliding_nn import SlidingNNClustering
6+
7+
# from .sliding_nn import SlidingNNClustering
78
from .position_and_pca import PositionAndPCAClustering
89
from .position_ptp_scaled import PositionPTPScaledClustering
910
from .position_and_features import PositionAndFeaturesClustering
@@ -17,7 +18,7 @@
1718
"position_ptp_scaled": PositionPTPScaledClustering,
1819
"position_and_pca": PositionAndPCAClustering,
1920
"sliding_hdbscan": SlidingHdbscanClustering,
20-
"sliding_nn": SlidingNNClustering,
21+
# "sliding_nn": SlidingNNClustering,
2122
"position_and_features": PositionAndFeaturesClustering,
2223
"random_projections": RandomProjectionClustering,
2324
"circus": CircusClustering,

src/spikeinterface/sortingcomponents/clustering/random_projections.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class RandomProjectionClustering:
5656
"noise_threshold": 4,
5757
"tmp_folder": None,
5858
"verbose": True,
59+
"debug": False,
5960
}
6061

6162
@classmethod

src/spikeinterface/sortingcomponents/clustering/split.py

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def split_clusters(
2929
recursive=False,
3030
recursive_depth=None,
3131
returns_split_count=False,
32+
debug_folder=None,
3233
**job_kwargs,
3334
):
3435
"""
@@ -42,7 +43,7 @@ def split_clusters(
4243
Recording object
4344
features_dict_or_folder: dict or folder
4445
A dictionary of features precomputed with peak_pipeline or a folder containing npz file for features
45-
method: str, default: "hdbscan_on_local_pca"
46+
method: str, default: "local_feature_clustering"
4647
The method name
4748
method_kwargs: dict, default: dict()
4849
The method option
@@ -70,7 +71,7 @@ def split_clusters(
7071
original_labels = peak_labels
7172
peak_labels = peak_labels.copy()
7273
split_count = np.zeros(peak_labels.size, dtype=int)
73-
74+
recursion_level = 1
7475
Executor = get_poolexecutor(n_jobs)
7576

7677
with Executor(
@@ -82,19 +83,33 @@ def split_clusters(
8283
labels_set = np.setdiff1d(peak_labels, [-1])
8384
current_max_label = np.max(labels_set) + 1
8485
jobs = []
86+
87+
if debug_folder is not None:
88+
if debug_folder.exists():
89+
import shutil
90+
91+
shutil.rmtree(debug_folder)
92+
debug_folder.mkdir(parents=True, exist_ok=True)
93+
8594
for label in labels_set:
8695
peak_indices = np.flatnonzero(peak_labels == label)
96+
if debug_folder is not None:
97+
sub_folder = str(debug_folder / f"split_{label}")
98+
99+
else:
100+
sub_folder = None
87101
if peak_indices.size > 0:
88-
jobs.append(pool.submit(split_function_wrapper, peak_indices, 1))
102+
jobs.append(pool.submit(split_function_wrapper, peak_indices, recursion_level, sub_folder))
89103

90104
if progress_bar:
91-
iterator = tqdm(jobs, desc=f"split_clusters with {method}", total=len(labels_set))
92-
else:
93-
iterator = jobs
105+
pbar = tqdm(desc=f"split_clusters with {method}", total=len(labels_set))
106+
107+
for res in jobs:
108+
is_split, local_labels, peak_indices, sub_folder = res.result()
109+
110+
if progress_bar:
111+
pbar.update(1)
94112

95-
for res in iterator:
96-
is_split, local_labels, peak_indices = res.result()
97-
# print(is_split, local_labels, peak_indices)
98113
if not is_split:
99114
continue
100115

@@ -117,11 +132,21 @@ def split_clusters(
117132
new_labels_set = np.setdiff1d(peak_labels[peak_indices], [-1])
118133
for label in new_labels_set:
119134
peak_indices = np.flatnonzero(peak_labels == label)
135+
if sub_folder is not None:
136+
new_sub_folder = sub_folder + f"_{label}"
137+
else:
138+
new_sub_folder = None
120139
if peak_indices.size > 0:
121140
# print('Relaunched', label, len(peak_indices), recursion_level)
122-
jobs.append(pool.submit(split_function_wrapper, peak_indices, recursion_level))
141+
jobs.append(
142+
pool.submit(split_function_wrapper, peak_indices, recursion_level, new_sub_folder)
143+
)
123144
if progress_bar:
124-
iterator.total += 1
145+
pbar.total += 1
146+
147+
if progress_bar:
148+
pbar.close()
149+
del pbar
125150

126151
if returns_split_count:
127152
return peak_labels, split_count
@@ -149,13 +174,13 @@ def split_worker_init(
149174
_ctx["peaks"] = _ctx["features"]["peaks"]
150175

151176

152-
def split_function_wrapper(peak_indices, recursion_level):
177+
def split_function_wrapper(peak_indices, recursion_level, debug_folder):
153178
global _ctx
154179
with threadpool_limits(limits=_ctx["max_threads_per_worker"]):
155180
is_split, local_labels = _ctx["method_class"].split(
156-
peak_indices, _ctx["peaks"], _ctx["features"], recursion_level, **_ctx["method_kwargs"]
181+
peak_indices, _ctx["peaks"], _ctx["features"], recursion_level, debug_folder, **_ctx["method_kwargs"]
157182
)
158-
return is_split, local_labels, peak_indices
183+
return is_split, local_labels, peak_indices, debug_folder
159184

160185

161186
class LocalFeatureClustering:
@@ -178,14 +203,14 @@ def split(
178203
peaks,
179204
features,
180205
recursion_level=1,
206+
debug_folder=None,
181207
clusterer="hdbscan",
182208
feature_name="sparse_tsvd",
183209
neighbours_mask=None,
184210
waveforms_sparse_mask=None,
185211
clusterer_kwargs={"min_cluster_size": 25},
186212
min_size_split=25,
187213
n_pca_features=2,
188-
scale_n_pca_by_depth=False,
189214
minimum_overlap_ratio=0.25,
190215
):
191216
local_labels = np.zeros(peak_indices.size, dtype=np.int64)
@@ -213,26 +238,23 @@ def split(
213238

214239
local_labels[dont_have_channels] = -2
215240
kept = np.flatnonzero(~dont_have_channels)
216-
# print(recursion_level, kept.size, min_size_split)
241+
217242
if kept.size < min_size_split:
218243
return False, None
219244

220245
aligned_wfs = aligned_wfs[kept, :, :]
221-
222246
flatten_features = aligned_wfs.reshape(aligned_wfs.shape[0], -1)
223247

248+
is_split = False
249+
224250
if flatten_features.shape[1] > n_pca_features:
225251
from sklearn.decomposition import PCA
226252

227253
# from sklearn.decomposition import TruncatedSVD
228-
229-
if scale_n_pca_by_depth:
230-
# tsvd = TruncatedSVD(n_pca_features * recursion_level)
231-
tsvd = PCA(n_pca_features * recursion_level, whiten=True)
232-
else:
233-
# tsvd = TruncatedSVD(n_pca_features)
234-
tsvd = PCA(n_pca_features, whiten=True)
254+
# tsvd = TruncatedSVD(n_pca_features)
255+
tsvd = PCA(n_pca_features, whiten=True)
235256
final_features = tsvd.fit_transform(flatten_features)
257+
del tsvd
236258
else:
237259
final_features = flatten_features
238260

@@ -243,6 +265,7 @@ def split(
243265
clust.fit(final_features)
244266
possible_labels = clust.labels_
245267
is_split = np.setdiff1d(possible_labels, [-1]).size > 1
268+
del clust
246269
elif clusterer == "isocut5":
247270
min_cluster_size = clusterer_kwargs["min_cluster_size"]
248271
dipscore, cutpoint = isocut5(final_features[:, 0])
@@ -255,32 +278,43 @@ def split(
255278
else:
256279
is_split = False
257280
else:
258-
raise ValueError(f"wrong clusterer {clusterer}")
281+
raise ValueError(f"wrong clusterer {clusterer}. Possible options are 'hdbscan' or 'isocut5'.")
282+
283+
DEBUG = False # only for Sam or dirty hacking
259284

260-
# DEBUG = True
261-
DEBUG = False
262-
if DEBUG:
285+
if debug_folder is not None or DEBUG:
263286
import matplotlib.pyplot as plt
264287

265288
labels_set = np.setdiff1d(possible_labels, [-1])
266289
colors = plt.colormaps["tab10"].resampled(len(labels_set))
267290
colors = {k: colors(i) for i, k in enumerate(labels_set)}
268291
colors[-1] = "k"
269-
fix, axs = plt.subplots(nrows=2)
292+
fig, axs = plt.subplots(nrows=2)
270293

271294
flatten_wfs = aligned_wfs.swapaxes(1, 2).reshape(aligned_wfs.shape[0], -1)
272295

273-
sl = slice(None, None, 10)
296+
sl = slice(None, None, 100)
274297
for k in np.unique(possible_labels):
275298
mask = possible_labels == k
276299
ax = axs[0]
277-
ax.scatter(final_features[:, 0][mask][sl], final_features[:, 1][mask][sl], s=5, color=colors[k])
278-
300+
ax.scatter(final_features[:, 0][mask], final_features[:, 1][mask], s=5, color=colors[k])
301+
if k > -1:
302+
centroid = final_features[:, :2][mask].mean(axis=0)
303+
ax.text(centroid[0], centroid[1], f"Label {k}", fontsize=10, color="k")
279304
ax = axs[1]
280305
ax.plot(flatten_wfs[mask][sl].T, color=colors[k], alpha=0.5)
281-
282-
axs[0].set_title(f"{clusterer} {is_split} {peak_indices[0]} {np.unique(possible_labels)}")
283-
plt.show()
306+
if k > -1:
307+
ax.plot(np.median(flatten_wfs[mask].T, axis=1), color=colors[k], lw=2)
308+
ax.set_xlabel("PCA features")
309+
ymin, ymax = ax.get_ylim()
310+
ax.plot([n_pca_features, n_pca_features], [ymin, ymax], "k--")
311+
312+
axs[0].set_title(f"{clusterer} level={recursion_level}")
313+
if not DEBUG:
314+
fig.savefig(str(debug_folder) + ".png")
315+
plt.close(fig)
316+
else:
317+
plt.show()
284318

285319
if not is_split:
286320
return is_split, None

src/spikeinterface/sortingcomponents/clustering/tdc.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
162162
waveforms_sparse_mask=sparse_mask,
163163
min_size_split=min_cluster_size,
164164
n_pca_features=3,
165-
scale_n_pca_by_depth=True,
166165
),
167166
recursive=True,
168167
recursive_depth=3,

0 commit comments

Comments
 (0)