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/phasor/fret.py ADDED
@@ -0,0 +1,654 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ import numpy as np
4
+
5
+ @dataclass
6
+ class FRETChannelData:
7
+
8
+ real_cal: np.ndarray
9
+ imag_cal: np.ndarray
10
+ mean: np.ndarray
11
+ frequency: float
12
+ min_photons: float = 0.01
13
+
14
+ def __post_init__(self):
15
+ self.real_cal = np.asarray(self.real_cal, dtype=float)
16
+ self.imag_cal = np.asarray(self.imag_cal, dtype=float)
17
+ self.mean = np.asarray(self.mean, dtype=float)
18
+ if self.real_cal.shape != self.imag_cal.shape:
19
+ raise ValueError(
20
+ f"real_cal and imag_cal must have the same shape; "
21
+ f"got {self.real_cal.shape} and {self.imag_cal.shape}."
22
+ )
23
+ if self.mean.shape != self.real_cal.shape:
24
+ raise ValueError(
25
+ f"mean must have the same shape as real_cal; "
26
+ f"got {self.mean.shape} vs {self.real_cal.shape}."
27
+ )
28
+ if self.frequency <= 0:
29
+ raise ValueError(
30
+ f"frequency must be positive (MHz); got {self.frequency}."
31
+ )
32
+
33
+ @property
34
+ def valid_mask(self):
35
+ return (self.mean >= self.min_photons) & np.isfinite(self.real_cal)
36
+
37
+ @property
38
+ def valid_g(self):
39
+ return self.real_cal[self.valid_mask]
40
+
41
+ @property
42
+ def valid_s(self):
43
+ return self.imag_cal[self.valid_mask]
44
+
45
+
46
+ @dataclass
47
+ class FRETModelParameters:
48
+
49
+ donor_lifetime: float
50
+ fret_efficiency: float = 0.0
51
+ donor_fretting: float = 1.0
52
+ donor_background: float = 0.0
53
+ background_real: float = 0.0
54
+ background_imag: float = 0.0
55
+ acceptor_lifetime: float | None = None
56
+ donor_bleedthrough: float = 0.0
57
+ acceptor_bleedthrough: float = 0.0
58
+ acceptor_background: float = 0.0
59
+
60
+ def __post_init__(self):
61
+ if self.donor_lifetime <= 0:
62
+ raise ValueError(
63
+ f"donor_lifetime must be positive (ns); got {self.donor_lifetime}."
64
+ )
65
+ for attr, lo, hi in [
66
+ ('fret_efficiency', 0.0, 1.0),
67
+ ('donor_fretting', 0.0, 1.0),
68
+ ]:
69
+ v = getattr(self, attr)
70
+ if not (lo <= v <= hi):
71
+ raise ValueError(f"{attr} must be in [{lo}, {hi}]; got {v}.")
72
+
73
+
74
+ @dataclass
75
+ class FRETBounds:
76
+
77
+ fret_efficiency: tuple[float, float] = (0.0, 1.0)
78
+ donor_fretting: tuple[float, float] = (0.0, 1.0)
79
+ donor_background: tuple[float, float] = (0.0, 10.0)
80
+ donor_bleedthrough: tuple[float, float] = (0.0, 10.0)
81
+ acceptor_bleedthrough: tuple[float, float] = (0.0, 10.0)
82
+ acceptor_background: tuple[float, float] = (0.0, 10.0)
83
+
84
+ def _as_scipy(self, attrs):
85
+ lo = [getattr(self, a)[0] for a in attrs]
86
+ hi = [getattr(self, a)[1] for a in attrs]
87
+ return dict(lb=lo, ub=hi)
88
+
89
+ def donor_only_scipy(self):
90
+ return self._as_scipy(
91
+ ('fret_efficiency', 'donor_fretting', 'donor_background')
92
+ )
93
+
94
+ def joint_scipy(self):
95
+ return self._as_scipy((
96
+ 'fret_efficiency', 'donor_fretting',
97
+ 'donor_background', 'donor_bleedthrough',
98
+ 'acceptor_bleedthrough', 'acceptor_background',
99
+ ))
100
+
101
+
102
+ @dataclass
103
+ class FRETResult:
104
+
105
+ fret_efficiency: float
106
+ donor_fretting: float
107
+ donor_background: float
108
+ donor_real_model: float
109
+ donor_imag_model: float
110
+ residual: float
111
+ donor_bleedthrough: float = 0.0
112
+ acceptor_bleedthrough: float = 0.0
113
+ acceptor_background: float = 0.0
114
+ acceptor_real_model: float | None = None
115
+ acceptor_imag_model: float | None = None
116
+ converged: bool = True
117
+ message: str = ''
118
+
119
+ def print_summary(self):
120
+ print('\u2550\u2550\u2550 FRET fit result \u2550\u2550\u2550')
121
+ print(f" FRET efficiency : {self.fret_efficiency:.4f}")
122
+ print(f" Donor fretting : {self.donor_fretting:.4f}")
123
+ print(f" Donor background : {self.donor_background:.4f}")
124
+ print(f" Donor model (G,S) : "
125
+ f"({self.donor_real_model:.4f}, {self.donor_imag_model:.4f})")
126
+ if self.acceptor_real_model is not None:
127
+ print(f" Acceptor model (G,S): "
128
+ f"({self.acceptor_real_model:.4f}, {self.acceptor_imag_model:.4f})")
129
+ print(f" Donor bleedthrough : {self.donor_bleedthrough:.4f}")
130
+ print(f" Acceptor bleedthrough: {self.acceptor_bleedthrough:.4f}")
131
+ print(f" Acceptor background : {self.acceptor_background:.4f}")
132
+ print(f" Residual : {self.residual:.6g}")
133
+ status = '\u2713 converged' if self.converged else '\u2717 did not converge'
134
+ print(f" Optimizer : {status} \u2014 {self.message}")
135
+
136
+ def _require_phasorpy_fret_api():
137
+ try:
138
+ from phasorpy.lifetime import ( # noqa: F401
139
+ phasor_from_fret_donor,
140
+ phasor_from_fret_acceptor,
141
+ )
142
+ except ImportError as exc:
143
+ raise ImportError(
144
+ 'FRET analysis requires phasorpy >= 0.9 with '
145
+ 'phasor_from_fret_donor and phasor_from_fret_acceptor. '
146
+ "Install with: pip install 'phasorpy>=0.9'"
147
+ ) from exc
148
+
149
+ import inspect
150
+ from phasorpy.lifetime import phasor_from_fret_donor, phasor_from_fret_acceptor
151
+ for fn, required in [
152
+ (phasor_from_fret_donor, {'frequency', 'donor_lifetime',
153
+ 'fret_efficiency', 'donor_fretting'}),
154
+ (phasor_from_fret_acceptor, {'frequency', 'donor_lifetime',
155
+ 'acceptor_lifetime', 'fret_efficiency'}),
156
+ ]:
157
+ missing = required - set(inspect.signature(fn).parameters)
158
+ if missing:
159
+ raise ImportError(
160
+ f"phasorpy.{fn.__name__} is missing expected parameters: "
161
+ f"{missing}. Please update phasorpy."
162
+ )
163
+
164
+
165
+ def _single_lifetime_phasor(frequency, lifetime):
166
+ from phasorpy.lifetime import phasor_from_lifetime
167
+ real, imag = phasor_from_lifetime(frequency, lifetime)
168
+ return float(np.squeeze(real)), float(np.squeeze(imag))
169
+
170
+
171
+ def _fret_donor_phasor(
172
+ frequency,
173
+ donor_lifetime,
174
+ *,
175
+ fret_efficiency=0.0,
176
+ donor_fretting=1.0,
177
+ donor_background=0.0,
178
+ background_real=0.0,
179
+ background_imag=0.0,
180
+ ):
181
+ from phasorpy.lifetime import phasor_from_fret_donor
182
+ real, imag = phasor_from_fret_donor(
183
+ frequency, donor_lifetime,
184
+ fret_efficiency=fret_efficiency,
185
+ donor_fretting=donor_fretting,
186
+ donor_background=donor_background,
187
+ background_real=background_real,
188
+ background_imag=background_imag,
189
+ unit_conversion=1e-3,
190
+ )
191
+ return float(np.squeeze(real)), float(np.squeeze(imag))
192
+
193
+
194
+ def _fret_acceptor_phasor(
195
+ frequency,
196
+ donor_lifetime,
197
+ acceptor_lifetime,
198
+ *,
199
+ fret_efficiency=0.0,
200
+ donor_fretting=1.0,
201
+ donor_bleedthrough=0.0,
202
+ acceptor_bleedthrough=0.0,
203
+ acceptor_background=0.0,
204
+ background_real=0.0,
205
+ background_imag=0.0,
206
+ ):
207
+ from phasorpy.lifetime import phasor_from_fret_acceptor
208
+ real, imag = phasor_from_fret_acceptor(
209
+ frequency, donor_lifetime, acceptor_lifetime,
210
+ fret_efficiency=fret_efficiency,
211
+ donor_fretting=donor_fretting,
212
+ donor_bleedthrough=donor_bleedthrough,
213
+ acceptor_bleedthrough=acceptor_bleedthrough,
214
+ acceptor_background=acceptor_background,
215
+ background_real=background_real,
216
+ background_imag=background_imag,
217
+ unit_conversion=1e-3,
218
+ )
219
+ return float(np.squeeze(real)), float(np.squeeze(imag))
220
+
221
+
222
+ def predict_fret_trajectory(
223
+ frequency,
224
+ donor_lifetime,
225
+ *,
226
+ acceptor_lifetime=None,
227
+ donor_fretting=1.0,
228
+ donor_background=0.0,
229
+ background_real=0.0,
230
+ background_imag=0.0,
231
+ donor_bleedthrough=0.0,
232
+ acceptor_bleedthrough=0.0,
233
+ acceptor_background=0.0,
234
+ n_points=100,
235
+ ):
236
+ _require_phasorpy_fret_api()
237
+ efficiencies = np.linspace(0.0, 1.0, n_points)
238
+
239
+ from phasorpy.lifetime import phasor_from_fret_donor
240
+ donor_g, donor_s = phasor_from_fret_donor(
241
+ frequency, donor_lifetime,
242
+ fret_efficiency=efficiencies,
243
+ donor_fretting=donor_fretting,
244
+ donor_background=donor_background,
245
+ background_real=background_real,
246
+ background_imag=background_imag,
247
+ unit_conversion=1e-3,
248
+ )
249
+
250
+ acceptor_g = acceptor_s = None
251
+ if acceptor_lifetime is not None:
252
+ from phasorpy.lifetime import phasor_from_fret_acceptor
253
+ acceptor_g, acceptor_s = phasor_from_fret_acceptor(
254
+ frequency, donor_lifetime, acceptor_lifetime,
255
+ fret_efficiency=efficiencies,
256
+ donor_fretting=donor_fretting,
257
+ donor_bleedthrough=donor_bleedthrough,
258
+ acceptor_bleedthrough=acceptor_bleedthrough,
259
+ acceptor_background=acceptor_background,
260
+ background_real=background_real,
261
+ background_imag=background_imag,
262
+ unit_conversion=1e-3,
263
+ )
264
+
265
+ return dict(
266
+ efficiency=efficiencies,
267
+ donor_g=np.asarray(donor_g),
268
+ donor_s=np.asarray(donor_s),
269
+ acceptor_g=np.asarray(acceptor_g) if acceptor_g is not None else None,
270
+ acceptor_s=np.asarray(acceptor_s) if acceptor_s is not None else None,
271
+ )
272
+
273
+
274
+ def fit_donor_fret(
275
+ donor,
276
+ params,
277
+ bounds=None,
278
+ *,
279
+ weight_by_photons=True,
280
+ ):
281
+ _require_phasorpy_fret_api()
282
+ from phasorpy.lifetime import phasor_from_fret_donor
283
+ from scipy.optimize import least_squares
284
+
285
+ if bounds is None:
286
+ bounds = FRETBounds()
287
+
288
+ mask = donor.valid_mask
289
+ g_vals = donor.real_cal[mask]
290
+ s_vals = donor.imag_cal[mask]
291
+ if weight_by_photons:
292
+ w = donor.mean[mask]
293
+ w = w / w.sum()
294
+ else:
295
+ w = np.ones(g_vals.size) / g_vals.size
296
+
297
+ g_obs = float(np.dot(w, g_vals))
298
+ s_obs = float(np.dot(w, s_vals))
299
+
300
+ freq = donor.frequency
301
+ tau_d = params.donor_lifetime
302
+ bg_real = params.background_real
303
+ bg_imag = params.background_imag
304
+
305
+ def _residuals(x: np.ndarray) -> np.ndarray:
306
+ E, f, bg = x
307
+ g_m, s_m = phasor_from_fret_donor(
308
+ freq, tau_d,
309
+ fret_efficiency=E,
310
+ donor_fretting=f,
311
+ donor_background=bg,
312
+ background_real=bg_real,
313
+ background_imag=bg_imag,
314
+ unit_conversion=1e-3,
315
+ )
316
+ return np.array([float(g_m) - g_obs, float(s_m) - s_obs])
317
+
318
+ x0 = [params.fret_efficiency, params.donor_fretting, params.donor_background]
319
+ scipy_bounds = bounds.donor_only_scipy()
320
+ result = least_squares(
321
+ _residuals, x0,
322
+ bounds=(scipy_bounds['lb'], scipy_bounds['ub']),
323
+ method='trf',
324
+ )
325
+
326
+ E_fit, f_fit, bg_fit = result.x
327
+ g_model, s_model = phasor_from_fret_donor(
328
+ freq, tau_d,
329
+ fret_efficiency=E_fit,
330
+ donor_fretting=f_fit,
331
+ donor_background=bg_fit,
332
+ background_real=bg_real,
333
+ background_imag=bg_imag,
334
+ unit_conversion=1e-3,
335
+ )
336
+
337
+ return FRETResult(
338
+ fret_efficiency=float(E_fit),
339
+ donor_fretting=float(f_fit),
340
+ donor_background=float(bg_fit),
341
+ donor_real_model=float(g_model),
342
+ donor_imag_model=float(s_model),
343
+ residual=float(result.cost),
344
+ converged=bool(result.success),
345
+ message=result.message,
346
+ )
347
+
348
+
349
+ #Joint Donor+Acceptor Solver
350
+
351
+
352
+ def fit_joint_fret(
353
+ donor,
354
+ acceptor,
355
+ params,
356
+ bounds=None,
357
+ *,
358
+ weight_by_photons=True,
359
+ ):
360
+ if params.acceptor_lifetime is None:
361
+ raise ValueError(
362
+ 'fit_joint_fret requires params.acceptor_lifetime to be set.'
363
+ )
364
+ if donor.real_cal.shape != acceptor.real_cal.shape:
365
+ raise ValueError(
366
+ f"donor and acceptor arrays must have the same shape; "
367
+ f"got {donor.real_cal.shape} vs {acceptor.real_cal.shape}."
368
+ )
369
+ if donor.frequency != acceptor.frequency:
370
+ raise ValueError(
371
+ f"donor and acceptor must share the same frequency; "
372
+ f"got {donor.frequency} vs {acceptor.frequency} MHz."
373
+ )
374
+
375
+ _require_phasorpy_fret_api()
376
+ from phasorpy.lifetime import phasor_from_fret_donor, phasor_from_fret_acceptor
377
+ from scipy.optimize import least_squares
378
+
379
+ if bounds is None:
380
+ bounds = FRETBounds()
381
+
382
+ def _centroid(ch: FRETChannelData) -> tuple[float, float]:
383
+ mask = ch.valid_mask
384
+ g_vals = ch.real_cal[mask]
385
+ s_vals = ch.imag_cal[mask]
386
+ if weight_by_photons:
387
+ w = ch.mean[mask]
388
+ w = w / w.sum()
389
+ else:
390
+ w = np.ones(g_vals.size) / g_vals.size
391
+ return float(np.dot(w, g_vals)), float(np.dot(w, s_vals))
392
+
393
+ dg_obs, ds_obs = _centroid(donor)
394
+ ag_obs, as_obs = _centroid(acceptor)
395
+
396
+ freq = donor.frequency
397
+ tau_d = params.donor_lifetime
398
+ tau_a = params.acceptor_lifetime
399
+ bg_real = params.background_real
400
+ bg_imag = params.background_imag
401
+
402
+ def _residuals(x: np.ndarray) -> np.ndarray:
403
+ E, f, d_bg, d_bt, a_bt, a_bg = x
404
+ dg_m, ds_m = phasor_from_fret_donor(
405
+ freq, tau_d,
406
+ fret_efficiency=E,
407
+ donor_fretting=f,
408
+ donor_background=d_bg,
409
+ background_real=bg_real,
410
+ background_imag=bg_imag,
411
+ unit_conversion=1e-3,
412
+ )
413
+ ag_m, as_m = phasor_from_fret_acceptor(
414
+ freq, tau_d, tau_a,
415
+ fret_efficiency=E,
416
+ donor_fretting=f,
417
+ donor_bleedthrough=d_bt,
418
+ acceptor_bleedthrough=a_bt,
419
+ acceptor_background=a_bg,
420
+ background_real=bg_real,
421
+ background_imag=bg_imag,
422
+ unit_conversion=1e-3,
423
+ )
424
+ return np.array([
425
+ float(dg_m) - dg_obs,
426
+ float(ds_m) - ds_obs,
427
+ float(ag_m) - ag_obs,
428
+ float(as_m) - as_obs,
429
+ ])
430
+
431
+ x0 = [
432
+ params.fret_efficiency,
433
+ params.donor_fretting,
434
+ params.donor_background,
435
+ params.donor_bleedthrough,
436
+ params.acceptor_bleedthrough,
437
+ params.acceptor_background,
438
+ ]
439
+ scipy_bounds = bounds.joint_scipy()
440
+ result = least_squares(
441
+ _residuals, x0,
442
+ bounds=(scipy_bounds['lb'], scipy_bounds['ub']),
443
+ method='trf',
444
+ )
445
+
446
+ E_fit, f_fit, d_bg_fit, d_bt_fit, a_bt_fit, a_bg_fit = result.x
447
+
448
+ dg_model, ds_model = phasor_from_fret_donor(
449
+ freq, tau_d,
450
+ fret_efficiency=E_fit,
451
+ donor_fretting=f_fit,
452
+ donor_background=d_bg_fit,
453
+ background_real=bg_real,
454
+ background_imag=bg_imag,
455
+ unit_conversion=1e-3,
456
+ )
457
+ ag_model, as_model = phasor_from_fret_acceptor(
458
+ freq, tau_d, tau_a,
459
+ fret_efficiency=E_fit,
460
+ donor_fretting=f_fit,
461
+ donor_bleedthrough=d_bt_fit,
462
+ acceptor_bleedthrough=a_bt_fit,
463
+ acceptor_background=a_bg_fit,
464
+ background_real=bg_real,
465
+ background_imag=bg_imag,
466
+ unit_conversion=1e-3,
467
+ )
468
+
469
+ return FRETResult(
470
+ fret_efficiency=float(E_fit),
471
+ donor_fretting=float(f_fit),
472
+ donor_background=float(d_bg_fit),
473
+ donor_real_model=float(dg_model),
474
+ donor_imag_model=float(ds_model),
475
+ residual=float(result.cost),
476
+ donor_bleedthrough=float(d_bt_fit),
477
+ acceptor_bleedthrough=float(a_bt_fit),
478
+ acceptor_background=float(a_bg_fit),
479
+ acceptor_real_model=float(ag_model),
480
+ acceptor_imag_model=float(as_model),
481
+ converged=bool(result.success),
482
+ message=result.message,
483
+ )
484
+
485
+
486
+ #Pixelwise Maps
487
+
488
+
489
+ def map_fret_efficiency(
490
+ donor: FRETChannelData,
491
+ params: FRETModelParameters,
492
+ bounds: Optional[FRETBounds] = None,
493
+ *,
494
+ acceptor: Optional[FRETChannelData] = None,
495
+ weight_by_photons: bool = True,
496
+ ) -> dict:
497
+ if bounds is None:
498
+ bounds = FRETBounds()
499
+
500
+ Y, X = donor.real_cal.shape
501
+ efficiency = np.full((Y, X), np.nan)
502
+ fretting = np.full((Y, X), np.nan)
503
+ residual = np.full((Y, X), np.nan)
504
+ converged = np.zeros((Y, X), dtype=bool)
505
+
506
+ mask = donor.valid_mask
507
+ if acceptor is not None:
508
+ mask = mask & acceptor.valid_mask
509
+
510
+ ys, xs = np.where(mask)
511
+
512
+ for y, x in zip(ys, xs):
513
+ px_donor = FRETChannelData(
514
+ real_cal=donor.real_cal[y:y+1, x:x+1],
515
+ imag_cal=donor.imag_cal[y:y+1, x:x+1],
516
+ mean=donor.mean[y:y+1, x:x+1],
517
+ frequency=donor.frequency,
518
+ min_photons=donor.min_photons,
519
+ )
520
+ if acceptor is not None:
521
+ px_acceptor = FRETChannelData(
522
+ real_cal=acceptor.real_cal[y:y+1, x:x+1],
523
+ imag_cal=acceptor.imag_cal[y:y+1, x:x+1],
524
+ mean=acceptor.mean[y:y+1, x:x+1],
525
+ frequency=acceptor.frequency,
526
+ min_photons=acceptor.min_photons,
527
+ )
528
+ r = fit_joint_fret(
529
+ px_donor, px_acceptor, params, bounds,
530
+ weight_by_photons=weight_by_photons,
531
+ )
532
+ else:
533
+ r = fit_donor_fret(
534
+ px_donor, params, bounds,
535
+ weight_by_photons=weight_by_photons,
536
+ )
537
+ efficiency[y, x] = r.fret_efficiency
538
+ fretting[y, x] = r.donor_fretting
539
+ residual[y, x] = r.residual
540
+ converged[y, x] = r.converged
541
+
542
+ return dict(
543
+ efficiency=efficiency,
544
+ fretting=fretting,
545
+ residual=residual,
546
+ converged=converged,
547
+ )
548
+
549
+
550
+ #Visualization
551
+
552
+
553
+ def plot_fret_trajectory(
554
+ frequency: float,
555
+ donor_lifetime: float,
556
+ *,
557
+ acceptor_lifetime: Optional[float] = None,
558
+ donor_fretting: float = 1.0,
559
+ n_points: int = 100,
560
+ ax=None,
561
+ donor_kw: Optional[dict] = None,
562
+ acceptor_kw: Optional[dict] = None,
563
+ ) -> tuple:
564
+ import matplotlib.pyplot as plt
565
+
566
+ traj = predict_fret_trajectory(
567
+ frequency, donor_lifetime,
568
+ acceptor_lifetime=acceptor_lifetime,
569
+ donor_fretting=donor_fretting,
570
+ n_points=n_points,
571
+ )
572
+
573
+ if ax is None:
574
+ _, ax = plt.subplots()
575
+
576
+ _donor_kw = {'color': 'steelblue', 'label': 'donor trajectory'}
577
+ if donor_kw:
578
+ _donor_kw.update(donor_kw)
579
+
580
+ lines = [ax.plot(traj['donor_g'], traj['donor_s'], **_donor_kw)[0]]
581
+
582
+ if traj['acceptor_g'] is not None:
583
+ _acceptor_kw = {'color': 'tomato', 'label': 'acceptor trajectory'}
584
+ if acceptor_kw:
585
+ _acceptor_kw.update(acceptor_kw)
586
+ lines.append(
587
+ ax.plot(traj['acceptor_g'], traj['acceptor_s'], **_acceptor_kw)[0]
588
+ )
589
+
590
+ return ax, lines
591
+
592
+
593
+ def plot_fret_fit(
594
+ donor: FRETChannelData,
595
+ result: FRETResult,
596
+ frequency: float,
597
+ donor_lifetime: float,
598
+ *,
599
+ acceptor: Optional[FRETChannelData] = None,
600
+ acceptor_lifetime: Optional[float] = None,
601
+ n_trajectory: int = 100,
602
+ ax=None,
603
+ scatter_kw: Optional[dict] = None,
604
+ trajectory_kw: Optional[dict] = None,
605
+ ) -> tuple:
606
+ import matplotlib.pyplot as plt
607
+
608
+ if ax is None:
609
+ _, ax = plt.subplots()
610
+
611
+ _scatter_kw = {'marker': '.', 'alpha': 0.3, 'linestyle': 'none', 'color': 'steelblue'}
612
+ if scatter_kw:
613
+ _scatter_kw.update(scatter_kw)
614
+
615
+ artists: dict = {}
616
+
617
+ artists['donor_scatter'] = ax.plot(
618
+ donor.valid_g, donor.valid_s, **_scatter_kw
619
+ )[0]
620
+
621
+ if acceptor is not None:
622
+ _acc_scatter_kw = dict(_scatter_kw)
623
+ _acc_scatter_kw['color'] = 'tomato'
624
+ _acc_scatter_kw.pop('label', None)
625
+ artists['acceptor_scatter'] = ax.plot(
626
+ acceptor.valid_g, acceptor.valid_s, **_acc_scatter_kw
627
+ )[0]
628
+
629
+ _traj_kw = trajectory_kw or {}
630
+ plot_fret_trajectory(
631
+ frequency, donor_lifetime,
632
+ acceptor_lifetime=acceptor_lifetime,
633
+ donor_fretting=result.donor_fretting,
634
+ n_points=n_trajectory,
635
+ ax=ax,
636
+ **_traj_kw,
637
+ )
638
+
639
+ artists['donor_model'] = ax.plot(
640
+ result.donor_real_model, result.donor_imag_model,
641
+ marker='*', markersize=12, color='steelblue',
642
+ linestyle='none', label='donor fit',
643
+ zorder=5,
644
+ )[0]
645
+
646
+ if result.acceptor_real_model is not None:
647
+ artists['acceptor_model'] = ax.plot(
648
+ result.acceptor_real_model, result.acceptor_imag_model,
649
+ marker='*', markersize=12, color='tomato',
650
+ linestyle='none', label='acceptor fit',
651
+ zorder=5,
652
+ )[0]
653
+
654
+ return ax, artists