Skip to content

Commit ccc01f9

Browse files
authored
Merge branch 'master' into neuronexus
2 parents da56632 + 32ac313 commit ccc01f9

File tree

12 files changed

+244
-72
lines changed

12 files changed

+244
-72
lines changed

codemeta.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"license": "https://spdx.org/licenses/BSD-3-Clause",
55
"codeRepository": "https://github.com/NeuralEnsemble/python-neo",
66
"contIntegration": "https://github.com/NeuralEnsemble/python-neo/actions",
7-
"dateModified": "2024-08-01",
8-
"downloadUrl": "https://files.pythonhosted.org/packages/0f/16/4e22eb38621183d56acde0abbe591f15e79c6332e9ec360fc5db171b39ab/neo-0.13.2.tar.gz",
7+
"dateModified": "2024-08-28",
8+
"downloadUrl": "https://files.pythonhosted.org/packages/08/4b/c863c6bff783e94c92cb814f6ae821b35e6463c5a66e809b6864d0c66b4e/neo-0.13.3.tar.gz",
99
"issueTracker": "https://github.com/NeuralEnsemble/python-neo/issues",
1010
"name": "Neo",
11-
"version": "0.13.2",
11+
"version": "0.13.3",
1212
"identifier": "RRID:SCR_000634",
1313
"description": "Neo is a Python package for working with electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5.\n\nThe goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization.\n\nNeo is used by a number of other software tools, including SpykeViewer (data analysis and visualization), Elephant (data analysis), the G-node suite (databasing), PyNN (simulations), tridesclous_ (spike sorting) and ephyviewer (data visualization).\n\nNeo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities package, which in turn builds on NumPy by adding support for physical dimensions. Thus Neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion.",
1414
"applicationCategory": "neuroscience",
15-
"releaseNotes": "https://neo.readthedocs.io/en/stable/releases/0.13.2.html",
15+
"releaseNotes": "https://neo.readthedocs.io/en/stable/releases/0.13.3.html",
1616
"funding": "https://cordis.europa.eu/project/id/945539",
1717
"developmentStatus": "active",
1818
"referencePublication": "https://doi.org/10.3389/fninf.2014.00010",

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):

0 commit comments

Comments
 (0)