R/HierarchicalModel-class.R

Defines functions .DefaultHierarchicalModel HierarchicalModel h_hierarchical_compile_init h_hierarchical_compile_modelspecs h_hierarchical_compile_priormodel h_hierarchical_compile_datamodel h_hierarchical_model_body_lines h_hierarchical_filter_pooled_specs h_hierarchical_model_specs h_hierarchical_namespace_model h_hierarchical_pool_names h_hierarchical_correlated_pool_names h_hierarchical_indexed_node_info h_hierarchical_pool_hyperprior_lines h_hierarchical_pool_hyperprior_specs h_hierarchical_hyperprior_vector h_hierarchical_make_pool_map h_hierarchical_remove_pooled_prior_lines h_hierarchical_pooled_nodes h_hierarchical_reference_stochastic_node h_hierarchical_stochastic_subexpressions h_hierarchical_prior_relations h_hierarchical_root_symbols h_hierarchical_expr_key h_hierarchical_reference_expr h_hierarchical_parse_ref h_hierarchical_is_single_model h_hierarchical_supported_refs h_hierarchical_model_type h_hierarchical_safe_name

Documented in .DefaultHierarchicalModel h_hierarchical_compile_datamodel h_hierarchical_compile_init h_hierarchical_compile_modelspecs h_hierarchical_compile_priormodel h_hierarchical_expr_key h_hierarchical_filter_pooled_specs h_hierarchical_is_single_model h_hierarchical_make_pool_map h_hierarchical_model_body_lines h_hierarchical_model_specs h_hierarchical_model_type h_hierarchical_namespace_model h_hierarchical_parse_ref h_hierarchical_pooled_nodes h_hierarchical_pool_names h_hierarchical_prior_relations h_hierarchical_reference_expr h_hierarchical_reference_stochastic_node h_hierarchical_remove_pooled_prior_lines h_hierarchical_root_symbols h_hierarchical_safe_name h_hierarchical_stochastic_subexpressions h_hierarchical_supported_refs HierarchicalModel

#' @include Model-class.R
NULL

# HierarchicalModel helpers ----

#' Sanitize a Hierarchical Name for Generated JAGS Code
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Replaces every non-alphanumeric character with an underscore so that user
#' supplied arm and pool names can safely be embedded in generated JAGS
#' variable names.
#'
#' @param name (`string`)\cr the arm or pool name to sanitize.
#'
#' @return A character scalar suitable for generated JAGS identifiers.
#'
#' @keywords internal
h_hierarchical_safe_name <- function(name) {
  assert_string(name)
  gsub("[^A-Za-z0-9_]", "_", name)
}

#' Identify the Internal Type of a Hierarchical Arm Model
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Maps the currently supported hierarchical arm models to compact internal
#' labels used by the code generators.
#'
#' @param model (`GeneralModel`)\cr arm-specific model object.
#'
#' @return Either `"mono"` or `"combo"`.
#'
#' @keywords internal
h_hierarchical_model_type <- function(model) {
  if (is(model, "TwoDrugsCombo")) {
    return("combo")
  }
  if (h_hierarchical_is_single_model(model)) {
    return("mono")
  }
  stop("Unsupported hierarchical arm model.")
}

#' List Supported Exchangeable Parameter References for an Arm Model
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Returns the parameter reference syntax that may be used for a model in the
#' `exchangeable_parameters` argument of [HierarchicalModel()].
#'
#' @param model (`GeneralModel`)\cr arm-specific model object.
#'
#' @return Character vector of supported references.
#'
#' @keywords internal
h_hierarchical_supported_refs <- function(model) {
  if (is(model, "TwoDrugsCombo")) {
    return(as.character(unlist(lapply(
      seq_along(model@single_models),
      function(index) {
        paste0(model@single_models[[index]]@sample, "[", index, "]")
      }
    ))))
  }

  if (h_hierarchical_is_single_model(model)) {
    return(model@sample)
  }

  character()
}

#' Is a Compatible Hierarchical Single-Agent Model
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Uses the same structural compatibility contract as [TwoDrugsCombo()] for
#' single-agent binary outcome models.
#'
#' @param model (`GeneralModel`)\cr model object.
#'
#' @return A flag.
#'
#' @keywords internal
h_hierarchical_is_single_model <- function(model) {
  is(model, "GeneralModel") &&
    !is(model, "TwoDrugsCombo") &&
    setequal(model@datanames, c("nObs", "y", "x")) &&
    length(formalArgs(model@init)) == 0L &&
    setequal(
      setdiff(formalArgs(model@modelspecs), "from_prior"),
      character()
    )
}

