greta#855 (tf-warmup-i547): warmup and sampling in TensorFlow

How long mcmc() and opt() with adam() take on greta 0.6.0 from CRAN, main, greta#850 and greta#855, on four of greta’s example models. greta#855 is the fastest of the four at both, and its mcmc() samples the same posteriors.
Published

October 7, 2026

Setup
library(dplyr)
library(tidyr)
library(ggplot2)
library(bench)
library(here)

post_path <- "posts/2026-10-07-tf-warmup-i547"

# each `# ---- label ----` section of the scripts, shown below by its label
knitr::read_chunk(here(post_path, "benchmark.R"))
knitr::read_chunk(here(post_path, "run.R"))

# links to the scripts on GitHub, at the commit being rendered when GitHub
# Actions renders the site
github_ref <- Sys.getenv("GITHUB_SHA", "main")
github_file_url <- function(file) {
  paste0(
    "https://github.com/greta-dev/greta.benchmarks/blob/",
    github_ref,
    "/",
    post_path,
    "/",
    file
  )
}
# Quarto escapes the markdown in an inline `{r}` result unless it is wrapped
# in I(), so the link helpers return I()
file_link <- function(file) {
  I(paste0("[`", file, "`](", github_file_url(file), ")"))
}

# a link to the lines of a script's `# ---- label ----` section
section_link <- function(file, label) {
  lines <- readLines(here(post_path, file))
  headings <- grep("^# ---- .+ ----$", lines)
  start <- headings[lines[headings] == paste("# ----", label, "----")]
  end <- c(headings, length(lines) + 1)[match(start, headings) + 1] - 1
  while (end > start && !nzchar(trimws(lines[end]))) {
    end <- end - 1
  }
  I(paste0(
    "[`", label, "`](", github_file_url(file), "#L", start, "-L", end, ")"
  ))
}

# links to several sections of a script: "`a`, `b` and `c`"
section_links <- function(file, labels) {
  links <- vapply(labels, \(label) section_link(file, label), character(1))
  I(paste_and(unname(links)))
}

# white panels, so the grey band in Agreement shows
theme_set(theme_bw())

sessions <- list.files(
  here(post_path, "results"),
  pattern = "^session-[0-9]+[.]rds$",
  full.names = TRUE
) |>
  lapply(readRDS)

# run.R names the versions as they were when this ran. The PR, greta#855, has
# since taken the tf-warmup-i547 branch, so the page relabels them: tf-warmup
# is an earlier commit of the PR, and branch is the PR at 5bdab31e
version_labels <- c(
  CRAN = "CRAN",
  main = "main",
  `#850` = "#850",
  `tf-warmup` = "#855 early",
  branch = "#855"
)
pr_label <- version_labels[["branch"]]

versions <- sessions[[1]]$versions
# pak installed branch from a worktree, which the results record as a local
# path; run.R names the same commit on GitHub
on_github <- c(
  branch = "greta-dev/greta@5bdab31e9ec9295c845a4fac9d1201e1aabb9c93"
)
installed_locally <- startsWith(versions, "local::")
versions[installed_locally] <- on_github[names(versions)[installed_locally]]
names(versions) <- version_labels[names(versions)]
version_levels <- names(versions)
version_colours <- setNames(
  c("#E69F00", "#56B4E9", "#009E73", "#D55E00", "#CC79A7")[seq_along(
    version_levels
  )],
  version_levels
)
bench_settings <- sessions[[1]]$results[[1]]$bench_settings
n_sessions <- length(sessions)

# one row per session and version, holding that version's results
runs <- lapply(sessions, function(session_file) {
  result_names <- names(session_file$results)
  tibble(
    session = session_file$session,
    version = factor(
      unname(version_labels[result_names]),
      levels = version_levels
    ),
    position = match(result_names, session_file$order),
    results = unname(session_file$results)
  )
}) |>
  bind_rows()

model_levels <- names(runs$results[[1]]$mcmc_same)
# benchmark.R's section labels spell each model with hyphens
model_sections <- gsub("_", "-", model_levels)
setting_levels <- c("the same arguments", "each version's defaults")

# a part of the results that is a table or a value per run, stacked, with the
# run it came from
stack_tables <- function(part) {
  pieces <- lapply(seq_len(nrow(runs)), function(i) {
    values <- runs$results[[i]][[part]]
    if (!is.data.frame(values)) {
      values <- tibble(value = values)
    }
    tibble(session = runs$session[i], version = runs$version[i], values)
  })
  bind_rows(pieces)
}

# a part of the results that is a bench_mark per model, or one per run,
# stacked into one bench_mark, with the run and model each came from
stack_marks <- function(part) {
  pieces <- list()
  for (i in seq_len(nrow(runs))) {
    marks <- runs$results[[i]][[part]]
    if (inherits(marks, "bench_mark")) {
      marks <- list(marks)
    }
    for (j in seq_along(marks)) {
      mark <- marks[[j]]
      mark$session <- runs$session[i]
      mark$version <- runs$version[i]
      mark$model <- if (is.null(names(marks))) NA_character_ else names(marks)[j]
      pieces[[length(pieces) + 1]] <- mark
    }
  }
  vctrs::vec_rbind(!!!pieces)
}

provenance <- stack_tables("provenance")
sampler_iterations_per_draw <- stack_tables("sampler_iterations_per_draw")
opt_marks <- stack_marks("opt_adam")
opt_marks$setting <- NA_character_

# the two sets of timed mcmc() calls, stacked, with which arguments each used
same_marks <- stack_marks("mcmc_same")
same_marks$setting <- setting_levels[1]
defaults_marks <- stack_marks("mcmc_defaults")
defaults_marks$setting <- setting_levels[2]
mcmc_marks <- vctrs::vec_rbind(same_marks, defaults_marks)

# one row per timed call: bench keeps every call's time in the `time` column
every_call <- function(marks) {
  marks |>
    as_tibble() |>
    select(session, version, model, setting, time) |>
    mutate(call = lapply(time, seq_along)) |>
    unnest(c(time, call)) |>
    mutate(
      seconds = as.numeric(time),
      model = factor(model, levels = model_levels),
      setting = factor(setting, levels = setting_levels),
      .keep = "unused"
    )
}

# bench's own summary() of each mark, with the mean and longest of its calls
# added, all in seconds
bench_summary <- function(marks) {
  summaries <- summary(marks, filter_gc = FALSE)
  tibble(
    session = summaries$session,
    version = summaries$version,
    model = summaries$model,
    setting = summaries$setting,
    min = as.numeric(summaries$min),
    median = as.numeric(summaries$median),
    mean = vapply(marks$time, \(time) mean(as.numeric(time)), numeric(1)),
    max = vapply(marks$time, \(time) max(as.numeric(time)), numeric(1)),
    `itr/sec` = summaries$`itr/sec`,
    n_itr = summaries$n_itr,
    total_time = as.numeric(summaries$total_time)
  )
}

# The first call on a model traces its TensorFlow functions; later calls on the
# same model reuse them.
label_calls <- function(call) {
  last <- max(call)
  later <- if (last == 2) {
    "second call"
  } else if (last == 3) {
    "calls 2 and 3"
  } else {
    paste0("calls 2 to ", last)
  }
  factor(
    ifelse(call == 1, "first call", later),
    levels = c("first call", later)
  )
}

# three significant figures each, where format() would pad a vector to the
# decimal places of its smallest value; formatC() pads to a common width, so
# trim that
format_number <- function(x) {
  trimws(formatC(signif(x, 3), format = "fg", digits = 3, big.mark = ","))
}

# "CRAN 10.6, main 11.2, #843 0.643"
versions_text <- function(version, value) {
  paste(paste(version, format_number(value)), collapse = ", ")
}

# "CRAN, main and #843"
paste_and <- function(x) {
  if (length(x) <= 1) {
    return(x)
  }
  paste(paste(x[-length(x)], collapse = ", "), "and", x[length(x)])
}

# "2.49 to 3.21": the range over models of a ratio with #855 as its unit, from
# a table of each version's `lowest` and `highest`, filtered by `...`
ratio_range <- function(ratios, version_label, ...) {
  row <- filter(ratios, version == version_label, ...)
  paste(format_number(row$lowest), "to", format_number(row$highest))
}

