@@ -149,6 +149,28 @@ def vector_to_list_of_spiketrain_numba(sample_indices, unit_indices, num_units):
149149 return vector_to_list_of_spiketrain_numba
150150
151151
152+ def _is_flat_int64_view (dtype : np .dtype ) -> bool :
153+ """
154+ Whether a spike-vector dtype can be safely viewed as a flat (num_spikes, num_fields) int64
155+ matrix, which is what the numba counting sort moves rows through.
156+
157+ Requires every field to be int64, packed with no padding, and the first three fields to be
158+ `minimum_spike_dtype`'s in order, because the kernel addresses unit_index and segment_index
159+ positionally (columns 1 and 2) rather than by name.
160+
161+ The all-int64 rule is deliberately stricter than correctness demands -- the kernel copies rows
162+ bitwise, so any 8-byte field would in fact round-trip through the view. Keeping it narrow means
163+ the kernel only ever sees the layout it is written for, and it costs nothing in practice: every
164+ spike-vector dtype spikeinterface constructs is all-int64.
165+ """
166+ names = dtype .names
167+ if names is None or names [:3 ] != tuple (name for name , _ in minimum_spike_dtype ):
168+ return False
169+ if dtype .itemsize != 8 * len (names ):
170+ return False
171+ return all (dtype .fields [name ][0 ] == np .int64 and dtype .fields [name ][1 ] == 8 * i for i , name in enumerate (names ))
172+
173+
152174def reorder_spike_vector_by_unit_and_segment (
153175 spike_vector : np .ndarray ,
154176 num_units : int ,
@@ -176,8 +198,11 @@ def reorder_spike_vector_by_unit_and_segment(
176198
177199 Parameters
178200 ----------
179- spike_vector : np.ndarray
180- Structured array with dtype `minimum_spike_dtype`.
201+ spike_vector : np.ndarray or array-like
202+ Structured array whose first three fields are `minimum_spike_dtype`. Extra trailing fields
203+ (e.g. the "channel_index" that `to_spike_vector(main_channel_indices=...)` adds) are carried
204+ through to the output. Anything array-like is accepted and materialised with `np.asarray`,
205+ so lazily-backed spike vectors work, at the cost of being read into memory.
181206 num_units : int
182207 The number of units. Every `unit_index` must be in [0, num_units).
183208 num_segments : int
@@ -188,14 +213,17 @@ def reorder_spike_vector_by_unit_and_segment(
188213 Returns
189214 -------
190215 ordered_spikes : np.ndarray
191- Structured array of `minimum_spike_dtype`, the same length as `spike_vector`, with the
192- spikes grouped by bucket.
216+ Structured array of the same dtype and length as `spike_vector`, with the spikes grouped
217+ by bucket.
193218 order : np.ndarray
194219 1d int64 array such that `spike_vector[order]` equals `ordered_spikes`.
195220 counts : np.ndarray
196221 1d int64 array of length `num_units * num_segments`, the number of spikes in each bucket,
197222 in bucket order.
198223 """
224+ # Materialise anything array-like (e.g. a lazily zarr-backed spike vector).
225+ spike_vector = np .asarray (spike_vector )
226+
199227 num_units , num_segments = int (num_units ), int (num_segments )
200228 if num_units < 0 or num_segments < 0 :
201229 raise ValueError (f"`num_units` and `num_segments` must not be negative; got { num_units } and { num_segments } ." )
@@ -208,7 +236,7 @@ def reorder_spike_vector_by_unit_and_segment(
208236 num_spikes = spike_vector .size
209237 if num_spikes == 0 :
210238 return (
211- np .empty (0 , dtype = minimum_spike_dtype ),
239+ np .empty (0 , dtype = spike_vector . dtype ),
212240 np .empty (0 , dtype = np .int64 ),
213241 np .zeros (num_buckets , dtype = np .int64 ),
214242 )
@@ -217,20 +245,24 @@ def reorder_spike_vector_by_unit_and_segment(
217245 f"`spike_vector` has a unit_index outside [0, { num_units } ) or a segment_index outside [0, { num_segments } )."
218246 )
219247
220- if HAVE_NUMBA :
248+ # The numba kernel expects an all-int64 unpadded dtype (e.g. `minimum_spike_dtype`), but it is
249+ # possible that a spike vector has extra fields with other dtypes (`NumpySorting` allows that).
250+ # So we check taht the numba path is safe, and anything else takes the dtype-agnostic numpy path.
251+ if HAVE_NUMBA and _is_flat_int64_view (spike_vector .dtype ):
221252 reorder_spike_vector = get_numba_reorder_spike_vector ()
222253
223- # These flat (num_spikes, 3) int64 views are zero-copy
224- in_flat = np .ascontiguousarray (spike_vector ).view (np .int64 ).reshape (num_spikes , 3 )
225- out_flat = np .empty ((num_spikes , 3 ), dtype = np .int64 )
254+ num_fields = len (spike_vector .dtype .names )
255+ # These flat (num_spikes, num_fields) int64 views are zero-copy
256+ in_flat = np .ascontiguousarray (spike_vector ).view (np .int64 ).reshape (num_spikes , num_fields )
257+ out_flat = np .empty ((num_spikes , num_fields ), dtype = np .int64 )
226258 order = np .empty (num_spikes , dtype = np .int64 )
227259 counts = np .empty (num_buckets , dtype = np .int64 )
228260
229261 in_range = reorder_spike_vector (in_flat , unit_stride , segment_stride , num_buckets , out_flat , order , counts )
230262 if not in_range :
231263 raise ValueError (out_of_range_error )
232264
233- ordered_spikes = out_flat .view (minimum_spike_dtype ).reshape (num_spikes )
265+ ordered_spikes = out_flat .view (spike_vector . dtype ).reshape (num_spikes )
234266 return ordered_spikes , order , counts
235267
236268 # numpy fallback: a stable argsort by bucket is equivalent to the counting sort above.
@@ -260,7 +292,9 @@ def get_numba_reorder_spike_vector():
260292 @jit (nopython = True , nogil = True , cache = False )
261293 def reorder_spike_vector_numba (in_flat , unit_stride , segment_stride , num_buckets , out_flat , order , counts ):
262294 """
263- Stable counting-sort of a (N, 3) int64 spike-vector flat-buffer view by (unit, segment).
295+ Stable counting-sort of a (N, num_fields) int64 spike-vector flat-buffer view by
296+ (unit, segment). `num_fields` is 3 for `minimum_spike_dtype`, more when the spike vector
297+ carries extra int64 fields; the extra columns are copied along with their spike.
264298
265299 Each spike's bucket is derived on the fly as
266300 `unit_index * unit_stride + segment_index * segment_stride`, so no bucket array is needed.
@@ -280,7 +314,7 @@ def reorder_spike_vector_numba(in_flat, unit_stride, segment_stride, num_buckets
280314 Returns False if any spike falls outside [0, num_buckets),
281315 in which case the outputs are meaningless; True otherwise.
282316 """
283- num_spikes = in_flat .shape [ 0 ]
317+ num_spikes , num_fields = in_flat .shape
284318
285319 # Pass 1: histogram the buckets and do bounds-check (free! we already have to make the pass)
286320 for b in range (num_buckets ):
@@ -303,9 +337,8 @@ def reorder_spike_vector_numba(in_flat, unit_stride, segment_stride, num_buckets
303337 for i in range (num_spikes ):
304338 bucket = in_flat [i , 1 ] * unit_stride + in_flat [i , 2 ] * segment_stride
305339 pos = write_pos [bucket ]
306- out_flat [pos , 0 ] = in_flat [i , 0 ]
307- out_flat [pos , 1 ] = in_flat [i , 1 ]
308- out_flat [pos , 2 ] = in_flat [i , 2 ]
340+ for field in range (num_fields ):
341+ out_flat [pos , field ] = in_flat [i , field ]
309342 order [pos ] = i
310343 write_pos [bucket ] = pos + 1
311344
0 commit comments