igorpy.py 9.54 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
import sys as _sys
20

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

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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'
38

Paul Kienzle's avatar
Paul Kienzle committed
39

40
41
42
43
44
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'))
45
PYID = _re.compile(r"^[^\d\W]\w*$", _re.UNICODE)
46
47
48
49
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
50

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

55
class Variables(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
56
57
58
    """
    Contains system numeric variables (e.g., K0) and user numeric and string variables.
    """
59
60
61
62
63
64
65
    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
66
67
68
69
70
71
    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))

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

81
class Wave(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
82
83
84
    """
    Contains the data for a wave
    """
85
86
87
88
89
90
91
92
93
94
95
96
97
    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
98
        else:
99
100
101
102
103
104
105
106
107
108
109
110
111
            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
112
113
114
115
116
117
    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)
118

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

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

class Recreation(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
125
126
127
    """
    Contains the experiment's recreation procedures as plain text.
    """
128
129
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
130
131
    def format(self, indent=0):
        return " "*indent + "<Recreation>"
132
class Procedure(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
133
134
135
    """
    Contains the experiment's main procedure window text as plain text.
    """
136
137
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
138
139
    def format(self, indent=0):
        return " "*indent + "<Procedure>"
140
class GetHistory(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
141
142
143
144
145
146
147
148
    """
    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.
    """
149
150
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
151
152
    def format(self, indent=0):
        return " "*indent + "<GetHistory>"
153
class PackedFile(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
154
155
156
    """
    Contains the data for a procedure file or notebook in packed form.
    """
157
158
    def __init__(self, data):
        self.data = data
Paul Kienzle's avatar
Paul Kienzle committed
159
160
    def format(self, indent=0):
        return " "*indent + "<PackedFile>"
161
class Unknown(IgorObject):
Paul Kienzle's avatar
Paul Kienzle committed
162
163
164
    """
    Record type not documented in PTN003/TN003.
    """
165
    def __init__(self, data, type):
Paul Kienzle's avatar
Paul Kienzle committed
166
167
168
169
170
171
        self.data = data
        self.type = type
    def format(self, indent=0):
        return " "*indent + "<Unknown type %s>"%self.type


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

Paul Kienzle's avatar
Paul Kienzle committed
181
182
183
184
185
186
187
188
    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)
189

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

193
    __repr__ = __str__
194

Paul Kienzle's avatar
Paul Kienzle committed
195
    def append(self, record):
196
197
198
        """
        Add a record to the folder.
        """
Paul Kienzle's avatar
Paul Kienzle committed
199
        self.children.append(record)
200
        try:
201
202
203
204
205
206
            # 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)
207
208
        except AttributeError:
            pass
209

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

215
216

def loads(s, **kwargs):
Paul Kienzle's avatar
Paul Kienzle committed
217
    """Load an igor file from string"""
218
219
220
221
222
223
224
225
    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:
226
        if e.args[0].startswith('not enough data for the next record header'):
227
            raise IOError('invalid record header; bad pxp file?')
228
        elif e.args[0].startswith('not enough data for the next record'):
229
230
231
232
233
234
            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
235
    stack = [Folder(path=['root'])]
236
237
238
    for record in records:
        if isinstance(record, _UnknownRecord):
            if ignore_unknown:
Paul Kienzle's avatar
Paul Kienzle committed
239
240
                continue
            else:
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
269
                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
270
271
272
    if len(stack) != 1:
        raise IOError("FolderStart records do not match FolderEnd records")
    return stack[0]