.monolixPriorModel <- function() { ini({ tka <- 0.45 tcl <- log(c(0, 2.7, 100)) tv <- 3.45 temax <- logit(0.5) cl.wt <- 0 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 + WT * cl.wt) v <- exp(tv + eta.v) emax <- expit(temax) d/dt(depot) <- -depot*ka d/dt(central) <- depot*ka - cl*central/v cp <- emax * central / v cp ~ add(add.sd) }) } # the model, or a skip when this lotri/rxode2 cannot store priors .monolixPriorUi <- function() { .u <- rxode2::rxode2(.monolixPriorModel) .p <- try(ini(.u, prior(tka) ~ dnorm(0, 1)), silent=TRUE) if (inherits(.p, "try-error") || !any(names(.p$iniDf) == "prior")) { skip("this lotri/rxode2 does not support priors") } .u } .mlxLines <- function(txt, pattern) { .l <- strsplit(txt, "\n")[[1]] .l[grepl(pattern, .l)] } test_that("no priors, no [POPULATION] section and no MAP", { .u <- rxode2::rxode2(.monolixPriorModel) expect_equal(.u$mlxtranModelPopulation, "") expect_false(grepl("[POPULATION]", .u$mlxtranModel, fixed=TRUE)) expect_false(grepl("MAP", .u$mlxtranParameter, fixed=TRUE)) }) test_that("normal priors become Monolix MAP estimation", { .u <- ini(.monolixPriorUi(), prior(tka) ~ dnorm(log(1.5), 0.5)) .u <- ini(.u, prior(temax) ~ dnorm(0, 2)) .u <- ini(.u, prior(cl.wt) ~ stdNormal()) .par <- .u$mlxtranParameter expect_equal(.mlxLines(.par, "^ka_pop="), paste0("ka_pop={value=", exp(0.45), ", method=MAP}")) expect_equal(.mlxLines(.par, "^emax_pop="), "emax_pop={value=0.5, method=MAP}") expect_equal(.mlxLines(.par, "^beta_cl_WT="), "beta_cl_WT={value=0, method=MAP}") expect_equal(.mlxLines(.par, "^add__sd="), "add__sd={value=0.7, method=MLE}") # parameters without a prior are still maximum likelihood expect_equal(.mlxLines(.par, "^v_pop="), paste0("v_pop={value=", exp(3.45), ", method=MLE}")) expect_equal(.mlxLines(.par, "^omega_ka="), paste0("omega_ka={value=", sqrt(0.6), ", method=MLE}")) # the prior mean is back-transformed like the estimate; the sd is the # Gaussian-space sd Monolix uses for its prior distributions .pop <- .u$mlxtranModelPopulation expect_equal(.mlxLines(.pop, "^ka_pop "), "ka_pop = {distribution=logNormal, typical=1.5, sd=0.5}") expect_equal(.mlxLines(.pop, "^emax_pop "), "emax_pop = {distribution=logitNormal, min=0, max=1, typical=0.5, sd=2}") expect_equal(.mlxLines(.pop, "^beta_cl_WT "), "beta_cl_WT = {distribution=normal, typical=0, sd=1}") expect_length(.mlxLines(.pop, "^add__sd "), 0L) # [POPULATION] comes before [INDIVIDUAL] in .mod <- .u$mlxtranModel expect_true(regexpr("[POPULATION]", .mod, fixed=TRUE) < regexpr("[INDIVIDUAL]", .mod, fixed=TRUE)) }) test_that("bounded expit and untransformed parameters get the matching prior", { .u <- model(.monolixPriorUi(), emax <- expit(temax, 0, 10)) .u <- model(.u, v <- tv + eta.v) .u <- ini(.u, tv=30, eta.v=4) .u <- ini(.u, prior(temax) ~ dnorm(0, 2)) .u <- ini(.u, prior(tv) ~ dnorm(30, 5)) .pop <- .u$mlxtranModelPopulation # the logit prior keeps the parameter's own bounds, and its mean is the # bounded back-transform (expit(0, 0, 10) = 5) expect_equal(.mlxLines(.pop, "^emax_pop "), "emax_pop = {distribution=logitNormal, min=0, max=10, typical=5, sd=2}") # an untransformed parameter is normal in Monolix, so is its prior expect_equal(.mlxLines(.pop, "^v_pop "), "v_pop = {distribution=normal, typical=30, sd=5}") expect_equal(.mlxLines(.u$mlxtranParameter, "^v_pop="), "v_pop={value=30, method=MAP}") }) test_that("a normal prior written with named arguments is read the same", { .u <- ini(.monolixPriorUi(), prior(tka) ~ dnorm(mean=log(1.5), sd=0.5)) expect_equal(.mlxLines(.u$mlxtranModelPopulation, "^ka_pop "), "ka_pop = {distribution=logNormal, typical=1.5, sd=0.5}") }) test_that("a prior mean written with an rxode2 transform is evaluated", { .u <- ini(.monolixPriorUi(), prior(temax) ~ dnorm(logit(0.6), 0.01)) expect_equal(.mlxLines(.u$mlxtranModelPopulation, "^emax_pop "), "emax_pop = {distribution=logitNormal, min=0, max=1, typical=0.6, sd=0.01}") }) test_that("a fixed parameter keeps FIXED and has no prior definition", { .u <- ini(.monolixPriorUi(), prior(tka) ~ dnorm(log(1.5), 0.5)) .u <- ini(.u, tka=fix(0.45)) expect_equal(.mlxLines(.u$mlxtranParameter, "^ka_pop="), paste0("ka_pop={value=", exp(0.45), ", method=FIXED}")) expect_equal(.u$mlxtranModelPopulation, "") }) test_that("priors Monolix cannot represent are refused", { .u <- ini(.monolixPriorUi(), prior(om.eta.cl) ~ dnorm(0.3, 0.1)) expect_error(.u$mlxtranParameter, "prior on the omega") # a block prior is stored on the block's first diagonal element .u <- ini(.monolixPriorUi(), eta.ka + eta.cl ~ c(0.6, 0.01, 0.3)) .u <- ini(.u, prior(eta.ka, eta.cl) ~ invWishart(4)) expect_error(.u$mlxtranParameter, "prior on the omega") # Monolix's probitNormal is only on (0, 1) .p <- model(rxode2::rxode2(.monolixPriorModel), emax <- probitInv(temax, 0, 100)) .p <- ini(.p, temax=0) expect_error(ini(.p, prior(temax) ~ dnorm(0, 1))$mlxtranParameter, "probitInv") .p01 <- model(.p, emax <- probitInv(temax)) expect_equal(.mlxLines(ini(.p01, prior(temax) ~ dnorm(0, 1))$mlxtranModelPopulation, "^emax_pop "), "emax_pop = {distribution=probitNormal, typical=0.5, sd=1}") .u <- ini(.monolixPriorUi(), prior(tcl) ~ dcauchy(0, 1)) expect_error(.u$mlxtranParameter, "univariate normal prior") .u <- ini(.monolixPriorUi(), prior(tcl, tv) ~ multiNormal(c(1, 3), lotri(tcl + tv ~ c(1, 0.1, 1)))) expect_error(.u$mlxtranParameter, "univariate normal prior") # Monolix 2024R1 accepts a MAP prior on a residual error parameter and # then ignores it, so it is refused (sd or variance parameterization) .u <- ini(.monolixPriorUi(), prior(add.sd) ~ dnorm(0.7, 0.1)) expect_error(.u$mlxtranParameter, "residual error parameter 'add.sd'") .u2 <- try(model(.u, cp ~ add(add.sd) + var()), silent=TRUE) if (!inherits(.u2, "try-error")) { expect_error(.u2$mlxtranParameter, "residual error parameter 'add.sd'") } }) test_that("nlmixr2 writes a MAP mlxtran for a model with a normal prior", { .u <- ini(.monolixPriorUi(), prior(tka) ~ dnorm(log(1.5), 0.5)) withr::with_tempdir({ suppressMessages(try( nlmixr2(.u, nlmixr2data::theo_sd, "monolix", monolixControl(runCommand=NA, modelName="monolixPrior")), silent=TRUE)) .f <- "monolixPrior-monolix.mlxtran" expect_true(file.exists(.f)) .txt <- paste(readLines(.f), collapse="\n") expect_equal(.mlxLines(.txt, "^ka_pop="), paste0("ka_pop={value=", exp(0.45), ", method=MAP}")) expect_equal(.mlxLines(.txt, "^ka_pop "), "ka_pop = {distribution=logNormal, typical=1.5, sd=0.5}") }) }) test_that("nlmixr2 refuses an omega prior for monolix before writing files", { .u <- ini(.monolixPriorUi(), prior(om.eta.cl) ~ dnorm(0.3, 0.1)) withr::with_tempdir({ expect_error( nlmixr2(.u, nlmixr2data::theo_sd, "monolix", monolixControl(runCommand=NA, modelName="monolixPrior")), "omega") expect_false(file.exists("monolixPrior-monolix.mlxtran")) }) }) test_that("the normal prior reader takes positional or named arguments", { .b <- loadNamespace("babelmixr2") expect_equal(.b$.mlxtranPriorNormal("dnorm(log(2), 0.5)", "x"), c(log(2), 0.5)) expect_equal(.b$.mlxtranPriorNormal("dnorm(mean=log(2), sd=0.5)", "x"), c(log(2), 0.5)) expect_equal(.b$.mlxtranPriorNormal("dnorm(sd=0.5, mean=1)", "x"), c(1, 0.5)) expect_equal(.b$.mlxtranPriorNormal("dnorm(1, sd=0.5)", "x"), c(1, 0.5)) expect_equal(.b$.mlxtranPriorNormal("normal(logit(0.6), 2)", "x"), c(logit(0.6), 2)) expect_equal(.b$.mlxtranPriorNormal("stdNormal()", "x"), c(0, 1)) expect_error(.b$.mlxtranPriorNormal("dnorm(1)", "x"), "univariate normal") expect_error(.b$.mlxtranPriorNormal("dnorm(1, -1)", "x"), "univariate normal") expect_error(.b$.mlxtranPriorNormal("dnorm(m=1, s=2, z=3)", "x"), "univariate normal") expect_error(.b$.mlxtranPriorNormal("dnorm(1, nope(2))", "x"), "univariate normal") }) test_that("a prior on a dotted tainted mu-ref uses the __ Monolix name (#220)", { .u <- .monolixPriorUi() # cl.wt shared by cl and v is not a mu-referenced covariate, so it # becomes the tainted rx__cl.wt, written rx__cl__wt for Monolix .u <- model(.u, v <- exp(tv + eta.v + WT * cl.wt)) .u <- ini(.u, prior(cl.wt) ~ dnorm(0, 2)) expect_equal(.mlxLines(.u$mlxtranParameter, "^rx__cl__wt_pop="), "rx__cl__wt_pop={value=0, method=MAP}") expect_equal(.mlxLines(.u$mlxtranModelPopulation, "^rx__cl__wt_pop "), "rx__cl__wt_pop = {distribution=normal, typical=0, sd=2}") expect_true(grepl("rx__cl__wt,", .u$monolixModel, fixed=TRUE)) expect_false(grepl("cl.wt", .u$mlxtran, fixed=TRUE)) })