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
flimkit/FLIM/models.py ADDED
@@ -0,0 +1,391 @@
1
+ import numpy as np
2
+ from ..FLIM.irf_tools import build_full_irf
3
+
4
+ _N_QUAD = 200
5
+
6
+ def _exponential_kernel(tcspc_res, n_bins, taus, amps, bg):
7
+ t = np.arange(n_bins, dtype=float) * tcspc_res
8
+ return sum(a * np.exp(-t / max(tau, 1e-15))
9
+ for a, tau in zip(amps, taus)) + bg
10
+
11
+ def apply_pileup(model, n_sync):
12
+ # Forward first-photon pile-up: P(first photon of a pulse lands in bin i).
13
+ # Exact inverse of coates_pileup_correction, so the fit sees distorted model
14
+ # against raw (Poisson) data instead of corrected (non-Poisson) data.
15
+ # Needs the FULL-length model: bin i depends on the cumulative rate in every
16
+ # preceding bin, including bins outside the fit window.
17
+ n_s = float(n_sync)
18
+ lam = np.maximum(np.asarray(model, dtype=float), 0.0) / n_s
19
+ cum = np.concatenate([[0.0], np.cumsum(lam[:-1])])
20
+ return n_s * np.exp(-cum) * (1.0 - np.exp(-lam))
21
+
22
+ class _DECost:
23
+ def __init__(self, tcspc_res, n_bins, irf_prompt, n_exp, bg_fixed,
24
+ has_tail, fit_bg, fit_sigma,
25
+ fit_idx, decay, weights,
26
+ tvb_profile=None, fit_tvb=False, n_sync=None):
27
+ self.tcspc_res = tcspc_res
28
+ self.n_bins = n_bins
29
+ self.irf_prompt = irf_prompt
30
+ self.n_exp = n_exp
31
+ self.bg_fixed = bg_fixed
32
+ self.has_tail = has_tail
33
+ self.fit_bg = fit_bg
34
+ self.fit_sigma = fit_sigma
35
+ self.fit_idx = fit_idx
36
+ self.decay = decay
37
+ self.weights = weights
38
+ self.tvb_profile = tvb_profile
39
+ self.fit_tvb = fit_tvb
40
+ self.n_sync = n_sync
41
+
42
+ def __call__(self, params):
43
+ model = reconvolution_model(
44
+ params, self.tcspc_res, self.n_bins, self.irf_prompt,
45
+ self.n_exp, self.bg_fixed, self.has_tail,
46
+ self.fit_bg, self.fit_sigma,
47
+ tvb_profile=self.tvb_profile, fit_tvb=self.fit_tvb,
48
+ n_sync=self.n_sync)
49
+ res = ((model[self.fit_idx]
50
+ - self.decay[self.fit_idx])
51
+ / self.weights)
52
+ return np.sum(res**2)
53
+
54
+ class _DECostLogTau(_DECost):
55
+ def __call__(self, params):
56
+ params_lin = np.array(params, dtype=float)
57
+ params_lin[:self.n_exp] = 10.0 ** params_lin[:self.n_exp]
58
+ return super().__call__(params_lin)
59
+
60
+ class _DECostPoisson:
61
+ def __init__(self, tcspc_res, n_bins, irf_prompt, n_exp, bg_fixed,
62
+ has_tail, fit_bg, fit_sigma,
63
+ fit_idx, decay,
64
+ tvb_profile=None, fit_tvb=False, n_sync=None):
65
+ self.tcspc_res = tcspc_res
66
+ self.n_bins = n_bins
67
+ self.irf_prompt = irf_prompt
68
+ self.n_exp = n_exp
69
+ self.bg_fixed = bg_fixed
70
+ self.has_tail = has_tail
71
+ self.fit_bg = fit_bg
72
+ self.fit_sigma = fit_sigma
73
+ self.fit_idx = fit_idx
74
+ self.decay = decay
75
+ self.tvb_profile = tvb_profile
76
+ self.fit_tvb = fit_tvb
77
+ self.n_sync = n_sync
78
+
79
+ def __call__(self, params):
80
+ model = reconvolution_model(
81
+ params, self.tcspc_res, self.n_bins, self.irf_prompt,
82
+ self.n_exp, self.bg_fixed, self.has_tail,
83
+ self.fit_bg, self.fit_sigma,
84
+ tvb_profile=self.tvb_profile, fit_tvb=self.fit_tvb,
85
+ n_sync=self.n_sync)
86
+ n = self.decay[self.fit_idx]
87
+ m = np.maximum(model[self.fit_idx], 1e-10)
88
+ # Poisson deviance (C-statistic)
89
+ dev = m - n
90
+ pos = n > 0
91
+ dev[pos] += n[pos] * np.log(n[pos] / m[pos])
92
+ return 2.0 * np.sum(dev)
93
+
94
+ class _DECostPoissonLogTau(_DECostPoisson):
95
+
96
+ def __call__(self, params):
97
+ params_lin = np.array(params, dtype=float)
98
+ params_lin[:self.n_exp] = 10.0 ** params_lin[:self.n_exp]
99
+ return super().__call__(params_lin)
100
+
101
+ def unpack_tail_params(params, n_exp, fit_t0, fit_bg, fit_tvb,
102
+ t0_fixed=0.0, bg_fixed=0.0, tvb_fixed=0.0):
103
+ taus = np.clip(np.asarray(params[:n_exp], dtype=float), 1e-14, None)
104
+ amps = np.asarray(params[n_exp:2 * n_exp], dtype=float)
105
+ idx = 2 * n_exp
106
+ if fit_t0:
107
+ t0 = float(params[idx]); idx += 1
108
+ else:
109
+ t0 = float(t0_fixed)
110
+ if fit_bg:
111
+ bg = float(params[idx]); idx += 1
112
+ else:
113
+ bg = float(bg_fixed)
114
+ if fit_tvb:
115
+ tvb_scale = float(params[idx]); idx += 1
116
+ else:
117
+ tvb_scale = float(tvb_fixed)
118
+ return taus, amps, t0, bg, tvb_scale
119
+
120
+ def tail_basis(tcspc_res, n_bins, taus, t0):
121
+ t = np.arange(n_bins, dtype=float) * tcspc_res - t0
122
+ live = t >= 0.0
123
+ t_pos = np.maximum(t, 0.0)
124
+ return np.array([np.where(live, np.exp(-t_pos / max(tau, 1e-15)), 0.0)
125
+ for tau in taus])
126
+
127
+ def tail_model(params, tcspc_res, n_bins, n_exp, bg_fixed, fit_bg,
128
+ fit_t0=False, t0_fixed=0.0,
129
+ tvb_profile=None, fit_tvb=False, tvb_fixed=0.0,
130
+ n_sync=None):
131
+ taus, amps, t0, bg, tvb_scale = unpack_tail_params(
132
+ params, n_exp, fit_t0, fit_bg, fit_tvb,
133
+ t0_fixed=t0_fixed, bg_fixed=bg_fixed, tvb_fixed=tvb_fixed)
134
+ order = np.argsort(-taus)
135
+ taus = taus[order]
136
+ amps = amps[order]
137
+ model = amps @ tail_basis(tcspc_res, n_bins, taus, t0) + bg
138
+ if tvb_profile is not None:
139
+ model = model + tvb_scale * tvb_profile
140
+ if n_sync:
141
+ model = apply_pileup(model, n_sync)
142
+ return model
143
+
144
+ class _DECostTail:
145
+ def __init__(self, tcspc_res, n_bins, n_exp, bg_fixed, fit_bg,
146
+ fit_idx, decay, weights,
147
+ fit_t0=False, t0_fixed=0.0,
148
+ tvb_profile=None, fit_tvb=False, n_sync=None):
149
+ self.tcspc_res = tcspc_res
150
+ self.n_bins = n_bins
151
+ self.n_exp = n_exp
152
+ self.bg_fixed = bg_fixed
153
+ self.fit_bg = fit_bg
154
+ self.fit_idx = fit_idx
155
+ self.decay = decay
156
+ self.weights = weights
157
+ self.fit_t0 = fit_t0
158
+ self.t0_fixed = t0_fixed
159
+ self.tvb_profile = tvb_profile
160
+ self.fit_tvb = fit_tvb
161
+ self.n_sync = n_sync
162
+
163
+ def _model(self, params):
164
+ return tail_model(
165
+ params, self.tcspc_res, self.n_bins, self.n_exp,
166
+ self.bg_fixed, self.fit_bg,
167
+ fit_t0=self.fit_t0, t0_fixed=self.t0_fixed,
168
+ tvb_profile=self.tvb_profile, fit_tvb=self.fit_tvb,
169
+ n_sync=self.n_sync)
170
+
171
+ def __call__(self, params):
172
+ model = self._model(params)
173
+ res = (model[self.fit_idx] - self.decay[self.fit_idx]) / self.weights
174
+ return np.sum(res ** 2)
175
+
176
+ class _DECostTailLogTau(_DECostTail):
177
+ def __call__(self, params):
178
+ params_lin = np.array(params, dtype=float)
179
+ params_lin[:self.n_exp] = 10.0 ** params_lin[:self.n_exp]
180
+ return super().__call__(params_lin)
181
+
182
+ class _DECostTailPoisson(_DECostTail):
183
+ def __init__(self, tcspc_res, n_bins, n_exp, bg_fixed, fit_bg,
184
+ fit_idx, decay,
185
+ fit_t0=False, t0_fixed=0.0,
186
+ tvb_profile=None, fit_tvb=False, n_sync=None):
187
+ super().__init__(tcspc_res, n_bins, n_exp, bg_fixed, fit_bg,
188
+ fit_idx, decay, None,
189
+ fit_t0=fit_t0, t0_fixed=t0_fixed,
190
+ tvb_profile=tvb_profile, fit_tvb=fit_tvb, n_sync=n_sync)
191
+
192
+ def __call__(self, params):
193
+ model = self._model(params)
194
+ n = self.decay[self.fit_idx]
195
+ m = np.maximum(model[self.fit_idx], 1e-10)
196
+ dev = m - n
197
+ pos = n > 0
198
+ dev[pos] += n[pos] * np.log(n[pos] / m[pos])
199
+ return 2.0 * np.sum(dev)
200
+
201
+ class _DECostTailPoissonLogTau(_DECostTailPoisson):
202
+ def __call__(self, params):
203
+ params_lin = np.array(params, dtype=float)
204
+ params_lin[:self.n_exp] = 10.0 ** params_lin[:self.n_exp]
205
+ return super().__call__(params_lin)
206
+
207
+ def _alpha_gaussian(tau_grid, tau_center, sigma_tau):
208
+ return np.exp(-0.5 * ((tau_grid - tau_center) / max(sigma_tau, 1e-15)) ** 2)
209
+
210
+ def _alpha_lorentzian(tau_grid, tau_center, gamma):
211
+ half_g = max(gamma / 2.0, 1e-15)
212
+ return half_g ** 2 / ((tau_grid - tau_center) ** 2 + half_g ** 2)
213
+
214
+ def _dist_kernel(tcspc_res, n_bins, tau_center, width, amp, dist_type, n_quad=_N_QUAD):
215
+ spread = 4.0 * width if dist_type == 'gaussian' else 8.0 * max(width / 2.0, 1e-15)
216
+ tau_lo = max(tau_center - spread, 1e-12)
217
+ tau_hi = max(tau_center + spread, tau_lo + 1e-12)
218
+ tau_grid = np.linspace(tau_lo, tau_hi, n_quad)
219
+ alpha = (_alpha_gaussian(tau_grid, tau_center, width) if dist_type == 'gaussian'
220
+ else _alpha_lorentzian(tau_grid, tau_center, width))
221
+ alpha_sum = alpha.sum()
222
+ if alpha_sum > 0:
223
+ alpha = alpha / alpha_sum
224
+ t = np.arange(n_bins, dtype=float) * tcspc_res
225
+ exp_mat = np.exp(-t[None, :] / np.maximum(tau_grid[:, None], 1e-15))
226
+ return amp * (alpha @ exp_mat)
227
+
228
+ def dist_reconvolution_model(params, tcspc_res, n_bins, irf_prompt,
229
+ n_components, dist_type, bg_fixed, fit_bg, fit_sigma,
230
+ tvb_profile=None, fit_tvb=False, tvb_fixed=0.0,
231
+ n_sync=None):
232
+ # params layout: [tau_c×N, width×N, amp×N, shift, (sigma), (bg), (tvb_scale)]
233
+ tau_centers = np.clip(params[:n_components], 1e-14, None)
234
+ widths = np.clip(params[n_components:2 * n_components], 1e-14, None)
235
+ amps = params[2 * n_components:3 * n_components]
236
+ idx = 3 * n_components
237
+ shift = params[idx]; idx += 1
238
+ sigma = params[idx] if fit_sigma else 0.0
239
+ if fit_sigma:
240
+ idx += 1
241
+ if fit_bg:
242
+ bg = params[idx]; idx += 1
243
+ else:
244
+ bg = bg_fixed
245
+ if fit_tvb:
246
+ tvb_scale = params[idx]; idx += 1
247
+ else:
248
+ tvb_scale = tvb_fixed
249
+ kernel = np.zeros(n_bins, dtype=float)
250
+ for i in range(n_components):
251
+ kernel += _dist_kernel(tcspc_res, n_bins, tau_centers[i], widths[i], amps[i], dist_type)
252
+ kernel += bg
253
+ irf_full = build_full_irf(irf_prompt, shift, sigma, 0.0, 1.0, n_bins)
254
+ model = np.real(np.fft.ifft(np.fft.fft(kernel) * np.fft.fft(irf_full)))
255
+ if tvb_profile is not None:
256
+ model = model + tvb_scale * tvb_profile
257
+ if n_sync:
258
+ model = apply_pileup(model, n_sync)
259
+ return model
260
+
261
+ def build_dist_basis_grid(tcspc_res, n_bins, irf_fixed,
262
+ tau_grid, width_grid, dist_type, n_quad=_N_QUAD):
263
+ irf_fft = np.fft.fft(irf_fixed)
264
+ n_total = len(tau_grid) * len(width_grid)
265
+ basis = np.empty((n_total, n_bins), dtype=np.float32)
266
+ param_pairs = np.empty((n_total, 2), dtype=np.float32)
267
+ idx = 0
268
+ for tau_c in tau_grid:
269
+ for w in width_grid:
270
+ kernel = _dist_kernel(tcspc_res, n_bins, tau_c, w, 1.0, dist_type, n_quad)
271
+ basis[idx] = np.real(np.fft.ifft(np.fft.fft(kernel) * irf_fft)).astype(np.float32)
272
+ param_pairs[idx] = [tau_c, w]
273
+ idx += 1
274
+ return basis, param_pairs
275
+
276
+ class _DECostDist:
277
+ def __init__(self, tcspc_res, n_bins, irf_prompt, n_components, dist_type,
278
+ bg_fixed, fit_bg, fit_sigma, fit_idx, decay, weights,
279
+ tvb_profile=None, fit_tvb=False, n_sync=None):
280
+ self.tcspc_res = tcspc_res
281
+ self.n_bins = n_bins
282
+ self.irf_prompt = irf_prompt
283
+ self.n_components = n_components
284
+ self.dist_type = dist_type
285
+ self.bg_fixed = bg_fixed
286
+ self.fit_bg = fit_bg
287
+ self.fit_sigma = fit_sigma
288
+ self.fit_idx = fit_idx
289
+ self.decay = decay
290
+ self.weights = weights
291
+ self.tvb_profile = tvb_profile
292
+ self.fit_tvb = fit_tvb
293
+ self.n_sync = n_sync
294
+
295
+ def __call__(self, params):
296
+ model = dist_reconvolution_model(
297
+ params, self.tcspc_res, self.n_bins, self.irf_prompt,
298
+ self.n_components, self.dist_type,
299
+ self.bg_fixed, self.fit_bg, self.fit_sigma,
300
+ tvb_profile=self.tvb_profile, fit_tvb=self.fit_tvb,
301
+ n_sync=self.n_sync)
302
+ res = ((model[self.fit_idx]
303
+ - self.decay[self.fit_idx])
304
+ / self.weights)
305
+ return np.sum(res ** 2)
306
+
307
+ class _DECostDistLogParam(_DECostDist):
308
+ def __call__(self, params):
309
+ p = np.array(params, dtype=float)
310
+ n = self.n_components
311
+ p[:n] = 10.0 ** p[:n]
312
+ p[n:2*n] = 10.0 ** p[n:2*n]
313
+ return super().__call__(p)
314
+
315
+ class _DECostDistPoisson:
316
+ def __init__(self, tcspc_res, n_bins, irf_prompt, n_components, dist_type,
317
+ bg_fixed, fit_bg, fit_sigma, fit_idx, decay,
318
+ tvb_profile=None, fit_tvb=False, n_sync=None):
319
+ self.tcspc_res = tcspc_res
320
+ self.n_bins = n_bins
321
+ self.irf_prompt = irf_prompt
322
+ self.n_components = n_components
323
+ self.dist_type = dist_type
324
+ self.bg_fixed = bg_fixed
325
+ self.fit_bg = fit_bg
326
+ self.fit_sigma = fit_sigma
327
+ self.fit_idx = fit_idx
328
+ self.decay = decay
329
+ self.tvb_profile = tvb_profile
330
+ self.fit_tvb = fit_tvb
331
+ self.n_sync = n_sync
332
+
333
+ def __call__(self, params):
334
+ model = dist_reconvolution_model(
335
+ params, self.tcspc_res, self.n_bins, self.irf_prompt,
336
+ self.n_components, self.dist_type,
337
+ self.bg_fixed, self.fit_bg, self.fit_sigma,
338
+ tvb_profile=self.tvb_profile, fit_tvb=self.fit_tvb,
339
+ n_sync=self.n_sync)
340
+ n = self.decay[self.fit_idx]
341
+ m = np.maximum(model[self.fit_idx], 1e-10)
342
+ dev = m - n
343
+ pos = n > 0
344
+ dev[pos] += n[pos] * np.log(n[pos] / m[pos])
345
+ return 2.0 * np.sum(dev)
346
+
347
+ class _DECostDistPoissonLogParam(_DECostDistPoisson):
348
+ def __call__(self, params):
349
+ p = np.array(params, dtype=float)
350
+ n = self.n_components
351
+ p[:n] = 10.0 ** p[:n]
352
+ p[n:2*n] = 10.0 ** p[n:2*n]
353
+ return super().__call__(p)
354
+
355
+ def reconvolution_model(params, tcspc_res, n_bins, irf_prompt,
356
+ n_exp, bg_fixed, has_tail, fit_bg, fit_sigma,
357
+ tvb_profile=None, fit_tvb=False, tvb_fixed=0.0,
358
+ n_sync=None):
359
+ taus = np.clip(params[:n_exp], 1e-14, None)
360
+ amps = params[n_exp:2*n_exp]
361
+ order = np.argsort(-taus)
362
+ taus = taus[order]
363
+ amps = amps[order]
364
+ idx = 2 * n_exp
365
+ shift = params[idx]; idx += 1
366
+ if fit_sigma:
367
+ sigma = params[idx]; idx += 1
368
+ else:
369
+ sigma = 0.0
370
+ if fit_bg:
371
+ bg = params[idx]; idx += 1
372
+ else:
373
+ bg = bg_fixed
374
+ if fit_tvb:
375
+ tvb_scale = params[idx]; idx += 1
376
+ else:
377
+ tvb_scale = tvb_fixed
378
+ if has_tail:
379
+ tail_amp = params[idx]
380
+ tail_tau = params[idx + 1]
381
+ else:
382
+ tail_amp, tail_tau = 0.0, 1.0
383
+ irf_full = build_full_irf(irf_prompt, shift, sigma, tail_amp, tail_tau, n_bins)
384
+ kernel = _exponential_kernel(tcspc_res, n_bins, taus, amps, bg)
385
+ model = np.real(np.fft.ifft(np.fft.fft(kernel) * np.fft.fft(irf_full)))
386
+ if tvb_profile is not None:
387
+ model = model + tvb_scale * tvb_profile
388
+ # after bg and TVB: the detector piles up every photon reaching it, not just signal
389
+ if n_sync:
390
+ model = apply_pileup(model, n_sync)
391
+ return model
@@ -0,0 +1,85 @@
1
+ import sys
2
+ import warnings
3
+
4
+ def get_backend(prefer='auto'):
5
+ if prefer == 'auto':
6
+ for name in ('mlx', 'cuda', 'mps', 'rocm'):
7
+ b = _try_backend(name)
8
+ if b is not None:
9
+ return b
10
+ return None
11
+ return _try_backend(prefer)
12
+
13
+
14
+ def _try_backend(name):
15
+ if name == 'mlx':
16
+ return _try_mlx()
17
+ if name in ('cuda', 'mps', 'rocm'):
18
+ return _try_torch(name)
19
+ raise ValueError(
20
+ f"Unknown backend {name!r}. "
21
+ "Choose from: 'auto', 'mlx', 'cuda', 'mps', 'rocm'."
22
+ )
23
+
24
+
25
+ def _try_mlx():
26
+ if sys.platform != 'darwin':
27
+ return None
28
+ try:
29
+ import mlx.core as mx # noqa: F401
30
+ gpu = mx.Device(mx.gpu)
31
+ with mx.stream(gpu):
32
+ mx.eval(mx.array([1.0]) + 1)
33
+ except Exception:
34
+ return None
35
+ from flimkit.GPU.mlx_backend import MLXBackend
36
+ return MLXBackend()
37
+
38
+ def _cuda_available():
39
+ try:
40
+ import torch
41
+ with warnings.catch_warnings():
42
+ warnings.simplefilter('ignore')
43
+ return torch.cuda.is_available()
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ def _cuda_device_name():
49
+ try:
50
+ import torch
51
+ with warnings.catch_warnings():
52
+ warnings.simplefilter('ignore')
53
+ return torch.cuda.get_device_name(0).lower()
54
+ except Exception:
55
+ return ''
56
+
57
+
58
+ def _try_torch(name):
59
+ try:
60
+ import torch
61
+ except ImportError:
62
+ return None
63
+
64
+ if name == 'cuda':
65
+ if not _cuda_available():
66
+ return None
67
+ device = 'cuda'
68
+ elif name == 'mps':
69
+ if not (torch.backends.mps.is_available() and
70
+ torch.backends.mps.is_built()):
71
+ return None
72
+ device = 'mps'
73
+ elif name == 'rocm':
74
+ if not _cuda_available():
75
+ return None
76
+ name_str = _cuda_device_name()
77
+ if not any(k in name_str for k in ('amd', 'radeon', 'vega', 'navi', 'gfx')):
78
+ return None
79
+ device = 'cuda'
80
+ else:
81
+ return None
82
+
83
+ from flimkit.GPU.torch_backend import TorchBackend
84
+ return TorchBackend(device=device)
85
+