ftir-OMNIC-map-reader 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,409 @@
1
+ # %%
2
+ # /*##########################################################################
3
+ # Modifed by Henry Towbin June 2023 to convert OMNIC .MAP files to Xarray datasets
4
+ # This allows the code work outside of the PyMca software package and enables
5
+ # analysis of the data in a wider set of python libraries.
6
+
7
+ # The main function Load_Omnic_Map(dir_path: str) takes a path to an omnic .MAP file
8
+ # and returns an xarray dataset object with the x,y coordiantes and spectral range
9
+ # of the map as dimensions.
10
+ #
11
+ # ============================================================================
12
+ # Original License information:
13
+ # The PyMca X-Ray Fluorescence Toolkit
14
+ #
15
+ # Copyright (c) 2004-2023 European Synchrotron Radiation Facility
16
+ #
17
+ # This file is part of the PyMca X-ray Fluorescence Toolkit developed at
18
+ # the ESRF.
19
+ #
20
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
21
+ # of this software and associated documentation files (the "Software"), to deal
22
+ # in the Software without restriction, including without limitation the rights
23
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24
+ # copies of the Software, and to permit persons to whom the Software is
25
+ # furnished to do so, subject to the following conditions:
26
+ #
27
+ # The above copyright notice and this permission notice shall be included in
28
+ # all copies or substantial portions of the Software.
29
+ #
30
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
36
+ # THE SOFTWARE.
37
+ #
38
+ #############################################################################*/
39
+ # %%
40
+ __author__ = "V.A. Sole - ESRF"
41
+ __contact__ = "sole@esrf.fr"
42
+ __license__ = "MIT"
43
+ __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
44
+ import os
45
+ import sys
46
+ import re
47
+ import struct
48
+ import numpy
49
+ import copy
50
+ import logging
51
+ import xarray as xr
52
+
53
+
54
+ _logger = logging.getLogger(__name__)
55
+
56
+
57
+ class DataObject:
58
+ """
59
+ Simple container of an array and associated information.
60
+ Basically it has the members:
61
+ info: A dictionary
62
+ data: An array, usually 2D, 3D, ...
63
+
64
+ In the past also incorporated selection methods.
65
+ Now each different data source implements its selection methods.
66
+
67
+ Plotting routines may add additional members
68
+
69
+ x: A list containing arrays to be considered axes
70
+ y: A list of data to be considered as signals
71
+ m: A list containing the monitor data
72
+ """
73
+
74
+ def __init__(self):
75
+ """
76
+ Default Constructor
77
+ """
78
+ self.info = {}
79
+ self.data = numpy.array([])
80
+
81
+
82
+ class OmnicMap(DataObject):
83
+ """
84
+ Class to read OMNIC .map files
85
+
86
+ It reads the spectra into a DataObject instance.
87
+ This class info member contains all the parsed information.
88
+ This class data member contains the map itself as a 3D array.
89
+ """
90
+
91
+ def __init__(self, filename):
92
+ """
93
+ Parameters:
94
+ -----------
95
+ filename : str
96
+ Name of the .map file.
97
+ It is expected to work with OMNIC versions 7.x and 8.x
98
+ """
99
+ DataObject.__init__(self)
100
+ fid = open(filename, "rb")
101
+ data = fid.read()
102
+ fid.close()
103
+ fid = None
104
+
105
+ try:
106
+ omnicInfo = self._getOmnicInfo(data)
107
+ except Exception:
108
+ omnicInfo = None
109
+ self.sourceName = [filename]
110
+ if sys.version < "3.0":
111
+ searchedChain1 = ", XPos="
112
+ searchedChain2 = "Pixel "
113
+ else:
114
+ searchedChain1 = bytes(", XPos=", "utf-8")
115
+ searchedChain2 = bytes("Pixel ", "utf-8")
116
+
117
+ byteAnchor = data.index(searchedChain1)
118
+ firstByte = byteAnchor - (100-data[byteAnchor-100:byteAnchor+100].index(searchedChain2))
119
+ s = data[firstByte : (firstByte + 100 - 16)]
120
+ if sys.version >= "3.0":
121
+ s = str(s)
122
+ _logger.debug("firstByte = %d", firstByte)
123
+ _logger.debug("s1 = %s ", s)
124
+ exp = re.compile(r"(-?[0-9]+\.?[0-9]*)")
125
+ tmpValues = exp.findall(s)
126
+ pixelNumber = int(tmpValues[0])
127
+ # self.nSpectra = int(tmpValues[1]) # this doesnt exist in the array detector format
128
+ # if "XPos = " in s:
129
+ xPosition = float(tmpValues[1])
130
+ yPosition = float(tmpValues[2])
131
+ X_um = float(tmpValues[3])
132
+ Y_um = float(tmpValues[4])
133
+
134
+ # else:
135
+ # # I have to calculate them from the scan
136
+ # xPosition, yPosition = self.getPositionFromIndexAndInfo(0, omnicInfo)
137
+ _logger.debug(
138
+ "pixelNumber, nSpectra, xPosition, yPosition = %d %d %f %f",
139
+ pixelNumber,
140
+ # self.nSpectra,
141
+ xPosition,
142
+ yPosition,
143
+ )
144
+ if sys.version < "3.0":
145
+ chain = "Pixel"
146
+ else:
147
+ chain = bytes("Pixel", "utf-8")
148
+ secondByte = data[(firstByte + 1) :].index(chain)
149
+ secondByte += firstByte + 1
150
+ _logger.debug("secondByte = %s", secondByte)
151
+ self.nChannels = int((secondByte - firstByte - 100) / 4)
152
+ _logger.debug("nChannels = %d", self.nChannels)
153
+ self.firstSpectrumOffset = firstByte - 16
154
+
155
+ # fill the header
156
+ self.header = []
157
+ oldXPosition = xPosition
158
+ oldYPosition = yPosition
159
+ self.nRows = 0
160
+ xPosition_list = []
161
+ yPosition_list = []
162
+ x_list = []
163
+ y_list = []
164
+ pixelNumber_list = []
165
+
166
+ # xPositions = numpy.zeros(self.nSpectra)
167
+ # yPositions = numpy.zeros(self.nSpectra)
168
+ calculating = True
169
+ # offset = int(firstByte + i * (100 + self.nChannels * 4))
170
+
171
+ # for i in range(self.nSpectra):
172
+ i = 0
173
+ offset = 0
174
+ # Length of spectral data is not known
175
+ # so I have to read until the end of the file
176
+ # or until I find a problem
177
+ while offset < len(data):
178
+ try:
179
+ offset = int(firstByte + i * (100 + self.nChannels * 4))
180
+ if sys.version < "3.0":
181
+ s = data[offset : (offset + 100 - 16)]
182
+ else:
183
+ s = str(data[offset : (offset + 100 - 16)])
184
+ tmpValues = exp.findall(s)
185
+ pixelNumber = int(tmpValues[0])
186
+ if "XPos = " in s:
187
+ xPosition = float(tmpValues[1])
188
+ yPosition = float(tmpValues[2])
189
+ X_um = float(tmpValues[3])
190
+ Y_um = float(tmpValues[4])
191
+ else:
192
+ # I have to calculate them from the scan
193
+ xPosition, yPosition = self.getPositionFromIndexAndInfo(i, omnicInfo)
194
+ xPosition_list.append(xPosition)
195
+ yPosition_list.append(yPosition)
196
+ x_list.append(X_um)
197
+ y_list.append(Y_um)
198
+ pixelNumber_list.append(pixelNumber)
199
+
200
+ i += 1
201
+
202
+ if calculating:
203
+ if (abs(yPosition - oldYPosition) > 1.0e-6) and (
204
+ abs(xPosition - oldXPosition) < 1.0e-6
205
+ ):
206
+ calculating = False
207
+ continue
208
+ self.nRows += 1
209
+ except Exception:
210
+ break
211
+
212
+ self.nSpectra = i
213
+ xPositions = numpy.array(xPosition_list)
214
+ yPositions = numpy.array(yPosition_list)
215
+ x_list = numpy.array(x_list)
216
+ y_list = numpy.array(y_list)
217
+ self.xPositions = xPositions
218
+ self.yPositions = yPositions
219
+ self.x_list = x_list
220
+ self.y_list = y_list
221
+ self.pixelNumber_list = numpy.array(pixelNumber_list)
222
+
223
+ _logger.debug(
224
+ "DIMENSIONS X = %f Y=%d", self.nSpectra * 1.0 / self.nRows, self.nRows
225
+ )
226
+
227
+ # arrange as an EDF Stack
228
+ self.info = {}
229
+ self.__nFiles = int(self.nSpectra / self.nRows)
230
+ try:
231
+ deltaX = (xPositions[-1] - xPositions[0]) / (self.nRows - 1)
232
+ deltaY = (yPositions[-1] - yPositions[0]) / (self.__nFiles - 1)
233
+ except Exception:
234
+ deltaX = None
235
+ deltaY = None
236
+ _logger.warning("Cannot calculate scales")
237
+ self.data = numpy.zeros(
238
+ (self.__nFiles, self.nRows, self.nChannels), dtype=numpy.float32
239
+ )
240
+
241
+ self.__nImagesPerFile = 1
242
+ offset = firstByte - 16 + 100 # starting position of the data
243
+ delta = 100 + self.nChannels * 4
244
+ fmt = "%df" % self.nChannels
245
+ for i in range(self.__nFiles):
246
+ for j in range(self.nRows):
247
+ # this approach is inneficient when compared to a direct
248
+ # data readout, but it allows to deal with nan at the source
249
+ tmpData = numpy.zeros((self.nChannels,), dtype=numpy.float32)
250
+ tmpData[:] = struct.unpack(fmt, data[offset : (offset + delta - 100)])
251
+ finiteData = numpy.isfinite(tmpData)
252
+ self.data[i, j, finiteData] = tmpData[finiteData]
253
+ offset = int(offset + delta)
254
+ shape = self.data.shape
255
+ for i in range(len(shape)):
256
+ key = "Dim_%d" % (i + 1,)
257
+ self.info[key] = shape[i]
258
+
259
+ # self.info["SourceType"] = SOURCE_TYPE
260
+ self.info["SourceName"] = self.sourceName
261
+ self.info["Size"] = self.__nFiles * self.__nImagesPerFile
262
+ self.info["NumberOfFiles"] = self.__nFiles * 1
263
+ self.info["FileIndex"] = 0
264
+ self.info["Channel0"] = 0.0
265
+ if omnicInfo is not None:
266
+ self.info["McaCalib"] = [
267
+ omnicInfo["First X value"] * 1.0,
268
+ omnicInfo["Data spacing"] * 1.0,
269
+ 0.0,
270
+ ]
271
+ else:
272
+ self.info["McaCalib"] = [0.0, 1.0, 0.0]
273
+ self.info["OmnicInfo"] = omnicInfo
274
+ if deltaX and deltaY:
275
+ if (deltaX > 0.0) and (deltaY > 0.0):
276
+ self.info["xScale"] = [oldXPosition, deltaX]
277
+ self.info["yScale"] = [oldYPosition, deltaY]
278
+ self.info["positioners"] = {"X": xPositions, "Y": yPositions}
279
+
280
+ def _getOmnicInfo(self, data):
281
+ """
282
+ Parameters:
283
+ -----------
284
+ data : The contents of the .map file
285
+
286
+ Returns:
287
+ --------
288
+ A dictionary with acquisition information
289
+ """
290
+ # additional information
291
+ fmt = "I" # unsigned long in 32-bit
292
+ offset = 372 # 93*4 unsigned integers
293
+ infoBlockIndex = (
294
+ struct.unpack(fmt, data[offset : (offset + 4)])[0] - 204
295
+ ) / 4.0
296
+ infoBlockIndex = int(infoBlockIndex)
297
+ # infoblock is the position of the information block
298
+ offset = infoBlockIndex * 4
299
+ # read 13 unsigned integers
300
+ nValues = 13
301
+ fmt = "%dI" % nValues
302
+ values = struct.unpack(fmt, data[offset : (offset + 4 * nValues)])
303
+ ddict = {}
304
+ ddict["Number of points"] = values[0]
305
+ ddict["Number of scan points"] = values[6]
306
+ ddict["Interferogram peak position"] = values[7]
307
+ ddict["Number of sample scans"] = values[8]
308
+ ddict["Number of FFT points"] = values[10]
309
+ ddict["Number of background scans"] = values[12]
310
+ offset = (infoBlockIndex + 3) * 4
311
+ nFloats = 47
312
+ fmt = "%df" % nFloats
313
+ vFloats = struct.unpack(fmt, data[offset : (offset + 4 * nFloats)])
314
+ lastX = vFloats[0]
315
+ firstX = vFloats[1]
316
+ ddict["First X value"] = firstX
317
+ ddict["Last X value"] = lastX
318
+ ddict["Identifier for start indices of spectra"] = vFloats[14]
319
+ ddict["Laser frequency"] = vFloats[16]
320
+ ddict["Data spacing"] = (lastX - firstX) / (ddict["Number of points"] - 1.0)
321
+ ddict["Background gain"] = vFloats[10]
322
+ for key in ddict.keys():
323
+ _logger.debug("%s: %s", key, ddict[key])
324
+ ddict.update(self.getMapInformation(data))
325
+ return ddict
326
+
327
+ def getMapInformation(self, data):
328
+ """
329
+ Internal method to help finding spectra coordinates
330
+ Parameters:
331
+ -----------
332
+ data : Contents of the .map file
333
+
334
+ Returns:
335
+ --------
336
+ Dictionary with map gemoetrical acquisition parameters
337
+ """
338
+ # look for the chain 'Position'
339
+ if sys.version < "3.0":
340
+ chain = "Position"
341
+ else:
342
+ chain = bytes("Position", "utf-8")
343
+ offset = data.index(chain)
344
+ positions = [offset]
345
+ while True:
346
+ try:
347
+ a = data[(offset + 1) :].index(chain)
348
+ offset = a + offset + 1
349
+ positions.append(offset)
350
+ except ValueError:
351
+ break
352
+
353
+ ddict = {}
354
+ # map description position
355
+ if (positions[1] - positions[0]) == 66: # reverse engineered magic number :-)
356
+ mapDescriptionOffset = positions[0] - 90
357
+ mapDescription = struct.unpack(
358
+ "6f", data[mapDescriptionOffset : mapDescriptionOffset + 24]
359
+ )
360
+ y0, y1, deltaY, x0, x1, deltaX = mapDescription
361
+ ddict["First map location"] = [x0, y0]
362
+ ddict["Last map location"] = [x1, y1]
363
+ ddict["Mapping stage X step size"] = deltaX
364
+ ddict["Mapping stage Y step size"] = deltaY
365
+ ddict["Number of spectra"] = abs(
366
+ (1 + ((y1 - y0) / deltaY)) * (1 + ((x1 - x0) / deltaX))
367
+ )
368
+ for key in ddict.keys():
369
+ _logger.debug("%s: %s", key, ddict[key])
370
+ return ddict
371
+
372
+ def getOmnicInfo(self):
373
+ """
374
+ Returns a dictionary with the parsed OMNIC information
375
+ """
376
+ return copy.deepcopy(self.info["OmnicInfo"])
377
+
378
+ def getPositionFromIndexAndInfo(self, index, info=None):
379
+ """
380
+ Internal method to obtain the position at which a spectrum
381
+ was acquired
382
+ Parameters:
383
+ -----------
384
+ index : int
385
+ Index of spectrum
386
+ info : Dictionary
387
+ Information recovered with _getOmnicInfo
388
+ Returns:
389
+ --------
390
+ x, y : floats
391
+ Position at which the spectrum was acquired.
392
+ """
393
+ if info is None:
394
+ return 0.0, 0.0
395
+ ddict = info
396
+ # first variation on X and then on Y
397
+ try:
398
+ x0, y0 = ddict["First map location"]
399
+ except KeyError:
400
+ return 0.0, 0.0
401
+ x1, y1 = ddict["Last map location"]
402
+ deltaX = ddict["Mapping stage X step size"]
403
+ deltaY = ddict["Mapping stage Y step size"]
404
+ nX = int(1.5 + ((x1 - x0) / deltaX))
405
+ nY = int(1.5 + ((y1 - y0) / deltaY))
406
+ x = x0 + (index % nX) * deltaX
407
+ y = y0 + int(index / nX) * deltaY
408
+
409
+ return x, y
@@ -0,0 +1,97 @@
1
+ #%%
2
+ import numpy
3
+ import xarray as xr
4
+ # try:
5
+ # from .PyMca_Array_OmnicMap import OmnicArrayMap
6
+ # from .PyMca_OmnicMap import OmnicMap
7
+ # except ImportError:
8
+ # from PyMca_Array_OmnicMap import OmnicArrayMap
9
+ # from PyMca_OmnicMap import OmnicMap
10
+
11
+ try:
12
+ from . import PyMca_Array_OmnicMap
13
+ from . import PyMca_OmnicMap
14
+ except ImportError:
15
+ import PyMca_Array_OmnicMap
16
+ import PyMca_OmnicMap
17
+
18
+ #%%
19
+ def Load_Omnic_Map(dir_path: str, format: str = "Xarray-Dataset") -> xr.Dataset:
20
+ """_summary_
21
+
22
+ Args:
23
+ dir_path (str): _description_
24
+ format (str, optional): _description_. Defaults to "Xarray-Dataset". Other Options are "Xarray-DataArray" and "Dictionary".
25
+
26
+ Raises:
27
+ ValueError: Errors if the format is not supported.
28
+
29
+ Returns:
30
+ xr.Dataset: Dataset containing the spectral data, coordinates, and metadata.
31
+ xr.DataArray: DataArray containing the spectral data.
32
+ dict: Dictionary containing the spectral data, coordinates, and metadata.
33
+ """
34
+ try:
35
+ mapfile = PyMca_OmnicMap.OmnicMap(dir_path)
36
+ except Exception as e:
37
+ print(f"Error loading map file: {e}")
38
+ print("Trying to load as OmnicArrayMap...")
39
+ try:
40
+ mapfile = PyMca_Array_OmnicMap.OmnicArrayMap(dir_path)
41
+ print("Loaded as OmnicArrayMap.")
42
+ except Exception as e:
43
+ print(f"Error loading map file as OmnicArrayMap: {e}")
44
+ print("Unable to load the file.")
45
+ return None
46
+
47
+ wn0 = mapfile.info["OmnicInfo"]['First X value'] # cm^-1
48
+ wn1 = mapfile.info["OmnicInfo"]['Last X value'] # cm^-1
49
+
50
+ unique_x = numpy.unique(mapfile.info["positioners"]["X"])
51
+ unique_y = numpy.unique(mapfile.info["positioners"]["Y"])
52
+
53
+ x_coords = unique_x - unique_x[0] #µm
54
+ y_coords = unique_y - unique_y[0] #µm
55
+
56
+ len_wn = mapfile.data.shape[2]
57
+ wn = numpy.linspace(wn0, wn1, len_wn) #cm^-1
58
+
59
+ unit_names = {"x": "um", "y": "um", "wn": "cm^-1", "data": "absorbance"}
60
+ unit_long_names = {"x": "microns", "y": "microns", "wn": "wavenumbers", "data": "absorbance"}
61
+ metadata = {"unit_names": unit_names, "unit_long_names": unit_long_names, **mapfile.info}
62
+
63
+ # Reshape the data to match the unique x and y coordinates
64
+ # This assumes that the data is structured in a way that corresponds to the unique x and y coordinates.
65
+ lenX = len(unique_x)
66
+ lenY = len(unique_y)
67
+ reshaped = mapfile.data.reshape(lenY, lenX, len_wn)
68
+ data = {"spectra": (["y", "x", "wn"], reshaped)}
69
+
70
+
71
+ coords = {
72
+ "x": x_coords,
73
+ "y": y_coords,
74
+ "wn": wn,
75
+ }
76
+
77
+ dataset = xr.Dataset(
78
+ data_vars= data,
79
+ coords= coords,
80
+ attrs= metadata
81
+ )
82
+ if format == "Xarray-Dataset":
83
+ return dataset
84
+ elif format == "Xarray-DataArray":
85
+ return dataset["spectra"]
86
+ elif format == "Dictionary":
87
+ return {
88
+ "data": dataset["spectra"].values,
89
+ "x": dataset["x"].values,
90
+ "y": dataset["y"].values,
91
+ "wn": dataset["wn"].values,
92
+ "metadata": metadata
93
+ }
94
+ else:
95
+ raise ValueError(f"Unsupported format: {format}. Supported formats are 'Xarray-Dataset', 'Xarray-DataArray', and 'Dictionary'.")
96
+
97
+ # %%