R/blrm_exnex.R

Defines functions .validate_group_stratum_nesting model.matrix.blrmfit .make_label_factor .abbreviate_label .label_index print.blrmfit .get_X .get_cmdstan_model .priormix2stan .mvnormsigma .mvnormdim .is_list_mixmvnorm .is_mixmvnorm .comp_mix2stan .standata_empty_mixture .assert_list_mixmvnorm .assert_mixmvnorm .args2mix .comp_args2mix blrm_exnex

Documented in blrm_exnex .get_X .label_index print.blrmfit .validate_group_stratum_nesting

#' @title Bayesian Logistic Regression Model for N-compounds with EXNEX
#'
#' @description Bayesian Logistic Regression Model (BLRM) for N
#'     compounds using EXchangability and NonEXchangability (EXNEX)
#'     modeling.
#'
#' @param formula the model formula describing the linear predictors
#'   of the model. The lhs of the formula is a two-column matrix which
#'   are the number of occured events and the number of times no event
#'   occured. The rhs of the formula defines the linear predictors for
#'   the marginal models for each drug component, then the interaction
#'   model and at last the grouping and optional stratum factors of
#'   the models. These elements of the formula are separated by a
#'   vertical bar. The marginal models must follow a intercept and
#'   slope form while the interaction model must not include an
#'   interaction term. See the examples below for an example
#'   instantiation.
#' @param data optional data frame containing the variables of the
#'   model. If not found in \code{data}, the variables are taken from
#'   \code{environment(formula)}.
#' @param prior_EX_mu_comp List of bivariate normal mixture priors for
#'   intercept and slope parameters \eqn{\boldsymbol\mu_i =
#'   (\mu_{\alpha i}, \mu_{\beta i})} of each component. In case of a
#'   single drug model, then a mixture prior is accepted as well.
#' @param prior_EX_mu_mean_comp,prior_EX_mu_sd_comp <!------------->
#'   `r lifecycle::badge("deprecated")` Please use `prior_EX_mu_comp`
#'   instead. Mean and sd for the prior on the mean parameters
#'   \eqn{\boldsymbol\mu_i = (\mu_{\alpha i}, \mu_{\beta i})} of each
#'   component.  Two column matrix (intercept, log-slope) with one row
#'   per component.
#' @param prior_EX_tau_comp List of bivariate normal mixture priors
#'   for heterogeniety parameter \eqn{\boldsymbol\tau_{si} =
#'   (\tau_{\alpha s i}, \tau_{\beta s i})} of each stratum. If no
#'   differential discounting is required (i.e. if there is only one
#'   stratum \eqn{s = 1}), then it suffices to provide a bivariate
#'   normal mixture prior instead of a list with just one element.
#' @param prior_EX_tau_mean_comp,prior_EX_tau_sd_comp <!----------->
#'   `r lifecycle::badge("deprecated")` Please use `prior_EX_tau_comp`
#'   instead. Prior mean and sd for heterogeniety parameter
#'   \eqn{\boldsymbol\tau_{si} = (\tau_{\alpha s i}, \tau_{\beta s
#'   i})} of each stratum. If no differential discounting is required
#'   (i.e. if there is only one stratum \eqn{s = 1}), then it is a
#'   two-column matrix (intercept, log-slope) with one row per
#'   component. Otherwise it is a three-dimensional array whose first
#'   dimension indexes the strata, second dimension indexes the
#'   components, and third dimension of length two for (intercept,
#'   log-slope).
#' @param prior_EX_corr_eta_comp Prior LKJ correlation parameter for
#'   each component given as numeric vector. If missing, then a 1 is
#'   assumed corresponding to a marginal uniform prior of the
#'   correlation.
#' @param prior_EX_mu_inter Multivariate normal mixture prior for
#'   interaction parameter vector
#'   \eqn{\boldsymbol{\mu_{\eta}}}. Dimension must correspond to the
#'   number of interactions.
#' @param prior_EX_mu_mean_inter,prior_EX_mu_sd_inter <!------------>
#'   `r lifecycle::badge("deprecated")` Please use `prior_EX_mu_inter`
#'   instead. Prior mean and sd for population mean parameters
#'   \eqn{\mu_{\eta k}} of each interaction parameter. Vector of
#'   length equal to the number of interactions.
#' @param prior_EX_tau_inter List of multivariate normal mixture
#'   priors for heterogeniety interaction parameter vector
#'   \eqn{\boldsymbol{\tau_{\eta s}}} of each stratum. If no
#'   differential discounting is required (i.e. if there is only one
#'   stratum \eqn{s = 1}), then it suffices to provide a mixture prior
#'   instead of a list with just one element.
#' @param prior_EX_tau_mean_inter,prior_EX_tau_sd_inter <!---------->
#'   `r lifecycle::badge("deprecated")` Please use
#'   `prior_EX_tau_inter` instead. Prior mean and sd for heterogeniety
#'   parameter \eqn{\tau_{\eta s k}} of each stratum. Matrix with one
#'   column per interaction and one row per stratum.
#' @param prior_EX_corr_eta_inter Prior LKJ correlation parameter for
#'   interaction given as numeric. If missing, then a 1 is assumed
#'   corresponding to a marginal uniform prior of the correlations.
#' @param prior_EX_prob_comp Prior probability \eqn{p_{ij}} for
#'   exchangability of each component per group. Matrix with one
#'   column per component and one row per group. Values must lie in
#'   \eqn{[0-1]} range.
#' @param prior_EX_prob_inter Prior probability \eqn{p_{\eta k j}} for
#'   exchangability of each interaction per group. Matrix with one
#'   column per interaction and one row per group. Values must lie in
#'   \eqn{[0-1]} range.
#' @param prior_NEX_mu_comp List of bivariate normal mixture priors
#'   \eqn{\boldsymbol m_{ij}} and \eqn{\boldsymbol s_{ij} =
#'   \text{diag}(\boldsymbol S_{ij})} of each component for
#'   non-exchangable case. If missing set to the same prior as given
#'   for the EX part. It is required that the specification be the
#'   same across groups j.
#' @param prior_NEX_mu_mean_comp,prior_NEX_mu_sd_comp <!----------->
#'   `r lifecycle::badge("deprecated")` Please use `prior_NEX_mu_comp`
#'   instead. Prior mean \eqn{\boldsymbol m_{ij}} and sd
#'   \eqn{\boldsymbol s_{ij} = \text{diag}(\boldsymbol S_{ij})} of
#'   each component for non-exchangable case. Two column matrix
#'   (intercept, log-slope) with one row per component. If missing set
#'   to the same prior as given for the EX part. It is required that
#'   the specification be the same across groups j.
#' @param prior_NEX_mu_inter Multivariate normal mixture prior (mean
#'   \eqn{m_{\eta k j}}, sd \eqn{s_{\eta k j}} and covariance) for the
#'   interaction parameter vector for non-exchangable case. Dimension
#'   must correspond to the number of interactions. If missing set to
#'   the same prior as given for the EX part.
#' @param prior_NEX_mu_mean_inter,prior_NEX_mu_sd_inter <!--------->
#'   `r lifecycle::badge("deprecated")` Please use
#'   `prior_NEX_mu_inter` instead. Prior mean \eqn{m_{\eta k j}} and
#'   sd \eqn{s_{\eta k j}} for each interaction parameter for
#'   non-exchangable case. Vector of length equal to the number of
#'   interactions. If missing set to the same prior as given for the
#'   EX part.
#' @param prior_is_EXNEX_comp Defines if non-exchangability is
#'   admitted for a given component. Logical vector of length equal to
#'   the number of components. If missing \code{TRUE} is assumed for
#'   all components.
#' @param prior_is_EXNEX_inter Defines if non-exchangability is
#'   admitted for a given interaction parameter. Logical vector of
#'   length equal to the number of interactions. If missing
#'   \code{FALSE} is assumed for all interactions.
#' @param prior_tau_dist Defines the distribution used for
#'   heterogeniety parameters. Choices are 0=fixed to it's mean,
#'   1=log-normal, 2=truncated normal or \code{NULL} shutting off the
#'   hierarchical structure of the model.
#' @param sample_map Logical flag (defaults to \code{FALSE})
#'   controlling inclusion of MAP priors for each stratum defined as
#'   part of the generated posterior. If set to \code{TRUE} then the
#'   posterior samples will contain \code{map_log_beta} and
#'   \code{map_eta} variables.
#' @param prior_PD Logical flag (defaults to \code{FALSE}) indicating
#'   if to sample the prior predictive distribution instead of
#'   conditioning on the data.
#' @template args-sampling
#' @param verbose Logical flag (defaults to \code{FALSE}) controlling
#'   if additional output like stan progress is reported.
#' @template args-dots-ignored
#' @param digits number of digits to show
#' @param x \code{blrmfit} object to print
#'
#' @details
#'
#' \code{blrm_exnex} is a flexible function for Bayesian meta-analytic
#' modeling of binomial count data. In particular, it is designed to
#' model counts of the number of observed dose limiting toxicities
#' (DLTs) by dose, for guiding dose-escalation studies in Oncology. To
#' accommodate dose escalation over more than one agent, the dose may
#' consist of combinations of study drugs, with any number of
#' treatment components.
#'
#' In the simplest case, the aim is to model the probability \eqn{\pi} that
#' a patient experiences a DLT, by complementing the binomial likelihood with
#' a monotone logistic regression
#'
#' \deqn{\text{logit}\,\pi(d) = \log\,\alpha + \beta \, t(d),}
#'
#' where \eqn{\beta > 0}. Most typically, \eqn{d} represents the dose,
#' and \eqn{t(d)} is an appropriate transformation, such as \eqn{t(d)
#' = \log (d \big / d^*)}. A joint prior on \eqn{\boldsymbol \theta =
#' (\log\,\alpha, \log\,\beta)} completes the model and ensures
#' monotonicity \eqn{\beta > 0}.
#'
#' Many extensions are possible. The function supports general
#' combination regimens, and also provides framework for Bayesian
#' meta-analysis of dose-toxicity data from multiple historical and
#' concurrent sources.
#'
#' For an example of a single-agent trial refer to \code{\link{example-single-agent}}.
#'
#' @section Combination of two treatments:
#'
#' For a combination of two treatment components, the basic modeling
#' framework is that the DLT rate \eqn{\pi(d_1,d_2)} is comprised of
#' (1) a "no-interaction" baseline model \eqn{\tilde \pi(d_1,d_2)}
#' driven by the single-agent toxicity of each component, and (2)
#' optional interaction terms \eqn{\gamma(d_1,d_2)} representing
#' synergy or antagonism between the drugs. On the log-odds scale,
#'
#' \deqn{\text{logit} \,\pi(d_1,d_2) = \text{logit} \, \tilde \pi(d_1,d_2) + \eta \, \gamma(d_1,d_2). }
#'
#' The "no interaction" part \eqn{\tilde \pi(d_1,d_2)} represents the probability
#' of a DLT triggered by either treatment component acting \emph{independently}.
#' That is,
#' \deqn{ \tilde \pi(d_1,d_2) = 1- (1 - \pi_1(d_1))(1 - \pi_2(d_2)). }
#' In simple terms, P(no DLT for combination) = P(no DLT for drug 1) * P(no DLT from drug 2).
#' To complete this part, the treatment components can then be modeled with monotone
#' logistic regressions as before.
#'
#' \deqn{\text{logit} \, \pi_i(d_i) = \log\, \alpha_i + \beta_i \, t(d_i),}
#'
#' where \eqn{t(d_i)} is a monotone transformation of the doses of the
#' respective drug component $i$, such as \eqn{t(d_i) = \log (d_i \big
#' / d_i^*)}.
#'
#' The inclusion of an interaction term \eqn{\gamma(d_1,d_2)} allows
#' DLT rates above or below the "no-interaction" rate. The magnitude
#' of the interaction term may also be made dependent on the doses (or
#' other covariates) through regression. As an example, one could let
#'
#' \deqn{\gamma(d_1, d_2) = \frac{d_1}{d_1^*} \frac{d_1}{d_2^*}.}
#'
#' The specific functional form is specified in the usual notation for
#' a design matrix. The interaction model must respect the constraint
#' that whenever any dose approaches zero, then the interaction
#' term must vanish as well. Therefore, the interaction model must not
#' include an intercept term which would violate this consistency
#' requirement. A dual combination example can be found in
#' \code{\link{example-combo2}}.
#'
#' @section General combinations:
#'
#' The model is extended to general combination treatments consisting
#' of \eqn{N} components by expressing the probability \eqn{\pi} on
#' the logit scale as
#'
#' \deqn{ \text{logit} \, \pi(d_1,\ldots,d_N) = \text{logit} \Bigl( 1 - \prod_{i = 1}^N ( 1 - \pi_i(d_i) ) \Bigr) + \sum_{k=1}^K \eta_k \, \gamma_k(d_1,\ldots,d_N), }
#'
#' Multiple drug-drug interactions among the \eqn{N} components are
#' now possible, and are represented through the \eqn{K} interaction
#' terms \eqn{\gamma_k}.
#'
#' Regression models can be again be specified for each \eqn{\pi_i}
#' and \eqn{\gamma_k}, such as
#'
#' \deqn{ \text{logit}\, \pi_i(d_i) = \log\, \alpha_i + \beta_i \, t(d_i) }
#'
#' Interactions for some subset \eqn{I(k) \subset \{1,\ldots,N \}} of
#' the treatment components can be modeled with regression as well,
#' for example on products of doses,
#'
#' \deqn{ \gamma_k(d_1,\ldots,d_N) = \prod_{i \in I(k)} \frac{d_i}{d_i^*}.}
#'
#' For example, \eqn{I(k) = \{1,2,3\}} results in the three-way
#' interaction term
#'
#' \deqn{ \frac{d_1}{d_1^*} \frac{d_2}{d_2^*} \frac{d_3}{d_3^*} }
#'
#' for drugs 1, 2, and 3.
#'
#' For a triple combination example please refer to
#' \code{\link{example-combo3}}.
#'
#' @section Meta-analytic framework:
#'
#' Information on the toxicity of a drug may be available from
#' multiple studies or sources. Furthermore, one may wish to stratify
#' observations within a single study (for example into groups of
#' patients corresponding to different geographic regions, or multiple
#' dosing \code{dose_info} corresponding to different schedules).
#'
#' \code{blrm_exnex} provides tools for robust Bayesian hierarchical
#' modeling to jointly model data from multiple sources. An additional
#' index \eqn{j=1, \ldots, J} on the parameters and observations
#' denotes the \eqn{J} groups. The resulting model allows the DLT rate
#' to differ across the groups. The general \eqn{N}-component model
#' becomes
#'
#' \deqn{ \text{logit} \, \pi_j(d_1,\ldots,d_N) = \text{logit} \Bigl( 1 - \prod_{i = 1}^N ( 1 - \pi_{ij}(d_i) ) \Bigr) + \sum_{k=1}^K \eta_{kj} \, \gamma_{k}(d_1,\ldots,d_N), }
#'
#' for groups \eqn{j = 1,\ldots,J}. The component toxicities
#' \eqn{\pi_{ij}} and interaction terms \eqn{\gamma_{k}} are
#' modelled, as before, through regression. For example,
#' \eqn{\pi_{ij}} could be a logistic regression on \eqn{t(d_i) =
#' \log(d_i/d_i^*)} with intercept and log-slope \eqn{\boldsymbol
#' \theta_{ij}}, and \eqn{\gamma_{k}} regressed with coefficient
#' \eqn{\eta_{kj}} on a product \eqn{\prod_{i\in I(k)} (d_i/d_i^*)}
#' for some subset \eqn{I(k)} of components.
#'
#' Thus, for \eqn{j=1,\ldots,J}, we now have group-specific parameters
#' \eqn{\boldsymbol\theta_{ij} = (\log\, \alpha_{ij}, \log\,
#' \beta_{ij})} and \eqn{\boldsymbol\nu_{j} = (\eta_{1j}, \ldots,
#' \eta_{Kj})} for each component \eqn{i=1,\ldots,N} and interaction
#' \eqn{k=1,\ldots,K}.
#'
#' The structure of the prior on
#' \eqn{(\boldsymbol\theta_{i1},\ldots,\boldsymbol\theta_{iJ})} and
#' \eqn{(\boldsymbol\nu_{1}, \ldots, \boldsymbol\nu_{J})} determines how much
#' information will be shared across groups \eqn{j}. Several modeling
#' choices are available in the function.
#'
#' \itemize{
#'
#' \item \emph{EX (Full exchangeability):} One can assume the
#' parameters are conditionally exchangeable given hyperparameters
#'
#' \deqn{\boldsymbol \theta_{ij} \sim \text{N}\bigl( \boldsymbol \mu_{\boldsymbol \theta i}, \boldsymbol \Sigma_{\boldsymbol \theta i} \bigr), }
#'
#' independently across groups \eqn{j = 1,\ldots, J} and treatment
#' components \eqn{i=1,\ldots,N}. The covariance matrix
#' \eqn{\boldsymbol \Sigma_{\boldsymbol \theta i}} captures the patterns of cross-group
#' heterogeneity, and is parametrized with standard deviations
#' \eqn{\boldsymbol \tau_{\boldsymbol\theta i} = (\tau_{\alpha i},
#' \tau_{\beta i})} and the correlation \eqn{\rho_i}. Similarly for the
#' interactions, the fully-exchangeable model is
#'
#' \deqn{\boldsymbol \nu_{j} \sim \text{N}\bigl( \boldsymbol \mu_{\boldsymbol \nu}, \boldsymbol \Sigma_{\boldsymbol \nu} \bigr)}
#'
#' for groups \eqn{j = 1,\ldots, J} and interactions
#' \eqn{k=1,\ldots,K}, and the prior on the covariance matrix
#' \eqn{\boldsymbol \Sigma_{\boldsymbol \nu}} captures the amount of
#' heterogeneity expected in the interaction terms a-priori. The
#' covariance is again parametrized with standard deviations
#' \eqn{(\tau_{\eta 1}, \ldots, \tau_{\eta K})} and its correlation matrix.
#'
#' \item \emph{Differential discounting:} For one or more of the groups \eqn{j=1,\ldots,J},
#' larger deviations of \eqn{\boldsymbol\theta_{ij}} may be expected from the mean
#' \eqn{\boldsymbol\mu_i}, or of the interactions \eqn{\eta_{kj}} from the mean
#' \eqn{\mu_{\eta,k}}. Such differential heterogeneity can be modeled by mapping the groups
#' \eqn{j = 1,\ldots,J} to \emph{strata} through \eqn{s_j \in \{1,\ldots,S\}},
#' and modifying the model specification to
#' \deqn{\boldsymbol \theta_{ij} \sim \text{N}\bigl( \boldsymbol \mu_{\boldsymbol \theta i}, \boldsymbol \Sigma_{\boldsymbol \theta ij} \bigr), }
#' where
#' \deqn{\boldsymbol \Sigma_{\boldsymbol \theta ij} = \left( \begin{array}{cc}
#' \tau^2_{\alpha s_j i} & \rho_i \tau_{\alpha s_j i} \tau_{\beta s_j i}\\
#' \rho_i \tau_{\alpha s_j i} \tau_{\beta s_j i} & \tau^2_{\beta s_j i}
#' \end{array} \right).}
#' For the interactions, the model becomes
#' \deqn{\boldsymbol \nu_{j} \sim \text{N}\bigl( \boldsymbol \mu_{\boldsymbol \nu}, \boldsymbol \Sigma_{\boldsymbol \nu j} \bigr),}
#' where the covariance matrix \eqn{\boldsymbol \Sigma_{\boldsymbol \nu j}} is modelled as stratum specific standard deviations \eqn{(\tau_{\eta 1 s_j}, \ldots, \tau_{\eta K s_j})} and a stratum independent correlation matrix.
#' Each stratum
#' \eqn{s=1,\ldots,S} then corresponds to its own set of standard deviations \eqn{\tau} leading to different discounting per stratum.
#' Independent priors are specified for the component parameters
#' \eqn{\tau_{\alpha s i}} and \eqn{\tau_{\beta s i}} and
#' for the interaction parameters \eqn{\tau_{\eta s k}} for each stratum
#' \eqn{s=1,\ldots,S}. Inference for strata \eqn{s} where the prior is centered
#' on larger values of the \eqn{\tau} parameters will exhibit less shrinkage
#' towards the the means, \eqn{\boldsymbol\mu_{\boldsymbol \theta i}} and \eqn{\boldsymbol \mu_{\boldsymbol \nu}} respectively.
#'
#'
#' \item \emph{EXNEX (Partial exchangeability):} Another mechansim for increasing
#' robustness is to introduce mixture priors for the group-specific parameters,
#' where one mixture component is shared across groups, and the other is group-specific.
#' The result, known as an EXchangeable-NonEXchangeable (EXNEX) type prior, has a form
#'
#' \deqn{\boldsymbol \theta_{ij} \sim p_{\boldsymbol \theta ij}\, \text{N}\bigl( \boldsymbol \mu_{\boldsymbol \theta i}, \boldsymbol \Sigma_{\boldsymbol \theta i} \bigr) +(1-p_{\boldsymbol \theta ij})\, \text{N}\bigl(\boldsymbol m_{\boldsymbol \theta ij}, \boldsymbol S_{\boldsymbol \theta ij}\bigr)}
#'
#' when applied to the treatment-component parameters, and
#'
#' \deqn{\boldsymbol \nu_{kj} \sim p_{\boldsymbol \nu_{kj}} \,\text{N}\bigl(\mu_{\boldsymbol \nu}, \boldsymbol \Sigma_{\boldsymbol \nu}\bigr)_k + (1-p_{\boldsymbol \nu_{kj}})\, \text{N}(m_{\boldsymbol \nu_{kj}}, s^2_{\boldsymbol \nu_{kj}})}
#'
#' when applied to the interaction parameters. The \emph{exchangeability weights}
#' \eqn{p_{\boldsymbol \theta ij}} and \eqn{p_{\boldsymbol \nu_{kj}}} are fixed constants in the interval \eqn{[0,1]}
#' that control the degree to which inference for group \eqn{j} is informed
#' by the exchangeable mixture components. Larger values for the
#' weights correspond to greater exchange of information, while smaller values
#' increase robustness in case of outlying observations in individual groups
#' \eqn{j}.
#'
#'
#' }
#'
#'
#' @return The function returns a S3 object of type
#'     \code{blrmfit}.
#'
#'
#' @template ref-mac
#' @template ref-exnex
#' @template ref-critical_aspects
#' @template ref-bayesindustry
#'
#' @template start-example
#' @examples
#' # fit an example model. See documentation for "combo3" example
#' example_model("combo3")
#'
#' # print a summary of the prior
#' prior_summary(blrmfit, digits = 3)
#'
#' # print a summary of the posterior (model parameters)
#' print(blrmfit)
#'
#' # summary of posterior for DLT rate by dose for observed covariate levels
#' summ <- summary(blrmfit, interval_prob = c(0, 0.16, 0.33, 1))
#' print(cbind(hist_combo3, summ))
#'
#' # summary of posterior for DLT rate by dose for new set of covariate levels
#' newdata <- expand.grid(
#'   stratum_id = "BID", group_id = "Combo",
#'   drug_A = 400, drug_B = 800, drug_C = c(320, 400, 600, 800),
#'   stringsAsFactors = FALSE
#' )
#' summ_pred <- summary(blrmfit, newdata = newdata, interval_prob = c(0, 0.16, 0.33, 1))
#' print(cbind(newdata, summ_pred))
#'
#' # update the model after observing additional data
#' newdata$num_patients <- rep(3, nrow(newdata))
#' newdata$num_toxicities <- c(0, 1, 2, 2)
#' library(dplyr)
#' blrmfit_new <- update(blrmfit,
#'   data = rbind(hist_combo3, newdata) %>%
#'     arrange(stratum_id, group_id)
#' )
#'
#' # updated posterior summary
#' summ_upd <- summary(blrmfit_new, newdata = newdata, interval_prob = c(0, 0.16, 0.33, 1))
#' print(cbind(newdata, summ_upd))
#' @template stop-example
#'
#' @export
blrm_exnex <- function(
  formula,
  data,
  prior_EX_mu_comp,
  prior_EX_mu_mean_comp,
  prior_EX_mu_sd_comp,

  prior_EX_tau_comp,
  prior_EX_tau_mean_comp,
  prior_EX_tau_sd_comp,

  prior_EX_corr_eta_comp,

  prior_EX_mu_inter,
  prior_EX_mu_mean_inter,
  prior_EX_mu_sd_inter,

  prior_EX_tau_inter,
  prior_EX_tau_mean_inter,
  prior_EX_tau_sd_inter,

  prior_EX_corr_eta_inter,
  prior_is_EXNEX_inter,
  prior_is_EXNEX_comp,
  prior_EX_prob_comp,
  prior_EX_prob_inter,

  prior_NEX_mu_comp,
  prior_NEX_mu_mean_comp,
  prior_NEX_mu_sd_comp,

  prior_NEX_mu_inter,
  prior_NEX_mu_mean_inter,
  prior_NEX_mu_sd_inter,
  prior_tau_dist,

  sample_map = FALSE,

  iter = getOption("OncoBayes2.MC.iter", 2000),
  warmup = getOption("OncoBayes2.MC.warmup", 1000),
  save_warmup = getOption("OncoBayes2.MC.save_warmup", TRUE),
  thin = getOption("OncoBayes2.MC.thin", 1),
  init = getOption("OncoBayes2.MC.init", 0.5),
  chains = getOption("OncoBayes2.MC.chains", 4),
  cores = getOption("mc.cores", 1L),
  control = getOption("OncoBayes2.MC.control", list()),
  backend = getOption("OncoBayes2.MC.backend", "rstan"),
  prior_PD = FALSE,
  verbose = FALSE
) {
  call <- match.call()

  if (missing(data)) {
    data <- environment(formula)
  }

  mf <- match.call(expand.dots = FALSE)
  m <- match(c("formula", "data", "na.action"), names(mf), 0)
  mf <- mf[c(1, m)]

  f <- Formula::Formula(formula)
  mf[[1]] <- as.name("model.frame")
  ## mf[[1]] <- quote(stats::model.frame)
  mf$formula <- f
  mf <- eval(mf, parent.frame())

  num_rhs_terms <- length(f)[2]
  idx_group_term <- num_rhs_terms
  has_inter <- TRUE
  idx_inter_term <- num_rhs_terms - 1

  mt <- attr(mf, "terms")

  for (i in seq_len(num_rhs_terms)) {
    tc <- terms(f, rhs = i)
    nt <- length(tc) - 1
    if (nt == 2 && attr(tc, "intercept") == 1) {
      num_comp <- i
    } else {
      break
    }
  }

  if (num_comp == 1) {
    has_inter <- FALSE
    idx_inter_term <- 0
  }

  ## check if any of the deprecated non-mixture arguments have been
  ## used
  non_mixture_prior_args <- c(
    "prior_EX_mu_mean_comp",
    "prior_EX_mu_sd_comp",
    "prior_EX_tau_mean_comp",
    "prior_EX_tau_sd_comp",
    "prior_EX_mu_mean_inter",
    "prior_EX_mu_sd_inter",
    "prior_EX_tau_mean_inter",
    "prior_EX_tau_sd_inter",
    "prior_NEX_mu_mean_comp",
    "prior_NEX_mu_sd_comp",
    "prior_NEX_mu_mean_inter",
    "prior_NEX_mu_sd_inter"
  )

  mixture_prior_args <- c(
    "prior_EX_mu_comp",
    "prior_EX_tau_comp",
    "prior_EX_mu_inter",
    "prior_EX_tau_inter",
    "prior_NEX_mu_comp",
    "prior_NEX_mu_inter"
  )

  prior_args <- grep("^prior", names(call), value = TRUE)

  if ("prior_EX_mu_mean_comp" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_mu_mean_comp)",
      "blrm_exnex(prior_EX_mu_comp)"
    )
  if ("prior_EX_mu_sd_comp" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_mu_sd_comp)",
      "blrm_exnex(prior_EX_mu_comp)"
    )

  if ("prior_EX_mu_mean_inter" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_mu_mean_inter)",
      "blrm_exnex(prior_EX_mu_inter)"
    )
  if ("prior_EX_mu_sd_inter" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_mu_sd_inter)",
      "blrm_exnex(prior_EX_mu_inter)"
    )

  if ("prior_EX_tau_mean_comp" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_tau_mean_comp)",
      "blrm_exnex(prior_EX_tau_comp)"
    )
  if ("prior_EX_tau_sd_comp" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_tau_sd_comp)",
      "blrm_exnex(prior_EX_tau_comp)"
    )

  if ("prior_EX_tau_mean_inter" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_tau_mean_inter)",
      "blrm_exnex(prior_EX_tau_inter)"
    )
  if ("prior_EX_tau_sd_inter" %in% prior_args)
    deprecate_warn(
      "0.9.0",
      "blrm_exnex(prior_EX_tau_sd_inter)",
      "blrm_exnex(prior_EX_tau_inter)"
    )

  ## Determine if any of the old arguments are present
  if (any(prior_args %in% non_mixture_prior_args)) {
    ## in this case we check that none of the new arguments are used
    ## as we do not permit a mix and match of new and old
    deprecated_args <- intersect(prior_args, non_mixture_prior_args)
    assert_that(
      !any(mixture_prior_args %in% prior_args),
      msg = paste0(
        "Found deprecated (",
        paste0(deprecated_args, collapse = ", "),
        ") and not deprecated arguments for specification of the prior.\nPlease update all prior arguments to use mixture prior arguments."
      )
    )
  }
  use_non_mixture_prior_args <- any(prior_args %in% non_mixture_prior_args)

  ## we only support a single LHS
  assert_that(length(f)[1] == 1)
  ## check that we have an overall intercept for all components
  for (i in seq_len(num_comp)) {
    assert_that(
      attr(terms(f, rhs = i), "intercept") == 1,
      msg = "Intercept must be present for all components."
    )
  }

  ## the interaction model must not have an intercept
  if (has_inter) {
    assert_that(
      attr(terms(f, rhs = idx_inter_term), "intercept") == 0,
      msg = "No intercept must be present for the interaction model."
    )
  }

  y <- model.response(mf)
  ## model response must be a two-column matrix (responder,
  ## non-responder)
  assert_matrix(y, ncols = 2, any.missing = FALSE)
  nr <- array(y[, 2])
  r <- array(y[, 1])
  n <- r + nr

  num_obs <- length(r)

  group_index_term <- model.part(f, data = mf, rhs = idx_group_term)

  if (ncol(group_index_term) > 2) {
    stop(
      "Grouping factor must have at most two terms (study index and optionally a stratum)."
    )
  }

  if (ncol(group_index_term) == 0) {
    stop("Grouping factor must have at least one term (study index).")
  }

  if (ncol(group_index_term) == 2) {
    idx_group_index <- 2
    idx_strata_index <- 1
  } else {
    idx_group_index <- 1
    idx_strata_index <- NA
  }

  group_fct <- group_index_term[, idx_group_index]
  if (!is.factor(group_fct)) {
    group_fct <- factor(group_fct)
  }
  group_index <- as.integer(unclass(group_fct))
  num_groups <- nlevels(group_fct)
  assert_that(NROW(group_index) == num_obs)

  ## per group we must have an assignment to a tau stratum
  if (is.na(idx_strata_index)) {
    strata_fct <- rep(1, num_obs)
  } else {
    strata_fct <- group_index_term[, idx_strata_index]
  }
  if (!is.factor(strata_fct)) {
    strata_fct <- factor(strata_fct)
  }
  strata_index <- array(as.integer(unclass(strata_fct)))
  num_strata <- nlevels(strata_fct)
  assert_that(NROW(strata_index) == num_obs)

  ## ensure correct nesting of groups into strata such that any
  ## group is in only in one stratum at most
  .validate_group_stratum_nesting(group_index, strata_index)

  ## obtain group => stratum map ordered by group id
  group_strata <- data.frame(group_index = seq_len(num_groups)) %>%
    left_join(
      unique(data.frame(
        group_index = group_index,
        strata_index = strata_index
      )),
      by = "group_index"
    )
  ## since we allow groups to be specified through the factors,
  ## there can be groups without a stratum defined in case one level
  ## of the group is not present in the data. These need to be
  ## assigned to a default stratum which is stratum 1 at the moment.
  if (any(is.na(group_strata$strata_index))) {
    group_strata_undef <- which(is.na(group_strata$strata_index))
    message(
      "Info: The group(s) ",
      paste(levels(group_fct)[group_strata_undef], collapse = ", "),
      " have undefined strata. Assigning first stratum ",
      levels(strata_fct)[1],
      "."
    )
    group_strata$strata_index[is.na(group_strata$strata_index)] <- 1
  }
  group_index <- array(group_index)

  X <- .get_X(f, mf, num_comp, has_inter, idx_inter_term)
  X_comp <- X$comp
  X_comp_cols <- X$comp_cols
  X_inter <- X$inter

  num_inter <- ncol(X_inter)
  has_inter <- num_inter > 0

  ## check if the EXNEX part of the model is turned off
  assert_number(prior_tau_dist, lower = 0, upper = 2, null.ok = TRUE)
  stan_prior_tau_dist <- prior_tau_dist
  if (is.null(prior_tau_dist)) {
    ## in this case we do not allow for any of the EXNEX arguments for
    ## the heterogeneity or the EXNEX probabilities as we set these
    ## here such that this bit of the model is turned off.
    hierarchical_args <- c(
      "prior_EX_tau_comp",
      "prior_EX_tau_mean_comp",
      "prior_EX_tau_sd_comp",
      "prior_EX_tau_inter",
      "prior_EX_tau_mean_inter",
      "prior_EX_tau_sd_inter",
      "prior_is_EXNEX_comp",
      "prior_is_EXNEX_inter"
    )
    assert_that(
      sum(names(call) %in% hierarchical_args) == 0,
      msg = paste0(
        "Hierarchical model structure disabled (prior_tau_dist=NULL), but found disabled arguments: ",
        paste(intersect(names(call), hierarchical_args), collapse = ", ")
      )
    )

    if (num_groups != 1) {
      message(
        "Hierarchical model structure disabled, but found more than one group which will be pooled."
      )
    }
    if (num_strata != 1) {
      message(
        "Hierarchical model structure disabled, but found more than one stratum which will be ignored."
      )
    }

    prior_EX_tau_comp <- replicate(
      num_strata,
      replicate(num_comp, mixmvnorm(c(1, c(0, 0), diag(c(1, 1)))), FALSE),
      FALSE
    )
    if (has_inter) {
      prior_EX_tau_inter <- replicate(
        num_strata,
        mixmvnorm(c(1, rep(0, num_inter), diag(1, num_inter, num_inter))),
        FALSE
      )
    }
    prior_is_EXNEX_comp <- rep(FALSE, num_comp)
    prior_is_EXNEX_inter <- rep(FALSE, num_inter)
    prior_EX_prob_comp <- array(1, dim = c(num_groups, num_comp))
    prior_EX_prob_inter <- array(1, dim = c(num_groups, num_inter))
    stan_prior_tau_dist <- 0
  }

  ## note: most of the consistency checks of the prior are left for
  ## Stan

  if (use_non_mixture_prior_args) {
    assert_matrix(
      prior_EX_mu_mean_comp,
      any.missing = FALSE,
      nrows = num_comp,
      ncols = 2
    )
    assert_matrix(
      prior_EX_mu_sd_comp,
      any.missing = FALSE,
      nrows = num_comp,
      ncols = 2
    )

    prior_EX_mu_comp <- .comp_args2mix(
      prior_EX_mu_mean_comp,
      prior_EX_mu_sd_comp
    )
  }

  if (!is.list(prior_EX_mu_comp) & num_comp == 1) {
    prior_EX_mu_comp <- list(prior_EX_mu_comp)
  }

  .assert_list_mixmvnorm(prior_EX_mu_comp, len = num_comp, dim = 2)
  standata_prior_EX_mu_comp <- .comp_mix2stan(
    prior_EX_mu_comp,
    "prior_EX_mu_comp_"
  )

  ## in case a single stratum is used, the user can input a matrix
  if (use_non_mixture_prior_args) {
    if (num_strata == 1) {
      if (is.matrix(prior_EX_tau_mean_comp)) {
        prior_EX_tau_mean_comp <- array(
          prior_EX_tau_mean_comp,
          c(1, dim(prior_EX_tau_mean_comp))
        )
      }
      if (is.matrix(prior_EX_tau_sd_comp)) {
        prior_EX_tau_sd_comp <- array(
          prior_EX_tau_sd_comp,
          c(1, dim(prior_EX_tau_sd_comp))
        )
      }
    }
    assert_array(prior_EX_tau_mean_comp, any.missing = FALSE, d = 3)
    assert_array(prior_EX_tau_sd_comp, any.missing = FALSE, d = 3)
    assert_that(
      all(dim(prior_EX_tau_mean_comp) == c(num_strata, num_comp, 2)),
      msg = "prior_EX_tau_mean_comp must have dimensionality of num_strata x num_comp x 2.\nIn case of only one stratum a matrix of num_comp x 2 is sufficient."
    )
    assert_that(
      all(dim(prior_EX_tau_sd_comp) == c(num_strata, num_comp, 2)),
      msg = "prior_EX_tau_sd_comp must have dimensionality of num_strata x num_comp x 2.\nIn case of only one stratum a matrix of num_comp x 2 is sufficient."
    )

    prior_EX_tau_comp <- list()
    for (s in 1:num_strata) {
      prior_EX_tau_comp[[s]] <- .comp_args2mix(
        adrop(prior_EX_tau_mean_comp[s, , , drop = FALSE], 1),
        adrop(prior_EX_tau_sd_comp[s, , , drop = FALSE], 1)
      )
    }
  }

  if (num_strata == 1) {
    if (num_comp == 1 & .is_mixmvnorm(prior_EX_tau_comp)) {
      prior_EX_tau_comp <- list(list(prior_EX_tau_comp))
    }
    if (.is_list_mixmvnorm(prior_EX_tau_comp)) {
      prior_EX_tau_comp <- list(prior_EX_tau_comp)
    }
  }

  assert_list(prior_EX_tau_comp, any.missing = FALSE, len = num_strata)
  ## for each stratum we require a list of bivariate tau priors
  for (i in seq_len(num_strata)) {
    .assert_list_mixmvnorm(prior_EX_tau_comp[[i]], num_comp, 2)
  }

  standata_prior_EX_tau_comp_strata <- lapply(
    prior_EX_tau_comp,
    .comp_mix2stan,
    prefix = "prior_EX_tau_comp_"
  )
  standata_prior_EX_tau_comp <- list()
  for (item in names(standata_prior_EX_tau_comp_strata[[1]])) {
    standata_prior_EX_tau_comp[[item]] <- abind(
      lapply(standata_prior_EX_tau_comp_strata, "[[", item),
      along = 0
    )
  }

  if (missing(prior_EX_corr_eta_comp)) {
    prior_EX_corr_eta_comp <- rep(1.0, times = num_comp)
  }
  assert_numeric(
    prior_EX_corr_eta_comp,
    lower = 0,
    finite = TRUE,
    any.missing = FALSE,
    len = num_comp
  )

  if (use_non_mixture_prior_args) {
    if (!has_inter & missing(prior_EX_mu_mean_inter)) {
      prior_EX_mu_mean_inter <- array(0, dim = 0)
    }
    if (!has_inter & missing(prior_EX_mu_sd_inter)) {
      prior_EX_mu_sd_inter <- array(0, dim = 0)
    }
    assert_double(prior_EX_mu_mean_inter, any.missing = FALSE, len = num_inter)
    assert_double(
      prior_EX_mu_sd_inter,
      any.missing = FALSE,
      len = num_inter,
      lower = 0
    )

    if (has_inter) {
      prior_EX_mu_inter <- .args2mix(
        as.vector(prior_EX_mu_mean_inter),
        as.vector(prior_EX_mu_sd_inter)
      )
    }
  }

  if (!missing(prior_EX_mu_inter)) {
    .assert_mixmvnorm(prior_EX_mu_inter, dim = num_inter, null.ok = TRUE)
  }

  if (has_inter) {
    standata_prior_EX_mu_inter <- .priormix2stan(
      prior_EX_mu_inter,
      "prior_EX_mu_inter_"
    )
  } else {
    standata_prior_EX_mu_inter <- .standata_empty_mixture(
      num_inter,
      "prior_EX_mu_inter_"
    )
  }

  if (use_non_mixture_prior_args) {
    ## old argument case
    if (!has_inter & missing(prior_EX_tau_mean_inter)) {
      prior_EX_tau_mean_inter <- matrix(1, nrow = num_strata, ncol = num_inter)
    }
    if (!has_inter & missing(prior_EX_tau_sd_inter)) {
      prior_EX_tau_sd_inter <- matrix(1, nrow = num_strata, ncol = num_inter)
    }
    assert_matrix(
      prior_EX_tau_mean_inter,
      any.missing = FALSE,
      nrows = num_strata,
      ncols = num_inter
    )
    assert_matrix(
      prior_EX_tau_sd_inter,
      any.missing = FALSE,
      nrows = num_strata,
      ncols = num_inter
    )

    if (has_inter) {
      prior_EX_tau_inter <- list()
      for (s in 1:num_strata) {
        prior_EX_tau_inter[[s]] <- .args2mix(
          prior_EX_tau_mean_inter[s, , drop = TRUE],
          prior_EX_tau_sd_inter[s, , drop = TRUE]
        )
      }
      standata_prior_EX_tau_inter_strata <- lapply(
        prior_EX_tau_inter,
        .priormix2stan,
        prefix = "prior_EX_tau_inter_"
      )
    } else {
      standata_prior_EX_tau_inter_strata <- replicate(
        num_strata,
        .standata_empty_mixture(num_inter, "prior_EX_tau_inter_"),
        FALSE
      )
    }
  } else {
    ## new case with mixtures
    if (has_inter) {
      if (
        num_strata == 1 &
          !missing(prior_EX_tau_inter) &
          .is_mixmvnorm(prior_EX_tau_inter)
      ) {
        prior_EX_tau_inter <- list(prior_EX_tau_inter)
      }
      .assert_list_mixmvnorm(
        prior_EX_tau_inter,
        len = num_strata,
        dim = num_inter
      )
      standata_prior_EX_tau_inter_strata <- lapply(
        prior_EX_tau_inter,
        .priormix2stan,
        prefix = "prior_EX_tau_inter_"
      )
    } else {
      standata_prior_EX_tau_inter_strata <- replicate(
        num_strata,
        .standata_empty_mixture(num_inter, "prior_EX_tau_inter_"),
        FALSE
      )
    }
  }

  standata_prior_EX_tau_inter <- list()
  for (item in names(standata_prior_EX_tau_inter_strata[[1]])) {
    standata_prior_EX_tau_inter[[item]] <- abind(
      lapply(standata_prior_EX_tau_inter_strata, "[[", item),
      along = 0
    )
  }
  standata_prior_EX_tau_inter$prior_EX_tau_inter_Nc <- array(
    standata_prior_EX_tau_inter$prior_EX_tau_inter_Nc
  )

  if (!has_inter & missing(prior_EX_prob_inter)) {
    prior_EX_prob_inter <- matrix(1, nrow = num_groups, ncol = num_inter)
  }
  assert_matrix(
    prior_EX_prob_comp,
    any.missing = FALSE,
    nrows = num_groups,
    ncols = num_comp
  )
  assert_matrix(
    prior_EX_prob_inter,
    any.missing = FALSE,
    nrows = num_groups,
    ncols = num_inter
  )

  if (missing(prior_EX_corr_eta_inter)) {
    prior_EX_corr_eta_inter <- 1.0
  }
  assert_number(prior_EX_corr_eta_inter, lower = 0, finite = TRUE)

  if (use_non_mixture_prior_args) {
    if (missing(prior_NEX_mu_mean_comp)) {
      prior_NEX_mu_mean_comp <- prior_EX_mu_mean_comp
    }
    if (missing(prior_NEX_mu_sd_comp)) {
      prior_NEX_mu_sd_comp <- prior_EX_mu_sd_comp
    }
  } else {
    if (missing(prior_NEX_mu_comp)) {
      prior_NEX_mu_comp <- prior_EX_mu_comp
    }
  }

  if (use_non_mixture_prior_args) {
    assert_matrix(
      prior_NEX_mu_mean_comp,
      any.missing = FALSE,
      nrows = num_comp,
      ncols = 2
    )
    assert_matrix(
      prior_NEX_mu_sd_comp,
      any.missing = FALSE,
      nrows = num_comp,
      ncols = 2
    )

    prior_NEX_mu_comp <- .comp_args2mix(
      prior_NEX_mu_mean_comp,
      prior_NEX_mu_sd_comp
    )
  }

  standata_prior_NEX_mu_comp <- .comp_mix2stan(
    prior_NEX_mu_comp,
    "prior_NEX_mu_comp_"
  )

  if (use_non_mixture_prior_args) {
    if (missing(prior_NEX_mu_mean_inter)) {
      prior_NEX_mu_mean_inter <- prior_EX_mu_mean_inter
    }
    if (missing(prior_NEX_mu_sd_inter)) {
      prior_NEX_mu_sd_inter <- prior_EX_mu_sd_inter
    }
  } else {
    if (missing(prior_NEX_mu_inter) & !missing(prior_EX_mu_inter)) {
      prior_NEX_mu_inter <- prior_EX_mu_inter
    }
  }

  if (use_non_mixture_prior_args) {
    assert_numeric(
      prior_NEX_mu_mean_inter,
      any.missing = FALSE,
      len = num_inter
    )
    assert_numeric(
      prior_NEX_mu_sd_inter,
      any.missing = FALSE,
      len = num_inter,
      lower = 0
    )

    if (has_inter) {
      prior_NEX_mu_inter <- .args2mix(
        as.vector(prior_NEX_mu_mean_inter),
        as.vector(prior_NEX_mu_sd_inter)
      )
    }
  }

  if (has_inter) {
    standata_prior_NEX_mu_inter <- .priormix2stan(
      prior_NEX_mu_inter,
      "prior_NEX_mu_inter_"
    )
  } else {
    standata_prior_NEX_mu_inter <- .standata_empty_mixture(
      num_inter,
      "prior_NEX_mu_inter_"
    )
  }

  if (missing(prior_is_EXNEX_comp)) {
    prior_is_EXNEX_comp <- apply(prior_EX_prob_comp < 1, 2, any)
  } else {
    assert_that(
      !any(!prior_is_EXNEX_comp & apply(prior_EX_prob_comp < 1, 2, any)),
      msg = paste(
        "At least one drug component has",
        "prior_is_ENXEX_comp = FALSE, but",
        "prior_EX_prob_comp < 1 for one or more group_id's.",
        "For these component(s), if an EXNEX prior is desired",
        "set prior_is_EXNEX_comp = TRUE. Otherwise, if a",
        "fully exchangeable prior is desired, set",
        "prior_is_EX_prob_comp = 1."
      )
    )
    if (any((prior_is_EXNEX_comp & apply(prior_EX_prob_comp == 1, 2, all)))) {
      warning(paste(
        "At least one drug component has",
        "prior_is_ENXEX_comp = TRUE, but",
        "prior_EX_prob_comp = 1 for every group_id.",
        "The sampler will be more efficient if",
        "prior_is_EXNEX_comp is set to FALSE for those",
        "component(s)."
      ))
    }
  }

  if (missing(prior_is_EXNEX_inter)) {
    prior_is_EXNEX_inter <- apply(prior_EX_prob_inter < 1, 2, any)
  } else {
    assert_that(
      !any(!prior_is_EXNEX_inter & apply(prior_EX_prob_inter < 1, 2, any)),
      msg = paste(
        "At least one drug component has",
        "prior_is_ENXEX_inter = FALSE, but",
        "prior_EX_prob_inter < 1 for one or more group_id's.",
        "For these interaction(s), if an EXNEX prior is desired",
        "set prior_is_EXNEX_inter = TRUE. Otherwise, if a",
        "fully exchangeable prior is desired, set",
        "prior_is_EX_prob_inter = 1."
      )
    )
    if (any((prior_is_EXNEX_inter & apply(prior_EX_prob_inter == 1, 2, all)))) {
      warning(paste(
        "At least one interaction prior has",
        "prior_is_ENXEX_inter = TRUE, but",
        "prior_EX_prob_inter = 1 for every group_id.",
        "The sampler will be more efficient if",
        "prior_is_EXNEX_inter is set to FALSE for those",
        "interaction(s)."
      ))
    }
  }
  assert_logical(prior_is_EXNEX_comp, any.missing = FALSE, len = num_comp)
  assert_logical(prior_is_EXNEX_inter, any.missing = FALSE, len = num_inter)

  assert_choice(backend, c("rstan", "cmdstanr"))

  assert_number(stan_prior_tau_dist, lower = 0, upper = 2)

  assert_logical(sample_map, any.missing = FALSE, len = 1)

  assert_logical(prior_PD, any.missing = FALSE, len = 1)

  stan_data <- c(
    list(
      num_obs = num_obs,
      r = r,
      nr = nr,
      num_comp = num_comp,
      num_inter = num_inter,
      ## design matrices
      X_comp = X_comp,
      X_inter = X_inter,
      ## group and strata mapping
      num_groups = num_groups,
      num_strata = num_strata,
      group = group_index,
      stratum = strata_index,
      group_stratum_cid = array(group_strata$strata_index),
      ## priors
      prior_tau_dist = stan_prior_tau_dist,
      prior_EX_prob_comp = prior_EX_prob_comp,
      prior_EX_prob_inter = prior_EX_prob_inter,
      prior_EX_corr_eta_comp = array(prior_EX_corr_eta_comp, num_comp),
      prior_EX_corr_eta_inter = prior_EX_corr_eta_inter,
      prior_is_EXNEX_comp = array(1 * prior_is_EXNEX_comp, num_comp),
      prior_is_EXNEX_inter = array(1 * prior_is_EXNEX_inter, num_inter),
      sample_map = 1 * sample_map,
      prior_PD = 1 * prior_PD
    ),
    standata_prior_EX_mu_comp,
    standata_prior_EX_tau_comp,
    standata_prior_EX_mu_inter,
    standata_prior_EX_tau_inter,
    standata_prior_NEX_mu_comp,
    standata_prior_NEX_mu_inter
  )

  ## return(stan_data)

  control_sampling <- modifyList(
    list(adapt_delta = 0.99, stepsize = 0.1),
    control
  )

  ## whenever no warmup is saved, we also drop all unneccessary parameters as
  ## - raw parameters
  ## - correlation cholesky factors
  ## - NEX / EX paramters (only store by-group estimates)
  if (save_warmup) {
    exclude_pars <- c()
  } else {
    exclude_pars <- c(
      "log_beta_raw",
      "eta_raw",
      "tau_log_beta_raw",
      "tau_eta_raw",
      "L_corr_log_beta",
      "L_corr_eta",
      "beta",
      "eta",
      "beta_EX_prob",
      "eta_EX_prob"
    )
  }

  stan_msg <- character(0)
  if (backend == "rstan") {
    ## SW: the below call can trigger spurious warnings whenever tau
    ## is fixed to some value. In that case the sampler diagnostic
    ## functions in rstan are fooled to think that there is an issue
    ## with Rhat. Consider swallowing the warnings with
    ## suppressWarnings and instead use the posterior package to emit
    ## warnings wrt to sampling issues. The posterior package can
    ## detect if some parameters are constant.
    stan_msg <- capture.output(
      stanfit <- rstan::sampling(
        stanmodels$blrm_exnex,
        data = stan_data,
        warmup = warmup,
        iter = iter,
        chains = chains,
        cores = cores,
        thin = thin,
        init = init,
        control = control_sampling,
        algorithm = "NUTS",
        open_progress = FALSE,
        save_warmup = save_warmup,
        include = FALSE,
        pars = exclude_pars
      ),
      split = verbose
    )
    ## in sampling mode the mode is set to zero; anything else
    ## indicates an error
    stanfit_status_ok <- attr(stanfit, "mode") == 0
  } else if (backend == "cmdstanr") {
    if (!requireNamespace("cmdstanr", quietly = TRUE)) {
      stop("Please install the cmdstanr package.")
    }

    if (is.null(pkg_env$.cmdstanr_blrm_exnex_model)) {
      ## stan_model_file <- tempfile("blrm_exnex-", fileext=".stan")
      ## cat(.get_cmdstan_model(), file=stan_model_file)
      stan_model_file <- cmdstanr::write_stan_file(.get_cmdstan_model())
      ## good optimization options for cmdstanr installation are
      ## STAN_NO_RANGE_CHECKS=TRUE
      ## STAN_THREADS=FALSE (not so important)
      ## CXXFLAGS_OPTIM=-mtune=native -march=native -DNDEBUG -DEIGEN_NO_DEBUG
      ## CXXFLAGS_OPTIM_TBB=-mtune=native -march=native -DNDEBUG -DEIGEN_NO_DEBUG
      ## CXXFLAGS_OPTIM_SUNDIALS=-mtune=native -march=native -DNDEBUG -DEIGEN_NO_DEBUG
      pkg_env$.cmdstanr_blrm_exnex_model <- cmdstanr::cmdstan_model(
        stan_model_file,
        quiet = !verbose
      )
    }

    ## serialize out data to json file and manually delete after
    ## inference (cmdstanr does not do this instantly)
    stan_data_file <- tempfile(pattern = "stan-data-", fileext = ".json")
    cmdstanr::write_stan_json(stan_data, stan_data_file)
    stan_msg <- capture.output(
      cmdstanfit <- pkg_env$.cmdstanr_blrm_exnex_model$sample(
        data = stan_data_file,
        iter_warmup = warmup,
        iter_sampling = iter - warmup,
        seed = sample.int(.Machine$integer.max, 1),
        chains = chains,
        parallel_chains = cores,
        thin = thin,
        init = init,
        adapt_delta = control_sampling$adapt_delta,
        step_size = control_sampling$stepsize,
        term_buffer = control_sampling$adapt_term_buffer,
        init_buffer = control_sampling$adapt_init_buffer,
        window = control_sampling$adapt_window,
        max_treedepth = control_sampling$max_treedepth,
        metric = control_sampling$metric,
        save_warmup = save_warmup
      ),
      split = verbose
    )

    ## note: this will not exclude any variables as this is not
    ## possible, maybe we can prune the stanfit object directly??
    stanfit <- brms::read_csv_as_stanfit(
      files = cmdstanfit$output_files(),
      model = pkg_env$.cmdstanr_blrm_exnex_model
    )

    stanfit_status_ok <- attr(stanfit, "mode") == 0

    unlink(stan_data_file)
  }

  if (!stanfit_status_ok) {
    stop(
      "Calling Stan failed. Sampler output:\n",
      paste0(stan_msg, collapse = "\n")
    )
  }

  ## label parameters of stanfit object
  labels <- list()
  labels$param_log_beta <- .make_label_factor(c("intercept", "log_slope"))
  labels$param_beta <- .make_label_factor(c("intercept", "slope"))
  labels$component <- .make_label_factor(.abbreviate_label(sapply(
    X_comp_cols,
    "[",
    2
  )))
  stanfit <- .label_index(
    stanfit,
    "mu_log_beta",
    labels$component,
    labels$param_log_beta
  )
  stanfit <- .label_index(
    stanfit,
    "tau_log_beta",
    strata_fct,
    labels$component,
    labels$param_log_beta
  )
  stanfit <- .label_index(stanfit, "rho_log_beta", labels$component)
  stanfit <- .label_index(
    stanfit,
    "beta_group",
    group_fct,
    labels$component,
    labels$param_beta
  )
  if (save_warmup) {
    stanfit <- .label_index(
      stanfit,
      "beta_EX_prob",
      group_fct,
      labels$component
    )
  }
  stanfit <- .label_index(stanfit, "log_lik_group", group_fct)
  if (sample_map) {
    stanfit <- .label_index(
      stanfit,
      "map_log_beta",
      strata_fct,
      labels$component,
      labels$param_log_beta
    )
  }

  if (has_inter) {
    labels$param_eta <- .make_label_factor(.abbreviate_label(colnames(X_inter)))
    stanfit <- .label_index(stanfit, "eta_group", group_fct, labels$param_eta)
    if (save_warmup) {
      stanfit <- .label_index(
        stanfit,
        "eta_EX_prob",
        group_fct,
        labels$param_eta
      )
    }
    stanfit <- .label_index(stanfit, "mu_eta", labels$param_eta)
    stanfit <- .label_index(stanfit, "tau_eta", strata_fct, labels$param_eta)
    stanfit <- .label_index(
      stanfit,
      "Sigma_corr_eta",
      labels$param_eta,
      labels$param_eta
    )
    if (sample_map) {
      stanfit <- .label_index(stanfit, "map_eta", strata_fct, labels$param_eta)
    }
  }

  out <- list(
    call = call,
    group_strata = group_strata,
    standata = stan_data,
    stanfit = stanfit,
    formula = f,
    model = mf,
    terms = mt,
    xlevels = .getXlevels(mt, mf),
    data = data,
    idx_group_term = idx_group_term,
    idx_inter_term = idx_inter_term,
    has_inter = has_inter,
    group_fct = group_fct,
    strata_fct = strata_fct,
    labels = labels,
    stan_msg = stan_msg
  )
  structure(out, class = "blrmfit")
}

