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