Move 'Reading IGOR...' post to 'igor' and link to igor.git.
[blog.git] / posts / igor.mdwn
1 [[!meta  title="Reading IGOR files from Python"]]
2
3 This is the home page for the `igor` package, [[Python]] modules for
4 reading files written by [WaveMetrics][] IGOR Pro.  Note that if
5 you're designing a system, [[HDF5]] is almost certainly a better
6 choice for your data file format than IBW or PXP.  This package exists
7 for those of you who's data is already stuck in an IGOR format.
8
9 History
10 -------
11
12 When I joined Prof. Yang's lab, there was a good deal of data analysis
13 code written in IGOR, and a bunch of old data saved in IGOR binary
14 wave (IBW) and packed experiment (PXP) files.  I don't use MS Windows,
15 so I don't run IGOR, but I still needed a way to get at the data.
16 Luckily, the [WaveMetrics][] folks publish [some useful notes][TN]
17 which explain the fundamentals of these two file formats ([TN003][]
18 for IBW and [PTN003][] for PXP).  The file formats are in a goofy
19 format, but [strings][] pulls out enough meat to figure out what's
20 going on.
21
22 For a while I used a IBW → ASCII reader that I coded up in [[C]], but
23 when I joined the [[Hooke]] project during the winter of 2009–2010, I
24 translated the reader into [[Python]] to support the drivers for data
25 from Asylum Research's [MFP-*][MFP-1D] and related microscopes.  This
26 scratched my itch for a few years.
27
28 Fast forward to 2012, and for the first time I needed to extract data
29 from a PXP file.  Since my Python code only supported IBW's, I
30 searched around and found [igor.py][] by Paul Kienzle Merlijn van
31 Deen.  They had a PXP reader, but no reader for stand-alone IBW files.
32 I decided to merge the two projects, so I split my reader out of the
33 Hooke repository and hacked up the [[Git]] repository referenced
34 above.  Now it's easy to get a hold of all that useful metadata in a
35 hurry.  No writing ability yet, but I don't know why you'd want to
36 move data that direction anyway ;).
37
38 Parsing dynamic structures with Python
39 --------------------------------------
40
41 The IGOR file formats rely on lots of shenanigans with C `struct`s.
42 To meld all the structures together in a natural way, I've extended
43 Python's standard [struct][] library to support arbitrary nesting and
44 dynamic fields.  Take a look at [igor.struct][struct.py] for some
45 examples.  This framework makes it easy to load data from structures
46 like:
47
48     struct vector {
49       unsigned int length;
50       short data[length];
51     };
52
53 With the standard `struct` module, you'd read this using the
54 functional approach:
55
56     >>> import struct
57     >>> buffer = b'\x00\x00\x00\x02\x01\x02\x03\x04'
58     >>> length_struct = struct.Struct('>I')
59     >>> length = length_struct.unpack_from(buffer)[0]
60     >>> data = struct.unpack_from('>' + 'h'*length, buffer, length_struct.size)
61     >>> print(data)
62     (258, 772)
63
64 This obviously works, but keeping track of the offsets, byte ordering,
65 etc. can be tedious.  My `igor.struct` package allows you to use a
66 more object oriented approach:
67
68     >>> from pprint import pprint
69     >>> from igor.struct import Field, DynamicField, DynamicStructure
70     >>> class DynamicLengthField (DynamicField):
71     ...     def pre_pack(self, parents, data):
72     ...         "Set the 'length' value to match the data before packing"
73     ...         vector_structure = parents[-1]
74     ...         vector_data = self._get_structure_data(
75     ...             parents, data, vector_structure)
76     ...         length = len(vector_data['data'])
77     ...         vector_data['length'] = length
78     ...         data_field = vector_structure.get_field('data')
79     ...         data_field.count = length
80     ...         data_field.setup()
81     ...     def post_unpack(self, parents, data):
82     ...         "Adjust the expected data count to match the 'length' value"
83     ...         vector_structure = parents[-1]
84     ...         vector_data = self._get_structure_data(
85     ...             parents, data, vector_structure)
86     ...         length = vector_data['length']
87     ...         data_field = vector_structure.get_field('data')
88     ...         data_field.count = length
89     ...         data_field.setup()
90     >>> dynamic_length_vector = DynamicStructure('vector',
91     ...     fields=[
92     ...         DynamicLengthField('I', 'length'),
93     ...         Field('h', 'data', count=0, array=True),
94     ...         ],
95     ...     byte_order='>')
96     >>> vector = dynamic_length_vector.unpack(buffer)
97     >>> pprint(vector)
98     {'data': array([258, 772]), 'length': 2}
99
100 While this is overkill for such a simple example, it scales much more
101 cleanly than an approach using the standard `struct` module.  The main
102 benefit is that you can use `Structure` instances as format specifiers
103 for `Field` instances.  This means that you could specify a C
104 structure like:
105
106     struct vectors {
107       unsigned int length;
108       struct vector data[length];
109     };
110
111 With:
112
113     >>> dynamic_length_vectors = DynamicStructure('vectors',
114     ...     fields=[
115     ...         DynamicLengthField('I', 'length'),
116     ...         Field(dynamic_length_vector, 'data', count=0, array=True),
117     ...         ],
118     ...     byte_order='>')
119
120 The C code your mimicking probably only uses a handful of dynamic
121 approaches.  Once you've written classes to handle each of them, it is
122 easy to translate arbitrarily complex nested C structures into Python
123 representations.
124
125 The pre-pack and post-unpack hooks also give you a convenient place to
126 translate between some C struct's funky format and Python's native
127 types.  You take care off all that when you define the structure, and
128 then any part of your software that uses the structure gets the native
129 version automatically.
130
131
132 [WaveMetrics]: http://www.wavemetrics.com/
133 [TN]: ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/
134 [TN003]: ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN003.zip
135 [PTN003]: ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/PTN003.zip
136 [strings]: http://www.gnu.org/software/binutils/
137 [MFP-1D]: http://www.asylumresearch.com/Products/Mfp1D/Mfp1D.shtml
138 [igor.py]: http://pypi.python.org/pypi/igor.py
139 [struct]: http://docs.python.org/library/struct.html
140 [struct.py]: http://git.tremily.us/?p=igor.git;a=blob;f=igor/struct.py;hb=HEAD
141
142 [[!tag tags/programming]]