After PCA, each MEA culture is a point in state space, and a
timecourse is a path through it. NOVA’s
nova_trajectory_summary() describes that path
simply and honestly: how far each condition moved away
from baseline, how directly, and when — using the replicate wells to put
error bands on the key figure.
It deliberately stops there. A typical MEA experiment has only a handful of timepoints and a few wells per condition: enough to say “this drug moved the network far and fast and it held”, but not enough to fit velocities, “stable vs unstable” regimes, or transition models without turning noise into false precision. So those are intentionally out of scope.
This runs the standard NOVA workflow on the bundled MEA agonist data when it is present, and otherwise falls back to a small simulated dataset with the same shape, so the vignette always builds.
example_dir <- system.file("extdata", "MEA Neuronal Agonists", package = "NOVA")
if (!nzchar(example_dir)) {
alt <- file.path("..", "Example", "MEA Neuronal Agonists")
if (dir.exists(alt)) example_dir <- alt
}
pca <- NULL
if (nzchar(example_dir) && dir.exists(example_dir)) {
proc <- tryCatch(process_mea_flexible(
main_dir = example_dir, selected_experiments = c("MEA012", "MEA013"),
grouping_variables = c("Experiment", "Treatment", "Well"),
baseline_timepoint = "baseline", verbose = FALSE), error = function(e) NULL)
if (!is.null(proc))
pca <- tryCatch(pca_analysis_enhanced(processing_result = proc,
grouping_variables = c("Treatment", "Well"), verbose = FALSE), error = function(e) NULL)
}
if (is.null(pca)) {
timepoints <- c("baseline", "0min", "15min", "30min", "1h", "1h30", "2h")
tnum <- nova_time_to_minutes(timepoints); tnum[1] <- -10
s <- (tnum - min(tnum)) / (max(tnum) - min(tnum))
proto <- list(PBS = function(s) cbind(0 * s, 0 * s),
KA = function(s) cbind(5 * (1 - exp(-3 * s)), 2 * (1 - exp(-3 * s))),
Gabazine = function(s) cbind(-6 * s, 4 * s))
rows <- list()
for (tr in names(proto)) for (w in paste0("W", 1:6)) {
xy <- proto[[tr]](s) + matrix(rnorm(length(s) * 2, 0, 0.4), ncol = 2)
xy[1, ] <- xy[1, ] * 0
rows[[paste(tr, w)]] <- data.frame(PC1 = xy[, 1], PC2 = xy[, 2],
Treatment = tr, Well = w, Timepoint = timepoints)
}
pca <- list(plot_data = do.call(rbind, rows),
variance_explained = c(PC1 = 40, PC2 = 22))
}
cat("Timepoints:", paste(nova_order_timepoints(pca$plot_data$Timepoint), collapse = " -> "), "\n")
#> Timepoints: baseline -> 0min -> 15min -> 30min -> 45min -> 1h -> 1h15 -> 1h30 -> 1h45 -> 2hplot_pca_trajectories_general(
pca, trajectory_grouping = "Treatment",
timepoint_order = nova_order_timepoints(pca$plot_data$Timepoint),
color_by = "Treatment", save_plots = FALSE, verbose = FALSE)$plots$combined_average
#> Displaying: pbs_all
#> Displaying: pbs_avg
#> Displaying: dhpg_all
#> Displaying: dhpg_avg
#> Displaying: ka_all
#> Displaying: ka_avg
#> Displaying: gabazine_all
#> Displaying: gabazine_avg
#> Displaying: combined_all
#> Displaying: combined_avg
#> Displaying: combined_averages <- nova_trajectory_summary(pca, group_var = "Treatment", verbose = FALSE)
s$metrics
#> # A tibble: 4 × 7
#> group net_displacement path_length directness peak_timepoint peak_displacement
#> <chr> <dbl> <dbl> <dbl> <chr> <dbl>
#> 1 dhpg 4.67 11.0 0.423 0min 5.81
#> 2 gaba… 7.00 27.3 0.257 1h45 8.50
#> 3 ka 3.24 5.24 0.618 15min 4.02
#> 4 pbs 0.336 4.07 0.0825 1h15 0.727
#> # ℹ 1 more variable: final_displacement <dbl>The key figure — distance from baseline over time, with a mean ± SEM band across replicate wells:
The same paths drawn in PC space:
– Trajectory summary – Across 4 conditions, ‘gabazine’ moved farthest from baseline (7.00 PC units) and ‘pbs’ moved least (0.34).
‘gabazine’ moved 7.00 PC units from baseline via a wandering path, with most of the change by 1h45.
‘dhpg’ moved 4.67 PC units from baseline via a wandering path, with most of the change by 0min.
‘ka’ moved 3.24 PC units from baseline via a moderately direct path, with most of the change by 15min.
‘pbs’ moved 0.34 PC units from baseline via a wandering path, peaking near 1h15 then partly returning toward baseline.
net_displacement — how far the
condition ended up from baseline.directness (net / path,
0–1) — close to 1 means the network moved straight out and stayed; low
values mean it wandered or returned.peak_timepoint — when the displacement
was largest, a simple readout of response kinetics (fast vs slow)
without claiming a velocity.In an acute neuronal-agonist experiment this lines up with the biology: a vehicle control barely leaves baseline, while an excitatory agonist moves the network far and early and then holds — exactly the comparison a drug or genotype screen needs, described without over-interpreting the dynamics.
#> 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] ggplot2_4.0.3 NOVA_0.3.0 rmarkdown_2.31
#>
#> loaded via a namespace (and not attached):
#> [1] viridis_0.6.5 utf8_1.2.6 sass_0.4.10 generics_0.1.4
#> [5] tidyr_1.3.2 stringi_1.8.7 hms_1.1.4 digest_0.6.39
#> [9] magrittr_2.0.5 evaluate_1.0.5 grid_4.6.0 RColorBrewer_1.1-3
#> [13] fastmap_1.2.0 jsonlite_2.0.0 ggrepel_0.9.8 gridExtra_2.3
#> [17] purrr_1.2.2 viridisLite_0.4.3 scales_1.4.0 jquerylib_0.1.4
#> [21] cli_3.6.6 crayon_1.5.3 rlang_1.2.0 bit64_4.8.2
#> [25] withr_3.0.3 cachem_1.1.0 yaml_2.3.12 otel_0.2.0
#> [29] parallel_4.6.0 tools_4.6.0 tzdb_0.5.0 dplyr_1.2.1
#> [33] DT_0.34.0 buildtools_1.0.0 vctrs_0.7.3 R6_2.6.1
#> [37] lifecycle_1.0.5 stringr_1.6.0 bit_4.6.0 htmlwidgets_1.6.4
#> [41] vroom_1.7.1 pkgconfig_2.0.3 pillar_1.11.1 bslib_0.11.0
#> [45] gtable_0.3.6 glue_1.8.1 Rcpp_1.1.1-1.1 xfun_0.59
#> [49] tibble_3.3.1 tidyselect_1.2.1 sys_3.4.3 knitr_1.51
#> [53] farver_2.1.2 htmltools_0.5.9 labeling_0.4.3 maketools_1.3.2
#> [57] readr_2.2.0 pheatmap_1.0.13 compiler_4.6.0 S7_0.2.2