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,150 @@
1
+ import numpy as np
2
+ from scipy.optimize import fmin
3
+
4
+
5
+ def robust_mean(
6
+ data: np.ndarray, axis: int=None, k_sigma: float=3, fit_mean: bool=False):
7
+ '''
8
+ Calculate robust mean, standard deviation, and indices of inliers and outliers.
9
+
10
+ Parameters
11
+ ----------
12
+ data : numpy.ndarray
13
+ Input data.
14
+ axis : int, optional
15
+ Axis along which the mean is taken. Default is None.
16
+ k_sigma : float, optional
17
+ Number of sigmas at which to place the cut-off. Default is 3.
18
+ fit_mean : bool, optional
19
+ Whether or not to use fitting to robustly estimate the mean.
20
+ Default is False.
21
+ If True, mean is approximated by minimizing the median deviation.
22
+ If False, mean is approximated by the median.
23
+
24
+ Returns
25
+ -------
26
+ final_mean : float
27
+ Robust mean.
28
+ std_sample : float
29
+ Standard deviation of the data (divide by sqrt(n) to get std of the mean).
30
+ inlier_idx : numpy.ndarray
31
+ Index into data with the inliers.
32
+ outlier_idx : numpy.ndarray
33
+ Index into data with the outliers.
34
+
35
+ Warning
36
+ -------
37
+ NaN or Inf will be counted as neither in- nor outlier.
38
+ The code is based on (linear) Least Median Squares and
39
+ can be changed to include weights.
40
+
41
+ References
42
+ ----------
43
+ J. Ries https://github.com/jries/SMAP
44
+
45
+ Example
46
+ -------
47
+ >>> import numpy as np
48
+ >>> from scipy.stats import norm
49
+ >>> data = np.concatenate([norm.rvs(loc=0, scale=1, size=100),
50
+ ... norm.rvs(loc=10, scale=1, size=5)])
51
+ >>> final_mean, std_sample, inlier_idx, outlier_idx = robust_mean(data)
52
+ '''
53
+ if data.size == 0:
54
+ raise ValueError('Parameter data should be non-empty ndarray!')
55
+
56
+ if axis is None:
57
+ # make sure that the axis is correct if there's a vector
58
+ if np.any(np.array(data.shape) == 1) and len(data.shape) == 2:
59
+ axis = np.where(np.array(data.shape) > 1)[0][0]
60
+ else:
61
+ axis = 0
62
+
63
+ if k_sigma is None:
64
+ k_sigma = 3
65
+
66
+ if fit_mean and len(data.shape) > 1:
67
+ raise ValueError(f'Fitting {len(data.shape)}-D data is not supported!')
68
+
69
+ if np.sum(np.isfinite(data)) < 4:
70
+ print('Warning: Less than 4 finite data points!')
71
+ finite_data = data[np.isfinite(data)]
72
+ final_mean = np.nanmean(finite_data, axis=axis)
73
+ std_sample = np.nanstd(finite_data, axis=axis)
74
+ inlier_idx = np.where(np.isfinite(data))
75
+ outlier_idx = np.array([], dtype=int)
76
+ return final_mean, std_sample, inlier_idx, outlier_idx
77
+
78
+ # LEAST MEDIAN SQUARES
79
+ # define magic numbers:
80
+ magic_number2 = 1.4826**2 # see Danuser, 1992 or Rousseeuw & Leroy, 1987
81
+
82
+ # store data size and reduced data size
83
+ data_size = np.array(data.shape)
84
+ reduced_data_size = data_size.copy()
85
+ reduced_data_size[axis] = 1
86
+
87
+ # need this for later
88
+ blow_up_data_size = data_size // reduced_data_size
89
+
90
+ # count relevant dimensions besides choosen axis
91
+ real_dimensions = np.sum(data_size > 1)
92
+
93
+ # compute median
94
+ if fit_mean:
95
+ # minimize the median deviation from the mean
96
+ median_data = fmin(lambda x: np.median(np.abs(data - x)), np.median(data))
97
+ else:
98
+ median_data = np.nanmedian(data, axis=axis, keepdims=True)
99
+
100
+ # calculate statistics
101
+ res2 = (data - np.tile(median_data, blow_up_data_size))**2
102
+ med_res2 = np.maximum(
103
+ np.nanmedian(res2, axis=axis, keepdims=True), np.finfo(float).eps)
104
+
105
+ # test value to calculate weights
106
+ test_value = res2 / np.tile(magic_number2 * med_res2, blow_up_data_size)
107
+
108
+ if real_dimensions == 1:
109
+ # goodRows: weight 1, badRows: weight 0
110
+ inlier_idx = np.where(test_value <= k_sigma**2)
111
+ outlier_idx = np.where(test_value > k_sigma**2)
112
+
113
+ # calculate std of the sample
114
+ if len(inlier_idx[0]) > 4:
115
+ std_sample = np.sqrt(np.sum(res2[inlier_idx]) / (len(inlier_idx[0]) - 4))
116
+ else:
117
+ std_sample = np.nan
118
+
119
+ # MEAN
120
+ final_mean = np.mean(data[inlier_idx])
121
+
122
+ else:
123
+ # goodRows: weight 1, badRows: weight 0
124
+ inlier_idx = np.where(test_value <= k_sigma**2)
125
+ outlier_idx = np.where(test_value > k_sigma**2)
126
+
127
+ # mask outliers
128
+ res2[outlier_idx] = np.nan
129
+
130
+ # count inliers
131
+ n_inliers = np.sum(~np.isnan(res2), axis=axis, keepdims=True)
132
+
133
+ # calculate std of the sample
134
+
135
+ # put NaN wherever there are not enough data points to calculate a
136
+ # standard deviation
137
+ good_idx = np.sum(np.isfinite(res2), axis=axis, keepdims=True) > 4
138
+ good_idx_broadcasted = np.broadcast_to(good_idx, res2.shape)
139
+ std_sample = np.full_like(good_idx, np.nan, dtype=float).squeeze()
140
+ if np.any(good_idx_broadcasted):
141
+ std_sample[:] = np.sqrt(
142
+ np.nansum(
143
+ res2 * good_idx_broadcasted, axis=axis) / (n_inliers[good_idx] - 4)
144
+ ).squeeze()
145
+
146
+ # MEAN
147
+ data[outlier_idx] = np.nan
148
+ final_mean = np.nanmean(data, axis=axis)
149
+
150
+ return final_mean, std_sample, inlier_idx, outlier_idx
@@ -0,0 +1,167 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from scipy.interpolate import UnivariateSpline, interp1d
4
+
5
+ from microEye.analysis.fitting.pyfit3Dcspline.PSF.rubost_mean import robust_mean
6
+
7
+
8
+ class Localization:
9
+ def __init__(self) -> None:
10
+ self.frames = None
11
+ self.frame_z0 = None
12
+ self.rois = None
13
+
14
+ self.fit_x = None
15
+ self.fit_y = None
16
+ self.fit_sigmax = None
17
+ self.fit_sigmay = None
18
+ self.fit_intensity = None
19
+ self.fit_background = None
20
+
21
+
22
+ def get_spline_fits(beads: list[Localization], **kwargs):
23
+ # Initialize an empty list to store information about each curve
24
+ curves = []
25
+ dz = kwargs.get('dz', 10)
26
+
27
+ # Iterate over beads in reverse order
28
+ for bead in beads:
29
+ beadz0 = bead.frame_z0 * dz
30
+
31
+ if 'astig' in kwargs.get('zcorr', '') or 'corr' in kwargs.get('zcorr', ''):
32
+ beadz = (bead.frames * dz) - beadz0
33
+ else:
34
+ beadz = (bead.frames - kwargs['midpoint']) * dz
35
+
36
+ sx = bead.fit_sigmax
37
+ sy = bead.fit_sigmay
38
+ z = beadz
39
+ phot = bead.fit_intensity
40
+
41
+ # Filter values based on the specified z range
42
+ mask = (z >= kwargs['gaussrange'][0]) & (z <= kwargs['gaussrange'][1])
43
+
44
+ # Store information about the current curve
45
+ curves.append({
46
+ 'sx': np.array(sx[mask]),
47
+ 'sy': np.array(sy[mask]),
48
+ 'z': np.array(z[mask]),
49
+ 'phot': np.array(phot[mask]),
50
+ 'xpos': np.mean(bead.fit_x),
51
+ 'ypos': np.mean(bead.fit_y),
52
+ })
53
+
54
+ # Get calibrations
55
+ kwargs['ax'] = kwargs['ax_z']
56
+ spline, indgood = clean_and_get_spline(curves, **kwargs)
57
+
58
+ return spline, indgood, curves
59
+
60
+
61
+ def clean_and_get_spline(curves, **kwargs):
62
+ """
63
+ Cleans the curves, fits splines, and returns the results.
64
+
65
+ Parameters:
66
+ - curves: List of curves containing 'sx', 'sy', and 'z'.
67
+ - p: Dictionary of parameters.
68
+
69
+ Returns:
70
+ - spline_result: Dictionary with 'x', 'y', 'zrange', and 'maxmaxrange'.
71
+ - indgood_result: Boolean array indicating 'good' curves.
72
+ """
73
+ za = np.concatenate([curve['z'] for curve in curves])
74
+ Sxa = np.concatenate([curve['sx'] for curve in curves])
75
+ Sya = np.concatenate([curve['sy'] for curve in curves])
76
+
77
+ indz = (za > kwargs['gaussrange'][0]) & (za < kwargs['gaussrange'][2])
78
+ z = za[indz]
79
+ Sx = Sxa[indz]
80
+ Sy = Sya[indz]
81
+
82
+ splinex = get_spline_interpolator(Sx, z, np.ones_like(z))
83
+ spliney = get_spline_interpolator(Sy, z, np.ones_like(z))
84
+
85
+ indgood2 = np.ones(len(curves), dtype=bool)
86
+
87
+ zg = np.concatenate([curve['z'][indgood2[i]] for i, curve in enumerate(curves)])
88
+ indz = (zg > kwargs['gaussrange'][0]) & (zg < kwargs['gaussrange'][2])
89
+ zg = zg[indz]
90
+ sxg = np.concatenate([curve['sx'][indgood2[i]] for i, curve in enumerate(curves)])
91
+ syg = np.concatenate([curve['sy'][indgood2[i]] for i, curve in enumerate(curves)])
92
+ sxg = sxg[indz]
93
+ syg = syg[indz]
94
+
95
+ splinex2 = get_spline_interpolator(sxg, zg, 1. / (np.abs(sxg - splinex(zg)) + 0.1))
96
+ spliney2 = get_spline_interpolator(syg, zg, 1. / (np.abs(syg - spliney(zg)) + 0.1))
97
+
98
+ zt = np.arange(min(zg), max(zg) + 0.01, 0.01)
99
+
100
+ plot_curves_and_spline(curves, indgood2, splinex2, spliney2, zt)
101
+
102
+ return {
103
+ 'x': splinex2, 'y': spliney2,
104
+ 'zrange': [zt[0], zt[-1]],
105
+ 'maxmaxrange': [min(zg), max(zg)]}, indgood2
106
+
107
+
108
+ def get_spline_interpolator(S, z, weights, smoothing_param=0.96):
109
+ '''
110
+ Returns a 1D cubic spline interpolator for given data.
111
+
112
+ Parameters:
113
+ - S: Values to be interpolated.
114
+ - z: Corresponding positions.
115
+ - weights: Weights for each data point.
116
+ - smoothing_param: Smoothing parameter for the spline.
117
+
118
+ Returns:
119
+ - spline: Cubic spline interpolator.
120
+ '''
121
+ sorted_indices = np.argsort(z)
122
+ sorted_z = np.sort(z)
123
+ sorted_S = S[sorted_indices]
124
+ sorted_weights = weights[sorted_indices]
125
+
126
+ spline = UnivariateSpline(
127
+ sorted_z, sorted_S, w=sorted_weights, k=3, s=smoothing_param)
128
+
129
+ return spline
130
+
131
+ def plot_curves_and_spline(curves, indgood2, spline_x, spline_y, z_range):
132
+ """
133
+ Plots the curves, distinguished between 'good' and 'bad', and the splines.
134
+
135
+ Parameters:
136
+ - curves: List of curves containing 'sx', 'sy', and 'z'.
137
+ - indgood2: Boolean array indicating whether each curve is 'good'.
138
+ - spline_x: Interpolator for the x-coordinate.
139
+ - spline_y: Interpolator for the y-coordinate.
140
+ - z_range: Range of z values for plotting.
141
+ """
142
+ z1a, z2a, x1a, x2a = [], [], [], []
143
+
144
+ for k, curve in enumerate(curves):
145
+ if indgood2[k]:
146
+ z1a.extend(curve['z'])
147
+ x1a.extend(curve['sx'])
148
+ else:
149
+ z2a.extend(curve['z'])
150
+ x2a.extend(curve['sx'])
151
+
152
+ if not z2a: # If z2a is empty, plot a bad point to have the legend correct
153
+ z2a = [0]
154
+ x2a = [0]
155
+
156
+ plt.plot(z2a, x2a, 'g.', label='Bad Curves')
157
+ plt.plot(z1a, x1a, 'r.', label='Good Curves')
158
+ plt.plot(z_range, spline_x(z_range), 'k', label='Spline X')
159
+ plt.plot(z_range, spline_y(z_range), 'k', label='Spline Y')
160
+
161
+ plt.xlim([z_range[0], z_range[-1]])
162
+ plt.ylim([0, min(5, max(max(spline_x(z_range)), max(spline_y(z_range))))])
163
+ plt.xlabel('z (nm)')
164
+ plt.ylabel('PSFx, PSFy (pixel)')
165
+ plt.title('Lateral size of the PSF')
166
+ plt.legend()
167
+ plt.show()
@@ -0,0 +1,12 @@
1
+ from microEye.analysis.fitting.psf.stats.core import (
2
+ ConfidenceMethod,
3
+ StatConfig,
4
+ StatsCalculator,
5
+ )
6
+ from microEye.analysis.fitting.psf.stats.curve_fit import (
7
+ CurveAnalyzer,
8
+ CurveFitMethod,
9
+ CurveResult,
10
+ )
11
+ from microEye.analysis.fitting.psf.stats.io import export_fit_curve, import_fit_curve
12
+ from microEye.analysis.fitting.psf.stats.slope_fit import SlopeAnalyzer, SlopeResult
@@ -0,0 +1,295 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Callable, Optional, Union
4
+
5
+ import numpy as np
6
+ from scipy import stats
7
+
8
+ from microEye.analysis.fitting.results import PARAMETER_HEADERS
9
+
10
+
11
+ class ConfidenceMethod(Enum):
12
+ NONE = 'None'
13
+ MIN_MAX = 'min_max'
14
+ T_DIST = 't_dist'
15
+ MEDIAN_STD = 'median_std'
16
+ PERCENTILE = 'percentile'
17
+ BOOTSTRAP = 'bootstrap'
18
+
19
+
20
+ @dataclass
21
+ class StatConfig:
22
+ '''Configuration for a statistic calculation'''
23
+
24
+ name: str
25
+ required_params: list[str] # Parameters required from header
26
+ calculator: Callable # Function to calculate the statistic
27
+ multi_output: bool = False # Whether the stat returns multiple values
28
+
29
+
30
+ class StatsCalculator:
31
+ def __init__(self, zero_plane: int, z_step: float, fitting_method: str):
32
+ self.zero_plane = zero_plane
33
+ self.z_step = z_step
34
+ self.fitting_method = fitting_method
35
+ self.header = PARAMETER_HEADERS[fitting_method]
36
+ self._register_stats()
37
+
38
+ def _register_stats(self):
39
+ '''Register all available statistics calculations'''
40
+ self.available_stats: dict[str, StatConfig] = {
41
+ 'Counts': StatConfig(
42
+ name='Counts', required_params=[], calculator=lambda rois, _: len(rois)
43
+ ),
44
+ 'Sigma': StatConfig(
45
+ name='Sigma',
46
+ required_params=['sigmax', 'sigmay'],
47
+ calculator=self._calc_sigma,
48
+ multi_output=True,
49
+ ),
50
+ 'Sigma (sum)': StatConfig(
51
+ name='Sigma (sum)',
52
+ required_params=['sigmax', 'sigmay'],
53
+ calculator=lambda data_x, data_y: data_x + data_y,
54
+ ),
55
+ 'Sigma (diff)': StatConfig(
56
+ name='Sigma (diff)',
57
+ required_params=['sigmax', 'sigmay'],
58
+ calculator=lambda data_x, data_y: data_x - data_y,
59
+ ),
60
+ 'Sigma (abs(diff))': StatConfig(
61
+ name='Sigma (abs(diff))',
62
+ required_params=['sigmax', 'sigmay'],
63
+ calculator=lambda data_x, data_y: np.abs(data_x - data_y),
64
+ ),
65
+ 'Sigma (x/y)': StatConfig(
66
+ name='Sigma (x/y)',
67
+ required_params=['sigmax', 'sigmay'],
68
+ calculator=lambda data_x, data_y: np.divide(
69
+ data_x, data_y, out=np.zeros_like(data_x), where=data_y != 0
70
+ ),
71
+ ),
72
+ 'Sigma² (diff)': StatConfig(
73
+ name='Sigma² (diff)',
74
+ required_params=['sigmax', 'sigmay'],
75
+ calculator=lambda data_x, data_y: np.square(data_x) - np.square(data_y),
76
+ ),
77
+ 'Intensity': StatConfig(
78
+ name='Intensity',
79
+ required_params=['intensity'],
80
+ calculator=lambda data: data,
81
+ ),
82
+ 'Background': StatConfig(
83
+ name='Background',
84
+ required_params=['background'],
85
+ calculator=lambda data: data,
86
+ ),
87
+ }
88
+
89
+ def _calc_sigma(
90
+ self, data_x: Optional[np.ndarray], data_y: Optional[np.ndarray]
91
+ ) -> list[np.ndarray]:
92
+ '''Calculate sigma statistics for x and y dimensions'''
93
+ results = []
94
+ for data in [data_x, data_y]:
95
+ if data is not None:
96
+ results.append(data)
97
+ return results
98
+
99
+ def _get_param_data(self, z_slice: dict, param_name: str) -> Optional[np.ndarray]:
100
+ '''Extract parameter data from z_slice if parameter exists'''
101
+ if param_name in self.header:
102
+ return z_slice['params'][:, self.header.index(param_name)]
103
+ return None
104
+
105
+ def _calculate_confidence_interval(
106
+ self,
107
+ data: np.ndarray,
108
+ method: ConfidenceMethod,
109
+ confidence_level: float = 0.95,
110
+ n_bootstrap: int = 1000,
111
+ ) -> tuple[float, float]:
112
+ '''Calculate confidence intervals using various methods'''
113
+ if len(data) == 0 or method == ConfidenceMethod.NONE:
114
+ return np.nan, np.nan
115
+
116
+ if method == ConfidenceMethod.MIN_MAX:
117
+ return np.nanmin(data), np.nanmax(data)
118
+
119
+ elif method == ConfidenceMethod.T_DIST:
120
+ if len(data) < 2:
121
+ return np.nan, np.nan
122
+ mean = np.nanmean(data)
123
+ std_err = np.nanstd(data, ddof=1) / np.sqrt(len(data))
124
+ t_value = stats.t.ppf((1 + confidence_level) / 2, len(data) - 1)
125
+ margin = t_value * std_err
126
+ return mean - margin, mean + margin
127
+
128
+ elif method == ConfidenceMethod.MEDIAN_STD:
129
+ median = np.nanmedian(data)
130
+ std = np.nanstd(data)
131
+ return median - std, median + std
132
+
133
+ elif method == ConfidenceMethod.PERCENTILE:
134
+ lower = (1 - confidence_level) / 2 * 100
135
+ upper = (1 + confidence_level) / 2 * 100
136
+ return np.nanpercentile(data, [lower, upper])
137
+
138
+ elif method == ConfidenceMethod.BOOTSTRAP:
139
+ if len(data) < 2:
140
+ return np.nan, np.nan
141
+ bootstrap_means = []
142
+ for _ in range(n_bootstrap):
143
+ sample = np.random.choice(data, size=len(data), replace=True)
144
+ bootstrap_means.append(np.nanmean(sample))
145
+ return np.percentile(
146
+ bootstrap_means,
147
+ [(1 - confidence_level) / 2 * 100, (1 + confidence_level) / 2 * 100],
148
+ )
149
+
150
+ def _calculate_stats(
151
+ self,
152
+ z_slice: dict[str, Union[int, np.ndarray, list, float]],
153
+ stat_config: StatConfig,
154
+ confidence_method: ConfidenceMethod = ConfidenceMethod.MIN_MAX,
155
+ confidence_level: float = 0.95,
156
+ ) -> tuple[list[float], list[float], list[float]]:
157
+ '''Calculate statistics for a single z-slice'''
158
+ if z_slice['rois'] is None:
159
+ return self._get_nan_result(stat_config.multi_output)
160
+
161
+ valid_rois = z_slice['rois'][~np.isnan(z_slice['rois']).any(axis=(1, 2))]
162
+
163
+ if len(valid_rois) == 0 and stat_config.name != 'Counts':
164
+ return self._get_nan_result(stat_config.multi_output)
165
+
166
+ if stat_config.name == 'Counts':
167
+ return [len(valid_rois)], [len(valid_rois)], [len(valid_rois)]
168
+
169
+ # Extract required parameters
170
+ param_data = [
171
+ self._get_param_data(z_slice, param)
172
+ for param in stat_config.required_params
173
+ ]
174
+
175
+ if (
176
+ any(data is None for data in param_data)
177
+ and len(stat_config.required_params) > 0
178
+ ):
179
+ return self._get_nan_result(stat_config.multi_output)
180
+
181
+ # Calculate statistics
182
+ result = stat_config.calculator(*param_data)
183
+ if stat_config.multi_output:
184
+ means = [np.nanmean(r) for r in result]
185
+ conf_intervals = [
186
+ self._calculate_confidence_interval(
187
+ r, confidence_method, confidence_level
188
+ )
189
+ for r in result
190
+ ]
191
+ return (
192
+ means,
193
+ [ci[0] for ci in conf_intervals],
194
+ [ci[1] for ci in conf_intervals],
195
+ )
196
+
197
+ mean = np.nanmean(result)
198
+ lower, upper = self._calculate_confidence_interval(
199
+ result, confidence_method, confidence_level
200
+ )
201
+ return [mean], [lower], [upper]
202
+
203
+ def _get_nan_result(
204
+ self, multi_output: bool
205
+ ) -> tuple[list[float], list[float], list[float]]:
206
+ '''Return NaN results based on whether multiple outputs are expected'''
207
+ if multi_output:
208
+ return [np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]
209
+ return [np.nan], [np.nan], [np.nan]
210
+
211
+ def get_stats(
212
+ self,
213
+ zslices: list[dict[str, Union[int, np.ndarray, list, float]]],
214
+ selected_stat: str,
215
+ confidence_method: ConfidenceMethod = ConfidenceMethod.MIN_MAX,
216
+ confidence_level: float = 0.95,
217
+ ):
218
+ '''
219
+ Get the statistic data for all z-slices.
220
+
221
+ Parameters
222
+ ----------
223
+ selected_stat : str
224
+ The selected statistic to return
225
+
226
+ Returns
227
+ -------
228
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
229
+ Z-indices, statistic data, minimum data, maximum
230
+ '''
231
+ if selected_stat not in self.available_stats:
232
+ raise ValueError(f'Unknown statistic: {selected_stat}')
233
+
234
+ z_indices = (np.arange(len(zslices)) - self.zero_plane) * self.z_step
235
+ stat_config = self.available_stats[selected_stat]
236
+
237
+ param_stat = []
238
+ param_lower = []
239
+ param_upper = []
240
+
241
+ # Initialize lists for multi-output statistics
242
+ if stat_config.multi_output:
243
+ param_stat = [[] for _ in range(len(stat_config.required_params))]
244
+ param_lower = [[] for _ in range(len(stat_config.required_params))]
245
+ param_upper = [[] for _ in range(len(stat_config.required_params))]
246
+
247
+ for z_slice in zslices:
248
+ stats, mins, maxs = self._calculate_stats(
249
+ z_slice, stat_config, confidence_method, confidence_level
250
+ )
251
+
252
+ if stat_config.multi_output:
253
+ for i, (stat, min_val, max_val) in enumerate(zip(stats, mins, maxs)):
254
+ param_stat[i].append(stat)
255
+ param_lower[i].append(min_val)
256
+ param_upper[i].append(max_val)
257
+ else:
258
+ param_stat.extend(stats)
259
+ param_lower.extend(mins)
260
+ param_upper.extend(maxs)
261
+
262
+ return (
263
+ z_indices,
264
+ np.array(param_stat),
265
+ np.array(param_lower),
266
+ np.array(param_upper),
267
+ )
268
+
269
+ def add_statistic(
270
+ self,
271
+ name: str,
272
+ required_params: list[str],
273
+ calculator: Callable,
274
+ multi_output: bool = False,
275
+ ):
276
+ '''
277
+ Add a new statistic calculation method.
278
+
279
+ Parameters
280
+ ----------
281
+ name : str
282
+ Name of the new statistic
283
+ required_params : list[str]
284
+ List of parameter names required from the header
285
+ calculator : Callable
286
+ Function to calculate the statistic
287
+ multi_output : bool, optional
288
+ Whether the statistic returns multiple values, by default False
289
+ '''
290
+ self.available_stats[name] = StatConfig(
291
+ name=name,
292
+ required_params=required_params,
293
+ calculator=calculator,
294
+ multi_output=multi_output,
295
+ )