version_scale <- scale_colour_manual(values = version_colours, guide = "none")
version_fill <- scale_fill_manual(values = version_colours, guide = "none")
jitter <- position_jitter(width = 0, height = 0.2, seed = 2026)

# A raincloud for each version: the density of its calls above, their box plot
# in the middle, and every call as a dot below. The summaries are drawn first
# and faint, so the dots sit on top.
raincloud <- list(
  ggdist::stat_halfeye(
    aes(fill = version),
    width = 0.4,
    justification = -0.3,
    .width = 0,
    point_colour = NA,
    alpha = 0.3
  ),
  geom_boxplot(
    aes(fill = version),
    width = 0.25,
    outlier.shape = NA,
    alpha = 0.3
  ),
  # a fixed dot size, so a call looks the same size in every panel
  ggdist::stat_dots(
    aes(fill = version),
    side = "bottom",
    layout = "swarm",
    justification = 1.3,
    scale = 0.75,
    binwidth = unit(2.6, "mm"),
    overflow = "compress",
    colour = NA,
    alpha = 0.6
  ),
  version_scale,
  version_fill,
  theme_bw(base_size = 13)
)
raincloud_caption <- "Each dot is one call, from one session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles."
session_caption <- "Each small dot is one session's second call. The large faint dot behind them is their mean."

# a searchable table, its numbers rounded for reading
dt_table <- function(data, digits = 3) {
  table <- DT::datatable(
    data,
    rownames = FALSE,
    filter = "top",
    options = list(pageLength = 10, autoWidth = TRUE)
  )
  decimal_columns <- names(data)[vapply(data, is.double, logical(1))]
  if (length(decimal_columns) == 0) {
    return(table)
  }
  DT::formatRound(table, decimal_columns, digits)
}

# The PR at a glance, which the opening quotes: this post's mcmc() calls and
# the call overhead post's, on CRAN and the PR
glance_rows <- c(
  "`mcmc()`, each version's defaults, first call",
  "`mcmc()`, each version's defaults, calls after the first",
  "`mcmc()`, 1000 warmup and 1000 draws, calls after the first",
  "`mcmc()` with the progress bar, 1000 and 1000, calls after the first",
  "`mcmc()` with `one_by_one = TRUE`, 200 and 200, calls after the first",
  "`opt()` with `adam()`, 2000 iterations, first call",
  "`opt()` with `adam()`, 2000 iterations, calls after the first"
)

this_post_calls <- every_call(mcmc_marks) |>
  filter(version %in% c("CRAN", pr_label)) |>
  mutate(
    row = case_when(
      setting == setting_levels[2] & call == 1 ~ glance_rows[1],
      setting == setting_levels[2] & call > 1 ~ glance_rows[2],
      setting == setting_levels[1] & call > 1 ~ glance_rows[3]
    ),
    post = "[this post](#speed)"
  ) |>
  filter(!is.na(row)) |>
  select(row, post, version, model, seconds)

# the call overhead post names the PR `branch`. Each bench_mark keeps every
# call's time, and opt()'s has one row per iteration limit, of which this
# table takes 2000
overhead_path <- "posts/2026-10-07-call-overhead-i547"
overhead_sessions <- list.files(
  here(overhead_path, "results"),
  pattern = "^session-[0-9]+[.]rds$",
  full.names = TRUE
) |>
  lapply(readRDS)
calls_of <- function(time) {
  tibble(call = seq_along(time), seconds = as.numeric(time))
}
overhead_calls <- lapply(overhead_sessions, function(session_file) {
  lapply(c("CRAN", "branch"), function(version) {
    result <- session_file$results[[version]]
    lapply(names(result$mcmc), function(model) {
      mcmc_runs <- result$mcmc[[model]]
      opt_runs <- result$opt[[model]]
      bind_rows(
        calls_of(mcmc_runs$progress$time[[1]]) |>
          mutate(row = if_else(call > 1, glance_rows[4], NA)),
        calls_of(mcmc_runs$one_by_one$time[[1]]) |>
          mutate(row = if_else(call > 1, glance_rows[5], NA)),
        calls_of(opt_runs$time[[which(opt_runs$max_iterations == 2000)]]) |>
          mutate(row = if_else(call == 1, glance_rows[6], glance_rows[7]))
      ) |>
        mutate(version = version, model = model)
    }) |>
      bind_rows()
  }) |>
    bind_rows()
}) |>
  bind_rows() |>
  filter(!is.na(row)) |>
  mutate(
    version = if_else(version == "branch", pr_label, version),
    post = "[Call overhead](../2026-10-07-call-overhead-i547/#summary)"
  ) |>
  select(row, post, version, model, seconds)

glance_calls <- bind_rows(this_post_calls, overhead_calls) |>
  mutate(
    version = if_else(version == "CRAN", "CRAN", "PR"),
    model = as.character(model)
  )

glance_seconds <- glance_calls |>
  group_by(row, post, version) |>
  summarise(seconds = median(seconds), .groups = "drop") |>
  pivot_wider(names_from = version, values_from = seconds)

glance_ratios <- glance_calls |>
  group_by(row, version, model) |>
  summarise(seconds = median(seconds), .groups = "drop") |>
  pivot_wider(names_from = version, values_from = seconds) |>
  group_by(row) |>
  summarise(
    models = n(),
    lowest = min(CRAN / PR),
    highest = max(CRAN / PR),
    .groups = "drop"
  ) |>
  mutate(version = "CRAN")

greta#855 runs mcmc()’s warmup and sampling, and opt()’s loop with an optimiser such as adam(), inside TensorFlow, rather than returning to R every few iterations. This post compares it with greta 0.6.0 on CRAN, with main, and with greta#850, which it supersedes, on four of greta’s example models. Once the first call has traced the model:

Every version samples the same posteriors.

The PR at a glance

Three posts benchmark greta#855: this one, on the PR at 5bdab31e; Call overhead, on its last two changes, which keep opt()’s traced loop on the model and the chain’s state as tensors between calls, at f30ab146; and TFP’s loops, on two TensorFlow Probability loops tried and not adopted. Warmup schemes is about greta#853, a branch built on this one and not part of the PR.

The PR’s timings against CRAN, from the first two posts. Seconds are the median over every call on every model; the last numeric column is how many times as long CRAN takes as the PR, as the range over models of each model’s median. Each method was called several times on each model: the first call traces the model, and the calls after it reuse what it traced.

Code
glance_seconds |>
  left_join(glance_ratios, by = "row") |>
  mutate(row = factor(row, levels = glance_rows)) |>
  arrange(row) |>
  transmute(
    call = as.character(row),
    models,
    `CRAN 0.6.0, s` = format_number(CRAN),
    `this PR, s` = format_number(PR),
    `CRAN ÷ this PR` = paste(format_number(lowest), "to", format_number(highest)),
    `measured in` = post
  ) |>
  knitr::kable(align = "lrrrrl")
call models CRAN 0.6.0, s this PR, s CRAN ÷ this PR measured in
mcmc(), each version’s defaults, first call 4 3.23 1.52 1.86 to 2.3 this post
mcmc(), each version’s defaults, calls after the first 4 2.87 0.92 2.49 to 3.21 this post
mcmc(), 1000 warmup and 1000 draws, calls after the first 4 2.85 0.465 4.97 to 6.3 this post
mcmc() with the progress bar, 1000 and 1000, calls after the first 2 3.69 0.641 5.34 to 5.79 Call overhead
mcmc() with one_by_one = TRUE, 200 and 200, calls after the first 2 1.32 0.383 3.22 to 3.62 Call overhead
opt() with adam(), 2000 iterations, first call 2 2.18 0.469 2.87 to 7.91 Call overhead
opt() with adam(), 2000 iterations, calls after the first 2 1.75 0.0478 28.9 to 47.3 Call overhead

