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.
Files changed (53) hide show
  1. wasatch/AbstractUSBDevice.py +29 -0
  2. wasatch/AndorDevice.py +910 -0
  3. wasatch/AreaScanImage.py +19 -0
  4. wasatch/AutoRaman.py +457 -0
  5. wasatch/AutoRamanRequest.py +96 -0
  6. wasatch/BLEDevice.py +1032 -0
  7. wasatch/BalanceAcquisition.py +136 -0
  8. wasatch/CSVLoader.py +188 -0
  9. wasatch/CommandSettings.py +95 -0
  10. wasatch/ControlObject.py +17 -0
  11. wasatch/DetectorROI.py +36 -0
  12. wasatch/DetectorRegions.py +158 -0
  13. wasatch/DeviceFinderBLE.py +107 -0
  14. wasatch/DeviceFinderTCP.py +20 -0
  15. wasatch/DeviceFinderUSB.py +236 -0
  16. wasatch/DeviceID.py +344 -0
  17. wasatch/EEPROM.py +1450 -0
  18. wasatch/EEPROM_multical.sav +48 -0
  19. wasatch/FPGAOptions.py +125 -0
  20. wasatch/FeatureIdentificationDevice.py +3961 -0
  21. wasatch/FirmwareRequirements.py +76 -0
  22. wasatch/HardwareInfo.py +36 -0
  23. wasatch/IDSCamera.py +916 -0
  24. wasatch/IDSDevice.py +329 -0
  25. wasatch/IMX385.py +179 -0
  26. wasatch/InterfaceDevice.py +40 -0
  27. wasatch/MockUSBDevice.py +353 -0
  28. wasatch/OceanDevice.py +215 -0
  29. wasatch/PollStatus.py +15 -0
  30. wasatch/ProcessedReading.py +352 -0
  31. wasatch/ROI.py +33 -0
  32. wasatch/Reading.py +80 -0
  33. wasatch/RealUSBDevice.py +64 -0
  34. wasatch/SPIDevice.py +628 -0
  35. wasatch/SpectrometerRequest.py +15 -0
  36. wasatch/SpectrometerResponse.py +38 -0
  37. wasatch/SpectrometerSettings.py +446 -0
  38. wasatch/SpectrometerState.py +255 -0
  39. wasatch/StatusMessage.py +14 -0
  40. wasatch/TCPDevice.py +409 -0
  41. wasatch/TakeOneRequest.py +43 -0
  42. wasatch/WasatchBus.py +87 -0
  43. wasatch/WasatchDevice.py +1060 -0
  44. wasatch/WasatchDeviceWrapper.py +452 -0
  45. wasatch/WrapperWorker.py +304 -0
  46. wasatch/__init__.py +4 -0
  47. wasatch/applog.py +177 -0
  48. wasatch/simulation_protocol.py +227 -0
  49. wasatch/utils.py +731 -0
  50. wasatch-2.3.2.dist-info/LICENSE +21 -0
  51. wasatch-2.3.2.dist-info/METADATA +347 -0
  52. wasatch-2.3.2.dist-info/RECORD +53 -0
  53. 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)