Skip to content

Commit bf2ad6c

Browse files
committed
logic inside out
1 parent 3a3fa20 commit bf2ad6c

1 file changed

Lines changed: 115 additions & 64 deletions

File tree

src/spikeinterface/comparison/comparisontools.py

Lines changed: 115 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,59 @@ def do_count_event(sorting):
108108
return event_counts
109109

110110

111-
try:
112-
from numba import jit
111+
def get_optimized_compute_dot_product():
112+
"""
113+
This function wraps around the compute_dot_product function,
114+
which uses numba for JIT compilation. It caches the compiled function
115+
for improved performance on subsequent calls.
116+
"""
117+
118+
if hasattr(get_optimized_compute_dot_product, "_cached_function"):
119+
return get_optimized_compute_dot_product._cached_function
120+
121+
import numba
122+
123+
@numba.jit(nopython=True, nogil=True)
124+
def compute_dot_product(sample_frames, unit_indices, num_units_sorting1, num_units_sorting2, delta_frames):
125+
"""
126+
Compute the dot product matrix for two spike trains.
127+
128+
Note that the sample frames and unit indices must be sorted by sample frames.
129+
Also note that the unit_indices corresponding to the second spike train must be
130+
shifted by num_units_sorting1.
131+
132+
This creates a matrix that can be divided in four quadrants. Clokwise from top left:
133+
1. The dot product of spikes in the first spike train with themselves.
134+
2. The dot product of spikes in the first spike train with spikes in the second spike train.
135+
3. The dot product of spikes in the second spike train with spikes in the first spike train.
136+
4. The dot product of spikes in the second spike train with themselves.
137+
138+
Note that the norms of of the spike trains are the diagonals of the 1 and 4 quadrants.
139+
That is the only part of those quadrants that we need to calculate.
140+
141+
Parameters
142+
----------
143+
sample_frames : ndarray
144+
An array of integer frame numbers corresponding to spike times.
145+
unit_indices : ndarray
146+
An array of integers where unit_indices[i] gives the unit index associated with
147+
the spike at sample_frames[i]. Note that the unit_indices of the second spike train
148+
need to be shifted by num_units_sorting1.
149+
num_units_sorting1 : int
150+
The total count of unique units in the first spike train.
151+
num_units_sorting2 : int
152+
The total count of unique units in the second spike train.
153+
delta_frames : int
154+
The inclusive upper limit on the frame difference for which two spikes are considered in proximity.
155+
156+
Returns
157+
-------
158+
dot_product_matrix : ndarray
159+
A 2D numpy array of shape (num_units_sorting1 + num_units_sorting2, num_units_sorting1 + num_units_sorting2).
160+
Each element [i, j] represents the accumulated proximity score between unit i and unit j.
161+
"""
113162

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(
163+
dot_product_matrix = np.zeros(
117164
(num_units_sorting1 + num_units_sorting2, num_units_sorting1 + num_units_sorting2),
118165
dtype=np.float32,
119166
)
@@ -122,96 +169,100 @@ def calculate_distance_matrix(sample_frames, unit_indices, num_units_sorting1, n
122169
num_samples = len(sample_frames)
123170
for index1 in range(num_samples):
124171
frame1 = sample_frames[index1]
125-
unit_index = unit_indices[index1]
172+
unit_index1 = unit_indices[index1]
126173

127174
for index2 in range(minimal_search, num_samples):
128175
frame2 = sample_frames[index2]
176+
129177
if frame2 < frame1 - delta_frames:
130178
minimal_search += 1
131179
continue
132180
elif frame2 > frame1 + delta_frames:
133181
break
134182
else:
135183
unit_index2 = unit_indices[index2]
136-
dot_prouct_matrix[unit_index, unit_index2] += delta_frames - abs(frame1 - frame2)
184+
dot_product_matrix[unit_index1, unit_index2] += delta_frames - abs(frame1 - frame2)
137185

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)
186+
return dot_product_matrix
143187

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
188+
# Cache the compiled function
189+
get_optimized_compute_dot_product._cached_function = compute_dot_product
147190

148-
# Now perform the addition
149-
norm_matrix = norm1_reshaped + norm2_reshaped
191+
return compute_dot_product
150192

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]
154193

155-
dot_product = (dot_product12 + dot_product21.T) / 2
194+
def compute_distance_matrix(sorting1, sorting2, delta_frames):
195+
num_units_sorting1 = sorting1.get_num_units()
196+
num_units_sorting2 = sorting2.get_num_units()
197+
distance_matrix = np.zeros((num_units_sorting1, num_units_sorting2), dtype=np.float32)
156198

