R/fitdistrBayes.R

Defines functions log_lik log_lik.fitdistrBayes predict.fitdistrBayes plot.fitdistrBayes as.data.frame.fitdistrBayes confint.fitdistrBayes coef.fitdistrBayes print.summary.fitdistrBayes summary.fitdistrBayes print.fitdistrBayes fitdistrBayes .fdb_build_custom .fdb_build_builtin .fdb_log_contract_equal .fdb_call_prior .fdb_call_density .fdb_call_density_pointwise .fdb_valid_log_values .fdb_resolve_log_mode .fdb_from_unconstrained .fdb_to_unconstrained .fdb_custom_bounds .fdb_direct_chains .fdb_transform_matrix .fdb_long_draws .fdb_summarize .fdb_ess .fdb_ess_matrix .fdb_rhat .fdb_basic_rhat .fdb_rank_normalize .fdb_split_matrix .fdb_amwg .fdb_slice_chains .fdb_slice_one .fdb_with_seed .fdb_nbinom_entropy .fdb_poisson_entropy .fdb_t_B .fdb_gamma_joint_term .fdb_logsumexp .fdb_check_fixed .fdb_moment_status fitdistrBayes_routes .fdb_route_catalog .fdb_prior_name .fdb_model_name .fdb_normalize_name .fdb_weighted_lindley_log_prior .fdb_weighted_lindley_lambda_from_mean .fdb_log_positive_difference .fdb_log_expm1_positive .fdb_weighted_lindley_start .fdb_weibull_lmoment_start .fdb_student_start .fdb_cauchy_quantile_start .fdb_el_prior_terms .fdb_rician_logq_factory .fdb_gauss_laguerre .fdb_rician_moment_start .fdb_el_moment_start .fdb_nakagami_moment_start .fdb_lomax_lmoment_start .fdb_frechet_lmoment_start .fdb_sample_lmoments .fdb_polylog3_unit .fdb_polylog2_unit .fdb_log_bessel_i0 .fdb_softplus .fdb_second_central_moment .fdb_safe_scale .fdb_discrete .fdb_exp_positive .fdb_rgamma_positive .fdb_open_probability .fdb_positive_finite .fdb_finite_magnitude .fdb_nonconstant .fdb_validate_x .fdb_scalar_count .fdb_merge_control .fdb_warn .fdb_stop

Documented in as.data.frame.fitdistrBayes coef.fitdistrBayes confint.fitdistrBayes fitdistrBayes fitdistrBayes_routes log_lik log_lik.fitdistrBayes plot.fitdistrBayes predict.fitdistrBayes print.fitdistrBayes print.summary.fitdistrBayes summary.fitdistrBayes

# fitdistrBayes ---------------------------------------------------------------
#
# Dependency-free implementation of objective Bayesian distribution fitting.
# The public entry point is fitdistrBayes(). All internal helpers use the
# .fdb_ prefix and are deliberately kept outside the exported namespace.

.fdb_stop <- function(..., call. = FALSE) {
  stop(sprintf(...), call. = call.)
}

.fdb_warn <- function(...) {
  warning(sprintf(...), call. = FALSE, immediate. = TRUE)
}

.fdb_merge_control <- function(control) {
  defaults <- list(
    rhat_threshold = 1.01,
    ess_threshold = 400,
    target_accept = 0.44,
    adapt_interval = 50L,
    proposal_scale = 0.35,
    init_jitter = 0.35,
    slice_width = 1,
    slice_steps = 100L,
    entropy_tol = 1e-10,
    entropy_exact_limit = 2000L,
    max_init_tries = 100L,
    warn_convergence = TRUE,
    store_callables = TRUE,
    rng = NULL,
    rng_validator = NULL,
    density_is_log = NULL,
    prior_is_log = NULL,
    prior_style = "auto",
    lower = NULL,
    upper = NULL
  )
  if (is.null(control) || (is.list(control) && length(control) == 0L)) {
    return(defaults)
  }
  if (!is.list(control) || is.null(names(control)) ||
      any(!nzchar(names(control)))) {
    .fdb_stop("'control' must be a named list.")
  }
  unknown <- setdiff(names(control), names(defaults))
  if (length(unknown)) {
    .fdb_stop("Unknown control option%s: %s.",
              if (length(unknown) > 1L) "s" else "",
              paste(unknown, collapse = ", "))
  }
  defaults[names(control)] <- control
  positive_scalars <- c("rhat_threshold", "ess_threshold",
                        "proposal_scale", "init_jitter", "slice_width",
                        "entropy_tol")
  for (nm in positive_scalars) {
    value <- defaults[[nm]]
    if (length(value) != 1L || !is.numeric(value) ||
        !is.finite(value) || value <= 0) {
      .fdb_stop("control$%s must be one positive finite number.", nm)
    }
  }
  if (defaults$entropy_tol >= 0.1) {
    .fdb_stop("control$entropy_tol must be smaller than 0.1.")
  }
  if (defaults$rhat_threshold < 1) {
    .fdb_stop("control$rhat_threshold must be at least 1.")
  }
  if (length(defaults$target_accept) != 1L ||
      !is.numeric(defaults$target_accept) ||
      !is.finite(defaults$target_accept) ||
      defaults$target_accept <= 0 || defaults$target_accept >= 1) {
    .fdb_stop("control$target_accept must lie strictly between 0 and 1.")
  }
  integer_controls <- c("adapt_interval", "slice_steps",
                        "entropy_exact_limit", "max_init_tries")
  for (nm in integer_controls) {
    value <- defaults[[nm]]
    if (length(value) != 1L || !is.numeric(value) || !is.finite(value) ||
        value < 1 || value != as.integer(value)) {
      .fdb_stop("control$%s must be one positive integer.", nm)
    }
    defaults[[nm]] <- as.integer(value)
  }
  logical_controls <- c("warn_convergence", "store_callables")
  for (nm in logical_controls) {
    value <- defaults[[nm]]
    if (length(value) != 1L || !is.logical(value) || is.na(value)) {
      .fdb_stop("control$%s must be TRUE or FALSE.", nm)
    }
  }
  if (!is.null(defaults$rng) && !is.function(defaults$rng)) {
    .fdb_stop("control$rng must be NULL or a simulation function.")
  }
  if (!is.null(defaults$rng_validator) &&
      !is.function(defaults$rng_validator)) {
    .fdb_stop("control$rng_validator must be NULL or a validation function.")
  }
  for (nm in c("density_is_log", "prior_is_log")) {
    value <- defaults[[nm]]
    if (!is.null(value) &&
        (length(value) != 1L || !is.logical(value) || is.na(value))) {
      .fdb_stop("control$%s must be NULL, TRUE, or FALSE.", nm)
    }
  }
  if (length(defaults$prior_style) != 1L ||
      !is.character(defaults$prior_style) ||
      is.na(defaults$prior_style) ||
      !defaults$prior_style %in% c("auto", "scalar", "vector")) {
    .fdb_stop(
      "control$prior_style must be one of 'auto', 'scalar', or 'vector'."
    )
  }
  defaults
}

.fdb_scalar_count <- function(value, name, lower = 1L,
                              strict_lower = FALSE) {
  if (length(value) != 1L || !is.numeric(value) || !is.finite(value)) {
    .fdb_stop("'%s' must be one finite number.", name)
  }
  if (value > .Machine$integer.max) {
    .fdb_stop("'%s' must not exceed %d.", name, .Machine$integer.max)
  }
  ok <- if (strict_lower) value > lower else value >= lower
  if (!ok || value != as.integer(value)) {
    op <- if (strict_lower) "greater than" else "at least"
    .fdb_stop("'%s' must be an integer %s %s.", name, op, lower)
  }
  as.integer(value)
}

.fdb_validate_x <- function(x, na.action) {
  if (!is.numeric(x) || is.complex(x) || is.matrix(x) ||
      is.data.frame(x)) {
    .fdb_stop("'x' must be a numeric vector.")
  }
  if (!length(x)) {
    .fdb_stop("'x' must contain at least one observation.")
  }
  bad_na <- is.na(x) | is.nan(x)
  omitted <- which(bad_na)
  if (length(omitted)) {
    if (na.action == "fail") {
      .fdb_stop("'x' contains %d missing value%s; use na.action = \"omit\" to remove them.",
                length(omitted), if (length(omitted) == 1L) "" else "s")
    }
    x <- x[!bad_na]
  }
  if (!length(x)) {
    .fdb_stop("No observations remain after removing missing values.")
  }
  if (any(!is.finite(x))) {
    .fdb_stop("'x' must not contain Inf or -Inf.")
  }
  list(x = as.numeric(x), omitted = omitted)
}

.fdb_nonconstant <- function(x) {
  if (length(x) < 2L) return(FALSE)
  # The question is whether at least two supplied floating-point values are
  # distinct, not whether their range is large relative to their location.
  # A relative-to-|x| tolerance falsely labels translated samples such as
  # 1e12 + c(0, 1e-3) as constant, although the values are representable.
  any(x != x[1L])
}

.fdb_min_positive <- .Machine$double.xmin * .Machine$double.eps

.fdb_finite_magnitude <- function(x, what = "A generated value") {
  if (any(!is.finite(x))) {
    .fdb_stop(
      "%s exceeded the finite floating-point range; no clipping was applied.",
      what
    )
  }
  x
}

.fdb_positive_finite <- function(x, what = "A generated positive value") {
  if (any(!is.finite(x)) || any(x <= 0)) {
    .fdb_stop(
      paste0(
        "%s could not be represented as a strictly positive finite number; ",
        "no clipping was applied."
      ),
      what
    )
  }
  x
}

.fdb_open_probability <- function(x) {
  pmax(.fdb_min_positive,
       pmin(1 - .Machine$double.eps / 2, x))
}

.fdb_rgamma_positive <- function(n, shape, rate = 1) {
  .fdb_positive_finite(
    stats::rgamma(n, shape = shape, rate = rate),
    "A Gamma random variate"
  )
}

.fdb_exp_positive <- function(log_x) {
  lower <- log(.fdb_min_positive)
  upper <- log(.Machine$double.xmax)
  if (any(!is.finite(log_x)) || any(log_x < lower | log_x > upper)) {
    .fdb_stop(
      paste0(
        "A positive natural-scale draw exceeded the floating-point range; ",
        "no clipping was applied."
      )
    )
  }
  .fdb_positive_finite(exp(log_x), "A transformed positive draw")
}

.fdb_discrete <- function(x, lower = 0) {
  all(x >= lower & x == trunc(x))
}

.fdb_safe_scale <- function(x) {
  z <- stats::mad(x, constant = 1.4826)
  if (!is.finite(z) || z <= 0) z <- stats::IQR(x) / 1.349
  if (!is.finite(z) || z <= 0) z <- stats::sd(x)
  if (!is.finite(z) || z <= 0) z <- diff(range(x)) / 2
  if (!is.finite(z) || z <= 0) z <- max(1, abs(stats::median(x)))
  z
}

.fdb_second_central_moment <- function(x, center = mean(x)) {
  mean((x - center)^2)
}

.fdb_softplus <- function(x) {
  ans <- numeric(length(x))
  large <- x > 0
  ans[large] <- x[large] + log1p(exp(-x[large]))
  ans[!large] <- log1p(exp(x[!large]))
  ans
}

.fdb_log_bessel_i0 <- function(x) {
  if (any(x < 0) || any(!is.finite(x))) {
    return(rep(NA_real_, length(x)))
  }
  log(besselI(x, nu = 0, expon.scaled = TRUE)) + x
}

.fdb_polylog2_unit <- function(z) {
  if (length(z) != 1L || !is.finite(z) || z < 0 || z > 1) {
    .fdb_stop("Internal dilogarithm evaluation requires one value in [0,1].")
  }
  if (z == 0) return(0)
  if (z == 1) return(pi^2 / 6)
  series <- function(y) {
    total <- 0
    power <- y
    k <- 1L
    repeat {
      add <- power / k^2
      total <- total + add
      if (abs(add) <= 2e-16 * max(1, abs(total)) || k >= 100000L) break
      k <- k + 1L
      power <- power * y
    }
    total
  }
  if (z <= 0.5) return(series(z))
  pi^2 / 6 - log(z) * log1p(-z) - series(1 - z)
}

.fdb_polylog3_unit <- function(z) {
  if (length(z) != 1L || !is.finite(z) || z < 0 || z > 1) {
    .fdb_stop("Internal trilogarithm evaluation requires one value in [0,1].")
  }
  if (z == 0) return(0)
  if (z == 1) return(1.2020569031595942854)
  total <- 0
  power <- z
  k <- 1L
  repeat {
    add <- power / k^3
    total <- total + add
    if (abs(add) <= 2e-15 * max(1, abs(total)) || k >= 100000L) break
    k <- k + 1L
    power <- power * z
  }
  total
}

.fdb_sample_lmoments <- function(x) {
  n <- length(x)
  if (n < 2L) return(c(l1 = mean(x), l2 = NA_real_, tau = NA_real_))
  ordered <- sort(x)
  b0 <- mean(ordered)
  b1 <- sum(((seq_len(n) - 1) / (n - 1)) * ordered) / n
  l2 <- 2 * b1 - b0
  c(l1 = b0, l2 = l2, tau = l2 / b0)
}

.fdb_frechet_lmoment_start <- function(x) {
  lm <- .fdb_sample_lmoments(x)
  tau <- unname(lm["tau"])
  shape <- log(2) / log1p(tau)
  scale_parameter <- unname((lm["l1"] / gamma(1 - 1 / shape))^shape)
  method <- "Frechet L-moments (closed form)"
  if (!is.finite(shape) || shape <= 1 ||
      !is.finite(scale_parameter) || scale_parameter <= 0) {
    q <- stats::quantile(x, c(0.25, 0.75), names = FALSE, type = 8)
    shape <- log(log(4) / log(4 / 3)) / log(q[2L] / q[1L])
    median_x <- stats::median(x)
    scale_parameter <- log(2) * median_x^shape
    method <- paste0(method, "; quantile-matching fallback")
  }
  if (!is.finite(shape) || shape <= 0 ||
      !is.finite(scale_parameter) || scale_parameter <= 0) {
    .fdb_stop("Could not compute a finite Frechet initialization.")
  }
  list(values = c(shape = shape, scale = scale_parameter), method = method,
       sample_lmoments = lm)
}

.fdb_lomax_lmoment_start <- function(x) {
  if (length(x) < 2L) {
    return(list(
      values = c(shape = 2, scale = max(x, .fdb_min_positive)),
      method = "Lomax single-observation scale anchor (L-moments require n >= 2)"
    ))
  }
  lm <- .fdb_sample_lmoments(x)
  tau <- unname(lm["tau"])
  method <- "Lomax L-moments (closed form)"
  if (!is.finite(tau) || tau <= 0.5 || tau >= 1) {
    tau <- min(1 - 1e-6, max(0.5005, tau))
    method <- paste0(method, "; interior boundary correction")
  }
  shape <- tau / (2 * tau - 1)
  scale <- unname(lm["l1"] * (shape - 1))
  if (!is.finite(shape) || shape <= 1 ||
      !is.finite(scale) || scale <= 0) {
    shape <- 2
    scale <- max(mean(x), .fdb_min_positive)
    method <- paste0(method, "; finite moment fallback")
  }
  list(values = c(shape = shape, scale = scale), method = method,
       sample_lmoments = lm)
}

.fdb_nakagami_moment_start <- function(x) {
  y <- x^2
  omega <- mean(y)
  denominator <- mean(y^2) - omega^2
  shape <- omega^2 / denominator
  method <- "Nakagami method of moments (closed form)"
  if (!is.finite(shape) || shape <= 0) {
    shape <- 1e4
    method <- paste0(method, "; finite degenerate-sample limit")
  }
  list(values = c(shape = min(shape, 1e4), spread = omega), method = method)
}

.fdb_el_moment_start <- function(x) {
  mean_x <- mean(x)
  cv2 <- .fdb_second_central_moment(x, mean_x) / mean_x^2
  cv2_el <- function(theta) {
    log_theta <- log(theta)
    d2 <- .fdb_polylog2_unit(1 - theta)
    d3 <- .fdb_polylog3_unit(1 - theta)
    -2 * d3 * log_theta / d2^2 - 1
  }
  lower <- 1e-6
  upper <- 1 - 1e-8
  if (!is.finite(cv2) || cv2 <= 1) {
    theta <- upper
    method <- paste0(
      "Exponential-Logarithmic moments; exponential-boundary correction"
    )
  } else if (cv2 >= cv2_el(lower)) {
    theta <- lower
    method <- paste0(
      "Exponential-Logarithmic moments; heavy-tail boundary correction"
    )
  } else {
    theta <- stats::uniroot(
      function(z) cv2_el(z) - cv2,
      interval = c(lower, upper), tol = 1e-9
    )$root
    method <- "Exponential-Logarithmic method of moments (one-dimensional root)"
  }
  rate <- -.fdb_polylog2_unit(1 - theta) / (mean_x * log(theta))
  if (!is.finite(rate) || rate <= 0) rate <- 1 / mean_x
  list(values = c(theta = theta, rate = rate), method = method)
}

.fdb_rician_moment_start <- function(x) {
  m2 <- mean(x^2)
  m4 <- mean(x^4)
  eta4 <- 2 * m2^2 - m4
  method <- "Rician second/fourth method of moments (closed form)"
  if (is.finite(eta4) && eta4 > 0) {
    eta <- eta4^(1 / 4)
  } else {
    eta <- max(0.05 * sqrt(m2), .fdb_min_positive)
    method <- paste0(method, "; positive boundary correction")
  }
  alpha2 <- (m2 - eta^2) / 2
  if (!is.finite(alpha2) || alpha2 <= 0) {
    alpha2 <- max(m2 / 4, .fdb_min_positive)
    method <- paste0(method, "; scale fallback")
  }
  list(values = c(noncentrality = eta, scale = sqrt(alpha2)), method = method)
}

.fdb_gauss_laguerre <- function(order = 96L) {
  j <- seq_len(order)
  jacobi <- diag(2 * j - 1)
  off <- seq_len(order - 1L)
  jacobi[cbind(j[-1L], j[-order])] <- off
  jacobi[cbind(j[-order], j[-1L])] <- off
  eig <- eigen(jacobi, symmetric = TRUE)
  index <- order(eig$values)
  list(nodes = eig$values[index], weights = eig$vectors[1L, index]^2)
}

.fdb_rician_logq_factory <- function() {
  quadrature <- .fdb_gauss_laguerre(96L)
  log_grid <- seq(log(1e-4), log(25), length.out = 321L)
  q_one <- function(rho) {
    y <- sqrt(2 * rho * quadrature$nodes)
    i0e <- besselI(y, nu = 0, expon.scaled = TRUE)
    i1e <- besselI(y, nu = 1, expon.scaled = TRUE)
    integral <- sum(
      quadrature$weights * 2 * quadrature$nodes *
        exp(-rho / 2 + y) * i1e^2 / i0e
    )
    psi <- integral - rho
    pmax((rho + 1) * psi - rho, .fdb_min_positive)
  }
  log_values <- log(vapply(exp(log_grid), q_one, numeric(1)))
  interpolator <- stats::splinefun(log_grid, log_values, method = "monoH.FC")
  function(log_rho) {
    if (!is.finite(log_rho)) {
      return(if (log_rho > 0) log(0.5) else -Inf)
    }
    if (log_rho < log_grid[1L]) {
      rho <- exp(log_rho)
      correction <- pmax(1 - 3 * rho, .Machine$double.eps)
      return(3 * log_rho - log(4) + log(correction))
    }
    if (log_rho > log_grid[length(log_grid)]) {
      if (log_rho > log(.Machine$double.xmax)) return(log(0.5))
      inverse <- exp(-log_rho)
      q <- 0.5 - 0.75 * inverse - 0.75 * inverse^2 -
        (19 / 8) * inverse^3
      return(log(pmax(q, .fdb_min_positive)))
    }
    unname(interpolator(log_rho))
  }
}

