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/AreaScanImage.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class AreaScanImage:
|
|
2
|
+
|
|
3
|
+
FRAME_COUNT = 0
|
|
4
|
+
|
|
5
|
+
def __init__(self, data=None, width=None, height=None, format_name=None, width_orig=None, height_orig=None, pathname_png=None, line_index=None):
|
|
6
|
+
self.data = data
|
|
7
|
+
self.width = width
|
|
8
|
+
self.height = height
|
|
9
|
+
self.width_orig = width_orig
|
|
10
|
+
self.height_orig = height_orig
|
|
11
|
+
self.format_name = format_name
|
|
12
|
+
self.pathname_png = pathname_png
|
|
13
|
+
self.line_index = line_index
|
|
14
|
+
|
|
15
|
+
self.frame_count = AreaScanImage.FRAME_COUNT
|
|
16
|
+
AreaScanImage.FRAME_COUNT += 1
|
|
17
|
+
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
return f"AreaScanImage<frame {self.frame_count}, line_index {line_index}, width {self.width} (orig {self.width_orig}), height {self.height} (orig {self.height_orig}), format_name {self.format_name}, pathname_png {self.pathname_png}>"
|
wasatch/AutoRaman.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import math
|
|
4
|
+
import numpy as np
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from .SpectrometerResponse import SpectrometerResponse
|
|
10
|
+
from .SpectrometerRequest import SpectrometerRequest
|
|
11
|
+
from .Reading import Reading
|
|
12
|
+
|
|
13
|
+
from . import utils
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class AutoRaman:
|
|
18
|
+
"""
|
|
19
|
+
This class encapsulates Dieter's Auto-Raman algorithm, which optimizes
|
|
20
|
+
integration time (and gain on XS series spectrometers) to achieve a
|
|
21
|
+
target window of counts, then uses the configured measurement time
|
|
22
|
+
to maximize scan averaging at with those acquisition parameters.
|
|
23
|
+
|
|
24
|
+
As calling software will not necessarily expect the configured "default"
|
|
25
|
+
integration time and gain to change, the class restores those to previous
|
|
26
|
+
levels after a measurement.
|
|
27
|
+
|
|
28
|
+
@par Design Considerations
|
|
29
|
+
|
|
30
|
+
Mark made the following changes from Dieter's original algo:
|
|
31
|
+
|
|
32
|
+
- don't take throwaways during averaged signal or dark collections,
|
|
33
|
+
as acquisition parameters aren't changing and sensor should be
|
|
34
|
+
stable
|
|
35
|
+
- don't include laser warning delay when computing num_avg, since
|
|
36
|
+
laser is already enabled and firing
|
|
37
|
+
|
|
38
|
+
Mark also made the following decisions regarding ENLIGHTEN integration:
|
|
39
|
+
|
|
40
|
+
- return the optimized integration time and gain back to ENLIGHTEN
|
|
41
|
+
in the Reading object so they will be the new GUI settings if the
|
|
42
|
+
user simply hits "Play" to resume free-running spectra.
|
|
43
|
+
- ENLIGHTEN will override the init_int_time and init_gain defaults
|
|
44
|
+
in AutoRamanRequest so that if the user hits "Auto-Raman Measurement"
|
|
45
|
+
a second time, the previous settings will be used as the new starting
|
|
46
|
+
point, and (ideally) the initial spectrum will determine that no further
|
|
47
|
+
optimization is required.
|
|
48
|
+
|
|
49
|
+
Points to consider:
|
|
50
|
+
|
|
51
|
+
- consider rolling the "last optimization" measurement directly into the
|
|
52
|
+
averaged sample spectra (since it had its own throwaway and presumably
|
|
53
|
+
represents a "stable" reading). This could potentially allow for one more
|
|
54
|
+
averaged dark, depending on max_ms.
|
|
55
|
+
- ENLIGHTEN's laser button should still be able to turn the laser OFF at
|
|
56
|
+
any point.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
INTER_SPECTRA_DELAY_MS = 50
|
|
60
|
+
|
|
61
|
+
def __init__(self, wasatch_device):
|
|
62
|
+
self.wasatch_device = wasatch_device
|
|
63
|
+
|
|
64
|
+
self.progress_count = 0
|
|
65
|
+
self.progress_total = 0
|
|
66
|
+
self.optimizing = False
|
|
67
|
+
|
|
68
|
+
def measure(self, auto_raman_request):
|
|
69
|
+
if auto_raman_request.onboard:
|
|
70
|
+
return self.measure_firmware(auto_raman_request)
|
|
71
|
+
else:
|
|
72
|
+
return self.measure_software(auto_raman_request)
|
|
73
|
+
|
|
74
|
+
############################################################################
|
|
75
|
+
# #
|
|
76
|
+
# Firmware Implementation #
|
|
77
|
+
# #
|
|
78
|
+
############################################################################
|
|
79
|
+
|
|
80
|
+
def measure_firmware(self, auto_raman_request):
|
|
81
|
+
hardware = self.wasatch_device.hardware
|
|
82
|
+
|
|
83
|
+
# marshall the parameters into a binary payload
|
|
84
|
+
params = auto_raman_request.serialize()
|
|
85
|
+
log.debug(f"measure_firmware: params {utils.to_hex(params)}")
|
|
86
|
+
|
|
87
|
+
# generate a request for FID to execute (note this could be a BLEDevice in the future)
|
|
88
|
+
req = SpectrometerRequest("get_line", kwargs={"auto_raman_params": params})
|
|
89
|
+
log.debug(f"measure_firmware: req {req}")
|
|
90
|
+
|
|
91
|
+
# perform the measurement -- all the work occurs here
|
|
92
|
+
hardware.queue_message("progress_bar", -1)
|
|
93
|
+
result = hardware.handle_requests([req])[0]
|
|
94
|
+
hardware.queue_message("progress_bar", 100)
|
|
95
|
+
|
|
96
|
+
# process the result
|
|
97
|
+
if result is None or isinstance(result, bool) or result.error_msg != '':
|
|
98
|
+
raise(Exception(f"get_spectrum returned {result}"))
|
|
99
|
+
spectrum = result.data.spectrum
|
|
100
|
+
|
|
101
|
+
reading = Reading(hardware.device_id)
|
|
102
|
+
reading.spectrum = np.array(spectrum)
|
|
103
|
+
reading.dark = None
|
|
104
|
+
reading.averaged = True
|
|
105
|
+
reading.sum_count = 1 # todo: get_scans_to_average()
|
|
106
|
+
reading.new_integration_time_ms = None # todo: get_integration_time_ms()
|
|
107
|
+
reading.new_gain_db = None # todo: get_detector_gain()
|
|
108
|
+
reading.laser_enabled = True
|
|
109
|
+
reading.laser_power_perc = hardware.settings.state.laser_power_perc
|
|
110
|
+
reading.laser_power_mW = hardware.settings.state.laser_power_mW
|
|
111
|
+
|
|
112
|
+
return SpectrometerResponse(data=reading)
|
|
113
|
+
|
|
114
|
+
############################################################################
|
|
115
|
+
# #
|
|
116
|
+
# Software Implementation #
|
|
117
|
+
# #
|
|
118
|
+
############################################################################
|
|
119
|
+
|
|
120
|
+
def from_db_to_linear(self, x):
|
|
121
|
+
return 10 ** (x / 20.0)
|
|
122
|
+
|
|
123
|
+
def from_linear_to_db(self, x):
|
|
124
|
+
return 20 * math.log(x, 10)
|
|
125
|
+
|
|
126
|
+
def inter_spectrum_delay(self):
|
|
127
|
+
time.sleep(self.INTER_SPECTRA_DELAY_MS / 1000)
|
|
128
|
+
|
|
129
|
+
def measure_software(self, auto_raman_request):
|
|
130
|
+
"""
|
|
131
|
+
@returns a Reading wrapped in a SpectrometerResponse
|
|
132
|
+
"""
|
|
133
|
+
if auto_raman_request is None:
|
|
134
|
+
log.error("measure requires AutoRamanRequest")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
self.settings = self.wasatch_device.settings
|
|
138
|
+
self.hardware = self.wasatch_device.hardware
|
|
139
|
+
|
|
140
|
+
self.optimizing = True
|
|
141
|
+
|
|
142
|
+
log.debug(f"measure: auto_raman_request {auto_raman_request}")
|
|
143
|
+
self.bump_progress_bar()
|
|
144
|
+
|
|
145
|
+
# cache initial state
|
|
146
|
+
self.start_time = datetime.now()
|
|
147
|
+
initial_laser_warning_delay_sec = self.settings.state.laser_warning_delay_sec
|
|
148
|
+
|
|
149
|
+
# apply requested laser warning delay
|
|
150
|
+
if self.settings.is_xs():
|
|
151
|
+
self.hardware.set_laser_warning_delay_sec(auto_raman_request.laser_warning_delay_sec)
|
|
152
|
+
|
|
153
|
+
# generate auto-Raman measurement
|
|
154
|
+
reading = self.get_auto_spectrum(auto_raman_request)
|
|
155
|
+
if reading.spectrum is None:
|
|
156
|
+
log.debug("looks like Auto-Raman measurement was cancelled")
|
|
157
|
+
self.set_laser_enable(False)
|
|
158
|
+
|
|
159
|
+
# restore previous laser warning delay
|
|
160
|
+
if self.settings.is_xs():
|
|
161
|
+
self.hardware.set_laser_warning_delay_sec(initial_laser_warning_delay_sec)
|
|
162
|
+
|
|
163
|
+
return SpectrometerResponse(data=reading)
|
|
164
|
+
|
|
165
|
+
def bump_progress_bar(self):
|
|
166
|
+
if self.optimizing:
|
|
167
|
+
self.hardware.queue_message("progress_bar", -1)
|
|
168
|
+
else:
|
|
169
|
+
self.progress_count += 1
|
|
170
|
+
self.hardware.queue_message("progress_bar", 100 * (self.progress_count / self.progress_total))
|
|
171
|
+
|
|
172
|
+
def get_avg_spectrum(self, int_time, gain_db, num_avg, throwaway=True, first=None, label="unknown"):
|
|
173
|
+
""" Takes a single throwaway, then averages num_avg spectra """
|
|
174
|
+
|
|
175
|
+
if self.hardware.check_alert("auto_raman_cancel"):
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
self.set_integration_time_ms(int_time)
|
|
179
|
+
self.set_gain_db(gain_db)
|
|
180
|
+
self.inter_spectrum_delay()
|
|
181
|
+
|
|
182
|
+
# perform one throwaway
|
|
183
|
+
if throwaway:
|
|
184
|
+
throwaway = self.get_spectrum()
|
|
185
|
+
self.save(throwaway, f"{label} throwaway")
|
|
186
|
+
|
|
187
|
+
if self.hardware.check_alert("auto_raman_cancel"):
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if first is None:
|
|
191
|
+
sum_spectrum = np.zeros(self.settings.pixels())
|
|
192
|
+
start = 0
|
|
193
|
+
else:
|
|
194
|
+
# we were given the "first" spectrum for use in the average
|
|
195
|
+
sum_spectrum = first
|
|
196
|
+
start = 1
|
|
197
|
+
|
|
198
|
+
for i in range(start, num_avg):
|
|
199
|
+
self.bump_progress_bar()
|
|
200
|
+
|
|
201
|
+
spectrum = np.array(self.get_spectrum())
|
|
202
|
+
self.save(spectrum, f"{label} {i+1}/{num_avg}")
|
|
203
|
+
|
|
204
|
+
if self.hardware.check_alert("auto_raman_cancel"):
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
sum_spectrum += spectrum
|
|
208
|
+
self.inter_spectrum_delay()
|
|
209
|
+
|
|
210
|
+
return sum_spectrum / num_avg
|
|
211
|
+
|
|
212
|
+
def save(self, spectrum, label=None):
|
|
213
|
+
""" Save each spectrum in row-ordered CSV if debug environment variable enabled """
|
|
214
|
+
if "WASATCH_SAVE_AUTO_RAMAN" in os.environ:
|
|
215
|
+
with open("auto-raman-debug.csv", "a") as outfile:
|
|
216
|
+
now = datetime.now().strftime('%F %T.%f')[:-3]
|
|
217
|
+
values = ", ".join([f"{v:.2f}" for v in spectrum])
|
|
218
|
+
outfile.write(f"{now}, {label}, {values}\n")
|
|
219
|
+
|
|
220
|
+
def get_auto_spectrum(self, request):
|
|
221
|
+
"""
|
|
222
|
+
@returns a Reading with specturm, dark and sum_count populated
|
|
223
|
+
"""
|
|
224
|
+
log.debug(f"get_auto_spectrum: start (max_ms {request.max_ms})")
|
|
225
|
+
|
|
226
|
+
reading = Reading(self.hardware.device_id)
|
|
227
|
+
|
|
228
|
+
int_time = request.start_integ_ms
|
|
229
|
+
gain_db = request.start_gain_db
|
|
230
|
+
|
|
231
|
+
gain_linear = self.from_db_to_linear(gain_db)
|
|
232
|
+
min_gain_linear = self.from_db_to_linear(request.min_gain_db)
|
|
233
|
+
max_gain_linear = self.from_db_to_linear(request.max_gain_db)
|
|
234
|
+
|
|
235
|
+
num_avg = 1
|
|
236
|
+
|
|
237
|
+
# enable the laser and wait for it to fire
|
|
238
|
+
self.set_laser_enable(True)
|
|
239
|
+
self.hardware.queue_message("laser_firing_indicators", True)
|
|
240
|
+
warning_delay_sec = self.get_laser_warning_delay_sec()
|
|
241
|
+
if warning_delay_sec > 0:
|
|
242
|
+
self.hardware.queue_message("marquee_info", f"waiting {warning_delay_sec}sec for laser to fire")
|
|
243
|
+
time.sleep(warning_delay_sec)
|
|
244
|
+
|
|
245
|
+
laser_warmup_sec = self.settings.eeprom.laser_warmup_sec
|
|
246
|
+
if laser_warmup_sec > 0:
|
|
247
|
+
self.hardware.queue_message("marquee_info", f"waiting {laser_warmup_sec}sec for laser to stabilize")
|
|
248
|
+
time.sleep(laser_warmup_sec)
|
|
249
|
+
|
|
250
|
+
# get one Raman spectrum to start (no dark)
|
|
251
|
+
log.debug(f"taking initial spectrum (integ {int_time}, gain {gain_db})")
|
|
252
|
+
self.hardware.queue_message("marquee_info", "optimizing acquisition parameters")
|
|
253
|
+
spectrum = self.get_avg_spectrum(int_time, gain_db, num_avg=1, label="initial", throwaway=True)
|
|
254
|
+
if spectrum is None:
|
|
255
|
+
return reading
|
|
256
|
+
|
|
257
|
+
max_signal = spectrum.max()
|
|
258
|
+
|
|
259
|
+
# integration/gain scaling
|
|
260
|
+
quit_loop = False
|
|
261
|
+
loop_count = 0
|
|
262
|
+
while not quit_loop:
|
|
263
|
+
|
|
264
|
+
loop_count += 1
|
|
265
|
+
scale_factor = request.target_counts / max_signal
|
|
266
|
+
log.debug(f"loop {loop_count}: counts {max_signal}, scale {scale_factor:.2f}")
|
|
267
|
+
|
|
268
|
+
# We distribute scaling among integration time and linear gain
|
|
269
|
+
#
|
|
270
|
+
# mode: int time first
|
|
271
|
+
#
|
|
272
|
+
# if too small:
|
|
273
|
+
# 1. increase int time from start to max
|
|
274
|
+
# 2. increase gain from start to max
|
|
275
|
+
#
|
|
276
|
+
# if too large:
|
|
277
|
+
# 1. decrease gain to min
|
|
278
|
+
# 2. decrease int time
|
|
279
|
+
|
|
280
|
+
# in mode 'int time first':
|
|
281
|
+
# - we will first increase integration time - this will give best quality spectrum
|
|
282
|
+
# - if the integration time is at the maximum, we will increase gain to reach expected
|
|
283
|
+
# signal levels
|
|
284
|
+
|
|
285
|
+
prev_integration_time = int_time
|
|
286
|
+
prev_gain_db = gain_db
|
|
287
|
+
|
|
288
|
+
if scale_factor > 1.0:
|
|
289
|
+
|
|
290
|
+
# do not grow too fast
|
|
291
|
+
if scale_factor > request.max_factor:
|
|
292
|
+
scale_factor = request.max_factor
|
|
293
|
+
|
|
294
|
+
# increase int time first
|
|
295
|
+
log.debug(f"get_auto_spectrum: scaling int_time {int_time} UP by scale_factor {scale_factor:.2f}")
|
|
296
|
+
int_time *= scale_factor
|
|
297
|
+
|
|
298
|
+
# check int time does not exceed maximum
|
|
299
|
+
if int_time > request.max_integ_ms:
|
|
300
|
+
# however much int time exceeds the max, transfer scaling to gain
|
|
301
|
+
gain_factor = int_time / request.max_integ_ms
|
|
302
|
+
log.debug(f"get_auto_spectrum: scaling gain_linear {gain_linear} UP by gain_factor {gain_factor:.2f}")
|
|
303
|
+
gain_linear *= gain_factor
|
|
304
|
+
int_time = request.max_integ_ms
|
|
305
|
+
|
|
306
|
+
elif scale_factor < 1.0:
|
|
307
|
+
|
|
308
|
+
# if saturating, accelerate drop. do not use signal factor
|
|
309
|
+
if max_signal >= request.saturation:
|
|
310
|
+
scale_factor = request.drop_factor
|
|
311
|
+
|
|
312
|
+
# decrease gain first (INCREASE int time first implies DECREASE int time last)
|
|
313
|
+
log.debug(f"get_auto_spectrum: scaling gain_linear {gain_linear:.2f} DOWN by scale_factor {scale_factor:.2f}")
|
|
314
|
+
gain_linear *= scale_factor
|
|
315
|
+
|
|
316
|
+
# check we did not drop below min gain
|
|
317
|
+
if gain_linear < min_gain_linear:
|
|
318
|
+
# dump the overshoot into decreasing int time
|
|
319
|
+
int_factor = gain_linear / min_gain_linear
|
|
320
|
+
log.debug(f"get_auto_spectrum: scaling int_time {int_time} DOWN by int_factor {int_factor:.2f}")
|
|
321
|
+
int_time *= int_factor
|
|
322
|
+
gain_linear = min_gain_linear
|
|
323
|
+
|
|
324
|
+
# gain is rounded to 0.1 dB
|
|
325
|
+
gain_db = round(self.from_linear_to_db(gain_linear), 1)
|
|
326
|
+
gain_db = min(request.max_gain_db, max(gain_db, request.min_gain_db))
|
|
327
|
+
|
|
328
|
+
# integration time is integral (ms)
|
|
329
|
+
int_time = round(int_time)
|
|
330
|
+
|
|
331
|
+
if (int_time == prev_integration_time) and (gain_db == prev_gain_db):
|
|
332
|
+
# nothing has changed - rounding problem?
|
|
333
|
+
# Here we do not distinguish between int time forst and gain first, the changes are very small
|
|
334
|
+
if scale_factor > 1.0:
|
|
335
|
+
# was supposed to increase
|
|
336
|
+
|
|
337
|
+
# prefer to increase integration time
|
|
338
|
+
if int_time < request.max_integ_ms:
|
|
339
|
+
int_time += 1
|
|
340
|
+
else:
|
|
341
|
+
# failover to increasing gain
|
|
342
|
+
if self.settings.is_xs() and (gain_db < request.max_gain_db):
|
|
343
|
+
gain_db += 0.1
|
|
344
|
+
else:
|
|
345
|
+
quit_loop = True
|
|
346
|
+
else:
|
|
347
|
+
# was supposed to shrink
|
|
348
|
+
|
|
349
|
+
# prefer to shrink gain
|
|
350
|
+
if self.settings.is_xs() and (gain_db > request.min_gain_db):
|
|
351
|
+
gain_db -= 0.1
|
|
352
|
+
else:
|
|
353
|
+
# fail-over to shrinking integration
|
|
354
|
+
if int_time > request.min_integ_ms:
|
|
355
|
+
int_time -= 1
|
|
356
|
+
else:
|
|
357
|
+
quit_loop = True
|
|
358
|
+
|
|
359
|
+
log.debug(f"integ now {int_time}, gain now {gain_db:.1f} (linear {gain_linear:.4f})")
|
|
360
|
+
|
|
361
|
+
log.debug(f"Taking spectrum #{loop_count}")
|
|
362
|
+
self.hardware.queue_message("marquee_info", "optimizing acquisition parameters")
|
|
363
|
+
spectrum = self.get_avg_spectrum(int_time, gain_db, num_avg=1, label="optimizing", throwaway=True)
|
|
364
|
+
if spectrum is None:
|
|
365
|
+
return reading
|
|
366
|
+
|
|
367
|
+
max_signal = spectrum.max()
|
|
368
|
+
|
|
369
|
+
if max_signal < request.max_counts and max_signal > request.min_counts:
|
|
370
|
+
log.debug(f"===> achieved window (max_signal {max_signal} in range ({request.min_counts}, {request.max_counts}))")
|
|
371
|
+
quit_loop = True
|
|
372
|
+
elif max_signal < request.min_counts and int_time >= request.max_integ_ms and gain_db >= request.max_gain_db:
|
|
373
|
+
log.debug("can't achieve window within acquisition parameter limits")
|
|
374
|
+
quit_loop = True
|
|
375
|
+
|
|
376
|
+
# decide on number of averages - all times in ms
|
|
377
|
+
# include dark + signal
|
|
378
|
+
|
|
379
|
+
self.optimizing = False
|
|
380
|
+
total = math.floor(request.max_ms / int_time) # total number of sample + dark spectra we have time for
|
|
381
|
+
num_avg = math.ceil((total + 1) / 2) # how many darks to collect
|
|
382
|
+
num_avg = max(1, num_avg)
|
|
383
|
+
num_avg = min(num_avg, request.max_avg)
|
|
384
|
+
self.progress_count = 0
|
|
385
|
+
self.progress_total = 2 * num_avg - 1 # darks + remaining samples
|
|
386
|
+
expected_ms = int_time * self.progress_total
|
|
387
|
+
log.debug(f"based on max_ms {request.max_ms} and int_time {int_time} ms, computed num_avg {num_avg} (expected_ms {expected_ms})")
|
|
388
|
+
|
|
389
|
+
# now get the spectrum with the chosen parameters
|
|
390
|
+
# take two averaged spectra here: avg signal first, then turn laser off, then take dark
|
|
391
|
+
# (this saves the laser warm up)
|
|
392
|
+
|
|
393
|
+
# 1. signal - laser is still on
|
|
394
|
+
self.hardware.queue_message("marquee_info", f"averaging {num_avg} Raman spectra at {int_time}ms")
|
|
395
|
+
avg_sample = self.get_avg_spectrum(int_time, gain_db, num_avg, throwaway=False, first=spectrum, label="signal")
|
|
396
|
+
if avg_sample is None:
|
|
397
|
+
return reading
|
|
398
|
+
self.save(avg_sample, "averaged sample")
|
|
399
|
+
|
|
400
|
+
# 2. turn laser off
|
|
401
|
+
self.set_laser_enable(False)
|
|
402
|
+
self.hardware.queue_message("laser_firing_indicators", False)
|
|
403
|
+
|
|
404
|
+
# 3. take dark
|
|
405
|
+
self.hardware.queue_message("marquee_info", f"averaging {num_avg} dark spectra at {int_time}ms")
|
|
406
|
+
avg_dark = self.get_avg_spectrum(int_time, gain_db, num_avg, throwaway=True, label="dark")
|
|
407
|
+
if avg_dark is None:
|
|
408
|
+
return reading
|
|
409
|
+
self.save(avg_dark, "averaged dark")
|
|
410
|
+
|
|
411
|
+
# note that we don't actually perform dark subtraction here -- we return
|
|
412
|
+
# both the averaged Raman sample and the averaged dark, so that the
|
|
413
|
+
# caller can decide when / how to perform dark subtraction
|
|
414
|
+
|
|
415
|
+
reading.spectrum = avg_sample
|
|
416
|
+
reading.dark = avg_dark
|
|
417
|
+
reading.averaged = True
|
|
418
|
+
reading.sum_count = num_avg
|
|
419
|
+
reading.new_integration_time_ms = int_time
|
|
420
|
+
reading.new_gain_db = gain_db
|
|
421
|
+
reading.laser_enabled = True
|
|
422
|
+
reading.laser_power_perc = self.hardware.settings.state.laser_power_perc
|
|
423
|
+
reading.laser_power_mW = self.hardware.settings.state.laser_power_mW
|
|
424
|
+
|
|
425
|
+
log.debug("done")
|
|
426
|
+
return reading
|
|
427
|
+
|
|
428
|
+
############################################################################
|
|
429
|
+
# wrappers over stupidly complicated WasatchDevice interface
|
|
430
|
+
############################################################################
|
|
431
|
+
|
|
432
|
+
def get_laser_warning_delay_sec(self):
|
|
433
|
+
response = self.hardware.handle_requests([SpectrometerRequest('get_laser_warning_delay_sec')])[0]
|
|
434
|
+
log.debug(f"get_laser_warning_delay_sec: response {response}")
|
|
435
|
+
if response is None or response.data is None:
|
|
436
|
+
return 5
|
|
437
|
+
return response.data
|
|
438
|
+
|
|
439
|
+
def set_laser_enable(self, flag):
|
|
440
|
+
self.hardware.handle_requests([SpectrometerRequest('set_laser_enable', args=[flag])])
|
|
441
|
+
|
|
442
|
+
def set_integration_time_ms(self, ms):
|
|
443
|
+
if ms != self.settings.state.integration_time_ms:
|
|
444
|
+
self.hardware.handle_requests([SpectrometerRequest('set_integration_time_ms', args=[ms])])
|
|
445
|
+
|
|
446
|
+
def set_gain_db(self, db):
|
|
447
|
+
if self.settings.is_xs():
|
|
448
|
+
if abs(db - self.settings.state.gain_db) > 0.05:
|
|
449
|
+
self.hardware.handle_requests([SpectrometerRequest('set_detector_gain', args=[db])])
|
|
450
|
+
|
|
451
|
+
def get_spectrum(self):
|
|
452
|
+
result = self.hardware.handle_requests([SpectrometerRequest("get_line")])[0]
|
|
453
|
+
if result is None or isinstance(result, bool) or result.error_msg != '':
|
|
454
|
+
raise(Exception(f"get_spectrum returned {result}"))
|
|
455
|
+
spectrum = result.data.spectrum
|
|
456
|
+
self.inter_spectrum_delay()
|
|
457
|
+
return np.array(spectrum)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
class AutoRamanRequest:
|
|
2
|
+
|
|
3
|
+
MAX_MS = 10000
|
|
4
|
+
START_INTEG_MS = 100
|
|
5
|
+
START_GAIN_DB = 0
|
|
6
|
+
MAX_INTEG_MS = 2000
|
|
7
|
+
MIN_INTEG_MS = 10
|
|
8
|
+
MAX_GAIN_DB = 30
|
|
9
|
+
MIN_GAIN_DB = 0
|
|
10
|
+
TARGET_COUNTS = 45000
|
|
11
|
+
MAX_COUNTS = 50000
|
|
12
|
+
MIN_COUNTS = 40000
|
|
13
|
+
MAX_FACTOR = 5
|
|
14
|
+
DROP_FACTOR = 0.5
|
|
15
|
+
SATURATION = 65000
|
|
16
|
+
MAX_AVG = 100
|
|
17
|
+
LASER_WARNING_DELAY_SEC = 3
|
|
18
|
+
|
|
19
|
+
def __init__(self, max_ms = None,
|
|
20
|
+
start_integ_ms = None,
|
|
21
|
+
start_gain_db = None,
|
|
22
|
+
max_integ_ms = None,
|
|
23
|
+
min_integ_ms = None,
|
|
24
|
+
max_gain_db = None,
|
|
25
|
+
min_gain_db = None,
|
|
26
|
+
target_counts = None,
|
|
27
|
+
max_counts = None,
|
|
28
|
+
min_counts = None,
|
|
29
|
+
max_factor = None,
|
|
30
|
+
drop_factor = None,
|
|
31
|
+
saturation = None,
|
|
32
|
+
max_avg = None,
|
|
33
|
+
laser_warning_delay_sec = None,
|
|
34
|
+
onboard = False):
|
|
35
|
+
|
|
36
|
+
# this seems like it could be shorter, but Python doesn't allow
|
|
37
|
+
# 'max_ms = AutoRamanRequest.MAX_MS` constructor parameters
|
|
38
|
+
self.max_ms = max_ms if max_ms is not None else self.MAX_MS
|
|
39
|
+
self.start_integ_ms = start_integ_ms if start_integ_ms is not None else self.START_INTEG_MS
|
|
40
|
+
self.start_gain_db = start_gain_db if start_gain_db is not None else self.START_GAIN_DB
|
|
41
|
+
self.max_integ_ms = max_integ_ms if max_integ_ms is not None else self.MAX_INTEG_MS
|
|
42
|
+
self.min_integ_ms = min_integ_ms if min_integ_ms is not None else self.MIN_INTEG_MS
|
|
43
|
+
self.max_gain_db = max_gain_db if max_gain_db is not None else self.MAX_GAIN_DB
|
|
44
|
+
self.min_gain_db = min_gain_db if min_gain_db is not None else self.MIN_GAIN_DB
|
|
45
|
+
self.target_counts = target_counts if target_counts is not None else self.TARGET_COUNTS
|
|
46
|
+
self.max_counts = max_counts if max_counts is not None else self.MAX_COUNTS
|
|
47
|
+
self.min_counts = min_counts if min_counts is not None else self.MIN_COUNTS
|
|
48
|
+
self.max_factor = max_factor if max_factor is not None else self.MAX_FACTOR
|
|
49
|
+
self.drop_factor = drop_factor if drop_factor is not None else self.DROP_FACTOR
|
|
50
|
+
self.saturation = saturation if saturation is not None else self.SATURATION
|
|
51
|
+
self.max_avg = max_avg if max_avg is not None else self.MAX_AVG
|
|
52
|
+
self.laser_warning_delay_sec = laser_warning_delay_sec if laser_warning_delay_sec is not None else self.LASER_WARNING_DELAY_SEC
|
|
53
|
+
|
|
54
|
+
self.onboard = onboard
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
return f"AutoRamanRequest <onboard {self.onboard}, max_ms {self.max_ms}, start_integ_ms {self.start_integ_ms}, start_gain_db {self.start_gain_db}, max_integ_ms {self.max_integ_ms}, min_integ_ms {self.min_integ_ms}, max_gain_db {self.max_gain_db}, min_gain_db {self.min_gain_db}, target_counts {self.target_counts}, max_counts {self.max_counts}, min_counts {self.min_counts}, max_factor {self.max_factor}, drop_factor {self.drop_factor}, saturation {self.saturation}, max_avg {self.max_avg}, laser_warning_delay_sec {self.laser_warning_delay_sec}>"
|
|
58
|
+
|
|
59
|
+
# for use with onboard firmware-based AutoRaman
|
|
60
|
+
def serialize(self):
|
|
61
|
+
buf = []
|
|
62
|
+
buf.extend([self.max_ms & 0xff, (self.max_ms >> 8) & 0xff])
|
|
63
|
+
buf.extend([self.start_integ_ms & 0xff, (self.start_integ_ms>> 8) & 0xff])
|
|
64
|
+
buf.extend([self.start_gain_db & 0xff ])
|
|
65
|
+
buf.extend([self.max_integ_ms & 0xff, (self.max_integ_ms >> 8) & 0xff])
|
|
66
|
+
buf.extend([self.min_integ_ms & 0xff, (self.min_integ_ms >> 8) & 0xff])
|
|
67
|
+
buf.extend([self.max_gain_db & 0xff ])
|
|
68
|
+
buf.extend([self.min_gain_db & 0xff ])
|
|
69
|
+
buf.extend([self.target_counts & 0xff, (self.target_counts >> 8) & 0xff])
|
|
70
|
+
buf.extend([self.max_counts & 0xff, (self.max_counts >> 8) & 0xff])
|
|
71
|
+
buf.extend([self.min_counts & 0xff, (self.min_counts >> 8) & 0xff])
|
|
72
|
+
buf.extend([self.max_factor & 0xff ])
|
|
73
|
+
buf.extend([int(self.drop_factor) & 0xff, int((self.drop_factor - int(self.drop_factor)) * 256)])
|
|
74
|
+
buf.extend([self.saturation & 0xff, (self.saturation >> 8) & 0xff])
|
|
75
|
+
buf.extend([self.max_avg & 0xff ])
|
|
76
|
+
return buf
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
aar = AutoRamanRequest(
|
|
80
|
+
max_ms = 10000,
|
|
81
|
+
start_integ_ms = 100,
|
|
82
|
+
start_gain_db = 0,
|
|
83
|
+
max_integ_ms = 2000,
|
|
84
|
+
min_integ_ms = 10,
|
|
85
|
+
max_gain_db = 30,
|
|
86
|
+
min_gain_db = 0,
|
|
87
|
+
target_counts = 45000,
|
|
88
|
+
max_counts = 50000,
|
|
89
|
+
min_counts = 40000,
|
|
90
|
+
max_factor = 5,
|
|
91
|
+
drop_factor = 0.8,
|
|
92
|
+
saturation = 65000,
|
|
93
|
+
max_avg = 100)
|
|
94
|
+
|
|
95
|
+
buf = aar.serialize()
|
|
96
|
+
print("0x" + " ".join([f"{b:02x}" for b in buf]))
|