#' Parse a Hierarchical Parameter Reference
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Converts a user-facing parameter reference into metadata that can be used by
#' the JAGS code generators.
#'
#' @param model (`GeneralModel`)\cr arm-specific model object.
#' @param arm_name (`string`)\cr the user-facing arm name.
#' @param ref (`string`)\cr parameter reference.
#'
#' @return Named list with entries `kind`, `index`, `latent`, and `sample`.
#'
#' @keywords internal
h_hierarchical_parse_ref <- function(model, arm_name, ref) {
  assert_string(ref)

  safe_arm <- h_hierarchical_safe_name(arm_name)
  type <- h_hierarchical_model_type(model)

  if (type == "mono" && ref %in% model@sample) {
    return(list(
      kind = ref,
      index = match(ref, model@sample),
      latent = h_hierarchical_reference_stochastic_node(
        model = model,
        arm_name = arm_name,
        ref = ref
      ),
      sample = paste0(ref, "_", safe_arm)
    ))
  }

  if (type == "combo") {
    m <- regexec("^(.+)\\[([0-9]+)\\]$", ref)
    capture <- regmatches(ref, m)[[1L]]
    if (length(capture) == 3L) {
      kind <- capture[2L]
      drug_index <- as.integer(capture[3L])
      if (
        drug_index >= 1L &&
          drug_index <= length(model@single_models) &&
          kind %in% model@single_models[[drug_index]]@sample
      ) {
        return(list(
          kind = kind,
          index = match(kind, model@single_models[[drug_index]]@sample),
          arm_index = drug_index,
          latent = h_hierarchical_reference_stochastic_node(
            model = model,
            arm_name = arm_name,
            ref = ref
          ),
          sample = paste0(kind, "_", safe_arm, "[", drug_index, "]")
        ))
      }
    }
  }

  stop(
    "Unsupported hierarchical parameter reference '",
    ref,
    "' for arm '",
    arm_name,
    "'."
  )
}

#' Create the Namespaced Expression for a Parameter Reference
#'
#' @inheritParams h_hierarchical_parse_ref
#'
#' @return A symbol or indexed call for the public sample node.
#'
#' @keywords internal
h_hierarchical_reference_expr <- function(model, arm_name, ref) {
  safe_arm <- h_hierarchical_safe_name(arm_name)

  if (h_hierarchical_is_single_model(model)) {
    return(as.name(paste0(ref, "_", safe_arm)))
  }

  m <- regexec("^(.+)\\[([0-9]+)\\]$", ref)
  capture <- regmatches(ref, m)[[1L]]
  assert_true(length(capture) == 3L)
  h_two_drugs_combo_indexed_call(
    paste0(capture[2L], "_", safe_arm),
    as.integer(capture[3L])
  )
}

#' Normalize an Expression Key
#'
#' @param expr (`language`)\cr expression to identify.
#'
#' @return A character scalar.
#'
#' @keywords internal
h_hierarchical_expr_key <- function(expr) {
  gsub("\\s+", "", deparse1(expr))
}

#' Find the Root Symbol of an Expression
#'
#' @param expr (`language`)\cr expression to inspect.
#'
#' @return Character vector of root symbols.
#'
#' @keywords internal
h_hierarchical_root_symbols <- function(expr) {
  h_two_drugs_combo_lhs_symbols(expr)
}

#' Collect Prior Relations
#'
#' @param expr (`language`)\cr prior model body.
#' @param operator (`string`)\cr relation operator, either `"~"` or `"<-"`.
#'
#' @return A list of relation calls.
#'
#' @keywords internal
h_hierarchical_prior_relations <- function(expr, operator) {
  relations <- list()
  if (is.call(expr)) {
    if (identical(as.character(expr[[1L]]), operator)) {
      relations <- list(expr)
    }
    relations <- c(
      relations,
      unlist(
        lapply(
          as.list(expr)[-1L],
          h_hierarchical_prior_relations,
          operator = operator
        ),
        recursive = FALSE
      )
    )
  }
  relations
}

#' Find Stochastic Subexpressions in an Expression
#'
#' @param expr (`language`)\cr expression to inspect.
#' @param stochastic_roots (`character`)\cr stochastic node roots.
#'
#' @return Named list of stochastic subexpressions keyed by expression text.
#'
#' @keywords internal
h_hierarchical_stochastic_subexpressions <- function(expr, stochastic_roots) {
  out <- list()
  if (is.symbol(expr)) {
    if (as.character(expr) %in% stochastic_roots) {
      out[[h_hierarchical_expr_key(expr)]] <- expr
    }
    return(out)
  }
  if (is.call(expr)) {
    if (
      identical(as.character(expr[[1L]]), "[") &&
        any(h_hierarchical_root_symbols(expr) %in% stochastic_roots)
    ) {
      out[[h_hierarchical_expr_key(expr)]] <- expr
      return(out)
    }
    for (arg in as.list(expr)[-1L]) {
      out <- c(
        out,
        h_hierarchical_stochastic_subexpressions(
          arg,
          stochastic_roots
        )
      )
    }
  }
  out[!duplicated(names(out))]
}

#' Infer the Stochastic Node for a Parameter Reference
#'
#' @inheritParams h_hierarchical_parse_ref
#'
#' @return Character scalar naming the stochastic node to pool.
#'
#' @keywords internal
h_hierarchical_reference_stochastic_node <- function(model, arm_name, ref) {
  prior_model <- h_hierarchical_namespace_model(
    model = model,
    arm_name = arm_name,
    slot_name = "priormodel"
  )
  stochastic <- h_hierarchical_prior_relations(body(prior_model), "~")
  deterministic <- h_hierarchical_prior_relations(body(prior_model), "<-")
  stochastic_roots <- unique(unlist(lapply(
    stochastic,
    function(expr) h_hierarchical_root_symbols(expr[[2L]])
  )))
  deterministic_map <- setNames(
    lapply(deterministic, function(expr) expr[[3L]]),
    vapply(
      deterministic,
      function(expr) h_hierarchical_expr_key(expr[[2L]]),
      character(1L)
    )
  )

  current <- h_hierarchical_reference_expr(model, arm_name, ref)
  for (step in seq_len(50L)) {
    current_roots <- h_hierarchical_root_symbols(current)
    if (length(current_roots) == 1L && current_roots %in% stochastic_roots) {
      return(h_hierarchical_expr_key(current))
    }

    current_key <- h_hierarchical_expr_key(current)
    if (!current_key %in% names(deterministic_map)) {
      break
    }

    rhs <- deterministic_map[[current_key]]
    stochastic_exprs <- h_hierarchical_stochastic_subexpressions(
      rhs,
      stochastic_roots
    )
    if (length(stochastic_exprs) == 1L) {
      return(names(stochastic_exprs))
    }
    if (length(stochastic_exprs) > 1L) {
      stop(
        "Parameter reference '",
        ref,
        "' for arm '",
        arm_name,
        "' depends on multiple stochastic nodes."
      )
    }
    current <- rhs
  }

  stop(
    "Could not infer a stochastic prior node for parameter reference '",
    ref,
    "' in arm '",
    arm_name,
    "'."
  )
}

