R/tramTMB.R

Defines functions predict.tramTMB duplicate.tramTMB duplicate update.tramTMB .robustInv Hess .optim_start optim_control optim_tramTMB .constr_adj .npars .expand_map .get_par .check_par tramTMB tramTMB_inputs sm_terms re_terms fe_terms .mod_negative tramME_model .tramME2ctm remove_from_formula drop_terms

Documented in optim_control predict.tramTMB tramTMB

## Helper function to recursively drop terms from a fomula
## @param f Formula or call.
## @param what Things to remove from \code{f}.
## @note Adapted from \code{\link[lme4]{nobars_}}.
drop_terms <- function(f, what = c("|", "||", "s", "te", "ti", "t2")) {
  chk <- function(ff) any(sapply(what, function(x) ff == as.name(x)))

  if (!any(what %in% all.names(f))) return(f)
  if (is.call(f) && chk(f[[1]])) return(NULL)
  if (length(f) == 2) {
    if (is.null(f[[2]] <- drop_terms(f[[2]], what = what))) return(NULL)
    return(f)
  }
  f2 <- drop_terms(f[[2]], what = what)
  f3 <- drop_terms(f[[3]], what = what)
  if (is.null(f2)) return(f3)
  if (is.null(f3)) return(f2)
  f[[2]] <- f2
  f[[3]] <- f3
  return(f)
}


## Remove terms from a formula
## @param f Formula or call.
## @param what Things to remove from \code{f}.
## @param right_only Remove only from the right side of \code{f}.
## @note Adapted from \code{\link[lme4]{nobars}}.
##' @importFrom stats reformulate
remove_from_formula <- function(f, what = c("|", "||", "s", "te", "ti", "t2"),
                                right_only = TRUE) {
  env <- environment(f)
  if (is(f, "formula") && length(f) == 3) {
    nfr <- drop_terms(f[[3]], what)
    nfl <- if (!right_only) drop_terms(f[[2]], what) else f[[2]]
    if (is.null(nfl) && is.null(nfr))
      nf <- ~1
    else if (is.null(nfl))
      nf <- reformulate(deparse1(nfr))
    else if (is.null(nfr))
      nf <- reformulate("1", response = deparse1(nfl))
    else
      nf <- reformulate(deparse1(nfr), response = deparse1(nfl))
  } else {
    nf <- drop_terms(f, what)
  }
  if (is.null(nf))
    nf <- if (is(f, "formula")) ~1 else 1
  if (is(nf, "formula"))
    environment(nf) <- env
  return(nf)
}


## Create a corresponding ctm model for a tramME model
##
## Takes a tramME formula and generates the FE ctm model (model_only = TRUE)
## @param formula Model formula.
## @param mname tram(ME) model name.
## @param ... Optional arguments passed to \code{\link[tram]{tram}}
.tramME2ctm <- function(formula, mname, ...) {
  .nosmooth <- function(trm) mgcv::interpret.gam(trm)$pf
  .nobars   <- function(trm) remove_from_formula(trm, c("|", "||"))
  mname <- sub("ME", "", mname) ## NOTE: remove suffix if present
  formula <- .nosmooth(.nobars(formula)) ## NOTE: we have to remove the bars first
  args <- as.list(match.call(expand.dots = TRUE)[-1L])
  args$formula <- formula
  args$mname <- NULL
  args$model_only <- TRUE
  ctmod <- do.call(do.call(what = `::`, args = list("tram", mname)), args = args)
  return(ctmod)
}


## Create an object that defines a tramME_model
##
## There are two ways of defining tramME models:
## \enumerate{
##   \item A ctm model and a formula defining the random effects and smooth terms.
##   \item A formula combining the notation of \pkg{tram}, \pkg{lme4} and \pkg{mgcv},
##     a tram function name, and a dataset to set up the bases.
## }
## @param formula formula that either describes the whole model or
##   the random effects specification. If the model contains random effects or
##   smooth terms \code{formula} has to contain their definition in
##   \pkg{lme4}-style and \pkg{mgcv}-style notation, respectively.
## @inheritParams tram::tram
## @param tram tram model name: Lm, BoxCox, Colr, Polr, Coxph, Survreg, Lehmann,
##   Aareg, or the suffixed versions of these (e.g. ColrME). Ignored when a \code{ctm} model
##   is also supplied.
## @param ctm A \code{ctm} model
## @param smooth Optional pre-defined smooth specification of the class \code{tramME_smooth}.
##   If present, the smooth terms in the formula are ignored.
## @param negative an optional parameter that defines whether the random effects have
##   a positive or a negative sign in the model when the fixed effecst part is defined
##   through a ctm
## @param ... optional arguments passed to \pkg{tram} when the model is defined by the formula
## @return A tramME_model object that defines the mixed effects transfromation model.
## @note Similarly to \pkg{mlt}, the offsets and the weights are not part of the model,
##   but they are data and they are not saved in the returned object.
tramME_model <- function(formula = NULL, data = NULL, tram = NULL, ctm = NULL,
                         smooth = NULL, negative = NULL, ...) {
  ## -- TODO: add tensor products (t2) and remove this
  if (!is.null(formula) &&
      !identical(formula, remove_from_formula(formula, c("te", "ti", "t2"))))
    stop("Tensor product splines are not implemented yet.")
  ## --
  re <- lme4::findbars(formula)
  if (!is.null(smooth) && inherits(smooth, "tramME_smooth")) {
    sm <- smooth
  } else {
    sm <- if (is.null(formula)) NULL else mgcv::interpret.gam(formula)$smooth.spec
    sm <- if (length(sm)) sm else NULL
  }
  out <- structure(
    list(ctm = ctm, formula = formula, negative = negative, ranef = re,
         smooth = sm),
    class = "tramME_model")
  if (is.null(ctm)) {
    ## no ctm model, the mixed-effects model is defined by the formula
    stopifnot(!is.null(tram), !is.null(formula))
    args <- as.list(match.call(expand.dots = TRUE)[-1L])
    args$mname <- tram
    args[c("tram", "ctm", "smooth", "negative")] <- NULL
    out$ctm <- do.call(".tramME2ctm", args = args)
  } else {
    ## There is a ctm that describes the FE part, formula could describe the RE
    ## part.
    stopifnot(inherits(ctm, "ctm"))
    out$formula <- fake_formula(structure(list(model = out), class = "tramME"))
  }
  if (is.null(negative)) {
    out$negative <- .mod_negative(ctm, tram)
  }
  return(out)
}


