microeye 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. microEye/__init__.py +47 -0
  2. microEye/_version.py +2 -0
  3. microEye/analysis/__init__.py +1 -0
  4. microEye/analysis/checklist_dialog.py +143 -0
  5. microEye/analysis/cmosMaps.py +228 -0
  6. microEye/analysis/filters/__init__.py +9 -0
  7. microEye/analysis/filters/base.py +21 -0
  8. microEye/analysis/filters/spatial.py +338 -0
  9. microEye/analysis/filters/temporal.py +76 -0
  10. microEye/analysis/fitting/__init__.py +0 -0
  11. microEye/analysis/fitting/fit.py +680 -0
  12. microEye/analysis/fitting/nena.py +375 -0
  13. microEye/analysis/fitting/phasor_fit.py +90 -0
  14. microEye/analysis/fitting/processing.py +317 -0
  15. microEye/analysis/fitting/psf/__init__.py +6 -0
  16. microEye/analysis/fitting/psf/extract.py +1129 -0
  17. microEye/analysis/fitting/psf/rubost_mean.py +150 -0
  18. microEye/analysis/fitting/psf/spline.py +167 -0
  19. microEye/analysis/fitting/psf/stats/__init__.py +12 -0
  20. microEye/analysis/fitting/psf/stats/core.py +295 -0
  21. microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
  22. microEye/analysis/fitting/psf/stats/io.py +104 -0
  23. microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
  24. microEye/analysis/fitting/psf/temp.py +147 -0
  25. microEye/analysis/fitting/psf/test.py +47 -0
  26. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
  27. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
  28. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
  29. microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
  30. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
  31. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
  32. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
  33. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
  34. microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
  35. microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
  36. microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
  37. microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
  38. microEye/analysis/fitting/results.py +917 -0
  39. microEye/analysis/fitting/results_stats.py +251 -0
  40. microEye/analysis/fitting/tardis.py +209 -0
  41. microEye/analysis/multi_viewer.py +568 -0
  42. microEye/analysis/processing/__init__.py +1 -0
  43. microEye/analysis/processing/frc.py +120 -0
  44. microEye/analysis/rendering/__init__.py +3 -0
  45. microEye/analysis/rendering/base.py +452 -0
  46. microEye/analysis/rendering/cloud.py +215 -0
  47. microEye/analysis/rendering/core.py +47 -0
  48. microEye/analysis/rendering/volumetric.py +198 -0
  49. microEye/analysis/tools/__init__.py +0 -0
  50. microEye/analysis/tools/kymograms.py +1079 -0
  51. microEye/analysis/tools/roi_selectors.py +556 -0
  52. microEye/analysis/utils/__init__.py +3 -0
  53. microEye/analysis/utils/coordinates.py +18 -0
  54. microEye/analysis/utils/images.py +68 -0
  55. microEye/analysis/utils/windows.py +19 -0
  56. microEye/analysis/viewer/__init__.py +3 -0
  57. microEye/analysis/viewer/image_options_widget.py +615 -0
  58. microEye/analysis/viewer/images.py +1225 -0
  59. microEye/analysis/viewer/layers_widget.py +415 -0
  60. microEye/analysis/viewer/localizations.py +1304 -0
  61. microEye/analysis/viewer/psf.py +1003 -0
  62. microEye/analysis/viewer/volume.py +456 -0
  63. microEye/hardware/__init__.py +3 -0
  64. microEye/hardware/cams/__init__.py +29 -0
  65. microEye/hardware/cams/camera_calibration.py +99 -0
  66. microEye/hardware/cams/camera_list.py +528 -0
  67. microEye/hardware/cams/camera_options.py +694 -0
  68. microEye/hardware/cams/camera_panel.py +941 -0
  69. microEye/hardware/cams/dummy/__init__.py +1 -0
  70. microEye/hardware/cams/dummy/dummy_panel.py +759 -0
  71. microEye/hardware/cams/jobs.py +497 -0
  72. microEye/hardware/cams/line_profiler.py +99 -0
  73. microEye/hardware/cams/linescan/IR_Cam.py +474 -0
  74. microEye/hardware/cams/linescan/__init__.py +1 -0
  75. microEye/hardware/cams/micam.py +524 -0
  76. microEye/hardware/cams/pco/__init__.py +58 -0
  77. microEye/hardware/cams/pco/enums.py +382 -0
  78. microEye/hardware/cams/pco/pco_cam.py +761 -0
  79. microEye/hardware/cams/pco/pco_panel.py +477 -0
  80. microEye/hardware/cams/shortcuts.py +312 -0
  81. microEye/hardware/cams/thorlabs/__init__.py +1 -0
  82. microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
  83. microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
  84. microEye/hardware/cams/ueye/__init__.py +1 -0
  85. microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
  86. microEye/hardware/cams/ueye/ueye_panel.py +861 -0
  87. microEye/hardware/cams/vimba/__init__.py +1 -0
  88. microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
  89. microEye/hardware/cams/vimba/vimba_panel.py +813 -0
  90. microEye/hardware/device.py +60 -0
  91. microEye/hardware/lasers/__init__.py +13 -0
  92. microEye/hardware/lasers/io_matchbox.py +791 -0
  93. microEye/hardware/lasers/io_params.py +85 -0
  94. microEye/hardware/lasers/io_single_laser.py +742 -0
  95. microEye/hardware/lasers/laser_relay.py +594 -0
  96. microEye/hardware/mieye/__init__.py +1 -0
  97. microEye/hardware/mieye/acquisition_manager.py +467 -0
  98. microEye/hardware/mieye/devices_manager.py +533 -0
  99. microEye/hardware/mieye/miEye.py +659 -0
  100. microEye/hardware/misc/__init__.py +0 -0
  101. microEye/hardware/misc/acquisition_view.py +71 -0
  102. microEye/hardware/misc/reglo.py +761 -0
  103. microEye/hardware/misc/temp.py +188 -0
  104. microEye/hardware/port_config.py +59 -0
  105. microEye/hardware/protocols/__init__.py +2 -0
  106. microEye/hardware/protocols/actions.py +402 -0
  107. microEye/hardware/protocols/actions_items.py +703 -0
  108. microEye/hardware/protocols/designer.py +244 -0
  109. microEye/hardware/protocols/scene_manager.py +191 -0
  110. microEye/hardware/protocols/serialization.py +97 -0
  111. microEye/hardware/pycromanager/__init__.py +16 -0
  112. microEye/hardware/pycromanager/core.py +1433 -0
  113. microEye/hardware/pycromanager/devices.py +461 -0
  114. microEye/hardware/pycromanager/enums.py +107 -0
  115. microEye/hardware/pycromanager/headless.py +153 -0
  116. microEye/hardware/pycromanager/utils.py +34 -0
  117. microEye/hardware/pycromanager/widgets/__init__.py +5 -0
  118. microEye/hardware/pycromanager/widgets/bridges.py +407 -0
  119. microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
  120. microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
  121. microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
  122. microEye/hardware/stages/__init__.py +18 -0
  123. microEye/hardware/stages/elliptec/__init__.py +5 -0
  124. microEye/hardware/stages/elliptec/baseDevice.py +314 -0
  125. microEye/hardware/stages/elliptec/device.py +384 -0
  126. microEye/hardware/stages/elliptec/deviceID.py +212 -0
  127. microEye/hardware/stages/elliptec/devicePort.py +379 -0
  128. microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
  129. microEye/hardware/stages/elliptec/devicesView.py +706 -0
  130. microEye/hardware/stages/elliptec/ellDevices.py +134 -0
  131. microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
  132. microEye/hardware/stages/elliptec/motorInfo.py +153 -0
  133. microEye/hardware/stages/elliptec/stage.py +62 -0
  134. microEye/hardware/stages/elliptec/test.py +139 -0
  135. microEye/hardware/stages/kinesis/__init__.py +1 -0
  136. microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
  137. microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
  138. microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
  139. microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
  140. microEye/hardware/stages/kinesis/kinesis.py +776 -0
  141. microEye/hardware/stages/piezo_concept.py +607 -0
  142. microEye/hardware/stages/stabilizer.py +785 -0
  143. microEye/hardware/stages/stage.py +89 -0
  144. microEye/hardware/widgets/__init__.py +10 -0
  145. microEye/hardware/widgets/controller.py +246 -0
  146. microEye/hardware/widgets/devices.py +133 -0
  147. microEye/hardware/widgets/focusWidget.py +264 -0
  148. microEye/hardware/widgets/qlist_slider.py +113 -0
  149. microEye/hardware/widgets/scan_acquisition.py +424 -0
  150. microEye/icons/1024.png +0 -0
  151. microEye/icons/128.png +0 -0
  152. microEye/icons/16.png +0 -0
  153. microEye/icons/24.png +0 -0
  154. microEye/icons/256.png +0 -0
  155. microEye/icons/32.png +0 -0
  156. microEye/icons/48.png +0 -0
  157. microEye/icons/512.png +0 -0
  158. microEye/icons/64.png +0 -0
  159. microEye/icons/__init__.py +0 -0
  160. microEye/icons/close.svg +88 -0
  161. microEye/icons/mieye.png +0 -0
  162. microEye/icons/min.svg +83 -0
  163. microEye/icons/viewer.png +0 -0
  164. microEye/launcher.py +42 -0
  165. microEye/qt.py +181 -0
  166. microEye/utils/__init__.py +2 -0
  167. microEye/utils/enum_encoder.py +10 -0
  168. microEye/utils/expandable_groupbox.py +93 -0
  169. microEye/utils/gui_helper.py +457 -0
  170. microEye/utils/hid/__init__.py +8 -0
  171. microEye/utils/hid/controller.py +153 -0
  172. microEye/utils/hid/device.py +63 -0
  173. microEye/utils/hid/enums.py +140 -0
  174. microEye/utils/hid/utils.py +58 -0
  175. microEye/utils/labelled_slider.py +134 -0
  176. microEye/utils/metadata.py +599 -0
  177. microEye/utils/metadata_tree.py +718 -0
  178. microEye/utils/micro_launcher.py +245 -0
  179. microEye/utils/parameter_tree.py +325 -0
  180. microEye/utils/pyscripting.py +444 -0
  181. microEye/utils/retry_exec.py +35 -0
  182. microEye/utils/start_gui.py +112 -0
  183. microEye/utils/thread_worker.py +115 -0
  184. microEye/utils/uImage.py +1512 -0
  185. microeye-2.3.2.dist-info/METADATA +401 -0
  186. microeye-2.3.2.dist-info/RECORD +190 -0
  187. microeye-2.3.2.dist-info/WHEEL +5 -0
  188. microeye-2.3.2.dist-info/entry_points.txt +2 -0
  189. microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
  190. microeye-2.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,708 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Callable, Optional
