R/piping-ini.R

Defines functions .iniHandleDiag .iniHandleRmCov .iniDfRmDiag zeroRe .isQuotedLineRhsModifiesEstimates ini.default ini.rxUi .iniGetAppendArg .iniSimplifyAssignArrow .iniSimplifyFixUnfix .iniHandleLine .iniHandleDropType .iniHandleSwitchType .iniHandleRecalc .iniHandleAppend .iniHandleBackTransform .iniHandleLabel .isLotriAssignment .iniHandleLotriMatrix .iniAddCovarianceBetweenTwoEtaValues .iniHandleFixOrUnfixEqual .iniModifyThetaOrSingleEtaDf .iniModifyFixedForThetaOrEtablock .msgFix

Documented in ini.default .iniGetAppendArg .iniHandleAppend .iniHandleLine ini.rxUi zeroRe

#' Message about fixing or unfixing a parameter
#'
#' @param ini this is the iniDf data frame
#' @param w this indicates the row number of the item that is fixed or
#'   unfixed
#' @param fixedValue this is a boolean
#' @noRd
#' @author Matthew L. Fidler
.msgFix<- function(ini, w, fixedValue) {
  lapply(w, function(.w) {
    if (ini$fix[.w] != fixedValue) {
      if (fixedValue) {
        .minfo(paste0("fix {.code ", ini$name[.w], "} to {.code ", ini$est[.w], "}"))
      } else {
        .minfo(paste0("unfix {.code ", ini$name[.w], "} keeping initial estimate {.code ", ini$est[.w], "}"))
      }
    }
  })
}

#' This modifies the iniDf to fix (or unfix) parameters and related
#' values
#'
#' Note that the block of etas will be fixed/unfixed when a single
#' value is fixed/unfixed
#'
#' @param ini iniDf data.frame
#' @param w which item will be fixed
#' @param fixedValue should this be fixed `TRUE` or unfixed `FALSE`
#' @return nothing, called for side effects
#' @noRd
#' @author Matthew L. Fidler
.iniModifyFixedForThetaOrEtablock <- function(ini, w, fixedValue) {
  if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
    .msgFix(ini, w, fixedValue)
  }
  ini$fix[w] <- fixedValue
  .neta <- ini$neta1[w]
  if (!is.na(.neta)) {
    .etas <- .neta
    .fixedEtas <- NULL
    while (length(.etas) > 0) {
      .neta <- .etas[1]
      w <- which(ini$neta1 == .neta | ini$neta2 == .neta)
      if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
        .msgFix(ini, w, fixedValue)
      }
      ini$fix[w] <- fixedValue
      .etas <- unique(c(.etas, ini$neta1[w], ini$neta2[w]))
      .fixedEtas <- c(.neta, .fixedEtas)
      .etas <- .etas[!(.etas %in% .fixedEtas)]
    }
  }
  ini
}

#' Modify the population estimate in the internal `iniDf` data.frame
#'
#' @param ini This is the data frame for modifying
#' @param lhs This is the left hand expression as a character
#' @param rhs This is the right handed expression
#' @param doFix Fix the estimation variable
#' @param doUnfix Unfix the estimation variable
#' @param maxLen The maximum length is either 3 or 1
#' @return Modified ini variable
#' @author Matthew L. Fidler
#' @noRd
.iniModifyThetaOrSingleEtaDf <- function(ini, lhs, rhs, doFix, doUnfix, maxLen) {
  .w <- which(ini$name == lhs)
  if (length(.w) != 1) {
    stop("cannot find parameter '", lhs, "'", call.=FALSE)
  }
  .curFix <- ini$fix[.w]
  if (doFix) {
    if (.curFix) {
      warning("trying to fix '", lhs, "', but already fixed",
              call.=FALSE)
    } else {
      ini <- .iniModifyFixedForThetaOrEtablock(ini, .w, TRUE)
    }
  } else if (doUnfix) {
    if (.curFix) {
      ini <- .iniModifyFixedForThetaOrEtablock(ini, .w, FALSE)
    } else {
      warning("trying to unfix '", lhs, "', but already unfixed",
              call.=FALSE)
    }
  }

  if (is.null(rhs)) {
  } else if (length(rhs) == 1)  {
    ini$est[.w] <- rhs
    if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
      .minfo(paste0("change initial estimate of {.code ", ini$name[.w], "} to {.code ", ini$est[.w], "}"))
    }
    .lower <- ini$lower[.w]
    .upper <- ini$upper[.w]
    if (.lower >= rhs) {
      ini$lower[.w] <- -Inf
      if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
        .minfo(paste0("lower bound of  {.code ", ini$name[.w], "} reset to {.code -Inf}"))
      }
    }
    if (.upper <= rhs) {
      ini$upper[.w] <- Inf
      if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
        .minfo(paste0("upper bound of  {.code ", ini$name[.w], "} reset to {.code Inf}"))
      }
    }
  } else {
    if (maxLen == 1) {
      stop("piping for '", lhs, "' failed, the estimate should only be 1 value",
           call.=FALSE)
    } else if (length(rhs) == 2) {
      ini$lower[.w] <- rhs[1]
      ini$est[.w] <- rhs[2]
      if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
        .minfo(paste0("change initial estimate (", ini$est[.w], ") and lower bound (", ini$lower[.w], ") of {.code ", ini$name[.w], "}"))
      }
      # now check/change upper if needed
      .upper <- ini$upper[.w]
      if (.upper <= rhs[1] || .upper <= rhs[2]) {
        ini$upper[.w] <- Inf
        if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
          .minfo(paste0("upper bound for initial estimate (", ini$name[.w], ") reset to Inf"))
        }
      }
    } else if (length(rhs) == 3) {
      ini$lower[.w] <- rhs[1]
      ini$est[.w] <- rhs[2]
      ini$upper[.w] <- rhs[3]
      if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
        .minfo(paste0("change initial estimate (", ini$est[.w], ") and upper/lower bound (", ini$lower[.w], " to ", ini$upper[.w], ") of {.code ", ini$name[.w], "}"))
      }
    }
  }
  ini
}

