Skip to content

Commit 3e608c6

Browse files
ygerpre-commit-ci[bot]samuelgarcia
authored
Torch support for matching engines circus and OMP
* Fixes * Patches * Fixes for SC2 and for split clustering * debugging clustering * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Torch for convolutions * Forcing data structures to be float32 * Device and wobble * WIP * Speeding up wobble * WIP * WIP * Troch * WIP torch * WIP * WIP * Addition of a detection node for coherence * Doc * WIP * Default params * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * Handling context with torch on the fly * Dealing with torch * Adding support for torch in matching engines * Automatic handling of torch * Default back * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Adding gather_func to find_spikes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Gathering mode more explicit for matching * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * WIP * Fixes for SC2 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * Simplifications * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Naming for Sam * Optimize circus matching engine * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Optimizations * Remove the limit to chunk sizes in circus-omp-svd * WIP * Wobble also * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Wobble also * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Oups * WIP * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixes * Backward compatibility* * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Naming * Cleaning * Bringing back context for peak detectors * Update src/spikeinterface/benchmark/benchmark_matching.py * Update src/spikeinterface/sortingcomponents/matching/circus.py * WIP * Patch imports * WIP * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixing tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WIP * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * KSPeeler * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Moving KS in a new PR * Moving KS in a new PR * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Allow spawn and cuda for circus * Add push_to_torch to allow pickling of objects * Default * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Cleaning docs * WIP --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Garcia Samuel <sam.garcia.die@gmail.com>
1 parent 9fa21c9 commit 3e608c6

7 files changed

Lines changed: 235 additions & 89 deletions

File tree

src/spikeinterface/benchmark/benchmark_matching.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def plot_performances_comparison(self, **kwargs):
8080
def plot_collisions(self, case_keys=None, figsize=None):
8181
if case_keys is None:
8282
case_keys = list(self.cases.keys())
83+
import matplotlib.pyplot as plt
8384

8485
fig, axs = plt.subplots(ncols=len(case_keys), nrows=1, figsize=figsize, squeeze=False)
8586

src/spikeinterface/sorters/internal/spyking_circus2.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Spykingcircus2Sorter(ComponentsBasedSorter):
2727
"general": {"ms_before": 2, "ms_after": 2, "radius_um": 75},
2828
"sparsity": {"method": "snr", "amplitude_mode": "peak_to_peak", "threshold": 0.25},
2929
"filtering": {"freq_min": 150, "freq_max": 7000, "ftype": "bessel", "filter_order": 2},
30-
"whitening": {"mode": "local", "regularize": True},
30+
"whitening": {"mode": "local", "regularize": False},
3131
"detection": {"peak_sign": "neg", "detect_threshold": 4},
3232
"selection": {
3333
"method": "uniform",
@@ -100,6 +100,12 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose):
100100
except:
101101
HAVE_HDBSCAN = False
102102

103+
try:
104+
import torch
105+
except ImportError:
106+
HAVE_TORCH = False
107+
print("spykingcircus2 could benefit from using torch. Consider installing it")
108+
103109
assert HAVE_HDBSCAN, "spykingcircus2 needs hdbscan to be installed"
104110

105111
# this is importanted only on demand because numba import are too heavy

src/spikeinterface/sortingcomponents/clustering/circus.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def main_function(cls, recording, peaks, params):
9292
# SVD for time compression
9393
few_peaks = select_peaks(peaks, recording=recording, method="uniform", n_peaks=10000, margin=(nbefore, nafter))
9494
few_wfs = extract_waveform_at_max_channel(
95-
recording, few_peaks, ms_before=ms_before, ms_after=ms_after, **params["job_kwargs"]
95+
recording, few_peaks, ms_before=ms_before, ms_after=ms_after, **job_kwargs
9696
)
9797

9898
wfs = few_wfs[:, :, 0]
@@ -141,7 +141,7 @@ def main_function(cls, recording, peaks, params):
141141
all_pc_data = run_node_pipeline(
142142
recording,
143143
pipeline_nodes,
144-
params["job_kwargs"],
144+
job_kwargs,
145145
job_name="extracting features",
146146
)
147147