#' @keywords internal
.comp_args2mix <- function(prior_mean, prior_sd) {
  num_comp <- nrow(prior_mean)
  prior_comp <- list()
  for (i in 1:num_comp) {
    ## note that for OncoBayes2 we allow for sigma1 or sigma2 being
    ## 0. In this case the cov2cor function called in RBesT will throw
    ## a warning that it cannot convert from a covariance to a
    ## correlation matrix and return NA for the the correlation. This
    ## is why we need to work around here. TODO for RBesT to allow
    ## this so that no more workaround is needed here!!!
    suppressWarnings(
      prior_comp[[i]] <- RBesT::mixmvnorm(c(
        1.0,
        prior_mean[i, 1:2],
        diag(prior_sd[i, 1:2]^2)
      ))
    )
    if (any(prior_sd[i, 1:2] == 0)) {
      prior_comp[[i]][6, ] <- 0
    }
  }
  names(prior_comp) <- paste0("comp_", 1:num_comp)
  return(prior_comp)
}

#' @keywords internal
.args2mix <- function(prior_mean, prior_sd) {
  p <- length(prior_mean)
  ## note that for OncoBayes2 we allow for sigma1 or sigma2 being
  ## 0. In this case the cov2cor function called in RBesT will throw
  ## a warning that it cannot convert from a covariance to a
  ## correlation matrix and return NA for the the correlation. This
  ## is why we need to work around here.
  suppressWarnings(
    mix <- RBesT::mixmvnorm(c(1.0, prior_mean, diag(prior_sd^2, p, p)))
  )
  d <- .mvnormdim(mix[-1, 1])
  for (i in seq_len(d)) {
    if (mix[1 + d + i] == 0) {
      ## the respective standard deviation is 0 => set the correlation
      ## coefficients to 0 which are involved
      for (j in seq_len(d - i)) {
        k <- i + j
        idx <- paste0("rho[", k, ",", i, "]")
        mix[idx, 1] <- 0
      }
    }
  }
  return(mix)
}