#' Handle a fix or unfixed single expressionon for population or single eta
#'
#' This updates the `iniDf` within the rxode2 UI object
#'
#' @param expr Single assignment expression
#' @param rxui rxode2 UI object
#' @param envir Environment where the evaulation occurs
#' @param maxLen Maximum length of the argument
#' @return Nothing, called for side effects
#' @author Matthew L. Fidler
#' @noRd
.iniHandleFixOrUnfixEqual <- function(expr, rxui, envir=parent.frame(), maxLen=3L) {
  .tilde <- .isLotriAssignment(expr)
  .covs <- rxui$allCovs
  .lhs <- as.character(expr[[2]])
  .rhs <- expr[[3]]
  .doFix <- .doUnfix <- FALSE
  if (is.name(.rhs)) {
    if (identical(.rhs, quote(`fix`))) { # variations on fix are handled upstream
      .doFix <- TRUE
      .rhs <- NULL
    } else if (identical(.rhs, quote(`unfix`))) { # variations on unfix are handled upstream
      .doUnfix <- TRUE
      .rhs <- NULL
    }
  } else if (identical(.rhs[[1]], quote(`fix`))) {
    .doFix <- TRUE
    .rhs[[1]] <- quote(`c`)
  } else if (identical(.rhs[[1]], quote(`unfix`))) {
    .doUnfix <- TRUE
    .rhs[[1]] <- quote(`c`)
  }

  if (!is.null(.rhs)) {
    .rhs <- eval(.rhs, envir=envir)
    checkmate::assertNumeric(.rhs, any.missing=FALSE, min.len=1, max.len=3, .var.name=.lhs)
    if (!all(sort(.rhs) == .rhs)) {
      stop("the '", .lhs, "' piping lower, estimate, and/or upper estimate is in the wrong order",
           call.=FALSE)
    }
  }
  if (.lhs %in% .covs) {
    .addVariableToIniDf(.lhs, rxui, toEta=.tilde, value=.rhs, promote=TRUE)
    # assign is called again to handle the fixing of the variable
  }
  assign("iniDf", .iniModifyThetaOrSingleEtaDf(rxui$ini, .lhs, .rhs, .doFix, .doUnfix, maxLen=maxLen),
           envir=rxui)
  invisible()
}

#'  Add a covariance term between two eta values
#'
#' @param ini Data frame of initial estimates
#' @param neta1 Name of the first eta term
#' @param neta2 Name of the second eta term
#' @param est Estimate of the covariance
#' @param doFix Should this term be fixed
#' @param rxui is the rxui value
#' @return A modified (unsorted) data frame with the new covariance term appended
#' @author Matthew L. Fidler
#' @noRd
.iniAddCovarianceBetweenTwoEtaValues <- function(ini, neta1, neta2, est, doFix, rxui) {
  .covs <- rxui$allCovs
  if (neta1 %in% .covs) {
    .addVariableToIniDf(neta1, rxui, toEta=TRUE, value=NA, promote=TRUE)
    ini <- rxui$iniDf
    .covs <- rxui$allCovs
  }
  if (neta2 %in% .covs) {
    .addVariableToIniDf(neta2, rxui, toEta=TRUE, value=NA, promote=TRUE)
    ini <- rxui$iniDf
    .covs <- rxui$allCovs
  }
  .w1 <- which(ini$name == neta1)
  .w2 <- which(ini$name == neta2)
  if (length(.w1) != 1) stop("cannot find parameter '", neta1, "'", call.=FALSE)
  if (length(.w2) != 1) stop("cannot find parameter '", neta2, "'", call.=FALSE)
  if (ini$neta1[.w1] < ini$neta1[.w2]) {
    .tmp <- .w1
    .w1 <- .w2
    .w2 <- .tmp

    .tmp <- neta1
    neta1 <- neta2
    neta2 <- .tmp
  }
  .fix <- FALSE
  if (doFix) .fix <- TRUE
  .ini2 <- data.frame(ntheta= NA_integer_, neta1=ini$neta1[.w1], neta2=ini$neta1[.w2],
                      name=paste0("(", neta2, ",", neta1, ")"), lower= -Inf, est=est, upper=Inf,
                      fix=.fix, label=NA_character_, backTransform=NA_character_, condition="id",
                      err=NA_character_)
  if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
    .minfo(paste0("add covariance between {.code ", ini$name[.w1], "} and {.code ", ini$name[.w2], "} with initial estimate {.code ", est, "}"))
  }
  rbind(ini,.ini2)
}