## Helper function to figure out if negative = TRUE in a given model
## @inheritParams tramME_model
.mod_negative <- function(ctm, tram = NULL) {
  if (is.null(tram)) {
    if (!is.null(ctm$bases$shifting)) {
      neg <- get("negative", envir = environment(ctm$bases$shifting),
                 inherits = FALSE)
    } else {
      neg <- mget("negative", envir = environment(ctm$bases$interacting),
                  ifnotfound = FALSE)$negative
    }
    stopifnot(!is.null(neg))
    return(neg)
  } else {
    tram <- sub("ME$", "", tram)
    return(switch(tram, Coxph = , Aareg = , Colr = FALSE,
                  Survreg = , Polr = , Lm = , BoxCox = ,
                  Lehmann = TRUE,
                  stop("Unknown model type")))
  }
}

## Create fixed effects data and initial parameters
## @param mod a mlt model
fe_terms <- function(mod) {
  par <- coef(mod)
  ## -- data
  dat <- mget(c("iY", "eY", "offset"),
    envir = environment(mod$logliki), ifnotfound = list(NULL), inherits = TRUE)
  out <- list()
  ## === Constraints & parameter types
  if (!is.null(dat$eY)) {
    out$constr <- attr(dat$eY$Y, "constraint")
    assign <- attr(dat$eY$Y, "Assign")
  } else {
    out$constr <- attr(dat$iY$Yleft, "constraint")
    assign <- attr(dat$iY$Yleft, "Assign")
  }
  out$pargroup <- apply(assign, 2, function(x) {
    if (any(grepl("shifting", x))) {
      out <- "shift"
    } else {
      out <- "baseline"
    }
    out
  })
  ## === Setting up blank values
  if (is.null(dat$iY)) {
    nm <- colnames(dat$eY$Y)
    dat$iY$Yleft <- dat$iY$Yright <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$iY$Yleft) <- colnames(dat$iY$Yright) <- nm
    dat$iY$which <- integer(0)
    dat$iY$trunc$left <- dat$iY$trunc$right <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$iY$trunc$left) <- colnames(dat$iY$trunc$right) <- nm
  }
  if (is.null(dat$eY)) {
    nm <- colnames(dat$iY$Yleft)
    dat$eY$Y <- dat$eY$Yprime <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$eY$Y) <- colnames(dat$eY$Yprime) <- nm
    dat$eY$which <- integer(0)
    dat$eY$trunc$left <- dat$eY$trunc$right <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$eY$trunc$left) <- colnames(dat$eY$trunc$right) <- nm
  }
  ## === Censoring
  idxr <- which(is.finite(dat$iY$Yleft[, 1]) & !is.finite(dat$iY$Yright[, 1]))
  idxl <- which(!is.finite(dat$iY$Yleft[, 1]) & is.finite(dat$iY$Yright[, 1]))
  idxi <- which(is.finite(dat$iY$Yleft[, 1]) & is.finite(dat$iY$Yright[, 1]))
  out$censl <- list(ay = dat$iY$Yright[idxl, , drop = FALSE],
                    which = dat$iY$which[idxl])
  out$censr <- list(ay = dat$iY$Yleft[idxr, , drop = FALSE],
                    which = dat$iY$which[idxr])
  out$censi <- list(ayl = dat$iY$Yleft[idxi, , drop = FALSE],
                    ayr = dat$iY$Yright[idxi, , drop = FALSE],
                    which = dat$iY$which[idxi])
  ## === Exact observations
  out$exact <- list(ay = dat$eY$Y, aypr = dat$eY$Yprime, which = dat$eY$which)
  ## === Offsets, weights, error distribution, etc
  out$offset <- dat$offset
  ## out$weights <- mod$weights
  ## out$negative <- mod$negative
  out$errdistr <- mod$todistr$name
  ## === Truncation
  nm <- colnames(dat$iY$Yleft)
  if (is.null(dat$eY$trunc$left)) {
    dat$eY$trunc$left <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$eY$trunc$left) <- nm
  }
  if (is.null(dat$eY$trunc$right)) {
    dat$eY$trunc$right <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$eY$trunc$right) <- nm
  }
  if (is.null(dat$iY$trunc$left)) {
    dat$iY$trunc$left <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$iY$trunc$left) <- nm
  }
  if (is.null(dat$iY$trunc$right)) {
    dat$iY$trunc$right <- matrix(0, nrow = 0, ncol = length(nm))
    colnames(dat$iY$trunc$right) <- nm
  }
  ## Left
  idxi <- which(is.finite(dat$iY$trunc$left[, 1]) &
                !is.finite(dat$iY$trunc$right[, 1]))
  idxe <- which(is.finite(dat$eY$trunc$left[, 1]) &
                !is.finite(dat$eY$trunc$right[, 1]))
  out$truncl <- list(ay = rbind(dat$iY$trunc$left[idxi, , drop = FALSE],
                                dat$eY$trunc$left[idxe, , drop = FALSE]),
                     which = c(dat$iY$which[idxi], dat$eY$which[idxe]))
  ## Right
  idxi <- which(!is.finite(dat$iY$trunc$left[, 1]) &
                is.finite(dat$iY$trunc$right[, 1]))
  idxe <- which(!is.finite(dat$eY$trunc$left[, 1]) &
                is.finite(dat$eY$trunc$right[, 1]))
  out$truncr <- list(ay = rbind(dat$iY$trunc$right[idxi, , drop = FALSE],
                                dat$eY$trunc$right[idxe, , drop = FALSE]),
                     which = c(dat$iY$which[idxi], dat$eY$which[idxe]))
  ## Interval
  idxi <- which(is.finite(dat$iY$trunc$left[, 1]) &
                is.finite(dat$iY$trunc$right[, 1]))
  idxe <- which(is.finite(dat$eY$trunc$left[, 1]) &
                is.finite(dat$eY$trunc$right[, 1]))
  out$trunci <- list(ayl = rbind(dat$iY$trunc$left[idxi, , drop = FALSE],
                                 dat$eY$trunc$left[idxe, , drop = FALSE]),
                     ayr = rbind(dat$iY$trunc$right[idxi, , drop = FALSE],
                                 dat$eY$trunc$right[idxe, , drop = FALSE]),
                     which = c(dat$iY$which[idxi], dat$eY$which[idxe]))
  ## -- parameters
  out$offset <- NULL ## FIXME: check this
  out$beta <- par
  return(out)
}

