Skip to content

Commit afebd5d

Browse files
authored
Merge branch 'master' into fix-spikegadgets
2 parents 808087e + f0f4cc6 commit afebd5d

File tree

10 files changed

+180
-33
lines changed

10 files changed

+180
-33
lines changed

doc/source/releases.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Release notes
66
.. toctree::
77
:maxdepth: 1
88

9+
releases/0.13.3.rst
910
releases/0.13.2.rst
1011
releases/0.13.1.rst
1112
releases/0.13.0.rst

doc/source/releases/0.13.3.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
========================
2+
Neo 0.13.3 release notes
3+
========================
4+
5+
28 August 2024
6+
7+
This release of Neo contains bug fixes, still with a focus on the planned 1.0 release,
8+
and will be the last release not to support NumPy 2.0.
9+
10+
See all `pull requests`_ included in this release and the `list of closed issues`_.
11+
12+
13+
Updated dependencies
14+
--------------------
15+
16+
Neo has a limit of NumPy >= 1.19.5, < 2.0.0 and Quantities >= 14.0.1, < 0.16.0
17+
18+
19+
Bug fixes and improvements in IO modules
20+
----------------------------------------
21+
22+
Bug fixes and/or improvements have been made to :class:`PlexonIO`, :class:`SpikeGLXIO`,
23+
:class:`BiocamIO`.
24+
25+
Acknowledgements
26+
----------------
27+
28+
Thanks to Zach McKenzie, Heberto Mayorquin, and Alessio Buccino for their contributions to this release.
29+
30+
31+
.. _`pull requests`: https://github.com/NeuralEnsemble/python-neo/pulls?q=is%3Apr+is%3Aclosed+milestone%3A0.13.3
32+
33+
.. _`list of closed issues`: https://github.com/NeuralEnsemble/python-neo/issues?q=is%3Aissue+is%3Aclosed+milestone%3A0.13.3

neo/rawio/biocamrawio.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_spike_channel_dtype,
1818
_event_channel_dtype,
1919
)
20+
from neo.core import NeoReadWriteError
2021

2122

2223
class BiocamRawIO(BaseRawIO):
@@ -122,15 +123,53 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, strea
122123
i_start = 0
123124
if i_stop is None:
124125
i_stop = self._num_frames
125-
if channel_indexes is None:
126-
channel_indexes = slice(None)
126+
127+
# read functions are different based on the version of biocam
127128
data = self._read_function(self._filehandle, i_start, i_stop, self._num_channels)
128-
return data[:, channel_indexes]
129129

130+
# older style data returns array of (n_samples, n_channels), should be a view
131+
# but if memory issues come up we should doublecheck out how the file is being stored
132+
if data.ndim > 1:
133+
if channel_indexes is None:
134+
channel_indexes = slice(None)
135+
sig_chunk = data[:, channel_indexes]
136+
137+
# newer style data returns an initial flat array (n_samples * n_channels)
138+
# we iterate through channels rather than slicing
139+
# Due to the fact that Neo and SpikeInterface tend to prefer slices we need to add
140+
# some careful checks around slicing of None in the case we need to iterate through
141+
# channels. First check if None. Then check if slice and only if slice check that it is slice(None)
142+
else:
143+
if channel_indexes is None:
144+
channel_indexes = [ch for ch in range(self._num_channels)]
145+
elif isinstance(channel_indexes, slice):
146+
start = channel_indexes.start or 0
147+
stop = channel_indexes.stop or self._num_channels
148+
step = channel_indexes.step or 1
149+
channel_indexes = [ch for ch in range(start, stop, step)]
150+
151+
sig_chunk = np.zeros((i_stop - i_start, len(channel_indexes)), dtype=data.dtype)
152+
# iterate through channels to prevent loading all channels into memory which can cause
153+
# memory exhaustion. See https://github.com/SpikeInterface/spikeinterface/issues/3303
154+
for index, channel_index in enumerate(channel_indexes):
155+
sig_chunk[:, index] = data[channel_index :: self._num_channels]
156+
157+
return sig_chunk
130158