#' This function handles the lotri process and integrates into current UI
#'
#' This will update the matrix and integrate the initial estimates in the UI
#'
#' @param mat Lotri processed matrix from the piping ini function
#'
#' @param rxui rxode2 UI function
#'
#' @return Nothing, called for side effects
#'
#' @author Matthew L. Fidler
#'
#' @noRd
.iniHandleLotriMatrix <- function(mat, rxui) {
  .dn <- dimnames(mat)[[1]]
  .iniDf <- rxui$iniDf
  .drop <- FALSE
  .common <- rxui$iniDf$name[rxui$iniDf$name %in% .dn]
  if (all(is.na(rxui$iniDf$neta1))) {
    .maxEta <- 0
    .shift <- 0
  } else {
    .maxEta <- max(rxui$iniDf$neta1, na.rm=TRUE)
    .shift <- .maxEta - length(.common)
  }
  .ini2 <- NULL
  for (.i in seq_along(.dn)) {
    .n <- .dn[.i]
    .w <- which(.iniDf$name == .n)
    if (length(.w) == 1) {
      .oNum <- .iniDf$neta1[.w]
      .w2 <- which(.iniDf$neta1 == .oNum & .iniDf$neta2 != .oNum)
      .df1 <- .iniDf[.w, ]
      .df1$neta1 <- .i + .shift
      .df1$neta2 <- .i + .shift
      .ini2 <- c(.ini2, list(.df1))
      if (length(.w2) > 0) {
        .iniDf <- .iniDf[-.w2, ]
        .drop <- TRUE
      }
    }
  }
  if (isTRUE(getOption("rxode2.verbose.pipe", TRUE)) && .drop) {
    .minfo(paste0("some correlations may have been dropped for the variables: {.code ", paste(.dn, collapse="}, {.code "), "}"))
    .minfo("the piping should specify the needed covariances directly")
  }
  .dfTheta <- .iniDf[is.na(.iniDf$neta1), ]
  .dfEta <- .iniDf[!is.na(.iniDf$neta1), ]
  .dfEta <- .dfEta[!(.dfEta$name %in% .dn),, drop = FALSE]
  if (length(.dfEta$neta1) > 0) {
    .dfEta$neta1 <- factor(paste(.dfEta$neta1))
    .dfEta$neta2 <- factor(paste(.dfEta$neta2), levels=levels(.dfEta$neta1))
    .dfEta$neta1 <- as.integer(.dfEta$neta1)
    .dfEta$neta2 <- as.integer(.dfEta$neta2)
  }
  .iniDf <- do.call("rbind", c(list(.dfTheta), list(.dfEta), .ini2))
  assign("iniDf", .iniDf, envir=rxui)
  .covs <- rxui$allCovs
  .fixMatrix <- attr(mat, "lotriFix")
  .unfixMatrix <- attr(mat, "lotriUnfix")
  .n <- dimnames(mat)[[1]]
  .mat <- mat
  if (!inherits(.mat, "lotriFix"))
    class(.mat) <- c("lotriFix", class(.mat))
  .df <- as.data.frame(.mat)
  .lastFix <- FALSE
  for (i in seq_along(.df$neta1)) {
    if (!is.na(.df$neta1[i])) {
      .doFix <- FALSE
      .doUnfix <- FALSE
      if (!is.null(.fixMatrix) && .df$fix[i]) {
        .doFix <- TRUE
      }
      if (!is.null(.unfixMatrix) && !.df$fix[i]) {
        .doUnfix <- TRUE
      }
      if (.df$neta1[i] == .df$neta2[i]) {
        .var <- as.character(.df$name[i])
        if (.var %in% .covs) {
          .addVariableToIniDf(.var, rxui, toEta=TRUE, value=.df$est[i], promote=TRUE)
          .covs <- rxui$allCovs
        }
        assign("iniDf", .iniModifyThetaOrSingleEtaDf(rxui$iniDf, .var, .df$est[i], .doFix, .doUnfix, 1L),
               envir=rxui)
        .lastFix <- rxui$iniDf$fix[rxui$iniDf$name == .var]
      } else {
        .n1 <- paste0("(", .n[.df$neta1[i]], ",", .n[.df$neta2[i]], ")")
        assign("iniDf", .iniAddCovarianceBetweenTwoEtaValues(rxui$iniDf, .n[.df$neta1[i]], .n[.df$neta2[i]], .df$est[i],
                                                             .lastFix, rxui),
               envir=rxui)
        .covs <- rxui$allCovs
      }
    }
  }
}

# Determine if the input is an endpoint by being 3 long and the call part being
# a tilde
.isLotriAssignment <- function(expr) {
  .matchesLangTemplate(expr, str2lang(". ~ ."))
}

#' Modify the label in an iniDf
#'
#' @inheritParams .iniHandleLine
#' @return Nothing, called for side effects
#' @author Bill Denney & Matthew Fidler
#' @keywords internal
#' @noRd
.iniHandleLabel <- function(expr, rxui, envir) {
  .lhs <- as.character(expr[[2]])
  .newLabel <- expr[[3]][[2]]
  .ini <- rxui$ini
  .w <- which(.ini$name == .lhs)
  if (length(.w) != 1) {
    stop("cannot find parameter '", .lhs, "'", call.=FALSE)
  } else if (is.null(.newLabel)) {
    .newLabel <- NA_character_
  } else if (!is.character(.newLabel) || !(length(.newLabel) == 1)) {
    stop("the new label for '", .lhs, "' must be a character string",
         call.=FALSE)
  }
  .ini$label[.w] <- .newLabel
  assign("iniDf", .ini, envir=rxui)
  invisible()
}
#' This handles the backTransform() piping calls
#'
#' @param expr expression for backTransform() in `ini()` piping
#' @param rxui rxode2 ui function
#' @param envir evaluation environment
#' @return nothing, called for side effects
#' @noRd
#' @author Matthew L. Fidler
.iniHandleBackTransform <- function(expr, rxui, envir) {
  .lhs <- as.character(expr[[2]])
  .newExpr <- expr[[3]][[2]]
  .ini <- rxui$ini
  .w <- which(.ini$name == .lhs)
  .good <- TRUE
  if (length(.w) != 1) {
    stop("cannot find parameter '", .lhs, "'", call.=FALSE)
  } else if (is.null(.newExpr)) {
    .newExpr <- NA_character_
  } else if (checkmate::testCharacter(.newExpr, len=1, any.missing=FALSE,
                                      pattern="^[.]*[a-zA-Z]+[a-zA-Z0-9._]*$",
                                      min.chars = 1)) {
  } else {
    .newExpr <- deparse1(.newExpr)
    if (!checkmate::testCharacter(.newExpr, len=1, any.missing=FALSE,
                                 pattern="^[.]*[a-zA-Z]+[a-zA-Z0-9._]*$",
                                 min.chars = 1)) {
      .good <- FALSE
    }
  }
  if (!.good) {
    stop("backTransform specification malformed",
         call.=FALSE)
  }
  if (!is.na(.newExpr)) {
    if (!exists(.newExpr, envir=envir, mode="function")) {
      stop("tried use a backTransform(\"", .newExpr, "\") when the function does not exist",
           call.=FALSE)
    }
  }
  .ini$backTransform[.w] <- .newExpr
  assign("iniDf", .ini, envir=rxui)
  invisible()
}