With each version’s defaults, the PR runs 2000 warmup iterations and 2000 draws where CRAN runs 1000 of each: about the same number of sampler iterations, since each of CRAN’s draws costs two, so CRAN takes those times for half the draws. In the rows with the same warmup and draws, CRAN runs twice the sampler iterations, so those rows include greta#850’s thinning fix as well as the PR’s own changes. With verbose = FALSE, as here, the PR’s last two changes leave mcmc()’s time as it was, which Call overhead measured on its own.

How long does mcmc() take?

Every mcmc() call on each model, with two sets of arguments: the same warmup and n_samples (1000 and 1000) on every version, which is twice the sampler iterations on CRAN and main as on the others, and each version’s defaults, which is about the same number of iterations on all of them. The first call on a new model traces its TensorFlow functions, and the second call reuses them.

Code
mcmc_timings <- every_call(mcmc_marks) |>
  mutate(calls = label_calls(call))
mcmc_summary <- bench_summary(mcmc_marks)

# for each model and call, each version's median seconds over #855's
mcmc_ratios <- mcmc_timings |>
  group_by(setting, calls, model, version) |>
  summarise(seconds = median(seconds), .groups = "drop_last") |>
  mutate(times_as_long = seconds / seconds[version == pr_label]) |>
  group_by(setting, calls, version) |>
  summarise(
    lowest = min(times_as_long),
    highest = max(times_as_long),
    .groups = "drop"
  )
Code
ggplot(mcmc_timings, aes(x = seconds, y = version, colour = version)) +
  raincloud +
  scale_x_log10() +
  facet_grid(vars(model), vars(setting, calls)) +
  labs(x = "Seconds, on a log scale", y = NULL)

Each dot is one call, from one session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles.

Timed by benchmark.R’s bench-mcmc-same-linear, bench-mcmc-same-multiple-linear, bench-mcmc-same-hierarchical-linear and bench-mcmc-same-eight-schools sections (the same arguments), and bench-mcmc-defaults-linear, bench-mcmc-defaults-multiple-linear, bench-mcmc-defaults-hierarchical-linear and bench-mcmc-defaults-eight-schools (each version’s defaults).

With each version’s defaults, a second call takes 2.49 to 3.21 times as long on CRAN as on #855, 2.63 to 3.49 on main and 3.77 to 4.91 on #850; a first call takes 1.86 to 2.3 times as long on CRAN. With the same arguments, a second call takes 4.97 to 6.3 times as long on CRAN, which runs twice the iterations.

Table: bench’s summary() of every mark
Code
dt_table(mcmc_summary, digits = 4)

How long does opt() with adam() take?

Every call of opt() with adam() and up to 2000 iterations, 3 on each model, from the same seed each time.

Code
opt_timings <- every_call(opt_marks) |>
  mutate(calls = label_calls(call))
opt_summary <- bench_summary(opt_marks)

later_opt_medians <- opt_timings |>
  filter(call > 1) |>
  group_by(version) |>
  summarise(seconds = median(seconds))

# for each model and call, each version's median seconds over #855's
opt_ratios <- opt_timings |>
  group_by(calls, model, version) |>
  summarise(seconds = median(seconds), .groups = "drop_last") |>
  mutate(times_as_long = seconds / seconds[version == pr_label]) |>
  group_by(calls, version) |>
  summarise(
    lowest = min(times_as_long),
    highest = max(times_as_long),
    .groups = "drop"
  )
Code
ggplot(opt_timings, aes(x = seconds, y = version, colour = version)) +
  raincloud +
  scale_x_log10() +
  facet_grid(vars(model), vars(calls)) +
  labs(x = "Seconds, on a log scale", y = NULL)

Each dot is one call, from one session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles.

Timed by benchmark.R’s bench-opt-adam-linear, bench-opt-adam-multiple-linear, bench-opt-adam-hierarchical-linear and bench-opt-adam-eight-schools sections.

For a later call, CRAN takes 2.59 to 19.6 times as long as #855, and main 0.999 to 3.6. The median later call takes CRAN 1.84, main 0.362, #850 0.357, #855 early 0.124, #855 0.131 seconds. The PR’s last commit also keeps opt()’s traced loop on the model, and on it CRAN takes 28.9 to 47.3 times as long for a later call (the PR at a glance).

Table: bench’s summary() of every mark
Code
dt_table(opt_summary, digits = 4)

How many effective samples per second?

The bulk effective sample size (ESS) (Vehtari et al. 2021) of the parameter with the smallest, as Hoffman and Gelman (2014) report, per second of the second mcmc() call on each model, warmup included: the time-normalised ESS of Girolami and Calderhead (2011).

Code
# every model's posterior summary, from each run's draws; the checks below use
# it too
setting_names <- c(same = setting_levels[1], defaults = setting_levels[2])
posterior <- lapply(seq_len(nrow(runs)), function(i) {
  draws <- runs$results[[i]]$mcmc_draws
  lapply(names(setting_names), function(setting_name) {
    lapply(names(draws[[setting_name]]), function(model_name) {
      tibble(
        session = runs$session[i],
        version = runs$version[i],
        setting = setting_names[[setting_name]],
        model = model_name,
        posterior::summarise_draws(
          draws[[setting_name]][[model_name]],
          "mean",
          "mcse_mean",
          "rhat",
          "ess_bulk"
        )
      )
    }) |>
      bind_rows()
  }) |>
    bind_rows()
}) |>
  bind_rows() |>
  mutate(
    model = factor(model, levels = model_levels),
    setting = factor(setting, levels = setting_levels)
  )

second_calls <- mcmc_timings |>
  filter(call == 2) |>
  select(session, version, model, setting, seconds)

efficiency <- posterior |>
  group_by(session, version, model, setting) |>
  summarise(smallest_ess_bulk = min(ess_bulk), .groups = "drop") |>
  left_join(second_calls, by = c("session", "version", "model", "setting")) |>
  mutate(smallest_ess_per_second = smallest_ess_bulk / seconds)

efficiency_means <- efficiency |>
  group_by(model, setting, version) |>
  summarise(
    smallest_ess_per_second = mean(smallest_ess_per_second),
    .groups = "drop"
  )

# for each model, #855's mean over each version's: how many times that
# version's ESS per second #855 gives
ess_ratios <- efficiency_means |>
  group_by(setting, model) |>
  mutate(
    pr_gives = smallest_ess_per_second[version == pr_label] /
      smallest_ess_per_second
  ) |>
  group_by(setting, version) |>
  summarise(lowest = min(pr_gives), highest = max(pr_gives), .groups = "drop")

gg_sessions <- function(data, means, x, x_label) {
  ggplot(data, aes(x = {{ x }}, y = version, colour = version)) +
    geom_point(data = means, size = 5, alpha = 0.25) +
    geom_point(position = jitter, size = 1.2) +
    version_scale +
    facet_wrap(vars(model, setting), ncol = 2, scales = "free_x") +
    labs(x = x_label, y = NULL)
}
Code
gg_sessions(
  efficiency,
  efficiency_means,
  smallest_ess_per_second,
  "Smallest bulk ESS per second"
)

Each small dot is one session’s second call. The large faint dot behind them is their mean.

The draws are kept by benchmark.R’s mcmc-draws section, and the seconds are the second calls timed in How long does mcmc() take?.

With each version’s defaults, #855 gives 1.86 to 6.53 times CRAN’s smallest bulk ESS per second, and 3.27 to 7.01 times #850’s. With the same arguments, it gives 1.62 to 3.13 times CRAN’s.

Code
# for each model, main's median second call and mean smallest bulk ESS over
# #850's, with the same arguments
thinning_ratios <- efficiency |>
  filter(setting == setting_levels[1], version %in% c("main", "#850")) |>
  group_by(model, version) |>
  summarise(
    seconds = median(seconds),
    smallest_ess_bulk = mean(smallest_ess_bulk),
    .groups = "drop_last"
  ) |>
  summarise(
    seconds = seconds[version == "main"] / seconds[version == "#850"],
    smallest_ess_bulk = smallest_ess_bulk[version == "main"] /
      smallest_ess_bulk[version == "#850"]
  ) |>
  pivot_longer(-model, names_to = "measure", values_to = "main_over_850") |>
  group_by(measure) |>
  summarise(
    version = "main",
    lowest = min(main_over_850),
    highest = max(main_over_850)
  )

