plexonrawio.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. """
  2. Class for reading the old data format from Plexon
  3. acquisition system (.plx)
  4. Note that Plexon now use a new format PL2 which is NOT
  5. supported by this IO.
  6. Compatible with versions 100 to 106.
  7. Other versions have not been tested.
  8. This IO is developed thanks to the header file downloadable from:
  9. http://www.plexon.com/software-downloads
  10. This IO was rewritten in 2017 and this was a huge pain because
  11. the underlying file format is really inefficient.
  12. The rewrite is now based on numpy dtype and not on Python struct.
  13. This should be faster.
  14. If one day, somebody use it, consider to offer me a beer.
  15. Author: Samuel Garcia
  16. """
  17. # from __future__ import unicode_literals is not compatible with numpy.dtype both py2 py3
  18. from .baserawio import (BaseRawIO, _signal_channel_dtype, _unit_channel_dtype,
  19. _event_channel_dtype)
  20. import numpy as np
  21. from collections import OrderedDict
  22. import datetime
  23. class PlexonRawIO(BaseRawIO):
  24. extensions = ['plx']
  25. rawmode = 'one-file'
  26. def __init__(self, filename=''):
  27. BaseRawIO.__init__(self)
  28. self.filename = filename
  29. def _source_name(self):
  30. return self.filename
  31. def _parse_header(self):
  32. # global header
  33. with open(self.filename, 'rb') as fid:
  34. offset0 = 0
  35. global_header = read_as_dict(fid, GlobalHeader, offset=offset0)
  36. rec_datetime = datetime.datetime(global_header['Year'],
  37. global_header['Month'],
  38. global_header['Day'],
  39. global_header['Hour'],
  40. global_header['Minute'],
  41. global_header['Second'])
  42. # dsp channels header = spikes and waveforms
  43. nb_unit_chan = global_header['NumDSPChannels']
  44. offset1 = np.dtype(GlobalHeader).itemsize
  45. dspChannelHeaders = np.memmap(self.filename, dtype=DspChannelHeader, mode='r',
  46. offset=offset1, shape=(nb_unit_chan,))
  47. # event channel header
  48. nb_event_chan = global_header['NumEventChannels']
  49. offset2 = offset1 + np.dtype(DspChannelHeader).itemsize * nb_unit_chan
  50. eventHeaders = np.memmap(self.filename, dtype=EventChannelHeader, mode='r',
  51. offset=offset2, shape=(nb_event_chan,))
  52. # slow channel header = signal
  53. nb_sig_chan = global_header['NumSlowChannels']
  54. offset3 = offset2 + np.dtype(EventChannelHeader).itemsize * nb_event_chan
  55. slowChannelHeaders = np.memmap(self.filename, dtype=SlowChannelHeader, mode='r',
  56. offset=offset3, shape=(nb_sig_chan,))
  57. offset4 = offset3 + np.dtype(SlowChannelHeader).itemsize * nb_sig_chan
  58. # loop over data blocks and put them by type and channel
  59. block_headers = {1: {c: [] for c in dspChannelHeaders['Channel']},
  60. 4: {c: [] for c in eventHeaders['Channel']},
  61. 5: {c: [] for c in slowChannelHeaders['Channel']},
  62. }
  63. block_pos = {1: {c: [] for c in dspChannelHeaders['Channel']},
  64. 4: {c: [] for c in eventHeaders['Channel']},
  65. 5: {c: [] for c in slowChannelHeaders['Channel']},
  66. }
  67. data = self._memmap = np.memmap(self.filename, dtype='u1', offset=0, mode='r')
  68. pos = offset4
  69. while pos < data.size:
  70. bl_header = data[pos:pos + 16].view(DataBlockHeader)[0]
  71. length = bl_header['NumberOfWaveforms'] * bl_header['NumberOfWordsInWaveform'] * 2 + 16
  72. bl_type = int(bl_header['Type'])
  73. chan_id = int(bl_header['Channel'])
  74. block_headers[bl_type][chan_id].append(bl_header)
  75. block_pos[bl_type][chan_id].append(pos)
  76. pos += length
  77. self._last_timestamps = bl_header['UpperByteOf5ByteTimestamp'] * \
  78. 2 ** 32 + bl_header['TimeStamp']
  79. # ... and finalize them in self._data_blocks
  80. # for a faster acces depending on type (1, 4, 5)
  81. self._data_blocks = {}
  82. dt_base = [('pos', 'int64'), ('timestamp', 'int64'), ('size', 'int64')]
  83. dtype_by_bltype = {
  84. # Spikes and waveforms
  85. 1: np.dtype(dt_base + [('unit_id', 'uint16'), ('n1', 'uint16'), ('n2', 'uint16'), ]),
  86. # Events
  87. 4: np.dtype(dt_base + [('label', 'uint16'), ]),
  88. # Signals
  89. 5: np.dtype(dt_base + [('cumsum', 'int64'), ]),
  90. }
  91. for bl_type in block_headers:
  92. self._data_blocks[bl_type] = {}
  93. for chan_id in block_headers[bl_type]:
  94. bl_header = np.array(block_headers[bl_type][chan_id], dtype=DataBlockHeader)
  95. bl_pos = np.array(block_pos[bl_type][chan_id], dtype='int64')
  96. timestamps = bl_header['UpperByteOf5ByteTimestamp'] * \
  97. 2 ** 32 + bl_header['TimeStamp']
  98. n1 = bl_header['NumberOfWaveforms']
  99. n2 = bl_header['NumberOfWordsInWaveform']
  100. dt = dtype_by_bltype[bl_type]
  101. data_block = np.empty(bl_pos.size, dtype=dt)
  102. data_block['pos'] = bl_pos + 16
  103. data_block['timestamp'] = timestamps
  104. data_block['size'] = n1 * n2 * 2
  105. if bl_type == 1: # Spikes and waveforms
  106. data_block['unit_id'] = bl_header['Unit']
  107. data_block['n1'] = n1
  108. data_block['n2'] = n2
  109. elif bl_type == 4: # Events
  110. data_block['label'] = bl_header['Unit']
  111. elif bl_type == 5: # Signals
  112. if data_block.size > 0:
  113. # cumulative some of sample index for fast acces to chunks
  114. data_block['cumsum'][0] = 0
  115. data_block['cumsum'][1:] = np.cumsum(data_block['size'][:-1]) // 2
  116. self._data_blocks[bl_type][chan_id] = data_block
  117. # signals channels
  118. sig_channels = []
  119. all_sig_length = []
  120. for chan_index in range(nb_sig_chan):
  121. h = slowChannelHeaders[chan_index]
  122. name = h['Name'].decode('utf8')
  123. chan_id = h['Channel']
  124. length = self._data_blocks[5][chan_id]['size'].sum() // 2
  125. if length == 0:
  126. continue # channel not added
  127. all_sig_length.append(length)
  128. sampling_rate = float(h['ADFreq'])
  129. sig_dtype = 'int16'
  130. units = '' # I dont't knwon units
  131. if global_header['Version'] in [100, 101]:
  132. gain = 5000. / (2048 * h['Gain'] * 1000.)
  133. elif global_header['Version'] in [102]:
  134. gain = 5000. / (2048 * h['Gain'] * h['PreampGain'])
  135. elif global_header['Version'] >= 103:
  136. gain = global_header['SlowMaxMagnitudeMV'] / (
  137. .5 * (2 ** global_header['BitsPerSpikeSample']) *
  138. h['Gain'] * h['PreampGain'])
  139. offset = 0.
  140. group_id = 0
  141. sig_channels.append((name, chan_id, sampling_rate, sig_dtype,
  142. units, gain, offset, group_id))
  143. if len(all_sig_length) > 0:
  144. self._signal_length = min(all_sig_length)
  145. sig_channels = np.array(sig_channels, dtype=_signal_channel_dtype)
  146. self._global_ssampling_rate = global_header['ADFrequency']
  147. if slowChannelHeaders.size > 0:
  148. assert np.unique(slowChannelHeaders['ADFreq']
  149. ).size == 1, 'Signal do not have the same sampling rate'
  150. self._sig_sampling_rate = float(slowChannelHeaders['ADFreq'][0])
  151. # Determine number of units per channels
  152. self.internal_unit_ids = []
  153. for chan_id, data_clock in self._data_blocks[1].items():
  154. unit_ids = np.unique(data_clock['unit_id'])
  155. for unit_id in unit_ids:
  156. self.internal_unit_ids.append((chan_id, unit_id))
  157. # Spikes channels
  158. unit_channels = []
  159. for unit_index, (chan_id, unit_id) in enumerate(self.internal_unit_ids):
  160. c = np.nonzero(dspChannelHeaders['Channel'] == chan_id)[0][0]
  161. h = dspChannelHeaders[c]
  162. name = h['Name'].decode('utf8')
  163. _id = 'ch{}#{}'.format(chan_id, unit_id)
  164. wf_units = ''
  165. if global_header['Version'] < 103:
  166. wf_gain = 3000. / (2048 * h['Gain'] * 1000.)
  167. elif 103 <= global_header['Version'] < 105:
  168. wf_gain = global_header['SpikeMaxMagnitudeMV'] / (
  169. .5 * 2. ** (global_header['BitsPerSpikeSample']) *
  170. h['Gain'] * 1000.)
  171. elif global_header['Version'] >= 105:
  172. wf_gain = global_header['SpikeMaxMagnitudeMV'] / (
  173. .5 * 2. ** (global_header['BitsPerSpikeSample']) *
  174. h['Gain'] * global_header['SpikePreAmpGain'])
  175. wf_offset = 0.
  176. wf_left_sweep = -1 # DONT KNOWN
  177. wf_sampling_rate = global_header['WaveformFreq']
  178. unit_channels.append((name, _id, wf_units, wf_gain, wf_offset,
  179. wf_left_sweep, wf_sampling_rate))
  180. unit_channels = np.array(unit_channels, dtype=_unit_channel_dtype)
  181. # Event channels
  182. event_channels = []
  183. for chan_index in range(nb_event_chan):
  184. h = eventHeaders[chan_index]
  185. chan_id = h['Channel']
  186. name = h['Name'].decode('utf8')
  187. _id = h['Channel']
  188. event_channels.append((name, _id, 'event'))
  189. event_channels = np.array(event_channels, dtype=_event_channel_dtype)
  190. # fille into header dict
  191. self.header = {}
  192. self.header['nb_block'] = 1
  193. self.header['nb_segment'] = [1]
  194. self.header['signal_channels'] = sig_channels
  195. self.header['unit_channels'] = unit_channels
  196. self.header['event_channels'] = event_channels
  197. # Annotations
  198. self._generate_minimal_annotations()
  199. bl_annotations = self.raw_annotations['blocks'][0]
  200. seg_annotations = bl_annotations['segments'][0]
  201. for d in (bl_annotations, seg_annotations):
  202. d['rec_datetime'] = rec_datetime
  203. d['plexon_version'] = global_header['Version']
  204. def _segment_t_start(self, block_index, seg_index):
  205. return 0.
  206. def _segment_t_stop(self, block_index, seg_index):
  207. t_stop1 = float(self._last_timestamps) / self._global_ssampling_rate
  208. if hasattr(self, '_signal_length'):
  209. t_stop2 = self._signal_length / self._sig_sampling_rate
  210. return max(t_stop1, t_stop2)
  211. else:
  212. return t_stop1
  213. def _get_signal_size(self, block_index, seg_index, channel_indexes):
  214. return self._signal_length
  215. def _get_signal_t_start(self, block_index, seg_index, channel_indexes):
  216. return 0.
  217. def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, channel_indexes):
  218. if i_start is None:
  219. i_start = 0
  220. if i_stop is None:
  221. i_stop = self._signal_length
  222. if channel_indexes is None:
  223. channel_indexes = np.arange(self.header['signal_channels'].size)
  224. raw_signals = np.zeros((i_stop - i_start, len(channel_indexes)), dtype='int16')
  225. for c, channel_index in enumerate(channel_indexes):
  226. chan_header = self.header['signal_channels'][channel_index]
  227. chan_id = chan_header['id']
  228. data_blocks = self._data_blocks[5][chan_id]
  229. # loop over data blocks and get chunks
  230. bl0 = np.searchsorted(data_blocks['cumsum'], i_start, side='right') - 1
  231. bl1 = np.searchsorted(data_blocks['cumsum'], i_stop, side='right')
  232. ind = 0
  233. for bl in range(bl0, bl1):
  234. ind0 = data_blocks[bl]['pos']
  235. ind1 = data_blocks[bl]['size'] + ind0
  236. data = self._memmap[ind0:ind1].view('int16')
  237. if bl == bl1 - 1:
  238. # right border
  239. # be carfull that bl could be both bl0 and bl1!!
  240. border = data.size - (i_stop - data_blocks[bl]['cumsum'])
  241. data = data[:-border]
  242. if bl == bl0:
  243. # left border
  244. border = i_start - data_blocks[bl]['cumsum']
  245. data = data[border:]
  246. raw_signals[ind:data.size + ind, c] = data
  247. ind += data.size
  248. return raw_signals
  249. def _get_internal_mask(self, data_block, t_start, t_stop):
  250. timestamps = data_block['timestamp']
  251. if t_start is None:
  252. lim0 = 0
  253. else:
  254. lim0 = int(t_start * self._global_ssampling_rate)
  255. if t_stop is None:
  256. lim1 = self._last_timestamps
  257. else:
  258. lim1 = int(t_stop * self._global_ssampling_rate)
  259. keep = (timestamps >= lim0) & (timestamps <= lim1)
  260. return keep
  261. def _spike_count(self, block_index, seg_index, unit_index):
  262. chan_id, unit_id = self.internal_unit_ids[unit_index]
  263. data_block = self._data_blocks[1][chan_id]
  264. nb_spike = np.sum(data_block['unit_id'] == unit_id)
  265. return nb_spike
  266. def _get_spike_timestamps(self, block_index, seg_index, unit_index, t_start, t_stop):
  267. chan_id, unit_id = self.internal_unit_ids[unit_index]
  268. data_block = self._data_blocks[1][chan_id]
  269. keep = self._get_internal_mask(data_block, t_start, t_stop)
  270. keep &= data_block['unit_id'] == unit_id
  271. spike_timestamps = data_block[keep]['timestamp']
  272. return spike_timestamps
  273. def _rescale_spike_timestamp(self, spike_timestamps, dtype):
  274. spike_times = spike_timestamps.astype(dtype)
  275. spike_times /= self._global_ssampling_rate
  276. return spike_times
  277. def _get_spike_raw_waveforms(self, block_index, seg_index, unit_index, t_start, t_stop):
  278. chan_id, unit_id = self.internal_unit_ids[unit_index]
  279. data_block = self._data_blocks[1][chan_id]
  280. n1 = data_block['n1'][0]
  281. n2 = data_block['n2'][0]
  282. keep = self._get_internal_mask(data_block, t_start, t_stop)
  283. keep &= data_block['unit_id'] == unit_id
  284. data_block = data_block[keep]
  285. nb_spike = data_block.size
  286. waveforms = np.zeros((nb_spike, n1, n2), dtype='int16')
  287. for i, db in enumerate(data_block):
  288. ind0 = db['pos']
  289. ind1 = db['size'] + ind0
  290. data = self._memmap[ind0:ind1].view('int16').reshape(n1, n2)
  291. waveforms[i, :, :] = data
  292. return waveforms
  293. def _event_count(self, block_index, seg_index, event_channel_index):
  294. chan_id = int(self.header['event_channels'][event_channel_index]['id'])
  295. nb_event = self._data_blocks[4][chan_id].size
  296. return nb_event
  297. def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop):
  298. chan_id = int(self.header['event_channels'][event_channel_index]['id'])
  299. data_block = self._data_blocks[4][chan_id]
  300. keep = self._get_internal_mask(data_block, t_start, t_stop)
  301. db = data_block[keep]
  302. timestamps = db['timestamp']
  303. labels = db['label'].astype('U')
  304. durations = None
  305. return timestamps, durations, labels
  306. def _rescale_event_timestamp(self, event_timestamps, dtype):
  307. event_times = event_timestamps.astype(dtype)
  308. event_times /= self._global_ssampling_rate
  309. return event_times
  310. def read_as_dict(fid, dtype, offset=None):
  311. """
  312. Given a file descriptor
  313. and a numpy.dtype of the binary struct return a dict.
  314. Make conversion for strings.
  315. """
  316. if offset is not None:
  317. fid.seek(offset)
  318. dt = np.dtype(dtype)
  319. h = np.frombuffer(fid.read(dt.itemsize), dt)[0]
  320. info = OrderedDict()
  321. for k in dt.names:
  322. v = h[k]
  323. if dt[k].kind == 'S':
  324. v = v.decode('utf8')
  325. v = v.replace('\x03', '')
  326. v = v.replace('\x00', '')
  327. info[k] = v
  328. return info
  329. GlobalHeader = [
  330. ('MagicNumber', 'uint32'),
  331. ('Version', 'int32'),
  332. ('Comment', 'S128'),
  333. ('ADFrequency', 'int32'),
  334. ('NumDSPChannels', 'int32'),
  335. ('NumEventChannels', 'int32'),
  336. ('NumSlowChannels', 'int32'),
  337. ('NumPointsWave', 'int32'),
  338. ('NumPointsPreThr', 'int32'),
  339. ('Year', 'int32'),
  340. ('Month', 'int32'),
  341. ('Day', 'int32'),
  342. ('Hour', 'int32'),
  343. ('Minute', 'int32'),
  344. ('Second', 'int32'),
  345. ('FastRead', 'int32'),
  346. ('WaveformFreq', 'int32'),
  347. ('LastTimestamp', 'float64'),
  348. # version >103
  349. ('Trodalness', 'uint8'),
  350. ('DataTrodalness', 'uint8'),
  351. ('BitsPerSpikeSample', 'uint8'),
  352. ('BitsPerSlowSample', 'uint8'),
  353. ('SpikeMaxMagnitudeMV', 'uint16'),
  354. ('SlowMaxMagnitudeMV', 'uint16'),
  355. # version 105
  356. ('SpikePreAmpGain', 'uint16'),
  357. # version 106
  358. ('AcquiringSoftware', 'S18'),
  359. ('ProcessingSoftware', 'S18'),
  360. ('Padding', 'S10'),
  361. # all version
  362. ('TSCounts', 'int32', (650,)),
  363. ('WFCounts', 'int32', (650,)),
  364. ('EVCounts', 'int32', (512,)),
  365. ]
  366. DspChannelHeader = [
  367. ('Name', 'S32'),
  368. ('SIGName', 'S32'),
  369. ('Channel', 'int32'),
  370. ('WFRate', 'int32'),
  371. ('SIG', 'int32'),
  372. ('Ref', 'int32'),
  373. ('Gain', 'int32'),
  374. ('Filter', 'int32'),
  375. ('Threshold', 'int32'),
  376. ('Method', 'int32'),
  377. ('NUnits', 'int32'),
  378. ('Template', 'uint16', (320,)),
  379. ('Fit', 'int32', (5,)),
  380. ('SortWidth', 'int32'),
  381. ('Boxes', 'uint16', (40,)),
  382. ('SortBeg', 'int32'),
  383. # version 105
  384. ('Comment', 'S128'),
  385. # version 106
  386. ('SrcId', 'uint8'),
  387. ('reserved', 'uint8'),
  388. ('ChanId', 'uint16'),
  389. ('Padding', 'int32', (10,)),
  390. ]
  391. EventChannelHeader = [
  392. ('Name', 'S32'),
  393. ('Channel', 'int32'),
  394. # version 105
  395. ('Comment', 'S128'),
  396. # version 106
  397. ('SrcId', 'uint8'),
  398. ('reserved', 'uint8'),
  399. ('ChanId', 'uint16'),
  400. ('Padding', 'int32', (32,)),
  401. ]
  402. SlowChannelHeader = [
  403. ('Name', 'S32'),
  404. ('Channel', 'int32'),
  405. ('ADFreq', 'int32'),
  406. ('Gain', 'int32'),
  407. ('Enabled', 'int32'),
  408. ('PreampGain', 'int32'),
  409. # version 104
  410. ('SpikeChannel', 'int32'),
  411. # version 105
  412. ('Comment', 'S128'),
  413. # version 106
  414. ('SrcId', 'uint8'),
  415. ('reserved', 'uint8'),
  416. ('ChanId', 'uint16'),
  417. ('Padding', 'int32', (27,)),
  418. ]
  419. DataBlockHeader = [
  420. ('Type', 'uint16'),
  421. ('UpperByteOf5ByteTimestamp', 'uint16'),
  422. ('TimeStamp', 'int32'),
  423. ('Channel', 'uint16'),
  424. ('Unit', 'uint16'),
  425. ('NumberOfWaveforms', 'uint16'),
  426. ('NumberOfWordsInWaveform', 'uint16'),
  427. ] # 16 bytes