#' Find Pooled Nodes for an Arm
#'
#' @param model (`GeneralModel`)\cr arm-specific model object.
#' @param arm_name (`string`)\cr hierarchical arm name.
#' @param refs (`character`)\cr hierarchical references.
#' @param pool_names (`character`)\cr pool names matching `refs`.
#'
#' @return Character vector of namespaced nodes with exchangeable priors.
#'
#' @keywords internal
h_hierarchical_pooled_nodes <- function(model, arm_name, refs, pool_names) {
  refs <- refs[pool_names != ""]
  if (length(refs) == 0L) {
    return(character())
  }
  vapply(
    refs,
    function(ref) h_hierarchical_parse_ref(model, arm_name, ref)$latent,
    character(1L)
  )
}

#' Remove Fixed Priors for Pooled Nodes
#'
#' @param model_fun (`function`)\cr namespaced prior model function.
#' @param pooled_nodes (`character`)\cr namespaced nodes with exchangeable priors.
#'
#' @return A function with fixed prior distribution statements removed.
#'
#' @keywords internal
h_hierarchical_remove_pooled_prior_lines <- function(model_fun, pooled_nodes) {
  if (length(pooled_nodes) == 0L) {
    return(model_fun)
  }
  pooled_roots <- unique(vapply(
    pooled_nodes,
    function(node) h_hierarchical_root_symbols(parse(text = node)[[1L]])[[1L]],
    character(1L)
  ))

  body_expr <- body(model_fun)
  if (!is.call(body_expr) || !identical(as.character(body_expr[[1L]]), "{")) {
    return(model_fun)
  }

  keep <- vapply(
    as.list(body_expr)[-1L],
    function(expr) {
      if (
        is.call(expr) &&
          identical(as.character(expr[[1L]]), "~") &&
          (h_hierarchical_expr_key(expr[[2L]]) %in%
            pooled_nodes ||
            any(h_hierarchical_root_symbols(expr[[2L]]) %in% pooled_roots))
      ) {
        return(FALSE)
      }
      TRUE
    },
    logical(1L)
  )
  body(model_fun) <- as.call(c(
    list(as.name("{")),
    as.list(body_expr)[-1L][keep]
  ))
  model_fun
}

#' Flatten Hierarchical Pool Definitions into a Lookup Table
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Builds a lookup keyed by `"arm::reference"` so the compiler can quickly tell
#' whether a parameter is pooled or fixed.
#'
#' @param parameter_pools (`list`)\cr exchangeable parameter specification from
#'   [HierarchicalModel()].
#'
#' @return Named list mapping `"arm::reference"` keys to pool names.
#'
#' @keywords internal
h_hierarchical_make_pool_map <- function(parameter_pools) {
  pooled_map <- list()

  for (pool_name in names(parameter_pools)) {
    members <- parameter_pools[[pool_name]]
    for (arm_name in names(members)) {
      this_key <- paste0(arm_name, "::", members[[arm_name]])
      if (!test_null(pooled_map[[this_key]])) {
        stop("Duplicate hierarchical parameter reference '", this_key, "'.")
      }
      pooled_map[[this_key]] <- pool_name
    }
  }

  pooled_map
}

#' Extract and Validate a Hyperprior Specification
#'
#' @param x (`numeric` or `NULL`)\cr user-supplied two-element vector.
#' @param names (`character`)\cr allowed names.
#'
#' @return A named numeric vector or `NULL`.
#'
#' @keywords internal
#' @noRd
h_hierarchical_hyperprior_vector <- function(x, names) {
  if (is.null(x)) {
    return(NULL)
  }
  assert_numeric(x, len = 2L, any.missing = FALSE, finite = TRUE)
  if (!is.null(names(x))) {
    assert_set_equal(names(x), names)
    return(x[names])
  }
  stats::setNames(x, names)
}

#' Get Pool-Specific Hyperprior Specification Names and Values
#'
#' @param pool_name (`string`)\cr pool name.
#' @param first_info (`list`)\cr parsed metadata for the first pool member.
#' @param pool_priors (`list`)\cr optional user hyperprior overrides.
#'
#' @return A named list containing JAGS data values.
#'
#' @keywords internal
#' @noRd
h_hierarchical_pool_hyperprior_specs <- function(
  pool_name,
  first_info,
  pool_priors
) {
  safe_pool <- h_hierarchical_safe_name(pool_name)
  custom <- pool_priors[[pool_name]]
  mu <- h_hierarchical_hyperprior_vector(custom$mu, c("mean", "sd"))
  tau <- h_hierarchical_hyperprior_vector(custom$tau, c("meanlog", "sdlog"))

  if (is.null(mu) && identical(first_info$index, 1L)) {
    mu <- c(mean = qlogis(0.25), sd = 2.5)
  } else if (is.null(mu)) {
    mu <- c(mean = 0, sd = 0.7)
  }

  if (is.null(tau) && identical(first_info$index, 1L)) {
    tau <- c(meanlog = log(0.5), sdlog = log(2) / 1.96)
  } else if (is.null(tau)) {
    tau <- c(meanlog = log(0.25), sdlog = log(2) / 1.96)
  }

  stats::setNames(
    as.list(c(mu, tau)),
    paste0(
      c(
        "mu_",
        "mu_",
        "tau_",
        "tau_"
      ),
      safe_pool,
      c("_mean", "_sd", "_meanlog", "_sdlog")
    )
  )
}