#' @keywords internal
.assert_mixmvnorm <- function(
  mix,
  dim,
  .var.name = checkmate::vname(mix),
  null.ok = FALSE
) {
  if (null.ok & is.null(mix)) return(invisible(TRUE))
  assert_class(mix, "mvnormMix")
  if (!missing(dim)) {
    mix_dim <- .mvnormdim(mix[-1, 1])
    assert_that(
      mix_dim == dim,
      msg = paste0(
        "Assertion on '",
        .var.name,
        "' failed: Dimensionality must be ",
        dim,
        ", but found ",
        mix_dim,
        "."
      )
    )
  }
  return(invisible(TRUE))
}

#' @keywords internal
.assert_list_mixmvnorm <- function(list_mix, len = NULL, dim) {
  vn <- checkmate::vname(list_mix)
  assert_list(list_mix, len = len, .var.name = vn)
  checks <- rep.int(FALSE, length(list_mix))
  for (i in seq_len(length(list_mix))) {
    checks[i] <- .assert_mixmvnorm(
      list_mix[[i]],
      dim,
      paste0(vn, "[[", i, "]]")
    )
  }
  return(invisible(checks))
}

#' @keywords internal
.standata_empty_mixture <- function(p, prefix = "prior_") {
  prior_mix <- list()
  prior_mix$Nc <- 0
  prior_mix$w <- array(0, 0)
  prior_mix$m <- array(0, c(0, p))
  prior_mix$sigma <- array(0, c(0, p, p))
  names(prior_mix) <- paste0(prefix, names(prior_mix))
  return(prior_mix)
}

