API cleanup for binarywave (loadibw->load, move checksum to util, ...).
[igor.git] / igor / binarywave.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 "Read IGOR Binary Wave files into Numpy arrays."
20
21 # Based on WaveMetric's Technical Note 003, "Igor Binary Format"
22 #   ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN003.zip
23 # From ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN000.txt
24 #   We place no restrictions on copying Technical Notes, with the
25 #   exception that you cannot resell them. So read, enjoy, and
26 #   share. We hope IGOR Technical Notes will provide you with lots of
27 #   valuable information while you are developing IGOR applications.
28
29 import array as _array
30 import sys as _sys
31 import types as _types
32
33 import numpy as _numpy
34
35 from .struct import Structure as _Structure
36 from .struct import Field as _Field
37 from .util import assert_null as _assert_null
38 from .util import byte_order as _byte_order
39 from .util import checksum as _checksum
40
41
42 # Numpy doesn't support complex integers by default, see
43 #   http://mail.python.org/pipermail/python-dev/2002-April/022408.html
44 #   http://mail.scipy.org/pipermail/numpy-discussion/2007-October/029447.html
45 # So we roll our own types.  See
46 #   http://docs.scipy.org/doc/numpy/user/basics.rec.html
47 #   http://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.html
48 complexInt8 = _numpy.dtype([('real', _numpy.int8), ('imag', _numpy.int8)])
49 complexInt16 = _numpy.dtype([('real', _numpy.int16), ('imag', _numpy.int16)])
50 complexInt32 = _numpy.dtype([('real', _numpy.int32), ('imag', _numpy.int32)])
51 complexUInt8 = _numpy.dtype([('real', _numpy.uint8), ('imag', _numpy.uint8)])
52 complexUInt16 = _numpy.dtype(
53     [('real', _numpy.uint16), ('imag', _numpy.uint16)])
54 complexUInt32 = _numpy.dtype(
55     [('real', _numpy.uint32), ('imag', _numpy.uint32)])
56
57 # Begin IGOR constants and typedefs from IgorBin.h
58
59 # From IgorMath.h
60 TYPE_TABLE = {        # (key: integer flag, value: numpy dtype)
61     0:None,           # Text wave, not handled in ReadWave.c
62     1:_numpy.complex, # NT_CMPLX, makes number complex.
63     2:_numpy.float32, # NT_FP32, 32 bit fp numbers.
64     3:_numpy.complex64,
65     4:_numpy.float64, # NT_FP64, 64 bit fp numbers.
66     5:_numpy.complex128,
67     8:_numpy.int8,    # NT_I8, 8 bit signed integer. Requires Igor Pro
68                       # 2.0 or later.
69     9:complexInt8,
70     0x10:_numpy.int16,# NT_I16, 16 bit integer numbers. Requires Igor
71                       # Pro 2.0 or later.
72     0x11:complexInt16,
73     0x20:_numpy.int32,# NT_I32, 32 bit integer numbers. Requires Igor
74                       # Pro 2.0 or later.
75     0x21:complexInt32,
76 #   0x40:None,        # NT_UNSIGNED, Makes above signed integers
77 #                     # unsigned. Requires Igor Pro 3.0 or later.
78     0x48:_numpy.uint8,
79     0x49:complexUInt8,
80     0x50:_numpy.uint16,
81     0x51:complexUInt16,
82     0x60:_numpy.uint32,
83     0x61:complexUInt32,
84 }
85
86 # From wave.h
87 MAXDIMS = 4
88
89 # From binary.h
90 BinHeaderCommon = _Structure(  # WTK: this one is mine.
91     name='BinHeaderCommon',
92     fields=[
93         _Field('h', 'version', help='Version number for backwards compatibility.'),
94         ])
95
96 BinHeader1 = _Structure(
97     name='BinHeader1',
98     fields=[
99         _Field('h', 'version', help='Version number for backwards compatibility.'),
100         _Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
101         _Field('h', 'checksum', help='Checksum over this header and the wave header.'),
102         ])
103
104 BinHeader2 = _Structure(
105     name='BinHeader2',
106     fields=[
107         _Field('h', 'version', help='Version number for backwards compatibility.'),
108         _Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
109         _Field('l', 'noteSize', help='The size of the note text.'),
110         _Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
111         _Field('h', 'checksum', help='Checksum over this header and the wave header.'),
112         ])
113
114 BinHeader3 = _Structure(
115     name='BinHeader3',
116     fields=[
117         _Field('h', 'version', help='Version number for backwards compatibility.'),
118         _Field('h', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
119         _Field('l', 'noteSize', help='The size of the note text.'),
120         _Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
121         _Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
122         _Field('h', 'checksum', help='Checksum over this header and the wave header.'),
123         ])
124
125 BinHeader5 = _Structure(
126     name='BinHeader5',
127     fields=[
128         _Field('h', 'version', help='Version number for backwards compatibility.'),
129         _Field('h', 'checksum', help='Checksum over this header and the wave header.'),
130         _Field('l', 'wfmSize', help='The size of the WaveHeader5 data structure plus the wave data.'),
131         _Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
132         _Field('l', 'noteSize', help='The size of the note text.'),
133         _Field('l', 'dataEUnitsSize', help='The size of optional extended data units.'),
134         _Field('l', 'dimEUnitsSize', help='The size of optional extended dimension units.', count=MAXDIMS),
135         _Field('l', 'dimLabelsSize', help='The size of optional dimension labels.', count=MAXDIMS),
136         _Field('l', 'sIndicesSize', help='The size of string indicies if this is a text wave.'),
137         _Field('l', 'optionsSize1', default=0, help='Reserved. Write zero. Ignore on read.'),
138         _Field('l', 'optionsSize2', default=0, help='Reserved. Write zero. Ignore on read.'),
139         ])
140
141
142 # From wave.h
143 MAX_WAVE_NAME2 = 18 # Maximum length of wave name in version 1 and 2
144                     # files. Does not include the trailing null.
145 MAX_WAVE_NAME5 = 31 # Maximum length of wave name in version 5
146                     # files. Does not include the trailing null.
147 MAX_UNIT_CHARS = 3
148
149 # Header to an array of waveform data.
150
151 WaveHeader2 = _Structure(
152     name='WaveHeader2',
153     fields=[
154         _Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
155         _Field('P', 'next', default=0, help='Used in memory only. Write zero. Ignore on read.'),
156         _Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME2+2),
157         _Field('h', 'whVersion', default=0, help='Write 0. Ignore on read.'),
158         _Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
159         _Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
160         _Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
161         _Field('c', 'xUnits', default=0, help='Natural x-axis units go here - null if none.', count=MAX_UNIT_CHARS+1),
162         _Field('l', 'npnts', help='Number of data points in wave.'),
163         _Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
164         _Field('d', 'hsA', help='X value for point p = hsA*p + hsB'),
165         _Field('d', 'hsB', help='X value for point p = hsA*p + hsB'),
166         _Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
167         _Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
168         _Field('h', 'fsValid', help='True if full scale values have meaning.'),
169         _Field('d', 'topFullScale', help='The min full scale value for wave.'), # sic, 'min' should probably be 'max'
170         _Field('d', 'botFullScale', help='The min full scale value for wave.'),
171         _Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
172         _Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
173         _Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
174         _Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
175         _Field('L', 'creationDate', help='DateTime of creation.  Not used in version 1 files.'),
176         _Field('c', 'wUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=2),
177         _Field('L', 'modDate', help='DateTime of last modification.'),
178         _Field('P', 'waveNoteH', help='Used in memory only. Write zero. Ignore on read.'),
179         _Field('f', 'wData', help='The start of the array of waveform data.', count=4),
180         ])
181
182 WaveHeader5 = _Structure(
183     name='WaveHeader5',
184     fields=[
185         _Field('P', 'next', help='link to next wave in linked list.'),
186         _Field('L', 'creationDate', help='DateTime of creation.'),
187         _Field('L', 'modDate', help='DateTime of last modification.'),
188         _Field('l', 'npnts', help='Total number of points (multiply dimensions up to first zero).'),
189         _Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
190         _Field('h', 'dLock', default=0, help='Reserved. Write zero. Ignore on read.'),
191         _Field('c', 'whpad1', default=0, help='Reserved. Write zero. Ignore on read.', count=6),
192         _Field('h', 'whVersion', default=1, help='Write 1. Ignore on read.'),
193         _Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME5+1),
194         _Field('l', 'whpad2', default=0, help='Reserved. Write zero. Ignore on read.'),
195         _Field('P', 'dFolder', default=0, help='Used in memory only. Write zero. Ignore on read.'),
196         # Dimensioning info. [0] == rows, [1] == cols etc
197         _Field('l', 'nDim', help='Number of of items in a dimension -- 0 means no data.', count=MAXDIMS),
198         _Field('d', 'sfA', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
199         _Field('d', 'sfB', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
200         # SI units
201         _Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
202         _Field('c', 'dimUnits', default=0, help='Natural dimension units go here - null if none.', count=(MAXDIMS, MAX_UNIT_CHARS+1)),
203         _Field('h', 'fsValid', help='TRUE if full scale values have meaning.'),
204         _Field('h', 'whpad3', default=0, help='Reserved. Write zero. Ignore on read.'),
205         _Field('d', 'topFullScale', help='The max and max full scale value for wave'), # sic, probably "max and min"
206         _Field('d', 'botFullScale', help='The max and max full scale value for wave.'), # sic, probably "max and min"
207         _Field('P', 'dataEUnits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
208         _Field('P', 'dimEUnits', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
209         _Field('P', 'dimLabels', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
210         _Field('P', 'waveNoteH', default=0, help='Used in memory only. Write zero. Ignore on read.'),
211         _Field('l', 'whUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=16),
212         # The following stuff is considered private to Igor.
213         _Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
214         _Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
215         _Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
216         _Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
217         _Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
218         _Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
219         _Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
220         _Field('h', 'whpad4', default=0, help='Reserved. Write zero. Ignore on read.'),
221         _Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
222         _Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
223         _Field('P', 'sIndices', default=0, help='Used in memory only. Write zero. Ignore on read.'),
224         _Field('f', 'wData', help='The start of the array of data.  Must be 64 bit aligned.', count=1),
225         ])
226
227 # End IGOR constants and typedefs from IgorBin.h
228
229 # Begin functions from ReadWave.c
230
231 def _need_to_reorder_bytes(version):
232     # If the low order byte of the version field of the BinHeader
233     # structure is zero then the file is from a platform that uses
234     # different byte-ordering and therefore all data will need to be
235     # reordered.
236     return version & 0xFF == 0
237
238 def _version_structs(version, byte_order):
239     if version == 1:
240         bin = BinHeader1
241         wave = WaveHeader2
242     elif version == 2:
243         bin = BinHeader2
244         wave = WaveHeader2
245     elif version == 3:
246         bin = BinHeader3
247         wave = WaveHeader2
248     elif version == 5:
249         bin = BinHeader5
250         wave = WaveHeader5
251     else:
252         raise ValueError(
253             ('This does not appear to be a valid Igor binary wave file. '
254              'The version field = {}.\n').format(version))
255     checkSumSize = bin.size + wave.size
256     if version == 5:
257         checkSumSize -= 4  # Version 5 checksum does not include the wData field.
258     bin.set_byte_order(byte_order)
259     wave.set_byte_order(byte_order)
260     return (bin, wave, checkSumSize)
261
262 # Translated from ReadWave()
263 def load(filename, strict=True):
264     if hasattr(filename, 'read'):
265         f = filename  # filename is actually a stream object
266     else:
267         f = open(filename, 'rb')
268     try:
269         BinHeaderCommon.set_byte_order('=')
270         b = buffer(f.read(BinHeaderCommon.size))
271         version = BinHeaderCommon.unpack_dict_from(b)['version']
272         needToReorderBytes = _need_to_reorder_bytes(version)
273         byteOrder = _byte_order(needToReorderBytes)
274         
275         if needToReorderBytes:
276             BinHeaderCommon.set_byte_order(byteOrder)
277             version = BinHeaderCommon.unpack_dict_from(b)['version']
278         bin_struct,wave_struct,checkSumSize = _version_structs(
279             version, byteOrder)
280
281         b = buffer(b + f.read(bin_struct.size + wave_struct.size - BinHeaderCommon.size))
282         c = _checksum(b, byteOrder, 0, checkSumSize)
283         if c != 0:
284             raise ValueError(
285                 ('This does not appear to be a valid Igor binary wave file.  '
286                  'Error in checksum: should be 0, is {}.').format(c))
287         bin_info = bin_struct.unpack_dict_from(b)
288         wave_info = wave_struct.unpack_dict_from(b, offset=bin_struct.size)
289         if version in [1,2,3]:
290             tail = 16  # 16 = size of wData field in WaveHeader2 structure
291             waveDataSize = bin_info['wfmSize'] - wave_struct.size
292             # =  bin_info['wfmSize']-16 - (wave_struct.size - tail)
293         else:
294             assert version == 5, version
295             tail = 4  # 4 = size of wData field in WaveHeader5 structure
296             waveDataSize = bin_info['wfmSize'] - (wave_struct.size - tail)
297         # dtype() wrapping to avoid numpy.generic and
298         # getset_descriptor issues with the builtin numpy types
299         # (e.g. int32).  It has no effect on our local complex
300         # integers.
301         if version == 5:
302             shape = [n for n in wave_info['nDim'] if n > 0] or (0,)
303         else:
304             shape = (wave_info['npnts'],)
305         t = _numpy.dtype(_numpy.int8)  # setup a safe default
306         if wave_info['type'] == 0:  # text wave
307             shape = (waveDataSize,)
308         elif wave_info['type'] in TYPE_TABLE or wave_info['npnts']:
309             t = _numpy.dtype(TYPE_TABLE[wave_info['type']])
310             assert waveDataSize == wave_info['npnts'] * t.itemsize, (
311                 '{}, {}, {}, {}'.format(
312                     waveDataSize, wave_info['npnts'], t.itemsize, t))
313         else:
314             pass  # formula waves
315         if wave_info['npnts'] == 0:
316             data_b = buffer('')
317         else:
318             tail_data = _array.array('f', b[-tail:])
319             data_b = buffer(buffer(tail_data) + f.read(waveDataSize-tail))
320         data = _numpy.ndarray(
321             shape=shape,
322             dtype=t.newbyteorder(byteOrder),
323             buffer=data_b,
324             order='F',
325             )
326
327         if version == 1:
328             pass  # No post-data information
329         elif version == 2:
330             # Post-data info:
331             #   * 16 bytes of padding
332             #   * Optional wave note data
333             pad_b = buffer(f.read(16))  # skip the padding
334             _assert_null(pad_b, strict=strict)
335             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
336         elif version == 3:
337             # Post-data info:
338             #   * 16 bytes of padding
339             #   * Optional wave note data
340             #   * Optional wave dependency formula
341             """Excerpted from TN003:
342
343             A wave has a dependency formula if it has been bound by a
344             statement such as "wave0 := sin(x)". In this example, the
345             dependency formula is "sin(x)". The formula is stored with
346             no trailing null byte.
347             """
348             pad_b = buffer(f.read(16))  # skip the padding
349             _assert_null(pad_b, strict=strict)
350             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
351             bin_info['formula'] = str(f.read(bin_info['formulaSize'])).strip()
352         elif version == 5:
353             # Post-data info:
354             #   * Optional wave dependency formula
355             #   * Optional wave note data
356             #   * Optional extended data units data
357             #   * Optional extended dimension units data
358             #   * Optional dimension label data
359             #   * String indices used for text waves only
360             """Excerpted from TN003:
361
362             dataUnits - Present in versions 1, 2, 3, 5. The dataUnits
363               field stores the units for the data represented by the
364               wave. It is a C string terminated with a null
365               character. This field supports units of 0 to 3 bytes. In
366               version 1, 2 and 3 files, longer units can not be
367               represented. In version 5 files, longer units can be
368               stored using the optional extended data units section of
369               the file.
370
371             xUnits - Present in versions 1, 2, 3. The xUnits field
372               stores the X units for a wave. It is a C string
373               terminated with a null character.  This field supports
374               units of 0 to 3 bytes. In version 1, 2 and 3 files,
375               longer units can not be represented.
376
377             dimUnits - Present in version 5 only. This field is an
378               array of 4 strings, one for each possible wave
379               dimension. Each string supports units of 0 to 3
380               bytes. Longer units can be stored using the optional
381               extended dimension units section of the file.
382             """
383             bin_info['formula'] = str(f.read(bin_info['formulaSize'])).strip()
384             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
385             bin_info['dataEUnits'] = str(f.read(bin_info['dataEUnitsSize'])).strip()
386             bin_info['dimEUnits'] = [
387                 str(f.read(size)).strip() for size in bin_info['dimEUnitsSize']]
388             bin_info['dimLabels'] = []
389             for size in bin_info['dimLabelsSize']:
390                 labels = str(f.read(size)).split(chr(0)) # split null-delimited strings
391                 bin_info['dimLabels'].append([L for L in labels if len(L) > 0])
392             if wave_info['type'] == 0:  # text wave
393                 bin_info['sIndices'] = f.read(bin_info['sIndicesSize'])
394
395         if wave_info['type'] == 0:  # text wave
396             # use sIndices to split data into strings
397             strings = []
398             start = 0
399             for i,string_index in enumerate(bin_info['sIndices']):
400                 offset = ord(string_index)
401                 if offset > start:
402                     string = data[start:offset]
403                     strings.append(''.join(chr(x) for x in string))
404                     start = offset
405                 else:
406                     assert offset == 0, offset
407             data = _numpy.array(strings)
408             shape = [n for n in wave_info['nDim'] if n > 0] or (0,)
409             data.reshape(shape)
410     finally:
411         if not hasattr(filename, 'read'):
412             f.close()
413
414     return data, bin_info, wave_info
415
416
417 def save(filename):
418     raise NotImplementedError