@@ -108,15 +108,15 @@ def do_count_event(sorting):
108108 return event_counts
109109
110110
111- def get_optimized_compute_dot_product ():
111+ def get_optimized_compute_dot_product_all ():
112112 """
113113 This function wraps around the compute_dot_product function,
114114 which uses numba for JIT compilation. It caches the compiled function
115115 for improved performance on subsequent calls.
116116 """
117117
118- if hasattr (get_optimized_compute_dot_product , "_cached_function" ):
119- return get_optimized_compute_dot_product ._cached_function
118+ if hasattr (get_optimized_compute_dot_product_all , "_cached_function" ):
119+ return get_optimized_compute_dot_product_all ._cached_function
120120
121121 import numba
122122
@@ -187,11 +187,147 @@ def compute_dot_product(sample_frames, unit_indices, num_units_sorting1, num_uni
187187 return dot_product_matrix
188188
189189 # Cache the compiled function
190- get_optimized_compute_dot_product ._cached_function = compute_dot_product
190+ get_optimized_compute_dot_product_all ._cached_function = compute_dot_product
191191
192192 return compute_dot_product
193193
194194
195+ def get_optimized_dot_product_function ():
196+ """
197+ This function is to avoid the bare try-except pattern when importing the compute_dot_product function
198+ which uses numba. I tested using the numba dispatcher programatically to avoids this
199+ but the performance improvements were lost. Think you can do better? Don't forget to measure performance against
200+ the current implementation!
201+ TODO: unify numba decorator across all modules
202+ """
203+
204+ if hasattr (get_optimized_dot_product_function , "_cached_function" ):
205+ return get_optimized_dot_product_function ._cached_function
206+
207+ import numba
208+
209+ @numba .jit (nopython = True , nogil = True )
210+ def compute_dot_product (
211+ spike_frames_train1 ,
212+ spike_frames_train2 ,
213+ unit_indices1 ,
214+ unit_indices2 ,
215+ num_units_train1 ,
216+ num_units_train2 ,
217+ delta_frames ,
218+ ):
219+ """
220+ Computes the dot product between two spike trains.
221+ The dot product in this case is the dot product of the spikes viewed as box-care functions in
222+ the Hilbert space L2.
223+ The dot product gives a measure of the similarity between two spike trains. Each match is weighted by the
224+ delta_frames - abs(frame1 - frame2) where frame1 and frame2 are the frames of the matching spikes.
225+ When the spike trains are identical, the dot product returns all the matches within the same spike train.
226+ The sum of this dot product is the squared norm of the spike train in the Hilbert space L2.
227+ Parameters
228+ ----------
229+ spike_frames_train1 : ndarray
230+ An array of integer frame numbers corresponding to spike times for the first train. Must be in ascending order.
231+ spike_frames_train2 : ndarray
232+ An array of integer frame numbers corresponding to spike times for the second train. Must be in ascending order.
233+ unit_indices1 : ndarray
234+ An array of integers where `unit_indices1[i]` gives the unit index associated with the spike at `spike_frames_train1[i]`.
235+ unit_indices2 : ndarray
236+ An array of integers where `unit_indices2[i]` gives the unit index associated with the spike at `spike_frames_train2[i]`.
237+ num_units_train1 : int
238+ The total count of unique units in the first spike train.
239+ num_units_train2 : int
240+ The total count of unique units in the second spike train.
241+ delta_frames : int
242+ The inclusive upper limit on the frame difference for which two spikes are considered matching. That is
243+ if `abs(spike_frames_train1[i] - spike_frames_train2[j]) <= delta_frames` then the spikes at `spike_frames_train1[i]`
244+ and `spike_frames_train2[j]` are considered matching.
245+ Returns
246+ -------
247+ dot_product : ndarray
248+ A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
249+ the dot product between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
250+ Notes
251+ -----
252+ This algorithm follows the same logic as the one used in `compute_matching_matrix` but instead of counting
253+ the number of matches, it computes the dot product between the two spike trains by weighting each match
254+ by the delta_frames - abs(frame1 - frame2) where frame1 and frame2 are the frames of the matching spikes.
255+ """
256+
257+ dot_product = np .zeros ((num_units_train1 , num_units_train2 ), dtype = np .uint16 )
258+
259+ num_spike_frames_train1 = len (spike_frames_train1 )
260+ num_spike_frames_train2 = len (spike_frames_train2 )
261+
262+ # Keeps track of which frame in the second spike train should be used as a search start for matches
263+ second_train_search_start = 0
264+ for index1 in range (num_spike_frames_train1 ):
265+ frame1 = spike_frames_train1 [index1 ]
266+
267+ for index2 in range (second_train_search_start , num_spike_frames_train2 ):
268+ frame2 = spike_frames_train2 [index2 ]
269+ if frame2 < frame1 - delta_frames :
270+ # Frame2 too early, increase the second_train_search_start
271+ second_train_search_start += 1
272+ continue
273+ elif frame2 > frame1 + delta_frames :
274+ # No matches ahead, stop search in train2 and look for matches for the next spike in train1
275+ break
276+ else :
277+ # match
278+ unit_index1 , unit_index2 = unit_indices1 [index1 ], unit_indices2 [index2 ]
279+
280+ match_weight = delta_frames - abs (frame1 - frame2 )
281+ dot_product [unit_index1 , unit_index2 ] += match_weight
282+
283+ return dot_product
284+
285+ # Cache the compiled function
286+ get_optimized_dot_product_function ._cached_function = compute_dot_product
287+
288+ return compute_dot_product
289+
290+
291+ def get_optimized_compute_norm_function ():
292+ if hasattr (get_optimized_compute_norm_function , "_cached_function" ):
293+ return get_optimized_compute_norm_function ._cached_function
294+
295+ import numba
296+
297+ @numba .jit (nopython = True , nogil = True )
298+ def compute_norm (sample_frames , unit_indices , num_units_sorting , delta_frames ):
299+ norm_vector = np .zeros (num_units_sorting , dtype = np .uint32 )
300+
301+ minimal_search = 0
302+ num_samples = len (sample_frames )
303+ for index1 in range (num_samples ):
304+ frame1 = sample_frames [index1 ]
305+ unit_index1 = unit_indices [index1 ]
306+
307+ for index2 in range (minimal_search , num_samples ):
308+ frame2 = sample_frames [index2 ]
309+ unit_index2 = unit_indices [index2 ]
310+
311+ # Only compare spikes from the same unit
312+ if unit_index1 != unit_index2 :
313+ continue
314+
315+ if frame2 < frame1 - delta_frames :
316+ minimal_search += 1
317+ continue
318+ elif frame2 > frame1 + delta_frames :
319+ break
320+ else :
321+ norm_vector [unit_index1 ] += delta_frames - abs (frame1 - frame2 )
322+
323+ return norm_vector
324+
325+ # Cache the compiled function
326+ get_optimized_compute_norm_function ._cached_function = compute_norm
327+
328+ return compute_norm
329+
330+
195331def compute_distance_matrix (sorting1 , sorting2 , delta_frames ):
196332 num_units_sorting1 = sorting1 .get_num_units ()
197333 num_units_sorting2 = sorting2 .get_num_units ()
@@ -206,7 +342,8 @@ def compute_distance_matrix(sorting1, sorting2, delta_frames):
206342 num_segments_sorting1 == num_segments_sorting2
207343 ), "make_match_count_matrix : sorting1 and sorting2 must have the same segment number"
208344
209- optimized_compute_dot_product = get_optimized_compute_dot_product ()
345+ optimized_compute_dot_product = get_optimized_dot_product_function ()
346+ get_optimized_compute_norm = get_optimized_compute_norm_function ()
210347
211348 for segment_index in range (num_segments_sorting1 ):
212349 spike_vector1 = spike_vector1_segments [segment_index ]
@@ -215,42 +352,39 @@ def compute_distance_matrix(sorting1, sorting2, delta_frames):
215352 sample_frames1_sorted = spike_vector1 ["sample_index" ]
216353 sample_frames2_sorted = spike_vector2 ["sample_index" ]
217354
218- # Concatenate
219- sample_frames = np .concatenate ((sample_frames1_sorted , sample_frames2_sorted ))
220- unit_indices2 = spike_vector2 ["unit_index" ] + num_units_sorting1
221- unit_indices = np .concatenate ((spike_vector1 ["unit_index" ], unit_indices2 ))
355+ unit_indices1 = spike_vector1 ["unit_index" ]
356+ unit_indices2 = spike_vector2 ["unit_index" ]
222357
223- # Sort by sample frames
224- indices = sample_frames .argsort ()
225- sample_frames_sorted = sample_frames [indices ]
226- unit_indices_sorted = unit_indices [indices ]
358+ norm1 = get_optimized_compute_norm (
359+ sample_frames1_sorted ,
360+ unit_indices1 ,
361+ num_units_sorting1 ,
362+ delta_frames ,
363+ )
364+
365+ norm2 = get_optimized_compute_norm (
366+ sample_frames2_sorted ,
367+ unit_indices2 ,
368+ num_units_sorting2 ,
369+ delta_frames ,
370+ )
227371
228372 dot_product_matrix = optimized_compute_dot_product (
229- sample_frames_sorted ,
230- unit_indices_sorted ,
373+ sample_frames1_sorted ,
374+ sample_frames2_sorted ,
375+ unit_indices1 ,
376+ unit_indices2 ,
231377 num_units_sorting1 ,
232378 num_units_sorting2 ,
233379 delta_frames ,
234380 )
235381
236- # Diagonal is dot product of a spike with itself, hence norm
237- within_train1_dot_product = dot_product_matrix [:num_units_sorting1 , :num_units_sorting1 ]
238- within_train2_dot_product = dot_product_matrix [num_units_sorting1 :, num_units_sorting1 :]
239- norm1 = np .diag (within_train1_dot_product )
240- norm2 = np .diag (within_train2_dot_product )
241-
242382 # Now perform the addition
243383 norm_matrix = norm1 [:, np .newaxis ] + norm2 [np .newaxis , :]
244384
245- # Dot product are the matches between units in train1 and train2
246- dot_product12 = dot_product_matrix [:num_units_sorting1 , num_units_sorting1 :]
247- dot_product21 = dot_product_matrix [num_units_sorting1 :, :num_units_sorting1 ]
248-
249- dot_product = (dot_product12 + dot_product21 .T ) // 2
250-
251- distance_matrix += norm_matrix - 2 * dot_product
385+ distance_matrix += norm_matrix - 2 * dot_product_matrix
252386
253- return np .sqrt (distance_matrix ), dot_product
387+ return np .sqrt (distance_matrix ), dot_product_matrix
254388
255389
256390def get_optimized_compute_matching_matrix ():
0 commit comments