Skip to content

Commit 6f571f0

Browse files
authored
Merge pull request #1608 from h-mayorquin/use_device_index_in_neo
Open folders with user altered filenames in SpikeGLX
2 parents 17a3e8b + 8731e88 commit 6f571f0

File tree

2 files changed

+68
-30
lines changed

2 files changed

+68
-30
lines changed

neo/rawio/spikeglxrawio.py

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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"] = []

neo/test/rawiotest/test_spikeglxrawio.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class TestSpikeGLXRawIO(BaseTestRawIO, unittest.TestCase):
3232
"spikeglx/NP2_subset_with_sync",
3333
# NP-ultra
3434
"spikeglx/np_ultra_stub",
35+
# Filename changed by the user, multi-dock
36+
"spikeglx/multi_probe_multi_dock_multi_shank_filename_without_info",
3537
# CatGT
3638
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-A",
3739
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-B",

0 commit comments

Comments
 (0)