## Create random effects data and initial paramaters
## @param ranef a list of random effects formulas from \code{\link[lme4]{findbars}}
## @param data data.frame containing the variables of the model
## @param negative logical value that indicates whether the random effects have
##   a negative sign
## @param drop.unused.levels
## @return A list containing data and parameter values to be used in the TMB model.
re_terms <- function(ranef, data, negative, drop.unused.levels = TRUE) {
  if (is.null(ranef)) {
    out <- list(Zt = nullTMatrix(nrow = 0, ncol = nrow(data)),
                termsize = integer(0), blocksize = integer(0),
                ui = Matrix::Matrix(0, nrow = 0, ncol = 0),
                ci = numeric(0),
                gamma = numeric(0), theta = numeric(0))
  } else {
    rt <- lme4::mkReTrms(ranef, data, drop.unused.levels = drop.unused.levels,
                         reorder.terms = FALSE)
    out <- list()
    out$Zt <- rt$Zt
    if (negative) out$Zt <- -out$Zt
    ## --- Structure of the RE covariance matrix
    out$termsize <- sapply(rt$Ztlist, NROW)
    out$blocksize <- sapply(rt$cnms, length)
    out$npar <- sum(out$blocksize * (out$blocksize + 1) / 2)
    out$names <- rt$cnms
    out$levels <- lapply(rt$flist, levels)
    ## --- Constraints
    out$ui <- Matrix::Diagonal(out$npar)
    out$ci <- rep(-Inf, out$npar)
    out$gamma <- rep(0, nrow(out$Zt))
    out$theta <- rep(0, out$npar)
  }
  return(out)
}

## Create data and initial parameter values required for the smooth terms
##
## If \code{smooth} is of \code{tramME_smooth} class, it has an additional data attribute
## that contains the observations to be used to set up the smooth terms.
## @param smooth a list of smooth terms from \code{\link[mgcv]{interpret.gam}} or
##   an object of \code{tramME_smooth} class.
## @param data data.frame containing the variables of the model
## @param negative logical value that indicates whether the random effects have
##   a negative sign
## @return A list containing data and parameter values to be used in the TMB model.
sm_terms <- function(smooth, data, negative) {
  out <- list()
  if (is.null(smooth)) {
    out$X <- matrix(0, nrow = nrow(data), ncol = 0)
    out$Z <- nullTMatrix(nrow = nrow(data), ncol = 0)
    out$re_dims <- numeric(0)
  } else {
    if (inherits(smooth, "tramME_smooth")) {
      sm <- .sm_data(smooth, data = attr(smooth, "data"), newdata = data)
    } else {
      sm <- .sm_data(smooth, data)
    }
    out$X <- do.call("cbind", sm$X)
    if (length(sm$Z)) {
      out$Z <- as_dgTMatrix(do.call("cbind", sm$Z))
      out$re_dims <- sapply(sm$Z, ncol)
    } else {
      out$Z <- nullTMatrix(nrow = nrow(out$X), ncol = 0L)
      out$re_dims <- numeric(0)
    }
  }
  if (negative) {
    out$X <- -out$X
    out$Z <- -out$Z
  }
  out$uiX <- Matrix::Diagonal(ncol(out$X))
  out$ciX <- rep(-Inf, ncol(out$X))
  out$uiZ <- Matrix::Diagonal(length(out$re_dims))
  out$ciZ <- rep(-Inf, length(out$re_dims))
  out$beta <- rep(0, ncol(out$X))
  out$theta <- rep(0, length(out$re_dims))
  out$gamma <- rep(0, ncol(out$Z))
  return(out)
}


