R/variance.R

Defines functions variance_function evaluate_variance_function validate_custom_variance_values prepare_variance_candidates resolve_variance_spec auto_selected_label selected_variance_label selected_variance_model_name power_label auto_model_name power_model_name selection_label format_power_value validate_power_grid default_power_grid normalize_variance_model

Documented in variance_function

normalize_variance_model <- function(model) {
  aliases <- c(
    x_half = "sqrt",
    x_third = "cuberoot",
    cube_root = "cuberoot",
    power_auto = "auto",
    auto_power = "auto"
  )

  if (model %in% names(aliases)) {
    return(aliases[[model]])
  }

  model
}

default_power_grid <- function() {
  sort(unique(c(seq(0.2, 1.0, by = 0.05), 1 / 3, 1 / 2)))
}

validate_power_grid <- function(power_grid) {
  out <- sort(unique(as.numeric(power_grid)))
  keep <- is.finite(out) & out > 0
  out <- out[keep]
  if (!length(out)) {
    stop("power_grid must contain at least one positive finite value.")
  }
  out
}

format_power_value <- function(power, digits = 2L) {
  txt <- formatC(as.numeric(power), format = "f", digits = digits)
  txt <- sub("0+$", "", txt)
  txt <- sub("\\.$", "", txt)
  txt
}

selection_label <- function(selection) {
  switch(
    selection,
    quasi_aic = "quasi-AIC",
    quasi_bic = "quasi-BIC",
    selection
  )
}

power_model_name <- function(power) {
  paste0("power(", format_power_value(power), ")")
}

auto_model_name <- function(selection) {
  paste0("auto(", selection, ")")
}

power_label <- function(power) {
  paste0("Var = gamma S^", format_power_value(power))
}

selected_variance_model_name <- function(model, power = NULL) {
  if (identical(model, "power")) {
    return(power_model_name(power))
  }
  model
}

selected_variance_label <- function(model, power = NULL) {
  if (identical(model, "power")) {
    return(power_label(power))
  }
  model_display_label(model)
}

auto_selected_label <- function(label, selection) {
  paste0(label, " (selected by ", selection_label(selection), " from log/power candidates)")
}

resolve_variance_spec <- function(variance = "log",
                                  power = NULL) {
  if (!is.function(variance)) {
    variance <- normalize_variance_model(variance)
  }
  fun <- variance_function(model = variance, power = power)

  list(
    model = attr(fun, "variance_model", exact = TRUE),
    label = attr(fun, "variance_label", exact = TRUE),
    fun = fun
  )
}

prepare_variance_candidates <- function(variance_models = default_variance_models(),
                                        power_values = NULL,
                                        include_auto = FALSE,
                                        power_selection = c("quasi_aic", "quasi_bic"),
                                        power_grid = default_power_grid(),
                                        variance_functions = NULL) {
  power_selection <- match.arg(power_selection)
  power_grid <- validate_power_grid(power_grid)

  models <- unique(vapply(variance_models, normalize_variance_model, character(1)))
  if ("power" %in% models && is.null(power_values)) {
    stop("Use 'power_values' to add fixed power models to screen_mm() or group_mm().")
  }
  if ("auto" %in% models) {
    include_auto <- TRUE
  }
  models <- setdiff(models, c("power", "auto"))

  bad <- setdiff(models, c("constant", "log", "sqrt", "cuberoot"))
  if (length(bad)) {
    stop("Unsupported variance model(s): ", paste(bad, collapse = ", "))
  }

  candidates <- lapply(models, function(model) {
    list(
      id = model,
      label = model_display_label(model),
      fit_args = list(variance = model)
    )
  })

  if (!is.null(power_values)) {
    power_values <- validate_power_grid(power_values)
    candidates <- c(
      candidates,
      lapply(power_values, function(power) {
        list(
          id = power_model_name(power),
          label = power_label(power),
          fit_args = list(variance = "power", power = power)
        )
      })
    )
  }

  if (isTRUE(include_auto)) {
    candidates <- c(
      candidates,
      list(
        list(
          id = auto_model_name(power_selection),
          label = paste0("Auto: best of log/power by ", selection_label(power_selection)),
          fit_args = list(
            variance = "auto",
            power_selection = power_selection,
            power_grid = power_grid
          )
        )
      )
    )
  }

  keep <- !duplicated(vapply(candidates, `[[`, character(1), "id"))
  candidates <- candidates[keep]

  if (!is.null(variance_functions) && length(variance_functions)) {
    ids <- names(variance_functions)
    if (!is.list(variance_functions) || is.null(ids) || anyNA(ids) ||
        any(!nzchar(trimws(ids))) || anyDuplicated(ids) ||
        !all(vapply(variance_functions, is.function, logical(1)))) {
      stop("variance_functions must be a named list of functions with unique, non-empty names.",
           call. = FALSE)
    }
    reserved <- c("constant", "log", "sqrt", "cuberoot", "power", "auto",
                  "custom", "x_half", "x_third", "cube_root", "power_auto",
                  "auto_power", vapply(candidates, `[[`, character(1), "id"))
    if (any(ids %in% reserved)) {
      stop("Custom variance function names must not duplicate built-in candidate names.",
           call. = FALSE)
    }
    custom <- lapply(seq_along(variance_functions), function(i) {
      fun <- variance_functions[[i]]
      label <- attr(fun, "variance_label", exact = TRUE)
      if (is.null(label)) {
        label <- paste0("Var = gamma h(S) [", ids[i], "]")
      }
      fun <- variance_function(fun, label = label)
      list(id = ids[i], label = label, fit_args = list(variance = fun))
    })
    candidates <- c(candidates, custom)
  } else if (!is.null(variance_functions) && !is.list(variance_functions)) {
    stop("variance_functions must be a named list of functions.", call. = FALSE)
  }

  if (!length(candidates)) {
    stop("Supply at least one built-in or custom variance candidate.", call. = FALSE)
  }
  candidates
}