#' Reorder rows in iniDf
#'
#' @inheritParams .iniHandleLine
#' @param append Reorder theta parameters.  \code{NULL} means no change to
#'   parameter order.  A parameter name (as a character string) means to put the
#'   new parameter after the named parameter.  A number less than or equal to
#'   zero means to put the parameter at the beginning of the list.  A number
#'   greater than the last parameter number means to put the parameter at the
#'   end of the list.
#' @return Nothing, called for side effects
#' @keywords internal
.iniHandleAppend <- function(expr, rxui, envir, append) {
  ini <- rxui$ini
  if (is.null(append)) {
    # Do nothing
    return()
  } else if (is.logical(append)) {
    checkmate::assertLogical(append, any.missing = FALSE, len = 1)
    if (isTRUE(append)) {
      appendClean <- Inf
    } else if (isFALSE(append)) {
      appendClean <- 0
    }
  } else if (is.numeric(append)) {
    checkmate::assertNumber(append, null.ok = FALSE, na.ok = FALSE)
    appendClean <- append
  } else if (is.character(append)) {
    checkmate::assertCharacter(append, any.missing = FALSE, len = 1, null.ok = FALSE)
    checkmate::assertChoice(append, choices = ini$name)
    appendClean <- which(ini$name == append)
  } else {
    stop("'append' must be NULL, logical, numeric, or character/expression of variable in model",
         call. = FALSE)
  }

  lhs <- as.character(expr[[2]])
  wLhs <- which(ini$name == lhs)
  if (length(wLhs) != 1) {
    stop("cannot find parameter '", lhs, "'", call.=FALSE)
  } else if (length(appendClean) != 1) {
    # This likely cannot be reached because all scenarios should be handled
    # above in the input checking.  The line remains in the code defensively.
    stop("Cannot find parameter '", append, "'", call.=FALSE) # nocov
  } else if (appendClean == wLhs) {
    warning("parameter '", lhs, "' set to be moved after itself, no change in order made",
            call. = FALSE)
    return()
  } else if (is.na(ini$ntheta[wLhs])) {
    stop("only theta parameters can be moved.  '", lhs, "' is not a theta parameter",
         call. = FALSE)
  }

  # Do the movement
  if (appendClean <= 0) {
    # put it at the top
    ret <- rbind(ini[wLhs, ], ini[-wLhs, ])
  } else if (appendClean >= nrow(ini)) {
    # put it at the bottom
    ret <- rbind(ini[-wLhs, ], ini[wLhs, ])
  } else {
    # put it in the middle
    rowsAbove <- setdiff(seq_len(appendClean), wLhs)
    rowsBelow <- setdiff(seq(appendClean + 1, nrow(ini)), wLhs)
    ret <- rbind(ini[rowsAbove, ], ini[wLhs, ], ini[rowsBelow, ])
  }
  # Ensure that ntheta stays in order
  ini$ntheta[!is.na(ini$ntheta)] <- seq_len(sum(!is.na(ini$ntheta)))
  assign("iniDf", ret, envir=rxui)
  invisible()
}

.iniHandleRecalc <- function(rxui) {
  .fun <- rxUiDecompress(rxui$fun())
  for (.i in ls(.fun, all.names=TRUE)) {
    if (.i != "meta") {
      assign(.i, get(.i, envir=.fun), envir=rxui)
    }
  }
  invisible()
}

