blackrockio_deprecated.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. # -*- coding: utf-8 -*-
  2. """
  3. Module for reading binary file from Blackrock format.
  4. """
  5. import logging
  6. import struct
  7. import numpy as np
  8. import quantities as pq
  9. from neo.io.baseio import BaseIO
  10. from neo.core import (Block, Segment,
  11. RecordingChannel, ChannelIndex, AnalogSignal)
  12. from neo.io import tools
  13. class BlackrockIO(BaseIO):
  14. """
  15. Class for reading/writing data in a BlackRock Neuroshare ns5 files.
  16. """
  17. # Class variables demonstrating capabilities of this IO
  18. is_readable = True # This a only reading class
  19. is_writable = True # write is not supported
  20. # This IO can only manipulate continuous data, not spikes or events
  21. supported_objects = [Block, Segment, AnalogSignal, ChannelIndex, RecordingChannel]
  22. # Keep things simple by always returning a block
  23. readable_objects = [Block]
  24. # And write a block
  25. writeable_objects = [Block]
  26. # Not sure what these do, if anything
  27. has_header = False
  28. is_streameable = False
  29. # The IO name and the file extensions it uses
  30. name = 'Blackrock'
  31. extensions = ['ns5']
  32. # Operates on *.ns5 files
  33. mode = 'file'
  34. # GUI defaults for reading
  35. # Most information is acquired from the file header.
  36. read_params = {
  37. Block: [
  38. #('rangemin' , { 'value' : -10 } ),
  39. #('rangemax' , { 'value' : 10 } ),
  40. ]
  41. }
  42. # GUI defaults for writing (not supported)
  43. write_params = None
  44. def __init__(self, filename, full_range=8192.*pq.mV) :
  45. """Initialize Blackrock reader.
  46. **Arguments**
  47. filename: string, the filename to read
  48. full_range: Quantity, the full-scale analog range of the data.
  49. This is set by your digitizing hardware. It should be in
  50. volts or millivolts.
  51. """
  52. BaseIO.__init__(self)
  53. self.filename = filename
  54. self.full_range = full_range
  55. # The reading methods. The `lazy` and `cascade` parameters are imposed
  56. # by neo.io API
  57. def read_block(self, lazy=False, cascade=True,
  58. n_starts=None, n_stops=None, channel_list=None):
  59. """Reads the file and returns contents as a Block.
  60. The Block contains one Segment for each entry in zip(n_starts,
  61. n_stops). If these parameters are not specified, the default is
  62. to store all data in one Segment.
  63. The Block also contains one ChannelIndex for all channels.
  64. n_starts: list or array of starting times of each Segment in
  65. samples from the beginning of the file.
  66. n_stops: similar, stopping times of each Segment
  67. channel_list: list of channel numbers to get. The neural data channels
  68. are 1 - 128. The analog inputs are 129 - 144. The default
  69. is to acquire all channels.
  70. Returns: Block object containing the data.
  71. """
  72. # Create block
  73. block = Block(file_origin=self.filename)
  74. if not cascade:
  75. return block
  76. self.loader = Loader(self.filename)
  77. self.loader.load_file()
  78. self.header = self.loader.header
  79. # If channels not specified, get all
  80. if channel_list is None:
  81. channel_list = self.loader.get_neural_channel_numbers()
  82. # If not specified, load all as one Segment
  83. if n_starts is None:
  84. n_starts = [0]
  85. n_stops = [self.loader.header.n_samples]
  86. #~ # Add channel hierarchy
  87. #~ chx = ChannelIndex(name='allchannels',
  88. #~ description='group of all channels', file_origin=self.filename)
  89. #~ block.channel_indexes.append(chx)
  90. #~ self.channel_number_to_recording_channel = {}
  91. #~ # Add each channel at a time to hierarchy
  92. #~ for ch in channel_list:
  93. #~ ch_object = RecordingChannel(name='channel%d' % ch,
  94. #~ file_origin=self.filename, index=ch)
  95. #~ chx.index.append(ch_object.index)
  96. #~ chx.channel_names.append(ch_object.name)
  97. #~ chx.recordingchannels.append(ch_object)
  98. #~ self.channel_number_to_recording_channel[ch] = ch_object
  99. # Iterate through n_starts and n_stops and add one Segment
  100. # per each.
  101. for n, (t1, t2) in enumerate(zip(n_starts, n_stops)):
  102. # Create segment and add metadata
  103. seg = self.read_segment(n_start=t1, n_stop=t2, chlist=channel_list,
  104. lazy=lazy, cascade=cascade)
  105. seg.name = 'Segment %d' % n
  106. seg.index = n
  107. t1sec = t1 / self.loader.header.f_samp
  108. t2sec = t2 / self.loader.header.f_samp
  109. seg.description = 'Segment %d from %f to %f' % (n, t1sec, t2sec)
  110. # Link to block
  111. block.segments.append(seg)
  112. # Create hardware view, and bijectivity
  113. tools.populate_RecordingChannel(block)
  114. block.create_many_to_one_relationship()
  115. return block
  116. def read_segment(self, n_start, n_stop, chlist=None, lazy=False, cascade=True):
  117. """Reads a Segment from the file and stores in database.
  118. The Segment will contain one AnalogSignal for each channel
  119. and will go from n_start to n_stop (in samples).
  120. Arguments:
  121. n_start : time in samples that the Segment begins
  122. n_stop : time in samples that the Segment ends
  123. Python indexing is used, so n_stop is not inclusive.
  124. Returns a Segment object containing the data.
  125. """
  126. # If no channel numbers provided, get all of them
  127. if chlist is None:
  128. chlist = self.loader.get_neural_channel_numbers()
  129. # Conversion from bits to full_range units
  130. conversion = self.full_range / 2**(8*self.header.sample_width)
  131. # Create the Segment
  132. seg = Segment(file_origin=self.filename)
  133. t_start = float(n_start) / self.header.f_samp
  134. t_stop = float(n_stop) / self.header.f_samp
  135. seg.annotate(t_start=t_start)
  136. seg.annotate(t_stop=t_stop)
  137. # Load data from each channel and store
  138. for ch in chlist:
  139. if lazy:
  140. sig = np.array([]) * conversion
  141. else:
  142. # Get the data from the loader
  143. sig = np.array(\
  144. self.loader._get_channel(ch)[n_start:n_stop]) * conversion
  145. # Create an AnalogSignal with the data in it
  146. anasig = AnalogSignal(signal=sig,
  147. sampling_rate=self.header.f_samp*pq.Hz,
  148. t_start=t_start*pq.s, file_origin=self.filename,
  149. description='Channel %d from %f to %f' % (ch, t_start, t_stop),
  150. channel_index=int(ch))
  151. if lazy:
  152. anasig.lazy_shape = n_stop-n_start
  153. # Link the signal to the segment
  154. seg.analogsignals.append(anasig)
  155. # Link the signal to the recording channel from which it came
  156. #rc = self.channel_number_to_recording_channel[ch]
  157. #rc.analogsignals.append(anasig)
  158. return seg
  159. def write_block(self, block):
  160. """Writes block to `self.filename`.
  161. *.ns5 BINARY FILE FORMAT
  162. The following information is contained in the first part of the header
  163. file.
  164. The size in bytes, the variable name, the data type, and the meaning are
  165. given below. Everything is little-endian.
  166. 8B. File_Type_ID. char. Always "NEURALSG"
  167. 16B. File_Spec. char. Always "30 kS/s\0"
  168. 4B. Period. uint32. Always 1.
  169. 4B. Channel_Count. uint32. Generally 32 or 34.
  170. Channel_Count*4B. uint32. Channel_ID. One uint32 for each channel.
  171. Thus the total length of the header is 8+16+4+4+Channel_Count*4.
  172. Immediately after this header, the raw data begins.
  173. Each sample is a 2B signed int16.
  174. For our hardware, the conversion factor is 4096.0 / 2**16 mV/bit.
  175. The samples for each channel are interleaved, so the first Channel_Count
  176. samples correspond to the first sample from each channel, in the same
  177. order as the channel id's in the header.
  178. Variable names are consistent with the Neuroshare specification.
  179. """
  180. fi = open(self.filename, 'wb')
  181. self._write_header(block, fi)
  182. # Write each segment in order
  183. for seg in block.segments:
  184. # Create a 2d numpy array of analogsignals converted to bytes
  185. all_signals = np.array([
  186. np.rint(sig * 2**16 / self.full_range)
  187. for sig in seg.analogsignals],
  188. dtype=np.int)
  189. # Write to file. We transpose because channel changes faster
  190. # than time in this format.
  191. for vals in all_signals.transpose():
  192. fi.write(struct.pack('<%dh' % len(vals), *vals))
  193. fi.close()
  194. def _write_header(self, block, fi):
  195. """Write header info about block to fi"""
  196. if len(block.segments) > 0:
  197. channel_indexes = channel_indexes_in_segment(block.segments[0])
  198. else:
  199. channel_indexes = []
  200. # type of file
  201. fi.write('NEURALSG')
  202. # sampling rate, in text and integer
  203. fi.write('30 kS/s\0')
  204. for _ in range(8): fi.write('\0')
  205. fi.write(struct.pack('<I', 1))
  206. # channel count: one for each analogsignal, and then also for
  207. # each column in each analogsignalarray
  208. fi.write(struct.pack('<I', len(channel_indexes)))
  209. for chidx in channel_indexes:
  210. fi.write(struct.pack('<I', chidx))
  211. def channel_indexes_in_segment(seg):
  212. """List channel indexes of analogsignals and analogsignalarrays"""
  213. channel_indices = []
  214. for sig in seg.analogsignals:
  215. channel_indices.append(sig.recordingchannel.index)
  216. for asa in seg.analogsignals:
  217. channel_indices.append(asa.channel_index.index)
  218. return channel_indices
  219. class HeaderInfo:
  220. """Holds information from the ns5 file header about the file."""
  221. pass
  222. class Loader(object):
  223. """Object to load data from binary ns5 files.
  224. Methods
  225. -------
  226. load_file : actually create links to file on disk
  227. load_header : load header info and store in self.header
  228. get_channel_as_array : Returns 1d numpy array of the entire recording
  229. from requested channel.
  230. get_analog_channel_as_array : Same as get_channel_as_array, but works
  231. on analog channels rather than neural channels.
  232. get_analog_channel_ids : Returns an array of analog channel numbers
  233. existing in the file.
  234. get_neural_channel_ids : Returns an array of neural channel numbers
  235. existing in the file.
  236. regenerate_memmap : Deletes and restores the underlying memmap, which
  237. may free up memory.
  238. Issues
  239. ------
  240. Memory leaks may exist
  241. Not sure that regenerate_memmap actually frees up any memory.
  242. """
  243. def __init__(self, filename=None):
  244. """Creates a new object to load data from the ns5 file you specify.
  245. filename : path to ns5 file
  246. Call load_file() to actually get data from the file.
  247. """
  248. self.filename = filename
  249. self._mm = None
  250. self.file_handle = None
  251. def load_file(self, filename=None):
  252. """Loads an ns5 file, if not already done.
  253. *.ns5 BINARY FILE FORMAT
  254. The following information is contained in the first part of the header
  255. file.
  256. The size in bytes, the variable name, the data type, and the meaning are
  257. given below. Everything is little-endian.
  258. 8B. File_Type_ID. char. Always "NEURALSG"
  259. 16B. File_Spec. char. Always "30 kS/s\0"
  260. 4B. Period. uint32. Always 1.
  261. 4B. Channel_Count. uint32. Generally 32 or 34.
  262. Channel_Count*4B. uint32. Channel_ID. One uint32 for each channel.
  263. Thus the total length of the header is 8+16+4+4+Channel_Count*4.
  264. Immediately after this header, the raw data begins.
  265. Each sample is a 2B signed int16.
  266. For our hardware, the conversion factor is 4096.0 / 2**16 mV/bit.
  267. The samples for each channel are interleaved, so the first Channel_Count
  268. samples correspond to the first sample from each channel, in the same
  269. order as the channel id's in the header.
  270. Variable names are consistent with the Neuroshare specification.
  271. """
  272. # If filename specified, use it, else use previously specified
  273. if filename is not None: self.filename = filename
  274. # Load header info into self.header
  275. self.load_header()
  276. # build an internal memmap linking to the data on disk
  277. self.regenerate_memmap()
  278. def load_header(self, filename=None):
  279. """Reads ns5 file header and writes info to self.header"""
  280. # (Re-)initialize header
  281. self.header = HeaderInfo()
  282. # the width of each sample is always 2 bytes
  283. self.header.sample_width = 2
  284. # If filename specified, use it, else use previously specified
  285. if filename is not None: self.filename = filename
  286. self.header.filename = self.filename
  287. # first load the binary in directly
  288. self.file_handle = open(self.filename, 'rb') # buffering=?
  289. # Read File_Type_ID and check compatibility
  290. # If v2.2 is used, this value will be 'NEURALCD', which uses a slightly
  291. # more complex header. Currently unsupported.
  292. self.header.File_Type_ID = [chr(ord(c)) \
  293. for c in self.file_handle.read(8)]
  294. if "".join(self.header.File_Type_ID) != 'NEURALSG':
  295. logging.info( "Incompatible ns5 file format. Only v2.1 is supported.\nThis will probably not work.")
  296. # Read File_Spec and check compatibility.
  297. self.header.File_Spec = [chr(ord(c)) \
  298. for c in self.file_handle.read(16)]
  299. if "".join(self.header.File_Spec[:8]) != '30 kS/s\0':
  300. logging.info( "File_Spec seems to indicate you did not sample at 30KHz.")
  301. #R ead Period and verify that 30KHz was used. If not, the code will
  302. # still run but it's unlikely the data will be useful.
  303. self.header.period, = struct.unpack('<I', self.file_handle.read(4))
  304. if self.header.period != 1:
  305. logging.info( "Period seems to indicate you did not sample at 30KHz.")
  306. self.header.f_samp = self.header.period * 30000.0
  307. # Read Channel_Count and Channel_ID
  308. self.header.Channel_Count, = struct.unpack('<I',
  309. self.file_handle.read(4))
  310. self.header.Channel_ID = [struct.unpack('<I',
  311. self.file_handle.read(4))[0]
  312. for _ in xrange(self.header.Channel_Count)]
  313. # Compute total header length
  314. self.header.Header = 8 + 16 + 4 + 4 + \
  315. 4*self.header.Channel_Count # in bytes
  316. # determine length of file
  317. self.file_handle.seek(0, 2) # last byte
  318. self.header.file_total_size = self.file_handle.tell()
  319. self.header.n_samples = \
  320. (self.header.file_total_size - self.header.Header) / \
  321. self.header.Channel_Count / self.header.sample_width
  322. self.header.Length = np.float64(self.header.n_samples) / \
  323. self.header.Channel_Count
  324. if self.header.sample_width * self.header.Channel_Count * \
  325. self.header.n_samples + \
  326. self.header.Header != self.header.file_total_size:
  327. logging.info( "I got header of %dB, %d channels, %d samples, \
  328. but total file size of %dB" % (self.header.Header,
  329. self.header.Channel_Count, self.header.n_samples,
  330. self.header.file_total_size))
  331. # close file
  332. self.file_handle.close()
  333. def regenerate_memmap(self):
  334. """Delete internal memmap and create a new one, to save memory."""
  335. try:
  336. del self._mm
  337. except AttributeError:
  338. pass
  339. self._mm = np.memmap(\
  340. self.filename, dtype='h', mode='r',
  341. offset=self.header.Header,
  342. shape=(self.header.n_samples, self.header.Channel_Count))
  343. def __del__(self):
  344. # this deletion doesn't free memory, even though del l._mm does!
  345. if '_mm' in self.__dict__: del self._mm
  346. #else: logging.info( "gracefully skipping")
  347. def _get_channel(self, channel_number):
  348. """Returns slice into internal memmap for requested channel"""
  349. try:
  350. mm_index = self.header.Channel_ID.index(channel_number)
  351. except ValueError:
  352. logging.info( "Channel number %d does not exist" % channel_number)
  353. return np.array([])
  354. self.regenerate_memmap()
  355. return self._mm[:, mm_index]
  356. def get_channel_as_array(self, channel_number):
  357. """Returns data from requested channel as a 1d numpy array."""
  358. data = np.array(self._get_channel(channel_number))
  359. self.regenerate_memmap()
  360. return data
  361. def get_analog_channel_as_array(self, analog_chn):
  362. """Returns data from requested analog channel as a numpy array.
  363. Simply adds 128 to the channel number to convert to ns5 number.
  364. This is just the way Cyberkinetics numbers its channels.
  365. """
  366. return self.get_channel_as_array(analog_chn + 128)
  367. def get_audio_channel_numbers(self):
  368. """Deprecated, use get_analog_channel_ids"""
  369. return self.get_analog_channel_ids()
  370. def get_analog_channel_ids(self):
  371. """Returns array of analog channel ids existing in the file.
  372. These can then be loaded by calling get_analog_channel_as_array(chn).
  373. """
  374. return np.array(filter(lambda x: (x > 128) and (x <= 144),
  375. self.header.Channel_ID)) - 128
  376. def get_neural_channel_numbers(self):
  377. return np.array(filter(lambda x: x <= 128, self.header.Channel_ID))