#850 changes only main’s thinning, its default warmup and n_samples, and its progress bar, which these runs hide, so with the same arguments the two show what the thinning fix alone does. Main runs twice the sampler iterations, warmup included, takes 1.37 to 1.55 times as long as #850 for a second call, and gives 1.55 to 4.93 times its smallest bulk ESS.

Table: every session
Code
dt_table(efficiency, digits = 1)

Are more chains worth it?

mcmc() with each version’s defaults on 4, 16 and 64 chains, the second call on each model: greta#294 found that TensorFlow runs many chains at nearly the cost of a few.

Code
chain_seconds <- lapply(seq_len(nrow(runs)), function(i) {
  marks <- runs$results[[i]]$chains
  lapply(names(marks), function(model_name) {
    mark <- marks[[model_name]]
    tibble(
      session = runs$session[i],
      version = runs$version[i],
      model = model_name,
      chains = mark$chains,
      # each mark's calls are the first and the second on its model
      seconds = vapply(mark$time, \(time) as.numeric(time)[2], numeric(1))
    )
  }) |>
    bind_rows()
}) |>
  bind_rows()

chain_ess <- lapply(seq_len(nrow(runs)), function(i) {
  ess <- runs$results[[i]]$chains_ess
  lapply(names(ess), function(model_name) {
    tibble(
      session = runs$session[i],
      version = runs$version[i],
      model = model_name,
      ess[[model_name]]
    )
  }) |>
    bind_rows()
}) |>
  bind_rows()

chain_measures <- chain_seconds |>
  left_join(chain_ess, by = c("session", "version", "model", "chains")) |>
  mutate(smallest_ess_per_second = smallest_ess_bulk / seconds) |>
  select(session, version, model, chains, seconds, smallest_ess_per_second) |>
  pivot_longer(
    c(seconds, smallest_ess_per_second),
    names_to = "measure",
    values_to = "value"
  ) |>
  mutate(
    model = factor(model, levels = intersect(model_levels, model)),
    chains = factor(chains, levels = bench_settings$scaling_chains),
    measure = factor(
      measure,
      levels = c("seconds", "smallest_ess_per_second"),
      labels = c("seconds", "smallest bulk ESS per second")
    )
  )

chain_means <- chain_measures |>
  group_by(model, measure, chains, version) |>
  summarise(value = mean(value), .groups = "drop")

fewest_chains <- as.character(min(bench_settings$scaling_chains))
most_chains <- as.character(max(bench_settings$scaling_chains))

# for each model and measure, #855's mean at the most chains over its mean at
# the fewest
pr_growth <- chain_means |>
  filter(version == pr_label) |>
  group_by(model, measure) |>
  summarise(
    growth = value[chains == most_chains] / value[chains == fewest_chains],
    .groups = "drop"
  )

pr_time_growth <- with(
  filter(pr_growth, measure == "seconds"),
  setNames(format_number(growth), model)
)
pr_ess_growth <- with(
  filter(pr_growth, measure != "seconds"),
  setNames(format_number(growth), model)
)

# at the most chains, #855's mean ESS per second over CRAN's
pr_over_cran <- chain_means |>
  filter(chains == most_chains, measure == "smallest bulk ESS per second") |>
  group_by(model) |>
  summarise(
    ratio = value[version == pr_label] / value[version == "CRAN"],
    .groups = "drop"
  ) |>
  with(setNames(format_number(ratio), model))
Code
ggplot(chain_measures, aes(x = value, y = version, colour = version)) +
  geom_point(data = chain_means, size = 5, alpha = 0.25) +
  geom_point(position = jitter, size = 1.2) +
  version_scale +
  facet_grid(
    vars(chains),
    vars(model, measure),
    scales = "free_x",
    labeller = labeller(chains = label_both)
  ) +
  labs(x = NULL, y = NULL)

Each small dot is one session’s second call. The large faint dot behind them is their mean.

Timed by benchmark.R’s bench-chains-linear and bench-chains-eight-schools sections, which also keep each call’s smallest bulk ESS.

From 4 chains to 64, a call on #855 takes 1.96 times as long on linear and 2.12 on eight_schools, and gives 6.67 and 2.56 times the effective samples per second. At 64 chains, #855 gives 4.74 times CRAN’s effective samples per second on linear, and 2.35 times on eight_schools.

Table: every session
Code
chain_measures |>
  pivot_wider(names_from = measure, values_from = value) |>
  dt_table(digits = 2)

Do the versions sample the same posterior?

Faster sampling is only worth having if it samples the same posterior. Three checks, on the draws of each model’s second mcmc() call, which benchmark.R’s mcmc-draws section keeps.

Agreement

Whether two versions estimate the same posterior means. Each version’s means and Monte Carlo standard errors (MCSE) are pooled over its sessions, and the difference between two versions’ means is divided by the MCSE of that difference (Vehtari et al. 2021; Magnusson et al. 2025),

\[ z = \frac{\bar\theta_{\text{b}} - \bar\theta_{\text{a}}}{\sqrt{\text{se}^2_{\text{a}} + \text{se}^2_{\text{b}}}}, \]

where a version’s pooled standard error is \(\sqrt{\sum \text{mcse}^2} / n\) over its \(n\) sessions, each with a seed of its own. If both versions sample the same posterior, each \(z\) is close to a standard normal draw.

Code
pooled <- posterior |>
  group_by(setting, version, model, variable) |>
  summarise(
    sessions = n(),
    mean = mean(mean),
    se = sqrt(sum(mcse_mean^2)) / n(),
    .groups = "drop"
  )

pairs <- utils::combn(version_levels, 2)
agreement <- lapply(seq_len(ncol(pairs)), function(i) {
  reference <- filter(pooled, version == pairs[1, i])
  compared <- filter(pooled, version == pairs[2, i])
  inner_join(
    reference,
    compared,
    by = c("setting", "model", "variable"),
    suffix = c("_a", "_b")
  ) |>
    mutate(
      comparison = paste(pairs[2, i], "vs", pairs[1, i]),
      z = (mean_b - mean_a) / sqrt(se_a^2 + se_b^2)
    )
}) |>
  bind_rows() |>
  mutate(comparison = factor(comparison, levels = rev(unique(comparison))))

# the band all of a comparison's z fall inside with probability 0.95 when both
# versions sample the same posterior: a Bonferroni bound over its parameters
parameters_compared <- n_distinct(paste(agreement$model, agreement$variable))
band <- qnorm(1 - 0.05 / (2 * parameters_compared))

comparisons <- n_distinct(agreement$comparison, agreement$setting)
outside_band <- filter(agreement, abs(z) > band)
outside_text <- paste0(
  outside_band$comparison,
  "'s `",
  outside_band$variable,
  "` on ",
  outside_band$model,
  " with ",
  outside_band$setting,
  " (z = ",
  format_number(outside_band$z),
  ")"
)
Code
ggplot(agreement, aes(x = z, y = comparison)) +
  annotate(
    "rect",
    xmin = -band,
    xmax = band,
    ymin = -Inf,
    ymax = Inf,
    fill = "grey80",
    alpha = 0.5
  ) +
  geom_point(position = jitter, alpha = 0.5) +
  facet_wrap(vars(setting)) +
  labs(x = "z", y = NULL)

Each dot is one parameter. The grey band is where every parameter’s z falls, with probability at least 0.95, when both versions sample the same posterior.

Computed from the draws kept by benchmark.R’s mcmc-draws section.

The grey band is \(\pm\) 3.12: if two versions sample the same posterior, all 28 parameters’ \(z\) fall inside it with probability at least 0.95. Outside it, 1 of the 560 \(z\): main vs CRAN’s `coefs[2,1]` on multiple_linear with each version’s defaults (z = 3.3). Each of the 20 comparisons has at most a 0.05 chance of a \(z\) outside the band, so up to 1 such comparison is expected by chance.

Table: every parameter
Code
agreement |>
  select(setting, comparison, model, variable, mean_a, se_a, mean_b, se_b, z) |>
  dt_table()