#' Handle switching theta to eta and vice versa
#'
#' This is coded as model |> ini(~par)
#'
#' @param expr Expression, this would be the ~par expression
#' @param rxui rxui uncompressed environment
#' @param envir Environment for evaluation (if needed)
#' @return Nothing, called for side effects
#' @noRd
#' @author Matthew L. Fidler
.iniHandleSwitchType <- function(expr, rxui, envir=parent.frame()) {
  .var <- as.character(expr[[2]])
  .iniDf <- rxui$iniDf
  .w <- which(.iniDf$name == .var)
  if (length(.w) != 1L) stop("cannot switch parameter type for '", .var, "'", call.=FALSE)
  .theta <- .iniDf[!is.na(.iniDf$ntheta),, drop = FALSE]
  .eta <- .iniDf[is.na(.iniDf$ntheta),, drop = FALSE]
  if (is.na(.iniDf$ntheta[.w])) {
    # switch eta to theta
    .neta <- .iniDf$neta1[.w]
    .eta <- .eta[.eta$neta1 != .neta,, drop = FALSE]
    .eta <- .eta[.eta$neta2 != .neta,, drop = FALSE]
    .eta$neta1 <- .eta$neta1 - ifelse(.eta$neta1 < .neta, 0L, 1L)
    .eta$neta2 <- .eta$neta2 - ifelse(.eta$neta2 < .neta, 0L, 1L)
    .newTheta <- .iniDf[.w, ]
    .newTheta$neta1 <- NA_integer_
    .newTheta$neta2 <- NA_integer_
    if (length(.theta$ntheta) == 0L) {
      .newTheta$ntheta <- 1L
    } else {
      .newTheta$ntheta <- max(.theta$ntheta) + 1L
    }
    .minfo(paste0("convert '", .var, "' from between subject variability to population parameter"))
    .theta <- rbind(.theta, .newTheta)
  } else {
    # switch theta to eta
    if (!is.na(.iniDf$err[.w])) {
      stop("cannot switch error parameter '", .var,
           "' to a different type", call. = FALSE)
    }
    .ntheta <- .iniDf$ntheta[.w]
    .theta <- .theta[.theta$ntheta != .ntheta,, drop = FALSE]
    .theta$ntheta <- .theta$ntheta - ifelse(.theta$ntheta < .ntheta, 0L, 1L)
    .newEta <- .iniDf[.w, ]
    .newEta$ntheta <- NA_integer_
    if (length(.eta$neta1) == 0L) {
      .newEta$neta1 <- .newEta$neta2 <- 1L
    } else {
      .newEta$neta1 <- .newEta$neta2 <- max(.eta$neta1) + 1L
    }
    .minfo(paste0("convert '", .var, "' from population parameter to between subject variability"))
    if (.newEta$est == 0) {
      .minfo("old initial estimate is zero, changing to 1")
      .newEta$est <- 1
    } else if (.newEta$est < 0) {
      .minfo("old initial estimate was negative, changing to positive")
      .newEta$est <- -.newEta$est
    }
    .newEta$lower <- -Inf
    .newEta$upper <- Inf
    .newEta$condition <- "id"
    .eta <- rbind(.eta, .newEta)
  }
  .ini <- rbind(.theta, .eta)
  assign("iniDf", .ini, envir=rxui)
  .iniHandleRecalc(rxui)
  invisible()
}

#' Handle dropping parameter and treating as if it is a covariate
#'
#' This is coded as model |> ini(-par)
#'
#' @param expr Expression, this would be the ~par expression
#' @param rxui rxui uncompressed environment
#' @param envir Environment for evaluation (if needed)
#' @return Nothing, called for side effects
#' @noRd
#' @author Matthew L. Fidler
.iniHandleDropType <- function(expr, rxui, envir=parent.frame()) {
  .var <- as.character(expr[[2]])
  .iniDf <- rxui$iniDf
  .w <- which(.iniDf$name == .var)
  if (length(.w) != 1L) stop("no initial estimates for '", .var, "', cannot change to covariate", call.=FALSE)
  .theta <- .iniDf[!is.na(.iniDf$ntheta),, drop = FALSE]
  .eta <- .iniDf[is.na(.iniDf$ntheta),, drop = FALSE]
  if (is.na(.iniDf$ntheta[.w])) {
    .minfo(paste0("changing between subject variability parameter '", .var, "' to covariate parameter"))
    .neta <- .iniDf$neta1[.w]
    .eta <- .eta[.eta$neta1 != .neta,, drop = FALSE]
    .eta <- .eta[.eta$neta2 != .neta,, drop = FALSE]
    .eta$neta1 <- .eta$neta1 - ifelse(.eta$neta1 < .neta, 0L, 1L)
    .eta$neta2 <- .eta$neta2 - ifelse(.eta$neta2 < .neta, 0L, 1L)
  } else {
    if (!is.na(.iniDf$err[.w])) {
      stop("cannot switch error parameter '", .var,
           "' to a covariate", call. = FALSE)
    }
    .minfo(paste0("changing population parameter '", .var, "' to covariate parameter"))
    .ntheta <- .iniDf$ntheta[.w]
    .theta <- .theta[.theta$ntheta != .ntheta,, drop = FALSE]
    .theta$ntheta <- .theta$ntheta - ifelse(.theta$ntheta < .ntheta, 0L, 1L)
  }
  .ini <- rbind(.theta, .eta)
  assign("iniDf", .ini, envir=rxui)
  # This will change covariates, recalculate everything
  .iniHandleRecalc(rxui)
  invisible()
}

