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.
@@ -0,0 +1,202 @@
1
+ from typing import List, Optional, Tuple, Union
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy import signal
6
+
7
+
8
+ def get_norm_channel_name(channel: str) -> str:
9
+ """Return the name of the normalized channel.
10
+
11
+ Parameters
12
+ ----------
13
+ channel : str
14
+ Name of the channel to normalize.
15
+
16
+ Returns
17
+ -------
18
+ str
19
+ Name of the normalized channel.
20
+ """
21
+ return f"{channel}_NORM"
22
+
23
+
24
+ def get_avg_channel_name(channel: str) -> str:
25
+ """Return the name of the moving-averaged channel.
26
+
27
+ Parameters
28
+ ----------
29
+ channel : str
30
+ Name of the channel to average using moving average.
31
+
32
+ Returns
33
+ -------
34
+ str
35
+ Name of the moving-averaged channel.
36
+ """
37
+ return f"{channel}_AVG"
38
+
39
+
40
+ def norm(vector: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
41
+ """Normalize a vector by subtracting the min and dividing by (max - min).
42
+
43
+ Parameters
44
+ ----------
45
+ vector : Union[pd.Series, np.ndarray]
46
+ Vector to normalize.
47
+
48
+ Returns
49
+ -------
50
+ Union[pd.Series, np.ndarray]
51
+ Normalized vector.
52
+ """
53
+ max_ch = vector.max()
54
+ min_ch = vector.min()
55
+ norm_ch = np.round(
56
+ (vector - min_ch) / (max_ch - min_ch),
57
+ 2, # number of decimals
58
+ )
59
+
60
+ return norm_ch
61
+
62
+
63
+ # flake8: noqa: C901
64
+ def normalize_channels(
65
+ df: pd.DataFrame,
66
+ channels: Union[str, List[str]],
67
+ use_moving_average: bool = True,
68
+ moving_average_window: int = 7,
69
+ manual_min: Optional[List[float]] = None,
70
+ manual_max: Optional[List[float]] = None,
71
+ track_id_name: str = "TRACK_ID",
72
+ ) -> List[str]:
73
+ """Normalize channels, add in place the resulting columns to the
74
+ dataframe, and return the new columns' name.
75
+
76
+ A moving average can be applied to each individual track before normalization.
77
+
78
+ Normalization is performed by inferring the min at the position of the maximum
79
+ of the other channel. Then, the min is subtracted and the result is divided
80
+ by (max - min).
81
+ These values are computed across all spots in each channel. Note that the resulting
82
+ normalized values are rounded to the 2nd decimal.
83
+
84
+ The min and max values can be provided manually. They should be determined by
85
+ imaging a large number of cells statically and computing the min and max values
86
+ observed.
87
+ This option is meant for static imaging. It is assumed that there are enough cells
88
+ in the image to have enough samples from each phase of the cell cycle.
89
+
90
+ Parameters
91
+ ----------
92
+ df : pd.DataFrame
93
+ Dataframe
94
+ channels : Union[str, List[str]]
95
+ Name of the channels to normalize.
96
+ use_moving_average : bool
97
+ Whether to apply a moving average to each track before normalization.
98
+ moving_average_window : int
99
+ Size of the window used for the moving average, default 7.
100
+ manual_min : Optional[List[float]]
101
+ If provided, the minimum value to use for normalization.
102
+ manual_max : Optional[List[float]]
103
+ If provided, the maximum value to use for normalization.
104
+ track_id_name: str
105
+ Name of column with track IDs
106
+
107
+ Returns
108
+ -------
109
+ List[str]
110
+ Name of the new column(s).
111
+
112
+ Raises
113
+ ------
114
+ ValueError
115
+ If the dataframe does not contain the mandatory columns.
116
+ """
117
+ if not isinstance(channels, list):
118
+ channels = [channels]
119
+
120
+ if manual_min is not None:
121
+ # check that it has the same number of entries as there are channels
122
+ if len(manual_min) != len(channels):
123
+ raise ValueError(
124
+ f"Expected {len(channels)} values for manual_min, got {len(manual_min)}"
125
+ )
126
+ if manual_max is not None:
127
+ # check that it has the same number of entries as there are channels
128
+ if len(manual_max) != len(channels):
129
+ raise ValueError(
130
+ f"Expected {len(channels)} values for manual_max, got {len(manual_max)}"
131
+ )
132
+
133
+ # check that the dataframe contains the channel
134
+ new_columns = []
135
+ for channel in channels:
136
+ if channel not in df.columns:
137
+ raise ValueError(f"Column {channel} not found")
138
+
139
+ # compute the moving average for each track ID
140
+ if use_moving_average:
141
+ # apply moving average to each track ID
142
+ unique_track_IDs = df[track_id_name].unique()
143
+
144
+ avg_channel = get_avg_channel_name(channel)
145
+ for track_ID in unique_track_IDs:
146
+ index, ma = smooth_track(
147
+ df, track_ID, channel, track_id_name, moving_average_window
148
+ )
149
+ # update the dataframe by adding a new column
150
+ df.loc[index, avg_channel] = ma
151
+
152
+ # normalize channels
153
+ for channel in channels:
154
+ # moving average creates a new column with an own name
155
+ if use_moving_average:
156
+ avg_channel = get_avg_channel_name(channel)
157
+ else:
158
+ avg_channel = channel
159
+ # normalize channel
160
+ norm_ch = norm(df[avg_channel])
161
+
162
+ # add the new column
163
+ new_column = get_norm_channel_name(channel)
164
+ df[new_column] = norm_ch
165
+ new_columns.append(new_column)
166
+
167
+ return new_columns
168
+
169
+
170
+ def smooth_track(
171
+ df: pd.DataFrame,
172
+ track_ID: int,
173
+ channel: str,
174
+ track_id_name: str,
175
+ moving_average_window: int = 7,
176
+ ) -> Tuple[pd.Index, np.ndarray]:
177
+ """Smooth intensity in one channel for a single track.
178
+
179
+ Parameters
180
+ ----------
181
+ df : pd.DataFrame
182
+ Dataframe
183
+ track_ID: int
184
+ Index of track
185
+ channel : st
186
+ Name of the channel to smooth
187
+ track_id_name: str
188
+ Name of column with track IDs
189
+ moving_average_window : int
190
+ Size of the window used for the moving average, default 7.
191
+ """
192
+ # get the track
193
+ track: pd.DataFrame = df[df[track_id_name] == track_ID]
194
+
195
+ # compute the moving average
196
+ ma = signal.savgol_filter(
197
+ track[channel],
198
+ window_length=moving_average_window,
199
+ polyorder=3,
200
+ mode="nearest",
201
+ )
202
+ return track.index, ma
@@ -0,0 +1,47 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from monotonic_derivative import ensure_monotonic_derivative
4
+
5
+
6
+ def fit_percentages(frames: np.ndarray, percentages: np.ndarray) -> np.ndarray:
7
+ """Fit estimated percentages to function with non-negative derivative."""
8
+ best_fit: np.ndarray = ensure_monotonic_derivative(
9
+ x=frames,
10
+ y=percentages,
11
+ degree=1,
12
+ force_negative_derivative=False,
13
+ )
14
+ # clip to range (0, 100)
15
+ return np.clip(best_fit, 0.0, 100.0)
16
+
17
+
18
+ def postprocess_estimated_percentages(
19
+ df: pd.DataFrame, percentage_column: str, track_id_name: str = "TRACK_ID"
20
+ ) -> None:
21
+ """Make estimated percentages continuous."""
22
+ if percentage_column not in df:
23
+ raise ValueError("The name of the percentage column is not in the DataFrame")
24
+ indices = df[track_id_name].unique()
25
+ postprocessed_percentage_column = percentage_column + "_POST"
26
+ df[postprocessed_percentage_column] = np.nan
27
+ for index in indices:
28
+ if index == -1:
29
+ continue
30
+ track = df[df[track_id_name] == index]
31
+ frames = track["FRAME"]
32
+ percentages = track[percentage_column]
33
+ if np.all(np.isnan(percentages)):
34
+ print("WARNING: No percentages to postprocess")
35
+ return
36
+ try:
37
+ restored_percentages = fit_percentages(frames, percentages)
38
+ except ValueError:
39
+ print(f"Error in track {index}")
40
+ print(
41
+ "Make sure that the spots belong to a unique track,"
42
+ " i.e., not more than one spot per frame per track."
43
+ )
44
+ print(track)
45
+ df.loc[df[track_id_name] == index, postprocessed_percentage_column] = (
46
+ restored_percentages
47
+ )
@@ -0,0 +1,85 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from fucciphase.sensor import FUCCISASensor
5
+
6
+ # TODO improve simulation, use logistic functions
7
+
8
+
9
+ def simulate_single_channel(
10
+ t: np.ndarray, mean: float, sigma: float, amp: float = 1.0
11
+ ) -> np.ndarray:
12
+ """Simulate a single channel.
13
+
14
+ Parameters
15
+ ----------
16
+ t : np.ndarray
17
+ Time vector
18
+ mean : float
19
+ Mean of the Gaussian
20
+ sigma : float
21
+ Standard deviation of the Gaussian
22
+ amp : float
23
+ Amplitude of the Gaussian
24
+
25
+ Returns
26
+ -------
27
+ np.ndarray
28
+ Intensity vector
29
+ """
30
+ ch: np.ndarray = amp * np.exp(-((t - mean) ** 2) / (2 * sigma**2))
31
+
32
+ return np.round(ch, 2)
33
+
34
+
35
+ def simulate_single_track(track_id: float = 42, mean: float = 0.5) -> pd.DataFrame:
36
+ """Simulate a single track.
37
+
38
+ Parameters
39
+ ----------
40
+ track_id : int
41
+ Track ID
42
+ mean : float
43
+ Temporal mean corresponding to the crossing between the two channels
44
+
45
+ Returns
46
+ -------
47
+ pd.DataFrame
48
+ Dataframe mocking a Trackmate single track import.
49
+ """
50
+ # example data
51
+ phase_percentages = [33.3, 33.3, 33.3]
52
+ center = [20.0, 55.0, 70.0, 95.0]
53
+ sigma = [5.0, 5.0, 10.0, 1.0]
54
+ # create sensor
55
+ sensor = FUCCISASensor(
56
+ phase_percentages=phase_percentages,
57
+ center=center,
58
+ sigma=sigma,
59
+ )
60
+ # create the time vector
61
+ percentage = np.arange(0, 50) / 50
62
+ t = 24 * percentage
63
+ percentage *= 100
64
+
65
+ # create the channels as Gaussian of time
66
+ ch1, ch2 = sensor.get_expected_intensities(percentage)
67
+
68
+ # create dataframe
69
+ df = pd.DataFrame(
70
+ {
71
+ "LABEL": [f"ID{i}" for i in range(len(t))],
72
+ "ID": list(range(len(t))),
73
+ "TRACK_ID": [track_id for _ in range(len(t))],
74
+ "POSITION_X": [np.round(mean * i * 0.02, 2) for i in range(len(t))],
75
+ "POSITION_Y": [np.round(mean * i * 0.3, 2) for i in range(len(t))],
76
+ "POSITION_T": [np.round(i * 0.01, 2) for i in range(len(t))],
77
+ "FRAME": list(range(len(t))),
78
+ "PERCENTAGE": percentage,
79
+ "MEAN_INTENSITY_CH1": [0 for _ in range(len(t))],
80
+ "MEAN_INTENSITY_CH2": [1 for _ in range(len(t))],
81
+ "MEAN_INTENSITY_CH3": ch1,
82
+ "MEAN_INTENSITY_CH4": ch2,
83
+ }
84
+ )
85
+ return df