fucciphase 0.0.2__py3-none-any.whl → 0.0.4__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 +7 -1
- fucciphase/__main__.py +12 -0
- fucciphase/fucci_phase.py +123 -53
- fucciphase/io.py +18 -17
- fucciphase/main_cli.py +151 -34
- fucciphase/napari/tracks_to_napari.py +20 -21
- fucciphase/phase.py +350 -137
- fucciphase/plot.py +240 -88
- fucciphase/sensor.py +47 -33
- fucciphase/tracking_utilities.py +70 -9
- fucciphase/utils/__init__.py +14 -1
- fucciphase/utils/checks.py +2 -5
- fucciphase/utils/dtw.py +2 -4
- fucciphase/utils/normalize.py +46 -12
- fucciphase/utils/phase_fit.py +11 -7
- fucciphase/utils/simulator.py +1 -1
- fucciphase/utils/track_postprocessing.py +16 -11
- fucciphase/utils/trackmate.py +30 -13
- fucciphase-0.0.4.dist-info/METADATA +238 -0
- fucciphase-0.0.4.dist-info/RECORD +25 -0
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/WHEEL +1 -1
- fucciphase-0.0.2.dist-info/METADATA +0 -137
- fucciphase-0.0.2.dist-info/RECORD +0 -24
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/entry_points.txt +0 -0
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/licenses/LICENSE +0 -0
fucciphase/sensor.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
3
|
+
from typing import Union
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
from scipy import optimize
|
|
7
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
def logistic(
|
|
10
|
-
x:
|
|
11
|
-
) ->
|
|
13
|
+
x: float | np.ndarray, center: float, sigma: float, sign: float = 1.0
|
|
14
|
+
) -> float | np.ndarray:
|
|
12
15
|
"""Logistic function."""
|
|
13
16
|
tiny = 1.0e-15
|
|
14
17
|
arg = sign * (x - center) / max(tiny, sigma)
|
|
@@ -16,21 +19,21 @@ def logistic(
|
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def accumulation_function(
|
|
19
|
-
x:
|
|
22
|
+
x: float | np.ndarray,
|
|
20
23
|
center: float,
|
|
21
24
|
sigma: float,
|
|
22
25
|
offset_intensity: float = 0,
|
|
23
|
-
) ->
|
|
26
|
+
) -> float | np.ndarray:
|
|
24
27
|
"""Function to describe accumulation of sensor."""
|
|
25
28
|
return 1.0 - logistic(x, center, sigma) - offset_intensity
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
def degradation_function(
|
|
29
|
-
x:
|
|
32
|
+
x: float | np.ndarray,
|
|
30
33
|
center: float,
|
|
31
34
|
sigma: float,
|
|
32
35
|
offset_intensity: float = 0,
|
|
33
|
-
) ->
|
|
36
|
+
) -> float | np.ndarray:
|
|
34
37
|
"""Function to describe degradation of sensor."""
|
|
35
38
|
return 1.0 - logistic(x, center, sigma, sign=-1.0) - offset_intensity
|
|
36
39
|
|
|
@@ -41,9 +44,9 @@ class FUCCISensor(ABC):
|
|
|
41
44
|
@abstractmethod
|
|
42
45
|
def __init__(
|
|
43
46
|
self,
|
|
44
|
-
phase_percentages:
|
|
45
|
-
center:
|
|
46
|
-
sigma:
|
|
47
|
+
phase_percentages: list[float],
|
|
48
|
+
center: list[float],
|
|
49
|
+
sigma: list[float],
|
|
47
50
|
) -> None:
|
|
48
51
|
pass
|
|
49
52
|
|
|
@@ -55,17 +58,17 @@ class FUCCISensor(ABC):
|
|
|
55
58
|
|
|
56
59
|
@property
|
|
57
60
|
@abstractmethod
|
|
58
|
-
def phases(self) ->
|
|
61
|
+
def phases(self) -> list[str]:
|
|
59
62
|
"""Function to hard-code the supported phases of a sensor."""
|
|
60
63
|
pass
|
|
61
64
|
|
|
62
65
|
@property
|
|
63
|
-
def phase_percentages(self) ->
|
|
66
|
+
def phase_percentages(self) -> list[float]:
|
|
64
67
|
"""Percentage of individual phases."""
|
|
65
68
|
return self._phase_percentages
|
|
66
69
|
|
|
67
70
|
@phase_percentages.setter
|
|
68
|
-
def phase_percentages(self, values:
|
|
71
|
+
def phase_percentages(self, values: list[float]) -> None:
|
|
69
72
|
if len(values) != len(self.phases):
|
|
70
73
|
raise ValueError("Pass percentage for each phase.")
|
|
71
74
|
|
|
@@ -76,25 +79,25 @@ class FUCCISensor(ABC):
|
|
|
76
79
|
self._phase_percentages = values
|
|
77
80
|
|
|
78
81
|
@abstractmethod
|
|
79
|
-
def get_phase(self, phase_markers: Union[
|
|
82
|
+
def get_phase(self, phase_markers: Union[list[bool], "pd.Series[bool]"]) -> str:
|
|
80
83
|
"""Get the discrete phase based on phase markers.
|
|
81
84
|
|
|
82
85
|
Notes
|
|
83
86
|
-----
|
|
84
|
-
|
|
87
|
+
The discrete phase refers to, for example, G1 or S phase.
|
|
85
88
|
The phase_markers must match the number of used fluorophores.
|
|
86
89
|
"""
|
|
87
90
|
pass
|
|
88
91
|
|
|
89
92
|
@abstractmethod
|
|
90
93
|
def get_estimated_cycle_percentage(
|
|
91
|
-
self, phase: str, intensities:
|
|
94
|
+
self, phase: str, intensities: list[float]
|
|
92
95
|
) -> float:
|
|
93
96
|
"""Estimate percentage based on sensor intensities."""
|
|
94
97
|
pass
|
|
95
98
|
|
|
96
99
|
def set_accumulation_and_degradation_parameters(
|
|
97
|
-
self, center:
|
|
100
|
+
self, center: list[float], sigma: list[float]
|
|
98
101
|
) -> None:
|
|
99
102
|
"""Pass list of functions for logistic functions.
|
|
100
103
|
|
|
@@ -114,8 +117,8 @@ class FUCCISensor(ABC):
|
|
|
114
117
|
|
|
115
118
|
@abstractmethod
|
|
116
119
|
def get_expected_intensities(
|
|
117
|
-
self, percentage:
|
|
118
|
-
) ->
|
|
120
|
+
self, percentage: float | np.ndarray
|
|
121
|
+
) -> list[float | np.ndarray]:
|
|
119
122
|
"""Return value of calibrated curves."""
|
|
120
123
|
pass
|
|
121
124
|
|
|
@@ -124,7 +127,7 @@ class FUCCISASensor(FUCCISensor):
|
|
|
124
127
|
"""FUCCI(SA) sensor."""
|
|
125
128
|
|
|
126
129
|
def __init__(
|
|
127
|
-
self, phase_percentages:
|
|
130
|
+
self, phase_percentages: list[float], center: list[float], sigma: list[float]
|
|
128
131
|
) -> None:
|
|
129
132
|
self.phase_percentages = phase_percentages
|
|
130
133
|
self.set_accumulation_and_degradation_parameters(center, sigma)
|
|
@@ -135,11 +138,11 @@ class FUCCISASensor(FUCCISensor):
|
|
|
135
138
|
return 2
|
|
136
139
|
|
|
137
140
|
@property
|
|
138
|
-
def phases(self) ->
|
|
141
|
+
def phases(self) -> list[str]:
|
|
139
142
|
"""Function to hard-code the supported phases of a sensor."""
|
|
140
143
|
return ["G1", "G1/S", "S/G2/M"]
|
|
141
144
|
|
|
142
|
-
def get_phase(self, phase_markers: Union[
|
|
145
|
+
def get_phase(self, phase_markers: Union[list[bool], "pd.Series[bool]"]) -> str:
|
|
143
146
|
"""Return the discrete phase based channel ON / OFF data for the
|
|
144
147
|
FUCCI(SA) sensor.
|
|
145
148
|
"""
|
|
@@ -273,13 +276,13 @@ class FUCCISASensor(FUCCISensor):
|
|
|
273
276
|
)
|
|
274
277
|
)
|
|
275
278
|
except ValueError:
|
|
276
|
-
|
|
277
|
-
"
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Could not infer percentage in SG2M phase, using average phase value"
|
|
278
281
|
)
|
|
279
282
|
return g1s_perc + 0.5 * (100.0 - g1s_perc - g1_perc)
|
|
280
283
|
|
|
281
284
|
def get_estimated_cycle_percentage(
|
|
282
|
-
self, phase: str, intensities:
|
|
285
|
+
self, phase: str, intensities: list[float]
|
|
283
286
|
) -> float:
|
|
284
287
|
"""Estimate a cell cycle percentage based on intensities.
|
|
285
288
|
|
|
@@ -289,9 +292,20 @@ class FUCCISASensor(FUCCISensor):
|
|
|
289
292
|
Name of phase
|
|
290
293
|
intensities: List[float]
|
|
291
294
|
List of channel intensities for all fluorophores
|
|
295
|
+
|
|
296
|
+
Raises
|
|
297
|
+
------
|
|
298
|
+
ValueError
|
|
299
|
+
If the phase is not defined for this sensor or if the intensities
|
|
300
|
+
list does not have the expected number of elements.
|
|
292
301
|
"""
|
|
293
302
|
if phase not in self.phases:
|
|
294
303
|
raise ValueError(f"Phase {phase} is not defined for this sensor.")
|
|
304
|
+
if len(intensities) < self.fluorophores:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Expected {self.fluorophores} intensity values, "
|
|
307
|
+
f"but got {len(intensities)}."
|
|
308
|
+
)
|
|
295
309
|
if phase == "G1":
|
|
296
310
|
return self._find_g1_percentage(intensities[0])
|
|
297
311
|
if phase == "G1/S":
|
|
@@ -300,8 +314,8 @@ class FUCCISASensor(FUCCISensor):
|
|
|
300
314
|
return self._find_sg2m_percentage(intensities[1])
|
|
301
315
|
|
|
302
316
|
def get_expected_intensities(
|
|
303
|
-
self, percentage:
|
|
304
|
-
) ->
|
|
317
|
+
self, percentage: float | np.ndarray
|
|
318
|
+
) -> list[float | np.ndarray]:
|
|
305
319
|
"""Return value of calibrated curves."""
|
|
306
320
|
g1_acc = accumulation_function(
|
|
307
321
|
percentage, self._center_values[0], self._sigma_values[0]
|
|
@@ -332,7 +346,7 @@ class PIPFUCCISensor(FUCCISensor):
|
|
|
332
346
|
"""PIP-FUCCI sensor."""
|
|
333
347
|
|
|
334
348
|
def __init__(
|
|
335
|
-
self, phase_percentages:
|
|
349
|
+
self, phase_percentages: list[float], center: list[float], sigma: list[float]
|
|
336
350
|
) -> None:
|
|
337
351
|
self.phase_percentages = phase_percentages
|
|
338
352
|
self.set_accumulation_and_degradation_parameters(center, sigma)
|
|
@@ -343,11 +357,11 @@ class PIPFUCCISensor(FUCCISensor):
|
|
|
343
357
|
return 2
|
|
344
358
|
|
|
345
359
|
@property
|
|
346
|
-
def phases(self) ->
|
|
360
|
+
def phases(self) -> list[str]:
|
|
347
361
|
"""Function to hard-code the supported phases of a sensor."""
|
|
348
362
|
return ["G1", "S", "G2/M"]
|
|
349
363
|
|
|
350
|
-
def get_phase(self, phase_markers: Union[
|
|
364
|
+
def get_phase(self, phase_markers: Union[list[bool], "pd.Series[bool]"]) -> str:
|
|
351
365
|
"""Return the discrete phase based channel ON / OFF data for the
|
|
352
366
|
FUCCI(SA) sensor.
|
|
353
367
|
"""
|
|
@@ -414,7 +428,7 @@ class PIPFUCCISensor(FUCCISensor):
|
|
|
414
428
|
raise NotImplementedError("Percentage estimate not yet implemented!")
|
|
415
429
|
|
|
416
430
|
def get_estimated_cycle_percentage(
|
|
417
|
-
self, phase: str, intensities:
|
|
431
|
+
self, phase: str, intensities: list[float]
|
|
418
432
|
) -> float:
|
|
419
433
|
"""Estimate a cell cycle percentage based on intensities.
|
|
420
434
|
|
|
@@ -437,8 +451,8 @@ class PIPFUCCISensor(FUCCISensor):
|
|
|
437
451
|
return self._find_g2m_percentage(intensities[1])
|
|
438
452
|
|
|
439
453
|
def get_expected_intensities(
|
|
440
|
-
self, percentage:
|
|
441
|
-
) ->
|
|
454
|
+
self, percentage: float | np.ndarray
|
|
455
|
+
) -> list[float | np.ndarray]:
|
|
442
456
|
"""Return value of calibrated curves."""
|
|
443
457
|
raise NotImplementedError("Intensity estimate not yet implemented!")
|
|
444
458
|
|
fucciphase/tracking_utilities.py
CHANGED
|
@@ -4,9 +4,38 @@ import pandas as pd
|
|
|
4
4
|
def get_feature_value_at_frame(
|
|
5
5
|
labels: pd.DataFrame, label_name: str, label: int, feature: str
|
|
6
6
|
) -> float:
|
|
7
|
-
"""
|
|
7
|
+
"""
|
|
8
|
+
Helper function to get the value of a feature for a given label.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
labels : pandas.DataFrame
|
|
13
|
+
Dataframe containing at least the label and feature columns.
|
|
14
|
+
label_name : str
|
|
15
|
+
Column name containing label identifiers.
|
|
16
|
+
label : int
|
|
17
|
+
Label value to select.
|
|
18
|
+
feature : str
|
|
19
|
+
Column name of the feature whose value should be returned.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
float
|
|
24
|
+
The feature value corresponding to the specified label.
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
ValueError
|
|
29
|
+
If zero or multiple rows match the requested label.
|
|
30
|
+
"""
|
|
8
31
|
value = labels[labels[label_name] == label, feature].to_numpy()
|
|
9
|
-
|
|
32
|
+
if len(value) == 0:
|
|
33
|
+
raise ValueError(f"No rows match label '{label}' in column '{label_name}'.")
|
|
34
|
+
if len(value) > 1:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Multiple rows ({len(value)}) match label '{label}' "
|
|
37
|
+
f"in column '{label_name}'. Expected exactly one match."
|
|
38
|
+
)
|
|
10
39
|
return float(value[0])
|
|
11
40
|
|
|
12
41
|
|
|
@@ -18,21 +47,53 @@ def prepare_penalty_df(
|
|
|
18
47
|
label_name: str = "LABEL",
|
|
19
48
|
weight: float = 1.0,
|
|
20
49
|
) -> pd.DataFrame:
|
|
21
|
-
"""Prepare a
|
|
50
|
+
"""Prepare a dataframe with penalties for tracking.
|
|
51
|
+
|
|
52
|
+
This function is intended to construct a cost / penalty matrix for
|
|
53
|
+
linking detections between consecutive frames, inspired by the
|
|
54
|
+
formulations used in LapTrack and TrackMate.
|
|
22
55
|
|
|
23
56
|
Notes
|
|
24
57
|
-----
|
|
25
58
|
See more details here:
|
|
26
|
-
https://laptrack.readthedocs.io/en/stable/examples/custom_metric.html
|
|
59
|
+
- https://laptrack.readthedocs.io/en/stable/examples/custom_metric.html
|
|
60
|
+
- https://imagej.net/plugins/trackmate/trackers/lap-trackers#calculating-linking-costs
|
|
27
61
|
|
|
28
|
-
The penalty formulation is
|
|
29
|
-
see
|
|
30
|
-
https://imagej.net/plugins/trackmate/trackers/lap-trackers#calculating-linking-costs
|
|
31
|
-
The penalty is computed as:
|
|
62
|
+
The intended penalty formulation is:
|
|
32
63
|
P = 1 + sum(feature_penalties)
|
|
33
|
-
|
|
64
|
+
with each feature penalty of the form:
|
|
34
65
|
p = 3 * weight * abs(f1 - f2) / (f1 + f2)
|
|
35
66
|
|
|
67
|
+
**Current status:** this function is not yet stably implemented and will
|
|
68
|
+
raise ``NotImplementedError`` if called. The prototype implementation
|
|
69
|
+
below is kept for future development.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
df : pandas.DataFrame
|
|
74
|
+
Dataframe containing at least frame, label and feature columns.
|
|
75
|
+
feature_1 : str
|
|
76
|
+
Name of the first feature to use in the penalty.
|
|
77
|
+
feature_2 : str
|
|
78
|
+
Name of the second feature to use in the penalty.
|
|
79
|
+
frame_name : str, optional
|
|
80
|
+
Column name for frame indices. Default is ``"FRAME"``.
|
|
81
|
+
label_name : str, optional
|
|
82
|
+
Column name for object labels. Default is ``"LABEL"``.
|
|
83
|
+
weight : float, optional
|
|
84
|
+
Global scaling factor for feature penalties. Default is 1.0.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
pandas.DataFrame
|
|
89
|
+
Prototype would return a dataframe indexed by (frame, label1, label2)
|
|
90
|
+
with a ``penalty`` column.
|
|
91
|
+
|
|
92
|
+
Raises
|
|
93
|
+
------
|
|
94
|
+
NotImplementedError
|
|
95
|
+
This function is currently not stable and is intentionally disabled.
|
|
96
|
+
|
|
36
97
|
"""
|
|
37
98
|
raise NotImplementedError("This function is not yet stably implemented.")
|
|
38
99
|
|
fucciphase/utils/__init__.py
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Convenience functions for fucciphase - Utility submodule for FUCCIphase.
|
|
3
|
+
|
|
4
|
+
This package collects helper functions used throughout FUCCIphase, including:
|
|
5
|
+
- TrackMate XML parsing and rewriting
|
|
6
|
+
- Channel normalization
|
|
7
|
+
- Track splitting and postprocessing
|
|
8
|
+
- Motility and lineage visualizations
|
|
9
|
+
- Synthetic data generation
|
|
10
|
+
- DTW time-distortion utilities
|
|
11
|
+
|
|
12
|
+
Everything listed in ``__all__`` is considered part of the public FUCCIphase API.
|
|
13
|
+
|
|
14
|
+
"""
|
|
2
15
|
|
|
3
16
|
__all__ = [
|
|
4
17
|
"TrackMateXML",
|
fucciphase/utils/checks.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def check_channels(n_fluorophores: int, channels: List[str]) -> None:
|
|
1
|
+
def check_channels(n_fluorophores: int, channels: list[str]) -> None:
|
|
5
2
|
"""Check number of channels."""
|
|
6
3
|
if len(channels) != n_fluorophores:
|
|
7
4
|
raise ValueError(f"Need to provide {n_fluorophores} channel names.")
|
|
8
5
|
|
|
9
6
|
|
|
10
|
-
def check_thresholds(n_fluorophores: int, thresholds:
|
|
7
|
+
def check_thresholds(n_fluorophores: int, thresholds: list[float]) -> None:
|
|
11
8
|
"""Check correct format and range of thresholds."""
|
|
12
9
|
if len(thresholds) != n_fluorophores:
|
|
13
10
|
raise ValueError("Provide one threshold per channel.")
|
fucciphase/utils/dtw.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from typing import List, Union
|
|
2
|
-
|
|
3
1
|
import numpy as np
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
def get_time_distortion_coefficient(
|
|
7
|
-
path:
|
|
5
|
+
path: np.ndarray | list[list[float]],
|
|
8
6
|
) -> tuple[np.ndarray, float, int, int]:
|
|
9
7
|
"""Compute distortion coefficient from warping path.
|
|
10
8
|
|
|
@@ -20,7 +18,7 @@ def get_time_distortion_coefficient(
|
|
|
20
18
|
lmbd = np.zeros(len(path) - 1)
|
|
21
19
|
alpha = 0
|
|
22
20
|
beta = 0
|
|
23
|
-
p:
|
|
21
|
+
p: np.ndarray | list[float]
|
|
24
22
|
for idx, p in enumerate(path):
|
|
25
23
|
# first index is skipped
|
|
26
24
|
if idx == 0:
|
fucciphase/utils/normalize.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import List, Optional, Tuple, Union
|
|
2
|
-
|
|
3
1
|
import numpy as np
|
|
4
2
|
import pandas as pd
|
|
5
3
|
from scipy import signal
|
|
@@ -37,21 +35,44 @@ def get_avg_channel_name(channel: str) -> str:
|
|
|
37
35
|
return f"{channel}_AVG"
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
def norm(
|
|
38
|
+
def norm(
|
|
39
|
+
vector: pd.Series | np.ndarray,
|
|
40
|
+
max_ch: float | None = None,
|
|
41
|
+
min_ch: float | None = None,
|
|
42
|
+
) -> pd.Series | np.ndarray:
|
|
41
43
|
"""Normalize a vector by subtracting the min and dividing by (max - min).
|
|
42
44
|
|
|
43
45
|
Parameters
|
|
44
46
|
----------
|
|
45
47
|
vector : Union[pd.Series, np.ndarray]
|
|
46
48
|
Vector to normalize.
|
|
49
|
+
max_ch: Optional[float]
|
|
50
|
+
Optional value for the maximum used in normalization
|
|
51
|
+
min_ch: Optional[float]
|
|
52
|
+
Optional value for the minimum used in normalization
|
|
47
53
|
|
|
48
54
|
Returns
|
|
49
55
|
-------
|
|
50
56
|
Union[pd.Series, np.ndarray]
|
|
51
57
|
Normalized vector.
|
|
58
|
+
|
|
59
|
+
Raises
|
|
60
|
+
------
|
|
61
|
+
ValueError
|
|
62
|
+
If max_ch equals min_ch (constant signal), which would cause division by zero.
|
|
52
63
|
"""
|
|
53
|
-
max_ch
|
|
54
|
-
|
|
64
|
+
if max_ch is None:
|
|
65
|
+
max_ch = vector.max()
|
|
66
|
+
if min_ch is None:
|
|
67
|
+
min_ch = vector.min()
|
|
68
|
+
|
|
69
|
+
# Check for division by zero (constant signal)
|
|
70
|
+
if np.isclose(max_ch, min_ch):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Cannot normalize: max ({max_ch}) equals min ({min_ch}). "
|
|
73
|
+
"The signal appears to be constant."
|
|
74
|
+
)
|
|
75
|
+
|
|
55
76
|
norm_ch = np.round(
|
|
56
77
|
(vector - min_ch) / (max_ch - min_ch),
|
|
57
78
|
2, # number of decimals
|
|
@@ -63,13 +84,13 @@ def norm(vector: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
|
|
|
63
84
|
# flake8: noqa: C901
|
|
64
85
|
def normalize_channels(
|
|
65
86
|
df: pd.DataFrame,
|
|
66
|
-
channels:
|
|
87
|
+
channels: str | list[str],
|
|
67
88
|
use_moving_average: bool = True,
|
|
68
89
|
moving_average_window: int = 7,
|
|
69
|
-
manual_min:
|
|
70
|
-
manual_max:
|
|
90
|
+
manual_min: list[float] | None = None,
|
|
91
|
+
manual_max: list[float] | None = None,
|
|
71
92
|
track_id_name: str = "TRACK_ID",
|
|
72
|
-
) ->
|
|
93
|
+
) -> list[str]:
|
|
73
94
|
"""Normalize channels, add in place the resulting columns to the
|
|
74
95
|
dataframe, and return the new columns' name.
|
|
75
96
|
|
|
@@ -150,14 +171,22 @@ def normalize_channels(
|
|
|
150
171
|
df.loc[index, avg_channel] = ma
|
|
151
172
|
|
|
152
173
|
# normalize channels
|
|
153
|
-
for channel in channels:
|
|
174
|
+
for idx, channel in enumerate(channels):
|
|
154
175
|
# moving average creates a new column with an own name
|
|
155
176
|
if use_moving_average:
|
|
156
177
|
avg_channel = get_avg_channel_name(channel)
|
|
157
178
|
else:
|
|
158
179
|
avg_channel = channel
|
|
159
180
|
# normalize channel
|
|
160
|
-
|
|
181
|
+
# default: compute max and min per channel
|
|
182
|
+
max_ch = None
|
|
183
|
+
min_ch = None
|
|
184
|
+
# if manually specified limits, overwrite
|
|
185
|
+
if manual_max is not None:
|
|
186
|
+
max_ch = manual_max[idx]
|
|
187
|
+
if manual_min is not None:
|
|
188
|
+
min_ch = manual_min[idx]
|
|
189
|
+
norm_ch = norm(df[avg_channel], max_ch=max_ch, min_ch=min_ch)
|
|
161
190
|
|
|
162
191
|
# add the new column
|
|
163
192
|
new_column = get_norm_channel_name(channel)
|
|
@@ -173,7 +202,7 @@ def smooth_track(
|
|
|
173
202
|
channel: str,
|
|
174
203
|
track_id_name: str,
|
|
175
204
|
moving_average_window: int = 7,
|
|
176
|
-
) ->
|
|
205
|
+
) -> tuple[pd.Index, np.ndarray]:
|
|
177
206
|
"""Smooth intensity in one channel for a single track.
|
|
178
207
|
|
|
179
208
|
Parameters
|
|
@@ -188,10 +217,15 @@ def smooth_track(
|
|
|
188
217
|
Name of column with track IDs
|
|
189
218
|
moving_average_window : int
|
|
190
219
|
Size of the window used for the moving average, default 7.
|
|
220
|
+
Must be greater than 3.
|
|
191
221
|
"""
|
|
192
222
|
# get the track
|
|
193
223
|
track: pd.DataFrame = df[df[track_id_name] == track_ID]
|
|
194
224
|
|
|
225
|
+
# hard-coded polyorder is 3, window length must be longer
|
|
226
|
+
if moving_average_window <= 3:
|
|
227
|
+
raise ValueError("Use moving_average_window of at least 4.")
|
|
228
|
+
|
|
195
229
|
# compute the moving average
|
|
196
230
|
ma = signal.savgol_filter(
|
|
197
231
|
track[channel],
|
fucciphase/utils/phase_fit.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
from monotonic_derivative import ensure_monotonic_derivative
|
|
4
6
|
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
def fit_percentages(frames: np.ndarray, percentages: np.ndarray) -> np.ndarray:
|
|
7
11
|
"""Fit estimated percentages to function with non-negative derivative."""
|
|
@@ -12,7 +16,7 @@ def fit_percentages(frames: np.ndarray, percentages: np.ndarray) -> np.ndarray:
|
|
|
12
16
|
force_negative_derivative=False,
|
|
13
17
|
)
|
|
14
18
|
# clip to range (0, 100)
|
|
15
|
-
return np.clip(best_fit, 0.0, 100.0)
|
|
19
|
+
return np.clip(best_fit, 0.0, 100.0) # type: ignore[no-any-return]
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
def postprocess_estimated_percentages(
|
|
@@ -31,17 +35,17 @@ def postprocess_estimated_percentages(
|
|
|
31
35
|
frames = track["FRAME"]
|
|
32
36
|
percentages = track[percentage_column]
|
|
33
37
|
if np.all(np.isnan(percentages)):
|
|
34
|
-
|
|
38
|
+
logger.warning("No percentages to postprocess")
|
|
35
39
|
return
|
|
36
40
|
try:
|
|
37
41
|
restored_percentages = fit_percentages(frames, percentages)
|
|
38
42
|
except ValueError:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
+
logger.error(
|
|
44
|
+
"Error in track %s. Make sure that the spots belong to a unique track, "
|
|
45
|
+
"i.e., not more than one spot per frame per track.\n%s",
|
|
46
|
+
index,
|
|
47
|
+
track,
|
|
43
48
|
)
|
|
44
|
-
print(track)
|
|
45
49
|
df.loc[df[track_id_name] == index, postprocessed_percentage_column] = (
|
|
46
50
|
restored_percentages
|
|
47
51
|
)
|
fucciphase/utils/simulator.py
CHANGED
|
@@ -47,7 +47,7 @@ def simulate_single_track(track_id: float = 42, mean: float = 0.5) -> pd.DataFra
|
|
|
47
47
|
pd.DataFrame
|
|
48
48
|
Dataframe mocking a Trackmate single track import.
|
|
49
49
|
"""
|
|
50
|
-
#
|
|
50
|
+
# examples data
|
|
51
51
|
phase_percentages = [33.3, 33.3, 33.3]
|
|
52
52
|
center = [20.0, 55.0, 70.0, 95.0]
|
|
53
53
|
sigma = [5.0, 5.0, 10.0, 1.0]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
|
|
3
3
|
import matplotlib.pyplot as plt
|
|
4
4
|
import numpy as np
|
|
@@ -7,6 +7,8 @@ from LineageTree import lineageTree
|
|
|
7
7
|
from matplotlib import colormaps
|
|
8
8
|
from scipy import signal
|
|
9
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def split_track(
|
|
12
14
|
track: pd.DataFrame,
|
|
@@ -93,10 +95,13 @@ def split_all_tracks(
|
|
|
93
95
|
"""
|
|
94
96
|
if track_id_name not in track_df.columns:
|
|
95
97
|
raise ValueError(f"{track_id_name} column is missing.")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
|
|
99
|
+
# Use unique() to handle non-contiguous track IDs correctly
|
|
100
|
+
track_ids = track_df[track_id_name].unique()
|
|
101
|
+
highest_track_idx_counter = track_df[track_id_name].max()
|
|
102
|
+
|
|
98
103
|
# go through all tracks and split if needed
|
|
99
|
-
for track_idx in
|
|
104
|
+
for track_idx in track_ids:
|
|
100
105
|
track = track_df.loc[track_df[track_id_name] == track_idx]
|
|
101
106
|
if len(track) < minimum_track_length:
|
|
102
107
|
continue
|
|
@@ -157,7 +162,7 @@ def compute_motility_parameters(
|
|
|
157
162
|
|
|
158
163
|
|
|
159
164
|
def compute_displacements(
|
|
160
|
-
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z:
|
|
165
|
+
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z: np.ndarray | None
|
|
161
166
|
) -> np.ndarray:
|
|
162
167
|
"""Compute displacement w.r.t origin."""
|
|
163
168
|
N = len(centroids_x)
|
|
@@ -180,7 +185,7 @@ def compute_displacements(
|
|
|
180
185
|
|
|
181
186
|
|
|
182
187
|
def compute_velocities(
|
|
183
|
-
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z:
|
|
188
|
+
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z: np.ndarray | None
|
|
184
189
|
) -> np.ndarray:
|
|
185
190
|
"""Compute velocity."""
|
|
186
191
|
N = len(centroids_x)
|
|
@@ -205,7 +210,7 @@ def compute_velocities(
|
|
|
205
210
|
|
|
206
211
|
|
|
207
212
|
def compute_MSD(
|
|
208
|
-
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z:
|
|
213
|
+
centroids_x: np.ndarray, centroids_y: np.ndarray, centroids_z: np.ndarray | None
|
|
209
214
|
) -> np.ndarray:
|
|
210
215
|
"""Compute mean-squared distance.
|
|
211
216
|
|
|
@@ -357,9 +362,9 @@ def split_trackmate_tracks(
|
|
|
357
362
|
def export_lineage_tree_to_svg(
|
|
358
363
|
df: pd.DataFrame,
|
|
359
364
|
trackmate_file: str,
|
|
360
|
-
node_color_column:
|
|
361
|
-
stroke_width:
|
|
362
|
-
) ->
|
|
365
|
+
node_color_column: str | None = None,
|
|
366
|
+
stroke_width: float | None = None,
|
|
367
|
+
) -> list[str]:
|
|
363
368
|
"""Write a lineage tree colored by FUCCI phases.
|
|
364
369
|
|
|
365
370
|
Parameters
|
|
@@ -384,7 +389,7 @@ def export_lineage_tree_to_svg(
|
|
|
384
389
|
This function currently only supports
|
|
385
390
|
the standard FUCCISA sensor.
|
|
386
391
|
"""
|
|
387
|
-
|
|
392
|
+
logger.warning("Make sure that you updated the spot names using TrackMate actions!")
|
|
388
393
|
# initialise lineage tree
|
|
389
394
|
lt = lineageTree(trackmate_file, file_type="TrackMate")
|
|
390
395
|
cmap_name = "cool"
|