Skip to content

Commit

Permalink
Merge pull request #987 from samuelgarcia/ced_sonpy_implementation
Browse files Browse the repository at this point in the history
Ced sonpy implementation
  • Loading branch information
JuliaSprenger authored Jun 18, 2021
2 parents 73fe9a0 + fe63e4e commit a134ba1
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 1 deletion.
7 changes: 7 additions & 0 deletions neo/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* :attr:`BrainwareDamIO`
* :attr:`BrainwareF32IO`
* :attr:`BrainwareSrcIO`
* :attr:`CedIO`
* :attr:`ElanIO`
* :attr:`IgorIO`
* :attr:`IntanIO`
Expand Down Expand Up @@ -114,6 +115,10 @@
.. autoattribute:: extensions
.. autoclass:: neo.io.CedIO
.. autoattribute:: extensions
.. autoclass:: neo.io.ElanIO
.. autoattribute:: extensions
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -310,6 +316,7 @@
BrainwareDamIO,
BrainwareF32IO,
BrainwareSrcIO,
CedIO,
ElanIO,
# ElphyIO,
ExampleIO,
Expand Down
10 changes: 10 additions & 0 deletions neo/io/cedio.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions neo/rawio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* :attr:`AxonRawIO`
* :attr:`BlackrockRawIO`
* :attr:`BrainVisionRawIO`
* :attr:`CedRawIO`
* :attr:`ElanRawIO`
* :attr:`IntanRawIO`
* :attr:`MEArecRawIO`
Expand Down Expand Up @@ -58,6 +59,10 @@
.. autoattribute:: extensions
.. autoclass:: neo.rawio.CedRawIO
.. autoattribute:: extensions
.. autoclass:: neo.rawio.ElanRawIO
.. autoattribute:: extensions
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -169,6 +175,7 @@
AxonRawIO,
BlackrockRawIO,
BrainVisionRawIO,
CedRawIO,
ElanRawIO,
IntanRawIO,
MicromedRawIO,
Expand Down
177 changes: 177 additions & 0 deletions neo/rawio/cedrawio.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion neo/rawio/spike2rawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
20 changes: 20 additions & 0 deletions neo/test/iotest/test_cedio.py
Original file line number Diff line number Diff line change
@@ -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()
21 changes: 21 additions & 0 deletions neo/test/rawiotest/test_cedrawio.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions requirements_testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ ipython
coverage
coveralls
pillow
sonpy

0 comments on commit a134ba1

Please sign in to comment.