waveorder 2.2.1__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.
Files changed (58) hide show
  1. waveorder/_version.py +16 -3
  2. waveorder/acq/__init__.py +0 -0
  3. waveorder/acq/acq_functions.py +166 -0
  4. waveorder/assets/HSV_legend.png +0 -0
  5. waveorder/assets/JCh_legend.png +0 -0
  6. waveorder/assets/waveorder_plugin_logo.png +0 -0
  7. waveorder/calib/Calibration.py +1512 -0
  8. waveorder/calib/Optimization.py +470 -0
  9. waveorder/calib/__init__.py +0 -0
  10. waveorder/calib/calibration_workers.py +464 -0
  11. waveorder/cli/apply_inverse_models.py +328 -0
  12. waveorder/cli/apply_inverse_transfer_function.py +379 -0
  13. waveorder/cli/compute_transfer_function.py +432 -0
  14. waveorder/cli/gui_widget.py +58 -0
  15. waveorder/cli/main.py +39 -0
  16. waveorder/cli/monitor.py +163 -0
  17. waveorder/cli/option_eat_all.py +47 -0
  18. waveorder/cli/parsing.py +122 -0
  19. waveorder/cli/printing.py +16 -0
  20. waveorder/cli/reconstruct.py +67 -0
  21. waveorder/cli/settings.py +187 -0
  22. waveorder/cli/utils.py +175 -0
  23. waveorder/filter.py +1 -2
  24. waveorder/focus.py +136 -25
  25. waveorder/io/__init__.py +0 -0
  26. waveorder/io/_reader.py +61 -0
  27. waveorder/io/core_functions.py +272 -0
  28. waveorder/io/metadata_reader.py +195 -0
  29. waveorder/io/utils.py +175 -0
  30. waveorder/io/visualization.py +160 -0
  31. waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
  32. waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
  33. waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
  34. waveorder/models/isotropic_thin_3d.py +73 -72
  35. waveorder/models/phase_thick_3d.py +103 -4
  36. waveorder/napari.yaml +36 -0
  37. waveorder/plugin/__init__.py +9 -0
  38. waveorder/plugin/gui.py +1094 -0
  39. waveorder/plugin/gui.ui +1440 -0
  40. waveorder/plugin/job_manager.py +42 -0
  41. waveorder/plugin/main_widget.py +1605 -0
  42. waveorder/plugin/tab_recon.py +3294 -0
  43. waveorder/scripts/__init__.py +0 -0
  44. waveorder/scripts/launch_napari.py +13 -0
  45. waveorder/scripts/repeat-cal-acq-rec.py +147 -0
  46. waveorder/scripts/repeat-calibration.py +31 -0
  47. waveorder/scripts/samples.py +85 -0
  48. waveorder/scripts/simulate_zarr_acq.py +204 -0
  49. waveorder/util.py +1 -1
  50. waveorder/visuals/napari_visuals.py +1 -1
  51. waveorder-3.0.0.dist-info/METADATA +350 -0
  52. waveorder-3.0.0.dist-info/RECORD +69 -0
  53. {waveorder-2.2.1.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
  54. waveorder-3.0.0.dist-info/entry_points.txt +5 -0
  55. {waveorder-2.2.1.dist-info → waveorder-3.0.0.dist-info}/licenses/LICENSE +13 -1
  56. waveorder-2.2.1.dist-info/METADATA +0 -188
  57. waveorder-2.2.1.dist-info/RECORD +0 -27
  58. {waveorder-2.2.1.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