fucciphase 0.0.1__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.
- fucciphase/__init__.py +12 -0
- fucciphase/fucci_phase.py +178 -0
- fucciphase/io.py +67 -0
- fucciphase/napari/__init__.py +5 -0
- fucciphase/napari/tracks_to_napari.py +117 -0
- fucciphase/phase.py +501 -0
- fucciphase/plot.py +548 -0
- fucciphase/py.typed +5 -0
- fucciphase/sensor.py +454 -0
- fucciphase/tracking_utilities.py +81 -0
- fucciphase/utils/__init__.py +35 -0
- fucciphase/utils/checks.py +16 -0
- fucciphase/utils/dtw.py +59 -0
- fucciphase/utils/normalize.py +202 -0
- fucciphase/utils/phase_fit.py +47 -0
- fucciphase/utils/simulator.py +85 -0
- fucciphase/utils/track_postprocessing.py +454 -0
- fucciphase/utils/trackmate.py +295 -0
- fucciphase-0.0.1.dist-info/METADATA +137 -0
- fucciphase-0.0.1.dist-info/RECORD +22 -0
- fucciphase-0.0.1.dist-info/WHEEL +4 -0
- fucciphase-0.0.1.dist-info/licenses/LICENSE +29 -0
fucciphase/sensor.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from scipy import optimize
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def logistic(
|
|
10
|
+
x: Union[float, np.ndarray], center: float, sigma: float, sign: float = 1.0
|
|
11
|
+
) -> Union[float, np.ndarray]:
|
|
12
|
+
"""Logistic function."""
|
|
13
|
+
tiny = 1.0e-15
|
|
14
|
+
arg = sign * (x - center) / max(tiny, sigma)
|
|
15
|
+
return 1.0 / (1.0 + np.exp(arg))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def accumulation_function(
|
|
19
|
+
x: Union[float, np.ndarray],
|
|
20
|
+
center: float,
|
|
21
|
+
sigma: float,
|
|
22
|
+
offset_intensity: float = 0,
|
|
23
|
+
) -> Union[float, np.ndarray]:
|
|
24
|
+
"""Function to describe accumulation of sensor."""
|
|
25
|
+
return 1.0 - logistic(x, center, sigma) - offset_intensity
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def degradation_function(
|
|
29
|
+
x: Union[float, np.ndarray],
|
|
30
|
+
center: float,
|
|
31
|
+
sigma: float,
|
|
32
|
+
offset_intensity: float = 0,
|
|
33
|
+
) -> Union[float, np.ndarray]:
|
|
34
|
+
"""Function to describe degradation of sensor."""
|
|
35
|
+
return 1.0 - logistic(x, center, sigma, sign=-1.0) - offset_intensity
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FUCCISensor(ABC):
|
|
39
|
+
"""Base class for a FUCCI sensor."""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
phase_percentages: List[float],
|
|
45
|
+
center: List[float],
|
|
46
|
+
sigma: List[float],
|
|
47
|
+
) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def fluorophores(self) -> int:
|
|
53
|
+
"""Number of fluorophores."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def phases(self) -> List[str]:
|
|
59
|
+
"""Function to hard-code the supported phases of a sensor."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def phase_percentages(self) -> List[float]:
|
|
64
|
+
"""Percentage of individual phases."""
|
|
65
|
+
return self._phase_percentages
|
|
66
|
+
|
|
67
|
+
@phase_percentages.setter
|
|
68
|
+
def phase_percentages(self, values: List[float]) -> None:
|
|
69
|
+
if len(values) != len(self.phases):
|
|
70
|
+
raise ValueError("Pass percentage for each phase.")
|
|
71
|
+
|
|
72
|
+
# check that the sum of phase borders is less than 100
|
|
73
|
+
if not np.isclose(sum(values), 100.0, atol=0.2):
|
|
74
|
+
raise ValueError("Phase percentages do not sum to 100.")
|
|
75
|
+
|
|
76
|
+
self._phase_percentages = values
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_phase(self, phase_markers: Union[List[bool], "pd.Series[bool]"]) -> str:
|
|
80
|
+
"""Get the discrete phase based on phase markers.
|
|
81
|
+
|
|
82
|
+
Notes
|
|
83
|
+
-----
|
|
84
|
+
Discrete phase refers to, for example, G1 or S phase.
|
|
85
|
+
The phase_markers must match the number of used fluorophores.
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def get_estimated_cycle_percentage(
|
|
91
|
+
self, phase: str, intensities: List[float]
|
|
92
|
+
) -> float:
|
|
93
|
+
"""Estimate percentage based on sensor intensities."""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def set_accumulation_and_degradation_parameters(
|
|
97
|
+
self, center: List[float], sigma: List[float]
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Pass list of functions for logistic functions.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
center: List[float]
|
|
104
|
+
List of center values for accumulation and degradation curves.
|
|
105
|
+
sigma: List[float]
|
|
106
|
+
List of width values for accumulation and degradation curves.
|
|
107
|
+
"""
|
|
108
|
+
if len(center) != 2 * self.fluorophores:
|
|
109
|
+
raise ValueError("Need to supply 2 center values per fluorophore.")
|
|
110
|
+
if len(sigma) != 2 * self.fluorophores:
|
|
111
|
+
raise ValueError("Need to supply 2 width values per fluorophore.")
|
|
112
|
+
self._center_values = center
|
|
113
|
+
self._sigma_values = sigma
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def get_expected_intensities(
|
|
117
|
+
self, percentage: Union[float, np.ndarray]
|
|
118
|
+
) -> List[Union[float, np.ndarray]]:
|
|
119
|
+
"""Return value of calibrated curves."""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class FUCCISASensor(FUCCISensor):
|
|
124
|
+
"""FUCCI(SA) sensor."""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self, phase_percentages: List[float], center: List[float], sigma: List[float]
|
|
128
|
+
) -> None:
|
|
129
|
+
self.phase_percentages = phase_percentages
|
|
130
|
+
self.set_accumulation_and_degradation_parameters(center, sigma)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def fluorophores(self) -> int:
|
|
134
|
+
"""Number of fluorophores."""
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def phases(self) -> List[str]:
|
|
139
|
+
"""Function to hard-code the supported phases of a sensor."""
|
|
140
|
+
return ["G1", "G1/S", "S/G2/M"]
|
|
141
|
+
|
|
142
|
+
def get_phase(self, phase_markers: Union[List[bool], "pd.Series[bool]"]) -> str:
|
|
143
|
+
"""Return the discrete phase based channel ON / OFF data for the
|
|
144
|
+
FUCCI(SA) sensor.
|
|
145
|
+
"""
|
|
146
|
+
if not len(phase_markers) == 2:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"The markers for G1 and S/G2/M channel haveto be provided!"
|
|
149
|
+
)
|
|
150
|
+
g1_on = phase_markers[0]
|
|
151
|
+
s_g2_on = phase_markers[1]
|
|
152
|
+
# low intensity at the very beginning of cycle
|
|
153
|
+
if not g1_on and not s_g2_on:
|
|
154
|
+
return "G1"
|
|
155
|
+
elif g1_on and not s_g2_on:
|
|
156
|
+
return "G1"
|
|
157
|
+
elif not g1_on and s_g2_on:
|
|
158
|
+
return "S/G2/M"
|
|
159
|
+
# G1/S transition phase
|
|
160
|
+
else:
|
|
161
|
+
return "G1/S"
|
|
162
|
+
|
|
163
|
+
def _find_g1_percentage(self, intensity: float) -> float:
|
|
164
|
+
"""Find percentage in G1 phase.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
intensity: float
|
|
169
|
+
Intensity of cyan / green channel
|
|
170
|
+
|
|
171
|
+
Notes
|
|
172
|
+
-----
|
|
173
|
+
Checks the accumulation function of the first colour.
|
|
174
|
+
First colour means the colour indicating G1 phase.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
g1_perc = self.phase_percentages[0]
|
|
178
|
+
# intensity below expected minimal intensity
|
|
179
|
+
if intensity < accumulation_function(
|
|
180
|
+
0, self._center_values[0], self._sigma_values[0]
|
|
181
|
+
):
|
|
182
|
+
return 0.0
|
|
183
|
+
elif intensity > accumulation_function(
|
|
184
|
+
g1_perc, self._center_values[0], self._sigma_values[0]
|
|
185
|
+
):
|
|
186
|
+
return g1_perc
|
|
187
|
+
return float(
|
|
188
|
+
optimize.bisect(
|
|
189
|
+
accumulation_function,
|
|
190
|
+
0.0,
|
|
191
|
+
g1_perc,
|
|
192
|
+
args=(self._center_values[0], self._sigma_values[0], intensity),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _find_g1s_percentage(self, intensity: float) -> float:
|
|
197
|
+
"""Find percentage in G1/S phase.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
intensity: float
|
|
202
|
+
Intensity of cyan / green channel
|
|
203
|
+
|
|
204
|
+
Notes
|
|
205
|
+
-----
|
|
206
|
+
Checks the degradation function of the first colour.
|
|
207
|
+
First colour means the colour indicating G1 phase.
|
|
208
|
+
"""
|
|
209
|
+
g1_perc = self.phase_percentages[0]
|
|
210
|
+
g1s_perc = self.phase_percentages[1]
|
|
211
|
+
if intensity > degradation_function(
|
|
212
|
+
g1_perc, self._center_values[1], self._sigma_values[1]
|
|
213
|
+
):
|
|
214
|
+
return g1_perc
|
|
215
|
+
elif intensity < degradation_function(
|
|
216
|
+
g1_perc + g1s_perc, self._center_values[1], self._sigma_values[1]
|
|
217
|
+
):
|
|
218
|
+
return g1_perc + g1s_perc
|
|
219
|
+
return float(
|
|
220
|
+
optimize.bisect(
|
|
221
|
+
degradation_function,
|
|
222
|
+
g1_perc,
|
|
223
|
+
g1_perc + g1s_perc,
|
|
224
|
+
args=(self._center_values[1], self._sigma_values[1], intensity),
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _find_sg2m_percentage(self, intensity: float) -> float:
|
|
229
|
+
"""Find percentage in S/G2/M phase.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
intensity: float
|
|
234
|
+
Intensity of second colour (magenta / red)
|
|
235
|
+
|
|
236
|
+
Notes
|
|
237
|
+
-----
|
|
238
|
+
Checks the accumulation function of the second colour.
|
|
239
|
+
Second colour means the colour indicating S/G2/M phase.
|
|
240
|
+
"""
|
|
241
|
+
g1_perc = self.phase_percentages[0]
|
|
242
|
+
g1s_perc = self.phase_percentages[1]
|
|
243
|
+
|
|
244
|
+
# check if intensity is below smallest expected intensity
|
|
245
|
+
if intensity < accumulation_function(
|
|
246
|
+
g1_perc + g1s_perc, self._center_values[2], self._sigma_values[2]
|
|
247
|
+
):
|
|
248
|
+
return g1_perc + g1s_perc
|
|
249
|
+
# if intensity is very small, it is M phase
|
|
250
|
+
if intensity < 0.3 * accumulation_function(
|
|
251
|
+
100.0, self._center_values[2], self._sigma_values[2]
|
|
252
|
+
):
|
|
253
|
+
return 100.0
|
|
254
|
+
# return middle of interval if values are close
|
|
255
|
+
g1s_level = accumulation_function(
|
|
256
|
+
g1_perc + g1s_perc, self._center_values[2], self._sigma_values[2]
|
|
257
|
+
)
|
|
258
|
+
final_level = accumulation_function(
|
|
259
|
+
100.0, self._center_values[2], self._sigma_values[2]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if np.isclose(g1s_level, final_level):
|
|
263
|
+
return g1s_perc + 0.5 * (100.0 - g1s_perc - g1_perc)
|
|
264
|
+
try:
|
|
265
|
+
if np.greater_equal(intensity, final_level):
|
|
266
|
+
intensity = intensity - 2.0 * (intensity - final_level) # type: ignore[assignment]
|
|
267
|
+
return float(
|
|
268
|
+
optimize.bisect(
|
|
269
|
+
accumulation_function,
|
|
270
|
+
g1_perc + g1s_perc,
|
|
271
|
+
100.0,
|
|
272
|
+
args=(self._center_values[2], self._sigma_values[2], intensity),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
except ValueError:
|
|
276
|
+
print(
|
|
277
|
+
"WARNING: could not infer percentage in SG2M phase, using average phase"
|
|
278
|
+
)
|
|
279
|
+
return g1s_perc + 0.5 * (100.0 - g1s_perc - g1_perc)
|
|
280
|
+
|
|
281
|
+
def get_estimated_cycle_percentage(
|
|
282
|
+
self, phase: str, intensities: List[float]
|
|
283
|
+
) -> float:
|
|
284
|
+
"""Estimate a cell cycle percentage based on intensities.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
phase: str
|
|
289
|
+
Name of phase
|
|
290
|
+
intensities: List[float]
|
|
291
|
+
List of channel intensities for all fluorophores
|
|
292
|
+
"""
|
|
293
|
+
if phase not in self.phases:
|
|
294
|
+
raise ValueError(f"Phase {phase} is not defined for this sensor.")
|
|
295
|
+
if phase == "G1":
|
|
296
|
+
return self._find_g1_percentage(intensities[0])
|
|
297
|
+
if phase == "G1/S":
|
|
298
|
+
return self._find_g1s_percentage(intensities[0])
|
|
299
|
+
else:
|
|
300
|
+
return self._find_sg2m_percentage(intensities[1])
|
|
301
|
+
|
|
302
|
+
def get_expected_intensities(
|
|
303
|
+
self, percentage: Union[float, np.ndarray]
|
|
304
|
+
) -> List[Union[float, np.ndarray]]:
|
|
305
|
+
"""Return value of calibrated curves."""
|
|
306
|
+
g1_acc = accumulation_function(
|
|
307
|
+
percentage, self._center_values[0], self._sigma_values[0]
|
|
308
|
+
)
|
|
309
|
+
g1_deg = degradation_function(
|
|
310
|
+
percentage, self._center_values[1], self._sigma_values[1]
|
|
311
|
+
)
|
|
312
|
+
s_g2_m_acc = accumulation_function(
|
|
313
|
+
percentage, self._center_values[2], self._sigma_values[2]
|
|
314
|
+
)
|
|
315
|
+
s_g2_m_deg = degradation_function(
|
|
316
|
+
percentage, self._center_values[3], self._sigma_values[3]
|
|
317
|
+
)
|
|
318
|
+
return [g1_acc + g1_deg - 1.0, s_g2_m_acc + s_g2_m_deg - 1.0]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def get_fuccisa_default_sensor() -> FUCCISASensor:
|
|
322
|
+
"""Return sensor with default values.
|
|
323
|
+
|
|
324
|
+
Should only be used if the cell cycle percentage is not of interest.
|
|
325
|
+
"""
|
|
326
|
+
return FUCCISASensor(
|
|
327
|
+
phase_percentages=[25, 25, 50], center=[0, 0, 0, 0], sigma=[0, 0, 0, 0]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class PIPFUCCISensor(FUCCISensor):
|
|
332
|
+
"""PIP-FUCCI sensor."""
|
|
333
|
+
|
|
334
|
+
def __init__(
|
|
335
|
+
self, phase_percentages: List[float], center: List[float], sigma: List[float]
|
|
336
|
+
) -> None:
|
|
337
|
+
self.phase_percentages = phase_percentages
|
|
338
|
+
self.set_accumulation_and_degradation_parameters(center, sigma)
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def fluorophores(self) -> int:
|
|
342
|
+
"""Number of fluorophores."""
|
|
343
|
+
return 2
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def phases(self) -> List[str]:
|
|
347
|
+
"""Function to hard-code the supported phases of a sensor."""
|
|
348
|
+
return ["G1", "S", "G2/M"]
|
|
349
|
+
|
|
350
|
+
def get_phase(self, phase_markers: Union[List[bool], "pd.Series[bool]"]) -> str:
|
|
351
|
+
"""Return the discrete phase based channel ON / OFF data for the
|
|
352
|
+
FUCCI(SA) sensor.
|
|
353
|
+
"""
|
|
354
|
+
if not len(phase_markers) == 2:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"The markers for G1 and S/G2/M channel haveto be provided!"
|
|
357
|
+
)
|
|
358
|
+
g1_on = phase_markers[0]
|
|
359
|
+
s_on = phase_markers[1]
|
|
360
|
+
# low intensity at the very beginning of cycle
|
|
361
|
+
if not g1_on and not s_on:
|
|
362
|
+
return "S"
|
|
363
|
+
elif g1_on and not s_on:
|
|
364
|
+
return "G1"
|
|
365
|
+
elif not g1_on and s_on:
|
|
366
|
+
return "S"
|
|
367
|
+
else:
|
|
368
|
+
return "G2/M"
|
|
369
|
+
|
|
370
|
+
def _find_g1_percentage(self, intensity: float) -> float:
|
|
371
|
+
"""Find percentage in G1 phase.
|
|
372
|
+
|
|
373
|
+
Parameters
|
|
374
|
+
----------
|
|
375
|
+
intensity: float
|
|
376
|
+
Intensity of cyan / green channel
|
|
377
|
+
|
|
378
|
+
Notes
|
|
379
|
+
-----
|
|
380
|
+
Checks the accumulation function of the first colour.
|
|
381
|
+
First colour means the colour indicating G1 phase.
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
raise NotImplementedError("Percentage estimate not yet implemented!")
|
|
385
|
+
|
|
386
|
+
def _find_s_percentage(self, intensity: float) -> float:
|
|
387
|
+
"""Find percentage in S phase.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
intensity: float
|
|
392
|
+
Intensity of cyan / green channel
|
|
393
|
+
|
|
394
|
+
Notes
|
|
395
|
+
-----
|
|
396
|
+
Checks the degradation function of the first colour.
|
|
397
|
+
First colour means the colour indicating G1 phase.
|
|
398
|
+
"""
|
|
399
|
+
raise NotImplementedError("Percentage estimate not yet implemented!")
|
|
400
|
+
|
|
401
|
+
def _find_g2m_percentage(self, intensity: float) -> float:
|
|
402
|
+
"""Find percentage in G2/M phase.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
intensity: float
|
|
407
|
+
Intensity of second colour (magenta / red)
|
|
408
|
+
|
|
409
|
+
Notes
|
|
410
|
+
-----
|
|
411
|
+
Checks the accumulation function of the second colour.
|
|
412
|
+
Second colour means the colour indicating S/G2/M phase.
|
|
413
|
+
"""
|
|
414
|
+
raise NotImplementedError("Percentage estimate not yet implemented!")
|
|
415
|
+
|
|
416
|
+
def get_estimated_cycle_percentage(
|
|
417
|
+
self, phase: str, intensities: List[float]
|
|
418
|
+
) -> float:
|
|
419
|
+
"""Estimate a cell cycle percentage based on intensities.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
phase: str
|
|
424
|
+
Name of phase
|
|
425
|
+
intensities: List[float]
|
|
426
|
+
List of channel intensities for all fluorophores
|
|
427
|
+
"""
|
|
428
|
+
raise NotImplementedError("Percentage estimate not yet implemented!")
|
|
429
|
+
if phase not in self.phases:
|
|
430
|
+
raise ValueError(f"Phase {phase} is not defined for this sensor.")
|
|
431
|
+
# TODO fill the following structure with life!
|
|
432
|
+
if phase == "G1":
|
|
433
|
+
return self._find_g1_percentage(intensities[0])
|
|
434
|
+
if phase == "S":
|
|
435
|
+
return self._find_s_percentage(intensities[0])
|
|
436
|
+
else:
|
|
437
|
+
return self._find_g2m_percentage(intensities[1])
|
|
438
|
+
|
|
439
|
+
def get_expected_intensities(
|
|
440
|
+
self, percentage: Union[float, np.ndarray]
|
|
441
|
+
) -> List[Union[float, np.ndarray]]:
|
|
442
|
+
"""Return value of calibrated curves."""
|
|
443
|
+
raise NotImplementedError("Intensity estimate not yet implemented!")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def get_pipfucci_default_sensor() -> PIPFUCCISensor:
|
|
447
|
+
"""Return sensor with default values.
|
|
448
|
+
|
|
449
|
+
Should only be used if the cell cycle percentage is not of interest.
|
|
450
|
+
"""
|
|
451
|
+
# TODO update values
|
|
452
|
+
return PIPFUCCISensor(
|
|
453
|
+
phase_percentages=[25, 25, 50], center=[0, 0, 0, 0], sigma=[0, 0, 0, 0]
|
|
454
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_feature_value_at_frame(
|
|
5
|
+
labels: pd.DataFrame, label_name: str, label: int, feature: str
|
|
6
|
+
) -> float:
|
|
7
|
+
"""Helper function to get value of feature."""
|
|
8
|
+
value = labels[labels[label_name] == label, feature].to_numpy()
|
|
9
|
+
assert len(value) == 1
|
|
10
|
+
return float(value[0])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prepare_penalty_df(
|
|
14
|
+
df: pd.DataFrame,
|
|
15
|
+
feature_1: str,
|
|
16
|
+
feature_2: str,
|
|
17
|
+
frame_name: str = "FRAME",
|
|
18
|
+
label_name: str = "LABEL",
|
|
19
|
+
weight: float = 1.0,
|
|
20
|
+
) -> pd.DataFrame:
|
|
21
|
+
"""Prepare a DF with penalties for tracking.
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
See more details here:
|
|
26
|
+
https://laptrack.readthedocs.io/en/stable/examples/custom_metric.html
|
|
27
|
+
|
|
28
|
+
The penalty formulation is similar to TrackMate,
|
|
29
|
+
see
|
|
30
|
+
https://imagej.net/plugins/trackmate/trackers/lap-trackers#calculating-linking-costs
|
|
31
|
+
The penalty is computed as:
|
|
32
|
+
P = 1 + sum(feature_penalties)
|
|
33
|
+
Each feature penalty is:
|
|
34
|
+
p = 3 * weight * abs(f1 - f2) / (f1 + f2)
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError("This function is not yet stably implemented.")
|
|
38
|
+
|
|
39
|
+
penalty_records = []
|
|
40
|
+
frames = df[frame_name].unique()
|
|
41
|
+
for i, frame in enumerate(frames):
|
|
42
|
+
# skip last frame
|
|
43
|
+
if i == len(frames) - 1:
|
|
44
|
+
continue
|
|
45
|
+
next_frame = frames[i + 1]
|
|
46
|
+
labels = df.loc[df[frame_name] == frame, label_name]
|
|
47
|
+
next_labels = df.loc[df[frame_name] == next_frame, label_name]
|
|
48
|
+
for label in labels:
|
|
49
|
+
if label == 0:
|
|
50
|
+
continue
|
|
51
|
+
# get index where frame + label
|
|
52
|
+
value1 = get_feature_value_at_frame(labels, label_name, label, feature_1)
|
|
53
|
+
value2 = get_feature_value_at_frame(labels, label_name, label, feature_2)
|
|
54
|
+
for next_label in labels:
|
|
55
|
+
if next_label == 0:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
next_value1 = get_feature_value_at_frame(
|
|
59
|
+
next_labels, label_name, label, feature_1
|
|
60
|
+
)
|
|
61
|
+
next_value2 = get_feature_value_at_frame(
|
|
62
|
+
next_labels, label_name, label, feature_2
|
|
63
|
+
)
|
|
64
|
+
penalty = (
|
|
65
|
+
3.0 * weight * abs(value1 - next_value1) / (value1 + next_value1)
|
|
66
|
+
)
|
|
67
|
+
penalty += (
|
|
68
|
+
3.0 * weight * abs(value2 - next_value2) / (value2 + next_value2)
|
|
69
|
+
)
|
|
70
|
+
penalty += 1
|
|
71
|
+
penalty_records.append(
|
|
72
|
+
{
|
|
73
|
+
"frame": frame,
|
|
74
|
+
"label1": label,
|
|
75
|
+
"label2": next_label,
|
|
76
|
+
"penalty": penalty,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
penalty_df = pd.DataFrame.from_records(penalty_records)
|
|
80
|
+
|
|
81
|
+
return penalty_df.set_index(["frame", "label1", "label2"]).copy()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Convenience functions for fucciphase."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"TrackMateXML",
|
|
5
|
+
"check_channels",
|
|
6
|
+
"check_thresholds",
|
|
7
|
+
"compute_motility_parameters",
|
|
8
|
+
"export_lineage_tree_to_svg",
|
|
9
|
+
"fit_percentages",
|
|
10
|
+
"get_norm_channel_name",
|
|
11
|
+
"get_time_distortion_coefficient",
|
|
12
|
+
"norm",
|
|
13
|
+
"normalize_channels",
|
|
14
|
+
"plot_trackscheme",
|
|
15
|
+
"postprocess_estimated_percentages",
|
|
16
|
+
"simulate_single_track",
|
|
17
|
+
"split_all_tracks",
|
|
18
|
+
"split_track",
|
|
19
|
+
"split_trackmate_tracks",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
from .checks import check_channels, check_thresholds
|
|
23
|
+
from .dtw import get_time_distortion_coefficient
|
|
24
|
+
from .normalize import get_norm_channel_name, norm, normalize_channels
|
|
25
|
+
from .phase_fit import fit_percentages, postprocess_estimated_percentages
|
|
26
|
+
from .simulator import simulate_single_track
|
|
27
|
+
from .track_postprocessing import (
|
|
28
|
+
compute_motility_parameters,
|
|
29
|
+
export_lineage_tree_to_svg,
|
|
30
|
+
plot_trackscheme,
|
|
31
|
+
split_all_tracks,
|
|
32
|
+
split_track,
|
|
33
|
+
split_trackmate_tracks,
|
|
34
|
+
)
|
|
35
|
+
from .trackmate import TrackMateXML
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def check_channels(n_fluorophores: int, channels: List[str]) -> None:
|
|
5
|
+
"""Check number of channels."""
|
|
6
|
+
if len(channels) != n_fluorophores:
|
|
7
|
+
raise ValueError(f"Need to provide {n_fluorophores} channel names.")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_thresholds(n_fluorophores: int, thresholds: List[float]) -> None:
|
|
11
|
+
"""Check correct format and range of thresholds."""
|
|
12
|
+
if len(thresholds) != n_fluorophores:
|
|
13
|
+
raise ValueError("Provide one threshold per channel.")
|
|
14
|
+
# check that the thresholds are between 0 and 1
|
|
15
|
+
if not all(0 < t < 1 for t in thresholds):
|
|
16
|
+
raise ValueError("Thresholds must be between 0 and 1.")
|
fucciphase/utils/dtw.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_time_distortion_coefficient(
|
|
7
|
+
path: Union[np.ndarray, List[List[float]]],
|
|
8
|
+
) -> tuple[np.ndarray, float, int, int]:
|
|
9
|
+
"""Compute distortion coefficient from warping path.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
path: np.ndarray
|
|
14
|
+
Warping path, first dimension query index, second reference index
|
|
15
|
+
|
|
16
|
+
The warping path holds two indices: the index of the query (first entry)
|
|
17
|
+
and the index of the reference curve (second entry)
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
lmbd = np.zeros(len(path) - 1)
|
|
21
|
+
alpha = 0
|
|
22
|
+
beta = 0
|
|
23
|
+
p: Union[np.ndarray, List[float]]
|
|
24
|
+
for idx, p in enumerate(path):
|
|
25
|
+
# first index is skipped
|
|
26
|
+
if idx == 0:
|
|
27
|
+
continue
|
|
28
|
+
# stretch check
|
|
29
|
+
if p[0] == path[idx - 1][0]:
|
|
30
|
+
beta += 1
|
|
31
|
+
else:
|
|
32
|
+
# end beta count, add lambdas
|
|
33
|
+
if beta > 0:
|
|
34
|
+
beta += 1
|
|
35
|
+
lmbd[idx - beta - 1 : idx - 1] = 1.0 - 1.0 / beta
|
|
36
|
+
beta = 0
|
|
37
|
+
# compression check
|
|
38
|
+
if p[1] == path[idx - 1][1]:
|
|
39
|
+
alpha += 1
|
|
40
|
+
else:
|
|
41
|
+
if alpha > 0:
|
|
42
|
+
alpha += 1
|
|
43
|
+
lmbd[idx - alpha : idx - 1] = 1.0 - alpha
|
|
44
|
+
alpha = 0
|
|
45
|
+
|
|
46
|
+
# check final entry
|
|
47
|
+
if beta > 0:
|
|
48
|
+
beta += 1
|
|
49
|
+
lmbd[idx - beta : idx] = 1.0 - 1.0 / beta
|
|
50
|
+
beta = 0
|
|
51
|
+
if alpha > 0:
|
|
52
|
+
alpha += 1
|
|
53
|
+
lmbd[idx - alpha + 1 : idx] = 1.0 - alpha
|
|
54
|
+
alpha = 0
|
|
55
|
+
|
|
56
|
+
distortion_score = np.sum(np.abs(lmbd))
|
|
57
|
+
compress_count = int(np.count_nonzero(lmbd > 0))
|
|
58
|
+
stretch_count = int(np.count_nonzero(lmbd < 0))
|
|
59
|
+
return lmbd, distortion_score, compress_count, stretch_count
|