orm-iris 1.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 (129) hide show
  1. iris/__init__.py +86 -0
  2. iris/calibration/__init__.py +0 -0
  3. iris/calibration/calibration_generator.py +569 -0
  4. iris/calibration/test_calibrator_wavelength.py +123 -0
  5. iris/controllers/__init__.py +369 -0
  6. iris/controllers/camera_controller_dummy.py +89 -0
  7. iris/controllers/camera_controller_thorlabs_color.py +264 -0
  8. iris/controllers/camera_controller_thorlabs_mono.py +248 -0
  9. iris/controllers/camera_controller_webcam.py +119 -0
  10. iris/controllers/class_camera_controller.py +170 -0
  11. iris/controllers/class_spectrometer_controller.py +140 -0
  12. iris/controllers/class_xy_stage_controller.py +375 -0
  13. iris/controllers/class_z_stage_controller.py +270 -0
  14. iris/controllers/mcm301_wrapper.py +76 -0
  15. iris/controllers/oceandirect_wrapper.py +83 -0
  16. iris/controllers/raman_spectrometer_controller_Andor_dll.py +1280 -0
  17. iris/controllers/raman_spectrometer_controller_Andor_pylablib.py +438 -0
  18. iris/controllers/raman_spectrometer_controller_PI_dll.py +601 -0
  19. iris/controllers/raman_spectrometer_controller_PI_pylablib.py +419 -0
  20. iris/controllers/raman_spectrometer_controller_PI_trial.py +535 -0
  21. iris/controllers/raman_spectrometer_controller_QEPro.py +533 -0
  22. iris/controllers/raman_spectrometer_controller_WasatchEnlighten.py +237 -0
  23. iris/controllers/raman_spectrometer_controller_dummy.py +273 -0
  24. iris/controllers/xy_stage_controller_PI.py +498 -0
  25. iris/controllers/xy_stage_controller_PI_dll.py +884 -0
  26. iris/controllers/xy_stage_controller_dummy.py +309 -0
  27. iris/controllers/xy_stage_controller_m30xy.py +780 -0
  28. iris/controllers/xy_stage_controller_zaber.py +597 -0
  29. iris/controllers/z_stage_controller_dummy.py +261 -0
  30. iris/controllers/z_stage_controller_mcm301.py +408 -0
  31. iris/controllers/z_stage_controller_pfm450.py +390 -0
  32. iris/controllers/z_stage_controller_z825b.py +515 -0
  33. iris/data/__init__.py +86 -0
  34. iris/data/calibration_objective.py +672 -0
  35. iris/data/measurement_Raman.py +798 -0
  36. iris/data/measurement_RamanMap.py +2402 -0
  37. iris/data/measurement_coordinates.py +370 -0
  38. iris/data/measurement_image.py +1439 -0
  39. iris/gui/__init__.py +260 -0
  40. iris/gui/dataHub_MeaImg.py +704 -0
  41. iris/gui/dataHub_MeaRMap.py +1254 -0
  42. iris/gui/hilvl_Brightfield.py +214 -0
  43. iris/gui/hilvl_Raman.py +1469 -0
  44. iris/gui/hilvl_coorGen.py +380 -0
  45. iris/gui/image_calibration/Canvas_ROIdefinition.py +359 -0
  46. iris/gui/image_calibration/__init__.py +0 -0
  47. iris/gui/image_calibration/objective_calibration.py +1100 -0
  48. iris/gui/image_calibration/plotter_heatmap_overlay.py +421 -0
  49. iris/gui/motion_video.py +1857 -0
  50. iris/gui/raman.py +1222 -0
  51. iris/gui/shortcut_handler.py +246 -0
  52. iris/gui/submodules/__init__.py +0 -0
  53. iris/gui/submodules/heatmap_plotter_MeaRMap.py +836 -0
  54. iris/gui/submodules/image_tiling.py +544 -0
  55. iris/gui/submodules/mappingCoordinatesTreeview.py +337 -0
  56. iris/gui/submodules/meaCoor_generator/__init__.py +0 -0
  57. iris/gui/submodules/meaCoor_generator/line_zScan.py +171 -0
  58. iris/gui/submodules/meaCoor_generator/points_image.py +347 -0
  59. iris/gui/submodules/meaCoor_generator/rectangle_aroundCentre.py +196 -0
  60. iris/gui/submodules/meaCoor_generator/rectangle_endToEnd.py +309 -0
  61. iris/gui/submodules/meaCoor_generator/rectangle_image.py +514 -0
  62. iris/gui/submodules/meaCoor_generator/rectangle_video.py +505 -0
  63. iris/gui/submodules/meaCoor_generator/singlePoint_zScan.py +209 -0
  64. iris/gui/submodules/meaCoor_generator/ssfrm_tilemthd1_rect_around.py +294 -0
  65. iris/gui/submodules/meaCoor_modifier/__init__.py +0 -0
  66. iris/gui/submodules/meaCoor_modifier/ellipsify.py +219 -0
  67. iris/gui/submodules/meaCoor_modifier/every_z.py +440 -0
  68. iris/gui/submodules/meaCoor_modifier/gridify.py +1065 -0
  69. iris/gui/submodules/meaCoor_modifier/multitranslateXYZ.py +218 -0
  70. iris/gui/submodules/meaCoor_modifier/topology_visualiser.py +234 -0
  71. iris/gui/submodules/meaCoor_modifier/translateXYZ.py +501 -0
  72. iris/gui/submodules/meaCoor_modifier/zInterpolate.py +343 -0
  73. iris/gui/submodules/peakfinder_plotter_MeaRaman.py +313 -0
  74. iris/gui/timestamp_coorshift.py +123 -0
  75. iris/multiprocessing/__init__.py +46 -0
  76. iris/multiprocessing/basemanager.py +11 -0
  77. iris/multiprocessing/dataStreamer_Raman.py +635 -0
  78. iris/multiprocessing/dataStreamer_StageCam.py +758 -0
  79. iris/resources/__init__.py +0 -0
  80. iris/resources/coordinate_generators/__init__.py +0 -0
  81. iris/resources/coordinate_generators/convert_2Dto3D_ui.py +131 -0
  82. iris/resources/coordinate_generators/point_image_ui.py +150 -0
  83. iris/resources/coordinate_generators/point_zScanLinear_ui.py +157 -0
  84. iris/resources/coordinate_generators/rect_aroundcentre_ui.py +286 -0
  85. iris/resources/coordinate_generators/rect_image_ui.py +266 -0
  86. iris/resources/coordinate_generators/rect_startend_ui.py +265 -0
  87. iris/resources/coordinate_generators/rect_video_ui.py +254 -0
  88. iris/resources/coordinate_modifiers/__init__.py +0 -0
  89. iris/resources/coordinate_modifiers/ellipsify_ui.py +76 -0
  90. iris/resources/coordinate_modifiers/every_z_ui.py +211 -0
  91. iris/resources/coordinate_modifiers/gridify_setup_finetuning_ui.py +193 -0
  92. iris/resources/coordinate_modifiers/gridify_setup_naming_ui.py +163 -0
  93. iris/resources/coordinate_modifiers/gridify_setup_ui.py +486 -0
  94. iris/resources/coordinate_modifiers/multiTranslatorXYZ_ui.py +122 -0
  95. iris/resources/coordinate_modifiers/topology_visuliser_ui.py +86 -0
  96. iris/resources/coordinate_modifiers/translator_xyz_ui.py +341 -0
  97. iris/resources/coordinate_modifiers/zInterpolate_ui.py +118 -0
  98. iris/resources/dataHubPlus_Raman_ui.py +60 -0
  99. iris/resources/dataHub_Raman_ui.py +120 -0
  100. iris/resources/dataHub_coor_ui.py +79 -0
  101. iris/resources/dataHub_image_ui.py +109 -0
  102. iris/resources/font/__init__.py +0 -0
  103. iris/resources/heatmap_plotter_overlay_ui.py +74 -0
  104. iris/resources/heatmap_plotter_ui.py +262 -0
  105. iris/resources/hilvl_Raman_ui.py +276 -0
  106. iris/resources/hilvl_brightfield_ui.py +72 -0
  107. iris/resources/hilvl_coorGen_coorMod_ui.py +52 -0
  108. iris/resources/hilvl_coorGen_ui.py +127 -0
  109. iris/resources/main_analyser_ui.py +87 -0
  110. iris/resources/main_controller_ui.py +141 -0
  111. iris/resources/motion_video/__init__.py +0 -0
  112. iris/resources/motion_video/brightfieldcontrol_ui.py +144 -0
  113. iris/resources/motion_video/stagecontrol_ui.py +491 -0
  114. iris/resources/objective_calibration_controls_ui.py +215 -0
  115. iris/resources/objective_calibration_main_ui.py +107 -0
  116. iris/resources/objectives_ui.py +68 -0
  117. iris/resources/raman_ui.py +313 -0
  118. iris/resources/spectra_peak_finder_ui.py +185 -0
  119. iris/resources/spectrometer_calibration_ui.py +129 -0
  120. iris/resources/tiling_method_control_ui.py +139 -0
  121. iris/resources/tiling_method_ui.py +102 -0
  122. iris/utils/__init__.py +0 -0
  123. iris/utils/general.py +420 -0
  124. iris/utils/gridify.py +111 -0
  125. orm_iris-1.0.0.dist-info/METADATA +214 -0
  126. orm_iris-1.0.0.dist-info/RECORD +129 -0
  127. orm_iris-1.0.0.dist-info/WHEEL +5 -0
  128. orm_iris-1.0.0.dist-info/licenses/LICENSE +674 -0
  129. orm_iris-1.0.0.dist-info/top_level.txt +1 -0
