Nothing
## Collect additive terms from an expression tree, tracking sign.
## Returns list of {sign=+-1, expr}.
.collectAddTerms <- function(expr, sign = 1L) {
if (!is.call(expr)) return(list(list(sign = sign, expr = expr)))
.fn <- expr[[1]]
if (identical(.fn, quote(`(`)) && length(expr) == 2L) {
return(.collectAddTerms(expr[[2]], sign))
}
if (identical(.fn, quote(`+`)) && length(expr) == 3L) {
return(c(.collectAddTerms(expr[[2]], sign), .collectAddTerms(expr[[3]], sign)))
}
if (identical(.fn, quote(`-`)) && length(expr) == 3L) {
return(c(.collectAddTerms(expr[[2]], sign), .collectAddTerms(expr[[3]], -sign)))
}
if (identical(.fn, quote(`-`)) && length(expr) == 2L) {
return(.collectAddTerms(expr[[2]], -sign))
}
list(list(sign = sign, expr = expr))
}
## Return all state variable names referenced anywhere in an expression.
.statesInExpr <- function(expr, states) {
if (is.name(expr)) {
.nm <- as.character(expr)
return(if (.nm %in% states) .nm else character(0))
}
if (!is.call(expr)) return(character(0))
if (identical(expr[[1]], quote(`(`)) && length(expr) == 2L) {
return(.statesInExpr(expr[[2]], states))
}
unique(unlist(lapply(as.list(expr)[-1], .statesInExpr, states = states)))
}
## Return all free symbol names referenced in an expression, excluding the
## heads of calls (i.e. function names). Used to gather the PK parameter
## names appearing in ODE rate coefficients so they can be passed explicitly
## to linCmt().
.freeSymbolsInExpr <- function(expr) {
if (is.name(expr)) return(as.character(expr))
if (!is.call(expr)) return(character(0))
unique(unlist(lapply(as.list(expr)[-1L], .freeSymbolsInExpr)))
}
## Extract the coefficient from `coef * stateNm` or `stateNm * coef`,
## where coef does not reference any state variable.
## Returns the coefficient expression, or NULL if the term is not that form.
.extractMultCoef <- function(expr, stateNm, states) {
if (is.name(expr)) {
if (as.character(expr) == stateNm) return(quote(1))
return(NULL)
}
if (!is.call(expr)) return(NULL)
.fn <- expr[[1]]
if (identical(.fn, quote(`(`)) && length(expr) == 2L) {
return(.extractMultCoef(expr[[2]], stateNm, states))
}
if (!identical(.fn, quote(`*`))) return(NULL)
.lhs <- expr[[2]]; .rhs <- expr[[3]]
.lu <- length(.statesInExpr(.lhs, states)) > 0L
.ru <- length(.statesInExpr(.rhs, states)) > 0L
if (.lu && !.ru && is.name(.lhs) && as.character(.lhs) == stateNm) return(.rhs)
if (!.lu && .ru && is.name(.rhs) && as.character(.rhs) == stateNm) return(.lhs)
NULL
}
## Parse one additive term: returns {sign, coef, state} where state=NA means
## a non-state (input/forcing) term. Returns NULL if the term is nonlinear
## (references multiple states or uses a state nonlinearly).
.parseOneLinTerm <- function(sign, termExpr, states) {
.refs <- .statesInExpr(termExpr, states)
if (length(.refs) == 0L) {
return(list(sign = sign, coef = termExpr, state = NA_character_))
}
if (length(.refs) > 1L) return(NULL)
.state <- .refs[1L]
.coef <- .extractMultCoef(termExpr, .state, states)
if (is.null(.coef)) return(NULL)
list(sign = sign, coef = .coef, state = .state)
}
## Parse an ODE RHS into a flat list of {sign, coef, state} terms.
## Returns NULL if the RHS is not linear in all state variables.
.parseLinearRhs <- function(rhs, states) {
.raw <- .collectAddTerms(rhs)
.parsed <- lapply(.raw, function(.t) .parseOneLinTerm(.t$sign, .t$expr, states))
if (any(vapply(.parsed, is.null, logical(1)))) return(NULL)
.parsed
}
## Net signed coefficient (sum of sign*coef) of state `s`, as a single
## expression; NULL when `s` is absent. The parsed sign is folded into the
## coefficient (`-ka*depot` parses as a positive-sign term with coef `-ka`).
.odeToLinNetCoef <- function(terms, s) {
.rel <- Filter(function(.t) !is.na(.t$state) && .t$state == s, terms)
if (length(.rel) == 0L) return(NULL)
.signed <- lapply(.rel, function(.t) if (.t$sign < 0L) bquote(-(.(.t$coef))) else .t$coef)
Reduce(function(.a, .b) bquote(.(.a) + .(.b)), .signed)
}
## Verify mass balance for every non-central compartment: a genuine
## depot/peripheral loses exactly the flux central gains from it (the two net
## coefficients cancel). A metabolite has an extra independent-elimination
## term that breaks the cancellation, so it must NOT convert. Checked
## numerically at two distinct positive parameter assignments.
.odeToLinMassBalanced <- function(odes, central, others) {
.byCmt <- setNames(odes, vapply(odes, function(.o) .o$cmt, character(1)))
.centralOde <- .byCmt[[central]]
if (is.null(.centralOde)) return(FALSE)
for (.c in others) {
.ode <- .byCmt[[.c]]
if (is.null(.ode)) return(FALSE)
.selfNet <- .odeToLinNetCoef(.ode$terms, .c) # what C loses (a net outflow)
.centNet <- .odeToLinNetCoef(.centralOde$terms, .c) # what central gains from C
if (is.null(.selfNet) || is.null(.centNet)) return(FALSE)
.syms <- unique(c(.freeSymbolsInExpr(.selfNet), .freeSymbolsInExpr(.centNet)))
.balanced <- function(.offset) {
.vals <- as.list(setNames(seq_along(.syms) + .offset, .syms))
.v <- tryCatch(eval(bquote(.(.selfNet) + .(.centNet)), .vals, baseenv()),
error = function(e) NA_real_)
length(.v) == 1L && is.finite(.v) && abs(.v) < 1e-8
}
if (!.balanced(1.5) || !.balanced(3.7)) return(FALSE)
}
TRUE
}
## Detect the topology of a linear ODE system and classify each compartment.
## odes: list of {cmt, terms}; outputCmt: the central compartment (from the
## output line). Returns list(ncmt, oral0, central, depot, peripheral1,
## peripheral2) or NULL.
.odeToLinDetectTopology <- function(odes, outputCmt, cmtNames) {
.n <- length(odes)
if (.n == 0L || .n > 4L) return(NULL)
## Build inflow map: flowsIn[[cmt]] = list of {from, coef} for positive
## cross-compartment terms in cmt's ODE.
.flowsIn <- setNames(vector("list", .n), cmtNames)
.flowsOut <- setNames(vector("list", .n), cmtNames)
for (.ode in odes) {
.cmt <- .ode$cmt
for (.t in .ode$terms) {
if (is.na(.t$state) || .t$state == .cmt) next
if (.t$sign > 0L) {
.flowsIn[[.cmt]] <- c(.flowsIn[[.cmt]],
list(list(from = .t$state, coef = .t$coef)))
}
}
}
## Derive outflow map from inflow map.
for (.cmt in cmtNames) {
for (.f in .flowsIn[[.cmt]]) {
.flowsOut[[.f$from]] <- c(.flowsOut[[.f$from]], list(list(to = .cmt)))
}
}
.central <- outputCmt
if (!.central %in% cmtNames) return(NULL)
.depot <- NULL
.peripherals <- character(0)
for (.cmt in cmtNames[cmtNames != .central]) {
.nIn <- length(.flowsIn[[.cmt]])
.out <- .flowsOut[[.cmt]]
.nOut <- length(.out)
.allOutToCentral <- .nOut > 0L &&
all(vapply(.out, function(.f) .f$to == .central, logical(1)))
.allInFromCentral <- .nIn > 0L &&
all(vapply(.flowsIn[[.cmt]], function(.f) .f$from == .central, logical(1)))
if (.nIn == 0L && .nOut == 1L && .allOutToCentral) {
## Depot: no inflows, exactly one outflow to central
if (!is.null(.depot)) return(NULL)
.depot <- .cmt
} else if (.nIn > 0L && .allInFromCentral && .allOutToCentral) {
## Peripheral: inflow from central, outflow to central
.peripherals <- c(.peripherals, .cmt)
} else {
return(NULL)
}
}
.ncmt <- 1L + length(.peripherals)
if (.ncmt > 3L) return(NULL)
## Reject systems where a depot/peripheral has independent loss (e.g. a
## metabolite), which linCmt() cannot represent.
.others <- c(.peripherals, if (is.null(.depot)) character(0) else .depot)
if (!.odeToLinMassBalanced(odes, .central, .others)) return(NULL)
list(
ncmt = .ncmt,
oral0 = if (is.null(.depot)) 0L else 1L,
central = .central,
depot = .depot,
peripheral1 = if (length(.peripherals) >= 1L) .peripherals[1L] else NULL,
peripheral2 = if (length(.peripherals) >= 2L) .peripherals[2L] else NULL
)
}
## Find a line of the form var <- cmt / vExpr where cmt is a state variable.
## Returns {var, cmt, vExpr, lineIdx} or NULL.
.odeToLinFindOutput <- function(lstExpr, states) {
for (.i in seq_along(lstExpr)) {
.e <- lstExpr[[.i]]
if (!is.call(.e)) next
if (!identical(.e[[1]], quote(`<-`)) && !identical(.e[[1]], quote(`=`))) next
if (length(.e) < 3L || !is.name(.e[[2]])) next
.rhs <- .e[[3]]
if (!is.call(.rhs) || length(.rhs) != 3L) next
if (!identical(.rhs[[1]], quote(`/`))) next
.num <- .rhs[[2]]
if (!is.name(.num)) next
.cmtNm <- as.character(.num)
if (!.cmtNm %in% states) next
return(list(
var = as.character(.e[[2]]),
cmt = .cmtNm,
vExpr = .rhs[[3]],
lineIdx = .i
))
}
NULL
}
## Check if an expression is d/dt(name).
## In R's AST, `d/dt(x)` parses as call("/", d, call(dt, x)), NOT call("d/dt", x).
.isDtExpr <- function(expr) {
is.call(expr) && length(expr) == 3L &&
identical(expr[[1]], quote(`/`)) &&
is.name(expr[[2]]) && identical(expr[[2]], quote(d)) &&
is.call(expr[[3]]) && length(expr[[3]]) == 2L &&
is.name(expr[[3]][[1]]) && as.character(expr[[3]][[1]]) == "dt" &&
is.name(expr[[3]][[2]])
}
## Extract compartment name from d/dt(name) expression.
.getDtCmt <- function(expr) as.character(expr[[3]][[2]])
## Collect the names of state variables referenced as *values* in an
## expression. The compartment-position argument of an `f`/`rate`/`dur`/`alag`
## modifier or an adaptive-dosing call (`bolus`, `infuse`, ...) is NOT a value
## reference: those positions are rewritten to the standard linCmt compartment
## names by .odeToLinRenameExpr, so they never block conversion. Anything else
## that names a state -- e.g. a peripheral observable `periph` in
## `Cp <- periph / vp` -- is a value reference.
.odeToLinValueStateRefs <- function(expr, states) {
if (is.name(expr)) {
.nm <- as.character(expr)
return(if (.nm %in% states) .nm else character(0))
}
if (!is.call(expr)) return(character(0))
.fn <- if (is.name(expr[[1L]])) as.character(expr[[1L]]) else ""
## f/rate/dur/alag(<cmt>, ...): the first argument is a compartment position.
if (.fn %in% c("f", "rate", "dur", "alag")) {
.rest <- as.list(expr)[-1L]
if (length(.rest) >= 1L) .rest <- .rest[-1L]
return(unique(unlist(lapply(.rest, .odeToLinValueStateRefs, states = states))))
}
## Adaptive dosing calls: the compartment-position argument is rewritten too.
## Indices mirror .odeToLinRenameAdaptiveCall.
.cmtIdx <- switch(.fn,
bolus = 3L, replace = 3L, multiply = 3L, phantom = 3L,
infuse = 4L, infuseDur = 4L,
`evid_` = 5L,
NULL)
.idx <- seq_along(expr)[-1L]
if (!is.null(.cmtIdx)) .idx <- setdiff(.idx, .cmtIdx)
unique(unlist(lapply(.idx, function(.k) .odeToLinValueStateRefs(expr[[.k]], states))))
}
## TRUE when a compartment state is referenced as a value by some model line
## other than the ODE equations and the single central output line (e.g. a
## peripheral observable `Cp <- periph / vp`, or a state alias). Such a
## reference cannot be served by a bare central-only linCmt() output, so the
## caller takes the coupled-conversion path (rename the compartments to their
## canonical linCmt names) when it is safe, and otherwise keeps the explicit
## ODEs.
.odeToLinStateReferencedElsewhere <- function(lstExpr, cmtNames, odeIdx, outputIdx) {
for (.i in seq_along(lstExpr)) {
if (.i %in% odeIdx || .i == outputIdx) next
if (length(.odeToLinValueStateRefs(lstExpr[[.i]], cmtNames)) > 0L) {
return(TRUE)
}
}
FALSE
}
## TRUE when every residual/endpoint (`~`) line predicts the central output
## variable. A linCmt() model can only observe the central concentration as an
## estimated endpoint: mapping a peripheral observable to its own endpoint
## (`Cp ~ ... | peripheral1`) collides with linCmt()'s internal peripheral
## compartment ("required for linCmt() but defined in ODE too"). So a coupled
## system converts only when the peripheral references are *output-only*.
.odeToLinAllEndpointsCentral <- function(lstExpr, outputVar) {
for (.e in lstExpr) {
if (!is.call(.e) || !identical(.e[[1]], quote(`~`)) || length(.e) < 3L) next
.lhs <- .e[[2]]
if (!is.name(.lhs) || as.character(.lhs) != outputVar) return(FALSE)
}
TRUE
}
## Recursively rename every reference to an ODE compartment name to its
## canonical linCmt name throughout an expression (value references included,
## e.g. `Cp <- periph / vp` -> `Cp <- peripheral1 / vp`). Used only on the
## coupled-conversion path; compartment names live in a separate namespace from
## parameters, so a name match is unambiguous.
.odeToLinRenameValueRefs <- function(e, cmtMap) {
if (is.name(e)) {
.new <- cmtMap[as.character(e)]
if (!is.na(.new)) return(as.name(.new))
return(e)
}
if (!is.call(e)) return(e)
for (.i in seq_along(e)) e[[.i]] <- .odeToLinRenameValueRefs(e[[.i]], cmtMap)
e
}
## Map the central endpoint to the `central` compartment (`Cc ~ err` ->
## `Cc ~ err | central`). Anchoring the endpoint to the existing central
## compartment stops rxode2 injecting a fresh `cmt(<pred>)` observation slot,
## which would otherwise flip the model into explicit-compartment mode and make
## in-model peripheral references resolve to 0. Left unchanged if already
## conditioned with `|`.
.odeToLinAddCentralCond <- function(e) {
.rhs <- e[[3]]
if (is.call(.rhs) && identical(.rhs[[1]], quote(`|`))) return(e)
e[[3]] <- call("|", .rhs, as.name("central"))
e
}
## Names of model-defined variables whose value transitively depends on a
## compartment state -- e.g. the central concentration `Cc <- central / vc`,
## or anything built from it. A term in an ODE RHS that references one of
## these is state-dependent even when it names no state directly, so it must
## block linCmt() conversion (see `.odeToLinDetect`). Lines defining the ODEs
## themselves are excluded; only assignments to a plain name are tracked.
.odeToLinStateDerivedVars <- function(lstExpr, states, odeIdx) {
.tainted <- character(0)
repeat {
.added <- FALSE
for (.i in seq_along(lstExpr)) {
if (.i %in% odeIdx) next
.e <- lstExpr[[.i]]
if (!is.call(.e)) next
if (!identical(.e[[1]], quote(`<-`)) && !identical(.e[[1]], quote(`=`))) next
if (length(.e) < 3L || !is.name(.e[[2]])) next
.lhs <- as.character(.e[[2]])
if (.lhs %in% .tainted) next
if (any(all.vars(.e[[3]]) %in% c(states, .tainted))) {
.tainted <- c(.tainted, .lhs)
.added <- TRUE
}
}
if (!.added) break
}
unique(.tainted)
}
## Attempt to detect whether ui is a linear compartment ODE model.
## Returns a list with topology + output info, or NULL if not convertible.
.odeToLinDetect <- function(ui) {
.lstExpr <- ui$lstExpr
.states <- rxModelVars(ui)$state # nolint
if (length(.states) == 0L) return(NULL)
## Gather ODE lines and their compartment names.
.odeIdx <- integer(0)
.cmtNames <- character(0)
for (.i in seq_along(.lstExpr)) {
.e <- .lstExpr[[.i]]
if (!is.call(.e)) next
if (!identical(.e[[1]], quote(`<-`)) && !identical(.e[[1]], quote(`=`))) next
if (length(.e) < 3L || !is.call(.e[[2]])) next
if (!.isDtExpr(.e[[2]])) next
.odeIdx <- c(.odeIdx, .i)
.cmtNames <- c(.cmtNames, .getDtCmt(.e[[2]]))
}
if (length(.odeIdx) == 0L || length(.odeIdx) > 4L) return(NULL)
if (!all(.cmtNames %in% .states)) return(NULL)
## Parse each ODE RHS; bail if any is nonlinear in state variables.
.odes <- lapply(seq_along(.odeIdx), function(.j) {
.e <- .lstExpr[[.odeIdx[.j]]]
.terms <- .parseLinearRhs(.e[[3]], .states)
if (is.null(.terms)) return(NULL)
list(cmt = .cmtNames[.j], terms = .terms)
})
if (any(vapply(.odes, is.null, logical(1)))) return(NULL)
## Bail when an ODE RHS depends on a state indirectly through a state-derived
## value (e.g. Michaelis-Menten via `Cc <- central/vc`), which the direct
## linearity check misses; keep the explicit ODE states.
.stateDerived <- .odeToLinStateDerivedVars(.lstExpr, .states, .odeIdx)
if (length(.stateDerived) > 0L &&
any(vapply(.odes, function(.o) {
any(vapply(.o$terms,
function(.t) any(all.vars(.t$coef) %in% .stateDerived),
logical(1)))
}, logical(1)))) {
return(NULL)
}
## Find output line: var <- centralCmt / vExpr
.out <- .odeToLinFindOutput(.lstExpr, .states)
if (is.null(.out)) return(NULL)
## Classify topology.
.topo <- .odeToLinDetectTopology(.odes, .out$cmt, .cmtNames)
if (is.null(.topo)) return(NULL)
## Compartment states referenced as a value outside the ODEs and the central
## output line (e.g. `Cp <- periph/vp`): keep the model analytic by renaming
## the ODE compartments to their canonical linCmt names (the coupled path),
## valid only when every endpoint predicts the central output.
.coupled <- FALSE
if (.odeToLinStateReferencedElsewhere(.lstExpr, .cmtNames, .odeIdx, .out$lineIdx)) {
if (!.odeToLinAllEndpointsCentral(.lstExpr, .out$var)) return(NULL)
.coupled <- TRUE
}
## PK parameter names from the rate coefficients and volume expression,
## passed explicitly to linCmt() so it can infer the parameterization even
## for ini()-only parameters.
.params <- character(0)
for (.ode in .odes) {
for (.t in .ode$terms) {
if (!is.na(.t$state)) {
.params <- c(.params, .freeSymbolsInExpr(.t$coef))
}
}
}
.params <- c(.params, .freeSymbolsInExpr(.out$vExpr))
.params <- setdiff(unique(.params),
c(.states, .out$var, "t", "time", "pi"))
c(.topo, list(
outputVar = .out$var,
outputCmt = .out$cmt,
vExpr = .out$vExpr,
outputIdx = .out$lineIdx,
odeIdx = .odeIdx,
params = .params,
coupled = .coupled
))
}
## Build a new lstExpr with ODE lines removed and the output line replaced by
## `outputVar <- linCmt()`.
## Map ODE compartment names to the standard linCmt names ("depot", "central").
## linCmt() models always use these two names regardless of what the ODE called them.
.odeToLinCmtMap <- function(info) {
.map <- character(0)
if (isTRUE(info$oral0 == 1L) && !is.null(info$depot)) {
.map[info$depot] <- "depot"
}
if (!is.null(info$central)) {
.map[info$central] <- "central"
}
## On the coupled path the peripheral compartments are referenced by retained
## model lines (observables), so they too must map to their canonical linCmt
## names to resolve against the analytic solution's solved compartments.
if (isTRUE(info$coupled)) {
if (!is.null(info$peripheral1)) .map[info$peripheral1] <- "peripheral1"
if (!is.null(info$peripheral2)) .map[info$peripheral2] <- "peripheral2"
}
.map
}
## Apply compartment renaming to a single f/rate/dur/alag assignment line.
## Returns the line unchanged if it is not a modifier or its compartment is
## not in the mapping.
.odeToLinRenameCmt <- function(e, cmtMap) {
if (!is.call(e)) return(e)
if (!identical(e[[1]], quote(`<-`)) && !identical(e[[1]], quote(`=`))) return(e)
if (length(e) < 3L || !is.call(e[[2]])) return(e)
.fn <- as.character(e[[2]][[1]])
if (!(.fn %in% c("f", "rate", "dur", "alag"))) return(e)
if (length(e[[2]]) < 2L) return(e)
.cmt <- as.character(e[[2]][[2]])
.newCmt <- cmtMap[.cmt]
if (is.na(.newCmt)) return(e)
e[[2]][[2]] <- as.name(.newCmt)
e
}
## Rename the compartment argument in an adaptive dosing call. cmt AST index:
## bolus/replace/multiply/phantom -> e[[3]], infuse/infuseDur -> e[[4]],
## evid_ -> e[[5]]; obs()/reset() have no cmt and are left unchanged.
.odeToLinRenameAdaptiveCall <- function(e, cmtMap) {
if (!is.call(e)) return(e)
.fn <- as.character(e[[1]])
.cmtIdx <- switch(.fn,
bolus = 3L, replace = 3L, multiply = 3L, phantom = 3L,
infuse = 4L, infuseDur = 4L,
`evid_` = 5L,
NULL
)
if (is.null(.cmtIdx)) return(e)
if (length(e) < .cmtIdx) return(e)
.cmtArg <- e[[.cmtIdx]]
if (!is.name(.cmtArg)) return(e)
.cmtNm <- as.character(.cmtArg)
.newNm <- cmtMap[.cmtNm]
if (is.na(.newNm)) return(e)
e[[.cmtIdx]] <- as.name(.newNm)
e
}
## Recursively rename compartments throughout an expression tree.
## Handles f/rate/dur/alag assignments and adaptive dosing calls.
## Recurses into if/block/other constructs to find nested calls.
.odeToLinRenameExpr <- function(e, cmtMap) {
if (!is.call(e)) return(e)
.fn <- as.character(e[[1]])
if (.fn %in% c("<-", "=") && length(e) >= 3L && is.call(e[[2]])) {
.innerFn <- as.character(e[[2]][[1]])
if (.innerFn %in% c("f", "rate", "dur", "alag")) {
return(.odeToLinRenameCmt(e, cmtMap))
}
}
if (.fn %in% c("bolus", "infuse", "infuseDur", "replace", "multiply", "phantom", "evid_")) {
return(.odeToLinRenameAdaptiveCall(e, cmtMap))
}
for (.i in seq_along(e)) {
.child <- e[[.i]]
if (is.call(.child)) {
e[[.i]] <- .odeToLinRenameExpr(.child, cmtMap)
}
}
e
}
.odeToLinBuildExpr <- function(lstExpr, info) {
## Build `linCmt(<params>)` with the detected PK parameter names so the
## parameterization is inferred even when parameters live only in ini().
## linCmt() resolves its parameters by name, so the argument order does not
## matter. Fall back to a bare linCmt() if no parameters were detected.
.linCmtCall <- if (length(info$params) > 0L) {
as.call(c(list(as.name("linCmt")), lapply(info$params, as.name)))
} else {
str2lang("linCmt()")
}
.linCmtLine <- call("<-", as.name(info$outputVar), .linCmtCall)
.cmtMap <- .odeToLinCmtMap(info)
.ret <- list()
for (.i in seq_along(lstExpr)) {
if (.i %in% info$odeIdx) {
next # remove ODE lines
} else if (.i == info$outputIdx) {
.ret[[length(.ret) + 1L]] <- .linCmtLine # replace output with linCmt()
} else if (isTRUE(info$coupled)) {
## Coupled path: rename every compartment reference (values included) to
## its canonical linCmt name, and anchor the central endpoint to the
## central compartment so no observation compartment is injected.
.e <- lstExpr[[.i]]
if (is.call(.e) && identical(.e[[1]], quote(`~`)) && length(.e) >= 3L &&
is.name(.e[[2]]) && as.character(.e[[2]]) == info$outputVar) {
.e <- .odeToLinAddCentralCond(.e)
}
.ret[[length(.ret) + 1L]] <- .odeToLinRenameValueRefs(.e, .cmtMap)
} else {
.e <- .odeToLinRenameExpr(lstExpr[[.i]], .cmtMap)
.ret[[length(.ret) + 1L]] <- .e
}
}
.ret
}
## Package-scope cache: maps model-text key -> compiled converted rxUi.
## Prevents recompilation when rxSolve(..., useLinCmt=TRUE) is called repeatedly
## with the same model.
.odeToLinCache <- new.env(parent = emptyenv(), hash = TRUE)
## Cheap cache key: normalized text of all model lines plus the initial
## estimates. The converted model rebuilt by `.rebuildRxUiFromExpr()` bakes in
## `ui$iniFun`, so two models that share the same `model({})` equations but
## differ only in their `ini({})` block must map to distinct cache entries.
## Keying on the equations alone caused the second model to reuse the first
## model's parameters (see the rxSolve(useLinCmt=TRUE) regression).
.odeToLinCacheKey <- function(ui) {
.model <- paste0(vapply(ui$lstExpr, deparse1, character(1)), collapse = "\n")
.ini <- if (length(ui$iniDf$cond) > 0L) deparse1(ui$iniFun) else ""
paste0(.model, "\n#### ini ####\n", .ini)
}
## Package-scope cache: maps the same model-text key used by `.odeToLinCache`
## to the character vector of compartment names the conversion renamed away.
## Deriving these requires `rxModelVars()` on both the original and converted
## models, which is constant per converted model, so it is computed once here
## rather than on every solve.
.odeToLinLostCache <- new.env(parent = emptyenv(), hash = TRUE)
## The compartment names the ODE->linCmt conversion renamed away (i.e. present
## in the original model but not the converted one). Cached by model key.
.odeToLinLostStates <- function(cacheKey, original, converted) {
if (exists(cacheKey, envir = .odeToLinLostCache, inherits = FALSE)) {
return(get(cacheKey, envir = .odeToLinLostCache, inherits = FALSE))
}
.lost <- setdiff(rxModelVars(original)$state, rxModelVars(converted)$state)
assign(cacheKey, .lost, envir = .odeToLinLostCache)
.lost
}
## Is a converted linCmt() model safe to use for the given solve data?
##
## The ODE->linCmt conversion renames compartments (e.g. an ODE `centre`
## compartment becomes linCmt's `central`). If the event data addresses a
## compartment (in a dose or an observation record) by the *name* of an original
## ODE compartment that the converted model no longer has, that record would
## silently be routed nowhere -- producing all-zero predictions. In that case
## fall back to the original ODE model so behaviour matches solving without the
## linCmt optimization. `lost` is the (pre-computed) set of renamed-away
## compartment names. Numeric compartment indices are preserved by the
## conversion, and names that are not compartments in the original model (e.g.
## the special "(default)" placeholder in an event table) are handled
## identically by both models, so both are left untouched here.
.odeToLinCmtCompatible <- function(lost, data) {
if (length(lost) == 0L) {
return(TRUE)
}
if (is.null(data) || !is.data.frame(data)) {
return(TRUE)
}
.nm <- names(data)
.col <- .nm[tolower(.nm) == "cmt"]
if (length(.col) == 0L) {
return(TRUE)
}
.cmt <- data[[.col[1L]]]
if (is.factor(.cmt)) {
.cmt <- as.character(.cmt)
}
if (!is.character(.cmt)) {
## numeric indices are preserved by the conversion
return(TRUE)
}
.cmt <- unique(.cmt[!is.na(.cmt)])
if (length(.cmt) == 0L) {
return(TRUE)
}
!any(.cmt %in% lost)
}
## Rebuild an rxUi from a modified lstExpr (following the linToOde pattern).
.rebuildRxUiFromExpr <- function(ui, expr) {
.ls <- ls(ui$meta, all.names = TRUE)
.hasIni <- length(ui$iniDf$cond) > 0L
.ret <- vector("list", length(.ls) + if (.hasIni) 3L else 2L)
.ret[[1L]] <- quote(`{`)
for (.i in seq_along(.ls)) {
.ret[[.i + 1L]] <- rxUiDeparse(ui$meta[[.ls[.i]]], .ls[.i]) # nolint
}
.len <- length(.ls)
if (.hasIni) {
.ret[[.len + 2L]] <- ui$iniFun
.ret[[.len + 3L]] <- bquote(model(.(as.call(c(quote(`{`), expr)))))
} else {
.ret[[.len + 2L]] <- bquote(model(.(as.call(c(quote(`{`), expr)))))
}
.fun <- function() {}
body(.fun) <- as.call(.ret)
if (is.function(ui$model)) environment(.fun) <- environment(ui$model)
suppressMessages(as.rxUi(.fun)) # nolint
}
#' Convert ODE-based linear compartment models to analytical linCmt() form
#'
#' Detects whether a model's ODE equations form a standard 1-3 compartment PK
#' system and, if so, replaces the \code{d/dt()} equations and the central
#' output assignment (e.g. \code{cp <- central/v}) with \code{cp <- linCmt()},
#' preserving all other model lines. Detection requires linear ODE
#' right-hand sides, 1-4 compartments in a standard depot/central/peripheral
#' topology, and one output line of the form \code{var <- central_cmt / v_expr}.
#' Named parameter assignments are retained so \code{linCmt()} can infer the
#' parameterization.
#'
#' @param ui rxUi-like model object (function, rxUi, or anything accepted by
#' \code{as.rxUi}).
#' @return An rxUi model with ODE equations replaced by \code{linCmt()}, or
#' (with a message) the original model unchanged if conversion is not possible.
#' @seealso \code{\link{linToOde}} for the inverse transformation.
#' @examples
#' oneCmtOde <- function() {
#' ini({
#' tka <- 0.45
#' tcl <- log(2.7)
#' tv <- 3.45
#' add.sd <- 0.7
#' })
#' model({
#' ka <- exp(tka)
#' cl <- exp(tcl)
#' v <- exp(tv)
#' d/dt(depot) <- -ka * depot
#' d/dt(central) <- ka * depot - cl/v * central
#' cp <- central / v
#' cp ~ add(add.sd)
#' })
#' }
#' linCmtModel <- odeToLin(oneCmtOde)
#'
#' @author Matthew Fidler
#' @export
odeToLin <- function(ui) {
.ui <- as.rxUi(ui) # nolint
.ui <- rxUiDecompress(.ui) # nolint
.info <- .odeToLinDetect(.ui)
if (is.null(.info)) {
message("model does not appear to be a linear compartment ODE; returning unchanged")
return(rxUiCompress(.ui)) # nolint
}
.expr <- .odeToLinBuildExpr(.ui$lstExpr, .info)
.rebuildRxUiFromExpr(.ui, .expr)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.