Skip to content

Commit 1310e83

Browse files
authored
Merge pull request #1624 from h-mayorquin/fix_openephys_stream
`OpenEphysBinaryRawIO`: Separate Neural and Non-Neural Data into Distinct Streams
2 parents f05b92f + 6e704ee commit 1310e83

File tree

2 files changed

+126
-24
lines changed

2 files changed

+126
-24
lines changed

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+
228+
229+
# Check if ADC and non-ADC channels are contiguous
230+
is_channel_adc = ["ADC" in ch["channel_name"] for ch in info["channels"]]
231+
if any(is_channel_adc):
232+
first_adc_index = is_channel_adc.index(True)
233+
non_adc_channels_after_adc_channels = [not is_adc for is_adc in is_channel_adc[first_adc_index:]]
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/rawiotest/test_openephysbinaryrawio.py

Lines changed: 11 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,16 @@ 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+
91+
8192

8293
if __name__ == "__main__":
8394
unittest.main()

0 commit comments

Comments
 (0)