11from __future__ import annotations
22
33import warnings
4- from typing import Optional , Union
4+ from typing import Optional
55
66import numpy as np
77
@@ -131,12 +131,52 @@ def get_total_duration(self) -> float:
131131 def get_unit_spike_train (
132132 self ,
133133 unit_id : str | int ,
134- segment_index : Union [int , None ] = None ,
135- start_frame : Union [int , None ] = None ,
136- end_frame : Union [int , None ] = None ,
134+ segment_index : Optional [int ] = None ,
135+ start_frame : Optional [int ] = None ,
136+ end_frame : Optional [int ] = None ,
137137 return_times : bool = False ,
138138 use_cache : bool = True ,
139- ):
139+ ) -> np .ndarray :
140+ """
141+ Get spike train for a unit.
142+
143+ Parameters
144+ ----------
145+ unit_id : str or int
146+ The unit id to retrieve spike train for
147+ segment_index : int or None, default: None
148+ The segment index to retrieve spike train from.
149+ For multi-segment objects, it is required
150+ start_frame : int or None, default: None
151+ The start frame for spike train extraction
152+ end_frame : int or None, default: None
153+ The end frame for spike train extraction
154+ return_times : bool, default: False
155+ If True, returns spike times in seconds instead of frames
156+ use_cache : bool, default: True
157+ If True, uses cached spike trains when available
158+
159+ Returns
160+ -------
161+ spike_train : np.ndarray
162+ Spike frames (or times if return_times=True)
163+ """
164+
165+ if return_times :
166+ start_time = (
167+ self .sample_index_to_time (start_frame , segment_index = segment_index ) if start_frame is not None else None
168+ )
169+ end_time = (
170+ self .sample_index_to_time (end_frame , segment_index = segment_index ) if end_frame is not None else None
171+ )
172+
173+ return self .get_unit_spike_train_in_seconds (
174+ unit_id = unit_id ,
175+ segment_index = segment_index ,
176+ start_time = start_time ,
177+ end_time = end_time ,
178+ )
179+
140180 segment_index = self ._check_segment_index (segment_index )
141181 if use_cache :
142182 if segment_index not in self ._cached_spike_trains :
@@ -161,22 +201,101 @@ def get_unit_spike_train(
161201 unit_id = unit_id , start_frame = start_frame , end_frame = end_frame
162202 ).astype ("int64" )
163203
164- if return_times :
165- if self .has_recording ():
166- times = self .get_times (segment_index = segment_index )
167- return times [spike_frames ]
168- else :
169- segment = self ._sorting_segments [segment_index ]
170- t_start = segment ._t_start if segment ._t_start is not None else 0
171- spike_times = spike_frames / self .get_sampling_frequency ()
172- return t_start + spike_times
173- else :
174- return spike_frames
204+ return spike_frames
205+
206+ def get_unit_spike_train_in_seconds (
207+ self ,
208+ unit_id : str | int ,
209+ segment_index : Optional [int ] = None ,
210+ start_time : Optional [float ] = None ,
211+ end_time : Optional [float ] = None ,
212+ ) -> np .ndarray :
213+ """
214+ Get spike train for a unit in seconds.
215+
216+ This method uses a three-tier approach to get spike times:
217+ 1. If the sorting has a recording, use the recording's time conversion
218+ 2. If the segment implements get_unit_spike_train_in_seconds(), use that directly These are the native timestamps of the format
219+ 3. Fall back to standard frame-to-time conversion
220+
221+ This approach avoids double conversion for extractors that already store
222+ spike times in seconds (e.g., NWB format) and ensures consistent timing
223+ when a recording is associated with the sorting.
224+
225+ Parameters
226+ ----------
227+ unit_id : str or int
228+ The unit id to retrieve spike train for
229+ segment_index : int or None, default: None
230+ The segment index to retrieve spike train from.
231+ For multi-segment objects, it is required
232+ start_time : float or None, default: None
233+ The start time in seconds for spike train extraction
234+ end_time : float or None, default: None
235+ The end time in seconds for spike train extraction
236+
237+ Returns
238+ -------
239+ spike_times : np.ndarray
240+ Spike times in seconds
241+ """
242+ segment_index = self ._check_segment_index (segment_index )
243+ segment = self ._sorting_segments [segment_index ]
175244
176- def register_recording (self , recording , check_spike_frames = True ):
245+ # If sorting has a registered recording, get the frames and get the times from the recording
246+ # Note that this take into account the segment start time of the recording
247+ if self .has_recording ():
248+
249+ # Get all the spike times and then slice them
250+ start_frame = None
251+ end_frame = None
252+ spike_frames = self .get_unit_spike_train (
253+ unit_id = unit_id ,
254+ segment_index = segment_index ,
255+ start_frame = start_frame ,
256+ end_frame = end_frame ,
257+ return_times = False ,
258+ use_cache = True ,
259+ )
260+
261+ spike_times = self .sample_index_to_time (spike_frames , segment_index = segment_index )
262+
263+ # Filter to return only the spikes within the specified time range
264+ if start_time is not None :
265+ spike_times = spike_times [spike_times >= start_time ]
266+ if end_time is not None :
267+ spike_times = spike_times [spike_times <= end_time ]
268+
269+ return spike_times
270+
271+ # Use the native spiking times if available
272+ # Some instances might implement a method themselves to access spike times directly without having to convert
273+ # (e.g. NWB extractors)
274+ if hasattr (segment , "get_unit_spike_train_in_seconds" ):
275+ return segment .get_unit_spike_train_in_seconds (unit_id = unit_id , start_time = start_time , end_time = end_time )
276+
277+ # If no recording attached and all back to frame-based conversion
278+ # Get spike train in frames and convert to times using traditional method
279+ start_frame = self .time_to_sample_index (start_time , segment_index = segment_index ) if start_time else None
280+ end_frame = self .time_to_sample_index (end_time , segment_index = segment_index ) if end_time else None
281+
282+ spike_frames = self .get_unit_spike_train (
283+ unit_id = unit_id ,
284+ segment_index = segment_index ,
285+ start_frame = start_frame ,
286+ end_frame = end_frame ,
287+ return_times = False ,
288+ use_cache = True ,
289+ )
290+
291+ t_start = segment ._t_start if segment ._t_start is not None else 0
292+ spike_times = spike_frames / self .get_sampling_frequency ()
293+ return t_start + spike_times
294+
295+ def register_recording (self , recording , check_spike_frames : bool = True ):
177296 """
178297 Register a recording to the sorting. If the sorting and recording both contain
179- time information, the recording’ s time information will be used.
298+ time information, the recording' s time information will be used.
180299
181300 Parameters
182301 ----------
@@ -215,7 +334,7 @@ def set_sorting_info(self, recording_dict, params_dict, log_dict):
215334 def has_recording (self ) -> bool :
216335 return self ._recording is not None
217336
218- def has_time_vector (self , segment_index = None ) -> bool :
337+ def has_time_vector (self , segment_index : Optional [ int ] = None ) -> bool :
219338 """
220339 Check if the segment of the registered recording has a time vector.
221340 """
@@ -520,6 +639,20 @@ def time_to_sample_index(self, time, segment_index=0):
520639
521640 return sample_index
522641
642+ def sample_index_to_time (
643+ self , sample_index : int | np .ndarray , segment_index : Optional [int ] = None
644+ ) -> float | np .ndarray :
645+ """
646+ Transform sample index into time in seconds
647+ """
648+ segment_index = self ._check_segment_index (segment_index )
649+ if self .has_recording ():
650+ return self ._recording .sample_index_to_time (sample_index , segment_index = segment_index )
651+ else :
652+ segment = self ._sorting_segments [segment_index ]
653+ t_start = segment ._t_start if segment ._t_start is not None else 0
654+ return (sample_index / self .get_sampling_frequency ()) + t_start
655+
523656 def precompute_spike_trains (self , from_spike_vector = None ):
524657 """
525658 Pre-computes and caches all spike trains for this sorting
0 commit comments