Skip to content

Commit 6696426

Browse files
authored
Merge pull request #1668 from h-mayorquin/open_ephys_sync_separate
Separate sync as its own stream in `OpenEphysBinaryRawIO`
2 parents 58319c3 + 02bfcd3 commit 6696426

File tree

2 files changed

+63
-8
lines changed

2 files changed

+63
-8
lines changed

neo/rawio/openephysbinaryrawio.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ def __init__(self, dirname="", load_sync_channel=False, experiment_names=None):
7272
experiment_names = [experiment_names]
7373
self.experiment_names = experiment_names
7474
self.load_sync_channel = load_sync_channel
75+
if load_sync_channel:
76+
warn(
77+
"The load_sync_channel=True option is deprecated and will be removed in version 0.15. "
78+
"Use load_sync_channel=False instead, which will add sync channels as separate streams.",
79+
DeprecationWarning, stacklevel=2
80+
)
7581
self.folder_structure = None
7682
self._use_direct_evt_timestamps = None
7783

@@ -123,7 +129,8 @@ def _parse_header(self):
123129
# signals zone
124130
# create signals channel map: several channel per stream
125131
signal_channels = []
126-
132+
sync_stream_id_to_buffer_id = {}
133+
normal_stream_id_to_sync_stream_id = {}
127134
for stream_index, stream_name in enumerate(sig_stream_names):
128135
# stream_index is the index in vector stream names
129136
stream_id = str(stream_index)
@@ -134,21 +141,28 @@ def _parse_header(self):
134141
chan_id = chan_info["channel_name"]
135142

136143
units = chan_info["units"]
144+
channel_stream_id = stream_id
137145
if units == "":
138146
# When units are not provided they are microvolts for neural channels and volts for ADC channels
139147
# See https://open-ephys.github.io/gui-docs/User-Manual/Recording-data/Binary-format.html#continuous
140148
units = "uV" if "ADC" not in chan_id else "V"
141149

142150
# Special cases for stream
143151
if "SYNC" in chan_id and not self.load_sync_channel:
144-
# the channel is removed from stream but not the buffer
145-
stream_id = ""
152+
# Every stream sync channel is added as its own stream
153+
sync_stream_id = f"{stream_name}SYNC"
154+
sync_stream_id_to_buffer_id[sync_stream_id] = buffer_id
155+
156+
# We save this mapping for the buffer description protocol
157+
normal_stream_id_to_sync_stream_id[stream_id] = sync_stream_id
158+
# We then set the stream_id to the sync stream id
159+
channel_stream_id = sync_stream_id
146160

147161
if "ADC" in chan_id:
148162
# These are non-neural channels and their stream should be separated
149163
# We defined their stream_id as the stream_index of neural data plus the number of neural streams
150164
# This is to not break backwards compatbility with the stream_id numbering
151-
stream_id = str(stream_index + len(sig_stream_names))
165+
channel_stream_id = str(stream_index + len(sig_stream_names))
152166

153167
gain = float(chan_info["bit_volts"])
154168
sampling_rate = float(info["sample_rate"])
@@ -162,7 +176,7 @@ def _parse_header(self):
162176
units,
163177
gain,
164178
offset,
165-
stream_id,
179+
channel_stream_id,
166180
buffer_id,
167181
)
168182
)
@@ -174,12 +188,21 @@ def _parse_header(self):
174188
signal_buffers = []
175189

176190
unique_streams_ids = np.unique(signal_channels["stream_id"])
191+
192+
# This is getting too complicated, we probably should just have a table which would be easier to read
193+
# And for users to understand
177194
for stream_id in unique_streams_ids:
178-
# Handle special case of Synch channel having stream_id empty
179-
if stream_id == "":
195+
196+
# Handle sync channel on a special way
197+
if "SYNC" in stream_id:
198+
# This is a sync channel and should not be added to the signal streams
199+
buffer_id = sync_stream_id_to_buffer_id[stream_id]
200+
stream_name = stream_id
201+
signal_streams.append((stream_name, stream_id, buffer_id))
180202
continue
181-
stream_index = int(stream_id)
203+
182204
# Neural signal
205+
stream_index = int(stream_id)
183206
if stream_index < self._num_of_signal_streams:
184207
stream_name = sig_stream_names[stream_index]
185208
buffer_id = stream_id
@@ -254,7 +277,12 @@ def _parse_header(self):
254277

255278
if num_adc_channels == 0:
256279
if has_sync_trace and not self.load_sync_channel:
280+
# Exclude the sync channel from the main stream
257281
self._stream_buffer_slice[stream_id] = slice(None, -1)
282+
283+
# Add a buffer slice for the sync channel
284+
sync_stream_id = normal_stream_id_to_sync_stream_id[stream_id]
285+
self._stream_buffer_slice[sync_stream_id] = slice(-1, None)
258286
else:
259287
self._stream_buffer_slice[stream_id] = None
260288
else:
@@ -264,7 +292,12 @@ def _parse_header(self):
264292
self._stream_buffer_slice[stream_id_neural] = slice(0, num_neural_channels)
265293

266294
if has_sync_trace and not self.load_sync_channel:
295+
# Exclude the sync channel from the non-neural stream
267296
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, -1)
297+
298+
# Add a buffer slice for the sync channel
299+
sync_stream_id = normal_stream_id_to_sync_stream_id[stream_id]
300+
self._stream_buffer_slice[sync_stream_id] = slice(-1, None)
268301
else:
269302
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, None)
270303

neo/test/rawiotest/test_openephysbinaryrawio.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,28 @@ def test_sync(self):
4343
block_index=0, seg_index=0, i_start=0, i_stop=100, stream_index=stream_index
4444
)
4545
assert chunk.shape[1] == 384
46+
47+
def test_sync_channel_access(self):
48+
"""Test that sync channels can be accessed as separate streams when load_sync_channel=False."""
49+
rawio = OpenEphysBinaryRawIO(
50+
self.get_local_path("openephysbinary/v0.6.x_neuropixels_with_sync"), load_sync_channel=False
51+
)
52+
rawio.parse_header()
53+
54+
# Find sync channel streams
55+
sync_stream_names = [s_name for s_name in rawio.header["signal_streams"]["name"] if "SYNC" in s_name]
56+
assert len(sync_stream_names) > 0, "No sync channel streams found"
57+
58+
# Get the stream index for the first sync channel
59+
sync_stream_index = list(rawio.header["signal_streams"]["name"]).index(sync_stream_names[0])
60+
61+
# Check that we can access the sync channel data
62+
chunk = rawio.get_analogsignal_chunk(
63+
block_index=0, seg_index=0, i_start=0, i_stop=100, stream_index=sync_stream_index
64+
)
65+
66+
# Sync channel should have only one channel
67+
assert chunk.shape[1] == 1, f"Expected sync channel to have 1 channel, got {chunk.shape[1]}"
4668

4769
def test_no_sync(self):
4870
# requesting sync channel when there is none raises an error

0 commit comments

Comments
 (0)