Skip to content

Commit 50abfbc

Browse files
Merge pull request #1125 from samuelgarcia/refactor_spikeglx
improve spikeglx file naming
2 parents f62bd73 + 36b440e commit 50abfbc

File tree

3 files changed

+122
-36
lines changed

3 files changed

+122
-36
lines changed

neo/rawio/spikeglxrawio.py

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
imDatPrb_type=24 (NP 2.0, 4-shank)
4343
4444
Author : Samuel Garcia
45+
Some functions are copied from Graham Findlay
4546
"""
4647

4748
import warnings
@@ -90,6 +91,7 @@ def _parse_header(self):
9091
for info in self.signals_info_list:
9192
# key is (seg_index, stream_name)
9293
key = (info['seg_index'], info['stream_name'])
94+
assert key not in self.signals_info_dict
9395
self.signals_info_dict[key] = info
9496

9597
# create memmap
@@ -166,7 +168,7 @@ def _parse_header(self):
166168
# need probeinterface to be installed
167169
import probeinterface
168170
info = self.signals_info_dict[seg_index, stream_name]
169-
if 'imroTbl' in info['meta'] and info['signal_kind'] == 'ap':
171+
if 'imroTbl' in info['meta'] and info['stream_kind'] == 'ap':
170172
# only for ap channel
171173
probe = probeinterface.read_spikeglx(info['meta_file'])
172174
loc = probe.contact_positions
@@ -233,6 +235,11 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop,
233235
def scan_files(dirname):
234236
"""
235237
Scan for pairs of `.bin` and `.meta` files and return information about it.
238+
239+
After exploring the folder, the segment index (`seg_index`) is construct as follow:
240+
* if only one `gate_num=0` then `trigger_num` = `seg_index`
241+
* if only one `trigger_num=0` then `gate_num` = `seg_index`
242+
* if both are increasing then seg_index increased by gate_num, trigger_num order.
236243
"""
237244
info_list = []
238245

@@ -245,16 +252,99 @@ def scan_files(dirname):
245252
if meta_filename.exists() and bin_filename.exists():
246253
meta = read_meta_file(meta_filename)
247254
info = extract_stream_info(meta_filename, meta)
255+
248256
info['meta_file'] = str(meta_filename)
249257
info['bin_file'] = str(bin_filename)
250258
info_list.append(info)
251259

260+
# Let see if this will be anoying or not.
252261
if bin_filename.stat().st_size != meta['fileSizeBytes']:
253262
warnings.warn('.meta file has faulty value for .bin file size on disc')
254263

264+
# the segment index will depend on both 'gate_num' and 'trigger_num'
265+
# so we order by 'gate_num' then 'trigger_num'
266+
# None is before any int
267+
def make_key(info):
268+
k0 = info['gate_num']
269+
if k0 is None:
270+
k0 = -1
271+
k1 = info['trigger_num']
272+
if k1 is None:
273+
k1 = -1
274+
return (k0, k1)
275+
order_key = list({make_key(info) for info in info_list})
276+
order_key = sorted(order_key)
277+
for info in info_list:
278+
info['seg_index'] = order_key.index(make_key(info))
279+
255280
return info_list
256281

257282

283+
def parse_spikeglx_fname(fname):
284+
"""
285+
Parse recording identifiers from a SpikeGLX style filename.
286+
287+
spikeglx naming follow this rules:
288+
https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/UserManual.md#gates-and-triggers
289+
290+
Example file name structure:
291+
Consider the filenames: `Noise4Sam_g0_t0.nidq.bin` or `Noise4Sam_g0_t0.imec0.lf.bin`
292+
The filenames consist of 3 or 4 parts separated by `.`
293+
1. "Noise4Sam_g0_t0" will be the `name` variable. This choosen by the user at recording time.
294+
2. "_g0_" is the "gate_num"
295+
3. "_t0_" is the "trigger_num"
296+
4. "nidq" or "imec0" will give the `device`
297+
5. "lf" or "ap" will be the `stream_kind`
298+
`stream_name` variable is the concatenation of `device.stream_kind`
299+
300+
This function is copied/modified from Graham Findlay.
301+
302+
Notes:
303+
* Sometimes the original file name is modified by the user and "_gt0_" or "_t0_"
304+
are manually removed. In that case gate_name and trigger_num will be None.
305+
306+
Parameters
307+
---------
308+
fname: str
309+
The filename to parse without the extension, e.g. "my-run-name_g0_t1.imec2.lf"
310+
Returns
311+
-------
312+
run_name: str
313+
The run name, e.g. "my-run-name".
314+
gate_num: int or None
315+
The gate identifier, e.g. 0.
316+
trigger_num: int or None
317+
The trigger identifier, e.g. 1.
318+
device: str
319+
The probe identifier, e.g. "imec2"
320+
stream_kind: str or None
321+
The data type identifier, "lf" or "ap" or None
322+
"""
323+
r = re.findall(r'(\S*)_g(\d*)_t(\d*)\.(\S*).(ap|lf)', fname)
324+
if len(r) == 1:
325+
# standard case with probe
326+
run_name, gate_num, trigger_num, device, stream_kind = r[0]
327+
else:
328+
r = re.findall(r'(\S*)_g(\d*)_t(\d*)\.(\S*)', fname)
329+
if len(r) == 1:
330+
# case for nidaq
331+
run_name, gate_num, trigger_num, device = r[0]
332+
stream_kind = None
333+
else:
334+
# the naming do not correspond lets try something more easy
335+
r = re.findall(r'(\S*)\.(\S*).(ap|lf)', fname)
336+
if len(r) == 1:
337+
run_name, device, stream_kind = r[0]
338+
gate_num, trigger_num = None, None
339+
340+
if gate_num is not None:
341+
gate_num = int(gate_num)
342+
if trigger_num is not None:
343+
trigger_num = int(trigger_num)
344+
345+
return (run_name, gate_num, trigger_num, device, stream_kind)
346+
347+
258348
def read_meta_file(meta_file):
259349
"""parse the meta file"""
260350
with open(meta_file, mode='r') as f:
@@ -281,27 +371,13 @@ def extract_stream_info(meta_file, meta):
281371
"""Extract info from the meta dict"""
282372

283373
num_chan = int(meta['nSavedChans'])
374+
fname = Path(meta_file).stem
375+
run_name, gate_num, trigger_num, device, stream_kind = parse_spikeglx_fname(fname)
376+
device = fname.split('.')[1]
284377

285-
# Example file name structure:
286-
# Consider the filenames: `Noise4Sam_g0_t0.nidq.bin` or `Noise4Sam_g0_t0.imec0.lf.bin`
287-
# The filenames consist of 3 or 4 parts separated by `.`
288-
# 1. "Noise4Sam_g0_t0" will be the `name` variable. This is chosen by the user
289-
# at recording time.
290-
# 2. "_gt0_" will give the `seg_index` (here 0)
291-
# 3. "nidq" or "imec0" will give the `device` variable
292-
# 4. "lf" or "ap" will be the `signal_kind` variable
293-
# `stream_name` variable is the concatenation of `device.signal_kind`
294-
name = Path(meta_file).stem
295-
r = re.findall(r'_g(\d*)_t', name)
296-
if len(r) == 0:
297-
# when manual renaming _g0_ can be removed
298-
seg_index = 0
299-
else:
300-
seg_index = int(r[0][0])
301-
device = name.split('.')[1]
302378
if 'imec' in device:
303-
signal_kind = name.split('.')[2]
304-
stream_name = device + '.' + signal_kind
379+
stream_kind = fname.split('.')[2]
380+
stream_name = device + '.' + stream_kind
305381
units = 'uV'
306382
# please note the 1e6 in gain for this uV
307383

@@ -313,16 +389,16 @@ def extract_stream_info(meta_file, meta):
313389
# https://github.com/billkarsh/SpikeGLX/blob/gh-pages/Support/Metadata_3A.md#imec
314390
# https://github.com/billkarsh/SpikeGLX/blob/gh-pages/Support/Metadata_3B1.md#imec
315391
# https://github.com/billkarsh/SpikeGLX/blob/gh-pages/Support/Metadata_3B2.md#imec
316-
if signal_kind == 'ap':
392+
if stream_kind == 'ap':
317393
index_imroTbl = 3
318-
elif signal_kind == 'lf':
394+
elif stream_kind == 'lf':
319395
index_imroTbl = 4
320396
for c in range(num_chan - 1):
321397
v = meta['imroTbl'][c].split(' ')[index_imroTbl]
322398
per_channel_gain[c] = 1. / float(v)
323399
gain_factor = float(meta['imAiRangeMax']) / 512
324400
channel_gains = gain_factor * per_channel_gain * 1e6
325-
elif meta['imDatPrb_type'] in ('21', '24') and signal_kind == 'ap':
401+
elif meta['imDatPrb_type'] in ('21', '24') and stream_kind == 'ap':
326402
# This work with NP 2.0 case with different metadata versions
327403
# https://github.com/billkarsh/SpikeGLX/blob/gh-pages/Support/Metadata_20.md#channel-entries-by-type
328404
# https://github.com/billkarsh/SpikeGLX/blob/gh-pages/Support/Metadata_20.md#imec
@@ -334,7 +410,7 @@ def extract_stream_info(meta_file, meta):
334410
raise NotImplementedError('This meta file version of spikeglx'
335411
'is not implemented')
336412
else:
337-
signal_kind = ''
413+
stream_kind = ''
338414
stream_name = device
339415
units = 'V'
340416
channel_gains = np.ones(num_chan)
@@ -352,17 +428,18 @@ def extract_stream_info(meta_file, meta):
352428
channel_gains = per_channel_gain * gain_factor
353429

354430
info = {}
355-
info['name'] = name
431+
info['fname'] = fname
356432
info['meta'] = meta
357433
for k in ('niSampRate', 'imSampRate'):
358434
if k in meta:
359435
info['sampling_rate'] = float(meta[k])
360436
info['num_chan'] = num_chan
361437

362438
info['sample_length'] = int(meta['fileSizeBytes']) // 2 // num_chan
363-
info['seg_index'] = seg_index
439+
info['gate_num'] = gate_num
440+
info['trigger_num'] = trigger_num
364441
info['device'] = device
365-
info['signal_kind'] = signal_kind
442+
info['stream_kind'] = stream_kind
366443
info['stream_name'] = stream_name
367444
info['units'] = units
368445
info['channel_names'] = [txt.split(';')[0] for txt in meta['snsChanMap']]

neo/test/iotest/test_spikeglxio.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@
66

77
from neo.io import SpikeGLXIO
88
from neo.test.iotest.common_io_test import BaseTestIO
9+
from neo.test.rawiotest.test_spikeglxrawio import TestSpikeGLXRawIO
910

1011

1112
class TestSpikeGLXIO(BaseTestIO, unittest.TestCase):
1213
ioclass = SpikeGLXIO
13-
entities_to_download = [
14-
'spikeglx'
15-
]
16-
entities_to_test = [
17-
'spikeglx/Noise4Sam_g0',
18-
'spikeglx/TEST_20210920_0_g0'
19-
]
20-
14+
entities_to_download = TestSpikeGLXRawIO.entities_to_download
15+
entities_to_test = TestSpikeGLXRawIO.entities_to_test
2116

2217

2318
if __name__ == "__main__":

neo/test/rawiotest/test_spikeglxrawio.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,21 @@ class TestSpikeGLXRawIO(BaseTestRawIO, unittest.TestCase):
1515
]
1616
entities_to_test = [
1717
'spikeglx/Noise4Sam_g0',
18-
'spikeglx/TEST_20210920_0_g0'
18+
'spikeglx/TEST_20210920_0_g0',
19+
20+
# this is only g0 multi index
21+
'spikeglx/multi_trigger_multi_gate/SpikeGLX/5-19-2022-CI0/5-19-2022-CI0_g0'
22+
# this is only g1 multi index
23+
'spikeglx/multi_trigger_multi_gate/SpikeGLX/5-19-2022-CI0/5-19-2022-CI0_g1'
24+
# this mix both multi gate and multi trigger (and also multi probe)
25+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI0',
26+
27+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI1',
28+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI2',
29+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI3',
30+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI4',
31+
'spikeglx/sample_data_v2/SpikeGLX/5-19-2022-CI5',
32+
1933
]
2034

2135
def test_with_location(self):

0 commit comments

Comments
 (0)