## Create inputs of the tramTMB model
##
## The function generates random values for \code{NA} values in the list of initial parameter
## values.
## @param model a list describing the structure of the model as returned by \code{tramME_model}
## @param ft fixed effects terms as returned by the function \code{fe_terms}
## @param rt random effects terms as returned by the function \code{re_terms}
## @param st smooth terms as returned by the function \code{sm_terms}
## @param data model frame containing offsets and weights
## @param fixed optional named vector with \code{c("name" = value)} pairs of
##   parameters to be fixed
## @param param optional named list of initial parameter values
## @return A list with data matrices and initial parameter values
##' @importFrom stats model.offset model.weights
## FIXME: update man
tramTMB_inputs <- function(model, ft, rt, st, data, param, initpar= NULL) {
  os <- model.offset(data) ## NOTE: get offset and weights from the model frame
  if (is.null(os)) os <- rep(0, nrow(data))
  we <- model.weights(data)
  if (is.null(we)) we <- rep(1, nrow(data))
  errdist <-  which(model$ctm$todistr$name ==
    c("normal", "logistic", "minimum extreme value", "maximum extreme value", "exponential")) - 1L
  bl <- ft$pargroup == "baseline"
  X <- rbind(ft$exact$ay[, !bl, drop = FALSE], ft$censl$ay[, !bl, drop = FALSE],
             ft$censr$ay[, !bl, drop = FALSE], ft$censi$ayl[, !bl, drop = FALSE])
  idx <- c(ft$exact$which, ft$censl$which, ft$censr$which, ft$censi$which)
  X <- X[order(idx), ]
  out <- list(
    data = list(
      errdist = rep(errdist,
        length(ft$censl$which) + length(ft$censr$which) +
        length(ft$censi$which) + length(ft$exact$which)),
      Yl = ft$censl$ay[, bl, drop = FALSE], Yr = ft$censr$ay[, bl, drop = FALSE],
      Yil = ft$censi$ayl[, bl, drop = FALSE], Yir = ft$censi$ayr[, bl, drop = FALSE],
      Ye = ft$exact$ay[, bl, drop = FALSE], Yeprime = ft$exact$aypr[, bl, drop = FALSE],
      whichl = ft$censl$which - 1L, whichr = ft$censr$which - 1L,
      whichi = ft$censi$which - 1L, whiche = ft$exact$which - 1L,
      Ytl = ft$truncl$ay[, bl, drop = FALSE], Ytr = ft$truncr$ay[, bl, drop = FALSE],
      Ytil = ft$trunci$ayl[, bl, drop = FALSE], Ytir = ft$trunci$ayr[, bl, drop = FALSE],
      whichtl = ft$truncl$which - 1L, whichtr = ft$truncr$which - 1L,
      whichti = ft$trunci$which - 1L,
      X = cbind(X, st$X),
      Z = cbind(Matrix::t(rt$Zt), st$Z),
      re_termsize = c(rt$termsize, st$re_dims),
      re_blocksize = c(rt$blocksize, rep(1L, length(st$re_dims))),
      offset = os, weights = we
    ),
    parameters = list(beta0 = ft$beta[bl], beta = c(ft$beta[!bl], st$beta),
      gamma = c(rt$gamma, st$gamma), theta = c(rt$theta, st$theta)),
    constraint = list(ui = as.matrix(Matrix::bdiag(ft$constr$ui, st$uiX, rt$ui, st$uiZ)),
                      ci = c(ft$constr$ci, st$ciX, rt$ci, st$ciZ)),
    negative = model$negative
  )
  if (is.list(initpar)) {
    if (!is.null(initpar$beta)) {
      ## NOTE: initpar is expected to have the same parameter structure as a tramME
      ## (list(beta, theta)), while in tramTMB, we split up beta to beta0 (baseline & interacting)
      ## beta (shifting FE) vectors. Convert from the fist to the second
      initpar$beta0 <- initpar$beta[bl]
      initpar$beta <- initpar$beta[!bl]
    }
    for (n in names(initpar)) {
      stopifnot(length(out$parameters[[n]]) == length(initpar[[n]]))
      out$parameters[[n]][] <- initpar[[n]]
    }
  }
  ## -- missing values are substituted with random initial values
  out$parameters$beta0[is.na(out$parameters$beta0)] <-
    sort(runif(sum(is.na(out$parameters$beta0))))
  out$parameters$beta[is.na(out$parameters$beta)] <-
    runif(sum(is.na(out$parameters$beta)))
  out$parameters$gamma[is.na(out$parameters$gamma)] <-
    runif(sum(is.na(out$parameters$gamma)))
  out$parameters$theta[is.na(out$parameters$theta)] <-
    runif(sum(is.na(out$parameters$theta)))
  ## -- convert fixed parameters as input to TMB (map)
  mp <- list()
  ## beta0 & beta
  bl <- attr(param$beta, "type") == "bl"
  fx <- attr(param$beta, "fixed")
  fx0 <- fx[bl]
  fx1 <- fx[!bl]
  if (any(fx0)) {
    mp$beta0 <- rep(NA, length(fx0))
    mp$beta0[!fx0] <- seq(sum(!fx0))
    mp$beta0 <- as.factor(mp$beta0)
  }
  if (any(fx1)) {
    mp$beta <- rep(NA, length(fx1))
    mp$beta[!fx1] <- seq(sum(!fx1))
    mp$beta <- as.factor(mp$beta)
  }
  for (n in c("theta", "gamma")) {
    fx <- attr(param[[n]], "fixed")
    if (any(fx)) {
      out$parameters[[n]][fx] <- param[[n]][fx]
      mp[[n]] <- rep(NA, length(fx))
      mp[[n]][!fx] <- seq(sum(!fx))
      mp[[n]] <- as.factor(mp[[n]])
    }
  }
  out$map <- mp
  return(out)
}