#' Update the iniDf of a model
#'
#' @param expr Expression for parsing
#' @param rxui User interface function
#' @param envir Environment for parsing
#' @inheritParams .iniHandleAppend
#' @return Nothing, called for side effects
#' @author Matthew L. Fidler
#' @keywords internal
#' @export
.iniHandleLine <- function(expr, rxui, envir=parent.frame(), append = NULL) {
  if (.matchesLangTemplate(expr, str2lang("~diag()"))) {
    .iniHandleDiag(expr=NULL, rxui=rxui)
    return(invisible())
  } else if (length(expr) == 2L &&
               identical(expr[[1]], quote(`~`)) &&
               is.call(expr[[2]]) && length(expr[[2]]) >= 2L &&
               identical(expr[[2]][[1]], quote(`diag`))) {
    # .matchesLangTemplate(expr, str2lang("~diag(.)")) doesn't work
    .iniHandleDiag(expr=expr, rxui=rxui)
    return(invisible())
  }
  # Convert all variations on fix, fixed, FIX, FIXED; unfix, unfixed, UNFIX,
  # UNFIXED to fix and unfix to simplify all downstream operations
  expr <- .iniSimplifyFixUnfix(expr)
  # Convert assignment equal ("=") to left arrows ("<-") to simplify all
  # downstream operations
  expr <- .iniSimplifyAssignArrow(expr)

  if (.matchesLangTemplate(expr, str2lang(".name <- NULL")) ||
        .matchesLangTemplate(expr, str2lang(".name ~ NULL")) ||
        .matchesLangTemplate(expr, str2lang("cov(.name, .name) <- NULL")) ||
        .matchesLangTemplate(expr, str2lang("cor(.name, .name) <- NULL")) ||
        .matchesLangTemplate(expr, str2lang("cov(.name, .name) ~ NULL")) ||
        .matchesLangTemplate(expr, str2lang("cor(.name, .name) ~ NULL"))) {
    expr <- as.call(list(quote(`-`), expr[[2]]))
  }

  # now handle dropping covariances
  if (.matchesLangTemplate(expr, str2lang("-cov(.name, .name)")) ||
        .matchesLangTemplate(expr, str2lang("-cor(.name, .name)"))) {
    .iniHandleRmCov(expr=expr, rxui=rxui)
    return(invisible())
  }

  # Convert fix(name) or unfix(name) to name <- fix or name <- unfix
  if (.matchesLangTemplate(expr, str2lang("fix(.name)"))) {
    expr <- as.call(list(quote(`<-`), expr[[2]], quote(`fix`)))
  } else if (.matchesLangTemplate(expr, str2lang("unfix(.name)"))) {
    expr <- as.call(list(quote(`<-`), expr[[2]], quote(`unfix`)))
  }
  if (.matchesLangTemplate(expr, str2lang(".name <- label(.)"))) {
    .iniHandleLabel(expr=expr, rxui=rxui, envir=envir)
  } else if (.matchesLangTemplate(expr, str2lang(".name <- backTransform(.)"))) {
    .iniHandleBackTransform(expr=expr, rxui=rxui, envir=envir)
  } else if (.isAssignment(expr) && is.character(expr[[3]])) {
    stop(
      sprintf(
        "to assign a new label, use '%s <- label(\"%s\")'",
        as.character(expr[[2]]), expr[[3]]
      ), call.=FALSE
    )
  } else if (.isAssignment(expr)) {
    .iniHandleFixOrUnfixEqual(expr=expr, rxui=rxui, envir=envir)
  } else if (.isLotriAssignment(expr)) {
    .rhs <- expr[[2]]
    if (length(.rhs) > 1) {
      if (identical(.rhs[[1]], quote(`+`))) {
        .iniHandleLotriMatrix(eval(as.call(list(quote(`lotri`), as.call(list(quote(`{`), expr)))),
                                   envir=envir),
                              rxui)
        return(invisible())
      }
    }
    expr[[3]] <- eval(as.call(list(quote(`lotri`), as.call(list(quote(`{`), expr)))),
                      envir=envir)[1, 1]
    .iniHandleFixOrUnfixEqual(expr=expr, rxui=rxui, envir=envir, maxLen=1L)
  } else if (.isTildeExpr(expr)) {
    .iniHandleSwitchType(expr=expr, rxui=rxui, envir=envir)
  } else if (.isIniDropExpression(expr)) {
    .iniHandleDropType(expr=expr, rxui=rxui, envir=envir)
  } else {
    # Can this error be improved to clarify what is the expression causing the
    # issue?  It needs a single character string representation of something
    # that is not a character string.
    stop("invalid expr for ini() modification", call.=FALSE)
  }

  # (Maybe) update parameter order; this must be at the end so that the
  # parameter exists in case it is promoted from a covariate
  .iniHandleAppend(expr = expr, rxui = rxui, envir = envir, append = append)

  # now take out ETAs that no longer exist
  .iniDf <- get("iniDf", envir=rxui)
  .w <- which(is.na(.iniDf$neta1) & !is.na(.iniDf$neta2))
  .reassign <- FALSE
  if (length(.w) > 0) {
    .iniDf <- .iniDf[-.w, ]
    .reassign <- TRUE
  }
  .iniDf <- get("iniDf", envir=rxui)
  .w <- which(!is.na(.iniDf$neta1) & is.na(.iniDf$neta2))
  if (length(.w) > 0) {
    .iniDf <- .iniDf[-.w, ]
    .reassign <- TRUE
  }
  if (.reassign) {
    assign("iniDf", .iniDf, envir=rxui)
  }
}

# TODO: while nlmixr2est is changed
#' @rdname dot-iniHandleLine
#' @export
.iniHandleFixOrUnfix <- .iniHandleLine

#' Simplify variants of fix and unfix to just those two
#'
#' @param expr An R call or similar object
#' @return \code{expr} where all variants of fix (fixed, FIX, FIXED) and unfix
#'   (unfixed, UNFIX, and UNFIXED) are converted to fix and unfix
#' @noRd
.iniSimplifyFixUnfix <- function(expr) {
  if (identical(expr, as.name("fixed")) ||
      identical(expr, as.name("FIX")) ||
      identical(expr, as.name("FIXED"))) {
    expr <- as.name("fix")
  } else if (identical(expr, as.name("unfixed")) ||
             identical(expr, as.name("UNFIX")) ||
             identical(expr, as.name("UNFIXED"))) {
    expr <- as.name("unfix")
  } else if (is.call(expr)) {
    for (idx in seq_along(expr)) {
      # Do not perform a NULL assignment so that NULL comes out of the result
      if (!is.null(expr[[idx]])) {
        expr[[idx]] <- .iniSimplifyFixUnfix(expr[[idx]])
      }
    }
  } else {
    # do nothing
  }
  expr
}