#' Get Pool-Specific Hyperprior Lines
#'
#' @param pool_name (`string`)\cr pool name.
#'
#' @return Character vector containing JAGS prior lines.
#'
#' @keywords internal
#' @noRd
h_hierarchical_pool_hyperprior_lines <- function(pool_name) {
  safe_pool <- h_hierarchical_safe_name(pool_name)
  mu_name <- paste0("mu_", safe_pool)
  tau_name <- paste0("tau_", safe_pool)

  c(
    paste0(
      mu_name,
      " ~ dnorm(",
      mu_name,
      "_mean, pow(",
      mu_name,
      "_sd, -2))"
    ),
    paste0(
      tau_name,
      " ~ dlnorm(",
      tau_name,
      "_meanlog, pow(",
      tau_name,
      "_sdlog, -2))"
    )
  )
}

#' Identify Indexed Latent Nodes Used in a Correlated Pool Block
#'
#' @param node (`string`)\cr namespaced latent node.
#'
#' @return A list with entries `root` and `index`.
#'
#' @keywords internal
#' @noRd
h_hierarchical_indexed_node_info <- function(node) {
  expr <- parse(text = node)[[1L]]
  if (
    !is.call(expr) ||
      !identical(as.character(expr[[1L]]), "[") ||
      !is.symbol(expr[[2L]]) ||
      length(expr) != 3L
  ) {
    stop("Correlated pool nodes must be indexed elements of the same vector.")
  }
  index <- eval(expr[[3L]], envir = baseenv())
  if (!is.numeric(index) || length(index) != 1L || index != as.integer(index)) {
    stop("Correlated pool nodes must use scalar integer indices.")
  }
  list(root = as.character(expr[[2L]]), index = as.integer(index))
}

#' Build a Lookup of Pools Used in Correlated Blocks
#'
#' @param pool_correlations (`list`)\cr named list of correlations between
#'   exactly two pools.
#'
#' @return Character vector of pool names.
#'
#' @keywords internal
#' @noRd
h_hierarchical_correlated_pool_names <- function(pool_correlations) {
  unique(unlist(pool_correlations, use.names = FALSE))
}

#' Find the Pool Name for One or More Parameter References
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Returns the hierarchical pool membership of each parameter reference, or an
#' empty string if the parameter keeps its arm-specific fixed prior.
#'
#' @param arm_name (`string`)\cr arm name.
#' @param refs (`character`)\cr parameter references belonging to that arm.
#' @param pooled_map (`list`)\cr output of
#'   [h_hierarchical_make_pool_map()].
#'
#' @return Character vector of pool names, using `""` for unpooled parameters.
#'
#' @keywords internal
h_hierarchical_pool_names <- function(arm_name, refs, pooled_map) {
  vapply(
    refs,
    function(ref) {
      key <- paste0(arm_name, "::", ref)
      if (is.null(pooled_map[[key]])) "" else pooled_map[[key]]
    },
    character(1L)
  )
}

#' Add an Arm Suffix to a Hierarchical Model Fragment
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Namespaces one arm-specific model fragment using code supplied by the arm
#' model class itself.
#'
#' @param model (`GeneralModel`)\cr arm-specific model object.
#' @param arm_name (`string`)\cr hierarchical arm name.
#' @param slot_name (`string`)\cr model-function slot to namespace.
#'
#' @return A function containing the namespaced JAGS model fragment.
#'
#' @keywords internal
h_hierarchical_namespace_model <- function(model, arm_name, slot_name) {
  safe_arm <- h_hierarchical_safe_name(arm_name)
  suffix <- paste0("_", safe_arm)
  model_fun <- slot(model, slot_name)
  prior_specs <- h_two_drugs_combo_single_model_specs(model, from_prior = TRUE)
  full_specs <- h_two_drugs_combo_single_model_specs(model, from_prior = FALSE)
  prior_inits <- do.call(model@init, list())
  prior_nodes <- h_two_drugs_combo_assigned_nodes(body(model@priormodel))
  data_nodes <- h_two_drugs_combo_assigned_nodes(body(model@datamodel))
  nodes <- setdiff(
    unique(c(
      names(prior_specs),
      names(full_specs),
      names(prior_inits),
      prior_nodes,
      data_nodes,
      model@sample,
      model@datanames
    )),
    character()
  )
  replacements <- setNames(
    lapply(nodes, function(name) as.name(paste0(name, suffix))),
    nodes
  )

  body(model_fun) <- h_two_drugs_combo_replace_symbols(
    body(model_fun),
    replacements
  )
  model_fun
}

#' Suffix Single-Arm Model Specifications
#'
#' @inheritParams h_hierarchical_namespace_model
#' @param from_prior (`flag`)\cr whether to request prior-only specifications.
#'
#' @return A named list of suffixed model specifications.
#'
#' @keywords internal
h_hierarchical_model_specs <- function(model, arm_name, from_prior) {
  specs <- h_two_drugs_combo_single_model_specs(model, from_prior = from_prior)
  h_two_drugs_combo_suffix_names(
    specs,
    paste0("_", h_hierarchical_safe_name(arm_name))
  )
}