4
+
5
+ import numpy as np
6
+ from scipy import interpolate, optimize, stats
7
+
8
+
9
+ class CurveFitMethod(Enum):
10
+ '''Methods for curve fitting'''
11
+
12
+ CSPLINE = 'cspline'
13
+ LINEAR = 'linear'
14
+ SIGMOID = 'sigmoid'
15
+ POLYNOMIAL = 'polynomial'
16
+ PIECEWISE = 'piecewise'
17
+ ASTIGMATIC_PSF = 'astigmatic_psf'
18
+
19
+ def __str__(self):
20
+ return self.value
21
+
22
+ def __reduce__(self):
23
+ '''__reduce__ function for pickle'''
24
+ return (CurveFitMethod, (self.value,))
25
+
26
+
27
+ @dataclass
28
+ class CurveResult:
29
+ '''Results from curve analysis'''
30
+
31
+ method: CurveFitMethod
32
+ parameters: dict
33
+ r_squared: float
34
+ derivative_max: float
35
+ inflection_point: Optional[float] = None
36
+ significant_region: tuple[float, float] = None
37
+
38
+ def to_dict(self):
39
+ return {
40
+ 'Method': [self.method.value],
41
+ 'R²': [self.r_squared],
42
+ 'Max dY/dZ': [self.derivative_max],
43
+ 'Inflection Point': [self.inflection_point],
44
+ 'Significant Region': list(self.significant_region)
45
+ if self.significant_region
46
+ else None,
47
+ **{f'Parameter {k}': [v] for k, v in self.parameters.items()},
48
+ }
49
+
50
+ def get_spline_func(self) -> Optional[interpolate.UnivariateSpline]:
51
+ '''Get the spline function if method is CSPLINE'''
52
+ if self.method == CurveFitMethod.CSPLINE:
53
+ return interpolate.UnivariateSpline(
54
+ self.parameters['x_data'],
55
+ self.parameters['y_data'],
56
+ s=self.parameters['smoothing_factor'],
57
+ k=self.parameters['degree'],
58
+ )
59
+
60
+ return None
61
+
62
+ def get_data(self, x_data: np.ndarray):
63
+ return CurveAnalyzer.get_data(x_data, self)
64
+
65
+
66
+ @dataclass
67
+ class PSFParameters:
68
+ '''Parameters for astigmatic Gaussian PSF model'''
69
+
70
+ sigma0x: float # Base sigma x
71
+ Ax: float # First coefficient x
72
+ Bx: float # Second coefficient x
73
+ sigma0y: float # Base sigma y
74
+ Ay: float # First coefficient y
75
+ By: float # Second coefficient y
76
+ gamma: float # Scale factor
77
+ d: float # Characteristic distance
78
+
79
+ def to_dict(self):
80
+ return {
81
+ 'σ₀ₓ': self.sigma0x,
82
+ 'Aₓ': self.Ax,
83
+ 'Bₓ': self.Bx,
84
+ 'σ₀y': self.sigma0y,
85
+ 'Ay': self.Ay,
86
+ 'By': self.By,
87
+ 'γ': self.gamma,
88
+ 'd': self.d,
89
+ }
90
+
91
+
92
+ class CurveAnalyzer:
93
+ @staticmethod
94
+ def sigmoid(x: np.ndarray, L: float, k: float, x0: float, b: float) -> np.ndarray:
95
+ '''Generalized sigmoid function'''
96
+ return L / (1 + np.exp(-k * (x - x0))) + b
97
+
98
+ @staticmethod
99
+ def sigmoid_derivative(
100
+ x: np.ndarray, L: float, k: float, x0: float, b: float
101
+ ) -> np.ndarray:
102
+ '''Derivative of sigmoid function'''
103
+ exp_term = np.exp(-k * (x - x0))
104
+ return (L * k * exp_term) / (1 + exp_term) ** 2
105
+
106
+ @staticmethod
107
+ def sigma_model(z, sigma0, A0, B0, gamma, d):
108
+ '''
109
+ Gaussian PSF model for σ(z).
110
+
111
+ Parameters
112
+ ----------
113
+ z : np.ndarray
114
+ Z positions
115
+ sigma0 : float
116
+ σ at z=0
117
+ A0 : float
118
+ Cubic term coefficient
119
+ B0 : float
120
+ Quartic term coefficient
121
+ gamma : float
122
+ Z-offset
123
+ d : float
124
+ Characteristic length scale
125
+
126
+ Returns
127
+ -------
128
+ np.ndarray
129
+ σ values
130
+ '''
131
+ # Center and normalize z
132
+ z_norm = (z - gamma) / d
133
+
134
+ # Calculate σ(z)
135
+ sigma = sigma0 * np.sqrt(1 + z_norm**2 + A0 * z_norm**3 + B0 * z_norm**4)
136
+
137
+ return sigma
138
+
139
+ @staticmethod
140
+ def xy_sigma_model(z, sigma0x, Ax, Bx, sigma0y, Ay, By, gamma, d):
141
+ '''
142
+ Astigmatic Gaussian PSF model for σx(z) and σy(z).
143
+
144
+ Parameters
145
+ ----------
146
+ z : np.ndarray
147
+ Z positions
148
+ sigma0x : float
149
+ σx at z=0
150
+ Ax : float
151
+ Cubic term coefficient for σx
152
+ Bx : float
153
+ Quartic term coefficient for σx
154
+ sigma0y : float
155
+ σy at z=0
156
+ Ay : float
157
+ Cubic term coefficient for σy
158
+ By : float
159
+ Quartic term coefficient for σy
160
+ gamma : float
161
+ Z-offset
162
+ d : float
163
+ Characteristic length scale
164
+
165
+ Returns
166
+ -------
167
+ tuple[np.ndarray, np.ndarray]
168
+ σx and σy values
169
+ '''
170
+ # Center and normalize z
171
+ z_norm = (z - gamma) / d
172
+
173
+ # Calculate σx(z) and σy(z)
174
+ sigma_x = sigma0x * np.sqrt(1 + z_norm**2 + Ax * z_norm**3 + Bx * z_norm**4)
175
+ sigma_y = sigma0y * np.sqrt(1 + z_norm**2 + Ay * z_norm**3 + By * z_norm**4)
176
+
177
+ return sigma_x, sigma_y
178
+
179
+ @staticmethod
180
+ def fit_stat_curve(
181
+ selected_stat: str,
182
+ get_stats: Callable,
183
+ region: tuple[int, int],
184
+ method: CurveFitMethod = CurveFitMethod.CSPLINE,
185
+ derivative_threshold: float = 0.01,
186
+ smoothing_factor: Optional[float] = 0,
187
+ ) -> Optional[CurveResult]:
188
+ '''
189
+ Analyze the curve characteristics using specified method.
190
+
191
+ Parameters
192
+ ----------
193
+ selected_stat : str
194
+ The statistic to analyze
195
+ get_stats : Callable
196
+ Function to get the statistical data
197
+ region : tuple[int, int]
198
+ The z-region to analyze (start, end)
199
+ method : CurveFitMethod
200
+ Method to fit the curve
201
+ derivative_threshold : float
202
+ Threshold for considering dY/dZ significant
203
+ smoothing_factor : Optional[float]
204
+ Smoothing factor for cubic spline (None for auto)
205
+
206
+ Returns
207
+ -------
208
+ Optional[CurveResult]
209
+ Curve analysis results including fit parameters and characteristics
210
+ '''
211
+ if (
212
+ selected_stat not in ['Sigma (diff)', 'Sigma (x/y)', 'Sigma² (diff)']
213
+ or method == CurveFitMethod.ASTIGMATIC_PSF
214
+ ):
215
+ return None
216
+
217
+ # Get data
218
+ z_indices, stat_data, lower_bounds, upper_bounds = get_stats()
219
+
220
+ # Select data within region
221
+ mask = (z_indices >= region[0]) & (z_indices <= region[1])
222
+ x_data = z_indices[mask]
223
+ y_data = stat_data[mask]
224
+
225
+ # Remove NaN values
226
+ valid_mask = ~np.isnan(y_data)
227
+ x_data = x_data[valid_mask]
228
+ y_data = y_data[valid_mask]
229
+
230
+ if len(x_data) < 4:
231
+ return None
232
+
233
+ if method == CurveFitMethod.SIGMOID:
234
+ # Estimate initial parameters
235
+ L = np.max(y_data) - np.min(y_data)
236
+ b = np.min(y_data)
237
+ x0 = x_data[len(x_data) // 2] # Initial guess for midpoint
238
+ k = 0.01 # Initial guess for steepness
239
+
240
+ try:
241
+ # Fit sigmoid
242
+ popt, _ = optimize.curve_fit(
243
+ CurveAnalyzer.sigmoid,
244
+ x_data,
245
+ y_data,
246
+ p0=[L, k, x0, b],
247
+ bounds=([0, 0, -np.inf, -np.inf], [np.inf, np.inf, np.inf, np.inf]),
248
+ )
249
+
250
+ # Calculate R²
251
+ y_fit = CurveAnalyzer.sigmoid(x_data, *popt)
252
+ residuals = y_data - y_fit
253
+ ss_res = np.sum(residuals**2)
254
+ ss_tot = np.sum((y_data - np.mean(y_data)) ** 2)
255
+ r_squared = 1 - (ss_res / ss_tot)
256
+
257
+ # Find inflection point (maximum of derivative)
258
+ x_fine = np.linspace(x_data[0], x_data[-1], 1000)
259
+ dy_dx = CurveAnalyzer.sigmoid_derivative(x_fine, *popt)
260
+ inflection_idx = np.argmax(dy_dx)
261
+ inflection_point = x_fine[inflection_idx]
262
+ max_derivative = dy_dx[inflection_idx]
263
+
264
+ # Find region where derivative is significant
265
+ significant_mask = dy_dx > derivative_threshold * max_derivative
266
+ significant_region = (
267
+ x_fine[np.where(significant_mask)[0][0]],
268
+ x_fine[np.where(significant_mask)[0][-1]],
269
+ )
270
+
271
+ parameters = {'L': popt[0], 'k': popt[1], 'x0': popt[2], 'b': popt[3]}
272
+
273
+ parameters['x_data'] = x_data.tolist()
274
+ parameters['y_data'] = y_data.tolist()
275
+
276
+ return CurveResult(
277
+ method=method,
278
+ parameters=parameters,
279
+ r_squared=r_squared,
280
+ derivative_max=max_derivative,
281
+ inflection_point=inflection_point,
282
+ significant_region=significant_region,
283
+ )
284
+
285
+ except (RuntimeError, optimize.OptimizeWarning):
286
+ return None
287
+
288
+ elif method == CurveFitMethod.POLYNOMIAL:
289
+ # Implement polynomial fitting if sigmoid doesn't work well
290
+ degree = 3 # Cubic polynomial by default
291
+ coeffs = np.polyfit(x_data, y_data, degree)
292
+ y_fit = np.polyval(coeffs, x_data)
293
+
294
+ # Calculate R²
295
+ residuals = y_data - y_fit
296
+ ss_res = np.sum(residuals**2)
297
+ ss_tot = np.sum((y_data - np.mean(y_data)) ** 2)
298
+ r_squared = 1 - (ss_res / ss_tot)
299
+
300
+ # Find maximum derivative
301
+ deriv_coeffs = np.polyder(coeffs)
302
+ x_fine = np.linspace(x_data[0], x_data[-1], 1000)
303
+ dy_dx = np.polyval(deriv_coeffs, x_fine)
304
+ max_derivative = np.max(np.abs(dy_dx))
305
+
306
+ # Find region where derivative is significant
307
+ significant_mask = np.abs(dy_dx) > derivative_threshold * max_derivative
308
+ significant_region = (
309
+ x_fine[np.where(significant_mask)[0][0]],
310
+ x_fine[np.where(significant_mask)[0][-1]],
311
+ )
312
+
313
+ parameters = {f'c{i}': c for i, c in enumerate(coeffs[::-1])}
314
+
315
+ parameters['x_data'] = x_data.tolist()
316
+ parameters['y_data'] = y_data.tolist()
317
+
318
+ return CurveResult(
319
+ method=method,
320
+ parameters=parameters,
321
+ r_squared=r_squared,
322
+ derivative_max=max_derivative,
323
+ significant_region=significant_region,
324
+ )
325
+ elif method == CurveFitMethod.CSPLINE:
326
+ try:
327
+ # Create cubic spline
328
+ cs = interpolate.UnivariateSpline(
329
+ x_data, y_data, s=smoothing_factor, k=3
330
+ )
331
+
332
+ # Generate fine grid for analysis
333
+ x_fine = np.linspace(x_data[0], x_data[-1], 1000)
334
+ y_fit = cs(x_fine)
335
+
336
+ # Calculate first derivative
337
+ dy_dx = cs.derivative(1)(x_fine)
338
+ max_derivative = np.max(np.abs(dy_dx))
339
+
340
+ # Find inflection points (zeros of second derivative)
341
+ d2y_dx2 = cs.derivative(2)(x_fine)
342
+ inflection_indices = np.where(np.diff(np.signbit(d2y_dx2)))[0]
343
+
344
+ if len(inflection_indices) > 0:
345
+ # Find inflection point with maximum absolute derivative
346
+ max_deriv_at_inflection = np.max(np.abs(dy_dx[inflection_indices]))
347
+ inflection_point = x_fine[
348
+ inflection_indices[np.argmax(np.abs(dy_dx[inflection_indices]))]
349
+ ]
350
+ else:
351
+ inflection_point = None
352
+
353
+ # Find region where derivative is significant
354
+ significant_mask = np.abs(dy_dx) > derivative_threshold * max_derivative
355
+ if np.any(significant_mask):
356
+ significant_region = (
357
+ x_fine[np.where(significant_mask)[0][0]],
358
+ x_fine[np.where(significant_mask)[0][-1]],
359
+ )
360
+ else:
361
+ significant_region = None
362
+
363
+ # Calculate R² on original data points
364
+ y_fit_original = cs(x_data)
365
+ residuals = y_data - y_fit_original
366
+ ss_res = np.sum(residuals**2)
367
+ ss_tot = np.sum((y_data - np.mean(y_data)) ** 2)
368
+ r_squared = 1 - (ss_res / ss_tot)
369
+
370
+ # Assuming `cs` is already created
371
+ parameters = {
372
+ 'x_data': x_data.tolist(),
373
+ 'y_data': y_data.tolist(),
374
+ 'degree': 3,
375
+ 'smoothing_factor': smoothing_factor,
376
+ }
377
+
378
+ return CurveResult(
379
+ method=method,
380
+ parameters=parameters,
381
+ r_squared=r_squared,
382
+ derivative_max=max_derivative,
383
+ inflection_point=inflection_point,
384
+ significant_region=significant_region,
385
+ )
386
+
387
+ except (ValueError, RuntimeError):
388
+ return None
389
+
390
+ return None
391
+
392
+ @staticmethod
393
+ def get_z_from_y(
394
+ y_value: float, curve_result: CurveResult, tolerance: float = 1e-6
395
+ ) -> Optional[float]:
396
+ '''
397
+ Reverse extract Z value from Y using the fitted curve.
398
+
399
+ Parameters
400
+ ----------
401
+ y_value : float
402
+ The Y value to find Z for
403
+ curve_result : CurveResult
404
+ The curve fitting results
405
+ tolerance : float
406
+ Tolerance for numerical solution
407
+
408
+ Returns
409
+ -------
410
+ Optional[float]
411
+ The corresponding Z value if found
412
+ '''
413
+ if curve_result.method == CurveFitMethod.CSPLINE:
414
+ # For spline, use numerical root finding on (f(x) - y_value)
415
+ x_range = np.linspace(
416
+ curve_result.significant_region[0],
417
+ curve_result.significant_region[1],
418
+ 1000,
419
+ )
420
+ cs = curve_result.get_spline_func()
421
+ y_spline = cs(x_range)
422
+
423
+ # Find closest points to target y_value
424
+ idx = np.argmin(np.abs(y_spline - y_value))
425
+ z_approx = x_range[idx]
426
+
427
+ # Refine using optimization
428
+ def objective(x):
429
+ return abs(cs(x) - y_value)
430
+
431
+ result = optimize.minimize(
432
+ objective,
433
+ z_approx,
434
+ bounds=[
435
+ (
436
+ curve_result.significant_region[0],
437
+ curve_result.significant_region[1],
438
+ )
439
+ ],
440
+ )
441
+
442
+ if result.success and objective(result.x[0]) < tolerance:
443
+ return result.x[0]
444
+
445
+ return None
446
+ elif curve_result.method == CurveFitMethod.SIGMOID:
447
+ # For sigmoid, we can solve analytically
448
+ L = curve_result.parameters['L']
449
+ k = curve_result.parameters['k']
450
+ x0 = curve_result.parameters['x0']
451
+ b = curve_result.parameters['b']
452
+
453
+ try:
454
+ z = x0 - (1 / k) * np.log((L / (y_value - b)) - 1)
455
+ # Check if z is within significant region
456
+ if (
457
+ curve_result.significant_region
458
+ and curve_result.significant_region[0]
459
+ <= z
460
+ <= curve_result.significant_region[1]
461
+ ):
462
+ return z
463
+ except (ValueError, RuntimeWarning):
464
+ return None
465
+
466
+ elif curve_result.method == CurveFitMethod.POLYNOMIAL:
467
+ # For polynomial, use numerical root finding
468
+ coeffs = [
469
+ curve_result.parameters[f'c{i}']
470
+ for i in range(len(curve_result.parameters))
471
+ ]
472
+ poly = np.polynomial.Polynomial(coeffs) - y_value
473
+
474
+ try:
475
+ roots = poly.roots()
476
+ real_roots = roots[np.abs(roots.imag) < tolerance].real
477
+
478
+ # Filter roots within significant region
479
+ valid_roots = [
480
+ r
481
+ for r in real_roots
482
+ if (
483
+ curve_result.significant_region
484
+ and curve_result.significant_region[0]
485
+ <= r
486
+ <= curve_result.significant_region[1]
487
+ )
488
+ ]
489
+
490
+ return valid_roots[0] if valid_roots else None
491
+
492
+ except (ValueError, IndexError):
493
+ return None
494
+
495
+ return None
496
+
497
+ @staticmethod
498
+ def get_z_from_y_array(
499
+ y_values: np.ndarray,
500
+ curve_result: CurveResult,
501
+ region: tuple[float, float] = None,
502
+ tolerance: float = 1e-6,
503
+ ) -> np.ndarray:
504
+ '''
505
+ Get Z values from array of Y values using the fitted curve.
506
+
507
+ Parameters
508
+ ----------
509
+ y_values : np.ndarray
510
+ Array of Y values to find Z for
511
+ curve_result : CurveResult
512
+ The curve fitting results
513
+ region : tuple[float, float], optional
514
+ The significant region for the curve
515
+ tolerance : float
516
+ Tolerance for numerical solution
517
+
518
+ Returns
519
+ -------
520
+ np.ndarray
521
+ Array of corresponding Z values,
522
+ with np.nan for values where no solution is found
523
+ '''
524
+ z_values = np.full_like(y_values, np.nan)
525
+ if region is None:
526
+ region = curve_result.significant_region
527
+
528
+ if curve_result.method == CurveFitMethod.CSPLINE:
529
+ cs = curve_result.get_spline_func()
530
+ x_range = np.linspace(
531
+ region[0],
532
+ region[1],
533
+ 1000,
534
+ )
535
+ y_spline = cs(x_range)
536
+
537
+ # Vectorized initial approximation
538
+ for i, y_value in enumerate(y_values):
539
+ idx = np.argmin(np.abs(y_spline - y_value))
540
+ z_approx = x_range[idx]
541
+
542
+ # Refine using optimization
543
+ def objective(x, y):
544
+ return abs(cs(x) - y)
545
+
546
+ result = optimize.minimize(
547
+ objective,
548
+ z_approx,
549
+ args=(y_value,),
550
+ bounds=[
551
+ (
552
+ region[0],
553
+ region[1],
554
+ )
555
+ ],
556
+ )
557
+
558
+ if result.success and objective(result.x[0], y_value) < tolerance:
559
+ z_values[i] = result.x[0]
560
+
561
+ elif curve_result.method == CurveFitMethod.SIGMOID:
562
+ # Vectorized analytical solution for sigmoid
563
+ L = curve_result.parameters['L']
564
+ k = curve_result.parameters['k']
565
+ x0 = curve_result.parameters['x0']
566
+ b = curve_result.parameters['b']
567
+
568
+ with np.errstate(divide='ignore', invalid='ignore'):
569
+ z_values = x0 - (1 / k) * np.log((L / (y_values - b)) - 1)
570
+
571
+ # Mask values outside significant region
572
+ mask = (z_values >= region[0]) & (
573
+ z_values <= region[1]
574
+ )
575
+ z_values[~mask] = np.nan
576
+
577
+ return z_values
578
+
579
+ @staticmethod
580
+ def get_z_from_y_array_optimized(
581
+ y_values: np.ndarray,
582
+ curve_result: CurveResult,
583
+ region: tuple[float, float] = None,
584
+ tolerance: float = 1e-6,
585
+ num_points: int = 10000,
586
+ ) -> np.ndarray:
587
+ '''
588
+ Optimized version to get Z values from array of Y values using the fitted curve.
589
+ Creates a dense lookup table for faster interpolation.
590
+
591
+ Parameters
592
+ ----------
593
+ y_values : np.ndarray
594
+ Array of Y values to find Z for
595
+ curve_result : CurveResult
596
+ The curve fitting results
597
+ region : tuple[float, float], optional
598
+ The significant region for the curve
599
+ tolerance : float
600
+ Tolerance for numerical solution
601
+ num_points : int
602
+ Number of points in the lookup table
603
+
604
+ Returns
605
+ -------
606
+ np.ndarray
607
+ Array of corresponding Z values,
608
+ with np.nan for values where no solution is found
609
+ '''
610
+ z_values = np.full_like(y_values, np.nan)
611
+ if region is None:
612
+ region = curve_result.significant_region
613
+
614
+ if curve_result.method == CurveFitMethod.CSPLINE:
615
+ # Create dense lookup table
616
+ z_lookup = np.linspace(
617
+ region[0],
618
+ region[1],
619
+ num_points,
620
+ )
621
+ cs = curve_result.get_spline_func()
622
+ y_lookup = cs(z_lookup)
623
+
624
+ # Sort lookup table by y values for faster searching
625
+ sort_idx = np.argsort(y_lookup)
626
+ y_lookup = y_lookup[sort_idx]
627
+ z_lookup = z_lookup[sort_idx]
628
+
629
+ # Find nearest points using binary search
630
+ indices = np.searchsorted(y_lookup, y_values)
631
+
632
+ # Handle edge cases
633
+ indices = np.clip(indices, 1, len(y_lookup) - 1)
634
+
635
+ # Linear interpolation between nearest points
636
+ y_low = y_lookup[indices - 1]
637
+ y_high = y_lookup[indices]
638
+ z_low = z_lookup[indices - 1]
639
+ z_high = z_lookup[indices]
640
+
641
+ # Avoid division by zero in interpolation
642
+ valid_mask = np.abs(y_high - y_low) > tolerance
643
+ t = np.zeros_like(y_values)
644
+ t[valid_mask] = (y_values[valid_mask] - y_low[valid_mask]) / (
645
+ y_high[valid_mask] - y_low[valid_mask]
646
+ )
647
+
648
+ z_values = z_low + t * (z_high - z_low)
649
+
650
+ # Mask values outside the valid range
651
+ valid_y_range = (y_values >= np.min(y_lookup)) & (
652
+ y_values <= np.max(y_lookup)
653
+ )
654
+ z_values[~valid_y_range] = np.nan
655
+
656
+ elif curve_result.method == CurveFitMethod.SIGMOID:
657
+ L = curve_result.parameters['L']
658
+ k = curve_result.parameters['k']
659
+ x0 = curve_result.parameters['x0']
660
+ b = curve_result.parameters['b']
661
+
662
+ with np.errstate(divide='ignore', invalid='ignore'):
663
+ z_values = x0 - (1 / k) * np.log((L / (y_values - b)) - 1)
664
+
665
+ # Mask values outside significant region
666
+ mask = (z_values >= region[0]) & (z_values <= region[1])
667
+ z_values[~mask] = np.nan
668
+
669
+ return z_values
670
+
671
+ @staticmethod
672
+ def get_data(x_data: np.ndarray, curve_result: CurveResult) -> np.ndarray:
673
+ '''
674
+ Get the fitted curve data for the specified X values.
675
+
676
+ Parameters
677
+ ----------
678
+ x_data : np.ndarray
679
+ The X values to evaluate the curve at
680
+ curve_result : CurveResult
681
+ The curve fitting results
682
+
683
+ Returns
684
+ -------
685
+ np.ndarray
686
+ The fitted curve data
687
+ '''
688
+ if isinstance(x_data, list):
689
+ x_data = np.array(x_data)
690
+
691
+ if curve_result.method == CurveFitMethod.CSPLINE:
692
+ return curve_result.get_spline_func()(x_data)
693
+ elif curve_result.method == CurveFitMethod.SIGMOID:
694
+ return CurveAnalyzer.sigmoid(
695
+ x_data,
696
+ curve_result.parameters['L'],
697
+ curve_result.parameters['k'],
698
+ curve_result.parameters['x0'],
699
+ curve_result.parameters['b'],
700
+ )
701
+ elif curve_result.method == CurveFitMethod.POLYNOMIAL:
702
+ coeffs = [
703
+ curve_result.parameters[f'c{i}']
704
+ for i in range(len(curve_result.parameters))
705
+ ]
706
+ return np.polyval(coeffs, x_data)
707
+
708
+ return np.zeros_like(x_data)