#' Simplify all assignments to be left arrows (and not equal signs)
#'
#' @param expr An R call or similar object
#' @return \code{expr} where all variants assignment equal signs are converted
#'   to \code{<-}
#' @noRd
.iniSimplifyAssignArrow <- function(expr) {
  if (is.call(expr) && length(expr) == 3) {
    if (expr[[1]] == as.name("=")) {
      expr[[1]] <- as.name("<-")
    }
  } else {
    # do nothing
  }
  expr
}
#' This gets the append arg for the ini({}) piping
#'
#' @param f this is the `try(force(append))` argument,
#' @param s this is the `as.character(substitute(append))` argument
#' @return corrected ini piping argument
#'
#' This is exported for creating new ini methods that have the same
#' requirements for piping
#'
#' @export
#' @author Matthew L. Fidler
#' @keywords internal
.iniGetAppendArg <- function(f, s) {
  if (inherits(f, "try-error") &&
        checkmate::testCharacter(s, len=1, any.missing=FALSE,
                                 pattern="^[.]*[a-zA-Z]+[a-zA-Z0-9._]*$",
                                 min.chars = 1)) {
    return(s)
  }
  if (is.null(f)) {
    return(NULL)
  } else if (checkmate::testCharacter(f, len=1, any.missing=FALSE,
                                      pattern="^[.]*[a-zA-Z]+[a-zA-Z0-9._]*$",
                                      min.chars = 1)) {
    return(f)
  } else if (is.infinite(f)) {
    return(f)
  } else if (checkmate::testIntegerish(f, len=1, any.missing=FALSE)) {
    if (f < 0) {
      stop("'append' cannot be a negative integer", call.=FALSE)
    }
    return(f)
  } else if (checkmate::testLogical(f, len=1)) {
    # NA for model piping prepends
    if (is.na(f)) return(FALSE)
    return(f)
  }
  stop("'append' must be NULL, logical, numeric, or character/expression of variable in model",
       call.=FALSE)
}

#' @export
#' @rdname ini
ini.rxUi <- function(x, ..., envir=parent.frame(), append = NULL) {
  .s  <- as.character(substitute(append))
  .f <- try(force(append), silent=TRUE)
  append <- .iniGetAppendArg(.f, .s)
  .ret <- rxUiDecompress(.copyUi(x)) # copy so (as expected) old UI isn't affected by the call
  .iniDf <- .ret$iniDf
  .iniLines <- .quoteCallInfoLines(match.call(expand.dots = TRUE)[-(1:2)], envir=envir, iniDf= .iniDf)
  if (length(.iniLines) == 0L) return(.ret$iniFun)
  lapply(.iniLines, function(line) {
    .iniHandleLine(expr = line, rxui = .ret, envir = envir, append=append)
  })
  if (inherits(x, "rxUi")) {
    .x <- rxUiDecompress(x)
    .ret <- .newModelAdjust(.ret, .x)
  }
  .ret <- rxUiCompress(.ret)
  if (inherits(x, "rxUi")) {
    .cls <- setdiff(class(x), class(.ret))
    if (length(.cls) > 0) {
      class(.ret) <- c(.cls, class(.ret))
    }
  }
  .ret
}

#' @rdname ini
#' @export
ini.default <- function(x, ..., envir=parent.frame(), append = NULL) {
  .s  <- as.character(substitute(append))
  .f <- try(force(append), silent=TRUE)
  append <- .iniGetAppendArg(.f, .s)
  .ret <- try(as.rxUi(x), silent = TRUE)
  if (inherits(.ret, "try-error")) {
    stop("cannot figure out what to do with the ini({}) function", call.=FALSE)
  }
  .ret <- rxUiDecompress(.ret)
  .iniDf <- .ret$iniDf
  .iniLines <- .quoteCallInfoLines(match.call(expand.dots = TRUE)[-(1:2)], envir=envir, iniDf = .iniDf)
  if (length(.iniLines) == 0L) return(.ret$iniFun)
  lapply(.iniLines, function(line) {
    .iniHandleLine(expr = line, rxui = .ret, envir=envir, append = append)
  })
  rxUiCompress(.ret)
}

#' This tells if the line is modifying an estimate instead of a line of the model
#'
#' @param line Quoted line
#' @param rxui rxode2 UI object
#' @return Boolean indicating if the line defines an `ini` change.
#' @author Matthew L. Fidler
#' @noRd
.isQuotedLineRhsModifiesEstimates <- function(line, rxui) {
  if (length(line) != 3) return(FALSE)
  .rhs <- line[[2]]
  if (length(.rhs) > 1) {
    if (identical(.rhs[[1]], quote(`+`))) {
      return(TRUE)
    }
  }
  .c <- as.character(.rhs)
  if (any(rxui$iniDf$name == .c)) return(TRUE)
  return(FALSE)
}

#' Set random effects and residual error to zero
#'
#' @param object The model to modify
#' @param which The types of parameters to set to zero
#' @param fix Should the parameters be fixed to the zero value?
#' @return The `object` with some parameters set to zero
#' @family Initial conditions
#' @author Bill Denney
#' @examples
#' one.compartment <- function() {
#'   ini({
#'     tka <- log(1.57); label("Ka")
#'     tcl <- log(2.72); label("Cl")
#'     tv <- log(31.5); label("V")
#'     eta.ka ~ 0.6
#'     eta.cl ~ 0.3
#'     eta.v ~ 0.1
#'     add.sd <- 0.7
#'   })
#'   model({
#'     ka <- exp(tka + eta.ka)
#'     cl <- exp(tcl + eta.cl)
#'     v <- exp(tv + eta.v)
#'     d/dt(depot) = -ka * depot
#'     d/dt(center) = ka * depot - cl / v * center
#'     cp = center / v
#'     cp ~ add(add.sd)
#'   })
#' }
#' zeroRe(one.compartment)
#' @export
zeroRe <- function(object, which = c("omega", "sigma"), fix = TRUE) {
  object <- assertRxUi(object)
  which <- match.arg(which, several.ok = TRUE)
  .ret <- rxUiDecompress(.copyUi(object)) # copy so (as expected) old UI isn't affected by the call
  iniDf <- .ret$iniDf
  # In the code below there is no test for bounds since the bounds are typically (0, Inf).
  if ("omega" %in% which) {
    maskOmega <- !is.na(iniDf$neta1)
    if (sum(maskOmega) == 0) {
      cli::cli_warn("No omega parameters in the model")
    } else {
      iniDf$est[maskOmega] <- 0
      if (fix) {
        iniDf$fix[maskOmega] <- TRUE
      }
    }
  }
  if ("sigma" %in% which) {
    maskSigma <- !is.na(iniDf$err)
    if (sum(maskSigma) == 0) {
      cli::cli_warn("No sigma parameters in the model")
    } else {
      iniDf$est[maskSigma] <- 0
      maskLowerBound <- maskSigma & iniDf$lower > 0
      if (any(maskLowerBound)) {
        iniDf$lower[maskLowerBound] <- 0
      }
      if (fix) {
        iniDf$fix[maskSigma] <- TRUE
      }

    }
  }
  ini(.ret) <- iniDf
  .ret
}

