@@ -82,14 +82,11 @@ class SpikeGLXRawIO(BaseRawWithBufferApiIO):
8282
8383 Notes
8484 -----
85- * Contrary to other implementations this IO reads the entire folder and subfolders and:
86- deals with several segments based on the `_gt0`, `_gt1`, `_gt2`, etc postfixes
87- deals with all signals "imec0", "imec1" for neuropixel probes and also
88- external signal like"nidq". This is the "device"
89- * For imec device both "ap" and "lf" are extracted so one device have several "streams"
90- * There are several versions depending the neuropixel probe generation (`1.x`/`2.x`/`3.x`)
91- * Here, we assume that the `meta` file has the same structure across all generations.
92- * This IO is developed based on neuropixel generation 2.0, single shank recordings.
85+ * This IO reads the entire folder and subfolders locating the `.bin` and `.meta` files
86+ * Handles gates and triggers as segments (based on the `_gt0`, `_gt1`, `_t0` , `_t1` in filenames)
87+ * Handles all signals coming from different acquisition cards ("imec0", "imec1", etc) in a typical
88+ PXIe chassis setup and also external signal like "nidq".
89+ * For imec devices both "ap" and "lf" are extracted so even a one device setup will have several "streams"
9390
9491 Examples
9592 --------
@@ -125,7 +122,6 @@ def _parse_header(self):
125122 stream_names = sorted (list (srates .keys ()), key = lambda e : srates [e ])[::- 1 ]
126123 nb_segment = np .unique ([info ["seg_index" ] for info in self .signals_info_list ]).size
127124
128- # self._memmaps = {}
129125 self .signals_info_dict = {}
130126 # one unique block
131127 self ._buffer_descriptions = {0 : {}}
@@ -166,7 +162,6 @@ def _parse_header(self):
166162
167163 stream_id = stream_name
168164
169- stream_index = stream_names .index (info ["stream_name" ])
170165 signal_streams .append ((stream_name , stream_id , buffer_id ))
171166
172167 # add channels to global list
@@ -229,14 +224,25 @@ def _parse_header(self):
229224 spike_channels = np .array (spike_channels , dtype = _spike_channel_dtype )
230225
231226 # deal with nb_segment and t_start/t_stop per segment
232- self ._t_starts = {seg_index : 0.0 for seg_index in range (nb_segment )}
227+
228+ self ._t_starts = {stream_name : {} for stream_name in stream_names }
233229 self ._t_stops = {seg_index : 0.0 for seg_index in range (nb_segment )}
234- for seg_index in range (nb_segment ):
235- for stream_name in stream_names :
230+
231+ for stream_name in stream_names :
232+ for seg_index in range (nb_segment ):
236233 info = self .signals_info_dict [seg_index , stream_name ]
234+
235+ frame_start = float (info ["meta" ]["firstSample" ])
236+ sampling_frequency = info ["sampling_rate" ]
237+ t_start = frame_start / sampling_frequency
238+
239+ self ._t_starts [stream_name ][seg_index ] = t_start
237240 t_stop = info ["sample_length" ] / info ["sampling_rate" ]
238241 self ._t_stops [seg_index ] = max (self ._t_stops [seg_index ], t_stop )
239242
243+
244+
245+
240246 # fille into header dict
241247 self .header = {}
242248 self .header ["nb_block" ] = 1
@@ -250,7 +256,6 @@ def _parse_header(self):
250256 # insert some annotation at some place
251257 self ._generate_minimal_annotations ()
252258 self ._generate_minimal_annotations ()
253- block_ann = self .raw_annotations ["blocks" ][0 ]
254259
255260 for seg_index in range (nb_segment ):
256261 seg_ann = self .raw_annotations ["blocks" ][0 ]["segments" ][seg_index ]
@@ -282,7 +287,8 @@ def _segment_t_stop(self, block_index, seg_index):
282287 return self ._t_stops [seg_index ]
283288
284289 def _get_signal_t_start (self , block_index , seg_index , stream_index ):
285- return 0.0
290+ stream_name = self .header ["signal_streams" ][stream_index ]["name" ]
291+ return self ._t_starts [stream_name ][seg_index ]
286292
287293 def _event_count (self , event_channel_idx , block_index = None , seg_index = None ):
288294 timestamps , _ , _ = self ._get_event_timestamps (block_index , seg_index , event_channel_idx , None , None )
@@ -354,23 +360,56 @@ def scan_files(dirname):
354360 if len (info_list ) == 0 :
355361 raise FileNotFoundError (f"No appropriate combination of .meta and .bin files were detected in { dirname } " )
356362
357- # the segment index will depend on both 'gate_num' and 'trigger_num'
358- # so we order by 'gate_num' then 'trigger_num'
359- # None is before any int
360- def make_key (info ):
361- k0 = info ["gate_num" ]
362- if k0 is None :
363- k0 = - 1
364- k1 = info ["trigger_num" ]
365- if k1 is None :
366- k1 = - 1
367- return (k0 , k1 )
368-
369- order_key = list ({make_key (info ) for info in info_list })
370- order_key = sorted (order_key )
363+ # This sets non-integers values before integers
364+ normalize = lambda x : x if isinstance (x , int ) else - 1
365+
366+ # Segment index is determined by the gate_num and trigger_num in that order
367+ def get_segment_tuple (info ):
368+ # Create a key from the normalized gate_num and trigger_num
369+ gate_num = normalize (info .get ("gate_num" ))
370+ trigger_num = normalize (info .get ("trigger_num" ))
371+ return (gate_num , trigger_num )
372+
373+ unique_segment_tuples = {get_segment_tuple (info ) for info in info_list }
374+ sorted_keys = sorted (unique_segment_tuples )
375+
376+ # Map each unique key to a corresponding index
377+ segment_tuple_to_segment_index = {key : idx for idx , key in enumerate (sorted_keys )}
378+
371379 for info in info_list :
372- info ["seg_index" ] = order_key .index (make_key (info ))
380+ info ["seg_index" ] = segment_tuple_to_segment_index [get_segment_tuple (info )]
381+
382+
383+ # Probe index calculation
384+ # The calculation is ordered by slot, port, dock in that order, this is the number that appears in the filename
385+ # after imec when using native names (e.g. imec0, imec1, etc.)
386+ def get_probe_tuple (info ):
387+ slot = normalize (info .get ("probe_slot" ))
388+ port = normalize (info .get ("probe_port" ))
389+ dock = normalize (info .get ("probe_dock" ))
390+ return (slot , port , dock )
391+
392+ # TODO: handle one box case
393+ info_list_imec = [info for info in info_list if info .get ("device" ) != "nidq" ]
394+ unique_probe_tuples = {get_probe_tuple (info ) for info in info_list_imec }
395+ sorted_probe_keys = sorted (unique_probe_tuples )
396+ probe_tuple_to_probe_index = {key : idx for idx , key in enumerate (sorted_probe_keys )}
373397
398+ for info in info_list :
399+ if info .get ("device" ) == "nidq" :
400+ info ["device_index" ] = "" # TODO: Handle multi nidq case, maybe use meta["typeNiEnabled"]
401+ else :
402+ info ["device_index" ] = probe_tuple_to_probe_index [get_probe_tuple (info )]
403+
404+ # Define stream base on device [imec|nidq], device_index and stream_kind [ap|lf] for imec
405+ for info in info_list :
406+ device_kind = info ["device_kind" ]
407+ device_index = info ["device_index" ]
408+ stream_kind = f".{ info ['stream_kind' ]} " if info ["stream_kind" ] else ""
409+ stream_name = f"{ device_kind } { device_index } { stream_kind } "
410+
411+ info ["stream_name" ] = stream_name
412+
374413 return info_list
375414
376415
@@ -488,13 +527,15 @@ def extract_stream_info(meta_file, meta):
488527 else :
489528 # NIDQ case
490529 has_sync_trace = False
491- fname = Path (meta_file ).stem
530+
531+ bin_file_path = meta ["fileName" ]
532+ fname = Path (bin_file_path ).stem
533+
492534 run_name , gate_num , trigger_num , device , stream_kind = parse_spikeglx_fname (fname )
493535
494536 if "imec" in fname .split ("." )[- 2 ]:
495537 device = fname .split ("." )[- 2 ]
496538 stream_kind = fname .split ("." )[- 1 ]
497- stream_name = device + "." + stream_kind
498539 units = "uV"
499540 # please note the 1e6 in gain for this uV
500541
@@ -534,7 +575,6 @@ def extract_stream_info(meta_file, meta):
534575 else :
535576 device = fname .split ("." )[- 1 ]
536577 stream_kind = ""
537- stream_name = device
538578 units = "V"
539579 channel_gains = np .ones (num_chan )
540580
@@ -550,6 +590,10 @@ def extract_stream_info(meta_file, meta):
550590 gain_factor = float (meta ["niAiRangeMax" ]) / 32768
551591 channel_gains = per_channel_gain * gain_factor
552592
593+ probe_slot = meta .get ("imDatPrb_slot" , None )
594+ probe_port = meta .get ("imDatPrb_port" , None )
595+ probe_dock = meta .get ("imDatPrb_dock" , None )
596+
553597 info = {}
554598 info ["fname" ] = fname
555599 info ["meta" ] = meta
@@ -563,12 +607,16 @@ def extract_stream_info(meta_file, meta):
563607 info ["trigger_num" ] = trigger_num
564608 info ["device" ] = device
565609 info ["stream_kind" ] = stream_kind
566- info ["stream_name" ] = stream_name
610+ # All non-production probes (phase 3B onwards) have "typeThis", otherwise revert to file parsing
611+ info ["device_kind" ] = meta .get ("typeThis" , device .split ("." )[0 ])
567612 info ["units" ] = units
568613 info ["channel_names" ] = [txt .split (";" )[0 ] for txt in meta ["snsChanMap" ]]
569614 info ["channel_gains" ] = channel_gains
570615 info ["channel_offsets" ] = np .zeros (info ["num_chan" ])
571616 info ["has_sync_trace" ] = has_sync_trace
617+ info ["probe_slot" ] = int (probe_slot ) if probe_slot else None
618+ info ["probe_port" ] = int (probe_port ) if probe_port else None
619+ info ["probe_dock" ] = int (probe_dock ) if probe_dock else None
572620
573621 if "nidq" in device :
574622 info ["digital_channels" ] = []
0 commit comments