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.
- microEye/__init__.py +47 -0
- microEye/_version.py +2 -0
- microEye/analysis/__init__.py +1 -0
- microEye/analysis/checklist_dialog.py +143 -0
- microEye/analysis/cmosMaps.py +228 -0
- microEye/analysis/filters/__init__.py +9 -0
- microEye/analysis/filters/base.py +21 -0
- microEye/analysis/filters/spatial.py +338 -0
- microEye/analysis/filters/temporal.py +76 -0
- microEye/analysis/fitting/__init__.py +0 -0
- microEye/analysis/fitting/fit.py +680 -0
- microEye/analysis/fitting/nena.py +375 -0
- microEye/analysis/fitting/phasor_fit.py +90 -0
- microEye/analysis/fitting/processing.py +317 -0
- microEye/analysis/fitting/psf/__init__.py +6 -0
- microEye/analysis/fitting/psf/extract.py +1129 -0
- microEye/analysis/fitting/psf/rubost_mean.py +150 -0
- microEye/analysis/fitting/psf/spline.py +167 -0
- microEye/analysis/fitting/psf/stats/__init__.py +12 -0
- microEye/analysis/fitting/psf/stats/core.py +295 -0
- microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
- microEye/analysis/fitting/psf/stats/io.py +104 -0
- microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
- microEye/analysis/fitting/psf/temp.py +147 -0
- microEye/analysis/fitting/psf/test.py +47 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
- microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
- microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
- microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
- microEye/analysis/fitting/results.py +917 -0
- microEye/analysis/fitting/results_stats.py +251 -0
- microEye/analysis/fitting/tardis.py +209 -0
- microEye/analysis/multi_viewer.py +568 -0
- microEye/analysis/processing/__init__.py +1 -0
- microEye/analysis/processing/frc.py +120 -0
- microEye/analysis/rendering/__init__.py +3 -0
- microEye/analysis/rendering/base.py +452 -0
- microEye/analysis/rendering/cloud.py +215 -0
- microEye/analysis/rendering/core.py +47 -0
- microEye/analysis/rendering/volumetric.py +198 -0
- microEye/analysis/tools/__init__.py +0 -0
- microEye/analysis/tools/kymograms.py +1079 -0
- microEye/analysis/tools/roi_selectors.py +556 -0
- microEye/analysis/utils/__init__.py +3 -0
- microEye/analysis/utils/coordinates.py +18 -0
- microEye/analysis/utils/images.py +68 -0
- microEye/analysis/utils/windows.py +19 -0
- microEye/analysis/viewer/__init__.py +3 -0
- microEye/analysis/viewer/image_options_widget.py +615 -0
- microEye/analysis/viewer/images.py +1225 -0
- microEye/analysis/viewer/layers_widget.py +415 -0
- microEye/analysis/viewer/localizations.py +1304 -0
- microEye/analysis/viewer/psf.py +1003 -0
- microEye/analysis/viewer/volume.py +456 -0
- microEye/hardware/__init__.py +3 -0
- microEye/hardware/cams/__init__.py +29 -0
- microEye/hardware/cams/camera_calibration.py +99 -0
- microEye/hardware/cams/camera_list.py +528 -0
- microEye/hardware/cams/camera_options.py +694 -0
- microEye/hardware/cams/camera_panel.py +941 -0
- microEye/hardware/cams/dummy/__init__.py +1 -0
- microEye/hardware/cams/dummy/dummy_panel.py +759 -0
- microEye/hardware/cams/jobs.py +497 -0
- microEye/hardware/cams/line_profiler.py +99 -0
- microEye/hardware/cams/linescan/IR_Cam.py +474 -0
- microEye/hardware/cams/linescan/__init__.py +1 -0
- microEye/hardware/cams/micam.py +524 -0
- microEye/hardware/cams/pco/__init__.py +58 -0
- microEye/hardware/cams/pco/enums.py +382 -0
- microEye/hardware/cams/pco/pco_cam.py +761 -0
- microEye/hardware/cams/pco/pco_panel.py +477 -0
- microEye/hardware/cams/shortcuts.py +312 -0
- microEye/hardware/cams/thorlabs/__init__.py +1 -0
- microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
- microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
- microEye/hardware/cams/ueye/__init__.py +1 -0
- microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
- microEye/hardware/cams/ueye/ueye_panel.py +861 -0
- microEye/hardware/cams/vimba/__init__.py +1 -0
- microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
- microEye/hardware/cams/vimba/vimba_panel.py +813 -0
- microEye/hardware/device.py +60 -0
- microEye/hardware/lasers/__init__.py +13 -0
- microEye/hardware/lasers/io_matchbox.py +791 -0
- microEye/hardware/lasers/io_params.py +85 -0
- microEye/hardware/lasers/io_single_laser.py +742 -0
- microEye/hardware/lasers/laser_relay.py +594 -0
- microEye/hardware/mieye/__init__.py +1 -0
- microEye/hardware/mieye/acquisition_manager.py +467 -0
- microEye/hardware/mieye/devices_manager.py +533 -0
- microEye/hardware/mieye/miEye.py +659 -0
- microEye/hardware/misc/__init__.py +0 -0
- microEye/hardware/misc/acquisition_view.py +71 -0
- microEye/hardware/misc/reglo.py +761 -0
- microEye/hardware/misc/temp.py +188 -0
- microEye/hardware/port_config.py +59 -0
- microEye/hardware/protocols/__init__.py +2 -0
- microEye/hardware/protocols/actions.py +402 -0
- microEye/hardware/protocols/actions_items.py +703 -0
- microEye/hardware/protocols/designer.py +244 -0
- microEye/hardware/protocols/scene_manager.py +191 -0
- microEye/hardware/protocols/serialization.py +97 -0
- microEye/hardware/pycromanager/__init__.py +16 -0
- microEye/hardware/pycromanager/core.py +1433 -0
- microEye/hardware/pycromanager/devices.py +461 -0
- microEye/hardware/pycromanager/enums.py +107 -0
- microEye/hardware/pycromanager/headless.py +153 -0
- microEye/hardware/pycromanager/utils.py +34 -0
- microEye/hardware/pycromanager/widgets/__init__.py +5 -0
- microEye/hardware/pycromanager/widgets/bridges.py +407 -0
- microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
- microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
- microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
- microEye/hardware/stages/__init__.py +18 -0
- microEye/hardware/stages/elliptec/__init__.py +5 -0
- microEye/hardware/stages/elliptec/baseDevice.py +314 -0
- microEye/hardware/stages/elliptec/device.py +384 -0
- microEye/hardware/stages/elliptec/deviceID.py +212 -0
- microEye/hardware/stages/elliptec/devicePort.py +379 -0
- microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
- microEye/hardware/stages/elliptec/devicesView.py +706 -0
- microEye/hardware/stages/elliptec/ellDevices.py +134 -0
- microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
- microEye/hardware/stages/elliptec/motorInfo.py +153 -0
- microEye/hardware/stages/elliptec/stage.py +62 -0
- microEye/hardware/stages/elliptec/test.py +139 -0
- microEye/hardware/stages/kinesis/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
- microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
- microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
- microEye/hardware/stages/kinesis/kinesis.py +776 -0
- microEye/hardware/stages/piezo_concept.py +607 -0
- microEye/hardware/stages/stabilizer.py +785 -0
- microEye/hardware/stages/stage.py +89 -0
- microEye/hardware/widgets/__init__.py +10 -0
- microEye/hardware/widgets/controller.py +246 -0
- microEye/hardware/widgets/devices.py +133 -0
- microEye/hardware/widgets/focusWidget.py +264 -0
- microEye/hardware/widgets/qlist_slider.py +113 -0
- microEye/hardware/widgets/scan_acquisition.py +424 -0
- microEye/icons/1024.png +0 -0
- microEye/icons/128.png +0 -0
- microEye/icons/16.png +0 -0
- microEye/icons/24.png +0 -0
- microEye/icons/256.png +0 -0
- microEye/icons/32.png +0 -0
- microEye/icons/48.png +0 -0
- microEye/icons/512.png +0 -0
- microEye/icons/64.png +0 -0
- microEye/icons/__init__.py +0 -0
- microEye/icons/close.svg +88 -0
- microEye/icons/mieye.png +0 -0
- microEye/icons/min.svg +83 -0
- microEye/icons/viewer.png +0 -0
- microEye/launcher.py +42 -0
- microEye/qt.py +181 -0
- microEye/utils/__init__.py +2 -0
- microEye/utils/enum_encoder.py +10 -0
- microEye/utils/expandable_groupbox.py +93 -0
- microEye/utils/gui_helper.py +457 -0
- microEye/utils/hid/__init__.py +8 -0
- microEye/utils/hid/controller.py +153 -0
- microEye/utils/hid/device.py +63 -0
- microEye/utils/hid/enums.py +140 -0
- microEye/utils/hid/utils.py +58 -0
- microEye/utils/labelled_slider.py +134 -0
- microEye/utils/metadata.py +599 -0
- microEye/utils/metadata_tree.py +718 -0
- microEye/utils/micro_launcher.py +245 -0
- microEye/utils/parameter_tree.py +325 -0
- microEye/utils/pyscripting.py +444 -0
- microEye/utils/retry_exec.py +35 -0
- microEye/utils/start_gui.py +112 -0
- microEye/utils/thread_worker.py +115 -0
- microEye/utils/uImage.py +1512 -0
- microeye-2.3.2.dist-info/METADATA +401 -0
- microeye-2.3.2.dist-info/RECORD +190 -0
- microeye-2.3.2.dist-info/WHEEL +5 -0
- microeye-2.3.2.dist-info/entry_points.txt +2 -0
- microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
- 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)
|