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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
log = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
class BalanceAcquisition:
|
|
7
|
+
|
|
8
|
+
INTEGRATION = 0
|
|
9
|
+
LASER = 1
|
|
10
|
+
LASER_THEN_INTEGRATION = 2
|
|
11
|
+
|
|
12
|
+
def __init__(self, mode=INTEGRATION, intensity=45000, threshold=2500, pixel=None, device=None, max_integration_time_ms=5000, max_tries=20):
|
|
13
|
+
self.mode = mode
|
|
14
|
+
self.intensity = intensity
|
|
15
|
+
self.threshold = threshold
|
|
16
|
+
self.pixel = pixel
|
|
17
|
+
self.device = device
|
|
18
|
+
self.max_tries = max_tries
|
|
19
|
+
self.max_integration_time_ms = min(max_integration_time_ms, device.settings.eeprom.max_integration_time_ms)
|
|
20
|
+
|
|
21
|
+
if not isinstance(self.mode, int):
|
|
22
|
+
self.mode = self.parse_mode(self.mode)
|
|
23
|
+
|
|
24
|
+
def using_integ(self):
|
|
25
|
+
return self.mode == self.INTEGRATION
|
|
26
|
+
|
|
27
|
+
def using_laser(self):
|
|
28
|
+
return self.mode == self.LASER
|
|
29
|
+
|
|
30
|
+
def balance(self):
|
|
31
|
+
if self.using_integ():
|
|
32
|
+
return self.balance_pass(self.adjust_integration)
|
|
33
|
+
elif self.using_laser():
|
|
34
|
+
return self.balance_pass(self.adjust_laser)
|
|
35
|
+
else:
|
|
36
|
+
# due to the way we're halving overshoots, the laser+integration
|
|
37
|
+
# combination likely adds little value over integration alone
|
|
38
|
+
if not self.balance_pass(self.adjust_laser):
|
|
39
|
+
return False
|
|
40
|
+
return self.balance_pass(self.adjust_integration)
|
|
41
|
+
|
|
42
|
+
def balance_pass(self, adjust_func):
|
|
43
|
+
if self.device is None:
|
|
44
|
+
log.error("missing device")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
self.overshoot_count = 0
|
|
48
|
+
try_count = 0
|
|
49
|
+
same_count = 0
|
|
50
|
+
while True:
|
|
51
|
+
self.device.change_setting("acquire", True, allow_immediate = False)
|
|
52
|
+
reading = self.device.acquire_data()
|
|
53
|
+
if reading is None or isinstance(reading, bool) or reading.spectrum is None:
|
|
54
|
+
log.error("failed to get spectrum")
|
|
55
|
+
return False
|
|
56
|
+
spectrum = reading.spectrum
|
|
57
|
+
|
|
58
|
+
state = self.device.settings.state
|
|
59
|
+
|
|
60
|
+
peak = spectrum[self.pixel] if self.pixel is not None else max(spectrum)
|
|
61
|
+
delta = self.intensity - peak
|
|
62
|
+
|
|
63
|
+
log.debug("integration_time_ms %d, laser_power %d, peak %d, delta %d",
|
|
64
|
+
state.integration_time_ms, state.laser_power, peak, delta)
|
|
65
|
+
|
|
66
|
+
# exit case
|
|
67
|
+
if abs(delta) <= self.threshold:
|
|
68
|
+
log.debug("balanced")
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
# adjust
|
|
72
|
+
last_value = state.integration_time_ms if self.using_integ() else state.laser_power
|
|
73
|
+
if not adjust_func(peak):
|
|
74
|
+
return False
|
|
75
|
+
new_value = state.integration_time_ms if self.using_integ() else state.laser_power
|
|
76
|
+
|
|
77
|
+
# check if we're up against a limit
|
|
78
|
+
if last_value == new_value:
|
|
79
|
+
same_count += 1
|
|
80
|
+
if same_count >= 3:
|
|
81
|
+
log.error("adjusted to same value (%s) %d times...giving up", last_value, same_count)
|
|
82
|
+
return False
|
|
83
|
+
else:
|
|
84
|
+
same_count = 0
|
|
85
|
+
|
|
86
|
+
try_count += 1
|
|
87
|
+
|
|
88
|
+
if try_count >= self.max_tries:
|
|
89
|
+
log.error("giving up after %d tries", try_count)
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def adjust_integration(self, peak):
|
|
93
|
+
state = self.device.settings.state
|
|
94
|
+
if peak > self.intensity:
|
|
95
|
+
n = int(state.integration_time_ms / 2)
|
|
96
|
+
self.overshoot_count += 1
|
|
97
|
+
if self.overshoot_count > 5:
|
|
98
|
+
log.error("too many overshoots")
|
|
99
|
+
return False
|
|
100
|
+
else:
|
|
101
|
+
n = int(1.0 * state.integration_time_ms * self.intensity / peak)
|
|
102
|
+
|
|
103
|
+
n = max(self.device.settings.eeprom.min_integration_time_ms, min(self.max_integration_time_ms, n))
|
|
104
|
+
|
|
105
|
+
log.debug("new integ = %d", n)
|
|
106
|
+
self.device.hardware.set_integration_time_ms(n)
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def adjust_laser(self, peak):
|
|
110
|
+
state = self.device.settings.state
|
|
111
|
+
if peak > self.intensity:
|
|
112
|
+
n = int(state.laser_power / 2)
|
|
113
|
+
self.overshoot_count += 1
|
|
114
|
+
if self.overshoot_count > 5:
|
|
115
|
+
log.error("too many overshoots")
|
|
116
|
+
return False
|
|
117
|
+
else:
|
|
118
|
+
n = int(1.0 * state.laser_power * self.intensity / peak)
|
|
119
|
+
|
|
120
|
+
n = max(1, min(100, n))
|
|
121
|
+
|
|
122
|
+
log.debug("new power = %d", n)
|
|
123
|
+
self.device.hardware.set_laser_power_perc(n)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def parse_mode(self, s):
|
|
127
|
+
s = s.strip().lower()
|
|
128
|
+
if re.match("integ", s):
|
|
129
|
+
return self.INTEGRATION
|
|
130
|
+
elif re.match("laser.*integ", s):
|
|
131
|
+
return self.LASER_THEN_INTEGRATION
|
|
132
|
+
elif re.match("laser", s):
|
|
133
|
+
return self.LASER
|
|
134
|
+
else:
|
|
135
|
+
raise Exception("invalid BalanceAcquisition mode: " + s)
|
|
136
|
+
|
wasatch/CSVLoader.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import csv
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .Reading import Reading
|
|
7
|
+
from .ProcessedReading import ProcessedReading
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# A file parser to deserialize one Measurement from a column-ordered CSV file.
|
|
13
|
+
#
|
|
14
|
+
# It looks like the basic usage is to instantiate it with a CSV pathname, call
|
|
15
|
+
# load_data() with or without scalar_metadata, then access the .processed_reading
|
|
16
|
+
# and .metadata elements. So, arguably this could have been a separate
|
|
17
|
+
# constructor for ProcessedReading?
|
|
18
|
+
#
|
|
19
|
+
# Given the similarity between the columnar CSV and "export" file formats, it
|
|
20
|
+
# would be SO TEMPTING to imagine you could easily generalize them. I thought
|
|
21
|
+
# they're just different enough that it would be a nightmare, so here we are.
|
|
22
|
+
#
|
|
23
|
+
# It is expected that this will be able to handle "raw" columnar CSV formats as
|
|
24
|
+
# well, which contain no metadata, but instead begin directly with the header
|
|
25
|
+
# row. In that case, wavelength and wavenumber are used directly from the input
|
|
26
|
+
# data, as no wavecal coefficients or excitation are available.
|
|
27
|
+
#
|
|
28
|
+
# Currently this class is used by at least three callers:
|
|
29
|
+
#
|
|
30
|
+
# - enlighten.parser.ColumnFileParser
|
|
31
|
+
# - wasatch.MockUSBDevice
|
|
32
|
+
# - an OEM plugin
|
|
33
|
+
# - maybe a shell script or two? (check enlighten/scripts)
|
|
34
|
+
#
|
|
35
|
+
class CSVLoader:
|
|
36
|
+
|
|
37
|
+
def __init__(self, pathname, encoding="utf-8"):
|
|
38
|
+
self.pathname = pathname
|
|
39
|
+
self.encoding = encoding
|
|
40
|
+
|
|
41
|
+
# default
|
|
42
|
+
self.timestamp = datetime.datetime.now()
|
|
43
|
+
|
|
44
|
+
# define buckets into which we will load the data in load_data()
|
|
45
|
+
self.metadata = {
|
|
46
|
+
"pixel": []
|
|
47
|
+
}
|
|
48
|
+
self.headers = []
|
|
49
|
+
self.processed_reading = ProcessedReading()
|
|
50
|
+
self.processed_reading.reading = Reading(device_id = "LOAD:" + pathname)
|
|
51
|
+
|
|
52
|
+
def __repr__(self):
|
|
53
|
+
return f"CSVLoader<{self.pathname}> with ProcessedReading {self.processed_reading} and metadata {self.metadata.keys()}"
|
|
54
|
+
|
|
55
|
+
def _parse_metadata(self, line, scalar_metadata=False):
|
|
56
|
+
"""
|
|
57
|
+
MZ: I'm not sure who is using this method and wants the metadata
|
|
58
|
+
values to be returned as lists, but it unnecessarily complicates
|
|
59
|
+
ColumnFileParser, so adding the scalar option.
|
|
60
|
+
"""
|
|
61
|
+
line = list(line)
|
|
62
|
+
key = line[0]
|
|
63
|
+
if scalar_metadata:
|
|
64
|
+
value = line[1]
|
|
65
|
+
else:
|
|
66
|
+
value = line[1:] # for lists with only one element this will give []
|
|
67
|
+
# Useful for cases like Declared Match,,,,,,,,
|
|
68
|
+
self.metadata[key] = value
|
|
69
|
+
|
|
70
|
+
def _parse_header(self, line):
|
|
71
|
+
self.headers = [ x.lower().strip() for x in line ] # force lowercase
|
|
72
|
+
if "processed" in self.headers: self.processed_reading.processed = []
|
|
73
|
+
if "raw" in self.headers: self.processed_reading.raw = []
|
|
74
|
+
if "dark" in self.headers: self.processed_reading.dark = []
|
|
75
|
+
if "reference" in self.headers: self.processed_reading.reference = []
|
|
76
|
+
if "corrected" in self.headers: self.processed_reading.processed = []
|
|
77
|
+
# log.debug("_parse_header: headers = %s", self.headers)
|
|
78
|
+
|
|
79
|
+
def load_data(self, scalar_metadata=False):
|
|
80
|
+
|
|
81
|
+
# create empty arrays for all of these so we have someplace to put data as we load lines
|
|
82
|
+
self.processed_reading.processed = []
|
|
83
|
+
self.processed_reading.raw = []
|
|
84
|
+
self.processed_reading.dark = []
|
|
85
|
+
self.processed_reading.reference = []
|
|
86
|
+
self.processed_reading.wavelengths = []
|
|
87
|
+
self.processed_reading.wavenumbers = []
|
|
88
|
+
|
|
89
|
+
state = "reading_metadata"
|
|
90
|
+
data_rows_read = 0
|
|
91
|
+
with open(self.pathname, "r", encoding=self.encoding) as infile:
|
|
92
|
+
csv_lines = csv.reader(infile)
|
|
93
|
+
for line in csv_lines:
|
|
94
|
+
# skip comments and blanks
|
|
95
|
+
if len(line) == 0 or line[0].startswith('#'):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
line[-1] = line[-1].strip() # remove the \n
|
|
99
|
+
# log.debug("load_data[%s]: %s", state, line)
|
|
100
|
+
|
|
101
|
+
if state == "reading_metadata":
|
|
102
|
+
|
|
103
|
+
# check for end of metadata
|
|
104
|
+
looks_like_header = False
|
|
105
|
+
for tok in [ part.strip().lower() for part in line ]:
|
|
106
|
+
if tok in ["pixel", "wavelength", "wavenumber", "processed", "intensity", "raw"]:
|
|
107
|
+
looks_like_header = True
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if looks_like_header:
|
|
111
|
+
self._parse_header(line)
|
|
112
|
+
state = "reading_data"
|
|
113
|
+
else:
|
|
114
|
+
# still in metadata
|
|
115
|
+
self._parse_metadata(line, scalar_metadata)
|
|
116
|
+
|
|
117
|
+
elif state == "reading_metadata_final":
|
|
118
|
+
self._parse_metadata(line, scalar_metadata)
|
|
119
|
+
|
|
120
|
+
elif state == "reading_data":
|
|
121
|
+
values = [x.strip() for x in line]
|
|
122
|
+
|
|
123
|
+
# if we find more metadata after data ended, store it but
|
|
124
|
+
# do not transition back
|
|
125
|
+
if not re.match(r'^[-+]?\d', values[0]):
|
|
126
|
+
state = "reading_metadata_final"
|
|
127
|
+
self._parse_metadata(line, scalar_metadata)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Assume each value read aligns with a known headers, but recognize that there
|
|
131
|
+
# could be more headers than there are values (some columns with headers may
|
|
132
|
+
# not actually have populated data, blank or otherwise). This is the number of
|
|
133
|
+
# comma-delimited fields actually read (or the number of headers, if more values
|
|
134
|
+
# were read than had headers).
|
|
135
|
+
count = min(len(self.headers), len(values))
|
|
136
|
+
# log.debug(f"parsing {count} fields")
|
|
137
|
+
|
|
138
|
+
for i in range(count):
|
|
139
|
+
header = self.headers[i]
|
|
140
|
+
# log.debug(f"parsing header {i} ({header})")
|
|
141
|
+
|
|
142
|
+
# SKIP nulls. Note we're APPENDING data to each list, so this means that
|
|
143
|
+
# if there are blanks (rather than '0' zeros) in the MIDDLE of a column,
|
|
144
|
+
# the resulting spectral matrix will have different-length columns and
|
|
145
|
+
# improperly-associated "rows".
|
|
146
|
+
value = values[i]
|
|
147
|
+
if len(value) == 0:
|
|
148
|
+
# log.debug(f"skipping value {i} ({value})")
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# MZ: honestly not sure if we should skip these or treat as zero
|
|
152
|
+
if value == "NA":
|
|
153
|
+
# log.debug(f"treating value {i} ({value}) as zero")
|
|
154
|
+
value = 0
|
|
155
|
+
|
|
156
|
+
# add to array
|
|
157
|
+
array = None
|
|
158
|
+
if header in ["processed", "intensity"]: array = self.processed_reading.processed
|
|
159
|
+
elif header == "corrected": array = self.processed_reading.processed
|
|
160
|
+
elif header == "raw": array = self.processed_reading.raw
|
|
161
|
+
elif header == "dark": array = self.processed_reading.dark
|
|
162
|
+
elif header == "reference": array = self.processed_reading.reference
|
|
163
|
+
elif header == "wavelength": array = self.processed_reading.wavelengths
|
|
164
|
+
elif header == "wavenumber": array = self.processed_reading.wavenumbers
|
|
165
|
+
elif header == "pixel": array = self.metadata["pixel"]
|
|
166
|
+
|
|
167
|
+
if array is not None:
|
|
168
|
+
# log.debug(f"appending to {header}: {value}")
|
|
169
|
+
array.append(float(value))
|
|
170
|
+
else:
|
|
171
|
+
log.error(f"load_data: null array? headers {self.headers}, header {header}, value {value}, line {line} ({self.pathname})")
|
|
172
|
+
|
|
173
|
+
if data_rows_read == 0 and header == "pixel" and int(value) != 0:
|
|
174
|
+
value = int(value)
|
|
175
|
+
# log.debug(f"first pixel = {value}")
|
|
176
|
+
self.processed_reading.first_pixel = value
|
|
177
|
+
|
|
178
|
+
data_rows_read += 1
|
|
179
|
+
|
|
180
|
+
# clear any arrays we ended up not filling
|
|
181
|
+
self.processed_reading.post_load_cleanup()
|
|
182
|
+
|
|
183
|
+
log.debug("returning processed_reading:")
|
|
184
|
+
self.processed_reading.dump()
|
|
185
|
+
|
|
186
|
+
log.debug(f"returning metadata {self.metadata}")
|
|
187
|
+
|
|
188
|
+
return self.processed_reading, self.metadata
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
log = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
## Not used - incomplete!
|
|
6
|
+
SETTINGS = {
|
|
7
|
+
# EEPROM
|
|
8
|
+
"adc_to_degC_coeffs": { "datatype": "float[]" },
|
|
9
|
+
"degC_to_dac_coeffs": { "datatype": "float[]" },
|
|
10
|
+
"wavelength_coeffs": { "datatype": "float[]" },
|
|
11
|
+
"update_eeprom": { "datatype": "object" },
|
|
12
|
+
"replace_eeprom": { "datatype": "object" },
|
|
13
|
+
"write_eeprom": { "datatype": "void" },
|
|
14
|
+
|
|
15
|
+
# Detector
|
|
16
|
+
"integration_time_ms": { "datatype": "int" },
|
|
17
|
+
"detector_tec_enable": { "datatype": "bool" },
|
|
18
|
+
"detector_tec_setpoint_degC": { "datatype": "int" },
|
|
19
|
+
"high_gain_mode_enable": { "datatype": "bool" },
|
|
20
|
+
|
|
21
|
+
# Laser
|
|
22
|
+
"laser_enable": { "datatype": "bool" },
|
|
23
|
+
"laser_power_perc": { "datatype": "float" },
|
|
24
|
+
"laser_power_mW": { "datatype": "float" },
|
|
25
|
+
"laser_temperature_setpoint_raw": { "datatype": "int" },
|
|
26
|
+
|
|
27
|
+
# Processing Modes
|
|
28
|
+
"area_scan_enable": { "datatype": "bool" },
|
|
29
|
+
"bad_pixel_mode": { "datatype": "int" },
|
|
30
|
+
"invert_x_axis": { "datatype": "bool" },
|
|
31
|
+
"scans_to_average": { "datatype": "int" },
|
|
32
|
+
|
|
33
|
+
# Secondary ADC
|
|
34
|
+
"enable_secondary_adc": { "datatype": "bool" },
|
|
35
|
+
|
|
36
|
+
# FPGA registers
|
|
37
|
+
"ccd_gain": { "datatype": "float" },
|
|
38
|
+
"ccd_offset": { "datatype": "int" },
|
|
39
|
+
|
|
40
|
+
# Triggering
|
|
41
|
+
"trigger_source": { "datatype": "int" },
|
|
42
|
+
|
|
43
|
+
# other
|
|
44
|
+
"log_level": { "datatype": "string" },
|
|
45
|
+
"max_usb_interval_ms": { "datatype": "int" },
|
|
46
|
+
"min_usb_interval_ms": { "datatype": "int" },
|
|
47
|
+
"reset_fpga": { "datatype": "void" },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# This class encapsulates information about the "ControlObject" settings
|
|
52
|
+
# supported by WasatchDevice hardware classes (FID and SP). These are
|
|
53
|
+
# traditionally called "settings" when passing ControlObjects from ENLIGHTEN
|
|
54
|
+
# down into Wasatch.PY via "settings queues", but are very different from
|
|
55
|
+
# SpectrometerSettings (actual settings of the spectrometer, vs commands
|
|
56
|
+
# being passed between processes).
|
|
57
|
+
#
|
|
58
|
+
# We're not actually using this at the moment, but it would be a way to provide
|
|
59
|
+
# some automated data validation and type-checking on callers like wasatch-shell.py
|
|
60
|
+
class CommandSettings:
|
|
61
|
+
|
|
62
|
+
def get_settings(self):
|
|
63
|
+
return sorted(SETTINGS.keys())
|
|
64
|
+
|
|
65
|
+
def get_datatype(self, setting):
|
|
66
|
+
if setting not in SETTINGS:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return SETTINGS[setting]["datatype"]
|
|
70
|
+
|
|
71
|
+
def valid(self, setting):
|
|
72
|
+
return setting in SETTINGS
|
|
73
|
+
|
|
74
|
+
def convert_type(self, setting, value):
|
|
75
|
+
if setting not in SETTINGS:
|
|
76
|
+
log.error("invalid setting: %s", setting)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
dt = SETTINGS[setting]["datatype"]
|
|
80
|
+
if dt == "bool":
|
|
81
|
+
return "true" in value.lower()
|
|
82
|
+
elif dt == "int":
|
|
83
|
+
return int(value)
|
|
84
|
+
elif dt == "float":
|
|
85
|
+
return float(value)
|
|
86
|
+
elif dt == "string":
|
|
87
|
+
return str(value)
|
|
88
|
+
elif dt == "float[]":
|
|
89
|
+
values = []
|
|
90
|
+
for tok in value.split(','):
|
|
91
|
+
values.append(float(tok))
|
|
92
|
+
return values
|
|
93
|
+
else:
|
|
94
|
+
log.debug("don't know how to convert %s %s settings", setting, dt)
|
|
95
|
+
return value
|
wasatch/ControlObject.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class ControlObject:
|
|
2
|
+
"""
|
|
3
|
+
A class holding setting-value pairs sent from ENLIGHTEN to the spectrometer.
|
|
4
|
+
Compare to StatusMessage as an outbound (spectrometer thread -> ENLIGHTEN) counterpart.
|
|
5
|
+
|
|
6
|
+
There is no enumeration of supported settings, but CommandSettings
|
|
7
|
+
is an (unused) step in that direction. For now, the true master list
|
|
8
|
+
would be the set implemented by FeatureIdentificationDevice.write_setting.
|
|
9
|
+
|
|
10
|
+
These are also used by the alert_queue.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, setting, value):
|
|
13
|
+
self.setting = setting
|
|
14
|
+
self.value = value
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return "%s -> %s" % (self.setting, self.value)
|
wasatch/DetectorROI.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
log = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
# This class represents a single Detector Region-of-Interest for a single
|
|
7
|
+
# "region" of DetectorRegions.
|
|
8
|
+
#
|
|
9
|
+
# We have not yet addressed the multiple wavecals required for DetectorRegions.
|
|
10
|
+
class DetectorROI:
|
|
11
|
+
def __init__(self, region, y0, y1, x0, x1, enabled=False):
|
|
12
|
+
self.region = region
|
|
13
|
+
self.y0 = y0
|
|
14
|
+
self.y1 = y1
|
|
15
|
+
self.x0 = x0
|
|
16
|
+
self.x1 = x1
|
|
17
|
+
|
|
18
|
+
self.enabled = enabled
|
|
19
|
+
|
|
20
|
+
def crop(self, a):
|
|
21
|
+
if self.x0 >= len(a) or self.x1 > len(a) + 1:
|
|
22
|
+
log.error("can't crop array exceeding ROI")
|
|
23
|
+
return a
|
|
24
|
+
return a[self.x0 : self.x1]
|
|
25
|
+
|
|
26
|
+
def width(self):
|
|
27
|
+
return self.x1 - self.x0 # not +1
|
|
28
|
+
|
|
29
|
+
def height(self):
|
|
30
|
+
return self.y1 - self.y0 # not +1
|
|
31
|
+
|
|
32
|
+
def __eq__(self, rhs):
|
|
33
|
+
return self.__dict__ == rhs.__dict__
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
return f"[DetectorROI: region {self.region} ({self.x0}, {self.y0}), ({self.x1}, {self.y1}), width {self.width()}, height {self.height()}, enabled {self.enabled}]"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from wasatch.DetectorROI import DetectorROI
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# This class encapsulates all of the DetectorROI regions that have been
|
|
9
|
+
# configured for the current detetor. If no DetectorROI regions have been
|
|
10
|
+
# configured for a detector, SpectrometerState.detector_regions may be None
|
|
11
|
+
# (indicating the full detector is being vertically binned and read-out).
|
|
12
|
+
#
|
|
13
|
+
# Note that if DetectorROI regions have been configured for a detector, the
|
|
14
|
+
# total number of pixels to be read-out from the detector may be either greater
|
|
15
|
+
# or less than the physical number of pixels.
|
|
16
|
+
#
|
|
17
|
+
# DetectorRegions are currently only supported on the Sony IMX series of
|
|
18
|
+
# detector.
|
|
19
|
+
#
|
|
20
|
+
# There are two other spectrometer features that are similar to or conceptually
|
|
21
|
+
# overlap DetectorRegions: HorizontalROI (aka vignetting or cropping), and
|
|
22
|
+
# start/stop lines (vertical ROI). This feature is definitely related to
|
|
23
|
+
# vertical ROI, but very different from vignetting because this actually
|
|
24
|
+
# changes the number of pixels read-out by the spectrometer during an
|
|
25
|
+
# acquisition.
|
|
26
|
+
#
|
|
27
|
+
# We have not yet addressed the multiple wavecals required for DetectorRegions.
|
|
28
|
+
class DetectorRegions:
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.regions = {}
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Will add or replace if region already exists.
|
|
35
|
+
#
|
|
36
|
+
# @param roi: DetectorROI
|
|
37
|
+
def add(self, roi):
|
|
38
|
+
self.regions[roi.region] = roi
|
|
39
|
+
|
|
40
|
+
def remove(self, roi):
|
|
41
|
+
region = None
|
|
42
|
+
if isinstance(roi, DetectorROI):
|
|
43
|
+
region = roi.region
|
|
44
|
+
else:
|
|
45
|
+
region = roi
|
|
46
|
+
self.regions.pop(region, None)
|
|
47
|
+
|
|
48
|
+
def count(self):
|
|
49
|
+
return len(self.regions)
|
|
50
|
+
|
|
51
|
+
def has_region(self, region): # -> bool
|
|
52
|
+
return region in self.regions
|
|
53
|
+
|
|
54
|
+
def get_roi(self, region):
|
|
55
|
+
return self.regions.get(region, None)
|
|
56
|
+
|
|
57
|
+
## guaranteed to be in the same order as split spectra
|
|
58
|
+
def get_region_list(self):
|
|
59
|
+
return [self.regions[region] for region in sorted(self.regions)]
|
|
60
|
+
|
|
61
|
+
## sum of widths of enabled regions
|
|
62
|
+
def total_pixels(self):
|
|
63
|
+
pixels = 0
|
|
64
|
+
for region in self.regions:
|
|
65
|
+
roi = self.regions[region]
|
|
66
|
+
if roi.enabled:
|
|
67
|
+
pixels += self.regions[region].width()
|
|
68
|
+
log.debug(f"total_pixels = {pixels}")
|
|
69
|
+
return pixels
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Given a long array (like wavelengths or wavenumbers) which was presumably
|
|
73
|
+
# generated for a full detector, chop into the sections indicated by the
|
|
74
|
+
# configured regions. Some elements of the source array may appear in
|
|
75
|
+
# multiple output arrays, if the regions overlap in x-coordinates.
|
|
76
|
+
#
|
|
77
|
+
# Differs from split() in that some of the data will be trimmed and left on
|
|
78
|
+
# the floor, if it doesn't fall within any configured region. Also, some
|
|
79
|
+
# input values may appear in multiple output arrays.
|
|
80
|
+
#
|
|
81
|
+
# Ignores disabled regions.
|
|
82
|
+
#
|
|
83
|
+
# Example:
|
|
84
|
+
# - passed 1920 wavelengths (based on active_pixels_horizontal)
|
|
85
|
+
# - configured region 0 is from (300, 1932) (pixel space)
|
|
86
|
+
# - function doesn't know that 1920 was defined as 1932 - 12 (the physical
|
|
87
|
+
# pixel range to which the wavelengths actually correspond)
|
|
88
|
+
# - all function knows is that new range is 1632 pixels wide -- not where
|
|
89
|
+
# those 1632 pixels fall within the 1920 (in this case, they SHOULD
|
|
90
|
+
# start at 300 - 12 = 288
|
|
91
|
+
# - basically, DetectorRegions needs to know the (x, y) in pixel
|
|
92
|
+
# space corresponding to 'a' -- (12, 1932) in this case
|
|
93
|
+
# - that is what orig_roi now provides
|
|
94
|
+
def chop(self, a, flatten=False, orig_roi=None):
|
|
95
|
+
log.debug(f"chopping array of {len(a)} pixels into {self.count()} subarrays")
|
|
96
|
+
subarrays = []
|
|
97
|
+
for region in sorted(self.regions):
|
|
98
|
+
roi = self.regions[region]
|
|
99
|
+
if not roi.enabled:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if roi.width() > len(a):
|
|
103
|
+
log.error(f"width ({roi.width}) of {roi.region} exceeds input array")
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
start = roi.x0 if orig_roi is None else orig_roi.start - roi.x0
|
|
107
|
+
log.debug(f"chop: region {roi.region} of width {roi.width()} from orig {orig_roi}")
|
|
108
|
+
subarray = a[start : start + roi.width() + 1]
|
|
109
|
+
log.debug(f"chop: subarray = {subarray[:3]} .. {subarray[-3:]}")
|
|
110
|
+
|
|
111
|
+
if flatten:
|
|
112
|
+
subarrays.extend(subarray)
|
|
113
|
+
else:
|
|
114
|
+
subarrays.append(subarray)
|
|
115
|
+
return subarrays
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Given a concatenated array (spectrum) which is logically composed of
|
|
119
|
+
# multiple shorter spectra, use the configured regions to split into the
|
|
120
|
+
# presumed constituent components.
|
|
121
|
+
#
|
|
122
|
+
# Differs from chop() in that no data is thrown on the floor; all is presumed
|
|
123
|
+
# part of one of the configured regions. Also any given source value will
|
|
124
|
+
# only go into a single output subspectrum.
|
|
125
|
+
#
|
|
126
|
+
# Note that split() could be called on the flattened result of chop() to dice
|
|
127
|
+
# a concatenated list of wavelengths for multiples regions back into
|
|
128
|
+
# individual per-region blocks.
|
|
129
|
+
#
|
|
130
|
+
# Ignores disabled regions.
|
|
131
|
+
def split(self, spectrum, flatten=False):
|
|
132
|
+
log.debug("splitting spectrum of %d pixels into %d subspectra", len(spectrum), self.count())
|
|
133
|
+
subspectra = []
|
|
134
|
+
start = 0
|
|
135
|
+
for region in sorted(self.regions):
|
|
136
|
+
roi = self.regions[region]
|
|
137
|
+
if not roi.enabled:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
end = start + roi.width()
|
|
141
|
+
if end > len(spectrum):
|
|
142
|
+
log.error("computed end %d of region %d overran colleted spectrum", end, region)
|
|
143
|
+
return None
|
|
144
|
+
subspectrum = spectrum[start:end]
|
|
145
|
+
log.debug(f"split: region {roi.region} of width {roi.width()}: {subspectrum[:3]} .. {subspectrum[-3:]}")
|
|
146
|
+
if flatten:
|
|
147
|
+
subspectra.extend(subspectrum)
|
|
148
|
+
else:
|
|
149
|
+
subspectra.append(subspectrum)
|
|
150
|
+
start = end
|
|
151
|
+
return subspectra
|
|
152
|
+
|
|
153
|
+
def __str__(self):
|
|
154
|
+
tok = []
|
|
155
|
+
for region in self.regions:
|
|
156
|
+
tok.append(str(self.regions[region]))
|
|
157
|
+
concat = ", ".join(tok)
|
|
158
|
+
return f"[DetectorRegions: count {self.count()}, total_pixels {self.total_pixels()}, regions: {concat}]"
|