flimkit 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. flimkit/FLIM/__init__.py +0 -0
  2. flimkit/FLIM/assemble.py +254 -0
  3. flimkit/FLIM/batch.py +681 -0
  4. flimkit/FLIM/bg_tools.py +51 -0
  5. flimkit/FLIM/fit_tools.py +244 -0
  6. flimkit/FLIM/fitters.py +1471 -0
  7. flimkit/FLIM/irf_tools.py +617 -0
  8. flimkit/FLIM/models.py +391 -0
  9. flimkit/GPU/__init__.py +85 -0
  10. flimkit/GPU/_base.py +391 -0
  11. flimkit/GPU/cuda.py +10 -0
  12. flimkit/GPU/mlx_backend.py +381 -0
  13. flimkit/GPU/mps.py +10 -0
  14. flimkit/GPU/rocm.py +10 -0
  15. flimkit/GPU/torch_backend.py +385 -0
  16. flimkit/UI/app_state.py +10 -0
  17. flimkit/UI/controller.py +139 -0
  18. flimkit/UI/expert_settings.py +248 -0
  19. flimkit/UI/fit_help.py +206 -0
  20. flimkit/UI/fov_preview.py +1085 -0
  21. flimkit/UI/gui.py +3919 -0
  22. flimkit/UI/icon.icns +0 -0
  23. flimkit/UI/icon.ico +0 -0
  24. flimkit/UI/icon.png +0 -0
  25. flimkit/UI/irf_widget.py +103 -0
  26. flimkit/UI/mode_controller.py +118 -0
  27. flimkit/UI/modes/__init__.py +0 -0
  28. flimkit/UI/modes/base.py +3 -0
  29. flimkit/UI/modes/batch_mode.py +312 -0
  30. flimkit/UI/modes/fov_mode.py +164 -0
  31. flimkit/UI/modes/irf_mode.py +80 -0
  32. flimkit/UI/modes/phasor_mode.py +131 -0
  33. flimkit/UI/modes/stitch_mode.py +254 -0
  34. flimkit/UI/phasor_panel.py +1087 -0
  35. flimkit/UI/progress_window.py +113 -0
  36. flimkit/UI/project_panel.py +262 -0
  37. flimkit/UI/results_panel.py +332 -0
  38. flimkit/UI/roi_tools.py +794 -0
  39. flimkit/UI/utils.py +217 -0
  40. flimkit/__init__.py +0 -0
  41. flimkit/_version.py +41 -0
  42. flimkit/cli.py +120 -0
  43. flimkit/configs.py +148 -0
  44. flimkit/dialogs.py +46 -0
  45. flimkit/formats/BH/__init__.py +0 -0
  46. flimkit/formats/BH/reader.py +296 -0
  47. flimkit/formats/BH/writer.py +86 -0
  48. flimkit/formats/ISS/__init__.py +0 -0
  49. flimkit/formats/ISS/fdflim.py +86 -0
  50. flimkit/formats/ISS/image.py +114 -0
  51. flimkit/formats/ISS/reader.py +223 -0
  52. flimkit/formats/PS/__init__.py +0 -0
  53. flimkit/formats/PS/reader.py +202 -0
  54. flimkit/formats/PTU/__init__.py +0 -0
  55. flimkit/formats/PTU/decode.py +27 -0
  56. flimkit/formats/PTU/phu.py +85 -0
  57. flimkit/formats/PTU/reader.py +235 -0
  58. flimkit/formats/PTU/series.py +258 -0
  59. flimkit/formats/PTU/stitch.py +1182 -0
  60. flimkit/formats/PTU/tools.py +94 -0
  61. flimkit/formats/__init__.py +2 -0
  62. flimkit/formats/flim_file.py +232 -0
  63. flimkit/formats/phasor.py +132 -0
  64. flimkit/formats/signal.py +170 -0
  65. flimkit/image/tools.py +124 -0
  66. flimkit/interactive.py +1857 -0
  67. flimkit/mpl_backend.py +22 -0
  68. flimkit/phasor/__init__.py +40 -0
  69. flimkit/phasor/filters.py +127 -0
  70. flimkit/phasor/fret.py +654 -0
  71. flimkit/phasor/interactive.py +556 -0
  72. flimkit/phasor/peaks.py +186 -0
  73. flimkit/phasor/signal.py +90 -0
  74. flimkit/phasor_launcher.py +314 -0
  75. flimkit/plugins/__init__.py +137 -0
  76. flimkit/plugins/bindings.py +116 -0
  77. flimkit/plugins/builtin/__init__.py +3 -0
  78. flimkit/plugins/builtin/core_tools.py +28 -0
  79. flimkit/plugins/loader.py +371 -0
  80. flimkit/plugins/registry.py +406 -0
  81. flimkit/project.py +197 -0
  82. flimkit/synth.py +145 -0
  83. flimkit/utils/__init__.py +0 -0
  84. flimkit/utils/batch_fit.py +301 -0
  85. flimkit/utils/config_manager.py +119 -0
  86. flimkit/utils/config_snapshot.py +30 -0
  87. flimkit/utils/crash_handler.py +183 -0
  88. flimkit/utils/display.py +197 -0
  89. flimkit/utils/enhanced_outputs.py +345 -0
  90. flimkit/utils/fancy.py +103 -0
  91. flimkit/utils/lifetime_image.py +243 -0
  92. flimkit/utils/misc.py +111 -0
  93. flimkit/utils/plotting.py +190 -0
  94. flimkit/utils/roi.py +370 -0
  95. flimkit/utils/session.py +51 -0
  96. flimkit/utils/update_check.py +198 -0
  97. flimkit/utils/xlsx_tools.py +97 -0
  98. flimkit/utils/xml_utils.py +219 -0
  99. flimkit-0.12.0.dist-info/METADATA +356 -0
  100. flimkit-0.12.0.dist-info/RECORD +104 -0
  101. flimkit-0.12.0.dist-info/WHEEL +5 -0
  102. flimkit-0.12.0.dist-info/entry_points.txt +2 -0
  103. flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
  104. flimkit-0.12.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ from flimkit.formats import FLIMFile
