77from ..core .template_tools import get_dense_templates_array
88from ..core .sparsity import ChannelSparsity
99
10+ try :
11+ import numba
12+
13+ HAVE_NUMBA = True
14+ except ImportError :
15+ HAVE_NUMBA = False
16+
1017
1118class ComputeTemplateSimilarity (AnalyzerExtension ):
1219 """Compute similarity between templates with several methods.
@@ -147,54 +154,15 @@ def _get_data(self):
147154compute_template_similarity = ComputeTemplateSimilarity .function_factory ()
148155
149156
150- def compute_similarity_with_templates_array (
151- templates_array , other_templates_array , method , support = "union" , num_shifts = 0 , sparsity = None , other_sparsity = None
152- ):
153- import sklearn .metrics .pairwise
157+ def _compute_similarity_matrix_numpy (templates_array , other_templates_array , num_shifts , mask , method ):
154158
155- if method == "cosine_similarity" :
156- method = "cosine"
157-
158- all_metrics = ["cosine" , "l1" , "l2" ]
159-
160- if method not in all_metrics :
161- raise ValueError (f"compute_template_similarity (method { method } ) not exists" )
162-
163- assert (
164- templates_array .shape [1 ] == other_templates_array .shape [1 ]
165- ), "The number of samples in the templates should be the same for both arrays"
166- assert (
167- templates_array .shape [2 ] == other_templates_array .shape [2 ]
168- ), "The number of channels in the templates should be the same for both arrays"
169159 num_templates = templates_array .shape [0 ]
170160 num_samples = templates_array .shape [1 ]
171- num_channels = templates_array .shape [2 ]
172161 other_num_templates = other_templates_array .shape [0 ]
173162
174- same_array = np .array_equal (templates_array , other_templates_array )
175-
176- mask = None
177- if sparsity is not None and other_sparsity is not None :
178- if support == "intersection" :
179- mask = np .logical_and (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
180- elif support == "union" :
181- mask = np .logical_and (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
182- units_overlaps = np .sum (mask , axis = 2 ) > 0
183- mask = np .logical_or (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
184- mask [~ units_overlaps ] = False
185- if mask is not None :
186- units_overlaps = np .sum (mask , axis = 2 ) > 0
187- overlapping_templates = {}
188- for i in range (num_templates ):
189- overlapping_templates [i ] = np .flatnonzero (units_overlaps [i ])
190- else :
191- # here we make a dense mask and overlapping templates
192- overlapping_templates = {i : np .arange (other_num_templates ) for i in range (num_templates )}
193- mask = np .ones ((num_templates , other_num_templates , num_channels ), dtype = bool )
194-
195- assert num_shifts < num_samples , "max_lag is too large"
196163 num_shifts_both_sides = 2 * num_shifts + 1
197164 distances = np .ones ((num_shifts_both_sides , num_templates , other_num_templates ), dtype = np .float32 )
165+ same_array = np .array_equal (templates_array , other_templates_array )
198166
199167 # We can use the fact that dist[i,j] at lag t is equal to dist[j,i] at time -t
200168 # So the matrix can be computed only for negative lags and be transposed
@@ -210,8 +178,9 @@ def compute_similarity_with_templates_array(
210178 tgt_sliced_templates = other_templates_array [:, num_shifts + shift : num_samples - num_shifts + shift ]
211179 for i in range (num_templates ):
212180 src_template = src_sliced_templates [i ]
213- tgt_templates = tgt_sliced_templates [overlapping_templates [i ]]
214- for gcount , j in enumerate (overlapping_templates [i ]):
181+ overlapping_templates = np .flatnonzero (np .sum (mask [i ], 1 ))
182+ tgt_templates = tgt_sliced_templates [overlapping_templates ]
183+ for gcount , j in enumerate (overlapping_templates ):
215184 # symmetric values are handled later
216185 if same_array and j < i :
217186 # no need exhaustive looping when same template
@@ -222,23 +191,156 @@ def compute_similarity_with_templates_array(
222191 if method == "l1" :
223192 norm_i = np .sum (np .abs (src ))
224193 norm_j = np .sum (np .abs (tgt ))
225- distances [count , i , j ] = sklearn . metrics . pairwise . pairwise_distances (src , tgt , metric = "l1" ). item ( )
194+ distances [count , i , j ] = np . sum ( np . abs (src - tgt ) )
226195 distances [count , i , j ] /= norm_i + norm_j
227196 elif method == "l2" :
228197 norm_i = np .linalg .norm (src , ord = 2 )
229198 norm_j = np .linalg .norm (tgt , ord = 2 )
230- distances [count , i , j ] = sklearn . metrics . pairwise . pairwise_distances (src , tgt , metric = "l2" ). item ( )
199+ distances [count , i , j ] = np . linalg . norm (src - tgt , ord = 2 )
231200 distances [count , i , j ] /= norm_i + norm_j
232- else :
233- distances [count , i , j ] = sklearn .metrics .pairwise .pairwise_distances (
234- src , tgt , metric = "cosine"
235- ).item ()
201+ elif method == "cosine" :
202+ norm_i = np .linalg .norm (src , ord = 2 )
203+ norm_j = np .linalg .norm (tgt , ord = 2 )
204+ distances [count , i , j ] = np .sum (src * tgt )
205+ distances [count , i , j ] /= norm_i * norm_j
206+ distances [count , i , j ] = 1 - distances [count , i , j ]
236207
237208 if same_array :
238209 distances [count , j , i ] = distances [count , i , j ]
239210
240211 if same_array and num_shifts != 0 :
241212 distances [num_shifts_both_sides - count - 1 ] = distances [count ].T
213+ return distances
214+
215+
216+ if HAVE_NUMBA :
217+
218+ from math import sqrt
219+
220+ @numba .jit (nopython = True , parallel = True , fastmath = True , nogil = True )
221+ def _compute_similarity_matrix_numba (templates_array , other_templates_array , num_shifts , mask , method ):
222+ num_templates = templates_array .shape [0 ]
223+ num_samples = templates_array .shape [1 ]
224+ other_num_templates = other_templates_array .shape [0 ]
225+
226+ num_shifts_both_sides = 2 * num_shifts + 1
227+ distances = np .ones ((num_shifts_both_sides , num_templates , other_num_templates ), dtype = np .float32 )
228+ same_array = np .array_equal (templates_array , other_templates_array )
229+
230+ # We can use the fact that dist[i,j] at lag t is equal to dist[j,i] at time -t
231+ # So the matrix can be computed only for negative lags and be transposed
232+
233+ if same_array :
234+ # optimisation when array are the same because of symetry in shift
235+ shift_loop = list (range (- num_shifts , 1 ))
236+ else :
237+ shift_loop = list (range (- num_shifts , num_shifts + 1 ))
238+
239+ if method == "l1" :
240+ metric = 0
241+ elif method == "l2" :
242+ metric = 1
243+ elif method == "cosine" :
244+ metric = 2
245+
246+ for count in range (len (shift_loop )):
247+ shift = shift_loop [count ]
248+ src_sliced_templates = templates_array [:, num_shifts : num_samples - num_shifts ]
249+ tgt_sliced_templates = other_templates_array [:, num_shifts + shift : num_samples - num_shifts + shift ]
250+ for i in numba .prange (num_templates ):
251+ src_template = src_sliced_templates [i ]
252+ overlapping_templates = np .flatnonzero (np .sum (mask [i ], 1 ))
253+ tgt_templates = tgt_sliced_templates [overlapping_templates ]
254+ for gcount in range (len (overlapping_templates )):
255+
256+ j = overlapping_templates [gcount ]
257+ # symmetric values are handled later
258+ if same_array and j < i :
259+ # no need exhaustive looping when same template
260+ continue
261+ src = src_template [:, mask [i , j ]].flatten ()
262+ tgt = (tgt_templates [gcount ][:, mask [i , j ]]).flatten ()
263+
264+ norm_i = 0
265+ norm_j = 0
266+ distances [count , i , j ] = 0
267+
268+ for k in range (len (src )):
269+ if metric == 0 :
270+ norm_i += abs (src [k ])
271+ norm_j += abs (tgt [k ])
272+ distances [count , i , j ] += abs (src [k ] - tgt [k ])
273+ elif metric == 1 :
274+ norm_i += src [k ] ** 2
275+ norm_j += tgt [k ] ** 2
276+ distances [count , i , j ] += (src [k ] - tgt [k ]) ** 2
277+ elif metric == 2 :
278+ distances [count , i , j ] += src [k ] * tgt [k ]
279+ norm_i += src [k ] ** 2
280+ norm_j += tgt [k ] ** 2
281+
282+ if metric == 0 :
283+ distances [count , i , j ] /= norm_i + norm_j
284+ elif metric == 1 :
285+ norm_i = sqrt (norm_i )
286+ norm_j = sqrt (norm_j )
287+ distances [count , i , j ] = sqrt (distances [count , i , j ])
288+ distances [count , i , j ] /= norm_i + norm_j
289+ elif metric == 2 :
290+ norm_i = sqrt (norm_i )
291+ norm_j = sqrt (norm_j )
292+ distances [count , i , j ] /= norm_i * norm_j
293+ distances [count , i , j ] = 1 - distances [count , i , j ]
294+
295+ if same_array :
296+ distances [count , j , i ] = distances [count , i , j ]
297+
298+ if same_array and num_shifts != 0 :
299+ distances [num_shifts_both_sides - count - 1 ] = distances [count ].T
300+
301+ return distances
302+
303+ _compute_similarity_matrix = _compute_similarity_matrix_numba
304+ else :
305+ _compute_similarity_matrix = _compute_similarity_matrix_numpy
306+
307+
308+ def compute_similarity_with_templates_array (
309+ templates_array , other_templates_array , method , support = "union" , num_shifts = 0 , sparsity = None , other_sparsity = None
310+ ):
311+
312+ if method == "cosine_similarity" :
313+ method = "cosine"
314+
315+ all_metrics = ["cosine" , "l1" , "l2" ]
316+
317+ if method not in all_metrics :
318+ raise ValueError (f"compute_template_similarity (method { method } ) not exists" )
319+
320+ assert (
321+ templates_array .shape [1 ] == other_templates_array .shape [1 ]
322+ ), "The number of samples in the templates should be the same for both arrays"
323+ assert (
324+ templates_array .shape [2 ] == other_templates_array .shape [2 ]
325+ ), "The number of channels in the templates should be the same for both arrays"
326+ num_templates = templates_array .shape [0 ]
327+ num_samples = templates_array .shape [1 ]
328+ num_channels = templates_array .shape [2 ]
329+ other_num_templates = other_templates_array .shape [0 ]
330+
331+ mask = np .ones ((num_templates , other_num_templates , num_channels ), dtype = bool )
332+
333+ if sparsity is not None and other_sparsity is not None :
334+ if support == "intersection" :
335+ mask = np .logical_and (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
336+ elif support == "union" :
337+ mask = np .logical_and (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
338+ units_overlaps = np .sum (mask , axis = 2 ) > 0
339+ mask = np .logical_or (sparsity .mask [:, np .newaxis , :], other_sparsity .mask [np .newaxis , :, :])
340+ mask [~ units_overlaps ] = False
341+
342+ assert num_shifts < num_samples , "max_lag is too large"
343+ distances = _compute_similarity_matrix (templates_array , other_templates_array , num_shifts , mask , method )
242344
243345 distances = np .min (distances , axis = 0 )
244346 similarity = 1 - distances
0 commit comments