fix doc typo
[igor.git] / igor.py
1 # This program is in the public domain
2 """
3 IGOR file reader.
4
5 igor.load('filename') or igor.loads('data') loads the content of an igore file
6 into memory as a folder structure.
7
8 Returns the root folder.
9
10 Folders have name, path and children.
11 Children can be indexed by folder[i] or by folder['name'].
12 To see the whole tree, use: print folder.format()
13
14 The usual igor folder types are given in the technical reports
15 PTN003.ifn and TN003.ifn.
16 """
17 __version__="0.9"
18
19 import struct
20 import numpy
21 import sys
22 import re
23
24 decode = lambda s: s.decode(sys.getfilesystemencoding())
25
26 PYKEYWORDS = set(('and','as','assert','break','class','continue',
27                   'def','elif','else','except','exec','finally',
28                   'for','global','if','import','in','is','lambda',
29                   'or','pass','print','raise','return','try','with',
30                   'yield'))
31 PYID = re.compile(r"^[^\d\W]\w*$", re.UNICODE)
32 def valid_identifier(s):
33     """Check if a name is a valid identifier"""
34     return PYID.match(s) and s not in PYKEYWORDS
35
36 NUMTYPE = {
37      1: numpy.complex64,
38      2: numpy.float32,
39      3: numpy.complex64,
40      4: numpy.float64,
41      5: numpy.complex128,
42      8: numpy.int8,
43     16: numpy.int16,
44     32: numpy.int32,
45  64+ 8: numpy.uint8,
46  64+16: numpy.uint16,
47  64+32: numpy.uint32,
48 }
49
50 ORDER_NUMTYPE = {
51      1: 'c8',
52      2: 'f4',
53      3: 'c8',
54      4: 'f8',
55      5: 'c16',
56      8: 'i1',
57     16: 'i2',
58     32: 'i4',
59  64+ 8: 'u2',
60  64+16: 'u2',
61  64+32: 'u4',
62 }
63
64
65 class IgorObject(object):
66     """ Parent class for all objects the parser can return """
67     pass
68
69 class Formula(IgorObject):
70     def __init__(self, formula, value):
71         self.formula = formula
72         self.value = value
73
74 class Variables(IgorObject):
75     """
76     Contains system numeric variables (e.g., K0) and user numeric and string variables.
77     """
78     def __init__(self, data, order):
79         version, = struct.unpack(order+"h",data[:2])
80         if version == 1:
81             pos = 8
82             nSysVar, nUserVar, nUserStr \
83                 = struct.unpack(order+"hhh",data[2:pos])
84             nDepVar, nDepStr = 0, 0
85         elif version == 2:
86             pos = 12
87             nSysVar, nUservar, nUserStr, nDepVar, nDepStr \
88                 = struct.unpack(order+"hhh",data[2:pos])
89         else:
90             raise ValueError("Unknown variable record version "+str(version))
91         self.sysvar, pos = _parse_sys_numeric(nSysVar, order, data, pos)
92         self.uservar, pos = _parse_user_numeric(nUserVar, order, data, pos)
93         if version == 1:
94             self.userstr, pos = _parse_user_string1(nUserStr, order, data, pos)
95         else:
96             self.userstr, pos = _parse_user_string2(nUserStr, order, data, pos)
97         self.depvar, pos = _parse_dep_numeric(nDepVar, order, data, pos)
98         self.depstr, pos = _parse_dep_string(nDepStr, order, data, pos)
99     def format(self, indent=0):
100         return " "*indent+"<Variables: system %d, user %d, dependent %s>"\
101             %(len(self.sysvar),
102               len(self.uservar)+len(self.userstr),
103               len(self.depvar)+len(self.depstr))
104
105 class History(IgorObject):
106     """
107     Contains the experiment's history as plain text.
108     """
109     def __init__(self, data, order): self.data = data
110     def format(self, indent=0):
111         return " "*indent+"<History>"
112
113 class Wave(IgorObject):
114     """
115     Contains the data for a wave
116     """
117     def __init__(self, data, order):
118         version, = struct.unpack(order+'h',data[:2])
119         if version == 1:
120             pos = 8
121             extra_offset,checksum = struct.unpack(order+'ih',data[2:pos])
122             formula_size = note_size = pic_size = 0
123         elif version == 2:
124             pos = 16
125             extra_offset,note_size,pic_size,checksum \
126                 = struct.unpack(order+'iiih',data[2:pos])
127             formula_size = 0
128         elif version == 3:
129             pos = 20
130             extra_offset,note_size,formula_size,pic_size,checksum \
131                 = struct.unpack(order+'iiiih',data[2:pos])
132         elif version == 5:
133             checksum,extra_offset,formula_size,note_size, \
134                 = struct.unpack(order+'hiii',data[2:16])
135             Esize = struct.unpack(order+'iiiiiiiii',data[16:52])
136             textindsize, = struct.unpack('i',data[52:56])
137             picsize = 0
138             pos = 64
139         else:
140             raise ValueError("unknown wave version "+str(version))
141         extra_offset += pos
142
143         if version in (1,2,3):
144             type, = struct.unpack(order+'h',data[pos:pos+2])
145             name = data[pos+6:data.find(chr(0),pos+6,pos+26)]
146             #print "name3",name,type
147             data_units = data[pos+34:data.find(chr(0),pos+34,pos+38)]
148             xaxis = data[pos+38:data.find(chr(0),pos+38,pos+42)]
149             points, = struct.unpack(order+'i',data[pos+42:pos+46])
150             hsA,hsB = struct.unpack(order+'dd',data[pos+48:pos+64])
151             fsValid,fsTop,fsBottom \
152                 = struct.unpack(order+'hdd',data[pos+70:pos+88])
153             created,_,modified = struct.unpack(order+'IhI',data[pos+98:pos+108])
154             pos += 110
155             dims = (points,0,0,0)
156             sf = (hsA,0,0,0,hsB,0,0,0)
157             axis_units = (xaxis,"","","")
158         else: # version is 5
159             created,modified,points,type \
160                 = struct.unpack(order+'IIih',data[pos+4:pos+18])
161             name = data[pos+28:data.find(chr(0),pos+28,pos+60)]
162             #print "name5",name,type
163             dims = struct.unpack(order+'iiii',data[pos+68:pos+84])
164             sf = struct.unpack(order+'dddddddd',data[pos+84:pos+148])
165             data_units = data[pos+148:data.find(chr(0),pos+148,pos+152)]
166             axis_units = tuple(data[pos+152+4*i
167                                  : data.find(chr(0),pos+152+4*i,pos+156+4*i)]
168                          for i in range(4))
169             fsValid,_,fsTop,fsBottom \
170                 = struct.unpack(order+'hhdd',data[pos+172:pos+192])
171             pos += 320
172
173         if type == 0:
174             text = data[pos:extra_offset]
175             textind = numpy.fromstring(data[-textindsize:], order+'i')
176             textind = numpy.hstack((0,textind))
177             value = [text[textind[i]:textind[i+1]]
178                      for i in range(len(textind)-1)]
179         else:
180             trimdims = tuple(d for d in dims if d)
181             dtype = order+ORDER_NUMTYPE[type]
182             size = int(dtype[2:])*numpy.prod(trimdims)
183             value = numpy.fromstring(data[pos:pos+size],dtype)
184             value = value.reshape(trimdims)
185
186         pos = extra_offset
187         formula = data[pos:pos+formula_size]
188         pos += formula_size
189         notes = data[pos:pos+note_size]
190         pos += note_size
191         if version == 5:
192             offset = numpy.cumsum(numpy.hstack((pos,Esize)))
193             Edata_units = data[offset[0]:offset[1]]
194             Eaxis_units = [data[offset[i]:offset[i+1]] for i in range(1,5)]
195             Eaxis_labels = [data[offset[i]:offset[i+1]] for i in range(5,9)]
196             if Edata_units: data_units = Edata_units
197             for i,u in enumerate(Eaxis_units):
198                 if u: axis_units[i] = u
199             axis_labels = Eaxis_labels
200             pos = offset[-1]
201
202
203         self.name = decode(name)
204         self.data = value
205         self.data_units = data_units
206         self.axis_units = axis_units
207         self.fs,self.fstop,self.fsbottom = fsValid,fsTop,fsBottom
208         self.axis = [numpy.linspace(a,b,n)
209                      for a,b,n in zip(sf[:4],sf[4:],dims)]
210         self.formula = formula
211         self.notes = notes
212     def format(self, indent=0):
213         if isinstance(self.data, list):
214             type,size = "text", "%d"%len(self.data)
215         else:
216             type,size = "data", "x".join(str(d) for d in self.data.shape)
217         return " "*indent+"%s %s (%s)"%(self.name, type, size)
218
219     def __array__(self):
220         return self.data
221
222     __repr__ = __str__ = lambda s: u"<igor.Wave %s>" % s.format()
223
224 class Recreation(IgorObject):
225     """
226     Contains the experiment's recreation procedures as plain text.
227     """
228     def __init__(self, data, order): self.data = data
229     def format(self, indent=0):
230         return " "*indent + "<Recreation>"
231 class Procedure(IgorObject):
232     """
233     Contains the experiment's main procedure window text as plain text.
234     """
235     def __init__(self, data, order): self.data = data
236     def format(self, indent=0):
237         return " "*indent + "<Procedure>"
238 class GetHistory(IgorObject):
239     """
240     Not a real record but rather, a message to go back and read the history text.
241
242     The reason for GetHistory is that IGOR runs Recreation when it loads the
243     datafile.  This puts entries in the history that shouldn't be there.  The
244     GetHistory entry simply says that the Recreation has run, and the History
245     can be restored from the previously saved value.
246     """
247     def __init__(self, data, order): self.data = data
248     def format(self, indent=0):
249         return " "*indent + "<GetHistory>"
250 class PackedFile(IgorObject):
251     """
252     Contains the data for a procedure file or notebook in packed form.
253     """
254     def __init__(self, data, order): self.data = data
255     def format(self, indent=0):
256         return " "*indent + "<PackedFile>"
257 class Unknown(IgorObject):
258     """
259     Record type not documented in PTN003/TN003.
260     """
261     def __init__(self, data, order, type):
262         self.data = data
263         self.type = type
264     def format(self, indent=0):
265         return " "*indent + "<Unknown type %s>"%self.type
266
267 class _FolderStart(IgorObject):
268     """
269     Marks the start of a new data folder.
270     """
271     def __init__(self, data, order):
272         self.name = decode(data[:data.find(chr(0))])
273 class _FolderEnd(IgorObject):
274     """
275     Marks the end of a data folder.
276     """
277     def __init__(self, data, order): self.data = data
278
279 class Folder(IgorObject):
280     """
281     Hierarchical record container.
282     """
283     def __init__(self, path):
284         self.name = path[-1]
285         self.path = path
286         self.children = []
287
288     def __getitem__(self, key):
289         if isinstance(key, int):
290             return self.children[key]
291         else:
292             for r in self.children:
293                 if isinstance(r, (Folder,Wave)) and r.name == key:
294                     return r
295             raise KeyError("Folder %s does not exist"%key)
296
297     def __str__(self):
298         return u"<igor.Folder %s>" % "/".join(self.path)
299
300     __repr__ = __str__
301
302     def append(self, record):
303         """
304         Add a record to the folder.
305         """
306         self.children.append(record)
307         try:
308             # Record may not have a name, the name may be invalid, or it
309             # may already be in use.   The noname case will be covered by
310             # record.name raising an attribute error.  The others we need
311             # to test for explicitly.
312             if valid_identifier(record.name) and not hasattr(self, record.name):
313                 setattr(self, record.name, record)
314         except AttributeError:
315             pass
316
317     def format(self, indent=0):
318         parent = u" "*indent+self.name
319         children = [r.format(indent=indent+2) for r in self.children]
320         return u"\n".join([parent]+children)
321
322 PARSER = {
323 1: Variables,
324 2: History,
325 3: Wave,
326 4: Recreation,
327 5: Procedure,
328 7: GetHistory,
329 8: PackedFile,
330 9: _FolderStart,
331 10: _FolderEnd,
332 }
333
334 def loads(s, ignore_unknown=True):
335     """Load an igor file from string"""
336     max = len(s)
337     pos = 0
338     ret = []
339     stack = [Folder(path=[u'root'])]
340     while pos < max:
341         if pos+8 > max:
342             raise IOError("invalid record header; bad pxp file?")
343         ignore = ord(s[pos])&0x80
344         order = '<' if ord(s[pos])&0x77 else '>'
345         type, version, length = struct.unpack(order+'hhi',s[pos:pos+8])
346         pos += 8
347         if pos+length > len(s):
348             raise IOError("final record too long; bad pxp file?")
349         data = s[pos:pos+length]
350         pos += length
351         if not ignore:
352             parse = PARSER.get(type, None)
353             if parse:
354                 record = parse(data, order)
355             elif ignore_unknown:
356                 continue
357             else:
358                 record = Unknown(data=data, order=order, type=type)
359             if isinstance(record, _FolderStart):
360                 path = stack[-1].path+[record.name]
361                 folder = Folder(path)
362                 stack[-1].append(folder)
363                 stack.append(folder)
364             elif isinstance(record, _FolderEnd):
365                 stack.pop()
366             else:
367                 stack[-1].append(record)
368     if len(stack) != 1:
369         raise IOError("FolderStart records do not match FolderEnd records")
370     return stack[0]
371
372 def load(filename, ignore_unknown=True):
373     """Load an igor file"""
374     return loads(open(filename,'rb').read(),
375                  ignore_unknown=ignore_unknown)
376
377 # ============== Variable parsing ==============
378 def _parse_sys_numeric(n, order, data, pos):
379     values = numpy.fromstring(data[pos:pos+n*4], order+'f')
380     pos += n*4
381     var = dict(('K'+str(i),v) for i,v in enumerate(values))
382     return var, pos
383
384 def _parse_user_numeric(n, order, data, pos):
385     var = {}
386     for i in range(n):
387         name = data[pos:data.find(chr(0),pos,pos+32)]
388         type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52])
389         dtype = NUMTYPE[numtype]
390         if dtype in (numpy.complex64, numpy.complex128):
391             value = dtype(real+1j*imag)
392         else:
393             value = dtype(real)
394         var[name] = value
395         pos += 56
396     return var, pos
397
398 def _parse_dep_numeric(n, order, data, pos):
399     var = {}
400     for i in range(n):
401         name = data[pos:data.find(chr(0),pos,pos+32)]
402         type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52])
403         dtype = NUMTYPE[numtype]
404         if dtype in (numpy.complex64, numpy.complex128):
405             value = dtype(real+1j*imag)
406         else:
407             value = dtype(real)
408         length, = struct.unpack(order+"h",data[pos+56:pos+58])
409         var[name] = Formula(data[pos+58:pos+58+length-1], value)
410         pos += 58+length
411     return var, pos
412
413 def _parse_dep_string(n, order, data, pos):
414     var = {}
415     for i in range(n):
416         name = data[pos:data.find(chr(0),pos,pos+32)]
417         length, = struct.unpack(order+"h",data[pos+48:pos+50])
418         var[name] = Formula(data[pos+50:pos+50+length-1], "")
419         pos += 50+length
420     return var, pos
421
422 def _parse_user_string1(n, order, data, pos):
423     var = {}
424     for i in range(n):
425         name = data[pos:data.find(chr(0),pos,pos+32)]
426         length, = struct.unpack(order+"h",data[pos+32:pos+34])
427         value = data[pos+34:pos+34+length]
428         pos += 34+length
429         var[name] = value
430     return var, pos
431
432 def _parse_user_string2(n, order, data, pos):
433     var = {}
434     for i in range(n):
435         name = data[pos:data.find(chr(0),pos,pos+32)]
436         length, = struct.unpack(order+"i",data[pos+32:pos+36])
437         value = data[pos+36:pos+36+length]
438         pos += 36+length
439         var[name] = value
440     return var, pos