#' @keywords internal
.comp_mix2stan <- function(prior_mix_comp, prefix = "prior_") {
  num_comp <- length(prior_mix_comp)
  prior_mix_comp_Nc <- array(sapply(prior_mix_comp, ncol))
  prior_mix_comp_Nc_max <- max(prior_mix_comp_Nc)
  prior_mix_comp_w <- array(0, c(num_comp, prior_mix_comp_Nc_max))
  prior_mix_comp_m <- array(0, c(num_comp, prior_mix_comp_Nc_max, 2))
  prior_mix_comp_sigma <- array(0, c(num_comp, prior_mix_comp_Nc_max, 2, 2))
  for (i in 1:num_comp) {
    for (j in 1:prior_mix_comp_Nc_max) {
      prior_mix_comp_sigma[i, j, , ] <- diag(c(1, 1))
    }
  }
  for (i in 1:num_comp) {
    for (j in 1:prior_mix_comp_Nc[i]) {
      prior_mix_comp_w[i, j] <- prior_mix_comp[[i]][1, j]
      prior_mix_comp_m[i, j, 1:2] <- prior_mix_comp[[i]][2:3, j]
      comp_s <- prior_mix_comp[[i]][4:5, j]
      comp_rho <- prior_mix_comp[[i]][6, j]
      prior_mix_comp_sigma[i, j, 1:2, 1:2] <- matrix(
        c(
          comp_s[1]^2,
          comp_s[1] * comp_s[2] * comp_rho,
          comp_s[1] * comp_s[2] * comp_rho,
          comp_s[2]^2
        ),
        2,
        2
      )
    }
  }
  res <- list(
    Nc = prior_mix_comp_Nc,
    w = prior_mix_comp_w,
    m = prior_mix_comp_m,
    sigma = prior_mix_comp_sigma
  )
  names(res) <- paste0(prefix, names(res))
  res
}

