igorpy.py 9.52 KB
Newer Older
Paul Kienzle's avatar
Paul Kienzle committed
1
# This program is in the public domain
2
"""`igor.py` compatibility layer on top of the `igor` package.
Paul Kienzle's avatar
Paul Kienzle committed
3
4
5
6
7
8
9
10
11
12
13
14
15

igor.load('filename') or igor.loads('data') loads the content of an igore file
into memory as a folder structure.

Returns the root folder.

Folders have name, path and children.
Children can be indexed by folder[i] or by folder['name'].
To see the whole tree, use: print folder.format()

The usual igor folder types are given in the technical reports
PTN003.ifn and TN003.ifn.
"""
16
from __future__ import absolute_import
17
18
import io as _io
import re as _re
19

20
import numpy as _numpy
Paul Kienzle's avatar
Paul Kienzle committed
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from .binarywave import MAXDIMS as _MAXDIMS
from .packed import load as _load
from .record.base import UnknownRecord as _UnknownRecord
from .record.folder import FolderStartRecord as _FolderStartRecord
from .record.folder import FolderEndRecord as _FolderEndRecord
from .record.history import HistoryRecord as _HistoryRecord
from .record.history import GetHistoryRecord as _GetHistoryRecord
from .record.history import RecreationRecord as _RecreationRecord
from .record.packedfile import PackedFileRecord as _PackedFileRecord
from .record.procedure import ProcedureRecord as _ProcedureRecord
from .record.wave import WaveRecord as _WaveRecord
from .record.variables import VariablesRecord as _VariablesRecord


__version__='0.10'
37

Paul Kienzle's avatar
Paul Kienzle committed
38

39
40
41
42
43
PYKEYWORDS = set(('and','as','assert','break','class','continue',
                  'def','elif','else','except','exec','finally',
                  'for','global','if','import','in','is','lambda',
                  'or','pass','print','raise','return','try','with',
                  'yield'))
44
PYID = _re.compile(r"^[^\d\W]\w*$", _re.UNICODE)
45
46
47
48
def valid_identifier(s):
    """Check if a name is a valid identifier"""
    return PYID.match(s) and s not in PYKEYWORDS

Paul Kienzle's avatar
Paul Kienzle committed
49

50
class IgorObject(object):
51
52
    """ Parent class for all objects the parser can return """
    pass
Paul Kienzle's avatar
Paul Kienzle committed
53