##' Create a tramTMB object
##'
##' @useDynLib tramME, .registration = TRUE
##' @param constraint list describing the constarints on the parameters
##' @param negative logical, whether the model is parameterized with negative values
##' @param map same as map argument of \code{TMB::MakeADFun}
##' @inheritParams TMB::MakeADFun
##' @param resid logical, indicating whether the score residuals are calculated
##'   from the resulting object
##' @param do_update logical, indicating whether the model should be set up with
##'   updateable offsets and weights
##' @param check_const Logical; if \code{TRUE} check the parameter constraints before
##'   evaluating the returned functions.
##' @param no_int Logical; if \code{FALSE} skip the numerical integration step.
##' @param ... optional parameters passed to \code{TMB::MakeADFun}
##' @return A tramTMB object.
##' @importFrom utils tail
##' @note The post-estimation parameters are supplied as a part of \code{data}
##' @export
## FIXME: check_const to manual
## FIXME: no_int to manual
## FIXME: consolidate no_int & part
tramTMB <- function(data, parameters, constraint, negative, map = list(),
                    resid = FALSE, do_update = FALSE, check_const = TRUE,
                    no_int = FALSE, ...) {
  ## --- ME or FE
  random <- NULL
  if (length(parameters$gamma) > 0 && !no_int)
    random <- "gamma"
  if (no_int) check_const <- FALSE
  ## TODO: REML option by adding beta to random
  ## --- add auxiliary parameters for residuals
  if (resid) {
    nn <- length(data$offset)
    parameters$alpha0 <- rep(0, nn)
  } else {
    parameters$alpha0 <- numeric(0)
  }
  if (do_update) {
    data$do_update <- 1
  } else {
    data$do_update <- 0
  }
  ## --- Adding missing post-estimation data
  if (is.null(data$postest_scale) || data$postest_scale == 0L) {
    data$Ype <- matrix(0, nrow = 0, ncol = length(parameters$beta0))
    data$Xpe <- matrix(0, nrow = 0, ncol = length(parameters$beta))
    data$Zpe <- nullTMatrix(nrow = 0L, ncol = length(parameters$gamma))
    data$postest_scale <- 0
  }
  if (is.null(data$as_lm)) data$as_lm <- 0
  ## --- Evaluate separate parts of the nll
  if (is.null(data$part))  data$part <- 0
  ## NOTE: if we want to evaluate the unpenalized part or the penalty
  ## of the nll, we usually want to treat all parameters as random
  ## We are not doing an optimization in these cases, just evaluation.
  if (data$part > 0) {
    random <- NULL
    check_const <- FALSE
  }
  ## --- create the TMB object
  obj <- TMB::MakeADFun(data = data, parameters = parameters, random = random,
                        DLL = "tramME", map = map, ...)
  fn <- obj$fn
  gr <- obj$gr
  he <- obj$he
  if (resid) {
    resid_idx <- tail(seq(length(obj$par)), nn)
    out <- list(
      fn = function(par, ...) {
        ## check_par(par)
        fn(c(par, rep(0, length(resid_idx))), ...)
      },
      gr = function(par, ...) {
        gr(c(par, rep(0, length(resid_idx))), ...)[-resid_idx]
      },
      he = function(par, ...) {
        he(c(par, rep(0, length(resid_idx))), ...)[-resid_idx, -resid_idx]
      },
      resid = function(par, ...) {
        res <- gr(c(par, rep(0, length(resid_idx))), ...)[resid_idx]
        if (!negative)
          res <- -res
        return(res)
      }
    )
  } else {
    resid_idx <- NULL
    out <- list(
      fn = function(par, ...) {
        ## check_par(par)
        fn(par, ...)
      },
      gr = function(par, ...) gr(par, ...),
      he = function(par, ...) he(par, ...),
      resid = function(par, ...) {
        stop("Residuals are not calculated in this model.")
      }
    )
  }

  out <- c(out, obj[!(names(obj) %in% c("fn", "gr", "he"))])

  if (resid)
    out$par <- obj$par[-resid_idx]

  ## --- add some info to out$env
  out$env$negative <- negative
  out$env$do_update <- do_update
  out$env$resid <- resid
  out$env$resid_idx <- resid_idx
  out$env$check_const <- check_const

  ## --- adjust constraints to map
  out$env$constraint <- .constr_adj(par = parameters, constr = constraint, map = map)
  ## --- Parameter checking: constraints
  out$env$par_checked <- NULL
  stopifnot(.check_par(out, out$par)) ## check initial parameters
  class(out) <- c("tramTMB", class(out))
  rm(list = setdiff(ls(), c("fn", "gr", "he", "resid_idx", "negative", "out")))
  return(out)
}

## Helper function to check parameter constarints
## @param obj A \code{tramTMB} object
## @param par A parameter vector
## @param eps Tolearnce level
## @param ... optional arguments
.check_par <- function(obj, par, eps = 1e-7, ...) {
  ## NOTE: tolerance (eps) is implied by the default value in auglag which is also
  ## used by tram
  ## FIXME: clean this up
  if (!obj$env$check_const) return(invisible(TRUE))
  if (nrow(obj$env$constraint$ui) > 0)
    out <- all(obj$env$constraint$ui %*% par - obj$env$constraint$ci > -eps)
  else out <- TRUE
  if (isTRUE(out)) {
    obj$env$par_checked <- par ## FIXME: par_checked[] to keep names?
  }
  invisible(out)
}

## Helper function to extract formatted parameters
## @param obj A \code{tramTMB} object
## @param par A parameter vector to be formatted
## @param fixed Logical; get fixed parameters, too
## @param full Should the unused parameters also be returned?
.get_par <- function(obj, par = obj$env$par_checked, fixed = TRUE, full = FALSE) {
  res <- obj$gr(par) ## NOTE: to update last.par
  if (any(obj$env$resid)) {
    par <- c(par, rep(0, length(obj$env$resid_idx)))
    out <- obj$env$parList(x = par)
    out$alpha0 <- NULL ## remove residuals
  } else {
    out <- obj$env$parList(x = par)
  }
  if (!full) {
    nz <- sapply(out, function(x) length(x) > 0)
    out <- out[nz]
  }
  map <- obj$env$map
  if (!fixed && length(map) > 0) {
    for (n in names(map)) {
      out[[n]] <- out[[n]][!is.na(map[[n]])]
    }
  }
  out
}

## Returns an extended map list with the same structure as par
## @param map Named and possibly incomplete list of parameters that shows
##   the fixed parameters. (See \code{TMB})
## @param par A complete named list of parameters
.expand_map <- function(map, par) {
  out <- lapply(par, function(x) as.factor(seq_along(x)))
  for (n in names(map)) {
    out[[n]] <- map[[n]]
  }
  return(out)
}

## Returns the number of parameters in an object
## @param obj A \code{tramTMB} object
## @param names A character vector of names to restrict
## @param fixed Logical, count the fixed parameters, too.
.npars <- function(obj, names = NULL, fixed = TRUE) {
  pr <- .get_par(obj, fixed = fixed)
  if (!is.null(names))
    pr <- pr[names]
  length(unlist(pr))
}