157-
distance_matrix = norm_matrix - 2 * dot_product
199+
spike_vector1_segments = sorting1.to_spike_vector(concatenated=False)
200+
spike_vector2_segments = sorting2.to_spike_vector(concatenated=False)
158201

159-
return distance_matrix
202+
num_segments_sorting1 = sorting1.get_num_segments()
203+
num_segments_sorting2 = sorting2.get_num_segments()
204+
assert (
205+
num_segments_sorting1 == num_segments_sorting2
206+
), "make_match_count_matrix : sorting1 and sorting2 must have the same segment number"
160207

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)
208+
optimized_compute_dot_product = get_optimized_compute_dot_product()
165209

166-
spike_vector1_segments = sorting1.to_spike_vector(concatenated=False)
167-
spike_vector2_segments = sorting2.to_spike_vector(concatenated=False)
210+
for segment_index in range(num_segments_sorting1):
211+
spike_vector1 = spike_vector1_segments[segment_index]
212+
spike_vector2 = spike_vector2_segments[segment_index]
168213

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"
214+
sample_frames1_sorted = spike_vector1["sample_index"]
215+
sample_frames2_sorted = spike_vector2["sample_index"]
174216

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]
217+
# Concatenate
218+
sample_frames = np.concatenate((sample_frames1_sorted, sample_frames2_sorted))
219+
unit_indices2 = spike_vector2["unit_index"] + num_units_sorting1
220+
unit_indices = np.concatenate((spike_vector1["unit_index"], unit_indices2))
178221

179-
sample_frames1_sorted = spike_vector1["sample_index"]
180-
sample_frames2_sorted = spike_vector2["sample_index"]
222+
# Sort by sample frames
223+
indices = sample_frames.argsort()
224+
sample_frames_sorted = sample_frames[indices]
225+
unit_indices_sorted = unit_indices[indices]
181226

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))
227+
dot_product_matrix = optimized_compute_dot_product(
228+
sample_frames_sorted,
229+
unit_indices_sorted,
230+
num_units_sorting1,
231+
num_units_sorting2,
232+
delta_frames,
233+
)
186234

187-
# Sort by sample frames
188-
indices = sample_frames.argsort()
189-
sample_frames_sorted = sample_frames[indices]
190-
unit_indices_sorted = unit_indices[indices]
235+
# Diagonal is dot product of a spike with itself, hence norm
236+
within_train1_dot_product = dot_product_matrix[:num_units_sorting1, :num_units_sorting1]
237+
within_train2_dot_product = dot_product_matrix[num_units_sorting1:, num_units_sorting1:]
238+
norm2 = np.diag(within_train2_dot_product)
239+
norm1 = np.diag(within_train1_dot_product)
191240

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-
)
241+
# Assuming norm1 and norm2 are 1D arrays
242+
norm1_reshaped = norm1.reshape((-1, 1)) # Reshape to a column vector
243+
norm2_reshaped = norm2.reshape((1, -1)) # Reshape to a row vector
244+
245+
# Now perform the addition
246+
norm_matrix = norm1_reshaped + norm2_reshaped
199247

200-
distance_matrix += distance_matrix_segment
248+
# Dot product are the matches between units in train1 and train2
249+
dot_product12 = dot_product_matrix[:num_units_sorting1, num_units_sorting1:]
250+
dot_product21 = dot_product_matrix[num_units_sorting1:, :num_units_sorting1]
251+
252+
dot_product = (dot_product12 + dot_product21.T) / 2
201253

202-
distance_matrix = np.sqrt(distance_matrix)
203-
# Build a data frame from the matching matrix
204-
import pandas as pd
254+
distance_matrix += norm_matrix - 2 * dot_product
205255

206-
unit_ids_of_sorting1 = sorting1.get_unit_ids()
207-
unit_ids_of_sorting2 = sorting2.get_unit_ids()
256+
# distance_matrix = np.sqrt(distance_matrix)
257+
# # Build a data frame from the matching matrix
258+
# import pandas as pd
208259

209-
match_event_counts_df = pd.DataFrame(distance_matrix, index=unit_ids_of_sorting1, columns=unit_ids_of_sorting2)
260+
# unit_ids_of_sorting1 = sorting1.get_unit_ids()
261+
# unit_ids_of_sorting2 = sorting2.get_unit_ids()
210262

211-
return match_event_counts_df
263+
# match_event_counts_df = pd.DataFrame(distance_matrix, index=unit_ids_of_sorting1, columns=unit_ids_of_sorting2)
212264

213-
except:
214-
pass # numba not installed
265+
return distance_matrix, dot_product
215266

216267

217268
def get_optimized_compute_matching_matrix():

0 commit comments

Comments
 (0)