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,385 @@
1
+ import threading
2
+
3
+ import numpy as np
4
+ from flimkit.GPU._base import _BackendMixin, fit_window, pixel_blocks
5
+ from flimkit.FLIM.fit_tools import (calibrated_chi2, distribution_dof,
6
+ estimate_bg, coates_pileup_correction)
7
+
8
+ _MATMUL_PRECISION_LOCK = threading.Lock()
9
+
10
+ class TorchBackend(_BackendMixin):
11
+
12
+ def __init__(self, device='cuda'):
13
+ import torch
14
+ self._torch = torch
15
+ self.device = torch.device(device)
16
+
17
+ def _matmul_full_precision(self, left, right):
18
+ if self.device.type != 'cuda':
19
+ return left @ right
20
+ with _MATMUL_PRECISION_LOCK:
21
+ previous = self._torch.get_float32_matmul_precision()
22
+ self._torch.set_float32_matmul_precision('highest')
23
+ try:
24
+ return left @ right
25
+ finally:
26
+ self._torch.set_float32_matmul_precision(previous)
27
+
28
+ def __repr__(self):
29
+ return f"TorchBackend(device='{self.device}')"
30
+
31
+ def batch_fixed_tau(
32
+ self,
33
+ stack,
34
+ A,
35
+ taus_fixed,
36
+ min_photons,
37
+ correct_pileup,
38
+ n_sync_px,
39
+ progress_callback=None,
40
+ tvb_profile=None,
41
+ fit_tvb=False,
42
+ fit_idx=None,
43
+ ):
44
+ torch = self._torch
45
+ ny, nx, n_bins = stack.shape
46
+ n_exp = A.shape[1]
47
+ taus_ns = taus_fixed * 1e9
48
+ raw = stack.reshape(ny * nx, n_bins)
49
+ win = fit_window(fit_idx, n_bins)
50
+ A = A if win is None else A[win]
51
+ valid_idx = np.where(raw.sum(axis=1) >= min_photons)[0]
52
+ maps = self._init_maps(
53
+ ny, nx, n_exp,
54
+ intensity = stack.sum(axis=2),
55
+ taus_fixed_ns = taus_ns,
56
+ free_tau = False,
57
+ )
58
+ if valid_idx.size == 0:
59
+ return maps
60
+ with_tvb = fit_tvb and tvb_profile is not None
61
+ B_col = None
62
+ if with_tvb:
63
+ B_col = np.asarray(tvb_profile, dtype=np.float32)
64
+ B_col = B_col if win is None else B_col[win]
65
+ A_aug = np.column_stack(
66
+ [A, B_col, np.ones(A.shape[0], dtype=np.float32)]).astype(np.float32)
67
+ A_pinv = torch.linalg.pinv(torch.as_tensor(A_aug, device='cpu')).to(self.device)
68
+ else:
69
+ A_cpu = torch.as_tensor(A, dtype=torch.float32, device='cpu')
70
+ A_pinv = torch.linalg.pinv(A_cpu).to(self.device)
71
+ n_fit = A.shape[0]
72
+ for first, last in pixel_blocks(valid_idx.size, 4 * (2 * n_bins + n_fit + n_exp)):
73
+ block = valid_idx[first:last]
74
+ decay = raw[block].astype(np.float32)
75
+ if with_tvb:
76
+ data_in = (decay if win is None else decay[:, win]).copy()
77
+ if correct_pileup and n_sync_px > 0:
78
+ for row in range(data_in.shape[0]):
79
+ data_in[row] = coates_pileup_correction(data_in[row], n_sync_px)
80
+ data_t = torch.as_tensor(data_in, dtype=torch.float32, device=self.device)
81
+ coeffs = torch.clamp(data_t @ A_pinv.T, min=0.0).cpu().numpy()
82
+ amps = coeffs[:, :n_exp]
83
+ tvb = coeffs[:, n_exp]
84
+ bg = coeffs[:, n_exp + 1]
85
+ decay_fit = data_in
86
+ else:
87
+ bg = self._estimate_bg_batch(decay, np.ones(decay.shape[0], dtype=bool))
88
+ corrected = np.maximum(decay - bg[:, None], 0.0)
89
+ corrected = corrected if win is None else corrected[:, win]
90
+ if correct_pileup and n_sync_px > 0:
91
+ for row in range(corrected.shape[0]):
92
+ corrected[row] = coates_pileup_correction(corrected[row], n_sync_px)
93
+ data_t = torch.as_tensor(corrected, dtype=torch.float32, device=self.device)
94
+ amps = torch.clamp(data_t @ A_pinv.T, min=0.0).cpu().numpy()
95
+ tvb = None
96
+ decay_fit = decay if win is None else decay[:, win]
97
+ self._scatter_fixed_tau(
98
+ maps,
99
+ valid_idx = block,
100
+ amps = amps,
101
+ bg = bg,
102
+ decay_valid = decay_fit,
103
+ A = A,
104
+ taus_ns = taus_ns,
105
+ ny = ny, nx = nx,
106
+ tvb = tvb,
107
+ tvb_profile = B_col if with_tvb else None,
108
+ )
109
+ if progress_callback is not None:
110
+ progress_callback(last, valid_idx.size)
111
+ return maps
112
+
113
+ def batch_grid_scan_1exp(
114
+ self,
115
+ stack,
116
+ basis_grid,
117
+ bb_grid,
118
+ tau_grid,
119
+ min_photons,
120
+ correct_pileup,
121
+ n_sync_px,
122
+ progress_callback=None,
123
+ tvb_profile=None,
124
+ fit_tvb=False,
125
+ fit_idx=None,
126
+ ):
127
+ torch = self._torch
128
+ ny, nx, n_bins = stack.shape
129
+ N_GRID = len(tau_grid)
130
+ raw = stack.reshape(ny * nx, n_bins)
131
+ win = fit_window(fit_idx, n_bins)
132
+ if win is not None:
133
+ if fit_tvb and tvb_profile is not None:
134
+ raise ValueError('a fit window with time-varying background is not '
135
+ 'supported on the GPU for one-exponential fits')
136
+ basis_grid = basis_grid[:, win]
137
+ bb_grid = np.maximum((basis_grid ** 2).sum(axis=1), 1e-20)
138
+ n_fit = len(win)
139
+ else:
140
+ n_fit = n_bins
141
+ valid_idx = np.where(raw.sum(axis=1) >= min_photons)[0]
142
+ maps = self._init_maps(
143
+ ny, nx, n_exp=1,
144
+ intensity=stack.sum(axis=2),
145
+ taus_fixed_ns=np.array([tau_grid[N_GRID // 2] * 1e9]),
146
+ free_tau=True,
147
+ )
148
+ if valid_idx.size == 0:
149
+ return maps
150
+ with_tvb = fit_tvb and tvb_profile is not None
151
+ if with_tvb:
152
+ U, U_pinv, basis_perp, bb_perp = self._tvb_grid_prep(
153
+ basis_grid, tvb_profile, n_bins)
154
+ basis_t = torch.as_tensor(basis_perp, dtype=torch.float32, device=self.device)
155
+ bbp_t = torch.as_tensor(bb_perp, dtype=torch.float32, device=self.device)
156
+ else:
157
+ basis_t = torch.as_tensor(basis_grid, dtype=torch.float32, device=self.device)
158
+ bb_t = torch.as_tensor(bb_grid, dtype=torch.float32, device=self.device)
159
+ per_pixel = 4 * (2 * n_bins + n_fit + N_GRID)
160
+ for first, last in pixel_blocks(valid_idx.size, per_pixel):
161
+ block = valid_idx[first:last]
162
+ decay = raw[block].astype(np.float32)
163
+ if with_tvb:
164
+ data_in = decay.copy()
165
+ if correct_pileup and n_sync_px > 0:
166
+ for row in range(data_in.shape[0]):
167
+ data_in[row] = coates_pileup_correction(data_in[row], n_sync_px)
168
+ d_perp = self._tvb_project_data(
169
+ data_in.astype(np.float64), U, U_pinv).astype(np.float32)
170
+ dperp_t = torch.as_tensor(d_perp, dtype=torch.float32, device=self.device)
171
+ bd = self._matmul_full_precision(dperp_t, basis_t.T)
172
+ dsq = (dperp_t ** 2).sum(dim=1)
173
+ costs = dsq[:, None] - torch.clamp(bd, min=0.0) ** 2 / bbp_t[None, :]
174
+ best_g = costs.argmin(dim=1).cpu().numpy()
175
+ bd_np = bd.cpu().numpy()
176
+ amp_v = np.maximum(
177
+ bd_np[np.arange(block.size), best_g] / bb_perp[best_g], 0.0)
178
+ basis_best = basis_grid[best_g]
179
+ resid_after = data_in.astype(np.float64) - amp_v[:, None] * basis_best
180
+ vz = resid_after @ U_pinv.T
181
+ self._scatter_1exp(
182
+ maps, valid_idx=block, tau_v=tau_grid[best_g], amp_v=amp_v,
183
+ bg_v=vz[:, 1].astype(np.float32), decay_valid=data_in,
184
+ basis_best=basis_best, ny=ny, nx=nx, n_bins=n_bins,
185
+ tvb=np.maximum(vz[:, 0], 0.0).astype(np.float32),
186
+ tvb_profile=np.asarray(tvb_profile, dtype=np.float32),
187
+ )
188
+ else:
189
+ bg = self._estimate_bg_batch(decay, np.ones(decay.shape[0], dtype=bool))
190
+ corrected = np.maximum(decay - bg[:, None], 0.0)
191
+ if correct_pileup and n_sync_px > 0:
192
+ for row in range(corrected.shape[0]):
193
+ corrected[row] = coates_pileup_correction(
194
+ corrected[row], n_sync_px)
195
+ corrected = corrected if win is None else corrected[:, win]
196
+ dc_t = torch.as_tensor(corrected, dtype=torch.float32, device=self.device)
197
+ bd = dc_t @ basis_t.T
198
+ dc_sq = (dc_t ** 2).sum(dim=1)
199
+ costs = dc_sq[:, None] - torch.clamp(bd, min=0.0) ** 2 / bb_t[None, :]
200
+ best_g = costs.argmin(dim=1).cpu().numpy()
201
+ bd_np = bd.cpu().numpy()
202
+ amp_v = np.maximum(
203
+ bd_np[np.arange(block.size), best_g] / bb_grid[best_g], 0.0)
204
+ self._scatter_1exp(
205
+ maps,
206
+ valid_idx = block,
207
+ tau_v = tau_grid[best_g],
208
+ amp_v = amp_v,
209
+ bg_v = bg,
210
+ decay_valid = decay if win is None else decay[:, win],
211
+ basis_best = basis_grid[best_g],
212
+ ny = ny, nx = nx,
213
+ n_bins = n_fit,
214
+ )
215
+ if progress_callback is not None:
216
+ progress_callback(last, valid_idx.size)
217
+ return maps
218
+
219
+ def batch_free_tau_fit(
220
+ self,
221
+ stack,
222
+ irf_array,
223
+ tcspc_res,
224
+ taus_init,
225
+ tau_min_s,
226
+ tau_max_s,
227
+ n_exp,
228
+ min_photons,
229
+ correct_pileup,
230
+ n_sync_px,
231
+ n_steps=50,
232
+ lr=None,
233
+ tvb_profile=None,
234
+ fit_tvb=False,
235
+ fit_idx=None,
236
+ ):
237
+ torch = self._torch
238
+ ny, nx, n_bins = stack.shape
239
+ taus_ns_init = taus_init * 1e9
240
+ flat = stack.reshape(ny * nx, n_bins).astype(np.float32)
241
+ intensity_flat = flat.sum(axis=1)
242
+ valid_mask = intensity_flat >= min_photons
243
+ valid_idx = np.where(valid_mask)[0]
244
+ maps = self._init_maps(
245
+ ny, nx, n_exp,
246
+ intensity=stack.sum(axis=2),
247
+ taus_fixed_ns=taus_ns_init,
248
+ free_tau=True,
249
+ )
250
+ if valid_idx.size == 0:
251
+ return maps
252
+ bg_flat = self._estimate_bg_batch(flat, valid_mask)
253
+ dc_flat = np.maximum(flat - bg_flat[:, None], 0.0)
254
+ if correct_pileup and n_sync_px > 0:
255
+ for idx in valid_idx:
256
+ dc_flat[idx] = coates_pileup_correction(dc_flat[idx], n_sync_px)
257
+ raw_valid = flat[valid_idx].astype(np.float32)
258
+ bg_valid = bg_flat[valid_idx].astype(np.float32)
259
+ B = len(valid_idx)
260
+ taus_out, amps_out, chi2r_out, chi2c_out, _, valid_b, tvb_out = self._scipy_parallel_free_tau_fit(
261
+ raw_valid, bg_valid, irf_array, tcspc_res,
262
+ taus_init, tau_min_s, tau_max_s, n_exp, n_bins,
263
+ tvb_profile=tvb_profile, fit_tvb=fit_tvb, fit_idx=fit_idx,
264
+ )
265
+ self._scatter_free_tau(
266
+ maps, valid_idx=valid_idx[valid_b],
267
+ taus_s=taus_out[valid_b], amps=amps_out[valid_b],
268
+ chi2_r=chi2r_out[valid_b], calibrated_values=chi2c_out[valid_b],
269
+ ny=ny, nx=nx, n_exp=n_exp,
270
+ tvb=tvb_out[valid_b] if fit_tvb else None,
271
+ )
272
+ return maps
273
+
274
+ def batch_dist_scan_unimodal(
275
+ self,
276
+ stack,
277
+ basis,
278
+ bb_grid,
279
+ param_pairs,
280
+ irf_fixed,
281
+ tcspc_res,
282
+ n_bins,
283
+ dist_type,
284
+ min_photons,
285
+ progress_callback=None,
286
+ tvb_profile=None,
287
+ fit_tvb=False,
288
+ fit_idx=None,
289
+ ):
290
+ torch = self._torch
291
+ ny, nx, _ = stack.shape
292
+ flat = stack.reshape(ny * nx, n_bins).astype(np.float32)
293
+ win = fit_window(fit_idx, n_bins)
294
+ n_fit = n_bins if win is None else len(win)
295
+ flat_fit = flat if win is None else flat[:, win]
296
+ intensity_flat = flat.sum(axis=1)
297
+ valid_mask = intensity_flat >= min_photons
298
+ valid_idx = np.where(valid_mask)[0]
299
+ maps = dict(
300
+ intensity = stack.sum(axis=2),
301
+ tau_mean_amp = np.full((ny, nx), np.nan),
302
+ tau_mean_int = np.full((ny, nx), np.nan),
303
+ chi2_r = np.full((ny, nx), np.nan),
304
+ calibrated_chi2_r = np.full((ny, nx), np.nan),
305
+ tau_center_1 = np.full((ny, nx), np.nan),
306
+ width_1 = np.full((ny, nx), np.nan),
307
+ alpha_1 = np.full((ny, nx), np.nan),
308
+ frac_1 = np.full((ny, nx), np.nan),
309
+ )
310
+ if valid_idx.size == 0:
311
+ return maps
312
+ if fit_tvb and tvb_profile is not None:
313
+ maps['tvb_scale'] = np.full((ny, nx), np.nan)
314
+ tvb_fit = np.asarray(tvb_profile) if win is None else np.asarray(tvb_profile)[win]
315
+ U, U_pinv, basis_perp, bb_perp = self._tvb_grid_prep(basis, tvb_fit, n_fit)
316
+ d_valid = flat_fit[valid_idx]
317
+ d_perp = self._tvb_project_data(d_valid.astype(np.float64), U, U_pinv).astype(np.float32)
318
+ basis_pt = torch.as_tensor(basis_perp, dtype=torch.float32, device=self.device)
319
+ bbp_t = torch.as_tensor(bb_perp, dtype=torch.float32, device=self.device)
320
+ dperp_t = torch.as_tensor(d_perp, dtype=torch.float32, device=self.device)
321
+ bd_t = self._matmul_full_precision(dperp_t, basis_pt.T)
322
+ dsq_t = (dperp_t ** 2).sum(dim=1)
323
+ costs_t = dsq_t[:, None] - torch.clamp(bd_t, min=0.0) ** 2 / bbp_t[None, :]
324
+ best_g = costs_t.argmin(dim=1).cpu().numpy()
325
+ bd_np = bd_t.cpu().numpy()
326
+ tau_v = param_pairs[best_g, 0]
327
+ w_v = param_pairs[best_g, 1]
328
+ amp_v = np.maximum(bd_np[np.arange(len(valid_idx)), best_g] / bb_perp[best_g], 0.0)
329
+ good = amp_v > 0
330
+ tau_amp_ns = tau_v * 1e9
331
+ tau_int_ns = (tau_v + w_v ** 2 / np.maximum(tau_v, 1e-15)) * 1e9
332
+ basis_best = basis[best_g].astype(np.float64)
333
+ resid_after = d_valid.astype(np.float64) - amp_v[:, None] * basis_best
334
+ vz = resid_after @ U_pinv.T
335
+ tvb_v = np.maximum(vz[:, 0], 0.0)
336
+ bg_z = vz[:, 1]
337
+ B_arr = np.asarray(tvb_fit, dtype=np.float64)
338
+ model_v = amp_v[:, None] * basis_best + tvb_v[:, None] * B_arr[None, :] + bg_z[:, None]
339
+ resid_v = d_valid.astype(np.float64) - model_v
340
+ chi2_v = (resid_v ** 2 / np.maximum(model_v, 1.0)).sum(axis=1) / distribution_dof(n_fit, 1, True)
341
+ chi2_cal_v = calibrated_chi2(d_valid, model_v, axis=1)
342
+ yi_arr, xi_arr = np.unravel_index(valid_idx, (ny, nx))
343
+ maps['tau_center_1'][yi_arr[good], xi_arr[good]] = tau_amp_ns[good]
344
+ maps['width_1'][yi_arr[good], xi_arr[good]] = w_v[good] * 1e9
345
+ maps['alpha_1'][yi_arr[good], xi_arr[good]] = amp_v[good]
346
+ maps['frac_1'][yi_arr[good], xi_arr[good]] = 1.0
347
+ maps['tau_mean_amp'][yi_arr[good], xi_arr[good]] = tau_amp_ns[good]
348
+ maps['tau_mean_int'][yi_arr[good], xi_arr[good]] = tau_int_ns[good]
349
+ maps['chi2_r'][yi_arr[good], xi_arr[good]] = chi2_v[good]
350
+ maps['calibrated_chi2_r'][yi_arr[good], xi_arr[good]] = chi2_cal_v[good]
351
+ maps['tvb_scale'][yi_arr[good], xi_arr[good]] = tvb_v[good]
352
+ return maps
353
+ bg_flat = self._estimate_bg_batch(flat, valid_mask)
354
+ dc_flat = np.maximum(flat_fit - bg_flat[:, None], 0.0)
355
+ dc_valid = dc_flat[valid_idx]
356
+ basis_t = torch.as_tensor(basis, dtype=torch.float32, device=self.device)
357
+ bb_t = torch.as_tensor(bb_grid, dtype=torch.float32, device=self.device)
358
+ dc_t = torch.as_tensor(dc_valid, dtype=torch.float32, device=self.device)
359
+ bd_t = dc_t @ basis_t.T
360
+ dc_sq_t = (dc_t ** 2).sum(dim=1)
361
+ costs_t = dc_sq_t[:, None] - torch.clamp(bd_t, min=0.0) ** 2 / bb_t[None, :]
362
+ best_g = costs_t.argmin(dim=1).cpu().numpy()
363
+ bd_np = bd_t.cpu().numpy()
364
+ tau_v = param_pairs[best_g, 0]
365
+ w_v = param_pairs[best_g, 1]
366
+ amp_v = np.maximum(
367
+ bd_np[np.arange(len(valid_idx)), best_g] / bb_grid[best_g].astype(np.float64), 0.0)
368
+ good = amp_v > 0
369
+ tau_amp_ns = tau_v * 1e9
370
+ tau_int_ns = (tau_v + w_v ** 2 / np.maximum(tau_v, 1e-15)) * 1e9
371
+ basis_best = basis[best_g].astype(np.float64)
372
+ model_v = amp_v[:, None] * basis_best + bg_flat[valid_idx, None]
373
+ resid_v = flat_fit[valid_idx].astype(np.float64) - model_v
374
+ chi2_v = (resid_v ** 2 / np.maximum(model_v, 1.0)).sum(axis=1) / distribution_dof(n_fit, 1, False)
375
+ chi2_cal_v = calibrated_chi2(flat_fit[valid_idx], model_v, axis=1)
376
+ yi_arr, xi_arr = np.unravel_index(valid_idx, (ny, nx))
377
+ maps['tau_center_1'][yi_arr[good], xi_arr[good]] = tau_amp_ns[good]
378
+ maps['width_1'][yi_arr[good], xi_arr[good]] = w_v[good] * 1e9
379
+ maps['alpha_1'][yi_arr[good], xi_arr[good]] = amp_v[good]
380
+ maps['frac_1'][yi_arr[good], xi_arr[good]] = 1.0
381
+ maps['tau_mean_amp'][yi_arr[good], xi_arr[good]] = tau_amp_ns[good]
382
+ maps['tau_mean_int'][yi_arr[good], xi_arr[good]] = tau_int_ns[good]
383
+ maps['chi2_r'][yi_arr[good], xi_arr[good]] = chi2_v[good]
384
+ maps['calibrated_chi2_r'][yi_arr[good], xi_arr[good]] = chi2_cal_v[good]
385
+ return maps
@@ -0,0 +1,10 @@
1
+ class AppState:
2
+ def values(self) -> dict:
3
+ # Snapshot of every tk variable currently held, name -> current value.
4
+ out = {}
5
+ for nam, var in self.__dict__.items():
6
+ try:
7
+ out[nam] = var.get()
8
+ except Exception:
9
+ pass
10
+ return out
@@ -0,0 +1,139 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from flimkit.UI.utils import _C, _flt, _thresh
4
+
5
+ class FLIMKitController:
6
+ def __init__(self, builder):
7
+ self.b = builder
8
+
9
+ def fov_args(self):
10
+ cfg = _C()
11
+ ptu = self.b.sv_ptu.get().strip()
12
+ irf = self.b._irf_fov.get_args(xlsx_fallback=self.b.sv_xlsx.get().strip())
13
+ a = argparse.Namespace()
14
+ a.ptu = ptu
15
+ a.xlsx = self.b.sv_xlsx.get().strip() or None
16
+ a.debug_xlsx = False
17
+ a.print_config = False
18
+ a.irf = irf['irf']
19
+ a.irf_xlsx = irf['irf_xlsx']
20
+ a.estimate_irf = irf['estimate_irf']
21
+ a.no_xlsx_irf = irf['no_xlsx_irf']
22
+ a.machine_irf = irf.get('machine_irf') or str(_C()['MACHINE_IRF_DEFAULT_PATH'])
23
+ a.irf_bins = cfg['IRF_BINS']
24
+ a.irf_fit_width = cfg['IRF_FIT_WIDTH']
25
+ a.irf_fwhm = cfg['IRF_FWHM']
26
+ _model = self.b.sv_fit_model_fov.get()
27
+ a.dist_type = _model
28
+ if _model in ('discrete', 'tail'):
29
+ a.nexp = self.b.iv_nexp_fov.get()
30
+ a.dist_n_components = 1
31
+ else:
32
+ a.nexp = 2
33
+ a.dist_n_components = self.b.iv_ncomp_dist_fov.get()
34
+ a.tau_min = float(self.b.sv_tau_min_fov.get() or cfg['Tau_min'])
35
+ a.tau_max = float(self.b.sv_tau_max_fov.get() or cfg['Tau_max'])
36
+ a.mode = self.b.sv_mode_fov.get()
37
+ a.binning = cfg['binning_factor']
38
+ a.min_photons = cfg['MIN_PHOTONS_PERPIX']
39
+ a.optimizer = cfg['Optimizer']
40
+ a.restarts = cfg['lm_restarts']
41
+ a.de_population = cfg['de_population']
42
+ a.de_maxiter = cfg['de_maxiter']
43
+ a.workers = cfg['n_workers']
44
+ a.no_polish = False
45
+ a.channel = cfg['channels']
46
+ _out_raw = self.b.sv_out_fov.get().strip() or cfg['OUT_NAME']
47
+ if Path(_out_raw).parent == Path('.'):
48
+ a.out = str(Path(ptu).parent / _out_raw)
49
+ else:
50
+ a.out = _out_raw
51
+ a.no_plots = False
52
+ a.cell_mask = self.b.bv_cell.get()
53
+ a.correct_pileup = self.b.bv_correct_pileup.get()
54
+ a.intensity_threshold = _thresh(self.b.bv_thr_fov, self.b.sv_thr_fov)
55
+ a.irf_align = 'steepest_rise'
56
+ a.irf_shift_bins = 2
57
+ a.tvb_ptu = (self.b.sv_tvb_ptu_fov.get().strip() or None) if hasattr(self.b, 'sv_tvb_ptu_fov') else None
58
+ a.tvb_channel = None
59
+ self.b._apply_expert_overrides(a)
60
+ return a
61
+
62
+ def stitch_args(self):
63
+ xlif = self.b.sv_xlif.get().strip()
64
+ ptu_dir = self.b.sv_ptu_dir.get().strip()
65
+ out_base = self.b.sv_out_st.get().strip()
66
+ pipeline = self.b.sv_pipeline.get()
67
+ if pipeline == 'series_fit' and not xlif:
68
+ roi_name = Path(ptu_dir).name.replace(' ', '_') or 'series'
69
+ ptu_basename = None
70
+ else:
71
+ roi_name = Path(xlif).stem.replace(' ', '_')
72
+ ptu_basename = Path(xlif).stem
73
+ output_dir = str(Path(out_base) / roi_name)
74
+ a = argparse.Namespace()
75
+ a.xlif = xlif
76
+ a.ptu_dir = ptu_dir
77
+ a.output_dir = output_dir
78
+ a.ptu_basename = ptu_basename
79
+ a.rotate_tiles = self.b.bv_rotate.get()
80
+ cfg = _C()
81
+ irf = self.b._irf_st.get_args()
82
+ a.irf = irf['irf']
83
+ a.irf_xlsx = irf['irf_xlsx']
84
+ a.no_xlsx_irf = irf['no_xlsx_irf']
85
+ a.estimate_irf = irf['estimate_irf'] if irf['estimate_irf'] != 'none' else 'gaussian'
86
+ a.machine_irf = irf.get('machine_irf') or str(cfg['MACHINE_IRF_DEFAULT_PATH'])
87
+ _model_st = self.b.sv_fit_model_st.get()
88
+ a.dist_type = _model_st
89
+ if _model_st in ('discrete', 'tail'):
90
+ a.nexp = self.b.iv_nexp_st.get()
91
+ a.dist_n_components = 1
92
+ else:
93
+ a.nexp = 2
94
+ a.dist_n_components = self.b.iv_ncomp_dist_st.get()
95
+ a.tau_min = float(self.b.sv_tau_fit_lo.get() or cfg['Tau_min'])
96
+ a.tau_max = float(self.b.sv_tau_fit_hi.get() or cfg['Tau_max'])
97
+ a.register_tiles = self.b.bv_register.get()
98
+ a.reg_max_shift_px = int(self.b.sv_reg_max_shift.get() or 120)
99
+ a.binning = cfg['binning_factor']
100
+ a.min_photons = cfg['MIN_PHOTONS_PERPIX']
101
+ a.optimizer = 'de'
102
+ a.restarts = cfg['lm_restarts']
103
+ a.de_population = cfg['de_population']
104
+ a.de_maxiter = cfg['de_maxiter']
105
+ a.workers = cfg['n_workers']
106
+ a.no_polish = False
107
+ a.channel = cfg['channels']
108
+ a.irf_fwhm = cfg['IRF_FWHM']
109
+ a.irf_bins = cfg['IRF_BINS']
110
+ a.irf_fit_width = cfg['IRF_FIT_WIDTH']
111
+ a.tau_display_min = _flt(self.b.sv_tau_lo)
112
+ a.tau_display_max = _flt(self.b.sv_tau_hi)
113
+ a.intensity_display_min = _flt(self.b.sv_int_lo)
114
+ a.intensity_display_max = _flt(self.b.sv_int_hi)
115
+ a.intensity_threshold = _thresh(self.b.bv_thr_st, self.b.sv_thr_st)
116
+ a.correct_pileup = self.b.bv_correct_pileup_st.get()
117
+ a.save_individual = self.b.bv_save_ind.get()
118
+ a.save_tau_weighted = self.b.bv_save_tau_weighted.get()
119
+ a.save_int_weighted = self.b.bv_save_int_weighted.get()
120
+ a.save_amp_weighted = self.b.bv_save_amp_weighted.get()
121
+ a.irf_align = 'steepest_rise'
122
+ a.irf_shift_bins = 2
123
+ a.tvb_ptu = (self.b.sv_tvb_ptu_st.get().strip() or None) if hasattr(self.b, 'sv_tvb_ptu_st') else None
124
+ a.tvb_channel = None
125
+ self.b._apply_expert_overrides(a)
126
+ if pipeline == 'tile_fit':
127
+ a.mode = 'both'
128
+ a.no_plots = True
129
+ a.cell_mask = False
130
+ a.debug_xlsx = False
131
+ a.print_config = False
132
+ a.xlsx = None
133
+ a.out = None
134
+ a.irf_xlsx_dir = self.b.sv_tile_irf_dir.get().strip() or None
135
+ else:
136
+ a.mode = 'both' if self.b.bv_perpix.get() else 'summed'
137
+ a.no_plots = False
138
+ a.irf_xlsx_dir = None
139
+ return a