Skip to content

Commit 212a880

Browse files
committed
WIP
1 parent 3291976 commit 212a880

2 files changed

Lines changed: 202 additions & 20 deletions

File tree

src/spikeinterface/comparison/comparisontools.py

Lines changed: 200 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ def count_matching_events(times1, times2, delta=10):
3636
return len(inds2) + 1
3737

3838

39+
def count_match_spikes(times1, all_times2, delta_frames): # , event_counts1, event_counts2 unit2_ids,
40+
"""
41+
Computes matching spikes between one spike train and a list of others.
42+
43+
Parameters
44+
----------
45+
times1: array
46+
Spike train 1 frames
47+
all_times2: list of array
48+
List of spike trains from sorting 2
49+
50+
Returns
51+
-------
52+
matching_events_count: list
53+
List of counts of matching events
54+
"""
55+
matching_event_counts = np.zeros(len(all_times2), dtype="int64")
56+
for i2, times2 in enumerate(all_times2):
57+
num_matches = count_matching_events(times1, times2, delta=delta_frames)
58+
matching_event_counts[i2] = num_matches
59+
return matching_event_counts
60+
61+
3962
def compute_agreement_score(num_matches, num1, num2):
4063
"""
4164
Computes agreement score.
@@ -85,27 +108,186 @@ def do_count_event(sorting):
85108
return event_counts
86109

87110

88-
def count_match_spikes(times1, all_times2, delta_frames): # , event_counts1, event_counts2 unit2_ids,
111+
def get_optimized_dot_product():
112+
"""
113+
This function is to avoid the bare try-except pattern when importing the compute_dot_product function
114+
which uses numba. I tested using the numba dispatcher programatically to avoids this
115+
but the performance improvements were lost. Think you can do better? Don't forget to measure performance against
116+
the current implementation!
117+
TODO: unify numba decorator across all modules
89118
"""
90-
Computes matching spikes between one spike train and a list of others.
91119

92-
Parameters
93-
----------
94-
times1: array
95-
Spike train 1 frames
96-
all_times2: list of array
97-
List of spike trains from sorting 2
120+
if hasattr(get_optimized_dot_product, "_cached_function"):
121+
return get_optimized_dot_product._cached_function
98122

99-
Returns
100-
-------
101-
matching_events_count: list
102-
List of counts of matching events
103-
"""
104-
matching_event_counts = np.zeros(len(all_times2), dtype="int64")
105-
for i2, times2 in enumerate(all_times2):
106-
num_matches = count_matching_events(times1, times2, delta=delta_frames)
107-
matching_event_counts[i2] = num_matches
108-
return matching_event_counts
123+
import numba
124+
125+
@numba.jit(nopython=True, nogil=True)
126+
def compute_dot_product(
127+
spike_frames_train1,
128+
spike_frames_train2,
129+
unit_indices1,
130+
unit_indices2,
131+
num_units_train1,
132+
num_units_train2,
133+
delta_frames,
134+
):
135+
"""
136+
Computes the dot product between two spike trains.
137+
138+
The dot product in this case is the dot product of the spikes viewed as box-care functions in
139+
the Hilbert space L2.
140+
141+
The dot product gives a measure of the similarity between two spike trains. Each match is weighted by the
142+
delta_frames - abs(frame1 - frame2) where frame1 and frame2 are the frames of the matching spikes.
143+
144+
When the spike trains are identical, the dot product returns all the matches within the same spike train.
145+
The sum of this dot product is the squared norm of the spike train in the Hilbert space L2.
146+
147+
148+
Parameters
149+
----------
150+
spike_frames_train1 : ndarray
151+
An array of integer frame numbers corresponding to spike times for the first train. Must be in ascending order.
152+
spike_frames_train2 : ndarray
153+
An array of integer frame numbers corresponding to spike times for the second train. Must be in ascending order.
154+
unit_indices1 : ndarray
155+
An array of integers where `unit_indices1[i]` gives the unit index associated with the spike at `spike_frames_train1[i]`.
156+
unit_indices2 : ndarray
157+
An array of integers where `unit_indices2[i]` gives the unit index associated with the spike at `spike_frames_train2[i]`.
158+
num_units_train1 : int
159+
The total count of unique units in the first spike train.
160+
num_units_train2 : int
161+
The total count of unique units in the second spike train.
162+
delta_frames : int
163+
The inclusive upper limit on the frame difference for which two spikes are considered matching. That is
164+
if `abs(spike_frames_train1[i] - spike_frames_train2[j]) <= delta_frames` then the spikes at `spike_frames_train1[i]`
165+
and `spike_frames_train2[j]` are considered matching.
166+
167+
Returns
168+
-------
169+
dot_product : ndarray
170+
A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
171+
the dot product between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
172+
173+
174+
Notes
175+
-----
176+
This algorithm follows the same logic as the one used in `compute_matching_matrix` but instead of counting
177+
the number of matches, it computes the dot product between the two spike trains by weighting each match
178+
by the delta_frames - abs(frame1 - frame2) where frame1 and frame2 are the frames of the matching spikes.
179+
180+
"""
181+
182+
dot_product = np.zeros((num_units_train1, num_units_train2), dtype=np.uint16)
183+
184+
num_spike_frames_train1 = len(spike_frames_train1)
185+
num_spike_frames_train2 = len(spike_frames_train2)
186+
187+
# Keeps track of which frame in the second spike train should be used as a search start for matches
188+
second_train_search_start = 0
189+
for index1 in range(num_spike_frames_train1):
190+
frame1 = spike_frames_train1[index1]
191+
192+
for index2 in range(second_train_search_start, num_spike_frames_train2):
193+
frame2 = spike_frames_train2[index2]
194+
if frame2 < frame1 - delta_frames:
195+
# Frame2 too early, increase the second_train_search_start
196+
second_train_search_start += 1
197+
continue
198+
elif frame2 > frame1 + delta_frames:
199+
# No matches ahead, stop search in train2 and look for matches for the next spike in train1
200+
break
201+
else:
202+
# match
203+
unit_index1, unit_index2 = unit_indices1[index1], unit_indices2[index2]
204+
205+
dot_product[unit_index1, unit_index2] += delta_frames - abs(frame1 - frame2)
206+
207+
return dot_product
208+
209+
# Cache the compiled function
210+
get_optimized_dot_product._cached_function = compute_dot_product
211+
212+
return compute_dot_product
213+
214+
215+
def compute_distance_matrix(sorting1, sorting2, delta_frames):
216+
num_units_sorting1 = sorting1.get_num_units()
217+
num_units_sorting2 = sorting2.get_num_units()
218+
distance_matrix = np.zeros((num_units_sorting1, num_units_sorting2), dtype=np.uint16)
219+
220+
spike_vector1_segments = sorting1.to_spike_vector(concatenated=False)
221+
spike_vector2_segments = sorting2.to_spike_vector(concatenated=False)
222+
223+
num_segments_sorting1 = sorting1.get_num_segments()
224+
num_segments_sorting2 = sorting2.get_num_segments()
225+
assert (
226+
num_segments_sorting1 == num_segments_sorting2
227+
), "make_match_count_matrix : sorting1 and sorting2 must have the same segment number"
228+
229+
# Segments should be matched one by one
230+
dot_product_function = get_optimized_dot_product()
231+
232+
for segment_index in range(num_segments_sorting1):
233+
spike_vector1 = spike_vector1_segments[segment_index]
234+
spike_vector2 = spike_vector2_segments[segment_index]
235+
236+
sample_frames1_sorted = spike_vector1["sample_index"]
237+
sample_frames2_sorted = spike_vector2["sample_index"]
238+
239+
unit_indices1_sorted = spike_vector1["unit_index"]
240+
unit_indices2_sorted = spike_vector2["unit_index"]
241+
242+
dot_product = dot_product_function(
243+
sample_frames1_sorted,
244+
sample_frames2_sorted,
245+
unit_indices1_sorted,
246+
unit_indices2_sorted,
247+
num_units_sorting1,
248+
num_units_sorting2,
249+
delta_frames,
250+
)
251+
252+
norm_spike_vector1 = dot_product_function(
253+
sample_frames1_sorted,
254+
sample_frames1_sorted,
255+
unit_indices1_sorted,
256+
unit_indices1_sorted,
257+
num_units_sorting1,
258+
num_units_sorting1,
259+
delta_frames,
260+
)
261+
262+
norm_spike_vector2 = dot_product_function(
263+
sample_frames2_sorted,
264+
sample_frames2_sorted,
265+
unit_indices2_sorted,
266+
unit_indices2_sorted,
267+
num_units_sorting2,
268+
num_units_sorting2,
269+
delta_frames,
270+
)
271+
272+
norm_spike_vector1_diag = np.diag(norm_spike_vector1)
273+
norm_spike_vector2_diag = np.diag(norm_spike_vector2)
274+
275+
segment_distance = (
276+
norm_spike_vector1_diag[:, np.newaxis] + norm_spike_vector2_diag[np.newaxis, :] - 2 * dot_product
277+
)
278+
279+
distance_matrix += segment_distance
280+
281+
distance_matrix = np.sqrt(distance_matrix)
282+
283+
# Build a data frame from the matching matrix
284+
import pandas as pd
285+
286+
unit_ids_of_sorting1 = sorting1.get_unit_ids()
287+
unit_ids_of_sorting2 = sorting2.get_unit_ids()
288+
match_event_counts_df = pd.DataFrame(distance_matrix, index=unit_ids_of_sorting1, columns=unit_ids_of_sorting2)
289+
290+
return match_event_counts_df
109291

110292

111293
def get_optimized_compute_matching_matrix():

src/spikeinterface/comparison/tests/test_comparisontools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ def test_make_match_count_matrix_no_double_matching():
121121

122122
def test_make_match_count_matrix_repeated_matching_but_no_double_counting():
123123
# Challenging condition, this was failing with the previous approach that used np.where and np.diff
124-
frames_spike_train1 = [100, 105, 110] # Will fail with [100, 105, 110, 120]
124+
frames_spike_train1 = [100, 105, 110, 120] # Will fail with [100, 105, 110, 120]
125125
frames_spike_train2 = [100, 105, 110]
126-
unit_indices1 = [0, 0, 0] # Will fail with [0, 0, 0, 0]
126+
unit_indices1 = [0, 0, 0, 0] # Will fail with [0, 0, 0, 0]
127127
unit_indices2 = [0, 0, 0]
128128
delta_frames = 20 # long enough, so all frames in both sortings are within each other reach
129129

0 commit comments

Comments
 (0)