Skip to content

Commit f570790

Browse files
authored
Merge branch 'master' into fix_spikeglx
2 parents 87b277a + 07fa5b3 commit f570790

File tree

9 files changed

+149
-51
lines changed

9 files changed

+149
-51
lines changed

neo/io/__init__.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,24 +280,17 @@
280280

281281
import pathlib
282282
from collections import Counter
283+
import importlib.util
283284

284-
# try to import the neuroshare library.
285+
# check if neuroshare library exists
285286
# if it is present, use the neuroshareapiio to load neuroshare files
286287
# if it is not present, use the neurosharectypesio to load files
287-
try:
288-
import neuroshare as ns
289-
except ModuleNotFoundError as err:
290-
from neo.io.neurosharectypesio import NeurosharectypesIO as NeuroshareIO
291-
292-
# print("\n neuroshare library not found, loading data with ctypes" )
293-
# print("\n to use the API be sure to install the library found at:")
294-
# print("\n www.http://pythonhosted.org/neuroshare/")
295288

296-
else:
289+
neuroshare_spec = importlib.util.find_spec("neuroshare")
290+
if neuroshare_spec is not None:
297291
from neo.io.neuroshareapiio import NeuroshareapiIO as NeuroshareIO
298-
299-
# print("neuroshare library successfully imported")
300-
# print("\n loading with API...")
292+
else:
293+
from neo.io.neurosharectypesio import NeurosharectypesIO as NeuroshareIO
301294

302295
from neo.io.alphaomegaio import AlphaOmegaIO
303296
from neo.io.asciiimageio import AsciiImageIO

neo/io/baseio.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@
1313

1414
from __future__ import annotations
1515
from pathlib import Path
16-
17-
try:
18-
from collections.abc import Sequence
19-
except ImportError:
20-
from collections import Sequence
16+
from collections.abc import Sequence
2117
import logging
2218

2319
from neo import logging_handler

neo/io/nixio.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from uuid import uuid4
2828
import warnings
2929
from packaging.version import Version
30+
import importlib.util
31+
import importlib.metadata
3032

3133
import quantities as pq
3234
import numpy as np
@@ -122,17 +124,15 @@ def dt_from_nix(nixdt, annotype):
122124

123125

124126
def check_nix_version():
125-
try:
126-
import nixio
127-
except ImportError:
128-
raise Exception(
127+
nixio_spec = importlib.util.find_spec("nixio")
128+
if nixio_spec is None:
129+
raise ImportError(
129130
"Failed to import NIX. "
130131
"The NixIO requires the Python package for NIX "
131132
"(nixio on PyPi). Try `pip install nixio`."
132133
)
133134

134-
# nixio version numbers have a 'v' prefix which breaks the comparison
135-
nixverstr = nixio.__version__.lstrip("v")
135+
nixverstr = importlib.metadata.version("nixio")
136136
try:
137137
nixver = Version(nixverstr)
138138
except ValueError:

neo/io/stimfitio.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
08 Feb 2014, C. Schmidt-Hieber, University College London
2727
"""
2828

29+
import importlib.util
30+
2931
import numpy as np
3032
import quantities as pq
3133

@@ -92,7 +94,9 @@ def __init__(self, filename=None):
9294
"""
9395
# We need this module, so try importing now so that it fails on
9496
# instantiation rather than read_block
95-
import stfio # noqa
97+
stfio_spec = importlib.util.find_spec("stfio")
98+
if stfio_spec is None:
99+
raise ImportError("stfio must be installed to use StimfitIO")
96100

97101
BaseIO.__init__(self)
98102

neo/rawio/intanrawio.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""
2-
32
Support for intan tech rhd and rhs files.
43
54
These 2 formats are more or less the same but:

neo/rawio/openephysbinaryrawio.py

Lines changed: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def _parse_header(self):
9696
else:
9797
event_stream_names = []
9898

99+
self._num_of_signal_streams = len(sig_stream_names)
100+
99101
# first loop to reassign stream by "stream_index" instead of "stream_name"
100102
self._sig_streams = {}
101103
self._evt_streams = {}
@@ -121,45 +123,75 @@ def _parse_header(self):
121123
# signals zone
122124
# create signals channel map: several channel per stream
123125
signal_channels = []
126+
124127
for stream_index, stream_name in enumerate(sig_stream_names):
125-
# stream_index is the index in vector sytream names
128+
# stream_index is the index in vector stream names
126129
stream_id = str(stream_index)
127130
buffer_id = stream_id
128131
info = self._sig_streams[0][0][stream_index]
129132
new_channels = []
130133
for chan_info in info["channels"]:
131134
chan_id = chan_info["channel_name"]
135+
136+
units = chan_info["units"]
137+
if units == "":
138+
# When units are not provided they are microvolts for neural channels and volts for ADC channels
139+
# See https://open-ephys.github.io/gui-docs/User-Manual/Recording-data/Binary-format.html#continuous
140+
units = "uV" if "ADC" not in chan_id else "V"
141+
142+
# Special cases for stream
132143
if "SYNC" in chan_id and not self.load_sync_channel:
133144
# the channel is removed from stream but not the buffer
134145
stream_id = ""
135-
if chan_info["units"] == "":
136-
# in some cases for some OE version the unit is "", but the gain is to "uV"
137-
units = "uV"
138-
else:
139-
units = chan_info["units"]
146+
147+
if "ADC" in chan_id:
148+
# These are non-neural channels and their stream should be separated
149+
# We defined their stream_id as the stream_index of neural data plus the number of neural streams
150+
# This is to not break backwards compatbility with the stream_id numbering
151+
stream_id = str(stream_index + len(sig_stream_names))
152+
153+
gain = float(chan_info["bit_volts"])
154+
sampling_rate = float(info["sample_rate"])
155+
offset = 0.0
140156
new_channels.append(
141157
(
142158
chan_info["channel_name"],
143159
chan_id,
144-
float(info["sample_rate"]),
160+
sampling_rate,
145161
info["dtype"],
146162
units,
147-
chan_info["bit_volts"],
148-
0.0,
163+
gain,
164+
offset,
149165
stream_id,
150166
buffer_id,
151167
)
152168
)
153169
signal_channels.extend(new_channels)
170+
154171
signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype)
155172