.fdb_el_prior_terms <- function(log_theta) {
  log_theta <- unname(log_theta)
  theta <- exp(log_theta)
  one_minus <- -expm1(log_theta)
  if (!is.finite(theta) || theta <= 0 || one_minus <= 0) {
    return(c(log_dilog = NA_real_, log_zeta = NA_real_,
             log_information_theta = NA_real_))
  }
  dilog <- .fdb_polylog2_unit(one_minus)
  if (theta < 1e-4) {
    log_i <- -log(2) - 2 * log_theta - log(-log_theta)
    log_zeta <- log(pi^2 / 12 - 1 / 4) -
      2 * log_theta - 2 * log(-log_theta)
  } else if (one_minus < 1e-4) {
    log_i <- -log(12)
    log_zeta <- -log(48)
  } else {
    numerator <- 2 * one_minus^2 + (1 - theta^2) * log_theta
    denominator <- 2 * theta^2 * one_minus^2 * log_theta^2
    information_theta <- -numerator / denominator
    cross_term <- (one_minus + theta * log_theta)^2 /
      (4 * theta^2 * one_minus^2 * log_theta^2)
    zeta <- information_theta * (-dilog / log_theta) - cross_term
    if (!is.finite(information_theta) || information_theta <= 0 ||
        !is.finite(zeta) || zeta <= 0) {
      return(c(log_dilog = NA_real_, log_zeta = NA_real_,
               log_information_theta = NA_real_))
    }
    log_i <- log(information_theta)
    log_zeta <- log(zeta)
  }
  c(log_dilog = log(dilog), log_zeta = log_zeta,
    log_information_theta = log_i)
}

.fdb_cauchy_quantile_start <- function(x) {
  q <- stats::quantile(x, c(0.25, 0.5, 0.75), names = FALSE,
                       type = 8)
  scale <- (q[3L] - q[1L]) / 2
  method <- "Cauchy quantile matching (median and half-IQR)"
  if (!is.finite(scale) || scale <= 0) {
    scale <- .fdb_safe_scale(x)
    method <- paste0(method, "; robust-scale fallback")
  }
  list(values = c(location = q[2L], scale = scale), method = method)
}

.fdb_student_start <- function(x, df = NULL) {
  if (!is.null(df) && df > 2) {
    location <- mean(x)
    m2 <- .fdb_second_central_moment(x, location)
    scale <- sqrt(m2 * (df - 2) / df)
    return(list(
      values = c(location = location, scale = scale),
      method = "Student-t method of moments (fixed df)"
    ))
  }

  if (!is.null(df)) {
    q <- stats::quantile(x, c(0.25, 0.5, 0.75), names = FALSE,
                         type = 8)
    theoretical_iqr <- stats::qt(0.75, df = df) -
      stats::qt(0.25, df = df)
    scale <- (q[3L] - q[1L]) / theoretical_iqr
    if (!is.finite(scale) || scale <= 0) scale <- .fdb_safe_scale(x)
    return(list(
      values = c(location = q[2L], scale = scale),
      method = paste0(
        "Student-t quantile matching (fixed df <= 2; moments unavailable)"
      )
    ))
  }

  location <- mean(x)
  centered <- x - location
  m2 <- mean(centered^2)
  m4 <- mean(centered^4)
  excess <- m4 / m2^2 - 3
  if (is.finite(excess) && excess > 0) {
    df_start <- 4 + 6 / excess
    method <- paste0(
      "Student-t method of moments (variance and excess kurtosis)"
    )
  } else {
    df_start <- 100
    method <- paste0(
      "Student-t method of moments; finite normal-limit fallback for df"
    )
  }
  # Very small positive empirical excess kurtosis implies an arbitrarily
  # large moment estimate.  A finite cap represents the normal limit while
  # keeping the transformed MCMC initialization numerically useful.
  df_start <- min(max(df_start, 4 + sqrt(.Machine$double.eps)), 100)
  scale <- sqrt(m2 * (df_start - 2) / df_start)
  if (!is.finite(scale) || scale <= 0) scale <- .fdb_safe_scale(x)
  list(
    values = c(location = location, scale = scale, df = df_start),
    method = method
  )
}

.fdb_weibull_lmoment_start <- function(x) {
  n <- length(x)
  ordered <- sort(x)
  b0 <- mean(ordered)
  b1 <- sum(((seq_len(n) - 1) / (n - 1)) * ordered) / n
  l1 <- b0
  l2 <- 2 * b1 - b0
  tau <- l2 / l1
  shape <- -log(2) / log1p(-tau)
  scale <- l1 / gamma(1 + 1 / shape)
  method <- "Weibull L-moments (closed form)"

  if (!is.finite(shape) || shape <= 0 ||
      !is.finite(scale) || scale <= 0) {
    q <- stats::quantile(x, c(0.25, 0.75), names = FALSE, type = 8)
    shape <- log(log(4) / log(4 / 3)) / log(q[2L] / q[1L])
    scale <- l1 / gamma(1 + 1 / shape)
    method <- paste0(method, "; quantile-matching fallback")
  }
  if (!is.finite(shape) || shape <= 0 ||
      !is.finite(scale) || scale <= 0) {
    .fdb_stop("Could not compute a finite Weibull L-moment initialization.")
  }
  list(values = c(shape = shape, scale = scale), method = method,
       sample_lmoments = c(l1 = l1, l2 = l2, tau = tau))
}

.fdb_weighted_lindley_start <- function(x) {
  lx <- log(x)
  mean_x <- mean(x)
  mean_log_x <- mean(lx)
  mean_x_log_x <- mean(x * lx)
  x_over_one_plus_x <- ifelse(x > 1, 1 / (1 + 1 / x), x / (1 + x))
  z_bar <- 1 + mean(x_over_one_plus_x * lx)
  linear_term <- z_bar * (1 - mean_x) + mean_x_log_x
  discriminant_term <- 4 * z_bar * mean_x * (z_bar - mean_log_x)
  discriminant <- linear_term^2 + discriminant_term

  sigma <- phi <- lambda <- NA_real_
  if (is.finite(discriminant) && discriminant >= 0 &&
      is.finite(z_bar) && z_bar > 0 && is.finite(mean_x) && mean_x > 0) {
    root <- sqrt(discriminant)
    # The alternative form avoids cancellation when the linear term is
    # negative and nearly equal in magnitude to the square root.
    sigma <- if (linear_term >= 0) {
      (linear_term + root) / (2 * z_bar * mean_x)
    } else {
      discriminant_term / (2 * z_bar * mean_x * (root - linear_term))
    }
    denominator <- sigma * mean_x_log_x - mean_log_x
    phi <- z_bar / denominator
    lambda <- sigma * phi
  }

  values <- c(lambda = lambda, phi = phi)
  valid <- all(is.finite(values)) && all(values > 0)
  if (valid) {
    return(list(
      values = values,
      method = "weighted Lindley closed-form likelihood estimator"
    ))
  }

  variance_x <- .fdb_second_central_moment(x, mean_x)
  phi0 <- if (is.finite(variance_x) && variance_x > 0) {
    max(0.1, mean_x^2 / variance_x)
  } else {
    1
  }
  sigma0 <- 1 / mean_x
  if (!is.finite(sigma0) || sigma0 <= 0) sigma0 <- 1
  initial <- c(log_ratio = log(sigma0), log_phi = log(phi0))
  sum_log_x <- sum(lx)
  sum_x <- sum(x)
  objective <- function(u) {
    if (length(u) != 2L || any(!is.finite(u))) return(.Machine$double.xmax)
    log_phi <- u[2L]
    log_lambda <- u[1L] + log_phi
    if (log_phi > log(.Machine$double.xmax) ||
        log_lambda > log(.Machine$double.xmax)) {
      return(.Machine$double.xmax)
    }
    phi_value <- exp(log_phi)
    lambda_value <- exp(log_lambda)
    if (!is.finite(phi_value) || !is.finite(lambda_value) ||
        phi_value <= 0 || lambda_value <= 0) {
      return(.Machine$double.xmax)
    }
    log_lambda_plus_phi <- log_phi + .fdb_softplus(u[1L])
    ll <- length(x) * (phi_value + 1) * log_lambda -
      length(x) * log_lambda_plus_phi -
      length(x) * lgamma(phi_value) +
      (phi_value - 1) * sum_log_x - lambda_value * sum_x
    if (is.finite(ll)) -ll else .Machine$double.xmax
  }
  optimum <- tryCatch(
    stats::optim(initial, objective, method = "Nelder-Mead",
                 control = list(maxit = 2000L, reltol = 1e-10)),
    error = function(e) NULL
  )
  if (is.null(optimum) || !is.finite(optimum$value) ||
      any(!is.finite(optimum$par))) {
    .fdb_stop(
      "Could not compute finite automatic starting values for the weighted Lindley model."
    )
  }
  phi <- exp(optimum$par[2L])
  lambda <- exp(sum(optimum$par))
  values <- c(lambda = lambda, phi = phi)
  if (any(!is.finite(values)) || any(values <= 0)) {
    .fdb_stop(
      "Could not compute positive automatic starting values for the weighted Lindley model."
    )
  }
  list(
    values = values,
    method = "weighted Lindley numerical maximum-likelihood fallback"
  )
}

.fdb_log_expm1_positive <- function(log_x) {
  if (length(log_x) != 1L || !is.finite(log_x) || log_x <= 0) {
    return(-Inf)
  }
  if (log_x < 50) return(log(expm1(log_x)))
  log_x + log1p(-exp(-log_x))
}

.fdb_log_positive_difference <- function(log_positive,
                                         log_subtracted) {
  if (length(log_positive) != 1L || length(log_subtracted) != 1L ||
      !is.finite(log_positive) || !is.finite(log_subtracted) ||
      log_subtracted >= log_positive) {
    return(-Inf)
  }
  log_positive + log1p(-exp(log_subtracted - log_positive))
}

.fdb_weighted_lindley_lambda_from_mean <- function(mu, phi) {
  if (length(mu) != 1L || length(phi) != 1L ||
      !is.finite(mu) || !is.finite(phi) || mu <= 0 || phi <= 0) {
    return(NA_real_)
  }
  linear <- phi * (mu - 1)
  constant <- phi * (phi + 1)
  discriminant <- linear^2 + 4 * mu * constant
  if (!is.finite(discriminant) || discriminant <= 0) return(NA_real_)
  root <- sqrt(discriminant)
  lambda <- if (linear >= 0) {
    2 * constant / (root + linear)
  } else {
    (root - linear) / (2 * mu)
  }
  if (is.finite(lambda) && lambda > 0) lambda else NA_real_
}

.fdb_weighted_lindley_log_prior <- function(lambda, phi, prior) {
  if (!is.finite(phi) || !is.finite(lambda) || phi <= 0 || lambda <= 0) {
    return(-Inf)
  }
  log_phi <- log(phi)
  log_lambda <- log(lambda)
  log_sum <- .fdb_logsumexp(c(log_lambda, log_phi))
  trigamma_phi <- trigamma(phi)
  if (!is.finite(trigamma_phi) || trigamma_phi <= 0) return(-Inf)
  log_trigamma <- log(trigamma_phi)
  log_c <- .fdb_log_expm1_positive(log_phi + log_trigamma)
  if (!is.finite(log_c)) return(-Inf)

  if (prior == "first-rule") return(-log_lambda - log_phi)

  if (prior %in% c("jeffreys", "reference")) {
    log_A <- .fdb_logsumexp(c(
      2 * log_sum, log(2) + log_lambda, log_phi
    ))
    log_radicand <- .fdb_log_expm1_positive(log_A + log_c)
    if (!is.finite(log_radicand)) return(-Inf)
    return(0.5 * log_radicand - log_lambda - log_sum)
  }

  if (prior == "independence-jeffreys") {
    log_first_positive <- log1p(phi) + 2 * log_sum
    log_first_negative <- 2 * log_lambda
    difference <- log_first_negative - log_first_positive
    if (!is.finite(difference) || difference >= 0) return(-Inf)
    log_first <- log_first_positive + log1p(-exp(difference))
    log_second <- .fdb_log_expm1_positive(
      log(trigamma_phi) + 2 * log_sum
    )
    if (!is.finite(log_first) || !is.finite(log_second)) return(-Inf)
    return(0.5 * (log_first + log_second) - log_lambda - 2 * log_sum)
  }

  if (prior == "reference-lambda") {
    log_information_phi <- .fdb_log_positive_difference(
      log_trigamma, -2 * log_sum
    )
    if (!is.finite(log_information_phi)) return(-Inf)
    return(-log_lambda + 0.5 * log_information_phi)
  }

  if (prior == "reference-phi") {
    log_A <- .fdb_logsumexp(c(
      2 * log_sum, log(2) + log_lambda, log_phi
    ))
    log_sqrt_phi_plus_one <- 0.5 * log1p(phi)
    log_sqrt_phi <- 0.5 * log_phi
    log_s <- .fdb_logsumexp(c(log_sqrt_phi_plus_one, log_sqrt_phi))
    weight_zero <- exp(log_sqrt_phi_plus_one - log_s)
    weight_infinity <- exp(log_sqrt_phi - log_s)
    log_q_zero <- .fdb_log_positive_difference(
      log(trigamma(phi + 1)), -log1p(phi)
    )
    log_q_infinity <- .fdb_log_positive_difference(
      log_trigamma, -log_phi
    )
    if (!is.finite(log_q_zero) || !is.finite(log_q_infinity)) return(-Inf)
    return(
      0.5 * (log_phi + log_A) - log_lambda - log_sum - log_s +
        0.5 * weight_zero * log_q_zero +
        0.5 * weight_infinity * log_q_infinity
    )
  }

  -Inf
}

.fdb_normalize_name <- function(x) {
  y <- tolower(trimws(x))
  y <- gsub("[_.]+", "-", y)
  y <- gsub("[[:space:]]+", "-", y)
  y <- gsub("-+", "-", y)
  y
}

.fdb_model_name <- function(distr) {
  key <- .fdb_normalize_name(distr)
  aliases <- c(
    "beta" = "beta",
    "cauchy" = "cauchy",
    "chi-squared" = "chi-squared",
    "chi-square" = "chi-squared",
    "chisquared" = "chi-squared",
    "chisq" = "chi-squared",
    "exponential" = "exponential",
    "exp" = "exponential",
    "exponential-logarithmic" = "exponential-logarithmic",
    "exponential-logarithmic-distribution" = "exponential-logarithmic",
    "el" = "exponential-logarithmic",
    "frechet" = "frechet",
    "frechet-distribution" = "frechet",
    "gamma" = "gamma",
    "geometric" = "geometric",
    "geom" = "geometric",
    "gumbel" = "gumbel",
    "gumbel-maximum" = "gumbel",
    "log-normal" = "lognormal",
    "lognormal" = "lognormal",
    "lnorm" = "lognormal",
    "logistic" = "logistic",
    "lomax" = "lomax",
    "pareto-ii" = "lomax",
    "pareto-type-ii" = "lomax",
    "nakagami" = "nakagami",
    "nakagami-m" = "nakagami",
    "negative-binomial" = "negative binomial",
    "negativebinomial" = "negative binomial",
    "nbinom" = "negative binomial",
    "normal" = "normal",
    "gaussian" = "normal",
    "norm" = "normal",
    "poisson" = "Poisson",
    "rice" = "rician",
    "rician" = "rician",
    "t" = "t",
    "student-t" = "t",
    "student" = "t",
    "weibull" = "weibull",
    "weighted-lindley" = "weighted lindley",
    "weightedlindley" = "weighted lindley",
    "wl" = "weighted lindley"
  )
  if (!key %in% names(aliases)) {
    .fdb_stop(
      "Unknown distribution '%s'. Recognized models are beta, cauchy, chi-squared, exponential, exponential-logarithmic, Frechet, gamma, geometric, Gumbel, log-normal/lognormal, logistic, Lomax, Nakagami-m, negative binomial, normal, Poisson, Rician, t, weibull, and weighted Lindley.",
      distr
    )
  }
  unname(aliases[[key]])
}

.fdb_prior_name <- function(prior, model = NULL) {
  key <- .fdb_normalize_name(prior)
  if (identical(model, "weighted lindley")) {
    if (key %in% c("reference-lambda", "reference-rate")) {
      return("reference-lambda")
    }
    if (key %in% c("reference-phi", "reference-shape")) {
      return("reference-phi")
    }
  }
  aliases <- c(
    "j" = "jeffreys",
    "jeffreys" = "jeffreys",
    "jeffreys-rule" = "jeffreys",
    "joint-jeffreys" = "jeffreys",
    "reference" = "reference",
    "ref" = "reference",
    "r" = "reference",
    "mdi" = "mdi",
    "maximal-data-information" = "mdi",
    "maximal-information" = "mdi",
    "independence-jeffreys" = "independence-jeffreys",
    "jeffreys-independence" = "independence-jeffreys",
    "first-rule" = "first-rule",
    "jeffreys-first-rule" = "first-rule",
    "reference-shape" = "reference-shape",
    "reference-phi" = "reference-phi",
    "reference-rate" = "reference-rate",
    "reference-theta" = "reference-theta",
    "reference-lambda" = "reference-rate"
  )
  if (!key %in% names(aliases)) {
    .fdb_stop(
      "Unknown objective prior '%s'. Use Jeffreys, Jeffreys first-rule, reference, MDI, independence-Jeffreys, reference-shape, reference-phi, reference-theta, or reference-rate as appropriate.",
      prior
    )
  }
  unname(aliases[[key]])
}

