wasatch 2.3.2__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.
- wasatch/AbstractUSBDevice.py +29 -0
- wasatch/AndorDevice.py +910 -0
- wasatch/AreaScanImage.py +19 -0
- wasatch/AutoRaman.py +457 -0
- wasatch/AutoRamanRequest.py +96 -0
- wasatch/BLEDevice.py +1032 -0
- wasatch/BalanceAcquisition.py +136 -0
- wasatch/CSVLoader.py +188 -0
- wasatch/CommandSettings.py +95 -0
- wasatch/ControlObject.py +17 -0
- wasatch/DetectorROI.py +36 -0
- wasatch/DetectorRegions.py +158 -0
- wasatch/DeviceFinderBLE.py +107 -0
- wasatch/DeviceFinderTCP.py +20 -0
- wasatch/DeviceFinderUSB.py +236 -0
- wasatch/DeviceID.py +344 -0
- wasatch/EEPROM.py +1450 -0
- wasatch/EEPROM_multical.sav +48 -0
- wasatch/FPGAOptions.py +125 -0
- wasatch/FeatureIdentificationDevice.py +3961 -0
- wasatch/FirmwareRequirements.py +76 -0
- wasatch/HardwareInfo.py +36 -0
- wasatch/IDSCamera.py +916 -0
- wasatch/IDSDevice.py +329 -0
- wasatch/IMX385.py +179 -0
- wasatch/InterfaceDevice.py +40 -0
- wasatch/MockUSBDevice.py +353 -0
- wasatch/OceanDevice.py +215 -0
- wasatch/PollStatus.py +15 -0
- wasatch/ProcessedReading.py +352 -0
- wasatch/ROI.py +33 -0
- wasatch/Reading.py +80 -0
- wasatch/RealUSBDevice.py +64 -0
- wasatch/SPIDevice.py +628 -0
- wasatch/SpectrometerRequest.py +15 -0
- wasatch/SpectrometerResponse.py +38 -0
- wasatch/SpectrometerSettings.py +446 -0
- wasatch/SpectrometerState.py +255 -0
- wasatch/StatusMessage.py +14 -0
- wasatch/TCPDevice.py +409 -0
- wasatch/TakeOneRequest.py +43 -0
- wasatch/WasatchBus.py +87 -0
- wasatch/WasatchDevice.py +1060 -0
- wasatch/WasatchDeviceWrapper.py +452 -0
- wasatch/WrapperWorker.py +304 -0
- wasatch/__init__.py +4 -0
- wasatch/applog.py +177 -0
- wasatch/simulation_protocol.py +227 -0
- wasatch/utils.py +731 -0
- wasatch-2.3.2.dist-info/LICENSE +21 -0
- wasatch-2.3.2.dist-info/METADATA +347 -0
- wasatch-2.3.2.dist-info/RECORD +53 -0
- wasatch-2.3.2.dist-info/WHEEL +4 -0
wasatch/utils.py
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
# ##############################################################################
|
|
2
|
+
# #
|
|
3
|
+
# utils.py #
|
|
4
|
+
# #
|
|
5
|
+
# ##############################################################################
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import ctypes
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# see https://stackoverflow.com/questions/1026431/cross-platform-way-to-check-admin-rights-in-a-python-script-under-windows
|
|
23
|
+
def check_admin():
|
|
24
|
+
try:
|
|
25
|
+
is_admin = os.getuid() == 0
|
|
26
|
+
except AttributeError:
|
|
27
|
+
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
|
28
|
+
return is_admin
|
|
29
|
+
|
|
30
|
+
## convert unicode string to ascii
|
|
31
|
+
def remove_unicode(s):
|
|
32
|
+
if isinstance(s, str):
|
|
33
|
+
return s.encode('ascii', 'ignore')
|
|
34
|
+
return s
|
|
35
|
+
|
|
36
|
+
def pixel_to_wavelength(x: int, coeffs: list[float]): # -> float
|
|
37
|
+
wavelength = 0.0
|
|
38
|
+
log.debug(f"converting pixel {x} to wavelen with coeffs {coeffs}")
|
|
39
|
+
for i in range(len(coeffs)):
|
|
40
|
+
wavelength += coeffs[i] * pow(x, i)
|
|
41
|
+
return wavelength
|
|
42
|
+
|
|
43
|
+
## expand 3rd-order wavelength polynomial into array of wavelengths
|
|
44
|
+
def generate_wavelengths(pixels, coeffs):
|
|
45
|
+
if coeffs is None:
|
|
46
|
+
log.error(f"generate_wavelengths: coeffs {coeffs}")
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if pixels is None or pixels < 128:
|
|
50
|
+
log.error(f"generate_wavelengths: pixels {pixels}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
wavelengths = []
|
|
54
|
+
for x in range(pixels):
|
|
55
|
+
wavelength = 0.0
|
|
56
|
+
for i in range(len(coeffs)):
|
|
57
|
+
wavelength += coeffs[i] * pow(x, i)
|
|
58
|
+
wavelengths.append(wavelength)
|
|
59
|
+
return wavelengths
|
|
60
|
+
|
|
61
|
+
def generate_wavelengths_from_wavenumbers(excitation, wavenumbers):
|
|
62
|
+
return [1.0 / ((1.0 / excitation) - (wavenumber * 1e-7)) for wavenumber in wavenumbers]
|
|
63
|
+
|
|
64
|
+
## convert wavelengths into Raman shifts in 1/cm wavenumbers from the given
|
|
65
|
+
# excitation wavelength
|
|
66
|
+
def generate_wavenumbers(excitation, wavelengths, wavenumber_correction=0):
|
|
67
|
+
wavenumbers = []
|
|
68
|
+
if not wavelengths or excitation < 1:
|
|
69
|
+
return wavenumbers
|
|
70
|
+
|
|
71
|
+
base = 1e7 / float(excitation)
|
|
72
|
+
for i in range(len(wavelengths)):
|
|
73
|
+
wavenumber = 0
|
|
74
|
+
if wavelengths[i] != 0:
|
|
75
|
+
wavenumber = base - 1e7 / wavelengths[i]
|
|
76
|
+
wavenumbers.append(wavenumber + wavenumber_correction)
|
|
77
|
+
return wavenumbers
|
|
78
|
+
|
|
79
|
+
## convert a single wavelength to wavenumber
|
|
80
|
+
def wavelength_to_wavenumber(wavelength, excitation):
|
|
81
|
+
return 1e7 / float(excitation) - 1e7 / wavelength
|
|
82
|
+
|
|
83
|
+
## convert a single (uncorrected) wavenumber to wavelength
|
|
84
|
+
def wavenumber_to_wavelength(excitation, wavenumber):
|
|
85
|
+
return 1.0 / ((1.0 / excitation) - (wavenumber * 1e-7))
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# If we've loaded a CSV that had wavelength and wavenumber columns, but no
|
|
89
|
+
# metadata, use this to infer the excitation wavelength. Useful for
|
|
90
|
+
# interpolation.
|
|
91
|
+
def generate_excitation(wavelengths, wavenumbers):
|
|
92
|
+
if wavelengths is None or wavenumbers is None or len(wavelengths) != len(wavenumbers) or len(wavelengths) < 1:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
total = 0.0
|
|
96
|
+
count = len(wavelengths)
|
|
97
|
+
for i in range(count):
|
|
98
|
+
excitation = 1e7 / (wavenumbers[i] + 1e7 / wavelengths[i])
|
|
99
|
+
total += excitation
|
|
100
|
+
return total / count
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# apply a boxcar convolution of the given half_width to input array 'a'
|
|
104
|
+
def apply_boxcar(a, half_width):
|
|
105
|
+
out = []
|
|
106
|
+
for i in range(len(a)):
|
|
107
|
+
# hw is smaller than half_width near the fringes
|
|
108
|
+
hw = min(i, half_width, len(a)-1-i)
|
|
109
|
+
# each pixel is the mean of itself and `hw` pixels to the left and right
|
|
110
|
+
out.append(sum(a[i-hw:i+hw+1]) / (2*hw+1))
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
## similar to Perl's Data::Dumper
|
|
114
|
+
def dump(foo, indent=0):
|
|
115
|
+
spc = ' ' * indent
|
|
116
|
+
spc1 = ' ' * (indent + 1)
|
|
117
|
+
s = ""
|
|
118
|
+
|
|
119
|
+
if isinstance(foo, dict):
|
|
120
|
+
s += spc + '{\n'
|
|
121
|
+
for key,val in foo.items():
|
|
122
|
+
if isinstance(val, (dict, list, tuple)):
|
|
123
|
+
s += spc1 + str(key) + '=>\n'
|
|
124
|
+
s += dump(val, indent+2)
|
|
125
|
+
else:
|
|
126
|
+
s += spc1 + str(key) + '=> ' + str(val)
|
|
127
|
+
s += spc + '}\n'
|
|
128
|
+
|
|
129
|
+
elif isinstance(foo, list):
|
|
130
|
+
s += spc + '[\n'
|
|
131
|
+
for item in foo:
|
|
132
|
+
s += dump(item, indent+1)
|
|
133
|
+
s += spc + ']\n'
|
|
134
|
+
|
|
135
|
+
elif isinstance(foo, tuple):
|
|
136
|
+
s += spc + '(\n'
|
|
137
|
+
for item in foo:
|
|
138
|
+
s += dump(item, indent+1)
|
|
139
|
+
s += spc + ')\n'
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
s += spc + str(foo)
|
|
143
|
+
|
|
144
|
+
return s
|
|
145
|
+
|
|
146
|
+
## given a destination object and a source dictionary, update any existing
|
|
147
|
+
# attributes of the destination object from like-named keys in the source
|
|
148
|
+
# dictionary
|
|
149
|
+
#
|
|
150
|
+
# @param obj (In/Out) the object whose attributes to update
|
|
151
|
+
# @param d (Input) the dictionary whose keys should be treated as attributes
|
|
152
|
+
def update_obj_from_dict(obj, d):
|
|
153
|
+
if obj is None or d is None:
|
|
154
|
+
return
|
|
155
|
+
for k in sorted(obj.__dict__.keys()):
|
|
156
|
+
v = dict_get_norm(d, k)
|
|
157
|
+
if v is not None:
|
|
158
|
+
# log.debug("%s -> %s", k, v)
|
|
159
|
+
setattr(obj, k, v)
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Similar to dict.get(), but case-insensitive and normalizes-out spaces,
|
|
163
|
+
# underscores, periods and hyphens.
|
|
164
|
+
#
|
|
165
|
+
# @param d (input) dictionary
|
|
166
|
+
# @param k (input) case-insensitive key (can be prioritized list)
|
|
167
|
+
#
|
|
168
|
+
# Note that this function does not distiguish between the dictionary not having
|
|
169
|
+
# a key, and the value of the key being None.
|
|
170
|
+
def dict_get_norm(d, keys):
|
|
171
|
+
|
|
172
|
+
# if we weren't passed a list, make it one
|
|
173
|
+
if not isinstance(keys, list):
|
|
174
|
+
keys = [ keys ]
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
pat = r"[ ._-]"
|
|
178
|
+
for key in keys:
|
|
179
|
+
key = re.sub(pat, "", key).lower()
|
|
180
|
+
for k, v in d.items():
|
|
181
|
+
k = re.sub(pat, "", k).lower()
|
|
182
|
+
if k == key:
|
|
183
|
+
return v
|
|
184
|
+
except:
|
|
185
|
+
log.error("dict_get_norm: %s", keys, exc_info=1)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# Similar to dict.get(), but takes a list of keys to be traversed in sequence.
|
|
190
|
+
#
|
|
191
|
+
# @param d (input) dictionary
|
|
192
|
+
# @param keys (input) list of case-insensitive keys
|
|
193
|
+
def dict_get_path(d, keys):
|
|
194
|
+
try:
|
|
195
|
+
while len(keys) > 0:
|
|
196
|
+
k = keys.pop(0)
|
|
197
|
+
v = dict_get_norm(d, k)
|
|
198
|
+
if v is None:
|
|
199
|
+
return
|
|
200
|
+
elif len(keys) == 0:
|
|
201
|
+
return v
|
|
202
|
+
else:
|
|
203
|
+
d = v
|
|
204
|
+
except:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
## convenience wrapper to load a JSON file
|
|
208
|
+
def load_json(pathname):
|
|
209
|
+
try:
|
|
210
|
+
with open(pathname) as infile:
|
|
211
|
+
return json.load(infile)
|
|
212
|
+
except:
|
|
213
|
+
log.error("unable to load %s", pathname, exc_info=1)
|
|
214
|
+
|
|
215
|
+
def get_default_data_dir():
|
|
216
|
+
""" copy of enlighten.common method of the same name """
|
|
217
|
+
if os.name == "nt":
|
|
218
|
+
return os.path.join(os.path.expanduser("~"), "Documents", "EnlightenSpectra")
|
|
219
|
+
return os.path.join(os.environ["HOME"], "EnlightenSpectra")
|
|
220
|
+
|
|
221
|
+
## iterate down a directory, returning pathnames that match the given pattern
|
|
222
|
+
def get_pathnames_from_directory(rootdir, pattern=None, recursive=False):
|
|
223
|
+
pathnames = []
|
|
224
|
+
# log.debug("searching %s matching %s with recursive %s", rootdir, pattern, recursive)
|
|
225
|
+
if recursive:
|
|
226
|
+
for (directory, _, filenames) in os.walk(rootdir):
|
|
227
|
+
for filename in filenames:
|
|
228
|
+
pathname = os.path.join(directory, filename)
|
|
229
|
+
if pattern:
|
|
230
|
+
if re.search(pattern, filename):
|
|
231
|
+
pathnames.append(pathname)
|
|
232
|
+
else:
|
|
233
|
+
# log.debug("%s does not match %s", pathname, pattern)
|
|
234
|
+
pass
|
|
235
|
+
else:
|
|
236
|
+
pathnames.append(pathname)
|
|
237
|
+
elif os.path.isdir(rootdir):
|
|
238
|
+
for filename in os.listdir(rootdir):
|
|
239
|
+
pathname = os.path.join(rootdir, filename)
|
|
240
|
+
if os.path.isfile(pathname):
|
|
241
|
+
if pattern:
|
|
242
|
+
if re.search(pattern, filename):
|
|
243
|
+
pathnames.append(pathname)
|
|
244
|
+
else:
|
|
245
|
+
# log.debug("%s does not match %s", pathname, pattern)
|
|
246
|
+
pass
|
|
247
|
+
else:
|
|
248
|
+
pathnames.append(pathname)
|
|
249
|
+
# log.debug("returning %s", pathnames)
|
|
250
|
+
return pathnames
|
|
251
|
+
|
|
252
|
+
##
|
|
253
|
+
# Given a spectrum (array 'a'), with an x_axis, a 'center' along that x_axis, and
|
|
254
|
+
# an allowed 'tolerance' (in same units as the x_axis), find the local maxima
|
|
255
|
+
# within 'tolerance' of 'center'.
|
|
256
|
+
#
|
|
257
|
+
# @note probably a numpy shortcut for this
|
|
258
|
+
def find_local_maxima(a, x_axis, center, tolerance=0):
|
|
259
|
+
# log.debug("find_local_maxima: center %.2f (tolerance %.2f)", center, tolerance)
|
|
260
|
+
# generate subset of array within tolerance of center
|
|
261
|
+
x = []
|
|
262
|
+
y = []
|
|
263
|
+
indices = []
|
|
264
|
+
for i in range(len(x_axis)):
|
|
265
|
+
x_value = x_axis[i]
|
|
266
|
+
if center - tolerance <= x_value <= center + tolerance:
|
|
267
|
+
indices.append(i)
|
|
268
|
+
x.append(x_value)
|
|
269
|
+
y.append(a[i])
|
|
270
|
+
|
|
271
|
+
# log.debug(" range x: %s", x)
|
|
272
|
+
# log.debug(" range y: %s", y)
|
|
273
|
+
|
|
274
|
+
if not x:
|
|
275
|
+
raise "no points within %s of %s"
|
|
276
|
+
|
|
277
|
+
# find maxima within subset
|
|
278
|
+
best_x_index = indices[0]
|
|
279
|
+
best_x_value = x_axis[0]
|
|
280
|
+
best_y_value = y[0]
|
|
281
|
+
for i in range(len(x)):
|
|
282
|
+
if best_y_value < y[i]:
|
|
283
|
+
best_x_index = indices[i]
|
|
284
|
+
best_x_value = x_axis[best_x_index]
|
|
285
|
+
best_y_value = y[i]
|
|
286
|
+
|
|
287
|
+
# no point with linear interpolation, as it would only go "down"
|
|
288
|
+
# (could do Gaussian / polynomial fit)
|
|
289
|
+
|
|
290
|
+
# log.debug(" best_x_index: %d", best_x_index)
|
|
291
|
+
# log.debug(" best_x_value: %.2f", best_x_value)
|
|
292
|
+
# log.debug(" best_y_value: %.2f", best_y_value)
|
|
293
|
+
|
|
294
|
+
return (best_y_value, best_x_value, best_x_index)
|
|
295
|
+
|
|
296
|
+
##
|
|
297
|
+
# Given a spectrum and an x_axis, find the indexes of the left and right
|
|
298
|
+
# 'feet' of the peak centered on x_index. Internally apply then given boxcar
|
|
299
|
+
# for added smoothing.
|
|
300
|
+
def find_peak_feet_indices(spectrum, x_axis, x_index, boxcar_half_width=0):
|
|
301
|
+
if boxcar_half_width:
|
|
302
|
+
smoothed = apply_boxcar(spectrum, boxcar_half_width)
|
|
303
|
+
else:
|
|
304
|
+
smoothed = spectrum
|
|
305
|
+
|
|
306
|
+
left_index = x_index
|
|
307
|
+
for i in range(x_index - (boxcar_half_width + 1), -1, -1):
|
|
308
|
+
if i == 0 or smoothed[i] > smoothed[left_index]:
|
|
309
|
+
break
|
|
310
|
+
left_index = i
|
|
311
|
+
|
|
312
|
+
right_index = x_index
|
|
313
|
+
for i in range(x_index + (boxcar_half_width + 1), len(spectrum)):
|
|
314
|
+
if i + 1 == len(spectrum) or smoothed[i] > smoothed[right_index]:
|
|
315
|
+
break
|
|
316
|
+
right_index = i
|
|
317
|
+
|
|
318
|
+
return (left_index, right_index)
|
|
319
|
+
|
|
320
|
+
## integrate the 'area under the curve' for the given spectrum and x_axis,
|
|
321
|
+
# using the peak centered on x_axis, with optional smoothing given the
|
|
322
|
+
# boxcar width.
|
|
323
|
+
def area_under_peak(spectrum, x_axis, x_index, boxcar_half_width=0):
|
|
324
|
+
# find left and right "feet" of the peak
|
|
325
|
+
(left_index, right_index) = find_peak_feet_indices(
|
|
326
|
+
spectrum, x_axis, x_index, boxcar_half_width)
|
|
327
|
+
|
|
328
|
+
# generate baseline-subtracted subspectrum of just the peak, considering
|
|
329
|
+
# the baseline to be a straight line between the two feet
|
|
330
|
+
slope = float(spectrum[right_index] - spectrum[left_index]) / \
|
|
331
|
+
(x_axis[right_index] - x_axis[left_index])
|
|
332
|
+
subspectrum = []
|
|
333
|
+
subaxis = []
|
|
334
|
+
for i in range (left_index, right_index + 1):
|
|
335
|
+
baseline = spectrum[left_index] + slope * (x_axis[i] - x_axis[left_index])
|
|
336
|
+
subspectrum.append(spectrum[i] - baseline)
|
|
337
|
+
subaxis.append(x_axis[i])
|
|
338
|
+
|
|
339
|
+
# 4. integrate subspectrum
|
|
340
|
+
area = numpy.trapz(subspectrum, subaxis)
|
|
341
|
+
return area
|
|
342
|
+
|
|
343
|
+
def peak_height_above_background(spectrum, x_axis, x_index, boxcar_half_width=0):
|
|
344
|
+
# find left and right "feet" of the peak
|
|
345
|
+
(left_index, right_index) = find_peak_feet_indices(
|
|
346
|
+
spectrum, x_axis, x_index, boxcar_half_width)
|
|
347
|
+
|
|
348
|
+
width_wn = x_axis[right_index] - x_axis[left_index]
|
|
349
|
+
width_px = right_index - left_index + 1
|
|
350
|
+
|
|
351
|
+
# generate baseline-subtracted subspectrum of just the peak, considering
|
|
352
|
+
# the baseline to be a straight line between the two feet
|
|
353
|
+
slope = float(spectrum[right_index] - spectrum[left_index]) / width_wn
|
|
354
|
+
baseline = spectrum[left_index] + slope * (x_axis[x_index] - x_axis[left_index])
|
|
355
|
+
height = spectrum[x_index] - baseline
|
|
356
|
+
|
|
357
|
+
log.debug("peak_height_above_background: peak at x_index %d (boxcar %d)", x_index, boxcar_half_width)
|
|
358
|
+
log.debug("peak_height_above_background: abs height: %.2f", spectrum[x_index])
|
|
359
|
+
log.debug("peak_height_above_background: peak width: (%d px, %.2f cm-1)", width_px, width_wn)
|
|
360
|
+
log.debug("peak_height_above_background: feet: (%d, %d)", left_index, right_index)
|
|
361
|
+
log.debug("peak_height_above_background: feet height: (%.2f, %.2f)", spectrum[left_index], spectrum[right_index])
|
|
362
|
+
log.debug("peak_height_above_background: slope: %.2f", slope)
|
|
363
|
+
log.debug("peak_height_above_background: peak baseline: %.2f", baseline)
|
|
364
|
+
log.debug("peak_height_above_background: relative height: %.2f", height)
|
|
365
|
+
|
|
366
|
+
return (height, width_wn, width_px)
|
|
367
|
+
|
|
368
|
+
def find_nearest_index(L, value):
|
|
369
|
+
a = numpy.asarray(L)
|
|
370
|
+
return (numpy.abs(a - value)).argmin()
|
|
371
|
+
|
|
372
|
+
def find_nearest_value(L, value):
|
|
373
|
+
i = find_nearest_index(L, value)
|
|
374
|
+
return L[i]
|
|
375
|
+
|
|
376
|
+
##
|
|
377
|
+
# Interpolate the passed spectrum over a fixed x-axis (e.g. integral wavelengths
|
|
378
|
+
# or wavenumbers).
|
|
379
|
+
def interpolate_array(spectrum, old_axis, new_axis):
|
|
380
|
+
if not spectrum or not old_axis or not new_axis or len(spectrum) != len(old_axis) or len(new_axis) < 1:
|
|
381
|
+
return
|
|
382
|
+
return numpy.interp(new_axis, old_axis, spectrum)
|
|
383
|
+
|
|
384
|
+
## I might be making this more difficult than it needs to be
|
|
385
|
+
def interpolate_value(spectrum, old_axis, x):
|
|
386
|
+
if not spectrum or not old_axis or len(spectrum) != len(old_axis):
|
|
387
|
+
return
|
|
388
|
+
new_axis = [ x-1, x, x+1 ]
|
|
389
|
+
new_y = numpy.interp(new_axis, old_axis, spectrum)
|
|
390
|
+
if new_y is not None and len(new_y) == len(new_axis):
|
|
391
|
+
return new_y[1]
|
|
392
|
+
|
|
393
|
+
## render a spectrum as ASCII-art
|
|
394
|
+
def ascii_spectrum(spectrum, rows, cols, x_axis, x_unit):
|
|
395
|
+
spectral_min = min(spectrum)
|
|
396
|
+
spectral_max = max(spectrum)
|
|
397
|
+
spectral_avg = 1.0 * sum(spectrum) / len(spectrum)
|
|
398
|
+
|
|
399
|
+
# histogram into bins
|
|
400
|
+
bins = [0] * cols
|
|
401
|
+
for i in range(len(spectrum)):
|
|
402
|
+
col = int(1.0 * cols * i / len(spectrum))
|
|
403
|
+
bins[col] += spectrum[i] - spectral_min
|
|
404
|
+
|
|
405
|
+
# render histogram
|
|
406
|
+
lines = []
|
|
407
|
+
bin_hi = max(bins)
|
|
408
|
+
for row in range(rows - 1, -1, -1):
|
|
409
|
+
s = "| "
|
|
410
|
+
for col in range(cols):
|
|
411
|
+
s += "*" if bins[col] >= (1.0 * row / rows) * bin_hi else " "
|
|
412
|
+
lines.append(s)
|
|
413
|
+
|
|
414
|
+
# graph footer
|
|
415
|
+
lines.append("+-" + "-" * cols)
|
|
416
|
+
lines.append(" Min: %8.2f Max: %8.2f Mean: %8.2f (range %.2f, %.2f%s)" % (
|
|
417
|
+
spectral_min, spectral_max, spectral_avg,
|
|
418
|
+
x_axis[0], x_axis[-1], x_unit))
|
|
419
|
+
|
|
420
|
+
return lines
|
|
421
|
+
|
|
422
|
+
def timestamp():
|
|
423
|
+
return datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
424
|
+
|
|
425
|
+
def truthy(flag):
|
|
426
|
+
if flag is None:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
if len(flag) > 0:
|
|
431
|
+
return True
|
|
432
|
+
except:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
return True if flag else False
|
|
436
|
+
|
|
437
|
+
def clean_nan(a):
|
|
438
|
+
for i in range(len(a)):
|
|
439
|
+
if math.isnan(a[i]):
|
|
440
|
+
a[i] = 0
|
|
441
|
+
|
|
442
|
+
##
|
|
443
|
+
# Can be used as a sanity-check for any set of coefficients.
|
|
444
|
+
#
|
|
445
|
+
# Checks that coeffs:
|
|
446
|
+
#
|
|
447
|
+
# - are not None
|
|
448
|
+
# - have no NaN
|
|
449
|
+
# - are not all the same (zeros, -1 etc)
|
|
450
|
+
# - are not [0, 1, 0, 0]
|
|
451
|
+
# - checks count if provided
|
|
452
|
+
def coeffs_look_valid(coeffs, count=None):
|
|
453
|
+
|
|
454
|
+
if coeffs is None:
|
|
455
|
+
log.debug("no coeffs, returning False")
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
if count is not None and len(coeffs) != count:
|
|
459
|
+
log.debug("coeff count is wrong, returning False")
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
# check for NaN
|
|
463
|
+
for i in range(len(coeffs)):
|
|
464
|
+
if math.isnan(coeffs[i]):
|
|
465
|
+
log.debug("found NaN in coeff, returning False")
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
# check for [0, 1, 0...] default pattern
|
|
469
|
+
all_default = True
|
|
470
|
+
for i in range(len(coeffs)):
|
|
471
|
+
if i == 1:
|
|
472
|
+
if coeffs[i] != 1.0:
|
|
473
|
+
all_default = False
|
|
474
|
+
elif coeffs[i] != 0.0:
|
|
475
|
+
all_default = False
|
|
476
|
+
if all_default:
|
|
477
|
+
log.debug("coeffs all default, returning False")
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
# check for constants (all coefficients the same value)
|
|
481
|
+
all_const = True
|
|
482
|
+
# log.debug(coeffs)
|
|
483
|
+
for i in range(1, len(coeffs)):
|
|
484
|
+
if coeffs[0] != coeffs[i]:
|
|
485
|
+
all_const = False
|
|
486
|
+
if all_const:
|
|
487
|
+
log.debug("coeffs all const, returning False")
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
return True
|
|
491
|
+
|
|
492
|
+
##
|
|
493
|
+
# "Stomps" the first "count" elements with the first non-stomped value.
|
|
494
|
+
#
|
|
495
|
+
# @param a (Input) array to modify
|
|
496
|
+
# @param count (Input) HOW MANY leading elements to stomp, so the index of the
|
|
497
|
+
# first GOOD pixel should be one more than this
|
|
498
|
+
def stomp_first(a, count):
|
|
499
|
+
for i in range(count):
|
|
500
|
+
a[i] = a[count]
|
|
501
|
+
|
|
502
|
+
## "stomps" the last "count" elements with the last non-stomped value
|
|
503
|
+
def stomp_last(a, count):
|
|
504
|
+
for i in range(count):
|
|
505
|
+
a[-(i+1)] = a[-(count+1)]
|
|
506
|
+
|
|
507
|
+
def clamp_to_int16(n):
|
|
508
|
+
return max(-32768, min(32767, int(n)))
|
|
509
|
+
|
|
510
|
+
def all_same(a):
|
|
511
|
+
for i in range(1, len(a)):
|
|
512
|
+
if a[0] != a[i]:
|
|
513
|
+
return False
|
|
514
|
+
return True
|
|
515
|
+
|
|
516
|
+
##
|
|
517
|
+
# Given an array of doubles and a peak index, use the peak and its two
|
|
518
|
+
# neighbors to form a parabola and return the interpolated maximum height of the
|
|
519
|
+
# parabola.
|
|
520
|
+
#
|
|
521
|
+
# "pixel" is ideally the array index of the pinnacle of a previously-
|
|
522
|
+
# identified peak within the spectrum, although though this will
|
|
523
|
+
# technically generate a parabola through any pixel and its two
|
|
524
|
+
# neighbors.
|
|
525
|
+
#
|
|
526
|
+
# @param pixel index of a point on the spectrum
|
|
527
|
+
# @param x x-axis (wavelengths or wavenumbers)
|
|
528
|
+
# @param y y-axis (intensity)
|
|
529
|
+
#
|
|
530
|
+
# @see https://stackoverflow.com/a/717833
|
|
531
|
+
#
|
|
532
|
+
# @returns a point representing the interpolated vertex of a parabola drawn
|
|
533
|
+
# through the specified pixel and its two neighbors (in x-axis space)
|
|
534
|
+
#
|
|
535
|
+
def parabolic_approximation(pixel, x, y):
|
|
536
|
+
if len(x) != len(y):
|
|
537
|
+
log.error("parabolic approximation array lengths differ")
|
|
538
|
+
return 0, 0
|
|
539
|
+
if pixel - 1 < 0:
|
|
540
|
+
return y[0]
|
|
541
|
+
elif pixel + 1 >= len(y):
|
|
542
|
+
return y[-1]
|
|
543
|
+
|
|
544
|
+
x1 = pixel - 1
|
|
545
|
+
x2 = pixel
|
|
546
|
+
x3 = pixel + 1
|
|
547
|
+
|
|
548
|
+
y1 = y[x1]
|
|
549
|
+
y2 = y[x2]
|
|
550
|
+
y3 = y[x3]
|
|
551
|
+
|
|
552
|
+
if y1 >= y2 or y3 >= y2:
|
|
553
|
+
log.debug("parabolic approximation: peak misformed or saturated")
|
|
554
|
+
|
|
555
|
+
denom = (x1 - x2) * (x1 - x3) * (x2 - x3)
|
|
556
|
+
A = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom
|
|
557
|
+
B = (x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1) + x1 * x1 * (y2 - y3)) / denom
|
|
558
|
+
C = (x2 * x3 * (x2 - x3) * y1 + x3 * x1 * (x3 - x1) * y2 + x1 * x2 * (x1 - x2) * y3) / denom
|
|
559
|
+
|
|
560
|
+
vertex_x = -B / (2 * A) # pixel space
|
|
561
|
+
vertex_y = C - B * B / (4 * A)
|
|
562
|
+
|
|
563
|
+
if vertex_x < x1 or vertex_x > x3:
|
|
564
|
+
log.error("parabolic approximation failed (x exceeded limits)")
|
|
565
|
+
return (0, 0)
|
|
566
|
+
|
|
567
|
+
if vertex_x == x2:
|
|
568
|
+
return (x[x2], vertex_y)
|
|
569
|
+
elif vertex_x < x2:
|
|
570
|
+
left = x1
|
|
571
|
+
right = x2
|
|
572
|
+
else:
|
|
573
|
+
left = x2
|
|
574
|
+
right = x3
|
|
575
|
+
x_coord = x[left] + (x[right] - x[left]) * (vertex_x - left)
|
|
576
|
+
|
|
577
|
+
if False:
|
|
578
|
+
log.debug("parabolic approximation: x1 %d, x2 %d, x3 %d", x1, x2, x3)
|
|
579
|
+
log.debug("parabolic approximation: x.x1 %.2f, x.x2 %.2f, x.x3 %.2f", x[x1], x[x2], x[x3])
|
|
580
|
+
log.debug("parabolic approximation: y.x1 %.2f, y.x2 %.2f, y.x3 %.2f", y[x1], y[x2], y[x3])
|
|
581
|
+
log.debug("parabolic approximation: vertex_x %.2f, vertex_y %.2f", vertex_x, vertex_y)
|
|
582
|
+
log.debug("parabolic approximation: left %d, right %d", left, right)
|
|
583
|
+
log.debug("parabolic approximation: x.left %.2f, x.right %.2f", x[left], x[right])
|
|
584
|
+
log.debug("parabolic approximation: x.coord %.2f", x_coord)
|
|
585
|
+
|
|
586
|
+
return (x_coord, vertex_y)
|
|
587
|
+
|
|
588
|
+
##
|
|
589
|
+
# @see https://stackoverflow.com/a/9147327/6436775
|
|
590
|
+
def twos_complement(val, bits):
|
|
591
|
+
if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255
|
|
592
|
+
val = val - (1 << bits) # compute negative value
|
|
593
|
+
return val
|
|
594
|
+
|
|
595
|
+
def to_bool(value):
|
|
596
|
+
if isinstance(value, bool):
|
|
597
|
+
return value
|
|
598
|
+
elif isinstance(value, int):
|
|
599
|
+
return 0 != value
|
|
600
|
+
elif isinstance(value, float):
|
|
601
|
+
return 0 != value
|
|
602
|
+
elif isinstance(value, str):
|
|
603
|
+
s = value.lower().strip()
|
|
604
|
+
return s in ['true', 'y', 'yes', 'on', '1']
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
def uint16_to_little_endian(values):
|
|
608
|
+
a = []
|
|
609
|
+
for n in values:
|
|
610
|
+
a.append(n & 0xff) # lsb
|
|
611
|
+
a.append((n >> 8) & 0xff) # msb
|
|
612
|
+
return a
|
|
613
|
+
|
|
614
|
+
def resize_file(path, nbytes, ensure_no_overwrite=False):
|
|
615
|
+
"""
|
|
616
|
+
Keep only the last `nbytes` of a text file specified by `path`.
|
|
617
|
+
|
|
618
|
+
This function is encoding (utf-8, utf-16, ascii) agnostic because it uses the line-
|
|
619
|
+
continuation character '\n' as a boundary. It is system independent because it slices
|
|
620
|
+
before that character. This was written to keep our default log file within a reasonable
|
|
621
|
+
filesize, suitable for emailing.
|
|
622
|
+
|
|
623
|
+
Call this on a file that is not yet opened. Expect the file to be closed and flushed
|
|
624
|
+
by the time the function returns.
|
|
625
|
+
|
|
626
|
+
returns True if the file was changed.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
# limit how much memory is used to copy file contents
|
|
630
|
+
# NOTE: this NOT the filesize limit, that is passed in via nbytes
|
|
631
|
+
# see applog.py's call of resize_file for the log file limit
|
|
632
|
+
MAX_BYTES_MEMORY = 2_000_000
|
|
633
|
+
|
|
634
|
+
START_OF_STREAM = 0 # seek from start of a file
|
|
635
|
+
CURRENT_STREAM = 1 # seek from current location in file
|
|
636
|
+
END_OF_STREAM = 2 # seek from end of file, offset should be negative
|
|
637
|
+
|
|
638
|
+
# open file with the intention of reading bytes, preserving what's there
|
|
639
|
+
f = open(path, "rb")
|
|
640
|
+
|
|
641
|
+
# path to temporary file
|
|
642
|
+
tpath = path+".temp"
|
|
643
|
+
|
|
644
|
+
# don't overwrite any local files
|
|
645
|
+
# defaults to false so we don't err on the side of a bunch of extra .temp files
|
|
646
|
+
if ensure_no_overwrite:
|
|
647
|
+
while os.path.exists(tpath):
|
|
648
|
+
tpath += ".temp"
|
|
649
|
+
|
|
650
|
+
f2 = open(tpath, "wb")
|
|
651
|
+
|
|
652
|
+
# fast-forward to end of file and keep track of total_bytes
|
|
653
|
+
tbytes = f.seek(0, END_OF_STREAM)
|
|
654
|
+
|
|
655
|
+
if tbytes <= nbytes:
|
|
656
|
+
# don't do anything if file is already smaller than target size
|
|
657
|
+
f2.close()
|
|
658
|
+
os.remove(tpath)
|
|
659
|
+
return False
|
|
660
|
+
|
|
661
|
+
# go to nbytes from the end
|
|
662
|
+
f.seek(-nbytes, END_OF_STREAM)
|
|
663
|
+
|
|
664
|
+
# seek to a newline
|
|
665
|
+
for i in range(500):
|
|
666
|
+
if f.read(1) == b'\n':
|
|
667
|
+
break
|
|
668
|
+
|
|
669
|
+
# make sure to land before the newline
|
|
670
|
+
t_start = f.seek(-1, CURRENT_STREAM)
|
|
671
|
+
|
|
672
|
+
# decrease nbytes by the amount we skipped forward (usually small)
|
|
673
|
+
nbytes = tbytes-t_start
|
|
674
|
+
|
|
675
|
+
# copy the end of the file into the beginning of a temporary one
|
|
676
|
+
f.seek(t_start, START_OF_STREAM)
|
|
677
|
+
i = nbytes
|
|
678
|
+
while i:
|
|
679
|
+
block_size = min(MAX_BYTES_MEMORY, i)
|
|
680
|
+
|
|
681
|
+
b = f.read(block_size)
|
|
682
|
+
f2.write(b)
|
|
683
|
+
i -= block_size
|
|
684
|
+
|
|
685
|
+
# wrap up resource use
|
|
686
|
+
f.close()
|
|
687
|
+
f2.flush()
|
|
688
|
+
f2.close()
|
|
689
|
+
|
|
690
|
+
# move temporary file in place
|
|
691
|
+
shutil.move(tpath, path)
|
|
692
|
+
|
|
693
|
+
return True
|
|
694
|
+
|
|
695
|
+
def vercmp(a, b, delim=None):
|
|
696
|
+
"""
|
|
697
|
+
@returns vercmp("1.2.3.4", "1.2.4.3") -> -1
|
|
698
|
+
@note Python probably already something like this...?
|
|
699
|
+
"""
|
|
700
|
+
if a is None or b is None:
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
if delim is None:
|
|
704
|
+
for c in [".", "_", "-", " "]:
|
|
705
|
+
if c in a and c in b:
|
|
706
|
+
delim = c
|
|
707
|
+
break
|
|
708
|
+
if delim is None:
|
|
709
|
+
delim = "." # maybe there is no delimiter
|
|
710
|
+
|
|
711
|
+
tok_a = str(a).split(delim)
|
|
712
|
+
tok_b = str(b).split(delim)
|
|
713
|
+
|
|
714
|
+
int_a = int(tok_a[0])
|
|
715
|
+
int_b = int(tok_b[0])
|
|
716
|
+
|
|
717
|
+
if int_a > int_b: return +1
|
|
718
|
+
elif int_a < int_b: return -1
|
|
719
|
+
elif len(tok_a) == 1 and len(tok_b) == 1: return 0
|
|
720
|
+
elif len(tok_a) > 1 and len(tok_b) > 1: return vercmp(delim.join(tok_a[1:]), delim.join(tok_b[1:]), delim=delim)
|
|
721
|
+
elif len(tok_b) > 1: return -1
|
|
722
|
+
else: return +1
|
|
723
|
+
|
|
724
|
+
def to_hex(a):
|
|
725
|
+
return "[ " + ", ".join([f"0x{v:02x}" for v in a]) + " ]"
|
|
726
|
+
|
|
727
|
+
def from_db_to_linear(x):
|
|
728
|
+
return 10 ** (x / 20.)
|
|
729
|
+
|
|
730
|
+
def from_linear_to_db(x):
|
|
731
|
+
return 20 * math.log(x, 10)
|