@@ -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+
3962def 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
111293def get_optimized_compute_matching_matrix ():
0 commit comments