From fa69e9a528231e6680bd3b6cf1e7dd6d1c529996 Mon Sep 17 00:00:00 2001 From: Joe Z Date: Sun, 10 Oct 2021 15:56:46 +0100 Subject: [PATCH 1/7] added wavesurferrawio --- neo/rawio/wavesurferrawio.py | 181 +++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 neo/rawio/wavesurferrawio.py diff --git a/neo/rawio/wavesurferrawio.py b/neo/rawio/wavesurferrawio.py new file mode 100644 index 000000000..4e7f2a308 --- /dev/null +++ b/neo/rawio/wavesurferrawio.py @@ -0,0 +1,181 @@ +""" +ExampleRawIO is a class of a fake example. +This is to be used when coding a new RawIO. + + +Rules for creating a new class: + 1. Step 1: Create the main class + * Create a file in **neo/rawio/** that endith with "rawio.py" + * Create the class that inherits from BaseRawIO + * copy/paste all methods that need to be implemented. + * code hard! The main difficulty is `_parse_header()`. + In short you have a create a mandatory dict than + contains channel informations:: + + self.header = {} + self.header['nb_block'] = 2 + self.header['nb_segment'] = [2, 3] + self.header['signal_streams'] = signal_streams + self.header['signal_channels'] = signal_channels + self.header['spike_channels'] = spike_channels + self.header['event_channels'] = event_channels + + 2. Step 2: RawIO test: + * create a file in neo/rawio/tests with the same name with "test_" prefix + * copy paste neo/rawio/tests/test_examplerawio.py and do the same + + 3. Step 3 : Create the neo.io class with the wrapper + * Create a file in neo/io/ that ends with "io.py" + * Create a class that inherits both your RawIO class and BaseFromRaw class + * copy/paste from neo/io/exampleio.py + + 4.Step 4 : IO test + * create a file in neo/test/iotest with the same previous name with "test_" prefix + * copy/paste from neo/test/iotest/test_exampleio.py +""" + +from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, + _spike_channel_dtype, _event_channel_dtype) + +import numpy as np + + +class WaveSurferIO(BaseRawIO): + """ + Class for "reading" fake data from an imaginary file. + + For the user, it gives access to raw data (signals, event, spikes) as they + are in the (fake) file int16 and int64. + + For a developer, it is just an example showing guidelines for someone who wants + to develop a new IO module. + + Two rules for developers: + * Respect the :ref:`neo_rawio_API` + * Follow the :ref:`io_guiline` + + This fake IO: + * has 2 blocks + * blocks have 2 and 3 segments + * has 2 signals streams of 8 channel each (sample_rate = 10000) so 16 channels in total + * has 3 spike_channels + * has 2 event channels: one has *type=event*, the other has + *type=epoch* + + + Usage: + >>> import neo.rawio + >>> r = neo.rawio.ExampleRawIO(filename='itisafake.nof') + >>> r.parse_header() + >>> print(r) + >>> raw_chunk = r.get_analogsignal_chunk(block_index=0, seg_index=0, + i_start=0, i_stop=1024, channel_names=channel_names) + >>> float_chunk = reader.rescale_signal_raw_to_float(raw_chunk, dtype='float64', + channel_indexes=[0, 3, 6]) + >>> spike_timestamp = reader.spike_timestamps(spike_channel_index=0, + t_start=None, t_stop=None) + >>> spike_times = reader.rescale_spike_timestamp(spike_timestamp, 'float64') + >>> ev_timestamps, _, ev_labels = reader.event_timestamps(event_channel_index=0) + + """ + extensions = ['fake'] + rawmode = 'one-file' + + def __init__(self, filename=''): + BaseRawIO.__init__(self) + # note that this filename is ued in self._source_name + self.filename = filename + + def _source_name(self): + # this function is used by __repr__ + # for general cases self.filename is good + # But for URL you could mask some part of the URL to keep + # the main part. + return self.filename + + def _parse_header(self): + """ + talk about scaling + """ + # TODO: add wavesurfer dependency check + import sys + sys.path.append(r"C:\fMRIData\git-repo\PyWaveSurfer") + from pywavesurfer import ws + + pyws_data = ws.loadDataFile(self.filename, format_string="double") + header = pyws_data["header"] + +# Raw Data ------------------------------------------------------------------------------------------------------------------------------------------- + # TODO: find out if its worth importthing AO and digital channels + self._raw_signals = {} + self._t_starts = {} + + for seg_index in range(int(header["NSweepsPerRun"])): + + sweep_id = "sweep_{0:04d}".format(seg_index + 1) # e.g. "sweep_0050" + self._raw_signals[seg_index] = pyws_data[sweep_id]["analogScans"].T # reshape to data x channel for Neo standard + self._t_starts[seg_index] = np.float64(pyws_data[sweep_id]["timestamp"]) # TODO: find out why native this is double-nested list for a scalar (e.g. [[time]] + +# Header --------------------------------------------------------------------------------------------------------------------------------------------- + + # Signal Channels + # For now just grab the used AI channels + signal_channels = [] + ai_channel_names = header["AIChannelNames"].astype(str) # TODO: are channel units ever entered by the user or always in standard form? + ai_channel_units = header["AIChannelUnits"].astype(str) + self._sampling_rate = np.float64(pyws_data["header"]["AcquisitionSampleRate"]) # TODO: find out why native this is double-nested list for a scalar (e.g. [[SR]] + + for ch_idx, (ch_name, ch_units) in enumerate(zip(ai_channel_names, + ai_channel_units)): + ch_id = ch_idx + 1 + dtype = "float64" # as loaded with "double" argument from PyWaveSurfer + gain = 1 + offset = 0 + stream_id = "0" # chan_id # TODO: dont understand this, for now treat all channels as the same. I think different units is fine, just not samplign rate + + signal_channels.append((ch_name, ch_id, self._sampling_rate, dtype, ch_units, gain, offset, stream_id)) + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) + + # No spikes + spike_channels = [] + spike_channels = np.array(spike_channels, dtype=_spike_channel_dtype) + + # No events TODO: I am not sure about this. Timestamps are in each segment (?) + event_channels = [] + event_channels = np.array(event_channels, dtype=_event_channel_dtype) + + # FIND OUT: what is U64, dtype and ID. + # Sampling rate is always unique. But units are different across channels. Presume this is okay based on axonrawio. + signal_streams = np.array([('Signals', '0')], dtype=_signal_stream_dtype) # TODO: maybe these are split at a later level?? Do not understand this, copied from AxonIO + + self.header = {} + self.header['nb_block'] = 1 + self.header['nb_segment'] = [int(header["NSweepsPerRun"])] + 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() # TODO: return to this, # TODO: ADD ANNOTATIONS + + def _segment_t_start(self, block_index, seg_index): # TODO: check these timestamps are definately time that starts # ASK + return self._t_starts[seg_index] + + def _segment_t_stop(self, block_index, seg_index): + t_stop = self._t_starts[seg_index] + \ + self._raw_signals[seg_index].shape[0] / self._sampling_rate + return t_stop + + def _get_signal_size(self, block_index, seg_index, stream_index): + shape = self._raw_signals[seg_index].shape + return shape[0] + + def _get_signal_t_start(self, block_index, seg_index, stream_index): # TODO: check several samplign rates are not supported in WaveSurfer + return self._t_starts[seg_index] + + def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, + stream_index, channel_indexes): + if channel_indexes is None: + channel_indexes = slice(None) + raw_signals = self._raw_signals[seg_index][slice(i_start, i_stop), channel_indexes] + return raw_signals From 5318ec717051c187e77beee876da22c90734751e Mon Sep 17 00:00:00 2001 From: Joe Z Date: Sun, 10 Oct 2021 16:07:57 +0100 Subject: [PATCH 2/7] added wavesurferrawio to rawio __init__ --- neo/rawio/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/neo/rawio/__init__.py b/neo/rawio/__init__.py index 9e74fda5a..ae3fc278f 100644 --- a/neo/rawio/__init__.py +++ b/neo/rawio/__init__.py @@ -37,6 +37,7 @@ * :attr:`SpikeGadgetsRawIO` * :attr:`SpikeGLXRawIO` * :attr:`TdtRawIO` +* :attr:`WaveSurferRawIO` * :attr:`WinEdrRawIO` * :attr:`WinWcpRawIO` @@ -141,6 +142,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.rawio.WaveSurferRawIO + + .. autoattribute:: extensions + .. autoclass:: neo.rawio.WinEdrRawIO .. autoattribute:: extensions @@ -178,6 +183,7 @@ from neo.rawio.spikegadgetsrawio import SpikeGadgetsRawIO from neo.rawio.spikeglxrawio import SpikeGLXRawIO from neo.rawio.tdtrawio import TdtRawIO +from neo.rawio.wavesurferrawio import WaveSurferRawIO from neo.rawio.winedrrawio import WinEdrRawIO from neo.rawio.winwcprawio import WinWcpRawIO @@ -207,6 +213,7 @@ SpikeGadgetsRawIO, SpikeGLXRawIO, TdtRawIO, + WaveSurferRawIO, WinEdrRawIO, WinWcpRawIO, ] From beced3ab6890d876f970953d257e44c4e67e3c86 Mon Sep 17 00:00:00 2001 From: Joe Z Date: Sun, 10 Oct 2021 16:13:54 +0100 Subject: [PATCH 3/7] added wavesurferio to io __init__ --- neo/io/wavesurferio.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 neo/io/wavesurferio.py diff --git a/neo/io/wavesurferio.py b/neo/io/wavesurferio.py new file mode 100644 index 000000000..9829693a0 --- /dev/null +++ b/neo/io/wavesurferio.py @@ -0,0 +1,29 @@ +""" +neo.io have been split in 2 level API: + * neo.io: this API give neo object + * neo.rawio: this API give raw data as they are in files. + +Developper are encourage to use neo.rawio. + +When this is done the neo.io is done automagically with +this king of following code. + +Author: sgarcia + +""" + +from neo.io.basefromrawio import BaseFromRaw +from neo.rawio.wavesurferrawio import WaveSurferRawIO + +class WaveSurferIO(WaveSurferRawIO, BaseFromRaw): + name = 'example IO' + description = "Fake IO" + + # This is an inportant choice when there are several channels. + # 'split-all' : 1 AnalogSignal each 1 channel + # 'group-by-same-units' : one 2D AnalogSignal for each group of channel with same units + _prefered_signal_group_mode = "split-all" + + def __init__(self, filename=''): + WaveSurferRawIO.__init__(self, filename=filename) + BaseFromRaw.__init__(self, filename) From 27ae6125fddcb0830ea87566daf9f146d0424da8 Mon Sep 17 00:00:00 2001 From: Joe Z Date: Thu, 14 Oct 2021 00:16:21 +0100 Subject: [PATCH 4/7] tidied up wavesurfer rawio and added import check --- neo/rawio/wavesurferrawio.py | 129 ++++++++++++----------------------- 1 file changed, 44 insertions(+), 85 deletions(-) diff --git a/neo/rawio/wavesurferrawio.py b/neo/rawio/wavesurferrawio.py index 4e7f2a308..3dddf9d31 100644 --- a/neo/rawio/wavesurferrawio.py +++ b/neo/rawio/wavesurferrawio.py @@ -1,34 +1,9 @@ """ -ExampleRawIO is a class of a fake example. -This is to be used when coding a new RawIO. - - -Rules for creating a new class: - 1. Step 1: Create the main class - * Create a file in **neo/rawio/** that endith with "rawio.py" - * Create the class that inherits from BaseRawIO - * copy/paste all methods that need to be implemented. - * code hard! The main difficulty is `_parse_header()`. - In short you have a create a mandatory dict than - contains channel informations:: - - self.header = {} - self.header['nb_block'] = 2 - self.header['nb_segment'] = [2, 3] - self.header['signal_streams'] = signal_streams - self.header['signal_channels'] = signal_channels - self.header['spike_channels'] = spike_channels - self.header['event_channels'] = event_channels 2. Step 2: RawIO test: - * create a file in neo/rawio/tests with the same name with "test_" prefix + * create a file in neo/rawio/tests with the same name with "test_" prefix ####################### TODO (email people) * copy paste neo/rawio/tests/test_examplerawio.py and do the same - 3. Step 3 : Create the neo.io class with the wrapper - * Create a file in neo/io/ that ends with "io.py" - * Create a class that inherits both your RawIO class and BaseFromRaw class - * copy/paste from neo/io/exampleio.py - 4.Step 4 : IO test * create a file in neo/test/iotest with the same previous name with "test_" prefix * copy/paste from neo/test/iotest/test_exampleio.py @@ -36,94 +11,78 @@ from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, _spike_channel_dtype, _event_channel_dtype) - import numpy as np +try: + from pywavesurfer import ws +except ImportError as err: + HAS_PYWAVESURFER = False + PYWAVESURFER_ERR = err +else: + HAS_PYWAVESURFER = True + PYWAVESURFER_ERR = None -class WaveSurferIO(BaseRawIO): +class WaveSurferRawIO(BaseRawIO): """ - Class for "reading" fake data from an imaginary file. - - For the user, it gives access to raw data (signals, event, spikes) as they - are in the (fake) file int16 and int64. - - For a developer, it is just an example showing guidelines for someone who wants - to develop a new IO module. - Two rules for developers: * Respect the :ref:`neo_rawio_API` * Follow the :ref:`io_guiline` - - This fake IO: - * has 2 blocks - * blocks have 2 and 3 segments - * has 2 signals streams of 8 channel each (sample_rate = 10000) so 16 channels in total - * has 3 spike_channels - * has 2 event channels: one has *type=event*, the other has - *type=epoch* - - - Usage: - >>> import neo.rawio - >>> r = neo.rawio.ExampleRawIO(filename='itisafake.nof') - >>> r.parse_header() - >>> print(r) - >>> raw_chunk = r.get_analogsignal_chunk(block_index=0, seg_index=0, - i_start=0, i_stop=1024, channel_names=channel_names) - >>> float_chunk = reader.rescale_signal_raw_to_float(raw_chunk, dtype='float64', - channel_indexes=[0, 3, 6]) - >>> spike_timestamp = reader.spike_timestamps(spike_channel_index=0, - t_start=None, t_stop=None) - >>> spike_times = reader.rescale_spike_timestamp(spike_timestamp, 'float64') - >>> ev_timestamps, _, ev_labels = reader.event_timestamps(event_channel_index=0) - """ extensions = ['fake'] rawmode = 'one-file' def __init__(self, filename=''): BaseRawIO.__init__(self) - # note that this filename is ued in self._source_name self.filename = filename + if not HAS_PYWAVESURFER: + raise PYWAVESURFER_ERR + def _source_name(self): - # this function is used by __repr__ - # for general cases self.filename is good - # But for URL you could mask some part of the URL to keep - # the main part. return self.filename def _parse_header(self): """ talk about scaling + + WAVESURFER + + 1) ask about the files at do not work in the test + 2) ask if it is okay to upload the test files (e.g. test2) to the website + 3) ask if single sampling rate only possible + 4) # TODO: are channel units ever entered by the user or always in standard form? + 5 TODO: check these timestamps are definately time that starts # ASK + 6) # TODO: find out why native this is double-nested list for a scalar (e.g. [[time]] (dont ask) + + NEO + 1) document this well and IO + 2) ask about and upload to tests, push to repo + 3) ask if required to handle AI, DI and DO + 4) sampling streams: # TODO: dont understand this, for now treat all channels as the same. I think different units is fine, just not samplign rate. # TODO: maybe these are split at a later level?? Do not understand this, copied from AxonIO # Sampling rate is always unique. But units are different across channels. Presume this is okay based on axonrawio. + 5) double check events channel (No events TODO: I am not sure about this. Timestamps are in each segment (?)) + """ - # TODO: add wavesurfer dependency check import sys sys.path.append(r"C:\fMRIData\git-repo\PyWaveSurfer") - from pywavesurfer import ws pyws_data = ws.loadDataFile(self.filename, format_string="double") header = pyws_data["header"] -# Raw Data ------------------------------------------------------------------------------------------------------------------------------------------- - # TODO: find out if its worth importthing AO and digital channels + # Raw Data self._raw_signals = {} self._t_starts = {} for seg_index in range(int(header["NSweepsPerRun"])): - sweep_id = "sweep_{0:04d}".format(seg_index + 1) # e.g. "sweep_0050" + sweep_id = "sweep_{0:04d}".format(seg_index + 1) # e.g. "sweep_0050" self._raw_signals[seg_index] = pyws_data[sweep_id]["analogScans"].T # reshape to data x channel for Neo standard - self._t_starts[seg_index] = np.float64(pyws_data[sweep_id]["timestamp"]) # TODO: find out why native this is double-nested list for a scalar (e.g. [[time]] - -# Header --------------------------------------------------------------------------------------------------------------------------------------------- + self._t_starts[seg_index] = np.float64(pyws_data[sweep_id]["timestamp"]) # Signal Channels - # For now just grab the used AI channels signal_channels = [] - ai_channel_names = header["AIChannelNames"].astype(str) # TODO: are channel units ever entered by the user or always in standard form? + ai_channel_names = header["AIChannelNames"].astype(str) ai_channel_units = header["AIChannelUnits"].astype(str) - self._sampling_rate = np.float64(pyws_data["header"]["AcquisitionSampleRate"]) # TODO: find out why native this is double-nested list for a scalar (e.g. [[SR]] + self._sampling_rate = np.float64(pyws_data["header"]["AcquisitionSampleRate"]) for ch_idx, (ch_name, ch_units) in enumerate(zip(ai_channel_names, ai_channel_units)): @@ -131,23 +90,23 @@ def _parse_header(self): dtype = "float64" # as loaded with "double" argument from PyWaveSurfer gain = 1 offset = 0 - stream_id = "0" # chan_id # TODO: dont understand this, for now treat all channels as the same. I think different units is fine, just not samplign rate - + stream_id = "0" # chan_id signal_channels.append((ch_name, ch_id, self._sampling_rate, dtype, ch_units, gain, offset, stream_id)) + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) - # No spikes + # Spike Channels (no spikes) spike_channels = [] spike_channels = np.array(spike_channels, dtype=_spike_channel_dtype) - # No events TODO: I am not sure about this. Timestamps are in each segment (?) + # Event Channels (no events) event_channels = [] event_channels = np.array(event_channels, dtype=_event_channel_dtype) - # FIND OUT: what is U64, dtype and ID. - # Sampling rate is always unique. But units are different across channels. Presume this is okay based on axonrawio. - signal_streams = np.array([('Signals', '0')], dtype=_signal_stream_dtype) # TODO: maybe these are split at a later level?? Do not understand this, copied from AxonIO + # Signal Streams + signal_streams = np.array([('Signals', '0')], dtype=_signal_stream_dtype) + # Header Dict self.header = {} self.header['nb_block'] = 1 self.header['nb_segment'] = [int(header["NSweepsPerRun"])] @@ -158,7 +117,7 @@ def _parse_header(self): self._generate_minimal_annotations() # TODO: return to this, # TODO: ADD ANNOTATIONS - def _segment_t_start(self, block_index, seg_index): # TODO: check these timestamps are definately time that starts # ASK + def _segment_t_start(self, block_index, seg_index): return self._t_starts[seg_index] def _segment_t_stop(self, block_index, seg_index): @@ -170,7 +129,7 @@ def _get_signal_size(self, block_index, seg_index, stream_index): shape = self._raw_signals[seg_index].shape return shape[0] - def _get_signal_t_start(self, block_index, seg_index, stream_index): # TODO: check several samplign rates are not supported in WaveSurfer + def _get_signal_t_start(self, block_index, seg_index, stream_index): return self._t_starts[seg_index] def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, From 29dfe3cbebbbdadda0daa3af85a394a9c141603e Mon Sep 17 00:00:00 2001 From: Joe Z Date: Sat, 16 Oct 2021 21:36:41 +0100 Subject: [PATCH 5/7] update neo/io/__init__.py --- neo/io/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/neo/io/__init__.py b/neo/io/__init__.py index 6d10278ff..6ee8a0801 100644 --- a/neo/io/__init__.py +++ b/neo/io/__init__.py @@ -58,6 +58,7 @@ * :attr:`StimfitIO` * :attr:`TdtIO` * :attr:`TiffIO` +* :attr:`WaveSurferIO` * :attr:`WinEdrIO` * :attr:`WinWcpIO` @@ -242,6 +243,10 @@ .. autoattribute:: extensions +.. autoclass:: neo.io.WaveSurferIO + + .. autoattribute:: extensions + .. autoclass:: neo.io.WinEdrIO .. autoattribute:: extensions @@ -316,6 +321,7 @@ from neo.io.stimfitio import StimfitIO from neo.io.tdtio import TdtIO from neo.io.tiffio import TiffIO +from neo.io.wavesurferio import WaveSurferIO from neo.io.winedrio import WinEdrIO from neo.io.winwcpio import WinWcpIO @@ -366,6 +372,7 @@ StimfitIO, TdtIO, TiffIO, + WaveSurferIO, WinEdrIO, WinWcpIO ] From 171e5cb6f4a7c1537b6984d3a6db4822ffb008c6 Mon Sep 17 00:00:00 2001 From: Joe Z Date: Sat, 16 Oct 2021 21:54:24 +0100 Subject: [PATCH 6/7] final tidy up and list TODO and to discuss in header --- neo/rawio/wavesurferrawio.py | 56 ++++++++++++++---------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/neo/rawio/wavesurferrawio.py b/neo/rawio/wavesurferrawio.py index 3dddf9d31..05f36ca00 100644 --- a/neo/rawio/wavesurferrawio.py +++ b/neo/rawio/wavesurferrawio.py @@ -1,12 +1,26 @@ """ +In development 16/10/2021 - 2. Step 2: RawIO test: - * create a file in neo/rawio/tests with the same name with "test_" prefix ####################### TODO (email people) - * copy paste neo/rawio/tests/test_examplerawio.py and do the same +Class for reading data from WaveSurfer, a software written by +Boaz Mohar and Adam Taylor https://wavesurfer.janelia.org/ - 4.Step 4 : IO test - * create a file in neo/test/iotest with the same previous name with "test_" prefix - * copy/paste from neo/test/iotest/test_exampleio.py +Requires the PyWaveSurfer module written by Boaz Mohar and Adam Taylor. + +To Discuss: +- Wavesurfer also has analog output, and digital input / output channels, but here only supported analog input. Is this okay? +- I believe the signal streams field is configured correctly here, used AxonRawIO as a guide. +- each segment (sweep) has it's own timestamp, so I beleive no events_signals is correct (similar to winwcprawio not axonrawio) + +1) Upload test files (kindly provided by Boaz Mohar and Adam Taylor) to g-node portal +2) write RawIO and IO tests + +2. Step 2: RawIO test: +* create a file in neo/rawio/tests with the same name with "test_" prefix +* copy paste neo/rawio/tests/test_examplerawio.py and do the same + +4.Step 4 : IO test +* create a file in neo/test/iotest with the same previous name with "test_" prefix +* copy/paste from neo/test/iotest/test_exampleio.py """ from .baserawio import (BaseRawIO, _signal_channel_dtype, _signal_stream_dtype, @@ -23,11 +37,7 @@ PYWAVESURFER_ERR = None class WaveSurferRawIO(BaseRawIO): - """ - Two rules for developers: - * Respect the :ref:`neo_rawio_API` - * Follow the :ref:`io_guiline` - """ + extensions = ['fake'] rawmode = 'one-file' @@ -42,28 +52,6 @@ def _source_name(self): return self.filename def _parse_header(self): - """ - talk about scaling - - WAVESURFER - - 1) ask about the files at do not work in the test - 2) ask if it is okay to upload the test files (e.g. test2) to the website - 3) ask if single sampling rate only possible - 4) # TODO: are channel units ever entered by the user or always in standard form? - 5 TODO: check these timestamps are definately time that starts # ASK - 6) # TODO: find out why native this is double-nested list for a scalar (e.g. [[time]] (dont ask) - - NEO - 1) document this well and IO - 2) ask about and upload to tests, push to repo - 3) ask if required to handle AI, DI and DO - 4) sampling streams: # TODO: dont understand this, for now treat all channels as the same. I think different units is fine, just not samplign rate. # TODO: maybe these are split at a later level?? Do not understand this, copied from AxonIO # Sampling rate is always unique. But units are different across channels. Presume this is okay based on axonrawio. - 5) double check events channel (No events TODO: I am not sure about this. Timestamps are in each segment (?)) - - """ - import sys - sys.path.append(r"C:\fMRIData\git-repo\PyWaveSurfer") pyws_data = ws.loadDataFile(self.filename, format_string="double") header = pyws_data["header"] @@ -115,7 +103,7 @@ def _parse_header(self): self.header['spike_channels'] = spike_channels self.header['event_channels'] = event_channels - self._generate_minimal_annotations() # TODO: return to this, # TODO: ADD ANNOTATIONS + self._generate_minimal_annotations() # TODO: return to this and add annotations def _segment_t_start(self, block_index, seg_index): return self._t_starts[seg_index] From 6be83eccaa18fa0939d16e0a249701939edc475a Mon Sep 17 00:00:00 2001 From: Joe Z Date: Wed, 17 Nov 2021 22:30:27 +0000 Subject: [PATCH 7/7] updated to wrap pywavesurfer in the io, in preparation for discarding rawio version --- neo/io/wavesurferio.py | 150 +++++++++++++++++++++++++++++++++++------ 1 file changed, 130 insertions(+), 20 deletions(-) diff --git a/neo/io/wavesurferio.py b/neo/io/wavesurferio.py index 9829693a0..c5fd78f5c 100644 --- a/neo/io/wavesurferio.py +++ b/neo/io/wavesurferio.py @@ -1,29 +1,139 @@ """ -neo.io have been split in 2 level API: - * neo.io: this API give neo object - * neo.rawio: this API give raw data as they are in files. +Class for reading data from WaveSurfer, a software written by +Boaz Mohar and Adam Taylor https://wavesurfer.janelia.org/ -Developper are encourage to use neo.rawio. +This is a wrapper around the PyWaveSurfer module written by Boaz Mohar and Adam Taylor, +using the "double" argument to load the data as 64-bit double. +""" +import numpy as np +import quantities as pq -When this is done the neo.io is done automagically with -this king of following code. +from neo.io.baseio import BaseIO +from neo.core import Block, Segment, AnalogSignal +from ..rawio.baserawio import _signal_channel_dtype, _signal_stream_dtype, _spike_channel_dtype, _event_channel_dtype # TODO: not sure about this # from ..rawio. -Author: sgarcia +try: + from pywavesurfer import ws +except ImportError as err: + HAS_PYWAVESURFER = False + PYWAVESURFER_ERR = err +else: + HAS_PYWAVESURFER = True + PYWAVESURFER_ERR = None -""" -from neo.io.basefromrawio import BaseFromRaw -from neo.rawio.wavesurferrawio import WaveSurferRawIO +class WaveSurferIO(BaseIO): + """ + """ + + is_readable = True + is_writable = False + + supported_objects = [Block, Segment, AnalogSignal] + readable_objects = [Block] + writeable_objects = [] + + has_header = True + is_streameable = False + + read_params = {Block: []} + write_params = None + + name = 'WaveSurfer' + extensions = ['.h5'] + + mode = 'file' + + def __init__(self, filename=None): + """ + Arguments: + filename : a filename + """ + if not HAS_PYWAVESURFER: + raise PYWAVESURFER_ERR + + BaseIO.__init__(self) + + self.filename = filename + self.ws_rec = None + self.header = {} + self._sampling_rate = None + self.ai_channel_names = None + self.ai_channel_units = None + + self.read_block() + + def read_block(self, lazy=False): + assert not lazy, 'Do not support lazy' + + self.ws_rec = ws.loadDataFile(self.filename, format_string="double") + + ai_channel_names = self.ws_rec["header"]["AIChannelNames"].astype(str) + ai_channel_units = self.ws_rec["header"]["AIChannelUnits"].astype(str) + sampling_rate = np.float64(self.ws_rec["header"]["AcquisitionSampleRate"]) * 1 / pq.s + + self.fill_header(ai_channel_names, + ai_channel_units) + + bl = Block() + + # iterate over sections first: + for seg_index in range(int(self.ws_rec["header"]["NSweepsPerRun"])): + + seg = Segment(index=seg_index) + seg_id = "sweep_{0:04d}".format(seg_index + 1) # e.g. "sweep_0050" + + ws_seg = self.ws_rec[seg_id] + t_start = np.float64(ws_seg["timestamp"]) * pq.s + + # iterate over channels: + for chan_idx, recsig in enumerate(ws_seg["analogScans"]): + + unit = ai_channel_units[chan_idx] + name = ai_channel_names[chan_idx] + + signal = pq.Quantity(recsig, unit).T + + anaSig = AnalogSignal(signal, sampling_rate=sampling_rate, + t_start=t_start, name=name, + channel_index=chan_idx) + seg.analogsignals.append(anaSig) + bl.segments.append(seg) + + bl.create_many_to_one_relationship() + + return bl + + def fill_header(self, ai_channel_names, ai_channel_units): + + signal_channels = [] + + for ch_idx, (ch_name, ch_units) in enumerate(zip(ai_channel_names, + ai_channel_units)): + ch_id = ch_idx + 1 + dtype = "float64" # as loaded with "double" argument from PyWaveSurfer + gain = 1 + offset = 0 + stream_id = "0" + signal_channels.append((ch_name, ch_id, self._sampling_rate, dtype, ch_units, gain, offset, stream_id)) + + signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype) + + # Spike Channels (no spikes) + spike_channels = [] + spike_channels = np.array(spike_channels, dtype=_spike_channel_dtype) -class WaveSurferIO(WaveSurferRawIO, BaseFromRaw): - name = 'example IO' - description = "Fake IO" + # Event Channels (no events) + event_channels = [] + event_channels = np.array(event_channels, dtype=_event_channel_dtype) - # This is an inportant choice when there are several channels. - # 'split-all' : 1 AnalogSignal each 1 channel - # 'group-by-same-units' : one 2D AnalogSignal for each group of channel with same units - _prefered_signal_group_mode = "split-all" + # Signal Streams + signal_streams = np.array([('Signals', '0')], dtype=_signal_stream_dtype) - def __init__(self, filename=''): - WaveSurferRawIO.__init__(self, filename=filename) - BaseFromRaw.__init__(self, filename) + # Header Dict + self.header['nb_block'] = 1 + self.header['nb_segment'] = [int(self.ws_rec["header"]["NSweepsPerRun"])] + self.header['signal_streams'] = signal_streams + self.header['signal_channels'] = signal_channels + self.header['spike_channels'] = spike_channels + self.header['event_channels'] = event_channels