3
+
4
+ def _resample_decay(decay, src_res, dst_res, n_bins):
5
+ decay = np.asarray(decay, dtype=float)
6
+ src_t = np.arange(decay.size, dtype=float) * src_res
7
+ dst_t = np.arange(n_bins, dtype=float) * dst_res
8
+ return np.interp(dst_t, src_t, decay)
9
+
10
+ def _fit_to_grid(decay, n_bins):
11
+ decay = np.asarray(decay, dtype=float)
12
+ if decay.size < n_bins:
13
+ return np.concatenate([decay, np.zeros(n_bins - decay.size)])
14
+ return decay[:n_bins]
15
+
16
+ def _normalize_profile(decay, normalize):
17
+ decay = np.asarray(decay, dtype=float)
18
+ total = float(decay.sum())
19
+ if total <= 0:
20
+ raise ValueError('Background profile has no photons.')
21
+ if normalize:
22
+ return decay / total
23
+ return decay
24
+
25
+ def tvb_from_reference_ptu(path, ptu_ref, channel=None, normalize=True):
26
+ ref = FLIMFile(path, verbose=False)
27
+ decay = ref.summed_decay(channel=channel)
28
+ total = decay.sum()
29
+ if total == 0 and channel is not None:
30
+ decay = ref.summed_decay(channel=None)
31
+ total = decay.sum()
32
+ if total > 0:
33
+ print(f" Background PTU has no photons on channel {channel}; "
34
+ f"using auto-detected channel {ref.photon_channel} instead")
35
+ if total == 0:
36
+ raise ValueError(f"Background PTU {path!r} has no photons.")
37
+ if abs(ref.tcspc_res - ptu_ref.tcspc_res) > 1e-15:
38
+ decay = _resample_decay(decay, ref.tcspc_res, ptu_ref.tcspc_res, ptu_ref.n_bins)
39
+ else:
40
+ decay = _fit_to_grid(decay, ptu_ref.n_bins)
41
+ print(f" TVB profile from PTU: {total:,.0f} photons")
42
+ return _normalize_profile(decay, normalize)
43
+
44
+ def tvb_from_decay(decay, n_bins, src_tcspc_res=None, dst_tcspc_res=None, normalize=True):
45
+ decay = np.asarray(decay, dtype=float)
46
+ if (src_tcspc_res is not None and dst_tcspc_res is not None
47
+ and abs(src_tcspc_res - dst_tcspc_res) > 1e-15):
48
+ decay = _resample_decay(decay, src_tcspc_res, dst_tcspc_res, n_bins)
49
+ else:
50
+ decay = _fit_to_grid(decay, n_bins)
51
+ return _normalize_profile(decay, normalize)
@@ -0,0 +1,244 @@
1
+ import numpy as np
2
+ from scipy.ndimage import gaussian_filter1d
3
+ from scipy.optimize import curve_fit
4
+
5
+
6
+ def distribution_dof(n_fit, n_components=1, fit_tvb=False):
7
+ n_params = 3 * n_components + 1 + int(fit_tvb)
8
+ return max(int(n_fit) - n_params, 1)
9
+
10
+
11
+ def chi2_terms(data, model, axis=None):
12
+ data_arr, model_arr = np.broadcast_arrays(
13
+ np.asarray(data, dtype=float), np.asarray(model, dtype=float))
14
+ valid = np.all(
15
+ np.isfinite(data_arr) & (data_arr >= 0.0)
16
+ & np.isfinite(model_arr) & (model_arr >= 0.0),
17
+ axis=axis)
18
+ with np.errstate(divide='ignore', invalid='ignore', over='ignore'):
19
+ numerator = np.sum(
20
+ (data_arr - model_arr) ** 2 / np.maximum(model_arr, 1.0), axis=axis)
21
+ expected = np.sum(np.minimum(model_arr, 1.0), axis=axis)
22
+ return numerator, expected, valid
23
+
24
+
25
+ def calibrated_from_terms(numerator, expected, valid):
26
+ if np.ndim(expected) == 0:
27
+ return (float(numerator / expected)
28
+ if valid and expected > 0 else np.nan)
29
+ result = np.full(np.shape(expected), np.nan, dtype=float)
30
+ return np.divide(
31
+ numerator, expected, out=result,
32
+ where=valid & (expected > 0) & np.isfinite(numerator))
33
+
34
+
35
+ def calibrated_chi2(data, model, axis=None):
36
+ numerator, expected, valid = chi2_terms(data, model, axis=axis)
37
+ if np.ndim(expected) == 0:
38
+ return float(numerator / expected) if valid and expected > 0 else np.nan
39
+ return calibrated_from_terms(numerator, expected, valid)
40
+
41
+
42
+ def coates_pileup_correction(decay: np.ndarray, n_sync: int):
43
+ m = np.asarray(decay, dtype=float)
44
+ n_s = float(n_sync or 0.0)
45
+ total = float(m.sum())
46
+ if n_s <= 0.0 or n_s <= total:
47
+ raise ValueError(
48
+ f'coates_pileup_correction needs the excitation-pulse count N_sync; got '
49
+ f'n_sync={n_s:,.0f} for {total:,.0f} photons. N_sync must exceed the photon count.')
50
+ cumulative = np.concatenate([[0.0], np.cumsum(m[:-1])])
51
+ denom = n_s - cumulative
52
+ # Argument of log: 1 - M(t)/(N_s - C(t))
53
+ with np.errstate(divide='ignore', invalid='ignore'):
54
+ ratio = np.where(denom > 0, m / denom, 0.0)
55
+ arg = 1.0 - ratio
56
+ safe = arg > 0.0
57
+ corrected = np.where(safe, -n_s * np.log(arg), 0.0)
58
+ return corrected
59
+
60
+ def bins_from_ns(value_ns, tcspc_res):
61
+ return int(round(float(value_ns) * 1e-9 / tcspc_res))
62
+
63
+ def build_fit_idx(fit_start, fit_end, n_bins, exclude_bins=None):
64
+ # explicit bin set the fit runs over: a contiguous window minus any excluded
65
+ # bands (eg a reflection peak mid-decay). Dropping bins keeps the fit linear
66
+ # in the amplitudes, so the per-pixel projection is unaffected.
67
+ idx = np.arange(max(0, int(fit_start)), min(int(n_bins), int(fit_end)), dtype=int)
68
+ if exclude_bins:
69
+ keep = np.ones(idx.shape, dtype=bool)
70
+ for lo, hi in exclude_bins:
71
+ keep &= ~((idx >= int(lo)) & (idx < int(hi)))
72
+ idx = idx[keep]
73
+ if idx.size == 0:
74
+ raise ValueError('fit window is empty (check the window and any exclusion bands)')
75
+ return idx
76
+
77
+ def find_irf_peak_bin(decay: np.ndarray, smooth_sigma: float = 1.5):
78
+ smoothed = gaussian_filter1d(decay.astype(float), sigma=smooth_sigma)
79
+ deriv = np.gradient(smoothed)
80
+ # Only search in the first half of the histogram (rising edge region)
81
+ half = len(decay) // 2
82
+ peak_bin = int(np.argmax(deriv[:half]))
83
+ return peak_bin
84
+
85
+ def estimate_bg(decay: np.ndarray, peak_bin: int, pre_gap: int = 5):
86
+ end = max(0, peak_bin - pre_gap)
87
+ region = decay[:end]
88
+ if len(region) >= 5:
89
+ return max(float(np.median(region)), 0.0)
90
+ return max(float(np.median(decay[-30:])), 0.0)
91
+
92
+ def estimate_bg_from_histogram(hist, pre_bins=20):
93
+ if hist.ndim == 3:
94
+ bg_region = hist[..., :pre_bins].mean()
95
+ else:
96
+ bg_region = hist[:pre_bins].mean()
97
+ return float(bg_region)
98
+
99
+ def find_fit_start(decay: np.ndarray, irf_prompt: np.ndarray,
100
+ tcspc_res: float, pre_bins: int = 5):
101
+ threshold = irf_prompt.max() * 1e-3
102
+ onset_bins = np.where(irf_prompt >= threshold)[0]
103
+ if len(onset_bins) == 0:
104
+ return 0
105
+ onset = int(onset_bins[0])
106
+ fit_start = max(0, onset - pre_bins)
107
+ return fit_start
108
+
109
+
110
+ def find_tail_fit_start(decay, peak_bin, n_bins, frac=0.9):
111
+ d = np.asarray(decay, dtype=float)
112
+ peak_val = float(d[peak_bin]) if 0 <= peak_bin < n_bins else 0.0
113
+ if peak_val <= 0:
114
+ return min(peak_bin + 1, n_bins - 1)
115
+ below = np.where(d[peak_bin:] < frac * peak_val)[0]
116
+ if len(below) == 0:
117
+ return min(peak_bin + 1, n_bins - 1)
118
+ return int(min(peak_bin + below[0], n_bins - 1))
119
+
120
+ def _build_bounds_tail(n_exp, tau_min, tau_max, decay_peak, fit_bg,
121
+ bg_init=0.0, bg_upper=None,
122
+ fit_t0=False, t0_init=0.0, t0_range=0.0,
123
+ fit_tvb=False, tvb_init=0.0, tvb_upper=None):
124
+ lo = [tau_min] * n_exp + [0.0] * n_exp
125
+ hi = [tau_max] * n_exp + [10 * decay_peak] * n_exp
126
+ if fit_t0:
127
+ lo += [t0_init - t0_range]; hi += [t0_init + t0_range]
128
+ if fit_bg:
129
+ _bg_hi = bg_upper if bg_upper is not None else bg_init * 1.5 + 10
130
+ lo += [0.0]; hi += [_bg_hi]
131
+ if fit_tvb:
132
+ _tvb_hi = tvb_upper if tvb_upper is not None else tvb_init * 1.5 + 10
133
+ lo += [0.0]; hi += [_tvb_hi]
134
+ return lo, hi
135
+
136
+ def _pack_p0_tail(n_exp, tau_min, tau_max, decay_peak, fit_bg, bg_init,
137
+ tau_override=None, fit_t0=False, t0_init=0.0,
138
+ fit_tvb=False, tvb_init=0.0):
139
+ if tau_override is not None:
140
+ taus0 = np.asarray(tau_override)
141
+ else:
142
+ tmin = max(tau_min, 1e-14) * 1.001
143
+ tmax = tau_max * 0.999
144
+ taus0 = np.logspace(np.log10(tmin), np.log10(tmax), n_exp)
145
+ amps0 = np.full(n_exp, decay_peak / n_exp)
146
+ base = np.concatenate([taus0, amps0])
147
+ if fit_t0:
148
+ base = np.concatenate([base, [t0_init]])
149
+ if fit_bg:
150
+ base = np.concatenate([base, [bg_init]])
151
+ if fit_tvb:
152
+ base = np.concatenate([base, [tvb_init]])
153
+ return base
154
+
155
+ def find_fit_end(decay, peak_bin, tau_max_s, tcspc_res, n_bins):
156
+ candidate = min(n_bins, peak_bin + int(6.0 * tau_max_s / tcspc_res))
157
+ search_start = int(0.82 * n_bins)
158
+ tail = gaussian_filter1d(decay[search_start:].astype(float), sigma=2)
159
+ deriv = np.gradient(tail)
160
+ thresh = 3.0 * np.std(deriv[:max(1, len(deriv)//2)])
161
+ spikes = np.where(deriv > thresh)[0]
162
+ if len(spikes) > 0:
163
+ spike_abs = search_start + spikes[0]
164
+ if spike_abs < candidate:
165
+ print(f" Next-period artefact at bin {spike_abs} "
166
+ f"({spike_abs*tcspc_res*1e9:.2f} ns). Truncating fit window.")
167
+ candidate = spike_abs
168
+ return candidate
169
+
170
+ def _build_bounds(n_exp, tau_min, tau_max, decay_peak,
171
+ has_tail, fit_bg, fit_sigma, bg_init=0.0, bg_upper=None,
172
+ sigma_max=3.0, irf_shift_bins=5,
173
+ fit_tvb=False, tvb_init=0.0, tvb_upper=None):
174
+ lo = [tau_min] * n_exp + [0.0] * n_exp + [-float(irf_shift_bins)]
175
+ hi = [tau_max] * n_exp + [10 * decay_peak] * n_exp + [float(irf_shift_bins)]
176
+ if fit_sigma:
177
+ lo += [0.0]; hi += [sigma_max]
178
+ if fit_bg:
179
+ _bg_hi = bg_upper if bg_upper is not None else bg_init * 1.5 + 10
180
+ lo += [0.0]; hi += [_bg_hi]
181
+ if fit_tvb:
182
+ _tvb_hi = tvb_upper if tvb_upper is not None else tvb_init * 1.5 + 10
183
+ lo += [0.0]; hi += [_tvb_hi]
184
+ if has_tail:
185
+ lo += [0.0, 1.0]
186
+ hi += [5.0, 200.0]
187
+ return lo, hi
188
+
189
+ def _build_bounds_dist(n_components, tau_min, tau_max, decay_peak,
190
+ fit_bg, fit_sigma, bg_init=0.0, bg_upper=None,
191
+ sigma_max=3.0, irf_shift_bins=5,
192
+ fit_tvb=False, tvb_init=0.0, tvb_upper=None):
193
+ width_max = (tau_max - tau_min) / 2.0
194
+ width_min = tau_min / 20.0
195
+ lo = [tau_min] * n_components + [width_min] * n_components + [0.0] * n_components + [-float(irf_shift_bins)]
196
+ hi = [tau_max] * n_components + [width_max] * n_components + [10 * decay_peak] * n_components + [float(irf_shift_bins)]
197
+ if fit_sigma:
198
+ lo += [0.0]; hi += [sigma_max]
199
+ if fit_bg:
200
+ _bg_hi = bg_upper if bg_upper is not None else bg_init * 1.5 + 10
201
+ lo += [0.0]; hi += [_bg_hi]
202
+ if fit_tvb:
203
+ _tvb_hi = tvb_upper if tvb_upper is not None else tvb_init * 1.5 + 10
204
+ lo += [0.0]; hi += [_tvb_hi]
205
+ return lo, hi
206
+
207
+
208
+ def _pack_p0_dist(n_components, tau_min, tau_max, decay_peak,
209
+ fit_bg, fit_sigma, bg_init, fit_tvb=False, tvb_init=0.0):
210
+ tmin = max(tau_min, 1e-14) * 1.001
211
+ tmax = tau_max * 0.999
212
+ tau0 = np.logspace(np.log10(tmin), np.log10(tmax), n_components)
213
+ width0 = tau0 * 0.2
214
+ amp0 = np.full(n_components, decay_peak / n_components)
215
+ base = np.concatenate([tau0, width0, amp0, [0.0]])
216
+ if fit_sigma:
217
+ base = np.concatenate([base, [0.3]])
218
+ if fit_bg:
219
+ base = np.concatenate([base, [bg_init]])
220
+ if fit_tvb:
221
+ base = np.concatenate([base, [tvb_init]])
222
+ return base
223
+
224
+
225
+ def _pack_p0(n_exp, tau_min, tau_max, decay_peak,
226
+ has_tail, fit_bg, fit_sigma, bg_init,
227
+ tau_override=None, fit_tvb=False, tvb_init=0.0):
228
+ if tau_override is not None:
229
+ taus0 = np.asarray(tau_override)
230
+ else:
231
+ tmin = max(tau_min, 1e-14) * 1.001
232
+ tmax = tau_max * 0.999
233
+ taus0 = np.logspace(np.log10(tmin), np.log10(tmax), n_exp)
234
+ amps0 = np.full(n_exp, decay_peak / n_exp)
235
+ base = np.concatenate([taus0, amps0, [0.0]])
236
+ if fit_sigma:
237
+ base = np.concatenate([base, [0.3]])
238
+ if fit_bg:
239
+ base = np.concatenate([base, [bg_init]])
240
+ if fit_tvb:
241
+ base = np.concatenate([base, [tvb_init]])
242
+ if has_tail:
243
+ base = np.concatenate([base, [0.5, 20.0]])
244
+ return base