@@ -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
@@ -250,7 +245,6 @@ def _parse_header(self):
250245 # insert some annotation at some place
251246 self ._generate_minimal_annotations ()
252247 self ._generate_minimal_annotations ()
253- block_ann = self .raw_annotations ["blocks" ][0 ]
254248
255249 for seg_index in range (nb_segment ):
256250 seg_ann = self .raw_annotations ["blocks" ][0 ]["segments" ][seg_index ]
@@ -354,23 +348,56 @@ def scan_files(dirname):
354348 if len (info_list ) == 0 :
355349 raise FileNotFoundError (f"No appropriate combination of .meta and .bin files were detected in { dirname } " )
356350
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 )
351+ # This sets non-integers values before integers
352+ normalize = lambda x : x if isinstance (x , int ) else - 1
353+
354+ # Segment index is determined by the gate_num and trigger_num in that order
355+ def get_segment_tuple (info ):
356+ # Create a key from the normalized gate_num and trigger_num
357+ gate_num = normalize (info .get ("gate_num" ))
358+ trigger_num = normalize (info .get ("trigger_num" ))
359+ return (gate_num , trigger_num )
360+
361+ unique_segment_tuples = {get_segment_tuple (info ) for info in info_list }
362+ sorted_keys = sorted (unique_segment_tuples )
363+
364+ # Map each unique key to a corresponding index
365+ segment_tuple_to_segment_index = {key : idx for idx , key in enumerate (sorted_keys )}
366+
371367 for info in info_list :
372- info ["seg_index" ] = order_key .index (make_key (info ))
368+ info ["seg_index" ] = segment_tuple_to_segment_index [get_segment_tuple (info )]
369+
370+
371+ # Probe index calculation
372+ # The calculation is ordered by slot, port, dock in that order, this is the number that appears in the filename
373+ # after imec when using native names (e.g. imec0, imec1, etc.)
374+ def get_probe_tuple (info ):
375+ slot = normalize (info .get ("probe_slot" ))
376+ port = normalize (info .get ("probe_port" ))
377+ dock = normalize (info .get ("probe_dock" ))
378+ return (slot , port , dock )
379+
380+ # TODO: handle one box case
381+ info_list_imec = [info for info in info_list if info .get ("device" ) != "nidq" ]
382+ unique_probe_tuples = {get_probe_tuple (info ) for info in info_list_imec }
383+ sorted_probe_keys = sorted (unique_probe_tuples )
384+ probe_tuple_to_probe_index = {key : idx for idx , key in enumerate (sorted_probe_keys )}
385+
386+ for info in info_list :
387+ if info .get ("device" ) == "nidq" :
388+ info ["device_index" ] = "" # TODO: Handle multi nidq case, maybe use meta["typeNiEnabled"]
389+ else :
390+ info ["device_index" ] = probe_tuple_to_probe_index [get_probe_tuple (info )]
373391
392+ # Define stream base on device [imec|nidq], device_index and stream_kind [ap|lf] for imec
393+ for info in info_list :
394+ device_kind = info ["device_kind" ]
395+ device_index = info ["device_index" ]
396+ stream_kind = f".{ info ['stream_kind' ]} " if info ["stream_kind" ] else ""
397+ stream_name = f"{ device_kind } { device_index } { stream_kind } "
398+
399+ info ["stream_name" ] = stream_name
400+
374401 return info_list
375402
376403
@@ -488,13 +515,15 @@ def extract_stream_info(meta_file, meta):
488515 else :
489516 # NIDQ case
490517 has_sync_trace = False
491- fname = Path (meta_file ).stem
518+
519+ bin_file_path = meta ["fileName" ]
520+ fname = Path (bin_file_path ).stem
521+
492522 run_name , gate_num , trigger_num , device , stream_kind = parse_spikeglx_fname (fname )
493523
494524 if "imec" in fname .split ("." )[- 2 ]:
495525 device = fname .split ("." )[- 2 ]
496526 stream_kind = fname .split ("." )[- 1 ]
497- stream_name = device + "." + stream_kind
498527 units = "uV"
499528 # please note the 1e6 in gain for this uV
500529
@@ -534,7 +563,6 @@ def extract_stream_info(meta_file, meta):
534563 else :
535564 device = fname .split ("." )[- 1 ]
536565 stream_kind = ""
537- stream_name = device
538566 units = "V"
539567 channel_gains = np .ones (num_chan )
540568
@@ -550,6 +578,10 @@ def extract_stream_info(meta_file, meta):
550578 gain_factor = float (meta ["niAiRangeMax" ]) / 32768
551579 channel_gains = per_channel_gain * gain_factor
552580
581+ probe_slot = meta .get ("imDatPrb_slot" , None )
582+ probe_port = meta .get ("imDatPrb_port" , None )
583+ probe_dock = meta .get ("imDatPrb_dock" , None )
584+
553585 info = {}
554586 info ["fname" ] = fname
555587 info ["meta" ] = meta
@@ -563,12 +595,16 @@ def extract_stream_info(meta_file, meta):
563595 info ["trigger_num" ] = trigger_num
564596 info ["device" ] = device
565597 info ["stream_kind" ] = stream_kind
566- info ["stream_name" ] = stream_name
598+ # All non-production probes (phase 3B onwards) have "typeThis", otherwise revert to file parsing
599+ info ["device_kind" ] = meta .get ("typeThis" , device .split ("." )[0 ])
567600 info ["units" ] = units
568601 info ["channel_names" ] = [txt .split (";" )[0 ] for txt in meta ["snsChanMap" ]]
569602 info ["channel_gains" ] = channel_gains
570603 info ["channel_offsets" ] = np .zeros (info ["num_chan" ])
571604 info ["has_sync_trace" ] = has_sync_trace
605+ info ["probe_slot" ] = int (probe_slot ) if probe_slot else None
606+ info ["probe_port" ] = int (probe_port ) if probe_port else None
607+ info ["probe_dock" ] = int (probe_dock ) if probe_dock else None
572608
573609 if "nidq" in device :
574610 info ["digital_channels" ] = []
0 commit comments