validate_custom_variance_values <- function(values, x) {
  if (!is.numeric(values) || is.complex(values) || !is.null(dim(values)) ||
      length(values) != length(x)) {
    stop("A custom variance function must return a numeric vector with one value per concentration.",
         call. = FALSE)
  }
  if (any(!is.finite(values)) || any(values <= 0)) {
    stop("A custom variance function must return only finite, strictly positive values.",
         call. = FALSE)
  }
  as.numeric(values)
}

evaluate_variance_function <- function(fun, x) {
  values <- fun(x)
  if (identical(attr(fun, "variance_model", exact = TRUE), "custom")) {
    return(validate_custom_variance_values(values, x))
  }
  safe_pos(values)
}

#' Create Working Variance Functions
#'
#' Construct a working variance function for Michaelis-Menten models.
#'
#' @param model A built-in model name or a vectorized function of concentration
#'   returning one finite, strictly positive variance-shape value per input.
#' @param power Optional exponent used when `model = "power"`.
#' @param label Optional descriptive label for a user-defined function.
#'
#' @return A function of substrate concentration `x`.
#' @export
variance_function <- function(model = c("constant", "log", "sqrt", "cuberoot", "power"),
                              power = NULL,
                              label = NULL) {
  if (is.function(model)) {
    if (!is.null(power)) {
      stop("power cannot be supplied with a custom variance function.", call. = FALSE)
    }
    if (is.null(label)) {
      label <- attr(model, "variance_label", exact = TRUE)
      if (is.null(label)) label <- "Var = gamma h(S) (user-defined)"
    }
    if (!is.character(label) || length(label) != 1L || is.na(label) ||
        !nzchar(trimws(label))) {
      stop("label must be a single non-empty character string.", call. = FALSE)
    }
    if (identical(attr(model, "variance_model", exact = TRUE), "custom")) {
      out <- model
    } else {
      force(model)
      out <- function(x) {
        values <- tryCatch(model(x), error = function(e) {
          stop("Custom variance function failed: ", conditionMessage(e), call. = FALSE)
        })
        validate_custom_variance_values(values, x)
      }
    }
    attr(out, "variance_model") <- "custom"
    attr(out, "variance_label") <- label
    attr(out, "variance_power") <- NULL
    return(out)
  }
  if (!is.null(label)) {
    stop("label is only used with a custom variance function.", call. = FALSE)
  }
  model <- normalize_variance_model(match.arg(model))

  out <- switch(
    model,
    constant = function(x) rep(1, length(x)),
    log = function(x) log(x + 1),
    sqrt = function(x) x^(1 / 2),
    cuberoot = function(x) x^(1 / 3),
    power = {
      if (is.null(power) || !is.finite(power) || length(power) != 1L || power <= 0) {
        stop("A positive finite scalar 'power' must be supplied when model = 'power'.")
      }
      force(power)
      function(x) x^power
    }
  )

  attr(out, "variance_model") <- model
  attr(out, "variance_label") <- if (identical(model, "power")) {
    power_label(power)
  } else {
    model_display_label(model)
  }
  attr(out, "variance_power") <- if (identical(model, "power")) power else NULL
  out
}

Try the inferMM package in your browser

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

inferMM documentation built on Oct. 30, 2026, 9:06 a.m.