Skip to content

Separate sync as its own stream in OpenEphysBinaryRawIO #1668

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions neo/rawio/openephysbinaryrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ def __init__(self, dirname="", load_sync_channel=False, experiment_names=None):
experiment_names = [experiment_names]
self.experiment_names = experiment_names
self.load_sync_channel = load_sync_channel
if load_sync_channel:
warn(
"The load_sync_channel=True option is deprecated and will be removed in version 0.15. "
"Use load_sync_channel=False instead, which will add sync channels as separate streams.",
DeprecationWarning, stacklevel=2
)
self.folder_structure = None
self._use_direct_evt_timestamps = None

Expand Down Expand Up @@ -123,7 +129,8 @@ def _parse_header(self):
# signals zone
# create signals channel map: several channel per stream
signal_channels = []

sync_stream_id_to_buffer_id = {}
normal_stream_id_to_sync_stream_id = {}
for stream_index, stream_name in enumerate(sig_stream_names):
# stream_index is the index in vector stream names
stream_id = str(stream_index)
Expand All @@ -134,21 +141,28 @@ def _parse_header(self):
chan_id = chan_info["channel_name"]

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

# Special cases for stream
if "SYNC" in chan_id and not self.load_sync_channel:
# the channel is removed from stream but not the buffer
stream_id = ""
# Every stream sync channel is added as its own stream
sync_stream_id = f"{stream_name}SYNC"
sync_stream_id_to_buffer_id[sync_stream_id] = buffer_id

# We save this mapping for the buffer description protocol
normal_stream_id_to_sync_stream_id[stream_id] = sync_stream_id
# We then set the stream_id to the sync stream id
channel_stream_id = sync_stream_id

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

gain = float(chan_info["bit_volts"])
sampling_rate = float(info["sample_rate"])
Expand All @@ -162,7 +176,7 @@ def _parse_header(self):
units,
gain,
offset,
stream_id,
channel_stream_id,
buffer_id,
)
)
Expand All @@ -174,12 +188,21 @@ def _parse_header(self):
signal_buffers = []

unique_streams_ids = np.unique(signal_channels["stream_id"])

# This is getting too complicated, we probably should just have a table which would be easier to read
# And for users to understand
for stream_id in unique_streams_ids:
# Handle special case of Synch channel having stream_id empty
if stream_id == "":

# Handle sync channel on a special way
if "SYNC" in stream_id:
# This is a sync channel and should not be added to the signal streams
buffer_id = sync_stream_id_to_buffer_id[stream_id]
stream_name = stream_id
signal_streams.append((stream_name, stream_id, buffer_id))
continue
stream_index = int(stream_id)

# Neural signal
stream_index = int(stream_id)
if stream_index < self._num_of_signal_streams:
stream_name = sig_stream_names[stream_index]
buffer_id = stream_id
Expand Down Expand Up @@ -254,7 +277,12 @@ def _parse_header(self):

if num_adc_channels == 0:
if has_sync_trace and not self.load_sync_channel:
# Exclude the sync channel from the main stream
self._stream_buffer_slice[stream_id] = slice(None, -1)

# Add a buffer slice for the sync channel
sync_stream_id = normal_stream_id_to_sync_stream_id[stream_id]
self._stream_buffer_slice[sync_stream_id] = slice(-1, None)
else:
self._stream_buffer_slice[stream_id] = None
else:
Expand All @@ -264,7 +292,12 @@ def _parse_header(self):
self._stream_buffer_slice[stream_id_neural] = slice(0, num_neural_channels)

if has_sync_trace and not self.load_sync_channel:
# Exclude the sync channel from the non-neural stream
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, -1)

# Add a buffer slice for the sync channel
sync_stream_id = normal_stream_id_to_sync_stream_id[stream_id]
self._stream_buffer_slice[sync_stream_id] = slice(-1, None)
else:
self._stream_buffer_slice[stream_id_non_neural] = slice(num_neural_channels, None)

Expand Down
22 changes: 22 additions & 0 deletions neo/test/rawiotest/test_openephysbinaryrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,28 @@ def test_sync(self):
block_index=0, seg_index=0, i_start=0, i_stop=100, stream_index=stream_index
)
assert chunk.shape[1] == 384

def test_sync_channel_access(self):
"""Test that sync channels can be accessed as separate streams when load_sync_channel=False."""
rawio = OpenEphysBinaryRawIO(
self.get_local_path("openephysbinary/v0.6.x_neuropixels_with_sync"), load_sync_channel=False
)
rawio.parse_header()

# Find sync channel streams
sync_stream_names = [s_name for s_name in rawio.header["signal_streams"]["name"] if "SYNC" in s_name]
assert len(sync_stream_names) > 0, "No sync channel streams found"

# Get the stream index for the first sync channel
sync_stream_index = list(rawio.header["signal_streams"]["name"]).index(sync_stream_names[0])

# Check that we can access the sync channel data
chunk = rawio.get_analogsignal_chunk(
block_index=0, seg_index=0, i_start=0, i_stop=100, stream_index=sync_stream_index
)

# Sync channel should have only one channel
assert chunk.shape[1] == 1, f"Expected sync channel to have 1 channel, got {chunk.shape[1]}"

def test_no_sync(self):
# requesting sync channel when there is none raises an error
Expand Down
Loading