#' @keywords internal
.is_mixmvnorm <- function(mix) {
  inherits(mix, "mvnormMix")
}

#' @keywords internal
.is_list_mixmvnorm <- function(list_mix) {
  len <- length(list_mix)
  if (len == 0) return(FALSE)
  if (!is.list(list_mix)) return(FALSE)
  all(sapply(list_mix, .is_mixmvnorm))
}

# copied from RBesT
#' @keywords internal
.mvnormdim <- function(mvn) {
  p <- (-3 + sqrt(9 + 8 * length(mvn))) / 2
  assert_integerish(p, lower = 1, any.missing = FALSE, len = 1)
  as.integer(p)
}

#' @keywords internal
.mvnormsigma <- function(mvn) {
  p <- .mvnormdim(mvn)
  n <- length(mvn)
  s <- mvn[(p + 1):(2 * p)]
  Rho <- diag(p)
  Rho[lower.tri(Rho)] <- mvn[(2 * p + 1):n]
  Rho[upper.tri(Rho)] <- t(Rho)[upper.tri(Rho)]
  diag(s, nrow = p) %*% Rho %*% diag(s, nrow = p)
}

#' @keywords internal
.priormix2stan <- function(prior_mix, prefix = "prior_") {
  prior_mix_Nc <- ncol(prior_mix)
  p <- .mvnormdim(prior_mix[-1, 1])
  res <- list()
  res$Nc <- prior_mix_Nc
  res$w <- array(prior_mix[1, ], prior_mix_Nc)
  res$m <- t(array(prior_mix[2:(1 + p), ], c(p, prior_mix_Nc)))
  res$sigma <- array(0.0, c(prior_mix_Nc, p, p))
  for (j in 1:prior_mix_Nc) {
    res$sigma[j, , ] <- .mvnormsigma(prior_mix[-1, j])
  }
  names(res) <- paste0(prefix, names(res))
  res
}


