diff --git a/neo/io/__init__.py b/neo/io/__init__.py index aba95dff8..a5a94806d 100644 --- a/neo/io/__init__.py +++ b/neo/io/__init__.py @@ -43,6 +43,7 @@ * :attr:`NixIO` * :attr:`NSDFIO` * :attr:`OpenEphysIO` +* :attr:`OpenEphysBinaryIO` * :attr:`PhyIO` * :attr:`PickleIO` * :attr:`PlexonIO` @@ -177,6 +178,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.io.OpenEphysBinaryIO + + .. autoattribute:: extensions + .. autoclass:: neo.io.PhyIO .. autoattribute:: extensions @@ -276,6 +281,7 @@ from neo.io.nixio_fr import NixIO as NixIOFr from neo.io.nsdfio import NSDFIO from neo.io.openephysio import OpenEphysIO +from neo.io.openephysbinaryio import OpenEphysBinaryIO from neo.io.phyio import PhyIO from neo.io.pickleio import PickleIO from neo.io.plexonio import PlexonIO @@ -321,6 +327,7 @@ NeuroshareIO, NSDFIO, OpenEphysIO, + OpenEphysBinaryIO, PhyIO, PickleIO, PlexonIO, diff --git a/neo/io/openephysbinaryio.py b/neo/io/openephysbinaryio.py new file mode 100644 index 000000000..aad629fa9 --- /dev/null +++ b/neo/io/openephysbinaryio.py @@ -0,0 +1,11 @@ +from neo.io.basefromrawio import BaseFromRaw +from neo.rawio.openephysbinaryrawio import OpenEphysBinaryRawIO + + +class OpenEphysBinaryIO(OpenEphysBinaryRawIO, BaseFromRaw): + _prefered_signal_group_mode = 'group-by-same-units' + mode = 'dir' + + def __init__(self, dirname): + OpenEphysBinaryRawIO.__init__(self, dirname=dirname) + BaseFromRaw.__init__(self, dirname) diff --git a/neo/io/proxyobjects.py b/neo/io/proxyobjects.py index b17a7c5d4..c897b7708 100644 --- a/neo/io/proxyobjects.py +++ b/neo/io/proxyobjects.py @@ -33,10 +33,19 @@ def __init__(self, array_annotations=None, **annotations): # used to be str so raw bytes annotations['file_origin'] = str(self._rawio.source_name()) - # this mock the array annotaions to avoid inherits DataObject + if array_annotations is None: + array_annotations = {} + for k, v in array_annotations.items(): + array_annotations[k] = np.asarray(v) + + # clean array annotations that are not 1D + # TODO remove this once multi-dimensional array_annotations are possible + array_annotations = {k: v for k, v in array_annotations.items() + if v.ndim == 1} + + # this mock the array annotations to avoid inherits DataObject self.array_annotations = ArrayDict(self.shape[-1]) - if array_annotations is not None: - self.array_annotations.update(array_annotations) + self.array_annotations.update(array_annotations) BaseNeo.__init__(self, **annotations) diff --git a/neo/rawio/__init__.py b/neo/rawio/__init__.py index 3b47ce010..8ea62692c 100644 --- a/neo/rawio/__init__.py +++ b/neo/rawio/__init__.py @@ -25,6 +25,7 @@ * :attr:`NeuroScopeRawIO` * :attr:`NIXRawIO` * :attr:`OpenEphysRawIO` +* :attr:`OpenEphysBinaryRawIO` * :attr:'PhyRawIO' * :attr:`PlexonRawIO` * :attr:`RawBinarySignalRawIO` @@ -88,6 +89,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.rawio.OpenEphysBinaryRawIO + + .. autoattribute:: extensions + .. autoclass:: neo.rawio.PhyRawIO .. autoattribute:: extensions @@ -141,6 +146,7 @@ from neo.rawio.neuroscoperawio import NeuroScopeRawIO from neo.rawio.nixrawio import NIXRawIO from neo.rawio.openephysrawio import OpenEphysRawIO +from neo.rawio.openephysbinaryrawio import OpenEphysBinaryRawIO from neo.rawio.phyrawio import PhyRawIO from neo.rawio.plexonrawio import PlexonRawIO from neo.rawio.rawbinarysignalrawio import RawBinarySignalRawIO @@ -165,6 +171,7 @@ NeuroScopeRawIO, NIXRawIO, OpenEphysRawIO, + OpenEphysBinaryRawIO, PhyRawIO, PlexonRawIO, RawBinarySignalRawIO, diff --git a/neo/rawio/openephysbinaryrawio.py b/neo/rawio/openephysbinaryrawio.py new file mode 100644 index 000000000..1e3ac4c29 --- /dev/null +++ b/neo/rawio/openephysbinaryrawio.py @@ -0,0 +1,397 @@ +""" +This module implements the "new" binary OpenEphys format. +In this format channels are interleaved in one file. + + +See +https://open-ephys.github.io/gui-docs/User-Manual/Recording-data/Binary-format.html + +Author: Julia Sprenger and Samuel Garcia +""" + + +import os +import re +import json + +from pathlib import Path + +import numpy as np + +from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, + _spike_channel_dtype, _event_channel_dtype) + + +class OpenEphysBinaryRawIO(BaseRawIO): + """ + Handle several Blocks and several Segments. + + + # Correspondencies + Neo OpenEphys + block[n-1] experiment[n] New device start/stop + segment[s-1] recording[s] New recording start/stop + + This IO handles several signal streams. + Special event (npy) data are represented as array_annotations. + The current implementation does not handle spiking data, this will be added upon user request + + """ + extensions = [] + rawmode = 'one-dir' + + def __init__(self, dirname=''): + BaseRawIO.__init__(self) + self.dirname = dirname + + def _source_name(self): + return self.dirname + + def _parse_header(self): + all_streams, nb_block, nb_segment_per_block = explore_folder(self.dirname) + + sig_stream_names = sorted(list(all_streams[0][0]['continuous'].keys())) + event_stream_names = sorted(list(all_streams[0][0]['events'].keys())) + + # first loop to reasign stream by "stream_index" instead of "stream_name" + self._sig_streams = {} + self._evt_streams = {} + for block_index in range(nb_block): + self._sig_streams[block_index] = {} + self._evt_streams[block_index] = {} + for seg_index in range(nb_segment_per_block[block_index]): + self._sig_streams[block_index][seg_index] = {} + self._evt_streams[block_index][seg_index] = {} + for stream_index, stream_name in enumerate(sig_stream_names): + d = all_streams[block_index][seg_index]['continuous'][stream_name] + d['stream_name'] = stream_name + self._sig_streams[block_index][seg_index][stream_index] = d + for i, stream_name in enumerate(event_stream_names): + d = all_streams[block_index][seg_index]['events'][stream_name] + d['stream_name'] = stream_name + self._evt_streams[block_index][seg_index][i] = d + + # signals zone + # create signals channel map: several channel per stream + signal_channels = [] + for stream_index, stream_name in enumerate(sig_stream_names): + # stream_index is the index in vector sytream names + stream_id = str(stream_index) + d = self._sig_streams[0][0][stream_index] + new_channels = [] + for chan_info in d['channels']: + chan_id = chan_info['channel_name'] + new_channels.append((chan_info['channel_name'], + chan_id, float(d['sample_rate']), d['dtype'], chan_info['units'], + chan_info['bit_volts'], 0., stream_id)) + signal_channels.extend(new_channels) + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) + + signal_streams = [] + for stream_index, stream_name in enumerate(sig_stream_names): + stream_id = str(stream_index) + signal_streams.append((stream_name, stream_id)) + signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype) + + # create memmap for signals + for block_index in range(nb_block): + for seg_index in range(nb_segment_per_block[block_index]): + for stream_index, d in self._sig_streams[block_index][seg_index].items(): + num_channels = len(d['channels']) + print(d['raw_filename']) + memmap_sigs = np.memmap(d['raw_filename'], d['dtype'], + order='C', mode='r').reshape(-1, num_channels) + d['memmap'] = memmap_sigs + + # events zone + # channel map: one channel one stream + event_channels = [] + for stream_ind, stream_name in enumerate(event_stream_names): + d = self._evt_streams[0][0][stream_ind] + event_channels.append((d['channel_name'], stream_ind, 'event')) + event_channels = np.array(event_channels, dtype=_event_channel_dtype) + + # create memmap + for stream_ind, stream_name in enumerate(event_stream_names): + # inject memmap loaded into main dict structure + d = self._evt_streams[0][0][stream_ind] + + for name in _possible_event_stream_names: + if name + '_npy' in d: + data = np.load(d[name + '_npy'], mmap_mode='r') + d[name] = data + + # check that events have timestamps + assert 'timestamps' in d + + # for event the neo "label" will change depending the nature + # of event (ttl, text, binary) + # and this is transform into unicode + # all theses data are put in event array annotations + if 'text' in d: + # text case + d['labels'] = d['text'].astype('U') + elif 'metadata' in d: + # binary case + d['labels'] = d['channels'].astype('U') + elif 'channels' in d: + # ttl case use channels + d['labels'] = d['channels'].astype('U') + else: + raise ValueError(f'There is no possible labels for this event: {stream_name}') + + # no spike read yet + # can be implemented on user demand + spike_channels = np.array([], dtype=_spike_channel_dtype) + + # loop for t_start/t_stop on segment browse all object + self._t_start_segments = {} + self._t_stop_segments = {} + for block_index in range(nb_block): + self._t_start_segments[block_index] = {} + self._t_stop_segments[block_index] = {} + for seg_index in range(nb_segment_per_block[block_index]): + global_t_start = None + global_t_stop = None + + # loop over signals + for stream_index, d in self._sig_streams[block_index][seg_index].items(): + t_start = d['t_start'] + dur = d['memmap'].shape[0] / float(d['sample_rate']) + t_stop = t_start + dur + if global_t_start is None or global_t_start > t_start: + global_t_start = t_start + if global_t_stop is None or global_t_stop < t_stop: + global_t_stop = t_stop + + # loop over events + for stream_index, stream_name in enumerate(event_stream_names): + d = self._evt_streams[0][0][stream_index] + if d['timestamps'].size == 0: + continue + t_start = d['timestamps'][0] / d['sample_rate'] + t_stop = d['timestamps'][-1] / d['sample_rate'] + if global_t_start is None or global_t_start > t_start: + global_t_start = t_start + if global_t_stop is None or global_t_stop < t_stop: + global_t_stop = t_stop + + self._t_start_segments[block_index][seg_index] = global_t_start + self._t_stop_segments[block_index][seg_index] = global_t_stop + + # main header + self.header = {} + self.header['nb_block'] = nb_block + self.header['nb_segment'] = nb_segment_per_block + self.header['signal_streams'] = signal_streams + self.header['signal_channels'] = signal_channels + self.header['spike_channels'] = spike_channels + self.header['event_channels'] = event_channels + + # Annotate some objects from continuous files + self._generate_minimal_annotations() + for block_index in range(nb_block): + bl_ann = self.raw_annotations['blocks'][block_index] + for seg_index in range(nb_segment_per_block[block_index]): + seg_ann = bl_ann['segments'][seg_index] + + # array annotations for signal channels + for stream_index, stream_name in enumerate(sig_stream_names): + sig_ann = seg_ann['signals'][stream_index] + d = self._sig_streams[0][0][stream_index] + for k in ('identifier', 'history', 'source_processor_index', + 'recorded_processor_index'): + if k in d['channels'][0]: + values = np.array([chan_info[k] for chan_info in d['channels']]) + sig_ann['__array_annotations__'][k] = values + + # array annotations for event channels + # use other possible data in _possible_event_stream_names + for stream_index, stream_name in enumerate(event_stream_names): + ev_ann = seg_ann['events'][stream_index] + d = self._evt_streams[0][0][stream_index] + for k in _possible_event_stream_names: + if k in ('timestamps', ): + continue + if k in d: + # split custom dtypes into separate annotations + if d[k].dtype.names: + for name in d[k].dtype.names: + ev_ann['__array_annotations__'][name] = d[k][name].flatten() + else: + ev_ann['__array_annotations__'][k] = d[k] + + def _segment_t_start(self, block_index, seg_index): + return self._t_start_segments[block_index][seg_index] + + def _segment_t_stop(self, block_index, seg_index): + return self._t_stop_segments[block_index][seg_index] + + def _channels_to_group_id(self, channel_indexes): + if channel_indexes is None: + channel_indexes = slice(None) + channels = self.header['signal_channels'] + group_ids = channels[channel_indexes]['group_id'] + assert np.unique(group_ids).size == 1 + group_id = group_ids[0] + return group_id + + def _get_signal_size(self, block_index, seg_index, stream_index): + sigs = self._sig_streams[block_index][seg_index][stream_index]['memmap'] + return sigs.shape[0] + + def _get_signal_t_start(self, block_index, seg_index, stream_index): + t_start = self._sig_streams[block_index][seg_index][stream_index]['t_start'] + return t_start + + def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, + stream_index, channel_indexes): + sigs = self._sig_streams[block_index][seg_index][stream_index]['memmap'] + sigs = sigs[i_start:i_stop, :] + if channel_indexes is not None: + sigs = sigs[:, channel_indexes] + return sigs + + def _spike_count(self, block_index, seg_index, unit_index): + pass + + def _get_spike_timestamps(self, block_index, seg_index, unit_index, t_start, t_stop): + pass + + def _rescale_spike_timestamp(self, spike_timestamps, dtype): + pass + + def _get_spike_raw_waveforms(self, block_index, seg_index, unit_index, t_start, t_stop): + pass + + def _event_count(self, block_index, seg_index, event_channel_index): + d = self._evt_streams[0][0][event_channel_index] + return d['timestamps'].size + + def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop): + d = self._evt_streams[0][0][event_channel_index] + timestamps = d['timestamps'] + durations = None + labels = d['labels'] + + # slice it if needed + if t_start is not None: + ind_start = int(t_start * d['sample_rate']) + mask = timestamps >= ind_start + timestamps = timestamps[mask] + labels = labels[mask] + if t_stop is not None: + ind_stop = int(t_stop * d['sample_rate']) + mask = timestamps < ind_stop + timestamps = timestamps[mask] + labels = labels[mask] + return timestamps, durations, labels + + def _rescale_event_timestamp(self, event_timestamps, dtype, event_channel_index): + d = self._evt_streams[0][0][event_channel_index] + event_times = event_timestamps.astype(dtype) / float(d['sample_rate']) + return event_times + + def _rescale_epoch_duration(self, raw_duration, dtype): + pass + + +_possible_event_stream_names = ('timestamps', 'channels', 'text', + 'full_word', 'channel_states', 'data_array', 'metadata') + + +def explore_folder(dirname): + """ + Exploring the OpenEphys folder structure and structure.oebin + + Returns nested dictionary structure: + [block_index][seg_index][stream_type][stream_information] + where + - node_name is the open ephys node id + - block_index is the neo Block index + - segment_index is the neo Segment index + - stream_type can be 'continuous'/'events'/'spikes' + - stream_information is a dictionionary containing e.g. the sampling rate + + Parmeters + --------- + dirname (str): Root folder of the dataset + + Returns + ------- + nested dictionaries containing structure and stream information + """ + nb_block = 0 + nb_segment_per_block = [] + # nested dictionary: block_index > seg_index > data_type > stream_name + all_streams = {} + for root, dirs, files in os.walk(dirname): + for file in files: + if not file == 'structure.oebin': + continue + root = Path(root) + + node_name = root.parents[1].stem + if not node_name.startswith('Record'): + # before version 5.x.x there was not multi Node recording + # so no node_name + node_name = '' + + block_index = int(root.parents[0].stem.replace('experiment', '')) - 1 + if block_index not in all_streams: + all_streams[block_index] = {} + if block_index >= nb_block: + nb_block = block_index + 1 + nb_segment_per_block.append(0) + + seg_index = int(root.stem.replace('recording', '')) - 1 + if seg_index not in all_streams[block_index]: + all_streams[block_index][seg_index] = { + 'continuous': {}, + 'events': {}, + 'spikes': {}, + } + if seg_index >= nb_segment_per_block[block_index]: + nb_segment_per_block[block_index] = seg_index + 1 + + # metadata + with open(root / 'structure.oebin', encoding='utf8', mode='r') as f: + structure = json.load(f) + + if (root / 'continuous').exists() and len(structure['continuous']) > 0: + for d in structure['continuous']: + # when multi Record Node the stream name also contains + # the node name to make it unique + stream_name = node_name + '#' + d['folder_name'] + + raw_filename = root / 'continuous' / d['folder_name'] / 'continuous.dat' + + timestamp_file = root / 'continuous' / d['folder_name'] / 'timestamps.npy' + timestamps = np.load(str(timestamp_file), mmap_mode='r') + timestamp0 = timestamps[0] + t_start = timestamp0 / d['sample_rate'] + + # TODO for later : gap checking + signal_stream = d.copy() + signal_stream['raw_filename'] = str(raw_filename) + signal_stream['dtype'] = 'int16' + signal_stream['timestamp0'] = timestamp0 + signal_stream['t_start'] = t_start + + all_streams[block_index][seg_index]['continuous'][stream_name] = signal_stream + + if (root / 'events').exists() and len(structure['events']) > 0: + for d in structure['events']: + stream_name = node_name + '#' + d['folder_name'] + + event_stream = d.copy() + for name in _possible_event_stream_names: + npz_filename = root / 'events' / d['folder_name'] / f'{name}.npy' + if npz_filename.is_file(): + event_stream[f'{name}_npy'] = str(npz_filename) + + all_streams[block_index][seg_index]['events'][stream_name] = event_stream + + # TODO for later: check stream / channel consistency across segment + + return all_streams, nb_block, nb_segment_per_block diff --git a/neo/rawio/openephysrawio.py b/neo/rawio/openephysrawio.py index 000409a94..dd529b047 100644 --- a/neo/rawio/openephysrawio.py +++ b/neo/rawio/openephysrawio.py @@ -1,5 +1,9 @@ """ -This module implement OpenEphys format. +This module implement the "old" OpenEphys format. +In this format channels are split into several files + +https://open-ephys.github.io/gui-docs/User-Manual/Recording-data/Open-Ephys-format.html + Author: Samuel Garcia """ diff --git a/neo/test/iotest/test_openephysbinaryio.py b/neo/test/iotest/test_openephysbinaryio.py new file mode 100644 index 000000000..a0284d588 --- /dev/null +++ b/neo/test/iotest/test_openephysbinaryio.py @@ -0,0 +1,22 @@ +""" + +""" + +import unittest + +import quantities as pq + +from neo.io import OpenEphysBinaryIO +from neo.test.iotest.common_io_test import BaseTestIO +from neo.test.rawiotest.test_openephysbinaryrawio import TestOpenEphysBinaryRawIO + + +class TestOpenEphysBinaryIO(BaseTestIO, unittest.TestCase): + ioclass = OpenEphysBinaryIO + files_to_test = TestOpenEphysBinaryRawIO.entities_to_test + + files_to_download = TestOpenEphysBinaryRawIO.files_to_download + + +if __name__ == "__main__": + unittest.main() diff --git a/neo/test/rawiotest/test_openephysbinaryrawio.py b/neo/test/rawiotest/test_openephysbinaryrawio.py new file mode 100644 index 000000000..839d76d42 --- /dev/null +++ b/neo/test/rawiotest/test_openephysbinaryrawio.py @@ -0,0 +1,109 @@ +import unittest + +from neo.rawio.openephysbinaryrawio import OpenEphysBinaryRawIO +from neo.test.rawiotest.common_rawio_test import BaseTestRawIO + + +class TestOpenEphysBinaryRawIO(BaseTestRawIO, unittest.TestCase): + rawioclass = OpenEphysBinaryRawIO + entities_to_test = [ + 'v0.5.3_two_neuropixels_stream', + 'v0.4.4.1_with_video_tracking', + 'v0.5.x_two_nodes', + ] + + files_to_download = [ + "v0.5.3_two_neuropixels_stream/Record_Node_107/settings.xml", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/sync_messages.txt", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/structure.oebin", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/events/Neuropix-PXI-116.0/TTL_1/full_words.npy", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/events/Neuropix-PXI-116.0/TTL_1/channel_states.npy", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/events/Neuropix-PXI-116.0/TTL_1/timestamps.npy", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/events/Neuropix-PXI-116.0/TTL_1/channels.npy", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/continuous/Neuropix-PXI-116.1/continuous.dat", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/continuous/Neuropix-PXI-116.1/timestamps.npy", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/continuous/Neuropix-PXI-116.0/continuous.dat", + "v0.5.3_two_neuropixels_stream/Record_Node_107/experiment1/recording1/continuous/Neuropix-PXI-116.0/timestamps.npy", + + "v0.4.4.1_with_video_tracking/settings.xml", + "v0.4.4.1_with_video_tracking/experiment1/recording1/sync_messages.txt", + "v0.4.4.1_with_video_tracking/experiment1/recording1/structure.oebin", + "v0.4.4.1_with_video_tracking/experiment1/recording1/spikes/Spike_Viewer-125_124.0/spike_group_1/spike_electrode_indices.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/spikes/Spike_Viewer-125_124.0/spike_group_1/spike_waveforms.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/spikes/Spike_Viewer-125_124.0/spike_group_1/spike_times.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/spikes/Spike_Viewer-125_124.0/spike_group_1/spike_clusters.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Sync_Port-129.0/TTL_1/full_words.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Sync_Port-129.0/TTL_1/channel_states.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Sync_Port-129.0/TTL_1/timestamps.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Sync_Port-129.0/TTL_1/channels.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_2/metadata.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_2/timestamps.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_2/data_array.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_2/channels.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_1/metadata.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_1/timestamps.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_1/data_array.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Tracking_Port-127.0/BINARY_group_1/channels.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Rhythm_FPGA-100.0/TTL_1/full_words.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Rhythm_FPGA-100.0/TTL_1/channel_states.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Rhythm_FPGA-100.0/TTL_1/timestamps.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/events/Rhythm_FPGA-100.0/TTL_1/channels.npy", + "v0.4.4.1_with_video_tracking/experiment1/recording1/continuous/Rhythm_FPGA-100.0/continuous.dat", + "v0.4.4.1_with_video_tracking/experiment1/recording1/continuous/Rhythm_FPGA-100.0/timestamps.npy", + + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/structure.oebin", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording1/continuous/File_Reader-100.0/timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/structure.oebin", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording3/continuous/File_Reader-100.0/timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/structure.oebin", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode105/experiment1/recording2/continuous/File_Reader-100.0/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/structure.oebin", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording1/continuous/File_Reader-100.0/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/structure.oebin", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording3/continuous/File_Reader-100.0/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/sync_messages.txt", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/structure.oebin", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/text.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/events/Message_Center-904.0/TEXT_group_1/channels.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/continuous/File_Reader-100.0/synchronized_timestamps.npy", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/continuous/File_Reader-100.0/continuous.dat", + "v0.5.x_two_nodes/RecordNode103/experiment1/recording2/continuous/File_Reader-100.0/timestamps.npy", + ] + + +if __name__ == "__main__": + unittest.main()