pychemstation 0.4.7.dev1__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.
Files changed (109) hide show
  1. ag_hplc_macro/__init__.py +3 -0
  2. ag_hplc_macro/analysis/__init__.py +1 -0
  3. ag_hplc_macro/analysis/base_spectrum.py +509 -0
  4. ag_hplc_macro/analysis/spec_utils.py +304 -0
  5. ag_hplc_macro/analysis/utils.py +63 -0
  6. ag_hplc_macro/control/__init__.py +5 -0
  7. ag_hplc_macro/control/chromatogram.py +128 -0
  8. ag_hplc_macro/control/hplc.py +673 -0
  9. ag_hplc_macro/generated/__init__.py +56 -0
  10. ag_hplc_macro/generated/dad_method.py +367 -0
  11. ag_hplc_macro/generated/pump_method.py +519 -0
  12. ag_hplc_macro/utils/__init__.py +2 -0
  13. ag_hplc_macro/utils/chemstation.py +290 -0
  14. ag_hplc_macro/utils/constants.py +15 -0
  15. ag_hplc_macro/utils/hplc_param_types.py +185 -0
  16. hein-analytical-control/__init__.py +3 -0
  17. hein-analytical-control/analysis/__init__.py +1 -0
  18. hein-analytical-control/analysis/base_spectrum.py +509 -0
  19. hein-analytical-control/analysis/spec_utils.py +304 -0
  20. hein-analytical-control/analysis/utils.py +63 -0
  21. hein-analytical-control/devices/Agilent/__init__.py +3 -0
  22. hein-analytical-control/devices/Agilent/chemstation.py +290 -0
  23. hein-analytical-control/devices/Agilent/chromatogram.py +129 -0
  24. hein-analytical-control/devices/Agilent/hplc.py +436 -0
  25. hein-analytical-control/devices/Agilent/hplc_param_types.py +141 -0
  26. hein-analytical-control/devices/Magritek/Spinsolve/__init__.py +0 -0
  27. hein-analytical-control/devices/Magritek/Spinsolve/commands.py +495 -0
  28. hein-analytical-control/devices/Magritek/Spinsolve/spectrum.py +822 -0
  29. hein-analytical-control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
  30. hein-analytical-control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
  31. hein-analytical-control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
  32. hein-analytical-control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
  33. hein-analytical-control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
  34. hein-analytical-control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
  35. hein-analytical-control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
  36. hein-analytical-control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
  37. hein-analytical-control/devices/Magritek/__init__.py +0 -0
  38. hein-analytical-control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
  39. hein-analytical-control/devices/OceanOptics/IR/__init__.py +0 -0
  40. hein-analytical-control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
  41. hein-analytical-control/devices/OceanOptics/Raman/__init__.py +0 -0
  42. hein-analytical-control/devices/OceanOptics/Raman/raman_control.py +46 -0
  43. hein-analytical-control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
  44. hein-analytical-control/devices/OceanOptics/UV/QEPro2192.py +90 -0
  45. hein-analytical-control/devices/OceanOptics/UV/__init__.py +0 -0
  46. hein-analytical-control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
  47. hein-analytical-control/devices/OceanOptics/__init__.py +0 -0
  48. hein-analytical-control/devices/OceanOptics/oceanoptics.py +115 -0
  49. hein-analytical-control/devices/__init__.py +15 -0
  50. hein-analytical-control/generated/__init__.py +56 -0
  51. hein-analytical-control/generated/dad_method.py +367 -0
  52. hein-analytical-control/generated/pump_method.py +519 -0
  53. hein_analytical_control/__init__.py +3 -0
  54. hein_analytical_control/analysis/__init__.py +1 -0
  55. hein_analytical_control/analysis/base_spectrum.py +509 -0
  56. hein_analytical_control/analysis/spec_utils.py +304 -0
  57. hein_analytical_control/analysis/utils.py +63 -0
  58. hein_analytical_control/devices/Agilent/__init__.py +3 -0
  59. hein_analytical_control/devices/Agilent/chemstation.py +290 -0
  60. hein_analytical_control/devices/Agilent/chromatogram.py +129 -0
  61. hein_analytical_control/devices/Agilent/hplc.py +436 -0
  62. hein_analytical_control/devices/Agilent/hplc_param_types.py +141 -0
  63. hein_analytical_control/devices/Magritek/Spinsolve/__init__.py +0 -0
  64. hein_analytical_control/devices/Magritek/Spinsolve/commands.py +495 -0
  65. hein_analytical_control/devices/Magritek/Spinsolve/spectrum.py +822 -0
  66. hein_analytical_control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
  67. hein_analytical_control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
  68. hein_analytical_control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
  69. hein_analytical_control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
  70. hein_analytical_control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
  71. hein_analytical_control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
  72. hein_analytical_control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
  73. hein_analytical_control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
  74. hein_analytical_control/devices/Magritek/__init__.py +0 -0
  75. hein_analytical_control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
  76. hein_analytical_control/devices/OceanOptics/IR/__init__.py +0 -0
  77. hein_analytical_control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
  78. hein_analytical_control/devices/OceanOptics/Raman/__init__.py +0 -0
  79. hein_analytical_control/devices/OceanOptics/Raman/raman_control.py +46 -0
  80. hein_analytical_control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
  81. hein_analytical_control/devices/OceanOptics/UV/QEPro2192.py +90 -0
  82. hein_analytical_control/devices/OceanOptics/UV/__init__.py +0 -0
  83. hein_analytical_control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
  84. hein_analytical_control/devices/OceanOptics/__init__.py +0 -0
  85. hein_analytical_control/devices/OceanOptics/oceanoptics.py +115 -0
  86. hein_analytical_control/devices/__init__.py +15 -0
  87. hein_analytical_control/generated/__init__.py +56 -0
  88. hein_analytical_control/generated/dad_method.py +367 -0
  89. hein_analytical_control/generated/pump_method.py +519 -0
  90. pychemstation/__init__.py +3 -0
  91. pychemstation/analysis/__init__.py +1 -0
  92. pychemstation/analysis/base_spectrum.py +509 -0
  93. pychemstation/analysis/spec_utils.py +304 -0
  94. pychemstation/analysis/utils.py +63 -0
  95. pychemstation/control/__init__.py +5 -0
  96. pychemstation/control/chromatogram.py +128 -0
  97. pychemstation/control/hplc.py +673 -0
  98. pychemstation/generated/__init__.py +56 -0
  99. pychemstation/generated/dad_method.py +367 -0
  100. pychemstation/generated/pump_method.py +519 -0
  101. pychemstation/utils/__init__.py +2 -0
  102. pychemstation/utils/chemstation.py +290 -0
  103. pychemstation/utils/constants.py +15 -0
  104. pychemstation/utils/hplc_param_types.py +185 -0
  105. pychemstation-0.4.7.dev1.dist-info/LICENSE +395 -0
  106. pychemstation-0.4.7.dev1.dist-info/METADATA +102 -0
  107. pychemstation-0.4.7.dev1.dist-info/RECORD +109 -0
  108. pychemstation-0.4.7.dev1.dist-info/WHEEL +5 -0
  109. pychemstation-0.4.7.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,304 @@
