acia.analysis.doubling_time#

Per-cell doubling time from lineage topology, and its temporal evolution.

This module walks a TrackastraTracker-style tracklet_graph (one node per cell cycle, with start_frame/end_frame attributes; edges encode divisions) to compute, for every cleanly resolved division, the real-time duration between a mother cell’s birth and its daughters’ birth – compute_doubling_times(). plot_doubling_time_hose() then turns the resulting per-cell table into a “temporal hose”: a mean doubling-time curve with percentile-bootstrap confidence bands, showing how division timing evolves over the course of an experiment.

This is unrelated to acia.analysis.growth_rate.estimate_growth_rate(), which fits a whole-population exponential curve and derives an aggregate doubling time from its slope – nothing here is per-cell or lineage-aware.

acia.analysis.doubling_time.DEFAULT_CI_LEVELS = (0.95,)#

default confidence levels for the temporal hose plot’s bootstrap bands

acia.analysis.doubling_time.compute_doubling_times(tracklet_graph, source)[source]#

Compute the real-time doubling duration of every cleanly resolved division.

A tracklet n qualifies as a “clean division” only if it has exactly one identified mother and exactly two daughters: tracklet_graph.in_degree(n) == 1 and tracklet_graph.out_degree(n) == 2. Tracklets with in_degree(n) == 0 (a root, present at the movie start – birth time unknown/left-censored), out_degree(n) == 0 (alive at the movie end, exited the field of view, or lost tracking – division time unknown/right-censored), or out_degree(n) > 2 (a tracking/merge artifact, not a clean split) are excluded.

Time is always read via source.timepoints[frame_idx] – a per-frame pint Quantity array resolved once at the source level – never computed by hand as frame_delta * frame_interval. This is what makes the result correct for both a nominal fixed frame interval and genuinely irregular real timestamps.

Parameters:
Returns:

A DataFrame indexed by the qualifying tracklet’s label (index name "tracklet"), with columns start_time, end_time and doubling_time as pint[<unit>] columns (unit-safe, matching acia.analysis.attach_units()’s pint representation), where <unit> is source.timepoints’s unit. The unit is also recorded in df.attrs["units"] for consistency with the rest of acia.analysis. Empty (no qualifying division) if none is found.

Raises:

ValueError – if source.timepoints is None (no frame_interval and no explicit timepoints were set on source) – raised immediately, before tracklet_graph is touched at all.

Return type:

pd.DataFrame

acia.analysis.doubling_time.plot_doubling_time_hose(doubling_times_df, source, *, time_grid=None, ci_levels=(0.95,), min_n=5, n_bootstrap=1000, random_state=0, ax=None)[source]#

Plot mean doubling time evolving over the experiment as a “temporal hose”.

At every time t in time_grid, a cell contributes its doubling_time to the cross-section at t for as long as it was alive – i.e. whenever start_time <= t <= end_time – not just at the single instant it divided. Grid points with fewer than min_n alive-and-qualifying cells are left as NaN (a gap in the plotted line and bands, rather than a value computed from too few observations).

Why a percentile bootstrap instead of a parametric mean +/- SEM band: a symmetric normal-approximation interval can produce a negative lower bound whenever the sample is small or noisy relative to its mean – which happens easily near the min_n threshold – and a negative doubling time is nonsensical for a duration. A percentile-of-observed-values bootstrap is built entirely from resampled means of actually-observed, strictly positive values, so it structurally cannot produce a negative bound. It also yields every requested ci_levels percentile pair from the same bootstrap sample at no extra resampling cost, unlike computing each parametric band separately.

Parameters:
  • doubling_times_df (pd.DataFrame) – the output of compute_doubling_times() (start_time/end_time/doubling_time pint columns).

  • source (ImageSequenceSource) – the time-calibrated image sequence the tracklets were derived from; used to default time_grid to source.timepoints.

  • time_grid – pint Quantity array of times to evaluate the hose at. Defaults to source.timepoints when None.

  • ci_levels – confidence levels for the bootstrap bands, e.g. (0.95,) (default) or (0.5, 0.8, 0.95) for a multi-level fan chart. Each level is read from the same bootstrap sample at no extra cost.

  • min_n (int) – minimum number of alive-and-qualifying cells required at a grid point to compute a mean/CI there; below this, the point is recorded as NaN (default 5). Must be >= 1. Note: min_n=1 produces a mathematically-correct-but-zero-width band at any point with exactly one alive-and-qualifying cell, since the bootstrap then resamples that same single value every time – this is expected behavior reflecting genuinely low sample confidence at that point, not a bug.

  • n_bootstrap (int) – number of bootstrap resamples per grid point (default 1000). Must be >= 1 (same zero-width-band caveat as min_n=1 applies at n_bootstrap=1).

  • random_state – seed (or numpy.random.Generator-compatible value) for numpy.random.default_rng(), fixed by default (0) so repeated calls are reproducible.

  • ax (Axes | None) – optional matplotlib Axes to draw into. When None a new figure and axes are created.

Returns:

A matplotlib Figure with the mean doubling-time curve and one shaded fill_between band per ci_levels entry (multiple levels nest as a fan chart around the same mean line). NaN regions render as natural gaps. The figure is not shown (no plt.show()).

Raises:

ValueError – if time_grid is None and source.timepoints is also None (no time calibration to default to).

Return type:

Figure