#' Drop Fixed-Prior Specs for Fully Pooled Generic Parameters
#'
#' @param specs (`list`)\cr namespaced model specifications.
#' @param model (`GeneralModel`)\cr arm-specific model object.
#' @param arm_name (`string`)\cr hierarchical arm name.
#' @param refs (`character`)\cr supported hierarchical references.
#' @param pool_names (`character`)\cr pool names matching `refs`.
#'
#' @return Filtered `specs`.
#'
#' @keywords internal
h_hierarchical_filter_pooled_specs <- function(
  specs,
  model,
  arm_name,
  refs,
  pool_names
) {
  safe_arm <- h_hierarchical_safe_name(arm_name)
  pooled_refs <- refs[pool_names != ""]

  if (h_hierarchical_is_single_model(model)) {
    if (all(model@sample %in% pooled_refs)) {
      prior_specs <- h_two_drugs_combo_single_model_specs(
        model,
        from_prior = TRUE
      )
      remove_names <- paste0(names(prior_specs), "_", safe_arm)
      specs <- specs[!names(specs) %in% remove_names]
    }
    return(specs)
  }

  if (is(model, "TwoDrugsCombo")) {
    for (index in seq_along(model@single_models)) {
      single_model <- model@single_models[[index]]
      single_refs <- paste0(single_model@sample, "[", index, "]")
      if (all(single_refs %in% pooled_refs)) {
        prior_specs <- h_two_drugs_combo_single_model_specs(
          single_model,
          from_prior = TRUE
        )
        remove_names <- paste0(
          names(prior_specs),
          "_drug",
          index,
          "_",
          safe_arm
        )
        specs <- specs[!names(specs) %in% remove_names]
      }
    }
  }

  specs
}

#' Deparse a Model Body into JAGS Lines
#'
#' @inheritParams h_hierarchical_namespace_model
#'
#' @return Character vector containing the body expressions without braces.
#'
#' @keywords internal
h_hierarchical_model_body_lines <- function(model_fun) {
  body_lines <- deparse(body(model_fun))
  if (length(body_lines) > 1L && identical(body_lines[[1L]], "{")) {
    body_lines[-c(1L, length(body_lines))]
  } else {
    body_lines
  }
}

#' Compile the Hierarchical Data Model
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Concatenates the arm-specific likelihood code into a single JAGS model
#' function. Each arm receives a unique variable-name prefix derived from its
#' user-facing name.
#'
#' @param models_to_arms (`list`)\cr named arm-specific models.
#'
#' @return A function representing the compiled JAGS data model.
#'
#' @keywords internal
h_hierarchical_compile_datamodel <- function(models_to_arms) {
  Reduce(
    h_jags_join_models,
    lapply(names(models_to_arms), function(arm_name) {
      h_hierarchical_namespace_model(
        model = models_to_arms[[arm_name]],
        arm_name = arm_name,
        slot_name = "datamodel"
      )
    })
  )
}