## need to fix some division operators due to abilities in newer Stan
## versions
#' @keywords internal
.get_cmdstan_model <- function() {
  ## model <- strsplit(rstan::get_stancode(stanmodels$blrm_exnex), "\n")[[1]]
  model <- strsplit(stan_model_code, "\n")[[1]]
  line1 <- grep("current / base", model)
  model[line1] <- sub(" / ", " %/% ", model[line1])
  line2 <- grep("\\(left_ind \\+ right_ind\\) / 2", model)
  model[line2] <- sub(" / ", " %/% ", model[line2])
  paste(c(model, ""), collapse = "\n")
}


#' Obtain design matrices.
#' @keywords internal
.get_X <- function(formula, model_frame, num_comp, has_inter, idx_inter_term) {
  ## setup design matrices which must have intercept and slope
  X_comp <- list()
  X_comp_cols <- list()
  for (i in seq_len(num_comp)) {
    X_comp <- c(X_comp, list(model.matrix(formula, model_frame, rhs = i)))
    X_comp_cols <- c(X_comp_cols, list(colnames(X_comp[[i]])))
    assert_matrix(X_comp[[i]], ncols = 2, any.missing = FALSE)
  }
  X_comp <- do.call(abind, c(X_comp, list(along = 0)))

  if (has_inter) {
    X_inter <- model.matrix(formula, model_frame, rhs = idx_inter_term)
  } else {
    X_inter <- model.matrix(~0, model_frame)
  }

  list(comp = X_comp, inter = X_inter, comp_cols = X_comp_cols)
}

