Ran update_copyright.py on igorbinarywave.py
[hooke.git] / hooke / driver / igorbinarywave.py
1 #!/usr/bin/python
2 #
3 # igorbinarywave provides pure Python interface between IGOR Binary
4 # Wave files and Numpy arrays.
5 #
6 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
7 #
8 # This file is part of Hooke.
9 #
10 # Hooke is free software: you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public
12 # License as published by the Free Software Foundation, either
13 # version 3 of the License, or (at your option) any later version.
14 #
15 # Hooke is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with Hooke.  If not, see
22 # <http://www.gnu.org/licenses/>.
23
24 # Based on WaveMetric's Technical Note 003, "Igor Binary Format"
25 #   ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN003.zip
26 # From ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN000.txt
27 #   We place no restrictions on copying Technical Notes, with the
28 #   exception that you cannot resell them. So read, enjoy, and
29 #   share. We hope IGOR Technical Notes will provide you with lots of
30 #   valuable information while you are developing IGOR applications.
31
32 import array
33 import struct
34 import sys
35 import types
36
37 import numpy
38
39
40 __version__ = '0.1'
41
42
43 class Field (object):
44     """Represent a Structure field.
45
46     See Also
47     --------
48     Structure
49     """
50     def __init__(self, format, name, default=None, help=None, count=1):
51         self.format = format # See the struct documentation
52         self.name = name
53         self.default = None
54         self.help = help
55         self.count = count
56         self.total_count = numpy.prod(count)
57
58 class Structure (struct.Struct):
59     """Represent a C structure.
60
61     A convenient wrapper around struct.Struct that uses Fields and
62     adds dict-handling methods for transparent name assignment.
63
64     See Also
65     --------
66     Field
67
68     Examples
69     --------
70
71     Represent the C structure::
72
73         struct thing {
74           short version;
75           long size[3];
76         }
77
78     As
79
80     >>> from pprint import pprint
81     >>> thing = Structure(name='thing',
82     ...     fields=[Field('h', 'version'), Field('l', 'size', count=3)])
83     >>> thing.set_byte_order('>')
84     >>> b = array.array('b', range(2+4*3))
85     >>> d = thing.unpack_dict_from(buffer=b)
86     >>> pprint(d)
87     {'size': array([ 33752069, 101124105, 168496141]), 'version': 1}
88     >>> [hex(x) for x in d['size']]
89     ['0x2030405L', '0x6070809L', '0xa0b0c0dL']
90
91     You can even get fancy with multi-dimensional arrays.
92
93     >>> thing = Structure(name='thing',
94     ...     fields=[Field('h', 'version'), Field('l', 'size', count=(3,2))])
95     >>> thing.set_byte_order('>')
96     >>> b = array.array('b', range(2+4*3*2))
97     >>> d = thing.unpack_dict_from(buffer=b)
98     >>> d['size'].shape
99     (3, 2)
100     >>> pprint(d)
101     {'size': array([[ 33752069, 101124105],
102            [168496141, 235868177],
103            [303240213, 370612249]]),
104      'version': 1}
105     """
106     def __init__(self, name, fields, byte_order='='):
107         # '=' for native byte order, standard size and alignment
108         # See http://docs.python.org/library/struct for details
109         self.name = name
110         self.fields = fields
111         self.set_byte_order(byte_order)
112
113     def __str__(self):
114         return self.name
115
116     def set_byte_order(self, byte_order):
117         """Allow changing the format byte_order on the fly.
118         """
119         if (hasattr(self, 'format') and self.format != None
120             and self.format.startswith(byte_order)):
121             return  # no need to change anything
122         format = []
123         for field in self.fields:
124             format.extend([field.format]*field.total_count)
125         struct.Struct.__init__(self, format=byte_order+''.join(format).replace('P', 'L'))
126
127     def _flatten_args(self, args):
128         # handle Field.count > 0
129         flat_args = []
130         for a,f in zip(args, self.fields):
131             if f.total_count > 1:
132                 flat_args.extend(a)
133             else:
134                 flat_args.append(a)
135         return flat_args
136
137     def _unflatten_args(self, args):
138         # handle Field.count > 0
139         unflat_args = []
140         i = 0
141         for f in self.fields:
142             if f.total_count > 1:
143                 data = numpy.array(args[i:i+f.total_count])
144                 data = data.reshape(f.count)
145                 unflat_args.append(data)
146             else:
147                 unflat_args.append(args[i])
148             i += f.total_count
149         return unflat_args
150         
151     def pack(self, *args):
152         return struct.Struct.pack(self, *self._flatten_args(args))
153
154     def pack_into(self, buffer, offset, *args):
155         return struct.Struct.pack_into(self, buffer, offset,
156                                        *self._flatten_args(args))
157
158     def _clean_dict(self, dict):
159         for f in self.fields:
160             if f.name not in dict:
161                 if f.default != None:
162                     dict[f.name] = f.default
163                 else:
164                     raise ValueError('%s field not set for %s'
165                                      % f.name, self.__class__.__name__)
166         return dict
167
168     def pack_dict(self, dict):
169         dict = self._clean_dict(dict)
170         return self.pack(*[dict[f.name] for f in self.fields])
171
172     def pack_dict_into(self, buffer, offset, dict={}):
173         dict = self._clean_dict(dict)
174         return self.pack_into(buffer, offset,
175                               *[dict[f.name] for f in self.fields])
176
177     def unpack(self, string):
178         return self._unflatten_args(struct.Struct.unpack(self, string))
179
180     def unpack_from(self, buffer, offset=0):
181         return self._unflatten_args(
182             struct.Struct.unpack_from(self, buffer, offset))
183
184     def unpack_dict(self, string):
185         return dict(zip([f.name for f in self.fields],
186                         self.unpack(string)))
187
188     def unpack_dict_from(self, buffer, offset=0):
189         return dict(zip([f.name for f in self.fields],
190                         self.unpack_from(buffer, offset)))
191
192
193 # Numpy doesn't support complex integers by default, see
194 #   http://mail.python.org/pipermail/python-dev/2002-April/022408.html
195 #   http://mail.scipy.org/pipermail/numpy-discussion/2007-October/029447.html
196 # So we roll our own types.  See
197 #   http://docs.scipy.org/doc/numpy/user/basics.rec.html
198 #   http://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.html
199 complexInt8 = numpy.dtype([('real', numpy.int8), ('imag', numpy.int8)])
200 complexInt16 = numpy.dtype([('real', numpy.int16), ('imag', numpy.int16)])
201 complexInt32 = numpy.dtype([('real', numpy.int32), ('imag', numpy.int32)])
202 complexUInt8 = numpy.dtype([('real', numpy.uint8), ('imag', numpy.uint8)])
203 complexUInt16 = numpy.dtype([('real', numpy.uint16), ('imag', numpy.uint16)])
204 complexUInt32 = numpy.dtype([('real', numpy.uint32), ('imag', numpy.uint32)])
205
206
207 # Begin IGOR constants and typedefs from IgorBin.h
208
209 # From IgorMath.h
210 TYPE_TABLE = {       # (key: integer flag, value: numpy dtype)
211     0:None,          # Text wave, not handled in ReadWave.c
212     1:numpy.complex, # NT_CMPLX, makes number complex.
213     2:numpy.float32, # NT_FP32, 32 bit fp numbers.
214     3:numpy.complex64,
215     4:numpy.float64, # NT_FP64, 64 bit fp numbers.
216     5:numpy.complex128,
217     8:numpy.int8,    # NT_I8, 8 bit signed integer. Requires Igor Pro
218                      # 2.0 or later.
219     9:complexInt8,
220     0x10:numpy.int16,# NT_I16, 16 bit integer numbers. Requires Igor
221                      # Pro 2.0 or later.
222     0x11:complexInt16,
223     0x20:numpy.int32,# NT_I32, 32 bit integer numbers. Requires Igor
224                      # Pro 2.0 or later.
225     0x21:complexInt32,
226 #   0x40:None,       # NT_UNSIGNED, Makes above signed integers
227 #                    # unsigned. Requires Igor Pro 3.0 or later.
228     0x48:numpy.uint8,
229     0x49:complexUInt8,
230     0x50:numpy.uint16,
231     0x51:complexUInt16,
232     0x60:numpy.uint32,
233     0x61:complexUInt32,
234 }
235
236 # From wave.h
237 MAXDIMS = 4
238
239 # From binary.h
240 BinHeaderCommon = Structure(  # WTK: this one is mine.
241     name='BinHeaderCommon',
242     fields=[
243         Field('h', 'version', help='Version number for backwards compatibility.'),
244         ])
245
246 BinHeader1 = Structure(
247     name='BinHeader1',
248     fields=[
249         Field('h', 'version', help='Version number for backwards compatibility.'),
250         Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
251         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
252         ])
253
254 BinHeader2 = Structure(
255     name='BinHeader2',
256     fields=[
257         Field('h', 'version', help='Version number for backwards compatibility.'),
258         Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
259         Field('l', 'noteSize', help='The size of the note text.'),
260         Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
261         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
262         ])
263
264 BinHeader3 = Structure(
265     name='BinHeader3',
266     fields=[
267         Field('h', 'version', help='Version number for backwards compatibility.'),
268         Field('h', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
269         Field('l', 'noteSize', help='The size of the note text.'),
270         Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
271         Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
272         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
273         ])
274
275 BinHeader5 = Structure(
276     name='BinHeader5',
277     fields=[
278         Field('h', 'version', help='Version number for backwards compatibility.'),
279         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
280         Field('l', 'wfmSize', help='The size of the WaveHeader5 data structure plus the wave data.'),
281         Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
282         Field('l', 'noteSize', help='The size of the note text.'),
283         Field('l', 'dataEUnitsSize', help='The size of optional extended data units.'),
284         Field('l', 'dimEUnitsSize', help='The size of optional extended dimension units.', count=MAXDIMS),
285         Field('l', 'dimLabelsSize', help='The size of optional dimension labels.', count=MAXDIMS),
286         Field('l', 'sIndicesSize', help='The size of string indicies if this is a text wave.'),
287         Field('l', 'optionsSize1', default=0, help='Reserved. Write zero. Ignore on read.'),
288         Field('l', 'optionsSize2', default=0, help='Reserved. Write zero. Ignore on read.'),
289         ])
290
291
292 # From wave.h
293 MAX_WAVE_NAME2 = 18 # Maximum length of wave name in version 1 and 2
294                     # files. Does not include the trailing null.
295 MAX_WAVE_NAME5 = 31 # Maximum length of wave name in version 5
296                     # files. Does not include the trailing null.
297 MAX_UNIT_CHARS = 3
298
299 # Header to an array of waveform data.
300
301 WaveHeader2 = Structure(
302     name='WaveHeader2',
303     fields=[
304         Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
305         Field('P', 'next', default=0, help='Used in memory only. Write zero. Ignore on read.'),
306         Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME2+2),
307         Field('h', 'whVersion', default=0, help='Write 0. Ignore on read.'),
308         Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
309         Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
310         Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
311         Field('c', 'xUnits', default=0, help='Natural x-axis units go here - null if none.', count=MAX_UNIT_CHARS+1),
312         Field('l', 'npnts', help='Number of data points in wave.'),
313         Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
314         Field('d', 'hsA', help='X value for point p = hsA*p + hsB'),
315         Field('d', 'hsB', help='X value for point p = hsA*p + hsB'),
316         Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
317         Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
318         Field('h', 'fsValid', help='True if full scale values have meaning.'),
319         Field('d', 'topFullScale', help='The min full scale value for wave.'), # sic, 'min' should probably be 'max'
320         Field('d', 'botFullScale', help='The min full scale value for wave.'),
321         Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
322         Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
323         Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
324         Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
325         Field('L', 'creationDate', help='DateTime of creation.  Not used in version 1 files.'),
326         Field('c', 'wUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=2),
327         Field('L', 'modDate', help='DateTime of last modification.'),
328         Field('P', 'waveNoteH', help='Used in memory only. Write zero. Ignore on read.'),
329         Field('f', 'wData', help='The start of the array of waveform data.', count=4),
330         ])
331
332 WaveHeader5 = Structure(
333     name='WaveHeader5',
334     fields=[
335         Field('P', 'next', help='link to next wave in linked list.'),
336         Field('L', 'creationDate', help='DateTime of creation.'),
337         Field('L', 'modDate', help='DateTime of last modification.'),
338         Field('l', 'npnts', help='Total number of points (multiply dimensions up to first zero).'),
339         Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
340         Field('h', 'dLock', default=0, help='Reserved. Write zero. Ignore on read.'),
341         Field('c', 'whpad1', default=0, help='Reserved. Write zero. Ignore on read.', count=6),
342         Field('h', 'whVersion', default=1, help='Write 1. Ignore on read.'),
343         Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME5+1),
344         Field('l', 'whpad2', default=0, help='Reserved. Write zero. Ignore on read.'),
345         Field('P', 'dFolder', default=0, help='Used in memory only. Write zero. Ignore on read.'),
346         # Dimensioning info. [0] == rows, [1] == cols etc
347         Field('l', 'nDim', help='Number of of items in a dimension -- 0 means no data.', count=MAXDIMS),
348         Field('d', 'sfA', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
349         Field('d', 'sfB', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
350         # SI units
351         Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
352         Field('c', 'dimUnits', default=0, help='Natural dimension units go here - null if none.', count=(MAXDIMS, MAX_UNIT_CHARS+1)),
353         Field('h', 'fsValid', help='TRUE if full scale values have meaning.'),
354         Field('h', 'whpad3', default=0, help='Reserved. Write zero. Ignore on read.'),
355         Field('d', 'topFullScale', help='The max and max full scale value for wave'), # sic, probably "max and min"
356         Field('d', 'botFullScale', help='The max and max full scale value for wave.'), # sic, probably "max and min"
357         Field('P', 'dataEUnits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
358         Field('P', 'dimEUnits', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
359         Field('P', 'dimLabels', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
360         Field('P', 'waveNoteH', default=0, help='Used in memory only. Write zero. Ignore on read.'),
361         Field('l', 'whUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=16),
362         # The following stuff is considered private to Igor.
363         Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
364         Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
365         Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
366         Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
367         Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
368         Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
369         Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
370         Field('h', 'whpad4', default=0, help='Reserved. Write zero. Ignore on read.'),
371         Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
372         Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
373         Field('P', 'sIndices', default=0, help='Used in memory only. Write zero. Ignore on read.'),
374         Field('f', 'wData', help='The start of the array of data.  Must be 64 bit aligned.', count=1),
375         ])
376
377 # End IGOR constants and typedefs from IgorBin.h
378
379 # Begin functions from ReadWave.c
380
381 def need_to_reorder_bytes(version):
382     # If the low order byte of the version field of the BinHeader
383     # structure is zero then the file is from a platform that uses
384     # different byte-ordering and therefore all data will need to be
385     # reordered.
386     return version & 0xFF == 0
387
388 def byte_order(needToReorderBytes):
389     little_endian = sys.byteorder == 'little'
390     if needToReorderBytes:
391         little_endian = not little_endian
392     if little_endian:
393         return '<'  # little-endian
394     return '>'  # big-endian    
395
396 def version_structs(version, byte_order):
397     if version == 1:
398         bin = BinHeader1
399         wave = WaveHeader2
400     elif version == 2:
401         bin = BinHeader2
402         wave = WaveHeader2
403     elif version == 3:
404         bin = BinHeader3
405         wave = WaveHeader2
406     elif version == 5:
407         bin = BinHeader5
408         wave = WaveHeader5
409     else:
410         raise ValueError('This does not appear to be a valid Igor binary wave file. The version field = %d.\n', version);
411     checkSumSize = bin.size + wave.size
412     if version == 5:
413         checkSumSize -= 4  # Version 5 checksum does not include the wData field.
414     bin.set_byte_order(byte_order)
415     wave.set_byte_order(byte_order)
416     return (bin, wave, checkSumSize)
417
418 def checksum(buffer, byte_order, oldcksum, numbytes):
419     x = numpy.ndarray(
420         (numbytes/2,), # 2 bytes to a short -- ignore trailing odd byte
421         dtype=numpy.dtype(byte_order+'h'),
422         buffer=buffer)
423     oldcksum += x.sum()
424     if oldcksum > 2**31:  # fake the C implementation's int rollover
425         oldcksum %= 2**32
426         if oldcksum > 2**31:
427             oldcksum -= 2**31
428     return oldcksum & 0xffff
429
430 # Translated from ReadWave()
431 def loadibw(filename):
432     if hasattr(filename, 'read'):
433         f = filename  # filename is actually a stream object
434     else:
435         f = open(filename, 'rb')
436     try:
437         b = buffer(f.read(BinHeaderCommon.size))
438         version = BinHeaderCommon.unpack_dict_from(b)['version']
439         needToReorderBytes = need_to_reorder_bytes(version)
440         byteOrder = byte_order(needToReorderBytes)
441         
442         if needToReorderBytes:
443             BinHeaderCommon.set_byte_order(byteOrder)
444             version = BinHeaderCommon.unpack_dict_from(b)['version']
445         bin_struct,wave_struct,checkSumSize = version_structs(version, byteOrder)
446
447         b = buffer(b + f.read(bin_struct.size + wave_struct.size - BinHeaderCommon.size))
448         c = checksum(b, byteOrder, 0, checkSumSize)
449         if c != 0:
450             raise ValueError('Error in checksum - should be 0, is %d.  This does not appear to be a valid Igor binary wave file.' % c)
451         bin_info = bin_struct.unpack_dict_from(b)
452         wave_info = wave_struct.unpack_dict_from(b, offset=bin_struct.size)
453         if wave_info['type'] == 0:
454             raise NotImplementedError('Text wave')
455         if version in [1,2,3]:
456             tail = 16  # 16 = size of wData field in WaveHeader2 structure
457             waveDataSize = bin_info['wfmSize'] - wave_struct.size
458             # =  bin_info['wfmSize']-16 - (wave_struct.size - tail)
459         else:
460             assert version == 5, version
461             tail = 4  # 4 = size of wData field in WaveHeader5 structure
462             waveDataSize = bin_info['wfmSize'] - (wave_struct.size - tail)
463         # dtype() wrapping to avoid numpy.generic and
464         # getset_descriptor issues with the builtin Numpy types
465         # (e.g. int32).  It has no effect on our local complex
466         # integers.
467         t = numpy.dtype(TYPE_TABLE[wave_info['type']])
468         assert waveDataSize == wave_info['npnts'] * t.itemsize, \
469             ('%d, %d, %d, %s' % (waveDataSize, wave_info['npnts'], t.itemsize, t))
470         tail_data = array.array('f', b[-tail:])
471         data_b = buffer(buffer(tail_data) + f.read(waveDataSize-tail))
472         data = numpy.ndarray(
473             wave_info['npnts'],
474             dtype=t.newbyteorder(byteOrder),
475             buffer=data_b
476             )
477     finally:
478         if not hasattr(filename, 'read'):
479             f.close()
480
481     return data, bin_info, wave_info
482
483
484 def saveibw(filename):
485     raise NotImplementedError
486
487
488 if __name__ == '__main__':
489     """IBW -> ASCII conversion
490     """
491     import optparse
492     import sys
493
494     p = optparse.OptionParser(version=__version__)
495
496     p.add_option('-f', '--infile', dest='infile', metavar='FILE',
497                  default='-', help='Input IGOR Binary Wave (.ibw) file.')
498     p.add_option('-o', '--outfile', dest='outfile', metavar='FILE',
499                  default='-', help='File for ASCII output.')
500     p.add_option('-v', '--verbose', dest='verbose', default=0,
501                  action='count', help='Increment verbosity')
502     p.add_option('-t', '--test', dest='test', default=False,
503                  action='store_true', help='Run internal tests and exit.')
504
505     options,args = p.parse_args()
506
507     if options.test == True:
508         import doctest
509         num_failures,num_tests = doctest.testmod(verbose=options.verbose)
510         sys.exit(min(num_failures, 127))
511
512     if len(args) > 0 and options.infile == None:
513         options.infile = args[0]
514     if options.infile == '-':
515         options.infile = sys.stdin
516     if options.outfile == '-':
517         options.outfile = sys.stdout
518
519     data,bin_info,wave_info = loadibw(options.infile)
520     numpy.savetxt(options.outfile, data, fmt='%g', delimiter='\n')
521     if options.verbose > 0:
522         import pprint
523         pprint.pprint(bin_info)
524         pprint.pprint(wave_info)