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.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- 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
|