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/sensor.py CHANGED
@@ -1,14 +1,17 @@
1
+ import logging
1
2
  from abc import ABC, abstractmethod
2
- from typing import List, Union
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: Union[float, np.ndarray], center: float, sigma: float, sign: float = 1.0
11
- ) -> Union[float, np.ndarray]:
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: Union[float, np.ndarray],
22
+ x: float | np.ndarray,
20
23
  center: float,
21
24
  sigma: float,
22
25
  offset_intensity: float = 0,
23
- ) -> Union[float, np.ndarray]:
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: Union[float, np.ndarray],
32
+ x: float | np.ndarray,
30
33
  center: float,
31
34
  sigma: float,
32
35
  offset_intensity: float = 0,
33
- ) -> Union[float, np.ndarray]:
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: List[float],
45
- center: List[float],
46
- sigma: List[float],
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) -> List[str]:
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) -> List[float]:
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: List[float]) -> None:
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[List[bool], "pd.Series[bool]"]) -> str:
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
- Discrete phase refers to, for example, G1 or S phase.
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: List[float]
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: List[float], sigma: List[float]
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: Union[float, np.ndarray]
118
- ) -> List[Union[float, np.ndarray]]:
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: List[float], center: List[float], sigma: List[float]
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) -> List[str]:
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[List[bool], "pd.Series[bool]"]) -> str:
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
- print(
277
- "WARNING: could not infer percentage in SG2M phase, using average phase"
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: List[float]
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: Union[float, np.ndarray]
304
- ) -> List[Union[float, np.ndarray]]:
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: List[float], center: List[float], sigma: List[float]
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) -> List[str]:
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[List[bool], "pd.Series[bool]"]) -> str:
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: List[float]
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: Union[float, np.ndarray]
441
- ) -> List[Union[float, np.ndarray]]:
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
 
@@ -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
- """Helper function to get value of feature."""
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
- assert len(value) == 1
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 DF with penalties for tracking.
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 similar to TrackMate,
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
- Each feature penalty is:
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
 
@@ -1,4 +1,17 @@
1
- """Convenience functions for fucciphase."""
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",
@@ -1,13 +1,10 @@
1
- from typing import List
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: List[float]) -> None:
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: Union[np.ndarray, List[List[float]]],
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: Union[np.ndarray, List[float]]
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:
@@ -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(vector: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
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 = vector.max()
54
- min_ch = vector.min()
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: Union[str, List[str]],
87
+ channels: str | list[str],
67
88
  use_moving_average: bool = True,
68
89
  moving_average_window: int = 7,
69
- manual_min: Optional[List[float]] = None,
70
- manual_max: Optional[List[float]] = None,
90
+ manual_min: list[float] | None = None,
91
+ manual_max: list[float] | None = None,
71
92
  track_id_name: str = "TRACK_ID",
72
- ) -> List[str]:
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
- norm_ch = norm(df[avg_channel])
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
- ) -> Tuple[pd.Index, np.ndarray]:
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],
@@ -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
- print("WARNING: No percentages to postprocess")
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
- 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
+ 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
  )
@@ -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
- # example data
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
- from typing import List, Optional
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
- highest_track_idx = track_df[track_id_name].max()
97
- highest_track_idx_counter = highest_track_idx
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 range(highest_track_idx):
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: Optional[np.ndarray]
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: Optional[np.ndarray]
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: Optional[np.ndarray]
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: Optional[str] = None,
361
- stroke_width: Optional[float] = None,
362
- ) -> List[str]:
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
- print("Warning: make sure that you updated the spot names using TrackMate actions!")
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"