Fit the centered eight-schools model with cmdstanr, review its
sampler diagnostics, and rerun a non-centered version on a branch. The
chunks use eval = FALSE with output captured from a real
run, so the vignette builds without CmdStan on CI.
Setup
library(bayesgrove)
handle <- bg_init(
path = file.path(tempdir(), "bg-eight-schools"),
project_name = "Eight Schools"
)
# Activate the default review loop and the cmdstanr built-in executors.
bg_use_workflow_packs(handle, "bayesgrove.default_bayesian")
bg_use_cmdstanr(handle)Build the graph
# Eight-schools data (Rubin, 1981; Gelman et al., 2013).
schools_data <- list(
J = 8L,
y = c(28, 8, -3, 7, -1, 1, 18, 12),
sigma = c(15, 10, 16, 11, 9, 11, 10, 18)
)
# Node params are data, never code. To preserve R types that JSON serialization
# would mangle (e.g. integers), attach the data object via the content-addressed
# store with bg_set_node_data().
n_data <- bg_add_node(
handle,
kind = "stan_data",
label = "Schools data"
)
bg_set_node_data(handle, n_data, schools_data)The centered parameterization of the hierarchical model is a standard
example of funnel geometry: as tau approaches zero, the
conditional distribution of theta contracts sharply, and
HMC produces divergent transitions (Betancourt and Girolami, 2015). The
package ships the example programs so the code below runs as pasted:
stan_file <- system.file(
"stan/eight_schools_centered.stan",
package = "bayesgrove",
mustWork = TRUE
)
n_fit <- bg_add_node(
handle,
kind = "cmdstanr_fit",
label = "Centered",
inputs = n_data,
params = list(
stan_file = stan_file,
chains = 4,
iter_warmup = 1000,
iter_sampling = 1000,
seed = 42
)
)Run the model and inspect diagnostics
run <- bg_run(handle, targets = n_fit)#> Starting run "run_179e87ae" with 1 node to execute.
#> Running node "node_9f8f0f45"...
#> Running node "node_55624d87"...
#> Running MCMC with 4 sequential chains...
#> All 4 chains finished successfully.
#> Warning: 81 of 4000 (2.0%) transitions ended with a divergence.
#> Warning: 2 of 4 chains had an E-BFMI less than 0.3.
The executor stores these diagnostics as a typed
hmc_diagnostics summary:
summaries <- bg_read_summaries(handle)
summaries[[1]]$severity
#> [1] "error"
summaries[[1]]$metrics[c("divergences", "e_bfmi", "max_rhat")]
#> $divergences
#> [1] 81
#>
#> $e_bfmi
#> [1] 0.1574
#>
#> $max_rhat
#> [1] 1.0401Because the summary carries severity = "error", the
default pack raises a blocking review_computation_validity
obligation and suggests two actions: record the review decision, or
branch and modify the fit.
guide <- bg_next_actions(handle)
vapply(guide$obligations, `[[`, character(1), "kind")
#> obl_ef958c36
#> "review_computation_validity"
vapply(guide$actions, `[[`, character(1), "kind")
#> act_2260dedb act_663128f1
#> "record_decision" "branch_and_modify"Record a review
The suggested record_decision action carries the
decision kind and the ids of the summaries under review in its payload.
Recording a computation_review decision with those ids
resolves the obligation; a decision of another kind, or one that does
not reference the summaries, does not.
review <- Filter(
function(a) identical(a$payload$decision_type, "computation_review"),
guide$actions
)[[1]]
bg_record_decision(
handle,
scope = "project",
prompt = "Is the centered fit acceptable for downstream use?",
choice = "reject_and_reparametrize",
rationale = paste(
"81 of 4000 transitions diverged and E-BFMI is low: the sampler cannot",
"explore the funnel between tau and theta. Switch to the non-centered",
"parameterization."
),
kind = "computation_review",
metadata = review$payload[c("node_ids", "summary_ids")]
)
vapply(bg_next_actions(handle)$obligations, `[[`, character(1), "kind")
#> named character(0)Branch to the non-centered parameterization
Create a branch from the centered fit, retaining the original fit and
its diagnostics. Set an inferential goal, as required for each active
branch, and select the non-centered program.
bg_update_node() merges params, so the chains, iteration,
and seed settings carry over.
branch <- bg_branch(handle, n_fit, label = "Non-centered")
bg_set_goal(
project = handle,
branch_id = branch$branch_id,
kind = "latent_inference",
label = "School effects, non-centered",
rationale = "Same model; reparameterized so HMC can explore the posterior."
)
stan_file_nc <- system.file(
"stan/eight_schools_noncentered.stan",
package = "bayesgrove",
mustWork = TRUE
)
bg_update_node(
handle,
branch$root_node_id,
params = list(stan_file = stan_file_nc)
)Run and review the non-centered fit
run2 <- bg_run(handle, targets = branch$root_node_id)#> Starting run "run_a26583a8" with 1 node to execute.
#> Running node "node_4c430039"...
#> Running MCMC with 4 sequential chains...
#> All 4 chains finished successfully.
The non-centered fit has no sampler warnings. Both fits remain in the record, with their diagnostic summaries.
for (s in bg_read_summaries(handle, include_stale = FALSE)) {
cat(s$node_id, "-", s$severity, "- divergences:", s$metrics$divergences, "\n")
}
#> node_55624d87 - error - divergences: 81
#> node_4c430039 - ok - divergences: 0One obligation remains. Decisions are scope-specific (see
vignette("concepts")): the project-scope review recorded
above does not cover the new branch. Record a branch-scope review of the
centered fit’s error summary in its lineage.
branch_guide <- bg_next_actions(
handle,
scope = "branch",
branch_id = branch$branch_id
)
vapply(branch_guide$obligations, `[[`, character(1), "kind")
#> obl_e02b096b
#> "review_computation_validity"
review_nc <- Filter(
function(a) identical(a$payload$decision_type, "computation_review"),
branch_guide$actions
)[[1]]
bg_record_decision(
handle,
scope = branch$branch_id,
prompt = "Is the centered fit acceptable as this branch's baseline?",
choice = "superseded_by_non_centered",
rationale = paste(
"The centered fit's divergences are the reason this branch exists;",
"the non-centered fit replaces it."
),
kind = "computation_review",
metadata = review_nc$payload[c("node_ids", "summary_ids")]
)
bg_next_actions(handle)$obligations
#> named list()No model comparison is needed here: the two fits target the same
posterior, and the reparameterization is a computational repair rather
than a change of model. For distinct candidate models, the default pack
requires a comparison decision and an accept-or-reject disposition for
each branch. See vignette("case-study-roaches").
Export a report
print(bg_read_graph(handle))
#> ── Execution Graph ──
#>
#> └── Schools data <stan_data> [new]
#> ├── Centered <cmdstanr_fit> [new]
#> └── Non-centered <cmdstanr_fit> [new]
bg_export_report(handle, format = "md", path = "eight-schools-report.md")The report contains the graph with both fits, the review decisions at each scope, and the artifact index. Diagnostic summaries remain in the project.