diff --git a/neo/io/__init__.py b/neo/io/__init__.py index ed8796b21..0b5f4e8e2 100644 --- a/neo/io/__init__.py +++ b/neo/io/__init__.py @@ -28,6 +28,7 @@ * :attr:`BrainwareDamIO` * :attr:`BrainwareF32IO` * :attr:`BrainwareSrcIO` +* :attr:`CedIO` * :attr:`ElanIO` * :attr:`IgorIO` * :attr:`IntanIO` @@ -114,6 +115,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.io.CedIO + + .. autoattribute:: extensions + .. autoclass:: neo.io.ElanIO .. autoattribute:: extensions @@ -264,6 +269,7 @@ from neo.io.brainwaredamio import BrainwareDamIO from neo.io.brainwaref32io import BrainwareF32IO from neo.io.brainwaresrcio import BrainwareSrcIO +from neo.io.cedio import CedIO from neo.io.elanio import ElanIO # from neo.io.elphyio import ElphyIO from neo.io.exampleio import ExampleIO @@ -310,6 +316,7 @@ BrainwareDamIO, BrainwareF32IO, BrainwareSrcIO, + CedIO, ElanIO, # ElphyIO, ExampleIO, diff --git a/neo/io/cedio.py b/neo/io/cedio.py new file mode 100644 index 000000000..602e49c41 --- /dev/null +++ b/neo/io/cedio.py @@ -0,0 +1,10 @@ +from neo.io.basefromrawio import BaseFromRaw +from neo.rawio.cedrawio import CedRawIO + + +class CedIO(CedRawIO, BaseFromRaw): + __doc__ = CedRawIO.__doc__ + + def __init__(self, filename, entfile=None, posfile=None): + CedRawIO.__init__(self, filename=filename) + BaseFromRaw.__init__(self, filename) diff --git a/neo/rawio/__init__.py b/neo/rawio/__init__.py index 9a27d93f5..571a49bf2 100644 --- a/neo/rawio/__init__.py +++ b/neo/rawio/__init__.py @@ -17,6 +17,7 @@ * :attr:`AxonRawIO` * :attr:`BlackrockRawIO` * :attr:`BrainVisionRawIO` +* :attr:`CedRawIO` * :attr:`ElanRawIO` * :attr:`IntanRawIO` * :attr:`MEArecRawIO` @@ -58,6 +59,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.rawio.CedRawIO + + .. autoattribute:: extensions + .. autoclass:: neo.rawio.ElanRawIO .. autoattribute:: extensions @@ -142,6 +147,7 @@ from neo.rawio.axonrawio import AxonRawIO from neo.rawio.blackrockrawio import BlackrockRawIO from neo.rawio.brainvisionrawio import BrainVisionRawIO +from neo.rawio.cedrawio import CedRawIO from neo.rawio.elanrawio import ElanRawIO from neo.rawio.examplerawio import ExampleRawIO from neo.rawio.intanrawio import IntanRawIO @@ -169,6 +175,7 @@ AxonRawIO, BlackrockRawIO, BrainVisionRawIO, + CedRawIO, ElanRawIO, IntanRawIO, MicromedRawIO, diff --git a/neo/rawio/cedrawio.py b/neo/rawio/cedrawio.py new file mode 100644 index 000000000..6b74e45b3 --- /dev/null +++ b/neo/rawio/cedrawio.py @@ -0,0 +1,177 @@ +""" +Class for reading data from CED (Cambridge Electronic Design) +http://ced.co.uk/ + +This read *.smrx (and *.smr) from spike2 and signal software. + +Note Spike2RawIO/Spike2IO is the old implementation in neo. +It still works without any dependency and should be faster. +Spike2IO only works for smr (32 bit) and not for smrx (64 bit) files. + +This implementation depends on the SONPY package: +https://pypi.org/project/sonpy/ + +Please note that the SONPY package: + * is NOT open source + * internally uses a list instead of numpy.ndarray, potentially causing slow data reading + * is maintained by CED + + +Author : Samuel Garcia +""" + +from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, + _spike_channel_dtype, _event_channel_dtype) + +import numpy as np +from copy import deepcopy + +try: + import sonpy + HAVE_SONPY = True +except ImportError: + HAVE_SONPY = False + + +class CedRawIO(BaseRawIO): + """ + Class for reading data from CED (Cambridge Electronic Design) spike2. + This internally uses the sonpy package which is closed source. + + This IO reads smr and smrx files + """ + extensions = ['smr', 'smrx'] + rawmode = 'one-file' + + def __init__(self, filename='', take_ideal_sampling_rate=False, ): + BaseRawIO.__init__(self) + self.filename = filename + + self.take_ideal_sampling_rate = take_ideal_sampling_rate + + def _source_name(self): + return self.filename + + def _parse_header(self): + assert HAVE_SONPY, 'sonpy must be installed' + + self.smrx_file = sonpy.lib.SonFile(sName=str(self.filename), bReadOnly=True) + smrx = self.smrx_file + + channel_infos = [] + signal_channels = [] + for chan_ind in range(smrx.MaxChannels()): + chan_type = smrx.ChannelType(chan_ind) + if chan_type == sonpy.lib.DataType.Adc: + physical_chan = smrx.PhysicalChannel(chan_ind) + divide = smrx.ChannelDivide(chan_ind) + if self.take_ideal_sampling_rate: + sr = smrx.GetIdealRate(chan_ind) + else: + sr = 1. / (smrx.GetTimeBase() * divide) + max_time = smrx.ChannelMaxTime(chan_ind) + first_time = smrx.FirstTime(chan_ind, 0, max_time) + # max_times is included so +1 + time_size = (max_time - first_time) / divide + 1 + channel_infos.append((first_time, max_time, divide, time_size, sr)) + gain = smrx.GetChannelScale(chan_ind) / 6553.6 + offset = smrx.GetChannelOffset(chan_ind) + units = smrx.GetChannelUnits(chan_ind) + ch_name = smrx.GetChannelTitle(chan_ind) + chan_id = str(chan_ind) + dtype = 'int16' + # set later after grouping + stream_id = '0' + signal_channels.append((ch_name, chan_id, sr, dtype, + units, gain, offset, stream_id)) + + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) + + # channels are grouped into stream if they have a common start, stop, size, divide and sampling_rate + channel_infos = np.array(channel_infos, + dtype=[('first_time', 'i8'), ('max_time', 'i8'), + ('divide', 'i8'), ('size', 'i8'), ('sampling_rate', 'f8')]) + unique_info = np.unique(channel_infos) + self.stream_info = unique_info + signal_streams = [] + for i, info in enumerate(unique_info): + stream_id = str(i) + mask = channel_infos == info + signal_channels['stream_id'][mask] = stream_id + num_chans = np.sum(mask) + stream_name = f'{stream_id} {num_chans}chans' + signal_streams.append((stream_name, stream_id)) + signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype) + + # spike channels not handled + spike_channels = [] + spike_channels = np.array([], dtype=_spike_channel_dtype) + + # event channels not handled + event_channels = [] + event_channels = np.array(event_channels, dtype=_event_channel_dtype) + + self._seg_t_start = np.inf + self._seg_t_stop = -np.inf + for info in self.stream_info: + self._seg_t_start = min(self._seg_t_start, + info['first_time'] / info['sampling_rate']) + self._seg_t_stop = max(self._seg_t_stop, + info['max_time'] / info['sampling_rate']) + + self.header = {} + self.header['nb_block'] = 1 + self.header['nb_segment'] = [1] + self.header['signal_streams'] = signal_streams + self.header['signal_channels'] = signal_channels + self.header['spike_channels'] = spike_channels + self.header['event_channels'] = event_channels + + self._generate_minimal_annotations() + + def _segment_t_start(self, block_index, seg_index): + return self._seg_t_start + + def _segment_t_stop(self, block_index, seg_index): + return self._seg_t_stop + + def _get_signal_size(self, block_index, seg_index, stream_index): + size = self.stream_info[stream_index]['size'] + return size + + def _get_signal_t_start(self, block_index, seg_index, stream_index): + info = self.stream_info[stream_index] + t_start = info['first_time'] / info['sampling_rate'] + return t_start + + def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, + stream_index, channel_indexes): + + if i_start is None: + i_start = 0 + if i_stop is None: + i_stop = self.stream_info[stream_index]['size'] + + stream_id = self.header['signal_streams']['id'][stream_index] + signal_channels = self.header['signal_channels'] + mask = signal_channels['stream_id'] == stream_id + signal_channels = signal_channels[mask] + if channel_indexes is not None: + signal_channels = signal_channels[channel_indexes] + + num_chans = len(signal_channels) + + size = i_stop - i_start + sigs = np.zeros((size, num_chans), dtype='int16') + + info = self.stream_info[stream_index] + t_from = info['first_time'] + info['divide'] * i_start + t_upto = info['first_time'] + info['divide'] * i_stop + + for i, chan_id in enumerate(signal_channels['id']): + chan_ind = int(chan_id) + sig = self.smrx_file.ReadInts(chan=chan_ind, + nMax=size, tFrom=t_from, tUpto=t_upto) + sigs[:, i] = sig + + return sigs diff --git a/neo/rawio/spike2rawio.py b/neo/rawio/spike2rawio.py index 220553335..374f7f3a5 100644 --- a/neo/rawio/spike2rawio.py +++ b/neo/rawio/spike2rawio.py @@ -27,7 +27,8 @@ class Spike2RawIO(BaseRawIO): """ - + This implementation in neo read only old smr files. + For smrx files you need to use CedRawIO which is based on sonpy. """ extensions = ['smr'] rawmode = 'one-file' diff --git a/neo/test/iotest/test_cedio.py b/neo/test/iotest/test_cedio.py new file mode 100644 index 000000000..3b5795207 --- /dev/null +++ b/neo/test/iotest/test_cedio.py @@ -0,0 +1,20 @@ +import unittest + +from neo.io import CedIO +from neo.test.iotest.common_io_test import BaseTestIO + + +class TestCedIO(BaseTestIO, unittest.TestCase, ): + ioclass = CedIO + entities_to_test = [ + 'spike2/m365_1sec.smrx', + 'spike2/File_spike2_1.smr', + 'spike2/Two-mice-bigfile-test000.smr' + ] + entities_to_download = [ + 'spike2' + ] + + +if __name__ == "__main__": + unittest.main() diff --git a/neo/test/rawiotest/test_cedrawio.py b/neo/test/rawiotest/test_cedrawio.py new file mode 100644 index 000000000..50a0e4beb --- /dev/null +++ b/neo/test/rawiotest/test_cedrawio.py @@ -0,0 +1,21 @@ +import unittest + +from neo.rawio.cedrawio import CedRawIO + +from neo.test.rawiotest.common_rawio_test import BaseTestRawIO + + +class TestCedRawIO(BaseTestRawIO, unittest.TestCase, ): + rawioclass = CedRawIO + entities_to_test = [ + 'spike2/m365_1sec.smrx', + 'spike2/File_spike2_1.smr', + 'spike2/Two-mice-bigfile-test000.smr' + ] + entities_to_download = [ + 'spike2' + ] + + +if __name__ == "__main__": + unittest.main() diff --git a/requirements_testing.txt b/requirements_testing.txt index 8d6b6a112..24536ae8b 100644 --- a/requirements_testing.txt +++ b/requirements_testing.txt @@ -12,3 +12,4 @@ ipython coverage coveralls pillow +sonpy