Neural Output Visualization and Analysis — turn MEA recordings into interpretable state-space figures.
NOVA turns high-dimensional Multi-Electrode Array (MEA) recordings into interpretable neuronal state-space representations — from raw Axion CSV discovery through PCA, heatmaps, per-metric plots, and trajectory analysis — with publication-ready figures in a few lines of code.
![]() Treatment groups traced through PCA state space over time. |
![]() New in 0.3.0: how far each condition moved from baseline over time (mean ± SEM). |
min / h / s / 1h30 / DIV7 labels sorted by real elapsed timeTrajectories are just one view. Each of these is a single function call on the same processed data — the state-space, per-metric, and clustered-heatmap figures you actually put in a paper.
![]() PCA state space with 95% confidence ellipses per group — pca_plots_enhanced().
|
![]() Every MEA metric, z-scored and clustered by condition — create_mea_heatmaps_enhanced().
|
![]() Any single metric as bar / box / violin, split by group and timepoint — plot_mea_metric().
|
![]() Two variables at once: colour by group, shape by timepoint — pca_plots_enhanced().
|
# install.packages("remotes")
# remotes::install_github("atudoras/nova")
install.packages("NOVA", repos = c('https://atudoras.r-universe.dev', 'https://cloud.r-project.org'))
Upgrading from 0.3.x or earlier? Your numbers will change, and the new ones are the correct ones. Well IDs repeat on every plate, so
Experimentis what tells two wells apart — and several functions did not use it. Multi-plate analyses were normalising wells against other plates' baselines, and the trajectory figure could render as a flat line at zero. See NEWS.md for what changed and why. Single-plate analyses are largely unaffected.
library(NOVA)
# 1. Read it. Axion's CSV export...
processed <- process_mea_flexible(
main_dir = "path/to/your/MEA_data",
baseline_timepoint = "baseline"
)
# ...or any tidy table, including published datasets (see "Data format").
# processed <- process_mea_table(df, experiment = "plate", well = "well",
# timepoint = "div", treatment = "compound",
# metrics = c("firing_rate", "n_bursts"),
# normalize = "baseline")
# 2. PCA — the shared state space.
pca <- pca_analysis_enhanced(normalized_data = processed$normalized_data,
grouping_variables = "Treatment")
# 3. How did each condition move away from baseline?
traj <- nova_trajectory_summary(pca, group_var = "Treatment")
traj$plots$displacement # distance from baseline over time, mean +/- SEM across wells
traj$plots$map # the path through PC space
traj$metrics # net displacement, path length, directness, peak timing
# 4. In plain language.
nova_describe(traj)
nova_trajectory_summary() takes the PCA you already computed (or any embedding) and describes, simply and robustly, how each condition's network moved relative to its baseline — how far, how directly, and when. It reports only what this kind of data supports: distance travelled, path directness, and timing — no over-fitted velocities or regimes.
s <- nova_trajectory_summary(pca, group_var = "Treatment")
s$metrics # net displacement, path length, directness, peak timepoint
s$plots$displacement # distance-from-baseline over time (mean ± SEM across wells)
s$plots$map # the trajectory map in PC space
nova_describe(s) # a plain-language summary
![]() The vehicle stays near baseline; agonists jump quickly and plateau. |
![]() The same trajectories drawn in PC space (square = baseline). |
nova_describe()example output: "Across 4 conditions, 'gabazine' moved farthest from baseline (7.00 PC units) and 'pbs' moved least (0.34). 'ka' moved 3.24 PC units from baseline via a moderately direct path, with most of the change by 15min."
| Reported quantity | Meaning |
|---|---|
net_displacement |
Final distance from baseline (how far it ended up) |
path_length |
Total distance travelled along the trajectory |
directness |
net / path in [0,1] — 1 = straight out, low = wandering |
peak_timepoint / peak_displacement
|
When (and how far) the network was most displaced |
Read
| Function | Description |
|---|---|
discover_mea_structure |
Scan a directory and report detected MEA experiments and timepoints |
process_mea_flexible |
Read and merge Axion CSV exports across experiments and timepoints |
process_mea_table |
Ingest an already-tidy table (published data, or your own); normalise to a baseline timepoint or to control wells |
Analyse
| Function | Description |
|---|---|
pca_analysis_enhanced |
Run PCA on the processed feature matrix |
nova_extract_trajectories |
Tidy, correctly ordered paths through an embedding |
nova_trajectory_summary |
How each condition moved from baseline: distance, path length, directness, timing |
nova_describe |
Plain-language read-out of a trajectory result (rule-based, no AI) |
Plot
| Function | Description |
|---|---|
pca_plots_enhanced |
PCA scatter, ellipses, loadings, variance plots |
plot_pca_trajectories_general |
Per-well and group-mean PCA trajectories across timepoints |
create_mea_heatmaps_enhanced |
Heatmaps of MEA metrics (raw or normalized) |
plot_mea_metric |
Bar/box/violin/line plot for a single MEA variable |
Utilities
| Function | Description |
|---|---|
nova_order_timepoints / nova_time_to_minutes
|
Baseline-first timepoint ordering; parses 1h30, 90min, DIV7
|
nova_unit_cols / nova_unit_id
|
What identifies one replicate well. Ask these rather than counting on Well: well IDs repeat across plates, so n_distinct(Well) merges the same ID from different plates into one replicate and understates your replication |
nova_theme / nova_palette
|
Consistent figure styling |
NOVA takes two kinds of input.
process_mea_flexible()
The directory layout Axion BioSystems software produces:
MEA + digits (e.g. MEA001, MEA016a)<plate>_<timepoint>.csv (e.g. MEA001_baseline.csv, MEA001_1h.csv)baseline, 0min, 1h30, DIV7; NOVA finds the metadata rows automatically and orders timepoints by real time.MEA_data/
├── MEA001/
│ ├── MEA001_baseline.csv
│ ├── MEA001_1h.csv
│ └── MEA001_24h.csv
└── MEA002/
├── MEA002_baseline.csv
└── MEA002_1h.csv
process_mea_table()
Published datasets essentially never arrive in the Axion layout. They arrive as a plain
table: one row per well × timepoint, metrics in columns (or already in long form).
process_mea_table() maps that onto the same schema, so everything downstream is identical.
res <- process_mea_table(
df,
experiment = c("culture_date", "plate_id"), # see below
well = "well",
timepoint = "DIV", # "DIV7" and "1h30" both parse to real time
treatment = "compound",
metrics = c("meanfiringrate", "burst.per.min", "nAE"),
normalize = "control", # or "baseline", or "none"
control = df$dose == 0 # which wells are the controls
)
experiment often needs more than one column, and getting it wrong is silent. A well is
only identified once you know which experiment it came from — and the experiment is not
always one column. In the EPA's developmental-neurotoxicity dataset, for instance, plate
serial numbers are reused across culture dates: four serials, six experiments. Keying on
the serial alone merges two different cultures into one well. Pass every column that
identifies the experiment.
Normalisation is a choice, not a default. "baseline" divides each well by its own
earliest timepoint — a fold-change over time. "control" divides by the control wells on the
same plate at the same timepoint — the toxicology convention, and the only workable option
when the earliest timepoint is not a usable reference (in a developmental assay, every well
may be silent at the first timepoint, making the ratio undefined). Both are ratios: undefined
against zero, which yields NA, never Inf.
Example/nova_quickstart.R (set DATA_DIR, run, get all figures)vignettes/NOVA_Trajectory_Summary.Rmd
docs/user-guide/NOVA-User-Guide.md
If you use NOVA in published research, please cite:
Escoubas CC, Guney E, Tudoras Miravet À, Magee N, Phua R, Ruggero D, Molofsky AV, Weiss WA (2025). NOVA: a novel R-package enabling multi-parameter analysis and visualization of neural activity in MEA recordings. bioRxiv. https://doi.org/10.1101/2025.10.01.679841
Bug reports and feature requests are welcome via GitHub Issues. Pull requests should follow standard R package conventions and include tests where applicable. Questions: Alex Tudoras — alex.tudorasmiravet@ucsf.edu.
NOVA is released under the GPL-3 License.