Convergence

For each session’s second mcmc() call, the largest rank-normalised split-\(\hat{R}\) over the model’s parameters (Vehtari et al. 2021).

Code
convergence <- posterior |>
  group_by(session, version, model, setting) |>
  summarise(largest_rhat = max(rhat), .groups = "drop")

convergence_means <- convergence |>
  group_by(model, setting, version) |>
  summarise(largest_rhat = mean(largest_rhat), .groups = "drop")

rhat_threshold <- 1.01
unconverged <- convergence |>
  group_by(version) |>
  summarise(runs = n(), above = sum(largest_rhat > rhat_threshold))
worst_run <- slice_max(convergence, largest_rhat, n = 1)
Code
gg_sessions(convergence, convergence_means, largest_rhat, "Largest R-hat")

Each small dot is one session’s second call. The large faint dot behind them is their mean.

Computed from the draws kept by benchmark.R’s mcmc-draws section.

The largest \(\hat{R}\) is above 1.01 in about as many runs on every version, of 40 each: CRAN 30, main 36, #850 32, #855 early 34, #855 34. No version converges in every run, as four chains of these lengths are short for these models. The highest, 2.63, is #855 early’s on multiple_linear with the same arguments.

Table: every session
Code
dt_table(convergence)

Sampler iterations per kept draw

?mcmc says each kept draw is thin iterations of the sampler after the one before. benchmark.R counts the iterations between draws at thin = 1 and thin = 3, with a random walk that accepts every proposal.

Code
iterations_by_version <- sampler_iterations_per_draw |>
  group_by(version, thin) |>
  summarise(
    iterations = paste(sort(unique(iterations_per_draw)), collapse = " or "),
    .groups = "drop"
  )

as_documented <- sampler_iterations_per_draw |>
  group_by(version) |>
  summarise(matches = all(iterations_per_draw == thin)) |>
  filter(matches) |>
  pull(version) |>
  as.character()

iterations_by_version |>
  pivot_wider(
    names_from = thin,
    values_from = iterations,
    names_prefix = "thin = "
  ) |>
  knitr::kable()
version thin = 1 thin = 3
CRAN 2 4
main 2 4
#850 1 3
#855 early 1 3
#855 1 3

Counted by benchmark.R’s benchmark-sampler-iterations section, in every session.

Each kept draw costs thin iterations, as ?mcmc says, on #850, #855 early and #855.

Traceplots and posterior densities

Every draw of each chain from each model’s second mcmc() call with each version’s defaults: traceplots from session 1, numbered from the end of warmup, with a row of panels for each version; and densities pooled over sessions, every version’s overlaid in one panel per parameter. CRAN and main kept every second iteration, so their traces have half the draws.

Code
# every draw of every run with each version's defaults, one row per draw and
# parameter
draws <- lapply(seq_len(nrow(runs)), function(i) {
  model_draws <- runs$results[[i]]$mcmc_draws$defaults
  lapply(names(model_draws), function(model_name) {
    posterior::as_draws_df(model_draws[[model_name]]) |>
      as.data.frame() |>
      pivot_longer(
        -c(.chain, .iteration, .draw),
        names_to = "variable",
        values_to = "value"
      ) |>
      mutate(
        session = runs$session[i],
        version = runs$version[i],
        model = model_name
      )
  }) |>
    bind_rows()
}) |>
  bind_rows()

# a model's draws array is draws by chains by parameters
parameter_count <- function(name) {
  dim(runs$results[[1]]$mcmc_draws$same[[name]])[3]
}

# wide enough for a column of panels per parameter
draws_width <- function(name) {
  max(8, 1.6 * parameter_count(name))
}

densities_height <- function(name) {
  1.5 + 2 * ceiling(parameter_count(name) / 4)
}

gg_trace <- function(name) {
  model_draws <- draws |>
    filter(session == 1, model == name) |>
    mutate(variable = factor(variable, levels = unique(variable)))
  # each parameter's panels share its range across the versions, so they can
  # be compared down a column
  parameter_ranges <- model_draws |>
    group_by(variable) |>
    reframe(value = range(value)) |>
    crossing(version = unique(model_draws$version)) |>
    mutate(.iteration = 1L)
  ggplot(model_draws, aes(x = .iteration, y = value, colour = factor(.chain))) +
    geom_line(linewidth = 0.2) +
    geom_blank(
      data = parameter_ranges,
      aes(x = .iteration, y = value),
      inherit.aes = FALSE
    ) +
    facet_wrap(
      vars(version, variable),
      ncol = n_distinct(model_draws$variable),
      scales = "free_y"
    ) +
    labs(x = "Draw", y = NULL, colour = "Chain")
}

# every version's density in one panel per parameter, each with its own line
# type as well as colour, so a line drawn on top of an identical one still shows
gg_densities <- function(name) {
  draws |>
    filter(model == name) |>
    mutate(variable = factor(variable, levels = unique(variable))) |>
    ggplot(aes(x = value, colour = version, linetype = version)) +
    geom_density(linewidth = 0.7) +
    scale_colour_manual(values = version_colours) +
    facet_wrap(~variable, scales = "free", ncol = 4) +
    labs(x = NULL, y = NULL, colour = NULL, linetype = NULL) +
    theme(legend.position = "top")
}
Code
gg_trace("linear")

Code
gg_densities("linear")

Code
gg_trace("multiple_linear")

Code
gg_densities("multiple_linear")

Code
gg_trace("hierarchical_linear")

Code
gg_densities("hierarchical_linear")

Code
gg_trace("eight_schools")

Code
gg_densities("eight_schools")

How it was measured

Two scripts made every result here: benchmark.R is what each version runs, in a fresh R session of its own, and run.R runs it for each version, in 5 sessions, with the versions in a new random order each session. Each was on Darwin 25.5.0 arm64 with 8 cores. mcmc() ran with 4 chains on 4 cores and verbose = FALSE, as set in benchmark.R’s benchmark-settings section.

Code
version_notes <- c(
  CRAN = "greta 0.6.0, the release users upgrade from",
  main = "greta's main branch",
  `#850` = "makes each kept draw cost `thin` iterations, and doubles the default `warmup` and `n_samples` to 2000",
  `#855 early` = "an earlier commit of the PR: tunes warmup, draws `hmc()`'s leapfrog steps and runs `opt()`'s loop inside TensorFlow",
  `#855` = "the PR at 5bdab31e: also traces warmup and sampling as one function, for the number of chains, and keeps it on the model"
)

provenance |>
  distinct(version, greta_version, greta_sha, tensorflow_version, tfp_version, r_version) |>
  arrange(version) |>
  mutate(
    what_it_is = version_notes[as.character(version)],
    installed_from = versions[as.character(version)],
    # pak records a CRAN install's version in place of a commit, and a local
    # install nothing, so where it recorded no commit, take the one run.R names
    commit = ifelse(
      grepl("^[0-9a-f]{40}$", greta_sha),
      greta_sha,
      sub("^.*@", "", installed_from)
    ),
    installed_from = ifelse(
      grepl("^[0-9a-f]{40}$", commit),
      paste0(
        "[",
        sub("@.*", "", installed_from),
        "@",
        substr(commit, 1, 8),
        "](https://github.com/greta-dev/greta/commit/",
        commit,
        ")"
      ),
      installed_from
    )
  ) |>
  select(
    version,
    `what it is` = what_it_is,
    `installed from` = installed_from,
    greta = greta_version,
    TensorFlow = tensorflow_version,
    TFP = tfp_version,
    R = r_version
  ) |>
  knitr::kable()
version what it is installed from greta TensorFlow TFP R
CRAN greta 0.6.0, the release users upgrade from greta@0.6.0 0.6.0 2.15.1 0.23.0 4.6.1
main greta’s main branch greta-dev/greta@0a22f8c7 0.6.0.9000 2.21.0 0.25.0 4.6.1
#850 makes each kept draw cost thin iterations, and doubles the default warmup and n_samples to 2000 greta-dev/greta@5a02f6c5 0.6.0.9000 2.21.0 0.25.0 4.6.1
#855 early an earlier commit of the PR: tunes warmup, draws hmc()’s leapfrog steps and runs opt()’s loop inside TensorFlow greta-dev/greta@59da5cc9 0.6.0.9000 2.21.0 0.25.0 4.6.1
#855 the PR at 5bdab31e: also traces warmup and sampling as one function, for the number of chains, and keeps it on the model greta-dev/greta@5bdab31e 0.6.0.9000 2.21.0 0.25.0 4.6.1

