3030"""
3131
3232
33- from pycellin .classes import CellLineage , CycleLineage
34- from pycellin .classes import Feature
35- from pycellin .classes import Data
33+ from pycellin .classes import CellLineage , CycleLineage , Data , Feature
34+ from pycellin .classes .exceptions import FusionError
3635from pycellin .classes .feature_calculator import NodeGlobalFeatureCalculator
3736
3837# TODO: should I add the word Calc or Calculator to the class names?
@@ -148,7 +147,7 @@ def compute( # type: ignore[override]
148147 return age_in_frame * self .time_step
149148
150149
151- class CellCycleCompleteness (NodeGlobalFeatureCalculator ):
150+ class CycleCompleteness (NodeGlobalFeatureCalculator ):
152151 """
153152 Calculator to compute the cell cycle completeness.
154153
@@ -202,14 +201,97 @@ def compute( # type: ignore[override]
202201 return True
203202
204203
204+ def _get_cell_lin_frames (lineage : CellLineage , noi : int ) -> tuple [int , int ]:
205+ """
206+ Get the frames of the divisions defining the cell cycle.
207+
208+ This function is used by the DivisionTime and DivisionRate calculators.
209+
210+ Parameters
211+ ----------
212+ lineage : CellLineage
213+ Lineage graph containing the node of interest.
214+ noi : int
215+ Node ID (cell_ID) of the cell of interest.
216+
217+ Returns
218+ -------
219+ tuple[int, int]
220+ Frames of the current and previous division.
221+
222+ Raises
223+ ------
224+ KeyError
225+ If the cell is not in the lineage.
226+ FusionError
227+ If the cell has more than one ancestor.
228+ """
229+ if noi not in lineage .nodes :
230+ raise KeyError (f"Cell { noi } not in the lineage." )
231+ cells = lineage .get_cell_cycle (noi )
232+ frame_current_div = lineage .nodes [cells [- 1 ]]["frame" ]
233+ ancestors = list (lineage .predecessors (cells [0 ]))
234+ if len (ancestors ) > 1 :
235+ raise FusionError (noi , lineage .graph ["lineage_ID" ])
236+ elif len (ancestors ) == 0 :
237+ frame_prev_div = lineage .nodes [cells [0 ]]["frame" ]
238+ else :
239+ frame_prev_div = lineage .nodes [ancestors [0 ]]["frame" ]
240+ return frame_current_div , frame_prev_div
241+
242+
243+ def _get_cycle_lin_frames (
244+ data : Data , lineage : CycleLineage , noi : int
245+ ) -> tuple [int , int ]:
246+ """
247+ Get the frames of the divisions defining the cell cycle.
248+
249+ This function is used by the DivisionTime and DivisionRate calculators.
250+
251+ Parameters
252+ ----------
253+ data : Data
254+ Data object containing the lineage.
255+ lineage : CycleLineage
256+ Lineage graph containing the node of interest.
257+ noi : int
258+ Node ID (cell_ID) of the cell of interest.
259+
260+ Returns
261+ -------
262+ tuple[int, int]
263+ Frames of the current and previous division.
264+
265+ Raises
266+ ------
267+ KeyError
268+ If the cycle is not in the lineage.
269+ FusionError
270+ If the cycle has more than one ancestor.
271+ """
272+ if noi not in lineage .nodes :
273+ raise KeyError (f"Cycle { noi } not in the lineage." )
274+ cells = lineage .nodes [noi ]["cells" ]
275+ cell_lin = data .cell_data [lineage .graph ["lineage_ID" ]]
276+ frame_current_div = cell_lin .nodes [cells [- 1 ]]["frame" ]
277+ ancestors = list (lineage .predecessors (noi ))
278+ if len (ancestors ) > 1 :
279+ raise FusionError (noi , lineage .graph ["lineage_ID" ])
280+ elif len (ancestors ) == 0 :
281+ frame_prev_div = cell_lin .nodes [cells [0 ]]["frame" ]
282+ else :
283+ prev_cells = lineage .nodes [ancestors [0 ]]["cells" ]
284+ frame_prev_div = cell_lin .nodes [prev_cells [- 1 ]]["frame" ]
285+ return frame_current_div , frame_prev_div
286+
287+
205288class DivisionTime (NodeGlobalFeatureCalculator ):
206289 """
207290 Calculator to compute the division time of cells.
208291
209- Division time is defined as the time between 2 divisions.
210- It is also the length of the cell cycle of the cell of interest.
211- It is given in frames by default, but can be converted
212- to the time unit of the model if specified.
292+ Division time is defined as the time elapsed between the 2 divisions
293+ that define the cell cycle. It is given in frames by default, but can
294+ be converted to the time unit of the model if specified.
213295 """
214296
215297 def __init__ (self , feature : Feature , time_step : int | float = 1 ):
@@ -250,14 +332,16 @@ def compute( # type: ignore[override]
250332 If the cell or cycle is not in the lineage.
251333 """
252334 if isinstance (lineage , CellLineage ):
253- if noi not in lineage .nodes :
254- raise KeyError (f"Cell { noi } not in the lineage." )
255- cell_cycle = lineage .get_cell_cycle (noi )
256- return len (cell_cycle ) * self .time_step
335+ frame_curr_div , frame_prev_div = _get_cell_lin_frames (lineage , noi )
257336 elif isinstance (lineage , CycleLineage ):
258- if noi not in lineage .nodes :
259- raise KeyError (f"Cycle { noi } not in the lineage." )
260- return lineage .nodes [noi ]["cycle_length" ] * self .time_step
337+ frame_curr_div , frame_prev_div = _get_cycle_lin_frames (data , lineage , noi )
338+ else :
339+ raise TypeError (
340+ f"Lineage must be of type CellLineage or CycleLineage, "
341+ f"not { type (lineage )} ."
342+ )
343+
344+ return (frame_curr_div - frame_prev_div ) * self .time_step
261345
262346
263347class DivisionRate (NodeGlobalFeatureCalculator ):
@@ -270,17 +354,29 @@ class DivisionRate(NodeGlobalFeatureCalculator):
270354 to divisions per time unit of the model if specified.
271355 """
272356
273- def __init__ (self , feature : Feature , time_step : int | float = 1 ):
357+ def __init__ (
358+ self , feature : Feature , time_step : int | float = 1 , use_div_time : bool = False
359+ ):
274360 """
275361 Parameters
276362 ----------
277363 feature : Feature
278364 Feature object to which the calculator is associated.
279365 time_step : int | float, optional
280366 Time step between 2 frames, in time unit. Default is 1.
367+ use_div_time : bool, optional
368+ If True, use the division time already computed in the lineage.
369+ If False, compute the division time from the lineage. Default is False.
370+ The first option is faster but you need to ensure that the division time
371+ is computed and updated BEFORE division rate. This can be ensured
372+ by adding to the model the division time feature before the division
373+ rate feature. Moreover, if `use_div_time` is True, `time_step` will be
374+ ignored: division rate will use the division time unit (e.g. if division
375+ time is in frames, division rate will be in divisions per frame).
281376 """
282377 super ().__init__ (feature )
283378 self .time_step = time_step
379+ self .use_div_time = use_div_time
284380
285381 def compute ( # type: ignore[override]
286382 self , data : Data , lineage : CellLineage | CycleLineage , noi : int
@@ -307,15 +403,40 @@ def compute( # type: ignore[override]
307403 KeyError
308404 If the cell or cycle is not in the lineage.
309405 """
310- if isinstance ( lineage , CellLineage ) :
406+ if self . use_div_time :
311407 if noi not in lineage .nodes :
312- raise KeyError (f"Cell { noi } not in the lineage." )
313- cell_cycle = lineage .get_cell_cycle (noi )
314- return 1 / (len (cell_cycle ) * self .time_step )
408+ if isinstance (lineage , CellLineage ):
409+ lin_txt = "Cell"
410+ elif isinstance (lineage , CycleLineage ):
411+ lin_txt = "Cycle"
412+ else :
413+ raise TypeError (
414+ f"Lineage must be of type CellLineage or CycleLineage, "
415+ f"not { type (lineage )} ."
416+ )
417+ raise KeyError (f"{ lin_txt } { noi } not in the lineage." )
418+ try :
419+ div_time = lineage .nodes [noi ]["division_time" ]
420+ except KeyError :
421+ raise KeyError (
422+ f"Division time not present for cell { noi } in lineage "
423+ f"{ lineage .graph ['lineage_ID' ]} ."
424+ )
425+ return 1 / div_time
426+
427+ if isinstance (lineage , CellLineage ):
428+ frame_curr_div , frame_prev_div = _get_cell_lin_frames (lineage , noi )
315429 elif isinstance (lineage , CycleLineage ):
316- if noi not in lineage .nodes :
317- raise KeyError (f"Cycle { noi } not in the lineage." )
318- return 1 / (lineage .nodes [noi ]["cycle_length" ] * self .time_step )
430+ frame_curr_div , frame_prev_div = _get_cycle_lin_frames (data , lineage , noi )
431+ else :
432+ raise TypeError (
433+ f"Lineage must be of type CellLineage or CycleLineage, "
434+ f"not { type (lineage )} ."
435+ )
436+
437+ div_time = (frame_curr_div - frame_prev_div ) * self .time_step
438+ div_rate = 1 / div_time
439+ return div_rate
319440
320441
321442# class CellPhase(NodeGlobalFeatureCalculator):
0 commit comments