156173
signal_streams = []
157174
signal_buffers = []
158-
for stream_index, stream_name in enumerate(sig_stream_names):
159-
stream_id = str(stream_index)
160-
buffer_id = str(stream_index)
161-
signal_buffers.append((stream_name, buffer_id))
175+
176+
unique_streams_ids = np.unique(signal_channels["stream_id"])
177+
for stream_id in unique_streams_ids:
178+
# Handle special case of Synch channel having stream_id empty
179+
if stream_id == "":
180+
continue
181+
stream_index = int(stream_id)
182+
# Neural signal
183+
if stream_index < self._num_of_signal_streams:
184+
stream_name = sig_stream_names[stream_index]
185+
buffer_id = stream_id
186+
# We add the buffers here as both the neural and the ADC channels are in the same buffer
187+
signal_buffers.append((stream_name, buffer_id))
188+
else: # This names the ADC streams
189+
neural_stream_index = stream_index - self._num_of_signal_streams
190+
neural_stream_name = sig_stream_names[neural_stream_index]
191+
stream_name = f"{neural_stream_name}_ADC"
192+
buffer_id = str(neural_stream_index)
162193
signal_streams.append((stream_name, stream_id, buffer_id))
194+
163195
signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype)
164196
signal_buffers = np.array(signal_buffers, dtype=_signal_buffer_dtype)
165197

@@ -192,10 +224,49 @@ def _parse_header(self):
192224
"SYNC channel is not present in the recording. " "Set load_sync_channel to False"
193225
)
194226

195-
if has_sync_trace and not self.load_sync_channel:
196-
self._stream_buffer_slice[stream_id] = slice(None, -1)
227+
# Check if ADC and non-ADC channels are contiguous
228+
is_channel_adc = ["ADC" in ch["channel_name"] for ch in info["channels"]]
229+
if any(is_channel_adc):
230+
first_adc_index = is_channel_adc.index(True)
231+
non_adc_channels_after_adc_channels = [
232+
not is_adc for is_adc in is_channel_adc[first_adc_index:]
233+
]
234+
if any(non_adc_channels_after_adc_channels):
235+
raise ValueError(
236+
"Interleaved ADC and non-ADC channels are not supported. "
237+
"ADC channels must be contiguous. Open an issue in python-neo to request this feature."
238+
)
239+
240+
# Find sync channel and verify it's the last channel
241+
sync_index = next(
242+
(index for index, ch in enumerate(info["channels"]) if ch["channel_name"].endswith("_SYNC")),
243+
None,
244+
)
245+
if sync_index is not None and sync_index != num_channels - 1:
246+
raise ValueError(
247+
"SYNC channel must be the last channel in the buffer. Open an issue in python-neo to request this feature."
248+
)
249+
250+
neural_channels = [ch for ch in info["channels"] if "ADC" not in ch["channel_name"]]
251+
adc_channels = [ch for ch in info["channels"] if "ADC" in ch["channel_name"]]
252+
num_neural_channels = len(neural_channels)
253+
num_adc_channels = len(adc_channels)
254+
255+
if num_adc_channels == 0:
256+
if has_sync_trace and not self.load_sync_channel:
257+
self._stream_buffer_slice[stream_id] = slice(None, -1)
258+
else:
259+
self._stream_buffer_slice[stream_id] = None
197260
else:
198-
self._stream_buffer_slice[stream_id] = None
261+
stream_id_neural = stream_id
262+
stream_id_non_neural = str(int(stream_id) + self._num_of_signal_streams)
263+
264+
self._stream_buffer_slice[stream_id_neural] = slice(0, num_neural_channels)
265+
266+
if has_sync_trace and not self.load_sync_channel:
267+
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, -1)
268+
else:
269+
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, None)
199270

200271
# events zone
201272
# channel map: one channel one stream
@@ -375,17 +446,32 @@ def _parse_header(self):
375446
seg_ann = bl_ann["segments"][seg_index]
376447