#' Compile the Hierarchical Prior Model
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Builds the prior part of a hierarchical JAGS model. Unpooled parameters keep
#' their arm-specific fixed priors, while pooled parameters are linked through
#' exchangeable normal distributions with simple hyperpriors.
#'
#' @param models_to_arms (`list`)\cr named arm-specific models.
#' @param parameter_pools (`list`)\cr exchangeable parameter specification from
#'   [HierarchicalModel()].
#' @param pool_correlations (`list`)\cr optional named list pairing exactly two
#'   scalar exchangeable pools into correlated bivariate normal blocks. Higher
#'   dimensional correlation blocks, such as correlations across three or more
#'   parameters, are not supported.
#' @param pool_priors (`list`)\cr optional named list of pool-specific
#'   hyperprior overrides.
#'
#' @return A list with entries `priormodel` and `sample`.
#'
#' @keywords internal
h_hierarchical_compile_priormodel <- function(
  models_to_arms,
  parameter_pools,
  pool_correlations = list(),
  pool_priors = list()
) {
  pooled_map <- h_hierarchical_make_pool_map(parameter_pools)
  fixed_lines <- character()
  sample_names <- character()

  for (arm_name in names(models_to_arms)) {
    model <- models_to_arms[[arm_name]]
    safe_arm <- h_hierarchical_safe_name(arm_name)

    model_refs <- h_hierarchical_supported_refs(model)
    pool_names <- h_hierarchical_pool_names(
      arm_name = arm_name,
      refs = model_refs,
      pooled_map = pooled_map
    )

    if (all(pool_names == "")) {
      fixed_lines <- c(
        fixed_lines,
        h_hierarchical_model_body_lines(h_hierarchical_namespace_model(
          model = model,
          arm_name = arm_name,
          slot_name = "priormodel"
        ))
      )
      sample_names <- c(
        sample_names,
        paste0(model@sample, "_", safe_arm)
      )
      next
    }

    pooled_nodes <- h_hierarchical_pooled_nodes(
      model = model,
      arm_name = arm_name,
      refs = model_refs,
      pool_names = pool_names
    )
    fixed_model <- h_hierarchical_namespace_model(
      model = model,
      arm_name = arm_name,
      slot_name = "priormodel"
    )
    fixed_model <- h_hierarchical_remove_pooled_prior_lines(
      model_fun = fixed_model,
      pooled_nodes = pooled_nodes
    )
    fixed_lines <- c(
      fixed_lines,
      h_hierarchical_model_body_lines(fixed_model)
    )
    sample_names <- c(
      sample_names,
      paste0(model@sample, "_", safe_arm)
    )
  }

  hyper_lines <- character()
  hyper_names <- character()
  correlated_pools <- h_hierarchical_correlated_pool_names(pool_correlations)

  for (correlation_name in names(pool_correlations)) {
    pair <- pool_correlations[[correlation_name]]
    first_pool_name <- pair[[1L]]
    second_pool_name <- pair[[2L]]
    first_pool <- parameter_pools[[first_pool_name]]
    second_pool <- parameter_pools[[second_pool_name]]
    safe_correlation <- h_hierarchical_safe_name(correlation_name)
    mu_vector <- paste0("mu_", safe_correlation, "_corr")
    prec_matrix <- paste0("prec_", safe_correlation, "_corr")
    rho_name <- paste0("rho_", safe_correlation)
    rho_lower <- paste0(rho_name, "_lower")
    rho_upper <- paste0(rho_name, "_upper")
    first_safe_pool <- h_hierarchical_safe_name(first_pool_name)
    second_safe_pool <- h_hierarchical_safe_name(second_pool_name)
    first_mu <- paste0("mu_", first_safe_pool)
    second_mu <- paste0("mu_", second_safe_pool)
    first_tau <- paste0("tau_", first_safe_pool)
    second_tau <- paste0("tau_", second_safe_pool)

    for (arm_name in names(first_pool)) {
      first_info <- h_hierarchical_parse_ref(
        models_to_arms[[arm_name]],
        arm_name,
        first_pool[[arm_name]]
      )
      second_info <- h_hierarchical_parse_ref(
        models_to_arms[[arm_name]],
        arm_name,
        second_pool[[arm_name]]
      )
      first_node <- h_hierarchical_indexed_node_info(first_info$latent)
      second_node <- h_hierarchical_indexed_node_info(second_info$latent)
      if (
        !identical(first_node$root, second_node$root) ||
          !identical(first_node$index, 1L) ||
          !identical(second_node$index, 2L)
      ) {
        stop(
          "Correlated pools '",
          first_pool_name,
          "' and '",
          second_pool_name,
          "' must refer to indices 1 and 2 of the same latent vector for arm '",
          arm_name,
          "'."
        )
      }
      hyper_lines <- c(
        hyper_lines,
        paste0(
          first_node$root,
          "[1:2] ~ dmnorm(",
          mu_vector,
          "[], ",
          prec_matrix,
          "[,])"
        )
      )
    }

    first_ref_info <- h_hierarchical_parse_ref(
      models_to_arms[[names(first_pool)[1L]]],
      names(first_pool)[1L],
      first_pool[[1L]]
    )
    second_ref_info <- h_hierarchical_parse_ref(
      models_to_arms[[names(second_pool)[1L]]],
      names(second_pool)[1L],
      second_pool[[1L]]
    )
    hyper_lines <- c(
      hyper_lines,
      paste0(mu_vector, "[1] <- ", first_mu),
      paste0(mu_vector, "[2] <- ", second_mu),
      paste0(rho_name, " ~ dunif(", rho_lower, ", ", rho_upper, ")"),
      paste0(
        prec_matrix,
        "[1, 1] <- 1 / (pow(",
        first_tau,
        ", 2) * (1 - pow(",
        rho_name,
        ", 2)))"
      ),
      paste0(
        prec_matrix,
        "[2, 2] <- 1 / (pow(",
        second_tau,
        ", 2) * (1 - pow(",
        rho_name,
        ", 2)))"
      ),
      paste0(
        prec_matrix,
        "[1, 2] <- -",
        rho_name,
        " / (",
        first_tau,
        " * ",
        second_tau,
        " * (1 - pow(",
        rho_name,
        ", 2)))"
      ),
      paste0(prec_matrix, "[2, 1] <- ", prec_matrix, "[1, 2]"),
      h_hierarchical_pool_hyperprior_lines(pool_name = first_pool_name),
      h_hierarchical_pool_hyperprior_lines(pool_name = second_pool_name)
    )
    hyper_names <- c(
      hyper_names,
      rho_name,
      first_mu,
      first_tau,
      second_mu,
      second_tau
    )
  }

  for (pool_name in names(parameter_pools)) {
    if (pool_name %in% correlated_pools) {
      next
    }
    members <- parameter_pools[[pool_name]]
    first_arm <- names(members)[1L]
    first_ref <- members[[1L]]
    first_info <- h_hierarchical_parse_ref(
      models_to_arms[[first_arm]],
      first_arm,
      first_ref
    )
    safe_pool <- h_hierarchical_safe_name(pool_name)
    mu_name <- paste0("mu_", safe_pool)
    tau_name <- paste0("tau_", safe_pool)

    # Each pooled arm-level latent parameter gets an exchangeable normal prior
    # centered on the pool-specific mean and SD.
    hyper_lines <- c(
      hyper_lines,
      vapply(
        seq_along(members),
        function(i) {
          arm_name <- names(members)[i]
          arm_ref <- members[[i]]
          arm_info <- h_hierarchical_parse_ref(
            models_to_arms[[arm_name]],
            arm_name,
            arm_ref
          )
          paste0(
            arm_info$latent,
            " ~ dnorm(",
            mu_name,
            ", pow(",
            tau_name,
            ", -2))"
          )
        },
        character(1L)
      )
    )

    hyper_lines <- c(
      hyper_lines,
      h_hierarchical_pool_hyperprior_lines(pool_name = pool_name)
    )

    hyper_names <- c(hyper_names, mu_name, tau_name)
  }

  list(
    priormodel = eval(parse(
      text = paste(
        c("function() {", fixed_lines, hyper_lines, "}"),
        collapse = "\n"
      )
    )),
    sample = unique(c(sample_names, hyper_names))
  )
}