The versions are pinned to these commits in run.R’s run-versions section. Every version’s greta_sitrep() report and sessioninfo::session_info() are in Session details.

Table: the order of each session
Code
runs |>
  select(session, position, version) |>
  arrange(session, position) |>
  dt_table()
run.R
# Runs benchmark.R against CRAN, main, greta#850 and two commits of the
# tf-warmup-i547 branch built on it, in a fresh R session per version, in five
# sessions:
#
#   Rscript --quiet --vanilla posts/2026-10-07-tf-warmup-i547/run.R
#
# Each session writes results/session-<n>.rds, and sessions already there are
# skipped, so a rerun after a crash carries on. Each `# ---- label ----` line
# starts a section that index.qmd shows by that label.

# ---- run-versions ----
# Pin every version to a commit: they move, and the post links each one.
# tf-warmup is a commit of the tf-warmup-i547 branch, with warmup in
# TensorFlow. branch is the head of tf-sampler-i547, which adds functions
# traced for the sampler's number of chains, one traced function for warmup
# and sampling, and that function cached on the model. It was not on GitHub
# when this ran, so pak installed it from a worktree, clean at the commit, and
# the saved results record that local path
versions <- c(
  CRAN = "greta@0.6.0",
  main = "greta-dev/greta@0a22f8c7e63ac32fa0926cdf67a85c9f69acd1d5",
  `#850` = "greta-dev/greta@5a02f6c5ff5497363ac4bf419368bb3c5925d5cb",
  `tf-warmup` = "greta-dev/greta@59da5cc9ef79eae5437072b8f1837e466ea7196a",
  branch = "greta-dev/greta@5bdab31e9ec9295c845a4fac9d1201e1aabb9c93"
)
n_sessions <- 5
post_dir <- here::here("posts", "2026-10-07-tf-warmup-i547")

# ---- run-sessions ----
dir.create(file.path(post_dir, "results"), showWarnings = FALSE)

for (session in seq_len(n_sessions)) {
  session_file <- file.path(
    post_dir,
    "results",
    sprintf("session-%d.rds", session)
  )
  if (file.exists(session_file)) {
    next
  }

  # a fresh order each session, so no version is always measured first or last
  order <- sample(names(versions))

  # cross::run_versions() installs each version into a library of its own and
  # evaluates the expression in a fresh R session against it. That session
  # cannot see this one's variables, so the script's path and the session
  # number reach it as environment variables: args_callr is passed on to
  # callr::r(), whose `env` sets variables for the child process (see ?callr::r
  # and ?callr::rcmd_safe_env, whose defaults are kept).
  measured <- cross::run_versions(
    source(Sys.getenv("GRETA_BENCH_SCRIPT"), local = TRUE)$value,
    pkgs = unname(versions[order]),
    args_callr = list(
      env = c(
        callr::rcmd_safe_env(),
        GRETA_BENCH_SCRIPT = file.path(post_dir, "benchmark.R"),
        GRETA_BENCH_SESSION = session
      )
    )
  )

  saveRDS(
    list(
      session = session,
      versions = versions,
      order = order,
      results = setNames(measured$result, order)
    ),
    session_file,
    compress = "xz"
  )
}
benchmark.R
# What each version of greta runs, in a fresh R session of its own: run.R
# sources it once per version per session, and keeps the list it ends with.
# Each `# ---- label ----` line starts a section that index.qmd shows by that
# label.
#
# Run it alone to try it against whichever greta is installed:
#
#   Rscript --quiet --vanilla posts/2026-10-07-tf-warmup-i547/benchmark.R

# ---- benchmark-setup ----
library(greta)
library(bench)
session <- as.integer(Sys.getenv("GRETA_BENCH_SESSION", "1"))
session_seed <- 2026 - 10 - 7 + session

# ---- benchmark-settings ----
# warmup and n_samples are mcmc()'s arguments, so they count draws; the
# *_calls settings count timed calls; scaling_chains are the numbers of chains
# the chain-count section runs
bench_settings <- list(
  warmup = 1000,
  n_samples = 1000,
  chains = 4,
  n_cores = 4,
  mcmc_calls = 2,
  opt_calls = 3,
  opt_max_iterations = 2000,
  scaling_chains = c(4, 16, 64)
)

# ---- model-linear ----
build_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + coef * attitude$complaints
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coef, sd)
}

# ---- model-multiple-linear ----
build_multiple_linear <- function() {
  design <- as.matrix(attitude[, 2:7])
  int <- normal(0, 10)
  coefs <- normal(0, 10, dim = ncol(design))
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + design %*% coefs
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coefs, sd)
}

# ---- model-hierarchical-linear ----
build_hierarchical_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  species_sd <- lognormal(0, 1)
  species_offset <- normal(0, species_sd, dim = 2)
  species_effect <- rbind(0, species_offset)
  species_id <- as.numeric(iris$Species)
  mu <- int + coef * iris$Sepal.Width + species_effect[species_id]
  distribution(iris$Sepal.Length) <- normal(mu, sd)
  model(int, coef, sd, species_sd, species_offset)
}

# ---- model-eight-schools ----
build_eight_schools <- function() {
  y <- c(28, 8, -3, 7, -1, 1, 18, 12)
  sigma_y <- c(15, 10, 16, 11, 9, 11, 10, 18)
  sigma_eta <- inverse_gamma(1, 1)
  eta <- normal(0, sigma_eta, dim = 8)
  mu_theta <- normal(0, 100)
  xi <- normal(0, 5)
  theta <- mu_theta + xi * eta
  distribution(y) <- normal(theta, sigma_y)
  model(sigma_eta, eta, mu_theta, xi)
}

# ---- benchmark-sampler-iterations ----
# A random walk on a target far wider than its steps accepts every proposal, so
# the variance between kept draws over one step's variance counts the sampler
# iterations between them. It is also the session's first mcmc() call, so
# TensorFlow's start-up costs fall here rather than on the first model.
step_sd <- 0.1
x <- normal(0, 1e6)
walk_model <- model(x)
iterations_per_draw <- function(thin) {
  walk <- mcmc(
    walk_model,
    sampler = rwmh(epsilon = step_sd, diag_sd = 1),
    warmup = 0,
    n_samples = 4000 * thin,
    thin = thin,
    chains = 1,
    initial_values = initials(x = 0),
    verbose = FALSE
  )
  round(var(diff(as.vector(walk[[1]]))) / step_sd^2)
}
sampler_iterations_per_draw <- data.frame(
  thin = c(1, 3),
  iterations_per_draw = c(iterations_per_draw(1), iterations_per_draw(3))
)

# ---- bench-mcmc-same ----
# The same warmup and n_samples on every version. Both of a model's calls are
# on the same model: the first traces its TensorFlow functions and the second
# reuses them. Each `draws_` object keeps the second call's draws.

