
Empirically Calibrated Simulations for bifrost, Part 1: Performance
Source:vignettes/simulation-study-part-1.Rmd
simulation-study-part-1.RmdThis is Part 1 of the empirically calibrated simulation workflow. Part 2 uses these performance scenarios to tune search settings and apply them to an empirical analysis.
As part of a bifrost workflow, it helps to know how the
method behaves on datasets that resemble the one you actually care
about. A practical way to answer that question is to calibrate
simulations to the empirical dataset. Here, calibration means
parameterizing simulations to match the empirical tree, trait
dimensionality, fitted mean structure, and residual covariance
scale.
We then compare search behavior under three scenarios:
- a null model with no shifts, to estimate expected false positives,
- the current generating model, where shifts are proportional covariance scalings, and
- a deliberately non-generating robustness scenario coupling reduced integration with faster marginal rates, or increased integration with slower marginal rates.
Empirically calibrated simulations can be used before or after an empirical search. Before the final search, they help choose search settings; after a search, they help evaluate whether those settings behave reasonably for the empirical tree, trait dimensionality, fitted mean structure, and residual covariance scale. In both cases, the workflow uses the same ingredients: a global calibration template, null and shifted simulation scenarios, false-positive and recovery summaries, and a way to compare candidate search settings. This is the same broad calibration strategy used in Berv et al. (2026). On large trees, repeated subtree resampling is helpful because it lets simulations vary across many local topologies and parameter draws without requiring a full-tree search on every replicate.
The package generator improves on the manuscript implementation for new simulation studies by drawing ancestral covariance structures around the complete fitted empirical covariance and by representing integration-rate shifts in both biological directions. This refinement provides a more direct robustness test and does not alter the manuscript’s empirical analyses or conclusions.
Start by loading bifrost.
Build an empirically calibrated template
Here we use the empirical calibration structure from Berv et al. (2026), which models 12 skeletal traits jointly while keeping log body mass as a phylogenetic covariate.
# Locate the packaged passerine tree and calibrated skeletal measurements.
tree_path <- pkg_file("extdata", "avian-skeleton", "passerine_bodyplan_tree.tre")
trait_path <- pkg_file("extdata", "avian-skeleton", "passerine_bodyplan_data.RDS")
# Align the trait rows to the tree's tip order.
bird_tree <- ape::read.tree(tree_path)
bodyplan_data <- readRDS(trait_path)
bodyplan_data <- as.matrix(bodyplan_data[bird_tree$tip.label, , drop = FALSE])
# Keep body mass as the predictor and the skeletal variables as responses.
skeletal_cols <- setdiff(colnames(bodyplan_data), "vertnet_mass")
bodyplan_data <- bodyplan_data[, c(skeletal_cols, "vertnet_mass"), drop = FALSE]
# Express the multivariate response and body-mass covariate by column index.
formula_str <- sprintf(
"trait_data[, 1:%d] ~ trait_data[, %d]",
length(skeletal_cols),
ncol(bodyplan_data)
)
# Confirm the aligned sample size and response dimensionality.
c(
species = nrow(bodyplan_data),
skeletal_traits = length(skeletal_cols),
total_columns = ncol(bodyplan_data)
)
#> species skeletal_traits total_columns
#> 2057 12 13The template comes from a single-regime global fit. That fit provides
the empirical variance and covariance scale used to generate a fresh
ancestral covariance matrix for each simulation replicate. Here the
calibration model is a single-regime multivariate BM fit with
mvMORPH::mvgls() using method = "LL",
following the multivariate phylogenetic GLS framework described by Clavel, Aristide, and
Morlon (2019) and Clavel and Morlon
(2020).
# Fit the single-regime model that calibrates means and residual covariance.
bodyplan_template <- createSimulationTemplate(
baseline_tree = bird_tree,
trait_data = bodyplan_data,
formula = formula_str,
response_columns = seq_along(skeletal_cols),
predictor_columns = ncol(bodyplan_data),
method = "LL",
error = TRUE
)#> Bifrost Simulation Template
#> ===========================
#>
#> Tree
#> Tips: 2057
#> Response traits: 12
#> Predictor columns: 1
#>
#> Model
#> Calibration formula: trait_data[, 1:12] ~ trait_data[, 13]
#> Simulation search: trait_data ~ 1
#> Method: LL
#> Error term: TRUE
#>
#> Empirical Covariance Summaries
#> Variance mean: 0.000215
#> Variance sd: 0.000124
#> Covariance mean: 0.000018
#> Covariance sd: 0.000109
The template stores the aligned calibration data, the fitted global
mvgls model, fitted means, and empirical
variance/covariance summaries used to generate a fresh ancestral
covariance matrix for each simulation replicate. Covariates such as body
mass can shape those fitted means and residual covariance, but the
simulation studies regenerate only the response block and evaluate
intercept-only shift searches on those simulated responses. If no
calibration covariates are needed, the same workflow reduces to the
simpler trait_data ~ 1 template. In all cases, response
columns must be raw numeric variables that can be regenerated in each
replicate.
The examples below use
simulation_generator = "empirical", the recommended option
when the fitted full covariance structure should anchor new draws.
Simulation scenarios
The figure below places the three package scenarios side by side: an
empirical ancestral covariance matrix, a proportional rescaling, and the
integration-rate trade-off. The last scenario raises the eigenvalues of
the ancestral correlation matrix to a power g, renormalizes
to a correlation matrix, and multiplies the covariance matrix by
1 / g. Thus g < 1 reduces integration while
increasing every marginal evolutionary variance; g > 1
increases integration while decreasing every marginal variance.

