Allow subfolders to be reached as attributes
[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     def __getitem__(self, key):
277         if isinstance(key, int):
278             return self.children[key]
279         else:
280             for r in self.children:
281                 if isinstance(r, (Folder,Wave)) and r.name == key:
282                     return r
283             raise KeyError("Folder %s does not exist"%key)
284             
285     def __str__(self):
286         return u"<igor.Folder %s>" % "/".join(self.path)
287     
288     __repr__ = __str__
289             
290     def append(self, record):
291         self.children.append(record)
292         try:
293             setattr(self, record.name, record)
294         except AttributeError:
295             pass
296         
297     def format(self, indent=0):
298         parent = u" "*indent+self.name
299         children = [r.format(indent=indent+2) for r in self.children]
300         return u"\n".join([parent]+children)
301
302 PARSER = {
303 1: Variables,
304 2: History,
305 3: Wave,
306 4: Recreation,
307 5: Procedure,
308 7: GetHistory,
309 8: PackedFile,
310 9: _FolderStart,
311 10: _FolderEnd,
312 }
313
314 def loads(s, ignore_unknown=True):
315     """Load an igor file from string"""
316     max = len(s)
317     pos = 0
318     ret = []
319     stack = [Folder(path=[u'root'])]
320     while pos < max:
321         if pos+8 > max:
322             raise IOError("invalid record header; bad pxp file?")
323         ignore = ord(s[pos])&0x80
324         order = '<' if ord(s[pos])&0x77 else '>'
325         type, version, length = struct.unpack(order+'hhi',s[pos:pos+8])
326         pos += 8
327         if pos+length > len(s):
328             raise IOError("final record too long; bad pxp file?")
329         data = s[pos:pos+length]
330         pos += length
331         if not ignore:
332             parse = PARSER.get(type, None)
333             if parse:
334                 record = parse(data, order)
335             elif ignore_unknown:
336                 continue
337             else:
338                 record = Unknown(data=data, order=order, type=type)
339             if isinstance(record, _FolderStart):
340                  path = stack[-1].path+[record.name]
341                  folder = Folder(path)
342                  stack[-1].append(folder)
343                  stack.append(folder)
344             elif isinstance(record, _FolderEnd):
345                  stack.pop()
346             else:
347                  stack[-1].append(record)
348     if len(stack) != 1:
349         raise IOError("FolderStart records do not match FolderEnd records")
350     return stack[0]
351
352 def load(filename, ignore_unknown=True):
353     """Load an igor file"""
354     return loads(open(filename,'rb').read(),
355                  ignore_unknown=ignore_unknown)
356
357 # ============== Variable parsing ==============
358 def _parse_sys_numeric(n, order, data, pos):
359     values = numpy.fromstring(data[pos:pos+n*4], order+'f')
360     pos += n*4
361     var = dict(('K'+str(i),v) for i,v in enumerate(values)) 
362     return var, pos
363
364 def _parse_user_numeric(n, order, data, pos):
365     var = {}
366     for i in range(n):
367         name = data[pos:data.find(chr(0),pos,pos+32)]
368         type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52])
369         dtype = NUMTYPE[numtype]
370         if dtype in (numpy.complex64, numpy.complex128):
371             value = dtype(real+1j*imag)
372         else:
373             value = dtype(real) 
374         var[name] = value
375         pos += 56
376     return var, pos
377
378 def _parse_dep_numeric(n, order, data, pos):
379     var = {}
380     for i in range(n):
381         name = data[pos:data.find(chr(0),pos,pos+32)]
382         type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52])
383         dtype = NUMTYPE[numtype]
384         if dtype in (numpy.complex64, numpy.complex128):
385             value = dtype(real+1j*imag)
386         else:
387             value = dtype(real) 
388         length, = struct.unpack(order+"h",data[pos+56:pos+58])
389         var[name] = Formula(data[pos+58:pos+58+length-1], value)
390         pos += 58+length
391     return var, pos
392
393 def _parse_dep_string(n, order, data, pos):
394     var = {}
395     for i in range(n):
396         name = data[pos:data.find(chr(0),pos,pos+32)]
397         length, = struct.unpack(order+"h",data[pos+48:pos+50])
398         var[name] = Formula(data[pos+50:pos+50+length-1], "")
399         pos += 50+length
400     return var, pos
401
402 def _parse_user_string1(n, order, data, pos):
403     var = {}
404     for i in range(n):
405         name = data[pos:data.find(chr(0),pos,pos+32)]
406         length, = struct.unpack(order+"h",data[pos+32:pos+34])
407         value = data[pos+34:pos+34+length]
408         pos += 34+length
409         var[name] = value
410     return var, pos
411
412 def _parse_user_string2(n, order, data, pos):
413     var = {}
414     for i in range(n):
415         name = data[pos:data.find(chr(0),pos,pos+32)]
416         length, = struct.unpack(order+"i",data[pos+32:pos+36])
417         value = data[pos+36:pos+36+length]
418         pos += 36+length
419         var[name] = value
420     return var, pos
421