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