One of the most powerful features of CellODE is its ability to infer and visualize cellular velocity fields. The vector field represents the instantaneous direction and magnitude of cellular state changes, providing insights into:
This vignette demonstrates how to use CellODE’s built-in functions for vector field analysis and visualization.
First, let’s create a Seurat object with simulated trajectory data:
set.seed(42)
n_cells <- 500
n_genes <- 100
# Simulate trajectory
t_true <- seq(0, 1, length.out = n_cells)
# Create expression data (genes change along trajectory)
counts <- matrix(0, nrow = n_genes, ncol = n_cells)
for (i in 1:n_genes) {
phase <- runif(1, 0, 2 * pi)
amplitude <- runif(1, 3, 10)
counts[i, ] <- rpois(n_cells, lambda = pmax(1, amplitude * (1 + sin(t_true * 2 * pi + phase))))
}
rownames(counts) <- paste0("Gene", seq_len(n_genes))
colnames(counts) <- paste0("Cell", seq_len(n_cells))
# Create Seurat object
srt <- CreateSeuratObject(counts = counts)
# Add UMAP embedding (curved trajectory)
umap1 <- t_true * 10 + rnorm(n_cells, sd = 0.5)
umap2 <- 4 * sin(t_true * 2 * pi) + rnorm(n_cells, sd = 0.3)
umap_embeddings <- matrix(c(umap1, umap2), ncol = 2)
rownames(umap_embeddings) <- colnames(srt)
colnames(umap_embeddings) <- c("UMAP_1", "UMAP_2")
srt[["umap"]] <- CreateDimReducObject(
embeddings = umap_embeddings,
key = "UMAP_",
assay = "RNA"
)
# Add pseudotime
srt$pseudotime <- t_true + rnorm(n_cells, sd = 0.02)
srt$pseudotime <- pmax(0, pmin(1, srt$pseudotime))In a real workflow, you would train a CellODE model. Here we simulate the output:
# Simulate latent space (normally from model output)
n_latent <- 5
zs <- matrix(rnorm(n_cells * n_latent), ncol = n_latent)
# Make latent space correlate with trajectory
zs[, 1] <- t_true * 2 + rnorm(n_cells, sd = 0.2)
zs[, 2] <- sin(t_true * pi) + rnorm(n_cells, sd = 0.1)
rownames(zs) <- colnames(srt)
# Simulate vector field in latent space
vf <- matrix(0, nrow = n_cells, ncol = n_latent)
vf[, 1] <- 1 + rnorm(n_cells, sd = 0.1) # Constant velocity in z1
vf[, 2] <- cos(t_true * pi) * pi / 2 + rnorm(n_cells, sd = 0.1) # Derivative of sin
rownames(vf) <- colnames(srt)
# Store in Seurat object (as CellODE does)
srt@misc$X_zs <- zs
srt@misc$X_VF <- vfUse cosine_similarity() to compute transition
probabilities in latent space:
Use vector_field_embedding() to project velocities to
UMAP space:
# Get embedding coordinates
E <- Embeddings(srt, "umap")
# Compute velocity in embedding space
V_embed <- vector_field_embedding(
T_mat = T_mat,
E = E,
scale = 10
)
cat("Embedding velocity dimensions:", dim(V_embed), "\n")
#> Embedding velocity dimensions: 500 2
cat("Mean velocity magnitude:", mean(sqrt(rowSums(V_embed^2))), "\n")
#> Mean velocity magnitude: 0.240518Use vector_field_embedding_grid() for cleaner
visualization:
Use plot_pseudotime() to visualize pseudotime with
trajectory arrows:
# Basic pseudotime plot
p1 <- plot_pseudotime(
srt,
time_key = "pseudotime",
arrow_show = TRUE,
arrow_density = 1.5,
point_palette = "Spectral",
title = "Pseudotime with Direction Arrows"
)
print(p1)Use plot_vector_field() with grid mode for clean
visualization:
# Vector field - grid mode
p2 <- plot_vector_field(
srt,
plot_type = "grid",
color_by = "pseudotime",
arrow_density = 0.8,
arrow_color = "grey20",
point_palette = "Spectral",
title = "Vector Field (Grid Mode)"
)
print(p2)Stream mode provides colored arrows based on velocity magnitude:
# Vector field - stream mode with colored arrows
p3 <- plot_vector_field(
srt,
plot_type = "stream",
color_by = "pseudotime",
arrow_density = 1.2,
point_palette = "viridis",
title = "Vector Field (Stream Mode)"
)
print(p3)Raw mode shows arrows at each cell position:
# Vector field - raw mode (per-cell arrows)
p4 <- plot_vector_field(
srt,
plot_type = "raw",
color_by = "pseudotime",
arrow_density = 0.3, # Subsample for clarity
arrow_color = "navy",
point_palette = "magma",
title = "Vector Field (Raw Mode)"
)
print(p4)# Custom arrow styling
p_custom <- plot_vector_field(
srt,
plot_type = "grid",
color_by = "pseudotime",
arrow_type = "closed", # Filled arrowheads
arrow_color = "#E63946", # Red arrows
arrow_density = 0.6,
arrow_length = 0.2,
arrow_width = 0.8,
arrow_angle = 30,
point_palette = "RdYlBu",
point_size = 0.8,
title = "Custom Arrow Styling"
)
print(p_custom)The Neural ODE in CellODE defines the velocity in latent space:
\[\mathbf{v}_\text{latent}(t) = \frac{d\mathbf{z}}{dt} = f_\theta(\mathbf{z}(t), t)\]
The cosine_similarity() function computes:
\[\text{cos\_sim}_{ij} = \frac{\mathbf{v}_i \cdot (\mathbf{z}_j - \mathbf{z}_i)}{||\mathbf{v}_i|| \cdot ||\mathbf{z}_j - \mathbf{z}_i||}\]
The transition probabilities are computed as:
\[T_{ij} = \text{sign}(\text{cos\_sim}_{ij}) \cdot (\exp(|\text{cos\_sim}_{ij}| \cdot s) - 1)\]
The vector_field_embedding() function computes:
\[\mathbf{v}_i^\text{embed} = \sum_j T_{ij} \cdot \frac{\mathbf{e}_j - \mathbf{e}_i}{||\mathbf{e}_j - \mathbf{e}_i||}\]
| Aspect | CellODE | RNA Velocity (scVelo) |
|---|---|---|
| Input | Gene expression only | Spliced/unspliced counts |
| Model | Neural ODE | Steady-state/dynamic model |
| Time inference | Joint with dynamics | Separate |
| R package | Native R | Python (reticulate needed) |
CellODE provides a complete workflow for vector field analysis:
| Function | Purpose |
|---|---|
cosine_similarity() |
Compute transition probabilities |
vector_field_embedding() |
Project to embedding space |
vector_field_embedding_grid() |
Grid-based smoothing |
plot_vector_field() |
Visualize vector field |
plot_pseudotime() |
Visualize pseudotime with arrows |
sessionInfo()
#> R version 4.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] Seurat_5.5.0 SeuratObject_5.4.0 sp_2.2-1 ggplot2_4.0.3
#> [5] CellODE_1.0.0 rmarkdown_2.31
#>
#> loaded via a namespace (and not attached):
#> [1] deldir_2.0-4 pbapply_1.7-4 gridExtra_2.3
#> [4] rlang_1.2.0 magrittr_2.0.5 RcppAnnoy_0.0.23
#> [7] otel_0.2.0 spatstat.geom_3.8-1 matrixStats_1.5.0
#> [10] ggridges_0.5.7 compiler_4.6.0 reshape2_1.4.5
#> [13] png_0.1-9 callr_3.7.6 vctrs_0.7.3
#> [16] stringr_1.6.0 pkgconfig_2.0.3 fastmap_1.2.0
#> [19] labeling_0.4.3 promises_1.5.0 ps_1.9.3
#> [22] torch_0.17.0 purrr_1.2.2 bit_4.6.0
#> [25] xfun_0.57 cachem_1.1.0 jsonlite_2.0.0
#> [28] goftest_1.2-3 later_1.4.8 spatstat.utils_3.2-3
#> [31] irlba_2.3.7 parallel_4.6.0 cluster_2.1.8.2
#> [34] R6_2.6.1 ica_1.0-3 spatstat.data_3.1-9
#> [37] stringi_1.8.7 bslib_0.11.0 RColorBrewer_1.1-3
#> [40] reticulate_1.46.0 spatstat.univar_3.2-0 parallelly_1.47.0
#> [43] scattermore_1.2 lmtest_0.9-40 jquerylib_0.1.4
#> [46] Rcpp_1.1.1-1.1 knitr_1.51 tensor_1.5.1
#> [49] future.apply_1.20.2 zoo_1.8-15 sctransform_0.4.3
#> [52] httpuv_1.6.17 Matrix_1.7-5 splines_4.6.0
#> [55] igraph_2.3.1 tidyselect_1.2.1 abind_1.4-8
#> [58] yaml_2.3.12 spatstat.random_3.5-0 spatstat.explore_3.8-1
#> [61] codetools_0.2-20 miniUI_0.1.2 processx_3.9.0
#> [64] listenv_0.10.1 plyr_1.8.9 lattice_0.22-9
#> [67] tibble_3.3.1 shiny_1.13.0 withr_3.0.2
#> [70] S7_0.2.2 ROCR_1.0-12 evaluate_1.0.5
#> [73] Rtsne_0.17 future_1.70.0 fastDummies_1.7.6
#> [76] survival_3.8-6 polyclip_1.10-7 fitdistrplus_1.2-6
#> [79] pillar_1.11.1 KernSmooth_2.23-26 plotly_4.12.0
#> [82] generics_0.1.4 RcppHNSW_0.6.0 scales_1.4.0
#> [85] coro_1.1.0 globals_0.19.1 xtable_1.8-8
#> [88] glue_1.8.1 lazyeval_0.2.3 maketools_1.3.2
#> [91] tools_4.6.0 sys_3.4.3 data.table_1.18.4
#> [94] RSpectra_0.16-2 RANN_2.6.2 buildtools_1.0.0
#> [97] dotCall64_1.2 cowplot_1.2.0 grid_4.6.0
#> [100] tidyr_1.3.2 nlme_3.1-169 patchwork_1.3.2
#> [103] cli_3.6.6 spatstat.sparse_3.2-0 spam_2.11-3
#> [106] viridisLite_0.4.3 dplyr_1.2.1 uwot_0.2.4
#> [109] gtable_0.3.6 sass_0.4.10 digest_0.6.39
#> [112] progressr_0.19.0 ggrepel_0.9.8 htmlwidgets_1.6.4
#> [115] farver_2.1.2 htmltools_0.5.9 lifecycle_1.0.5
#> [118] httr_1.4.8 mime_0.13 bit64_4.8.2
#> [121] MASS_7.3-65