#' Compile the Hierarchical Model-Specification Function
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Creates the `modelspecs` function used by [mcmc()] to prepare the JAGS data
#' needed by the dynamically compiled hierarchical model.
#'
#' @param models_to_arms (`list`)\cr named arm-specific models.
#' @param parameter_pools (`list`)\cr exchangeable parameter specification from
#'   [HierarchicalModel()].
#' @param pool_correlations (`list`)\cr optional named list pairing exactly two
#'   scalar exchangeable pools into correlated bivariate normal blocks. Higher
#'   dimensional correlation blocks, such as correlations across three or more
#'   parameters, are not supported.
#' @param pool_priors (`list`)\cr optional named list of pool-specific
#'   hyperprior overrides.
#'
#' @return A function returning the JAGS data list.
#'
#' @keywords internal
h_hierarchical_compile_modelspecs <- function(
  models_to_arms,
  parameter_pools,
  pool_correlations = list(),
  pool_priors = list()
) {
  pooled_map <- h_hierarchical_make_pool_map(parameter_pools)

  function(arms, from_prior) {
    assert_list(arms, any.missing = FALSE)
    assert_flag(from_prior)

    specs <- list()
    for (pool_name in names(parameter_pools)) {
      members <- parameter_pools[[pool_name]]
      first_arm <- names(members)[1L]
      first_ref <- members[[1L]]
      first_info <- h_hierarchical_parse_ref(
        models_to_arms[[first_arm]],
        first_arm,
        first_ref
      )
      specs <- c(
        specs,
        h_hierarchical_pool_hyperprior_specs(
          pool_name = pool_name,
          first_info = first_info,
          pool_priors = pool_priors
        )
      )
    }

    for (correlation_name in names(pool_correlations)) {
      safe_correlation <- h_hierarchical_safe_name(correlation_name)
      specs[[paste0("rho_", safe_correlation, "_lower")]] <- -1
      specs[[paste0("rho_", safe_correlation, "_upper")]] <- 1
    }

    for (arm_name in names(models_to_arms)) {
      model <- models_to_arms[[arm_name]]
      safe_arm <- h_hierarchical_safe_name(arm_name)
      model_refs <- h_hierarchical_supported_refs(model)
      pool_names <- h_hierarchical_pool_names(
        arm_name = arm_name,
        refs = model_refs,
        pooled_map = pooled_map
      )

      if (all(pool_names == "")) {
        specs <- c(
          specs,
          h_hierarchical_model_specs(
            model = model,
            arm_name = arm_name,
            from_prior = from_prior
          )
        )
        next
      }

      fixed_specs <- h_hierarchical_model_specs(
        model = model,
        arm_name = arm_name,
        from_prior = from_prior
      )
      fixed_specs <- h_hierarchical_filter_pooled_specs(
        specs = fixed_specs,
        model = model,
        arm_name = arm_name,
        refs = model_refs,
        pool_names = pool_names
      )
      specs <- c(
        specs,
        fixed_specs
      )
    }

    specs
  }
}

#' Compile the Hierarchical Initial-Value Function
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' Wraps the per-arm `init` functions and renames their outputs so they match
#' the dynamically generated hierarchical JAGS variable names.
#'
#' @param models_to_arms (`list`)\cr named arm-specific models.
#' @param parameter_pools (`list`)\cr exchangeable parameter specification from
#'   [HierarchicalModel()].
#' @param pool_correlations (`list`)\cr optional named list pairing exactly two
#'   scalar exchangeable pools into correlated bivariate normal blocks. Higher
#'   dimensional correlation blocks, such as correlations across three or more
#'   parameters, are not supported.
#'
#' @return A function returning a named list of JAGS initial values.
#'
#' @keywords internal
h_hierarchical_compile_init <- function(
  models_to_arms,
  parameter_pools,
  pool_correlations = list()
) {
  function(arms) {
    assert_list(arms, any.missing = FALSE)

    init <- list()
    for (arm_name in names(models_to_arms)) {
      model <- models_to_arms[[arm_name]]
      safe_arm <- h_hierarchical_safe_name(arm_name)
      arm_inits <- do.call(model@init, list())

      # Hierarchical compilation prefixes arm-local initial values by arm.
      for (init_name in names(arm_inits)) {
        init[[paste0(init_name, "_", safe_arm)]] <- arm_inits[[init_name]]
      }
    }

    for (pool_name in names(parameter_pools)) {
      safe_pool <- h_hierarchical_safe_name(pool_name)
      init[[paste0("mu_", safe_pool)]] <- 0
      init[[paste0("tau_", safe_pool)]] <- 0.5
    }
    for (correlation_name in names(pool_correlations)) {
      safe_correlation <- h_hierarchical_safe_name(correlation_name)
      init[[paste0("rho_", safe_correlation)]] <- 0
    }

    init
  }
}

# HierarchicalModel ----

## class ----