131-
def open_biocam_file_header(filename):
159+
160+
def open_biocam_file_header(filename) -> dict:
132161
"""Open a Biocam hdf5 file, read and return the recording info, pick the correct method to access raw data,
133-
and return this to the caller."""
162+
and return this to the caller
163+
164+
Parameters
165+
----------
166+
filename: str
167+
The file to be parsed
168+
169+
Returns
170+
-------
171+
dict
172+
The information necessary to read a biocam file (gain, n_samples, n_channels, etc)."""
134173
import h5py
135174

136175
rf = h5py.File(filename, "r")
@@ -154,9 +193,9 @@ def open_biocam_file_header(filename):
154193
elif file_format in (101, 102) or file_format is None:
155194
num_channels = int(rf["3BData/Raw"].shape[0] / num_frames)
156195
else:
157-
raise Exception("Unknown data file format.")
196+
raise NeoReadWriteError("Unknown data file format.")
158197

159-
# # get channels
198+
# get channels
160199
channels = rf["3BRecInfo/3BMeaStreams/Raw/Chs"][:]
161200

162201
# determine correct function to read data
@@ -166,14 +205,14 @@ def open_biocam_file_header(filename):
166205
elif signal_inv == -1:
167206
read_function = readHDF5t_100_i
168207
else:
169-
raise Exception("Unknown signal inversion")
208+
raise NeoReadWriteError("Unknown signal inversion")
170209
else:
171210
if signal_inv == 1:
172211
read_function = readHDF5t_101
173212
elif signal_inv == -1:
174213
read_function = readHDF5t_101_i
175214
else:
176-
raise Exception("Unknown signal inversion")
215+
raise NeoReadWriteError("Unknown signal inversion")
177216

178217
gain = (max_uv - min_uv) / (2**bit_depth)
179218
offset = min_uv
@@ -200,19 +239,22 @@ def open_biocam_file_header(filename):
200239
scale_factor = experiment_settings["ValueConverter"]["ScaleFactor"]
201240
sampling_rate = experiment_settings["TimeConverter"]["FrameRate"]
202241

242+
num_channels = None
203243
for key in rf:
204244
if key[:5] == "Well_":
205245
num_channels = len(rf[key]["StoredChIdxs"])
206246
if len(rf[key]["Raw"]) % num_channels:
207-
raise RuntimeError(f"Length of raw data array is not multiple of channel number in {key}")
247+
raise NeoReadWriteError(f"Length of raw data array is not multiple of channel number in {key}")
208248
num_frames = len(rf[key]["Raw"]) // num_channels
209249
break
210-
try:
250+
251+
if num_channels is not None:
211252
num_channels_x = num_channels_y = int(np.sqrt(num_channels))
212-
except NameError:
213-
raise RuntimeError("No Well found in the file")
253+
else:
254+
raise NeoReadWriteError("No Well found in the file")
255+
214256
if num_channels_x * num_channels_y != num_channels:
215-
raise RuntimeError(f"Cannot determine structure of the MEA plate with {num_channels} channels")
257+
raise NeoReadWriteError(f"Cannot determine structure of the MEA plate with {num_channels} channels")
216258
channels = 1 + np.concatenate(np.transpose(np.meshgrid(range(num_channels_x), range(num_channels_y))))
217259

218260
gain = scale_factor * (max_uv - min_uv) / (max_digital - min_digital)
@@ -231,6 +273,11 @@ def open_biocam_file_header(filename):
231273
)
232274

233275

276+
######################################################################
277+
# Helper functions to obtain the raw data split by Biocam version.
278+
279+
280+
# return the full array for the old datasets
234281
def readHDF5t_100(rf, t0, t1, nch):
235282
return rf["3BData/Raw"][t0:t1]
236283

@@ -239,15 +286,16 @@ def readHDF5t_100_i(rf, t0, t1, nch):
239286
return 4096 - rf["3BData/Raw"][t0:t1]
240287

241288

289+
# return flat array that we will iterate through
242290
def readHDF5t_101(rf, t0, t1, nch):
243-
return rf["3BData/Raw"][nch * t0 : nch * t1].reshape((t1 - t0, nch), order="C")
291+
return rf["3BData/Raw"][nch * t0 : nch * t1]
244292

245293

246294
def readHDF5t_101_i(rf, t0, t1, nch):
247-
return 4096 - rf["3BData/Raw"][nch * t0 : nch * t1].reshape((t1 - t0, nch), order="C")
295+
return 4096 - rf["3BData/Raw"][nch * t0 : nch * t1]
248296

249297

250298
def readHDF5t_brw4(rf, t0, t1, nch):
251299
for key in rf:
252300
if key[:5] == "Well_":
253-
return rf[key]["Raw"][nch * t0 : nch * t1].reshape((t1 - t0, nch), order="C")
301+
return rf[key]["Raw"][nch * t0 : nch * t1]

neo/rawio/blackrockrawio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,8 @@ def __read_nsx_dataheader_variant_c(
986986
index = 0
987987

988988
if offset is None:
989-
offset = self.__nsx_basic_header[nsx_nb]["bytes_in_headers"]
989+
# This is read as an uint32 numpy scalar from the header so we transform it to python int
990+
offset = int(self.__nsx_basic_header[nsx_nb]["bytes_in_headers"])
990991

991992
ptp_dt = [
992993
("reserved", "uint8"),

neo/rawio/medrawio.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,18 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, strea
240240
self.sess.set_channel_active(self._stream_info[stream_index]["raw_chans"])
241241
num_channels = len(self._stream_info[stream_index]["raw_chans"])
242242
self.sess.set_reference_channel(self._stream_info[stream_index]["raw_chans"][0])
243+
244+
# in the case we have a slice or we give an ArrayLike we need to iterate through the channels
245+
# in order to activate them.
243246
else:
244-
if any(channel_indexes < 0):
245-
raise IndexError(f"Can not index negative channels: {channel_indexes}")
247+
if isinstance(channel_indexes, slice):
248+
start = channel_indexes.start or 0
249+
stop = channel_indexes.stop or len(self._stream_info[stream_index]["raw_chans"])
250+
step = channel_indexes.step or 1
251+
channel_indexes = [ch for ch in range(start, stop, step)]
252+
else:
253+
if any(channel_indexes < 0):
254+
raise IndexError(f"Can not index negative channels: {channel_indexes}")
246255
# Set all channels to be inactive, then selectively set some of them to be active
247256
self.sess.set_channel_inactive("all")
248257
for i, channel_idx in enumerate(channel_indexes):

neo/rawio/plexonrawio.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import datetime
2626
from collections import OrderedDict
27+
import re
2728

2829
import numpy as np
2930

@@ -146,7 +147,7 @@ def _parse_header(self):
146147

147148
# Update tqdm with the number of bytes processed in this iteration
148149
if self.progress_bar:
149-
progress_bar.update(length)
150+
progress_bar.update(length) # This was clever, Sam : )
150151

151152
if self.progress_bar:
152153
progress_bar.close()
@@ -231,6 +232,7 @@ def _parse_header(self):
231232
# signals channels
232233
sig_channels = []
233234
all_sig_length = []
235+
source_id = []
234236
if self.progress_bar:
235237
chan_loop = trange(nb_sig_chan, desc="Parsing signal channels", leave=True)
236238
else:
@@ -242,6 +244,7 @@ def _parse_header(self):
242244
length = self._data_blocks[5][chan_id]["size"].sum() // 2
243245
if length == 0:
244246
continue # channel not added
247+
source_id.append(h["SrcId"])
245248
all_sig_length.append(length)
246249
sampling_rate = float(h["ADFreq"])
247250
sig_dtype = "int16"
@@ -255,7 +258,7 @@ def _parse_header(self):
255258
0.5 * (2 ** global_header["BitsPerSpikeSample"]) * h["Gain"] * h["PreampGain"]
256259
)
257260
offset = 0.0
258-
stream_id = "0"
261+
stream_id = "0" # This is overwritten later
259262
sig_channels.append((name, str(chan_id), sampling_rate, sig_dtype, units, gain, offset, stream_id))
260263

261264
sig_channels = np.array(sig_channels, dtype=_signal_channel_dtype)
@@ -264,22 +267,49 @@ def _parse_header(self):
264267
signal_streams = np.array([], dtype=_signal_stream_dtype)
265268

266269
else:
267-
# detect groups (aka streams)
268-
all_sig_length = all_sig_length = np.array(all_sig_length)
269-
groups = set(zip(sig_channels["sampling_rate"], all_sig_length))
270+
# Detect streams
271+
all_sig_length = np.asarray(all_sig_length)
272+
273+
# names are WB{number}, FPX{number}, SPKCX{number}, AI{number}, etc
274+
pattern = r"^\D+" # Match any non-digit character at the beginning of the string
275+
channels_prefixes = np.asarray([re.match(pattern, name).group(0) for name in sig_channels["name"]])
276+
buffer_stream_groups = set(zip(channels_prefixes, sig_channels["sampling_rate"], all_sig_length))
277+
278+
# There are explanations of the streams based on channel names
279+
# provided by a Plexon Engineer, see here:
280+
# https://github.com/NeuralEnsemble/python-neo/pull/1495#issuecomment-2184256894
281+
channel_prefix_to_stream_name = {
282+
"WB": "WB-Wideband",
283+
"FP": "FPl-Low Pass Filtered ",
284+
"SP": "SPKC-High Pass Filtered",
285+
"AI": "AI-Auxiliary Input",
286+
}
287+
288+
# Using a mapping to ensure consistent order of stream_index
289+
channel_prefix_to_stream_id = {
290+
"WB": "0",
291+
"FP": "1",
292+
"SP": "2",
293+
"AI": "3",
294+
}
270295

271296
signal_streams = []
272297
self._signal_length = {}
273298
self._sig_sampling_rate = {}
274-
for stream_index, (sr, length) in enumerate(groups):
275-
stream_id = str(stream_index)
299+
300+
for stream_index, (channel_prefix, sr, length) in enumerate(buffer_stream_groups):
301+
# The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`). This is not common but in that case
302+
# We assign the channel_prefix both as stream_name and stream_id
303+
stream_name = channel_prefix_to_stream_name.get(channel_prefix, channel_prefix)
304+
stream_id = channel_prefix_to_stream_id.get(channel_prefix, channel_prefix)
305+
276306
mask = (sig_channels["sampling_rate"] == sr) & (all_sig_length == length)
277307
sig_channels["stream_id"][mask] = stream_id
278308

279309
self._sig_sampling_rate[stream_index] = sr
280310
self._signal_length[stream_index] = length
281311

282-
signal_streams.append(("Signals " + stream_id, stream_id))
312+
signal_streams.append((stream_name, stream_id))
283313

284314
signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype)
285315

@@ -363,8 +393,8 @@ def _segment_t_start(self, block_index, seg_index):
363393
def _segment_t_stop(self, block_index, seg_index):
364394
t_stop = float(self._last_timestamps) / self._global_ssampling_rate
365395
if hasattr(self, "_signal_length"):
366-
for stream_id in self._signal_length:
367-
t_stop_sig = self._signal_length[stream_id] / self._sig_sampling_rate[stream_id]
396+
for stream_index in self._signal_length.keys():
397+
t_stop_sig = self._signal_length[stream_index] / self._sig_sampling_rate[stream_index]
368398
t_stop = max(t_stop, t_stop_sig)
369399
return t_stop
370400

neo/test/iotest/test_plexonio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class TestPlexonIO(
1818
"plexon/File_plexon_1.plx",
1919
"plexon/File_plexon_2.plx",
2020
"plexon/File_plexon_3.plx",
21+
"plexon/4chDemoPLX.plx"
2122
]
2223

2324

neo/test/rawiotest/rawio_compliance.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def read_analogsignals(reader):
172172
channel_names = signal_channels["name"][mask]
173173
channel_ids = signal_channels["id"][mask]
174174

175-
# acces by channel inde/ids/names should give the same chunk
175+
# acces by channel index/ids/names should give the same chunk
176176
channel_indexes2 = channel_indexes[::2]
177177
channel_names2 = channel_names[::2]
178178
channel_ids2 = channel_ids[::2]
@@ -214,6 +214,29 @@ def read_analogsignals(reader):
214214
)
215215
np.testing.assert_array_equal(raw_chunk0, raw_chunk1)
216216

217+
# test slice(None). This should return the same array as giving
218+
# all channel indexes or using None as an argument in `get_analogsignal_chunk`
219+
# see https://github.com/NeuralEnsemble/python-neo/issues/1533
220+
221+
raw_chunk_slice_none = reader.get_analogsignal_chunk(
222+
block_index=block_index,
223+
seg_index=seg_index,
224+
i_start=i_start,
225+
i_stop=i_stop,
226+
stream_index=stream_index,
227+
channel_indexes=slice(None)
228+
)
229+
raw_chunk_channel_indexes = reader.get_analogsignal_chunk(
230+
block_index=block_index,
231+
seg_index=seg_index,
232+
i_start=i_start,
233+
i_stop=i_stop,
234+
stream_index=stream_index,
235+
channel_indexes=channel_indexes
236+
)
237+
238+
np.testing.assert_array_equal(raw_chunk_slice_none, raw_chunk_channel_indexes)
239+
217240
# test prefer_slice=True/False
218241
if nb_chan >= 3:
219242
for prefer_slice in (True, False):

neo/test/rawiotest/test_plexonrawio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class TestPlexonRawIO(
1515
"plexon/File_plexon_1.plx",
1616
"plexon/File_plexon_2.plx",
1717
"plexon/File_plexon_3.plx",
18+
"plexon/4chDemoPLX.plx"
1819
]
1920

2021

0 commit comments

Comments
 (0)