Skip to content

Commit fbbb89f

Browse files
authored
Matched filtering with both peak signs simultaneously
1 parent 80cc888 commit fbbb89f

2 files changed

Lines changed: 59 additions & 71 deletions

File tree

src/spikeinterface/sortingcomponents/peak_detection.py

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -631,47 +631,31 @@ def __init__(
631631
self.conv_margin = prototype.shape[0]
632632

633633
assert peak_sign in ("both", "neg", "pos")
634-
idx = np.argmax(np.abs(prototype))
634+
self.nbefore = int(ms_before * recording.sampling_frequency / 1000)
635635
if peak_sign == "neg":
636-
assert prototype[idx] < 0, "Prototype should have a negative peak"
636+
assert prototype[self.nbefore] < 0, "Prototype should have a negative peak"
637637
peak_sign = "pos"
638638
elif peak_sign == "pos":
639-
assert prototype[idx] > 0, "Prototype should have a positive peak"
640-
elif peak_sign == "both":
641-
raise NotImplementedError("Matched filtering not working with peak_sign=both yet!")
639+
assert prototype[self.nbefore] > 0, "Prototype should have a positive peak"
642640

643641
self.peak_sign = peak_sign
644-
self.nbefore = int(ms_before * recording.sampling_frequency / 1000)
642+
self.prototype = np.flip(prototype) / np.linalg.norm(prototype)
643+
645644
contact_locations = recording.get_channel_locations()
646645
dist = np.linalg.norm(contact_locations[:, np.newaxis] - contact_locations[np.newaxis, :], axis=2)
647-
weights, self.z_factors = get_convolution_weights(dist, **weight_method)
646+
self.weights, self.z_factors = get_convolution_weights(dist, **weight_method)
647+
self.num_z_factors = len(self.z_factors)
648+
self.num_channels = recording.get_num_channels()
649+
self.num_templates = self.num_channels
650+
if peak_sign == "both":
651+
self.weights = np.hstack((self.weights, self.weights))
652+
self.weights[:, self.num_templates :, :] *= -1
653+
self.num_templates *= 2
648654

649-
num_channels = recording.get_num_channels()
650-
num_templates = num_channels * len(self.z_factors)
651-
weights = weights.reshape(num_templates, -1)
652-
653-
templates = weights[:, None, :] * prototype[None, :, None]
654-
templates -= templates.mean(axis=(1, 2))[:, None, None]
655-
temporal, singular, spatial = np.linalg.svd(templates, full_matrices=False)
656-
temporal = temporal[:, :, :rank]
657-
singular = singular[:, :rank]
658-
spatial = spatial[:, :rank, :]
659-
templates = np.matmul(temporal * singular[:, np.newaxis, :], spatial)
660-
norms = np.linalg.norm(templates, axis=(1, 2))
661-
del templates
662-
663-
temporal /= norms[:, np.newaxis, np.newaxis]
664-
temporal = np.flip(temporal, axis=1)
665-
spatial = np.moveaxis(spatial, [0, 1, 2], [1, 0, 2])
666-
temporal = np.moveaxis(temporal, [0, 1, 2], [1, 2, 0])
667-
singular = singular.T[:, :, np.newaxis]
668-
669-
self.temporal = temporal
670-
self.spatial = spatial
671-
self.singular = singular
655+
self.weights = self.weights.reshape(self.num_templates * self.num_z_factors, -1)
672656

673657
random_data = get_random_data_chunks(recording, return_scaled=False, **random_chunk_kwargs)
674-
conv_random_data = self.get_convolved_traces(random_data, temporal, spatial, singular)
658+
conv_random_data = self.get_convolved_traces(random_data)
675659
medians = np.median(conv_random_data, axis=1)
676660
medians = medians[:, None]
677661
noise_levels = np.median(np.abs(conv_random_data - medians), axis=1) / 0.6744897501960817
@@ -688,16 +672,13 @@ def get_trace_margin(self):
688672
def compute(self, traces, start_frame, end_frame, segment_index, max_margin):
689673

690674
assert HAVE_NUMBA, "You need to install numba"
691-
conv_traces = self.get_convolved_traces(traces, self.temporal, self.spatial, self.singular)
675+
conv_traces = self.get_convolved_traces(traces)
692676
conv_traces /= self.abs_thresholds[:, None]
693677
conv_traces = conv_traces[:, self.conv_margin : -self.conv_margin]
694678
traces_center = conv_traces[:, self.exclude_sweep_size : -self.exclude_sweep_size]
695679

696-
num_z_factors = len(self.z_factors)
697-
num_templates = traces.shape[1]
698-
699-
traces_center = traces_center.reshape(num_z_factors, num_templates, traces_center.shape[1])
700-
conv_traces = conv_traces.reshape(num_z_factors, num_templates, conv_traces.shape[1])
680+
traces_center = traces_center.reshape(self.num_z_factors, self.num_templates, traces_center.shape[1])
681+
conv_traces = conv_traces.reshape(self.num_z_factors, self.num_templates, conv_traces.shape[1])
701682
peak_mask = traces_center > 1
702683

703684
peak_mask = _numba_detect_peak_matched_filtering(
@@ -708,11 +689,13 @@ def compute(self, traces, start_frame, end_frame, segment_index, max_margin):
708689
self.abs_thresholds,
709690
self.peak_sign,
710691
self.neighbours_mask,
711-
num_templates,
692+
self.num_channels,
712693
)
713694

714695
# Find peaks and correct for time shift
715696
z_ind, peak_chan_ind, peak_sample_ind = np.nonzero(peak_mask)
697+
if self.peak_sign == "both":
698+
peak_chan_ind = peak_chan_ind % self.num_channels
716699

717700
# If we want to estimate z
718701
# peak_chan_ind = peak_chan_ind % num_channels
@@ -739,16 +722,11 @@ def compute(self, traces, start_frame, end_frame, segment_index, max_margin):
739722
# return is always a tuple
740723
return (local_peaks,)
741724

742-
def get_convolved_traces(self, traces, temporal, spatial, singular):
725+
def get_convolved_traces(self, traces):
743726
import scipy.signal
744727

745-
num_timesteps, num_templates = len(traces), temporal.shape[1]
746-
num_peaks = num_timesteps - self.conv_margin + 1
747-
scalar_products = np.zeros((num_templates, num_peaks), dtype=np.float32)
748-
spatially_filtered_data = np.matmul(spatial, traces.T[np.newaxis, :, :])
749-
scaled_filtered_data = spatially_filtered_data * singular
750-
objective_by_rank = scipy.signal.oaconvolve(scaled_filtered_data, temporal, axes=2, mode="valid")
751-
scalar_products += np.sum(objective_by_rank, axis=0)
728+
tmp = scipy.signal.oaconvolve(self.prototype[None, :], traces.T, axes=1, mode="valid")
729+
scalar_products = np.dot(self.weights, tmp)
752730
return scalar_products
753731

754732

@@ -873,37 +851,28 @@ def _numba_detect_peak_neg(
873851

874852
@numba.jit(nopython=True, parallel=False)
875853
def _numba_detect_peak_matched_filtering(
876-
traces, traces_center, peak_mask, exclude_sweep_size, abs_thresholds, peak_sign, neighbours_mask, num_templates
854+
traces, traces_center, peak_mask, exclude_sweep_size, abs_thresholds, peak_sign, neighbours_mask, num_channels
877855
):
878856
num_z = traces_center.shape[0]
857+
num_templates = traces_center.shape[1]
879858
for template_ind in range(num_templates):
880859
for z in range(num_z):
881860
for s in range(peak_mask.shape[2]):
882861
if not peak_mask[z, template_ind, s]:
883862
continue
884863
for neighbour in range(num_templates):
885-
if not neighbours_mask[template_ind, neighbour]:
886-
continue
887864
for j in range(num_z):
865+
if not neighbours_mask[template_ind % num_channels, neighbour % num_channels]:
866+
continue
888867
for i in range(exclude_sweep_size):
889-
if template_ind >= neighbour:
890-
if z >= j:
891-
peak_mask[z, template_ind, s] &= (
892-
traces_center[z, template_ind, s] >= traces_center[j, neighbour, s]
893-
)
894-
else:
895-
peak_mask[z, template_ind, s] &= (
896-
traces_center[z, template_ind, s] > traces_center[j, neighbour, s]
897-
)
898-
elif template_ind < neighbour:
899-
if z > j:
900-
peak_mask[z, template_ind, s] &= (
901-
traces_center[z, template_ind, s] > traces_center[j, neighbour, s]
902-
)
903-
else:
904-
peak_mask[z, template_ind, s] &= (
905-
traces_center[z, template_ind, s] > traces_center[j, neighbour, s]
906-
)
868+
if template_ind >= neighbour and z >= j:
869+
peak_mask[z, template_ind, s] &= (
870+
traces_center[z, template_ind, s] >= traces_center[j, neighbour, s]
871+
)
872+
else:
873+
peak_mask[z, template_ind, s] &= (
874+
traces_center[z, template_ind, s] > traces_center[j, neighbour, s]
875+
)
907876
peak_mask[z, template_ind, s] &= (
908877
traces_center[z, template_ind, s] > traces[j, neighbour, s + i]
909878
)

src/spikeinterface/sortingcomponents/tests/test_peak_detection.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,19 +328,38 @@ def test_detect_peaks_locally_exclusive_matched_filtering(recording, job_kwargs)
328328
)
329329
assert len(peaks_local_mf_filtering) > len(peaks_by_channel_np)
330330

331+
peaks_local_mf_filtering_both = detect_peaks(
332+
recording,
333+
method="matched_filtering",
334+
peak_sign="both",
335+
detect_threshold=5,
336+
exclude_sweep_ms=0.1,
337+
prototype=prototype,
338+
ms_before=1.0,
339+
**job_kwargs,
340+
)
341+
assert len(peaks_local_mf_filtering_both) > len(peaks_local_mf_filtering)
342+
331343
DEBUG = False
332344
if DEBUG:
333345
import matplotlib.pyplot as plt
334346

335-
peaks = peaks_local_mf_filtering
347+
peaks_local = peaks_by_channel_np
348+
peaks_mf_neg = peaks_local_mf_filtering
349+
peaks_mf_both = peaks_local_mf_filtering_both
350+
labels = ["locally_exclusive", "mf_neg", "mf_both"]
336351

337-
sample_inds, chan_inds, amplitudes = peaks["sample_index"], peaks["channel_index"], peaks["amplitude"]
352+
fig, ax = plt.subplots()
338353
chan_offset = 500
339354
traces = recording.get_traces().copy()
340355
traces += np.arange(traces.shape[1])[None, :] * chan_offset
341-
fig, ax = plt.subplots()
342356
ax.plot(traces, color="k")
343-
ax.scatter(sample_inds, chan_inds * chan_offset + amplitudes, color="r")
357+
358+
for count, peaks in enumerate([peaks_local, peaks_mf_neg, peaks_mf_both]):
359+
sample_inds, chan_inds, amplitudes = peaks["sample_index"], peaks["channel_index"], peaks["amplitude"]
360+
ax.scatter(sample_inds, chan_inds * chan_offset + amplitudes, label=labels[count])
361+
362+
ax.legend()
344363
plt.show()
345364

346365

0 commit comments

Comments
 (0)