1
+ """
2
+ Module contains various utility function for spectral data processing and
3
+ analysis.
4
+ """
5
+
6
+ import numpy as np
7
+ import scipy
8
+
9
+ from .utils import find_nearest_value_index
10
+
11
+
12
+ def create_binary_peak_map(data):
13
+ """Return binary map of the peaks within data points.
14
+
15
+ True values are assigned to potential peak points, False - to baseline.
16
+
17
+ Args:
18
+ data (:obj:np.array): 1D array with data points.
19
+
20
+ Returns:
21
+ :obj:np.array, dtype=bool: Mapping of data points, where True is
22
+ potential peak region point, False - baseline.
23
+ """
24
+ # copying array
25
+ data_c = np.copy(data)
26
+
27
+ # placeholder for the peak mapping
28
+ peak_map = np.full_like(data_c, False, dtype=bool)
29
+
30
+ for _ in range(100500): # shouldn't take more iterations
31
+
32
+ # looking for peaks
33
+ peaks_found = np.logical_or(
34
+ data_c > np.mean(data_c) + np.std(data_c) * 3,
35
+ data_c < np.mean(data_c) - np.std(data_c) * 3,
36
+ )
37
+
38
+ # merging with peak mapping
39
+ np.logical_or(peak_map, peaks_found, out=peak_map)
40
+
41
+ # if no peaks found - break
42
+ if not peaks_found.any():
43
+ break
44
+
45
+ # setting values to 0 and iterating again
46
+ data_c[peaks_found] = 0
47
+
48
+ return peak_map
49
+
50
+
51
+ def combine_map_to_regions(mapping):
52
+ """Combine True values into their indexes arrays.
53
+
54
+ Args:
55
+ mapping (:obj:np.array): Boolean mapping array to extract the indexes
56
+ from.
57
+
58
+ Returns:
59
+ :obj:np.array: 2D array with left and right borders of regions, where
60
+ mapping is True.
61
+
62
+ Example:
63
+ >>> combine_map_to_regions(np.array([True, True, False, True, False]))
64
+ array([[0, 1],
65
+ [3, 3]])
66
+ """
67
+
68
+ # No peaks identified, i.e. mapping is all False
69
+ if not mapping.any():
70
+ return np.array([], dtype="int64")
71
+
72
+ # region borders
73
+ region_borders = np.diff(mapping)
74
+
75
+ # corresponding indexes
76
+ border_indexes = np.argwhere(region_borders)
77
+
78
+ lefts = border_indexes[::2] + 1 # because diff was used to get the index
79
+
80
+ # edge case, where first peak doesn't have left border
81
+ if mapping[border_indexes][0]:
82
+ # just preppend 0 as first left border
83
+ # mind the vstack, as np.argwhere produces a vector array
84
+ lefts = np.vstack((0, lefts))
85
+
86
+ rights = border_indexes[1::2]
87
+
88
+ # another edge case, where last peak doesn't have a right border
89
+ if mapping[-1]: # True if last point identified as potential peak
90
+ # just append -1 as last peak right border
91
+ rights = np.vstack((rights, -1))
92
+
93
+ # columns as borders, rows as regions, i.e.
94
+ # :output:[0] -> first peak region
95
+ return np.hstack((lefts, rights))
96
+
97
+
98
+ def filter_regions(x_data, peaks_regions):
99
+ """Filter peak regions.
100
+
101
+ Peak regions are filtered to remove potential false positives (e.g. noise
102
+ spikes).
103
+
104
+ Args:
105
+ x_data (:obj:np.array): X data points, needed to pick up the data
106
+ resolution and map the region indexes to the corresponding data
107
+ points.
108
+ y_data (:obj:np.array): Y data points, needed to validate if the peaks
109
+ are actually present in the region and remove invalid regions.
110
+ peaks_regions (:obj:np.array): 2D Nx2 array with peak regions indexes
111
+ (rows) as left and right borders (columns).
112
+
113
+ Returns:
114
+ :obj:np.array: 2D Mx2 array with filtered peak regions indexes(rows) as
115
+ left and right borders (columns).
116
+ """
117
+
118
+ # filter peaks where region is smaller than spectrum resolution
119
+ # like single spikes, e.g. noise
120
+ # compute the regions first
121
+ x_data_regions = np.copy(x_data[peaks_regions])
122
+
123
+ # get arguments where absolute difference is greater than data resolution
124
+ resolution = np.absolute(np.mean(np.diff(x_data)))
125
+
126
+ # (N, 1) array!
127
+ valid_regions_map = np.absolute(np.diff(x_data_regions)) > resolution
128
+
129
+ # get their indexes, mind the flattening of all arrays!
130
+ valid_regions_indexes = np.argwhere(valid_regions_map.flatten()).flatten()
131
+
132
+ # filtering!
133
+ peaks_regions = peaks_regions[valid_regions_indexes]
134
+
135
+ return peaks_regions
136
+
137
+
138
+ def filter_noisy_regions(y_data, peaks_regions):
139
+ """Remove noisy regions from given regions array.
140
+
141
+ Peak regions are filtered to remove false positive noise regions, e.g.
142
+ incorrectly assigned due to curvy baseline. Filtering is performed by
143
+ computing average peak points/data points ratio.
144
+
145
+ Args:
146
+ y_data (:obj:np.array): Y data points, needed to validate if the peaks
147
+ are actually present in the region and remove invalid regions.
148
+ peaks_regions (:obj:np.array): 2D Nx2 array with peak regions indexes
149
+ (rows) as left and right borders (columns).
150
+
151
+ Returns:
152
+ :obj:np.array: 2D Mx2 array with filtered peak regions indexes(rows) as
153
+ left and right borders (columns).
154
+ """
155
+
156
+ # compute the actual regions data points
157
+ y_data_regions = []
158
+ for region in peaks_regions:
159
+ y_data_regions.append(y_data[region[0] : region[-1]])
160
+
161
+ # compute noise data regions, i.e. in between peak regions
162
+ noise_data_regions = []
163
+ for row, _ in enumerate(peaks_regions):
164
+ try:
165
+ noise_data_regions.append(
166
+ y_data[peaks_regions[row][1] : peaks_regions[row + 1][0]]
167
+ )
168
+ except IndexError:
169
+ # exception for the last row -> discard
170
+ pass
171
+
172
+ # compute average peaks/data points ratio for noisy regions
173
+ noise_peaks_ratio = []
174
+ for region in noise_data_regions:
175
+ # protection from empty regions
176
+ if region.size != 0:
177
+ # minimum height is pretty low to ensure enough noise is picked
178
+ peaks, _ = scipy.signal.find_peaks(region, height=region.max() * 0.2)
179
+ noise_peaks_ratio.append(peaks.size / region.size)
180
+
181
+ # compute average with weights equal to the region length
182
+ noise_peaks_ratio = np.average(
183
+ noise_peaks_ratio, weights=[region.size for region in noise_data_regions]
184
+ )
185
+
186
+ # filtering!
187
+ valid_regions_indexes = []
188
+ for row, region in enumerate(y_data_regions):
189
+ peaks, _ = scipy.signal.find_peaks(region, height=region.max() * 0.2)
190
+ if peaks.size != 0 and peaks.size / region.size < noise_peaks_ratio:
191
+ valid_regions_indexes.append(row)
192
+
193
+ # protecting from complete cleaning
194
+ if not valid_regions_indexes:
195
+ return peaks_regions
196
+
197
+ peaks_regions = peaks_regions[np.array(valid_regions_indexes)]
198
+
199
+ return peaks_regions
200
+
201
+
202
+ def merge_regions(x_data, peaks_regions, d_merge, recursively=True):
203
+ """Merge peak regions if distance between is less than delta.
204
+
205
+ Args:
206
+ x_data (:obj:np.array): X data points.
207
+ peaks_regions (:obj:np.array): 2D Nx2 array with peak regions indexes
208
+ (rows) as left and right borders (columns).
209
+ d_merge (float): Minimum distance in X data points to merge two or more
210
+ regions together.
211
+ recursively (bool, optional): If True - will repeat the procedure until
212
+ all regions with distance < than d_merge will merge.
213
+
214
+ Returns:
215
+ :obj:np.array: 2D Mx2 array with peak regions indexes (rows) as left and
216
+ right borders (columns), merged according to predefined minimal
217
+ distance.
218
+
219
+ Example:
220
+ >>> regions = np.array([
221
+ [1, 10],
222
+ [11, 20],
223
+ [25, 45],
224
+ [50, 75],
225
+ [100, 120],
226
+ [122, 134]
227
+ ])
228
+ >>> data = np.ones_like(regions) # ones as example
229
+ >>> merge_regions(data, regions, 1)
230
+ array([[ 1, 20],
231
+ [ 25, 45],
232
+ [ 50, 75],
233
+ [100, 120],
234
+ [122, 134]])
235
+ >>> merge_regions(data, regions, 20, True)
236
+ array([[ 1, 75],
237
+ [100, 134]])
238
+ """
239
+ # the code is pretty ugly but works
240
+ merged_regions = []
241
+
242
+ # converting to list to drop the data of the fly
243
+ regions = peaks_regions.tolist()
244
+
245
+ for i, _ in enumerate(regions):
246
+ try:
247
+ # check left border of i regions with right of i+1
248
+ if abs(x_data[regions[i][-1]] - x_data[regions[i + 1][0]]) <= d_merge:
249
+ # if lower append merge the regions
250
+ merged_regions.append([regions[i][0], regions[i + 1][-1]])
251
+ # drop the merged one
252
+ regions.pop(i + 1)
253
+ else:
254
+ # if nothing to merge, just append the current region
255
+ merged_regions.append(regions[i])
256
+ except IndexError:
257
+ # last row
258
+ merged_regions.append(regions[i])
259
+
260
+ merged_regions = np.array(merged_regions)
261
+
262
+ if not recursively:
263
+ return merged_regions
264
+
265
+ # if recursively, check for the difference
266
+ if (merged_regions == regions).all():
267
+ # done
268
+ return merged_regions
269
+
270
+ return merge_regions(x_data, merged_regions, d_merge, recursively=True)
271
+
272
+
273
+ def expand_regions(x_data, peaks_regions, d_expand):
274
+ """Expand the peak regions by the desired value.
275
+
276
+ Args:
277
+ x_data (:obj:np.array): X data points.
278
+ peaks_regions (:obj:np.array): 2D Nx2 array with peak regions indexes
279
+ (rows) as left and right borders (columns).
280
+ d_expand (float): Value to expand borders to (in X data scale).
281
+
282
+ Returns:
283
+ :obj:np.array: 2D Nx2 array with expanded peak regions indexes (rows) as
284
+ left and right borders (columns).
285
+ """
286
+
287
+ data_regions = np.copy(x_data[peaks_regions])
288
+
289
+ # determine scale orientation, i.e. decreasing (e.g. ppm on NMR spectrum)
290
+ # or increasing (e.g. wavelength on UV spectrum)
291
+ if (data_regions[:, 0] - data_regions[:, 1]).mean() > 0:
292
+ # ppm-like scale
293
+ data_regions[:, 0] += d_expand
294
+ data_regions[:, -1] -= d_expand
295
+ else:
296
+ # wavelength-like scale
297
+ data_regions[:, 0] -= d_expand
298
+ data_regions[:, -1] += d_expand
299
+
300
+ # converting new values to new indexes
301
+ for index_, value in np.ndenumerate(data_regions):
302
+ data_regions[index_] = find_nearest_value_index(x_data, value)[1]
303
+
304
+ return data_regions.astype(int)
@@ -0,0 +1,63 @@
1
+ import numpy as np
2
+
3
+
4
+ def find_nearest_value_index(array, value) -> tuple[float, int]:
5
+ """Returns closest value and its index in a given array.
6
+
7
+ :param array: An array to search in.
8
+ :type array: np.array(float)
9
+ :param value: Target value.
10
+ :type value: float
11
+
12
+ :returns: Nearest value in array and its index.
13
+ """
14
+
15
+ index_ = np.argmin(np.abs(array - value))
16
+ return array[index_], index_
17
+
18
+
19
+ def interpolate_to_index(array, ids, precision: int = 100) -> np.array:
20
+ """Find value in between arrays elements.
21
+
22
+ Constructs linspace of size "precision" between index+1 and index to
23
+ find approximate value for array[index], where index is float number.
24
+ Used for 2D data, where default scipy analysis occurs along one axis only,
25
+ e.g. signal.peak_width.
26
+
27
+ Rough equivalent of array[index], where index is float number.
28
+
29
+ :param array: Target array.
30
+ :type array: np.array(float)
31
+ :param ids: An array with "intermediate" indexes to interpolate to.
32
+ :type ids: np.array[float]
33
+ :param precision: Desired presion.
34
+
35
+ :returns: New array with interpolated values according to provided indexes "ids".
36
+
37
+ Example:
38
+ >>> interpolate_to_index(np.array([1.5]), np.array([1,2,3], 100))
39
+ array([2.50505051])
40
+ """
41
+
42
+ # breaking ids into fractional and integral parts
43
+ prec, ids = np.modf(ids)
44
+
45
+ # rounding and switching type to int
46
+ prec = np.around(prec * precision).astype("int32")
47
+ ids = ids.astype("int32")
48
+
49
+ # linear interpolation for each data point
50
+ # as (n x m) matrix where n is precision and m is number of indexes
51
+ space = np.linspace(array[ids], array[ids + 1], precision)
52
+
53
+ # due to rounding error the index may become 100 in (100, ) array
54
+ # as a consequence raising IndexError when such array is indexed
55
+ # therefore index 100 will become the last (-1)
56
+ prec[prec == 100] = -1
57
+
58
+ # precise slicing
59
+ true_values = np.array(
60
+ [space[:, index[0]][value] for index, value in np.ndenumerate(prec)]
61
+ )
62
+
63
+ return true_values
@@ -0,0 +1,3 @@
1
+ """
2
+ .. include:: README.md
3
+ """
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+
4
+ """
5
+ File parser for Chemstation files (*.ch)
6
+ Basically a port of the matlab script at:
7
+ https://github.com/chemplexity/chromatography/blob/master/Development/File%20Conversion/ImportAgilentFID.m
8
+
9
+ This file is a standalone file to parse the binary files created by Chemstation
10
+
11
+ I use it for file with version 130, genereted by an Agilent LC.
12
+ """
13
+
14
+ import struct
15
+ from struct import unpack
16
+ import numpy as np
17
+
18
+ # Constants used for binary file parsing
19
+ ENDIAN = ">"
20
+ STRING = ENDIAN + "{}s"
21
+ UINT8 = ENDIAN + "B"
22
+ UINT16 = ENDIAN + "H"
23
+ INT16 = ENDIAN + "h"
24
+ INT32 = ENDIAN + "i"
25
+ UINT32 = ENDIAN + "I"
26
+
27
+
28
+ def fread(fid, nelements, dtype):
29
+
30
+ """Equivalent to Matlab fread function"""
31
+
32
+ if dtype is str:
33
+ dt = np.uint8 # WARNING: assuming 8-bit ASCII for np.str!
34
+ else:
35
+ dt = dtype
36
+
37
+ data_array = np.fromfile(fid, dt, nelements)
38
+ data_array.shape = (nelements, 1)
39
+
40
+ return data_array
41
+
42
+
43
+ def parse_utf16_string(file_, encoding="UTF16"):
44
+
45
+ """Parse a pascal type UTF16 encoded string from a binary file object"""
46
+
47
+ # First read the expected number of CHARACTERS
48
+ string_length = unpack(UINT8, file_.read(1))[0]
49
+ # Then read and decode
50
+ parsed = unpack(STRING.format(2 * string_length), file_.read(2 * string_length))
51
+ return parsed[0].decode(encoding)
52
+
53
+
54
+ class cached_property(object):
55
+
56
+ """A property that is only computed once per instance and then replaces
57
+ itself with an ordinary attribute. Deleting the attribute resets the
58
+ property.
59
+
60
+ https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
61
+ """
62
+
63
+ def __init__(self, func):
64
+ self.__doc__ = getattr(func, "__doc__")
65
+ self.func = func
66
+
67
+ def __get__(self, obj, cls):
68
+ if obj is None:
69
+ return self
70
+ value = obj.__dict__[self.func.__name__] = self.func(obj)
71
+ return value
72
+
73
+
74
+ class CHFile(object):
75
+
76
+ """Class that implementats the Agilent .ch file format version
77
+ 130. Warning: Not all aspects of the file header is understood,
78
+ so there may and probably is information that is not parsed. See
79
+ _parse_header_status for an overview of which parts of the header
80
+ is understood.
81
+
82
+ Attributes:
83
+ values (numpy.array): The internsity values (y-value) or the
84
+ spectrum. The unit for the values is given in `metadata['units']`
85
+
86
+ metadata (dict): The extracted metadata
87
+
88
+ filepath (str): The filepath this object was loaded from
89
+ """
90
+
91
+ # Fields is a table of name, offset and type. Types 'x-time' and 'utf16'
92
+ # are specially handled, the rest are format arguments for struct unpack
93
+ fields = (
94
+ ("sequence_line_or_injection", 252, UINT16),
95
+ ("injection_or_sequence_line", 256, UINT16),
96
+ ("data_offset", 264, UINT32),
97
+ ("start_time", 282, "x-time"),
98
+ ("end_time", 286, "x-time"),
99
+ ("version_string", 326, "utf16"),
100
+ ("description", 347, "utf16"),
101
+ ("sample", 858, "utf16"),
102
+ ("operator", 1880, "utf16"),
103
+ ("date", 2391, "utf16"),
104
+ ("inlet", 2492, "utf16"),
105
+ ("instrument", 2533, "utf16"),
106
+ ("method", 2574, "utf16"),
107
+ ("software version", 3601, "utf16"),
108
+ ("software name", 3089, "utf16"),
109
+ ("software revision", 3802, "utf16"),
110
+ ("zero", 4110, INT32),
111
+ ("units", 4172, "utf16"),
112
+ ("detector", 4213, "utf16"),
113
+ ("yscaling", 4732, ENDIAN + "d"),
114
+ )
115
+
116
+ # The start position of the data
117
+ # Get it from metadata['data_offset'] * 512
118
+ data_start = 6144
119
+
120
+ # The versions of the file format supported by this implementation
121
+ supported_versions = {130}
122
+
123
+ def __init__(self, filepath):
124
+
125
+ self.filepath = filepath
126
+ self.metadata = {}
127
+ with open(self.filepath, "rb") as file_:
128
+ self._parse_header(file_)
129
+ self.values = self._parse_data(file_)
130
+
131
+ def _parse_header(self, file_):
132
+
133
+ """Parse the header"""
134
+
135
+ # Parse and check version
136
+ length = unpack(UINT8, file_.read(1))[0]
137
+ parsed = unpack(STRING.format(length), file_.read(length))
138
+ version = int(parsed[0])
139
+ if version not in self.supported_versions:
140
+ raise ValueError("Unsupported file version {}".format(version))
141
+ self.metadata["magic_number_version"] = version
142
+
143
+ # Parse all metadata fields
144
+ for name, offset, type_ in self.fields:
145
+ file_.seek(offset)
146
+ if type_ == "utf16":
147
+ self.metadata[name] = parse_utf16_string(file_)
148
+ elif type_ == "x-time":
149
+ self.metadata[name] = unpack(UINT32, file_.read(4))[0] / 60000
150
+ else:
151
+ self.metadata[name] = unpack(type_, file_.read(struct.calcsize(type_)))[
152
+ 0
153
+ ]
154
+
155
+ def _parse_header_status(self):
156
+
157
+ """Print known and unknown parts of the header"""
158
+
159
+ file_ = open(self.filepath, "rb")
160
+
161
+ print("Header parsing status")
162
+ # Map positions to fields for all the known fields
163
+ knowns = {item[1]: item for item in self.fields}
164
+ # A couple of places has a \x01 byte before a string, these we simply
165
+ # skip
166
+ skips = {325, 3600}
167
+ # Jump to after the magic number version
168
+ file_.seek(4)
169
+
170
+ # Initialize variables for unknown bytes
171
+ unknown_start = None
172
+ unknown_bytes = b""
173
+ # While we have not yet reached the data
174
+ while file_.tell() < self.data_start:
175
+ current_position = file_.tell()
176
+ # Just continue on skip bytes
177
+ if current_position in skips:
178
+ file_.read(1)
179
+ continue
180
+
181
+ # If we know about a data field that starts at this point
182
+ if current_position in knowns:
183
+ # If we have collected unknown bytes, print them out and reset
184
+ if unknown_bytes != b"":
185
+ print(
186
+ "Unknown at", unknown_start, repr(unknown_bytes.rstrip(b"\x00"))
187
+ )
188
+ unknown_bytes = b""
189
+ unknown_start = None
190
+
191
+ # Print out the position, type, name and value of the known
192
+ # value
193
+ print("Known field at {: >4},".format(current_position), end=" ")
194
+ name, _, type_ = knowns[current_position]
195
+ if type_ == "x-time":
196
+ print(
197
+ 'x-time, "{: <19}'.format(name + '"'),
198
+ unpack(ENDIAN + "f", file_.read(4))[0] / 60000,
199
+ )
200
+ elif type_ == "utf16":
201
+ print(
202
+ ' utf16, "{: <19}'.format(name + '"'), parse_utf16_string(file_)
203
+ )
204
+ else:
205
+ size = struct.calcsize(type_)
206
+ print(
207
+ '{: >6}, "{: <19}'.format(type_, name + '"'),
208
+ unpack(type_, file_.read(size))[0],
209
+ )
210
+
211
+ # We do not know about a data field at this position If we have
212
+ # already collected 4 zero bytes, assume that we are done with
213
+ # this unkonw field, print and reset
214
+ else:
215
+ if unknown_bytes[-4:] == b"\x00\x00\x00\x00":
216
+ print(
217
+ "Unknown at", unknown_start, repr(unknown_bytes.rstrip(b"\x00"))
218
+ )
219
+ unknown_bytes = b""
220
+ unknown_start = None
221
+
222
+ # Read one byte and save it
223
+ one_byte = file_.read(1)
224
+ if unknown_bytes == b"":
225
+ # Only start a new collection of unknown bytes, if this
226
+ # byte is not a zero byte
227
+ if one_byte != b"\x00":
228
+ unknown_bytes = one_byte
229
+ unknown_start = file_.tell() - 1
230
+ else:
231
+ unknown_bytes += one_byte
232
+
233
+ file_.close()
234
+
235
+ def _parse_data(self, file_):
236
+
237
+ """Parse the data. Decompress the delta-encoded data, and scale them
238
+ with y-scaling"""
239
+
240
+ scaling = self.metadata["yscaling"]
241
+
242
+ # Go to the end of the file
243
+ file_.seek(0, 2)
244
+ stop = file_.tell()
245
+
246
+ # Go to the start point of the data
247
+ file_.seek(self.data_start)
248
+
249
+ signal = []
250
+
251
+ buff = [0, 0, 0, 0]
252
+
253
+ while file_.tell() < stop:
254
+
255
+ buff[0] = fread(file_, 1, INT16)[0][0]
256
+ buff[1] = buff[3]
257
+
258
+ if buff[0] << 12 == 0:
259
+ break
260
+
261
+ for i in range(buff[0] & 4095):
262
+
263
+ buff[2] = fread(file_, 1, INT16)[0][0]
264
+
265
+ if buff[2] != -32768:
266
+ buff[1] = buff[1] + buff[2]
267
+ else:
268
+ buff[1] = fread(file_, 1, INT32)[0][0]
269
+
270
+ signal.append(buff[1])
271
+
272
+ buff[3] = buff[1]
273
+
274
+ signal = np.array(signal)
275
+ signal = signal * scaling
276
+
277
+ return signal
278
+
279
+ @cached_property
280
+ def times(self):
281
+
282
+ """The time values (x-value) for the data set in minutes"""
283
+
284
+ return np.linspace(
285
+ self.metadata["start_time"], self.metadata["end_time"], len(self.values)
286
+ )
287
+
288
+
289
+ if __name__ == "__main__":
290
+ CHFile("lcdiag.reg")