## Helper function to adjust the constraints consistently with the parameter
## restrictions
## @param par list containing the vector of parameters
## @param constr list containing the the constraints
## @inheritParams TMB::MakeADFun
## @return A list with adjusted constraints.
.constr_adj <- function(par, constr, map) {
  uin <- constr$ui
  cin <- constr$ci
  stopifnot(ncol(uin) == length(unlist(par[c("beta0", "beta", "theta")])))
  if (length(map) > 0) {
    map <- .expand_map(map, par)
    ## Fixing parameter values
    par <- c(par$beta0, par$beta, par$theta)
    mp <- which(c(is.na(map$beta0), is.na(map$beta), is.na(map$theta)))
    if (length(mp) > 0) {
      cin <- cin - c(uin[, mp, drop = FALSE] %*% par[mp])
      uin <- uin[, -mp, drop = FALSE]
    }
    ## Equality of parameter values (only within the same parameter vector)
    map <- lapply(map, function(x) x[!is.na(x)])
    if (length(map$beta0) > 0)
      map$beta0 <- paste0("b0", map$beta0)
    if (length(map$beta) > 0)
      map$beta <- paste0("b", map$beta)
    if (length(map$theta) > 0)
      map$theta <- paste0("th", map$theta)
    mp <- as.factor(c(map$beta0, map$beta, map$theta))
    uin <- do.call("cbind", lapply(unique(mp), function(x) {
      rowSums(uin[, mp == x, drop = FALSE])
    }))
  }
  ## Remove all zero rows
  re <- apply(uin, 1, function(x) all(x == 0))
  uin <- uin[!re, , drop = FALSE]
  cin <- cin[!re]
  ## Eliminate -Inf constraints
  re <- is.infinite(cin)
  uin <- uin[!re, , drop = FALSE]
  cin <- cin[!re]
  ## remove duplicate rows
  mm <- unique(cbind(uin, cin))
  uin <- mm[, -ncol(mm), drop = FALSE]
  cin <- mm[, ncol(mm)]
  return(list(ui = unname(uin), ci = unname(cin)))
}


## Optimize the tramTMB object
##
## Currently only with \code{alabama::auglag} with either \code{nlminb} or \code{optim}
## in the case of constrained optimization and \code{nlminb} if there are no constraints.
## @param obj a tramTMB object
## @param par optional vector of initial parameter values
## @param method the method used by \code{alabama::auglag}
## @param control a list of control parameters
## @param trace logical, whether the trace should be printed during the optimization
## @param ntry number of restarts with perturbed initial values when not converged
## @param scale Logical, if \code{TRUE}, the fixed effects design matrices are scaled
##   to improve convergence
## @param ... optional arguments, currently not in use
##' @importFrom stats nlminb
##' @importFrom stats optim
## FIXME: optional final check, w/ pdHess, mgc
optim_tramTMB <- function(obj, par = NULL, method = "nlminb", control = list(),
                          trace = FALSE, ntry = 5, scale = TRUE, ...) {
  if (!is.null(par)) {
    if (!.check_par(obj, par))
      warning(paste("The supplied initial values do not satisfy the constraints.",
                    "Falling back to the value par_checked."))
  }
  par <- obj$env$par_checked
  stopifnot(!is.null(par))
  ## --- scale
  if (scale) {
    mp <- .expand_map(obj$env$map, .get_par(obj, fixed = TRUE))
    fix <- is.na(mp$beta0)
    X <- do.call("rbind",
      obj$env$data[c("Yr", "Yl", "Yil", "Yir", "Ye",
                     "Ytl", "Ytr", "Ytil", "Ytir")])
    sc0 <- apply(abs(X[, !fix, drop = FALSE]), 2, max)
    if (.npars(obj, "beta") > 0) {
      fix <- is.na(mp$beta)
      sc1 <- apply(abs(obj$env$data$X[, !fix, drop = FALSE]), 2, max)
      sc <- c(sc0, sc1)
    } else {
      sc <- sc0
    }
    lt1 <- sc < 1.1
    gt1 <- sc >= 1.1
    sc[gt1] <- 1 / sc[gt1]
    sc[lt1] <- 1
    sc <- c(sc, rep(1, .npars(obj, "theta", fixed = FALSE)))
    par <- par / sc
    fn <- function(par) obj$fn(sc * par)
    gr <- function(par) obj$gr(sc * par) * sc
    ui <- obj$env$constraint$ui
    ci <- obj$env$constraint$ci
    if (!is.null(ui)) {
      ui <- t(t(ui) * sc)
    }
  } else {
    fn <- obj$fn
    gr <- obj$gr
    ui <- obj$env$constraint$ui
    ci <- obj$env$constraint$ci
  }
  ## ---
  warn <- NULL
  opt_time <- system.time( ## FIXME: decide if the timing is needed
    for (i in 1:ntry) {
      opt <- withCallingHandlers(
        if (!is.null(ui) && nrow(ui) > 0) {
          try(alabama::auglag(par = par, fn = fn, gr = gr,
                hin = function(par) ui %*% par - ci, hin.jac = function(par) ui,
                control.outer = list(method = method, kkt2.check = FALSE, trace = trace),
                control.optim = control)[c("par", "convergence", "value")],
              silent = !trace)
        } else {
          switch(method,
            nlminb = {
              try({
                control$trace <- trace
                op <- nlminb(par, objective = fn, gradient = gr,
                  control = control)[c("par", "convergence", "objective")]
                op$value <- op$objective
                op$objective <- NULL
                op}, silent = !trace)
            },
            try({
                control$trace <- trace
                op <- optim(par, fn = fn, gr = gr, method = method,
                  control = control)[c("par", "convergence", "value")]
                op}, silent = !trace))
        },
        warning = function(w) {
          warn <<- append(warn, conditionMessage(w))
          invokeRestart("muffleWarning")
        })
      if (!inherits(opt, "try-error") && opt$convergence == 0) break
      par <- .optim_start(obj, par = par)
      warn <- NULL
      warn <- paste0("Number of optimization restarts: ", i)
      if (inherits(opt, "try-error"))
        opt <- list(par = par, convergence = 1)
    }
  , gcFirst = FALSE)
  opt$time <- opt_time
  opt$warnings <- warn
  if (scale) {
    opt$par <- opt$par * sc
  }
  ## NOTE: final sanity check may be redundant
  if (opt$convergence == 0) {
    if (!.check_par(obj, opt$par))
      warning("The optimum does not satisfy the parameter constraints!")
  }
  return(opt)
}