54
class Variables(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
55
56
57
    """
    Contains system numeric variables (e.g., K0) and user numeric and string variables.
    """
58
59
60
61
62
63
64
    def __init__(self, record):
        self.sysvar = record.variables['variables']['sysVars']
        self.uservar = record.variables['variables']['userVars']
        self.userstr = record.variables['variables']['userStrs']
        self.depvar = record.variables['variables'].get('dependentVars', {})
        self.depstr = record.variables['variables'].get('dependentStrs', {})

Paul Kienzle's avatar
Paul Kienzle committed
65
66
67
68
69
70
    def format(self, indent=0):
        return " "*indent+"<Variables: system %d, user %d, dependent %s>"\
            %(len(self.sysvar),
              len(self.uservar)+len(self.userstr),
              len(self.depvar)+len(self.depstr))

71
class History(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
72
73
74
    """
    Contains the experiment's history as plain text.
    """
75
76
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
77
78
79
    def format(self, indent=0):
        return " "*indent+"<History>"

80
class Wave(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
81
82
83
    """
    Contains the data for a wave
    """
84
85
86
87
88
89
90
91
92
93
94
95
96
    def __init__(self, record):
        d = record.wave['wave']
        self.name = d['wave_header']['bname']
        self.data = d['wData']
        self.fs = d['wave_header']['fsValid']
        self.fstop = d['wave_header']['topFullScale']
        self.fsbottom = d['wave_header']['botFullScale']
        if record.wave['version'] in [1,2,3]:
            dims = [d['wave_header']['npnts']] + [0]*(_MAXDIMS-1)
            sfA = [d['wave_header']['hsA']] + [0]*(_MAXDIMS-1)
            sfB = [d['wave_header']['hsB']] + [0]*(_MAXDIMS-1)
            self.data_units = [d['wave_header']['dataUnits']]
            self.axis_units = [d['wave_header']['xUnits']]
Paul Kienzle's avatar
Paul Kienzle committed
97
        else:
98
99
100
101
102
103
104
105
106
107
108
109
110
            dims = d['wave_header']['nDim']
            sfA = d['wave_header']['sfA']
            sfB = d['wave_header']['sfB']
            # TODO find example with multiple data units
            self.data_units = [d['data_units']]
            self.axis_units = [d['dimension_units']]
        self.data_units.extend(['']*(_MAXDIMS-len(self.data_units)))
        self.data_units = tuple(self.data_units)
        self.axis_units.extend(['']*(_MAXDIMS-len(self.axis_units)))
        self.axis_units = tuple(self.axis_units)
        self.axis = [_numpy.linspace(a,b,c) for a,b,c in zip(sfA, sfB, dims)]
        self.formula = d.get('formula', '')
        self.notes = d.get('note', '')
Paul Kienzle's avatar
Paul Kienzle committed
111
112
113
114
115
116
    def format(self, indent=0):
        if isinstance(self.data, list):
            type,size = "text", "%d"%len(self.data)
        else:
            type,size = "data", "x".join(str(d) for d in self.data.shape)
        return " "*indent+"%s %s (%s)"%(self.name, type, size)
117

118
119
    def __array__(self):
        return self.data
120

121
    __repr__ = __str__ = lambda s: "<igor.Wave %s>" % s.format()
122
123

class Recreation(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
124
125
126
    """
    Contains the experiment's recreation procedures as plain text.
    """
127
128
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
129
130
    def format(self, indent=0):
        return " "*indent + "<Recreation>"
131
class Procedure(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
132
133
134
    """
    Contains the experiment's main procedure window text as plain text.
    """
135
136
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
137
138
    def format(self, indent=0):
        return " "*indent + "<Procedure>"
139
class GetHistory(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
140
141
142
143
144
145
146
147
    """
    Not a real record but rather, a message to go back and read the history text.

    The reason for GetHistory is that IGOR runs Recreation when it loads the
    datafile.  This puts entries in the history that shouldn't be there.  The
    GetHistory entry simply says that the Recreation has run, and the History
    can be restored from the previously saved value.
    """
148
149
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
150
151
    def format(self, indent=0):
        return " "*indent + "<GetHistory>"
152
class PackedFile(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
153
154
155
    """
    Contains the data for a procedure file or notebook in packed form.
    """
156
157
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
158
159
    def format(self, indent=0):
        return " "*indent + "<PackedFile>"
160
class Unknown(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
161
162
163
    """
    Record type not documented in PTN003/TN003.
    """
164
    def __init__(self, data, type):
Paul Kienzle's avatar
Paul Kienzle committed
165
166
167
168
169
170
        self.data = data
        self.type = type
    def format(self, indent=0):
        return " "*indent + "<Unknown type %s>"%self.type


171
class Folder(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
172
173
174
175
176
177
178
    """
    Hierarchical record container.
    """
    def __init__(self, path):
        self.name = path[-1]
        self.path = path
        self.children = []
179

Paul Kienzle's avatar
Paul Kienzle committed
180
181
182
183
184
185
186
187
    def __getitem__(self, key):
        if isinstance(key, int):
            return self.children[key]
        else:
            for r in self.children:
                if isinstance(r, (Folder,Wave)) and r.name == key:
                    return r
            raise KeyError("Folder %s does not exist"%key)
188

189
    def __str__(self):
190
        return "<igor.Folder %s>" % "/".join(self.path)
191

192
    __repr__ = __str__
193

Paul Kienzle's avatar
Paul Kienzle committed
194
    def append(self, record):
195
196
197
        """
        Add a record to the folder.
        """
Paul Kienzle's avatar
Paul Kienzle committed
198
        self.children.append(record)
199
        try:
200
201
202
203
204
205
            # Record may not have a name, the name may be invalid, or it
            # may already be in use.   The noname case will be covered by
            # record.name raising an attribute error.  The others we need
            # to test for explicitly.
            if valid_identifier(record.name) and not hasattr(self, record.name):
                setattr(self, record.name, record)
206
207
        except AttributeError:
            pass
208

Paul Kienzle's avatar
Paul Kienzle committed
209
    def format(self, indent=0):
210
        parent = " "*indent+self.name
Paul Kienzle's avatar
Paul Kienzle committed
211
        children = [r.format(indent=indent+2) for r in self.children]
212
        return "\n".join([parent]+children)
Paul Kienzle's avatar
Paul Kienzle committed
213

214
215

def loads(s, **kwargs):
Paul Kienzle's avatar
Paul Kienzle committed
216
    """Load an igor file from string"""
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
    stream = _io.BytesIO(s)
    return load(stream, **kwargs)

def load(filename, **kwargs):
    """Load an igor file"""
    try:
        packed_experiment = _load(filename)
    except ValueError as e:
        if e.message.startswith('not enough data for the next record header'):
            raise IOError('invalid record header; bad pxp file?')
        elif e.message.startswith('not enough data for the next record'):
            raise IOError('final record too long; bad pxp file?')
        raise
    return _convert(packed_experiment, **kwargs)

def _convert(packed_experiment, ignore_unknown=True):
    records, filesystem = packed_experiment
234
    stack = [Folder(path=['root'])]
235
236
237
    for record in records:
        if isinstance(record, _UnknownRecord):
            if ignore_unknown:
Paul Kienzle's avatar
Paul Kienzle committed
238
239
                continue
            else:
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
                r = Unknown(record.data, type=record.header['recordType'])
        elif isinstance(record, _GetHistoryRecord):
            r = GetHistory(record.text)
        elif isinstance(record, _HistoryRecord):
            r = History(record.text)
        elif isinstance(record, _PackedFileRecord):
            r = PackedFile(record.text)
        elif isinstance(record, _ProcedureRecord):
            r = Procedure(record.text)
        elif isinstance(record, _RecreationRecord):
            r = Recreation(record.text)
        elif isinstance(record, _VariablesRecord):
            r = Variables(record)
        elif isinstance(record, _WaveRecord):
            r = Wave(record)
        else:
            r = None

        if isinstance(record, _FolderStartRecord):
            path = stack[-1].path+[record.null_terminated_text]
            folder = Folder(path)
            stack[-1].append(folder)
            stack.append(folder)
        elif isinstance(record, _FolderEndRecord):
            stack.pop()
        elif r is None:
            raise NotImplementedError(record)
        else:
            stack[-1].append(r)
Paul Kienzle's avatar
Paul Kienzle committed
269
270
271
    if len(stack) != 1:
        raise IOError("FolderStart records do not match FolderEnd records")
    return stack[0]