axonrawio.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  1. """
  2. Class for reading data from pCLAMP and AxoScope
  3. files (.abf version 1 and 2), developed by Molecular device/Axon technologies.
  4. - abf = Axon binary file
  5. - atf is a text file based format from axon that could be
  6. read by AsciiIO (but this file is less efficient.)
  7. This code is a port of abfload and abf2load
  8. written in Matlab (BSD-2-Clause licence) by :
  9. - Copyright (c) 2009, Forrest Collman, fcollman@princeton.edu
  10. - Copyright (c) 2004, Harald Hentschke
  11. and available here:
  12. http://www.mathworks.com/matlabcentral/fileexchange/22114-abf2load
  13. Information on abf 1 and 2 formats is available here:
  14. http://www.moleculardevices.com/pages/software/developer_info.html
  15. This file supports the old (ABF1) and new (ABF2) format.
  16. ABF1 (clampfit <=9) and ABF2 (clampfit >10)
  17. All possible mode are possible :
  18. - event-driven variable-length mode 1 -> return several Segments per Block
  19. - event-driven fixed-length mode 2 or 5 -> return several Segments
  20. - gap free mode -> return one (or sevral) Segment in the Block
  21. Supported : Read
  22. Author: Samuel Garcia, JS Nowacki
  23. Note: j.s.nowacki@gmail.com has a C++ library with SWIG bindings which also
  24. reads abf files - would be good to cross-check
  25. """
  26. from .baserawio import (BaseRawIO, _signal_channel_dtype, _unit_channel_dtype,
  27. _event_channel_dtype)
  28. import numpy as np
  29. import struct
  30. import datetime
  31. import os
  32. from io import open, BufferedReader
  33. import numpy as np
  34. class AxonRawIO(BaseRawIO):
  35. extensions = ['abf']
  36. rawmode = 'one-file'
  37. def __init__(self, filename=''):
  38. BaseRawIO.__init__(self)
  39. self.filename = filename
  40. def _parse_header(self):
  41. info = self._axon_info = parse_axon_soup(self.filename)
  42. version = info['fFileVersionNumber']
  43. # file format
  44. if info['nDataFormat'] == 0:
  45. sig_dtype = np.dtype('i2')
  46. elif info['nDataFormat'] == 1:
  47. sig_dtype = np.dtype('f4')
  48. if version < 2.:
  49. nbchannel = info['nADCNumChannels']
  50. head_offset = info['lDataSectionPtr'] * BLOCKSIZE + info[
  51. 'nNumPointsIgnored'] * sig_dtype.itemsize
  52. totalsize = info['lActualAcqLength']
  53. elif version >= 2.:
  54. nbchannel = info['sections']['ADCSection']['llNumEntries']
  55. head_offset = info['sections']['DataSection'][
  56. 'uBlockIndex'] * BLOCKSIZE
  57. totalsize = info['sections']['DataSection']['llNumEntries']
  58. self._raw_data = np.memmap(self.filename, dtype=sig_dtype, mode='r',
  59. shape=(totalsize,), offset=head_offset)
  60. # 3 possible modes
  61. if version < 2.:
  62. mode = info['nOperationMode']
  63. elif version >= 2.:
  64. mode = info['protocol']['nOperationMode']
  65. assert mode in [1, 2, 3, 5], 'Mode {} is not supported'.formagt(mode)
  66. # event-driven variable-length mode (mode 1)
  67. # event-driven fixed-length mode (mode 2 or 5)
  68. # gap free mode (mode 3) can be in several episodes
  69. # read sweep pos
  70. if version < 2.:
  71. nbepisod = info['lSynchArraySize']
  72. offset_episode = info['lSynchArrayPtr'] * BLOCKSIZE
  73. elif version >= 2.:
  74. nbepisod = info['sections']['SynchArraySection'][
  75. 'llNumEntries']
  76. offset_episode = info['sections']['SynchArraySection'][
  77. 'uBlockIndex'] * BLOCKSIZE
  78. if nbepisod > 0:
  79. episode_array = np.memmap(
  80. self.filename, [('offset', 'i4'), ('len', 'i4')], 'r',
  81. shape=nbepisod, offset=offset_episode)
  82. else:
  83. episode_array = np.empty(1, [('offset', 'i4'), ('len', 'i4')])
  84. episode_array[0]['len'] = self._raw_data.size
  85. episode_array[0]['offset'] = 0
  86. # sampling_rate
  87. if version < 2.:
  88. self._sampling_rate = 1. / (info['fADCSampleInterval'] * nbchannel * 1.e-6)
  89. elif version >= 2.:
  90. self._sampling_rate = 1.e6 / info['protocol']['fADCSequenceInterval']
  91. # one sweep = one segment
  92. nb_segment = episode_array.size
  93. # Get raw data by segment
  94. self._raw_signals = {}
  95. self._t_starts = {}
  96. pos = 0
  97. for seg_index in range(nb_segment):
  98. length = episode_array[seg_index]['len']
  99. if version < 2.:
  100. fSynchTimeUnit = info['fSynchTimeUnit']
  101. elif version >= 2.:
  102. fSynchTimeUnit = info['protocol']['fSynchTimeUnit']
  103. if (fSynchTimeUnit != 0) and (mode == 1):
  104. length /= fSynchTimeUnit
  105. self._raw_signals[seg_index] = self._raw_data[pos:pos + length].reshape(-1, nbchannel)
  106. pos += length
  107. t_start = float(episode_array[seg_index]['offset'])
  108. if (fSynchTimeUnit == 0):
  109. t_start = t_start / self._sampling_rate
  110. else:
  111. t_start = t_start * fSynchTimeUnit * 1e-6
  112. self._t_starts[seg_index] = t_start
  113. # Create channel header
  114. if version < 2.:
  115. channel_ids = [chan_num for chan_num in
  116. info['nADCSamplingSeq'] if chan_num >= 0]
  117. else:
  118. channel_ids = list(range(nbchannel))
  119. sig_channels = []
  120. adc_nums = []
  121. for chan_index, chan_id in enumerate(channel_ids):
  122. if version < 2.:
  123. name = info['sADCChannelName'][chan_id].replace(b' ', b'')
  124. units = safe_decode_units(info['sADCUnits'][chan_id])
  125. adc_num = info['nADCPtoLChannelMap'][chan_id]
  126. elif version >= 2.:
  127. ADCInfo = info['listADCInfo'][chan_id]
  128. name = ADCInfo['ADCChNames'].replace(b' ', b'')
  129. units = safe_decode_units(ADCInfo['ADCChUnits'])
  130. adc_num = ADCInfo['nADCNum']
  131. adc_nums.append(adc_num)
  132. if info['nDataFormat'] == 0:
  133. # int16 gain/offset
  134. if version < 2.:
  135. gain = info['fADCRange']
  136. gain /= info['fInstrumentScaleFactor'][chan_id]
  137. gain /= info['fSignalGain'][chan_id]
  138. gain /= info['fADCProgrammableGain'][chan_id]
  139. gain /= info['lADCResolution']
  140. if info['nTelegraphEnable'][chan_id] == 0:
  141. pass
  142. elif info['nTelegraphEnable'][chan_id] == 1:
  143. gain /= info['fTelegraphAdditGain'][chan_id]
  144. else:
  145. self.logger.warning('ignoring buggy nTelegraphEnable')
  146. offset = info['fInstrumentOffset'][chan_id]
  147. offset -= info['fSignalOffset'][chan_id]
  148. elif version >= 2.:
  149. gain = info['protocol']['fADCRange']
  150. gain /= info['listADCInfo'][chan_id]['fInstrumentScaleFactor']
  151. gain /= info['listADCInfo'][chan_id]['fSignalGain']
  152. gain /= info['listADCInfo'][chan_id]['fADCProgrammableGain']
  153. gain /= info['protocol']['lADCResolution']
  154. if info['listADCInfo'][chan_id]['nTelegraphEnable']:
  155. gain /= info['listADCInfo'][chan_id]['fTelegraphAdditGain']
  156. offset = info['listADCInfo'][chan_id]['fInstrumentOffset']
  157. offset -= info['listADCInfo'][chan_id]['fSignalOffset']
  158. else:
  159. gain, offset = 1., 0.
  160. group_id = 0
  161. sig_channels.append((name, chan_id, self._sampling_rate,
  162. sig_dtype, units, gain, offset, group_id))
  163. sig_channels = np.array(sig_channels, dtype=_signal_channel_dtype)
  164. # only one events channel : tag
  165. # In ABF timstamps are not attached too any particular segment
  166. # so each segment acess all event
  167. timestamps = []
  168. labels = []
  169. comments = []
  170. for i, tag in enumerate(info['listTag']):
  171. timestamps.append(tag['lTagTime'])
  172. labels.append(str(tag['nTagType']))
  173. comments.append(clean_string(tag['sComment']))
  174. self._raw_ev_timestamps = np.array(timestamps)
  175. self._ev_labels = np.array(labels, dtype='U')
  176. self._ev_comments = np.array(comments, dtype='U')
  177. event_channels = [('Tag', '', 'event')]
  178. event_channels = np.array(event_channels, dtype=_event_channel_dtype)
  179. # No spikes
  180. unit_channels = []
  181. unit_channels = np.array(unit_channels, dtype=_unit_channel_dtype)
  182. # fille into header dict
  183. self.header = {}
  184. self.header['nb_block'] = 1
  185. self.header['nb_segment'] = [nb_segment]
  186. self.header['signal_channels'] = sig_channels
  187. self.header['unit_channels'] = unit_channels
  188. self.header['event_channels'] = event_channels
  189. # insert some annotation at some place
  190. self._generate_minimal_annotations()
  191. bl_annotations = self.raw_annotations['blocks'][0]
  192. bl_annotations['rec_datetime'] = info['rec_datetime']
  193. bl_annotations['abf_version'] = version
  194. for seg_index in range(nb_segment):
  195. seg_annotations = bl_annotations['segments'][seg_index]
  196. seg_annotations['abf_version'] = version
  197. for c in range(sig_channels.size):
  198. anasig_an = seg_annotations['signals'][c]
  199. anasig_an['nADCNum'] = adc_nums[c]
  200. for c in range(event_channels.size):
  201. ev_ann = seg_annotations['events'][c]
  202. ev_ann['comments'] = self._ev_comments
  203. def _source_name(self):
  204. return self.filename
  205. def _segment_t_start(self, block_index, seg_index):
  206. return self._t_starts[seg_index]
  207. def _segment_t_stop(self, block_index, seg_index):
  208. t_stop = self._t_starts[seg_index] + \
  209. self._raw_signals[seg_index].shape[0] / self._sampling_rate
  210. return t_stop
  211. def _get_signal_size(self, block_index, seg_index, channel_indexes):
  212. shape = self._raw_signals[seg_index].shape
  213. return shape[0]
  214. def _get_signal_t_start(self, block_index, seg_index, channel_indexes):
  215. return self._t_starts[seg_index]
  216. def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, channel_indexes):
  217. if channel_indexes is None:
  218. channel_indexes = slice(None)
  219. raw_signals = self._raw_signals[seg_index][slice(i_start, i_stop), channel_indexes]
  220. return raw_signals
  221. def _event_count(self, block_index, seg_index, event_channel_index):
  222. return self._raw_ev_timestamps.size
  223. def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop):
  224. # In ABF timstamps are not attached too any particular segment
  225. # so each segmetn acees all event
  226. timestamp = self._raw_ev_timestamps
  227. labels = self._ev_labels
  228. durations = None
  229. if t_start is not None:
  230. keep = timestamp >= int(t_start * self._sampling_rate)
  231. timestamp = timestamp[keep]
  232. labels = labels[keep]
  233. if t_stop is not None:
  234. keep = timestamp <= int(t_stop * self._sampling_rate)
  235. timestamp = timestamp[keep]
  236. labels = labels[keep]
  237. return timestamp, durations, labels
  238. def _rescale_event_timestamp(self, event_timestamps, dtype):
  239. event_times = event_timestamps.astype(dtype) / self._sampling_rate
  240. return event_times
  241. def read_raw_protocol(self):
  242. """
  243. Read the protocol waveform of the file, if present;
  244. function works with ABF2 only. Protocols can be reconstructed
  245. from the ABF1 header.
  246. Returns: list of segments (one for every episode)
  247. with list of analog signls (one for every DAC).
  248. Author: JS Nowacki
  249. """
  250. info = self._axon_info
  251. if info['fFileVersionNumber'] < 2.:
  252. raise IOError("Protocol section is only present in ABF2 files.")
  253. nADC = info['sections']['ADCSection'][
  254. 'llNumEntries'] # Number of ADC channels
  255. nDAC = info['sections']['DACSection'][
  256. 'llNumEntries'] # Number of DAC channels
  257. nSam = int(info['protocol'][
  258. 'lNumSamplesPerEpisode'] / nADC) # Number of samples per episode
  259. nEpi = info['lActualEpisodes'] # Actual number of episodes
  260. # Make a list of segments with analog signals with just holding levels
  261. # List of segments relates to number of episodes, as for recorded data
  262. sigs_by_segments = []
  263. for epiNum in range(nEpi):
  264. # One analog signal for each DAC in segment (episode)
  265. signals = []
  266. for DACNum in range(nDAC):
  267. sig = np.ones(nSam) * info['listDACInfo'][DACNum]['fDACHoldingLevel']
  268. # If there are epoch infos for this DAC
  269. if DACNum in info['dictEpochInfoPerDAC']:
  270. # Save last sample index
  271. i_last = int(nSam * 15625 / 10 ** 6)
  272. # TODO guess for first holding
  273. # Go over EpochInfoPerDAC and change the analog signal
  274. # according to the epochs
  275. epochInfo = info['dictEpochInfoPerDAC'][DACNum]
  276. for epochNum, epoch in epochInfo.items():
  277. i_begin = i_last
  278. i_end = i_last + epoch['lEpochInitDuration'] + \
  279. epoch['lEpochDurationInc'] * epiNum
  280. dif = i_end - i_begin
  281. sig[i_begin:i_end] = np.ones(dif) * \
  282. (epoch['fEpochInitLevel'] + epoch['fEpochLevelInc'] * epiNum)
  283. i_last += epoch['lEpochInitDuration'] + \
  284. epoch['lEpochDurationInc'] * epiNum
  285. signals.append(sig)
  286. sigs_by_segments.append(signals)
  287. sig_names = []
  288. sig_units = []
  289. for DACNum in range(nDAC):
  290. name = info['listDACInfo'][DACNum]['DACChNames'].decode("utf-8")
  291. units = safe_decode_units(info['listDACInfo'][DACNum]['DACChUnits'])
  292. sig_names.append(name)
  293. sig_units.append(units)
  294. return sigs_by_segments, sig_names, sig_units
  295. def parse_axon_soup(filename):
  296. """
  297. read the header of the file
  298. The strategy here differs from the original script under Matlab.
  299. In the original script for ABF2, it completes the header with
  300. information that is located in other structures.
  301. In ABF2 this function returns info with sub dict:
  302. sections (ABF2)
  303. protocol (ABF2)
  304. listTags (ABF1&2)
  305. listADCInfo (ABF2)
  306. listDACInfo (ABF2)
  307. dictEpochInfoPerDAC (ABF2)
  308. that contains more information.
  309. """
  310. with open(filename, 'rb') as fid:
  311. f = StructFile(fid)
  312. # version
  313. f_file_signature = f.read(4)
  314. if f_file_signature == b'ABF ':
  315. header_description = headerDescriptionV1
  316. elif f_file_signature == b'ABF2':
  317. header_description = headerDescriptionV2
  318. else:
  319. return None
  320. # construct dict
  321. header = {}
  322. for key, offset, fmt in header_description:
  323. val = f.read_f(fmt, offset=offset)
  324. if len(val) == 1:
  325. header[key] = val[0]
  326. else:
  327. header[key] = np.array(val)
  328. # correction of version number and starttime
  329. if f_file_signature == b'ABF ':
  330. header['lFileStartTime'] += header[
  331. 'nFileStartMillisecs'] * .001
  332. elif f_file_signature == b'ABF2':
  333. n = header['fFileVersionNumber']
  334. header['fFileVersionNumber'] = n[3] + 0.1 * n[2] + \
  335. 0.01 * n[1] + 0.001 * n[0]
  336. header['lFileStartTime'] = header['uFileStartTimeMS'] * .001
  337. if header['fFileVersionNumber'] < 2.:
  338. # tags
  339. listTag = []
  340. for i in range(header['lNumTagEntries']):
  341. f.seek(header['lTagSectionPtr'] + i * 64)
  342. tag = {}
  343. for key, fmt in TagInfoDescription:
  344. val = f.read_f(fmt)
  345. if len(val) == 1:
  346. tag[key] = val[0]
  347. else:
  348. tag[key] = np.array(val)
  349. listTag.append(tag)
  350. header['listTag'] = listTag
  351. # protocol name formatting
  352. header['sProtocolPath'] = clean_string(header['sProtocolPath'])
  353. header['sProtocolPath'] = header['sProtocolPath']. \
  354. replace(b'\\', b'/')
  355. elif header['fFileVersionNumber'] >= 2.:
  356. # in abf2 some info are in other place
  357. # sections
  358. sections = {}
  359. for s, sectionName in enumerate(sectionNames):
  360. uBlockIndex, uBytes, llNumEntries = \
  361. f.read_f('IIl', offset=76 + s * 16)
  362. sections[sectionName] = {}
  363. sections[sectionName]['uBlockIndex'] = uBlockIndex
  364. sections[sectionName]['uBytes'] = uBytes
  365. sections[sectionName]['llNumEntries'] = llNumEntries
  366. header['sections'] = sections
  367. # strings sections
  368. # hack for reading channels names and units
  369. # this section is not very detailed and so the code
  370. # not very robust. The idea is to remove the first
  371. # part by find ing one of th fowoling KEY
  372. # unfortunatly the later part contains a the file
  373. # taht can contain by accident also one of theses keys...
  374. f.seek(sections['StringsSection']['uBlockIndex'] * BLOCKSIZE)
  375. big_string = f.read(sections['StringsSection']['uBytes'])
  376. goodstart = -1
  377. for key in [b'AXENGN', b'clampex', b'Clampex', b'EDR3',
  378. b'CLAMPEX', b'axoscope', b'AxoScope', b'Clampfit']:
  379. # goodstart = big_string.lower().find(key)
  380. goodstart = big_string.find(b'\x00' + key)
  381. if goodstart != -1:
  382. break
  383. assert goodstart != -1, \
  384. 'This file does not contain clampex, axoscope or clampfit in the header'
  385. big_string = big_string[goodstart + 1:]
  386. strings = big_string.split(b'\x00')
  387. # ADC sections
  388. header['listADCInfo'] = []
  389. for i in range(sections['ADCSection']['llNumEntries']):
  390. # read ADCInfo
  391. f.seek(sections['ADCSection']['uBlockIndex']
  392. * BLOCKSIZE + sections['ADCSection']['uBytes'] * i)
  393. ADCInfo = {}
  394. for key, fmt in ADCInfoDescription:
  395. val = f.read_f(fmt)
  396. if len(val) == 1:
  397. ADCInfo[key] = val[0]
  398. else:
  399. ADCInfo[key] = np.array(val)
  400. ADCInfo['ADCChNames'] = strings[ADCInfo['lADCChannelNameIndex'] - 1]
  401. ADCInfo['ADCChUnits'] = strings[ADCInfo['lADCUnitsIndex'] - 1]
  402. header['listADCInfo'].append(ADCInfo)
  403. # protocol sections
  404. protocol = {}
  405. f.seek(sections['ProtocolSection']['uBlockIndex'] * BLOCKSIZE)
  406. for key, fmt in protocolInfoDescription:
  407. val = f.read_f(fmt)
  408. if len(val) == 1:
  409. protocol[key] = val[0]
  410. else:
  411. protocol[key] = np.array(val)
  412. header['protocol'] = protocol
  413. header['sProtocolPath'] = strings[header['uProtocolPathIndex'] - 1]
  414. # tags
  415. listTag = []
  416. for i in range(sections['TagSection']['llNumEntries']):
  417. f.seek(sections['TagSection']['uBlockIndex']
  418. * BLOCKSIZE + sections['TagSection']['uBytes'] * i)
  419. tag = {}
  420. for key, fmt in TagInfoDescription:
  421. val = f.read_f(fmt)
  422. if len(val) == 1:
  423. tag[key] = val[0]
  424. else:
  425. tag[key] = np.array(val)
  426. listTag.append(tag)
  427. header['listTag'] = listTag
  428. # DAC sections
  429. header['listDACInfo'] = []
  430. for i in range(sections['DACSection']['llNumEntries']):
  431. # read DACInfo
  432. f.seek(sections['DACSection']['uBlockIndex']
  433. * BLOCKSIZE + sections['DACSection']['uBytes'] * i)
  434. DACInfo = {}
  435. for key, fmt in DACInfoDescription:
  436. val = f.read_f(fmt)
  437. if len(val) == 1:
  438. DACInfo[key] = val[0]
  439. else:
  440. DACInfo[key] = np.array(val)
  441. DACInfo['DACChNames'] = strings[DACInfo['lDACChannelNameIndex']
  442. - 1]
  443. DACInfo['DACChUnits'] = strings[
  444. DACInfo['lDACChannelUnitsIndex'] - 1]
  445. header['listDACInfo'].append(DACInfo)
  446. # EpochPerDAC sections
  447. # header['dictEpochInfoPerDAC'] is dict of dicts:
  448. # - the first index is the DAC number
  449. # - the second index is the epoch number
  450. # It has to be done like that because data may not exist
  451. # and may not be in sorted order
  452. header['dictEpochInfoPerDAC'] = {}
  453. for i in range(sections['EpochPerDACSection']['llNumEntries']):
  454. # read DACInfo
  455. f.seek(sections['EpochPerDACSection']['uBlockIndex']
  456. * BLOCKSIZE + sections['EpochPerDACSection']['uBytes'] * i)
  457. EpochInfoPerDAC = {}
  458. for key, fmt in EpochInfoPerDACDescription:
  459. val = f.read_f(fmt)
  460. if len(val) == 1:
  461. EpochInfoPerDAC[key] = val[0]
  462. else:
  463. EpochInfoPerDAC[key] = np.array(val)
  464. DACNum = EpochInfoPerDAC['nDACNum']
  465. EpochNum = EpochInfoPerDAC['nEpochNum']
  466. # Checking if the key exists, if not, the value is empty
  467. # so we have to create empty dict to populate
  468. if DACNum not in header['dictEpochInfoPerDAC']:
  469. header['dictEpochInfoPerDAC'][DACNum] = {}
  470. header['dictEpochInfoPerDAC'][DACNum][EpochNum] = \
  471. EpochInfoPerDAC
  472. # Epoch sections
  473. header['EpochInfo'] = []
  474. for i in range(sections['EpochSection']['llNumEntries']):
  475. # read EpochInfo
  476. f.seek(sections['EpochSection']['uBlockIndex']
  477. * BLOCKSIZE + sections['EpochSection']['uBytes'] * i)
  478. EpochInfo = {}
  479. for key, fmt in EpochInfoDescription:
  480. val = f.read_f(fmt)
  481. if len(val) == 1:
  482. EpochInfo[key] = val[0]
  483. else:
  484. EpochInfo[key] = np.array(val)
  485. header['EpochInfo'].append(EpochInfo)
  486. # date and time
  487. if header['fFileVersionNumber'] < 2.:
  488. YY = 1900
  489. MM = 1
  490. DD = 1
  491. hh = int(header['lFileStartTime'] / 3600.)
  492. mm = int((header['lFileStartTime'] - hh * 3600) / 60)
  493. ss = header['lFileStartTime'] - hh * 3600 - mm * 60
  494. ms = int(np.mod(ss, 1) * 1e6)
  495. ss = int(ss)
  496. elif header['fFileVersionNumber'] >= 2.:
  497. YY = int(header['uFileStartDate'] / 10000)
  498. MM = int((header['uFileStartDate'] - YY * 10000) / 100)
  499. DD = int(header['uFileStartDate'] - YY * 10000 - MM * 100)
  500. hh = int(header['uFileStartTimeMS'] / 1000. / 3600.)
  501. mm = int((header['uFileStartTimeMS'] / 1000. - hh * 3600) / 60)
  502. ss = header['uFileStartTimeMS'] / 1000. - hh * 3600 - mm * 60
  503. ms = int(np.mod(ss, 1) * 1e6)
  504. ss = int(ss)
  505. header['rec_datetime'] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms)
  506. return header
  507. class StructFile(BufferedReader):
  508. def read_f(self, fmt, offset=None):
  509. if offset is not None:
  510. self.seek(offset)
  511. return struct.unpack(fmt, self.read(struct.calcsize(fmt)))
  512. def clean_string(s):
  513. s = s.rstrip(b'\x00')
  514. s = s.rstrip(b' ')
  515. return s
  516. def safe_decode_units(s):
  517. s = s.replace(b' ', b'')
  518. s = s.replace(b'\xb5', b'u') # \xb5 is µ
  519. s = s.replace(b'\xb0', b'\xc2\xb0') # \xb0 is °
  520. s = s.decode('utf-8')
  521. return s
  522. BLOCKSIZE = 512
  523. headerDescriptionV1 = [
  524. ('fFileSignature', 0, '4s'),
  525. ('fFileVersionNumber', 4, 'f'),
  526. ('nOperationMode', 8, 'h'),
  527. ('lActualAcqLength', 10, 'i'),
  528. ('nNumPointsIgnored', 14, 'h'),
  529. ('lActualEpisodes', 16, 'i'),
  530. ('lFileStartTime', 24, 'i'),
  531. ('lDataSectionPtr', 40, 'i'),
  532. ('lTagSectionPtr', 44, 'i'),
  533. ('lNumTagEntries', 48, 'i'),
  534. ('lSynchArrayPtr', 92, 'i'),
  535. ('lSynchArraySize', 96, 'i'),
  536. ('nDataFormat', 100, 'h'),
  537. ('nADCNumChannels', 120, 'h'),
  538. ('fADCSampleInterval', 122, 'f'),
  539. ('fSynchTimeUnit', 130, 'f'),
  540. ('lNumSamplesPerEpisode', 138, 'i'),
  541. ('lPreTriggerSamples', 142, 'i'),
  542. ('lEpisodesPerRun', 146, 'i'),
  543. ('fADCRange', 244, 'f'),
  544. ('lADCResolution', 252, 'i'),
  545. ('nFileStartMillisecs', 366, 'h'),
  546. ('nADCPtoLChannelMap', 378, '16h'),
  547. ('nADCSamplingSeq', 410, '16h'),
  548. ('sADCChannelName', 442, '10s' * 16),
  549. ('sADCUnits', 602, '8s' * 16),
  550. ('fADCProgrammableGain', 730, '16f'),
  551. ('fInstrumentScaleFactor', 922, '16f'),
  552. ('fInstrumentOffset', 986, '16f'),
  553. ('fSignalGain', 1050, '16f'),
  554. ('fSignalOffset', 1114, '16f'),
  555. ('nDigitalEnable', 1436, 'h'),
  556. ('nActiveDACChannel', 1440, 'h'),
  557. ('nDigitalHolding', 1584, 'h'),
  558. ('nDigitalInterEpisode', 1586, 'h'),
  559. ('nDigitalValue', 2588, '10h'),
  560. ('lDACFilePtr', 2048, '2i'),
  561. ('lDACFileNumEpisodes', 2056, '2i'),
  562. ('fDACCalibrationFactor', 2074, '4f'),
  563. ('fDACCalibrationOffset', 2090, '4f'),
  564. ('nWaveformEnable', 2296, '2h'),
  565. ('nWaveformSource', 2300, '2h'),
  566. ('nInterEpisodeLevel', 2304, '2h'),
  567. ('nEpochType', 2308, '20h'),
  568. ('fEpochInitLevel', 2348, '20f'),
  569. ('fEpochLevelInc', 2428, '20f'),
  570. ('lEpochInitDuration', 2508, '20i'),
  571. ('lEpochDurationInc', 2588, '20i'),
  572. ('nTelegraphEnable', 4512, '16h'),
  573. ('fTelegraphAdditGain', 4576, '16f'),
  574. ('sProtocolPath', 4898, '384s'),
  575. ]
  576. headerDescriptionV2 = [
  577. ('fFileSignature', 0, '4s'),
  578. ('fFileVersionNumber', 4, '4b'),
  579. ('uFileInfoSize', 8, 'I'),
  580. ('lActualEpisodes', 12, 'I'),
  581. ('uFileStartDate', 16, 'I'),
  582. ('uFileStartTimeMS', 20, 'I'),
  583. ('uStopwatchTime', 24, 'I'),
  584. ('nFileType', 28, 'H'),
  585. ('nDataFormat', 30, 'H'),
  586. ('nSimultaneousScan', 32, 'H'),
  587. ('nCRCEnable', 34, 'H'),
  588. ('uFileCRC', 36, 'I'),
  589. ('FileGUID', 40, 'I'),
  590. ('uCreatorVersion', 56, 'I'),
  591. ('uCreatorNameIndex', 60, 'I'),
  592. ('uModifierVersion', 64, 'I'),
  593. ('uModifierNameIndex', 68, 'I'),
  594. ('uProtocolPathIndex', 72, 'I'),
  595. ]
  596. sectionNames = [
  597. 'ProtocolSection',
  598. 'ADCSection',
  599. 'DACSection',
  600. 'EpochSection',
  601. 'ADCPerDACSection',
  602. 'EpochPerDACSection',
  603. 'UserListSection',
  604. 'StatsRegionSection',
  605. 'MathSection',
  606. 'StringsSection',
  607. 'DataSection',
  608. 'TagSection',
  609. 'ScopeSection',
  610. 'DeltaSection',
  611. 'VoiceTagSection',
  612. 'SynchArraySection',
  613. 'AnnotationSection',
  614. 'StatsSection',
  615. ]
  616. protocolInfoDescription = [
  617. ('nOperationMode', 'h'),
  618. ('fADCSequenceInterval', 'f'),
  619. ('bEnableFileCompression', 'b'),
  620. ('sUnused1', '3s'),
  621. ('uFileCompressionRatio', 'I'),
  622. ('fSynchTimeUnit', 'f'),
  623. ('fSecondsPerRun', 'f'),
  624. ('lNumSamplesPerEpisode', 'i'),
  625. ('lPreTriggerSamples', 'i'),
  626. ('lEpisodesPerRun', 'i'),
  627. ('lRunsPerTrial', 'i'),
  628. ('lNumberOfTrials', 'i'),
  629. ('nAveragingMode', 'h'),
  630. ('nUndoRunCount', 'h'),
  631. ('nFirstEpisodeInRun', 'h'),
  632. ('fTriggerThreshold', 'f'),
  633. ('nTriggerSource', 'h'),
  634. ('nTriggerAction', 'h'),
  635. ('nTriggerPolarity', 'h'),
  636. ('fScopeOutputInterval', 'f'),
  637. ('fEpisodeStartToStart', 'f'),
  638. ('fRunStartToStart', 'f'),
  639. ('lAverageCount', 'i'),
  640. ('fTrialStartToStart', 'f'),
  641. ('nAutoTriggerStrategy', 'h'),
  642. ('fFirstRunDelayS', 'f'),
  643. ('nChannelStatsStrategy', 'h'),
  644. ('lSamplesPerTrace', 'i'),
  645. ('lStartDisplayNum', 'i'),
  646. ('lFinishDisplayNum', 'i'),
  647. ('nShowPNRawData', 'h'),
  648. ('fStatisticsPeriod', 'f'),
  649. ('lStatisticsMeasurements', 'i'),
  650. ('nStatisticsSaveStrategy', 'h'),
  651. ('fADCRange', 'f'),
  652. ('fDACRange', 'f'),
  653. ('lADCResolution', 'i'),
  654. ('lDACResolution', 'i'),
  655. ('nExperimentType', 'h'),
  656. ('nManualInfoStrategy', 'h'),
  657. ('nCommentsEnable', 'h'),
  658. ('lFileCommentIndex', 'i'),
  659. ('nAutoAnalyseEnable', 'h'),
  660. ('nSignalType', 'h'),
  661. ('nDigitalEnable', 'h'),
  662. ('nActiveDACChannel', 'h'),
  663. ('nDigitalHolding', 'h'),
  664. ('nDigitalInterEpisode', 'h'),
  665. ('nDigitalDACChannel', 'h'),
  666. ('nDigitalTrainActiveLogic', 'h'),
  667. ('nStatsEnable', 'h'),
  668. ('nStatisticsClearStrategy', 'h'),
  669. ('nLevelHysteresis', 'h'),
  670. ('lTimeHysteresis', 'i'),
  671. ('nAllowExternalTags', 'h'),
  672. ('nAverageAlgorithm', 'h'),
  673. ('fAverageWeighting', 'f'),
  674. ('nUndoPromptStrategy', 'h'),
  675. ('nTrialTriggerSource', 'h'),
  676. ('nStatisticsDisplayStrategy', 'h'),
  677. ('nExternalTagType', 'h'),
  678. ('nScopeTriggerOut', 'h'),
  679. ('nLTPType', 'h'),
  680. ('nAlternateDACOutputState', 'h'),
  681. ('nAlternateDigitalOutputState', 'h'),
  682. ('fCellID', '3f'),
  683. ('nDigitizerADCs', 'h'),
  684. ('nDigitizerDACs', 'h'),
  685. ('nDigitizerTotalDigitalOuts', 'h'),
  686. ('nDigitizerSynchDigitalOuts', 'h'),
  687. ('nDigitizerType', 'h'),
  688. ]
  689. ADCInfoDescription = [
  690. ('nADCNum', 'h'),
  691. ('nTelegraphEnable', 'h'),
  692. ('nTelegraphInstrument', 'h'),
  693. ('fTelegraphAdditGain', 'f'),
  694. ('fTelegraphFilter', 'f'),
  695. ('fTelegraphMembraneCap', 'f'),
  696. ('nTelegraphMode', 'h'),
  697. ('fTelegraphAccessResistance', 'f'),
  698. ('nADCPtoLChannelMap', 'h'),
  699. ('nADCSamplingSeq', 'h'),
  700. ('fADCProgrammableGain', 'f'),
  701. ('fADCDisplayAmplification', 'f'),
  702. ('fADCDisplayOffset', 'f'),
  703. ('fInstrumentScaleFactor', 'f'),
  704. ('fInstrumentOffset', 'f'),
  705. ('fSignalGain', 'f'),
  706. ('fSignalOffset', 'f'),
  707. ('fSignalLowpassFilter', 'f'),
  708. ('fSignalHighpassFilter', 'f'),
  709. ('nLowpassFilterType', 'b'),
  710. ('nHighpassFilterType', 'b'),
  711. ('fPostProcessLowpassFilter', 'f'),
  712. ('nPostProcessLowpassFilterType', 'c'),
  713. ('bEnabledDuringPN', 'b'),
  714. ('nStatsChannelPolarity', 'h'),
  715. ('lADCChannelNameIndex', 'i'),
  716. ('lADCUnitsIndex', 'i'),
  717. ]
  718. TagInfoDescription = [
  719. ('lTagTime', 'i'),
  720. ('sComment', '56s'),
  721. ('nTagType', 'h'),
  722. ('nVoiceTagNumber_or_AnnotationIndex', 'h'),
  723. ]
  724. DACInfoDescription = [
  725. ('nDACNum', 'h'),
  726. ('nTelegraphDACScaleFactorEnable', 'h'),
  727. ('fInstrumentHoldingLevel', 'f'),
  728. ('fDACScaleFactor', 'f'),
  729. ('fDACHoldingLevel', 'f'),
  730. ('fDACCalibrationFactor', 'f'),
  731. ('fDACCalibrationOffset', 'f'),
  732. ('lDACChannelNameIndex', 'i'),
  733. ('lDACChannelUnitsIndex', 'i'),
  734. ('lDACFilePtr', 'i'),
  735. ('lDACFileNumEpisodes', 'i'),
  736. ('nWaveformEnable', 'h'),
  737. ('nWaveformSource', 'h'),
  738. ('nInterEpisodeLevel', 'h'),
  739. ('fDACFileScale', 'f'),
  740. ('fDACFileOffset', 'f'),
  741. ('lDACFileEpisodeNum', 'i'),
  742. ('nDACFileADCNum', 'h'),
  743. ('nConditEnable', 'h'),
  744. ('lConditNumPulses', 'i'),
  745. ('fBaselineDuration', 'f'),
  746. ('fBaselineLevel', 'f'),
  747. ('fStepDuration', 'f'),
  748. ('fStepLevel', 'f'),
  749. ('fPostTrainPeriod', 'f'),
  750. ('fPostTrainLevel', 'f'),
  751. ('nMembTestEnable', 'h'),
  752. ('nLeakSubtractType', 'h'),
  753. ('nPNPolarity', 'h'),
  754. ('fPNHoldingLevel', 'f'),
  755. ('nPNNumADCChannels', 'h'),
  756. ('nPNPosition', 'h'),
  757. ('nPNNumPulses', 'h'),
  758. ('fPNSettlingTime', 'f'),
  759. ('fPNInterpulse', 'f'),
  760. ('nLTPUsageOfDAC', 'h'),
  761. ('nLTPPresynapticPulses', 'h'),
  762. ('lDACFilePathIndex', 'i'),
  763. ('fMembTestPreSettlingTimeMS', 'f'),
  764. ('fMembTestPostSettlingTimeMS', 'f'),
  765. ('nLeakSubtractADCIndex', 'h'),
  766. ('sUnused', '124s'),
  767. ]
  768. EpochInfoPerDACDescription = [
  769. ('nEpochNum', 'h'),
  770. ('nDACNum', 'h'),
  771. ('nEpochType', 'h'),
  772. ('fEpochInitLevel', 'f'),
  773. ('fEpochLevelInc', 'f'),
  774. ('lEpochInitDuration', 'i'),
  775. ('lEpochDurationInc', 'i'),
  776. ('lEpochPulsePeriod', 'i'),
  777. ('lEpochPulseWidth', 'i'),
  778. ('sUnused', '18s'),
  779. ]
  780. EpochInfoDescription = [
  781. ('nEpochNum', 'h'),
  782. ('nDigitalValue', 'h'),
  783. ('nDigitalTrainValue', 'h'),
  784. ('nAlternateDigitalValue', 'h'),
  785. ('nAlternateDigitalTrainValue', 'h'),
  786. ('bEpochCompression', 'b'),
  787. ('sUnused', '21s'),
  788. ]