##' Set up and control optimization parameters
##' @param method Optimization procedure.
##' @param scale Logical; if \code{TRUE} rescale the fixed effects design matrix to improve
##'   convergence.
##' @param trace Logical; print trace of the optimization.
##' @param ntry Number of restarts with new random initialization if optimization
##'   fails to converge.
##' @param ... Optional arguments passed to \code{\link[alabama]{auglag}},
##'   \code{\link[stats]{nlminb}} or \code{\link[stats]{optim}} as a list of control
##'   parameters.
##' @export
optim_control <- function(method = c("nlminb", "BFGS", "CG", "L-BFGS-B"),
                          scale = TRUE, trace = FALSE, ntry = 5, ...) {
  method <- match.arg(method)
  list(method = method, scale = scale, trace = trace,
       ntry = ntry, control = list(...))
}


## Get starting values for the fixed effects parameter vector
## NOTE: adapted from .mlt_start
##' @importFrom stats qlogis qnorm qexp lm.fit rnorm runif
.optim_start <- function(obj, par = NULL, resp = NULL) {
  stopifnot(!(is.null(par) && is.null(resp)))
  dat <- obj$env$data
  ## 1) First try: use the strategy similar to mlt
  if (is.null(par)) {
    resp <- R(resp)$approxy
    ## -- NOTE: crude weighted ECDF
    we <- dat$weights
    rwe <- round(we)
    rresp <- rep(resp, rwe)
    if (inherits(resp, "factor")) {
      ra <- rank(rresp)
    } else {
      ra <- xtfrm(rresp)
    }
    pstart <- ra / max(ra)
    pstart <- pstart[cumsum(rwe)]
    pstart <- pmax(.01, pmin(pstart, .99))
    ## -- NOTE: alternative
    ## we <- dat$weights
    ## y <- mlt::R(object = resp)
    ## pstart <- attr(y, "prob")(we)(y$approxy)
    ## pstart <- pmax(.01, pmin(pstart, .99))
    ## --
    ## -- constraints
    nb <- .npars(obj, names = c("beta0", "beta"), fixed = FALSE)
    ui <- obj$env$constraint$ui[, 1:nb, drop = FALSE]
    ci <- obj$env$constraint$ci + sqrt(.Machine$double.eps)
    ## --
    X <- matrix(0, nrow = length(resp),
                ncol = .npars(obj, names = "beta0", fixed = TRUE))
    X[dat$whiche+1, ] <- dat$Ye
    X[dat$whichl+1, ] <- dat$Yl
    X[dat$whichi+1, ] <- dat$Yil
    X[dat$whichr+1, ] <- 0
    if (.npars(obj, "beta", fixed = TRUE) > 0) {
      X <- cbind(X, dat$X[c(dat$whiche+1, dat$whichl+1, dat$whichi+1, dat$whichr+1), ])
    }
    ## -- fixed
    mp <- unlist(.expand_map(obj$env$map, .get_par(obj, fixed = TRUE))[c("beta0", "beta")])
    fix <- is.na(mp)
    os <- dat$offset
    os <- os + X[, fix, drop = FALSE] %*% unlist(.get_par(obj)[c("beta0", "beta")])[fix]
    X <- X[, !fix, drop = FALSE]
    ## --
    ed <- dat$errdist
    z <- numeric(length(pstart))
    z[ed == 0] <- qnorm(pstart[ed == 0])
    z[ed == 1] <- qlogis(pstart[ed == 1])
    z[ed == 2] <- log(-log1p(-pstart[ed == 2]))
    z[ed == 3] <- -log(-log(pstart[ed == 3]))
    z[ed == 4] <- qexp(pstart[ed == 4])
    z <- z - os

    X <- X * sqrt(we)
    z <- z * sqrt(we)
    dvec <- crossprod(X, z)
    Dmat <- crossprod(X)
    diag(Dmat) <- diag(Dmat) + 1e-08

    if (!is.null(ui) && nrow(ui) > 0) {
      bb <- try(c(coneproj::qprog(Dmat, dvec, ui, ci, msg = FALSE)$thetahat),
                 silent = TRUE)
      if (inherits(bb, "try-error")) {
        diag(Dmat) <- diag(Dmat) + 1e-3
        bb <- c(coneproj::qprog(Dmat, dvec, ui, ci, msg = FALSE)$thetahat)
      }
    } else {
      bb <- lm.fit(x = X, y = z)$coef
    }
    out <- rep(0, length(obj$par))
    out[1:nb] <- bb
  }

  ## 2) Draw a random vector of initial parameter values
  if (!is.null(par) || any(is.na(out))) {
    ui <- obj$env$constraint$ui
    ci <- obj$env$constraint$ci
    ## NOTE: use the generalized inverse to invert the constraint matrix
    uii <- tcrossprod(MASS::ginv(crossprod(ui)), ui)
    bb <- NULL
    for (i in 1:100) {
      bb <- c(uii %*% exp(rnorm(ncol(uii), mean = 0, sd = 0.5)))
      if (all(ui %*% bb > ci)) {
        break
      }
    }
    if (is.null(bb)) {
      out <- sort(runif(length(par))) ## Last resort
    } else {
      out <- bb
    }
  }

  return(out)
}


