Skip to content

Commit 69b5403

Browse files
authored
Merge branch 'master' into sgrawio_chan_fix
2 parents 38a012b + 8d4dc9a commit 69b5403

File tree

7 files changed

+304
-194
lines changed

7 files changed

+304
-194
lines changed

neo/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ def list_candidate_ios(file_or_folder, ignore_patterns=["*.ini", "README.txt", "
453453

454454
elif file_or_folder.is_dir():
455455
# scan files in folder to determine io type
456-
filenames = [f for f in file_or_folder.glob("*") if f.is_file()]
456+
filenames = [f for f in file_or_folder.glob("**/*") if f.is_file()]
457457
# keep only relevant filenames
458458
filenames = [f for f in filenames if f.suffix and not any([f.match(p) for p in ignore_patterns])]
459459

neo/rawio/intanrawio.py

Lines changed: 231 additions & 151 deletions
Large diffs are not rendered by default.

neo/rawio/plexon2rawio/plexon2rawio.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def __init__(self, filename, pl2_dll_file_path=None):
107107
# download default PL2 dll once if not yet available
108108
if pl2_dll_file_path is None:
109109
architecture = platform.architecture()[0]
110-
if architecture == "64bit" and platform.system() == "Windows":
110+
if architecture == "64bit" and platform.system() in ["Windows", "Darwin"]:
111111
file_name = "PL2FileReader64.dll"
112112
else: # Apparently wine uses the 32 bit version in linux
113113
file_name = "PL2FileReader.dll"
@@ -120,16 +120,26 @@ def __init__(self, filename, pl2_dll_file_path=None):
120120
dist = urlopen(url=url)
121121

122122
with open(pl2_dll_file_path, "wb") as f:
123-
print(f"Downloading plexon dll to {pl2_dll_file_path}")
123+
warnings.warn(f"dll file does not exist, downloading plexon dll to {pl2_dll_file_path}")
124124
f.write(dist.read())
125125

126126
# Instantiate wrapper for Windows DLL
127127
from neo.rawio.plexon2rawio.pypl2.pypl2lib import PyPL2FileReader
128128

129129
self.pl2reader = PyPL2FileReader(pl2_dll_file_path=pl2_dll_file_path)
130130

131-
# Open the file.
132-
self.pl2reader.pl2_open_file(self.filename)
131+
reading_attempts = 10
132+
for attempt in range(reading_attempts):
133+
self.pl2reader.pl2_open_file(self.filename)
134+
135+
# Verify the file handle is valid.
136+
if self.pl2reader._file_handle.value != 0:
137+
# File handle is valid, exit the loop early
138+
break
139+
else:
140+
if attempt == reading_attempts - 1:
141+
self.pl2reader._print_error()
142+
raise IOError(f"Opening {self.filename} failed after {reading_attempts} attempts.")
133143

134144
def _source_name(self):
135145
return self.filename
@@ -146,7 +156,6 @@ def _parse_header(self):
146156
Source = namedtuple("Source", "id name sampling_rate n_samples")
147157
for c in range(self.pl2reader.pl2_file_info.m_TotalNumberOfAnalogChannels):
148158
achannel_info = self.pl2reader.pl2_get_analog_channel_info(c)
149-
150159
# only consider active channels
151160
if not achannel_info.m_ChannelEnabled:
152161
continue
@@ -262,8 +271,10 @@ def _parse_header(self):
262271
# python is 1..12 https://docs.python.org/3/library/datetime.html#datetime.datetime
263272
# so month needs to be tm_mon+1; also tm_sec could cause problems in the case of leap
264273
# seconds, but this is harder to defend against.
274+
year = tmo.tm_year # This has base 1900 in the c++ struct specification so we need to add 1900
275+
year += 1900
265276
dt = datetime(
266-
year=tmo.tm_year,
277+
year=year,
267278
month=tmo.tm_mon + 1,
268279
day=tmo.tm_mday,
269280
hour=tmo.tm_hour,
@@ -317,7 +328,6 @@ def _parse_header(self):
317328
"m_OneBasedChannelInTrode",
318329
"m_Source",
319330
"m_Channel",
320-
"m_Name",
321331
"m_MaximumNumberOfFragments",
322332
]
323333

neo/rawio/plexon2rawio/pypl2/pypl2lib.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525

2626
from zugbruecke import CtypesSession
2727

28-
ctypes = CtypesSession(log_level=100)
28+
if platform.system() == "Darwin":
29+
ctypes = CtypesSession(log_level=100, arch="win64")
30+
else:
31+
ctypes = CtypesSession(log_level=100)
2932

3033

3134
class tm(ctypes.Structure):
@@ -610,7 +613,7 @@ def pl2_get_spike_channel_info_by_name(self, channel_name):
610613

611614
self.pl2_dll.PL2_GetSpikeChannelInfoByName.argtypes = (
612615
ctypes.c_int,
613-
ctypes.c_char * len(channel_name),
616+
ctypes.POINTER(ctypes.c_char),
614617
ctypes.POINTER(PL2SpikeChannelInfo),
615618
)
616619

@@ -629,11 +632,9 @@ def pl2_get_spike_channel_info_by_name(self, channel_name):
629632
result = self.pl2_dll.PL2_GetSpikeChannelInfoByName(
630633
self._file_handle, channel_name, ctypes.byref(pl2_spike_channel_info)
631634
)
632-
633635
if not result:
634636
self._print_error()
635637
return None
636-
637638
return pl2_spike_channel_info
638639

639640
def pl2_get_spike_channel_info_by_source(self, source_id, one_based_channel_index_in_source):
@@ -744,7 +745,7 @@ def pl2_get_spike_channel_data_by_name(self, channel_name):
744745

745746
self.pl2_dll.PL2_GetSpikeChannelDataByName.argtypes = (
746747
ctypes.c_int,
747-
ctypes.c_char,
748+
ctypes.POINTER(ctypes.c_char),
748749
ctypes.POINTER(ctypes.c_ulonglong),
749750
ctypes.POINTER(ctypes.c_ulonglong),
750751
ctypes.POINTER(ctypes.c_ushort),

neo/rawio/plexonrawio.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ def _parse_header(self):
9595
)
9696

9797
# dsp channels header = spikes and waveforms
98-
nb_unit_chan = global_header["NumDSPChannels"]
98+
nb_unit_chan = int(global_header["NumDSPChannels"])
9999
offset1 = np.dtype(GlobalHeader).itemsize
100100
dspChannelHeaders = np.memmap(
101101
self.filename, dtype=DspChannelHeader, mode="r", offset=offset1, shape=(nb_unit_chan,)
102102
)
103103

104104
# event channel header
105-
nb_event_chan = global_header["NumEventChannels"]
105+
nb_event_chan = int(global_header["NumEventChannels"])
106106
offset2 = offset1 + np.dtype(DspChannelHeader).itemsize * nb_unit_chan
107107
eventHeaders = np.memmap(
108108
self.filename,
@@ -113,7 +113,7 @@ def _parse_header(self):
113113
)
114114

115115
# slow channel header = signal
116-
nb_sig_chan = global_header["NumSlowChannels"]
116+
nb_sig_chan = int(global_header["NumSlowChannels"])
117117
offset3 = offset2 + np.dtype(EventChannelHeader).itemsize * nb_event_chan
118118
slowChannelHeaders = np.memmap(
119119
self.filename, dtype=SlowChannelHeader, mode="r", offset=offset3, shape=(nb_sig_chan,)
@@ -136,7 +136,9 @@ def _parse_header(self):
136136

137137
while pos < data.size:
138138
bl_header = data[pos : pos + 16].view(DataBlockHeader)[0]
139-
length = bl_header["NumberOfWaveforms"] * bl_header["NumberOfWordsInWaveform"] * 2 + 16
139+
number_of_waveforms = int(bl_header["NumberOfWaveforms"])
140+
number_of_words_in_waveform = int(bl_header["NumberOfWordsInWaveform"])
141+
length = (number_of_waveforms * number_of_words_in_waveform * 2) + 16
140142
bl_type = int(bl_header["Type"])
141143
chan_id = int(bl_header["Channel"])
142144
block_pos[bl_type][chan_id].append(pos)

neo/rawio/spikeglxrawio.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -387,61 +387,71 @@ def parse_spikeglx_fname(fname):
387387
Consider the filenames: `Noise4Sam_g0_t0.nidq.bin` or `Noise4Sam_g0_t0.imec0.lf.bin`
388388
The filenames consist of 3 or 4 parts separated by `.`
389389
1. "Noise4Sam_g0_t0" will be the `name` variable. This choosen by the user at recording time.
390-
2. "_g0_" is the "gate_num"
391-
3. "_t0_" is the "trigger_num"
390+
2. "g0" is the "gate_num"
391+
3. "t0" is the "trigger_num"
392392
4. "nidq" or "imec0" will give the `device`
393393
5. "lf" or "ap" will be the `stream_kind`
394-
`stream_name` variable is the concatenation of `device.stream_kind`
394+
`stream_name` variable is the concatenation of `device.stream_kind`
395+
396+
If CatGT is used, then the trigger numbers in the file names ("t0"/"t1"/etc.)
397+
will be renamed to "tcat". In this case, the parsed "trigger_num" will be set to "cat".
395398
396399
This function is copied/modified from Graham Findlay.
397400
398401
Notes:
399-
* Sometimes the original file name is modified by the user and "_gt0_" or "_t0_"
402+
* Sometimes the original file name is modified by the user and "_g0_" or "_t0_"
400403
are manually removed. In that case gate_name and trigger_num will be None.
401404
402405
Parameters
403406
---------
404407
fname: str
405408
The filename to parse without the extension, e.g. "my-run-name_g0_t1.imec2.lf"
409+
406410
Returns
407411
-------
408412
run_name: str
409413
The run name, e.g. "my-run-name".
410414
gate_num: int or None
411415
The gate identifier, e.g. 0.
412-
trigger_num: int or None
413-
The trigger identifier, e.g. 1.
416+
trigger_num: int | str or None
417+
The trigger identifier, e.g. 1. If CatGT is used, then the trigger_num will be set to "cat".
414418
device: str
415419
The probe identifier, e.g. "imec2"
416420
stream_kind: str or None
417421
The data type identifier, "lf" or "ap" or None
418422
"""
419-
r = re.findall(r"(\S*)_g(\d*)_t(\d*)\.(\S*).(ap|lf)", fname)
420-
if len(r) == 1:
423+
re_standard = re.findall(r"(\S*)_g(\d*)_t(\d*)\.(\S*).(ap|lf)", fname)
424+
re_tcat = re.findall(r"(\S*)_g(\d*)_tcat.(\S*).(ap|lf)", fname)
425+
re_nidq = re.findall(r"(\S*)_g(\d*)_t(\d*)\.(\S*)", fname)
426+
if len(re_standard) == 1:
421427
# standard case with probe
422-
run_name, gate_num, trigger_num, device, stream_kind = r[0]
428+
run_name, gate_num, trigger_num, device, stream_kind = re_standard[0]
429+
elif len(re_tcat) == 1:
430+
# tcat case
431+
run_name, gate_num, device, stream_kind = re_tcat[0]
432+
trigger_num = "cat"
433+
elif len(re_nidq) == 1:
434+
# case for nidaq
435+
run_name, gate_num, trigger_num, device = re_nidq[0]
436+
stream_kind = None
423437
else:
424-
r = re.findall(r"(\S*)_g(\d*)_t(\d*)\.(\S*)", fname)
425-
if len(r) == 1:
426-
# case for nidaq
427-
run_name, gate_num, trigger_num, device = r[0]
428-
stream_kind = None
438+
# the naming do not correspond lets try something more easy
439+
# example: sglx_xxx.imec0.ap
440+
re_else = re.findall(r"(\S*)\.(\S*).(ap|lf)", fname)
441+
re_else_nidq = re.findall(r"(\S*)\.(\S*)", fname)
442+
if len(re_else) == 1:
443+
run_name, device, stream_kind = re_else_nidq[0]
444+
gate_num, trigger_num = None, None
445+
elif len(re_else_nidq) == 1:
446+
# easy case for nidaq, example: sglx_xxx.nidq
447+
run_name, device = re_else_nidq[0]
448+
gate_num, trigger_num, stream_kind = None, None, None
429449
else:
430-
# the naming do not correspond lets try something more easy
431-
# example: sglx_xxx.imec0.ap
432-
r = re.findall(r"(\S*)\.(\S*).(ap|lf)", fname)
433-
if len(r) == 1:
434-
run_name, device, stream_kind = r[0]
435-
gate_num, trigger_num = None, None
436-
else:
437-
# easy case for nidaq, example: sglx_xxx.nidq
438-
r = re.findall(r"(\S*)\.(\S*)", fname)
439-
run_name, device = r[0]
440-
gate_num, trigger_num, stream_kind = None, None, None
450+
raise ValueError(f"Cannot parse filename {fname}")
441451

442452
if gate_num is not None:
443453
gate_num = int(gate_num)
444-
if trigger_num is not None:
454+
if trigger_num is not None and trigger_num != "cat":
445455
trigger_num = int(trigger_num)
446456

447457
return (run_name, gate_num, trigger_num, device, stream_kind)

neo/test/rawiotest/test_spikeglxrawio.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class TestSpikeGLXRawIO(BaseTestRawIO, unittest.TestCase):
3232
"spikeglx/NP2_subset_with_sync",
3333
# NP-ultra
3434
"spikeglx/np_ultra_stub",
35+
# CatGT
36+
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-A",
37+
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-B",
38+
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-C",
39+
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-D",
40+
"spikeglx/multi_trigger_multi_gate/CatGT/CatGT-E",
41+
"spikeglx/multi_trigger_multi_gate/CatGT/Supercat-A",
3542
]
3643

3744
def test_with_location(self):

0 commit comments

Comments
 (0)