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