@@ -176,7 +176,7 @@ def main_function(cls, recording, peaks, params):
176176
_ = run_node_pipeline(
177177
recording,
178178
pipeline_nodes,
179-
params["job_kwargs"],
179+
job_kwargs,
180180
job_name="extracting features",
181181
gather_mode="npy",
182182
gather_kwargs=dict(exist_ok=True),

src/spikeinterface/sortingcomponents/matching/circus.py

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
("segment_index", "int64"),
1919
]
2020

21+
try:
22+
import torch
23+
import torch.nn.functional as F
24+
25+
HAVE_TORCH = True
26+
from torch.nn.functional import conv1d
27+
except ImportError:
28+
HAVE_TORCH = False
29+
2130
from .base import BaseTemplateMatching
2231

2332

@@ -43,9 +52,9 @@ def compress_templates(
4352

4453
temporal, singular, spatial = np.linalg.svd(templates_array, full_matrices=False)
4554
# Keep only the strongest components
46-
temporal = temporal[:, :, :approx_rank]
47-
singular = singular[:, :approx_rank]
48-
spatial = spatial[:, :approx_rank, :]
55+
temporal = temporal[:, :, :approx_rank].astype(np.float32)
56+
singular = singular[:, :approx_rank].astype(np.float32)
57+
spatial = spatial[:, :approx_rank, :].astype(np.float32)
4958

5059
if return_new_templates:
5160
templates_array = np.matmul(temporal * singular[:, np.newaxis, :], spatial)
@@ -107,18 +116,22 @@ class CircusOMPSVDPeeler(BaseTemplateMatching):
107116
108117
Parameters
109118
----------
110-
amplitude: tuple
119+
amplitude : tuple
111120
(Minimal, Maximal) amplitudes allowed for every template
112-
max_failures: int
121+
max_failures : int
113122
Stopping criteria of the OMP algorithm, as number of retry while updating amplitudes
114-
sparse_kwargs: dict
123+
sparse_kwargs : dict
115124
Parameters to extract a sparsity mask from the waveform_extractor, if not
116125
already sparse.
117-
rank: int, default: 5
126+
rank : int, default: 5
118127
Number of components used internally by the SVD
119-
vicinity: int
128+
vicinity : int
120129
Size of the area surrounding a spike to perform modification (expressed in terms
121130
of template temporal width)
131+
engine : string in ["numpy", "torch", "auto"]. Default "auto"
132+
The engine to use for the convolutions
133+
torch_device : string in ["cpu", "cuda", None]. Default "cpu"
134+
Controls torch device if the torch engine is selected
122135
-----
123136
"""
124137

@@ -148,6 +161,8 @@ def __init__(
148161
ignore_inds=[],
149162
vicinity=2,
150163
precomputed=None,
164+
engine="numpy",
165+
torch_device="cpu",
151166
):
152167

153168
BaseTemplateMatching.__init__(self, recording, templates, return_output=True, parents=None)
@@ -158,6 +173,19 @@ def __init__(
158173
self.nafter = templates.nafter
159174
self.sampling_frequency = recording.get_sampling_frequency()
160175
self.vicinity = vicinity * self.num_samples
176+
assert engine in ["numpy", "torch", "auto"], "engine should be numpy, torch or auto"
177+
if engine == "auto":
178+
if HAVE_TORCH:
179+
self.engine = "torch"
180+
else:
181+
self.engine = "numpy"
182+
else:
183+
if engine == "torch":
184+
assert HAVE_TORCH, "please install torch to use the torch engine"
185+
self.engine = engine
186+
187+
assert torch_device in ["cuda", "cpu", None]
188+
self.torch_device = torch_device
161189

162190
self.amplitudes = amplitudes
163191
self.stop_criteria = stop_criteria
@@ -183,6 +211,7 @@ def __init__(
183211
self.unit_overlaps_tables[i][self.unit_overlaps_indices[i]] = np.arange(len(self.unit_overlaps_indices[i]))
184212

185213
self.margin = 2 * self.num_samples
214+
self.is_pushed = False
186215

187216
def _prepare_templates(self):
188217

@@ -254,6 +283,14 @@ def _prepare_templates(self):
254283
self.temporal = np.moveaxis(self.temporal, [0, 1, 2], [1, 2, 0])
255284
self.singular = self.singular.T[:, :, np.newaxis]
256285

286+
def _push_to_torch(self):
287+
if self.engine == "torch":
288+
self.spatial = torch.as_tensor(self.spatial, device=self.torch_device)
289+
self.singular = torch.as_tensor(self.singular, device=self.torch_device)
290+
self.temporal = torch.as_tensor(self.temporal.copy(), device=self.torch_device).swapaxes(0, 1)
291+
self.temporal = torch.flip(self.temporal, (2,))
292+
self.is_pushed = True
293+
257294
def get_extra_outputs(self):
258295
output = {}
259296
for key in self._more_output_keys:
@@ -268,43 +305,52 @@ def compute_matching(self, traces, start_frame, end_frame, segment_index):
268305
import scipy
269306
from scipy import ndimage
270307

271-
(potrs,) = scipy.linalg.get_lapack_funcs(("potrs",), dtype=np.float32)
308+
if not self.is_pushed:
309+
self._push_to_torch()
272310

311+
(potrs,) = scipy.linalg.get_lapack_funcs(("potrs",), dtype=np.float32)
273312
(nrm2,) = scipy.linalg.get_blas_funcs(("nrm2",), dtype=np.float32)
274313

275-
overlaps_array = self.overlaps
276-
277314
omp_tol = np.finfo(np.float32).eps
278-
num_samples = self.nafter + self.nbefore
279-
neighbor_window = num_samples - 1
315+
neighbor_window = self.num_samples - 1
316+
280317
if isinstance(self.amplitudes, list):
281318
min_amplitude, max_amplitude = self.amplitudes
282319
else:
283320
min_amplitude, max_amplitude = self.amplitudes[:, 0], self.amplitudes[:, 1]
284321
min_amplitude = min_amplitude[:, np.newaxis]
285322
max_amplitude = max_amplitude[:, np.newaxis]
286323

287-
num_timesteps = len(traces)
324+
if self.engine == "torch":
325+
blank = np.zeros((neighbor_window, self.num_channels), dtype=np.float32)
326+
traces = np.vstack((blank, traces, blank))
327+
num_timesteps = traces.shape[0]
328+
torch_traces = torch.as_tensor(traces.T[np.newaxis, :, :], device=self.torch_device)
329+
num_templates, num_channels = self.temporal.shape[0], self.temporal.shape[1]
330+
spatially_filtered_data = torch.matmul(self.spatial, torch_traces)
331+
scaled_filtered_data = (spatially_filtered_data * self.singular).swapaxes(0, 1)
332+
scaled_filtered_data_ = scaled_filtered_data.reshape(1, num_templates * num_channels, num_timesteps)
333+
scalar_products = conv1d(scaled_filtered_data_, self.temporal, groups=num_templates, padding="valid")
334+
scalar_products = scalar_products.cpu().numpy()[0, :, self.num_samples - 1 : -neighbor_window]
335+
else:
336+
num_timesteps = traces.shape[0]
337+
num_peaks = num_timesteps - neighbor_window
338+
conv_shape = (self.num_templates, num_peaks)
339+
scalar_products = np.zeros(conv_shape, dtype=np.float32)
340+
# Filter using overlap-and-add convolution
341+
spatially_filtered_data = np.matmul(self.spatial, traces.T[np.newaxis, :, :])
342+
scaled_filtered_data = spatially_filtered_data * self.singular
343+
from scipy import signal
288344

289-
num_peaks = num_timesteps - num_samples + 1
290-
conv_shape = (self.num_templates, num_peaks)
291-
scalar_products = np.zeros(conv_shape, dtype=np.float32)
345+
objective_by_rank = signal.oaconvolve(scaled_filtered_data, self.temporal, axes=2, mode="valid")
346+
scalar_products += np.sum(objective_by_rank, axis=0)
347+
348+
num_peaks = scalar_products.shape[1]
292349

293350
# Filter using overlap-and-add convolution
294351
if len(self.ignore_inds) > 0:
295-
not_ignored = ~np.isin(np.arange(self.num_templates), self.ignore_inds)
296-
spatially_filtered_data = np.matmul(self.spatial[:, not_ignored, :], traces.T[np.newaxis, :, :])
297-
scaled_filtered_data = spatially_filtered_data * self.singular[:, not_ignored, :]
298-
objective_by_rank = scipy.signal.oaconvolve(
299-
scaled_filtered_data, self.temporal[:, not_ignored, :], axes=2, mode="valid"
300-
)
301-
scalar_products[not_ignored] += np.sum(objective_by_rank, axis=0)
302352
scalar_products[self.ignore_inds] = -np.inf
303-
else:
304-
spatially_filtered_data = np.matmul(self.spatial, traces.T[np.newaxis, :, :])
305-
scaled_filtered_data = spatially_filtered_data * self.singular
306-
objective_by_rank = scipy.signal.oaconvolve(scaled_filtered_data, self.temporal, axes=2, mode="valid")
307-
scalar_products += np.sum(objective_by_rank, axis=0)
353+
not_ignored = ~np.isin(np.arange(self.num_templates), self.ignore_inds)
308354

309355
num_spikes = 0
310356

@@ -322,7 +368,7 @@ def compute_matching(self, traces, start_frame, end_frame, segment_index):
322368
is_in_vicinity = np.zeros(0, dtype=np.int32)
323369

324370
if self.stop_criteria == "omp_min_sps":
325-
stop_criteria = self.omp_min_sps * np.maximum(self.norms, np.sqrt(self.num_channels * num_samples))
371+
stop_criteria = self.omp_min_sps * np.maximum(self.norms, np.sqrt(self.num_channels * self.num_samples))
326372
elif self.stop_criteria == "max_failures":
327373
num_valids = 0
328374
nb_failures = self.max_failures
@@ -354,11 +400,11 @@ def compute_matching(self, traces, start_frame, end_frame, segment_index):
354400

355401
if num_selection > 0:
356402
delta_t = selection[1] - peak_index
357-
idx = np.flatnonzero((delta_t < num_samples) & (delta_t > -num_samples))
403+
idx = np.flatnonzero((delta_t < self.num_samples) & (delta_t > -self.num_samples))
358404
myline = neighbor_window + delta_t[idx]
359405
myindices = selection[0, idx]
360406

361-
local_overlaps = overlaps_array[best_cluster_ind]
407+
local_overlaps = self.overlaps[best_cluster_ind]
362408
overlapping_templates = self.unit_overlaps_indices[best_cluster_ind]
363409
table = self.unit_overlaps_tables[best_cluster_ind]
364410

@@ -436,10 +482,10 @@ def compute_matching(self, traces, start_frame, end_frame, segment_index):
436482
for i in modified:
437483
tmp_best, tmp_peak = sub_selection[:, i]
438484
diff_amp = diff_amplitudes[i] * self.norms[tmp_best]
439-
local_overlaps = overlaps_array[tmp_best]
485+
local_overlaps = self.overlaps[tmp_best]
440486
overlapping_templates = self.units_overlaps[tmp_best]
441487
tmp = tmp_peak - neighbor_window
442-
idx = [max(0, tmp), min(num_peaks, tmp_peak + num_samples)]
488+
idx = [max(0, tmp), min(num_peaks, tmp_peak + self.num_samples)]
443489
tdx = [idx[0] - tmp, idx[1] - tmp]
444490
to_add = diff_amp * local_overlaps[:, tdx[0] : tdx[1]]
445491
scalar_products[overlapping_templates, idx[0] : idx[1]] -= to_add
@@ -500,27 +546,27 @@ class CircusPeeler(BaseTemplateMatching):
500546
501547
Parameters
502548
----------
503-
peak_sign: str
549+
peak_sign : str
504550
Sign of the peak (neg, pos, or both)
505-
exclude_sweep_ms: float
551+
exclude_sweep_ms : float
506552
The number of samples before/after to classify a peak (should be low)
507-
jitter: int
553+
jitter : int
508554
The number of samples considered before/after every peak to search for
509555
matches
510-
detect_threshold: int
556+
detect_threshold : int
511557
The detection threshold
512-
noise_levels: array
558+
noise_levels : array
513559
The noise levels, for every channels
514-
random_chunk_kwargs: dict
560+
random_chunk_kwargs : dict
515561
Parameters for computing noise levels, if not provided (sub optimal)
516-
max_amplitude: float
562+
max_amplitude : float
517563
Maximal amplitude allowed for every template
518-
min_amplitude: float
564+
min_amplitude : float
519565
Minimal amplitude allowed for every template
520-
use_sparse_matrix_threshold: float
566+
use_sparse_matrix_threshold : float
521567
If density of the templates is below a given threshold, sparse matrix
522568
are used (memory efficient)
523-
sparse_kwargs: dict
569+
sparse_kwargs : dict
524570
Parameters to extract a sparsity mask from the waveform_extractor, if not
525571
already sparse.
526572
-----

0 commit comments

Comments
 (0)