# ---- bench-mcmc-same-linear ----
build_linear
linear_for_mcmc <- build_linear()
set.seed(session_seed)
bench_mcmc_same_linear <- mark(
  draws_same_linear <- mcmc(
    linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-same-multiple-linear ----
build_multiple_linear
multiple_linear_for_mcmc <- build_multiple_linear()
set.seed(session_seed)
bench_mcmc_same_multiple_linear <- mark(
  draws_same_multiple_linear <- mcmc(
    multiple_linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-same-hierarchical-linear ----
build_hierarchical_linear
hierarchical_linear_for_mcmc <- build_hierarchical_linear()
set.seed(session_seed)
bench_mcmc_same_hierarchical_linear <- mark(
  draws_same_hierarchical_linear <- mcmc(
    hierarchical_linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-same-eight-schools ----
build_eight_schools
eight_schools_for_mcmc <- build_eight_schools()
set.seed(session_seed)
bench_mcmc_same_eight_schools <- mark(
  draws_same_eight_schools <- mcmc(
    eight_schools_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-defaults ----
# Each version's own default warmup and n_samples: 1000 each on CRAN and main,
# 2000 each on #850 and the branch. Each model is built afresh, so the first
# call traces again.

# ---- bench-mcmc-defaults-linear ----
build_linear
linear_for_defaults <- build_linear()
set.seed(session_seed)
bench_mcmc_defaults_linear <- mark(
  draws_defaults_linear <- mcmc(
    linear_for_defaults,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-defaults-multiple-linear ----
build_multiple_linear
multiple_linear_for_defaults <- build_multiple_linear()
set.seed(session_seed)
bench_mcmc_defaults_multiple_linear <- mark(
  draws_defaults_multiple_linear <- mcmc(
    multiple_linear_for_defaults,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-defaults-hierarchical-linear ----
build_hierarchical_linear
hierarchical_linear_for_defaults <- build_hierarchical_linear()
set.seed(session_seed)
bench_mcmc_defaults_hierarchical_linear <- mark(
  draws_defaults_hierarchical_linear <- mcmc(
    hierarchical_linear_for_defaults,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-defaults-eight-schools ----
build_eight_schools
eight_schools_for_defaults <- build_eight_schools()
set.seed(session_seed)
bench_mcmc_defaults_eight_schools <- mark(
  draws_defaults_eight_schools <- mcmc(
    eight_schools_for_defaults,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- mcmc-draws ----
# Every draw of every chain from each model's second call. A posterior draws
# array holds just the draws: the greta_mcmc_list mcmc() returns carries the
# model and its TensorFlow objects, which cannot be saved.
mcmc_draws <- list(
  same = list(
    linear = posterior::as_draws_array(draws_same_linear),
    multiple_linear = posterior::as_draws_array(draws_same_multiple_linear),
    hierarchical_linear = posterior::as_draws_array(
      draws_same_hierarchical_linear
    ),
    eight_schools = posterior::as_draws_array(draws_same_eight_schools)
  ),
  defaults = list(
    linear = posterior::as_draws_array(draws_defaults_linear),
    multiple_linear = posterior::as_draws_array(
      draws_defaults_multiple_linear
    ),
    hierarchical_linear = posterior::as_draws_array(
      draws_defaults_hierarchical_linear
    ),
    eight_schools = posterior::as_draws_array(draws_defaults_eight_schools)
  )
)

# ---- bench-opt-adam ----
# opt() with adam(), whose whole loop the branch runs in one call to
# TensorFlow, where the other versions return to R after every iteration. Each
# model's calls start from the same seed

# ---- bench-opt-adam-linear ----
build_linear
linear_for_opt <- build_linear()
set.seed(session_seed)
bench_opt_adam_linear <- mark(
  opt(
    linear_for_opt,
    optimiser = adam(),
    max_iterations = bench_settings$opt_max_iterations
  ),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-adam-multiple-linear ----
build_multiple_linear
multiple_linear_for_opt <- build_multiple_linear()
set.seed(session_seed)
bench_opt_adam_multiple_linear <- mark(
  opt(
    multiple_linear_for_opt,
    optimiser = adam(),
    max_iterations = bench_settings$opt_max_iterations
  ),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-adam-hierarchical-linear ----
build_hierarchical_linear
hierarchical_linear_for_opt <- build_hierarchical_linear()
set.seed(session_seed)
bench_opt_adam_hierarchical_linear <- mark(
  opt(
    hierarchical_linear_for_opt,
    optimiser = adam(),
    max_iterations = bench_settings$opt_max_iterations
  ),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-adam-eight-schools ----
build_eight_schools
eight_schools_for_opt <- build_eight_schools()
set.seed(session_seed)
bench_opt_adam_eight_schools <- mark(
  opt(
    eight_schools_for_opt,
    optimiser = adam(),
    max_iterations = bench_settings$opt_max_iterations
  ),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-chains ----
# mcmc() with each version's defaults on 4, 16 and 64 chains
# (greta-dev/greta#294), twice for each: the first call on a new model traces,
# the second reuses the trace. Each keeps the smallest and median bulk ESS of
# its draws rather than the draws, which would be large to save at 64 chains.
draws_ess <- function(draws) {
  summary <- posterior::summarise_draws(
    posterior::as_draws_array(draws),
    "ess_bulk"
  )
  data.frame(
    smallest_ess_bulk = min(summary$ess_bulk),
    median_ess_bulk = stats::median(summary$ess_bulk)
  )
}

# ---- bench-chains-linear ----
build_linear
chains_ess_linear <- list()
bench_chains_linear <- press(
  chains = bench_settings$scaling_chains,
  {
    linear_for_chains <- build_linear()
    set.seed(session_seed)
    marks <- mark(
      draws_chains <- mcmc(
        linear_for_chains,
        chains = chains,
        n_cores = bench_settings$n_cores,
        verbose = FALSE
      ),
      iterations = bench_settings$mcmc_calls,
      check = FALSE,
      memory = FALSE,
      filter_gc = FALSE
    )
    # press() evaluates this in an environment of its own, so the ESS goes
    # to the list outside it
    chains_ess_linear[[length(chains_ess_linear) + 1]] <<-
      data.frame(chains = chains, draws_ess(draws_chains))
    marks
  }
)

# ---- bench-chains-eight-schools ----
build_eight_schools
chains_ess_eight_schools <- list()
bench_chains_eight_schools <- press(
  chains = bench_settings$scaling_chains,
  {
    eight_schools_for_chains <- build_eight_schools()
    set.seed(session_seed)
    marks <- mark(
      draws_chains <- mcmc(
        eight_schools_for_chains,
        chains = chains,
        n_cores = bench_settings$n_cores,
        verbose = FALSE
      ),
      iterations = bench_settings$mcmc_calls,
      check = FALSE,
      memory = FALSE,
      filter_gc = FALSE
    )
    # press() evaluates this in an environment of its own, so the ESS goes
    # to the list outside it
    chains_ess_eight_schools[[length(chains_ess_eight_schools) + 1]] <<-
      data.frame(chains = chains, draws_ess(draws_chains))
    marks
  }
)

# ---- benchmark-provenance ----
session_info <- sessioninfo::session_info()
# greta_sitrep() reports through messages, so capture those
greta_sitrep_report <- utils::capture.output(greta_sitrep(), type = "message")

greta_sha <- packageDescription("greta")$RemoteSha
provenance <- data.frame(
  greta_version = as.character(packageVersion("greta")),
  greta_sha = if (is.null(greta_sha)) NA_character_ else greta_sha,
  r_version = paste(R.version$major, R.version$minor, sep = "."),
  tensorflow_version = tensorflow::tf$version$VERSION,
  tfp_version = reticulate::import("tensorflow_probability")$`__version__`,
  machine = paste(
    Sys.info()[c("sysname", "release", "machine")],
    collapse = " "
  ),
  cores = parallel::detectCores()
)

# ---- benchmark-results ----
# the value source() returns to run.R: every bench_mark object as mark() made
# it, and the summaries beside them
list(
  session_seed = session_seed,
  bench_settings = bench_settings,
  provenance = provenance,
  session_info = session_info,
  greta_sitrep_report = greta_sitrep_report,
  sampler_iterations_per_draw = sampler_iterations_per_draw,
  mcmc_same = list(
    linear = bench_mcmc_same_linear,
    multiple_linear = bench_mcmc_same_multiple_linear,
    hierarchical_linear = bench_mcmc_same_hierarchical_linear,
    eight_schools = bench_mcmc_same_eight_schools
  ),
  mcmc_defaults = list(
    linear = bench_mcmc_defaults_linear,
    multiple_linear = bench_mcmc_defaults_multiple_linear,
    hierarchical_linear = bench_mcmc_defaults_hierarchical_linear,
    eight_schools = bench_mcmc_defaults_eight_schools
  ),
  opt_adam = list(
    linear = bench_opt_adam_linear,
    multiple_linear = bench_opt_adam_multiple_linear,
    hierarchical_linear = bench_opt_adam_hierarchical_linear,
    eight_schools = bench_opt_adam_eight_schools
  ),
  chains = list(
    linear = bench_chains_linear,
    eight_schools = bench_chains_eight_schools
  ),
  chains_ess = list(
    linear = do.call(rbind, chains_ess_linear),
    eight_schools = do.call(rbind, chains_ess_eight_schools)
  ),
  mcmc_draws = mcmc_draws
)

The models

Four of greta’s inst/examples/, each defined in maths and by its section of benchmark.R. Normal and log-normal distributions are written with their mean and standard deviation, as greta’s normal() and lognormal() take them; \(\text{Cauchy}^{+}\) is a Cauchy truncated to positive values.

A regression of each department’s rating in attitude on its number of complaints. 3 parameters. Its section is model-linear.

\[ \begin{aligned} \text{rating}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 30 \\ \mu_i &= \alpha + \beta \, \text{complaints}_i \\ \alpha, \beta &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-linear
build_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + coef * attitude$complaints
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coef, sd)
}

The same rating on all six of attitude’s other columns, \(X\), through a matrix multiply. 8 parameters. Its section is model-multiple-linear.

\[ \begin{aligned} \text{rating}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 30 \\ \mu_i &= \alpha + \sum_{j = 1}^{6} X_{ij} \, \beta_j \\ \alpha, \beta_j &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-multiple-linear
build_multiple_linear <- function() {
  design <- as.matrix(attitude[, 2:7])
  int <- normal(0, 10)
  coefs <- normal(0, 10, dim = ncol(design))
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + design %*% coefs
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coefs, sd)
}

Sepal length on sepal width in iris, with an offset for each species, \(s[i]\) being flower \(i\)’s. Setosa’s offset is zero and the other two share a scale, \(\tau\). 6 parameters. Its section is model-hierarchical-linear.

\[ \begin{aligned} \text{Sepal.Length}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 150 \\ \mu_i &= \alpha + \beta \, \text{Sepal.Width}_i + \gamma_{s[i]} \\ \gamma_1 &= 0, \qquad \gamma_2, \gamma_3 \sim \text{Normal}(0, \tau) \\ \tau &\sim \text{LogNormal}(0, 1) \\ \alpha, \beta &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-hierarchical-linear
build_hierarchical_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  species_sd <- lognormal(0, 1)
  species_offset <- normal(0, species_sd, dim = 2)
  species_effect <- rbind(0, species_offset)
  species_id <- as.numeric(iris$Species)
  mu <- int + coef * iris$Sepal.Width + species_effect[species_id]
  distribution(iris$Sepal.Length) <- normal(mu, sd)
  model(int, coef, sd, species_sd, species_offset)
}

The effect of coaching in eight schools, \(y_j\), each measured with a known standard error, \(\sigma_j\). The scale of the school effects, \(\sigma_\eta\), is itself a parameter, which makes the posterior a funnel. 11 parameters. Its section is model-eight-schools.

\[ \begin{aligned} y_j &\sim \text{Normal}(\theta_j, \sigma_j), \quad j = 1, \dots, 8 \\ \theta_j &= \mu + \xi \, \eta_j \\ \eta_j &\sim \text{Normal}(0, \sigma_\eta) \\ \sigma_\eta &\sim \text{InverseGamma}(1, 1) \\ \mu &\sim \text{Normal}(0, 100) \\ \xi &\sim \text{Normal}(0, 5) \end{aligned} \]

benchmark.R: model-eight-schools
build_eight_schools <- function() {
  y <- c(28, 8, -3, 7, -1, 1, 18, 12)
  sigma_y <- c(15, 10, 16, 11, 9, 11, 10, 18)
  sigma_eta <- inverse_gamma(1, 1)
  eta <- normal(0, sigma_eta, dim = 8)
  mu_theta <- normal(0, 100)
  xi <- normal(0, 5)
  theta <- mu_theta + xi * eta
  distribution(y) <- normal(theta, sigma_y)
  model(sigma_eta, eta, mu_theta, xi)
}

Session details

What each version reported about itself in session 1, from benchmark.R’s benchmark-provenance section.

Code
# a saved session_info() prints as a plain list unless sessioninfo's print
# method is registered, which loading its namespace does
invisible(loadNamespace("sessioninfo"))
session_one <- runs |>
  filter(session == 1) |>
  arrange(version)
session_result <- function(version_label) {
  session_one$results[[which(session_one$version == version_label)]]
}
greta_sitrep()
Code
writeLines(session_result("CRAN")$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.11) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.15.1) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.23.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: uv cache present, offline mode will engage
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_result("CRAN")$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-07
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version  date (UTC) lib source
 abind            1.4-8    2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1    2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6    2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4    2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0    2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4    2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6    2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1 2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20   2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3    2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39   2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0    2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0   2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4    2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1   2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1    2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0    2026-07-20 [1] CRAN (R 4.6.1)
 hms              1.1.4    2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0    2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1   2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5    2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0    2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5    2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6    2026-07-25 [3] CRAN (R 4.6.1)
 matrixStats      1.5.0    2025-01-07 [2] CRAN (R 4.6.0)
 parallelly       1.48.0   2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1   2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3    2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9    2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0    2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0    2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0    2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3    2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1    2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2    2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0   2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0    2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4    2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1 2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0   2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2    2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4    2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1    2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3    2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1    2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3    2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/Rtmp1VQJcg/file176773902c7c8
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/libpython3.11.dylib
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g:/Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python311.zip:/Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python3.11:/Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python3.11/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/bin/python3.11
 3.11.15 (main, Mar 20 2026, 00:41:54) [Clang 22.1.1 ]
 3, 11
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages/numpy, 1                                                                                          , 26                                                                                         , 4                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_result("main")$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_result("main")$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-07
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-06 [1] Github (greta-dev/greta@0a22f8c)
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 matrixStats      1.5.0      2025-01-07 [2] CRAN (R 4.6.0)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/Rtmp1VQJcg/file17677442506d4
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_result("#850")$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_result("#850")$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-07
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-06 [1] Github (greta-dev/greta@5a02f6c)
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 matrixStats      1.5.0      2025-01-07 [2] CRAN (R 4.6.0)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/Rtmp1VQJcg/file1767789623b7
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_result("#855 early")$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_result("#855 early")$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-07
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-06 [1] Github (greta-dev/greta@59da5cc)
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 matrixStats      1.5.0      2025-01-07 [2] CRAN (R 4.6.0)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/Rtmp1VQJcg/file176775ae36b3f
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_result("#855")$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_result("#855")$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-07
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-07 [1] local
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 matrixStats      1.5.0      2025-01-07 [2] CRAN (R 4.6.0)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/Rtmp1VQJcg/file17677396b8db7
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────

References

Girolami, Mark, and Ben Calderhead. 2011. “Riemann Manifold Langevin and Hamiltonian Monte Carlo Methods.” Journal of the Royal Statistical Society: Series B (Statistical Methodology) 73 (2): 123–214. https://doi.org/10.1111/j.1467-9868.2010.00765.x.
Hoffman, Matthew D., and Andrew Gelman. 2014. “The No-U-Turn Sampler: Adaptively Setting Path Lengths in Hamiltonian Monte Carlo.” Journal of Machine Learning Research 15 (47): 1593–623. https://jmlr.org/papers/v15/hoffman14a.html.
Magnusson, Måns, Jakob Torgander, Paul-Christian Bürkner, Lu Zhang, Bob Carpenter, and Aki Vehtari. 2025. “Posteriordb: Testing, Benchmarking and Developing Bayesian Inference Algorithms.” Proceedings of the 28th International Conference on Artificial Intelligence and Statistics (AISTATS). https://arxiv.org/abs/2407.04967.
Vehtari, Aki, Andrew Gelman, Daniel Simpson, Bob Carpenter, and Paul-Christian Bürkner. 2021. “Rank-Normalization, Folding, and Localization: An Improved R̂ for Assessing Convergence of MCMC (with Discussion).” Bayesian Analysis 16 (2): 667–718. https://doi.org/10.1214/20-BA1221.