Skip to content

Commit 3a3fa20

Browse files
committed
working distance function implementation
1 parent 212a880 commit 3a3fa20

1 file changed

Lines changed: 76 additions & 152 deletions

File tree

src/spikeinterface/comparison/comparisontools.py

Lines changed: 76 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -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

293217
def get_optimized_compute_matching_matrix():

0 commit comments

Comments
 (0)