2020logger = logging .getLogger (__name__ )
2121
2222
23+ def resolve_neuron_indices (neuron_ids , neuron_indices , unit_ids , n_signals ):
24+ if neuron_ids is None and neuron_indices is None :
25+ return None
26+
27+ if neuron_ids is not None :
28+ ids_to_indexes = []
29+
30+ for nid in neuron_ids :
31+ match = np .where (unit_ids == nid )[0 ]
32+ if len (match ) == 0 :
33+ raise ValueError (f"Neuron id { nid } not found" )
34+ ids_to_indexes .append (int (match [0 ]))
35+
36+ if neuron_indices is None :
37+ return ids_to_indexes
38+
39+ if set (ids_to_indexes ) != set (neuron_indices ):
40+ raise ValueError ("neuron_ids and neuron_indices refer to different neurons" )
41+
42+ warnings .warn (
43+ "Both neuron_ids and neuron_indices provided; using neuron_indices" ,
44+ stacklevel = 2 ,
45+ )
46+
47+ return validate_neuron_indices (neuron_indices , n_signals )
48+
49+
50+ def validate_neuron_indices (neuron_indices , n_signals ):
51+ try :
52+ indexes_seq = list (neuron_indices )
53+ except TypeError as exc :
54+ raise TypeError ("neuron_indices must be iterable" ) from exc
55+
56+ if not all (isinstance (i , (int , np .integer )) for i in indexes_seq ):
57+ raise TypeError ("neuron_indices must contain integers" )
58+
59+ if indexes_seq :
60+ if min (indexes_seq ) < 0 or max (indexes_seq ) >= n_signals :
61+ raise ValueError ("neuron_indices out of bounds" )
62+
63+ if len (set (indexes_seq )) != len (indexes_seq ):
64+ raise ValueError ("neuron_indices contain duplicates" )
65+
66+ return indexes_seq
67+
68+
2369class Interpolator :
2470 """Abstract base class for time series interpolation.
2571
@@ -55,8 +101,10 @@ class Interpolator:
55101 Experiment : High-level interface that manages multiple interpolators.
56102 """
57103
58- def __init__ (self , root_folder : str ) -> None :
104+ def __init__ (self , root_folder : str | Path ) -> None :
59105 self .root_folder = Path (root_folder )
106+ self .n_signals : int = 0
107+ self .interpolation_mode : str | None = None
60108 self .start_time = None
61109 self .end_time = None
62110 # Valid interval can be different to start time and end time.
@@ -177,6 +225,14 @@ class SequenceInterpolator(Interpolator):
177225 If True, subtracts mean during normalization.
178226 normalize_std_threshold : float, optional
179227 Minimum std threshold to prevent division by near-zero values.
228+ neuron_ids : list, optional
229+ Biological neuron IDs to include. Converted to indexes using meta/unit_ids.npy.
230+ neuron_indices : list, optional
231+ Column indexes of neurons to include.
232+ neuron_ids : list, optional
233+ Biological neuron IDs to include. Converted to indexes using meta/unit_ids.npy.
234+ neuron_indices : list, optional
235+ Column indexes of neurons to include.
180236 **kwargs
181237 Additional keyword arguments (ignored).
182238
@@ -202,10 +258,12 @@ class SequenceInterpolator(Interpolator):
202258
203259 def __init__ (
204260 self ,
205- root_folder : str ,
261+ root_folder : str | Path ,
206262 cache_data : bool = False , # already cached, put it here for consistency
207263 keep_nans : bool = False ,
208264 interpolation_mode : str = "nearest_neighbor" ,
265+ neuron_ids : list [int ] | None = None ,
266+ neuron_indices : list [int ] | None = None ,
209267 normalize : bool = False ,
210268 normalize_subtract_mean : bool = False ,
211269 normalize_std_threshold : float | None = None , # or 0.01
@@ -215,18 +273,30 @@ def __init__(
215273 meta = self .load_meta ()
216274 self .keep_nans = keep_nans
217275 self .interpolation_mode = interpolation_mode
276+ self .neuron_ids = neuron_ids
218277 self .normalize = normalize
219278 self .normalize_subtract_mean = normalize_subtract_mean
220279 self .normalize_std_threshold = normalize_std_threshold
221280 self .sampling_rate = meta ["sampling_rate" ]
222281 self .time_delta = 1.0 / self .sampling_rate
223282 self .start_time = meta ["start_time" ]
224283 self .end_time = meta ["end_time" ]
225- self .is_mem_mapped = meta [ "is_mem_mapped" ] if "is_mem_mapped" in meta else False
284+ self .is_mem_mapped = meta . get ( "is_mem_mapped" , False )
226285 # Valid interval can be different to start time and end time.
227286 self .valid_interval = TimeInterval (self .start_time , self .end_time )
228287
229288 self .n_signals = meta ["n_signals" ]
289+ unit_ids = None
290+ if neuron_ids is not None :
291+ unit_ids = np .load (self .root_folder / "meta/unit_ids.npy" )
292+
293+ self .neuron_indices = resolve_neuron_indices (
294+ neuron_ids ,
295+ neuron_indices ,
296+ unit_ids ,
297+ self .n_signals ,
298+ )
299+
230300 # read .mem (memmap) or .npy file
231301 if self .is_mem_mapped :
232302 self ._data = np .memmap (
@@ -235,28 +305,47 @@ def __init__(
235305 mode = "r" ,
236306 shape = (meta ["n_timestamps" ], meta ["n_signals" ]),
237307 )
238-
239- if cache_data :
240- self ._data = np .array (self ._data ).astype (
241- np .float32
242- ) # Convert memmap to ndarray
243308 else :
244309 self ._data = np .load (self .root_folder / "data.npy" )
245310
311+ if self .neuron_indices is not None :
312+ self .n_signals = len (self .neuron_indices )
313+
314+ # Cache only selected data
315+ if self .is_mem_mapped and cache_data :
316+ if self .neuron_indices is None :
317+ self ._data = np .array (self ._data , dtype = np .float32 )
318+ else :
319+ self ._data = np .stack (
320+ [self ._data [:, i ] for i in self .neuron_indices ],
321+ axis = 1 ,
322+ ).astype (np .float32 )
323+
324+ self .is_mem_mapped = False
325+
246326 if self .normalize :
247327 self .normalize_init ()
248328
249329 def normalize_init (self ):
250- self .mean = np .load (self .root_folder / "meta/means.npy" )
251- self .std = np .load (self .root_folder / "meta/stds.npy" )
330+ mean = np .load (self .root_folder / "meta/means.npy" ) # shape: (n_total_signals,)
331+ std = np .load (self .root_folder / "meta/stds.npy" )
332+
333+ # Filter to selected neurons, before assertion
334+ if self .neuron_indices is not None :
335+ mean = mean [self .neuron_indices ]
336+ std = std [self .neuron_indices ]
337+
338+ self .mean = mean .T
339+ self .std = std .T
340+
341+ # Now n_signals and shape are guaranteed to match
252342 assert (
253343 self .mean .shape [0 ] == self .n_signals
254- ), f"mean shape does not match: { self .mean .shape } vs { self ._data . shape } "
344+ ), f"mean shape does not match: { self .mean .shape [ 0 ] } vs { self .n_signals } "
255345 assert (
256346 self .std .shape [0 ] == self .n_signals
257- ), f"std shape does not match: { self .std .shape } vs { self ._data .shape } "
258- self .mean = self .mean .T
259- self .std = self .std .T
347+ ), f"std shape does not match: { self .std .shape [0 ]} vs { self .n_signals } "
348+
260349 if self .normalize_std_threshold :
261350 threshold = self .normalize_std_threshold * np .nanmean (self .std )
262351 idx = self .std > threshold
@@ -294,7 +383,16 @@ def interpolate(
294383 )
295384
296385 if self .interpolation_mode == "nearest_neighbor" :
297- data = self ._data [idx_lower ]
386+ if self .neuron_indices is None :
387+ data = self ._data [idx_lower ]
388+ else :
389+ if self .is_mem_mapped :
390+ data = np .stack (
391+ [self ._data [idx_lower , i ] for i in self .neuron_indices ],
392+ axis = 1 ,
393+ )
394+ else :
395+ data = self ._data [idx_lower ]
298396
299397 return (data , valid ) if return_valid else data
300398
@@ -324,8 +422,22 @@ def interpolate(
324422 lower_signal_ratio = ((times_upper - times_valid ) / denom )[:, None ]
325423 upper_signal_ratio = ((times_valid - times_lower ) / denom )[:, None ]
326424
327- data_lower = self ._data [idx_lower ]
328- data_upper = self ._data [idx_upper ]
425+ if self .neuron_indices is None :
426+ data_lower = self ._data [idx_lower ]
427+ data_upper = self ._data [idx_upper ]
428+ else :
429+ if self .is_mem_mapped :
430+ data_lower = np .stack (
431+ [self ._data [idx_lower , i ] for i in self .neuron_indices ],
432+ axis = 1 ,
433+ )
434+ data_upper = np .stack (
435+ [self ._data [idx_upper , i ] for i in self .neuron_indices ],
436+ axis = 1 ,
437+ )
438+ else :
439+ data_lower = self ._data [idx_lower ]
440+ data_upper = self ._data [idx_upper ]
329441
330442 interpolated = (
331443 lower_signal_ratio * data_lower + upper_signal_ratio * data_upper
@@ -376,7 +488,7 @@ class PhaseShiftedSequenceInterpolator(SequenceInterpolator):
376488
377489 def __init__ (
378490 self ,
379- root_folder : str ,
491+ root_folder : str | Path ,
380492 cache_data : bool = False , # already cached, put it here for consistency
381493 keep_nans : bool = False ,
382494 interpolation_mode : str = "nearest_neighbor" ,
@@ -397,6 +509,10 @@ def __init__(
397509 )
398510
399511 self ._phase_shifts = np .load (self .root_folder / "meta/phase_shifts.npy" )
512+ # Forward the required indexes
513+ if self .neuron_indices is not None :
514+ self ._phase_shifts = self ._phase_shifts [self .neuron_indices ]
515+
400516 self .valid_interval = TimeInterval (
401517 self .start_time
402518 + (np .max (self ._phase_shifts ) if len (self ._phase_shifts ) > 0 else 0 ),
@@ -523,8 +639,8 @@ class ScreenInterpolator(Interpolator):
523639
524640 def __init__ (
525641 self ,
526- root_folder : str ,
527- cache_data : bool = False ,
642+ root_folder : str | Path ,
643+ cache_data : bool = False , # New parameter
528644 rescale : bool = False ,
529645 rescale_size : tuple [int , int ] | None = None ,
530646 normalize : bool = False ,
@@ -752,7 +868,7 @@ class TimeIntervalInterpolator(Interpolator):
752868 *i*-th valid time falls within any interval for the *j*-th label.
753869 """
754870
755- def __init__ (self , root_folder : str , cache_data : bool = False , ** kwargs ):
871+ def __init__ (self , root_folder : str | Path , cache_data : bool = False , ** kwargs ):
756872 super ().__init__ (root_folder )
757873 self .cache_data = cache_data
758874
@@ -1046,11 +1162,13 @@ class SpikeInterpolator(Interpolator):
10461162
10471163 def __init__ (
10481164 self ,
1049- root_folder : str ,
1165+ root_folder : str | Path ,
10501166 cache_data : bool = False ,
10511167 interpolation_window : float = 0.3 ,
10521168 interpolation_align : str = "center" ,
10531169 smoothing_sigma : float = 0.0 ,
1170+ neuron_ids : list [int ] | None = None ,
1171+ neuron_indices : list [int ] | None = None ,
10541172 ):
10551173 super ().__init__ (root_folder )
10561174
@@ -1100,6 +1218,42 @@ def __init__(
11001218 else :
11011219 self .spikes = np .load (self .dat_path )
11021220
1221+ unit_ids = None
1222+ if neuron_ids is not None :
1223+ unit_ids = np .load (self .root_folder / "meta/unit_ids.npy" )
1224+
1225+ neuron_indices = resolve_neuron_indices (
1226+ neuron_ids ,
1227+ neuron_indices ,
1228+ unit_ids ,
1229+ self .n_signals ,
1230+ )
1231+
1232+ # If specific neuron indexes are requested, rebuild the spike array so that it
1233+ # only contains spikes from the selected neurons. We also rebuild the indices
1234+ # array so that it matches the new compacted spike array.
1235+ if neuron_indices is not None :
1236+ if len (neuron_indices ) == 0 :
1237+ # No neurons selected: represent this as an empty spike train
1238+ self .spikes = np .empty ((0 ,), dtype = self .spikes .dtype )
1239+ self .indices = np .array ([0 ], dtype = np .int64 )
1240+ self .n_signals = 0
1241+ else :
1242+ new_indices = [0 ]
1243+ new_spikes = []
1244+
1245+ for i in neuron_indices :
1246+ start = self .indices [i ]
1247+ end = self .indices [i + 1 ]
1248+ neuron_spikes = self .spikes [start :end ]
1249+
1250+ new_spikes .append (neuron_spikes )
1251+ new_indices .append (new_indices [- 1 ] + len (neuron_spikes ))
1252+
1253+ self .spikes = np .concatenate (new_spikes )
1254+ self .indices = np .array (new_indices , dtype = np .int64 )
1255+ self .n_signals = len (neuron_indices )
1256+
11031257 def interpolate (
11041258 self , times : np .ndarray , return_valid : bool = False
11051259 ) -> tuple [np .ndarray , np .ndarray ] | np .ndarray :
0 commit comments