nsdfio.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. """
  2. Module for reading and writing NSDF files
  3. Author: Mieszko Grodzicki
  4. This module support both reading and writing NDSF files.
  5. Note: Read file must be written using this IO
  6. """
  7. import numpy as np
  8. import quantities as pq
  9. from uuid import uuid1
  10. import pickle
  11. from datetime import datetime
  12. import os
  13. try:
  14. import nsdf
  15. except ImportError as err:
  16. HAVE_NSDF = False
  17. NSDF_ERR = err
  18. else:
  19. HAVE_NSDF = True
  20. NSDF_ERR = None
  21. from neo.io.baseio import BaseIO
  22. from neo.core import Block, Segment, AnalogSignal, ChannelIndex
  23. class NSDFIO(BaseIO):
  24. """
  25. Class for reading and writing files in NSDF Format.
  26. It supports reading and writing: Block, Segment, AnalogSignal, ChannelIndex, with all relationships and metadata.
  27. """
  28. is_readable = True
  29. is_writable = False # True - disabled pending update to Neo 0.9 API
  30. supported_objects = [Block, Segment, AnalogSignal, ChannelIndex]
  31. readable_objects = [Block, Segment]
  32. writeable_objects = [] # [Block, Segment] - disabled pending update to Neo 0.9 API
  33. has_header = False
  34. is_streameable = False
  35. name = 'NSDF'
  36. extensions = ['h5']
  37. mode = 'file'
  38. def __init__(self, filename=None):
  39. """
  40. Initialise NSDFIO instance
  41. :param filename: Path to the file
  42. """
  43. if not HAVE_NSDF:
  44. raise Exception("Failed to import NSDF.")
  45. if filename is None:
  46. raise ValueError("Must provide an input file.")
  47. BaseIO.__init__(self)
  48. self.filename = filename
  49. self.dt_format = '%d/%m/%Y %H:%M:%S'
  50. self.modeltree_path = '/model/modeltree/neo/'
  51. def write_all_blocks(self, blocks):
  52. """
  53. Write list of blocks to the file
  54. :param blocks: List of blocks to be written
  55. """
  56. raise NotImplementedError("Implementation not yet updated for Neo 0.9")
  57. writer = self._init_writing()
  58. neo_model, blocks_model, segments_model = self._prepare_model_tree(writer)
  59. name_pattern = self._name_pattern(len(blocks))
  60. for i, block in enumerate(blocks):
  61. self.write_block(block, name_pattern.format(i), writer, blocks_model)
  62. def write_block(self, block=None, name='0', writer=None, parent=None):
  63. """
  64. Write a Block to the file
  65. :param block: Block to be written
  66. :param name: Name for block representation in NSDF model tree (optional)
  67. :param writer: NSDFWriter instance (optional)
  68. :param parent: NSDF ModelComponent which will be the parent of block NSDF representation (optional)
  69. """
  70. raise NotImplementedError("Implementation not yet updated for Neo 0.9")
  71. if not isinstance(block, Block):
  72. raise ValueError("Must provide a Block to write.")
  73. if writer is None:
  74. writer = self._init_writing()
  75. if parent is None:
  76. neo_model, parent, segments_model = self._prepare_model_tree(writer)
  77. block_model = nsdf.ModelComponent(name, uid=uuid1().hex, parent=parent)
  78. self._write_container_metadata(block, block_model)
  79. self._write_model_component(block_model, writer)
  80. self._write_block_children(block, block_model, writer)
  81. self._clean_nsdfio_annotations(block)
  82. def _write_block_children(self, block, block_model, writer):
  83. segments_model = nsdf.ModelComponent(name='segments', uid=uuid1().hex, parent=block_model)
  84. self._write_model_component(segments_model, writer)
  85. name_pattern = self._name_pattern(len(block.segments))
  86. for i, segment in enumerate(block.segments):
  87. self.write_segment(segment=segment, name=name_pattern.format(i),
  88. writer=writer, parent=segments_model)
  89. channel_indexes_model = nsdf.ModelComponent(
  90. name='channel_indexes', uid=uuid1().hex, parent=block_model)
  91. self._write_model_component(channel_indexes_model, writer)
  92. name_pattern = self._name_pattern(len(block.channel_indexes))
  93. for i, channelindex in enumerate(block.channel_indexes):
  94. self.write_channelindex(channelindex=channelindex, name=name_pattern.format(i),
  95. writer=writer, parent=channel_indexes_model)
  96. def write_segment(self, segment=None, name='0', writer=None, parent=None):
  97. """
  98. Write a Segment to the file
  99. :param segment: Segment to be written
  100. :param name: Name for segment representation in NSDF model tree (optional)
  101. :param writer: NSDFWriter instance (optional)
  102. :param parent: NSDF ModelComponent which will be the parent of segment NSDF representation (optional)
  103. """
  104. raise NotImplementedError("Implementation not yet updated for Neo 0.9")
  105. if not isinstance(segment, Segment):
  106. raise ValueError("Must provide a Segment to write.")
  107. if writer is None:
  108. writer = self._init_writing()
  109. single_segment = False
  110. if parent is None:
  111. neo_model, blocks_model, parent = self._prepare_model_tree(writer)
  112. single_segment = True
  113. model = nsdf.ModelComponent(name, uid=uuid1().hex, parent=parent)
  114. self._write_container_metadata(segment, model)
  115. self._write_model_component(model, writer)
  116. self._write_segment_children(model, segment, writer)
  117. if single_segment:
  118. self._clean_nsdfio_annotations(segment)
  119. def _write_segment_children(self, model, segment, writer):
  120. analogsignals_model = nsdf.ModelComponent(
  121. name='analogsignals', uid=uuid1().hex, parent=model)
  122. self._write_model_component(analogsignals_model, writer)
  123. name_pattern = self._name_pattern(len(segment.analogsignals))
  124. for i, signal in enumerate(segment.analogsignals):
  125. self.write_analogsignal(signal=signal, name=name_pattern.format(i),
  126. parent=analogsignals_model, writer=writer)
  127. def write_analogsignal(self, signal, name, writer, parent):
  128. """
  129. Write an AnalogSignal to the file
  130. :param signal: AnalogSignal to be written
  131. :param name: Name for signal representation in NSDF model tree
  132. :param writer: NSDFWriter instance
  133. :param parent: NSDF ModelComponent which will be the parent of signal NSDF representation
  134. """
  135. raise NotImplementedError("Implementation not yet updated for Neo 0.9")
  136. uid = uuid1().hex
  137. model = nsdf.ModelComponent(name, uid=uid, parent=parent)
  138. if signal.annotations.get('nsdfio_uid') is not None:
  139. model.attrs['reference_to'] = signal.annotations['nsdfio_uid']
  140. self._write_model_component(model, writer)
  141. return
  142. self._write_basic_metadata(model, signal)
  143. signal.annotations['nsdfio_uid'] = uid
  144. r_signal = np.swapaxes(signal, 0, 1)
  145. channels_model, channels, source_ds = self._create_signal_data_sources(
  146. model, r_signal, uid, writer)
  147. self._write_signal_data(model, channels, r_signal, signal, source_ds, writer)
  148. self._write_model_component(model, writer)
  149. self._write_model_component(channels_model, writer)
  150. for channel_model in channels:
  151. self._write_model_component(channel_model, writer)
  152. def write_channelindex(self, channelindex, name, writer, parent):
  153. """
  154. Write a ChannelIndex to the file
  155. :param channelindex: ChannelIndex to be written
  156. :param name: Name for channelindex representation in NSDF model tree
  157. :param writer: NSDFWriter instance
  158. :param parent: NSDF ModelComponent which will be the parent of channelindex NSDF representation
  159. """
  160. raise NotImplementedError("Implementation not yet updated for Neo 0.9")
  161. uid = uuid1().hex
  162. model = nsdf.ModelComponent(name, uid=uid, parent=parent)
  163. self._write_basic_metadata(model, channelindex)
  164. self._write_model_component(model, writer)
  165. self._write_channelindex_arrays(model, channelindex, writer)
  166. self._write_channelindex_children(channelindex, model, writer)
  167. def _write_channelindex_children(self, channelindex, model, writer):
  168. analogsignals_model = nsdf.ModelComponent(
  169. name='analogsignals', uid=uuid1().hex, parent=model)
  170. self._write_model_component(analogsignals_model, writer)
  171. name_pattern = self._name_pattern(len(channelindex.analogsignals))
  172. for i, signal in enumerate(channelindex.analogsignals):
  173. self.write_analogsignal(signal=signal, name=name_pattern.format(i),
  174. parent=analogsignals_model, writer=writer)
  175. def _init_writing(self):
  176. return nsdf.NSDFWriter(self.filename, mode='w')
  177. def _prepare_model_tree(self, writer):
  178. neo_model = nsdf.ModelComponent('neo', uid=uuid1().hex)
  179. self._write_model_component(neo_model, writer)
  180. blocks_model = nsdf.ModelComponent('blocks', uid=uuid1().hex, parent=neo_model)
  181. self._write_model_component(blocks_model, writer)
  182. segments_model = nsdf.ModelComponent('segments', uid=uuid1().hex, parent=neo_model)
  183. self._write_model_component(segments_model, writer)
  184. return neo_model, blocks_model, segments_model
  185. def _number_of_digits(self, n):
  186. return len(str(n))
  187. def _name_pattern(self, how_many_items):
  188. return '{{:0{}d}}'.format(self._number_of_digits(max(how_many_items - 1, 0)))
  189. def _clean_nsdfio_annotations(self, object):
  190. nsdfio_annotations = ('nsdfio_uid',)
  191. for key in nsdfio_annotations:
  192. object.annotations.pop(key, None)
  193. if hasattr(object, 'children'):
  194. for child in object.children:
  195. self._clean_nsdfio_annotations(child)
  196. def _write_model_component(self, model, writer):
  197. if model.parent is None:
  198. nsdf.add_model_component(model, writer.model['modeltree/'])
  199. else:
  200. nsdf.add_model_component(model, model.parent.hdfgroup)
  201. def _write_container_metadata(self, container, container_model):
  202. self._write_basic_metadata(container_model, container)
  203. self._write_datetime_attributes(container_model, container)
  204. self._write_index_attribute(container_model, container)
  205. def _write_basic_metadata(self, model, object):
  206. self._write_basic_attributes(model, object)
  207. self._write_annotations(model, object)
  208. def _write_basic_attributes(self, model, object):
  209. if object.name is not None:
  210. model.attrs['name'] = object.name
  211. if object.description is not None:
  212. model.attrs['description'] = object.description
  213. def _write_datetime_attributes(self, model, object):
  214. if object.rec_datetime is not None:
  215. model.attrs['rec_datetime'] = object.rec_datetime.strftime(self.dt_format)
  216. def _write_index_attribute(self, model, object):
  217. if object.index is not None:
  218. model.attrs['index'] = object.index
  219. def _write_annotations(self, model, object):
  220. if object.annotations is not None:
  221. model.attrs['annotations'] = pickle.dumps(object.annotations)
  222. def _write_signal_data(self, model, channels, r_signal, signal, source_ds, writer):
  223. dataobj = nsdf.UniformData('signal', unit=str(signal.units.dimensionality))
  224. dataobj.dtype = signal.dtype
  225. for i in range(len(channels)):
  226. dataobj.put_data(channels[i].uid, r_signal[i])
  227. dataobj.set_dt(float(signal.sampling_period.magnitude),
  228. str(signal.sampling_period.dimensionality))
  229. rescaled_tstart = signal.t_start.rescale(signal.sampling_period.dimensionality)
  230. writer.add_uniform_data(source_ds, dataobj,
  231. tstart=float(rescaled_tstart.magnitude))
  232. model.attrs['t_start_unit'] = str(signal.t_start.dimensionality)
  233. def _create_signal_data_sources(self, model, r_signal, uid, writer):
  234. channels = []
  235. channels_model = nsdf.ModelComponent(name='channels', uid=uuid1().hex, parent=model)
  236. name_pattern = '{{:0{}d}}'.format(self._number_of_digits(max(len(r_signal) - 1, 0)))
  237. for i in range(len(r_signal)):
  238. channels.append(nsdf.ModelComponent(name_pattern.format(i),
  239. uid=uuid1().hex,
  240. parent=channels_model))
  241. source_ds = writer.add_uniform_ds(uid, [channel.uid for channel in channels])
  242. return channels_model, channels, source_ds
  243. def _write_channelindex_arrays(self, model, channelindex, writer):
  244. group = model.hdfgroup
  245. self._write_array(group, 'index', channelindex.index)
  246. if channelindex.channel_names is not None:
  247. self._write_array(group, 'channel_names', channelindex.channel_names)
  248. if channelindex.channel_ids is not None:
  249. self._write_array(group, 'channel_ids', channelindex.channel_ids)
  250. if channelindex.coordinates is not None:
  251. self._write_array(group, 'coordinates', channelindex.coordinates)
  252. def _write_array(self, group, name, array):
  253. if isinstance(array, pq.Quantity):
  254. group.create_dataset(name, data=array.magnitude)
  255. group[name].attrs['dimensionality'] = str(array.dimensionality)
  256. else:
  257. group.create_dataset(name, data=array)
  258. def read_all_blocks(self, lazy=False):
  259. """
  260. Read all blocks from the file
  261. :param lazy: Enables lazy reading
  262. :return: List of read blocks
  263. """
  264. assert not lazy, 'Do not support lazy'
  265. reader = self._init_reading()
  266. blocks = []
  267. blocks_path = self.modeltree_path + 'blocks/'
  268. for block in reader.model[blocks_path].values():
  269. blocks.append(self.read_block(group=block, reader=reader))
  270. return blocks
  271. def read_block(self, lazy=False, group=None, reader=None):
  272. """
  273. Read a Block from the file
  274. :param lazy: Enables lazy reading
  275. :param group: HDF5 Group representing the block in NSDF model tree (optional)
  276. :param reader: NSDFReader instance (optional)
  277. :return: Read block
  278. """
  279. assert not lazy, 'Do not support lazy'
  280. block = Block()
  281. group, reader = self._select_first_container(group, reader, 'block')
  282. if group is None:
  283. return None
  284. attrs = group.attrs
  285. self._read_block_children(block, group, reader)
  286. block.create_many_to_one_relationship()
  287. self._read_container_metadata(attrs, block)
  288. return block
  289. def _read_block_children(self, block, group, reader):
  290. for child in group['segments/'].values():
  291. block.segments.append(self.read_segment(group=child, reader=reader))
  292. for child in group['channel_indexes/'].values():
  293. block.channel_indexes.append(self.read_channelindex(group=child, reader=reader))
  294. def read_segment(self, lazy=False, group=None, reader=None):
  295. """
  296. Read a Segment from the file
  297. :param lazy: Enables lazy reading
  298. :param group: HDF5 Group representing the segment in NSDF model tree (optional)
  299. :param reader: NSDFReader instance (optional)
  300. :return: Read segment
  301. """
  302. assert not lazy, 'Do not support lazy'
  303. segment = Segment()
  304. group, reader = self._select_first_container(group, reader, 'segment')
  305. if group is None:
  306. return None
  307. attrs = group.attrs
  308. self._read_segment_children(group, reader, segment)
  309. self._read_container_metadata(attrs, segment)
  310. return segment
  311. def _read_segment_children(self, group, reader, segment):
  312. for child in group['analogsignals/'].values():
  313. segment.analogsignals.append(self.read_analogsignal(group=child, reader=reader))
  314. def read_analogsignal(self, lazy=False, group=None, reader=None):
  315. """
  316. Read an AnalogSignal from the file (must be child of a Segment)
  317. :param lazy: Enables lazy reading
  318. :param group: HDF5 Group representing the analogsignal in NSDF model tree
  319. :param reader: NSDFReader instance
  320. :return: Read AnalogSignal
  321. """
  322. assert not lazy, 'Do not support lazy'
  323. attrs = group.attrs
  324. if attrs.get('reference_to') is not None:
  325. return self.objects_dict[attrs['reference_to']]
  326. uid = attrs['uid']
  327. data_group = reader.data['uniform/{}/signal'.format(uid)]
  328. t_start = self._read_analogsignal_t_start(attrs, data_group)
  329. signal = self._create_analogsignal(data_group, group, t_start, uid, reader)
  330. self._read_basic_metadata(attrs, signal)
  331. self.objects_dict[uid] = signal
  332. return signal
  333. def read_channelindex(self, lazy=False, group=None, reader=None):
  334. """
  335. Read a ChannelIndex from the file (must be child of a Block)
  336. :param lazy: Enables lazy reading
  337. :param group: HDF5 Group representing the channelindex in NSDF model tree
  338. :param reader: NSDFReader instance
  339. :return: Read ChannelIndex
  340. """
  341. assert not lazy, 'Do not support lazy'
  342. attrs = group.attrs
  343. channelindex = self._create_channelindex(group)
  344. self._read_channelindex_children(group, reader, channelindex)
  345. self._read_basic_metadata(attrs, channelindex)
  346. return channelindex
  347. def _read_channelindex_children(self, group, reader, channelindex):
  348. for child in group['analogsignals/'].values():
  349. channelindex.analogsignals.append(self.read_analogsignal(group=child, reader=reader))
  350. def _init_reading(self):
  351. reader = nsdf.NSDFReader(self.filename)
  352. self.file_datetime = datetime.fromtimestamp(os.stat(self.filename).st_mtime)
  353. self.objects_dict = {}
  354. return reader
  355. def _select_first_container(self, group, reader, name):
  356. if reader is None:
  357. reader = self._init_reading()
  358. if group is None:
  359. path = self.modeltree_path + name + 's/'
  360. if len(reader.model[path].values()) > 0:
  361. group = reader.model[path].values()[0]
  362. return group, reader
  363. def _read_container_metadata(self, attrs, container):
  364. self._read_basic_metadata(attrs, container)
  365. self._read_datetime_attributes(attrs, container)
  366. self._read_index_attribute(attrs, container)
  367. def _read_basic_metadata(self, attrs, signal):
  368. self._read_basic_attributes(attrs, signal)
  369. self._read_annotations(attrs, signal)
  370. def _read_basic_attributes(self, attrs, object):
  371. if attrs.get('name') is not None:
  372. object.name = attrs['name']
  373. if attrs.get('description') is not None:
  374. object.description = attrs['description']
  375. object.file_origin = self.filename
  376. def _read_datetime_attributes(self, attrs, object):
  377. object.file_datetime = self.file_datetime
  378. if attrs.get('rec_datetime') is not None:
  379. object.rec_datetime = datetime.strptime(attrs['rec_datetime'], self.dt_format)
  380. def _read_annotations(self, attrs, object):
  381. if attrs.get('annotations') is not None:
  382. object.annotations = pickle.loads(attrs['annotations'])
  383. def _read_index_attribute(self, attrs, object):
  384. if attrs.get('index') is not None:
  385. object.index = attrs['index']
  386. def _create_analogsignal(self, data_group, group, t_start, uid, reader):
  387. # for lazy
  388. # data_shape = data_group.shape
  389. # data_shape = (data_shape[1], data_shape[0])
  390. dataobj = reader.get_uniform_data(uid, 'signal')
  391. data = self._read_signal_data(dataobj, group)
  392. signal = self._create_normal_analogsignal(data, dataobj, uid, t_start)
  393. return signal
  394. def _read_analogsignal_t_start(self, attrs, data_group):
  395. t_start = float(data_group.attrs['tstart']) * pq.Quantity(1, data_group.attrs['tunit'])
  396. t_start = t_start.rescale(attrs['t_start_unit'])
  397. return t_start
  398. def _read_signal_data(self, dataobj, group):
  399. data = []
  400. for channel in group['channels/'].values():
  401. channel_uid = channel.attrs['uid']
  402. data += [dataobj.get_data(channel_uid)]
  403. return data
  404. def _create_normal_analogsignal(self, data, dataobj, uid, t_start):
  405. return AnalogSignal(np.swapaxes(data, 0, 1), dtype=dataobj.dtype, units=dataobj.unit,
  406. t_start=t_start, sampling_period=pq.Quantity(dataobj.dt, dataobj.tunit))
  407. def _create_channelindex(self, group):
  408. return ChannelIndex(index=self._read_array(group, 'index'),
  409. channel_names=self._read_array(group, 'channel_names'),
  410. channel_ids=self._read_array(group, 'channel_ids'),
  411. coordinates=self._read_array(group, 'coordinates'))
  412. def _read_array(self, group, name):
  413. if group.__contains__(name) == False:
  414. return None
  415. array = group[name][:]
  416. if group[name].attrs.get('dimensionality') is not None:
  417. return pq.Quantity(array, group[name].attrs['dimensionality'])
  418. return array