Figure 1. Empirically calibrated simulation scenarios. Panels A-C show the ancestral VCV, proportional scaling with s = 0.5, and a reduced-integration/faster-rate shift with g = 0.65. Panels D-F show their correlation matrices. Panels G-I summarize correlation effective dimensionality, covariance eigenvalue spectra, and mean marginal evolutionary variance. The proportional scenario changes scale without changing correlation structure; the integration-rate scenario changes covariance shape and rate together.
In the proportional panels, the covariance structure keeps the same shape and simply expands or contracts. In the integration-rate panels, weaker correlation structure is paired with larger marginal variance, so covariance shape and evolutionary rate change together. The shifted passerine examples below use these two simulation styles under repeated subtree resampling from the full tree, alongside a null no-shift baseline.
Calibration guardrails
Before launching a large study, keep four details in mind.
Simulation studies regenerate the response block only. Predictors shape the global calibration fit, but they are not re-fit inside each subtree-resampled search replicate.
Use the wrapper-level
seedargument for reproducibility. Do not pass a separate seed insidesimulation_options; the study wrappers manage replicate-level seeds.The wrappers inherit template fit settings such as
methodanderror, sosearch_optionsonly needs to include deliberate overrides. Parallelize either the study wrapper or each embedded search, not both; the examples parallelize replicates and keep each search serial.Check
per_replicate$n_candidates. The value ofmin_descendant_tipschanges the candidate-node universe rather than merely applying a different decision threshold to a fixed set of nodes. If it leaves no candidate shifts, that replicate is treated as unevaluable, and recovery specificity, false-positive rate, and balanced accuracy are reported asNA. Lowermin_descendant_tipsif too many replicates are unevaluable.
The next three sections show the individual study calls. The fixed-settings comparison then uses bundled preview results to show the same logic side by side.
1. Estimate False Positives
The null study asks the baseline error question: if the real dataset contained no shifts at all, how often would the search still infer one?
# Estimate false positives under an empirical-covariance null model.
fp_study <- runFalsePositiveSimulationStudy(
template = bodyplan_template,
n_replicates = 100,
tree_tip_count = 250,
simulation_options = list(
simulation_generator = "empirical"
),
search_options = list(
formula = "trait_data ~ 1",
min_descendant_tips = 10,
shift_acceptance_threshold = 10,
IC = "GIC",
num_cores = 1,
method = "LL",
error = TRUE,
plot = FALSE,
store_model_fit_history = FALSE
),
num_cores = 8,
seed = 5
)
# Inspect the study object, representative replicates, and aggregate summary.
fp_study
head(fp_study$per_replicate)
fp_study$study_summaryNull simulations are useful for screening candidate choices of
shift_acceptance_threshold, IC, and
min_descendant_tips.
2. Estimate Recovery Under Proportional Scaling
Recovery under proportional scaling evaluates the scenario that
matches the current bifrost BMM search assumptions: shifted
covariance matrices are scalar multiples of the background covariance
matrix.
# Measure recovery when simulated shifts match proportional BMM scaling.
recovery_prop <- runShiftRecoverySimulationStudy(
template = bodyplan_template,
n_replicates = 100,
tree_tip_count = 250,
simulation_options = list(
simulation_generator = "empirical",
num_shifts = 5,
min_shift_tips = 10,
max_shift_tips = 20,
scale_mode = "proportional",
scale_factor_range = c(0.1, 2.0),
exclude_range = c(0.5, 1.5),
buffer = 3
),
search_options = list(
formula = "trait_data ~ 1",
min_descendant_tips = 10,
shift_acceptance_threshold = 10,
IC = "GIC",
num_cores = 1,
method = "LL",
error = TRUE,
uncertaintyweights_par = TRUE,
plot = FALSE,
store_model_fit_history = FALSE
),
fuzzy_distance = 2,
weighted = TRUE,
num_cores = 8,
seed = 5
)
# Compare strict, fuzzy, and distance-weighted recovery summaries.
recovery_prop
recovery_prop$study_summary
recovery_prop$evaluation$strict
recovery_prop$evaluation$fuzzy
recovery_prop$evaluation$weighted3. Check the Integration-Rate Robustness Scenario
It is also useful to test a deliberately mismatched scenario in which
trait integration and evolutionary rate evolve together. This asks how
well the same search settings behave when the data depart from strict
proportional VCV scaling. The moderate power bands below sample reduced
integration from 0.5-0.8 and increased integration from
1.1-1.25; each power g scales marginal
variances by exactly 1 / g.
# Stress-test the same search under integration-rate model misspecification.
recovery_integration <- runShiftRecoverySimulationStudy(
template = bodyplan_template,
n_replicates = 100,
tree_tip_count = 250,
simulation_options = list(
simulation_generator = "empirical",
num_shifts = 5,
min_shift_tips = 10,
max_shift_tips = 20,
scale_mode = "correlation",
integration_power_range = c(0.5, 1.25),
integration_exclude_range = c(0.8, 1.1),
buffer = 3
),
search_options = list(
formula = "trait_data ~ 1",
min_descendant_tips = 10,
shift_acceptance_threshold = 10,
IC = "GIC",
num_cores = 1,
method = "LL",
error = TRUE,
uncertaintyweights_par = TRUE,
plot = FALSE,
store_model_fit_history = FALSE
),
fuzzy_distance = 2,
weighted = TRUE,
num_cores = 8,
seed = 5
)
# Review the overall study and the fuzzy recovery result used for reporting.
recovery_integration
recovery_integration$study_summary
recovery_integration$evaluation$fuzzyThis scenario is not the current generating model for
bifrost, so the goal is not perfect recovery. It
deliberately changes covariance shape while the fitted search model
permits only proportional covariance shifts. Lower recovery than in the
proportional scenario is therefore expected and measures robustness to
biologically motivated model misspecification rather than failure under
the model’s own assumptions.
4. Compare Fixed Settings Across Scenarios
After running each scenario on its own, it helps to put one fixed search configuration side by side across the null, proportional, and integration-rate cases. This worked example uses 100 repeated 250-tip subtree resamples from the passerine tree.
The comparison uses one search configuration across all three
scenarios: shift_acceptance_threshold = 10 and
min_descendant_tips = 10. In the shifted scenarios, each
replicate adds five simulated shifts spanning clades with 10 to 20
descendant tips, matching the simulation-performance settings described
in Berv et al. (2026). Every scenario uses the explicit empirical
generator. The point here is not to optimize yet; it is to compare how
null and shifted datasets behave under the same search settings.
The table below uses bundled summaries from the code shown here, so package builds do not rerun the longer simulation studies.
# Prevent nested BLAS/OpenMP parallelism during replicate-level parallel runs.
if (requireNamespace("RhpcBLASctl", quietly = TRUE)) {
RhpcBLASctl::blas_set_num_threads(1)
RhpcBLASctl::omp_set_num_threads(1)
}
# Reuse the fitted empirical template for all fixed-setting comparisons.
medium_case_template <- preview_template
# Define the proportional-shift scenario shared by GIC and BIC.
fixed_setting_proportional_options <- list(
simulation_generator = "empirical",
num_shifts = 5,
min_shift_tips = 10,
max_shift_tips = 20,
scale_factor_range = c(0.1, 2.0),
exclude_range = c(0.5, 1.5),
buffer = 3
)
# Hold every search and simulation setting fixed except the IC family.
fixed_setting_args <- list(
shift_acceptance_thresholds = 10,
min_descendant_tips_values = 10,
tree_tip_count = 250,
null_replicates = 100,
recovery_replicates = 100,
null_simulation_options = list(
simulation_generator = "empirical"
),
proportional_simulation_options = fixed_setting_proportional_options,
correlation_simulation_options = list(
simulation_generator = "empirical",
integration_power_range = c(0.5, 1.25),
integration_exclude_range = c(0.8, 1.1)
),
base_search_options = list(
formula = "trait_data ~ 1",
method = "LL",
error = TRUE,
num_cores = 1,
uncertaintyweights_par = TRUE,
plot = FALSE,
store_model_fit_history = FALSE,
progress = FALSE
),
fuzzy_distance = 2,
weighted = TRUE,
num_cores = 8,
seed = 5,
store_studies = FALSE
)
# Run the identical comparison once with GIC and once with BIC.
gic_fixed <- do.call(
runSearchTuningGrid,
c(list(template = medium_case_template, IC = "GIC"), fixed_setting_args)
)
bic_fixed <- do.call(
runSearchTuningGrid,
c(list(template = medium_case_template, IC = "BIC"), fixed_setting_args)
)Null and shifted simulations answer different questions, so they are reported separately. The first table shows null false-positive behavior; the second shows recovery only where true shifts are known. This avoids treating an inapplicable recovery metric in a null simulation as a missing result.
| IC | Mean FP | Any FP | Mean shifts |
|---|---|---|---|
| GIC | 0.0017 | 0.100 | 0.10 |
| BIC | 0.0007 | 0.040 | 0.04 |
| IC | Scenario | Fuzzy rec. | Fuzzy spec. | Fuzzy F1 | Fuzzy BA | Mean shifts |
|---|---|---|---|---|---|---|
| GIC | Proportional | 0.846 | 0.993 | 0.883 | 0.920 | 4.58 |
| GIC | Integration-rate | 0.667 | 0.982 | 0.717 | 0.824 | 4.26 |
| BIC | Proportional | 0.768 | 0.996 | 0.848 | 0.882 | 4.06 |
| BIC | Integration-rate | 0.588 | 0.992 | 0.702 | 0.790 | 3.31 |
Candidate nodes without a true shift generally outnumber true-shift nodes, creating class imbalance. Raw accuracy can therefore look strong even when sensitivity is poor. Fuzzy balanced accuracy gives recall and specificity equal influence, while fuzzy matching credits biologically close near misses instead of counting every small node displacement as a complete failure. Balanced accuracy complements rather than replaces F1: balanced accuracy exposes the sensitivity-specificity trade-off, whereas F1 focuses on precision and recall and remains useful when false discoveries among inferred shifts are the main concern.
For GIC, the cache reports fuzzy balanced accuracy of 0.920 under proportional shifts and 0.824 under the integration-rate trade-off. The corresponding BIC values are 0.882 and 0.790. These cache-derived comparisons describe performance under the fitted generating model and under deliberate covariance-shape misspecification without hard-coding a particular cache result into the narrative.
Performance Summary
These checks establish false-positive behavior under the null, recovery under the proportional generating model, and robustness to the integration-rate trade-off. Part 2 uses those scenarios to tune search settings and apply them to the empirical analysis.
References
If you use these data or reproduce this workflow, the most relevant citations are:
- Berv, Jacob S., Charlotte M. Probst, Santiago Claramunt, J. Ryan Shipley, Matt Friedman, Stephen A. Smith, David F. Fouhey, and Brian C. Weeks. 2026. “Rates of passerine body plan evolution in time and space.” Nature Ecology & Evolution. https://doi.org/10.1038/s41559-026-03110-5
- Berv, Jacob S., Charlotte M. Probst, Santiago Claramunt, J. Ryan Shipley, Matt Friedman, Stephen A. Smith, David F. Fouhey, and Brian C. Weeks. 2026. “Supplementary data archive for Rates of passerine body plan evolution in time and space” (v1.0.0) [Data set]. Zenodo. https://doi.org/10.5281/zenodo.19198393
- Clavel, Julien, Laurent Aristide, and Helene Morlon. 2019. “A penalized likelihood framework for high-dimensional phylogenetic comparative methods and an application to New-World monkeys brain evolution.” Systematic Biology 68(1):93-116. https://doi.org/10.1093/sysbio/syy045
- Clavel, Julien, and Helene Morlon. 2020. “Reliable phylogenetic regressions for multivariate comparative data: Illustration with the MANOVA and application to the effect of diet on mandible morphology in phyllostomid bats.” Systematic Biology 69(5):927-943. https://doi.org/10.1093/sysbio/syaa010
- Clavel, Julien, Gilles Escarguel, and Gildas Merceron. 2015. “mvMORPH: an R package for fitting multivariate evolutionary models to morphometric data.” Methods in Ecology and Evolution 6(11):1311-1319. https://doi.org/10.1111/2041-210X.12420
- Smith, Stephen A., Nicholas Walker-Hale, and Caroline T. Parins-Fukuchi. 2023. “Compositional shifts associated with major evolutionary transitions in plants.” New Phytologist 239(6):2404-2415. https://doi.org/10.1111/nph.19099
Software Used in This Vignette
-
bifrostfor empirical simulation templates, null and shifted datasets, repeated simulation studies, recovery evaluation, and tuning-grid summaries. -
mvMORPH/mvglsmachinery underlies the global calibration fit and empirical shift searches. -
apeandphytoolsunderlie the phylogenetic tree, SIMMAP, and subtree operations used by the simulation helpers. -
ggplot2,patchwork,viridis, andreshape2draw the schematic simulation-scenario figure. -
knitrandrmarkdownrender the vignette and preview tables.