.fdb_route_catalog <- function() {
  rows <- function(model, priors, parameters, fixed, engine, condition) {
    if (length(engine) == 1L) engine <- rep(engine, length(priors))
    if (length(condition) == 1L) condition <- rep(condition, length(priors))
    if (length(engine) != length(priors) ||
        length(condition) != length(priors)) {
      .fdb_stop("Internal route catalogue error for model '%s'.", model)
    }
    data.frame(
      model = rep(model, length(priors)),
      prior = priors,
      parameters = rep(parameters, length(priors)),
      required_fixed = rep(fixed, length(priors)),
      engine = engine,
      posterior_condition = condition,
      stringsAsFactors = FALSE,
      check.names = FALSE
    )
  }
  do.call(rbind, list(
    rows("beta", c("jeffreys", "reference"), "shape1, shape2", "none",
         "adaptive Metropolis", "n >= 2; 0 < x < 1; nonconstant sample"),
    rows("cauchy", c("jeffreys", "reference", "mdi"), "location, scale", "none",
         "adaptive Metropolis",
         c("largest multiplicity m satisfies 2m < n",
           "largest multiplicity m satisfies 2m < n + 1",
           "largest multiplicity m satisfies 2m < n + 1")),
    rows("chi-squared", c("jeffreys", "reference"), "df", "none",
         "univariate slice sampling", "n >= 1; x > 0"),
    rows("exponential", c("jeffreys", "reference", "mdi"), "rate", "none",
         "exact Gamma posterior", "n >= 1; x >= 0; sum(x) > 0"),
    rows("exponential-logarithmic",
         c("jeffreys", "mdi", "reference-theta", "reference-rate"),
         "theta, rate", "none", "adaptive Metropolis",
         "x > 0; n > 2 for Jeffreys/reference, n >= 1 for MDI"),
    rows("frechet", c("jeffreys", "reference"), "shape, scale", "none",
         "marginal slice + exact conditional Gamma",
         "n >= 2; x > 0; nonconstant sample"),
    rows("gamma", c("jeffreys", "first-rule", "reference-shape", "reference-rate"),
         "shape, rate", "none", "marginal slice + exact conditional Gamma",
         "n >= 2; x > 0; nonconstant sample"),
    rows("geometric", c("jeffreys", "reference", "mdi"), "prob", "none",
         c("exact Beta posterior", "exact Beta posterior", "univariate slice sampling"),
         "n >= 1; nonnegative integer x"),
    rows("gumbel", c("jeffreys", "reference", "mdi"), "location, scale", "none",
         "adaptive Metropolis", "n >= 2; nonconstant sample"),
    rows("lognormal", c("jeffreys", "reference"), "meanlog, sdlog", "none",
         "exact Normal-inverse-Gamma posterior",
         "n >= 2; x > 0; nonconstant log(x)"),
    rows("logistic", c("jeffreys", "reference", "mdi"), "location, scale", "none",
         "adaptive Metropolis", "n >= 2; nonconstant sample"),
    rows("lomax", "jeffreys", "shape, scale", "none", "adaptive Metropolis",
         "n >= 1; x > 0"),
    rows("nakagami-m", c("jeffreys", "reference"), "shape, spread", "none",
         "marginal slice + exact conditional inverse-Gamma",
         "n >= 2; x > 0; nonconstant sample"),
    rows("negative binomial", c("jeffreys", "reference", "mdi"), "mu", "size",
         c("exact transformed-Beta posterior", "exact transformed-Beta posterior",
           "univariate slice sampling with entropy evaluation"),
         "n >= 1; nonnegative integer x; fixed size > 0"),
    rows("normal", c("jeffreys", "reference", "mdi"), "mean, sd", "none",
         "exact Normal-inverse-Gamma posterior", "n >= 2; nonconstant sample"),
    rows("Poisson", c("jeffreys", "reference", "mdi"), "lambda", "none",
         c("exact Gamma posterior", "exact Gamma posterior",
           "univariate slice sampling with entropy evaluation"),
         "n >= 1; nonnegative integer x"),
    rows("rician", "jeffreys", "noncentrality, scale", "none",
         "adaptive Metropolis with Gauss-Laguerre Jeffreys factor",
         "n > 2; x > 0; nonconstant sample"),
    rows("t", c("jeffreys", "reference", "mdi"), "location, scale", "df",
         "adaptive Metropolis",
         c("fixed df > 0; n+2 > 2 and df*(n-m)-m > 0",
           "fixed df > 0; n+1 > 2 and df*(n-m)-m+1 > 0",
           "fixed df > 0; n+1 > 2 and df*(n-m)-m+1 > 0")),
    rows("t", "independence-jeffreys", "location, scale, df", "none",
         "adaptive Metropolis", "n >= 2; nonconstant sample"),
    rows("weibull", c("jeffreys", "reference"), "shape, scale", "none",
         "marginal slice + exact conditional Gamma transform",
         "n >= 2; x > 0; nonconstant sample"),
    rows("weighted lindley",
         c("jeffreys", "reference", "first-rule",
           "independence-jeffreys", "reference-lambda", "reference-phi"),
         "lambda, phi", "none",
         "adaptive Metropolis in Fisher-orthogonal mean coordinates",
         "n >= 2; x > 0; nonconstant sample")
  ))
}

#' List the built-in objective Bayesian fitting routes
#'
#' Returns the machine-readable catalogue used to document and test the
#' available model--prior combinations. The condition column is a concise
#' summary; the fitting function remains the authoritative executable check.
#'
#' @param model Optional recognized distribution name. If supplied, only its
#'   routes are returned. Use `model = "t"` to display both fixed- and
#'   unknown-degrees-of-freedom Student-t routes.
#' @return A data frame with one row for every enabled model--prior route.
#' @export
fitdistrBayes_routes <- function(model = NULL) {
  catalog <- .fdb_route_catalog()
  if (is.null(model)) return(catalog)
  if (!is.character(model) || length(model) != 1L || is.na(model) ||
      !nzchar(trimws(model))) {
    .fdb_stop("'model' must be NULL or one recognized distribution name.")
  }
  key <- .fdb_model_name(model)
  if (identical(key, "nakagami")) key <- "nakagami-m"
  catalog[catalog$model == key, , drop = FALSE]
}

.fdb_moment_status <- function(model, prior, parameters, n, fixed, x = NULL) {
  answer <- data.frame(
    parameter = parameters,
    mean_exists = rep(NA, length(parameters)),
    variance_exists = rep(NA, length(parameters)),
    note = rep("not certified by the current moment registry", length(parameters)),
    stringsAsFactors = FALSE
  )
  certify <- function(result, par, mean, variance, note) {
    index <- match(par, result$parameter)
    result$mean_exists[index] <- mean
    result$variance_exists[index] <- variance
    result$note[index] <- note
    result
  }

  if (model %in% c("beta", "chi-squared", "exponential", "gamma",
                   "geometric", "Poisson")) {
    answer <- certify(
      answer, parameters, TRUE, TRUE,
      "all positive integer posterior moments are finite"
    )
  } else if (model %in% c("normal", "lognormal")) {
    a <- if (prior == "jeffreys") 2 else 1
    df_marginal <- n + a - 2
    answer <- certify(
      answer, parameters,
      df_marginal > 1,
      df_marginal > 2,
      sprintf("absolute posterior moments of order q exist iff q < %g",
              df_marginal)
    )
  } else if (model == "negative binomial") {
    nr <- n * fixed$size
    answer <- certify(
      answer, parameters,
      nr > 1,
      nr > 2,
      sprintf("posterior moments of mu of order q exist iff q < n*size = %g",
              nr)
    )
  } else if (model == "weighted lindley") {
    answer <- certify(
      answer, parameters, TRUE, TRUE,
      "all positive integer posterior moments are finite"
    )
  } else if (model == "weibull") {
    answer <- certify(
      answer, "shape", TRUE, TRUE,
      "all positive integer posterior moments are finite"
    )
    answer <- certify(
      answer, "scale", FALSE, FALSE,
      "no positive posterior moment of scale exists under this improper objective prior"
    )
  } else if (model == "t" && is.null(fixed$df)) {
    answer <- certify(
      answer, "df", FALSE, FALSE,
      "positive integer posterior moments of df do not exist"
    )
  } else if (model == "frechet") {
    answer <- certify(
      answer, "shape", TRUE, TRUE,
      "all positive posterior moments of shape are finite for a positive nonconstant sample"
    )
    minimum <- min(x)
    log_ratio_product <- sum(log(x / minimum))
    mean_ok <- log(minimum) < log_ratio_product
    variance_ok <- 2 * log(minimum) < log_ratio_product
    answer <- certify(
      answer, "scale", mean_ok, variance_ok,
      paste0(
        "the q-th scale moment exists iff q*log(min(x)) < ",
        "sum(log(x/min(x))); this is data dependent"
      )
    )
  } else if (model == "nakagami") {
    answer <- certify(
      answer, "shape", TRUE, TRUE,
      paste0(
        "all positive posterior moments of shape are finite for ",
        "n >= 2 and a positive nonconstant sample"
      )
    )
    answer <- certify(
      answer, "spread", FALSE, FALSE,
      "no positive spread moment exists on the full shape support (0,infinity)"
    )
  } else if (model == "exponential-logarithmic") {
    answer <- certify(
      answer, parameters, TRUE, TRUE,
      "all positive integer posterior moments are finite"
    )
  }
  answer
}

.fdb_check_fixed <- function(fixed, allowed) {
  if (is.null(fixed)) return(list())
  if (!is.list(fixed) || is.null(names(fixed)) ||
      any(!nzchar(names(fixed))) || anyDuplicated(names(fixed))) {
    .fdb_stop("'fixed' must be NULL or a uniquely named list.")
  }
  bad <- setdiff(names(fixed), allowed)
  if (length(bad)) {
    .fdb_stop("Unknown fixed parameter%s: %s.",
              if (length(bad) > 1L) "s" else "",
              paste(bad, collapse = ", "))
  }
  fixed
}

.fdb_logsumexp <- function(x) {
  z <- max(x)
  if (!is.finite(z)) return(z)
  z + log(sum(exp(x - z)))
}

.fdb_gamma_joint_term <- function(shape) {
  ans <- shape * trigamma(shape) - 1
  large <- shape > 1e5
  if (any(large)) {
    a <- shape[large]
    ans[large] <- 1 / (2 * a) + 1 / (6 * a^2) - 1 / (30 * a^4)
  }
  pmax(ans, .Machine$double.xmin)
}

.fdb_t_B <- function(df) {
  ans <- trigamma(df / 2) - trigamma((df + 1) / 2) -
    2 * (df + 3) / (df * (df + 1)^2)
  # Direct subtraction loses precision in the far tail.  The leading
  # positive expansion below is used only where that cancellation matters.
  large <- df > 1e4 | !is.finite(ans) | ans <= 0
  if (any(large)) {
    v <- df[large]
    ans[large] <- 6 / v^4 - 12 / v^5 + 14 / v^6 -
      12 / v^7 + 22 / v^8 - 60 / v^9
  }
  pmax(ans, .Machine$double.xmin)
}

.fdb_poisson_entropy <- function(lambda, control) {
  if (!is.finite(lambda) || lambda <= 0) {
    return(if (identical(lambda, 0)) 0 else Inf)
  }
  if (lambda > control$entropy_exact_limit) {
    return(0.5 * log(2 * pi * exp(1) * lambda) -
             1 / (12 * lambda) - 1 / (24 * lambda^2) -
             19 / (360 * lambda^3))
  }
  lo <- max(0, stats::qpois(control$entropy_tol / 2, lambda))
  hi <- stats::qpois(1 - control$entropy_tol / 2, lambda)
  k <- seq.int(lo, hi)
  lp <- stats::dpois(k, lambda, log = TRUE)
  p <- exp(lp)
  -sum(p * lp)
}

.fdb_nbinom_entropy <- function(size, mu, control) {
  if (!is.finite(mu) || mu <= 0) {
    return(if (identical(mu, 0)) 0 else Inf)
  }

  # Exact integral representation of the negative-binomial entropy; see
  # Cheraghchi (2019), IEEE Transactions on Information Theory 65, 3999--4009.
  # Numerical integration has essentially constant cost as mu grows, unlike
  # direct truncation of the infinite probability sum.  The u = -log(1-z)
  # form below also removes the endpoint singularities of the z-integral.
  log1p_ratio <- function(a, b) {
    ratio <- a / b
    answer <- log1p(ratio)
    overflow <- is.infinite(ratio)
    if (any(overflow)) {
      aa <- rep_len(a, length(ratio))
      bb <- rep_len(b, length(ratio))
      answer[overflow] <- log(aa[overflow]) - log(bb[overflow])
    }
    answer
  }

  base <- mu * log1p_ratio(size, mu) +
    size * log1p_ratio(mu, size)
  if (size == 1) return(base)  # geometric entropy, available in closed form

  integrand <- function(u) {
    one_minus_exp <- -expm1(-u)
    denominator <- one_minus_exp * u
    numerator <- if (size < 1) {
      exp(-size * u) * expm1(-(1 - size) * u)
    } else {
      exp(-u) * (-expm1(-(size - 1) * u))
    }
    scaled_mean <- one_minus_exp * mu
    bracket <- expm1(-size * log1p_ratio(scaled_mean, size))
    numerator / denominator * bracket
  }

  integral <- stats::integrate(
    integrand, lower = 0, upper = Inf,
    rel.tol = control$entropy_tol, abs.tol = control$entropy_tol,
    subdivisions = 200L, stop.on.error = FALSE
  )
  value <- base + integral$value
  if (!identical(integral$message, "OK") || !is.finite(value) || value < 0) {
    return(Inf)
  }
  value
}

.fdb_with_seed <- function(seed, code) {
  if (is.null(seed)) return(force(code))
  if (length(seed) != 1L || !is.numeric(seed) || !is.finite(seed) ||
      seed < 0 || seed > .Machine$integer.max || seed != as.integer(seed)) {
    .fdb_stop("'seed' must be NULL or one integer between 0 and %d.",
              .Machine$integer.max)
  }
  set.seed(as.integer(seed))
  force(code)
}

.fdb_slice_one <- function(current, log_density, width, max_steps) {
  log_y <- log_density(current) - stats::rexp(1)
  if (!is.finite(log_y)) {
    .fdb_stop("The slice sampler encountered a non-finite density at its current state.")
  }
  u <- stats::runif(1)
  left <- current - width * u
  right <- left + width
  j <- floor(stats::runif(1, 0, max_steps))
  k <- (max_steps - 1L) - j
  while (j > 0L && log_density(left) > log_y) {
    left <- left - width
    j <- j - 1L
  }
  while (k > 0L && log_density(right) > log_y) {
    right <- right + width
    k <- k - 1L
  }
  repeat {
    proposal <- stats::runif(1, left, right)
    lp <- log_density(proposal)
    if (is.finite(lp) && lp >= log_y) return(proposal)
    if (proposal < current) left <- proposal else right <- proposal
    if (!is.finite(left + right) || right - left <
        .Machine$double.eps * max(1, abs(current))) {
      .fdb_stop("The slice sampler collapsed to a numerically empty interval.")
    }
  }
}

.fdb_slice_chains <- function(log_density, init, iter, warmup, thin, chains,
                              control) {
  n_save <- floor((iter - warmup) / thin)
  out <- vector("list", chains)
  for (ch in seq_len(chains)) {
    current <- init + stats::rnorm(1, 0, control$init_jitter)
    tries <- 0L
    while (!is.finite(log_density(current)) &&
           tries < control$max_init_tries) {
      current <- init + stats::rnorm(1, 0, control$init_jitter)
      tries <- tries + 1L
    }
    if (!is.finite(log_density(current))) {
      .fdb_stop("Could not find a finite initial state for slice chain %d.", ch)
    }
    saved <- numeric(n_save)
    pos <- 0L
    for (i in seq_len(iter)) {
      current <- .fdb_slice_one(
        current, log_density,
        width = control$slice_width,
        max_steps = control$slice_steps
      )
      if (i > warmup && ((i - warmup) %% thin == 0L)) {
        pos <- pos + 1L
        saved[pos] <- current
      }
    }
    out[[ch]] <- saved
  }
  out
}

.fdb_amwg <- function(log_density, init, iter, warmup, thin, chains, control) {
  p <- length(init)
  n_save <- floor((iter - warmup) / thin)
  out <- vector("list", chains)
  acceptance_post <- matrix(0, nrow = chains, ncol = p)
  acceptance_warmup <- matrix(0, nrow = chains, ncol = p)
  acceptance_all <- matrix(0, nrow = chains, ncol = p)
  colnames(acceptance_post) <- names(init)
  colnames(acceptance_warmup) <- names(init)
  colnames(acceptance_all) <- names(init)

  # Work in model-relevant units. For built-in location-scale families the
  # unconstrained location coordinate has the units of the observations,
  # whereas log(scale) is dimensionless. A common absolute proposal standard
  # deviation makes small-scale or large-scale data mix extremely poorly.
  coordinate_scale <- rep(1, p)
  location_index <- match("location", names(init))
  log_scale_index <- match("log_scale", names(init))
  if (!is.na(location_index) && !is.na(log_scale_index)) {
    initial_scale <- exp(init[log_scale_index])
    if (is.finite(initial_scale) && initial_scale > 0) {
      coordinate_scale[location_index] <- initial_scale
    }
  }
  jitter_sd <- pmax(
    .fdb_min_positive, control$init_jitter * coordinate_scale
  )
  initial_proposal_sd <- pmax(
    .fdb_min_positive, control$proposal_scale * coordinate_scale
  )

  for (ch in seq_len(chains)) {
    current <- init + stats::rnorm(p, 0, jitter_sd)
    names(current) <- names(init)
    lp <- log_density(current)
    tries <- 0L
    while (!is.finite(lp) && tries < control$max_init_tries) {
      current <- init + stats::rnorm(p, 0, jitter_sd)
      names(current) <- names(init)
      lp <- log_density(current)
      tries <- tries + 1L
    }
    if (!is.finite(lp)) {
      current <- init
      lp <- log_density(current)
    }
    if (!is.finite(lp)) {
      .fdb_stop("Could not find a finite initial state for Metropolis chain %d.", ch)
    }

    log_sd <- log(initial_proposal_sd)
    block_accept <- numeric(p)
    warmup_accept <- numeric(p)
    post_accept <- numeric(p)
    saved <- matrix(NA_real_, nrow = n_save, ncol = p,
                    dimnames = list(NULL, names(init)))
    pos <- 0L

    for (i in seq_len(iter)) {
      for (j in seq_len(p)) {
        proposal <- current
        proposal[j] <- stats::rnorm(1, current[j], exp(log_sd[j]))
        lp_new <- log_density(proposal)
        accepted <- is.finite(lp_new) &&
          log(stats::runif(1)) < (lp_new - lp)
        if (accepted) {
          current <- proposal
          lp <- lp_new
          block_accept[j] <- block_accept[j] + 1
          if (i <= warmup) {
            warmup_accept[j] <- warmup_accept[j] + 1
          } else {
            post_accept[j] <- post_accept[j] + 1
          }
        }
      }

      if (i <= warmup && i %% control$adapt_interval == 0L) {
        batch <- i / control$adapt_interval
        gain <- min(0.1, 1 / sqrt(batch))
        rate <- block_accept / control$adapt_interval
        log_sd <- log_sd + gain * (rate - control$target_accept)
        log_sd <- pmin(
          log(.Machine$double.xmax) / 2,
          pmax(log(.Machine$double.xmin) / 2, log_sd)
        )
        block_accept[] <- 0
      }

      if (i > warmup && ((i - warmup) %% thin == 0L)) {
        pos <- pos + 1L
        saved[pos, ] <- current
      }
    }
    out[[ch]] <- saved
    acceptance_warmup[ch, ] <- if (warmup > 0L) {
      warmup_accept / warmup
    } else {
      NA_real_
    }
    acceptance_post[ch, ] <- post_accept / (iter - warmup)
    acceptance_all[ch, ] <- (warmup_accept + post_accept) / iter
  }
  list(
    chains = out,
    acceptance = acceptance_post,
    acceptance_warmup = acceptance_warmup,
    acceptance_all = acceptance_all
  )
}

.fdb_split_matrix <- function(chains, parameter) {
  n <- min(vapply(chains, nrow, integer(1)))
  half <- floor(n / 2)
  if (half < 2L) return(NULL)
  ans <- matrix(NA_real_, nrow = half, ncol = 2L * length(chains))
  col <- 0L
  for (ch in chains) {
    z <- ch[seq_len(n), parameter]
    col <- col + 1L
    ans[, col] <- z[seq_len(half)]
    col <- col + 1L
    ans[, col] <- z[n - half + seq_len(half)]
  }
  ans
}

.fdb_rank_normalize <- function(x) {
  n <- length(x)
  stats::qnorm((rank(x, ties.method = "average") - 3 / 8) / (n + 1 / 4))
}

.fdb_basic_rhat <- function(x) {
  n <- nrow(x)
  chain_var <- apply(x, 2, stats::var)
  W <- mean(chain_var)
  between <- stats::var(colMeans(x))
  scale <- max(1, mean(x^2))
  tolerance <- 100 * .Machine$double.eps * scale
  if (!is.finite(W) || !is.finite(between)) return(NA_real_)
  if (W <= tolerance) {
    return(if (between <= tolerance) 1 else Inf)
  }
  B <- n * between
  var_plus <- (n - 1) / n * W + B / n
  sqrt(max(var_plus / W, 1))
}

.fdb_rhat <- function(x) {
  if (is.null(x)) return(NA_real_)
  z <- matrix(.fdb_rank_normalize(as.vector(x)),
              nrow = nrow(x), ncol = ncol(x))
  folded_raw <- abs(x - stats::median(x))
  folded <- matrix(.fdb_rank_normalize(as.vector(folded_raw)),
                   nrow = nrow(x), ncol = ncol(x))
  max(.fdb_basic_rhat(z), .fdb_basic_rhat(folded))
}