377448
# array annotations for signal channels
378-
for stream_index, stream_name in enumerate(sig_stream_names):
449+
for stream_index, stream_name in enumerate(self.header["signal_streams"]["name"]):
379450
sig_ann = seg_ann["signals"][stream_index]
380-
info = self._sig_streams[block_index][seg_index][stream_index]
381-
has_sync_trace = self._sig_streams[block_index][seg_index][stream_index]["has_sync_trace"]
451+
if stream_index < self._num_of_signal_streams:
452+
_sig_stream_index = stream_index
453+
is_neural_stream = True
454+
else:
455+
_sig_stream_index = stream_index - self._num_of_signal_streams
456+
is_neural_stream = False
457+
info = self._sig_streams[block_index][seg_index][_sig_stream_index]
458+
has_sync_trace = self._sig_streams[block_index][seg_index][_sig_stream_index]["has_sync_trace"]
459+
460+
for key in ("identifier", "history", "source_processor_index", "recorded_processor_index"):
461+
if key in info["channels"][0]:
462+
values = np.array([chan_info[key] for chan_info in info["channels"]])
382463

383-
for k in ("identifier", "history", "source_processor_index", "recorded_processor_index"):
384-
if k in info["channels"][0]:
385-
values = np.array([chan_info[k] for chan_info in info["channels"]])
386464
if has_sync_trace:
387465
values = values[:-1]
388-
sig_ann["__array_annotations__"][k] = values
466+
467+
neural_channels = [ch for ch in info["channels"] if "ADC" not in ch["channel_name"]]
468+
num_neural_channels = len(neural_channels)
469+
if is_neural_stream:
470+
values = values[:num_neural_channels]
471+
else:
472+
values = values[num_neural_channels:]
473+
474+
sig_ann["__array_annotations__"][key] = values
389475

390476
# array annotations for event channels
391477
# use other possible data in _possible_event_stream_names
@@ -429,7 +515,12 @@ def _channels_to_group_id(self, channel_indexes):
429515
return group_id
430516

431517
def _get_signal_t_start(self, block_index, seg_index, stream_index):
432-
t_start = self._sig_streams[block_index][seg_index][stream_index]["t_start"]
518+
if stream_index < self._num_of_signal_streams:
519+
_sig_stream_index = stream_index
520+
else:
521+
_sig_stream_index = stream_index - self._num_of_signal_streams
522+
523+
t_start = self._sig_streams[block_index][seg_index][_sig_stream_index]["t_start"]
433524
return t_start
434525

435526
def _spike_count(self, block_index, seg_index, unit_index):

neo/test/generate_datasets.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def random_segment():
165165
rec_datetime=random_datetime(),
166166
**random_annotations(4),
167167
)
168+
168169
n_sigs = random.randint(0, 5)
169170
for i in range(n_sigs):
170171
seg.analogsignals.append(random_signal())
@@ -185,6 +186,10 @@ def random_segment():
185186
seg.imagesequences.append(random_image_sequence())
186187
# todo: add some and ROI objects
187188

189+
# if we generate a completely empty segment then test suite fails
190+
if n_sigs + n_irrsigs + n_events + n_epochs + n_spiketrains + n_imgs == 0:
191+
seg = random_segment()
192+
188193
return seg
189194

190195

neo/test/rawiotest/test_openephysbinaryrawio.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class TestOpenEphysBinaryRawIO(BaseTestRawIO, unittest.TestCase):
1616
"openephysbinary/v0.6.x_neuropixels_multiexp_multistream",
1717
"openephysbinary/v0.6.x_neuropixels_with_sync",
1818
"openephysbinary/v0.6.x_neuropixels_missing_folders",
19+
"openephysbinary/neural_and_non_neural_data_mixed",
1920
]
2021

2122
def test_sync(self):
@@ -78,6 +79,15 @@ def test_multiple_ttl_events_parsing(self):
7879
assert np.allclose(ttl_events["durations"][ttl_events["labels"] == "6"], 0.025, atol=0.001)
7980
assert np.allclose(ttl_events["durations"][ttl_events["labels"] == "7"], 0.016666, atol=0.001)
8081

82+
def test_separating_stream_for_non_neural_data(self):
83+
rawio = OpenEphysBinaryRawIO(
84+
self.get_local_path("openephysbinary/neural_and_non_neural_data_mixed"), load_sync_channel=False
85+
)
86+
rawio.parse_header()
87+
# Check that the non-neural data stream is correctly separated
88+
assert len(rawio.header["signal_streams"]["name"]) == 2
89+
assert rawio.header["signal_streams"]["name"].tolist() == ["Rhythm_FPGA-100.0", "Rhythm_FPGA-100.0_ADC"]
90+
8191

8292
if __name__ == "__main__":
8393
unittest.main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ download = "http://pypi.python.org/pypi/neo"
3636

3737

3838
[build-system]
39-
requires = ["setuptools>=62.0"]
39+
requires = ["setuptools>=78.0.2"]
4040
build-backend = "setuptools.build_meta"
4141

4242
[project.optional-dependencies]

0 commit comments

Comments
 (0)