waveorder 2.2.1b0__py3-none-any.whl → 3.0.0__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.
- waveorder/_version.py +16 -3
- waveorder/acq/__init__.py +0 -0
- waveorder/acq/acq_functions.py +166 -0
- waveorder/assets/HSV_legend.png +0 -0
- waveorder/assets/JCh_legend.png +0 -0
- waveorder/assets/waveorder_plugin_logo.png +0 -0
- waveorder/calib/Calibration.py +1512 -0
- waveorder/calib/Optimization.py +470 -0
- waveorder/calib/__init__.py +0 -0
- waveorder/calib/calibration_workers.py +464 -0
- waveorder/cli/apply_inverse_models.py +328 -0
- waveorder/cli/apply_inverse_transfer_function.py +379 -0
- waveorder/cli/compute_transfer_function.py +432 -0
- waveorder/cli/gui_widget.py +58 -0
- waveorder/cli/main.py +39 -0
- waveorder/cli/monitor.py +163 -0
- waveorder/cli/option_eat_all.py +47 -0
- waveorder/cli/parsing.py +122 -0
- waveorder/cli/printing.py +16 -0
- waveorder/cli/reconstruct.py +67 -0
- waveorder/cli/settings.py +187 -0
- waveorder/cli/utils.py +175 -0
- waveorder/filter.py +1 -2
- waveorder/focus.py +136 -25
- waveorder/io/__init__.py +0 -0
- waveorder/io/_reader.py +61 -0
- waveorder/io/core_functions.py +272 -0
- waveorder/io/metadata_reader.py +195 -0
- waveorder/io/utils.py +175 -0
- waveorder/io/visualization.py +160 -0
- waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
- waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
- waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
- waveorder/models/isotropic_thin_3d.py +73 -72
- waveorder/models/phase_thick_3d.py +103 -4
- waveorder/napari.yaml +36 -0
- waveorder/plugin/__init__.py +9 -0
- waveorder/plugin/gui.py +1094 -0
- waveorder/plugin/gui.ui +1440 -0
- waveorder/plugin/job_manager.py +42 -0
- waveorder/plugin/main_widget.py +1605 -0
- waveorder/plugin/tab_recon.py +3294 -0
- waveorder/scripts/__init__.py +0 -0
- waveorder/scripts/launch_napari.py +13 -0
- waveorder/scripts/repeat-cal-acq-rec.py +147 -0
- waveorder/scripts/repeat-calibration.py +31 -0
- waveorder/scripts/samples.py +85 -0
- waveorder/scripts/simulate_zarr_acq.py +204 -0
- waveorder/util.py +1 -1
- waveorder/visuals/napari_visuals.py +1 -1
- waveorder-3.0.0.dist-info/METADATA +350 -0
- waveorder-3.0.0.dist-info/RECORD +69 -0
- {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
- waveorder-3.0.0.dist-info/entry_points.txt +5 -0
- {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info/licenses}/LICENSE +13 -1
- waveorder-2.2.1b0.dist-info/METADATA +0 -187
- waveorder-2.2.1b0.dist-info/RECORD +0 -27
- {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1512 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from importlib_metadata import version
|
|
11
|
+
from iohub import open_ome_zarr
|
|
12
|
+
from iohub.ngff.models import TransformationMeta
|
|
13
|
+
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
|
|
14
|
+
from napari.utils.notifications import show_warning
|
|
15
|
+
from scipy.interpolate import interp1d
|
|
16
|
+
from scipy.optimize import least_squares
|
|
17
|
+
from scipy.stats import linregress
|
|
18
|
+
|
|
19
|
+
from waveorder.calib.Optimization import BrentOptimizer, MinScalarOptimizer
|
|
20
|
+
from waveorder.io.core_functions import *
|
|
21
|
+
from waveorder.io.utils import MockEmitter
|
|
22
|
+
|
|
23
|
+
LC_DEVICE_NAME = "MeadowlarkLC"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QLIPP_Calibration:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
mmc,
|
|
30
|
+
mm,
|
|
31
|
+
group="Channel",
|
|
32
|
+
lc_control_mode="MM-Retardance",
|
|
33
|
+
interp_method="schnoor_fit",
|
|
34
|
+
wavelength=532,
|
|
35
|
+
optimization="min_scalar",
|
|
36
|
+
print_details=True,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
mmc : object
|
|
43
|
+
Micro-Manager core instance
|
|
44
|
+
mm : object
|
|
45
|
+
Micro-Manager Studio instance
|
|
46
|
+
group : str
|
|
47
|
+
Name of the Micro-Manager channel group used defining LC states [State0, State1, State2, ...]
|
|
48
|
+
lc_control_mode : str
|
|
49
|
+
Defined the control mode of the liquid crystals. One of the following:
|
|
50
|
+
* MM-Retardance: The retardance of the LC is set directly through the Micro-Manager LC device adapter. The
|
|
51
|
+
Micro-Manager device adapter determines the corresponding voltage which is sent to the LC.
|
|
52
|
+
* MM-Voltage: The CalibrationData class in waveorder uses the LC calibration data to determine the correct
|
|
53
|
+
LC voltage for a given retardance. The LC voltage is set through the Micro-Manager LC device adapter.
|
|
54
|
+
* DAC: The CalibrationData class in waveorder uses the LC calibration data to determine the correct
|
|
55
|
+
LC voltage for a given retardance. The voltage is applied to the IO port of the LC controller through the
|
|
56
|
+
TriggerScope DAC outputs.
|
|
57
|
+
interp_method : str
|
|
58
|
+
Method of interpolating the LC retardance-to-voltage calibration curve. One of the following:
|
|
59
|
+
* linear: linear interpolation of retardance as a function of voltage and wavelength
|
|
60
|
+
* schnoor_fit: Schnoor fit interpolation as described in https://doi.org/10.1364/AO.408383
|
|
61
|
+
wavelength : float
|
|
62
|
+
Measurement wavelength
|
|
63
|
+
optimization : str
|
|
64
|
+
LC retardance optimization method, 'min_scalar' (default) or 'brent'
|
|
65
|
+
print_details : bool
|
|
66
|
+
Set verbose option
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Micro-Manager API
|
|
70
|
+
self.mm = mm
|
|
71
|
+
self.mmc = mmc
|
|
72
|
+
self.snap_manager = mm.getSnapLiveManager()
|
|
73
|
+
|
|
74
|
+
# Meadowlark LC Device Adapter Property Names
|
|
75
|
+
self.PROPERTIES = {
|
|
76
|
+
"LCA": (LC_DEVICE_NAME, "Retardance LC-A [in waves]"),
|
|
77
|
+
"LCB": (LC_DEVICE_NAME, "Retardance LC-B [in waves]"),
|
|
78
|
+
"LCA-Voltage": (LC_DEVICE_NAME, "Voltage (V) LC-A"),
|
|
79
|
+
"LCB-Voltage": (LC_DEVICE_NAME, "Voltage (V) LC-B"),
|
|
80
|
+
"LCA-DAC": ("TS_DAC01", "Volts"),
|
|
81
|
+
"LCB-DAC": ("TS_DAC02", "Volts"),
|
|
82
|
+
"State0": (
|
|
83
|
+
LC_DEVICE_NAME,
|
|
84
|
+
"Pal. elem. 00; enter 0 to define; 1 to activate",
|
|
85
|
+
),
|
|
86
|
+
"State1": (
|
|
87
|
+
LC_DEVICE_NAME,
|
|
88
|
+
"Pal. elem. 01; enter 0 to define; 1 to activate",
|
|
89
|
+
),
|
|
90
|
+
"State2": (
|
|
91
|
+
LC_DEVICE_NAME,
|
|
92
|
+
"Pal. elem. 02; enter 0 to define; 1 to activate",
|
|
93
|
+
),
|
|
94
|
+
"State3": (
|
|
95
|
+
LC_DEVICE_NAME,
|
|
96
|
+
"Pal. elem. 03; enter 0 to define; 1 to activate",
|
|
97
|
+
),
|
|
98
|
+
"State4": (
|
|
99
|
+
LC_DEVICE_NAME,
|
|
100
|
+
"Pal. elem. 04; enter 0 to define; 1 to activate",
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
self.group = group
|
|
104
|
+
|
|
105
|
+
# GUI Emitter
|
|
106
|
+
self.intensity_emitter = MockEmitter()
|
|
107
|
+
self.plot_sequence_emitter = MockEmitter()
|
|
108
|
+
|
|
109
|
+
# Set Mode
|
|
110
|
+
# TODO: make sure LC or TriggerScope are loaded in the respective modes
|
|
111
|
+
allowed_modes = ["MM-Retardance", "MM-Voltage", "DAC"]
|
|
112
|
+
if lc_control_mode not in allowed_modes:
|
|
113
|
+
raise ValueError(f"LC control mode must be one of {allowed_modes}")
|
|
114
|
+
self.mode = lc_control_mode
|
|
115
|
+
self.LC_DAC_conversion = 4 # convert between the input range of LCs (0-20V) and the output range of the DAC (0-5V)
|
|
116
|
+
|
|
117
|
+
# Initialize calibration class
|
|
118
|
+
allowed_interp_methods = ["schnoor_fit", "linear"]
|
|
119
|
+
if interp_method not in allowed_interp_methods:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"LC calibration data interpolation method must be one of "
|
|
122
|
+
f"{allowed_interp_methods}"
|
|
123
|
+
)
|
|
124
|
+
dir_path = mmc.getDeviceAdapterSearchPaths().get(
|
|
125
|
+
0
|
|
126
|
+
) # MM device adapter directory
|
|
127
|
+
self.calib = CalibrationData(
|
|
128
|
+
os.path.join(dir_path, "mmgr_dal_MeadowlarkLC.csv"),
|
|
129
|
+
interp_method=interp_method,
|
|
130
|
+
wavelength=wavelength,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Optimizer
|
|
134
|
+
if optimization == "min_scalar":
|
|
135
|
+
self.optimizer = MinScalarOptimizer(self)
|
|
136
|
+
elif optimization == "brent":
|
|
137
|
+
self.optimizer = BrentOptimizer(self)
|
|
138
|
+
else:
|
|
139
|
+
raise ModuleNotFoundError(f"No optimizer named {optimization}")
|
|
140
|
+
|
|
141
|
+
# User / Calculated Parameters
|
|
142
|
+
self.swing = None
|
|
143
|
+
self.wavelength = None
|
|
144
|
+
self.lc_bound = None
|
|
145
|
+
self.I_Black = None
|
|
146
|
+
self.ratio = 1.793
|
|
147
|
+
self.print_details = print_details
|
|
148
|
+
self.calib_scheme = "4-State"
|
|
149
|
+
|
|
150
|
+
# LC States
|
|
151
|
+
self.lca_ext = None
|
|
152
|
+
self.lcb_ext = None
|
|
153
|
+
self.lca_0 = None
|
|
154
|
+
self.lcb_0 = None
|
|
155
|
+
self.lca_45 = None
|
|
156
|
+
self.lcb_45 = None
|
|
157
|
+
self.lca_60 = None
|
|
158
|
+
self.lcb_60 = None
|
|
159
|
+
self.lca_90 = None
|
|
160
|
+
self.lcb_90 = None
|
|
161
|
+
self.lca_120 = None
|
|
162
|
+
self.lcb_120 = None
|
|
163
|
+
self.lca_135 = None
|
|
164
|
+
self.lcb_135 = None
|
|
165
|
+
|
|
166
|
+
# Calibration Outputs
|
|
167
|
+
self.I_Ext = None
|
|
168
|
+
self.I_Ref = None
|
|
169
|
+
self.I_Elliptical = None
|
|
170
|
+
self.inten = []
|
|
171
|
+
self.swing0 = None
|
|
172
|
+
self.swing45 = None
|
|
173
|
+
self.swing60 = None
|
|
174
|
+
self.swing90 = None
|
|
175
|
+
self.swing120 = None
|
|
176
|
+
self.swing135 = None
|
|
177
|
+
self.height = None
|
|
178
|
+
self.width = None
|
|
179
|
+
self.directory = None
|
|
180
|
+
self.inst_mat = None
|
|
181
|
+
|
|
182
|
+
# Shutter
|
|
183
|
+
self.shutter_device = self.mmc.getShutterDevice()
|
|
184
|
+
self._auto_shutter_state = None
|
|
185
|
+
self._shutter_state = None
|
|
186
|
+
|
|
187
|
+
def set_dacs(self, lca_dac, lcb_dac):
|
|
188
|
+
self.PROPERTIES["LCA-DAC"] = (f"TS_{lca_dac}", "Volts")
|
|
189
|
+
self.PROPERTIES["LCB-DAC"] = (f"TS_{lcb_dac}", "Volts")
|
|
190
|
+
|
|
191
|
+
def set_wavelength(self, wavelength):
|
|
192
|
+
self.calib.set_wavelength(wavelength)
|
|
193
|
+
self.wavelength = self.calib.wavelength
|
|
194
|
+
|
|
195
|
+
def set_lc(self, retardance, LC: str):
|
|
196
|
+
"""
|
|
197
|
+
Set LC state to given retardance in waves
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
retardance : float
|
|
202
|
+
Retardance in waves
|
|
203
|
+
LC : str
|
|
204
|
+
LCA or LCB
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
if self.mode == "MM-Retardance":
|
|
212
|
+
set_lc_waves(self.mmc, self.PROPERTIES[f"{LC}"], retardance)
|
|
213
|
+
elif self.mode == "MM-Voltage":
|
|
214
|
+
volts = self.calib.get_voltage(retardance)
|
|
215
|
+
set_lc_voltage(self.mmc, self.PROPERTIES[f"{LC}-Voltage"], volts)
|
|
216
|
+
elif self.mode == "DAC":
|
|
217
|
+
volts = self.calib.get_voltage(retardance)
|
|
218
|
+
dac_volts = volts / self.LC_DAC_conversion
|
|
219
|
+
set_lc_daq(self.mmc, self.PROPERTIES[f"{LC}-DAC"], dac_volts)
|
|
220
|
+
|
|
221
|
+
def get_lc(self, LC: str):
|
|
222
|
+
"""
|
|
223
|
+
Get LC retardance in waves
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
LC : str
|
|
228
|
+
LCA or LCB
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
LC retardance in waves
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
if self.mode == "MM-Retardance":
|
|
236
|
+
retardance = get_lc(self.mmc, self.PROPERTIES[f"{LC}"])
|
|
237
|
+
elif self.mode == "MM-Voltage":
|
|
238
|
+
volts = get_lc(
|
|
239
|
+
self.mmc, self.PROPERTIES[f"{LC}-Voltage"]
|
|
240
|
+
) # returned value is in volts
|
|
241
|
+
retardance = self.calib.get_retardance(volts)
|
|
242
|
+
elif self.mode == "DAC":
|
|
243
|
+
dac_volts = get_lc(self.mmc, self.PROPERTIES[f"{LC}-DAC"])
|
|
244
|
+
volts = dac_volts * self.LC_DAC_conversion
|
|
245
|
+
retardance = self.calib.get_retardance(volts)
|
|
246
|
+
|
|
247
|
+
return retardance
|
|
248
|
+
|
|
249
|
+
def define_lc_state(self, state, lca_retardance, lcb_retardance):
|
|
250
|
+
"""
|
|
251
|
+
Define of the two LCs after calibration
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
state: str
|
|
256
|
+
Polarization stage (e.g. State0)
|
|
257
|
+
lca_retardance: float
|
|
258
|
+
LCA retardance in waves
|
|
259
|
+
lcb_retardance: float
|
|
260
|
+
LCB retardance in waves
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if self.mode == "MM-Retardance":
|
|
268
|
+
self.set_lc(lca_retardance, "LCA")
|
|
269
|
+
self.set_lc(lcb_retardance, "LCB")
|
|
270
|
+
define_meadowlark_state(self.mmc, self.PROPERTIES[state])
|
|
271
|
+
elif self.mode == "DAC":
|
|
272
|
+
lca_volts = (
|
|
273
|
+
self.calib.get_voltage(lca_retardance) / self.LC_DAC_conversion
|
|
274
|
+
)
|
|
275
|
+
lcb_volts = (
|
|
276
|
+
self.calib.get_voltage(lcb_retardance) / self.LC_DAC_conversion
|
|
277
|
+
)
|
|
278
|
+
define_config_state(
|
|
279
|
+
self.mmc,
|
|
280
|
+
self.group,
|
|
281
|
+
state,
|
|
282
|
+
[self.PROPERTIES["LCA-DAC"], self.PROPERTIES["LCB-DAC"]],
|
|
283
|
+
[lca_volts, lcb_volts],
|
|
284
|
+
)
|
|
285
|
+
elif self.mode == "MM-Voltage":
|
|
286
|
+
lca_volts = self.calib.get_voltage(lca_retardance)
|
|
287
|
+
lcb_volts = self.calib.get_voltage(lcb_retardance)
|
|
288
|
+
define_config_state(
|
|
289
|
+
self.mmc,
|
|
290
|
+
self.group,
|
|
291
|
+
state,
|
|
292
|
+
[
|
|
293
|
+
self.PROPERTIES["LCA-Voltage"],
|
|
294
|
+
self.PROPERTIES["LCB-Voltage"],
|
|
295
|
+
],
|
|
296
|
+
[lca_volts, lcb_volts],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def opt_lc(self, x, device_property, reference, normalize=False):
|
|
300
|
+
if isinstance(x, list) or isinstance(x, tuple):
|
|
301
|
+
x = x[0]
|
|
302
|
+
|
|
303
|
+
self.set_lc(x, device_property)
|
|
304
|
+
|
|
305
|
+
mean = snap_and_average(self.snap_manager)
|
|
306
|
+
|
|
307
|
+
if normalize:
|
|
308
|
+
max_ = 65335
|
|
309
|
+
min_ = self.I_Black
|
|
310
|
+
|
|
311
|
+
val = (mean - min_) / (max_ - min_)
|
|
312
|
+
ref = (reference - min_) / (max_ - min_)
|
|
313
|
+
|
|
314
|
+
logging.debug(f"LC-Value: {x}")
|
|
315
|
+
logging.debug(f"F-Value:{val - ref}\n")
|
|
316
|
+
return val - ref
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
logging.debug(str(mean))
|
|
320
|
+
self.intensity_emitter.emit(mean)
|
|
321
|
+
self.inten.append(mean - reference)
|
|
322
|
+
|
|
323
|
+
return np.abs(mean - reference)
|
|
324
|
+
|
|
325
|
+
def opt_lc_cons(self, x, device_property, reference, mode):
|
|
326
|
+
self.set_lc(x, device_property)
|
|
327
|
+
swing = (self.lca_ext - x) * self.ratio
|
|
328
|
+
|
|
329
|
+
if mode == "60":
|
|
330
|
+
self.set_lc(self.lcb_ext + swing, "LCB")
|
|
331
|
+
|
|
332
|
+
if mode == "120":
|
|
333
|
+
self.set_lc(self.lcb_ext - swing, "LCB")
|
|
334
|
+
|
|
335
|
+
mean = snap_and_average(self.snap_manager)
|
|
336
|
+
logging.debug(str(mean))
|
|
337
|
+
|
|
338
|
+
# append to intensity array for plotting later
|
|
339
|
+
self.intensity_emitter.emit(mean)
|
|
340
|
+
self.inten.append(mean - reference)
|
|
341
|
+
|
|
342
|
+
return np.abs(mean - reference)
|
|
343
|
+
|
|
344
|
+
def opt_lc_grid(self, a_min, a_max, b_min, b_max, step):
|
|
345
|
+
"""
|
|
346
|
+
Exhaustive Search method
|
|
347
|
+
|
|
348
|
+
Finds the minimum intensity value for a given
|
|
349
|
+
grid of LCA,LCB values
|
|
350
|
+
|
|
351
|
+
:param a_min: float
|
|
352
|
+
Minimum value of LCA
|
|
353
|
+
:param a_max: float
|
|
354
|
+
Maximum value of LCA
|
|
355
|
+
:param b_min: float
|
|
356
|
+
Minimum value of LCB
|
|
357
|
+
:param b_max: float
|
|
358
|
+
Maximum value of LCB
|
|
359
|
+
:param step: float
|
|
360
|
+
step size of the grid between max/min values
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
:return best_lca: float
|
|
364
|
+
LCA value corresponding to lowest mean Intensity
|
|
365
|
+
:return best_lcb: float
|
|
366
|
+
LCB value corresponding to lowest mean Intensity
|
|
367
|
+
:return min_int: float
|
|
368
|
+
Lowest value of mean Intensity
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
min_int = 65536
|
|
372
|
+
better_lca = -1
|
|
373
|
+
better_lcb = -1
|
|
374
|
+
|
|
375
|
+
# coarse search
|
|
376
|
+
for lca in np.arange(a_min, a_max, step):
|
|
377
|
+
for lcb in np.arange(b_min, b_max, step):
|
|
378
|
+
self.set_lc(lca, "LCA")
|
|
379
|
+
self.set_lc(lcb, "LCB")
|
|
380
|
+
|
|
381
|
+
# current_int = np.mean(snap_image(calib.mmc))
|
|
382
|
+
current_int = snap_and_average(self.snap_manager)
|
|
383
|
+
self.intensity_emitter.emit(current_int)
|
|
384
|
+
|
|
385
|
+
if current_int < min_int:
|
|
386
|
+
better_lca = lca
|
|
387
|
+
better_lcb = lcb
|
|
388
|
+
min_int = current_int
|
|
389
|
+
logging.debug(
|
|
390
|
+
"update (%f, %f, %f)"
|
|
391
|
+
% (min_int, better_lca, better_lcb)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
logging.debug("coarse search done")
|
|
395
|
+
logging.debug("better lca = " + str(better_lca))
|
|
396
|
+
logging.debug("better lcb = " + str(better_lcb))
|
|
397
|
+
logging.debug("better int = " + str(min_int))
|
|
398
|
+
|
|
399
|
+
best_lca = better_lca
|
|
400
|
+
best_lcb = better_lcb
|
|
401
|
+
|
|
402
|
+
return best_lca, best_lcb, min_int
|
|
403
|
+
|
|
404
|
+
# ========== Optimization wrappers =============
|
|
405
|
+
# ==============================================
|
|
406
|
+
def opt_Iext(self):
|
|
407
|
+
self.plot_sequence_emitter.emit("Coarse")
|
|
408
|
+
logging.info("Calibrating State0 (Extinction)...")
|
|
409
|
+
logging.debug("Calibrating State0 (Extinction)...")
|
|
410
|
+
|
|
411
|
+
set_lc_state(self.mmc, self.group, "State0")
|
|
412
|
+
time.sleep(2)
|
|
413
|
+
|
|
414
|
+
# Perform exhaustive search with step 0.1 over range:
|
|
415
|
+
# 0.01 < LCA < 0.5
|
|
416
|
+
# 0.25 < LCB < 0.75
|
|
417
|
+
step = 0.1
|
|
418
|
+
logging.debug(f"================================")
|
|
419
|
+
logging.debug(f"Starting first grid search, step = {step}")
|
|
420
|
+
logging.debug(f"================================")
|
|
421
|
+
|
|
422
|
+
best_lca, best_lcb, i_ext_ = self.opt_lc_grid(
|
|
423
|
+
0.01, 0.5, 0.25, 0.75, step
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
logging.debug("grid search done")
|
|
427
|
+
logging.debug("lca = " + str(best_lca))
|
|
428
|
+
logging.debug("lcb = " + str(best_lcb))
|
|
429
|
+
logging.debug("intensity = " + str(i_ext_))
|
|
430
|
+
|
|
431
|
+
self.set_lc(best_lca, "LCA")
|
|
432
|
+
self.set_lc(best_lcb, "LCB")
|
|
433
|
+
|
|
434
|
+
logging.debug(f"================================")
|
|
435
|
+
logging.debug(f"Starting fine search")
|
|
436
|
+
logging.debug(f"================================")
|
|
437
|
+
|
|
438
|
+
# Perform brent optimization around results of 2nd grid search
|
|
439
|
+
# threshold not very necessary here as intensity value will
|
|
440
|
+
# vary between exposure/lamp intensities
|
|
441
|
+
self.plot_sequence_emitter.emit("Fine")
|
|
442
|
+
lca, lcb, I_ext = self.optimizer.optimize(
|
|
443
|
+
state="ext",
|
|
444
|
+
lca_bound=0.1,
|
|
445
|
+
lcb_bound=0.1,
|
|
446
|
+
reference=self.I_Black,
|
|
447
|
+
thresh=1,
|
|
448
|
+
n_iter=5,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Set the Extinction state to values output from optimization
|
|
452
|
+
self.define_lc_state("State0", lca, lcb)
|
|
453
|
+
|
|
454
|
+
self.lca_ext = lca
|
|
455
|
+
self.lcb_ext = lcb
|
|
456
|
+
self.I_Ext = I_ext
|
|
457
|
+
|
|
458
|
+
logging.debug("fine search done")
|
|
459
|
+
logging.info(f"LCA State0 (Extinction) = {lca:.3f}")
|
|
460
|
+
logging.debug(f"LCA State0 (Extinction) = {lca:.5f}")
|
|
461
|
+
logging.info(f"LCB State0 (Extinction) = {lcb:.3f}")
|
|
462
|
+
logging.debug(f"LCB State0 (Extinction) = {lcb:.5f}")
|
|
463
|
+
logging.info(f"Intensity (Extinction) = {I_ext:.0f}")
|
|
464
|
+
logging.debug(f"Intensity (Extinction) = {I_ext:.3f}")
|
|
465
|
+
|
|
466
|
+
logging.debug("--------done--------")
|
|
467
|
+
logging.info("--------done--------")
|
|
468
|
+
|
|
469
|
+
def opt_I0(self):
|
|
470
|
+
"""
|
|
471
|
+
no optimization performed for this. Simply apply swing and read intensity
|
|
472
|
+
This is the same as "Ielliptical". Used for both schemes.
|
|
473
|
+
:return: float
|
|
474
|
+
mean of image
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
logging.info("Calibrating State1 (I0)...")
|
|
478
|
+
logging.debug("Calibrating State1 (I0)...")
|
|
479
|
+
|
|
480
|
+
self.lca_0 = self.lca_ext - self.swing
|
|
481
|
+
self.lcb_0 = self.lcb_ext
|
|
482
|
+
self.set_lc(self.lca_0, "LCA")
|
|
483
|
+
self.set_lc(self.lcb_0, "LCB")
|
|
484
|
+
|
|
485
|
+
self.define_lc_state("State1", self.lca_0, self.lcb_0)
|
|
486
|
+
intensity = snap_and_average(self.snap_manager)
|
|
487
|
+
self.I_Elliptical = intensity
|
|
488
|
+
self.swing0 = np.sqrt(
|
|
489
|
+
(self.lcb_0 - self.lcb_ext) ** 2 + (self.lca_0 - self.lca_ext) ** 2
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
logging.info(f"LCA State1 (I0) = {self.lca_0:.3f}")
|
|
493
|
+
logging.debug(f"LCA State1 (I0) = {self.lca_0:.5f}")
|
|
494
|
+
logging.info(f"LCB State1 (I0) = {self.lcb_0:.3f}")
|
|
495
|
+
logging.debug(f"LCB State1 (I0) = {self.lcb_0:.5f}")
|
|
496
|
+
logging.info(f"Intensity (I0) = {intensity:.0f}")
|
|
497
|
+
logging.debug(f"Intensity (I0) = {intensity:.3f}")
|
|
498
|
+
logging.info("--------done--------")
|
|
499
|
+
logging.debug("--------done--------")
|
|
500
|
+
|
|
501
|
+
def opt_I45(self, lca_bound, lcb_bound):
|
|
502
|
+
"""
|
|
503
|
+
optimized relative to Ielliptical (opt_I90)
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
lca_bound
|
|
507
|
+
lcb_bound
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
lca, lcb value at optimized state
|
|
511
|
+
intensity value at optimized state
|
|
512
|
+
"""
|
|
513
|
+
self.inten = []
|
|
514
|
+
logging.info("Calibrating State2 (I45)...")
|
|
515
|
+
logging.debug("Calibrating State2 (I45)...")
|
|
516
|
+
|
|
517
|
+
self.set_lc(self.lca_ext, "LCA")
|
|
518
|
+
self.set_lc(self.lcb_ext - self.swing, "LCB")
|
|
519
|
+
|
|
520
|
+
self.lca_45, self.lcb_45, intensity = self.optimizer.optimize(
|
|
521
|
+
"45",
|
|
522
|
+
lca_bound,
|
|
523
|
+
lcb_bound,
|
|
524
|
+
reference=self.I_Elliptical,
|
|
525
|
+
n_iter=5,
|
|
526
|
+
thresh=0.01,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
self.define_lc_state("State2", self.lca_45, self.lcb_45)
|
|
530
|
+
|
|
531
|
+
self.swing45 = np.sqrt(
|
|
532
|
+
(self.lcb_45 - self.lcb_ext) ** 2
|
|
533
|
+
+ (self.lca_45 - self.lca_ext) ** 2
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
logging.info(f"LCA State2 (I45) = {self.lca_45:.3f}")
|
|
537
|
+
logging.debug(f"LCA State2 (I45) = {self.lca_45:.5f}")
|
|
538
|
+
logging.info(f"LCB State2 (I45) = {self.lcb_45:.3f}")
|
|
539
|
+
logging.debug(f"LCB State2 (I45) = {self.lcb_45:.5f}")
|
|
540
|
+
logging.info(f"Intensity (I45) = {intensity:.0f}")
|
|
541
|
+
logging.debug(f"Intensity (I45) = {intensity:.3f}")
|
|
542
|
+
logging.info("--------done--------")
|
|
543
|
+
logging.debug("--------done--------")
|
|
544
|
+
|
|
545
|
+
def opt_I60(self, lca_bound, lcb_bound):
|
|
546
|
+
"""
|
|
547
|
+
optimized relative to Ielliptical (opt_I0_4State)
|
|
548
|
+
Parameters
|
|
549
|
+
----------
|
|
550
|
+
lca_bound
|
|
551
|
+
lcb_bound
|
|
552
|
+
Returns
|
|
553
|
+
-------
|
|
554
|
+
lca, lcb value at optimized state
|
|
555
|
+
intensity value at optimized state
|
|
556
|
+
"""
|
|
557
|
+
self.inten = []
|
|
558
|
+
|
|
559
|
+
logging.info("Calibrating State2 (I60)...")
|
|
560
|
+
logging.debug("Calibrating State2 (I60)...")
|
|
561
|
+
|
|
562
|
+
# Calculate Initial Swing for initial guess to optimize around
|
|
563
|
+
# Based on ratio calculated from ellpiticity/orientation of LC simulation
|
|
564
|
+
swing_ell = np.sqrt(
|
|
565
|
+
(self.lca_ext - self.lca_0) ** 2 + (self.lcb_ext - self.lcb_0) ** 2
|
|
566
|
+
)
|
|
567
|
+
lca_swing = np.sqrt(swing_ell**2 / (1 + self.ratio**2))
|
|
568
|
+
lcb_swing = self.ratio * lca_swing
|
|
569
|
+
|
|
570
|
+
# Optimization
|
|
571
|
+
self.set_lc(self.lca_ext + lca_swing, "LCA")
|
|
572
|
+
self.set_lc(self.lcb_ext + lcb_swing, "LCB")
|
|
573
|
+
|
|
574
|
+
self.lca_60, self.lcb_60, intensity = self.optimizer.optimize(
|
|
575
|
+
"60",
|
|
576
|
+
lca_bound,
|
|
577
|
+
lcb_bound,
|
|
578
|
+
reference=self.I_Elliptical,
|
|
579
|
+
n_iter=5,
|
|
580
|
+
thresh=0.01,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self.define_lc_state("State2", self.lca_60, self.lcb_60)
|
|
584
|
+
|
|
585
|
+
self.swing60 = np.sqrt(
|
|
586
|
+
(self.lcb_60 - self.lcb_ext) ** 2
|
|
587
|
+
+ (self.lca_60 - self.lca_ext) ** 2
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Print comparison of target swing, target ratio
|
|
591
|
+
# Ratio determines the orientation of the elliptical state
|
|
592
|
+
# should be close to target. Swing will vary to optimize ellipticity
|
|
593
|
+
logging.debug(
|
|
594
|
+
f"ratio: swing_LCB / swing_LCA = {(self.lcb_ext - self.lcb_60) / (self.lca_ext - self.lca_60):.4f} \
|
|
595
|
+
| target ratio: {-self.ratio}"
|
|
596
|
+
)
|
|
597
|
+
logging.debug(
|
|
598
|
+
f"total swing = {self.swing60:.4f} | target = {swing_ell}"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
logging.info(f"LCA State2 (I60) = {self.lca_60:.3f}")
|
|
602
|
+
logging.debug(f"LCA State2 (I60) = {self.lca_60:.5f}")
|
|
603
|
+
logging.info(f"LCB State2 (I60) = {self.lcb_60:.3f}")
|
|
604
|
+
logging.debug(f"LCB State2 (I60) = {self.lcb_60:.5f}")
|
|
605
|
+
logging.info(f"Intensity (I60) = {intensity:.0f}")
|
|
606
|
+
logging.debug(f"Intensity (I60) = {intensity:.3f}")
|
|
607
|
+
logging.info("--------done--------")
|
|
608
|
+
logging.debug("--------done--------")
|
|
609
|
+
|
|
610
|
+
def opt_I90(self, lca_bound, lcb_bound):
|
|
611
|
+
"""
|
|
612
|
+
optimized relative to Ielliptical (opt_I90)
|
|
613
|
+
Parameters
|
|
614
|
+
----------
|
|
615
|
+
lca_bound
|
|
616
|
+
lcb_bound
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
lca, lcb value at optimized state
|
|
620
|
+
intensity value at optimized state
|
|
621
|
+
"""
|
|
622
|
+
logging.info("Calibrating State3 (I90)...")
|
|
623
|
+
logging.debug("Calibrating State3 (I90)...")
|
|
624
|
+
|
|
625
|
+
self.inten = []
|
|
626
|
+
|
|
627
|
+
self.set_lc(self.lca_ext + self.swing, "LCA")
|
|
628
|
+
self.set_lc(self.lcb_ext, "LCB")
|
|
629
|
+
|
|
630
|
+
self.lca_90, self.lcb_90, intensity = self.optimizer.optimize(
|
|
631
|
+
"90",
|
|
632
|
+
lca_bound,
|
|
633
|
+
lcb_bound,
|
|
634
|
+
reference=self.I_Elliptical,
|
|
635
|
+
n_iter=5,
|
|
636
|
+
thresh=0.01,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
self.define_lc_state("State3", self.lca_90, self.lcb_90)
|
|
640
|
+
|
|
641
|
+
self.swing90 = np.sqrt(
|
|
642
|
+
(self.lcb_90 - self.lcb_ext) ** 2
|
|
643
|
+
+ (self.lca_90 - self.lca_ext) ** 2
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
logging.info(f"LCA State3 (I90) = {self.lca_90:.3f}")
|
|
647
|
+
logging.debug(f"LCA State3 (I90) = {self.lca_90:.5f}")
|
|
648
|
+
logging.info(f"LCB State3 (I90) = {self.lcb_90:.3f}")
|
|
649
|
+
logging.debug(f"LCB State3 (I90) = {self.lcb_90:.5f}")
|
|
650
|
+
logging.info(f"Intensity (I90) = {intensity:.0f}")
|
|
651
|
+
logging.debug(f"Intensity (I90) = {intensity:.3f}")
|
|
652
|
+
logging.info("--------done--------")
|
|
653
|
+
logging.debug("--------done--------")
|
|
654
|
+
|
|
655
|
+
def opt_I120(self, lca_bound, lcb_bound):
|
|
656
|
+
"""
|
|
657
|
+
optimized relative to Ielliptical (opt_I0_4State)
|
|
658
|
+
Parameters
|
|
659
|
+
----------
|
|
660
|
+
lca_bound
|
|
661
|
+
lcb_bound
|
|
662
|
+
Returns
|
|
663
|
+
-------
|
|
664
|
+
lca, lcb value at optimized state
|
|
665
|
+
intensity value at optimized state
|
|
666
|
+
"""
|
|
667
|
+
logging.info("Calibrating State3 (I120)...")
|
|
668
|
+
logging.debug("Calibrating State3 (I120)...")
|
|
669
|
+
|
|
670
|
+
# Calculate Initial Swing for initial guess to optimize around
|
|
671
|
+
# Based on ratio calculated from ellpiticity/orientation of LC simulation
|
|
672
|
+
swing_ell = np.sqrt(
|
|
673
|
+
(self.lca_ext - self.lca_0) ** 2 + (self.lcb_ext - self.lcb_0) ** 2
|
|
674
|
+
)
|
|
675
|
+
lca_swing = np.sqrt(swing_ell**2 / (1 + self.ratio**2))
|
|
676
|
+
lcb_swing = self.ratio * lca_swing
|
|
677
|
+
|
|
678
|
+
# Brent Optimization
|
|
679
|
+
self.set_lc(self.lca_ext + lca_swing, "LCA")
|
|
680
|
+
self.set_lc(self.lcb_ext - lcb_swing, "LCB")
|
|
681
|
+
|
|
682
|
+
self.lca_120, self.lcb_120, intensity = self.optimizer.optimize(
|
|
683
|
+
"120",
|
|
684
|
+
lca_bound,
|
|
685
|
+
lcb_bound,
|
|
686
|
+
reference=self.I_Elliptical,
|
|
687
|
+
n_iter=5,
|
|
688
|
+
thresh=0.01,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
self.define_lc_state("State3", self.lca_120, self.lcb_120)
|
|
692
|
+
|
|
693
|
+
self.swing120 = np.sqrt(
|
|
694
|
+
(self.lcb_120 - self.lcb_ext) ** 2
|
|
695
|
+
+ (self.lca_120 - self.lca_ext) ** 2
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Print comparison of target swing, target ratio
|
|
699
|
+
# Ratio determines the orientation of the elliptical state
|
|
700
|
+
# should be close to target. Swing will vary to optimize ellipticity
|
|
701
|
+
logging.debug(
|
|
702
|
+
f"ratio: swing_LCB / swing_LCA = {(self.lcb_ext - self.lcb_120) / (self.lca_ext - self.lca_120):.4f}\
|
|
703
|
+
| target ratio: {self.ratio}"
|
|
704
|
+
)
|
|
705
|
+
logging.debug(
|
|
706
|
+
f"total swing = {self.swing120:.4f} | target = {swing_ell}"
|
|
707
|
+
)
|
|
708
|
+
logging.info(f"LCA State3 (I120) = {self.lca_120:.3f}")
|
|
709
|
+
logging.debug(f"LCA State3 (I120) = {self.lca_120:.5f}")
|
|
710
|
+
logging.info(f"LCB State3 (I120) = {self.lcb_120:.3f}")
|
|
711
|
+
logging.debug(f"LCB State3 (I120) = {self.lcb_120:.5f}")
|
|
712
|
+
logging.info(f"Intensity (I120) = {intensity:.0f}")
|
|
713
|
+
logging.debug(f"Intensity (I120) = {intensity:.3f}")
|
|
714
|
+
logging.info("--------done--------")
|
|
715
|
+
logging.debug("--------done--------")
|
|
716
|
+
|
|
717
|
+
def opt_I135(self, lca_bound, lcb_bound):
|
|
718
|
+
"""
|
|
719
|
+
optimized relative to Ielliptical (opt_I0)
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
lca_bound
|
|
723
|
+
lcb_bound
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
lca, lcb value at optimized state
|
|
727
|
+
intensity value at optimized state
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
logging.info("Calibrating State4 (I135)...")
|
|
731
|
+
logging.debug("Calibrating State4 (I135)...")
|
|
732
|
+
self.inten = []
|
|
733
|
+
|
|
734
|
+
self.set_lc(self.lca_ext, "LCA")
|
|
735
|
+
self.set_lc(self.lcb_ext + self.swing, "LCB")
|
|
736
|
+
|
|
737
|
+
self.lca_135, self.lcb_135, intensity = self.optimizer.optimize(
|
|
738
|
+
"135",
|
|
739
|
+
lca_bound,
|
|
740
|
+
lcb_bound,
|
|
741
|
+
reference=self.I_Elliptical,
|
|
742
|
+
n_iter=5,
|
|
743
|
+
thresh=0.01,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
self.define_lc_state("State4", self.lca_135, self.lcb_135)
|
|
747
|
+
|
|
748
|
+
self.swing135 = np.sqrt(
|
|
749
|
+
(self.lcb_135 - self.lcb_ext) ** 2
|
|
750
|
+
+ (self.lca_135 - self.lca_ext) ** 2
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
logging.info(f"LCA State4 (I135) = {self.lca_135:.3f}")
|
|
754
|
+
logging.debug(f"LCA State4 (I135) = {self.lca_135:.5f}")
|
|
755
|
+
logging.info(f"LCB State4 (I135) = {self.lcb_135:.3f}")
|
|
756
|
+
logging.debug(f"LCB State4 (I135) = {self.lcb_135:.5f}")
|
|
757
|
+
logging.info(f"Intensity (I135) = {intensity:.0f}")
|
|
758
|
+
logging.debug(f"Intensity (I135) = {intensity:.3f}")
|
|
759
|
+
logging.info("--------done--------")
|
|
760
|
+
logging.debug("--------done--------")
|
|
761
|
+
|
|
762
|
+
def open_shutter(self):
|
|
763
|
+
if self.shutter_device == "": # no shutter
|
|
764
|
+
input("Please manually open the shutter and press <Enter>")
|
|
765
|
+
else:
|
|
766
|
+
self.mmc.setShutterOpen(True)
|
|
767
|
+
|
|
768
|
+
def reset_shutter(self):
|
|
769
|
+
"""
|
|
770
|
+
Return autoshutter to its original state before closing
|
|
771
|
+
|
|
772
|
+
Returns
|
|
773
|
+
-------
|
|
774
|
+
|
|
775
|
+
"""
|
|
776
|
+
if self.shutter_device == "": # no shutter
|
|
777
|
+
input(
|
|
778
|
+
"Please reset the shutter to its original state and press <Enter>"
|
|
779
|
+
)
|
|
780
|
+
logging.info(
|
|
781
|
+
"This is the end of the command-line instructions. You can return to the napari window."
|
|
782
|
+
)
|
|
783
|
+
else:
|
|
784
|
+
self.mmc.setAutoShutter(self._auto_shutter_state)
|
|
785
|
+
self.mmc.setShutterOpen(self._shutter_state)
|
|
786
|
+
|
|
787
|
+
def close_shutter_and_calc_blacklevel(self):
|
|
788
|
+
self._auto_shutter_state = self.mmc.getAutoShutter()
|
|
789
|
+
self._shutter_state = self.mmc.getShutterOpen()
|
|
790
|
+
|
|
791
|
+
if self.shutter_device == "": # no shutter
|
|
792
|
+
show_warning(
|
|
793
|
+
"No shutter found. Please follow the command-line instructions..."
|
|
794
|
+
)
|
|
795
|
+
shutter_warning_msg = """
|
|
796
|
+
waveorder could not find an automatic shutter configured through Micro-Manager.
|
|
797
|
+
>>> If you would like manually enter the black level, enter an integer or float and press <Enter>
|
|
798
|
+
>>> If you would like to estimate the black level, please close the shutter and press <Enter>
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
in_string = input(shutter_warning_msg)
|
|
802
|
+
if in_string.isdigit(): # True if positive integer
|
|
803
|
+
self.I_Black = float(in_string)
|
|
804
|
+
return
|
|
805
|
+
else:
|
|
806
|
+
self.mmc.setAutoShutter(False)
|
|
807
|
+
self.mmc.setShutterOpen(False)
|
|
808
|
+
|
|
809
|
+
n_avg = 20
|
|
810
|
+
avgs = []
|
|
811
|
+
for i in range(n_avg):
|
|
812
|
+
mean = snap_and_average(self.snap_manager)
|
|
813
|
+
self.intensity_emitter.emit(mean)
|
|
814
|
+
avgs.append(mean)
|
|
815
|
+
|
|
816
|
+
blacklevel = np.mean(avgs)
|
|
817
|
+
self.I_Black = blacklevel
|
|
818
|
+
|
|
819
|
+
def calculate_extinction(
|
|
820
|
+
self, swing, black_level, intensity_extinction, intensity_elliptical
|
|
821
|
+
):
|
|
822
|
+
"""
|
|
823
|
+
Returns the extinction ratio, the ratio of the largest and smallest intensities that the imaging system can transmit above background.
|
|
824
|
+
See `/docs/calibration-guide.md` for a derivation of this expressions.
|
|
825
|
+
"""
|
|
826
|
+
return np.round(
|
|
827
|
+
(1 / np.sin(np.pi * swing) ** 2)
|
|
828
|
+
* (intensity_elliptical - intensity_extinction)
|
|
829
|
+
/ (intensity_extinction - black_level)
|
|
830
|
+
+ 1,
|
|
831
|
+
2,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
def calc_inst_matrix(self):
|
|
835
|
+
if self.calib_scheme == "4-State":
|
|
836
|
+
chi = self.swing
|
|
837
|
+
inst_mat = np.array(
|
|
838
|
+
[
|
|
839
|
+
[1, 0, 0, -1],
|
|
840
|
+
[1, np.sin(2 * np.pi * chi), 0, -np.cos(2 * np.pi * chi)],
|
|
841
|
+
[
|
|
842
|
+
1,
|
|
843
|
+
-0.5 * np.sin(2 * np.pi * chi),
|
|
844
|
+
np.sqrt(3) * np.cos(np.pi * chi) * np.sin(np.pi * chi),
|
|
845
|
+
-np.cos(2 * np.pi * chi),
|
|
846
|
+
],
|
|
847
|
+
[
|
|
848
|
+
1,
|
|
849
|
+
-0.5 * np.sin(2 * np.pi * chi),
|
|
850
|
+
-np.sqrt(3) / 2 * np.sin(2 * np.pi * chi),
|
|
851
|
+
-np.cos(2 * np.pi * chi),
|
|
852
|
+
],
|
|
853
|
+
]
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
return inst_mat
|
|
857
|
+
|
|
858
|
+
if self.calib_scheme == "5-State":
|
|
859
|
+
chi = self.swing * 2 * np.pi
|
|
860
|
+
|
|
861
|
+
inst_mat = np.array(
|
|
862
|
+
[
|
|
863
|
+
[1, 0, 0, -1],
|
|
864
|
+
[1, np.sin(chi), 0, -np.cos(chi)],
|
|
865
|
+
[1, 0, np.sin(chi), -np.cos(chi)],
|
|
866
|
+
[1, -np.sin(chi), 0, -np.cos(chi)],
|
|
867
|
+
[1, 0, -np.sin(chi), -np.cos(chi)],
|
|
868
|
+
]
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
return inst_mat
|
|
872
|
+
|
|
873
|
+
def write_metadata(self, notes=None):
|
|
874
|
+
inst_mat = self.calc_inst_matrix()
|
|
875
|
+
inst_mat = np.around(inst_mat, decimals=5).tolist()
|
|
876
|
+
|
|
877
|
+
metadata = {
|
|
878
|
+
"Summary": {
|
|
879
|
+
"Timestamp": str(datetime.now()),
|
|
880
|
+
"waveorder version": version("waveorder"),
|
|
881
|
+
},
|
|
882
|
+
"Calibration": {
|
|
883
|
+
"Calibration scheme": self.calib_scheme,
|
|
884
|
+
"Swing (waves)": self.swing,
|
|
885
|
+
"Wavelength (nm)": self.wavelength,
|
|
886
|
+
"Retardance to voltage interpolation method": self.calib.interp_method,
|
|
887
|
+
"LC control mode": self.mode,
|
|
888
|
+
"Black level": np.round(self.I_Black, 2),
|
|
889
|
+
"Extinction ratio": self.extinction_ratio,
|
|
890
|
+
},
|
|
891
|
+
"Notes": notes,
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if self.calib_scheme == "4-State":
|
|
895
|
+
metadata["Calibration"].update(
|
|
896
|
+
{
|
|
897
|
+
"Channel names": [f"State{i}" for i in range(4)],
|
|
898
|
+
"LC retardance": {
|
|
899
|
+
f"LC{i}_{j}": np.around(
|
|
900
|
+
getattr(self, f"lc{i.lower()}_{j}"), decimals=6
|
|
901
|
+
)
|
|
902
|
+
for j in ["ext", "0", "60", "120"]
|
|
903
|
+
for i in ["A", "B"]
|
|
904
|
+
},
|
|
905
|
+
"LC voltage": {
|
|
906
|
+
f"LC{i}_{j}": np.around(
|
|
907
|
+
self.calib.get_voltage(
|
|
908
|
+
getattr(self, f"lc{i.lower()}_{j}")
|
|
909
|
+
),
|
|
910
|
+
decimals=4,
|
|
911
|
+
)
|
|
912
|
+
for j in ["ext", "0", "60", "120"]
|
|
913
|
+
for i in ["A", "B"]
|
|
914
|
+
},
|
|
915
|
+
"Swing_0": np.around(self.swing0, decimals=3),
|
|
916
|
+
"Swing_60": np.around(self.swing60, decimals=3),
|
|
917
|
+
"Swing_120": np.around(self.swing120, decimals=3),
|
|
918
|
+
"Instrument matrix": inst_mat,
|
|
919
|
+
}
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
elif self.calib_scheme == "5-State":
|
|
923
|
+
metadata["Calibration"].update(
|
|
924
|
+
{
|
|
925
|
+
"Channel names": [f"State{i}" for i in range(5)],
|
|
926
|
+
"LC retardance": {
|
|
927
|
+
f"LC{i}_{j}": np.around(
|
|
928
|
+
getattr(self, f"lc{i.lower()}_{j}"), decimals=6
|
|
929
|
+
)
|
|
930
|
+
for j in ["ext", "0", "45", "90", "135"]
|
|
931
|
+
for i in ["A", "B"]
|
|
932
|
+
},
|
|
933
|
+
"LC voltage": {
|
|
934
|
+
f"LC{i}_{j}": np.around(
|
|
935
|
+
self.calib.get_voltage(
|
|
936
|
+
getattr(self, f"lc{i.lower()}_{j}")
|
|
937
|
+
),
|
|
938
|
+
decimals=4,
|
|
939
|
+
)
|
|
940
|
+
for j in ["ext", "0", "45", "90", "135"]
|
|
941
|
+
for i in ["A", "B"]
|
|
942
|
+
},
|
|
943
|
+
"Swing_0": np.around(self.swing0, decimals=3),
|
|
944
|
+
"Swing_45": np.around(self.swing45, decimals=3),
|
|
945
|
+
"Swing_90": np.around(self.swing90, decimals=3),
|
|
946
|
+
"Swing_135": np.around(self.swing135, decimals=3),
|
|
947
|
+
"Instrument matrix": inst_mat,
|
|
948
|
+
}
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
with open(self.meta_file, "w") as metafile:
|
|
952
|
+
json.dump(metadata, metafile, indent=1)
|
|
953
|
+
|
|
954
|
+
def _add_colorbar(self, mappable):
|
|
955
|
+
last_axes = plt.gca()
|
|
956
|
+
ax = mappable.axes
|
|
957
|
+
fig = ax.figure
|
|
958
|
+
divider = make_axes_locatable(ax)
|
|
959
|
+
cax = divider.append_axes("right", size="5%", pad=0.05)
|
|
960
|
+
cbar = fig.colorbar(mappable, cax=cax)
|
|
961
|
+
plt.sca(last_axes)
|
|
962
|
+
return cbar
|
|
963
|
+
|
|
964
|
+
def _capture_state(self, state: str, n_avg: int):
|
|
965
|
+
"""Set the LCs to a certain state, then snap and average over a number of images.
|
|
966
|
+
|
|
967
|
+
Parameters
|
|
968
|
+
----------
|
|
969
|
+
state : str
|
|
970
|
+
Name of the LC config, e.g. `"State0"`
|
|
971
|
+
n_avg : int
|
|
972
|
+
Number of images to capture and average
|
|
973
|
+
|
|
974
|
+
Returns
|
|
975
|
+
-------
|
|
976
|
+
ndarray
|
|
977
|
+
Average of N images
|
|
978
|
+
"""
|
|
979
|
+
with suspend_live_sm(self.snap_manager) as sm:
|
|
980
|
+
set_lc_state(self.mmc, self.group, state)
|
|
981
|
+
imgs = []
|
|
982
|
+
for i in range(n_avg):
|
|
983
|
+
imgs.append(snap_and_get_image(sm))
|
|
984
|
+
return np.mean(imgs, axis=0)
|
|
985
|
+
|
|
986
|
+
def _plot_bg_images(self, imgs):
|
|
987
|
+
img_names = (
|
|
988
|
+
["Extinction", "0", "60", "120"]
|
|
989
|
+
if len(imgs) == 4
|
|
990
|
+
else ["Extinction", "0", "45", "90", 135]
|
|
991
|
+
)
|
|
992
|
+
fig, ax = (
|
|
993
|
+
plt.subplots(2, 2, figsize=(20, 20))
|
|
994
|
+
if len(imgs) == 4
|
|
995
|
+
else plt.subplots(3, 2, figsize=(20, 20))
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
img_idx = 0
|
|
999
|
+
for ax1 in range(len(ax[:, 0])):
|
|
1000
|
+
for ax2 in range(len(ax[0, :])):
|
|
1001
|
+
if img_idx < len(imgs):
|
|
1002
|
+
im = ax[ax1, ax2].imshow(imgs[img_idx], "gray")
|
|
1003
|
+
ax[ax1, ax2].set_title(img_names[img_idx])
|
|
1004
|
+
self._add_colorbar(im)
|
|
1005
|
+
else:
|
|
1006
|
+
try:
|
|
1007
|
+
fig.delaxes(ax[2, 1])
|
|
1008
|
+
except:
|
|
1009
|
+
break
|
|
1010
|
+
plt.show()
|
|
1011
|
+
|
|
1012
|
+
@property
|
|
1013
|
+
def pol_states(self):
|
|
1014
|
+
"""The polarization states of this calibration.
|
|
1015
|
+
|
|
1016
|
+
Returns
|
|
1017
|
+
-------
|
|
1018
|
+
tuple
|
|
1019
|
+
Names of all the polarization states.
|
|
1020
|
+
|
|
1021
|
+
Raises
|
|
1022
|
+
------
|
|
1023
|
+
ValueError
|
|
1024
|
+
Found illegal calibration state.
|
|
1025
|
+
"""
|
|
1026
|
+
if self.calib_scheme == "4-State":
|
|
1027
|
+
pols = ("ext", "0", "60", "120")
|
|
1028
|
+
elif self.calib_scheme == "5-State":
|
|
1029
|
+
pols = ("ext", "0", "45", "90", "135")
|
|
1030
|
+
else:
|
|
1031
|
+
raise ValueError(
|
|
1032
|
+
f"Invalid calibration state: {self.calib_scheme}."
|
|
1033
|
+
)
|
|
1034
|
+
return pols
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def lc_states(self):
|
|
1038
|
+
"""The optimized LC retardance values of this calibration.
|
|
1039
|
+
|
|
1040
|
+
Returns
|
|
1041
|
+
-------
|
|
1042
|
+
dict
|
|
1043
|
+
`Dict{"LCA": List[ext, ...], "LCB": List[ext, ...]}`
|
|
1044
|
+
"""
|
|
1045
|
+
lc_sides = ["A", "B"]
|
|
1046
|
+
return {
|
|
1047
|
+
f"LC{lc_side}": [
|
|
1048
|
+
self.__getattribute__("lc" + lc_side.lower() + "_" + pol)
|
|
1049
|
+
for pol in self.pol_states
|
|
1050
|
+
]
|
|
1051
|
+
for lc_side in lc_sides
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
def capture_bg(self, n_avg, directory):
|
|
1055
|
+
""" "
|
|
1056
|
+
This function will capture an image at every state
|
|
1057
|
+
and save to specified directory
|
|
1058
|
+
This may throw errors depending on the Micro-Manager config file--
|
|
1059
|
+
modify 'State_' to match to the corresponding channel preset in config
|
|
1060
|
+
:param: n_states (int)
|
|
1061
|
+
Number of states used for calibration
|
|
1062
|
+
:param: directory (string)
|
|
1063
|
+
Directory to save images
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
if not os.path.exists(directory):
|
|
1067
|
+
os.makedirs(directory)
|
|
1068
|
+
|
|
1069
|
+
logging.info("Capturing Background")
|
|
1070
|
+
self._auto_shutter_state = self.mmc.getAutoShutter()
|
|
1071
|
+
self._shutter_state = self.mmc.getShutterOpen()
|
|
1072
|
+
self.mmc.setAutoShutter(False)
|
|
1073
|
+
self.open_shutter()
|
|
1074
|
+
|
|
1075
|
+
num_states = int(self.calib_scheme[0])
|
|
1076
|
+
|
|
1077
|
+
# Acquire background data
|
|
1078
|
+
yx_list = []
|
|
1079
|
+
for channel in range(num_states):
|
|
1080
|
+
logging.debug(f"Capturing Background State{channel}")
|
|
1081
|
+
yx_list.append(self._capture_state(f"State{channel}", n_avg))
|
|
1082
|
+
logging.debug(f"Saving Background State{channel}")
|
|
1083
|
+
cyx_data = np.array(yx_list)
|
|
1084
|
+
yx_scale = self.mmc.getPixelSizeUm()
|
|
1085
|
+
|
|
1086
|
+
# Save to zarr
|
|
1087
|
+
with open_ome_zarr(
|
|
1088
|
+
os.path.join(directory, "background.zarr"),
|
|
1089
|
+
layout="hcs",
|
|
1090
|
+
mode="w",
|
|
1091
|
+
channel_names=[f"State{i}" for i in range(num_states)],
|
|
1092
|
+
) as dataset:
|
|
1093
|
+
position = dataset.create_position("0", "0", "0")
|
|
1094
|
+
position.create_zeros(
|
|
1095
|
+
name="0",
|
|
1096
|
+
shape=(1, num_states, 1, cyx_data.shape[1], cyx_data.shape[2]),
|
|
1097
|
+
dtype=np.float32,
|
|
1098
|
+
chunks=(1, 1, 1, cyx_data.shape[1], cyx_data.shape[2]),
|
|
1099
|
+
transform=[
|
|
1100
|
+
TransformationMeta(
|
|
1101
|
+
type="scale", scale=[1, 1, 1, yx_scale, yx_scale]
|
|
1102
|
+
)
|
|
1103
|
+
],
|
|
1104
|
+
)
|
|
1105
|
+
position["0"][0, :, 0] = cyx_data # save to 1C1YX array
|
|
1106
|
+
|
|
1107
|
+
# self._plot_bg_images(np.asarray(imgs))
|
|
1108
|
+
self.reset_shutter()
|
|
1109
|
+
|
|
1110
|
+
return cyx_data
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
class CalibrationData:
|
|
1114
|
+
"""
|
|
1115
|
+
Interpolates LC calibration data between retardance (in waves), voltage (in mV), and wavelength (in nm)
|
|
1116
|
+
"""
|
|
1117
|
+
|
|
1118
|
+
def __init__(self, path, wavelength=532, interp_method="linear"):
|
|
1119
|
+
"""
|
|
1120
|
+
|
|
1121
|
+
Parameters
|
|
1122
|
+
----------
|
|
1123
|
+
path : str
|
|
1124
|
+
path to .csv calibration data file
|
|
1125
|
+
wavelength : int
|
|
1126
|
+
usage wavelength, in nanometers
|
|
1127
|
+
interp_method : str
|
|
1128
|
+
interpolation method, either "linear" or "schnoor_fit" (https://doi.org/10.1364/AO.408383)
|
|
1129
|
+
"""
|
|
1130
|
+
|
|
1131
|
+
header, raw_data = self.read_data(path)
|
|
1132
|
+
self.calib_wavelengths = np.array(
|
|
1133
|
+
[i[:3] for i in header[1::3]]
|
|
1134
|
+
).astype("double")
|
|
1135
|
+
|
|
1136
|
+
self.wavelength = None
|
|
1137
|
+
self.V_min = 0.0
|
|
1138
|
+
self.V_max = 20.0
|
|
1139
|
+
|
|
1140
|
+
if interp_method in ["linear", "schnoor_fit"]:
|
|
1141
|
+
self.interp_method = interp_method
|
|
1142
|
+
else:
|
|
1143
|
+
raise ValueError("Unknown interpolation method.")
|
|
1144
|
+
|
|
1145
|
+
self.set_wavelength(wavelength)
|
|
1146
|
+
if interp_method == "linear":
|
|
1147
|
+
self.interpolate_data(
|
|
1148
|
+
raw_data, self.calib_wavelengths
|
|
1149
|
+
) # calib_wavelengths is not used, values hardcoded
|
|
1150
|
+
elif interp_method == "schnoor_fit":
|
|
1151
|
+
self.fit_params = self.fit_data(raw_data, self.calib_wavelengths)
|
|
1152
|
+
|
|
1153
|
+
self.ret_min = self.get_retardance(self.V_max)
|
|
1154
|
+
self.ret_max = self.get_retardance(self.V_min)
|
|
1155
|
+
|
|
1156
|
+
@staticmethod
|
|
1157
|
+
def read_data(path):
|
|
1158
|
+
"""
|
|
1159
|
+
Read raw calibration data
|
|
1160
|
+
|
|
1161
|
+
Example calibration data format:
|
|
1162
|
+
|
|
1163
|
+
Voltage(mv),490-A,490-B,Voltage(mv),546-A,546-B,Voltage(mv),630-A,630-B
|
|
1164
|
+
-,-,-,-,-,-,-,-,-
|
|
1165
|
+
0,490,490,0,546,546,0,630,630
|
|
1166
|
+
0,970.6205,924.4288,0,932.2446,891.2008,0,899.6626,857.2885
|
|
1167
|
+
200,970.7488,924.4422,200,932.2028,891.1546,200,899.5908,857.3078
|
|
1168
|
+
...
|
|
1169
|
+
20000,40.5954,40.4874,20000,38.6905,39.5402,20000,35.5043,38.1445
|
|
1170
|
+
-,-,-,-,-,-,-,-,-
|
|
1171
|
+
|
|
1172
|
+
The first row of the CSV file is a header row, structured as [Voltage (mV), XXX-A, XXX-B,
|
|
1173
|
+
Voltage (nm), XXX-A, XXX-B, ...] where XXX is the calibration wavelength in nanometers. For example 532-A would
|
|
1174
|
+
contain measurements of the retardance of LCA as a function of applied voltage at 532 nm. The second row
|
|
1175
|
+
contains dashes in every column. The third row contains "0" in the Voltage column and the calibration wavelength
|
|
1176
|
+
in the retardance columns, e.g [0, 532, 532]. The following rows contain the LC calibration data. Retardance is
|
|
1177
|
+
recorded in nanometers and voltage is recorded in millivolts. The last row contains dashes in every column.
|
|
1178
|
+
|
|
1179
|
+
Parameters
|
|
1180
|
+
----------
|
|
1181
|
+
path : str
|
|
1182
|
+
path to .csv calibration data file
|
|
1183
|
+
|
|
1184
|
+
Returns
|
|
1185
|
+
-------
|
|
1186
|
+
header : list
|
|
1187
|
+
Calibration data file header line. Contains information on calibration wavelength
|
|
1188
|
+
raw_data : ndarray
|
|
1189
|
+
Calibration data. Voltage is in millivolts and retardance is in nanometers
|
|
1190
|
+
|
|
1191
|
+
"""
|
|
1192
|
+
with open(path, "r") as f:
|
|
1193
|
+
header = f.readline().strip().split(",")
|
|
1194
|
+
|
|
1195
|
+
raw_data = np.loadtxt(path, delimiter=",", comments="-", skiprows=3)
|
|
1196
|
+
return header, raw_data
|
|
1197
|
+
|
|
1198
|
+
@staticmethod
|
|
1199
|
+
def schnoor_fit(V, a, b1, b2, c, d, e, wavelength):
|
|
1200
|
+
"""
|
|
1201
|
+
|
|
1202
|
+
Parameters
|
|
1203
|
+
----------
|
|
1204
|
+
V : float
|
|
1205
|
+
Voltage in volts
|
|
1206
|
+
a, b1, b2, c, d, e : float
|
|
1207
|
+
Fit parameters
|
|
1208
|
+
wavelength : float
|
|
1209
|
+
Wavelength in nanometers
|
|
1210
|
+
|
|
1211
|
+
Returns
|
|
1212
|
+
-------
|
|
1213
|
+
retardance : float
|
|
1214
|
+
Retardance in nanometers
|
|
1215
|
+
|
|
1216
|
+
"""
|
|
1217
|
+
retardance = a + (b1 + b2 / wavelength**2) / (1 + (V / c) ** d) ** e
|
|
1218
|
+
|
|
1219
|
+
return retardance
|
|
1220
|
+
|
|
1221
|
+
@staticmethod
|
|
1222
|
+
def schnoor_fit_inv(retardance, a, b1, b2, c, d, e, wavelength):
|
|
1223
|
+
"""
|
|
1224
|
+
|
|
1225
|
+
Parameters
|
|
1226
|
+
----------
|
|
1227
|
+
retardance : float
|
|
1228
|
+
Retardance in nanometers
|
|
1229
|
+
a, b1, b2, c, d, e : float
|
|
1230
|
+
Fit parameters
|
|
1231
|
+
wavelength : float
|
|
1232
|
+
Wavelength in nanometers
|
|
1233
|
+
|
|
1234
|
+
Returns
|
|
1235
|
+
-------
|
|
1236
|
+
voltage : float
|
|
1237
|
+
Voltage in volts
|
|
1238
|
+
|
|
1239
|
+
"""
|
|
1240
|
+
|
|
1241
|
+
voltage = c * (
|
|
1242
|
+
((b1 + b2 / wavelength**2) / (retardance - a)) ** (1 / e) - 1
|
|
1243
|
+
) ** (1 / d)
|
|
1244
|
+
|
|
1245
|
+
return voltage
|
|
1246
|
+
|
|
1247
|
+
@staticmethod
|
|
1248
|
+
def _fun(x, wavelengths, xdata, ydata):
|
|
1249
|
+
fval = CalibrationData.schnoor_fit(xdata, *x, wavelengths)
|
|
1250
|
+
res = ydata - fval
|
|
1251
|
+
return res.flatten()
|
|
1252
|
+
|
|
1253
|
+
def set_wavelength(self, wavelength):
|
|
1254
|
+
if (
|
|
1255
|
+
len(self.calib_wavelengths) == 1
|
|
1256
|
+
and wavelength != self.calib_wavelengths
|
|
1257
|
+
):
|
|
1258
|
+
raise ValueError(
|
|
1259
|
+
"Calibration is not provided at this wavelength. "
|
|
1260
|
+
"Wavelength dependence of LC retardance vs voltage cannot be extrapolated."
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
if (
|
|
1264
|
+
wavelength < self.calib_wavelengths.min()
|
|
1265
|
+
or wavelength > self.calib_wavelengths.max()
|
|
1266
|
+
):
|
|
1267
|
+
warnings.warn(
|
|
1268
|
+
"Specified wavelength is outside of the calibration range. "
|
|
1269
|
+
"LC retardance vs voltage data will be extrapolated at this wavelength."
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
self.wavelength = wavelength
|
|
1273
|
+
if self.interp_method == "linear":
|
|
1274
|
+
# Interpolation of calib beyond this range produce strange results.
|
|
1275
|
+
if self.wavelength < 450:
|
|
1276
|
+
self.wavelength = 450
|
|
1277
|
+
warnings.warn(
|
|
1278
|
+
"Wavelength is limited to 450-720 nm for this interpolation method."
|
|
1279
|
+
)
|
|
1280
|
+
if self.wavelength > 720:
|
|
1281
|
+
self.wavelength = 720
|
|
1282
|
+
warnings.warn(
|
|
1283
|
+
"Wavelength is limited to 450-720 nm for this interpolation method."
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
def fit_data(self, raw_data, calib_wavelengths):
|
|
1287
|
+
"""
|
|
1288
|
+
Perform Schnoor fit on interpolation data
|
|
1289
|
+
|
|
1290
|
+
Parameters
|
|
1291
|
+
----------
|
|
1292
|
+
raw_data : np.array
|
|
1293
|
+
LC calibration data in (Voltage, LCA retardance, LCB retardance) format. Only the LCA retardance vs voltage
|
|
1294
|
+
curve is used.
|
|
1295
|
+
calib_wavelengths : 1D np.array
|
|
1296
|
+
Calibration wavelength for each (Voltage, LCA retardance, LCB retardance) set in the calibration data
|
|
1297
|
+
|
|
1298
|
+
Returns
|
|
1299
|
+
-------
|
|
1300
|
+
|
|
1301
|
+
"""
|
|
1302
|
+
xdata = raw_data[:, 0::3] / 1000 # convert to volts
|
|
1303
|
+
ydata = raw_data[:, 1::3] # in nanometers
|
|
1304
|
+
|
|
1305
|
+
x0 = [10, 1000, 1e7, 1, 10, 0.1]
|
|
1306
|
+
p = least_squares(
|
|
1307
|
+
self._fun,
|
|
1308
|
+
x0,
|
|
1309
|
+
method="trf",
|
|
1310
|
+
args=(calib_wavelengths, xdata, ydata),
|
|
1311
|
+
bounds=((-np.inf, 0, 0, 0, 0, 0), (np.inf,) * 6),
|
|
1312
|
+
x_scale=[10, 1000, 1e7, 1, 10, 0.1],
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
if not p.success:
|
|
1316
|
+
raise RuntimeError("Schnoor fit to calibration data did not work.")
|
|
1317
|
+
|
|
1318
|
+
y = ydata.flatten()
|
|
1319
|
+
y_hat = y - p.fun
|
|
1320
|
+
slope, intercept, r_value, *_ = linregress(y, y_hat)
|
|
1321
|
+
r_squared = r_value**2
|
|
1322
|
+
if r_squared < 0.999:
|
|
1323
|
+
warnings.warn(
|
|
1324
|
+
f"Schnoor fit has R2 value of {r_squared:.5f}, fit may not have worked well."
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
return p.x
|
|
1328
|
+
|
|
1329
|
+
def interpolate_data(self, raw_data, calib_wavelengths):
|
|
1330
|
+
"""
|
|
1331
|
+
Perform linear interpolation of LC calibration data
|
|
1332
|
+
|
|
1333
|
+
Parameters
|
|
1334
|
+
----------
|
|
1335
|
+
raw_data : np.array
|
|
1336
|
+
LC calibration data in (Voltage, LCA retardance, LCB retardance) format. Only the LCA retardance vs voltage
|
|
1337
|
+
curve is used.
|
|
1338
|
+
calib_wavelengths : 1D np.array
|
|
1339
|
+
Calibration wavelength for each (Voltage, LCA retardance, LCB retardance) set in the calibration data
|
|
1340
|
+
These values are not used in this method. Instead, the [490, 546, 630] wavelengths are hardcoded.
|
|
1341
|
+
|
|
1342
|
+
Returns
|
|
1343
|
+
-------
|
|
1344
|
+
|
|
1345
|
+
"""
|
|
1346
|
+
# 0V to 20V step size 1 mV
|
|
1347
|
+
x_range = np.arange(0, np.max(raw_data[:, ::3]), 1)
|
|
1348
|
+
|
|
1349
|
+
# interpolate calib - only LCA data is used
|
|
1350
|
+
spline490 = interp1d(raw_data[:, 0], raw_data[:, 1])
|
|
1351
|
+
spline546 = interp1d(raw_data[:, 3], raw_data[:, 4])
|
|
1352
|
+
spline630 = interp1d(raw_data[:, 6], raw_data[:, 7])
|
|
1353
|
+
|
|
1354
|
+
if self.wavelength < 490:
|
|
1355
|
+
new_a1_y = np.interp(x_range, x_range, spline490(x_range))
|
|
1356
|
+
new_a2_y = np.interp(x_range, x_range, spline546(x_range))
|
|
1357
|
+
|
|
1358
|
+
wavelength_new = 490 + (490 - self.wavelength)
|
|
1359
|
+
fact1 = np.abs(490 - wavelength_new) / (546 - 490)
|
|
1360
|
+
fact2 = np.abs(546 - wavelength_new) / (546 - 490)
|
|
1361
|
+
|
|
1362
|
+
temp_curve = np.asarray(
|
|
1363
|
+
[
|
|
1364
|
+
[
|
|
1365
|
+
i,
|
|
1366
|
+
2 * new_a1_y[i]
|
|
1367
|
+
- (fact1 * new_a1_y[i] + fact2 * new_a2_y[i]),
|
|
1368
|
+
]
|
|
1369
|
+
for i in range(len(new_a1_y))
|
|
1370
|
+
]
|
|
1371
|
+
)
|
|
1372
|
+
self.spline = interp1d(temp_curve[:, 0], temp_curve[:, 1])
|
|
1373
|
+
self.curve = self.spline(x_range)
|
|
1374
|
+
|
|
1375
|
+
elif self.wavelength > 630:
|
|
1376
|
+
new_a1_y = np.interp(x_range, x_range, spline546(x_range))
|
|
1377
|
+
new_a2_y = np.interp(x_range, x_range, spline630(x_range))
|
|
1378
|
+
|
|
1379
|
+
wavelength_new = 630 + (630 - self.wavelength)
|
|
1380
|
+
fact1 = np.abs(630 - wavelength_new) / (630 - 546)
|
|
1381
|
+
fact2 = np.abs(546 - wavelength_new) / (630 - 546)
|
|
1382
|
+
|
|
1383
|
+
temp_curve = np.asarray(
|
|
1384
|
+
[
|
|
1385
|
+
[
|
|
1386
|
+
i,
|
|
1387
|
+
2 * new_a1_y[i]
|
|
1388
|
+
- (fact1 * new_a1_y[i] + fact2 * new_a2_y[i]),
|
|
1389
|
+
]
|
|
1390
|
+
for i in range(len(new_a1_y))
|
|
1391
|
+
]
|
|
1392
|
+
)
|
|
1393
|
+
self.spline = interp1d(temp_curve[:, 0], temp_curve[:, 1])
|
|
1394
|
+
self.curve = self.spline(x_range)
|
|
1395
|
+
|
|
1396
|
+
elif 490 < self.wavelength < 546:
|
|
1397
|
+
new_a1_y = np.interp(x_range, x_range, spline490(x_range))
|
|
1398
|
+
new_a2_y = np.interp(x_range, x_range, spline546(x_range))
|
|
1399
|
+
|
|
1400
|
+
fact1 = np.abs(490 - self.wavelength) / (546 - 490)
|
|
1401
|
+
fact2 = np.abs(546 - self.wavelength) / (546 - 490)
|
|
1402
|
+
|
|
1403
|
+
temp_curve = np.asarray(
|
|
1404
|
+
[
|
|
1405
|
+
[i, fact1 * new_a1_y[i] + fact2 * new_a2_y[i]]
|
|
1406
|
+
for i in range(len(new_a1_y))
|
|
1407
|
+
]
|
|
1408
|
+
)
|
|
1409
|
+
self.spline = interp1d(temp_curve[:, 0], temp_curve[:, 1])
|
|
1410
|
+
self.curve = self.spline(x_range)
|
|
1411
|
+
|
|
1412
|
+
elif 546 < self.wavelength < 630:
|
|
1413
|
+
new_a1_y = np.interp(x_range, x_range, spline546(x_range))
|
|
1414
|
+
new_a2_y = np.interp(x_range, x_range, spline630(x_range))
|
|
1415
|
+
|
|
1416
|
+
fact1 = np.abs(546 - self.wavelength) / (630 - 546)
|
|
1417
|
+
fact2 = np.abs(630 - self.wavelength) / (630 - 546)
|
|
1418
|
+
|
|
1419
|
+
temp_curve = np.asarray(
|
|
1420
|
+
[
|
|
1421
|
+
[i, fact1 * new_a1_y[i] + fact2 * new_a2_y[i]]
|
|
1422
|
+
for i in range(len(new_a1_y))
|
|
1423
|
+
]
|
|
1424
|
+
)
|
|
1425
|
+
self.spline = interp1d(temp_curve[:, 0], temp_curve[:, 1])
|
|
1426
|
+
self.curve = self.spline(x_range)
|
|
1427
|
+
|
|
1428
|
+
elif self.wavelength == 490:
|
|
1429
|
+
self.curve = spline490(x_range)
|
|
1430
|
+
self.spline = spline490
|
|
1431
|
+
|
|
1432
|
+
elif self.wavelength == 546:
|
|
1433
|
+
self.curve = spline546(x_range)
|
|
1434
|
+
self.spline = spline546
|
|
1435
|
+
|
|
1436
|
+
elif self.wavelength == 630:
|
|
1437
|
+
self.curve = spline630(x_range)
|
|
1438
|
+
self.spline = spline630
|
|
1439
|
+
|
|
1440
|
+
else:
|
|
1441
|
+
raise ValueError(f"Wavelength {self.wavelength} not understood")
|
|
1442
|
+
|
|
1443
|
+
def get_voltage(self, retardance):
|
|
1444
|
+
"""
|
|
1445
|
+
|
|
1446
|
+
Parameters
|
|
1447
|
+
----------
|
|
1448
|
+
retardance : float
|
|
1449
|
+
retardance in waves
|
|
1450
|
+
|
|
1451
|
+
Returns
|
|
1452
|
+
-------
|
|
1453
|
+
voltage
|
|
1454
|
+
voltage in volts
|
|
1455
|
+
|
|
1456
|
+
"""
|
|
1457
|
+
|
|
1458
|
+
retardance = np.asarray(retardance, dtype="double")
|
|
1459
|
+
voltage = None
|
|
1460
|
+
ret_nanometers = retardance * self.wavelength
|
|
1461
|
+
|
|
1462
|
+
if retardance < self.ret_min:
|
|
1463
|
+
voltage = self.V_max
|
|
1464
|
+
elif retardance > self.ret_max:
|
|
1465
|
+
voltage = self.V_min
|
|
1466
|
+
else:
|
|
1467
|
+
if self.interp_method == "linear":
|
|
1468
|
+
voltage = np.abs(self.curve - ret_nanometers).argmin() / 1000
|
|
1469
|
+
elif self.interp_method == "schnoor_fit":
|
|
1470
|
+
voltage = self.schnoor_fit_inv(
|
|
1471
|
+
ret_nanometers, *self.fit_params, self.wavelength
|
|
1472
|
+
)
|
|
1473
|
+
|
|
1474
|
+
return voltage
|
|
1475
|
+
|
|
1476
|
+
def get_retardance(self, volts):
|
|
1477
|
+
"""
|
|
1478
|
+
|
|
1479
|
+
Parameters
|
|
1480
|
+
----------
|
|
1481
|
+
volts : float
|
|
1482
|
+
voltage in volts
|
|
1483
|
+
|
|
1484
|
+
Returns
|
|
1485
|
+
-------
|
|
1486
|
+
retardance : float
|
|
1487
|
+
retardance in waves
|
|
1488
|
+
|
|
1489
|
+
"""
|
|
1490
|
+
|
|
1491
|
+
volts = np.asarray(volts, dtype="double")
|
|
1492
|
+
ret_nanometers = None
|
|
1493
|
+
|
|
1494
|
+
if volts < self.V_min:
|
|
1495
|
+
volts = self.V_min
|
|
1496
|
+
elif volts >= self.V_max:
|
|
1497
|
+
if self.interp_method == "linear":
|
|
1498
|
+
volts = (
|
|
1499
|
+
self.V_max - 1e-3
|
|
1500
|
+
) # interpolation breaks down at upper boundary
|
|
1501
|
+
else:
|
|
1502
|
+
volts = self.V_max
|
|
1503
|
+
|
|
1504
|
+
if self.interp_method == "linear":
|
|
1505
|
+
ret_nanometers = self.spline(volts * 1000)
|
|
1506
|
+
elif self.interp_method == "schnoor_fit":
|
|
1507
|
+
ret_nanometers = self.schnoor_fit(
|
|
1508
|
+
volts, *self.fit_params, self.wavelength
|
|
1509
|
+
)
|
|
1510
|
+
retardance = ret_nanometers / self.wavelength
|
|
1511
|
+
|
|
1512
|
+
return retardance
|