.fdb_ess_matrix <- function(x) {
  if (is.null(x)) return(NA_real_)
  n <- nrow(x)
  m <- ncol(x)
  W <- mean(apply(x, 2, stats::var))
  between <- stats::var(colMeans(x))
  scale <- max(1, mean(x^2))
  tolerance <- 100 * .Machine$double.eps * scale
  if (!is.finite(W) || !is.finite(between)) return(NA_real_)
  if (W <= tolerance) {
    return(if (between <= tolerance) n * m else 0)
  }
  B <- n * between
  var_plus <- (n - 1) / n * W + B / n
  if (!is.finite(var_plus) || var_plus <= 0) return(NA_real_)

  max_lag <- min(n - 1L, 1000L)
  acov <- vapply(seq_len(m), function(j) {
    as.numeric(stats::acf(x[, j], lag.max = max_lag,
                          type = "covariance", plot = FALSE,
                          demean = TRUE)$acf)
  }, numeric(max_lag + 1L))
  mean_acov <- rowMeans(acov)
  rho <- 1 - (W - mean_acov) / var_plus
  rho[1L] <- 1

  pair_sums <- numeric()
  k <- 1L
  while ((2L * k) <= length(rho)) {
    pair <- rho[2L * k - 1L] + rho[2L * k]
    if (!is.finite(pair) || pair < 0) break
    pair_sums <- c(pair_sums, pair)
    k <- k + 1L
  }
  if (length(pair_sums) > 1L) {
    for (j in 2:length(pair_sums)) {
      pair_sums[j] <- min(pair_sums[j], pair_sums[j - 1L])
    }
  }
  tau <- if (length(pair_sums)) -1 + 2 * sum(pair_sums) else 1
  tau <- max(tau, 1 / log10(n * m))
  min(n * m, n * m / tau)
}

.fdb_ess <- function(x, type = c("bulk", "tail")) {
  type <- match.arg(type)
  if (is.null(x)) return(NA_real_)
  if (type == "bulk") {
    z <- matrix(.fdb_rank_normalize(as.vector(x)),
                nrow = nrow(x), ncol = ncol(x))
    return(.fdb_ess_matrix(z))
  }
  q <- stats::quantile(x, c(0.05, 0.95), names = FALSE, type = 8)
  low <- (x <= q[1]) * 1
  high <- (x >= q[2]) * 1
  min(.fdb_ess_matrix(low), .fdb_ess_matrix(high))
}

.fdb_summarize <- function(chains, independent = FALSE) {
  parameters <- colnames(chains[[1L]])
  total <- do.call(rbind, chains)
  ans <- lapply(seq_along(parameters), function(j) {
    values <- total[, j]
    q <- stats::quantile(values, c(0.025, 0.25, 0.5, 0.75, 0.975),
                         names = FALSE, type = 8)
    if (independent) {
      rhat <- 1
      ess_mean <- length(values)
      ess_bulk <- length(values)
      ess_tail <- length(values)
    } else {
      split <- .fdb_split_matrix(chains, j)
      rhat <- .fdb_rhat(split)
      ess_mean <- .fdb_ess_matrix(split)
      ess_bulk <- .fdb_ess(split, "bulk")
      ess_tail <- .fdb_ess(split, "tail")
    }
    sdev <- stats::sd(values)
    data.frame(
      parameter = parameters[j],
      mean = mean(values),
      sd = sdev,
      median = q[3],
      mad = stats::mad(values),
      q2.5 = q[1],
      q25 = q[2],
      q75 = q[4],
      q97.5 = q[5],
      mcse_mean = sdev / sqrt(ess_mean),
      rhat = rhat,
      ess_mean = ess_mean,
      ess_bulk = ess_bulk,
      ess_tail = ess_tail,
      row.names = NULL,
      check.names = FALSE
    )
  })
  do.call(rbind, ans)
}

.fdb_long_draws <- function(chains) {
  out <- lapply(seq_along(chains), function(ch) {
    z <- as.data.frame(chains[[ch]], check.names = FALSE)
    data.frame(
      .chain = ch,
      .iteration = seq_len(nrow(z)),
      .draw = (ch - 1L) * nrow(z) + seq_len(nrow(z)),
      z,
      check.names = FALSE
    )
  })
  do.call(rbind, out)
}

.fdb_transform_matrix <- function(chains_u, transform, parameter_names) {
  lapply(chains_u, function(ch) {
    values <- apply(ch, 1L, transform)
    ans <- if (length(parameter_names) == 1L) {
      matrix(values, ncol = 1L)
    } else {
      t(values)
    }
    colnames(ans) <- parameter_names
    ans
  })
}

.fdb_direct_chains <- function(draw_one_chain, chains, n_save) {
  lapply(seq_len(chains), function(i) draw_one_chain(n_save))
}

.fdb_custom_bounds <- function(start, lower, upper) {
  p <- length(start)
  nm <- names(start)
  expand <- function(z, default, label) {
    if (is.null(z)) return(stats::setNames(rep(default, p), nm))
    if (!is.numeric(z) || is.complex(z) || anyNA(z)) {
      .fdb_stop("control$%s must be real numeric and must not contain NA.",
                label)
    }
    if (!is.null(names(z))) {
      if (any(!nzchar(names(z))) || anyDuplicated(names(z))) {
        .fdb_stop("Names in control$%s must be unique and nonempty.", label)
      }
      bad <- setdiff(names(z), nm)
      if (length(bad)) .fdb_stop("Unknown name in control$%s: %s.",
                                 label, paste(bad, collapse = ", "))
      ans <- stats::setNames(rep(default, p), nm)
      ans[names(z)] <- z
      return(ans)
    }
    if (length(z) == 1L) z <- rep(z, p)
    if (length(z) != p) {
      .fdb_stop("control$%s must have length 1 or length(start).", label)
    }
    stats::setNames(z, nm)
  }
  lo <- expand(lower, -Inf, "lower")
  hi <- expand(upper, Inf, "upper")
  if (any(lo >= hi)) .fdb_stop("Every custom lower bound must be below its upper bound.")
  if (any(start <= lo | start >= hi)) {
    .fdb_stop("Every custom starting value must lie strictly inside its bounds.")
  }
  list(lower = lo, upper = hi)
}

.fdb_to_unconstrained <- function(theta, lower, upper) {
  ans <- numeric(length(theta))
  for (j in seq_along(theta)) {
    if (is.finite(lower[j]) && is.finite(upper[j])) {
      ans[j] <- stats::qlogis((theta[j] - lower[j]) /
                               (upper[j] - lower[j]))
    } else if (is.finite(lower[j])) {
      ans[j] <- log(theta[j] - lower[j])
    } else if (is.finite(upper[j])) {
      ans[j] <- log(upper[j] - theta[j])
    } else {
      ans[j] <- theta[j]
    }
  }
  names(ans) <- names(theta)
  ans
}

.fdb_from_unconstrained <- function(u, lower, upper, jacobian = FALSE) {
  theta <- numeric(length(u))
  log_jac <- 0
  for (j in seq_along(u)) {
    if (is.finite(lower[j]) && is.finite(upper[j])) {
      p <- stats::plogis(u[j])
      theta[j] <- lower[j] + (upper[j] - lower[j]) * p
      log_jac <- log_jac + log(upper[j] - lower[j]) +
        stats::plogis(u[j], log.p = TRUE) +
        stats::plogis(u[j], lower.tail = FALSE, log.p = TRUE)
    } else if (is.finite(lower[j])) {
      theta[j] <- lower[j] + exp(u[j])
      log_jac <- log_jac + u[j]
    } else if (is.finite(upper[j])) {
      theta[j] <- upper[j] - exp(u[j])
      log_jac <- log_jac + u[j]
    } else {
      theta[j] <- u[j]
    }
  }
  names(theta) <- names(u)
  if (jacobian) list(theta = theta, log_jacobian = log_jac) else theta
}

.fdb_resolve_log_mode <- function(fun, requested, label) {
  fml <- names(formals(fun))
  explicit <- "log" %in% fml
  if (is.null(requested)) return(explicit)
  if (isTRUE(requested) && !explicit && !"..." %in% fml) {
    .fdb_stop(
      paste0(
        "control$%s is TRUE, but the custom %s accepts neither an explicit ",
        "'log' argument nor '...'."
      ),
      if (label == "density") "density_is_log" else "prior_is_log",
      label
    )
  }
  isTRUE(requested)
}

.fdb_valid_log_values <- function(ans, expected_length, label) {
  if (!is.numeric(ans) || length(ans) != expected_length ||
      anyNA(ans) || any(is.nan(ans)) || any(ans == Inf)) {
    .fdb_stop(
      paste0(
        "The custom %s must return %s numeric log-density value%s; ",
        "-Inf is allowed outside the support, but +Inf, NA, and NaN are not."
      ),
      label,
      if (expected_length == 1L) "one" else expected_length,
      if (expected_length == 1L) "" else "s"
    )
  }
  ans
}

.fdb_call_density_pointwise <- function(fun, x, theta, fixed, dots,
                                        log_mode) {
  fml <- names(formals(fun))
  args <- c(list(x), as.list(theta), fixed, dots)
  if (log_mode) {
    ans <- do.call(fun, c(args, list(log = TRUE)))
    return(.fdb_valid_log_values(ans, length(x), "density"))
  }
  ordinary_args <- if ("log" %in% fml) {
    c(args, list(log = FALSE))
  } else {
    args
  }
  dens <- do.call(fun, ordinary_args)
  if (!is.numeric(dens) || length(dens) != length(x) ||
      anyNA(dens) || any(is.nan(dens)) || any(!is.finite(dens)) ||
      any(dens < 0)) {
    .fdb_stop("The custom density must return one nonnegative finite density per observation.")
  }
  log(dens)
}

.fdb_call_density <- function(fun, x, theta, fixed, dots, log_mode) {
  sum(.fdb_call_density_pointwise(
    fun, x, theta, fixed, dots, log_mode = log_mode
  ))
}

.fdb_call_prior <- function(fun, theta, log_mode, vector_style) {
  fml <- names(formals(fun))
  if (vector_style) {
    args <- list(theta)
  } else {
    args <- as.list(theta)
  }
  if (log_mode) {
    ans <- do.call(fun, c(args, list(log = TRUE)))
  } else {
    ordinary_args <- if ("log" %in% fml) {
      c(args, list(log = FALSE))
    } else {
      args
    }
    ans <- do.call(fun, ordinary_args)
    if (length(ans) == 1L && is.numeric(ans) && !is.na(ans) &&
        !is.nan(ans) && is.finite(ans) && ans > 0) {
      ans <- log(ans)
    } else if (length(ans) == 1L && is.numeric(ans) &&
               !is.na(ans) && !is.nan(ans) && is.finite(ans) && ans == 0) {
      ans <- -Inf
    } else {
      .fdb_stop("A custom prior without a 'log' argument must return one nonnegative density.")
    }
  }
  .fdb_valid_log_values(ans, 1L, "prior")
}

.fdb_log_contract_equal <- function(log_mode, ordinary_mode,
                                    tolerance = 1e-8) {
  same_infinity <- is.infinite(log_mode) & is.infinite(ordinary_mode) &
    sign(log_mode) == sign(ordinary_mode)
  finite <- is.finite(log_mode) & is.finite(ordinary_mode)
  close <- rep(FALSE, length(log_mode))
  close[finite] <- abs(log_mode[finite] - ordinary_mode[finite]) <=
    tolerance * (1 + abs(ordinary_mode[finite]))
  all(same_infinity | close)
}

