Skip to content
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
result instead of about three.
* `subset_draws()` returns its input without copying it when every variable is
selected.
* All warnings now have the class `posterior_warning` and a class for each
cause. Calling packages can catch, count, or muffle each type of warning (#470).

### Bug Fixes

Expand Down
3 changes: 2 additions & 1 deletion R/as_draws.R
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ check_variables_are_numeric <- function(
if (any(non_numeric_cols)) {
warning_no_call(
to,
" does not support non-numeric variables (e.g., factors). Converting non-numeric variables to numeric."
" does not support non-numeric variables (e.g., factors). Converting non-numeric variables to numeric.",
class = "posterior_warning_non_numeric"
)
}
if (convert) {
Expand Down
5 changes: 4 additions & 1 deletion R/as_draws_df.R
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ dplyr_reconstruct.draws_df <- function(data, template) {
# from the data frame
drop_draws_class_if_metadata_removed <- function(x, warn = TRUE) {
if (!all(reserved_df_variables() %in% names(x))) {
if (warn) warning_no_call("Dropping 'draws_df' class as required metadata was removed.")
if (warn) warning_no_call(
"Dropping 'draws_df' class as required metadata was removed.",
class = "posterior_warning_draws_df_dropped"
)
class(x) <- setdiff(class(x), c("draws_df", "draws"))
}
x
Expand Down
5 changes: 4 additions & 1 deletion R/convergence.R
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,10 @@ fold_draws <- function(x) {
# Safety check for negative values and with max ess equal to ess*log10(ess)
tau_bound <- 1 / log10(ess)
if (tau_hat < tau_bound) {
warning_no_call("The ESS has been capped to avoid unstable estimates.")
warning_no_call(
"The ESS has been capped to avoid unstable estimates.",
class = "posterior_warning_ess_capped"
)
tau_hat <- tau_bound
}
ess <- ess / tau_hat
Expand Down
2 changes: 1 addition & 1 deletion R/gpd.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ qgeneralized_pareto <- function(p, mu = 0, sigma = 1, k = 0, lower.tail = TRUE,
# and replacing them here avoids a second warning from log() or log1p().
invalid <- which(if (log.p) p > 0 else p < 0 | p > 1)
if (length(invalid)) {
warning_no_call("NaNs produced")
warning_no_call("NaNs produced", class = "posterior_warning_nans_produced")
p[invalid] <- NaN
}
if (log.p) {
Expand Down
3 changes: 2 additions & 1 deletion R/merge_chains.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ warn_merge_chains <- function(type = c("match", "index")) {
switch(type, ".",
match = " due to chain information not matching.",
index = " due to manually indexing draws."
)
),
class = "posterior_warning_chains_dropped"
)
}
invisible(NULL)
Expand Down
4 changes: 2 additions & 2 deletions R/misc.R
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ stop_no_call <- function(...) {
stop(..., call. = FALSE)
}

warning_no_call <- function(...) {
warning(..., call. = FALSE)
warning_no_call <- function(..., class = NULL) {
rlang::warn(paste0(...), class = c(class, "posterior_warning"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok, but there's something we should keep in mind for when we add .frequency = "once", since it interacts with the handlers that are enabled in this PR. I checked with Claude and then check myself and once a warning has been shown, rlang doesn't signal it again at all, so a handler like the one in the PR description only sees the first one. Also, a muffled warning still counts as shown. If loo muffles the first ESS-capped warning, the user won't see it again that session, even when calling posterior directly.

So maybe we'll want .frequency only on the noisy warnings (ESS capped, the Pareto ones) rather than inside warning_no_call() for every call site.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I actually didn't thought about using .frequency = "once". I rather thought to use a corresponding caller at the sites where a posterior function is called multiple times (e.g., loo package) . This caller then collects the warning by class and reports a summary, for example "ESS capped in 12 of 100 calls". Then loo's handlers see every warning. Or do I misunderstand something?

@jgabry jgabry Oct 28, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I think your approach is better for the loo case. loo can collect warnings by class and report one summary, and it sees every warning. .frequency = "once" would conflict with that because after the first warning rlang stops signaling them and loo wouldn't be able to count them.

Users calling posterior directly might want once per session warnings, but we could add that in the future if we want it for certain specific warnings only. If we do that then loo could use rlib_warning_verbosity = "verbose" in those cases.

But for now fine to go with your approach.

}

SW <- function(expr) {
Expand Down
6 changes: 4 additions & 2 deletions R/nested_rhat.R
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ rhat_nested.rvar <- function(x, superchain_ids, ...) {
# check that all chains are assigned a superchain
if (length(superchain_ids) != nchains) {
warning_no_call("Length of superchain_ids not equal to number of chains, ",
"returning NA.")
"returning NA.",
class = "posterior_warning_superchain_ids_length")
return(NA_real_)
}

Expand All @@ -72,7 +73,8 @@ rhat_nested.rvar <- function(x, superchain_ids, ...) {

if (nchains_per_superchain != min(superchain_id_table)) {
warning_no_call("Number of chains per superchain is not the same for ",
"each superchain, returning NA.")
"each superchain, returning NA.",
class = "posterior_warning_superchain_unequal")
return(NA_real_)
}

Expand Down
24 changes: 18 additions & 6 deletions R/pareto_smooth.R
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@ pareto_smooth.default <- function(x,

# check for infinite or na values
if (should_return_NA(x)) {
warning_no_call("Input contains infinite or NA values, is constant or has constant tail. Fitting of generalized Pareto distribution not performed.")
warning_no_call(
"Input contains infinite or NA values, is constant or has constant tail. Fitting of generalized Pareto distribution not performed.",
class = "posterior_warning_pareto_invalid_input"
)
return(pareto_diags_na(x, return_k, extra_diags))
}

Expand All @@ -301,18 +304,25 @@ pareto_smooth.default <- function(x,
}

if (is.na(ndraws_tail)) {
warning_no_call("Input contains infinite or NA values, is constant, or has constant tail. Fitting of generalized Pareto distribution not performed.")
warning_no_call(
"Input contains infinite or NA values, is constant, or has constant tail. Fitting of generalized Pareto distribution not performed.",
class = "posterior_warning_pareto_invalid_input"
)
return(pareto_diags_na(x, return_k, extra_diags))
}

if (ndraws_tail < 5) {
warning_no_call("Number of tail draws cannot be less than 5. ",
"Changing to ", 5, ".")
"Changing to ", 5, ".",
class = "posterior_warning_pareto_tail_too_short")
ndraws_tail <- 5
}

if (ndraws_tail >= length(x)){
warning_no_call("Tail draws (", ndraws_tail, ") not strictly less than total draws (", length(x), "). Fitting of generalized Pareto distribution not performed.")
warning_no_call(
"Tail draws (", ndraws_tail, ") not strictly less than total draws (", length(x), "). Fitting of generalized Pareto distribution not performed.",
class = "posterior_warning_pareto_tail_exceeds_draws"
)
return(pareto_diags_na(x, return_k, extra_diags))
}

Expand All @@ -321,7 +331,8 @@ pareto_smooth.default <- function(x,
if (ndraws_tail > ndraws / 2) {
warning_no_call("Number of tail draws cannot be more than half ",
"the total number of draws if both tails are fit, ",
"changing to ", ndraws / 2, ".")
"changing to ", ndraws / 2, ".",
class = "posterior_warning_pareto_tail_too_long")
ndraws_tail <- ndraws / 2
}

Expand Down Expand Up @@ -488,7 +499,8 @@ ps_tail <- function(x,
if (ndraws_tail < 5) {
warning_no_call(
"Can't fit generalized Pareto distribution ",
"because ndraws_tail is less than 5."
"because ndraws_tail is less than 5.",
class = "posterior_warning_pareto_tail_not_fitted"
)
return(list(x = x, k = NA))
}
Expand Down
8 changes: 5 additions & 3 deletions R/pit.R
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ pit.draws_matrix <- function(x, y, weights = NULL, log = FALSE, ...) {
max(pit),
"\nRounding PIT > 1 to 1.",
sep = ""
)
),
class = "posterior_warning_pit_above_one"
)
}

Expand Down Expand Up @@ -445,7 +446,7 @@ validate_pit_weights <- function(weights, x, log) {
cols <- lapply(seq_along(vars), function(var_idx) {
tryCatch(
validate_weights(weights[, var_idx], x[, var_idx], log),
posterior_degenerate_weights_error = function(cnd) NULL
posterior_error_degenerate_weights = function(cnd) NULL
)
})
degenerate <- vapply(cols, is.null, logical(1))
Expand All @@ -454,7 +455,8 @@ validate_pit_weights <- function(weights, x, log) {
"All draws have zero weight for ",
paste0("'", vars[degenerate], "'", collapse = ", "),
". Returning NA for ",
if (sum(degenerate) > 1) "those variables." else "that variable."
if (sum(degenerate) > 1) "those variables." else "that variable.",
class = "posterior_warning_zero_weight"
Comment thread
florence-bockting marked this conversation as resolved.
Resolved
)
cols[degenerate] <- list(rep(NA_real_, ndraws(x)))
}
Expand Down
40 changes: 39 additions & 1 deletion R/posterior-package.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#' Tools for working with posterior (and prior) distributions
#'
#' @name posterior-package
#' @aliases posterior
#' @aliases posterior posterior_warning
#'
#' @import checkmate
#' @import stats
Expand Down Expand Up @@ -65,4 +65,42 @@
#' match between two objects involved in a binary operation. Whether this
#' causes a warning can be controlled by this option.
#'
#' @section Warning and error classes:
#'
#' Every warning signaled by \pkg{posterior} has the class
#' `posterior_warning` and one class for its cause. Use these classes to
#' handle or muffle a specific warning, for example with
#' [withCallingHandlers()] and `invokeRestart("muffleWarning")`.
#'
#' * `posterior_warning_ess_capped`: An ESS estimate was capped.
#' * `posterior_warning_superchain_ids_length`: The length of
#' `superchain_ids` does not equal the number of chains.
#' * `posterior_warning_superchain_unequal`: The superchains do not have the
#' same number of chains.
#' * `posterior_warning_non_numeric`: Non-numeric variables (e.g., factors)
#' were converted to numeric.
#' * `posterior_warning_chains_dropped`: Chains were dropped.
#' * `posterior_warning_no_variables`: No variables with unreserved names
#' were found.
#' * `posterior_warning_odd_iterations`: The number of iterations is not
#' even.
#' * `posterior_warning_pit_above_one`: Some PIT values are larger than 1.
#' * `posterior_warning_zero_weight`: All draws of a variable have zero
#' weight.
#' * `posterior_warning_nans_produced`: NaNs were produced.
#' * `posterior_warning_draws_df_dropped`: The `draws_df` class was dropped.
#' * `posterior_warning_pareto_invalid_input`: The input to Pareto smoothing
#' contains infinite or `NA` values, is constant, or has a constant tail.
#' * `posterior_warning_pareto_tail_too_short`: The number of tail draws was
#' increased to 5.
#' * `posterior_warning_pareto_tail_exceeds_draws`: The number of tail draws
#' is not less than the total number of draws.
#' * `posterior_warning_pareto_tail_too_long`: The number of tail draws is
#' more than half of the total number of draws.
#' * `posterior_warning_pareto_tail_not_fitted`: The generalized Pareto
#' distribution was not fitted because there are fewer than 5 tail draws.
#'
#' The error signaled when all weights are zero has the class
#' `posterior_error_degenerate_weights`.
#'
"_PACKAGE"
3 changes: 2 additions & 1 deletion R/split_chains.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ split_chains.draws <- function(x, ...) {
if (niter %% 2 != 0) {
warning_no_call(
"Number of iterations is not even. Removing the last iteration ",
"in order to split chains into two parts of equal length."
"in order to split chains into two parts of equal length.",
class = "posterior_warning_odd_iterations"
)
niter <- niter - 1
}
Expand Down
3 changes: 2 additions & 1 deletion R/summarise_draws.R
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ summarise_draws.draws <- function(
if (!length(variables_x)) {
warning_no_call(
"The draws object contained no variables with unreserved names. ",
"No summaries were computed."
"No summaries were computed.",
class = "posterior_warning_no_variables"
)
return(tibble::tibble(character()))
}
Expand Down
2 changes: 1 addition & 1 deletion R/weight_draws.R
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ validate_weights <- function(weights, draws, log = FALSE) {
"If the weights underflowed to zero, pass them on the log scale ",
"with `log = TRUE`."
),
class = "posterior_degenerate_weights_error"
class = "posterior_error_degenerate_weights"
))
}
weights
Expand Down
42 changes: 42 additions & 0 deletions man/posterior-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions man/qgeneralized_pareto.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions tests/testthat/test-convergence.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ test_that("negative ess estimates are avoided with a warning", {
expect_equal(ess, 3000)
})

test_that("capped ess warning has posterior warning classes", {
Comment thread
florence-bockting marked this conversation as resolved.
Resolved
x <- stats::arima.sim(list(ar=-0.9), 1000)
w <- tryCatch(ess_basic(x), warning = identity)
expect_s3_class(w, "posterior_warning_ess_capped")
expect_s3_class(w, "posterior_warning")
})

test_that("calling code can count and muffle classed warnings", {
Comment thread
jgabry marked this conversation as resolved.
Resolved
x <- replicate(3, stats::arima.sim(list(ar=-0.9), 1000))
n_capped <- 0
expect_silent(withCallingHandlers(
apply(x, 2, ess_basic),
posterior_warning_ess_capped = function(w) {
n_capped <<- n_capped + 1
invokeRestart("muffleWarning")
}
))
expect_equal(n_capped, 3)
})

test_that("mcse diagnostics return reasonable values", {
tau <- extract_variable_matrix(example_draws(), "tau")

Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/test-misc.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
test_that("warning_no_call() adds the cause and shared classes", {
w <- tryCatch(
warning_no_call("a", 1, class = "posterior_warning_test"),
warning = identity
)
expect_s3_class(w, "posterior_warning_test")
expect_s3_class(w, "posterior_warning")
expect_equal(conditionMessage(w), "a1")
expect_null(conditionCall(w))
})
2 changes: 1 addition & 1 deletion tests/testthat/test-weight_draws.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test_that("weight_draws rejects weights with no probability mass", {
expect_error(weight_draws(x, exp(rep(-800, n))), "log = TRUE", fixed = TRUE)

# the error is classed, so callers can handle it per variable
expect_error(weight_draws(x, rep(0, n)), class = "posterior_degenerate_weights_error")
expect_error(weight_draws(x, rep(0, n)), class = "posterior_error_degenerate_weights")

# a single draw carrying all the mass is still valid
w <- c(1, rep(0, n - 1))
Expand Down
Loading