@@ -126,6 +126,7 @@ def _source_name(self):
126126
127127 def _parse_header (self ):
128128 self .signals_info_list = scan_files (self .dirname )
129+ _add_segment_order (self .signals_info_list )
129130
130131 # sort stream_name by higher sampling rate first
131132 srates = {info ["stream_name" ]: info ["sampling_rate" ] for info in self .signals_info_list }
@@ -165,8 +166,9 @@ def _parse_header(self):
165166 sync_stream_id_to_buffer_id = {}
166167
167168 for stream_name in stream_names :
168- # take first segment
169- info = self .signals_info_dict [0 , stream_name ]
169+ # take first segment to extract the signal info
170+ segment_index = 0
171+ info = self .signals_info_dict [segment_index , stream_name ]
170172
171173 buffer_id = stream_name
172174 buffer_name = stream_name
@@ -175,44 +177,42 @@ def _parse_header(self):
175177 stream_id = stream_name
176178
177179 signal_streams .append ((stream_name , stream_id , buffer_id ))
178-
180+ signal_channel_names = info [ "analog_channels" ] if info [ "device_kind" ] in [ "obx" , "nidq" ] else info [ "channel_names" ]
179181 # add channels to global list
180- for local_chan in range (info ["num_chan" ]):
181- chan_name = info ["channel_names" ][local_chan ]
182- chan_id = f"{ stream_name } #{ chan_name } "
183-
184- # Sync channel
185- if (
186- "nidq" not in stream_name
187- and "SY0" in chan_name
188- and not self .load_sync_channel
189- and local_chan == info ["num_chan" ] - 1
190- ):
182+ for local_channel_index , channel_name in enumerate (signal_channel_names ):
183+ # This should be unique across all streams
184+ chan_id = f"{ stream_name } #{ channel_name } "
185+
186+ # Separate sync channel in its own stream
187+ is_sync_channel = "SY0" in channel_name and not self .load_sync_channel
188+ if is_sync_channel :
191189 # This is a sync channel and should be added as its own stream
192190 sync_stream_id = f"{ stream_name } -SYNC"
193191 sync_stream_id_to_buffer_id [sync_stream_id ] = buffer_id
194192 stream_id_for_chan = sync_stream_id
193+
195194 else :
196195 stream_id_for_chan = stream_id
197196
198197 signal_channels .append (
199198 (
200- chan_name ,
199+ channel_name ,
201200 chan_id ,
202201 info ["sampling_rate" ],
203202 "int16" ,
204203 info ["units" ],
205- info ["channel_gains" ][local_chan ],
206- info ["channel_offsets" ][local_chan ],
204+ info ["channel_gains" ][local_channel_index ],
205+ info ["channel_offsets" ][local_channel_index ],
207206 stream_id_for_chan ,
208207 buffer_id ,
209208 )
210209 )
211210
212- # all channel by default unless load_sync_channel=False
211+ # None here means that the all the channels in the buffer will be included
213212 self ._stream_buffer_slice [stream_id ] = None
214213
215- # check sync channel validity
214+ # Then we modify this if sync channel is present to slice the last channel
215+ # out of the stream buffer
216216 if "nidq" not in stream_name :
217217 if not self .load_sync_channel and info ["has_sync_trace" ]:
218218 # the last channel is removed from the stream but not from the buffer
@@ -227,7 +227,7 @@ def _parse_header(self):
227227
228228 signal_buffers = np .array (signal_buffers , dtype = _signal_buffer_dtype )
229229
230- # Add sync channels as their own streams
230+ # Add sync channels as their own streams. We do it here to keep the order of the streams
231231 for sync_stream_id , buffer_id in sync_stream_id_to_buffer_id .items ():
232232 signal_streams .append ((sync_stream_id , sync_stream_id , buffer_id ))
233233
@@ -259,7 +259,6 @@ def _parse_header(self):
259259 spike_channels = np .array (spike_channels , dtype = _spike_channel_dtype )
260260
261261 # deal with nb_segment and t_start/t_stop per segment
262-
263262 self ._t_starts = {stream_name : {} for stream_name in stream_names }
264263 self ._t_stops = {seg_index : 0.0 for seg_index in range (nb_segment )}
265264
@@ -378,12 +377,7 @@ def _get_analogsignal_buffer_description(self, block_index, seg_index, buffer_id
378377
379378def scan_files (dirname ):
380379 """
381- Scan for pairs of `.bin` and `.meta` files and return information about it.
382-
383- After exploring the folder, the segment index (`seg_index`) is construct as follow:
384- * if only one `gate_num=0` then `trigger_num` = `seg_index`
385- * if only one `trigger_num=0` then `gate_num` = `seg_index`
386- * if both are increasing then seg_index increased by gate_num, trigger_num order.
380+ Scan for pairs of `.bin` and `.meta` files and parse the metadata file to extract signal information.
387381 """
388382 info_list = []
389383
@@ -392,7 +386,15 @@ def scan_files(dirname):
392386 if not file .endswith (".meta" ):
393387 continue
394388 meta_filename = Path (root ) / file
395- bin_filename = meta_filename .with_suffix (".bin" )
389+
390+ # Handle both regular .bin files and OneBox .obx.bin files
391+ if file .endswith (".obx.meta" ):
392+ # OneBox case: .obx.meta -> .obx.bin
393+ bin_filename = meta_filename .with_suffix (".bin" )
394+ else :
395+ # Regular case: .meta -> .bin
396+ bin_filename = meta_filename .with_suffix (".bin" )
397+
396398 if meta_filename .exists () and bin_filename .exists ():
397399 meta = read_meta_file (meta_filename )
398400 info = extract_stream_info (meta_filename , meta )
@@ -404,9 +406,21 @@ def scan_files(dirname):
404406 if len (info_list ) == 0 :
405407 raise FileNotFoundError (f"No appropriate combination of .meta and .bin files were detected in { dirname } " )
406408
409+ return info_list
410+
411+ def _add_segment_order (info_list ):
412+ """
413+ Uses gate and trigger numbers to construct a segment index (`seg_index`) for each signal in `info_list`.
414+
415+ After exploring the folder, the segment index (`seg_index`) is construct as follow:
416+ * if only one `gate_num=0` then `trigger_num` = `seg_index`
417+ * if only one `trigger_num=0` then `gate_num` = `seg_index`
418+ * if both are increasing then seg_index increased by gate_num, trigger_num order.
419+
420+ """
407421 # This sets non-integers values before integers
408422 normalize = lambda x : x if isinstance (x , int ) else - 1
409-
423+
410424 # Segment index is determined by the gate_num and trigger_num in that order
411425 def get_segment_tuple (info ):
412426 # Create a key from the normalized gate_num and trigger_num
@@ -423,25 +437,6 @@ def get_segment_tuple(info):
423437 for info in info_list :
424438 info ["seg_index" ] = segment_tuple_to_segment_index [get_segment_tuple (info )]
425439
426- for info in info_list :
427- # device_kind is imec, nidq
428- if info .get ("device_kind" ) == "imec" :
429- info ["device_index" ] = info ["device" ].split ("imec" )[- 1 ]
430- else :
431- info ["device_index" ] = "" # TODO: Handle multi nidq case, maybe use meta["typeNiEnabled"]
432-
433- # Define stream base on device_kind [imec|nidq], device_index and stream_kind [ap|lf] for imec
434- # Stream format is "{device_kind}{device_index}.{stream_kind}"
435- for info in info_list :
436- device_kind = info ["device_kind" ]
437- device_index = info ["device_index" ]
438- stream_kind = f".{ info ['stream_kind' ]} " if info ["stream_kind" ] else ""
439- stream_name = f"{ device_kind } { device_index } { stream_kind } "
440-
441- info ["stream_name" ] = stream_name
442-
443- return info_list
444-
445440
446441def parse_spikeglx_fname (fname ):
447442 """
@@ -451,13 +446,13 @@ def parse_spikeglx_fname(fname):
451446 https://github.com/billkarsh/SpikeGLX/blob/15ec8898e17829f9f08c226bf04f46281f106e5f/Markdown/UserManual.md#gates-and-triggers
452447
453448 Example file name structure:
454- Consider the filenames: `Noise4Sam_g0_t0.nidq.bin` or `Noise4Sam_g0_t0.imec0.lf.bin`
449+ Consider the filenames: `Noise4Sam_g0_t0.nidq.bin`, `Noise4Sam_g0_t0.imec0.lf.bin`, or `myRun_g0_t0.obx0.obx .bin`
455450 The filenames consist of 3 or 4 parts separated by `.`
456451 1. "Noise4Sam_g0_t0" will be the `name` variable. This choosen by the user at recording time.
457452 2. "g0" is the "gate_num"
458453 3. "t0" is the "trigger_num"
459- 4. "nidq" or "imec0 " will give the `device`
460- 5. "lf" or "ap " will be the `stream_kind`
454+ 4. "nidq", "imec0", or "obx0 " will give the `device`
455+ 5. "lf", "ap", or "obx " will be the `stream_kind`
461456 `stream_name` variable is the concatenation of `device.stream_kind`
462457
463458 If CatGT is used, then the trigger numbers in the file names ("t0"/"t1"/etc.)
@@ -472,7 +467,7 @@ def parse_spikeglx_fname(fname):
472467 Parameters
473468 ---------
474469 fname: str
475- The filename to parse without the extension, e.g. "my-run-name_g0_t1.imec2.lf"
470+ The filename to parse without the extension, e.g. "my-run-name_g0_t1.imec2.lf" or "my-run-name_g0_t0.obx0.obx"
476471
477472 Returns
478473 -------
@@ -483,27 +478,33 @@ def parse_spikeglx_fname(fname):
483478 trigger_num: int | str or None
484479 The trigger identifier, e.g. 1. If CatGT is used, then the trigger_num will be set to "cat".
485480 device: str
486- The probe identifier, e.g. "imec2"
481+ The probe identifier, e.g. "imec2" or "obx0"
487482 stream_kind: str or None
488- The data type identifier, "lf" or "ap" or None
483+ The data type identifier, "lf", "ap", "obx", or None
489484 """
490485 re_standard = re .findall (r"(\S*)_g(\d*)_t(\d*)\.(\S*).(ap|lf)" , fname )
491486 re_tcat = re .findall (r"(\S*)_g(\d*)_tcat.(\S*).(ap|lf)" , fname )
492487 re_nidq = re .findall (r"(\S*)_g(\d*)_t(\d*)\.(\S*)" , fname )
488+ re_obx = re .findall (r"(\S*)_g(\d*)_t(\d*)\.(\S*)\.obx" , fname )
489+
493490 if len (re_standard ) == 1 :
494491 # standard case with probe
495492 run_name , gate_num , trigger_num , device , stream_kind = re_standard [0 ]
496493 elif len (re_tcat ) == 1 :
497494 # tcat case
498495 run_name , gate_num , device , stream_kind = re_tcat [0 ]
499496 trigger_num = "cat"
497+ elif len (re_obx ) == 1 :
498+ # OneBox case
499+ run_name , gate_num , trigger_num , device = re_obx [0 ]
500+ stream_kind = "obx"
500501 elif len (re_nidq ) == 1 :
501502 # case for nidaq
502503 run_name , gate_num , trigger_num , device = re_nidq [0 ]
503504 stream_kind = None
504505 else :
505506 # the naming do not correspond lets try something more easy
506- # example: sglx_xxx.imec0.ap
507+ # example: sglx_xxx.imec0.ap or sglx_xxx.obx0.obx
507508 re_else = re .findall (r"(\S*)\.(\S*).(ap|lf)" , fname )
508509 re_else_nidq = re .findall (r"(\S*)\.(\S*)" , fname )
509510 if len (re_else ) == 1 :
@@ -554,13 +555,17 @@ def extract_stream_info(meta_file, meta):
554555 # AP and LF meta have this field
555556 ap , lf , sy = [int (s ) for s in meta ["snsApLfSy" ].split ("," )]
556557 has_sync_trace = sy == 1
558+ elif "snsXaDwSy" in meta :
559+ # OneBox case
560+ xa , dw , sy = [int (s ) for s in meta ["snsXaDwSy" ].split ("," )]
561+ has_sync_trace = sy == 1
557562 else :
558563 # NIDQ case
559564 has_sync_trace = False
560565
561566 # This is the original name that the file had. It might not match the current name if the user changed it
562- bin_file_path = meta ["fileName" ]
563- fname = Path (bin_file_path ).stem
567+ original_bin_file_path = meta ["fileName" ]
568+ fname = Path (original_bin_file_path ).stem
564569
565570 run_name , gate_num , trigger_num , device , stream_kind = parse_spikeglx_fname (fname )
566571
@@ -603,6 +608,18 @@ def extract_stream_info(meta_file, meta):
603608 channel_gains = gain_factor * per_channel_gain * 1e6
604609 else :
605610 raise NotImplementedError ("This meta file version of spikeglx" " is not implemented" )
611+ elif meta .get ("typeThis" ) == "obx" :
612+ # OneBox case
613+ device = fname .split ("." )[- 2 ] if "." in fname else device
614+ stream_kind = ""
615+ units = "V"
616+
617+ # OneBox gain calculation
618+ # V = i * Vmax / Imax where Imax = obMaxInt, Vmax = obAiRangeMax
619+ # See: https://billkarsh.github.io/SpikeGLX/Sgl_help/Metadata_30.html
620+ max_int = int (meta ["obMaxInt" ])
621+ gain_factor = float (meta ["obAiRangeMax" ]) / max_int
622+ channel_gains = np .ones (num_chan ) * gain_factor
606623 else :
607624 device = fname .split ("." )[- 1 ]
608625 stream_kind = ""
@@ -624,11 +641,14 @@ def extract_stream_info(meta_file, meta):
624641 probe_slot = meta .get ("imDatPrb_slot" , None )
625642 probe_port = meta .get ("imDatPrb_port" , None )
626643 probe_dock = meta .get ("imDatPrb_dock" , None )
644+
645+ # OneBox specific metadata
646+ obx_slot = meta .get ("imDatObx_slot" , None )
627647
628648 info = {}
629649 info ["fname" ] = fname
630650 info ["meta" ] = meta
631- for k in ("niSampRate" , "imSampRate" ):
651+ for k in ("niSampRate" , "imSampRate" , "obSampRate" ):
632652 if k in meta :
633653 info ["sampling_rate" ] = float (meta [k ])
634654 info ["num_chan" ] = num_chan
@@ -648,8 +668,26 @@ def extract_stream_info(meta_file, meta):
648668 info ["probe_slot" ] = int (probe_slot ) if probe_slot else None
649669 info ["probe_port" ] = int (probe_port ) if probe_port else None
650670 info ["probe_dock" ] = int (probe_dock ) if probe_dock else None
671+ info ["obx_slot" ] = int (obx_slot ) if obx_slot else None
672+
673+ # Add device index
674+ if info .get ("device_kind" ) == "imec" :
675+ info ["device_index" ] = info ["device" ].split ("imec" )[- 1 ]
676+ elif info .get ("device_kind" ) == "obx" :
677+ info ["device_index" ] = info ["device" ].split ("obx" )[- 1 ]
678+ else :
679+ info ["device_index" ] = "" # TODO: Handle multi nidq case, maybe use meta["typeNiEnabled"]
651680
652- if "nidq" in device :
681+ # Define stream base on device_kind [imec|nidq|obx], device_index and stream_kind [ap|lf] for imec
682+ # Stream format is "{device_kind}{device_index}.{stream_kind}"
683+ device_kind = info ["device_kind" ]
684+ device_index = info ["device_index" ]
685+ stream_kind = f".{ info ['stream_kind' ]} " if info ["stream_kind" ] else ""
686+ info ["stream_name" ] = f"{ device_kind } { device_index } { stream_kind } "
687+
688+ # Separate analog and digital channels
689+ if device_kind == "nidq" :
690+ # Note that we only handling the non-multiplexed channels here
653691 info ["digital_channels" ] = []
654692 info ["analog_channels" ] = [channel for channel in info ["channel_names" ] if not channel .startswith ("XD" )]
655693 # Digital/event channels are encoded within the digital word, so that will need more handling
@@ -661,4 +699,10 @@ def extract_stream_info(meta_file, meta):
661699 info ["digital_channels" ].extend ([f"XD{ i } " for i in range (start , end + 1 )])
662700 else :
663701 info ["digital_channels" ].append (f"XD{ int (item )} " )
702+ elif device_kind == "obx" :
703+ # OneBox channel handling - focus on analog channels only as requested
704+ info ["digital_channels" ] = [channel for channel in info ["channel_names" ] if channel .startswith ("XD" )]
705+ info ["analog_channels" ] = [channel for channel in info ["channel_names" ] if channel .startswith ("XA" )]
706+
707+
664708 return info
0 commit comments