#' This removes the off-diagonal BSV from a rxode2 iniDf
#'
#' @param ui rxode2 ui model
#'
#' @param diag character vector of diagonal values to remove
#'
#' @return iniDf with modified diagonal
#' @noRd
#' @author Matthew L. Fidler
.iniDfRmDiag <- function(iniDf, diag=character(0)) {
  .iniDf <- iniDf
  .theta <- .iniDf[!is.na(.iniDf$ntheta),,drop=FALSE]
  .eta <- .iniDf[is.na(.iniDf$ntheta),,drop=FALSE]
  if (length(diag) == 0) {
    .w <- which(.eta$neta1 == .eta$neta2)
    .rmNames <- .eta[-.w, "name"]
    .eta <- .eta[.w,, drop=FALSE]
    .iniDf <- rbind(.theta, .eta)
  } else {
    .rmNames <- character(0)
    for (.e in diag) {
      .w <- which(.eta$name == .e)
      if (length(.w) == 1L) {
        .n <- .eta$neta1[.w]
        .w <- vapply(seq_along(.eta$neta1),
                     function(i) {
                       if (.eta$neta1[i] == .eta$neta2[i]) {
                         TRUE
                       } else if (.eta$neta1[i] == .n && .eta$neta2[i] != .n) {
                         FALSE
                       } else if (.eta$neta2[i] == .n && .eta$neta1[i] != .n) {
                         FALSE
                       } else {
                         TRUE
                       }
                     }, logical(1), USE.NAMES = TRUE)
        .rmNames <- c(.rmNames, .eta$name[!.w])
        .eta <- .eta[.w,,drop=FALSE]
      } else {
        stop("cannot find parameter '", .e, "' for covariance removal", call.=FALSE)
      }
    }
    .mat <- lotri::as.lotri(.eta)
    .mat <- lotri::rcm(.mat)
    class(.mat) <- c("lotriFix", class(.mat))
    .eta <- as.data.frame(.mat)
    .eta$err <- NA_character_
    .iniDf <- rbind(.theta, .eta)
  }
  if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
    for (.v in .rmNames) {
      .minfo(paste0("remove covariance {.code ", .v, "}"))
    }
  }
  .iniDf
}

.iniHandleRmCov <- function(expr, rxui) {
  .iniDf <- rxui$iniDf
  .theta <- .iniDf[!is.na(.iniDf$ntheta),, drop = FALSE]
  .eta <- .iniDf[is.na(.iniDf$ntheta),, drop = FALSE]
  .mat <- lotri::as.lotri(.eta)
  .n1 <- as.character(expr[[2]][[2]])
  .v1 <- which(.n1==dimnames(.mat)[[1]])
  if (length(.v1) != 1) {
    stop("cannot find parameter '", .n1, "' for covariance removal", call.=FALSE)
  }
  .n2 <- as.character(expr[[2]][[3]])
  .v2 <- which(.n2==dimnames(.mat)[[1]])
  if (length(.v2) != 1) {
    stop("cannot find parameter '", .n2, "' for covariance removal", call.=FALSE)
  }
  if (isTRUE(getOption("rxode2.verbose.pipe", TRUE))) {
    .minfo(paste0("remove covariance {.code (", .n1, ", ", .n2, ")}"))
  }

  .mat[.v1, .v2] <- .mat[.v2, .v1] <- 0
  .mat <- lotri::rcm(.mat)
  class(.mat) <- c("lotriFix", class(.mat))
  .eta <- as.data.frame(.mat)
  .eta$err <- NA_character_
  .iniDf <- rbind(.theta, .eta)
  assign("iniDf", .iniDf, envir=rxui)
}

.iniHandleDiag <- function(expr, rxui){
  if (is.null(expr)) {
    assign("iniDf", .iniDfRmDiag(rxui$iniDf), envir=rxui)
  } else {
    # now get the variables in the diag expression
    .env <- new.env(parent=emptyenv())
    .env$names <- character(0)
    .f <- function(x) {
      if (is.name(x)) {
        .env$names <- c(.env$names, as.character(x))
      } else if (is.call(x)) {
        lapply(lapply(seq_along(x)[-1], function(i) {x[[i]]}), .f)
      }
    }
    expr <- expr[[2]]
    lapply(seq_along(expr)[-1],
           function(i) {
              .f(expr[[i]])
           })
    assign("iniDf", .iniDfRmDiag(rxui$iniDf, .env$names), envir=rxui)
  }
}
nlmixr2/rxode2 documentation built on Jan. 11, 2025, 8:48 a.m.