-
Notifications
You must be signed in to change notification settings - Fork 249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open folders with user altered filenames in SpikeGLX #1608
base: master
Are you sure you want to change the base?
Changes from 6 commits
aac6733
42e128b
6e1a5b5
986f262
069e2dd
fc4752d
822f7fa
f47e99f
c201b74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,14 +82,11 @@ class SpikeGLXRawIO(BaseRawWithBufferApiIO): | |
|
||
Notes | ||
----- | ||
* Contrary to other implementations this IO reads the entire folder and subfolders and: | ||
deals with several segments based on the `_gt0`, `_gt1`, `_gt2`, etc postfixes | ||
deals with all signals "imec0", "imec1" for neuropixel probes and also | ||
external signal like"nidq". This is the "device" | ||
* For imec device both "ap" and "lf" are extracted so one device have several "streams" | ||
* There are several versions depending the neuropixel probe generation (`1.x`/`2.x`/`3.x`) | ||
* Here, we assume that the `meta` file has the same structure across all generations. | ||
* This IO is developed based on neuropixel generation 2.0, single shank recordings. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a note of the first version but we have gradually added support. This supports the shanked version just fine and also NHP and other varieties. |
||
* This IO reads the entire folder and subfolders locating the `.bin` and `.meta` files | ||
* Handles gates and triggers as segments (based on the `_gt0`, `_gt1`, `_t0` , `_t1` in filenames) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added specific mention to gates and triggers as that is more in line with SpikeGLX documentation. |
||
* Handles all signals coming from different acquisition cards ("imec0", "imec1", etc) in a typical | ||
PXIe chassis setup and also external signal like "nidq". | ||
* For imec devices both "ap" and "lf" are extracted so even a one device setup will have several "streams" | ||
|
||
Examples | ||
-------- | ||
|
@@ -125,7 +122,6 @@ def _parse_header(self): | |
stream_names = sorted(list(srates.keys()), key=lambda e: srates[e])[::-1] | ||
nb_segment = np.unique([info["seg_index"] for info in self.signals_info_list]).size | ||
|
||
# self._memmaps = {} | ||
self.signals_info_dict = {} | ||
# one unique block | ||
self._buffer_descriptions = {0: {}} | ||
|
@@ -166,7 +162,6 @@ def _parse_header(self): | |
|
||
stream_id = stream_name | ||
|
||
stream_index = stream_names.index(info["stream_name"]) | ||
signal_streams.append((stream_name, stream_id, buffer_id)) | ||
|
||
# add channels to global list | ||
|
@@ -250,7 +245,6 @@ def _parse_header(self): | |
# insert some annotation at some place | ||
self._generate_minimal_annotations() | ||
self._generate_minimal_annotations() | ||
block_ann = self.raw_annotations["blocks"][0] | ||
|
||
for seg_index in range(nb_segment): | ||
seg_ann = self.raw_annotations["blocks"][0]["segments"][seg_index] | ||
|
@@ -354,23 +348,54 @@ def scan_files(dirname): | |
if len(info_list) == 0: | ||
raise FileNotFoundError(f"No appropriate combination of .meta and .bin files were detected in {dirname}") | ||
|
||
# the segment index will depend on both 'gate_num' and 'trigger_num' | ||
# so we order by 'gate_num' then 'trigger_num' | ||
# None is before any int | ||
def make_key(info): | ||
k0 = info["gate_num"] | ||
if k0 is None: | ||
k0 = -1 | ||
k1 = info["trigger_num"] | ||
if k1 is None: | ||
k1 = -1 | ||
return (k0, k1) | ||
|
||
order_key = list({make_key(info) for info in info_list}) | ||
order_key = sorted(order_key) | ||
# This sets non-integers values before integers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this section is the core of the PR. It uses the same mechanism that we have for gates and triggers before but also for probe index.:
The normalize lambda unifies this behavior. Maybe there is a better naming but I could not come with anything, it just follows the convention that Sam had before. |
||
normalize = lambda x: x if isinstance(x, int) else -1 | ||
|
||
# Segment index is determined by the gate_num and trigger_num in that order | ||
def get_segment_tuple(info): | ||
# Create a key from the normalized gate_num and trigger_num | ||
gate_num = normalize(info.get("gate_num")) | ||
trigger_num = normalize(info.get("trigger_num")) | ||
return (gate_num, trigger_num) | ||
|
||
unique_segment_tuples = {get_segment_tuple(info) for info in info_list} | ||
sorted_keys = sorted(unique_segment_tuples) | ||
|
||
# Map each unique key to a corresponding index | ||
segment_tuple_to_segment_index = {key: idx for idx, key in enumerate(sorted_keys)} | ||
|
||
for info in info_list: | ||
info["seg_index"] = order_key.index(make_key(info)) | ||
info["seg_index"] = segment_tuple_to_segment_index[get_segment_tuple(info)] | ||
|
||
|
||
# Probe index calculation | ||
# This ensures that all nidq entries come before any other keys, which corresponds to index 0. | ||
def get_probe_tuple(info): | ||
slot = normalize(info.get("probe_slot")) | ||
port = normalize(info.get("probe_port")) | ||
dock = normalize(info.get("probe_dock")) | ||
return (slot, port, dock) | ||
|
||
info_list_imec = [info for info in info_list if info.get("device") != "nidq"] | ||
unique_probe_tuples = {get_probe_tuple(info) for info in info_list_imec} | ||
sorted_probe_keys = sorted(unique_probe_tuples) | ||
probe_tuple_to_probe_index = {key: idx for idx, key in enumerate(sorted_probe_keys)} | ||
|
||
for info in info_list: | ||
if info.get("device") == "nidq": | ||
info["device_index"] = "" # TODO: Handle multi nidq case, maybe use meta["typeNiEnabled"] | ||
else: | ||
info["device_index"] = probe_tuple_to_probe_index[get_probe_tuple(info)] | ||
|
||
# Define stream base on device [imec|nidq], device_index and stream_kind [ap|lf] for imec | ||
for info in info_list: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This defines the stream here after all the information is available. Previously, it was done inside the |
||
device_kind = info["device_kind"] | ||
device_index = info["device_index"] | ||
stream_kind = f".{info['stream_kind']}" if info["stream_kind"] else "" | ||
stream_name = f"{device_kind}{device_index}{stream_kind}" | ||
|
||
info["stream_name"] = stream_name | ||
|
||
return info_list | ||
|
||
|
||
|
@@ -488,13 +513,15 @@ def extract_stream_info(meta_file, meta): | |
else: | ||
# NIDQ case | ||
has_sync_trace = False | ||
fname = Path(meta_file).stem | ||
|
||
bin_file_path = meta["fileName"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the part that uses the meta["fileName"] (the original one) instead of the file_path in the system (which the user might have changed). |
||
fname = Path(bin_file_path).stem | ||
|
||
run_name, gate_num, trigger_num, device, stream_kind = parse_spikeglx_fname(fname) | ||
|
||
if "imec" in fname.split(".")[-2]: | ||
device = fname.split(".")[-2] | ||
stream_kind = fname.split(".")[-1] | ||
stream_name = device + "." + stream_kind | ||
units = "uV" | ||
# please note the 1e6 in gain for this uV | ||
|
||
|
@@ -534,7 +561,6 @@ def extract_stream_info(meta_file, meta): | |
else: | ||
device = fname.split(".")[-1] | ||
stream_kind = "" | ||
stream_name = device | ||
units = "V" | ||
channel_gains = np.ones(num_chan) | ||
|
||
|
@@ -550,6 +576,10 @@ def extract_stream_info(meta_file, meta): | |
gain_factor = float(meta["niAiRangeMax"]) / 32768 | ||
channel_gains = per_channel_gain * gain_factor | ||
|
||
probe_slot = meta.get("imDatPrb_slot", None) | ||
probe_port = meta.get("imDatPrb_port", None) | ||
probe_dock = meta.get("imDatPrb_dock", None) | ||
|
||
info = {} | ||
info["fname"] = fname | ||
info["meta"] = meta | ||
|
@@ -563,12 +593,15 @@ def extract_stream_info(meta_file, meta): | |
info["trigger_num"] = trigger_num | ||
info["device"] = device | ||
info["stream_kind"] = stream_kind | ||
info["stream_name"] = stream_name | ||
info["device_kind"] = meta.get("typeThis", device.split(".")[0]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Get the device kind directly from the metadata. imec or nidq. All non-production probes should have this in their metadata. For production probes this would rely on file parsing as we were doing before: |
||
info["units"] = units | ||
info["channel_names"] = [txt.split(";")[0] for txt in meta["snsChanMap"]] | ||
info["channel_gains"] = channel_gains | ||
info["channel_offsets"] = np.zeros(info["num_chan"]) | ||
info["has_sync_trace"] = has_sync_trace | ||
info["probe_slot"] = int(probe_slot) if probe_slot else None | ||
info["probe_port"] = int(probe_port) if probe_port else None | ||
info["probe_dock"] = int(probe_dock) if probe_dock else None | ||
|
||
if "nidq" in device: | ||
info["digital_channels"] = [] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed this because it is not clear what other implementations this is referring to, probably the ones before but then I don't think it should be here.