#'
#' @describeIn blrm_exnex print function.
#' @template args-prob
#' @method print blrmfit
#' @export
#'
print.blrmfit <- function(x, ..., prob = 0.95, digits = 2) {
  cat(
    "Bayesian Logistic Regression Model with EXchangeability-NonEXchangeability\n\n"
  )
  cat("Number of observations:", x$standata$num_obs, "\n")
  cat("Number of groups      :", x$standata$num_groups, "\n")
  cat("Number of strata      :", x$standata$num_strata, "\n")
  cat("Number of components  :", x$standata$num_comp, "\n")
  cat("Number of interactions:", x$standata$num_inter, "\n")
  cat("EXNEX components      :", sum(x$standata$prior_is_EXNEX_comp), "\n")
  cat("EXNEX interactions    :", sum(x$standata$prior_is_EXNEX_inter), "\n")

  assert_number(prob, lower = 0, upper = 1, finite = TRUE)

  probs <- c(0.5 - prob / 2, 0.5, 0.5 + prob / 2)

  ## dummy definitions to silence R CMD check
  Stratum <- Group <- total <- n_total <- NULL

  cat("\nObservations per group:\n")
  ds <- as.data.frame(table(x$group_fct))
  rownames(ds) <- match(ds$Var1, levels(x$group_fct))
  names(ds) <- c("Group", "n")
  totals <- data.frame(
    Stratum = x$strata_fct,
    Group = x$group_fct,
    total = x$standata$nr + x$standata$r
  ) %>%
    group_by(Stratum, Group) %>%
    dplyr::summarise(n_total = sum(total)) %>%
    ungroup()
  ds <- left_join(ds, totals, by = "Group")
  ds$Stratum <- levels(x$strata_fct)[x$group_strata$strata_index]
  ds$n_total[is.na(ds$n_total)] <- 0
  print(ds)

  cat("\nGroups per stratum:\n")
  si <- levels(x$strata_fct)[x$group_strata$strata_index]
  ds <- as.data.frame(table(si), stringsAsFactors = FALSE)
  names(ds) <- c("Stratum", "Groups")
  ds$Stratum <- factor(ds$Stratum, levels = levels(x$strata_fct))
  ds <- ds[order(ds$Stratum, ds$Groups), ]

  totals_stratum <- totals %>%
    group_by(Stratum) %>%
    dplyr::summarise(n_total = sum(n_total))

  ds <- left_join(ds, totals_stratum, by = "Stratum")
  print(ds)

  comp_idx <- function(labels) {
    inter <- grep("intercept\\]$", labels)
    slope <- grep("slope\\]$", labels)
    list(inter = inter, slope = slope)
  }
  strip_variable <- function(labels) {
    gsub("^([A-Za-z_]+\\[)(.*)\\]$", "\\2", labels)
  }

  cat("\nComponent posterior:\n")
  cat("Population mean posterior mu_log_beta\n")
  mu_log_beta <- summary(
    x$stanfit,
    pars = c("mu_log_beta"),
    probs = probs
  )$summary
  rs <- rownames(mu_log_beta)
  idx <- comp_idx(rs)
  rownames(mu_log_beta) <- gsub(
    "^(.*),intercept|,log_slope$",
    "\\1",
    strip_variable(rs)
  )
  cat("intercept:\n")
  print(mu_log_beta[idx$inter, ], digits = digits)
  cat("log-slope:\n")
  print(mu_log_beta[idx$slope, ], digits = digits)

  cat("\nPopulation heterogeniety posterior tau_log_beta\n")
  tau_log_beta <- summary(
    x$stanfit,
    pars = c("tau_log_beta"),
    probs = probs
  )$summary
  rs <- rownames(tau_log_beta)
  idx <- comp_idx(rs)
  rownames(tau_log_beta) <- gsub(
    "^(.*),intercept|,log_slope$",
    "\\1",
    strip_variable(rs)
  )
  cat("intercept:\n")
  print(tau_log_beta[idx$inter, ], digits = digits)
  cat("log-slope:\n")
  print(tau_log_beta[idx$slope, ], digits = digits)

  cat("\nPopulation correlation posterior rho_log_beta\n")
  rho_log_beta <- summary(
    x$stanfit,
    pars = c("rho_log_beta"),
    probs = probs
  )$summary
  rownames(rho_log_beta) <- strip_variable(rownames(rho_log_beta))
  print(rho_log_beta, digits = digits)

  if (x$standata$num_inter > 0) {
    cat("\nInteraction model posterior:\n")
    cat("Population mean posterior mu_eta\n")
    mu_eta <- summary(x$stanfit, pars = c("mu_eta"), probs = probs)$summary
    rownames(mu_eta) <- strip_variable(rownames(mu_eta))
    print(mu_eta, digits = digits)

    cat("\nPopulation heterogeniety posterior tau_eta\n")
    tau_eta <- summary(x$stanfit, pars = c("tau_eta"), probs = probs)$summary
    rownames(tau_eta) <- strip_variable(rownames(tau_eta))
    print(tau_eta, digits = digits)

    cat("\nPopulation correlation posterior Sigma_corr_eta\n")
    ## TODO: do not display symmetric values
    Sigma_corr_eta <- summary(
      x$stanfit,
      pars = c("Sigma_corr_eta"),
      probs = probs
    )$summary
    rownames(Sigma_corr_eta) <- strip_variable(rownames(Sigma_corr_eta))
    print(Sigma_corr_eta, digits = digits)
  } else {
    cat("\nNo interaction model posterior specified.\n")
  }

  if (x$stanfit@stan_args[[1]]$algorithm == "NUTS") {
    div_trans <- sum(rstan::get_divergent_iterations(x$stanfit))
    num_sim <- nsamples(x)
    if (div_trans > 0) {
      warning(
        "The sampler detected ",
        div_trans,
        " out of ",
        num_sim,
        " transitions ending in a divergence after warmup.\n",
        "Increasing 'adapt_delta' closer to 1 may help to avoid these. Use for example: \n",
        paste0("options(OncoBayes2.MC.control=list(adapt_delta=0.995))"),
        call. = FALSE
      )
    }
  }
  Rhats <- rhat(x)
  if (any(Rhats > 1.1, na.rm = TRUE)) {
    warning(
      "Parts of the model have not converged (some Rhats are > 1.1).\n",
      "Be careful when analysing the results! It is recommend to run\n",
      "more iterations and/or setting stronger priors.",
      call. = FALSE
    )
  }

  invisible(x)
}