#' `HierarchicalModel`
#'
#' @description `r lifecycle::badge("experimental")`
#'
#' [`HierarchicalModel`] is a class for the prototype hierarchical model that
#' links compatible single-agent binary outcome arms and/or [`TwoDrugsCombo`]
#' arms.
#'
#' @details The class currently stores the structural pieces from the design
#'   prototype as a named list of arm-specific models and a named list of
#'   exchangeable parameter pools used to dynamically compile a joint JAGS model.
#'
#' @slot models_to_arms (`list`)\cr named list of arm-specific models. Each
#'   entry must be either a compatible single-agent binary outcome
#'   [`GeneralModel`] object using `nObs`, `y`, and `x` inputs, or a
#'   [`TwoDrugsCombo`] object.
#' @slot parameter_pools (`list`)\cr named list describing which parameters are
#'   exchangeable across arms. Each list entry contains arm names as names and
#'   parameter references as values.
#' @slot pool_correlations (`list`)\cr named list pairing exactly two scalar
#'   exchangeable parameter pools that should use a correlated bivariate normal
#'   hierarchy.
#' @slot pool_priors (`list`)\cr named list of pool-specific hyperprior
#'   overrides.
#'
#' @seealso [`HierarchicalData`], [`TwoDrugsCombo`].
#'
#' @aliases HierarchicalModel
#' @export
#'
.HierarchicalModel <- setClass(
  Class = "HierarchicalModel",
  contains = "GeneralModel",
  slots = c(
    models_to_arms = "list",
    parameter_pools = "list",
    pool_correlations = "list",
    pool_priors = "list"
  ),
  validity = v_hierarchical_model
)

## constructor ----

#' @rdname HierarchicalModel-class
#'
#' @param ... named model objects describing the trial arms.
#' @param exchangeable_parameters a named list describing
#'   which parameters are exchangeable across arms. This will be
#'   used to define the hierarchical structure of the model. Each
#'   list entry contains the arms as names and the parameters to be shared
#'   as a string.
#' @param pool_correlations optional named list pairing exactly two scalar
#'   entries from `exchangeable_parameters` into a correlated bivariate
#'   hierarchy. Each pair must refer to indices 1 and 2 of the same latent
#'   parameter vector in every shared arm. Correlating three or more parameters
#'   in one multivariate hierarchy is not supported.
#' @param pool_priors optional named list of hyperprior overrides for entries in
#'   `exchangeable_parameters`. Each entry may contain `mu = c(mean, sd)` and/or
#'   `tau = c(meanlog, sdlog)`.
#'
#' @export
#' @example examples/Model-class-HierarchicalModel.R
HierarchicalModel <- function(
  ...,
  exchangeable_parameters = list(),
  pool_correlations = list(),
  pool_priors = list()
) {
  args <- list(...)
  assert_list(args, any.missing = FALSE, min.len = 2L)
  assert_true(!is.null(names(args)))
  assert_character(names(args), unique = TRUE, any.missing = FALSE)
  assert_true(all(vapply(
    args,
    function(model) {
      h_hierarchical_is_single_model(model) || is(model, "TwoDrugsCombo")
    },
    logical(1L)
  )))
  assert_list(exchangeable_parameters, any.missing = FALSE, null.ok = TRUE)
  assert_list(pool_correlations, any.missing = FALSE, null.ok = TRUE)
  assert_list(pool_priors, any.missing = FALSE, null.ok = TRUE)
  assert_character(sapply(names(args), h_hierarchical_safe_name), unique = TRUE)
  if (length(exchangeable_parameters) > 0L) {
    assert_character(
      sapply(names(exchangeable_parameters), h_hierarchical_safe_name),
      unique = TRUE
    )
  }
  if (length(pool_correlations) > 0L) {
    assert_character(
      sapply(names(pool_correlations), h_hierarchical_safe_name),
      unique = TRUE
    )
  }

  compiled_datamodel <- h_hierarchical_compile_datamodel(args)
  compiled_prior <- h_hierarchical_compile_priormodel(
    models_to_arms = args,
    parameter_pools = exchangeable_parameters,
    pool_correlations = pool_correlations,
    pool_priors = pool_priors
  )

  .HierarchicalModel(
    models_to_arms = args,
    parameter_pools = exchangeable_parameters,
    pool_correlations = pool_correlations,
    pool_priors = pool_priors,
    datamodel = compiled_datamodel,
    priormodel = compiled_prior$priormodel,
    modelspecs = h_hierarchical_compile_modelspecs(
      models_to_arms = args,
      parameter_pools = exchangeable_parameters,
      pool_correlations = pool_correlations,
      pool_priors = pool_priors
    ),
    init = h_hierarchical_compile_init(
      models_to_arms = args,
      parameter_pools = exchangeable_parameters,
      pool_correlations = pool_correlations
    ),
    datanames = "arms",
    datanames_prior = "arms",
    sample = compiled_prior$sample
  )
}

## default constructor ----

#' @rdname HierarchicalModel-class
#' @note Typically, end users will not use the `.DefaultHierarchicalModel()`
#'   function directly.
#' @export
.DefaultHierarchicalModel <- function() {
  mono_model <- LogisticLogNormal(
    mean = c(-0.85, 1),
    cov = matrix(c(1, -0.5, -0.5, 1), nrow = 2),
    ref_dose = 10
  )
  combo_model <- .DefaultTwoDrugsCombo()
  first_ref <- mono_model@sample[[1L]]
  second_ref <- mono_model@sample[[2L]]

  HierarchicalModel(
    mono = mono_model,
    combo = combo_model,
    exchangeable_parameters = list(
      mono_intercept = list(
        mono = first_ref,
        combo = paste0(first_ref, "[1]")
      ),
      mono_slope = list(
        mono = second_ref,
        combo = paste0(second_ref, "[1]")
      )
    )
  )
}

Try the crmPack package in your browser

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

crmPack documentation built on July 5, 2026, 9:06 a.m.