iris/__init__.py ADDED
@@ -0,0 +1,86 @@
1
+ import os
2
+ from enum import Enum
3
+
4
+ from .utils.general import read_update_config_file_section
5
+
6
+ dict_lib_default = {
7
+ 'spectrometer_calibration_path': '',
8
+ 'objective_calibration_directory': './calibrations/objectives/',
9
+ }
10
+
11
+ dict_lib_comments = {
12
+ 'spectrometer_calibration_path': 'Path to the spectrometer calibration file',
13
+ 'objective_calibration_directory': 'Path to the objective calibration directory',
14
+ }
15
+
16
+ dict_lib_read = read_update_config_file_section(
17
+ dict_controllers_default=dict_lib_default,
18
+ dict_controllers_comments=dict_lib_comments,
19
+ section='LIBRARY',
20
+ )
21
+
22
+ for key in dict_lib_default.keys():
23
+ if key.endswith('_path'):
24
+ if dict_lib_read[key] == '': continue
25
+ assert os.path.isfile(dict_lib_read[key]),\
26
+ f"Error: {key} directory inserted in the config.ini file is invalid.\
27
+ Please check the directory path and try again."
28
+
29
+ class LibraryConfigEnum(Enum):
30
+ """
31
+ Enum class for the app-wide configuration parameters.
32
+ """
33
+ SPECTROMETER_CALIBRATION_PATH = dict_lib_read['spectrometer_calibration_path']
34
+ OBJECTIVE_CALIBRATION_DIR = dict_lib_read['objective_calibration_directory']
35
+
36
+ SPECTROMETER_CALIBRATION_DIR_DEFAULT = './calibrations/spectrometers/'
37
+
38
+ ############################################################################################################
39
+ # >>>>>> Data analysis specific parameters <<<<<<
40
+ ############################################################################################################
41
+ dict_dataAnalysis_default = {
42
+ # > Basic data analysis parameters <
43
+ 'laser_wavelength_nm': float(785),
44
+ 'similarity_threshold': 0.1,
45
+ 'laser_power_milliwatt': float(50),
46
+ # > Labels <
47
+ 'wavelength_label': 'Wavelength [nm]', # Label for the wavelength dataframe column and plot axis
48
+ 'intensity_label': 'Intensity [a.u.]', # Label for the intensity dataframe column and plot axis
49
+ 'raman_shift_label': 'Raman Shift [cm^-1]', # Label for the Raman shift plot axis
50
+ }
51
+
52
+ dict_dataAnalysis_comments = {
53
+ # > Basic data analysis parameters <
54
+ 'laser_wavelength_nm': 'Default value for the laser excitation wavelength in [nm] metadata',
55
+ 'similarity_threshold': 'Threshold for the similarity of the spectra for wavelength similarity check',
56
+ 'laser_power_milliwatt': 'Default value for the laser power in [mW] metadata',
57
+ # > Labels <
58
+ 'wavelength_label': 'Label for the wavelength dataframe column and plot axis',
59
+ 'intensity_label': 'Label for the intensity dataframe column and plot axis',
60
+ 'raman_shift_label': 'Label for the Raman shift plot axis',
61
+ }
62
+
63
+ dict_dataAnalysis_read = read_update_config_file_section(
64
+ dict_controllers_default=dict_dataAnalysis_default,
65
+ dict_controllers_comments=dict_dataAnalysis_comments,
66
+ section='DATA ANALYSIS',
67
+ )
68
+
69
+ class DataAnalysisConfigEnum(Enum):
70
+ """
71
+ Enum class for the app-wide configuration parameters.
72
+ """
73
+ # > Basic data analysis parameters <
74
+ LASER_WAVELENGTH_NM = dict_dataAnalysis_read['laser_wavelength_nm']
75
+ SIMILARITY_THRESHOLD = dict_dataAnalysis_read['similarity_threshold']
76
+ LASER_POWER_MILLIWATT = dict_dataAnalysis_read['laser_power_milliwatt']
77
+ # > Labels <
78
+ WAVELENGTH_LABEL = dict_dataAnalysis_read['wavelength_label']
79
+ INTENSITY_LABEL = dict_dataAnalysis_read['intensity_label']
80
+ RAMANSHIFT_LABEL = dict_dataAnalysis_read['raman_shift_label']
81
+ COORX_LABEL = 'coor_x'
82
+ COORY_LABEL = 'coor_y'
83
+ COORZ_LABEL = 'coor_z'
84
+ ID_TIMESTAMP_LABEL = 'timestamp'
85
+ LIST_MEA_LABEL = 'list_df'
86
+ AVE_MEA_LABEL = 'averaged_df'
File without changes
@@ -0,0 +1,569 @@
1
+ """
2
+ This class generates the calibration for the spectrometer to map pixels to wavelengths,
3
+ and the intensity calibration of a Raman spectrometer
4
+ """
5
+ import os
6
+ import sys
7
+ import time
8
+ from typing import Any, Callable
9
+
10
+ import threading
11
+ import multiprocessing as mp
12
+ from multiprocessing.managers import DictProxy
13
+ from multiprocessing.connection import Connection, Pipe
14
+
15
+ import PySide6.QtWidgets as qw
16
+ from PySide6.QtCore import Signal, Slot, QObject, QThread, QTimer
17
+
18
+ from scipy.optimize import curve_fit
19
+ import numpy as np
20
+
21
+ import pandas as pd
22
+
23
+ import matplotlib
24
+ import matplotlib.pyplot as plt
25
+ from matplotlib.axes import Axes
26
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
27
+ matplotlib.use('Agg')
28
+
29
+ import json
30
+
31
+ if __name__ == '__main__':
32
+ import sys
33
+ import os
34
+ libdir = os.path.abspath(r'.\iris')
35
+ sys.path.insert(0, os.path.dirname(libdir))
36
+
37
+
38
+ from iris import LibraryConfigEnum, DataAnalysisConfigEnum
39
+
40
+ from iris.resources.spectrometer_calibration_ui import Ui_spectrometerCalibrator
41
+
42
+ # Make a calibration parameters class to standardise the calibration parameters to store the following info:
43
+ # wavelen_poly_coeffs, wavelen_list_measured, wavelen_list_reference, intensity_poly_coeffs, intensity_list_measured, intensity_list_reference
44
+ class CalibrationParams(dict):
45
+ def __init__(self):
46
+ super().__init__()
47
+ self['wavelen_poly_coeffs'] = (1,0)
48
+ self['wavelen_list_measured'] = []
49
+ self['wavelen_list_reference'] = []
50
+ self['intensity_poly_coeffs'] = (0,1)
51
+ self['intensity_list_pixel_idx'] = []
52
+ self['intensity_list_measured'] = []
53
+ self['intensity_list_reference'] = []
54
+
55
+ # Polynomial coefficients of the transfer function y = poly_coeffs[0]x^(len(poly_coeffs)-1) + poly_coeffs[1]x^(len(poly_coeffs)-2) + ... + poly_coeffs[-1]
56
+ # Note: the intensity list has to be in the order of the spectrometer pixel idx
57
+
58
+ class SpectrometerCalibrator():
59
+ """
60
+ The backend for the wavelength calibration to be used in the RamanMeasurementHub
61
+ """
62
+ def __init__(self, pipe_update: Connection, pipe_measurement: Connection):
63
+ self._cal_params = CalibrationParams()
64
+
65
+ # The table to map the measured wavelengths to the reference wavelengths
66
+ self._transTable_wv = {}
67
+ self._transTable_int = {}
68
+
69
+ # Pipe to receive the path to update the calibration parameters
70
+ self._pipe_update = pipe_update
71
+ self._pipe_mea = pipe_measurement
72
+ self._flg_isrunning = mp.Event()
73
+ self._thd_update = threading.Thread(target=self._auto_update)
74
+ self._thd_update.start()
75
+ self._thd_calibrate = threading.Thread(target=self._auto_calibrate)
76
+ self._thd_calibrate.start()
77
+
78
+ def terminate(self):
79
+ """
80
+ Terminates the backend
81
+ """
82
+ self._flg_isrunning.clear()
83
+ self._thd_update.join()
84
+ self._pipe_update.close()
85
+
86
+ def calibrate_measurement(self, measurement:pd.DataFrame):
87
+ """
88
+ Calibrates the spectrometer measurement based on the calibration parameters
89
+
90
+ Args:
91
+ measurement (pd.DataFrame): The measurement data to be calibrated
92
+ """
93
+ list_wavelength_raw = measurement[DataAnalysisConfigEnum.WAVELENGTH_LABEL.value].values
94
+ list_intensity_raw = measurement[DataAnalysisConfigEnum.INTENSITY_LABEL.value].values
95
+ list_wavelength_cal = [self.get_wavelength(wavelength_raw)\
96
+ for wavelength_raw in list_wavelength_raw]
97
+ list_intensity_cal = [self.get_intensity(wavelength_raw,intensity_raw)\
98
+ for wavelength_raw,intensity_raw in zip(list_wavelength_raw,list_intensity_raw)]
99
+
100
+ # Reconstruct the dataframe with the calibrated values
101
+ cal_spectrum = measurement.copy()
102
+ cal_spectrum[DataAnalysisConfigEnum.WAVELENGTH_LABEL.value] = list_wavelength_cal
103
+ cal_spectrum[DataAnalysisConfigEnum.INTENSITY_LABEL.value] = list_intensity_cal
104
+
105
+ return cal_spectrum
106
+
107
+ def _auto_calibrate(self):
108
+ """
109
+ Automatically calibrates the spectrometer measurement
110
+ """
111
+ self._flg_isrunning.set()
112
+ while self._flg_isrunning.is_set():
113
+ if self._pipe_mea.poll(timeout=1):
114
+ measurement = self._pipe_mea.recv()
115
+ ret = None
116
+ try: ret = self.calibrate_measurement(measurement)
117
+ except Exception as e: print('ERROR SpectrometerCalibrator._auto_calibrate: ',e)
118
+ finally: self._pipe_mea.send(ret)
119
+
120
+ def _auto_update(self):
121
+ """
122
+ Automatically updates the calibration parameters
123
+ """
124
+ self._flg_isrunning.set()
125
+ while self._flg_isrunning.is_set():
126
+ if self._pipe_update.poll(timeout=1):
127
+ recv = self._pipe_update.recv()
128
+ self._cal_params = recv
129
+ self._transTable_wv.clear()
130
+ self._transTable_int.clear()
131
+ time.sleep(0.5)
132
+
133
+ def get_wavelength(self,pixel_idx:float) -> float:
134
+ """
135
+ Gets the wavelength based on the pixel index
136
+
137
+ Args:
138
+ pixel_idx (float): The pixel index
139
+
140
+ Returns:
141
+ float: The wavelength
142
+ """
143
+ assert isinstance(pixel_idx,(int,float)), 'ERROR SpectrometerCalibrator.get_wavelength: The pixel index must be a number'
144
+ pixel_req = '{:.3f}'.format(pixel_idx)
145
+ if pixel_req in self._transTable_wv.keys():
146
+ wavelength_cal = self._transTable_wv[pixel_req]
147
+ else:
148
+ wavelength_cal = np.polyval(self._cal_params['wavelen_poly_coeffs'],pixel_idx)
149
+ self._transTable_wv[pixel_req] = wavelength_cal
150
+ return wavelength_cal
151
+
152
+ def get_intensity(self,pixel_idx:float,intensity:float) -> float:
153
+ """
154
+ Gets the intensity based on the pixel index and the measured intensity
155
+
156
+ Args:
157
+ pixel_idx (float): The pixel index
158
+ intensity (float): The measured intensity
159
+
160
+ Returns:
161
+ float: The calibrated intensity
162
+ """
163
+ assert isinstance(pixel_idx,(int,float)), 'ERROR SpectrometerCalibrator.get_intensity: The pixel index must be a number'
164
+ assert isinstance(intensity,(int,float)), 'ERROR SpectrometerCalibrator.get_intensity: The intensity must be a number'
165
+ pixel_req = '{:.3f}'.format(pixel_idx)
166
+
167
+ if pixel_req in self._transTable_int.keys():
168
+ cal_ratio = self._transTable_int[pixel_req]
169
+ else:
170
+ cal_ratio = np.polyval(self._cal_params['intensity_poly_coeffs'],pixel_idx)
171
+ self._transTable_int[pixel_req] = cal_ratio
172
+
173
+ intensity_cal = cal_ratio*intensity
174
+ return intensity_cal
175
+
176
+ class Wdg_SpectrometerCalibrationGenerator(Ui_spectrometerCalibrator, qw.QWidget):
177
+ """
178
+ GUI for the spectrometer calibration generator
179
+
180
+ Args:
181
+ master (tk.Tk): The root window
182
+ pipe_update (Connection): The pipe to update the calibration parameters in the backend
183
+ dict_cal (DictCalibration): The calibration parameters dictionary
184
+ """
185
+ def __init__(self, parent:qw.QWidget, pipe_update: Connection):
186
+ super().__init__(parent)
187
+ self.setupUi(self)
188
+
189
+ # >> General parameters <<
190
+ figsize_in = (3.5,3.5)
191
+
192
+ # >> Backend functions <<
193
+ # Pipe to update the calibration parameters in the backend
194
+ self._pipe_update = pipe_update
195
+
196
+ # >> Global calibration parameters and widgets <<
197
+ # Frontend params to temporarily store the calibration parameters
198
+ self._cal_params = CalibrationParams()
199
+
200
+ # Buttons to load, save, and calculate the calibration params
201
+ self.btnLoad.clicked.connect(self._load_calibration)
202
+ self.btnSave.clicked.connect(self._save_calibration)
203
+
204
+ # >> Wavelength calibration parameters and widgets <<
205
+ # Canvas to show the wavelength calibration transfer function
206
+ self._fig_wv, self._ax_wv = plt.subplots(1,1,figsize=figsize_in)
207
+ self._canvas_wv = FigureCanvas(figure=self._fig_wv)
208
+ self.lytPixelmapCanvas.addWidget(self._canvas_wv)
209
+ self._canvas_wv.draw_idle()
210
+
211
+ # Treeview to show the measured and reference wavelengths
212
+ self._tree = self.treePixelmap
213
+ self._tree.setColumnCount(2)
214
+ self._tree.setHeaderLabels(['Measured peak loc [pixel OR nm]','Reference peak loc [nm]'])
215
+
216
+ # Button to load the measured and reference wavelengths
217
+ self.btnLoadPixelmap.clicked.connect(self._load_wavelength)
218
+
219
+ # >> Intensity calibration parameters and widgets<<
220
+ # Canvas to show the intensity calibration transfer function
221
+ figsize_in_int = (figsize_in[0]*2,figsize_in[1])
222
+ self._fig_int, axes = plt.subplots(1,2,figsize=figsize_in_int)
223
+ self._ax_int_raw, self._ax_int_cal = axes
224
+ self._canvas_int = FigureCanvas(figure=self._fig_int)
225
+ self.lytIntensityCanvas.addWidget(self._canvas_int)
226
+ self._canvas_int.draw_idle()
227
+
228
+ # Button to load the measured and reference intensities
229
+ self.btnLoadIntensity.clicked.connect(self._load_intensity)
230
+
231
+ # >> Auto-load the calibration file from the last session <<
232
+ cal_filepath = LibraryConfigEnum.SPECTROMETER_CALIBRATION_PATH.value
233
+ if os.path.isfile(cal_filepath) and cal_filepath.endswith('.json'):
234
+ QTimer.singleShot(0, lambda: self._load_calibration(cal_filepath))
235
+
236
+ def _load_calibration(self,loadpath:str|None=None) -> None:
237
+ """
238
+ Loads the calibration file
239
+
240
+ Args:
241
+ loadpath (str|None): The path to the calibration file. If None, a file dialog will be opened
242
+
243
+ Returns:
244
+ bool: True if the calibration file is loaded successfully, False otherwise
245
+ """
246
+ if loadpath is None or not os.path.exists(loadpath) or not os.path.isfile(loadpath) or not loadpath.endswith('.json'):
247
+ init_file = LibraryConfigEnum.SPECTROMETER_CALIBRATION_PATH.value
248
+ init_dir = os.path.dirname(init_file) if os.path.exists(init_file) else ''
249
+ loadpath = qw.QFileDialog.getOpenFileName(
250
+ self,
251
+ 'Load calibration file',
252
+ init_dir,
253
+ 'JSON files (*.json)',
254
+ )[0]
255
+ if loadpath == '': return
256
+
257
+ with open(loadpath,'r') as f: dict_params = json.load(f)
258
+ try:
259
+ for key in dict_params:
260
+ val = dict_params[key]
261
+ if isinstance(val,list): self._cal_params[key] = [float(v) for v in val]
262
+ elif isinstance(val,tuple): self._cal_params[key] = tuple([float(v) for v in val])
263
+ else: self._cal_params[key] = float(val)
264
+ except Exception as e: print('ERROR _load_calibration file read: ',e); return
265
+
266
+ try:self._analyse_intensity_calibration_params(calculate_transfunc=False)
267
+ except Exception as e: qw.QMessageBox.warning(self,'Load calibration error','Error in loading intensity calibration parameters:\n{}'.format(e))
268
+
269
+ try:self._analyse_wavelength_calibration_params(calculate_transfunc=False)
270
+ except Exception as e: qw.QMessageBox.warning(self,'Load calibration error','Error in loading wavelength calibration parameters:\n{}'.format(e))
271
+
272
+ # Send the loadpath to the backend
273
+ self._pipe_update.send(self._cal_params)
274
+
275
+ def _save_calibration(self):
276
+ """
277
+ Saves the calibration file as a json file
278
+ """
279
+ filepath = qw.QFileDialog.getSaveFileName(
280
+ self,
281
+ 'Save calibration file',
282
+ LibraryConfigEnum.SPECTROMETER_CALIBRATION_DIR_DEFAULT.value,
283
+ 'JSON files (*.json)',
284
+ )[0]
285
+ if filepath == '': return
286
+ with open(filepath,'w') as f: json.dump(self._cal_params,f)
287
+
288
+ def _calculate_transferFunc_int_cubic(self,list_pixel_idx:list[float],list_int_mea:list[float],list_int_ref:list[float])\
289
+ -> tuple[tuple[float,float,float,float],list[tuple[float,float]]]:
290
+ """
291
+ Generates the transfer function from the measured and reference/expected values
292
+
293
+ Args:
294
+ list_pixel_idx (list[float]): List of the pixel indices
295
+ list_int_mea (list[float]): List of the measured intensities
296
+ list_int_ref (list[float]): List of the reference intensities
297
+
298
+ Returns:
299
+ tuple[tuple[float,float,float,float],list[tuple[float,float]]]: The coefficients of
300
+ the transfer function (a,b,c,d) for the cubic function of a*x**3 + b*x**2 + c*x + d
301
+ and the list of coordinates of the intensity ratio [ref/mea]
302
+ """
303
+ def cubic(x,a,b,c,d): return a*x**3 + b*x**2 + c*x + d
304
+
305
+ # Calculate the intensity ratio (reference/measured)
306
+ list_int_ratio = [int_ref/int_mea for int_mea,int_ref in zip(list_int_mea,list_int_ref)]
307
+
308
+ list_ratio_coors = list(zip(list_pixel_idx,list_int_ratio))
309
+
310
+ # Fit the transfer function
311
+ popt,_ = curve_fit(cubic,xdata=list_pixel_idx,ydata=list_int_ratio)
312
+ popt = tuple(popt) # Convert the array to tuple for json serialisation during saving
313
+ return popt, list_ratio_coors
314
+
315
+ def _calculate_pxlMappingFunc_wv_cubic(self,list_mea:list[float],list_ref:list[float])\
316
+ -> tuple[float,float,float,float]:
317
+ """
318
+ Generates the pixel mapping function from the measured and reference/expected values
319
+
320
+ Args:
321
+ list_mea (list[float]): List of the measured values
322
+ list_ref (list[float]): List of the reference/expected values
323
+
324
+ Returns:
325
+ tuple[float,float,float,float]: The coefficients of the mapping function (a,b,c,d) for the cubic function of a*x**3 + b*x**2 + c*x + d
326
+ """
327
+ def cubic(x,a,b,c,d): return a*x**3 + b*x**2 + c*x + d
328
+
329
+ # Fit the transfer function
330
+ popt,_ = curve_fit(cubic,xdata=list_mea,ydata=list_ref)
331
+ popt = tuple(popt) # Convert the array to tuple for json serialisation during saving
332
+ return popt
333
+
334
+ def _load_intensity(self):
335
+ """
336
+ Loads the measured and reference intensities
337
+ """
338
+ qw.QMessageBox.information(
339
+ self,
340
+ 'Load intensity calibration points','Please select the csv file containing the measured'
341
+ ' and reference intensities\nThe csv file should have 3 columns: Pixel index (or raw wavelength),'
342
+ ' Measured intensity [a.u.], Reference intensity [a.u.].\nThe first line (header) will be skipped')
343
+
344
+ filepath = qw.QFileDialog.getOpenFileName(
345
+ self,
346
+ 'Load intensity calibration points',
347
+ '',
348
+ 'CSV files (*.csv)',
349
+ )[0]
350
+ if filepath == '': return
351
+
352
+ df = pd.read_csv(filepath,header=None,skiprows=1)
353
+
354
+ self._cal_params['intensity_list_pixel_idx'] = df.iloc[:,0].tolist()
355
+ self._cal_params['intensity_list_measured'] = df.iloc[:,1].tolist()
356
+ self._cal_params['intensity_list_reference'] = df.iloc[:,2].tolist()
357
+
358
+ # Calculate the transfer function and plot it
359
+ self._analyse_intensity_calibration_params()
360
+ self._pipe_update.send(self._cal_params)
361
+
362
+ def _analyse_intensity_calibration_params(self,calculate_transfunc:bool=True):
363
+ """
364
+ Analyse the intensity calibration based on the values stored in the
365
+ calibration parameters dictionary. Also assigns the calculatedtransfer function
366
+ coefficients into the dictionary if requested.
367
+
368
+ Args:
369
+ calculate_transfunc (bool): If True, the transfer function will be calculated and stored
370
+ """
371
+ list_pixel_idx = self._cal_params['intensity_list_pixel_idx']
372
+ list_int_mea = self._cal_params['intensity_list_measured']
373
+ list_int_ref = self._cal_params['intensity_list_reference']
374
+ transfer_func_coeff, list_ratio_coors = self._calculate_transferFunc_int_cubic(list_pixel_idx,list_int_mea,list_int_ref)
375
+
376
+ # Store the transfer function coefficients
377
+ if calculate_transfunc:self._cal_params['intensity_poly_coeffs'] = transfer_func_coeff
378
+
379
+ # Plot the transfer function
380
+ self._plot_int_transfer_func(self._cal_params['intensity_list_pixel_idx'],self._cal_params['intensity_list_measured'],
381
+ self._cal_params['intensity_list_reference'],list_ratio_coors)
382
+
383
+ def _plot_int_transfer_func(self,list_pixel_idx:list[float],list_int_mea:list[float],list_int_ref:list[float],
384
+ list_ratio_coors:list[tuple[float,float]]):
385
+ """
386
+ Plots the raw values and the transfer function for the intensity calibration based on given values
387
+
388
+ Args:
389
+ list_pixel_idx (list[float]): List of the pixel indices
390
+ list_int_mea (list[float]): List of the measured intensities
391
+ list_int_ref (list[float]): List of the reference intensities
392
+ list_ratio_coors (list[tuple[float,float]]): List of the coordinates of the intensity ratio [ref/mea]
393
+ """
394
+ # > Raw values <
395
+ # Generate the plot for the raw values
396
+ fig,ax = self._fig_int, self._ax_int_raw
397
+ ax.cla()
398
+ ax.scatter(list_pixel_idx,list_int_mea,c='r',label='Measured intensity',marker='x',s=1)
399
+ ax.scatter(list_pixel_idx,list_int_ref,c='b',label='Reference intensity',marker='x',s=1)
400
+ ax.set_xlabel('Pixel index')
401
+ ax.set_ylabel('Intensity [a.u.]')
402
+ ax.set_title('Intensity calibration')
403
+ ax.legend()
404
+
405
+ # > Transfer function <
406
+ # Grab the transfer function
407
+ transfer_func_coeff = self._cal_params['intensity_poly_coeffs']
408
+
409
+ # Generate the points for the plot
410
+ x_min = min(list_pixel_idx)
411
+ x_max = max(list_pixel_idx)
412
+ x = np.linspace(x_min,x_max,100)
413
+ y = np.polyval(transfer_func_coeff,x)
414
+
415
+ x2 = [coor[0] for coor in list_ratio_coors]
416
+ y2 = [coor[1] for coor in list_ratio_coors]
417
+ y2_min = min(y2)
418
+ y2_max = max(y2)
419
+ if y2_min > 0 and y2_max > 0: y2_min = 0
420
+ if y2_min < 0 and y2_max < 0: y2_max = 0
421
+
422
+ # Generate the plot
423
+ fig,ax = self._fig_int, self._ax_int_cal
424
+ ax:Axes
425
+ ax.cla()
426
+ ax.plot(x,y,c='r',label='Transfer function')
427
+ ax.scatter(x2,y2,c='b',label='Intensity ratio [ref/mea]',marker='x',s=1)
428
+ ax.set_ylim(y2_min-abs(y2_min)*0.1,y2_max+abs(y2_max)*0.1)
429
+ ax.set_xlabel('Pixel index [pixel] OR Wavelength [nm]')
430
+ ax.set_ylabel('Intensity [a.u.]')
431
+ ax.set_title('Intensity calibration transfer function')
432
+ ax.legend()
433
+
434
+ # Show the plot and redraw the canvas
435
+ self._canvas_int.draw()
436
+
437
+ def _load_wavelength(self):
438
+ """
439
+ Loads the measured and reference wavelengths
440
+ """
441
+ qw.QMessageBox.information(
442
+ self,
443
+ 'Load wavelength calibration points','Please select the csv file containing the measured'
444
+ ' and reference wavelengths\nThe csv file should have 2 columns: Measured peak loc [pixel OR nm],'
445
+ ' Reference peak loc [nm].\nThe first line (header) will be skipped')
446
+
447
+ filepath = qw.QFileDialog.getOpenFileName(
448
+ self,
449
+ 'Load wavelength calibration points',
450
+ '',
451
+ 'CSV files (*.csv)',
452
+ )[0]
453
+ if filepath == '': return
454
+
455
+ df = pd.read_csv(filepath,header=None,skiprows=1)
456
+
457
+ self._cal_params['wavelen_list_measured'] = df.iloc[:,0].tolist()
458
+ self._cal_params['wavelen_list_reference'] = df.iloc[:,1].tolist()
459
+
460
+ # Calculate the transfer function
461
+ self._analyse_wavelength_calibration_params()
462
+ self._pipe_update.send(self._cal_params)
463
+
464
+ def _analyse_wavelength_calibration_params(self,calculate_transfunc:bool=True):
465
+ """
466
+ Analyse the wavelength calibration based on the values stored in the
467
+ calibration parameters dictionary. Also assigns the calculated transfer function
468
+ coefficients into the dictionary.
469
+
470
+ Args:
471
+ calculate_transfunc (bool): If True, the transfer function will be calculated
472
+ """
473
+ if calculate_transfunc:
474
+ list_peakRS_mea = self._cal_params['wavelen_list_measured']
475
+ list_peakRS_ref = self._cal_params['wavelen_list_reference']
476
+ transfer_func_coeff = self._calculate_pxlMappingFunc_wv_cubic(list_peakRS_mea,list_peakRS_ref)
477
+ self._cal_params['wavelen_poly_coeffs'] = transfer_func_coeff
478
+
479
+ # Update the treeview
480
+ self._update_treeview_wv()
481
+
482
+ # Plot the transfer function
483
+ self._plot_wv_pxlMapping_func(self._cal_params['wavelen_list_measured'],self._cal_params['wavelen_list_reference'])
484
+
485
+ def _plot_wv_pxlMapping_func(self,list_peakRS_mea:list[float],list_peakRS_ref:list[float]):
486
+ """
487
+ Plots the pixel mapping function for the wavelength calibration
488
+
489
+ Args:
490
+ list_peakRS_mea (list[float]): List of the Raman shift peaks of the sample representative
491
+ list_peakRS_ref (list[float]): List of the Raman shift peaks of the reference
492
+ """
493
+ # > Grab the transfer function <
494
+ transfer_func_coeff = self._cal_params['wavelen_poly_coeffs']
495
+
496
+ # Generate the x and y values from the transfer function
497
+ x_min = min(list_peakRS_mea)
498
+ x_max = max(list_peakRS_mea)
499
+ x = np.linspace(x_min,x_max,100)
500
+ y = np.polyval(transfer_func_coeff,x)
501
+
502
+ # Generate the plot
503
+ fig,ax = self._fig_wv, self._ax_wv
504
+ ax.cla()
505
+ ax.plot(x,y,c='b',label='Pixel mapping func')
506
+ ax.scatter(list_peakRS_mea,list_peakRS_ref,c='r',label='Peaks')
507
+ ax.set_xlabel('Measurement Raman peaks [nm OR pixel]')
508
+ ax.set_ylabel('Reference Raman peaks [nm]')
509
+ ax.set_title('Spectrometer pixel mapping')
510
+ ax.legend()
511
+
512
+ # Add a label to each scatter points
513
+ for i,peakRS in enumerate(list_peakRS_mea):
514
+ ax.annotate(str(peakRS),(list_peakRS_mea[i],list_peakRS_ref[i]))
515
+
516
+ # Show the plot and redraw the canvas
517
+ self._canvas_wv.draw()
518
+
519
+ def _update_treeview_wv(self):
520
+ """
521
+ Updates the treeview for the wavelength calibration
522
+ """
523
+ self._tree.clear()
524
+ for (mea,ref) in zip(self._cal_params['wavelen_list_measured'],self._cal_params['wavelen_list_reference']):
525
+ item = qw.QTreeWidgetItem()
526
+ item.setText(0,str(mea))
527
+ item.setText(1,str(ref))
528
+
529
+ self._tree.addTopLevelItem(item)
530
+
531
+ class MainWindow_SpectrometerCalibrationGenerator(qw.QMainWindow):
532
+ """
533
+ Main window for the spectrometer calibration generator
534
+ """
535
+ def __init__(self, pipe_update: Connection):
536
+ super().__init__()
537
+ self.setWindowTitle('Spectrometer Calibration Generator')
538
+ self._wdg = Wdg_SpectrometerCalibrationGenerator(self, pipe_update)
539
+ self.setCentralWidget(self._wdg)
540
+
541
+ def get_WdgCalibrator(self) -> Wdg_SpectrometerCalibrationGenerator:
542
+ """
543
+ Gets the calibrator widget
544
+
545
+ Returns:
546
+ Wdg_SpectrometerCalibrationGenerator: The calibrator widget
547
+ """
548
+ return self._wdg
549
+
550
+ @Slot()
551
+ def closeEvent(self, event):
552
+ """
553
+ Overrides the close event to hide the window instead of closing it
554
+ """
555
+ self.hide()
556
+ event.ignore()
557
+
558
+ def test():
559
+ app = qw.QApplication([])
560
+ main_window = qw.QMainWindow()
561
+ pipe_update, pipe_update_child = Pipe()
562
+ wdg = Wdg_SpectrometerCalibrationGenerator(main_window, pipe_update)
563
+ # SpectrometerCalibrator(pipe_update_child, Pipe()[0])
564
+ main_window.setCentralWidget(wdg)
565
+ main_window.show()
566
+ sys.exit(app.exec())
567
+
568
+ if __name__ == '__main__':
569
+ test()