.fdb_build_builtin <- function(x, model, prior, fixed, start = NULL) {
  n <- length(x)
  S <- sum(x)
  sum_log_x <- if (all(x > 0)) sum(log(x)) else NA_real_
  m <- max(tabulate(match(x, unique(x))))

  reject_mdi <- function(model_label) {
    .fdb_stop(
      "The MDI prior for the %s model is disabled because its posterior is improper for every finite sample.",
      model_label
    )
  }
  require_prior <- function(allowed, model_label) {
    if (!prior %in% allowed) {
      .fdb_stop("Prior '%s' is not available for %s. Available choice%s: %s.",
                prior, model_label, if (length(allowed) == 1L) " is" else "s are",
                paste(allowed, collapse = ", "))
    }
  }
  initialization_state <- new.env(parent = emptyenv())
  initialization_state$value <- list(
    source = "not applicable",
    method = "independent exact posterior simulation",
    automatic = NULL,
    center = NULL,
    supplied = NULL,
    sampled_parameters = character()
  )
  resolve_start <- function(defaults, method, positive = character(),
                            probability = character(),
                            sampled_parameters = names(defaults)) {
    automatic <- defaults
    supplied <- NULL
    if (!is.null(start)) {
      supplied <- if (is.list(start)) unlist(start, use.names = TRUE) else start
    }
    if (is.null(supplied)) {
      initialization_state$value <- list(
        source = "automatic",
        method = method,
        automatic = automatic,
        center = defaults,
        supplied = NULL,
        sampled_parameters = sampled_parameters
      )
      return(defaults)
    }
    if (!is.numeric(supplied) || any(!is.finite(supplied))) {
      .fdb_stop("'start' must contain finite numeric values.")
    }
    if (is.null(names(supplied))) {
      if (length(supplied) != length(defaults)) {
        .fdb_stop("Unnamed 'start' must have length %d for this model.",
                  length(defaults))
      }
      names(supplied) <- names(defaults)
    }
    if (any(!nzchar(names(supplied))) || anyDuplicated(names(supplied))) {
      .fdb_stop("Named starting values must have unique, nonempty names.")
    }
    bad <- setdiff(names(supplied), names(defaults))
    if (length(bad)) {
      .fdb_stop("Unknown starting parameter%s: %s.",
                if (length(bad) > 1L) "s" else "",
                paste(bad, collapse = ", "))
    }
    defaults[names(supplied)] <- supplied
    if (length(positive) && any(defaults[positive] <= 0)) {
      .fdb_stop("Positive starting value%s required for: %s.",
                if (length(positive) > 1L) "s are" else " is",
                paste(positive, collapse = ", "))
    }
    if (length(probability) &&
        any(defaults[probability] <= 0 | defaults[probability] >= 1)) {
      .fdb_stop("Starting value%s must lie in (0,1): %s.",
                if (length(probability) > 1L) "s" else "",
                 paste(probability, collapse = ", "))
    }
    initialization_state$value <- list(
      source = if (setequal(names(supplied), names(defaults)))
        "user-supplied" else "automatic with user overrides",
      method = method,
      automatic = automatic,
      center = defaults,
      supplied = supplied,
      sampled_parameters = sampled_parameters
    )
    defaults
  }
  make_mh <- function(logpost, init, transform, params, label) {
    function(iter, warmup, thin, chains, control) {
      ans <- .fdb_amwg(logpost, init, iter, warmup, thin, chains, control)
      list(
        chains = .fdb_transform_matrix(ans$chains, transform, params),
        acceptance = ans$acceptance,
        acceptance_warmup = ans$acceptance_warmup,
        acceptance_all = ans$acceptance_all,
        independent = FALSE,
        engine = label,
        initialization = initialization_state$value
      )
    }
  }
  make_slice <- function(logpost, init, transform, params, label) {
    function(iter, warmup, thin, chains, control) {
      raw <- .fdb_slice_chains(logpost, init, iter, warmup, thin,
                               chains, control)
      out <- lapply(raw, function(z) {
        values <- vapply(z, transform, numeric(length(params)))
        ans <- if (length(params) == 1L) {
          matrix(values, ncol = 1L)
        } else {
          t(values)
        }
        colnames(ans) <- params
        ans
      })
      list(chains = out, acceptance = NULL, independent = FALSE,
           acceptance_warmup = NULL, acceptance_all = NULL,
           engine = label, initialization = initialization_state$value)
    }
  }
  make_direct <- function(draw_chain, label) {
    function(iter, warmup, thin, chains, control) {
      n_save <- floor((iter - warmup) / thin)
      list(
        chains = .fdb_direct_chains(draw_chain, chains, n_save),
        acceptance = NULL,
        acceptance_warmup = NULL,
        acceptance_all = NULL,
        independent = TRUE,
        engine = label,
        initialization = initialization_state$value
      )
    }
  }

  if (model == "beta") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("Beta")
    require_prior(c("jeffreys", "reference"), "Beta")
    if (n < 2L || any(x <= 0 | x >= 1) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Beta with Jeffreys/reference prior requires n >= 2, 0 < x[i] < 1, and a nonconstant sample."
      )
    }
    mu <- mean(x)
    v <- .fdb_second_central_moment(x, mu)
    common <- mu * (1 - mu) / v - 1
    sv <- resolve_start(
      c(shape1 = max(0.1, mu * common),
        shape2 = max(0.1, (1 - mu) * common)),
      method = "Beta method of moments",
      positive = c("shape1", "shape2")
    )
    beta_mean <- sv["shape1"] / sum(sv)
    concentration <- sum(sv)
    init <- c(logit_mean = stats::qlogis(beta_mean),
              log_concentration = log(concentration))
    logpost <- function(u) {
      beta_mean <- stats::plogis(u[1])
      concentration <- exp(u[2])
      a <- beta_mean * concentration
      b <- (1 - beta_mean) * concentration
      if (!is.finite(a + b) || a <= 0 || b <= 0) return(-Inf)
      ia <- trigamma(a) - trigamma(a + b)
      ib <- trigamma(b) - trigamma(a + b)
      iab <- -trigamma(a + b)
      determinant <- ia * ib - iab^2
      if (!is.finite(determinant) || determinant <= 0) return(-Inf)
      sum(stats::dbeta(x, a, b, log = TRUE)) +
        0.5 * log(determinant) + 2 * u[2] +
        log(beta_mean) + log1p(-beta_mean)
    }
    transform <- function(u) {
      beta_mean <- stats::plogis(u[1])
      concentration <- exp(u[2])
      c(shape1 = beta_mean * concentration,
        shape2 = (1 - beta_mean) * concentration)
    }
    sampler <- make_mh(logpost, init, transform,
                       c("shape1", "shape2"), "adaptive Metropolis")
    loglik <- function(theta) stats::dbeta(x, theta["shape1"],
                                           theta["shape2"], log = TRUE)
    rng <- function(theta, size) stats::rbeta(size, theta["shape1"],
                                              theta["shape2"])
    return(list(
      sampler = sampler, parameters = c("shape1", "shape2"),
      prior_label = if (prior == "reference") "reference (regular block)" else "joint Jeffreys",
      prior_kernel = "sqrt(det(I(shape1, shape2)))",
      propriety = "proper: n >= 2, observations in (0,1), nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "cauchy") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "Cauchy")
    a <- if (prior == "jeffreys") 2 else 1
    proper <- if (a == 2) 2 * m < n else 2 * m < n + 1
    if (!proper) {
      .fdb_stop(
        "The Cauchy posterior is improper for this sample/prior. The largest multiplicity is m = %d; %s requires %s.",
        m, prior, if (a == 2) "2m < n" else "2m < n + 1"
      )
    }
    classical <- .fdb_cauchy_quantile_start(x)
    sv <- resolve_start(classical$values,
                        method = classical$method,
                        positive = "scale")
    init <- c(location = unname(sv["location"]),
              log_scale = log(unname(sv["scale"])))
    logpost <- function(u) {
      scale <- exp(u[2])
      if (!is.finite(scale) || scale <= 0) return(-Inf)
      sum(stats::dcauchy(x, u[1], scale, log = TRUE)) -
        a * log(scale) + u[2]
    }
    transform <- function(u) c(location = u[1], scale = exp(u[2]))
    sampler <- make_mh(logpost, init, transform,
                       c("location", "scale"), "adaptive Metropolis")
    loglik <- function(theta) stats::dcauchy(x, theta["location"],
                                             theta["scale"], log = TRUE)
    rng <- function(theta, size) stats::rcauchy(size, theta["location"],
                                                theta["scale"])
    return(list(
      sampler = sampler, parameters = c("location", "scale"),
      prior_label = if (a == 2) "joint Jeffreys" else if (prior == "mdi") "MDI" else "reference",
      prior_kernel = sprintf("scale^(-%d)", a),
      propriety = sprintf("proper for this sample: m = %d and %s", m,
                          if (a == 2) "2m < n" else "2m < n + 1"),
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "chi-squared") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("chi-squared")
    require_prior(c("jeffreys", "reference"), "chi-squared")
    if (any(x <= 0)) {
      .fdb_stop("Chi-squared data must be strictly positive.")
    }
    sv <- resolve_start(c(df = mean(x)),
                        method = "chi-squared method of moments",
                        positive = "df")
    init <- log(sv["df"])
    logpost <- function(u) {
      eta <- u
      df <- exp(eta)
      if (!is.finite(df) || df <= 0) return(-Inf)
      sum(stats::dchisq(x, df, log = TRUE)) +
        0.5 * log(trigamma(df / 2)) + eta
    }
    sampler <- make_slice(logpost, init,
                          function(z) c(df = exp(z)), "df",
                          "univariate slice sampling")
    loglik <- function(theta) stats::dchisq(x, theta["df"], log = TRUE)
    rng <- function(theta, size) stats::rchisq(size, theta["df"])
    return(list(
      sampler = sampler, parameters = "df",
      prior_label = "Jeffreys/reference",
      prior_kernel = "sqrt(trigamma(df/2))",
      propriety = "proper: n >= 1 and all observations are positive",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "exponential") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "exponential")
    if (any(x < 0) || S <= 0) {
      .fdb_stop("Exponential inference requires x[i] >= 0 and sum(x) > 0.")
    }
    shape_post <- if (prior == "mdi") n + 2 else n
    draw_chain <- function(n_save) {
      ans <- matrix(.fdb_rgamma_positive(
        n_save, shape = shape_post, rate = S
      ),
                    ncol = 1L)
      colnames(ans) <- "rate"
      ans
    }
    sampler <- make_direct(draw_chain, "exact Gamma posterior")
    loglik <- function(theta) stats::dexp(x, theta["rate"], log = TRUE)
    rng <- function(theta, size) stats::rexp(size, theta["rate"])
    return(list(
      sampler = sampler, parameters = "rate",
      prior_label = if (prior == "mdi") "MDI" else "Jeffreys/reference",
      prior_kernel = if (prior == "mdi") "rate" else "1/rate",
      propriety = "proper: n >= 1 and sum(x) > 0",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "exponential-logarithmic") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(
      c("jeffreys", "reference", "reference-theta", "reference-rate", "mdi"),
      "Exponential-Logarithmic"
    )
    if (any(x <= 0)) {
      .fdb_stop("Exponential-Logarithmic data must be strictly positive.")
    }
    if (prior != "mdi" && n <= 2L) {
      .fdb_stop(
        "Exponential-Logarithmic Jeffreys/reference posteriors require n > 2. The MDI route is proper for n >= 1."
      )
    }
    classical <- .fdb_el_moment_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = "rate",
      probability = "theta"
    )
    init <- c(log_neglog_theta = log(-log(sv["theta"])),
              log_rate = log(sv["rate"]))
    logpost <- function(u) {
      neg_log_theta <- exp(u[1L])
      rate <- exp(u[2L])
      if (!is.finite(neg_log_theta + rate) ||
          neg_log_theta <= 0 || rate <= 0) return(-Inf)
      log_theta <- -neg_log_theta
      theta <- exp(log_theta)
      one_minus <- -expm1(log_theta)
      if (!is.finite(theta) || theta <= 0 ||
          !is.finite(one_minus) || one_minus <= 0) return(-Inf)
      attenuation <- exp(-rate * x)
      product <- one_minus * attenuation
      if (any(!is.finite(product)) || any(product >= 1)) return(-Inf)
      ll <- n * u[2L] + n * log(one_minus) - n * u[1L] -
        rate * S - sum(log1p(-product))
      if (!is.finite(ll)) return(-Inf)
      terms <- .fdb_el_prior_terms(log_theta)
      if (any(!is.finite(terms))) return(-Inf)
      log_prior <- switch(
        prior,
        "jeffreys" = -u[2L] + 0.5 * terms["log_zeta"],
        "mdi" = u[2L] +
          0.5 * (log(one_minus) - log(theta) - u[1L]) +
          exp(terms["log_dilog"]) / log_theta,
        "reference" = -u[2L] + 0.5 * (
          terms["log_zeta"] + u[1L] - terms["log_dilog"]
        ),
        "reference-theta" = -u[2L] + 0.5 * (
          terms["log_zeta"] + u[1L] - terms["log_dilog"]
        ),
        "reference-rate" = -u[2L] +
          0.5 * terms["log_information_theta"]
      )
      # theta = exp{-exp(u1)} and rate = exp(u2).
      ll + log_prior + log_theta + u[1L] + u[2L]
    }
    transform <- function(u) {
      c(theta = exp(-exp(u[1L])), rate = exp(u[2L]))
    }
    sampler <- make_mh(
      logpost, init, transform, c("theta", "rate"), "adaptive Metropolis"
    )
    loglik <- function(theta) {
      probability <- theta["theta"]
      rate <- theta["rate"]
      log_probability <- log(probability)
      one_minus <- -expm1(log_probability)
      log(rate) + log(one_minus) - log(-log_probability) - rate * x -
        log1p(-one_minus * exp(-rate * x))
    }
    rng <- function(theta, size) {
      probability <- theta["theta"]
      rate <- theta["rate"]
      log_probability <- log(probability)
      u <- .fdb_open_probability(stats::runif(size))
      numerator <- -expm1((1 - u) * log_probability)
      denominator <- -expm1(log_probability)
      .fdb_positive_finite(
        -(log(numerator) - log(denominator)) / rate
      )
    }
    labels <- c(
      "jeffreys" = "joint Jeffreys",
      "mdi" = "MDI",
      "reference" = "reference (theta interest)",
      "reference-theta" = "reference (theta interest)",
      "reference-rate" = "reference (rate interest)"
    )
    kernels <- c(
      "jeffreys" = "sqrt(zeta(theta))/rate",
      "mdi" = "rate*sqrt((1-theta)/(-theta*log(theta)))*exp(dilog(theta)/log(theta))",
      "reference" = "sqrt(-zeta(theta)*log(theta)/dilog(theta))/rate",
      "reference-theta" = "sqrt(-zeta(theta)*log(theta)/dilog(theta))/rate",
      "reference-rate" = "sqrt(I_theta_theta(theta))/rate"
    )
    return(list(
      sampler = sampler, parameters = c("theta", "rate"),
      prior_label = unname(labels[prior]),
      prior_kernel = unname(kernels[prior]),
      propriety = if (prior == "mdi")
        "proper: n >= 1 and all observations are positive; all positive moments are finite" else
        "proper: n > 2 and all observations are positive; all positive moments are finite",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "frechet") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") {
      .fdb_stop("An MDI route is not available for Frechet in the certified source registry.")
    }
    require_prior(c("jeffreys", "reference"), "Frechet")
    if (n < 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Frechet Jeffreys/reference inference requires n >= 2, positive observations, and a nonconstant sample."
      )
    }
    lx <- log(x)
    classical <- .fdb_frechet_lmoment_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = c("shape", "scale"),
      sampled_parameters = "shape"
    )
    init_shape <- sv["shape"]
    logpost <- function(u) {
      shape <- exp(u)
      if (!is.finite(shape) || shape <= 0) return(-Inf)
      log_sum_inverse <- .fdb_logsumexp(-shape * lx)
      # Integrating scale from
      # L(shape, scale) / (shape * scale) leaves shape^(n - 1).
      (n - 1) * log(shape) - shape * sum(lx) -
        n * log_sum_inverse + u
    }
    sampler <- function(iter, warmup, thin, chains, control) {
      raw <- .fdb_slice_chains(
        logpost, log(init_shape), iter, warmup, thin, chains, control
      )
      out <- lapply(raw, function(eta) {
        shape <- exp(eta)
        log_sum_inverse <- vapply(
          shape,
          function(a) .fdb_logsumexp(-a * lx),
          numeric(1)
        )
        log_scale <- log(.fdb_rgamma_positive(length(shape), shape = n)) -
          log_sum_inverse
        cbind(shape = shape, scale = .fdb_exp_positive(log_scale))
      })
      list(
        chains = out, acceptance = NULL, independent = FALSE,
        engine = "marginal slice + exact conditional Gamma",
        initialization = initialization_state$value
      )
    }
    loglik <- function(theta) {
      shape <- theta["shape"]
      log_scale <- log(theta["scale"])
      exponent <- log_scale - shape * lx
      base <- log_scale + log(shape) - (shape + 1) * lx
      answer <- rep(-Inf, length(x))
      representable <- is.finite(exponent) & is.finite(base) &
        exponent <= log(.Machine$double.xmax)
      answer[representable] <- base[representable] -
        exp(exponent[representable])
      answer[is.nan(answer) | answer == Inf] <- -Inf
      answer
    }
    rng <- function(theta, size) {
      log_x <- (log(theta["scale"]) -
                  log(.fdb_positive_finite(stats::rexp(size)))) /
        theta["shape"]
      .fdb_exp_positive(log_x)
    }
    return(list(
      sampler = sampler, parameters = c("shape", "scale"),
      prior_label = "Jeffreys/reference",
      prior_kernel = "1/(shape*scale)",
      propriety = "proper: n >= 2, positive nonconstant sample; scale moments are data dependent",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "gumbel") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "Gumbel")
    if (n < 2L || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Gumbel objective inference requires n >= 2 and a nonconstant sample."
      )
    }
    location <- mean(x)
    scale <- sqrt(6 * .fdb_second_central_moment(x, location)) / pi
    location <- location - 0.5772156649015329 * scale
    sv <- resolve_start(
      c(location = location, scale = scale),
      method = "Gumbel method of moments (closed form)",
      positive = "scale"
    )
    init <- c(location = unname(sv["location"]),
              log_scale = log(unname(sv["scale"])))
    prior_power <- if (prior == "jeffreys") 2 else 1
    logpost <- function(u) {
      scale <- exp(u[2L])
      if (!is.finite(scale) || scale <= 0) return(-Inf)
      z <- (x - u[1L]) / scale
      if (any(!is.finite(z)) ||
          any(-z > log(.Machine$double.xmax))) return(-Inf)
      ll <- -n * u[2L] - sum(z) - sum(exp(-z))
      ll - prior_power * u[2L] + u[2L]
    }
    transform <- function(u) c(location = u[1L], scale = exp(u[2L]))
    sampler <- make_mh(
      logpost, init, transform, c("location", "scale"),
      "adaptive Metropolis"
    )
    loglik <- function(theta) {
      z <- (x - theta["location"]) / theta["scale"]
      -log(theta["scale"]) - z - exp(-z)
    }
    rng <- function(theta, size) {
      theta["location"] - theta["scale"] *
        log(-log(.fdb_open_probability(stats::runif(size))))
    }
    return(list(
      sampler = sampler, parameters = c("location", "scale"),
      prior_label = if (prior == "jeffreys") "joint Jeffreys" else
        if (prior == "mdi") "MDI" else "reference",
      prior_kernel = if (prior == "jeffreys") "scale^(-2)" else "scale^(-1)",
      propriety = "proper: n >= 2 and nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "gamma") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("Gamma")
    if (prior == "reference") {
      .fdb_stop(
        "Gamma reference priors depend on the parameter of interest; use 'reference-shape' or 'reference-rate'."
      )
    }
    require_prior(c("jeffreys", "first-rule",
                    "reference-shape", "reference-rate"), "Gamma")
    if (n < 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Gamma objective posteriors require n >= 2, positive observations, and a nonconstant sample."
      )
    }
    mean_x <- mean(x)
    var_x <- .fdb_second_central_moment(x, mean_x)
    init_shape <- mean_x^2 / var_x
    sv <- resolve_start(c(shape = init_shape, rate = init_shape / mean_x),
                        method = "Gamma method of moments",
                        positive = c("shape", "rate"),
                        sampled_parameters = "shape")
    init_shape <- sv["shape"]
    log_h <- switch(
      prior,
      "jeffreys" = function(a) 0.5 * log(.fdb_gamma_joint_term(a)),
      "first-rule" = function(a) -log(a),
      "reference-shape" = function(a) {
        0.5 * (log(.fdb_gamma_joint_term(a)) - log(a))
      },
      "reference-rate" = function(a) 0.5 * log(trigamma(a))
    )
    logpost_eta <- function(eta) {
      shape <- exp(eta)
      if (!is.finite(shape) || shape <= 0) return(-Inf)
      log_h(shape) + lgamma(n * shape) - n * lgamma(shape) +
        (shape - 1) * sum_log_x - n * shape * log(S) + eta
    }
    sampler <- function(iter, warmup, thin, chains, control) {
      raw <- .fdb_slice_chains(logpost_eta, log(init_shape), iter, warmup,
                               thin, chains, control)
      out <- lapply(raw, function(eta) {
        shape <- exp(eta)
        rate <- .fdb_rgamma_positive(
          length(shape), shape = n * shape, rate = S
        )
        cbind(shape = shape, rate = rate)
      })
      list(chains = out, acceptance = NULL, independent = FALSE,
           engine = "marginal slice + exact conditional Gamma",
           initialization = initialization_state$value)
    }
    loglik <- function(theta) stats::dgamma(x, shape = theta["shape"],
                                            rate = theta["rate"], log = TRUE)
    rng <- function(theta, size) .fdb_rgamma_positive(
      size, shape = theta["shape"], rate = theta["rate"]
    )
    labels <- c(
      "jeffreys" = "joint Jeffreys",
      "first-rule" = "Jeffreys' first-rule prior",
      "reference-shape" = "reference (shape interest)",
      "reference-rate" = "reference (rate interest)"
    )
    kernels <- c(
      "jeffreys" = "sqrt(shape*trigamma(shape)-1)/rate",
      "first-rule" = "1/(shape*rate)",
      "reference-shape" = "sqrt((shape*trigamma(shape)-1)/shape)/rate",
      "reference-rate" = "sqrt(trigamma(shape))/rate"
    )
    return(list(
      sampler = sampler, parameters = c("shape", "rate"),
      prior_label = unname(labels[prior]),
      prior_kernel = unname(kernels[prior]),
      propriety = "proper: n >= 2, positive nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "geometric") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "geometric")
    if (!.fdb_discrete(x, 0)) {
      .fdb_stop("Geometric data must be nonnegative integers (R convention: 0,1,...).")
    }
    x <- round(x)
    S <- sum(x)
    if (prior != "mdi") {
      draw_chain <- function(n_save) {
        probability <- .fdb_open_probability(
          stats::rbeta(n_save, n, S + 0.5)
        )
        ans <- matrix(probability, ncol = 1L)
        colnames(ans) <- "prob"
        ans
      }
      sampler <- make_direct(draw_chain, "exact Beta posterior")
    } else {
      prob_mom <- 1 / (1 + mean(x))
      geometric_method <- "geometric method of moments"
      if (prob_mom >= 1) {
        prob_mom <- (n + 0.5) / (n + 1)
        geometric_method <- paste0(
          geometric_method, "; interior continuity correction"
        )
      }
      sv <- resolve_start(c(prob = prob_mom),
                          method = geometric_method,
                          probability = "prob")
      init <- stats::qlogis(sv["prob"])
      logpost <- function(u) {
        eta <- u
        p <- stats::plogis(eta)
        if (!is.finite(p) || p <= 0 || p >= 1) return(-Inf)
        log_p <- log(p)
        log_q <- log1p(-p)
        n * log_p + S * log_q +
          log_p + ((1 - p) / p) * log_q +
          log_p + log_q
      }
      sampler <- make_slice(logpost, init,
                            function(z) c(prob = stats::plogis(z)),
                            "prob", "univariate slice sampling")
    }
    loglik <- function(theta) stats::dgeom(x, theta["prob"], log = TRUE)
    rng <- function(theta, size) stats::rgeom(size, theta["prob"])
    return(list(
      sampler = sampler, parameters = "prob",
      prior_label = if (prior == "mdi") "MDI" else "Jeffreys/reference",
      prior_kernel = if (prior == "mdi")
        "prob*(1-prob)^((1-prob)/prob)" else
        "1/(prob*sqrt(1-prob))",
      propriety = "proper for every n >= 1",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "lognormal") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("lognormal")
    require_prior(c("jeffreys", "reference"), "lognormal")
    if (any(x <= 0)) .fdb_stop("Lognormal data must be strictly positive.")
    y <- log(x)
    if (n < 2L || !.fdb_nonconstant(y)) {
      .fdb_stop("Lognormal Jeffreys/reference inference requires n >= 2 and nonconstant log-data.")
    }
    a <- if (prior == "jeffreys") 2 else 1
    q <- sum((y - mean(y))^2)
    shape_s2 <- (n + a - 2) / 2
    draw_chain <- function(n_save) {
      log_precision <- log(.fdb_rgamma_positive(
        n_save, shape = shape_s2, rate = q / 2
      ))
      s2 <- .fdb_exp_positive(-log_precision)
      meanlog <- .fdb_finite_magnitude(
        stats::rnorm(n_save, mean(y), sqrt(s2 / n))
      )
      cbind(meanlog = meanlog, sdlog = sqrt(s2))
    }
    sampler <- make_direct(draw_chain, "exact Normal-inverse-Gamma posterior")
    loglik <- function(theta) stats::dlnorm(x, theta["meanlog"],
                                            theta["sdlog"], log = TRUE)
    rng <- function(theta, size) stats::rlnorm(size, theta["meanlog"],
                                               theta["sdlog"])
    return(list(
      sampler = sampler, parameters = c("meanlog", "sdlog"),
      prior_label = if (a == 2) "joint Jeffreys" else "reference",
      prior_kernel = sprintf("sdlog^(-%d)", a),
      propriety = "proper: n >= 2 and nonconstant log-data",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "logistic") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "logistic")
    if (n < 2L || !.fdb_nonconstant(x)) {
      .fdb_stop("Logistic objective inference requires n >= 2 and a nonconstant sample.")
    }
    a <- if (prior == "jeffreys") 2 else 1
    logistic_location <- mean(x)
    logistic_scale <- sqrt(
      3 * .fdb_second_central_moment(x, logistic_location)
    ) / pi
    sv <- resolve_start(
      c(location = logistic_location, scale = logistic_scale),
      method = "logistic method of moments",
      positive = "scale"
    )
    init <- c(location = unname(sv["location"]),
              log_scale = log(unname(sv["scale"])))
    logpost <- function(u) {
      scale <- exp(u[2])
      if (!is.finite(scale) || scale <= 0) return(-Inf)
      sum(stats::dlogis(x, u[1], scale, log = TRUE)) -
        a * log(scale) + u[2]
    }
    transform <- function(u) c(location = u[1], scale = exp(u[2]))
    sampler <- make_mh(logpost, init, transform,
                       c("location", "scale"), "adaptive Metropolis")
    loglik <- function(theta) stats::dlogis(x, theta["location"],
                                            theta["scale"], log = TRUE)
    rng <- function(theta, size) stats::rlogis(size, theta["location"],
                                               theta["scale"])
    return(list(
      sampler = sampler, parameters = c("location", "scale"),
      prior_label = if (a == 2) "joint Jeffreys" else if (prior == "mdi") "MDI" else "reference",
      prior_kernel = sprintf("scale^(-%d)", a),
      propriety = "proper: n >= 2 and nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "lomax") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "reference") {
      .fdb_stop(
        "The independent Jeffreys/reference posterior for Lomax is improper for every finite sample. Use 'jeffreys'."
      )
    }
    if (prior == "mdi") {
      .fdb_stop("An MDI route is not available for Lomax in the certified source registry.")
    }
    require_prior("jeffreys", "Lomax")
    if (any(x <= 0)) {
      .fdb_stop(
        "Lomax Jeffreys inference requires strictly positive observations; an exact zero makes this improper-prior route nonregular."
      )
    }
    classical <- .fdb_lomax_lmoment_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = c("shape", "scale")
    )
    # The likelihood is nearly ridge-shaped in (log shape, log scale).  The
    # ratio scale/shape is much better identified and substantially reduces
    # posterior correlation without changing the public parameterization.
    init <- c(log_shape = log(sv["shape"]),
              log_scale_per_shape = log(sv["scale"] / sv["shape"]))
    logpost <- function(u) {
      shape <- exp(u[1L])
      log_scale <- u[1L] + u[2L]
      scale <- exp(log_scale)
      if (!is.finite(shape + scale) || shape <= 0 || scale <= 0) {
        return(-Inf)
      }
      log_ratio <- log(x) - log_scale
      log_survival_base <- .fdb_softplus(log_ratio)
      ll <- n * u[1L] - n * log_scale -
        (shape + 1) * sum(log_survival_base)
      log_prior <- -log_scale - 0.5 * u[1L] - log1p(shape) -
        0.5 * log(shape + 2)
      # Jacobian for shape=exp(u1), scale=exp(u1+u2).
      ll + log_prior + u[1L] + log_scale
    }
    transform <- function(u) {
      c(shape = exp(u[1L]), scale = exp(u[1L] + u[2L]))
    }
    sampler <- make_mh(
      logpost, init, transform, c("shape", "scale"),
      "adaptive Metropolis"
    )
    loglik <- function(theta) {
      log(theta["shape"]) - log(theta["scale"]) -
        (theta["shape"] + 1) *
        .fdb_softplus(log(x) - log(theta["scale"]))
    }
    rng <- function(theta, size) {
      u <- .fdb_open_probability(stats::runif(size))
      .fdb_positive_finite(
        theta["scale"] * expm1(-log(u) / theta["shape"])
      )
    }
    return(list(
      sampler = sampler, parameters = c("shape", "scale"),
      prior_label = "joint Jeffreys (dependent)",
      prior_kernel = "1/(scale*sqrt(shape)*(shape+1)*sqrt(shape+2))",
      propriety = "proper: n >= 1 and all observations are strictly positive; reference is improper",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "nakagami") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("Nakagami-m")
    require_prior(c("jeffreys", "reference"), "Nakagami-m")
    if (n < 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        paste0(
          "Nakagami-m Jeffreys/reference inference requires n >= 2, ",
          "strictly positive observations, and a nonconstant sample."
        )
      )
    }
    lx <- log(x)
    sum_squares <- sum(x^2)
    classical <- .fdb_nakagami_moment_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = c("shape", "spread"),
      sampled_parameters = "shape"
    )
    init_shape <- sv["shape"]
    log_h <- if (prior == "jeffreys") {
      function(shape) 0.5 * log(.fdb_gamma_joint_term(shape))
    } else {
      function(shape) 0.5 * (
        log(.fdb_gamma_joint_term(shape)) - log(shape)
      )
    }
    logpost <- function(u) {
      shape <- exp(u)
      if (!is.finite(shape) || shape <= 0) return(-Inf)
      log_h(shape) + lgamma(n * shape) - n * lgamma(shape) +
        (2 * shape - 1) * sum(lx) - n * shape * log(sum_squares) + u
    }
    sampler <- function(iter, warmup, thin, chains, control) {
      raw <- .fdb_slice_chains(
        logpost, log(init_shape), iter, warmup, thin, chains, control
      )
      out <- lapply(raw, function(eta) {
        shape <- exp(eta)
        gamma_draw <- .fdb_rgamma_positive(length(shape), shape = n * shape)
        log_spread <- log(shape) + log(sum_squares) - log(gamma_draw)
        cbind(shape = shape, spread = .fdb_exp_positive(log_spread))
      })
      list(
        chains = out, acceptance = NULL, independent = FALSE,
        engine = "marginal slice + exact conditional inverse-Gamma",
        initialization = initialization_state$value
      )
    }
    loglik <- function(theta) {
      shape <- theta["shape"]
      spread <- theta["spread"]
      log(2) - lgamma(shape) + shape * (log(shape) - log(spread)) +
        (2 * shape - 1) * lx - shape * x^2 / spread
    }
    rng <- function(theta, size) {
      sqrt(.fdb_rgamma_positive(
        size, shape = theta["shape"],
        rate = theta["shape"] / theta["spread"]
      ))
    }
    return(list(
      sampler = sampler, parameters = c("shape", "spread"),
      prior_label = if (prior == "jeffreys") "joint Jeffreys" else
        "overall reference",
      prior_kernel = if (prior == "jeffreys")
        "sqrt(shape*trigamma(shape)-1)/spread" else
        "sqrt((shape*trigamma(shape)-1)/shape)/spread",
      propriety = paste0(
        "proper: n >= 2 and a positive nonconstant sample; ",
        "MDI is improper"
      ),
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "negative binomial") {
    fixed <- .fdb_check_fixed(fixed, "size")
    if (is.null(fixed$size)) {
      if (prior == "mdi") {
        .fdb_stop("The full negative-binomial MDI posterior is improper. Supply a fixed positive size.")
      }
      .fdb_stop(
        "The exact full-model %s prior is not in the certified registry. Supply fixed = list(size = ...) for the audited one-parameter model.",
        prior
      )
    }
    size <- fixed$size
    if (length(size) != 1L || !is.numeric(size) || !is.finite(size) || size <= 0) {
      .fdb_stop("fixed$size must be one positive finite number.")
    }
    require_prior(c("jeffreys", "reference", "mdi"),
                  "negative binomial with fixed size")
    if (!.fdb_discrete(x, 0)) {
      .fdb_stop("Negative-binomial data must be nonnegative integers.")
    }
    x <- round(x)
    S <- sum(x)
    if (prior != "mdi") {
      draw_chain <- function(n_save) {
        p <- stats::rbeta(n_save, n * size, S + 0.5)
        log_mu <- log(size) + log1p(-p) - log(p)
        mu <- .fdb_exp_positive(log_mu)
        ans <- matrix(mu, ncol = 1L)
        colnames(ans) <- "mu"
        ans
      }
      sampler <- make_direct(draw_chain, "exact transformed-Beta posterior")
    } else {
      mu_mom <- mean(x)
      nb_method <- "negative-binomial method of moments (fixed size)"
      if (mu_mom <= 0) {
        mu_mom <- 0.5 / n
        nb_method <- paste0(nb_method, "; interior continuity correction")
      }
      sv <- resolve_start(c(mu = mu_mom), method = nb_method,
                          positive = "mu")
      init <- log(sv["mu"])
      logpost_nbinom_mdi <- function(u, entropy_control) {
        eta <- u
        mu <- exp(eta)
        if (!is.finite(mu) || mu <= 0) return(-Inf)
        sum(stats::dnbinom(x, size = size, mu = mu, log = TRUE)) -
          .fdb_nbinom_entropy(size, mu, entropy_control) + eta
      }
      sampler <- function(iter, warmup, thin, chains, control) {
        current_logpost <- function(u) logpost_nbinom_mdi(u, control)
        raw <- .fdb_slice_chains(current_logpost, init, iter, warmup, thin,
                                 chains, control)
        out <- lapply(raw, function(z) {
          ans <- matrix(exp(z), ncol = 1L)
          colnames(ans) <- "mu"
          ans
        })
        list(chains = out, acceptance = NULL, independent = FALSE,
             engine = "univariate slice sampling with entropy evaluation",
             initialization = initialization_state$value)
      }
    }
    loglik <- function(theta) stats::dnbinom(x, size = size,
                                             mu = theta["mu"], log = TRUE)
    nb_size <- size
    rng <- function(theta, size) stats::rnbinom(size, size = nb_size,
                                                  mu = theta["mu"])
    return(list(
      sampler = sampler, parameters = "mu",
      prior_label = if (prior == "mdi") "MDI (fixed size)" else "Jeffreys/reference (fixed size)",
      prior_kernel = if (prior == "mdi") "exp(-H_NB(size, mu))" else
        "1/sqrt(mu*(size+mu))",
      propriety = "proper for every n >= 1 with fixed positive size",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "normal") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "normal")
    if (n < 2L || !.fdb_nonconstant(x)) {
      .fdb_stop("Normal objective inference requires n >= 2 and a nonconstant sample.")
    }
    a <- if (prior == "jeffreys") 2 else 1
    q <- sum((x - mean(x))^2)
    shape_s2 <- (n + a - 2) / 2
    draw_chain <- function(n_save) {
      log_precision <- log(.fdb_rgamma_positive(
        n_save, shape = shape_s2, rate = q / 2
      ))
      s2 <- .fdb_exp_positive(-log_precision)
      mu <- .fdb_finite_magnitude(
        stats::rnorm(n_save, mean(x), sqrt(s2 / n))
      )
      cbind(mean = mu, sd = sqrt(s2))
    }
    sampler <- make_direct(draw_chain, "exact Normal-inverse-Gamma posterior")
    loglik <- function(theta) stats::dnorm(x, theta["mean"],
                                           theta["sd"], log = TRUE)
    rng <- function(theta, size) stats::rnorm(size, theta["mean"],
                                              theta["sd"])
    return(list(
      sampler = sampler, parameters = c("mean", "sd"),
      prior_label = if (a == 2) "joint Jeffreys" else if (prior == "mdi") "MDI" else "reference",
      prior_kernel = sprintf("sd^(-%d)", a),
      propriety = "proper: n >= 2 and nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "Poisson") {
    fixed <- .fdb_check_fixed(fixed, character())
    require_prior(c("jeffreys", "reference", "mdi"), "Poisson")
    if (!.fdb_discrete(x, 0)) {
      .fdb_stop("Poisson data must be nonnegative integers.")
    }
    x <- round(x)
    S <- sum(x)
    if (prior != "mdi") {
      draw_chain <- function(n_save) {
        ans <- matrix(.fdb_rgamma_positive(
          n_save, shape = S + 0.5, rate = n
        ),
                      ncol = 1L)
        colnames(ans) <- "lambda"
        ans
      }
      sampler <- make_direct(draw_chain, "exact Gamma posterior")
    } else {
      lambda_mom <- mean(x)
      poisson_method <- "Poisson method of moments"
      if (lambda_mom <= 0) {
        lambda_mom <- 0.5 / n
        poisson_method <- paste0(
          poisson_method, "; interior continuity correction"
        )
      }
      sv <- resolve_start(c(lambda = lambda_mom),
                          method = poisson_method,
                          positive = "lambda")
      init <- log(sv["lambda"])
      logpost_poisson_mdi <- function(u, entropy_control) {
        eta <- u
        lambda <- exp(eta)
        if (!is.finite(lambda) || lambda <= 0) return(-Inf)
        S * eta - n * lambda -
          .fdb_poisson_entropy(lambda, entropy_control) + eta
      }
      sampler <- function(iter, warmup, thin, chains, control) {
        current_logpost <- function(u) logpost_poisson_mdi(u, control)
        raw <- .fdb_slice_chains(current_logpost, init, iter, warmup, thin,
                                 chains, control)
        out <- lapply(raw, function(z) {
          ans <- matrix(exp(z), ncol = 1L)
          colnames(ans) <- "lambda"
          ans
        })
        list(chains = out, acceptance = NULL, independent = FALSE,
             engine = "univariate slice sampling with entropy evaluation",
             initialization = initialization_state$value)
      }
    }
    loglik <- function(theta) stats::dpois(x, theta["lambda"], log = TRUE)
    rng <- function(theta, size) stats::rpois(size, theta["lambda"])
    return(list(
      sampler = sampler, parameters = "lambda",
      prior_label = if (prior == "mdi") "MDI" else "Jeffreys/reference",
      prior_kernel = if (prior == "mdi") "exp(-H_Poisson(lambda))" else
        "lambda^(-1/2)",
      propriety = "proper for every n >= 1, including an all-zero sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "rician") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "reference") {
      .fdb_stop(
        "A reference prior for the Rician model is not available in the certified source registry."
      )
    }
    if (prior == "mdi") {
      .fdb_stop(
        "An MDI route for the Rician model is not available in the certified source registry."
      )
    }
    require_prior("jeffreys", "Rician")
    if (n <= 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Rician Jeffreys inference requires n > 2, positive observations, and a nonconstant sample."
      )
    }
    classical <- .fdb_rician_moment_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = c("noncentrality", "scale")
    )
    init <- c(log_noncentrality = log(sv["noncentrality"]),
              log_scale = log(sv["scale"]))
    log_q <- .fdb_rician_logq_factory()
    pointwise_loglik <- function(log_noncentrality, log_scale) {
      noncentrality <- exp(log_noncentrality)
      scale <- exp(log_scale)
      if (!is.finite(noncentrality + scale) ||
          noncentrality <= 0 || scale <= 0) return(NULL)
      log_argument <- log_noncentrality + log(x) - 2 * log_scale
      if (any(!is.finite(log_argument)) ||
          any(log_argument > log(.Machine$double.xmax))) return(NULL)
      argument <- exp(log_argument)
      scaled_i0 <- besselI(argument, nu = 0, expon.scaled = TRUE)
      standardized_difference <- (x - noncentrality) / scale
      if (any(!is.finite(scaled_i0)) || any(scaled_i0 <= 0) ||
          any(!is.finite(standardized_difference))) return(NULL)
      log(x) - 2 * log_scale + log(scaled_i0) -
        0.5 * standardized_difference^2
    }
    logpost <- function(u) {
      ll <- pointwise_loglik(u[1L], u[2L])
      if (is.null(ll) || any(!is.finite(ll))) return(-Inf)
      log_rho <- 2 * (u[1L] - u[2L])
      log_q_value <- log_q(log_rho)
      if (!is.finite(log_q_value)) return(-Inf)
      sum(ll) + 0.5 * log_q_value + u[1L] - u[2L]
    }
    transform <- function(u) {
      c(noncentrality = exp(u[1L]), scale = exp(u[2L]))
    }
    sampler <- make_mh(
      logpost, init, transform, c("noncentrality", "scale"),
      "adaptive Metropolis with Gauss-Laguerre Jeffreys factor"
    )
    loglik <- function(theta) {
      answer <- pointwise_loglik(
        log(theta["noncentrality"]), log(theta["scale"])
      )
      if (is.null(answer)) rep(-Inf, length(x)) else answer
    }
    rng <- function(theta, size) {
      sqrt(
        stats::rnorm(size, theta["noncentrality"], theta["scale"])^2 +
          stats::rnorm(size, 0, theta["scale"])^2
      )
    }
    return(list(
      sampler = sampler, parameters = c("noncentrality", "scale"),
      prior_label = "joint Jeffreys",
      prior_kernel = "sqrt((rho+1)*Psi(rho)-rho)/scale^2, rho=noncentrality^2/scale^2",
      propriety = "proper: n > 2, positive nonconstant sample; reference and MDI routes are unavailable",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "t") {
    fixed <- .fdb_check_fixed(fixed, "df")
    if (!is.null(fixed$df)) {
      df <- fixed$df
      if (length(df) != 1L || !is.numeric(df) || !is.finite(df) || df <= 0) {
        .fdb_stop("fixed$df must be one positive finite number.")
      }
      require_prior(c("jeffreys", "reference", "mdi"),
                    "Student-t with fixed df")
      a <- if (prior == "jeffreys") 2 else 1
      if (!(n + a > 2 && df * (n - m) - m - a + 2 > 0)) {
        .fdb_stop(
          "The fixed-df Student-t posterior is improper: it requires n+a>2 and df*(n-m)-m-a+2>0 (here n=%d, m=%d, a=%d, df=%g).",
          n, m, a, df
        )
      }
      classical <- .fdb_student_start(x, df = df)
      sv <- resolve_start(classical$values,
                          method = classical$method,
                          positive = "scale")
      init <- c(location = unname(sv["location"]),
                log_scale = log(unname(sv["scale"])))
      logpost <- function(u) {
        scale <- exp(u[2])
        if (!is.finite(scale) || scale <= 0) return(-Inf)
        sum(stats::dt((x - u[1]) / scale, df, log = TRUE) -
              log(scale)) - a * log(scale) + u[2]
      }
      transform <- function(u) c(location = u[1], scale = exp(u[2]))
      sampler <- make_mh(logpost, init, transform,
                         c("location", "scale"), "adaptive Metropolis")
      loglik <- function(theta) stats::dt(
        (x - theta["location"]) / theta["scale"], df, log = TRUE
      ) - log(theta["scale"])
      rng <- function(theta, size) theta["location"] +
        theta["scale"] * stats::rt(size, df)
      return(list(
        sampler = sampler, parameters = c("location", "scale"),
        prior_label = if (a == 2) "joint Jeffreys (fixed df)" else if (prior == "mdi") "MDI (fixed df)" else "reference (fixed df)",
        prior_kernel = sprintf("scale^(-%d)", a),
        propriety = sprintf("proper for this sample: n=%d, m=%d, df=%g", n, m, df),
        loglik = loglik, rng = rng, fixed = fixed
      ))
    }

    if (prior == "jeffreys") {
      .fdb_stop("Joint Jeffreys for Student-t with unknown df has an improper posterior. Use 'independence-jeffreys' or fix df.")
    }
    if (prior == "mdi") {
      .fdb_stop("The MDI posterior for Student-t with unknown df is improper. Fix df to use MDI.")
    }
    if (prior %in% c("reference", "reference-shape", "reference-rate")) {
      .fdb_stop("Unknown-df Student-t reference priors are not yet in the certified full-joint registry. Fix df or use 'independence-jeffreys'.")
    }
    require_prior("independence-jeffreys", "Student-t with unknown df")
    if (n <= 1L || !.fdb_nonconstant(x)) {
      .fdb_stop("Unknown-df Student-t independence Jeffreys requires n > 1 and a nondegenerate sample.")
    }
    classical <- .fdb_student_start(x)
    sv <- resolve_start(classical$values,
                        method = classical$method,
                        positive = c("scale", "df"))
    init <- c(location = unname(sv["location"]),
              log_scale = log(unname(sv["scale"])),
              log_df = log(unname(sv["df"])))
    logpost <- function(u) {
      scale <- exp(u[2]); df <- exp(u[3])
      if (!is.finite(scale + df) || scale <= 0 || df <= 0) return(-Inf)
      B <- .fdb_t_B(df)
      log_prior <- -log(scale) +
        0.5 * (log(df) - log(df + 3) + log(B))
      sum(stats::dt((x - u[1]) / scale, df, log = TRUE) -
            log(scale)) + log_prior + u[2] + u[3]
    }
    transform <- function(u) c(location = u[1], scale = exp(u[2]),
                               df = exp(u[3]))
    sampler <- make_mh(logpost, init, transform,
                       c("location", "scale", "df"),
                       "adaptive Metropolis")
    loglik <- function(theta) stats::dt(
      (x - theta["location"]) / theta["scale"], theta["df"], log = TRUE
    ) - log(theta["scale"])
    rng <- function(theta, size) theta["location"] +
      theta["scale"] * stats::rt(size, theta["df"])
    return(list(
      sampler = sampler, parameters = c("location", "scale", "df"),
      prior_label = "independence Jeffreys",
      prior_kernel = "scale^(-1)*sqrt(df*B(df)/(df+3))",
      propriety = "proper for intercept-only model with n > 1 outside degenerate samples; positive df moments do not exist",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "weibull") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("Weibull")
    require_prior(c("jeffreys", "reference"), "Weibull")
    if (n < 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop("Weibull objective posteriors require n >= 2, positive observations, and a nonconstant sample.")
    }
    lx <- log(x)
    classical <- .fdb_weibull_lmoment_start(x)
    sv <- resolve_start(classical$values,
                        method = classical$method,
                        positive = c("shape", "scale"),
                        sampled_parameters = "shape")
    init_shape <- sv["shape"]
    power <- if (prior == "jeffreys") n - 1 else n - 2
    logpost <- function(u) {
      eta <- u
      shape <- exp(eta)
      if (!is.finite(shape) || shape <= 0) return(-Inf)
      log_A <- .fdb_logsumexp(shape * lx)
      power * log(shape) + (shape - 1) * sum(lx) -
        n * log_A + eta
    }
    sampler <- function(iter, warmup, thin, chains, control) {
      raw <- .fdb_slice_chains(logpost, log(init_shape), iter, warmup,
                               thin, chains, control)
      out <- lapply(raw, function(eta) {
        shape <- exp(eta)
        log_A <- vapply(shape, function(k) .fdb_logsumexp(k * lx),
                        numeric(1))
        log_g <- log(.fdb_rgamma_positive(
          length(shape), shape = n, rate = 1
        ))
        scale <- .fdb_exp_positive((log_A - log_g) / shape)
        cbind(shape = shape, scale = scale)
      })
      list(chains = out, acceptance = NULL, independent = FALSE,
           engine = "marginal slice + exact conditional Gamma transform",
           initialization = initialization_state$value)
    }
    loglik <- function(theta) {
      shape <- theta["shape"]
      log_scale <- log(theta["scale"])
      log_ratio <- lx - log_scale
      powered_log_ratio <- shape * log_ratio
      answer <- rep(-Inf, length(x))
      ordinary <- is.finite(powered_log_ratio) &
        is.finite(log_ratio) &
        powered_log_ratio <= log(.Machine$double.xmax)
      answer[ordinary] <- log(shape) - log_scale +
        powered_log_ratio[ordinary] - log_ratio[ordinary] -
        exp(powered_log_ratio[ordinary])
      answer[is.nan(answer) | answer == Inf] <- -Inf
      answer
    }
    rng <- function(theta, size) stats::rweibull(size, theta["shape"],
                                                 theta["scale"])
    return(list(
      sampler = sampler, parameters = c("shape", "scale"),
      prior_label = if (prior == "jeffreys") "joint Jeffreys" else "reference",
      prior_kernel = if (prior == "jeffreys") "1/scale" else "1/(shape*scale)",
      propriety = "proper: n >= 2, positive nonconstant sample",
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  if (model == "weighted lindley") {
    fixed <- .fdb_check_fixed(fixed, character())
    if (prior == "mdi") reject_mdi("weighted Lindley")
    require_prior(
      c("jeffreys", "reference", "first-rule",
        "independence-jeffreys", "reference-lambda", "reference-phi"),
      "weighted Lindley"
    )
    if (n < 2L || any(x <= 0) || !.fdb_nonconstant(x)) {
      .fdb_stop(
        "Weighted Lindley objective posteriors require n >= 2, positive observations, and a nonconstant sample."
      )
    }
    if (!is.finite(S) || !is.finite(sum_log_x)) {
      .fdb_stop(
        "Weighted Lindley sufficient statistics exceeded the finite floating-point range."
      )
    }

    classical <- .fdb_weighted_lindley_start(x)
    sv <- resolve_start(
      classical$values,
      method = classical$method,
      positive = c("lambda", "phi")
    )
    lambda_start <- unname(sv["lambda"])
    phi_start <- unname(sv["phi"])
    mean_start <- phi_start * (lambda_start + phi_start + 1) /
      (lambda_start * (lambda_start + phi_start))
    init <- c(log_mean = log(mean_start), log_phi = log(phi_start))
    logpost <- function(u) {
      if (length(u) != 2L || any(!is.finite(u))) return(-Inf)
      log_mean <- u[1L]
      log_phi <- u[2L]
      if (log_mean > log(.Machine$double.xmax) ||
          log_phi > log(.Machine$double.xmax)) {
        return(-Inf)
      }
      mean_parameter <- exp(log_mean)
      phi <- exp(log_phi)
      lambda <- .fdb_weighted_lindley_lambda_from_mean(mean_parameter, phi)
      if (!is.finite(mean_parameter) || !is.finite(phi) ||
          !is.finite(lambda) || mean_parameter <= 0 || phi <= 0 ||
          lambda <= 0) {
        return(-Inf)
      }
      log_lambda <- log(lambda)
      log_lambda_plus_phi <- .fdb_logsumexp(c(log_lambda, log_phi))
      ll <- n * (phi + 1) * log_lambda -
        n * log_lambda_plus_phi - n * lgamma(phi) +
        phi * sum_log_x - lambda * S
      log_prior <- .fdb_weighted_lindley_log_prior(lambda, phi, prior)
      log_A <- .fdb_logsumexp(c(
        2 * log_lambda_plus_phi, log(2) + log_lambda, log_phi
      ))
      log_information_lambda <- log_phi + log_A -
        2 * log_lambda - 2 * log_lambda_plus_phi
      log_jacobian <- log_mean + log_phi - log_information_lambda
      answer <- ll + log_prior + log_jacobian
      if (is.finite(answer)) answer else -Inf
    }
    transform <- function(u) {
      mean_parameter <- .fdb_exp_positive(u[1L])
      phi <- .fdb_exp_positive(u[2L])
      lambda <- .fdb_weighted_lindley_lambda_from_mean(mean_parameter, phi)
      lambda <- .fdb_positive_finite(
        lambda, "A transformed weighted Lindley rate"
      )
      c(lambda = lambda, phi = phi)
    }
    sampler <- make_mh(
      logpost, init, transform, c("lambda", "phi"),
      "adaptive Metropolis in Fisher-orthogonal mean coordinates"
    )
    loglik <- function(theta) {
      lambda <- theta["lambda"]
      phi <- theta["phi"]
      if (length(lambda) != 1L || length(phi) != 1L ||
          !is.finite(lambda) || !is.finite(phi) ||
          lambda <= 0 || phi <= 0) {
        return(rep(-Inf, length(x)))
      }
      log_lambda <- log(lambda)
      log_phi <- log(phi)
      log_lambda_plus_phi <- .fdb_logsumexp(c(log_lambda, log_phi))
      answer <- (phi + 1) * log_lambda - log_lambda_plus_phi -
        lgamma(phi) + (phi - 1) * log(x) + log1p(x) - lambda * x
      answer[is.nan(answer) | answer == Inf] <- -Inf
      answer
    }
    rng <- function(theta, size) {
      lambda <- .fdb_positive_finite(theta["lambda"], "Weighted Lindley lambda")
      phi <- .fdb_positive_finite(theta["phi"], "Weighted Lindley phi")
      first_component <- stats::runif(size) <
        stats::plogis(log(lambda) - log(phi))
      shapes <- phi + as.numeric(!first_component)
      .fdb_rgamma_positive(size, shape = shapes, rate = lambda)
    }
    prior_label <- switch(
      prior,
      jeffreys = "Fisher-information Jeffreys",
      reference = "one-group reference (equal to Fisher-information Jeffreys)",
      `first-rule` = "Jeffreys' first rule",
      `independence-jeffreys` = "independence Jeffreys",
      `reference-lambda` = "ordered reference (lambda interest)",
      `reference-phi` = "ordered reference (phi interest)"
    )
    prior_kernel <- switch(
      prior,
      jeffreys = "sqrt(A(lambda,phi)*c(phi)-1)/(lambda*(lambda+phi))",
      reference = "sqrt(A(lambda,phi)*c(phi)-1)/(lambda*(lambda+phi))",
      `first-rule` = "1/(lambda*phi)",
      `independence-jeffreys` = paste0(
        "sqrt((((phi+1)*(lambda+phi)^2-lambda^2)*",
        "(trigamma(phi)*(lambda+phi)^2-1)))/(lambda*(lambda+phi)^2)"
      ),
      `reference-lambda` = paste0(
        "sqrt(trigamma(phi)-1/(lambda+phi)^2)/lambda"
      ),
      `reference-phi` = paste0(
        "sqrt(phi*A(lambda,phi))/(lambda*(lambda+phi)*s(phi))*",
        "Q0(phi)^(w0(phi)/2)*Qinf(phi)^(winf(phi)/2)"
      )
    )
    return(list(
      sampler = sampler, parameters = c("lambda", "phi"),
      prior_label = prior_label,
      prior_kernel = prior_kernel,
      propriety = paste0(
        "proper: n >= 2, positive nonconstant sample; all positive ",
        "posterior moments of lambda and phi are finite"
      ),
      loglik = loglik, rng = rng, fixed = fixed
    ))
  }

  .fdb_stop("Internal error: model '%s' has no implementation.", model)
}

.fdb_build_custom <- function(x, density, prior, start, fixed, dots, control,
                              spec = NULL) {
  if (!is.function(prior)) {
    .fdb_stop("A user-defined density requires a user-defined prior function.")
  }
  if (is.null(start) || !is.numeric(start) || is.null(names(start)) ||
      any(!nzchar(names(start))) || anyDuplicated(names(start)) ||
      any(!is.finite(start))) {
    .fdb_stop(
      "A user-defined density requires 'start' as a finite, uniquely named numeric vector."
    )
  }
  if (is.null(fixed)) fixed <- list()
  if (!is.list(fixed) || (length(fixed) &&
      (is.null(names(fixed)) || any(!nzchar(names(fixed))) ||
       anyDuplicated(names(fixed))))) {
    .fdb_stop("'fixed' must be NULL or a uniquely named list.")
  }
  overlap <- intersect(names(start), names(fixed))
  if (length(overlap)) {
    .fdb_stop("Parameters cannot be both unknown and fixed: %s.",
              paste(overlap, collapse = ", "))
  }
  support_checked <- .fdb_extension_validate_data(spec, x, fixed)
  lower <- if (!is.null(control$lower)) control$lower else
    if (!is.null(spec)) spec$lower else NULL
  upper <- if (!is.null(control$upper)) control$upper else
    if (!is.null(spec)) spec$upper else NULL
  bounds <- .fdb_custom_bounds(start, lower, upper)
  requested_density_log <- if (!is.null(control$density_is_log)) {
    control$density_is_log
  } else if (!is.null(spec)) {
    spec$density_is_log
  } else NULL
  requested_prior_log <- if (!is.null(control$prior_is_log)) {
    control$prior_is_log
  } else if (!is.null(spec)) {
    spec$prior_is_log
  } else NULL
  density_log_mode <- .fdb_resolve_log_mode(
    density, requested_density_log, "density"
  )
  prior_log_mode <- .fdb_resolve_log_mode(
    prior, requested_prior_log, "prior"
  )
  prior_formals <- names(formals(prior))
  prior_style <- if (!is.null(spec) && identical(control$prior_style, "auto"))
    spec$prior_style else control$prior_style
  prior_vector_style <- switch(
    prior_style,
    "vector" = TRUE,
    "scalar" = FALSE,
    "auto" = length(prior_formals) > 0L &&
      prior_formals[1L] %in% c("theta", "par", "parameters")
  )

  if (density_log_mode) {
    logged <- .fdb_call_density_pointwise(
      density, x, start, fixed, dots, log_mode = TRUE
    )
    ordinary <- .fdb_call_density_pointwise(
      density, x, start, fixed, dots, log_mode = FALSE
    )
    if (!.fdb_log_contract_equal(logged, ordinary)) {
      .fdb_stop(
        paste0(
          "The custom density does not satisfy its log-density contract at ",
          "'start': values from log=TRUE differ from log(values from log=FALSE)."
        )
      )
    }
  }
  if (prior_log_mode) {
    logged <- .fdb_call_prior(
      prior, start, log_mode = TRUE, vector_style = prior_vector_style
    )
    ordinary <- .fdb_call_prior(
      prior, start, log_mode = FALSE, vector_style = prior_vector_style
    )
    if (!.fdb_log_contract_equal(logged, ordinary)) {
      .fdb_stop(
        paste0(
          "The custom prior does not satisfy its log-density contract at ",
          "'start': log=TRUE differs from log(value from log=FALSE)."
        )
      )
    }
  }
  init <- .fdb_to_unconstrained(start, bounds$lower, bounds$upper)
  initialization <- list(
    source = "user-supplied",
    method = if (is.null(spec)) {
      "user-specified starting values for a custom density"
    } else {
      "starting values stored in or supplied for a model specification"
    },
    automatic = NULL,
    center = start,
    supplied = start,
    sampled_parameters = names(start)
  )
  propriety <- .fdb_extension_propriety(spec, x, fixed)
  moment_status <- .fdb_extension_moments(
    spec, x, fixed, parameters = names(start)
  )
  logpost <- function(u) {
    tr <- .fdb_from_unconstrained(u, bounds$lower, bounds$upper,
                                  jacobian = TRUE)
    ll <- .fdb_call_density(
      density, x, tr$theta, fixed, dots, log_mode = density_log_mode
    )
    if (!is.finite(ll)) return(-Inf)
    lp <- .fdb_call_prior(
      prior, tr$theta, log_mode = prior_log_mode,
      vector_style = prior_vector_style
    )
    if (!is.finite(lp)) return(-Inf)
    ll + lp + tr$log_jacobian
  }
  logpost_natural <- function(theta) {
    theta <- if (is.list(theta)) unlist(theta, use.names = TRUE) else theta
    if (!is.numeric(theta) || length(theta) != length(start) ||
        any(!is.finite(theta))) return(-Inf)
    if (is.null(names(theta))) names(theta) <- names(start)
    if (!setequal(names(theta), names(start)) || anyDuplicated(names(theta))) {
      return(-Inf)
    }
    theta <- theta[names(start)]
    if (any(theta <= bounds$lower | theta >= bounds$upper)) return(-Inf)
    ll <- .fdb_call_density(
      density, x, theta, fixed, dots, log_mode = density_log_mode
    )
    if (!is.finite(ll)) return(-Inf)
    lp <- .fdb_call_prior(
      prior, theta, log_mode = prior_log_mode,
      vector_style = prior_vector_style
    )
    if (!is.finite(lp)) return(-Inf)
    ll + lp
  }
  transform <- function(u) {
    if (is.null(names(u))) names(u) <- names(start)
    .fdb_from_unconstrained(u, bounds$lower, bounds$upper,
                            jacobian = FALSE)
  }
  to_unconstrained <- function(theta) {
    if (is.null(names(theta))) names(theta) <- names(start)
    .fdb_to_unconstrained(theta[names(start)], bounds$lower, bounds$upper)
  }
  selected_engine <- if (is.null(spec)) "adaptive_metropolis" else spec$engine
  sampler <- switch(
    selected_engine,
    adaptive_metropolis = function(iter, warmup, thin, chains, control) {
      ans <- .fdb_amwg(logpost, init, iter, warmup, thin, chains, control)
      list(
        chains = .fdb_transform_matrix(ans$chains, transform, names(start)),
        acceptance = ans$acceptance,
        acceptance_warmup = ans$acceptance_warmup,
        acceptance_all = ans$acceptance_all,
        independent = FALSE,
        engine = "generic adaptive Metropolis",
        initialization = initialization
      )
    },
    slice = function(iter, warmup, thin, chains, control) {
      if (length(start) != 1L) {
        .fdb_stop("The generic slice engine requires exactly one unknown parameter.")
      }
      raw <- .fdb_slice_chains(
        logpost, unname(init), iter, warmup, thin, chains, control
      )
      raw <- lapply(raw, function(z) {
        ans <- matrix(z, ncol = 1L)
        colnames(ans) <- names(start)
        ans
      })
      list(
        chains = .fdb_transform_matrix(raw, transform, names(start)),
        acceptance = NULL,
        acceptance_warmup = NULL,
        acceptance_all = NULL,
        independent = FALSE,
        engine = "generic univariate slice sampling",
        initialization = initialization
      )
    },
    custom = function(iter, warmup, thin, chains, control) {
      n_save <- floor((iter - warmup) / thin)
      label <- spec$engine_label %||% "user-supplied posterior sampler"
      .fdb_extension_sampler(
        spec$sampler,
        arguments = list(
          x = x,
          log_posterior = logpost_natural,
          log_posterior_unconstrained = logpost,
          start = start,
          fixed = fixed,
          lower = bounds$lower,
          upper = bounds$upper,
          to_unconstrained = to_unconstrained,
          from_unconstrained = transform,
          iter = iter,
          warmup = warmup,
          thin = thin,
          chains = chains,
          n_save = n_save,
          control = control,
          dots = dots
        ),
        parameters = names(start), chains = chains, n_save = n_save,
        independent = spec$independent, engine_label = label,
        initialization = initialization
      )
    }
  )
  loglik <- function(theta) {
    .fdb_call_density_pointwise(
      density, x, theta, fixed, dots, log_mode = density_log_mode
    )
  }
  rng <- NULL
  rng_source <- if (is.function(control$rng)) control$rng else
    if (!is.null(spec) && is.function(spec$rng)) spec$rng else NULL
  rng_validator <- if (is.function(control$rng_validator)) {
    control$rng_validator
  } else if (!is.null(spec) && is.function(spec$rng_validator)) {
    spec$rng_validator
  } else NULL
  if (is.function(rng_source)) {
    user_rng <- rng_source
    rng <- function(theta, size) {
      ans <- do.call(user_rng,
                     c(list(size), as.list(theta), fixed, dots))
      if (!is.numeric(ans) || length(ans) != size || anyNA(ans) ||
          any(is.nan(ans)) || any(!is.finite(ans))) {
        .fdb_stop(
          paste0(
            "control$rng must return exactly 'size' finite numeric values ",
            "without NA or NaN."
          )
        )
      }
      if (is.function(rng_validator)) {
        valid <- rng_validator(ans)
        if (!is.logical(valid) || !length(valid) || anyNA(valid) ||
            !(length(valid) %in% c(1L, size)) || !all(valid)) {
          .fdb_stop(
            paste0(
              "control$rng_validator must return TRUE, or one TRUE value ",
              "per simulated observation."
            )
          )
        }
      }
      ans
    }
  }
  list(
    sampler = sampler,
    parameters = names(start),
    prior_label = if (is.null(spec)) "user-defined" else spec$prior_label,
    prior_kernel = if (is.null(spec)) "user-supplied function" else
      spec$prior_kernel,
    propriety = propriety$message,
    propriety_declared = propriety$declared,
    moment_status = moment_status,
    loglik = loglik,
    rng = rng,
    fixed = fixed,
    bounds = bounds,
    support_checked = support_checked,
    extension = if (is.null(spec)) NULL else list(
      engine = selected_engine,
      reference = spec$reference,
      propriety_source = if (propriety$declared) "user-supplied" else
        "not supplied",
      moment_source = if (is.null(moment_status)) "not supplied" else
        "user-supplied"
    ),
    custom_contract = list(
      density_is_log = density_log_mode,
      prior_is_log = prior_log_mode,
      prior_style = if (prior_vector_style) "vector" else "scalar",
      rng_validated = is.function(rng_validator)
    )
  )
}

#' Objective Bayesian fitting of parametric distributions
#'
#' @param x Numeric vector of observations.
#' @param distr A recognized distribution name (case-insensitive), a density
#'   function whose first argument is the observation vector, or an object made
#'   by [fitdistrBayes_model()]. Version 0.2.2
#'   includes 20 built-in distributions, including Gumbel, Frechet, Lomax,
#'   Nakagami-m, Exponential-Logarithmic, Rician, and weighted Lindley.
#' @param prior An objective-prior name or, for a custom density function, a
#'   prior function.  Omit this argument when `distr` is an object made by
#'   [fitdistrBayes_model()], because its prior is part of the specification.
#'   A custom prior may accept named scalar parameters or a named vector as its
#'   first argument, and may optionally accept log = TRUE.
#' @param start Optional named starting values. Required for custom densities.
#' @param fixed Optional named list of fixed parameters. Currently used for
#'   negative-binomial size and Student-t degrees of freedom.
#' @param iter Total iterations per chain, including warmup.
#' @param warmup Warmup iterations per chain.
#' @param thin Positive thinning interval.
#' @param chains Number of chains.
#' @param seed Optional reproducibility seed passed to `set.seed()` before
#'   posterior simulation.
#' @param na.action Either "fail" or "omit".
#' @param control Named list of computational controls.
#' @param ... Extra fixed arguments forwarded to a custom density/RNG.
#'
#' @return An object of class "fitdistrBayes".
#' @export
fitdistrBayes <- function(x, distr, prior = NULL, start = NULL, fixed = NULL,
                          iter = 4000L, warmup = floor(iter / 2),
                          thin = 1L, chains = 4L, seed = NULL,
                          na.action = c("fail", "omit"),
                          control = list(), ...) {
  call <- match.call()
  na.action <- match.arg(na.action)
  iter <- .fdb_scalar_count(iter, "iter", lower = 20L)
  warmup <- .fdb_scalar_count(warmup, "warmup", lower = 0L)
  thin <- .fdb_scalar_count(thin, "thin", lower = 1L)
  chains <- .fdb_scalar_count(chains, "chains", lower = 1L)
  if (warmup >= iter) .fdb_stop("'warmup' must be smaller than 'iter'.")
  n_save <- floor((iter - warmup) / thin)
  if (n_save < 20L) {
    .fdb_stop("The settings retain only %d draws per chain; at least 20 are required.",
              n_save)
  }

  control <- .fdb_merge_control(control)
  validated <- .fdb_validate_x(x, na.action)
  x_used <- validated$x
  dots <- list(...)

  model_spec <- inherits(distr, "fitdistrBayes_model")
  custom_density <- is.function(distr)
  custom <- model_spec || custom_density
  if (!custom && (!is.character(distr) || length(distr) != 1L ||
                  is.na(distr) || !nzchar(trimws(distr)))) {
    .fdb_stop("'distr' must be one recognized character string or a density function.")
  }

  if (model_spec) {
    if (!is.null(prior)) {
      .fdb_stop(
        "Do not supply 'prior' when 'distr' is a fitdistrBayes_model object; the prior is part of the model specification."
      )
    }
    spec <- distr
    model <- spec$name
    start_used <- if (is.null(start)) spec$start else start
    fixed_used <- spec$fixed
    if (!is.null(fixed)) {
      if (!is.list(fixed) || is.null(names(fixed)) ||
          any(!nzchar(names(fixed))) || anyDuplicated(names(fixed))) {
        .fdb_stop("'fixed' must be NULL or a uniquely named list.")
      }
      fixed_used[names(fixed)] <- fixed
    }
    built <- .fdb_build_custom(
      x_used, spec$density, spec$prior, start_used, fixed_used,
      dots, control, spec = spec
    )
    prior_key <- "user-defined"
    if (!isTRUE(built$propriety_declared)) {
      .fdb_warn(
        "Posterior propriety for this user-defined model has not been supplied and cannot be certified automatically."
      )
    }
  } else if (custom_density) {
    model <- "user-defined"
    built <- .fdb_build_custom(x_used, distr, prior, start, fixed,
                               dots, control, spec = NULL)
    prior_key <- "user-defined"
    .fdb_warn(
      "Posterior propriety for a user-defined density/prior cannot be certified automatically."
    )
  } else {
    if (!is.character(prior) || length(prior) != 1L ||
        is.na(prior) || !nzchar(trimws(prior))) {
      .fdb_stop("Built-in distributions require 'prior' as one character string.")
    }
    model <- .fdb_model_name(distr)
    prior_key <- .fdb_prior_name(prior, model = model)
    built <- .fdb_build_builtin(x_used, model, prior_key, fixed, start)
  }

  sampled <- .fdb_with_seed(
    seed,
    built$sampler(iter, warmup, thin, chains, control)
  )
  if (!is.null(start) &&
      identical(sampled$initialization$source, "not applicable")) {
    .fdb_warn(
      "'start' was ignored because this model-prior route uses independent exact posterior simulation."
    )
  }
  chain_list <- sampled$chains
  if (length(chain_list) != chains ||
      any(vapply(chain_list, nrow, integer(1)) != n_save) ||
      any(vapply(chain_list, function(z) any(!is.finite(z)), logical(1)))) {
    .fdb_stop("Internal sampling error: invalid posterior chains were produced.")
  }
  posterior_summary <- .fdb_summarize(
    chain_list, independent = sampled$independent
  )
  moment_status <- if (!is.null(built$moment_status)) {
    built$moment_status
  } else {
    .fdb_moment_status(
      model, prior_key, built$parameters, length(x_used), built$fixed, x_used
    )
  }
  moment_index <- match(posterior_summary$parameter,
                        moment_status$parameter)
  posterior_summary$mean_exists <- moment_status$mean_exists[moment_index]
  posterior_summary$variance_exists <-
    moment_status$variance_exists[moment_index]
  posterior_summary$moment_note <- moment_status$note[moment_index]
  certified_mean <- posterior_summary$mean_exists %in% TRUE
  certified_variance <- posterior_summary$variance_exists %in% TRUE
  posterior_summary$mean[!certified_mean] <- NA_real_
  posterior_summary$sd[!certified_variance] <- NA_real_
  posterior_summary$mcse_mean[!(certified_mean & certified_variance)] <-
    NA_real_
  draws <- .fdb_long_draws(chain_list)

  if (sampled$independent) {
    converged <- TRUE
    diagnostic_messages <- "Independent posterior simulation: MCMC convergence is not applicable."
  } else {
    good_rhat <- is.finite(posterior_summary$rhat) &
      posterior_summary$rhat <= control$rhat_threshold
    good_ess <- is.finite(posterior_summary$ess_bulk) &
      posterior_summary$ess_bulk >= control$ess_threshold
    good_tail_ess <- is.finite(posterior_summary$ess_tail) &
      posterior_summary$ess_tail >= control$ess_threshold
    converged <- all(good_rhat & good_ess & good_tail_ess)
    diagnostic_messages <- character()
    if (any(!good_rhat)) {
      diagnostic_messages <- c(
        diagnostic_messages,
        sprintf("R-hat above %.3f for: %s.",
                control$rhat_threshold,
                paste(posterior_summary$parameter[!good_rhat],
                      collapse = ", "))
      )
    }
    if (any(!good_ess)) {
      diagnostic_messages <- c(
        diagnostic_messages,
        sprintf("Bulk ESS below %d for: %s.",
                control$ess_threshold,
                paste(posterior_summary$parameter[!good_ess],
                      collapse = ", "))
      )
    }
    if (any(!good_tail_ess)) {
      diagnostic_messages <- c(
        diagnostic_messages,
        sprintf("Tail ESS below %d for: %s.",
                control$ess_threshold,
                paste(posterior_summary$parameter[!good_tail_ess],
                      collapse = ", "))
      )
    }
    if (!length(diagnostic_messages)) {
      diagnostic_messages <- "Rank-normalized split R-hat and ESS thresholds were satisfied."
    }
  }

  acceptance <- sampled$acceptance
  if (!is.null(acceptance)) {
    warmup_acceptance <- sampled$acceptance_warmup
    all_acceptance <- sampled$acceptance_all
    acceptance_summary <- data.frame(
      chain = seq_len(nrow(acceptance)),
      overall = rowMeans(acceptance),
      warmup_overall = if (is.null(warmup_acceptance)) NA_real_ else
        rowMeans(warmup_acceptance),
      all_iterations_overall = if (is.null(all_acceptance)) NA_real_ else
        rowMeans(all_acceptance),
      acceptance,
      check.names = FALSE,
      row.names = NULL
    )
  } else {
    acceptance_summary <- NULL
  }

  diagnostics <- list(
    converged = converged,
    exact_or_independent = sampled$independent,
    rhat_threshold = control$rhat_threshold,
    ess_threshold = control$ess_threshold,
    max_rhat = max(posterior_summary$rhat, na.rm = TRUE),
    min_ess_bulk = min(posterior_summary$ess_bulk, na.rm = TRUE),
    min_ess_tail = min(posterior_summary$ess_tail, na.rm = TRUE),
    acceptance = acceptance_summary,
    messages = diagnostic_messages,
    definition = paste0(
      "rank-normalized split/folded R-hat; mean/bulk/tail ESS; MCSE(mean); ",
      "acceptance rates labeled 'overall' use post-warmup iterations"
    )
  )

  answer <- list(
    call = call,
    model = list(
      name = model,
      distribution_input = if (model_spec) {
        "<fitdistrBayes_model>"
      } else if (custom_density) {
        "<function>"
      } else distr,
      parameters = built$parameters,
      fixed = built$fixed,
      n = length(x_used),
      support_checked = if (custom) isTRUE(built$support_checked) else TRUE,
      extension = if (custom) built$extension else NULL,
      custom_contract = if (custom) built$custom_contract else NULL
    ),
    prior = list(
      input = if (is.function(prior)) "<function>" else prior,
      key = prior_key,
      label = built$prior_label,
      kernel = built$prior_kernel,
      posterior_propriety = built$propriety
    ),
    initialization = sampled$initialization,
    engine = list(
      algorithm = sampled$engine,
      chains = chains,
      iterations = iter,
      warmup = warmup,
      thin = thin,
      saved_per_chain = n_save,
      seed = seed
    ),
    estimates = stats::setNames(posterior_summary$median,
                                posterior_summary$parameter),
    summary = posterior_summary,
    moment_status = moment_status,
    diagnostics = diagnostics,
    capabilities = list(
      callables_stored = isTRUE(control$store_callables),
      prediction = isTRUE(control$store_callables) && !is.null(built$rng),
      pointwise_log_likelihood = isTRUE(control$store_callables)
    ),
    chains = chain_list,
    draws = draws,
    data = x_used,
    omitted = validated$omitted,
    control = control
  )
  if (control$store_callables) {
    answer$.loglik <- built$loglik
    answer$.rng <- built$rng
  }
  class(answer) <- "fitdistrBayes"

  if (!converged && isTRUE(control$warn_convergence)) {
    .fdb_warn(
      "The convergence targets were not all met. Inspect fit$diagnostics and plot(fit, type = \"trace\"); increase 'iter' if needed."
    )
  }
  answer
}

# Methods ----------------------------------------------------------------------

#' @export
print.fitdistrBayes <- function(x, digits = max(3L, getOption("digits") - 3L),
                                ...) {
  cat("\nObjective Bayesian distribution fit\n")
  cat("-----------------------------------\n")
  cat("Model:       ", x$model$name, "\n", sep = "")
  if (length(x$model$fixed)) {
    fixed_text <- paste(
      sprintf("%s=%s", names(x$model$fixed),
              vapply(x$model$fixed,
                     function(z) paste(as.character(z), collapse = "/"),
                     character(1))),
      collapse = ", "
    )
    cat("Fixed:       ", fixed_text, "\n", sep = "")
  }
  cat("Prior:       ", x$prior$label, "\n", sep = "")
  cat("Engine:      ", x$engine$algorithm, "\n", sep = "")
  if (identical(x$initialization$source, "not applicable")) {
    cat("Initialization: not required (independent exact draws)\n")
  } else {
    cat("Initialization: ", x$initialization$source, "; ",
        x$initialization$method, "\n", sep = "")
    center_text <- paste(
      sprintf("%s=%.*g", names(x$initialization$center), digits,
              x$initialization$center),
      collapse = ", "
    )
    cat("Initial center:", center_text, "\n")
  }
  cat("Observations:", x$model$n, "\n")
  propriety_parts <- strsplit(
    x$prior$posterior_propriety, ": ", fixed = TRUE
  )[[1L]]
  cat("Propriety:   ", propriety_parts[1L], "\n", sep = "")
  if (length(propriety_parts) > 1L) {
    cat("Condition:    ",
        paste(propriety_parts[-1L], collapse = ": "), "\n", sep = "")
  }
  cat("Diagnostic:  ",
      if (x$diagnostics$converged) "OK" else "attention required",
      "\n\n", sep = "")
  shown <- x$summary[, c("parameter", "mean", "sd", "q2.5", "median",
                         "q97.5", "rhat", "ess_bulk")]
  print(shown, row.names = FALSE, digits = digits)
  if (any(!(x$summary$mean_exists %in% TRUE) |
          !(x$summary$variance_exists %in% TRUE))) {
    cat("\nNA posterior moments are intentionally suppressed when they are infinite or not certified.\n")
    cat("Posterior medians are used by coef() and fit$estimates.\n")
  }
  invisible(x)
}

#' @export
summary.fitdistrBayes <- function(object, ...) {
  answer <- list(
    call = object$call,
    model = object$model,
    prior = object$prior,
    initialization = object$initialization,
    engine = object$engine,
    posterior = object$summary,
    moment_status = object$moment_status,
    diagnostics = object$diagnostics,
    capabilities = object$capabilities
  )
  class(answer) <- "summary.fitdistrBayes"
  answer
}

#' @export
print.summary.fitdistrBayes <- function(x, digits = max(3L, getOption("digits") - 3L),
                                        ...) {
  cat("\nSummary of objective Bayesian distribution fit\n")
  cat("Model:", x$model$name, " | Prior:", x$prior$label,
      " | Engine:", x$engine$algorithm, "\n")
  if (identical(x$initialization$source, "not applicable")) {
    cat("Initialization: not required (independent exact draws)\n")
  } else {
    cat("Initialization:", x$initialization$source, "|",
        x$initialization$method, "\n")
    print(x$initialization$center, digits = digits)
  }
  cat("Posterior propriety:", x$prior$posterior_propriety, "\n\n")
  print(x$posterior, row.names = FALSE, digits = digits)
  cat("\nDiagnostics:\n")
  cat(paste0("- ", x$diagnostics$messages, collapse = "\n"), "\n")
  if (!is.null(x$diagnostics$acceptance)) {
    cat("\nAcceptance rates:\n")
    print(x$diagnostics$acceptance, row.names = FALSE, digits = digits)
  }
  if (any(!(x$posterior$mean_exists %in% TRUE) |
          !(x$posterior$variance_exists %in% TRUE))) {
    cat("\nMoment audit:\n")
    moment_lines <- unique(sprintf(
      "- %s: %s",
      x$posterior$parameter,
      x$posterior$moment_note
    ))
    cat(paste(moment_lines, collapse = "\n"), "\n")
    cat("Posterior medians are the default point estimates.\n")
  }
  invisible(x)
}

#' @export
coef.fitdistrBayes <- function(object, ...) {
  object$estimates
}

#' @export
confint.fitdistrBayes <- function(object, parm = object$model$parameters,
                                  level = 0.95, ...) {
  if (length(level) != 1L || !is.numeric(level) || !is.finite(level) ||
      level <= 0 || level >= 1) {
    .fdb_stop("'level' must lie strictly between 0 and 1.")
  }
  bad <- setdiff(parm, object$model$parameters)
  if (length(bad)) .fdb_stop("Unknown parameter%s: %s.",
                             if (length(bad) > 1L) "s" else "",
                             paste(bad, collapse = ", "))
  alpha <- (1 - level) / 2
  values <- object$draws[, parm, drop = FALSE]
  ans <- t(vapply(values, stats::quantile, numeric(2),
                  probs = c(alpha, 1 - alpha), names = FALSE, type = 8))
  colnames(ans) <- paste0(format(100 * c(alpha, 1 - alpha),
                                 trim = TRUE), "%")
  ans
}

#' @export
as.data.frame.fitdistrBayes <- function(x, row.names = NULL, optional = FALSE,
                                        ...) {
  x$draws
}

#' @export
plot.fitdistrBayes <- function(x,
                               type = c("trace", "density", "acf", "pairs"),
                               pars = x$model$parameters, ...) {
  type <- match.arg(type)
  bad <- setdiff(pars, x$model$parameters)
  if (length(bad)) .fdb_stop("Unknown plotting parameter%s: %s.",
                             if (length(bad) > 1L) "s" else "",
                             paste(bad, collapse = ", "))
  if (type == "pairs") {
    if (length(pars) < 2L) .fdb_stop("'pairs' requires at least two parameters.")
    graphics::pairs(x$draws[, pars, drop = FALSE], ...)
    return(invisible(x))
  }
  old <- graphics::par(no.readonly = TRUE)
  on.exit(graphics::par(old), add = TRUE)
  panel_count <- if (type == "acf") {
    length(pars) * length(x$chains)
  } else {
    length(pars)
  }
  graphics::par(mfrow = grDevices::n2mfrow(panel_count),
                mar = c(3.2, 3.2, 2.2, 1))
  colors <- seq_along(x$chains)
  for (p in pars) {
    if (type == "trace") {
      yr <- range(vapply(x$chains, function(z) range(z[, p]),
                         numeric(2)))
      graphics::plot(x$chains[[1L]][, p], type = "l", col = colors[1L],
                     xlab = "Saved iteration", ylab = p, ylim = yr,
                     main = paste("Trace:", p), ...)
      if (length(x$chains) > 1L) {
        for (ch in 2:length(x$chains)) {
          graphics::lines(x$chains[[ch]][, p], col = colors[ch])
        }
      }
    } else if (type == "density") {
      dens <- lapply(x$chains, function(z) stats::density(z[, p]))
      xr <- range(vapply(dens, function(z) range(z$x), numeric(2)))
      yr <- range(vapply(dens, function(z) range(z$y), numeric(2)))
      graphics::plot(dens[[1L]], col = colors[1L], xlim = xr, ylim = yr,
                     xlab = p, main = paste("Posterior:", p), ...)
      if (length(dens) > 1L) {
        for (ch in 2:length(dens)) {
          graphics::lines(dens[[ch]], col = colors[ch])
        }
      }
    } else {
      for (ch in seq_along(x$chains)) {
        stats::acf(
          x$chains[[ch]][, p],
          main = sprintf("ACF: %s, chain %d", p, ch),
          ...
        )
      }
    }
  }
  invisible(x)
}

#' Draw from the posterior predictive distribution
#' @export
predict.fitdistrBayes <- function(object, draws = 1000L, size = 1L,
                                  seed = NULL, ...) {
  if (is.null(object$.rng)) {
    .fdb_stop(
      "Posterior prediction is unavailable. For a custom density, supply control$rng; do not set control$store_callables = FALSE."
    )
  }
  draws <- .fdb_scalar_count(draws, "draws", lower = 1L)
  size <- .fdb_scalar_count(size, "size", lower = 1L)
  all_draws <- object$draws
  answer <- .fdb_with_seed(seed, {
    index <- sample.int(nrow(all_draws), draws, replace = draws > nrow(all_draws))
    out <- matrix(NA_real_, nrow = draws, ncol = size)
    for (i in seq_len(draws)) {
      theta <- unlist(all_draws[index[i], object$model$parameters,
                                drop = FALSE], use.names = TRUE)
      out[i, ] <- object$.rng(theta, size)
    }
    out
  })
  if (anyNA(answer) || any(is.nan(answer))) {
    .fdb_stop("Posterior prediction produced undefined numeric values.")
  }
  infinite <- is.infinite(answer)
  if (any(infinite)) {
    n_infinite <- sum(infinite)
    .fdb_stop(
      paste0(
        "%d posterior predictive value%s exceeded the floating-point range; ",
        "prediction stopped and no clipping was applied."
      ),
      n_infinite, if (n_infinite == 1L) "" else "s"
    )
  }
  colnames(answer) <- paste0("y_rep[", seq_len(size), "]")
  if (size == 1L) as.numeric(answer[, 1L]) else answer
}

#' Extract a pointwise log-likelihood matrix
#' @export
log_lik.fitdistrBayes <- function(object, draws = NULL, seed = NULL, ...) {
  if (is.null(object$.loglik)) {
    .fdb_stop("Pointwise log-likelihood storage was disabled.")
  }
  all_draws <- object$draws
  if (is.null(draws)) {
    index <- seq_len(nrow(all_draws))
  } else {
    draws <- .fdb_scalar_count(draws, "draws", lower = 1L)
    index <- .fdb_with_seed(
      seed,
      sample.int(nrow(all_draws), draws, replace = draws > nrow(all_draws))
    )
  }
  values <- vapply(index, function(i) {
    theta <- unlist(all_draws[i, object$model$parameters, drop = FALSE],
                    use.names = TRUE)
    object$.loglik(theta)
  }, numeric(object$model$n))
  # vapply() simplifies to a vector when n = 1. Rebuild the documented
  # draws-by-observations matrix explicitly before transposing.
  ans <- t(matrix(
    values, nrow = object$model$n, ncol = length(index)
  ))
  colnames(ans) <- paste0("log_lik[", seq_len(object$model$n), "]")
  ans
}

log_lik <- function(object, ...) UseMethod("log_lik")

Try the fitdistrBayes package in your browser

Any scripts or data that you put into this service are public.

fitdistrBayes documentation built on Aug. 30, 2026, 1:07 a.m.