## internal -----

#'
#' Utility function to label parameter indices according to factor
#' levels.
#' @param stanfit stan fit which names are being modified
#' @param par parameter selected
#' @param ... must include as many factors as there are indices which
#'     are used in the order given to translate indices to text labels
#'
#' @keywords internal
.label_index <- function(stanfit, par, ...) {
  idx <- grep(paste0("^", par, "\\["), names(stanfit))
  str <- names(stanfit)[idx]
  fct <- list(...)
  idx_str <- t(sapply(
    strsplit(gsub("(.*)\\[([0-9,]*)\\]$", "\\2", str), ","),
    as.numeric
  ))
  if (length(fct) == 1) {
    idx_str <- matrix(idx_str, ncol = 1)
  }
  ni <- ncol(idx_str)
  colnames(idx_str) <- paste0("idx_", seq_len(ni))
  idx_str <- as.data.frame(idx_str)
  assert_that(
    ni == length(fct),
    msg = "Insufficient number of indices specified"
  )
  for (i in seq_len(ni)) {
    f <- fct[[i]]
    key <- data.frame(idx = seq_len(nlevels(f)), label = levels(f))
    names(key) <- paste0(names(key), "_", i)
    idx_str <- left_join(idx_str, key, by = paste0("idx_", i))
  }
  labs <- paste0("label_", seq_len(ni))
  names(stanfit)[idx] <- paste0(
    par,
    "[",
    do.call(paste, c(idx_str[labs], list(sep = ","))),
    "]"
  )
  stanfit
}


#' @keywords internal
.abbreviate_label <- function(label) {
  minlength <- getOption("OncoBayes2.abbreviate.min", 0)
  if (minlength > 0) {
    return(abbreviate(label, minlength = minlength))
  }
  label
}

#' @keywords internal
.make_label_factor <- function(labels) {
  factor(seq_along(labels), levels = seq_along(labels), labels = labels)
}

#' @method model.matrix blrmfit
#' @export
model.matrix.blrmfit <- function(object, ...) {
  return(model.matrix.default(object, object$data))
}


#' Test if each group is assigned to exactly 1 stratum. Error
#' otherwise.
#' @param group_def groups of data
#' @param strata_def assigned stratum to group
#' @keywords internal
.validate_group_stratum_nesting <- function(group_def, strata_def) {
  group_id <- strata_id <- NULL
  strata_per_group <- data.frame(
    group_id = group_def,
    strata_id = strata_def
  ) %>%
    group_by(group_id) %>%
    dplyr::summarize(num_strata = length(unique(strata_id)))
  assert_that(
    all(strata_per_group$num_strata == 1),
    msg = "Inconsistent nesting of groups into strata. Any group must belong to a single stratum."
  )
}

Try the OncoBayes2 package in your browser

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

OncoBayes2 documentation built on June 8, 2025, 1:10 p.m.