## Hessian of the negative log-likelihood function
## @param object A \code{tramTMB} object.
## @param par An optional vector of parameter values.
## @param method Method for calculating the covariance matrix.
## @param control Optional named list of controls to be passed to the specific methods.
## @param joint If \code{TRUE}, calculate joint precision.
## @param sparse If \code{TRUE}, the output is forced to be a symmetric sparse matrix
## @param ... Optional arguments (ignored)
##' @importFrom stats optimHess
##' @importFrom Matrix forceSymmetric
Hess <- function(object, par = object$env$par_checked,
                 method = c("optimHess", "numDeriv", "analytical"),
                 control = list(), joint = FALSE,
                 sparse = FALSE, ...) {
  if (missing(method) && is.null(object$env$random))
    method <- "analytical"
  method <- match.arg(method)
  if (!.check_par(object, par))
    stop("The supplied parameter vector does not satisfy the constraints.")
  he <- switch(method,
    optimHess = optimHess(par, object$fn, object$gr, control = control),
    numDeriv = {
      if (!is.null(control$method)) {
        meth <- control$method
        control$method <- NULL
      } else {
        meth <- "Richardson"
      }
      numDeriv::jacobian(func = object$gr, x = par,
                         method = meth, method.args = control)
    },
    analytical = {
      stopifnot(is.null(object$env$random))
      object$he(par)
    })
  rownames(he) <- colnames(he) <- names(object$par)
  if (joint) {
    sdr <- TMB::sdreport(object, par.fixed = par, hessian.fixed = he,
                         getJointPrecision = TRUE)
    he <- sdr$jointPrecision
  }
  if (sparse)
    he <- forceSymmetric(he)
  return(he)
}

## FIXME: replace this with try_solve in predict.tramTMB
## Trying harder to invert the Hessian (same as in \code{vcov.mlt})
## @param he The Hessian matrix
## @param lam Adjustmet factor. \code{lam = 0} switches off the robust option.
## @param ret_Hess Return the adjusted Hessian
## @return The variance-covariance matrix
.robustInv <- function(he, lam = 1e-6, ret_Hess = FALSE, ...) {
  step <- 0
  while((step <- step + 1) <= 3) {
    he2 <- he + (step - 1) * lam * diag(nrow(he))
    vc <- try(solve(he2), silent = TRUE)
    if (!inherits(vc, "try-error")) {
      if (step > 1) warning("Hessian could not be inverted, an approximation is used.")
      break
    }
    if (lam == 0) break
  }
  if (inherits(vc, "try-error")) vc <- he * NaN
  if (ret_Hess) return(he2)
  return(vc)
}

##' @importFrom stats update
##' @export
## FIXME: updating map this way will very likely cause errors because constraints are adjusted
update.tramTMB <- function(object, ...) {
  argn <- intersect(union(names(formals(tramTMB)), names(formals(TMB::MakeADFun))),
                    ls(object$env))
  argn <- setdiff(argn, c("random", "DLL"))
  args <- as.list(object$env)[argn]
  newargs <- list(...)
  args[names(newargs)] <- newargs
  do.call("tramTMB", args)
}

## Generic for copying objects that are (partly) modified in place
## @param object An object.
## @param ... Optional parameters.
## @export
duplicate <- function(object, ...) {
  UseMethod("duplicate")
}

## Create a duplicate of the tramTMB object
## @param object A \code{tramTMB} object.
## @param ... Optional parameters (not used).
## @importFrom utils lsf.str
##' @export
duplicate.tramTMB <- function(object, ...) {
    unserialize(serialize(object,NULL))
}

##' Post-estimation calculations in a tramTMB model
##' @param object A \code{tramTMB} object
##' @param parameters A named list of parameter values
##' @param scale The scale on which the post-estimation calculations are done
##' @param newdata A named list with elements Y, X and Z (not all necessary)
##' @param cov Logical; If \code{TRUE}, calculate the full covariance matrix
##'   of the calculated values
##' @param as.lm Logical; reparameterize as a LMM
##' @param ... Optional arguments (ignored).
##' @importFrom stats predict
##' @export
predict.tramTMB <- function(object, newdata,
                            parameters = .get_par(object, full = TRUE),
                            scale = c("lp", "trafo"), cov = FALSE,
                            as.lm = FALSE, ...) {
  scale <- as.numeric(match(match.arg(scale), eval(formals(predict.tramTMB)$scale)))
  if (scale > 1 && as.lm)
    warning("Reparametrization is only avalable for 'lp'. as.lm is set to FALSE.")
  data <- object$env$data
  names(newdata) <- paste0(names(newdata), "pe")
  data[names(newdata)] <- newdata
  data$postest_scale <- scale
  data$as_lm <- as.numeric(as.lm)
  newobj <- update(object, data = data, parameters = parameters)
  ## -- FIXME: this part to a separate function to standardize sdr calls later
  sdr <- TMB::sdreport(newobj, getReportCovariance = cov) ## try w/ default setup
  pr <- names(sdr$value) == "pred"
  if (any(is.nan(sdr$sd[pr]))) { ## NOTE: problems with inverting the Hessian. Try a bit harder
    prf <- newobj$env$last.par
    if (!is.null(newobj$env$random)) prf <- prf[-newobj$env$random]
    he <- numDeriv::jacobian(func = newobj$gr, x = prf)
    he <- .robustInv(he, ret_Hess = TRUE)
    sdr <- TMB::sdreport(newobj, par.fixed = prf, hessian.fixed = he)
  }
  ## --
  out <- list(pred = sdr$value[pr], se = sdr$sd[pr])
  if (cov)
    out$cov <- sdr$cov[pr, pr]
  return(out)
}

Try the tramME package in your browser

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

tramME documentation built on July 9, 2023, 7:10 p.m.