@@ -108,186 +108,110 @@ def do_count_event(sorting):
108108 return event_counts
109109
110110
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
118- """
119-
120- if hasattr (get_optimized_dot_product , "_cached_function" ):
121- return get_optimized_dot_product ._cached_function
122-
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 )
111+ try :
112+ from numba import jit
113+
114+ @jit (nopython = True , nogil = True )
115+ def calculate_distance_matrix (sample_frames , unit_indices , num_units_sorting1 , num_units_sorting2 , delta_frames ):
116+ dot_prouct_matrix = np .zeros (
117+ (num_units_sorting1 + num_units_sorting2 , num_units_sorting1 + num_units_sorting2 ),
118+ dtype = np .float32 ,
119+ )
186120
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 ]
121+ minimal_search = 0
122+ num_samples = len (sample_frames )
123+ for index1 in range (num_samples ):
124+ frame1 = sample_frames [index1 ]
125+ unit_index = unit_indices [index1 ]
191126
192- for index2 in range (second_train_search_start , num_spike_frames_train2 ):
193- frame2 = spike_frames_train2 [index2 ]
127+ for index2 in range (minimal_search , num_samples ):
128+ frame2 = sample_frames [index2 ]
194129 if frame2 < frame1 - delta_frames :
195- # Frame2 too early, increase the second_train_search_start
196- second_train_search_start += 1
130+ minimal_search += 1
197131 continue
198132 elif frame2 > frame1 + delta_frames :
199- # No matches ahead, stop search in train2 and look for matches for the next spike in train1
200133 break
201134 else :
202- # match
203- unit_index1 , unit_index2 = unit_indices1 [ index1 ], unit_indices2 [ index2 ]
135+ unit_index2 = unit_indices [ index2 ]
136+ dot_prouct_matrix [ unit_index , unit_index2 ] += delta_frames - abs ( frame1 - frame2 )
204137
205- dot_product [unit_index1 , unit_index2 ] += delta_frames - abs (frame1 - frame2 )
138+ # Diagonal is dot product of a spike with itself, hence norm
139+ within_train1_dot_product = dot_prouct_matrix [:num_units_sorting1 , :num_units_sorting1 ]
140+ within_train2_dot_product = dot_prouct_matrix [num_units_sorting1 :, num_units_sorting1 :]
141+ norm2 = np .diag (within_train2_dot_product )
142+ norm1 = np .diag (within_train1_dot_product )
206143
207- return dot_product
144+ # Assuming norm1 and norm2 are 1D arrays
145+ norm1_reshaped = norm1 .reshape ((- 1 , 1 )) # Reshape to a column vector
146+ norm2_reshaped = norm2 .reshape ((1 , - 1 )) # Reshape to a row vector
208147
209- # Cache the compiled function
210- get_optimized_dot_product . _cached_function = compute_dot_product
148+ # Now perform the addition
149+ norm_matrix = norm1_reshaped + norm2_reshaped
211150
212- return compute_dot_product
151+ # Dot product are the matches between units in train1 and train2
152+ dot_product12 = dot_prouct_matrix [:num_units_sorting1 , num_units_sorting1 :]
153+ dot_product21 = dot_prouct_matrix [num_units_sorting1 :, :num_units_sorting1 ]
213154
155+ dot_product = (dot_product12 + dot_product21 .T ) / 2
214156
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 )
157+ distance_matrix = norm_matrix - 2 * dot_product
219158
220- spike_vector1_segments = sorting1 .to_spike_vector (concatenated = False )
221- spike_vector2_segments = sorting2 .to_spike_vector (concatenated = False )
159+ return distance_matrix
222160
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"
161+ def compute_distance_matrix (sorting1 , sorting2 , delta_frames ):
162+ num_units_sorting1 = sorting1 .get_num_units ()
163+ num_units_sorting2 = sorting2 .get_num_units ()
164+ distance_matrix = np .zeros ((num_units_sorting1 , num_units_sorting2 ), dtype = np .float32 )
228165
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 ]
166+ spike_vector1_segments = sorting1 .to_spike_vector (concatenated = False )
167+ spike_vector2_segments = sorting2 .to_spike_vector (concatenated = False )
235168
236- sample_frames1_sorted = spike_vector1 ["sample_index" ]
237- sample_frames2_sorted = spike_vector2 ["sample_index" ]
169+ num_segments_sorting1 = sorting1 .get_num_segments ()
170+ num_segments_sorting2 = sorting2 .get_num_segments ()
171+ assert (
172+ num_segments_sorting1 == num_segments_sorting2
173+ ), "make_match_count_matrix : sorting1 and sorting2 must have the same segment number"
238174
239- unit_indices1_sorted = spike_vector1 ["unit_index" ]
240- unit_indices2_sorted = spike_vector2 ["unit_index" ]
175+ for segment_index in range (num_segments_sorting1 ):
176+ spike_vector1 = spike_vector1_segments [segment_index ]
177+ spike_vector2 = spike_vector2_segments [segment_index ]
241178
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- )
179+ sample_frames1_sorted = spike_vector1 ["sample_index" ]
180+ sample_frames2_sorted = spike_vector2 ["sample_index" ]
251181
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- )
182+ # Concatenate
183+ sample_frames = np .concatenate ((sample_frames1_sorted , sample_frames2_sorted ))
184+ unit_indices2 = spike_vector2 ["unit_index" ] + num_units_sorting1
185+ unit_indices = np .concatenate ((spike_vector1 ["unit_index" ], unit_indices2 ))
261186
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- )
187+ # Sort by sample frames
188+ indices = sample_frames .argsort ()
189+ sample_frames_sorted = sample_frames [indices ]
190+ unit_indices_sorted = unit_indices [indices ]
271191
272- norm_spike_vector1_diag = np .diag (norm_spike_vector1 )
273- norm_spike_vector2_diag = np .diag (norm_spike_vector2 )
192+ distance_matrix_segment = calculate_distance_matrix (
193+ sample_frames_sorted ,
194+ unit_indices_sorted ,
195+ num_units_sorting1 ,
196+ num_units_sorting2 ,
197+ delta_frames ,
198+ )
274199
275- segment_distance = (
276- norm_spike_vector1_diag [:, np .newaxis ] + norm_spike_vector2_diag [np .newaxis , :] - 2 * dot_product
277- )
200+ distance_matrix += distance_matrix_segment
278201
279- distance_matrix += segment_distance
202+ distance_matrix = np .sqrt (distance_matrix )
203+ # Build a data frame from the matching matrix
204+ import pandas as pd
280205
281- distance_matrix = np .sqrt (distance_matrix )
206+ unit_ids_of_sorting1 = sorting1 .get_unit_ids ()
207+ unit_ids_of_sorting2 = sorting2 .get_unit_ids ()
282208
283- # Build a data frame from the matching matrix
284- import pandas as pd
209+ match_event_counts_df = pd .DataFrame (distance_matrix , index = unit_ids_of_sorting1 , columns = unit_ids_of_sorting2 )
285210
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 )
211+ return match_event_counts_df
289212
290- return match_event_counts_df
213+ except :
214+ pass # numba not installed
291215
292216
293217def get_optimized_compute_matching_matrix ():
0 commit comments