@@ -86,6 +86,9 @@ def compute_agreement_score(num_matches, num1, num2):
8686def do_count_event (sorting ):
8787 """
8888 Count event for each units in a sorting.
89+
90+ Kept for backward compatibility sorting.count_num_spikes_per_unit() is doing the same.
91+
8992 Parameters
9093 ----------
9194 sorting: SortingExtractor
@@ -98,14 +101,7 @@ def do_count_event(sorting):
98101 """
99102 import pandas as pd
100103
101- unit_ids = sorting .get_unit_ids ()
102- ev_counts = np .zeros (len (unit_ids ), dtype = "int64" )
103- for segment_index in range (sorting .get_num_segments ()):
104- ev_counts += np .array (
105- [len (sorting .get_unit_spike_train (u , segment_index = segment_index )) for u in unit_ids ], dtype = "int64"
106- )
107- event_counts = pd .Series (ev_counts , index = unit_ids )
108- return event_counts
104+ return pd .Series (sorting .count_num_spikes_per_unit ())
109105
110106
111107def get_optimized_dot_product_function ():
@@ -328,11 +324,9 @@ def compute_matching_matrix(
328324 delta_frames ,
329325 ):
330326 """
331- Compute a matrix representing the matches between two spike trains.
332-
333- Given two spike trains, this function finds matching spikes based on a temporal proximity criterion
334- defined by `delta_frames`. The resulting matrix indicates the number of matches between units
335- in `spike_frames_train1` and `spike_frames_train2`.
327+ Internal function used by `make_match_count_matrix()`.
328+ This function is for one segment only.
329+ The loop over segment is done in `make_match_count_matrix()`
336330
337331 Parameters
338332 ----------
@@ -359,31 +353,9 @@ def compute_matching_matrix(
359353 A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
360354 the count of matching spike pairs between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
361355
362-
363- Notes
364- -----
365- This algorithm identifies matching spikes between two ordered spike trains.
366- By iterating through each spike in the first train, it compares them against spikes in the second train,
367- determining matches based on the two spikes frames being within `delta_frames` of each other.
368-
369- To avoid redundant comparisons the algorithm maintains a reference, `second_train_search_start `,
370- which signifies the minimal index in the second spike train that might match the upcoming spike
371- in the first train.
372-
373- The logic can be summarized as follows:
374- 1. Iterate through each spike in the first train
375- 2. For each spike, find the first match in the second train.
376- 3. Save the index of the first match as the new `second_train_search_start `
377- 3. For each match, find as many matches as possible from the first match onwards.
378-
379- An important condition here is that the same spike is not matched twice. This is managed by keeping track
380- of the last matched frame for each unit pair in `last_match_frame1` and `last_match_frame2`
381-
382- For more details on the rationale behind this approach, refer to the documentation of this module and/or
383- the metrics section in SpikeForest documentation.
384356 """
385357
386- matching_matrix = np .zeros ((num_units_train1 , num_units_train2 ), dtype = np .uint16 )
358+ matching_matrix = np .zeros ((num_units_train1 , num_units_train2 ), dtype = np .uint64 )
387359
388360 # Used to avoid the same spike matching twice
389361 last_match_frame1 = - np .ones_like (matching_matrix , dtype = np .int64 )
@@ -411,11 +383,11 @@ def compute_matching_matrix(
411383 unit_index1 , unit_index2 = unit_indices1 [index1 ], unit_indices2 [index2 ]
412384
413385 if (
414- frame1 != last_match_frame1 [unit_index1 , unit_index2 ]
415- and frame2 != last_match_frame2 [unit_index1 , unit_index2 ]
386+ index1 != last_match_frame1 [unit_index1 , unit_index2 ]
387+ and index2 != last_match_frame2 [unit_index1 , unit_index2 ]
416388 ):
417- last_match_frame1 [unit_index1 , unit_index2 ] = frame1
418- last_match_frame2 [unit_index1 , unit_index2 ] = frame2
389+ last_match_frame1 [unit_index1 , unit_index2 ] = index1
390+ last_match_frame2 [unit_index1 , unit_index2 ] = index2
419391
420392 matching_matrix [unit_index1 , unit_index2 ] += 1
421393
@@ -427,10 +399,65 @@ def compute_matching_matrix(
427399 return compute_matching_matrix
428400
429401
430- def make_match_count_matrix (sorting1 , sorting2 , delta_frames ):
402+ def make_match_count_matrix (sorting1 , sorting2 , delta_frames , ensure_symmetry = False ):
403+ """
404+ Computes a matrix representing the matches between two Sorting objects.
405+
406+ Given two spike trains, this function finds matching spikes based on a temporal proximity criterion
407+ defined by `delta_frames`. The resulting matrix indicates the number of matches between units
408+ in `spike_frames_train1` and `spike_frames_train2` for each pair of units.
409+
410+ Note that this algo is not symmetric and is biased with `sorting1` representing ground truth for the comparison
411+
412+ Parameters
413+ ----------
414+ sorting1 : Sorting
415+ An array of integer frame numbers corresponding to spike times for the first train. Must be in ascending order.
416+ sorting2 : Sorting
417+ An array of integer frame numbers corresponding to spike times for the second train. Must be in ascending order.
418+ delta_frames : int
419+ The inclusive upper limit on the frame difference for which two spikes are considered matching. That is
420+ if `abs(spike_frames_train1[i] - spike_frames_train2[j]) <= delta_frames` then the spikes at
421+ `spike_frames_train1[i]` and `spike_frames_train2[j]` are considered matching.
422+ ensure_symmetry: bool, default False
423+ If ensure_symmetry=True, then the algo is run two times by switching sorting1 and sorting2.
424+ And the minimum of the two results is taken.
425+ Returns
426+ -------
427+ matching_matrix : ndarray
428+ A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
429+ the count of matching spike pairs between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
430+
431+ Notes
432+ -----
433+ This algorithm identifies matching spikes between two ordered spike trains.
434+ By iterating through each spike in the first train, it compares them against spikes in the second train,
435+ determining matches based on the two spikes frames being within `delta_frames` of each other.
436+
437+ To avoid redundant comparisons the algorithm maintains a reference, `second_train_search_start `,
438+ which signifies the minimal index in the second spike train that might match the upcoming spike
439+ in the first train.
440+
441+ The logic can be summarized as follows:
442+ 1. Iterate through each spike in the first train
443+ 2. For each spike, find the first match in the second train.
444+ 3. Save the index of the first match as the new `second_train_search_start `
445+ 3. For each match, find as many matches as possible from the first match onwards.
446+
447+ An important condition here is that the same spike is not matched twice. This is managed by keeping track
448+ of the last matched frame for each unit pair in `last_match_frame1` and `last_match_frame2`
449+ There are corner cases where a spike can be counted twice in the spiketrain 2 if there are bouts of bursting activity
450+ (below delta_frames) in the spiketrain 1. To ensure that the number of matches does not exceed the number of spikes,
451+ we apply a final clip.
452+
453+
454+ For more details on the rationale behind this approach, refer to the documentation of this module and/or
455+ the metrics section in SpikeForest documentation.
456+ """
457+
431458 num_units_sorting1 = sorting1 .get_num_units ()
432459 num_units_sorting2 = sorting2 .get_num_units ()
433- matching_matrix = np .zeros ((num_units_sorting1 , num_units_sorting2 ), dtype = np .uint16 )
460+ matching_matrix = np .zeros ((num_units_sorting1 , num_units_sorting2 ), dtype = np .uint64 )
434461
435462 spike_vector1_segments = sorting1 .to_spike_vector (concatenated = False )
436463 spike_vector2_segments = sorting2 .to_spike_vector (concatenated = False )
@@ -452,7 +479,7 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
452479 unit_indices1_sorted = spike_vector1 ["unit_index" ]
453480 unit_indices2_sorted = spike_vector2 ["unit_index" ]
454481
455- matching_matrix + = get_optimized_compute_matching_matrix ()(
482+ matching_matrix_seg = get_optimized_compute_matching_matrix ()(
456483 sample_frames1_sorted ,
457484 sample_frames2_sorted ,
458485 unit_indices1_sorted ,
@@ -462,6 +489,26 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
462489 delta_frames ,
463490 )
464491
492+ if ensure_symmetry :
493+ matching_matrix_seg_switch = get_optimized_compute_matching_matrix ()(
494+ sample_frames2_sorted ,
495+ sample_frames1_sorted ,
496+ unit_indices2_sorted ,
497+ unit_indices1_sorted ,
498+ num_units_sorting2 ,
499+ num_units_sorting1 ,
500+ delta_frames ,
501+ )
502+ matching_matrix_seg = np .maximum (matching_matrix_seg , matching_matrix_seg_switch .T )
503+
504+ matching_matrix += matching_matrix_seg
505+
506+ # ensure the number of match do not exceed the number of spike in train 2
507+ # this is a simple way to handle corner cases for bursting in sorting1
508+ spike_count2 = np .array (list (sorting2 .count_num_spikes_per_unit ().values ()))
509+ spike_count2 = spike_count2 [np .newaxis , :]
510+ matching_matrix = np .clip (matching_matrix , None , spike_count2 )
511+
465512 # Build a data frame from the matching matrix
466513 import pandas as pd
467514
@@ -472,12 +519,12 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
472519 return match_event_counts_df
473520
474521
475- def make_agreement_scores (sorting1 , sorting2 , delta_frames ):
522+ def make_agreement_scores (sorting1 , sorting2 , delta_frames , ensure_symmetry = True ):
476523 """
477524 Make the agreement matrix.
478525 No threshold (min_score) is applied at this step.
479526
480- Note : this computation is symmetric.
527+ Note : this computation is symmetric by default .
481528 Inverting sorting1 and sorting2 give the transposed matrix.
482529
483530 Parameters
@@ -488,7 +535,9 @@ def make_agreement_scores(sorting1, sorting2, delta_frames):
488535 The second sorting extractor
489536 delta_frames: int
490537 Number of frames to consider spikes coincident
491-
538+ ensure_symmetry: bool, default: True
539+ If ensure_symmetry is True, then the algo is run two times by switching sorting1 and sorting2.
540+ And the minimum of the two results is taken.
492541 Returns
493542 -------
494543 agreement_scores: array (float)
@@ -504,7 +553,7 @@ def make_agreement_scores(sorting1, sorting2, delta_frames):
504553 event_counts1 = pd .Series (ev_counts1 , index = unit1_ids )
505554 event_counts2 = pd .Series (ev_counts2 , index = unit2_ids )
506555
507- match_event_count = make_match_count_matrix (sorting1 , sorting2 , delta_frames )
556+ match_event_count = make_match_count_matrix (sorting1 , sorting2 , delta_frames , ensure_symmetry = ensure_symmetry )
508557
509558 agreement_scores = make_agreement_scores_from_count (match_event_count , event_counts1 , event_counts2 )
510559
0 commit comments