axographrawio.py 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521
  1. """
  2. AxographRawIO
  3. =============
  4. RawIO class for reading AxoGraph files (.axgd, .axgx)
  5. Original author: Jeffrey Gill
  6. Documentation of the AxoGraph file format provided by the developer is
  7. incomplete and in some cases incorrect. The primary sources of official
  8. documentation are found in two out-of-date documents:
  9. - AxoGraph X User Manual, provided with AxoGraph and also available online:
  10. https://axograph.com/documentation/AxoGraph%20User%20Manual.pdf
  11. - AxoGraph_ReadWrite.h, a header file that is part of a C++ program
  12. provided with AxoGraph, and which is also available here:
  13. https://github.com/CWRUChielLab/axographio/blob/master/
  14. axographio/include/axograph_readwrite/AxoGraph_ReadWrite.h
  15. These were helpful starting points for building this RawIO, especially for
  16. reading the beginnings of AxoGraph files, but much of the rest of the file
  17. format was deciphered by reverse engineering and guess work. Some portions of
  18. the file format remain undeciphered.
  19. The AxoGraph file format is versatile in that it can represent both time series
  20. data collected during data acquisition and non-time series data generated
  21. through manual or automated analysis, such as power spectrum analysis. This
  22. implementation of an AxoGraph file format reader makes no effort to decoding
  23. non-time series data. For simplicity, it makes several assumptions that should
  24. be valid for any file generated directly from episodic or continuous data
  25. acquisition without significant post-acquisition modification by the user.
  26. Detailed logging is provided during header parsing for debugging purposes:
  27. >>> import logging
  28. >>> r = AxographRawIO(filename)
  29. >>> r.logger.setLevel(logging.DEBUG)
  30. >>> r.parse_header()
  31. Background and Terminology
  32. --------------------------
  33. Acquisition modes:
  34. AxoGraph can operate in two main data acquisition modes:
  35. - Episodic "protocol-driven" acquisition mode, in which the program records
  36. from specified signal channels for a fixed duration each time a trigger
  37. is detected. Each trigger-activated recording is called an "episode".
  38. From files acquired in this mode, AxographRawIO creates multiple Neo
  39. Segments, one for each episode, unless force_single_segment=True.
  40. - Continuous "chart recorder" acquisition mode, in which it creates a
  41. continuous recording that can be paused and continued by the user
  42. whenever they like. From files acquired in this mode, AxographRawIO
  43. creates a single Neo Segment.
  44. "Episode": analogous to a Neo Segment
  45. See descriptions of acquisition modes above and of groups below.
  46. "Column": analogous to a Quantity array
  47. A column is a 1-dimensional array of data, stored in any one of a number of
  48. data types (e.g., scaled ints or floats). In the oldest version of the
  49. AxoGraph file format, even time was stored as a 1-dimensional array. In
  50. newer versions, time is stored as a special type of "column" that is really
  51. just a starting time and a sampling period.
  52. Column data appears in series in the file, i.e., all of the first column's
  53. data appears before the second column's. As an aside, because of this
  54. design choice AxoGraph cannot write data to disk as it is collected but
  55. must store it all in memory until data acquisition ends. This also affected
  56. how file slicing was implmented for this RawIO: Instead of using a single
  57. memmap to address into a 2-dimensional block of data, AxographRawIO
  58. constructs multiple 1-dimensional memmaps, one for each column, each with
  59. its own offset.
  60. Each column's data array is preceded by a header containing the column
  61. title, which normally contains the units (e.g., "Current (nA)"). Data
  62. recorded in episodic acquisition mode will contain a repeating sequence of
  63. column names, where each repetition corresponds to an episode (e.g.,
  64. "Time", "Column A", "Column B", "Column A", "Column B", etc.).
  65. AxoGraph offers a spreadsheet view for viewing all column data.
  66. "Trace": analogous to a single-channel Neo AnalogSignal
  67. A trace is a 2-dimensional series. Raw data is not stored in the part of
  68. the file concerned with traces. Instead, in the header for each trace are
  69. indexes pointing to two data columns, defined earlier in the file,
  70. corresponding to the trace's x and y data. These indexes can be changed in
  71. AxoGraph under the "Assign X and Y Columns" tool, though doing so may
  72. violate assumptions made by AxographRawIO.
  73. For time series data collected under the usual data acquisition modes that
  74. has not been modified after collection by the user, the x-index always
  75. points to the time column; one trace exists for each non-time column, with
  76. the y-index pointing to that column.
  77. Traces are analogous to AnalogSignals in Neo. However, for simplicity of
  78. implementation, AxographRawIO does not actually check the pairing of
  79. columns in the trace headers. Instead it assumes the default pairing
  80. described above when it creates signal channels while scanning through
  81. columns. Older versions of the AxoGraph file format lack trace headers
  82. entirely, so this is the most general solution.
  83. Trace headers contain additional information about the series, such as plot
  84. style, which is parsed by AxographRawIO and made available in
  85. self.info['trace_header_info_list'] but is otherwise unused.
  86. "Group": analogous to a Neo ChannelIndex for matching channels across Segments
  87. A group is a collection of one or more traces. Like traces, raw data is not
  88. stored in the part of the file concerned with groups. Instead, each trace
  89. header contains an index pointing to the group it is assigned to. Group
  90. assignment of traces can be changed in AxoGraph under the "Group Traces"
  91. tool, or by using the "Merge Traces" or "Separate Traces" commands, though
  92. doing so may violate assumptions made by AxographRawIO.
  93. Files created in episodic acquisition mode contain multiple traces per
  94. group, one for each episode. In that mode, a group corresponds to a signal
  95. channel and is analogous to a ChannelIndex in Neo; the traces within the
  96. group represent the time series recorded for that channel across episodes
  97. and are analogous to AnalogSignals from multiple Segments in Neo.
  98. In contrast, files created in continuous acquisition mode contain one trace
  99. per group, each corresponding to a signal channel. In that mode, groups and
  100. traces are basically conceptually synonymous, though the former can still
  101. be thought of as analogous to ChannelIndexes in Neo for a single-Segment.
  102. Group headers are only consulted by AxographRawIO to determine if is safe
  103. to interpret a file as episodic and therefore translatable to multiple
  104. Segments in Neo. Certain criteria have to be met, such as all groups
  105. containing equal numbers of traces and each group having homogeneous signal
  106. parameters. If trace grouping was modified by the user after data
  107. acquisition, this may result in the file being interpretted as
  108. non-episodic. Older versions of the AxoGraph file format lack group headers
  109. entirely, so these files are never deemed safe to interpret as episodic,
  110. even if the column names follow a repeating sequence as described above.
  111. "Tag" / "Event marker": analogous to a Neo Event
  112. In continuous acquisition mode, the user can press a hot key to tag a
  113. moment in time with a short label. Additionally, if the user stops or
  114. restarts data acquisition in this mode, a tag is created automatically with
  115. the label "Stop" or "Start", respectively. These are displayed by AxoGraph
  116. as event markers. AxographRawIO will organize all event markers into a
  117. single Neo Event channel with the name "AxoGraph Tags".
  118. In episodic acquisition mode, the tag hot key behaves differently. The
  119. current episode number is recorded in a user-editable notes section of the
  120. file, made available by AxographRawIO in self.info['notes']. Because these
  121. do not correspond to moments in time, they are not processed into Neo
  122. Events.
  123. "Interval bar": analogous to a Neo Epoch
  124. After data acquisition, the user can annotate an AxoGraph file with
  125. horizontal, labeled bars called interval bars that span a specified period
  126. of time. These are not episode specific. AxographRawIO will organize all
  127. interval bars into a single Neo Epoch channel with the name "AxoGraph
  128. Intervals".
  129. """
  130. from .baserawio import (BaseRawIO, _signal_channel_dtype, _unit_channel_dtype,
  131. _event_channel_dtype)
  132. import os
  133. from datetime import datetime
  134. from io import open, BufferedReader
  135. from struct import unpack, calcsize
  136. import numpy as np
  137. class AxographRawIO(BaseRawIO):
  138. """
  139. RawIO class for reading AxoGraph files (.axgd, .axgx)
  140. Args:
  141. filename (string):
  142. File name of the AxoGraph file to read.
  143. force_single_segment (bool):
  144. Episodic files are normally read as multi-Segment Neo objects. This
  145. parameter can force AxographRawIO to put all signals into a single
  146. Segment. Default: False.
  147. Example:
  148. >>> import neo
  149. >>> r = neo.rawio.AxographRawIO(filename=filename)
  150. >>> r.parse_header()
  151. >>> print(r)
  152. >>> # get signals
  153. >>> raw_chunk = r.get_analogsignal_chunk(
  154. ... block_index=0, seg_index=0,
  155. ... i_start=0, i_stop=1024,
  156. ... channel_names=channel_names)
  157. >>> float_chunk = r.rescale_signal_raw_to_float(
  158. ... raw_chunk,
  159. ... dtype='float64',
  160. ... channel_names=channel_names)
  161. >>> print(float_chunk)
  162. >>> # get event markers
  163. >>> ev_raw_times, _, ev_labels = r.get_event_timestamps(
  164. ... event_channel_index=0)
  165. >>> ev_times = r.rescale_event_timestamp(
  166. ... ev_raw_times, dtype='float64')
  167. >>> print([ev for ev in zip(ev_times, ev_labels)])
  168. >>> # get interval bars
  169. >>> ep_raw_times, ep_raw_durations, ep_labels = r.get_event_timestamps(
  170. ... event_channel_index=1)
  171. >>> ep_times = r.rescale_event_timestamp(
  172. ... ep_raw_times, dtype='float64')
  173. >>> ep_durations = r.rescale_epoch_duration(
  174. ... ep_raw_durations, dtype='float64')
  175. >>> print([ep for ep in zip(ep_times, ep_durations, ep_labels)])
  176. >>> # get notes
  177. >>> print(r.info['notes'])
  178. >>> # get other miscellaneous info
  179. >>> print(r.info)
  180. """
  181. name = 'AxographRawIO'
  182. description = 'This IO reads .axgd/.axgx files created with AxoGraph'
  183. extensions = ['axgd', 'axgx']
  184. rawmode = 'one-file'
  185. def __init__(self, filename, force_single_segment=False):
  186. BaseRawIO.__init__(self)
  187. self.filename = filename
  188. self.force_single_segment = force_single_segment
  189. def _parse_header(self):
  190. self.header = {}
  191. self._scan_axograph_file()
  192. if not self.force_single_segment and self._safe_to_treat_as_episodic():
  193. self.logger.debug('Will treat as episodic')
  194. self._convert_to_multi_segment()
  195. else:
  196. self.logger.debug('Will not treat as episodic')
  197. self.logger.debug('')
  198. self._generate_minimal_annotations()
  199. blk_annotations = self.raw_annotations['blocks'][0]
  200. blk_annotations['format_ver'] = self.info['format_ver']
  201. blk_annotations['comment'] = self.info['comment'] if 'comment' in self.info else None
  202. blk_annotations['notes'] = self.info['notes'] if 'notes' in self.info else None
  203. blk_annotations['rec_datetime'] = self._get_rec_datetime()
  204. # modified time is not ideal but less prone to
  205. # cross-platform issues than created time (ctime)
  206. blk_annotations['file_datetime'] = datetime.fromtimestamp(
  207. os.path.getmtime(self.filename))
  208. def _source_name(self):
  209. return self.filename
  210. def _segment_t_start(self, block_index, seg_index):
  211. # same for all segments
  212. return self._t_start
  213. def _segment_t_stop(self, block_index, seg_index):
  214. # same for all signals in all segments
  215. t_stop = self._t_start + \
  216. len(self._raw_signals[seg_index][0]) * self._sampling_period
  217. return t_stop
  218. ###
  219. # signal and channel zone
  220. def _get_signal_size(self, block_index, seg_index, channel_indexes):
  221. # same for all signals in all segments
  222. return len(self._raw_signals[seg_index][0])
  223. def _get_signal_t_start(self, block_index, seg_index, channel_indexes):
  224. # same for all signals in all segments
  225. return self._t_start
  226. def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop,
  227. channel_indexes):
  228. if channel_indexes is None or \
  229. np.all(channel_indexes == slice(None, None, None)):
  230. channel_indexes = range(self.signal_channels_count())
  231. raw_signals = [self._raw_signals
  232. [seg_index]
  233. [channel_index]
  234. [slice(i_start, i_stop)]
  235. for channel_index in channel_indexes]
  236. raw_signals = np.array(raw_signals).T # loads data into memory
  237. return raw_signals
  238. ###
  239. # spiketrain and unit zone
  240. def _spike_count(self, block_index, seg_index, unit_index):
  241. # not supported
  242. return None
  243. def _get_spike_timestamps(self, block_index, seg_index, unit_index,
  244. t_start, t_stop):
  245. # not supported
  246. return None
  247. def _rescale_spike_timestamp(self, spike_timestamps, dtype):
  248. # not supported
  249. return None
  250. ###
  251. # spike waveforms zone
  252. def _get_spike_raw_waveforms(self, block_index, seg_index, unit_index,
  253. t_start, t_stop):
  254. # not supported
  255. return None
  256. ###
  257. # event and epoch zone
  258. def _event_count(self, block_index, seg_index, event_channel_index):
  259. # Retrieve size of either event or epoch channel:
  260. # event_channel_index: 0 AxoGraph Tags, 1 AxoGraph Intervals
  261. # AxoGraph tags can only be inserted in continuous data acquisition
  262. # mode. When the tag hot key is pressed in episodic acquisition mode,
  263. # the notes are updated with the current episode number instead of an
  264. # instantaneous event marker being created. This means that Neo-like
  265. # Events cannot be generated by AxoGraph for multi-Segment (episodic)
  266. # files. Furthermore, Neo-like Epochs (interval markers) are not
  267. # episode specific. For these reasons, this function ignores seg_index.
  268. return self._raw_event_epoch_timestamps[event_channel_index].size
  269. def _get_event_timestamps(self, block_index, seg_index,
  270. event_channel_index, t_start, t_stop):
  271. # Retrieve either event or epoch data, unscaled:
  272. # event_channel_index: 0 AxoGraph Tags, 1 AxoGraph Intervals
  273. # AxoGraph tags can only be inserted in continuous data acquisition
  274. # mode. When the tag hot key is pressed in episodic acquisition mode,
  275. # the notes are updated with the current episode number instead of an
  276. # instantaneous event marker being created. This means that Neo-like
  277. # Events cannot be generated by AxoGraph for multi-Segment (episodic)
  278. # files. Furthermore, Neo-like Epochs (interval markers) are not
  279. # episode specific. For these reasons, this function ignores seg_index.
  280. timestamps = self._raw_event_epoch_timestamps[event_channel_index]
  281. durations = self._raw_event_epoch_durations[event_channel_index]
  282. labels = self._event_epoch_labels[event_channel_index]
  283. if durations is None:
  284. # events
  285. if t_start is not None:
  286. # keep if event occurs after t_start ...
  287. keep = timestamps >= int(t_start / self._sampling_period)
  288. timestamps = timestamps[keep]
  289. labels = labels[keep]
  290. if t_stop is not None:
  291. # ... and before t_stop
  292. keep = timestamps <= int(t_stop / self._sampling_period)
  293. timestamps = timestamps[keep]
  294. labels = labels[keep]
  295. else:
  296. # epochs
  297. if t_start is not None:
  298. # keep if epoch ends after t_start ...
  299. keep = timestamps + durations >= \
  300. int(t_start / self._sampling_period)
  301. timestamps = timestamps[keep]
  302. durations = durations[keep]
  303. labels = labels[keep]
  304. if t_stop is not None:
  305. # ... and starts before t_stop
  306. keep = timestamps <= int(t_stop / self._sampling_period)
  307. timestamps = timestamps[keep]
  308. durations = durations[keep]
  309. labels = labels[keep]
  310. return timestamps, durations, labels
  311. def _rescale_event_timestamp(self, event_timestamps, dtype):
  312. # Scale either event or epoch start times from sample index to seconds
  313. # (t_start shouldn't be added)
  314. event_times = event_timestamps.astype(dtype) * self._sampling_period
  315. return event_times
  316. def _rescale_epoch_duration(self, raw_duration, dtype):
  317. # Scale epoch durations from samples to seconds
  318. epoch_durations = raw_duration.astype(dtype) * self._sampling_period
  319. return epoch_durations
  320. ###
  321. # multi-segment zone
  322. def _safe_to_treat_as_episodic(self):
  323. """
  324. The purpose of this fuction is to determine if the file contains any
  325. irregularities in its grouping of traces such that it cannot be treated
  326. as episodic. Even "continuous" recordings can be treated as
  327. single-episode recordings and could be identified as safe by this
  328. function. Recordings in which the user has changed groupings to create
  329. irregularities should be caught by this function.
  330. """
  331. # First check: Old AxoGraph file formats do not contain enough metadata
  332. # to know for certain that the file is episodic.
  333. if self.info['format_ver'] < 3:
  334. self.logger.debug('Cannot treat as episodic because old format '
  335. 'contains insufficient metadata')
  336. return False
  337. # Second check: If the file is episodic, it should report that it
  338. # contains more than 1 episode.
  339. if 'n_episodes' not in self.info:
  340. self.logger.debug('Cannot treat as episodic because episode '
  341. 'metadata is missing or could not be parsed')
  342. return False
  343. if self.info['n_episodes'] == 1:
  344. self.logger.debug('Cannot treat as episodic because file reports '
  345. 'one episode')
  346. return False
  347. # Third check: If the file is episodic, groups of traces should all
  348. # contain the same number of traces, one for each episode. This is
  349. # generally true of "continuous" (single-episode) recordings as well,
  350. # which normally have 1 trace per group.
  351. if 'group_header_info_list' not in self.info:
  352. self.logger.debug('Cannot treat as episodic because group '
  353. 'metadata is missing or could not be parsed')
  354. return False
  355. if 'trace_header_info_list' not in self.info:
  356. self.logger.debug('Cannot treat as episodic because trace '
  357. 'metadata is missing or could not be parsed')
  358. return False
  359. group_id_to_col_indexes = {}
  360. for group_id in self.info['group_header_info_list']:
  361. col_indexes = []
  362. for trace_header in self.info['trace_header_info_list'].values():
  363. if trace_header['group_id_for_this_trace'] == group_id:
  364. col_indexes.append(trace_header['y_index'])
  365. group_id_to_col_indexes[group_id] = col_indexes
  366. n_traces_by_group = {k: len(v) for k, v in
  367. group_id_to_col_indexes.items()}
  368. all_groups_have_same_number_of_traces = len(np.unique(list(
  369. n_traces_by_group.values()))) == 1
  370. if not all_groups_have_same_number_of_traces:
  371. self.logger.debug('Cannot treat as episodic because groups differ '
  372. 'in number of traces')
  373. return False
  374. # Fourth check: The number of traces in each group should equal
  375. # n_episodes.
  376. n_traces_per_group = np.unique(list(n_traces_by_group.values()))
  377. if n_traces_per_group != self.info['n_episodes']:
  378. self.logger.debug('Cannot treat as episodic because n_episodes '
  379. 'does not match number of traces per group')
  380. return False
  381. # Fifth check: If the file is episodic, all traces within a group
  382. # should have identical signal channel parameters (e.g., name, units)
  383. # except for their unique ids. This too is generally true of
  384. # "continuous" (single-episode) files, which normally have 1 trace per
  385. # group.
  386. signal_channels_with_ids_dropped = \
  387. self.header['signal_channels'][
  388. [n for n in self.header['signal_channels'].dtype.names
  389. if n != 'id']]
  390. group_has_uniform_signal_parameters = {}
  391. for group_id, col_indexes in group_id_to_col_indexes.items():
  392. # subtract 1 from indexes in next statement because time is not
  393. # included in signal_channels
  394. signal_params_for_group = np.array(
  395. signal_channels_with_ids_dropped[np.array(col_indexes) - 1])
  396. group_has_uniform_signal_parameters[group_id] = \
  397. len(np.unique(signal_params_for_group)) == 1
  398. all_groups_have_uniform_signal_parameters = \
  399. np.all(list(group_has_uniform_signal_parameters.values()))
  400. if not all_groups_have_uniform_signal_parameters:
  401. self.logger.debug('Cannot treat as episodic because some groups '
  402. 'have heterogeneous signal parameters')
  403. return False
  404. # all checks passed
  405. self.logger.debug('Can treat as episodic')
  406. return True
  407. def _convert_to_multi_segment(self):
  408. """
  409. Reshape signal headers and signal data for an episodic file
  410. """
  411. self.header['nb_segment'] = [self.info['n_episodes']]
  412. # drop repeated signal headers
  413. self.header['signal_channels'] = \
  414. self.header['signal_channels'].reshape(
  415. self.info['n_episodes'], -1)[0]
  416. # reshape signal memmap list
  417. new_sig_memmaps = []
  418. n_channels = len(self.header['signal_channels'])
  419. sig_memmaps = self._raw_signals[0]
  420. for first_index in np.arange(0, len(sig_memmaps), n_channels):
  421. new_sig_memmaps.append(
  422. sig_memmaps[first_index:first_index + n_channels])
  423. self._raw_signals = new_sig_memmaps
  424. self.logger.debug('New number of segments: {}'.format(
  425. self.info['n_episodes']))
  426. return
  427. def _get_rec_datetime(self):
  428. """
  429. Determine the date and time at which the recording was started from
  430. automatically generated notes. How these notes should be parsed differs
  431. depending on whether the recording was obtained in episodic or
  432. continuous acquisition mode.
  433. """
  434. rec_datetime = None
  435. date_string = ''
  436. time_string = ''
  437. datetime_string = ''
  438. if 'notes' not in self.info:
  439. return None
  440. for note_line in self.info['notes'].split('\n'):
  441. # episodic acquisition mode
  442. if note_line.startswith('Created on '):
  443. date_string = note_line.strip('Created on ')
  444. if note_line.startswith('Start data acquisition at '):
  445. time_string = note_line.strip('Start data acquisition at ')
  446. # continuous acquisition mode
  447. if note_line.startswith('Created : '):
  448. datetime_string = note_line.strip('Created : ')
  449. if date_string and time_string:
  450. datetime_string = ' '.join([date_string, time_string])
  451. if datetime_string:
  452. try:
  453. rec_datetime = datetime.strptime(datetime_string,
  454. '%a %b %d %Y %H:%M:%S')
  455. except ValueError:
  456. pass
  457. return rec_datetime
  458. def _scan_axograph_file(self):
  459. """
  460. This function traverses the entire AxoGraph file, constructing memmaps
  461. for signals and collecting channel information and other metadata
  462. """
  463. self.info = {}
  464. with open(self.filename, 'rb') as fid:
  465. f = StructFile(fid)
  466. self.logger.debug('filename: {}'.format(self.filename))
  467. self.logger.debug('')
  468. # the first 4 bytes are always a 4-character file type identifier
  469. # - for early versions of AxoGraph, this identifier was 'AxGr'
  470. # - starting with AxoGraph X, the identifier is 'axgx'
  471. header_id = f.read(4).decode('utf-8')
  472. self.info['header_id'] = header_id
  473. assert header_id in ['AxGr', 'axgx'], \
  474. 'not an AxoGraph binary file! "{}"'.format(self.filename)
  475. self.logger.debug('header_id: {}'.format(header_id))
  476. # the next two numbers store the format version number and the
  477. # number of data columns to follow
  478. # - for 'AxGr' files, these numbers are 2-byte unsigned short ints
  479. # - for 'axgx' files, these numbers are 4-byte long ints
  480. # - the 4-character identifier changed from 'AxGr' to 'axgx' with
  481. # format version 3
  482. if header_id == 'AxGr':
  483. format_ver, n_cols = f.read_f('HH')
  484. assert format_ver == 1 or format_ver == 2, \
  485. 'mismatch between header identifier "{}" and format ' \
  486. 'version "{}"!'.format(header_id, format_ver)
  487. elif header_id == 'axgx':
  488. format_ver, n_cols = f.read_f('ll')
  489. assert format_ver >= 3, \
  490. 'mismatch between header identifier "{}" and format ' \
  491. 'version "{}"!'.format(header_id, format_ver)
  492. else:
  493. raise NotImplementedError(
  494. 'unimplemented file header identifier "{}"!'.format(
  495. header_id))
  496. self.info['format_ver'] = format_ver
  497. self.info['n_cols'] = n_cols
  498. self.logger.debug('format_ver: {}'.format(format_ver))
  499. self.logger.debug('n_cols: {}'.format(n_cols))
  500. self.logger.debug('')
  501. ##############################################
  502. # BEGIN COLUMNS
  503. sig_memmaps = []
  504. sig_channels = []
  505. for i in range(n_cols):
  506. self.logger.debug('== COLUMN INDEX {} =='.format(i))
  507. ##############################################
  508. # NUMBER OF DATA POINTS IN COLUMN
  509. n_points = f.read_f('l')
  510. self.logger.debug('n_points: {}'.format(n_points))
  511. ##############################################
  512. # COLUMN TYPE
  513. # depending on the format version, data columns may have a type
  514. # - prior to verion 3, column types did not exist and data was
  515. # stored in a fixed pattern
  516. # - beginning with version 3, several data types are available
  517. # as documented in AxoGraph_ReadWrite.h
  518. if format_ver == 1 or format_ver == 2:
  519. col_type = None
  520. elif format_ver >= 3:
  521. col_type = f.read_f('l')
  522. else:
  523. raise NotImplementedError(
  524. 'unimplemented file format version "{}"!'.format(
  525. format_ver))
  526. self.logger.debug('col_type: {}'.format(col_type))
  527. ##############################################
  528. # COLUMN NAME AND UNITS
  529. # depending on the format version, column titles are stored
  530. # differently
  531. # - prior to version 3, column titles were stored as
  532. # fixed-length 80-byte Pascal strings
  533. # - beginning with version 3, column titles are stored as
  534. # variable-length strings (see StructFile.read_string for
  535. # details)
  536. if format_ver == 1 or format_ver == 2:
  537. title = f.read_f('80p').decode('utf-8')
  538. elif format_ver >= 3:
  539. title = f.read_f('S')
  540. else:
  541. raise NotImplementedError(
  542. 'unimplemented file format version "{}"!'.format(
  543. format_ver))
  544. self.logger.debug('title: {}'.format(title))
  545. # units are given in parentheses at the end of a column title,
  546. # unless units are absent
  547. if len(title.split()) > 0 and title.split()[-1][0] == '(' and \
  548. title.split()[-1][-1] == ')':
  549. name = ' '.join(title.split()[:-1])
  550. units = title.split()[-1].strip('()')
  551. else:
  552. name = title
  553. units = ''
  554. self.logger.debug('name: {}'.format(name))
  555. self.logger.debug('units: {}'.format(units))
  556. ##############################################
  557. # COLUMN DTYPE, SCALE, OFFSET
  558. if format_ver == 1:
  559. # for format version 1, all columns are arrays of floats
  560. dtype = 'f'
  561. gain, offset = 1, 0 # data is neither scaled nor off-set
  562. elif format_ver == 2:
  563. # for format version 2, the first column is a "series" of
  564. # regularly spaced values specified merely by a first value
  565. # and an increment, and all subsequent columns are arrays
  566. # of shorts with a scaling factor
  567. if i == 0:
  568. # series
  569. first_value, increment = f.read_f('ff')
  570. self.logger.debug(
  571. 'interval: {}, freq: {}'.format(
  572. increment, 1 / increment))
  573. self.logger.debug(
  574. 'start: {}, end: {}'.format(
  575. first_value,
  576. first_value + increment * (n_points - 1)))
  577. # assume this is the time column
  578. t_start, sampling_period = first_value, increment
  579. self.info['t_start'] = t_start
  580. self.info['sampling_period'] = sampling_period
  581. self.logger.debug('')
  582. continue # skip memmap, chan info for time col
  583. else:
  584. # scaled short
  585. dtype = 'h'
  586. gain, offset = \
  587. f.read_f('f'), 0 # data is scaled without offset
  588. elif format_ver >= 3:
  589. # for format versions 3 and later, the column type
  590. # determines how the data should be read
  591. # - column types 1, 2, 3, and 8 are not defined in
  592. # AxoGraph_ReadWrite.h
  593. # - column type 9 is different from the others in that it
  594. # represents regularly spaced values
  595. # (such as times at a fixed frequency) specified by a
  596. # first value and an increment, without storing a large
  597. # data array
  598. if col_type == 9:
  599. # series
  600. first_value, increment = f.read_f('dd')
  601. self.logger.debug(
  602. 'interval: {}, freq: {}'.format(
  603. increment, 1 / increment))
  604. self.logger.debug(
  605. 'start: {}, end: {}'.format(
  606. first_value,
  607. first_value + increment * (n_points - 1)))
  608. if i == 0:
  609. # assume this is the time column
  610. t_start, sampling_period = first_value, increment
  611. self.info['t_start'] = t_start
  612. self.info['sampling_period'] = sampling_period
  613. self.logger.debug('')
  614. continue # skip memmap, chan info for time col
  615. else:
  616. raise NotImplementedError(
  617. 'series data are supported only for the first '
  618. 'data column (time)!')
  619. elif col_type == 4:
  620. # short
  621. dtype = 'h'
  622. gain, offset = 1, 0 # data neither scaled nor off-set
  623. elif col_type == 5:
  624. # long
  625. dtype = 'l'
  626. gain, offset = 1, 0 # data neither scaled nor off-set
  627. elif col_type == 6:
  628. # float
  629. dtype = 'f'
  630. gain, offset = 1, 0 # data neither scaled nor off-set
  631. elif col_type == 7:
  632. # double
  633. dtype = 'd'
  634. gain, offset = 1, 0 # data neither scaled nor off-set
  635. elif col_type == 10:
  636. # scaled short
  637. dtype = 'h'
  638. gain, offset = f.read_f('dd') # data scaled w/ offset
  639. else:
  640. raise NotImplementedError(
  641. 'unimplemented column type "{}"!'.format(col_type))
  642. else:
  643. raise NotImplementedError(
  644. 'unimplemented file format version "{}"!'.format(
  645. format_ver))
  646. ##############################################
  647. # COLUMN MEMMAP AND CHANNEL INFO
  648. # create a memory map that allows accessing parts of the file
  649. # without loading it all into memory
  650. array = np.memmap(
  651. self.filename,
  652. mode='r',
  653. dtype=f.byte_order + dtype,
  654. offset=f.tell(),
  655. shape=n_points)
  656. # advance the file position to after the data array
  657. f.seek(array.nbytes, 1)
  658. if i == 0:
  659. # assume this is the time column containing n_points values
  660. # verify times are spaced regularly
  661. diffs = np.diff(array)
  662. increment = np.median(diffs)
  663. max_frac_step_deviation = np.max(np.abs(
  664. diffs / increment - 1))
  665. tolerance = 1e-3
  666. if max_frac_step_deviation > tolerance:
  667. self.logger.debug('largest proportional deviation '
  668. 'from median step size in the first '
  669. 'column exceeds the tolerance '
  670. 'of ' + str(tolerance) + ':'
  671. ' ' + str(max_frac_step_deviation))
  672. raise ValueError('first data column (assumed to be '
  673. 'time) is not regularly spaced')
  674. first_value = array[0]
  675. self.logger.debug(
  676. 'interval: {}, freq: {}'.format(
  677. increment, 1 / increment))
  678. self.logger.debug(
  679. 'start: {}, end: {}'.format(
  680. first_value,
  681. first_value + increment * (n_points - 1)))
  682. t_start, sampling_period = first_value, increment
  683. self.info['t_start'] = t_start
  684. self.info['sampling_period'] = sampling_period
  685. self.logger.debug('')
  686. continue # skip saving memmap, chan info for time col
  687. else:
  688. # not a time column
  689. self.logger.debug('gain: {}, offset: {}'.format(gain, offset))
  690. self.logger.debug('initial data: {}'.format(
  691. array[:5] * gain + offset))
  692. # channel_info will be cast to _signal_channel_dtype
  693. channel_info = (
  694. name, i, 1 / sampling_period, f.byte_order + dtype,
  695. units, gain, offset, 0)
  696. self.logger.debug('channel_info: {}'.format(channel_info))
  697. self.logger.debug('')
  698. sig_memmaps.append(array)
  699. sig_channels.append(channel_info)
  700. # END COLUMNS
  701. ##############################################
  702. # initialize lists for events and epochs
  703. raw_event_timestamps = []
  704. raw_epoch_timestamps = []
  705. raw_epoch_durations = []
  706. event_labels = []
  707. epoch_labels = []
  708. # the remainder of the file may contain metadata, events and epochs
  709. try:
  710. ##############################################
  711. # COMMENT
  712. self.logger.debug('== COMMENT ==')
  713. comment = f.read_f('S')
  714. self.info['comment'] = comment
  715. self.logger.debug(comment if comment else 'no comment!')
  716. self.logger.debug('')
  717. ##############################################
  718. # NOTES
  719. self.logger.debug('== NOTES ==')
  720. notes = f.read_f('S')
  721. self.info['notes'] = notes
  722. self.logger.debug(notes if notes else 'no notes!')
  723. self.logger.debug('')
  724. ##############################################
  725. # TRACES
  726. self.logger.debug('== TRACES ==')
  727. n_traces = f.read_f('l')
  728. self.info['n_traces'] = n_traces
  729. self.logger.debug('n_traces: {}'.format(n_traces))
  730. self.logger.debug('')
  731. trace_header_info_list = {}
  732. group_ids = []
  733. for i in range(n_traces):
  734. # AxoGraph traces are 1-indexed in GUI, so use i+1 below
  735. self.logger.debug('== TRACE #{} =='.format(i + 1))
  736. trace_header_info = {}
  737. if format_ver < 6:
  738. # before format version 6, there was only one version
  739. # of the header, and version numbers were not provided
  740. trace_header_info['trace_header_version'] = 1
  741. else:
  742. # for format versions 6 and later, the header version
  743. # must be read
  744. trace_header_info['trace_header_version'] = \
  745. f.read_f('l')
  746. if trace_header_info['trace_header_version'] == 1:
  747. TraceHeaderDescription = TraceHeaderDescriptionV1
  748. elif trace_header_info['trace_header_version'] == 2:
  749. TraceHeaderDescription = TraceHeaderDescriptionV2
  750. else:
  751. raise NotImplementedError(
  752. 'unimplemented trace header version "{}"!'.format(
  753. trace_header_info['trace_header_version']))
  754. for key, fmt in TraceHeaderDescription:
  755. trace_header_info[key] = f.read_f(fmt)
  756. # AxoGraph traces are 1-indexed in GUI, so use i+1 below
  757. trace_header_info_list[i + 1] = trace_header_info
  758. group_ids.append(
  759. trace_header_info['group_id_for_this_trace'])
  760. self.logger.debug(trace_header_info)
  761. self.logger.debug('')
  762. self.info['trace_header_info_list'] = trace_header_info_list
  763. ##############################################
  764. # GROUPS
  765. self.logger.debug('== GROUPS ==')
  766. n_groups = f.read_f('l')
  767. self.info['n_groups'] = n_groups
  768. group_ids = \
  769. np.sort(list(set(group_ids))) # remove duplicates and sort
  770. assert n_groups == len(group_ids), \
  771. 'expected group_ids to have length {}: {}'.format(
  772. n_groups, group_ids)
  773. self.logger.debug('n_groups: {}'.format(n_groups))
  774. self.logger.debug('group_ids: {}'.format(group_ids))
  775. self.logger.debug('')
  776. group_header_info_list = {}
  777. for i in group_ids:
  778. # AxoGraph groups are 0-indexed in GUI, so use i below
  779. self.logger.debug('== GROUP #{} =='.format(i))
  780. group_header_info = {}
  781. if format_ver < 6:
  782. # before format version 6, there was only one version
  783. # of the header, and version numbers were not provided
  784. group_header_info['group_header_version'] = 1
  785. else:
  786. # for format versions 6 and later, the header version
  787. # must be read
  788. group_header_info['group_header_version'] = \
  789. f.read_f('l')
  790. if group_header_info['group_header_version'] == 1:
  791. GroupHeaderDescription = GroupHeaderDescriptionV1
  792. else:
  793. raise NotImplementedError(
  794. 'unimplemented group header version "{}"!'.format(
  795. group_header_info['group_header_version']))
  796. for key, fmt in GroupHeaderDescription:
  797. group_header_info[key] = f.read_f(fmt)
  798. # AxoGraph groups are 0-indexed in GUI, so use i below
  799. group_header_info_list[i] = group_header_info
  800. self.logger.debug(group_header_info)
  801. self.logger.debug('')
  802. self.info['group_header_info_list'] = group_header_info_list
  803. ##############################################
  804. # UNKNOWN
  805. self.logger.debug('>> UNKNOWN 1 <<')
  806. # 36 bytes of undeciphered data (types here are guesses)
  807. unknowns = f.read_f('9l')
  808. self.logger.debug(unknowns)
  809. self.logger.debug('')
  810. ##############################################
  811. # EPISODES
  812. self.logger.debug('== EPISODES ==')
  813. # a subset of episodes can be selected for "review", or
  814. # episodes can be paged through one by one, and the indexes of
  815. # those currently in review appear in this list
  816. episodes_in_review = []
  817. n_episodes = f.read_f('l')
  818. self.info['n_episodes'] = n_episodes
  819. for i in range(n_episodes):
  820. episode_bool = f.read_f('Z')
  821. if episode_bool:
  822. episodes_in_review.append(i + 1)
  823. self.info['episodes_in_review'] = episodes_in_review
  824. self.logger.debug('n_episodes: {}'.format(n_episodes))
  825. self.logger.debug('episodes_in_review: {}'.format(
  826. episodes_in_review))
  827. if format_ver == 5:
  828. # the test file for version 5 contains this extra list of
  829. # episode indexes with unknown purpose
  830. old_unknown_episode_list = []
  831. n_episodes2 = f.read_f('l')
  832. for i in range(n_episodes2):
  833. episode_bool = f.read_f('Z')
  834. if episode_bool:
  835. old_unknown_episode_list.append(i + 1)
  836. self.logger.debug('old_unknown_episode_list: {}'.format(
  837. old_unknown_episode_list))
  838. if n_episodes2 != n_episodes:
  839. self.logger.debug(
  840. 'n_episodes2 ({}) and n_episodes ({}) '
  841. 'differ!'.format(n_episodes2, n_episodes))
  842. # another list of episode indexes with unknown purpose
  843. unknown_episode_list = []
  844. n_episodes3 = f.read_f('l')
  845. for i in range(n_episodes3):
  846. episode_bool = f.read_f('Z')
  847. if episode_bool:
  848. unknown_episode_list.append(i + 1)
  849. self.logger.debug('unknown_episode_list: {}'.format(
  850. unknown_episode_list))
  851. if n_episodes3 != n_episodes:
  852. self.logger.debug(
  853. 'n_episodes3 ({}) and n_episodes ({}) '
  854. 'differ!'.format(n_episodes3, n_episodes))
  855. # episodes can be masked to be removed from the pool of
  856. # reviewable episodes completely until unmasked, and the
  857. # indexes of those currently masked appear in this list
  858. masked_episodes = []
  859. n_episodes4 = f.read_f('l')
  860. for i in range(n_episodes4):
  861. episode_bool = f.read_f('Z')
  862. if episode_bool:
  863. masked_episodes.append(i + 1)
  864. self.info['masked_episodes'] = masked_episodes
  865. self.logger.debug('masked_episodes: {}'.format(
  866. masked_episodes))
  867. if n_episodes4 != n_episodes:
  868. self.logger.debug(
  869. 'n_episodes4 ({}) and n_episodes ({}) '
  870. 'differ!'.format(n_episodes4, n_episodes))
  871. self.logger.debug('')
  872. ##############################################
  873. # UNKNOWN
  874. self.logger.debug('>> UNKNOWN 2 <<')
  875. # 68 bytes of undeciphered data (types here are guesses)
  876. unknowns = f.read_f('d 9l d 4l')
  877. self.logger.debug(unknowns)
  878. self.logger.debug('')
  879. ##############################################
  880. # FONTS
  881. if format_ver >= 6:
  882. font_categories = ['axis titles', 'axis labels (ticks)',
  883. 'notes', 'graph title']
  884. else:
  885. # would need an old version of AxoGraph to determine how it
  886. # used these settings
  887. font_categories = ['everything (?)']
  888. font_settings_info_list = {}
  889. for i in font_categories:
  890. self.logger.debug('== FONT SETTINGS FOR {} =='.format(i))
  891. font_settings_info = {}
  892. for key, fmt in FontSettingsDescription:
  893. font_settings_info[key] = f.read_f(fmt)
  894. # I don't know why two arbitrary values were selected to
  895. # represent this switch, but it seems they were
  896. # - setting1 could contain other undeciphered data as a
  897. # bitmask, like setting2
  898. assert font_settings_info['setting1'] in \
  899. [FONT_BOLD, FONT_NOT_BOLD], \
  900. 'expected setting1 ({}) to have value FONT_BOLD ' \
  901. '({}) or FONT_NOT_BOLD ({})'.format(
  902. font_settings_info['setting1'],
  903. FONT_BOLD,
  904. FONT_NOT_BOLD)
  905. # size is stored 10 times bigger than real value
  906. font_settings_info['size'] = \
  907. font_settings_info['size'] / 10.0
  908. font_settings_info['bold'] = \
  909. bool(font_settings_info['setting1'] == FONT_BOLD)
  910. font_settings_info['italics'] = \
  911. bool(font_settings_info['setting2'] & FONT_ITALICS)
  912. font_settings_info['underline'] = \
  913. bool(font_settings_info['setting2'] & FONT_UNDERLINE)
  914. font_settings_info['strikeout'] = \
  915. bool(font_settings_info['setting2'] & FONT_STRIKEOUT)
  916. font_settings_info_list[i] = font_settings_info
  917. self.logger.debug(font_settings_info)
  918. self.logger.debug('')
  919. self.info['font_settings_info_list'] = font_settings_info_list
  920. ##############################################
  921. # X-AXIS SETTINGS
  922. self.logger.debug('== X-AXIS SETTINGS ==')
  923. x_axis_settings_info = {}
  924. for key, fmt in XAxisSettingsDescription:
  925. x_axis_settings_info[key] = f.read_f(fmt)
  926. self.info['x_axis_settings_info'] = x_axis_settings_info
  927. self.logger.debug(x_axis_settings_info)
  928. self.logger.debug('')
  929. ##############################################
  930. # UNKNOWN
  931. self.logger.debug('>> UNKNOWN 3 <<')
  932. # 108 bytes of undeciphered data (types here are guesses)
  933. unknowns = f.read_f('8l 3d 13l')
  934. self.logger.debug(unknowns)
  935. self.logger.debug('')
  936. ##############################################
  937. # EVENTS / TAGS
  938. self.logger.debug('=== EVENTS / TAGS ===')
  939. n_events, n_events_again = f.read_f('ll')
  940. self.info['n_events'] = n_events
  941. self.logger.debug('n_events: {}'.format(n_events))
  942. # event / tag timing is stored as an index into time
  943. raw_event_timestamps = []
  944. event_labels = []
  945. for i in range(n_events_again):
  946. event_index = f.read_f('l')
  947. raw_event_timestamps.append(event_index)
  948. n_events_yet_again = f.read_f('l')
  949. for i in range(n_events_yet_again):
  950. title = f.read_f('S')
  951. event_labels.append(title)
  952. event_list = []
  953. for event_label, event_index in \
  954. zip(event_labels, raw_event_timestamps):
  955. # t_start shouldn't be added here
  956. event_time = event_index * sampling_period
  957. event_list.append({
  958. 'title': event_label,
  959. 'index': event_index,
  960. 'time': event_time})
  961. self.info['event_list'] = event_list
  962. for event in event_list:
  963. self.logger.debug(event)
  964. self.logger.debug('')
  965. ##############################################
  966. # UNKNOWN
  967. self.logger.debug('>> UNKNOWN 4 <<')
  968. # 28 bytes of undeciphered data (types here are guesses)
  969. unknowns = f.read_f('7l')
  970. self.logger.debug(unknowns)
  971. self.logger.debug('')
  972. ##############################################
  973. # EPOCHS / INTERVAL BARS
  974. self.logger.debug('=== EPOCHS / INTERVAL BARS ===')
  975. n_epochs = f.read_f('l')
  976. self.info['n_epochs'] = n_epochs
  977. self.logger.debug('n_epochs: {}'.format(n_epochs))
  978. epoch_list = []
  979. for i in range(n_epochs):
  980. epoch_info = {}
  981. for key, fmt in EpochInfoDescription:
  982. epoch_info[key] = f.read_f(fmt)
  983. epoch_list.append(epoch_info)
  984. self.info['epoch_list'] = epoch_list
  985. # epoch / interval bar timing and duration are stored in
  986. # seconds, so here they are converted to (possibly non-integer)
  987. # indexes into time to fit into the procrustean beds of
  988. # _rescale_event_timestamp and _rescale_epoch_duration
  989. raw_epoch_timestamps = []
  990. raw_epoch_durations = []
  991. epoch_labels = []
  992. for epoch in epoch_list:
  993. raw_epoch_timestamps.append(
  994. epoch['t_start'] / sampling_period)
  995. raw_epoch_durations.append(
  996. (epoch['t_stop'] - epoch['t_start']) / sampling_period)
  997. epoch_labels.append(epoch['title'])
  998. self.logger.debug(epoch)
  999. self.logger.debug('')
  1000. ##############################################
  1001. # UNKNOWN
  1002. self.logger.debug(
  1003. '>> UNKNOWN 5 (includes y-axis plot ranges) <<')
  1004. # lots of undeciphered data
  1005. rest_of_the_file = f.read()
  1006. self.logger.debug(rest_of_the_file)
  1007. self.logger.debug('')
  1008. self.logger.debug('End of file reached (expected)')
  1009. except EOFError as e:
  1010. if format_ver == 1 or format_ver == 2:
  1011. # for format versions 1 and 2, metadata like graph display
  1012. # information was stored separately in the "resource fork"
  1013. # of the file, so reaching the end of the file before all
  1014. # metadata is parsed is expected
  1015. self.logger.debug('End of file reached (expected)')
  1016. pass
  1017. else:
  1018. # for format versions 3 and later, there should be metadata
  1019. # stored at the end of the file, so warn that something may
  1020. # have gone wrong, but try to continue anyway
  1021. self.logger.warning('End of file reached unexpectedly '
  1022. 'while parsing metadata, will attempt '
  1023. 'to continue')
  1024. self.logger.debug(e, exc_info=True)
  1025. pass
  1026. except UnicodeDecodeError as e:
  1027. # warn that something went wrong with reading a string, but try
  1028. # to continue anyway
  1029. self.logger.warning('Problem decoding text while parsing '
  1030. 'metadata, will ignore any remaining '
  1031. 'metadata and attempt to continue')
  1032. self.logger.debug(e, exc_info=True)
  1033. pass
  1034. self.logger.debug('')
  1035. ##############################################
  1036. # RAWIO HEADER
  1037. # event_channels will be cast to _event_channel_dtype
  1038. event_channels = []
  1039. event_channels.append(('AxoGraph Tags', '', 'event'))
  1040. event_channels.append(('AxoGraph Intervals', '', 'epoch'))
  1041. # organize header
  1042. self.header['nb_block'] = 1
  1043. self.header['nb_segment'] = [1]
  1044. self.header['signal_channels'] = \
  1045. np.array(sig_channels, dtype=_signal_channel_dtype)
  1046. self.header['event_channels'] = \
  1047. np.array(event_channels, dtype=_event_channel_dtype)
  1048. self.header['unit_channels'] = \
  1049. np.array([], dtype=_unit_channel_dtype)
  1050. ##############################################
  1051. # DATA OBJECTS
  1052. # organize data
  1053. self._sampling_period = sampling_period
  1054. self._t_start = t_start
  1055. self._raw_signals = [sig_memmaps] # first index is seg_index
  1056. self._raw_event_epoch_timestamps = [
  1057. np.array(raw_event_timestamps),
  1058. np.array(raw_epoch_timestamps)]
  1059. self._raw_event_epoch_durations = [
  1060. None,
  1061. np.array(raw_epoch_durations)]
  1062. self._event_epoch_labels = [
  1063. np.array(event_labels, dtype='U'),
  1064. np.array(epoch_labels, dtype='U')]
  1065. class StructFile(BufferedReader):
  1066. """
  1067. A container for the file buffer with some added convenience functions for
  1068. reading AxoGraph files
  1069. """
  1070. def __init__(self, *args, **kwargs):
  1071. # As far as I've seen, every AxoGraph file uses big-endian encoding,
  1072. # regardless of the system architecture on which it was created, but
  1073. # here I provide means for controlling byte ordering in case a counter
  1074. # example is found.
  1075. self.byte_order = kwargs.pop('byte_order', '>')
  1076. if self.byte_order == '>':
  1077. # big-endian
  1078. self.utf_16_decoder = 'utf-16-be'
  1079. elif self.byte_order == '<':
  1080. # little-endian
  1081. self.utf_16_decoder = 'utf-16-le'
  1082. else:
  1083. # unspecified
  1084. self.utf_16_decoder = 'utf-16'
  1085. super().__init__(*args, **kwargs)
  1086. def read_and_unpack(self, fmt):
  1087. """
  1088. Calculate the number of bytes corresponding to the format string, read
  1089. in that number of bytes, and unpack them according to the format string
  1090. """
  1091. try:
  1092. return unpack(
  1093. self.byte_order + fmt,
  1094. self.read(calcsize(self.byte_order + fmt)))
  1095. except Exception as e:
  1096. if e.args[0].startswith('unpack requires a buffer of'):
  1097. raise EOFError(e)
  1098. else:
  1099. raise
  1100. def read_string(self):
  1101. """
  1102. The most common string format in AxoGraph files is a variable length
  1103. string with UTF-16 encoding, preceded by a 4-byte integer (long)
  1104. specifying the length of the string in bytes. Unlike a Pascal string
  1105. ('p' format), these strings are not stored in a fixed number of bytes
  1106. with padding at the end. This function reads in one of these variable
  1107. length strings
  1108. """
  1109. # length may be -1, 0, or a positive integer
  1110. length = self.read_and_unpack('l')[0]
  1111. if length > 0:
  1112. return self.read(length).decode(self.utf_16_decoder)
  1113. else:
  1114. return ''
  1115. def read_bool(self):
  1116. """
  1117. AxoGraph files encode each boolean as 4-byte integer (long) with value
  1118. 1 = True, 0 = False. This function reads in one of these booleans.
  1119. """
  1120. return bool(self.read_and_unpack('l')[0])
  1121. def read_f(self, fmt, offset=None):
  1122. """
  1123. This function is a wrapper for read_and_unpack that adds compatibility
  1124. with two new format strings:
  1125. 'S': a variable length UTF-16 string, readable with read_string
  1126. 'Z': a boolean encoded as a 4-byte integer, readable with read_bool
  1127. This method does not implement support for numbers before the new
  1128. format strings, such as '3Z' to represent 3 bools (use 'ZZZ' instead).
  1129. """
  1130. if offset is not None:
  1131. self.seek(offset)
  1132. # place commas before and after each instance of S or Z
  1133. for special in ['S', 'Z']:
  1134. fmt = fmt.replace(special, ',' + special + ',')
  1135. # split S and Z into isolated strings
  1136. fmt = fmt.split(',')
  1137. # construct a tuple of unpacked data
  1138. data = ()
  1139. for subfmt in fmt:
  1140. if subfmt == 'S':
  1141. data += (self.read_string(),)
  1142. elif subfmt == 'Z':
  1143. data += (self.read_bool(),)
  1144. else:
  1145. data += self.read_and_unpack(subfmt)
  1146. if len(data) == 1:
  1147. return data[0]
  1148. else:
  1149. return data
  1150. FONT_BOLD = 75 # mysterious arbitrary constant
  1151. FONT_NOT_BOLD = 50 # mysterious arbitrary constant
  1152. FONT_ITALICS = 1
  1153. FONT_UNDERLINE = 2
  1154. FONT_STRIKEOUT = 4
  1155. TraceHeaderDescriptionV1 = [
  1156. # documented in AxoGraph_ReadWrite.h
  1157. ('x_index', 'l'),
  1158. ('y_index', 'l'),
  1159. ('err_bar_index', 'l'),
  1160. ('group_id_for_this_trace', 'l'),
  1161. ('hidden', 'Z'), # AxoGraph_ReadWrite.h incorrectly states "shown" instead
  1162. ('min_x', 'd'),
  1163. ('max_x', 'd'),
  1164. ('min_positive_x', 'd'),
  1165. ('x_is_regularly_spaced', 'Z'),
  1166. ('x_increases_monotonically', 'Z'),
  1167. ('x_interval_if_regularly_spaced', 'd'),
  1168. ('min_y', 'd'),
  1169. ('max_y', 'd'),
  1170. ('min_positive_y', 'd'),
  1171. ('trace_color', 'xBBB'),
  1172. ('display_joined_line_plot', 'Z'),
  1173. ('line_thickness', 'd'),
  1174. ('pen_style', 'l'),
  1175. ('display_symbol_plot', 'Z'),
  1176. ('symbol_type', 'l'),
  1177. ('symbol_size', 'l'),
  1178. ('draw_every_data_point', 'Z'),
  1179. ('skip_points_by_distance_instead_of_pixels', 'Z'),
  1180. ('pixels_between_symbols', 'l'),
  1181. ('display_histogram_plot', 'Z'),
  1182. ('histogram_type', 'l'),
  1183. ('histogram_bar_separation', 'l'),
  1184. ('display_error_bars', 'Z'),
  1185. ('display_pos_err_bar', 'Z'),
  1186. ('display_neg_err_bar', 'Z'),
  1187. ('err_bar_width', 'l'),
  1188. ]
  1189. # documented in AxoGraph_ReadWrite.h
  1190. # - only one difference exists between versions 1 and 2
  1191. TraceHeaderDescriptionV2 = list(TraceHeaderDescriptionV1) # make a copy
  1192. TraceHeaderDescriptionV2.insert(3, ('neg_err_bar_index', 'l'))
  1193. GroupHeaderDescriptionV1 = [
  1194. # undocumented and reverse engineered
  1195. ('title', 'S'),
  1196. ('unknown1', 'h'), # 2 bytes of undeciphered data (types are guesses)
  1197. ('units', 'S'),
  1198. ('unknown2', 'hll'), # 10 bytes of undeciphered data (types are guesses)
  1199. ]
  1200. FontSettingsDescription = [
  1201. # undocumented and reverse engineered
  1202. ('font', 'S'),
  1203. ('size', 'h'), # divide this 2-byte integer by 10 to get font size
  1204. ('unknown1', '5b'), # 5 bytes of undeciphered data (types are guesses)
  1205. ('setting1', 'B'), # includes bold setting
  1206. ('setting2', 'B'), # italics, underline, strikeout specified in bitmap
  1207. ]
  1208. XAxisSettingsDescription = [
  1209. # undocumented and reverse engineered
  1210. ('unknown1', '3l2d'), # 28 bytes of undeciphered data (types are guesses)
  1211. ('plotted_x_range', 'dd'),
  1212. ('unknown2', 'd'), # 8 bytes of undeciphered data (types are guesses)
  1213. ('auto_x_ticks', 'Z'),
  1214. ('x_minor_ticks', 'd'),
  1215. ('x_major_ticks', 'd'),
  1216. ('x_axis_title', 'S'),
  1217. ('unknown3', 'h'), # 2 bytes of undeciphered data (types are guesses)
  1218. ('units', 'S'),
  1219. ('unknown4', 'h'), # 2 bytes of undeciphered data (types are guesses)
  1220. ]
  1221. EpochInfoDescription = [
  1222. # undocumented and reverse engineered
  1223. ('title', 'S'),
  1224. ('t_start', 'd'),
  1225. ('t_stop', 'd'),
  1226. ('y_pos', 'd'),
  1227. ]