diff --git a/neo/io/__init__.py b/neo/io/__init__.py index cbb2f1ed5..e3280da6f 100644 --- a/neo/io/__init__.py +++ b/neo/io/__init__.py @@ -39,6 +39,7 @@ * :attr:`KlustaKwikIO` * :attr:`KwikIO` * :attr:`MaxwellIO` +* :attr:`MedIO` * :attr:`MicromedIO` * :attr:`NeoMatlabIO` * :attr:`NestIO` @@ -165,6 +166,10 @@ .. autoclass:: neo.io.MaxwellIO .. autoattribute:: extensions + +.. autoclass:: neo.io.MedIO + + .. autoattribute:: extensions .. autoclass:: neo.io.MicromedIO @@ -313,6 +318,7 @@ from neo.io.kwikio import KwikIO from neo.io.mearecio import MEArecIO from neo.io.maxwellio import MaxwellIO +from neo.io.medio import MedIO from neo.io.micromedio import MicromedIO from neo.io.neomatlabio import NeoMatlabIO from neo.io.nestio import NestIO @@ -366,6 +372,7 @@ KwikIO, MEArecIO, MaxwellIO, + MedIO, MicromedIO, NixIO, NixIOFr, diff --git a/neo/io/medio.py b/neo/io/medio.py new file mode 100644 index 000000000..818ff78f3 --- /dev/null +++ b/neo/io/medio.py @@ -0,0 +1,52 @@ +""" +IO for reading MED datasets using dhn-med-py library. + +dhn-med-py +https://medformat.org +https://pypi.org/project/dhn-med-py/ + +MED Format Specifications: https://medformat.org + +Author: Dan Crepeau, Matt Stead +""" + +from neo.io.basefromrawio import BaseFromRaw +from neo.rawio.medrawio import MedRawIO + + +class MedIO(MedRawIO, BaseFromRaw): + """ + IO for reading MED datasets. + """ + name = 'MED IO' + description = "IO for reading MED datasets" + + _prefered_signal_group_mode = 'group-by-same-units' + mode = 'dir' + + def __init__(self, dirname=None, password=None, keep_original_times=False): + MedRawIO.__init__(self, dirname=dirname, password=password, + keep_original_times=keep_original_times) + """ + Initialise IO instance + + Parameters + ---------- + dirname : str + Directory containing data files + password : str + MED sessions can be optionally encrypted with a password. + Default: None + keep_original_times : bool + Preserve original time stamps as in data files. By default datasets are + shifted to begin at t_start = 0. When set to True, timestamps will be + returned as UTC (seconds since midnight 1 Jan 1970). + Default: False + """ + BaseFromRaw.__init__(self, dirname) + + def close(self): + MedRawIO.close(self) + + def __del__(self): + MedRawIO.__del__(self) diff --git a/neo/rawio/__init__.py b/neo/rawio/__init__.py index 29683c573..896a491fd 100644 --- a/neo/rawio/__init__.py +++ b/neo/rawio/__init__.py @@ -24,6 +24,7 @@ * :attr:`ElanRawIO` * :attr:`IntanRawIO` * :attr:`MaxwellRawIO` +* :attr:`MedRawIO` * :attr:`MEArecRawIO` * :attr:`MicromedRawIO` * :attr:`NeuralynxRawIO` @@ -93,6 +94,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.rawio.MedRawIO + + .. autoattribute:: extensions + .. autoclass:: neo.rawio.MEArecRawIO .. autoattribute:: extensions @@ -186,6 +191,7 @@ from neo.rawio.intanrawio import IntanRawIO from neo.rawio.maxwellrawio import MaxwellRawIO from neo.rawio.mearecrawio import MEArecRawIO +from neo.rawio.medrawio import MedRawIO from neo.rawio.micromedrawio import MicromedRawIO from neo.rawio.neuralynxrawio import NeuralynxRawIO from neo.rawio.neuroexplorerrawio import NeuroExplorerRawIO @@ -220,6 +226,7 @@ MicromedRawIO, MaxwellRawIO, MEArecRawIO, + MedRawIO, NeuralynxRawIO, NeuroExplorerRawIO, NeuroScopeRawIO, diff --git a/neo/rawio/medrawio.py b/neo/rawio/medrawio.py new file mode 100644 index 000000000..3a8ff2c21 --- /dev/null +++ b/neo/rawio/medrawio.py @@ -0,0 +1,353 @@ +""" +Class for reading MED (Multiscale Electrophysiology Data) Format. + +Uses the dhn-med-py python package, created by Dark Horse Neuro, Inc. + +Authors: Dan Crepeau, Matt Stead +""" + +from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, + _spike_channel_dtype, _event_channel_dtype) + +import numpy as np + + +class MedRawIO(BaseRawIO): + """ + Class for reading MED (Multiscale Electrophysiology Data) Format. + + Uses the dhn-med-py MED python package (version >= 1.0.0), created by + Dark Horse Neuro, Inc. and medformat.org. + + Currently reads the entire MED session. Every discontinuity is considered + to be a new segment. Channels are grouped by sampling frequency, to + create streams. In MED all channels will line up time-wise, so streams + will span the entire recording, and continuous sections of those streams + are divided up into segments. + + Timestamps generated are referenced to the beginning of the session, + with the beginning of the session being timestamp zero. If UTC timestamps + are desired, then the keep_original_times flag in the constructor can be + set to True (it defaults to False) and the timestamps used in the object + will be seconds, reference to midnight 1 Jan 1970 (assuming that that + data is available in the MED data session). + """ + + extensions = ['medd', 'rdat', 'ridx'] + rawmode = 'one-dir' + + def __init__(self, dirname=None, password=None, keep_original_times=False, **kargs): + BaseRawIO.__init__(self, **kargs) + + import dhn_med_py + from dhn_med_py import MedSession + + self.dirname = str(dirname) + self.password = password + self.keep_original_times = keep_original_times + + def _source_name(self): + return self.dirname + + def _parse_header(self): + + import dhn_med_py + from dhn_med_py import MedSession + + # Set a default password to improve compatibility and ease-of-use. + # This password will be ignored if an unencrypted MED dataset is being used. + if self.password is None: + self.password = "L2_password" + + # Open the MED session (open data file pointers and read metadata files) + self.sess = MedSession(self.dirname, self.password) + + # set the matrix calls to be "sample_major" as opposed to "channel_major" + self.sess.set_major_dimension("sample") + + # find number of segments + sess_contigua = self.sess.session_info['contigua'] + self._nb_segment = len(sess_contigua) + + # find overall session start time. + self._session_start_time = sess_contigua[0]['start_time'] + + # keep track of offset from metadata, if we are keeping original times. + if not self.keep_original_times: + self._session_time_offset = 0 - self._session_start_time + else: + self._session_time_offset = self.sess.session_info['metadata']['recording_time_offset'] + + # find start/stop times of each segment + self._seg_t_starts = [] + self._seg_t_stops = [] + for seg_idx in range(self._nb_segment): + self._seg_t_starts.append(sess_contigua[seg_idx]['start_time']) + self._seg_t_stops.append(sess_contigua[seg_idx]['end_time']) + + # find number of streams per segment + self._stream_info = [] + self._num_stream_info = 0 + for chan_idx, chan_info in enumerate(self.sess.session_info['channels']): + chan_freq = chan_info['metadata']['sampling_frequency'] + + # set MED session reference channel to be this channel, so the correct contigua is returned + self.sess.set_reference_channel(chan_info['metadata']['channel_name']) + contigua = self.sess.find_discontinuities() + + # find total number of samples in this channel + chan_num_samples = 0 + for seg_idx in range(len(contigua)): + chan_num_samples += (contigua[seg_idx]['end_index'] - contigua[seg_idx]['start_index']) + 1 + + # see if we need a new stream, or add channel to existing stream + add_to_existing_stream_info = False + for stream_info in self._stream_info: + if chan_freq == stream_info['sampling_frequency'] and chan_num_samples == stream_info['num_samples']: + # found a match, so add it! + add_to_existing_stream_info = True + stream_info['chan_list'].append((chan_idx, chan_info['metadata']['channel_name'])) + stream_info['raw_chans'].append(chan_info['metadata']['channel_name']) + break + + if not add_to_existing_stream_info: + self._num_stream_info += 1 + + new_stream_info = {'sampling_frequency': chan_info['metadata']['sampling_frequency'], \ + 'chan_list': [(chan_idx, chan_info['metadata']['channel_name'])], \ + 'contigua' : contigua, \ + 'raw_chans' : [chan_info['metadata']['channel_name']], \ + 'num_samples' : chan_num_samples} + + self._stream_info.append(new_stream_info) + + self.num_channels_in_session = len(self.sess.session_info['channels']) + self.num_streams_in_session = self._num_stream_info + + signal_streams = [] + signal_channels = [] + + # fill in signal_streams and signal_channels info + for signal_stream_counter, stream_info in enumerate(self._stream_info): + + # get the stream start time, which is the start time of the first continuous section + stream_start_time = (stream_info['contigua'][0]["start_time"] + self._session_time_offset ) / 1e6 + + # create stream name/id with info that we now have + name = f'stream (rate,#sample,t0): ({stream_info["sampling_frequency"]}, {stream_info["num_samples"]}, {stream_start_time})' + stream_id = signal_stream_counter + signal_streams.append((name, stream_id)) + + # add entry for signal_channels for each channel in a stream + for chan in stream_info['chan_list']: + signal_channels.append((chan[1], chan[0], stream_info['sampling_frequency'], 'int32', 'uV', 1, 0, stream_id)) + + signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype) + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) + + # no unit/epoch information contained in MED + spike_channels = [] + spike_channels = np.array(spike_channels, dtype=_spike_channel_dtype) + + # Events + event_channels = [] + event_channels.append(('Event', 'event_channel', 'event')) + event_channels.append(('Epoch', 'epoch_channel', 'epoch')) + event_channels = np.array(event_channels, dtype=_event_channel_dtype) + + # Create Neo header structure + self.header = {} + self.header['nb_block'] = 1 + self.header['nb_segment'] = [self._nb_segment] + self.header['signal_streams'] = signal_streams + self.header['signal_channels'] = signal_channels + self.header['spike_channels'] = spike_channels + self.header['event_channels'] = event_channels + + # `_generate_minimal_annotations()` must be called to generate the nested + # dict of annotations/array_annotations + self._generate_minimal_annotations() + + # Add custom annotations for neo objects + bl_ann = self.raw_annotations['blocks'][0] + bl_ann['name'] = 'MED Data Block' + # The following adds all of the MED session_info to the block annotations, + # which includes features like patient name, recording location, etc. + bl_ann.update(self.sess.session_info) + + # Give segments unique names + for i in range(self._nb_segment): + seg_ann = bl_ann['segments'][i] + seg_ann['name'] = 'Seg #' + str(i) + ' Block #0' + + # this pprint lines really help for understand the nested (and complicated sometimes) dict + #from pprint import pprint + #pprint(self.raw_annotations) + + def _segment_t_start(self, block_index, seg_index): + return (self._seg_t_starts[seg_index] + self._session_time_offset) / 1e6 + + def _segment_t_stop(self, block_index, seg_index): + return (self._seg_t_stops[seg_index] + self._session_time_offset) / 1e6 + + def _get_signal_size(self, block_index, seg_index, stream_index): + stream_segment_contigua = self._stream_info[stream_index]['contigua'] + return (stream_segment_contigua[seg_index]['end_index'] - stream_segment_contigua[seg_index]['start_index']) + 1 + + def _get_signal_t_start(self, block_index, seg_index, stream_index): + return (self._seg_t_starts[seg_index] + self._session_time_offset) / 1e6 + + def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, + stream_index, channel_indexes): + + import dhn_med_py + from dhn_med_py import MedSession + + # Correct for None start/stop inputs + if i_start is None: + i_start = 0 + if i_stop is None: + i_stop = self.get_signal_size(block_index=block_index, seg_index=seg_index, + stream_index=stream_index) + + # Check for invalid start/stop inputs + if i_start < 0 or i_stop > self.get_signal_size(block_index=block_index, seg_index=seg_index, stream_index=stream_index): + raise IndexError("MED read error: Too many samples requested!") + if i_start > i_stop: + raise IndexError("MED read error: i_start (" + i_start + ") is greater than i_stop (" + i_stop + ")") + + num_channels = 0 + if channel_indexes is None: + self.sess.set_channel_inactive('all') + self.sess.set_channel_active(self._stream_info[stream_index]['raw_chans']) + num_channels = len(self._stream_info[stream_index]['raw_chans']) + self.sess.set_reference_channel(self._stream_info[stream_index]['raw_chans'][0]) + else: + if any(channel_indexes < 0): + raise IndexError(f'Can not index negative channels: {channel_indexes}') + # Set all channels to be inactive, then selectively set some of them to be active + self.sess.set_channel_inactive('all') + for i, channel_idx in enumerate(channel_indexes): + num_channels += 1 + self.sess.set_channel_active(self._stream_info[stream_index]['raw_chans'][channel_idx]) + self.sess.set_reference_channel(self._stream_info[stream_index]['raw_chans'][channel_indexes[0]]) + + # Return empty dataset if start/stop samples are equal + if i_start == i_stop: + raw_signals = np.zeros((0, num_channels), dtype='int32') + return raw_signals + + # Otherwise, return the matrix 2D array returned by the MED library + start_sample_offset = self._stream_info[stream_index]['contigua'][seg_index]['start_index'] + self.sess.read_by_index(i_start + start_sample_offset, i_stop + start_sample_offset) + + raw_signals = np.empty((i_stop - i_start, num_channels), dtype=np.int32) + for i, chan in enumerate(self.sess.data['channels']): + raw_signals[:,i] = chan['data'] + + return raw_signals + + def _spike_count(self, block_index, seg_index, spike_channel_index): + return None + + def _get_spike_timestamps(self, block_index, seg_index, spike_channel_index, t_start, t_stop): + return None + + def _rescale_spike_timestamp(self, spike_timestamps, dtype): + return None + + def _get_spike_raw_waveforms(self, block_index, seg_index, spike_channel_index, + t_start, t_stop): + return None + + def _event_count(self, block_index, seg_index, event_channel_index): + + # there are no epochs to consider for MED in this interface + if self.header['event_channels']['type'][event_channel_index] == b'epoch': + return 0 + + records = self.sess.get_session_records() + + # Find segment boundaries + ts0 = (self.segment_t_start(block_index, seg_index) * 1e6) - self._session_time_offset + ts1 = (self.segment_t_stop(block_index, seg_index) * 1e6) - self._session_time_offset + + # Only count Note and Neuralynx type records. + count = 0 + for record in records: + if ((record['type_string'] == 'Note' or record['type_string'] == 'NlxP') and \ + (record['start_time'] >= ts0 and record['start_time'] < ts1)): + count += 1 + + return count + + def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop): + + # There are no epochs to consider for MED in this interface, + # so just bail out if that's what's being asked for + if self.header['event_channels']['type'][event_channel_index] == b'epoch': + return np.array([]), np.array([]), np.array([], dtype='U') + + if t_start is not None: + start_time = (t_start * 1e6) - self._session_time_offset + else: + start_time = (self.segment_t_start(block_index, seg_index) * 1e6) - self._session_time_offset + + if t_stop is not None: + end_time = (t_stop * 1e6) - self._session_time_offset + else: + end_time = (self.segment_t_stop(block_index, seg_index) * 1e6) - self._session_time_offset + + # Ask MED session for a list of events that match time parameters + records = self.sess.get_session_records(start_time, end_time) + + # create a subset of only Note and Neuralynx type records + records_subset = [] + for record in records: + if record['type_string'] == 'Note' or record['type_string'] == 'NlxP': + records_subset.append(record) + records = records_subset + + # if no records match our criteria, then we are done, output empty arrays + if (len(records) == 0): + return np.array([]), np.array([]), np.array([], dtype='U') + + # inialize output arrays + times = np.empty(shape=[len(records)]) + durations = None + labels = [] + + # populate output arrays of times and labels + for i, record in enumerate(records): + times[i] = (record['start_time'] + self._session_time_offset) / 1e6 + if record['type_string'] == 'Note': + labels.append(record['text']) + elif record['type_string'] == 'NlxP': + labels.append('NlxP subport: ' + str(record['subport']) + ' value: ' + str(record['value'])) + + labels = np.asarray(labels, dtype='U') + + return times, durations, labels + + def _rescale_event_timestamp(self, event_timestamps, dtype, event_channel_index): + return np.asarray(event_timestamps, dtype=dtype) + + def _rescale_epoch_duration(self, raw_duration, dtype, event_channel_index): + return np.asarray(raw_duration, dtype=dtype) + + def __del__(self): + try: + # Important to make sure the session is closed, since the MED library only allows + # one session to be open at a time. + self.sess.close() + del self.sess + except Exception: + pass + + def close(self): + try: + # Important to make sure the session is closed, since the MED library only allows + # one session to be open at a time. + self.sess.close() + except Exception: + pass diff --git a/neo/test/iotest/test_medio.py b/neo/test/iotest/test_medio.py new file mode 100644 index 000000000..9f75d7d54 --- /dev/null +++ b/neo/test/iotest/test_medio.py @@ -0,0 +1,123 @@ +""" +Tests of neo.io.medio +""" + +import pathlib +import unittest + +from neo.io.medio import MedIO +from neo.test.iotest.common_io_test import BaseTestIO +from neo.test.iotest.tools import get_test_file_full_path +from neo.io.proxyobjects import (AnalogSignalProxy, + SpikeTrainProxy, EventProxy, EpochProxy) +from neo import (AnalogSignal, SpikeTrain) + +import quantities as pq +import numpy as np + + +# This run standard tests, this is mandatory for all IOs +class TestMedIO(BaseTestIO, unittest.TestCase, ): + ioclass = MedIO + entities_to_download = ['med'] + entities_to_test = [ + 'med/sine_waves.medd', + 'med/test.medd' + ] + + def setUp(self): + + super().setUp() + self.dirname = self.get_local_path('med/sine_waves.medd') + self.dirname2 = self.get_local_path('med/test.medd') + self.password = 'L2_password' + + def test_read_segment_lazy(self): + + r = MedIO(self.dirname, self.password) + seg = r.read_segment(lazy=False) + + # There will only be one analogsignal in this reading + self.assertEqual(len(seg.analogsignals), 1) + # Test that the correct number of samples are read, 5760000 samps for 3 channels + self.assertEqual(seg.analogsignals[0].shape[0], 5760000) + self.assertEqual(seg.analogsignals[0].shape[1], 3) + + # Test the first sample value of all 3 channels, which are + # known to be [-1, -4, -4] + np.testing.assert_array_equal(seg.analogsignals[0][0][:3], [-1, -4, -4]) + + for anasig in seg.analogsignals: + self.assertNotEqual(anasig.size, 0) + for st in seg.spiketrains: + self.assertNotEqual(st.size, 0) + + # annotations + #assert 'seg_extra_info' in seg.annotations + assert seg.name == 'Seg #0 Block #0' + for anasig in seg.analogsignals: + assert anasig.name is not None + for ev in seg.events: + assert ev.name is not None + for ep in seg.epochs: + assert ep.name is not None + + r.close() + + def test_read_block(self): + + r = MedIO(self.dirname, self.password) + bl = r.read_block(lazy=True) + self.assertTrue(bl.annotations) + + for count, seg in enumerate(bl.segments): + assert seg.name == 'Seg #' + str(count) + ' Block #0' + + for anasig in seg.analogsignals: + assert anasig.name is not None + + # Verify that the block annotations from the MED session are + # read properly. There are a lot of annotations, so we'll just + # spot-check a couple of them. + assert(bl.annotations['metadata']['recording_country'] == 'United States') + assert(bl.annotations['metadata']['AC_line_frequency'] == 60.0) + + r.close() + + def test_read_segment_with_time_slice(self): + """ + Test loading of a time slice and check resulting times + """ + r = MedIO(self.dirname, self.password) + seg = r.read_segment(time_slice=None) + + # spike and epoch timestamps are not being read + self.assertEqual(len(seg.spiketrains), 0) + self.assertEqual(len(seg.epochs), 1) + self.assertEqual(len(seg.epochs[0]), 0) + + # Test for 180 events (1 per second for 3 minute recording) + self.assertEqual(len(seg.events), 1) + self.assertEqual(len(seg.events[0]), 180) + + for asig in seg.analogsignals: + self.assertEqual(asig.shape[0], 5760000) + n_channels = sum(a.shape[-1] for a in seg.analogsignals) + self.assertEqual(n_channels, 3) + + t_start, t_stop = 500 * pq.ms, 800 * pq.ms + seg = r.read_segment(time_slice=(t_start, t_stop)) + + # Test that 300 ms were read, which at 32 kHz, is 9600 samples + self.assertAlmostEqual(seg.analogsignals[0].shape[0], 9600, delta=1.) + # Test that it read from 3 channels + self.assertEqual(seg.analogsignals[0].shape[1], 3) + + self.assertAlmostEqual(seg.t_start.rescale(t_start.units), t_start, delta=5.) + self.assertAlmostEqual(seg.t_stop.rescale(t_stop.units), t_stop, delta=5.) + + r.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/neo/test/rawiotest/test_medrawio.py b/neo/test/rawiotest/test_medrawio.py new file mode 100644 index 000000000..7118cacfd --- /dev/null +++ b/neo/test/rawiotest/test_medrawio.py @@ -0,0 +1,241 @@ +import unittest +import numpy as np + +from neo.rawio.medrawio import MedRawIO + +from neo.test.rawiotest.common_rawio_test import BaseTestRawIO + +class TestMedRawIO(BaseTestRawIO, unittest.TestCase, ): + rawioclass = MedRawIO + entities_to_download = [ + 'med' + ] + entities_to_test = ['med/sine_waves.medd', 'med/test.medd'] + + def test_close(self): + + filename = self.get_local_path('med/sine_waves.medd') + + raw_io1 = MedRawIO(filename, password='L2_password') + raw_io1.parse_header() + raw_io1.close() + + raw_io2 = MedRawIO(filename, password='L2_password') + raw_io2.parse_header() + raw_io2.close() + + + def test_scan_med_directory(self): + + filename = self.get_local_path('med/sine_waves.medd') + + rawio = MedRawIO(filename, password='L2_password') + rawio.parse_header() + + # Test that correct metadata and boundaries are extracted + # from the MED session. We know the correct answers since + # we generated the test files. + self.assertEqual(rawio.signal_streams_count(), 1) + self.assertEqual(rawio._segment_t_start(0, 0), 0) + self.assertEqual(rawio._segment_t_stop(0, 0), 180) + + # Verify it found all 3 channels + self.assertEqual(rawio.num_channels_in_session, 3) + self.assertEqual(rawio.header['signal_channels'].size, 3) + + # Verify if found the names of the 3 channels + self.assertEqual(rawio.header['signal_channels'][0][0], 'CSC_0001') + self.assertEqual(rawio.header['signal_channels'][1][0], 'CSC_0002') + self.assertEqual(rawio.header['signal_channels'][2][0], 'CSC_0003') + + # Read first 3 seconds of data from all channels + raw_chunk = rawio.get_analogsignal_chunk(block_index=0, + seg_index=0, + i_start=0, + i_stop=96000, + stream_index=0, + channel_indexes=None) + + # Test the first sample value of all 3 channels, which are + # known to be [-1, -4, -4] + np.testing.assert_array_equal(raw_chunk[0][:3], [-1, -4, -4]) + + # Read 1 second of data from the second channel + raw_chunk = rawio.get_analogsignal_chunk(block_index=0, + seg_index=0, + i_start=0, + i_stop=32000, + stream_index=0, + channel_indexes=[1]) + + # Test known first sample of second channel: [-4] + self.assertEqual(raw_chunk[0][0], -4) + + rawio.close() + + # Test on second test dataset, test.medd. + filename = self.get_local_path('med/test.medd') + + rawio = MedRawIO(filename, password='L2_password') + rawio.parse_header() + + # Test that correct metadata and boundaries are extracted + # from the MED session. We know the correct answers since + # we generated the test files. + + # For this dataset, there are 3 continuous data ranges, with + # approximately 10 seconds between the ranges. + # There are 3 channels, two with a frequency of 1000 Hz and one + # with a frequency of 5000 Hz. + self.assertEqual(rawio.signal_streams_count(), 2) + + # Segment 0 + self.assertEqual(rawio._segment_t_start(0, 0), 0) + self.assertEqual(rawio._segment_t_stop(0, 0), 39.8898) + # Segment 1 + self.assertEqual(rawio._segment_t_start(0, 1), 50.809826) + self.assertEqual(rawio._segment_t_stop(0, 1), 87.337646) + # Segment 2 + self.assertEqual(rawio._segment_t_start(0, 2), 97.242057) + self.assertEqual(rawio._segment_t_stop(0, 2), 180.016702) + + # Verify it found all 3 channels. + self.assertEqual(rawio.num_channels_in_session, 3) + self.assertEqual(rawio.header['signal_channels'].size, 3) + + # Verity if found the names of the 3 channels + self.assertEqual(rawio.header['signal_channels'][0][0], '5k_ch1') + self.assertEqual(rawio.header['signal_channels'][1][0], '1k_ch1') + self.assertEqual(rawio.header['signal_channels'][2][0], '1k_ch2') + + # Read first 3 seconds of data from the first channel (5k_ch1) + raw_chunk = rawio.get_analogsignal_chunk(block_index=0, + seg_index=0, + i_start=0, + i_stop=15000, + stream_index=0, + channel_indexes=None) + + # Test the first three sample values returned, which are + # known to be [-80, -79, -78] + self.assertEqual(raw_chunk[0][0], -80) + self.assertEqual(raw_chunk[1][0], -79) + self.assertEqual(raw_chunk[2][0], -78) + + + # Read first 3 seconds of data from the second channel and third + # channels (1k_ch1 and 1k_ch2) + raw_chunk = rawio.get_analogsignal_chunk(block_index=0, + seg_index=0, + i_start=0, + i_stop=3000, + stream_index=1, + channel_indexes=None) + + # Test first sample returned of both channels, which are known + # to be [-79, -80] + np.testing.assert_array_equal(raw_chunk[0][:2], [-79, -80]) + + # Read first 3 seconds of data from the second segment of the first channel (5k_ch1) + raw_chunk = rawio.get_analogsignal_chunk(block_index=0, + seg_index=1, + i_start=0, + i_stop=15000, + stream_index=0, + channel_indexes=None) + + # Test the first three sample values returned, which are + # known to be [22, 23, 24] + self.assertEqual(raw_chunk[0][0], 22) + self.assertEqual(raw_chunk[1][0], 23) + self.assertEqual(raw_chunk[2][0], 24) + + self.assertEqual(len(rawio.header['event_channels']), 2) + + # Verify that there are 5 events in the dataset. + # They are 3 "Note" type events, and 2 "NlxP", or neuralynx, type events. + # The first segment has one event, and the second and third segments + # each have 2 events. + self.assertEqual(rawio.event_count(0, 0, 0), 1) + self.assertEqual(rawio.event_count(0, 1, 0), 2) + self.assertEqual(rawio.event_count(0, 2, 0), 2) + + # Get array of all events in first segment of data + events = rawio._get_event_timestamps(0, 0, 0, rawio._segment_t_start(0, 0), rawio._segment_t_stop(0, 0)) + # Make sure it read 1 event + self.assertEqual(len(events[0]), 1) + + # Get array of all events in second segment of data + events = rawio._get_event_timestamps(0, 1, 0, rawio._segment_t_start(0, 1), rawio._segment_t_stop(0, 1)) + # Make sure it read 2 events + self.assertEqual(len(events[0]), 2) + + # Verify the first event of the second segment is a Neuralynx type event, with correct time + self.assertEqual(events[2][0][:4], 'NlxP') + self.assertEqual(events[0][0], 51.703509) + + # Get array of all events in third segment of data + events = rawio._get_event_timestamps(0, 2, 0, rawio._segment_t_start(0, 2), rawio._segment_t_stop(0, 2)) + # Make sure it read 2 events + self.assertEqual(len(events[0]), 2) + + # Verify the second event of the second segment is a Neuralynx type event, with correct time + self.assertEqual(events[2][1][:4], 'NlxP') + self.assertEqual(events[0][1], 161.607036) + + rawio.close() + + # Test on second test dataset, test.medd, with preserving original timestamps. + # Timestamps here are in UTC (seconds since midnight, 1 Jan 1970) + filename = self.get_local_path('med/test.medd') + + rawio = MedRawIO(filename, password='L2_password', keep_original_times=True) + rawio.parse_header() + + # Segment 0 + self.assertEqual(rawio._segment_t_start(0, 0), 1678111774.012236) + self.assertEqual(rawio._segment_t_stop(0, 0), 1678111813.902036) + # Segment 1 + self.assertEqual(rawio._segment_t_start(0, 1), 1678111824.822062) + self.assertEqual(rawio._segment_t_stop(0, 1), 1678111861.349882) + # Segment 2 + self.assertEqual(rawio._segment_t_start(0, 2), 1678111871.254293) + self.assertEqual(rawio._segment_t_stop(0, 2), 1678111954.028938) + + # Verify that there are 5 events in the dataset. + # They are 3 "Note" type events, and 2 "NlxP", or neuralynx, type events. + # The first segment has one event, and the second and third segments + # each have 2 events. + self.assertEqual(rawio.event_count(0, 0, 0), 1) + self.assertEqual(rawio.event_count(0, 1, 0), 2) + self.assertEqual(rawio.event_count(0, 2, 0), 2) + + # Get array of all events in first segment of data + events = rawio._get_event_timestamps(0, 0, 0, rawio._segment_t_start(0, 0), rawio._segment_t_stop(0, 0)) + # Make sure it read 1 event + self.assertEqual(len(events[0]), 1) + + # Get array of all events in second segment of data + events = rawio._get_event_timestamps(0, 1, 0, rawio._segment_t_start(0, 1), rawio._segment_t_stop(0, 1)) + # Make sure it read 2 events + self.assertEqual(len(events[0]), 2) + + # Verify the first event of the second segment is a Neuralynx type event, with correct time + self.assertEqual(events[2][0][:4], 'NlxP') + self.assertEqual(events[0][0], 1678111825.715745) + + # Get array of all events in third segment of data + events = rawio._get_event_timestamps(0, 2, 0, rawio._segment_t_start(0, 2), rawio._segment_t_stop(0, 2)) + # Make sure it read 2 events + self.assertEqual(len(events[0]), 2) + + # Verify the second event of the second segment is a Neuralynx type event, with correct time + self.assertEqual(events[2][1][:4], 'NlxP') + self.assertEqual(events[0][1], 1678111935.619272) + + rawio.close() + + +if __name__ == "__main__": + unittest.main() + diff --git a/pyproject.toml b/pyproject.toml index 986e7f21e..d5228d753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ iocache = [ ] test = [ + "dhn_med_py>=1.0.0", "pytest", "pytest-cov", # datalad # this dependency is covered by conda (environment_testing.yml) @@ -57,13 +58,13 @@ test = [ "nixio", "matplotlib", "ipython", + "joblib>=1.0.0", "coverage", "coveralls", "pillow", "sonpy", "pynwb", "probeinterface", - "joblib>=1.0.0", "zugbruecke>=0.2", "wenv" ] @@ -96,11 +97,13 @@ ced = ["sonpy"] nwb = ["pynwb"] maxwell = ["h5py"] biocam = ["h5py"] +med = ["dhn_med_py>=1.0.0"] plexon2 = ["zugbruecke>=0.2; sys_platform!='win32'", "wenv; sys_platform!='win32'"] all = [ "coverage", "coveralls", + "dhn_med_py>=1.0.0", "h5py", "igor2", "ipython", @@ -119,6 +122,5 @@ all = [ "tqdm", "wenv; sys_platform!='win32'", "zugbruecke>=0.2; sys_platform!='win32'", - ] # we do not include 'stfio' in 'all' as it is not pip installable