Purpose
This tutorial presents a toy example of ODEinherit, using the dataset from our paper to estimate inheritance between mother and daughter cells.
A successful installation of ODEinherit
is required. See
the last section of this article for a list of all system and package
dependencies used to create this tutorial.
For a single mother–daughter cell pair, the entire workflow typically finishes in under 20 minutes when run on 8 cores of an Apple M1 chip.
Overview
Here we present the workflow for a single mother–daughter cell pair,
the same pair used to create Figure 2 in the paper. See
?cell_lineage_data
for more details about the dataset.
Step 0: Load the package and select a mother–daughter cell pair
cell_lineage_data <- ODEinherit::cell_lineage_data
names(cell_lineage_data)
#> [1] "metadata" "time_series"
time_series <- cell_lineage_data$time_series
metadata <- cell_lineage_data$metadata
var_names <- names(time_series)
Following convention, we refer to the time series as
“trajectories.”
The code below extracts and plots the trajectories for the selected
cells.
(Note: In the paper, each trajectory is centered to mean 0; here, we
omit centering for clearer illustration.)
# extract trajectories by specifying cell id
idx_M <- which(metadata$cell_id == "3-20-1")
idx_D <- which(metadata$cell_id == "3-20-17")
# birth time point indices (row indices of matrices in `time_series`)
btp_idx_M <- metadata$cell_birth_timepoint[idx_M]
btp_idx_D <- metadata$cell_birth_timepoint[idx_D]
# reshape data to the dimension: (# total time points, # variables)
Y_M_full <- sapply(time_series, function(mat){mat[idx_M,]})
Y_D_full <- sapply(time_series, function(mat){mat[idx_D,]})
dim(Y_M_full)
#> [1] 240 6
# plot the trajectories of the first variable
var_idx <- 1
yj_M <- Y_M_full[,var_idx]
yj_D <- Y_D_full[,var_idx]
par(mfrow = c(1,1))
time_grid <- 1:ncol(time_series[[1]])
plot(NA, type = "n",
main = paste0("Trajectory plot of variable ", var_idx, " (", var_names[var_idx], ")"),
xlab = "Time index", ylab = "Value",
xlim = range(time_grid, na.rm = T),
ylim = range(c(yj_M, yj_D), na.rm = T))
lines(time_grid, yj_M,
lty = 1, col = "darkred")
lines(time_grid, yj_D,
lty = 1, col = "darkgreen")
legend("topright",
legend = c("Mother", "Daughter"),
lty = 1,
col = c("darkred", "darkgreen"),
cex = 0.8)
Step 1: Network estimation with KernelODE
We begin with two numeric matrices, Y_M
and
Y_D
, representing the mother and daughter cells.
Rows correspond to time points, and columns correspond to protein
variables.
The ODE regulatory network for each cell is estimated
separately using KernelODE.
Before applying KernelODE, we:
- Remove the portion of each trajectory before cell birth.
- Standardize the observation time points to the interval
[0, 1]
for each cell. - Remove the linear trend in the time series to adjust for time effects.
In Step 1 of KernelODE, we fit cubic smoothing
splines to obtain smoothed trajectory estimates.
In Step 2, we estimate ODE regulatory networks within
an RKHS modeling framework.
For this example, we use a first-order Matern kernel.
See the original KernelODE paper for more details on the algorithm.
# remove the pre-birth portion
Y_M <- Y_M_full[btp_idx_M:nrow(Y_M_full),]
Y_D <- Y_D_full[btp_idx_D:nrow(Y_D_full),]
# Note: Y_M and Y_D now start at each cell’s birth (corresponding to their first row).
# standardize observation time to [0, 1] for each cell
n_M <- nrow(Y_M)
obs_time_M <- 1/n_M * (1:n_M)
n_D <- nrow(Y_D)
obs_time_D <- 1/n_D * (1:n_D)
# remove linear trend for each variable
linear_trend_M <- matrix(NA, nrow = nrow(Y_M), ncol = ncol(Y_M))
linear_trend_D <- matrix(NA, nrow = nrow(Y_D), ncol = ncol(Y_D))
for (j in 1:ncol(Y_M)){
yj <- Y_M[,j]
lt_fitted <- lm(yj ~ obs_time_M)$fitted.values # fitted linear trend
yj <- yj - lt_fitted # remove linear trend
Y_M[,j] <- yj
linear_trend_M[,j] <- lt_fitted
}
for (j in 1:ncol(Y_D)){
yj <- Y_D[,j]
lt_fitted <- lm(yj ~ obs_time_D)$fitted.values # fitted linear trend
yj <- yj - lt_fitted # remove linear trend
Y_D[,j] <- yj
linear_trend_D[,j] <- lt_fitted
}
Here we easily define a pipeline for network estimation:
kernelODE_pipeline <- function(Y,
obs_time,
kernel,
kernel_params,
prune_thres = 0.05, # network pruning threshold
depth = NULL # maximum number of regulator edges to prune for each variable
){
tt <- 0.001*(1:1000) # time grid for numerical integration, does not include 0
# KernelODE step 1: smooth the observed trajectories
res_step1 <- ODEinherit::kernelODE_step1(Y = Y,
obs_time = obs_time,
tt = tt)
yy_smth <- res_step1$yy_smth
# KernelODE step 2: estimate the derivative functions Fj's
res_step2 <- ODEinherit::kernelODE_step2(Y = Y,
obs_time = obs_time,
yy_smth = yy_smth,
tt = tt,
kernel = kernel,
kernel_params = kernel_params,
interaction_term = FALSE, # without interaction
verbose = 0)
network_est_original <- res_step2$network_est
# prune the network
res_prune <- ODEinherit::prune_network(network_original = network_est_original, # network to prune
prune_thres = prune_thres,
depth = depth,
Y_list = list(Y), # here we prune it cellwise
yy_smth_list = list(yy_smth),
obs_time_list = list(obs_time),
tt = tt,
kernel = kernel,
kernel_params_list = list(kernel_params),
interaction_term = FALSE,
parallel = TRUE,
verbose = 0)
network_est_pruned <- res_prune$network_pruned
return (list(network_est_pruned = network_est_pruned,
yy_smth = yy_smth,
tt = tt))
}
We now apply the pipeline separately to the mother and daughter cells
to obtain their respective regulatory networks. The pruning step is the
most time-consuming part in the pipeline. As a faster alternative, you
can specify nzero
argument in
ODEinherit::kernelODE_step2()
to directly control the
number of regulators selected by the Lasso.
# kernel configuration
kernel <- "matern"
kernel_params <- list(list(lengthscale=1)) # recycled for all variables, used for both mother and daughter cells
# run the pipeline separately for mother and daughter
res_KernelODE_M <- kernelODE_pipeline(Y = Y_M,
obs_time = obs_time_M,
kernel = kernel,
kernel_params = kernel_params)
#> Note: R2 increases when removing these edges: 1->1
res_KernelODE_D <- kernelODE_pipeline(Y = Y_D,
obs_time = obs_time_D,
kernel = kernel,
kernel_params = kernel_params)
#> Note: R2 increases when removing these edges: 4->2, 6->2, 1->4, 2->4, 3->4, 2->5, 3->5, 5->5, 2->6, 3->6, 5->6
#> Note: R2 increases when removing these edges: 2->2
# extract results
network_est_M <- res_KernelODE_M$network_est_pruned
network_est_D <- res_KernelODE_D$network_est_pruned
yy_smth_M <- res_KernelODE_M$yy_smth
yy_smth_D <- res_KernelODE_D$yy_smth
tt_M <- res_KernelODE_M$tt
tt_D <- res_KernelODE_D$tt
Step 2: Evaluate networks via daughter trajectory recovery
We refit KernelODE to recover the daughter trajectories using the mother-derived and daughter-derived networks, respectively, to evaluate how well each explains variation in the daughter trajectories. Goodness-of-fit is quantified using a heuristic \(R^2\) metric.
KODE_refit_MtoD <- ODEinherit::refit_kernel_ODE(Y = Y_D, # target: daughter's observed trajectories
obs_time = obs_time_D,
yy_smth = yy_smth_D,
tt = tt_D,
kernel = kernel, # same config as used for network estimation
kernel_params = kernel_params, # same config as used for network estimation
interaction_term = FALSE,
adj_matrix = network_est_M) # use mother network
KODE_refit_DtoD <- ODEinherit::refit_kernel_ODE(Y = Y_D, # target: daughter's observed trajectories
obs_time = obs_time_D,
yy_smth = yy_smth_D,
tt = tt_D,
kernel = kernel,
kernel_params = kernel_params,
interaction_term = FALSE,
adj_matrix = network_est_D) # use daughter network
We now plot the reconstructed daughter trajectories using each network.
Caveat: The refitted trajectories returned by
refit_kernel_ODE()
follow the same format as the observed
trajectories fed into the pipeline (i.e., with the pre–birth portion
removed).
par(mfrow = c(1,2))
# extract refitted daughter trajectories (both are /daughter/ trajectories!)
Y_refit_MtoD <- KODE_refit_MtoD$Y_refit
Y_refit_DtoD <- KODE_refit_DtoD$Y_refit
# add the linear trend back
Y_refit_MtoD <- Y_refit_MtoD + linear_trend_D
Y_refit_DtoD <- Y_refit_DtoD + linear_trend_D
# pad NA for the pre-birth portion
Y_refit_MtoD <- rbind(matrix(NA, nrow = btp_idx_D-1, ncol = ncol(Y_refit_MtoD)), Y_refit_MtoD)
Y_refit_DtoD <- rbind(matrix(NA, nrow = btp_idx_D-1, ncol = ncol(Y_refit_DtoD)), Y_refit_DtoD)
# select a variable to plot
var_idx <- 1
yj_MtoD <- Y_refit_MtoD[,var_idx]
yj_DtoD <- Y_refit_DtoD[,var_idx]
yj_D <- Y_D_full[,var_idx]
# plot Mother -> Daughter recovery
plot(NA, type = "n",
main = paste0("Daughter traj. reconstructed by mother network\nVar ", var_idx, " (", var_names[var_idx], ")"),
cex.main = 0.8,
xlab = "Time index", ylab = "Value",
xlim = range(time_grid, na.rm = T),
ylim = range(c(yj_MtoD, yj_D), na.rm = T)
)
lines(time_grid, yj_D,
lty = 1, col = "darkgreen") # observed daughter traj
lines(time_grid, yj_MtoD,
lty = 2, col = "darkred") # daughter traj recovered by mother
legend("topright",
legend = c("obs. D.", "M to D"),
lty = c(1, 2),
col = c("darkgreen", "darkred"),
cex = 0.8)
# plot Daughter -> Daughter recovery
plot(NA, type = "n",
main = paste0("Daughter traj. reconstructed by its own network\nVar ", var_idx, " (", var_names[var_idx], ")"),
cex.main = 0.8,
xlab = "Time index", ylab = "Value",
xlim = range(time_grid, na.rm = T),
ylim = range(c(yj_DtoD, yj_D), na.rm = T)
)
lines(time_grid, yj_D,
lty = 1, col = "darkgreen") # observed daughter traj
lines(time_grid, yj_DtoD,
lty = 2, col = "darkgreen") # daughter traj recovered by itself
legend("topright",
legend = c("obs. D.", "D to D"),
lty = c(1, 2),
col = c("darkgreen", "darkgreen"),
cex = 0.8)
Step 3: Calculate the inheritance score from the trajectory recovery metrics
We extract the \(R^2\) metrics (\(R^{2(M \to D)}\) and \(R^{2(D \to D)}\)) and calculate the inheritance score as \(\pi^{(M \to D)} = \frac{R^{2(M \to D)}}{R^{2(D \to D)}}\) with the value capped at 1.
R2_MtoD <- KODE_refit_MtoD$metrics$R2
R2_DtoD <- KODE_refit_DtoD$metrics$R2
pi_score <- min(R2_MtoD / R2_DtoD, 1)
pi_score
#> [1] 0.6315346
Setup
The following shows the suggested package versions that the developer (GitHub username: WenbinWu2001) used when developing the ODEinherit package.
> devtools::session_info()
─ Session info ───────────────────────────────────────────────────────
setting value
version R version 4.4.2 (2024-10-31)
os macOS Sequoia 15.3.2
system aarch64, darwin20
ui RStudio
language (EN)
collate en_US.UTF-8
ctype en_US.UTF-8
tz America/New_York
date 2025-08-13
rstudio 2024.12.1+563 Kousa Dogwood (desktop)
pandoc 3.2 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
quarto 1.5.57 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/quarto
─ Packages ───────────────────────────────────────────────────────────
package * version date (UTC) lib source
cachem 1.1.0 2024-05-16 [1] CRAN (R 4.4.1)
callr 3.7.6 2024-03-25 [1] CRAN (R 4.4.0)
cli 3.6.4 2025-02-13 [1] CRAN (R 4.4.1)
codetools 0.2-20 2024-03-31 [1] CRAN (R 4.4.2)
desc 1.4.3 2023-12-10 [1] CRAN (R 4.4.1)
devtools 2.4.5 2022-10-11 [1] CRAN (R 4.4.0)
digest 0.6.37 2024-08-19 [1] CRAN (R 4.4.1)
ellipsis 0.3.2 2021-04-29 [1] CRAN (R 4.4.0)
evaluate 1.0.3 2025-01-10 [1] CRAN (R 4.4.1)
fastmap 1.2.0 2024-05-15 [1] CRAN (R 4.4.1)
foreach 1.5.2 2022-02-02 [1] CRAN (R 4.4.0)
fs 1.6.5 2024-10-30 [1] CRAN (R 4.4.1)
glmnet 4.1-8 2023-08-22 [1] CRAN (R 4.4.0)
glue 1.8.0 2024-09-30 [1] CRAN (R 4.4.1)
htmltools 0.5.8.1 2024-04-04 [1] CRAN (R 4.4.0)
htmlwidgets 1.6.4 2023-12-06 [1] CRAN (R 4.4.0)
httpuv 1.6.15 2024-03-26 [1] CRAN (R 4.4.0)
iterators 1.0.14 2022-02-05 [1] CRAN (R 4.4.0)
knitr 1.49 2024-11-08 [1] CRAN (R 4.4.1)
later 1.4.1 2024-11-27 [1] CRAN (R 4.4.1)
lattice 0.22-6 2024-03-20 [1] CRAN (R 4.4.2)
lifecycle 1.0.4 2023-11-07 [1] CRAN (R 4.4.0)
magrittr 2.0.3 2022-03-30 [1] CRAN (R 4.4.0)
Matrix 1.7-2 2025-01-23 [1] CRAN (R 4.4.1)
memoise 2.0.1 2021-11-26 [1] CRAN (R 4.4.0)
mime 0.12 2021-09-28 [1] CRAN (R 4.4.0)
miniUI 0.1.1.1 2018-05-18 [1] CRAN (R 4.4.0)
ODEinherit * 1.0.0 2025-08-13 [1] local
pillar 1.10.1 2025-01-07 [1] CRAN (R 4.4.1)
pkgbuild 1.4.6 2025-01-16 [1] CRAN (R 4.4.1)
pkgconfig 2.0.3 2019-09-22 [1] CRAN (R 4.4.0)
pkgload 1.4.0 2024-06-28 [1] CRAN (R 4.4.0)
processx 3.8.5 2025-01-08 [1] CRAN (R 4.4.1)
profvis 0.4.0 2024-09-20 [1] CRAN (R 4.4.1)
promises 1.3.2 2024-11-28 [1] CRAN (R 4.4.1)
ps 1.8.1 2024-10-28 [1] CRAN (R 4.4.1)
purrr 1.0.4 2025-02-05 [1] CRAN (R 4.4.1)
R6 2.6.1 2025-02-15 [1] CRAN (R 4.4.1)
Rcpp 1.0.14 2025-01-12 [1] CRAN (R 4.4.1)
remotes 2.5.0 2024-03-17 [1] CRAN (R 4.4.1)
rlang 1.1.5 2025-01-17 [1] CRAN (R 4.4.1)
rmarkdown 2.29 2024-11-04 [1] CRAN (R 4.4.1)
rprojroot 2.0.4 2023-11-05 [1] CRAN (R 4.4.1)
rstudioapi 0.17.1 2024-10-22 [1] CRAN (R 4.4.1)
sessioninfo 1.2.3 2025-02-05 [1] CRAN (R 4.4.1)
shape 1.4.6.1 2024-02-23 [1] CRAN (R 4.4.1)
shiny 1.10.0 2024-12-14 [1] CRAN (R 4.4.1)
survival 3.8-3 2024-12-17 [1] CRAN (R 4.4.1)
tibble 3.2.1 2023-03-20 [1] CRAN (R 4.4.0)
urlchecker 1.0.1 2021-11-30 [1] CRAN (R 4.4.0)
usethis 3.1.0 2024-11-26 [1] CRAN (R 4.4.1)
vctrs 0.6.5 2023-12-01 [1] CRAN (R 4.4.0)
xfun 0.50 2025-01-07 [1] CRAN (R 4.4.1)
xtable 1.8-4 2019-04-21 [1] CRAN (R 4.4.0)
yaml 2.3.10 2024-07-26 [1] CRAN (R 4.4.1)