diff --git a/.gitignore b/.gitignore
index fb3b39042..d6cce0004 100755
--- a/.gitignore
+++ b/.gitignore
@@ -64,7 +64,6 @@ tests/testthat/Runs/
.vscode/
.Rd2pdf65996/
other/
-Examples
Experimental
.httr-oauth
inst/rust/template/*
@@ -73,11 +72,11 @@ inst/options/PMoptions.json
inst/doc
target/
-*_test.R
-*.csv
*.txt
1
2
3
Cargo.lock
docs/
+
+man/
diff --git a/Archived/PM_model_old.R b/Archived/PM_model_old.R
index ae4065c2e..ffcec5b3f 100755
--- a/Archived/PM_model_old.R
+++ b/Archived/PM_model_old.R
@@ -553,6 +553,7 @@ PM_model_list <- R6::R6Class("PM_model_list",
{
compile_model(
temp_model,
+ tools::R_user_dir(package = "Pmetrics"),
model_path, private$get_primary()
)
self$binary_path <- model_path
diff --git a/Cargo.lock b/Cargo.lock
index 1861811e7..a4064bec1 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
-version = "1.0.97"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "approx"
@@ -1286,9 +1286,9 @@ dependencies = [
[[package]]
name = "pharmsol"
-version = "0.19.0"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda98c255dc04fd94684cc5eb78a47f7cc759ae6836036fd59d7f33273b46c00"
+checksum = "2fc25564d039d0cd5701013aa3785a339b14cf0b51409d7b817320bc360dc944"
dependencies = [
"argmin",
"argmin-math",
@@ -1320,6 +1320,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"extendr-api",
+ "libloading",
"pmcore",
"rayon",
"tracing",
@@ -1328,13 +1329,12 @@ dependencies = [
[[package]]
name = "pmcore"
-version = "0.20.0"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6896a2df0fa57858e5dc2343bf6ba4d2be14b9c097601cc1aa2c75b41b72c9a2"
+checksum = "3866100507aa3bcba475381af3102d84b5e503bce82cb82f56cf0fa46cc1e408"
dependencies = [
"anyhow",
"argmin",
- "argmin-math",
"csv",
"faer",
"faer-ext",
diff --git a/Examples/Rscript/examples.R b/Examples/Rscript/examples.R
new file mode 100644
index 000000000..15ccf6921
--- /dev/null
+++ b/Examples/Rscript/examples.R
@@ -0,0 +1,778 @@
+# INTRODUCTION ------------------------------------------------------------
+
+# Lines that start with "#" are comments and ignored by R. Follow the
+# directions in them. Execute each non-comment line in this script by
+# putting your cursor on it and sending it to the R console.
+# You can do this in several ways:
+# Windows
+# R-studio
+# 1) The Run button at the top
+# 2) Ctrl-Enter
+# R GUI - when the script window is active
+# 1) The Run line or selection button at the top
+# 2) Ctrl-R
+# Mac
+# R-studio
+# 1) The Run button at the top
+# 2) Command-Enter
+# R GUI
+# 1) Command-Enter
+
+# This script also serves to introduce several R programming functions
+# and techniques. For any function, you can get help by typing ?function_name
+# in the R console (the lower left window pane in RStudio).
+
+# Load Pmetrics into memory. You must include this line at the beginning
+# of every script.
+
+library(Pmetrics)
+
+# EXERCISE 1 - NPAG RUN ------------------------------------------------
+
+# EXAMPLE NPAG RUN - tlag, ka, kel, vol
+
+# It is useful to annotate your runs as above, so that you can remember
+# what you did later!
+
+
+# Tell R where your working directory is going to be.
+# Windows users: Make sure that you separate directories with a
+# forward slash "/" or double backslashes "\\". Unfortunately, Windows is the only OS that uses
+# backslashes "\", so R conforms to Unix/Linux style.
+
+wd <- "C:/Users/siel3/code/LAPKB/Pmetrics_rust/Examples"
+
+wd <- glue::glue("{getwd()}/inst/Examples/Runs")
+
+# change to the working directory to the Examples folder
+setwd(wd)
+
+# DATA OBJECT
+
+# Pmetrics always needs data and a model to run
+# create our first data object
+
+# create a new data object by reading a file
+# set the limit of quantification (loq) to 1: see ?PM_data for help
+exData <- PM_data$new(data = "../src/ex.csv", loq = 1)
+
+# you can look at this file directly by opening it in
+# a spreadsheet program like Excel, or a text editor
+
+# exData is an R6 object, which means that contains both data and methods to
+# process that data, for example:
+exData$data # contains your original datafile
+exData$standard_data # contains the standardized and validated data,
+exData$summary() # prints the summary of the data to the terminal, or
+
+# another way to do that is using the more common S3 framework in R:
+summary(exData)
+
+# To look at the contents of an object:
+names(exData)
+
+# other examples of things that can be done with this object are
+exData # view the original data in the viewer
+exData$print(standard = TRUE) # view the standardized data in the viewer
+exData$print(viewer = FALSE) # view original data in console
+exData$plot() #plot the raw data; more on that later
+
+# MODEL OBJECT
+# You can specify a model by reading a file or directly as an object. We'll do both.
+# The following code creates the same model as in /src/model.txt file.
+# See PMmanual() for details on creating models in R compared to text files.
+# The advantage of creating them in R is that one does not need to copy model
+# files into folders to provide necessary inputs.
+
+mod1 <- PM_model$new(
+ pri = list(
+ Ka = ab(0.1, 0.9),
+ Ke = ab(0.001, 0.1),
+ V = ab(30, 120),
+ lag1 = ab(0, 4)
+ ),
+ cov = list(
+ wt = interp(),
+ africa = interp("none"),
+ age = interp(),
+ gender = interp("none"),
+ height = interp()
+ ),
+ eqn = function(){
+ two_comp_bolus
+ },
+ lag = function(){
+ lag[1] = lag1
+ },
+ out = function(){
+ Y[1] = X[2]/V
+ },
+ err = list(
+ proportional(5, c(0.02, 0.05, -0.0002, 0))
+ )
+)
+
+
+# look at it
+mod1
+
+#plot it
+mod1$plot()
+
+
+# in the working directory we have another file "model.txt" that contains the old
+# representation of the same model we previously presented, let's take a look at it.
+system("cat ../src/model.txt")
+
+# PM_model$new() also accepts the path to a model file
+# create the same model using this file
+mod1b <- PM_model$new("../src/model.txt")
+mod1b
+
+# PM_model provides a method to update the different elements of a model, for example:
+mod1b$update(
+ pri = list(
+ ka = ab(0.001, 5)
+))
+
+# It is case sensitive, so ka is different from Ka. To remove a parameter, set it to NULL.
+
+
+mod1b$arg_list$pri
+
+# to copy a model use the $clone() method.
+mod1b <- mod1$clone()
+
+# simply using mod1b <- mod1 will cause mod1b to be changed if mod1 is changed,
+# as R6 objects use reference semantics. For more details you can refer to
+# https://adv-r.hadley.nz/r6.html, Section 14.4.
+
+#lastly, use the app! PMmanual() and the article on models for details on this.
+build_model() #start from scratch
+build_model(exData) #start with data to match covariates
+build_model(mod1) #start with a model and update it
+
+
+
+# To keep everything tidy, we are working in a folder specific to store the runs
+
+
+run1 <- mod1$fit(data = exData, run = 1, overwrite = TRUE) # execute the fit and return the results to run1
+
+
+#
+# After the run is complete the results are returned to the assigned object, here 'run1'
+
+# you need get the extracted information back into R.
+# They will be sequentially numbered as /1, /2, /3,... in your working directory.
+
+# One benefit of having this fit object is that it is possible to run multiple
+# fittings without needing to move data files around
+
+getwd()
+list.files()
+
+# Result Object - the result is already returned to run1 above, but if you need to load it later,
+# you can use PM_load()
+run1 <- PM_load(1)
+
+# Create a PM_result object by reading a run folder. The "1" in the parentheses tells Pmetrics to
+# look in the /1 folder.
+
+# Plot the raw data using R6 with various options. Type ?plot.PM_data in the R console for help.
+run1$data$plot()
+run1$data$plot(overlay = FALSE, xlim = c(119, 145))
+run1$data$plot(xlim = c(119, 146), group = "gender", group_names = c("Male", "Female"),
+ marker = list(color = c("blue","red"), symbol = c("circle","triangle-up")))
+
+run1$data$plot(xlim = c(119, 146), group = "gender", group_names = c("Male", "Female"), marker = list(color = "Set2"))
+
+
+# The following are the older S3 method with plot(...) for the first two examples
+# You can use R6 or S3 for any Pmetrics object
+# We will focus on R6 as the more modern way.
+plot(run1$data)
+
+# here's a summary of the original data file; ?summary.PMmatrix for help
+run1$data$summary()
+
+# Plot some observed vs. predicted data. Type ?plot.PMop in the R console for help.
+# Click on points to highlight all points from the same subject.
+run1$op$plot()
+run1$op$plot(pred.type = "pop")
+run1$op$plot(line = list(lm = list(ci = 0, color = "red"), loess = FALSE))
+
+# the original op object data can be accessed via
+run1$op$data
+
+# get a summary with bias and imprecision of the population predictions;
+# ?summary.PMop for help
+run1$op$summary(pred.type = "pop")
+
+# the S3 way
+summary(run1$op, pred.type = "pop")
+
+# look at the summary for the posterior predictions (default pred.type) based
+# on means of parameter values
+run1$op$summary(icen = "mean")
+
+# The OP plot can be disaggregated into a Tidy compatible format from $data (see https://www.tidyverse.org/)
+# This allow pre processing in ways more flexible than the default plot method.
+library(tidyverse)
+run1$op$data %>% plot()
+run1$op$data %>%
+ filter(pred > 5) %>%
+ filter(pred < 10) %>%
+ summary()
+
+# see a header with the first 10 rows of the op object
+head(run1$op$data, 10)
+
+
+# Plot final population joint density information. Type ?plot.PMfinal in the R console for help.
+run1$final$plot()
+
+# add a kernel density curve
+run1$final$plot(line = list(density = list(color = "red")))
+run1$final$data %>% plot()
+
+
+# A bivariate plot. Plotting formulae in R are of the form 'y~x'
+run1$final$plot(ke ~ v,
+ marker = list(color = "red", symbol = "diamond"))
+
+
+
+# or the S3 way
+plot(run1$final)
+
+# The original final object can be accessed using
+run1$final$data
+names(run1$final$data)
+
+# see the population points
+run1$final$popPoints
+
+# or
+run1$final$data$popPoints
+
+# see the population mean parameter values
+run1$final$popMean
+
+# see a summary with confidence intervals around the medians
+# and the Median Absolute Weighted Difference (MAWD);
+# ?summary.PMfinal for help
+run1$final$summary()
+
+# summarize the cycle information; ?summary.PM_cycle for help
+run1$cycle$summary()
+run1$cycle$data %>% summary()
+
+
+# Plot cycle information
+# Type ?plot.PM_cycle in the R console for help.
+run1$cycle$plot()
+
+# names of the cycle object; ?makeCycle for help
+names(run1$cycle$data)
+
+# gamma/lamda value on last 6 cycles
+tail(run1$cycle$data$gamlam)
+
+# Plot covariate information. Type ?plot.PMcov in the R console for help.
+# Recall that plotting formulae in R are of the form 'y~x'
+run1$cov$plot(v ~ wt)
+
+run1$cov$data %>%
+ filter(age > 25) %>%
+ plot(v ~ wt)
+
+# Plot
+run1$cov$plot(ke ~ age, line = list(loess = FALSE, lm = TRUE),
+ marker = list(symbol = 3))
+
+# Another plot with mean Bayesian posterior parameter and covariate values...
+# Remember the 'icen' argument?
+run1$cov$plot(v ~ wt, icen = "mean")
+
+# When time is the x variable, the y variable is aggregated by subject.
+# In R plot formulae, calculations on the fly can be included using the I() function
+run1$cov$plot(I(v * wt) ~ time)
+
+# The previous cov object can be seen via:
+run1$cov
+
+# but to access individual elements, use:
+run1$cov$data[, 1:3] # for example
+names(run1$cov)
+
+# summarize with mean covariates; ?summary.PMcov for help
+run1$cov$summary(icen = "mean")
+
+
+# Look at all possible covariate-parameter relationships by multiple linear regression with forward
+# and backward elimination - type ?PM_step in the R console for help.
+run1$step()
+# or on the cov object directly
+run1$cov$step()
+# icen works here too....
+run1$step(icen = "mean")
+# forward elimination only
+run1$step(direction = "forward")
+
+
+# EXERCISE 2 - NPAG WITH COVARIATES ---------------------------------------
+
+# Again, without copying files, let's create another run object, this time using
+# a model that include covariates
+
+# First clone mod1
+mod2 <- mod1$clone()
+
+# Then update it
+mod2 <- mod2$update(
+ pri = list(
+ V0 = ab(30, 120),
+ V = NULL
+ ),
+ sec = function(x) {
+ V = V0*(WT/55)
+ },
+ err = list(
+ proportional(2.39, c(0.02, 0.05, -0.0002, 0), fixed = TRUE)
+ )
+)
+# we can also make a model object by loading a file
+mod2b <- PM_model$new("../src/model2.txt")
+
+
+run2 <- mod2$fit(data = exData, run = 2, overwrite = TRUE)
+
+# for future loading
+run2 <- PM_load(2)
+
+
+
+
+# EXERCISE 3 - COMPARING MODELS -------------------------------------------
+
+
+# Let's compare model 1 and model 2. You can compare any number of models.
+# Type ?PM_compare for help.
+PM_compare(run1, run2)
+
+
+
+# EXERCISE 4 - MODEL VALIDATION -------------------------------------------
+
+# MODEL VALIDATION EXAMPLES
+# Example of Pmetrics visual predictive check and prediction-corrected visual predictive check
+# for model validation - be sure to have executed the NPAG run above
+# Type ?makeValid in the R console for help.
+# Choose wt as the covariate to bin. Accept all default bin sizes.
+run2$validate(limits = c(0, 3))
+
+# To see what it contains, use:
+run2$valid
+
+# Default visual predictive check; ?plot.PM_valid for help
+run2$valid$plot()
+
+# or old S3
+plot(run2$valid)
+
+
+# Generate a prediction-corrected visual predictive check; type ?plot.PMvalid in the R console for help.
+run2$valid$plot(type = "pcvpc")
+
+# Create an npde plot
+run2$valid$plot(type = "npde")
+
+# Here is another way to generate a visual predicive check...
+npc_2 <- run2$valid$simdata$plot(obs = run2$op, log = FALSE, binSize = 0.5)
+
+# The jagged appearance of the plot when binSize=0 is because different subjects have
+# different doses, covariates, and observation times, which are all combined in one simulation.
+# Collapsing simulation times within 1 hour bins (binSize=1) smooths
+# the plot, but can change the P-values in the numerical predictive check below.
+
+npc_2
+# ...and here is a numerical predictive check
+# P-values are binomial test of proportion of observations less than
+# the respective quantile
+
+
+# EXERCISE 5 - SIMULATOR RUN ----------------------------------------------
+
+setwd(wd)
+dir.create("../Sim")
+setwd("../Sim")
+
+# The following will simulate 100 sets of parameters/concentrations using the
+# first subject in the data file as a template.
+# Limits are put on the simulated parameter ranges to be the same as in the model.
+# The population parameter values from the NPAG run in exercise 2 are used for the Monte Carlo Simulation.
+simdata <- run2$sim(include = 1, limits = NA, nsim = 100)
+
+# Below is the alternate way to simulate, which is particularly useful if you define
+# your own population parameters. See ?SIMrun for details on this as well as
+# the article on simulation linked by PMmanual().
+poppar <- list(
+ wt = 1,
+ mean = c(0.6, 0.05, 77.8, 1.2),
+ cov = diag(c(0.07, 0.0004, 830, 0.45))
+)
+
+simOther <- PM_sim$new(poppar = poppar, data = exData, model = mod1,
+ include = 1, limits = NA, nsim = 100)
+
+
+# simulate from a model with new data
+sim_new <- run2$sim(
+ data = "../src/ptaex1.csv",
+ include = 2, limits = NA,
+ predInt = c(120, 144, 0.5)
+)
+
+
+
+# Plot them; ?plot.PM_sim for help
+simdata$plot()
+simOther$plot()
+sim_new$plot(log = FALSE)
+
+# Simulate using multiple subjects as templates
+simdata <- run2$sim(include = 1:4, limits = NA, nsim = 100)
+
+# Plot the third simulation; use include/exclude to specify the ID numbers,
+# which are the same as the ID numbers in the template data file
+simdata$plot(include = 2)
+
+# or in S3
+plot(simdata$data, include = 3)
+
+# Parse and combine multiple files and plot them. Note that combining simulations from templates
+# with different simulated observation times can lead to unpredictable plots
+simdata2 <- run2$sim(include = 1:4, limits = NA, nsim = 100)
+simdata2$plot()
+
+# simulate with covariates
+# in this case we use the covariate-parameter correlations from run 2, which
+# are found in the cov.2 object; we re-define the mean weight to be 50 with
+# SD of 20, and limits of 10 to 70 kg. We fix africa, gender and height covariates,
+# but allow age (the last covariate) to be simulated, using the mean, sd, and
+# limits in the original population, since we didn't specify them.
+# See ?SIMrun for more help on this and the Pmetrics manual.
+
+covariate <- list(
+ cov = run2$cov,
+ mean = list(wt = 50),
+ sd = list(wt = 20),
+ limits = list(wt = c(10, 70)),
+ fix = c("africa", "gender", "height")
+)
+
+# now simulate with this covariate list object
+simdata3 <- run2$sim(include = 1:4, limits = NA, nsim = 100, covariate = covariate)
+
+# compare difference in simulations without covariates simulated...
+# PM_sim's plot function defaults to the first simulation if there
+# are multiple simulations and "at" is not specified.
+simdata$plot()
+
+# ...and with covariates simulated
+simdata3$plot()
+
+# Here are the simulated parameters and covariates for the first subject's
+# template; note that both wt and age are simulated, using proper covariances
+# with simulated PK parameters
+simdata3$data$parValues
+
+# We can summarize simulations too. See ?summary.PM_sim for help.
+simdata3$summary(field = "obs", include = 3)
+
+# look in the working directory and find the "c_simdata.csv" and "c_simmodel.txt" files
+# which were made when you simulated with covariates. Compare to original
+# "simdata.csv" and "simmoddel.txt" files to note that simulated covariates become
+# Primary block variables, and are removed from the template data file.
+
+# EXERCISE 6 - SAVING PMETRICS OBJECTS ------------------------------------
+
+setwd(wd)
+
+# The following objects have methods to save them to or load them from files:
+# PM_fit
+# PM_result
+# PM_sim
+# PM_pta
+
+# Example - save the PM_result (run2) to the "2" folder
+run2$save(file = "2/outputs/run2.rds") # rds is the recommended file extension
+list.files("2/outputs")
+copy_run2 <- PM_load(file = "2/outputs/run2.rds")
+copy_run2
+
+# If you want to save multiple objects into one single file, R provides the
+# following functionality
+
+save(exData, mod1, run1, simdata, file = "2/test_drug.Rdata")
+list.files("2")
+load("2/test_drug.Rdata")
+
+# or
+save.image("2/workspace.Rdata") # This will save all variables in your environment
+list.files("2")
+load("2/workspace.Rdata")
+
+
+# EXERCISE 7 - CONTINUING RUNS OR EXTERNAL VALIDATIONS --------------------
+
+# Example of a run with a non-uniform density
+# This is a good way to continue a previous run,
+# in this case it continues where run 1 left off
+
+# note that we can supply a run number to model, data, and prior arguments. The numbers do not
+# have to be the same. This will copy the appropriate files from the specified run to be used
+# in the current run. By specifying a prior, we are starting with the non-uniform density from the
+# end of the specified fun.
+run3 <- mod2$fit(data = exData, prior = 2)
+run3 <- PM_load(3)
+
+# We could also generate Bayesian posterior parameter estimates for a new population this way:
+# run3 <- mod2$fit(data=PM_data("newPop.csv"), prior = 2, cycles = 0)
+# This won't run because we don't have a newPop.csv file,
+# but shows you how it could be done.
+
+
+
+# EXERCISE 8 - PROBABILITY OF TARGET ATTAINMENT ---------------------------
+
+# Note: these can be computationally intense and take some time.
+
+# Examples of probability of target attainment analysis
+# Be sure to have executed the NPAG run above and used PM_load(2) in EXERCISE 2
+# Type ?PM_sim, ?PM_pta, or ?plot.PM_pta into the R console for help.
+
+
+# simulate with the template data file that contains different doses
+# Look at PM_sim for help on arguments to this function, including predInt,
+# seed, limits, nsim.
+
+simlist1 <- run1$sim(
+ limits = c(0, 3), data = "../src/ptaex1.csv",
+ predInt = c(120, 144, 0.5), seed = rep(-17, 4)
+)
+
+# now simulate with covariates; make sure that you defined the covariate
+# object first in Exercise 5 above and have loaded the results of Exercise 2
+# with PM_load(2)
+simlist2 <- run2$sim(
+ limits = 5, data = "../src/ptaex1.csv",
+ predInt = c(120, 144, 0.5), seed = rep(-17, 4),
+ covariate = covariate
+)
+
+# make the first PMpta object to calculate the time above each target for at
+# least 60% of the dosing
+# interval from 120 to 144 hours. Include labels for the simulations.
+# ?makePTA for help
+# define simulation labels first
+
+simlabels <- c("600 mg daily", "1200 mg daily", "300 mg bid", "600 mg bid")
+
+pta1_2 <- PM_pta$new(
+ simdata = simlist1,
+ target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32), target_type = "time",
+ success = 0.6, start = 120, end = 144
+)
+
+pta1b_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels,
+ target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32), target_type = "time",
+ success = 0.6, start = 120, end = 144
+)
+
+# summarize the results
+pta1_2$summary()
+
+# in the summary()$pta, reg_num is the simulation template ID number;
+# target in this case is the MIC; prop_success is the proportion of the simulated
+# profiles for each dose/MIC that are above the success threshold (0.6); pdi.mean and pdi.sd
+# are the mean and standard deviation of the pharmacodynamic index (PDI), in this case proportion of the interval > MIC.
+# In the $pdi, target and simnum are the same, but now the median and confidence
+# intervals (default 95%) PDI are shown.
+# ?summary.PMpta for help
+
+# Plot the first without covariates. We didn't include simulation
+# labels in the makePTA command, so generics are used here, but we move it to
+# the bottom left; ?legend for help on arguments to supply to the
+# legend list argument to plot.PMpta.
+pta1_2$plot(ylab = "Proportion with %T>MIC of at least 60%", grid = TRUE, legend = list(x = "bottomleft"))
+
+pta1b_2$summary()
+
+# Plot the second with covariates simulated. Note the regimen labels are included, but we move
+# the legend to the bottom left.
+pta1b_2$plot(
+ ylab = "Proportion with %T>MIC of at least 60%", grid = TRUE,
+ legend = list(x = "bottomleft")
+)
+
+# Now we'll define success as free auc:mic > 100 with a free drug fraction of 50%
+pta2_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels, target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32),
+ free_fraction = 0.7,
+ target_type = "auc", success = 100, start = 120, end = 144
+)
+summary(pta2_2)
+pta2_2$plot(
+ ylab = "Proportion with AUC/MIC of at least 100", grid = TRUE,
+ legend = list(x = "bottomleft")
+)
+
+# success is Cmax/MIC >=10
+pta3_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels,
+ target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32),
+ target_type = "peak", success = 10, start = 120, end = 144
+)
+pta3_2$summary()
+pta3_2$plot(ylab = "Proportion with peak/MIC of at least 10", grid = TRUE)
+
+# success = Cmin:MIC > 1
+pta4_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels,
+ target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32),
+ target_type = "min", success = 1, start = 120, end = 144
+)
+pta4_2$summary()
+pta4_2$plot(ylab = "Proportion with Cmin/MIC of at least 1", grid = TRUE, legend = list(x = "bottomleft"))
+
+# now plot the PDI (pharmacodynamic index) of each regimen, rather than the proportion
+# of successful profiles. A PDI plot is always available for PMpta objects.
+pta4_2$plot(at = 1, type = "pdi", ylab = "Cmin:MIC", grid = TRUE)
+
+# Each regimen has the 90% confidence interval PDI around the median curve,
+# in the corresponding, semi-transparent color. Make the CI much narrower...
+pta4_2$plot(at = 1, type = "pdi", ci = 0.1)
+
+# ...or gone altogether, put back the grid, redefine the colors, and make lines narrower
+pta4_2$plot(
+
+ at = 1, type = "pdi", ci = 0, grid = TRUE,
+ line = list(
+ color = c("blue", "purple", "black", "brown"),
+ width = 1
+ )
+)
+
+# now let's repeat the analysis but simulate the distribution of MICs
+# using susceptibility of Staphylococcus aureus to vancomycin contained
+# in the mic1 dataset within Pmetrics (?mic1)
+
+# see the source with ?mic1
+pta4b_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = c("600 mg daily", "1200 mg daily", "300 mg bid", "600 mg bid"),
+ target = makePTAtarget(mic1), target_type = "min", success = 1, start = 120, end = 144
+)
+
+pta4b_2$summary()
+# plot it
+pta4b_2$plot(
+ grid = TRUE, ylab = "Proportion with Cmin/MIC of at least 1",
+ marker = list(color = "red"), line = list(color = "black")
+)
+pta4b_2$plot(type = "pdi", grid = TRUE, ylab = "Proportion with Cmin/MIC of at least 1")
+
+# note that the plot changes since target MICs are no longer discrete
+# since most of the MICs are very low, the regimens all look very similar
+
+# success = concentration at time 3 hours:MIC > 2
+pta5_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels,
+ target = c(0.25, 0.5, 1, 2, 4, 8, 16, 32), target_type = 123, success = 2, start = 120, end = 144
+)
+pta5_2$summary()
+pta5_2$plot(ylab = "Proportion with C3/MIC of at least 1", grid = TRUE, legend = list(x = .3, y = 0.1))
+
+
+# success is trough >10
+pta6_2 <- PM_pta$new(
+ simdata = simlist2,
+ simlabels = simlabels,
+ target = 10, target_type = 144, success = 1, start = 120, end = 144
+)
+plot(pta6_2)
+pta6_2$summary()
+
+# EXERCISE 10 - OPTIMAL SAMPLE TIMES --------------------------------------
+
+setwd(wd)
+dir.create("../MMopt")
+setwd("../MMopt")
+
+# calculate MM-optimal sample times for Run 2, and the 1200 mg once daily dose in the PTA
+# By specifying the predInt to start and stop at 120 and 144 hours, with an interval of 1 hour,
+# we are sampling at steady state. Including "subject 2", means only the 1200 mg once daily dose
+# will serve as a simulation template.
+
+run2$opt(
+ data = "../src/ptaex1.csv",
+ nsamp = 2, predInt = c(120, 140, 1),
+ limits = NA,
+ include = 2
+)
+# see the optimal sample times and the Bayes Risk of misclassification,
+# which is only useful to compare optimal sampling regimens, i.e. the
+# absolute value is less helpful, but is the statistic minimized by the
+# selected optimal sample times for a given model
+
+mmopt_2 <- PM_opt$new(
+ poppar = run2$final,
+ model = run2$model,
+ data = "../src/ptaex1.csv",
+ nsamp = 2, predInt = c(120, 140, 1),
+ include = 2
+)
+
+# plot it, with the red lines indicating the optimal sample times.
+# see ?plot.MMopt for help
+mmopt_2$plot()
+plot(mmopt_2)
+plot(mmopt_2, line = list(color = "slategrey"), times = list(color = "orange"))
+
+# EXERCISE 11 - ASSAY ERROR -----------------------------------------------
+# see ?makeErrorPoly for more help
+# This will let you choose the best set of C0, C1, C2, C3 for your modeling,
+# based on assay validation data which includes the "obs", which are the
+# nominal concentrations of the standards, and "sd", which is the standard
+# deviation of replicate measurements of each of the standards, i.e. the
+# inter-day and/or intra-day standard deviation
+
+obs <- c(0, 25, 50, 100, 250, 500, 1000, 2000, 5000)
+sd <- c(0.5, 6.4, 8.6, 12, 8.6, 37.2, 60.1, 165.7, 483)
+
+# See plots.pdf, page 50
+makeErrorPoly(obs = obs, sd = sd)
+
+# choose the one with the best R-squared that will never result in a
+# negative value for the SD
+
+
+
+# Ancillary functions -----------------------------------------------------
+
+# Be sure to check out the help files for the following functions:
+#
+# makeAUC() - calculate AUC from a variety of inputs
+# makeNCA() - non-compartmental analysis
+# NM2PM() - convert NONMEM data files to Pmetrics data files
+# qgrowth() - CDC growth charts
+# zBMI() - CDC Pediatric BMI z-scores and percentiles
+# ss.PK() - sample size for Phase 1 PK studies
diff --git a/Examples/Runs/.gitkeep b/Examples/Runs/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/Examples/src/bad.csv b/Examples/src/bad.csv
new file mode 100644
index 000000000..0d644a159
--- /dev/null
+++ b/Examples/src/bad.csv
@@ -0,0 +1 @@
+POPDATA DEC_11,,,,,,,,,,,,,,,,,,
#ID,EVID,TIME,DUR,DOSE,ADDL,II,INPUT,OUT,OUTEQ,C0,C1,C2,C3,WT,AFRICA,AGE,GENDER,HEIGHT
1,1,0,1,600,.,.,1,.,..,.,.,.,.,46.7,1,21,1,160
1,.,24,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
1,1,48,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
1,1,72,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
1,1,96,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
1,0,120,.,.,.,.,.,10.44,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,1,120,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
1,0,121,.,.,.,.,.,12.89,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,0,122,.,.,.,.,.,14.98,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,0,125.99,.,.,.,.,.,16.69,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,0,129,.,.,.,.,.,20.15,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,0,132,.,.,.,.,.,14.97,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
1,0,143.98,.,.,.,.,.,12.57,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
2,1,0,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,1,24,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,1,48,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,1,72,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,1,96,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,0,120,.,.,.,.,.,3.56,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,1,120,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
2,0,120.98,.,.,.,.,.,5.84,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,0,121.98,.,.,.,.,.,6.54,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,0,126,.,.,.,.,.,6.14,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,0,129.02,.,.,.,.,.,6.56,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,0,132.02,.,.,.,.,.,4.44,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
2,0,144,.,.,.,.,.,3.76,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
3,1,0,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,1,24,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,1,48,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,1,72,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,1,96,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,1,120,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
3,0,120.08,.,.,.,.,.,4.06,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,121.07,.,.,.,.,.,3.24,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,122.08,.,.,.,.,.,3.09,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,126.08,.,.,.,.,.,7.98,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,129.05,.,.,.,.,.,7.23,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,132.1,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
3,0,144.08,.,.,.,.,.,3.82,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
4,1,0,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,1,24,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,1,48,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,1,72,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,1,96,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,0,120,.,.,.,.,.,2.1,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,1,120,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
4,0,121,.,.,.,.,.,3.05,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,0,122.02,.,.,.,.,.,5.21,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,0,126,.,.,.,.,.,5.09,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,0,129.03,.,.,.,.,.,4.24,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,0,132,.,.,.,.,.,3.69,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
4,0,144.02,.,.,.,.,.,1.96,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
5,1,0,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,1,24,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,1,48,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,1,72,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,1,96,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,0,120,.,.,.,.,.,2.93,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,1,120,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
5,0,121,.,.,.,.,.,2.64,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,0,122,.,.,.,.,.,4.8,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,0,126,.,.,.,.,.,3.7,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,0,129.02,.,.,.,.,.,4.13,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,0,132,.,.,.,.,.,2.81,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
5,0,144,.,.,.,.,.,2.21,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
6,1,0,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,1,24,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,1,48,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,1,72,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,1,96,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,0,120,.,.,.,.,.,6.92,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,1,120,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
6,0,121,.,.,.,.,.,6.89,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,0,121.98,.,.,.,.,.,6.64,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,0,126,.,.,.,.,.,13.72,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,0,129,.,.,.,.,.,12.69,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,0,131.98,.,.,.,.,.,10.58,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
6,0,144.98,.,.,.,.,.,6.62,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
7,1,0,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,1,24,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,1,48,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,1,72,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,1,96,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,0,120,.,.,.,.,.,5.41,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,1,120,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
7,0,121.03,.,.,.,.,.,4.46,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,0,122.03,.,.,.,.,.,4.54,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,0,126.02,.,.,.,.,.,12.19,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,0,129.08,.,.,.,.,.,12.1,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,0,132.03,.,.,.,.,.,8.61,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
7,0,144.03,.,.,.,.,.,6.37,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
8,1,0,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,1,24,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,1,48,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,1,72,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,1,96,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,0,120,.,.,.,.,.,6.19,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,1,120,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
8,0,121.03,.,.,.,.,.,6.33,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,0,122,.,.,.,.,.,6.24,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,0,125.98,.,.,.,.,.,13.03,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,0,128.98,.,.,.,.,.,11.86,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,0,132,.,.,.,.,.,11.45,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
8,0,143.98,.,.,.,.,.,7.83,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
9,1,0,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,1,24,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,1,48,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,1,72,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,1,96,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,0,120,.,.,.,.,.,2.85,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,1,120,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
9,0,120.97,.,.,.,.,.,3.7,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,0,122,.,.,.,.,.,6.65,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,0,125.98,.,.,.,.,.,6.81,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,0,128.98,.,.,.,.,.,6.51,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,0,132,.,.,.,.,.,7.48,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
9,0,143.98,.,.,.,.,.,4.51,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
10,1,0,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,1,24,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,1,48,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,1,72,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,1,96,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,0,120,.,.,.,.,.,2.93,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,1,120,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
10,0,121,.,.,.,.,.,4.36,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,0,122.02,.,.,.,.,.,7.79,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,0,126,.,.,.,.,.,11.02,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,0,129,.,.,.,.,.,8.86,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,0,131.97,.,.,.,.,.,6.09,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
10,0,144,.,.,.,.,.,4.15,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
11,1,0,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,1,24,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,1,48,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,1,72,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,1,96,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,0,120,.,.,.,.,.,2.09,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,1,120,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
11,0,121.03,.,.,.,.,.,2.68,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,0,122,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,0,125.98,.,.,.,.,.,7.71,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,0,129,.,.,.,.,.,6.31,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,0,132,.,.,.,.,.,5.82,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
11,0,144.13,.,.,.,.,.,2.63,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
12,1,0,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,1,24,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,1,48,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,1,72,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,1,96,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,0,120,.,.,.,.,.,7.09,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,1,120,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
12,0,121.03,.,.,.,.,.,6.18,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,0,122.13,.,.,.,.,.,8.66,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,0,126,.,.,.,.,.,11.16,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,0,129,.,.,.,.,.,9.51,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,0,132,.,.,.,.,.,8.14,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
12,0,144,.,.,.,.,.,7.89,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
13,1,0,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,1,24,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,1,48,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,1,72,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,1,96,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,0,120,.,.,.,.,.,6.62,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,1,120,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
13,0,121,.,.,.,.,.,3.18,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,0,122,.,.,.,.,.,5.41,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,0,126,.,.,.,.,.,10.18,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,0,129.02,.,.,.,.,.,12.84,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,0,132,.,.,.,.,.,12.35,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
13,0,144,.,.,.,.,.,8.06,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
14,1,0,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,1,24,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,1,48,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,1,72,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,1,96,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,0,120,.,.,.,.,.,3.63,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,1,120,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
14,0,121,.,.,.,.,.,4.49,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,0,122,.,.,.,.,.,5.5,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,0,126,.,.,.,.,.,7.28,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,0,129,.,.,.,.,.,5.27,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,0,132,.,.,.,.,.,4.89,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
14,0,144,.,.,.,.,.,2.68,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
15,1,0,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,1,24,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,1,48,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,1,72,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,1,96,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,0,120,.,.,.,.,.,5.53,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,1,120,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
15,0,121,.,.,.,.,.,4.81,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,0,122,.,.,.,.,.,8.14,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,0,126,.,.,.,.,.,9.96,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,0,129,.,.,.,.,.,8.55,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,0,132.05,.,.,.,.,.,7.54,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
15,0,144.05,.,.,.,.,.,5.74,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
16,1,0,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,1,24,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,1,48,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,1,72,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,1,96,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,0,120,.,.,.,.,.,5.48,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,1,120,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
16,0,121,.,.,.,.,.,6.59,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,0,122,.,.,.,.,.,8.91,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,0,126,.,.,.,.,.,10.57,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,0,129,.,.,.,.,.,9.52,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,0,132,.,.,.,.,.,7.83,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
16,0,143.97,.,.,.,.,.,4.96,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
17,1,0,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,1,24,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,1,48,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,1,72,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,1,96,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,0,120,.,.,.,.,.,2.11,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,1,120,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
17,0,121.02,.,.,.,.,.,1.86,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,0,122.02,.,.,.,.,.,6.92,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,0,126,.,.,.,.,.,9.11,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,0,129,.,.,.,.,.,6.96,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,0,132,.,.,.,.,.,5.64,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
17,0,144.08,.,.,.,.,.,3.59,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
18,1,0,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,1,24,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,1,48,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,1,72,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,1,96,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,0,120,.,.,.,.,.,7.95,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,1,120,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
18,0,120.98,.,.,.,.,.,7.47,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,0,121.98,.,.,.,.,.,8.67,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,0,126,.,.,.,.,.,13.83,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,0,129.17,.,.,.,.,.,14.01,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,0,132.17,.,.,.,.,.,8.97,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
18,0,143.97,.,.,.,.,.,8.4,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
19,1,0,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,1,24,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,1,48,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,1,72,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,1,96,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,0,120,.,.,.,.,.,5.42,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,1,120,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
19,0,121,.,.,.,.,.,7.08,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,0,122,.,.,.,.,.,7.27,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,0,125.98,.,.,.,.,.,20.07,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,0,128.98,.,.,.,.,.,18.24,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,0,132,.,.,.,.,.,15.36,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
19,0,144,.,.,.,.,.,10.92,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
20,1,0,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,1,24,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,1,48,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,1,72,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,1,96,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,0,120,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
20,1,120,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
20,0,120.77,.,.,.,.,.,4.5,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
20,0,121.75,.,.,.,.,.,3.35,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
20,0,125.67,.,.,.,.,.,12.35,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
20,0,128.67,.,.,.,.,.,11.56,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
20,0,143.67,.,.,.,.,.,6.45,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
\ No newline at end of file
diff --git a/Examples/src/ex.csv b/Examples/src/ex.csv
new file mode 100644
index 000000000..392a45499
--- /dev/null
+++ b/Examples/src/ex.csv
@@ -0,0 +1,260 @@
+#ID,TIME,DOSE,OUT,WT,AFRICA,AGE,GENDER,HEIGHT
+1,0,600,.,46.7,1,21,1,160
+1,24,600,.,46.7,1,21,1,160
+1,48,600,.,46.7,1,21,1,160
+1,72,600,.,46.7,1,21,1,160
+1,96,600,.,46.7,1,21,1,160
+1,120,.,10.44,46.7,1,21,1,160
+1,120,600,.,46.7,1,21,1,160
+1,121,.,12.89,46.7,1,21,1,160
+1,122,.,14.98,46.7,1,21,1,160
+1,125.99,.,16.69,46.7,1,21,1,160
+1,129,.,20.15,46.7,1,21,1,160
+1,132,.,14.97,46.7,1,21,1,160
+1,143.98,.,12.57,46.7,1,21,1,160
+2,0,600,.,66.5,1,30,1,174
+2,24,600,.,66.5,1,30,1,174
+2,48,600,.,66.5,1,30,1,174
+2,72,600,.,66.5,1,30,1,174
+2,96,600,.,66.5,1,30,1,174
+2,120,.,3.56,66.5,1,30,1,174
+2,120,600,.,66.5,1,30,1,174
+2,120.98,.,5.84,66.5,1,30,1,174
+2,121.98,.,6.54,66.5,1,30,1,174
+2,126,.,6.14,66.5,1,30,1,174
+2,129.02,.,6.56,66.5,1,30,1,174
+2,132.02,.,4.44,66.5,1,30,1,174
+2,144,.,3.76,66.5,1,30,1,174
+3,0,600,.,46.7,1,24,0,164
+3,24,600,.,46.7,1,24,0,164
+3,48,600,.,46.7,1,24,0,164
+3,72,600,.,46.7,1,24,0,164
+3,96,600,.,46.7,1,24,0,164
+3,120,600,.,46.7,1,24,0,164
+3,120.08,.,4.06,46.7,1,24,0,164
+3,121.07,.,3.24,46.7,1,24,0,164
+3,122.08,.,3.09,46.7,1,24,0,164
+3,126.08,.,7.98,46.7,1,24,0,164
+3,129.05,.,7.23,46.7,1,24,0,164
+3,132.1,.,4.71,46.7,1,24,0,164
+3,144.08,.,3.82,46.7,1,24,0,164
+4,0,600,.,50.8,1,25,1,165
+4,24,600,.,50.8,1,25,1,165
+4,48,600,.,50.8,1,25,1,165
+4,72,600,.,50.8,1,25,1,165
+4,96,600,.,50.8,1,25,1,165
+4,120,.,2.1,50.8,1,25,1,165
+4,120,600,.,50.8,1,25,1,165
+4,121,.,3.05,50.8,1,25,1,165
+4,122.02,.,5.21,50.8,1,25,1,165
+4,126,.,5.09,50.8,1,25,1,165
+4,129.03,.,4.24,50.8,1,25,1,165
+4,132,.,3.69,50.8,1,25,1,165
+4,144.02,.,1.96,50.8,1,25,1,165
+5,0,600,.,65.8,1,22,1,181
+5,24,600,.,65.8,1,22,1,181
+5,48,600,.,65.8,1,22,1,181
+5,72,600,.,65.8,1,22,1,181
+5,96,600,.,65.8,1,22,1,181
+5,120,.,2.93,65.8,1,22,1,181
+5,120,600,.,65.8,1,22,1,181
+5,121,.,2.64,65.8,1,22,1,181
+5,122,.,4.8,65.8,1,22,1,181
+5,126,.,3.7,65.8,1,22,1,181
+5,129.02,.,4.13,65.8,1,22,1,181
+5,132,.,2.81,65.8,1,22,1,181
+5,144,.,2.21,65.8,1,22,1,181
+6,0,600,.,65,1,23,1,177
+6,24,600,.,65,1,23,1,177
+6,48,600,.,65,1,23,1,177
+6,72,600,.,65,1,23,1,177
+6,96,600,.,65,1,23,1,177
+6,120,.,6.92,65,1,23,1,177
+6,120,600,.,65,1,23,1,177
+6,121,.,6.89,65,1,23,1,177
+6,121.98,.,6.64,65,1,23,1,177
+6,126,.,13.72,65,1,23,1,177
+6,129,.,12.69,65,1,23,1,177
+6,131.98,.,10.58,65,1,23,1,177
+6,144.98,.,6.62,65,1,23,1,177
+7,0,600,.,51.7,1,27,0,161
+7,24,600,.,51.7,1,27,0,161
+7,48,600,.,51.7,1,27,0,161
+7,72,600,.,51.7,1,27,0,161
+7,96,600,.,51.7,1,27,0,161
+7,120,.,5.41,51.7,1,27,0,161
+7,120,600,.,51.7,1,27,0,161
+7,121.03,.,4.46,51.7,1,27,0,161
+7,122.03,.,4.54,51.7,1,27,0,161
+7,126.02,.,12.19,51.7,1,27,0,161
+7,129.08,.,12.1,51.7,1,27,0,161
+7,132.03,.,8.61,51.7,1,27,0,161
+7,144.03,.,6.37,51.7,1,27,0,161
+8,0,600,.,51.2,1,22,1,163
+8,24,600,.,51.2,1,22,1,163
+8,48,600,.,51.2,1,22,1,163
+8,72,600,.,51.2,1,22,1,163
+8,96,600,.,51.2,1,22,1,163
+8,120,.,6.19,51.2,1,22,1,163
+8,120,600,.,51.2,1,22,1,163
+8,121.03,.,6.33,51.2,1,22,1,163
+8,122,.,6.24,51.2,1,22,1,163
+8,125.98,.,13.03,51.2,1,22,1,163
+8,128.98,.,11.86,51.2,1,22,1,163
+8,132,.,11.45,51.2,1,22,1,163
+8,143.98,.,7.83,51.2,1,22,1,163
+9,0,600,.,55,1,23,1,174
+9,24,600,.,55,1,23,1,174
+9,48,600,.,55,1,23,1,174
+9,72,600,.,55,1,23,1,174
+9,96,600,.,55,1,23,1,174
+9,120,.,2.85,55,1,23,1,174
+9,120,600,.,55,1,23,1,174
+9,120.97,.,3.7,55,1,23,1,174
+9,122,.,6.65,55,1,23,1,174
+9,125.98,.,6.81,55,1,23,1,174
+9,128.98,.,6.51,55,1,23,1,174
+9,132,.,7.48,55,1,23,1,174
+9,143.98,.,4.51,55,1,23,1,174
+10,0,600,.,52.1,1,32,1,163
+10,24,600,.,52.1,1,32,1,163
+10,48,600,.,52.1,1,32,1,163
+10,72,600,.,52.1,1,32,1,163
+10,96,600,.,52.1,1,32,1,163
+10,120,.,2.93,52.1,1,32,1,163
+10,120,600,.,52.1,1,32,1,163
+10,121,.,4.36,52.1,1,32,1,163
+10,122.02,.,7.79,52.1,1,32,1,163
+10,126,.,11.02,52.1,1,32,1,163
+10,129,.,8.86,52.1,1,32,1,163
+10,131.97,.,6.09,52.1,1,32,1,163
+10,144,.,4.15,52.1,1,32,1,163
+11,0,600,.,56.5,1,34,1,165
+11,24,600,.,56.5,1,34,1,165
+11,48,600,.,56.5,1,34,1,165
+11,72,600,.,56.5,1,34,1,165
+11,96,600,.,56.5,1,34,1,165
+11,120,.,2.09,56.5,1,34,1,165
+11,120,600,.,56.5,1,34,1,165
+11,121.03,.,2.68,56.5,1,34,1,165
+11,122,.,4.71,56.5,1,34,1,165
+11,125.98,.,7.71,56.5,1,34,1,165
+11,129,.,6.31,56.5,1,34,1,165
+11,132,.,5.82,56.5,1,34,1,165
+11,144.13,.,2.63,56.5,1,34,1,165
+12,0,600,.,47.9,1,54,0,160
+12,24,600,.,47.9,1,54,0,160
+12,48,600,.,47.9,1,54,0,160
+12,72,600,.,47.9,1,54,0,160
+12,96,600,.,47.9,1,54,0,160
+12,120,.,7.09,47.9,1,54,0,160
+12,120,600,.,47.9,1,54,0,160
+12,121.03,.,6.18,47.9,1,54,0,160
+12,122.13,.,8.66,47.9,1,54,0,160
+12,126,.,11.16,47.9,1,54,0,160
+12,129,.,9.51,47.9,1,54,0,160
+12,132,.,8.14,47.9,1,54,0,160
+12,144,.,7.89,47.9,1,54,0,160
+13,0,600,.,60.5,1,24,1,180
+13,24,600,.,60.5,1,24,1,180
+13,48,600,.,60.5,1,24,1,180
+13,72,600,.,60.5,1,24,1,180
+13,96,600,.,60.5,1,24,1,180
+13,120,.,6.62,60.5,1,24,1,180
+13,120,600,.,60.5,1,24,1,180
+13,121,.,3.18,60.5,1,24,1,180
+13,122,.,5.41,60.5,1,24,1,180
+13,126,.,10.18,60.5,1,24,1,180
+13,129.02,.,12.84,60.5,1,24,1,180
+13,132,.,12.35,60.5,1,24,1,180
+13,144,.,8.06,60.5,1,24,1,180
+14,0,600,.,59.2,1,26,1,174
+14,24,600,.,59.2,1,26,1,174
+14,48,600,.,59.2,1,26,1,174
+14,72,600,.,59.2,1,26,1,174
+14,96,600,.,59.2,1,26,1,174
+14,120,.,3.63,59.2,1,26,1,174
+14,120,600,.,59.2,1,26,1,174
+14,121,.,4.49,59.2,1,26,1,174
+14,122,.,5.5,59.2,1,26,1,174
+14,126,.,7.28,59.2,1,26,1,174
+14,129,.,5.27,59.2,1,26,1,174
+14,132,.,4.89,59.2,1,26,1,174
+14,144,.,2.68,59.2,1,26,1,174
+15,0,450,.,43,1,19,0,150
+15,24,450,.,43,1,19,0,150
+15,48,450,.,43,1,19,0,150
+15,72,450,.,43,1,19,0,150
+15,96,450,.,43,1,19,0,150
+15,120,.,5.53,43,1,19,0,150
+15,120,450,.,43,1,19,0,150
+15,121,.,4.81,43,1,19,0,150
+15,122,.,8.14,43,1,19,0,150
+15,126,.,9.96,43,1,19,0,150
+15,129,.,8.55,43,1,19,0,150
+15,132.05,.,7.54,43,1,19,0,150
+15,144.05,.,5.74,43,1,19,0,150
+16,0,600,.,64.4,1,25,1,173
+16,24,600,.,64.4,1,25,1,173
+16,48,600,.,64.4,1,25,1,173
+16,72,600,.,64.4,1,25,1,173
+16,96,600,.,64.4,1,25,1,173
+16,120,.,5.48,64.4,1,25,1,173
+16,120,600,.,64.4,1,25,1,173
+16,121,.,6.59,64.4,1,25,1,173
+16,122,.,8.91,64.4,1,25,1,173
+16,126,.,10.57,64.4,1,25,1,173
+16,129,.,9.52,64.4,1,25,1,173
+16,132,.,7.83,64.4,1,25,1,173
+16,143.97,.,4.96,64.4,1,25,1,173
+17,0,600,.,54.8,1,23,1,170
+17,24,600,.,54.8,1,23,1,170
+17,48,600,.,54.8,1,23,1,170
+17,72,600,.,54.8,1,23,1,170
+17,96,600,.,54.8,1,23,1,170
+17,120,.,2.11,54.8,1,23,1,170
+17,120,600,.,54.8,1,23,1,170
+17,121.02,.,1.86,54.8,1,23,1,170
+17,122.02,.,6.92,54.8,1,23,1,170
+17,126,.,9.11,54.8,1,23,1,170
+17,129,.,6.96,54.8,1,23,1,170
+17,132,.,5.64,54.8,1,23,1,170
+17,144.08,.,3.59,54.8,1,23,1,170
+18,0,450,.,44.3,1,20,0,164
+18,24,450,.,44.3,1,20,0,164
+18,48,450,.,44.3,1,20,0,164
+18,72,450,.,44.3,1,20,0,164
+18,96,450,.,44.3,1,20,0,164
+18,120,.,7.95,44.3,1,20,0,164
+18,120,450,.,44.3,1,20,0,164
+18,120.98,.,7.47,44.3,1,20,0,164
+18,121.98,.,8.67,44.3,1,20,0,164
+18,126,.,13.83,44.3,1,20,0,164
+18,129.17,.,14.01,44.3,1,20,0,164
+18,132.17,.,8.97,44.3,1,20,0,164
+18,143.97,.,8.4,44.3,1,20,0,164
+19,0,600,.,50,1,36,1,168
+19,24,600,.,50,1,36,1,168
+19,48,600,.,50,1,36,1,168
+19,72,600,.,50,1,36,1,168
+19,96,600,.,50,1,36,1,168
+19,120,.,5.42,50,1,36,1,168
+19,120,600,.,50,1,36,1,168
+19,121,.,7.08,50,1,36,1,168
+19,122,.,7.27,50,1,36,1,168
+19,125.98,.,20.07,50,1,36,1,168
+19,128.98,.,18.24,50,1,36,1,168
+19,132,.,15.36,50,1,36,1,168
+19,144,.,10.92,50,1,36,1,168
+20,0,600,.,59,1,31,1,170
+20,24,600,.,59,1,31,1,170
+20,48,600,.,59,1,31,1,170
+20,72,600,.,59,1,31,1,170
+20,96,600,.,59,1,31,1,170
+20,120,.,4.71,59,1,31,1,170
+20,120,600,.,59,1,31,1,170
+20,120.77,.,4.5,59,1,31,1,170
+20,121.75,.,3.35,59,1,31,1,170
+20,125.67,.,12.35,59,1,31,1,170
+20,128.67,.,11.56,59,1,31,1,170
+20,143.67,.,6.45,59,1,31,1,170
\ No newline at end of file
diff --git a/Examples/src/ex_full.csv b/Examples/src/ex_full.csv
new file mode 100644
index 000000000..0f3b8679c
--- /dev/null
+++ b/Examples/src/ex_full.csv
@@ -0,0 +1,261 @@
+POPDATA DEC_11,,,,,,,,,,,,,,,,,,
+#ID,EVID,TIME,DUR,DOSE,ADDL,II,INPUT,OUT,OUTEQ,C0,C1,C2,C3,WT,AFRICA,AGE,GENDER,HEIGHT
+1,1,0,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,1,24,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,1,48,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,1,72,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,1,96,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,0,120,.,.,.,.,.,10.44,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,1,120,0,600,.,.,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,0,121,.,.,.,.,.,12.89,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,0,122,.,.,.,.,.,14.98,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,0,125.99,.,.,.,.,.,16.69,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,0,129,.,.,.,.,.,20.15,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,0,132,.,.,.,.,.,14.97,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+1,0,143.98,.,.,.,.,.,12.57,1,0.02,0.0506,-0.0002,0,46.7,1,21,1,160
+2,1,0,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,1,24,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,1,48,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,1,72,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,1,96,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,0,120,.,.,.,.,.,3.56,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,1,120,0,600,.,.,1,.,.,.,.,.,.,66.5,1,30,1,174
+2,0,120.98,.,.,.,.,.,5.84,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,0,121.98,.,.,.,.,.,6.54,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,0,126,.,.,.,.,.,6.14,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,0,129.02,.,.,.,.,.,6.56,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,0,132.02,.,.,.,.,.,4.44,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+2,0,144,.,.,.,.,.,3.76,1,0.02,0.0506,-0.0002,0,66.5,1,30,1,174
+3,1,0,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,1,24,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,1,48,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,1,72,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,1,96,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,1,120,0,600,.,.,1,.,.,.,.,.,.,46.7,1,24,0,164
+3,0,120.08,.,.,.,.,.,4.06,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,121.07,.,.,.,.,.,3.24,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,122.08,.,.,.,.,.,3.09,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,126.08,.,.,.,.,.,7.98,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,129.05,.,.,.,.,.,7.23,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,132.1,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+3,0,144.08,.,.,.,.,.,3.82,1,0.02,0.0506,-0.0002,0,46.7,1,24,0,164
+4,1,0,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,1,24,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,1,48,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,1,72,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,1,96,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,0,120,.,.,.,.,.,2.1,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,1,120,0,600,.,.,1,.,.,.,.,.,.,50.8,1,25,1,165
+4,0,121,.,.,.,.,.,3.05,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,0,122.02,.,.,.,.,.,5.21,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,0,126,.,.,.,.,.,5.09,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,0,129.03,.,.,.,.,.,4.24,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,0,132,.,.,.,.,.,3.69,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+4,0,144.02,.,.,.,.,.,1.96,1,0.02,0.0506,-0.0002,0,50.8,1,25,1,165
+5,1,0,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,1,24,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,1,48,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,1,72,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,1,96,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,0,120,.,.,.,.,.,2.93,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,1,120,0,600,.,.,1,.,.,.,.,.,.,65.8,1,22,1,181
+5,0,121,.,.,.,.,.,2.64,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,0,122,.,.,.,.,.,4.8,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,0,126,.,.,.,.,.,3.7,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,0,129.02,.,.,.,.,.,4.13,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,0,132,.,.,.,.,.,2.81,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+5,0,144,.,.,.,.,.,2.21,1,0.02,0.0506,-0.0002,0,65.8,1,22,1,181
+6,1,0,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,1,24,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,1,48,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,1,72,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,1,96,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,0,120,.,.,.,.,.,6.92,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,1,120,0,600,.,.,1,.,.,.,.,.,.,65,1,23,1,177
+6,0,121,.,.,.,.,.,6.89,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,0,121.98,.,.,.,.,.,6.64,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,0,126,.,.,.,.,.,13.72,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,0,129,.,.,.,.,.,12.69,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,0,131.98,.,.,.,.,.,10.58,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+6,0,144.98,.,.,.,.,.,6.62,1,0.02,0.0506,-0.0002,0,65,1,23,1,177
+7,1,0,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,1,24,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,1,48,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,1,72,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,1,96,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,0,120,.,.,.,.,.,5.41,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,1,120,0,600,.,.,1,.,.,.,.,.,.,51.7,1,27,0,161
+7,0,121.03,.,.,.,.,.,4.46,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,0,122.03,.,.,.,.,.,4.54,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,0,126.02,.,.,.,.,.,12.19,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,0,129.08,.,.,.,.,.,12.1,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,0,132.03,.,.,.,.,.,8.61,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+7,0,144.03,.,.,.,.,.,6.37,1,0.02,0.0506,-0.0002,0,51.7,1,27,0,161
+8,1,0,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,1,24,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,1,48,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,1,72,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,1,96,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,0,120,.,.,.,.,.,6.19,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,1,120,0,600,.,.,1,.,.,.,.,.,.,51.2,1,22,1,163
+8,0,121.03,.,.,.,.,.,6.33,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,0,122,.,.,.,.,.,6.24,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,0,125.98,.,.,.,.,.,13.03,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,0,128.98,.,.,.,.,.,11.86,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,0,132,.,.,.,.,.,11.45,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+8,0,143.98,.,.,.,.,.,7.83,1,0.02,0.0506,-0.0002,0,51.2,1,22,1,163
+9,1,0,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,1,24,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,1,48,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,1,72,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,1,96,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,0,120,.,.,.,.,.,2.85,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,1,120,0,600,.,.,1,.,.,.,.,.,.,55,1,23,1,174
+9,0,120.97,.,.,.,.,.,3.7,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,0,122,.,.,.,.,.,6.65,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,0,125.98,.,.,.,.,.,6.81,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,0,128.98,.,.,.,.,.,6.51,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,0,132,.,.,.,.,.,7.48,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+9,0,143.98,.,.,.,.,.,4.51,1,0.02,0.0506,-0.0002,0,55,1,23,1,174
+10,1,0,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,1,24,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,1,48,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,1,72,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,1,96,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,0,120,.,.,.,.,.,2.93,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,1,120,0,600,.,.,1,.,.,.,.,.,.,52.1,1,32,1,163
+10,0,121,.,.,.,.,.,4.36,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,0,122.02,.,.,.,.,.,7.79,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,0,126,.,.,.,.,.,11.02,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,0,129,.,.,.,.,.,8.86,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,0,131.97,.,.,.,.,.,6.09,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+10,0,144,.,.,.,.,.,4.15,1,0.02,0.0506,-0.0002,0,52.1,1,32,1,163
+11,1,0,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,1,24,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,1,48,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,1,72,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,1,96,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,0,120,.,.,.,.,.,2.09,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,1,120,0,600,.,.,1,.,.,.,.,.,.,56.5,1,34,1,165
+11,0,121.03,.,.,.,.,.,2.68,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,0,122,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,0,125.98,.,.,.,.,.,7.71,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,0,129,.,.,.,.,.,6.31,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,0,132,.,.,.,.,.,5.82,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+11,0,144.13,.,.,.,.,.,2.63,1,0.02,0.0506,-0.0002,0,56.5,1,34,1,165
+12,1,0,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,1,24,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,1,48,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,1,72,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,1,96,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,0,120,.,.,.,.,.,7.09,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,1,120,0,600,.,.,1,.,.,.,.,.,.,47.9,1,54,0,160
+12,0,121.03,.,.,.,.,.,6.18,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,0,122.13,.,.,.,.,.,8.66,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,0,126,.,.,.,.,.,11.16,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,0,129,.,.,.,.,.,9.51,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,0,132,.,.,.,.,.,8.14,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+12,0,144,.,.,.,.,.,7.89,1,0.02,0.0506,-0.0002,0,47.9,1,54,0,160
+13,1,0,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,1,24,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,1,48,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,1,72,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,1,96,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,0,120,.,.,.,.,.,6.62,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,1,120,0,600,.,.,1,.,.,.,.,.,.,60.5,1,24,1,180
+13,0,121,.,.,.,.,.,3.18,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,0,122,.,.,.,.,.,5.41,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,0,126,.,.,.,.,.,10.18,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,0,129.02,.,.,.,.,.,12.84,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,0,132,.,.,.,.,.,12.35,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+13,0,144,.,.,.,.,.,8.06,1,0.02,0.0506,-0.0002,0,60.5,1,24,1,180
+14,1,0,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,1,24,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,1,48,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,1,72,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,1,96,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,0,120,.,.,.,.,.,3.63,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,1,120,0,600,.,.,1,.,.,.,.,.,.,59.2,1,26,1,174
+14,0,121,.,.,.,.,.,4.49,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,0,122,.,.,.,.,.,5.5,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,0,126,.,.,.,.,.,7.28,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,0,129,.,.,.,.,.,5.27,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,0,132,.,.,.,.,.,4.89,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+14,0,144,.,.,.,.,.,2.68,1,0.02,0.0506,-0.0002,0,59.2,1,26,1,174
+15,1,0,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,1,24,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,1,48,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,1,72,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,1,96,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,0,120,.,.,.,.,.,5.53,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,1,120,0,450,.,.,1,.,.,.,.,.,.,43,1,19,0,150
+15,0,121,.,.,.,.,.,4.81,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,0,122,.,.,.,.,.,8.14,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,0,126,.,.,.,.,.,9.96,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,0,129,.,.,.,.,.,8.55,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,0,132.05,.,.,.,.,.,7.54,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+15,0,144.05,.,.,.,.,.,5.74,1,0.02,0.0506,-0.0002,0,43,1,19,0,150
+16,1,0,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,1,24,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,1,48,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,1,72,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,1,96,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,0,120,.,.,.,.,.,5.48,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,1,120,0,600,.,.,1,.,.,.,.,.,.,64.4,1,25,1,173
+16,0,121,.,.,.,.,.,6.59,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,0,122,.,.,.,.,.,8.91,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,0,126,.,.,.,.,.,10.57,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,0,129,.,.,.,.,.,9.52,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,0,132,.,.,.,.,.,7.83,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+16,0,143.97,.,.,.,.,.,4.96,1,0.02,0.0506,-0.0002,0,64.4,1,25,1,173
+17,1,0,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,1,24,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,1,48,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,1,72,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,1,96,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,0,120,.,.,.,.,.,2.11,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,1,120,0,600,.,.,1,.,.,.,.,.,.,54.8,1,23,1,170
+17,0,121.02,.,.,.,.,.,1.86,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,0,122.02,.,.,.,.,.,6.92,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,0,126,.,.,.,.,.,9.11,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,0,129,.,.,.,.,.,6.96,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,0,132,.,.,.,.,.,5.64,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+17,0,144.08,.,.,.,.,.,3.59,1,0.02,0.0506,-0.0002,0,54.8,1,23,1,170
+18,1,0,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,1,24,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,1,48,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,1,72,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,1,96,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,0,120,.,.,.,.,.,7.95,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,1,120,0,450,.,.,1,.,.,.,.,.,.,44.3,1,20,0,164
+18,0,120.98,.,.,.,.,.,7.47,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,0,121.98,.,.,.,.,.,8.67,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,0,126,.,.,.,.,.,13.83,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,0,129.17,.,.,.,.,.,14.01,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,0,132.17,.,.,.,.,.,8.97,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+18,0,143.97,.,.,.,.,.,8.4,1,0.02,0.0506,-0.0002,0,44.3,1,20,0,164
+19,1,0,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,1,24,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,1,48,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,1,72,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,1,96,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,0,120,.,.,.,.,.,5.42,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,1,120,0,600,.,.,1,.,.,.,.,.,.,50,1,36,1,168
+19,0,121,.,.,.,.,.,7.08,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,0,122,.,.,.,.,.,7.27,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,0,125.98,.,.,.,.,.,20.07,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,0,128.98,.,.,.,.,.,18.24,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,0,132,.,.,.,.,.,15.36,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+19,0,144,.,.,.,.,.,10.92,1,0.02,0.0506,-0.0002,0,50,1,36,1,168
+20,1,0,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,1,24,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,1,48,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,1,72,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,1,96,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,0,120,.,.,.,.,.,4.71,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
+20,1,120,0,600,.,.,1,.,.,.,.,.,.,59,1,31,1,170
+20,0,120.77,.,.,.,.,.,4.5,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
+20,0,121.75,.,.,.,.,.,3.35,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
+20,0,125.67,.,.,.,.,.,12.35,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
+20,0,128.67,.,.,.,.,.,11.56,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
+20,0,143.67,.,.,.,.,.,6.45,1,0.02,0.0506,-0.0002,0,59,1,31,1,170
\ No newline at end of file
diff --git a/Examples/src/ptaex1.csv b/Examples/src/ptaex1.csv
new file mode 100644
index 000000000..93a207c9a
--- /dev/null
+++ b/Examples/src/ptaex1.csv
@@ -0,0 +1,10 @@
+POPDATA DEC_11,,,,,,,,,,,,,,,,,,
+#ID,EVID,TIME,DUR,DOSE,ADDL,II,INPUT,OUT,OUTEQ,C0,C1,C2,C3,WT,AFRICA,AGE,GENDER,HEIGHT
+1,1,0,0,600,5,24,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,0,144,.,.,.,.,.,-1,1,0.02,0.0506,-2.00E-04,0,46.7,1,21,1,160
+2,1,0,0,1200,5,24,1,.,.,.,.,.,.,46.7,1,21,1,160
+2,0,144,.,.,.,.,.,-1,1,0.02,0.0506,-2.00E-04,0,46.7,1,21,1,160
+3,1,0,0,300,11,12,1,.,.,.,.,.,.,46.7,1,21,1,160
+3,0,144,.,.,.,.,.,-1,1,0.02,0.0506,-2.00E-04,0,46.7,1,21,1,160
+4,1,0,0,600,11,12,1,.,.,.,.,.,.,46.7,1,21,1,160
+4,0,144,.,.,.,.,.,-1,1,0.02,0.0506,-2.00E-04,0,46.7,1,21,1,160
\ No newline at end of file
diff --git a/Examples/src/simTemp.csv b/Examples/src/simTemp.csv
new file mode 100644
index 000000000..a4d379b8a
--- /dev/null
+++ b/Examples/src/simTemp.csv
@@ -0,0 +1,10 @@
+POPDATA DEC_11,,,,,,,,,,,,,,,,,,
+#ID,EVID,TIME,DUR,DOSE,ADDL,II,INPUT,OUT,OUTEQ,C0,C1,C2,C3,WT,AFRICA,AGE,GENDER,HEIGHT
+1,1,0,0,500,5,24,1,.,.,.,.,.,.,46.7,1,21,1,160
+1,0,144,.,.,.,.,.,-1,1,.,.,.,.,.,.,.,.,.
+2,1,0,0,1000,5,24,1,.,.,.,.,.,.,46.7,1,21,1,160
+2,0,144,.,.,.,.,.,-1,1,.,.,.,.,.,.,.,.,.
+3,1,0,0,250,11,12,1,.,.,.,.,.,.,46.7,1,21,1,160
+3,0,144,.,.,.,.,.,-1,1,.,.,.,.,.,.,.,.,.
+4,1,0,0,500,11,12,1,.,.,.,.,.,.,46.7,1,21,1,160
+4,0,144,.,.,.,.,.,-1,1,.,.,.,.,.,.,.,.,.
\ No newline at end of file
diff --git a/NAMESPACE b/NAMESPACE
index 07e9b8d43..63feb0401 100755
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -47,6 +47,8 @@ export(NPreport)
export(NPrun)
export(PMFortranConfig)
export(PM_batch)
+export(PM_bestdose)
+export(PM_bestdose_problem)
export(PM_build)
export(PM_compare)
export(PM_cov)
@@ -94,6 +96,7 @@ export(add_renal)
export(add_shapes)
export(add_smooth)
export(additive)
+export(bestdose)
export(build_model)
export(build_plot)
export(cli_ask)
diff --git a/R/PM_bestdose.R b/R/PM_bestdose.R
new file mode 100644
index 000000000..684c04e02
--- /dev/null
+++ b/R/PM_bestdose.R
@@ -0,0 +1,381 @@
+bestdose_parse_prior <- function(prior) {
+ if (inherits(prior, "PM_result")) {
+ theta_path <- file.path(prior$rundir, "outputs", "theta.csv")
+ if (!file.exists(theta_path)) {
+ cli::cli_abort("theta.csv not found in PM_result outputs")
+ }
+ theta_path
+ } else if (inherits(prior, "PM_final")) {
+ temp_path <- tempfile(fileext = ".csv")
+ bestdose_write_prior_csv(prior, temp_path)
+ temp_path
+ } else if (is.character(prior)) {
+ if (!file.exists(prior)) {
+ cli::cli_abort("Prior file not found: {prior}")
+ }
+ prior
+ } else {
+ cli::cli_abort("prior must be PM_result, PM_final, or path to theta.csv")
+ }
+}
+
+bestdose_write_prior_csv <- function(prior, path) {
+ df <- as.data.frame(prior$popPoints)
+ df$prob <- prior$popProb
+ write.csv(df, path, row.names = FALSE, quote = FALSE)
+}
+
+bestdose_parse_model <- function(model) {
+ if (inherits(model, "PM_model")) {
+ compiled_path <- model$binary_path
+ if (is.null(compiled_path) || !file.exists(compiled_path)) {
+ cli::cli_abort("Model must be compiled first. Use model$compile()")
+ }
+
+ kind <- if (!is.null(model$model_list$analytical) && model$model_list$analytical) {
+ "analytical"
+ } else {
+ "ode"
+ }
+
+ list(path = compiled_path, kind = kind, model = model)
+ } else if (is.character(model)) {
+ if (!file.exists(model)) {
+ cli::cli_abort("Model file not found: {model}")
+ }
+ kind <- if (grepl("analytical", model, ignore.case = TRUE)) {
+ "analytical"
+ } else {
+ "ode"
+ }
+ list(path = model, kind = kind, model = NULL)
+ } else {
+ cli::cli_abort("model must be PM_model or path to compiled model")
+ }
+}
+
+bestdose_parse_data <- function(data) {
+ if (inherits(data, "PM_data")) {
+ temp_path <- tempfile(fileext = ".csv")
+ write.csv(data$standard_data, temp_path, row.names = FALSE, quote = FALSE)
+ temp_path
+ } else if (is.character(data)) {
+ if (!file.exists(data)) {
+ cli::cli_abort("Data file not found: {data}")
+ }
+ data
+ } else {
+ cli::cli_abort("data must be PM_data or path to CSV file")
+ }
+}
+
+bestdose_default_settings <- function(prior, model) {
+ if (inherits(prior, "PM_result")) {
+ return(prior$settings)
+ }
+
+ param_ranges <- lapply(model$model_list$pri, function(x) {
+ c(x$min, x$max)
+ })
+ names(param_ranges) <- tolower(names(param_ranges))
+
+ list(
+ algorithm = "NPAG",
+ ranges = param_ranges,
+ error_models = list(
+ list(
+ initial = 0.0,
+ type = "additive",
+ coeff = c(0.0, 0.2, 0.0, 0.0)
+ )
+ ),
+ max_cycles = 500,
+ points = 2028,
+ seed = 22,
+ prior = "prior.csv",
+ idelta = 0.25,
+ tad = 0.0
+ )
+}
+
+#' @title
+#' Object to contain BestDose optimization results
+#'
+#' @description
+#' `r lifecycle::badge("experimental")`
+#'
+#' This object is created after a successful BestDose optimization run.
+#' BestDose finds optimal dosing regimens to achieve target drug concentrations
+#' or AUC values using Bayesian optimization.
+#'
+#' @export
+PM_bestdose <- R6::R6Class(
+ "PM_bestdose",
+ public = list(
+ result = NULL,
+ problem = NULL,
+ bias_weight = NULL,
+ initialize = function(prior = NULL,
+ model = NULL,
+ past_data = NULL,
+ target = NULL,
+ dose_range = list(min = 0, max = 1000),
+ bias_weight = 0.5,
+ target_type = "concentration",
+ time_offset = NULL,
+ settings = NULL,
+ result = NULL,
+ problem_obj = NULL,
+ bias_override = NULL) {
+ if (!is.null(result)) {
+ private$.set_result(result, problem_obj, bias_override)
+ return(invisible(self))
+ }
+
+ if (is.null(target)) {
+ cli::cli_abort("target must be supplied when computing a new BestDose result")
+ }
+
+ problem <- PM_bestdose_problem$new(
+ prior = prior,
+ model = model,
+ past_data = past_data,
+ target = target,
+ dose_range = dose_range,
+ bias_weight = bias_weight,
+ target_type = target_type,
+ time_offset = time_offset,
+ settings = settings
+ )
+
+ raw <- problem$optimize_raw(bias_weight = bias_weight)
+ private$.set_result(raw$result, problem, raw$bias_weight)
+ invisible(self)
+ },
+
+ #' @description
+ #' Print summary of BestDose results
+ print = function() {
+ cat("BestDose Optimization Results\n")
+ cat("==============================\n\n")
+ cat(sprintf("Optimal doses: [%.2f, %.2f] mg\n", self$get_doses()[1], self$get_doses()[2]))
+ cat(sprintf("Objective function: %.10f\n", self$get_objf()))
+ cat(sprintf("ln(Objective): %.4f\n", log(self$get_objf())))
+ cat(sprintf("Method: %s\n", self$get_method()))
+ cat(sprintf("Status: %s\n", self$get_status()))
+ if (!is.null(self$bias_weight)) {
+ cat(sprintf("Bias weight (lambda): %.2f\n", self$bias_weight))
+ }
+ cat(sprintf("\nNumber of predictions: %d\n", nrow(self$result$predictions)))
+ if (!is.null(self$result$auc_predictions)) {
+ cat(sprintf("Number of AUC predictions: %d\n", nrow(self$result$auc_predictions)))
+ }
+ invisible(self)
+ },
+
+ #' @description
+ #' Get optimal dose values
+ #' @return Numeric vector of optimal doses
+ get_doses = function() {
+ self$result$doses
+ },
+
+ #' @description
+ #' Get concentration-time predictions
+ #' @return Data frame with predictions
+ get_predictions = function() {
+ self$result$predictions
+ },
+
+ #' @description
+ #' Get AUC predictions (if available)
+ #' @return Data frame with AUC predictions or NULL
+ get_auc_predictions = function() {
+ self$result$auc_predictions
+ },
+
+ #' @description
+ #' Get objective function value
+ #' @return Numeric objective function value
+ get_objf = function() {
+ self$result$objf
+ },
+
+ #' @description
+ #' Get optimization status
+ #' @return Character string with status
+ get_status = function() {
+ self$result$status
+ },
+
+ #' @description
+ #' Get optimization method used
+ #' @return Character string: "posterior" or "uniform"
+ get_method = function() {
+ self$result$method
+ },
+
+ #' @description
+ #' Save results to RDS file
+ #' @param filename Path to save file. Default: "bestdose_result.rds"
+ save = function(filename = "bestdose_result.rds") {
+ saveRDS(self, filename)
+ cli::cli_alert_success("Results saved to {filename}")
+ invisible(self)
+ }
+ ),
+ private = list(
+ .set_result = function(result, problem, bias_weight) {
+ self$result <- result
+ self$problem <- problem
+ self$bias_weight <- bias_weight
+ }
+ )
+)
+
+#' @title
+#' Prepare a reusable BestDose optimization problem
+#'
+#' @description
+#' `r lifecycle::badge("experimental")`
+#'
+#' Use `PM_bestdose_problem` to mirror the Rust workflow: compute posterior
+#' support points once, inspect them in R, and solve for multiple bias weights
+#' without repeating the expensive initialization step.
+#'
+#' @export
+PM_bestdose_problem <- R6::R6Class(
+ "PM_bestdose_problem",
+ public = list(
+ handle = NULL,
+ theta = NULL,
+ theta_dim = NULL,
+ param_names = NULL,
+ posterior_weights = NULL,
+ population_weights = NULL,
+ bias_weight = NULL,
+ target_type = NULL,
+ dose_range = NULL,
+ model_info = NULL,
+ settings = NULL,
+ initialize = function(prior,
+ model,
+ past_data = NULL,
+ target,
+ dose_range = list(min = 0, max = 1000),
+ bias_weight = 0.5,
+ target_type = "concentration",
+ time_offset = NULL,
+ settings = NULL) {
+ if (!target_type %in% c("concentration", "auc_from_zero", "auc_from_last_dose")) {
+ cli::cli_abort("target_type must be one of: concentration, auc_from_zero, auc_from_last_dose")
+ }
+
+ if (bias_weight < 0 || bias_weight > 1) {
+ cli::cli_abort("bias_weight must be between 0 and 1")
+ }
+
+ if (is.null(dose_range$min) || is.null(dose_range$max)) {
+ cli::cli_abort("dose_range must have both 'min' and 'max' elements")
+ }
+
+ if (dose_range$min >= dose_range$max) {
+ cli::cli_abort("dose_range$min must be less than dose_range$max")
+ }
+
+ prior_path <- bestdose_parse_prior(prior)
+ model_info <- bestdose_parse_model(model)
+ past_data_path <- if (!is.null(past_data)) bestdose_parse_data(past_data) else NULL
+ target_data_path <- bestdose_parse_data(target)
+
+ if (is.null(settings)) {
+ model_for_settings <- if (!is.null(model_info$model)) model_info$model else model
+ settings <- bestdose_default_settings(prior, model_for_settings)
+ }
+
+ prep <- bestdose_prepare(
+ model_path = model_info$path,
+ prior_path = prior_path,
+ past_data_path = past_data_path,
+ target_data_path = target_data_path,
+ time_offset = time_offset,
+ dose_min = dose_range$min,
+ dose_max = dose_range$max,
+ bias_weight = bias_weight,
+ target_type = target_type,
+ params = settings,
+ kind = model_info$kind
+ )
+
+ if (is.character(prep)) {
+ cli::cli_abort(prep)
+ }
+
+ dim <- as.integer(prep$theta_dim)
+ theta_matrix <- matrix(prep$theta_values, nrow = dim[1], ncol = dim[2])
+ colnames(theta_matrix) <- prep$param_names
+
+ self$handle <- prep$handle
+ self$theta <- theta_matrix
+ self$theta_dim <- dim
+ self$param_names <- prep$param_names
+ self$posterior_weights <- prep$posterior_weights
+ self$population_weights <- prep$population_weights
+ self$bias_weight <- prep$bias_weight
+ self$target_type <- prep$target_type
+ self$dose_range <- dose_range
+ self$model_info <- model_info
+ self$settings <- settings
+
+ cli::cli_alert_success("BestDose problem prepared with %d support points", dim[1])
+ },
+ finalize = function() {
+ self$handle <- NULL
+ },
+
+ #' @description
+ #' Run optimization and return raw list (doses, objf, predictions)
+ optimize_raw = function(bias_weight = NULL) {
+ private$.run_optimize(bias_weight)
+ },
+
+ #' @description
+ #' Run optimization and return a `PM_bestdose` result object
+ optimize = function(bias_weight = NULL) {
+ raw <- self$optimize_raw(bias_weight)
+ PM_bestdose$new(
+ result = raw$result,
+ problem_obj = self,
+ bias_override = raw$bias_weight
+ )
+ }
+ ),
+ private = list(
+ .run_optimize = function(bias_weight) {
+ if (is.null(self$handle)) {
+ cli::cli_abort("BestDose problem handle has been released")
+ }
+
+ bw <- if (is.null(bias_weight)) self$bias_weight else bias_weight
+
+ if (bw < 0 || bw > 1) {
+ cli::cli_abort("bias_weight must be between 0 and 1")
+ }
+
+ res <- bestdose_optimize(self$handle, bw)
+ if (is.character(res)) {
+ cli::cli_abort(res)
+ }
+
+ list(result = res, bias_weight = bw)
+ }
+ )
+)
+
+#' @export
+PM_bestdose$load <- function(filename = "bestdose_result.rds") {
+ if (!file.exists(filename)) {
+ cli::cli_abort("File not found: {filename}")
+ }
+ readRDS(filename)
+}
diff --git a/R/PM_model.R b/R/PM_model.R
index 9f37dbb71..2c6ff24c7 100644
--- a/R/PM_model.R
+++ b/R/PM_model.R
@@ -31,48 +31,48 @@
#'
#' * It's a complete example of a three compartment model with delayed absorption.
#' * We show the method of defining the model first and embedding the `PM_model$new()` within
-#' a `donttest` block to avoid automatic compilation.
+#' a `donttest` block to avoid automatic compilation.
#' * Since this model can also be solved analytically with algebra, we could have used
-#' `eqn = function(){three_comp_bolus}`.
-#' @examples
-#'
+#' `eqn = function(){three_comp_bolus}`.
+#' @examples
+#'
#' mod_list <- list(
#' pri = c(
-#' CL = ab(10, 200),
-#' V0 = ab(0, 100),
-#' ka = ab(0, 3),
-#' k23 = ab(0, 5),
-#' k32 = ab(0, 5),
-#' lag1 = ab(0, 2)
-#' ),
-#' cov = c(
-#' wt = interp()
-#' ),
-#' sec = function() {
-#' V = V0 * (wt/70)
-#' ke = CL/V # define here to make eqn simpler
-#' },
-#' eqn = function() {
-#' dx[1] = -ka * x[1]
-#' dx[2] = rateiv[1] + ka * x[1] - (ke + k23) * x[2] + k32 * x[3]
-#' dx[3] = k23 * x[2] - k32 * x[3]
-#' dx[4] = x[1] / V
-#' },
-#' lag = function() {
-#' tlag[1] = lag1
-#' },
-#' out = function() {
-#' y[1] = x[1]/V
-#' y[2] = x[4] # AUC, not fitted to any data, not required
-#' },
-#' err = c(
-#' proportional(2, c(0.1, 0.15, 0, 0)) # only applies to y[1]
-#' )
-#' )
+#' CL = ab(10, 200),
+#' V0 = ab(0, 100),
+#' ka = ab(0, 3),
+#' k23 = ab(0, 5),
+#' k32 = ab(0, 5),
+#' lag1 = ab(0, 2)
+#' ),
+#' cov = c(
+#' wt = interp()
+#' ),
+#' sec = function() {
+#' V <- V0 * (wt / 70)
+#' ke <- CL / V # define here to make eqn simpler
+#' },
+#' eqn = function() {
+#' dx[1] <- -ka * x[1]
+#' dx[2] <- rateiv[1] + ka * x[1] - (ke + k23) * x[2] + k32 * x[3]
+#' dx[3] <- k23 * x[2] - k32 * x[3]
+#' dx[4] <- x[1] / V
+#' },
+#' lag = function() {
+#' tlag[1] <- lag1
+#' },
+#' out = function() {
+#' y[1] <- x[1] / V
+#' y[2] <- x[4] # AUC, not fitted to any data, not required
+#' },
+#' err = c(
+#' proportional(2, c(0.1, 0.15, 0, 0)) # only applies to y[1]
+#' )
+#' )
#'
#' \donttest{
-#' mod <- PM_model$new(mod_list)
-#' }
+#' mod <- PM_model$new(mod_list)
+#' }
#'
#' @export
PM_model <- R6::R6Class(
@@ -85,42 +85,42 @@ PM_model <- R6::R6Class(
arg_list = NULL,
#' @field binary_path The full path and filename of the compiled model
binary_path = NULL,
- #' @description
+ #' @description
#' This is the method to create a new `PM_model` object. If all arguments are `NULL`,
#' e.g. `mod <- PM_model$new()` the model builder shiny app will launch by a call to [build_model()],
#' which will return the model object upon exit.
- #'
- #'
+ #'
+ #'
#' Otherwise, the first parameter allows creation of a model from a variety of pre-existing
#' sources, and if used, all the subsequent arguments will be ignored. If a model
#' is defined on the fly, the arguments form the building blocks. Blocks are of two types:
- #'
+ #'
#' * **Vectors** define *primary parameters*, *covariates*,
- #' and *error models*. These
+ #' and *error models*. These
#' portions of the model have specific and defined creator functions and no additional
#' R code is permissible. They take this form:
#' ```
- #' block_name = c(
- #' var1 = creator(),
+ #' block_name = c(
+ #' var1 = creator(),
#' var2 = creator()
- #' )
- #' ```
+ #' )
+ #' ```
#' Note the comma separating the creator functions, "`c(`" to open the vector and "`)`" to close the vector.
- #' Names are case-insensitive and are converted to lowercase for Rust.
+ #' Names are case-insensitive and are converted to lowercase for Rust.
#' * **Functions** define the other parts of the model, including *secondary (global)
#' equations*, *model equations* (e.g. ODEs), *lag time*, *bioavailability*, *initial conditions*,
#' and *outputs*. These parts of the model are defined as R functions without arguments,
#' but whose body contains any permissible R code.
#' ```
- #' block_name = function() {
- #'
- #' # any valid R code
+ #' block_name = function() {
+ #'
+ #' # any valid R code
#' # can use primary or secondary parameters and covariates
#' # lines are not separated by commas
- #'
- #' }
+ #'
+ #' }
#' ```
- #' Note the absence of arguments between the "`()`", the opening curly brace "`{`" to start
+ #' Note the absence of arguments between the "`()`", the opening curly brace "`{`" to start
#' the function body and the closing curly brace "`}`" to end the body.
#' Again, all R code will be converted to lowercase prior to translation into Rust.
#'
@@ -129,7 +129,7 @@ PM_model <- R6::R6Class(
#' @param x An optional argument, but if specified, all the subsequent
#' arguments will be ignored. `x` creates a `PM_model` from
#' existing appropriate input, which can be one of the following:
- #'
+ #'
#' * Quoted name of a model text file in the
#' working directory which will be read and passed to Rust engine.
#' * List that defines the model directly in R. This will be in the same format as if
@@ -145,7 +145,7 @@ PM_model <- R6::R6Class(
#' ```
#' * `PM_model` object, which will simply rebuild it, e.g. carrying on the
#' prior example: `PM_model$new(mod)`
- #'
+ #'
#' See the user manual [PM_manual()] for more help on directly defining models in R.
#' @param pri The first of the arguments used if `x` is not specified. This is
#' a named vector of primary parameters, which are the model parameters that
@@ -155,14 +155,14 @@ PM_model <- R6::R6Class(
#' pri = c(
#' Ke = ab(0, 5),
#' V = msd(100, 10)
- #')
+ #' )
#' ```
#' The [ab()] creator specifies the
#' initial range `[a, b]` of the parameter, while the [msd()] creator specifies
#' the initial mean and standard deviation of the parameter.
#' @param cov A vector whose names are some or all of the covariates in the data file.
- #' Unlike prior versions of Pmetrics, as of 3.0.0, they do not have to be listed in the same order
- #' as in the data file, and they do not need to be all present.
+ #' Unlike prior versions of Pmetrics, as of 3.0.0, they do not have to be listed in the same order
+ #' as in the data file, and they do not need to be all present.
#' **Only those covariates used in model equations need to be declared here.**
#' Values for each element in the covariate vector are the [interp()] creator function to declare
#' how each covariate is interpolated between entries in the data. The default argument
@@ -183,19 +183,19 @@ PM_model <- R6::R6Class(
#' are not estimated for these equations but they are available to every other block in the model.
#' For example:
#' ```
- #' sec = function() {
- #' V = V0 * (wt/70)
- #' }
+ #' sec = function() {
+ #' V = V0 * (wt/70)
+ #' }
#' ```
#' Note that the function
- #' must be defined with no arguments between the parentheses,
+ #' must be defined with no arguments between the parentheses,
#' and the body **must be in R syntax**. Any number of lines and R code, e.g.
#' `if` - `else` statements, etc. are permissible.
#' @param eqn A function defining the model equations. The function must have no arguments.
#' The body of the function may contain three kinds of equations, written in R syntax.
- #'
+ #'
#' * **Implicit equations** referenced by calling the name of a Pmetrics model library object
- #' detailed in [model_lib()]. The Pmetrics model library contains a number of template models
+ #' detailed in [model_lib()]. The Pmetrics model library contains a number of template models
#' solved analytically (algebraically) and may include user-defined models. For example, to use
#' a two-compartment model with intavenous input:
#' ```
@@ -223,9 +223,9 @@ PM_model <- R6::R6Class(
#' ... # more model blocks, including out, err
#' )
#' ```
- #' * **Explicit equations** are ordinary differential equations that directly define a model.
+ #' * **Explicit equations** are ordinary differential equations that directly define a model.
#' Use the following notation in equations:
- #' - `dx[i]` for the change in amount with respect to time (i.e., \eqn{dx/dt}),
+ #' - `dx[i]` for the change in amount with respect to time (i.e., \eqn{dx/dt}),
#' where `i` is the compartment number,
#' - `x[i]` for the compartment amount, where `i` is the compartment number.
#' - `rateiv[j]` for the infusion rate of input `j`, where `j` is the input number
@@ -233,23 +233,23 @@ PM_model <- R6::R6Class(
#' - Bolus doses are indicated by `DUR = 0` for dose events in the
#' data. Currently only one bolus input is allowed, which goes into compartment 1
#' and is not modifiable. It does not appear in the differential equations.
- #'
- #' For example,
+ #'
+ #' For example,
#' ```
#' eqn = function() {
#' dx[1] = -ka * x[1]
#' dx[2] = rateiv[1] + ka * x[1] - ke * x[2]
#' }
#' ```
- #' * **Additional equations** in R code can be defined in this block, which are similar to
+ #' * **Additional equations** in R code can be defined in this block, which are similar to
#' the `sec` block, but will only be available within the `eqn` block as opposed
- #' to global availability when defined in `sec`. They can be added to either
- #' @param lag A function defining the lag time (delayed absorption) for the bolus input.
+ #' to global availability when defined in `sec`. They can be added to either
+ #' @param lag A function defining the lag time (delayed absorption) for the bolus input.
#' The function must have no arguments,
#' and the equations must be defined
#' in R syntax The equations must be defined in the form of
#' `tlag[i] = par`, where `tlag[i]` is the lag for drug (input) `i` and
- #' `par` is the lag parameter used in the `pri` block.
+ #' `par` is the lag parameter used in the `pri` block.
#'
#' For example, if
#' `antacid` is a covariate in the data file, and `lag1` is a primary parameter,
@@ -259,7 +259,7 @@ PM_model <- R6::R6Class(
#' tlag[1] = if(antacid == 1) lag1 else 0
#' }
#' ```
- #' As for `eqn`, additional equations in R code can be defined in this block,
+ #' As for `eqn`, additional equations in R code can be defined in this block,
#' but will only be available within the `lag` block.
#' @param fa A function defining the bioavailability (fraction absorbed) equations,
#' similar to `lag`.
@@ -270,7 +270,7 @@ PM_model <- R6::R6Class(
#' fa[1] = if(antacid == 1) fa1 else 1
#' }
#' ```
- #' As for `eqn`, additional equations in R code can be defined in this block,
+ #' As for `eqn`, additional equations in R code can be defined in this block,
#' but will only be available within the `fa` block.
#' @param ini A function defining the initial conditions for a compartment
#' in the model. Structure is similar to `lag` and `fa`.
@@ -283,29 +283,29 @@ PM_model <- R6::R6Class(
#' ```
#' This sets the initial amount of drug in compartment 2 to the value
#' of a covariate `init2` multiplied by the volume of the compartment,
- #' `V`, assuming `V` is either a primary parameter or defined in the
+ #' `V`, assuming `V` is either a primary parameter or defined in the
#' `sec` block.
- #'
- #' As for `eqn`, additional equations in R code can be defined in this block,
+ #'
+ #' As for `eqn`, additional equations in R code can be defined in this block,
#' but will only be available within the `ini` block.
#' @param out A function defining the output equations, which are the predictions
#' from the model. The function must have no arguments,
#' and the equations for predictions must be defined
#' in R syntax.
- #'
+ #'
#' Use the following notation in equations:
- #'
+ #'
#' * `y[i]` for the predicted value, where `i` is the output equation number,
#' typically corresponding to an observation with `outeq = i` in the data, but not
#' always (see **Note** below).
#' * `x[j]` for the compartment amount, where `j` is the compartment number.
- #'
- #' As with all function blocks, secondary equations are permitted,
+ #'
+ #' As with all function blocks, secondary equations are permitted,
#' but will be specific to the `out` block.
- #'
+ #'
#' For example,
#' ```
- #' out = function() {
+ #' out = function() {
#' V = V0 * wt # only needed if not included in sec block
#' y[1] = x[1]/V
#' #Vp and Vm must be defined in pri or sec blocks
@@ -313,9 +313,9 @@ PM_model <- R6::R6Class(
#' y[3] = x[3]/Vm
#' }
#' ```
- #' This assumes `V`, `Vp`, and `Vm` are either primary parameters or defined in the
- #' `sec` block.
- #'
+ #' This assumes `V`, `Vp`, and `Vm` are either primary parameters or defined in the
+ #' `sec` block.
+ #'
#' **Note** that as of Pmetrics 3.0.0, you can have more output equations
#' than values for `outeq` in the data. This is not possible with prior
#' versions of Pmetrics. Outputs without corresponding observations
@@ -335,7 +335,7 @@ PM_model <- R6::R6Class(
#' err = c(
#' proportional(2, c(0.1, 0.15, 0, 0))
#' )
- #' ```
+ #' ```
#' If the data only contain observations for `y[1]`, i.e. the concentration
#' of drug in the plasma compartment with `outeq = 1`, the model will
#' use that information to optimize the parameter values, but will also
@@ -345,11 +345,11 @@ PM_model <- R6::R6Class(
#' AUC (`y[2]`) is not fitted to any observations; it is a calculation based on
#' the model state, given the optimized parameter values. It's not required, but
#' shown here for illustrative purposes.
- #'
+ #'
#' @param err An unammed vector of error models for each of the output equations
- #' with observations, i.e. those that have an `outeq` number associated with them in
- #' the data.
- #' Each error model is defined by the [proportional()] creator or
+ #' with observations, i.e. those that have an `outeq` number associated with them in
+ #' the data.
+ #' Each error model is defined by the [proportional()] creator or
#' the [additive()] creator, relative to the observation error.
#' For example, if there are three output equations corresponding to three
#' sources of observations in the data, the error models
@@ -363,9 +363,9 @@ PM_model <- R6::R6Class(
#' ```
#' This defines the first two output equations to have proportional error
#' with initial values of 2 and 3, respectively, and the third output equation
- #' to have additive error with initial value of 1. Each output is measured by
+ #' to have additive error with initial value of 1. Each output is measured by
#' a different assay with different error characteristics.
- #'
+ #'
#' If all the output equations have the same error model, you can
#' simply use a single error model embedded in [replicate()] , e.g.,
#' for 3 outputs with the same error model:
@@ -376,19 +376,67 @@ PM_model <- R6::R6Class(
#' ```
#' @param ... Not currently used.
initialize = function(x = NULL,
- pri = NULL,
- cov = NULL,
- sec = NULL,
- eqn = NULL,
- lag = NULL,
- fa = NULL,
- ini = NULL,
- out = NULL,
- err = NULL,
- ...) {
- # Store the original function arguments
- self$arg_list <- list(
- x = x,
+ pri = NULL,
+ cov = NULL,
+ sec = NULL,
+ eqn = NULL,
+ lag = NULL,
+ fa = NULL,
+ ini = NULL,
+ out = NULL,
+ err = NULL,
+ ...) {
+ # Store the original function arguments
+ self$arg_list <- list(
+ x = x,
+ pri = pri,
+ cov = cov,
+ sec = sec,
+ eqn = eqn,
+ lag = lag,
+ fa = fa,
+ ini = ini,
+ out = out,
+ err = err
+ )
+
+ if (!is.null(x)) {
+ model_sections <- c("pri", "cov", "sec", "eqn", "lag", "fa", "ini", "out", "err")
+ if (is.character(x) && length(x) == 1) { # x is a filename
+ if (!file.exists(x)) {
+ cli::cli_abort(c(
+ "x" = "File {.file {x}} does not exist.",
+ "i" = "Current directory: {getwd()}"
+ ))
+ }
+ self$arg_list <- private$R6fromFile(x) # read file and populate fields
+ } else if (is.list(x)) { # x is a list in R
+ purrr::walk(model_sections, \(s) {
+ if (s %in% names(x)) {
+ self$arg_list[[s]] <- x[[s]]
+ }
+ })
+ } else if (inherits(x, "PM_model")) { # x is a PM_model object
+ if (!"arg_list" %in% names(x)) {
+ cli::cli_abort(c(
+ "x" = "You have supplied an older {.code PM_model} format.",
+ "i" = "Please see for {.help Pmetrics::PM_model()} to remake it."
+ ))
+ }
+
+ purrr::walk(model_sections, \(s) {
+ if (s %in% names(x$arg_list)) {
+ self$arg_list[[s]] <- x$arg_list[[s]]
+ }
+ })
+ } else {
+ cli::cli_abort(c(
+ "x" = "Non supported input for {.arg x}: {typeof(x)}",
+ "i" = "It must be a filename, list, or current {.code PM_model} object."
+ ))
+ }
+ } else { # x is NULL, check if other arguments are NULL
+ named_args <- list(
pri = pri,
cov = cov,
sec = sec,
@@ -399,2028 +447,2053 @@ PM_model <- R6::R6Class(
out = out,
err = err
)
-
- if(!is.null(x)){
- model_sections <- c("pri", "cov", "sec", "eqn", "lag", "fa", "ini", "out", "err")
- if (is.character(x) && length(x) == 1) { # x is a filename
- if (!file.exists(x)) {
- cli::cli_abort(c("x" = "File {.file {x}} does not exist.",
- "i" = "Current directory: {getwd()}"))
- }
- self$arg_list <- private$R6fromFile(x) # read file and populate fields
-
-
- } else if (is.list(x)) { # x is a list in R
- purrr::walk(model_sections, \(s) {
- if (s %in% names(x)) {
- self$arg_list[[s]] <- x[[s]]
- }
- })
-
- } else if (inherits(x, "PM_model")) { # x is a PM_model object
- if(!"arg_list" %in% names(x)) {
- cli::cli_abort(c("x" = "You have supplied an older {.code PM_model} format.",
- "i" = "Please see for {.help Pmetrics::PM_model()} to remake it."))
- }
-
- purrr::walk(model_sections, \(s) {
- if (s %in% names(x$arg_list)) {
- self$arg_list[[s]] <- x$arg_list[[s]]
- }
- })
-
+ other_args <- list(...)
+ all_args <- c(named_args, other_args)
+ if (all(sapply(all_args, is.null))) { # everything is NULL
+ self <- build_model() # launch the shiny app
+ return(invisible(self))
+ }
+ } # no, some arguments were not NULL, so keep going
+
+
+ # Primary parameters must be provided
+ if (is.null(self$arg_list$pri)) {
+ cli::cli_abort(
+ c("x" = "Primary parameters are missing.", "i" = "Please provide a list of primary parameters.")
+ )
+ }
+
+
+ # Either an ODE-based model or an analytical model must be provided in eqn
+ if (is.null(self$arg_list$eqn)) {
+ cli::cli_abort(c(
+ "x" = "No equations or template provided.",
+ "i" = "Please provide either a template (see {.help model_lib()}) or differential equations using {.code eqn}."
+ ))
+ }
+
+
+
+ # Get model template name if present (NA if absent) and set type
+ model_template <- get_found_model(self$arg_list$eqn) # function defined below, returns 0 if not found, -1 if error
+
+ # change logic; need to accomodate library models that are ODEs
+ if (length(model_template) > 1 && model_template$analytical) {
+ type <- "Analytical"
+ } else {
+ if (model_template == -1) {
+ # length was 1, value 0
+ cli::cli_abort(c(
+ "x" = "You have included more than one model template.",
+ "i" = "A maximum of one model template can be included in a model."
+ ))
+ }
+
+ # length was 1, value 0
+ type <- "ODE"
+ }
+
+ # Number of equations
+ n_eqn <- if (type == "Analytical") {
+ model_template$ncomp
+ } else {
+ get_assignments(self$arg_list$eqn, "dx")
+ }
+ n_out <- get_assignments(self$arg_list$out, "y")
+
+ ## Get the names of the parameters
+ parameters <- tolower(names(self$arg_list$pri))
+ covariates <- tolower(names(self$arg_list$cov))
+ ## check to make sure required parameters present if Analytical
+ if (type == "Analytical") {
+ # look in pri, sec, eqn, lag, fa, ini, out blocks for required parameters
+ required_parameters <- tolower(model_template$parameters)
+ pri_list <- map_lgl(required_parameters, \(x){
+ if (x %in% parameters) {
+ return(TRUE)
} else {
- cli::cli_abort(c("x" = "Non supported input for {.arg x}: {typeof(x)}",
- "i" = "It must be a filename, list, or current {.code PM_model} object."))
+ return(FALSE)
}
- } else { # x is NULL, check if other arguments are NULL
- named_args <- list(
- pri = pri,
- cov = cov,
- sec = sec,
- eqn = eqn,
- lag = lag,
- fa = fa,
- ini = ini,
- out = out,
- err = err)
- other_args <- list(...)
- all_args <- c(named_args, other_args)
- if (all(sapply(all_args, is.null))) { # everything is NULL
- self <- build_model() # launch the shiny app
- return(invisible(self))
+ })
+
+ if (length(covariates) > 0) {
+ cov_list <- map_lgl(required_parameters, \(x){
+ if (x %in% covariates) {
+ return(TRUE)
+ } else {
+ return(FALSE)
}
- } # no, some arguments were not NULL, so keep going
-
-
- # Primary parameters must be provided
- if (is.null(self$arg_list$pri)) {
+ })
+ } else {
+ cov_list <- rep(FALSE, length(required_parameters))
+ }
+
+ if (!is.null(self$arg_list$sec)) {
+ sec_list <- map_lgl(required_parameters, \(x){
+ any(stringr::str_detect(tolower(func_to_char(self$arg_list$sec)), x))
+ })
+ } else {
+ sec_list <- rep(FALSE, length(required_parameters))
+ }
+
+ eqn_list <- map_lgl(required_parameters, \(x){
+ any(
+ stringr::str_detect(
+ stringr::str_remove_all(tolower(func_to_char(self$arg_list$eqn)), "\\s+"), # string
+ paste0(x, "(?=(<-|=))")
+ ) # pattern
+ )
+ })
+
+ if (!is.null(self$arg_lag)) {
+ lag_list <- map_lgl(required_parameters, \(x){
+ any(
+ stringr::str_detect(
+ stringr::str_remove_all(tolower(func_to_char(self$arg_list$lag)), "\\s+"), # string
+ paste0(x, "(?=(<-|=))")
+ ) # pattern
+ )
+ })
+ } else {
+ lag_list <- rep(FALSE, length(required_parameters))
+ }
+
+ if (!is.null(self$arg_fa)) {
+ lag_list <- map_lgl(required_parameters, \(x){
+ any(
+ stringr::str_detect(
+ stringr::str_remove_all(tolower(func_to_char(self$arg_list$fa)), "\\s+"), # string
+ paste0(x, "(?=(<-|=))")
+ ) # pattern
+ )
+ })
+ } else {
+ fa_list <- rep(FALSE, length(required_parameters))
+ }
+
+ if (!is.null(self$arg_ini)) {
+ ini_list <- map_lgl(required_parameters, \(x){
+ any(
+ stringr::str_detect(
+ stringr::str_remove_all(tolower(func_to_char(self$arg_list$ini)), "\\s+"), # string
+ paste0(x, "(?=(<-|=))")
+ ) # pattern
+ )
+ })
+ } else {
+ ini_list <- rep(FALSE, length(required_parameters))
+ }
+
+ out_list <- map_lgl(required_parameters, \(x){
+ any(
+ stringr::str_detect(
+ stringr::str_remove_all(tolower(func_to_char(self$arg_list$out)), "\\s+"), # string
+ paste0(x, "(?=(<-|=))")
+ ) # pattern
+ )
+ })
+
+ all_lists <- bind_rows(
+ tibble::tibble(
+ parameter = required_parameters,
+ pri = pri_list,
+ cov = cov_list,
+ sec = sec_list,
+ eqn = eqn_list,
+ lag = lag_list,
+ fa = fa_list,
+ ini = ini_list,
+ out = out_list
+ )
+ ) %>% mutate(ok = purrr::pmap_lgl(across(pri:out), any))
+
+
+ if (any(!all_lists$ok)) {
+ missing <- all_lists$parameter[!all_lists$ok]
+ cli::cli_abort(
+ c(
+ "x" = "The following parameters are required for the {.code {model_template$name}} model template but are missing: {missing}",
+ "i" = "They should be defined in one of the model blocks, likely {.code pri}, {.code sec}, {.code eqn}, or {.code out}.",
+ " " = "Parameters defined in {.code pri} and {.code sec} are available to all blocks.",
+ " " = "Parameters defined in other blocks are only available to that block."
+ )
+ )
+ }
+ } # end parameter checks for Analytical model
+
+
+ # if Analytical, need to combine sec and eqn
+ if (type == "Analytical") {
+ # shell function
+ sec_eqn <- function() {}
+ # define the body of the shell function
+ body(sec_eqn) <- suppressWarnings(as.call(c(
+ quote(`{`),
+ as.list(body(self$arg_list$eqn))[-1], # remove outer `{` of f1
+ as.list(body(self$arg_list$sec))[-1] # remove outer `{` of f2
+ )))
+
+ # this will include template and equations in both sec and eqn
+ }
+
+ # sec
+ # still needed for analytic, because these equations will be used
+ # in other blocks
+ if (!is.null(self$arg_list$sec)) {
+ sec <- transpile_sec(self$arg_list$sec)
+ } else {
+ sec <- ""
+ }
+
+ # eqn
+ if (type == "ODE") {
+ eqn <- transpile_ode_eqn(self$arg_list$eqn, parameters, covariates, sec)
+ } else if (type == "Analytical") {
+ eqn <- transpile_analytic_eqn(sec_eqn, parameters, covariates)
+ }
+
+ # fa
+ if (!is.null(self$arg_list$fa)) {
+ fa <- transpile_fa(self$arg_list$fa, parameters, covariates, sec)
+ } else {
+ fa <- empty_fa()
+ }
+
+ # lag
+ if (!is.null(self$arg_list$lag)) {
+ lag <- transpile_lag(self$arg_list$lag, parameters, covariates, sec)
+ } else {
+ lag <- empty_lag()
+ }
+
+ # ini
+ if (!is.null(self$arg_list$ini)) {
+ ini <- transpile_ini(self$arg_list$ini, parameters, covariates, sec)
+ } else {
+ ini <- empty_ini()
+ }
+
+ # out
+ if (!is.null(self$arg_list$out)) {
+ out <- transpile_out(self$arg_list$out, parameters, covariates, sec)
+ } else {
+ out <- empty_out()
+ }
+
+ # err
+ if (is.null(self$arg_list$err)) {
+ cli::cli_abort(
+ c(
+ "x" = "Error model is missing and required.",
+ "i" = "Please see help for {.help PM_model()}."
+ )
+ )
+ }
+
+ # ensure length err matches length outeqs
+ if (length(self$arg_list$err) != n_out) {
+ cli::cli_abort(
+ c(
+ "x" = "There must be one error model for each output equation.",
+ "i" = "Please check the error model."
+ )
+ )
+ }
+ err <- self$arg_list$err
+
+ # name
+ name <- if (type == "Analytical") {
+ model_template$name
+ } else {
+ "user"
+ }
+
+ # build the model list of rust components
+ model_list <- list(
+ pri = self$arg_list$pri,
+ eqn = eqn,
+ sec = sec,
+ lag = lag,
+ fa = fa,
+ ini = ini,
+ out = out,
+ n_eqn = n_eqn,
+ n_out = n_out,
+ parameters = parameters,
+ covariates = covariates,
+ err = err,
+ name = name
+ )
+ # make everything lower case if a character vector
+ self$model_list <- purrr::map(model_list, \(x) {
+ if (is.character(x)) {
+ tolower(x)
+ } else {
+ x
+ }
+ })
+
+ # this one needs to be capital
+ self$model_list$type <- type
+
+
+ extra_args <- list(...)
+ if (!is.null(purrr::pluck(extra_args, "compile"))) {
+ if (extra_args$compile) {
+ self$compile()
+ }
+ } else { # default is to compile
+ self$compile()
+ }
+ },
+
+ #' @description
+ #' Print the model summary.
+ #' @details
+ #' This method prints a summary of the model.
+ #' @param ... Not used.
+ print = function(...) {
+ cli::cli_div(theme = list(
+ span.eqs = list(color = navy())
+ ))
+
+ cli::cli_h1("Model summary")
+
+ cli::cli_h3(text = "Primary Parameters")
+ # pars = self$model_list$parameters
+ # cli::cli_text("{.eqs {pars}}")
+
+ self$arg_list$pri %>%
+ purrr::imap(\(x, y) cli::cli_text("{.strong {y}}: [{.strong {x$min}}, {.strong {x$max}}], {.emph ~N({round(x$mean,2)}}, {.emph {round(x$sd,2)})}")) %>%
+ invisible() # to suppress NULL
+
+
+ if (!is.null(self$model_list$covariates)) {
+ cli::cli_h3(text = "Covariates")
+
+ cov_list <- paste0(
+ self$model_list$covariates,
+ ifelse(self$arg_list$cov == 1, "", " (no interpolation)")
+ )
+
+ cli::cli_text("{.eqs {cov_list}}")
+ }
+
+ if (!is.null(self$arg_list$sec)) {
+ cli::cli_h3(text = "Secondary (Global) Equations")
+ eqs <- func_to_char(self$arg_list$sec) # function in PMutitlities
+ for (i in eqs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+ }
+
+ if (!is.null(self$arg_list$tem)) {
+ cli::cli_h3(text = "Analytical Model")
+ cli::cli_text("{.eqs {self$arg_list$tem$name}})")
+ }
+
+ if (!is.null(self$arg_list$eqn)) {
+ cli::cli_h3(text = "Primary Equations")
+ eqs <- func_to_char(self$arg_list$eqn) # function in PMutitlities
+ for (i in eqs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+ }
+
+ if (!is.null(self$arg_list$lag)) {
+ cli::cli_h3(text = "Lag Time")
+ eqs <- func_to_char(self$arg_list$lag) # function in PMutitlities
+ for (i in eqs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+ }
+
+ if (!is.null(self$arg_list$fa)) {
+ cli::cli_h3(text = "Bioavailability (Fraction Absorbed)")
+ eqs <- func_to_char(self$arg_list$fa) # function in PMutitlities
+ for (i in eqs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+ }
+
+ if (!is.null(self$arg_list$ini)) {
+ cli::cli_h3(text = "Initial Conditions")
+ eqs <- func_to_char(self$arg_list$ini) # function in PMutitlities
+ for (i in eqs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+ }
+
+ cli::cli_h3(text = "Outputs")
+ outs <- func_to_char(self$arg_list$out)
+ for (i in outs) {
+ cli::cli_text("{.eqs {i}}")
+ }
+
+ cli::cli_h3(text = "Error Model")
+ for (i in self$model_list$err) {
+ if (i$fixed) {
+ cli::cli_text("{.strong {tools::toTitleCase(i$type)}}, with fixed value of {.val {i$initial}} and coefficients {.val {i$coeff}}.")
+ } else {
+ cli::cli_text("{.strong {tools::toTitleCase(i$type)}}, with initial value of {.val {i$initial}} and coefficients {.val {i$coeff}}.")
+ }
+ }
+ cli::cli_end()
+
+ invisible(self)
+ },
+ #' @description
+ #' Plot the model.
+ #' @details
+ #' This method plots the model using the
+ #' [plot.PM_model()] function.
+ #' @param ... Additional arguments passed to the plot function.
+ plot = function(...) {
+ tryCatch(
+ plot.PM_model(self, ...),
+ error = function(e) {
+ cat(crayon::red("Error:"), e$message, "\n")
+ }
+ )
+ },
+ #' @description
+ #' This is the main method to run a population analysis.
+ #' @details
+ #' As of Pmetrics 3.0.0, models contain compiled code to fit
+ #' the model equations to the data, optimizing the parameter
+ #' value probability distributions in the population to
+ #' maximize their likelihood, or more precisely, minimize
+ #' the objective function, which is -2*log-likelihood.
+ #'
+ #' The `$fit()` method is the means of running that compiled
+ #' code to conduct to fitting procedure. At a minimum, it requires
+ #' a [PM_data] object, which can be created with
+ #' `PM_data$new()`. There are a number of additional arguments
+ #' to control the fitting procedure, such as the number of cycles
+ #' to run, the initial number of support points,
+ #' and the algorithm to use, among others.
+ #'
+ #' The `$fit()` method is the descendant of the legacy
+ #' [NPrun] function, which is maintained as a wrapper to `$fit()`
+ #' for backwards compatibility.
+ #'
+ #' @param data Either the name of a [PM_data]
+ #' object in memory or the quoted filename (with or without a path) of a Pmetrics
+ #' data file. If the path is not specified, the file is assumed to be in the current working directory,
+ #' unless the `path` argument below is also specified as a global option for the fit.
+ #' The file will be used to create a [PM_data]
+ #' object on the fly. However, if created on the fly, this object
+ #' will not be available to other
+ #' methods or other instances of `$fit()`.
+ #' @param path Optional full path or relative path from current working directory
+ #' to the folder where `data` and `model` are located if specified as filenames without
+ #' their own paths,
+ #' and where the output will be saved. Default is the current working directory.
+ #' @param run Specify the run number of the output folder. Default if missing is the next available number.
+ #' @param include Vector of subject id values in the data file to include in the analysis.
+ #' The default (missing) is all.
+ #' @param exclude A vector of subject IDs to exclude in the analysis, e.g. `c(4,6:14,16:20)`
+ # #' @param ode Ordinary Differential Equation solver log tolerance or stiffness.
+ # Default is -4, i.e. 0.0001. Higher values will result in faster
+ # #' runs, but parameter estimates may not be as accurate.
+ # #' @param tol Tolerance for convergence of NPAG. Smaller numbers make it harder to converge.
+ # #' Default value is 0.01.
+ # #' @param salt Vector of salt fractions for each drug in the data file, default is 1 for each drug. This is not the same as bioavailability.
+ #' @param cycles Number of cycles to run. Default is 100.
+ #' @param prior The distribution for the initial support points, which can be
+ #' one of several options.
+ #' * The default is "sobol", which is a semi-random distribution. This is the distribution
+ #' typically used when fitting a new model to the data. An example of this is
+ #' on our [website](https://www.lapk.org/images/sobol_3d_plot.html).
+ #'
+ #' The following all specify non-random, informative prior distributions. They
+ #' are useful for either continuing a previous
+ #' run which did not converge or for fitting a model to new data, whether to simply
+ #' calculate Bayesian posteriors with `cycles = 0` or to revise the model to a new
+ #' covergence with the new data.
+ #' * The name of a suitable [PM_result] object from a prior run loaded with [PM_load].
+ #' This starts from the non-uniform, informative distribution obtained at the end of a prior NPAG run.
+ #' Example: `run1 <- PM_load(1); fit1$run(prior = run1)`.
+ #'
+ #' * A character string with the filename of a csv file containing a prior distribution with
+ #' format as for 'theta.csv' in the output folder of a prior run: column headers are parameter
+ #' names, and rows are the support point values. A final column with probabilities
+ #' for each support point is not necessary, but if present will be ignored, as these
+ #' probabilities are calculated by the engine. Note that the parameter names must match the
+ #' names of the primary variables in the model. Example: `fit1$run(prior = "mytheta.csv")`.
+ #' * The number of a previous run with `theta.csv` in the output folder which will be read
+ #' as for the filename option above. Example: `fit1$run(prior = 2)`.
+ #' * A data frame obtained from reading an approriate file, such that the data frame
+ #' is in the required format described in the filename option above. Example:
+ #' `mytheta <- read_csv("mytheta.csv"); fit1$run(prior = mytheta)`.
+ #'
+ #' @param points The number of initial support points if one of
+ #' the semi-random, uniform distributions are selected in the `prior` argument
+ #' above. Default is 100. The initial points are
+ #' spread through the hyperspace defined by the random parameter ranges
+ #' and begin the search for the optimal
+ #' parameter value distribution (support points) in the population.
+ #' If there are fewer than 2 points per unit range for any parameter,
+ #' Pmetrics will suggest the minimum number of points that should be tried.
+ #' The greater the initial number of points, the less chance of
+ #' missing the globally maximally likely parameter value distribution,
+ #' but the slower the run.
+ #'
+ #' @param idelta How often to generate posterior predictions in units of time.
+ #' Default is 0.1, which means a prediction is generated every 0.1 hours (6 minutes)
+ #' if the unit of time is hours. Predictions are made at this interval until the time
+ #' of the last event (dose or observation) or until `tad` if that value is greater
+ #' than the time of the last dose or observation in the data.
+ #'
+ #' @param tad Length of time after the last dose event to add additional predictions
+ #' at frequency `idelta`. Default is 0, which means no additional predictions
+ #' beyond the last dose, assuming the dose is the last event. . If the
+ #' last observation in the data is after `tad`, then a prediction will be generated at
+ #' time = `tad` after the last dose
+ #'
+ #' @param seed Seed used if `prior = "sobol"`. Ignored otherwise.
+ #' @param intern Run NPAG in the R console without a batch script. Default is TRUE.
+ # #' @param quiet Boolean operator controlling whether a model summary report is given. Default is `TRUE`.
+ #' @param overwrite Boolean operator to overwrite existing run result folders. Default is `FALSE`.
+ # #' @param nocheck Suppress the automatic checking of the data file with [PM_data]. Default is `FALSE`.
+ # #' @param parallel Run NPAG in parallel. Default is `NA`, which will be set to `TRUE` for models that use
+ # #' differential equations, and `FALSE` for analytical/explicit models. The majority of the benefit for parallelization comes
+ # #' in the first cycle, with a speed-up of approximately 80\% of the number of available cores on your machine, e.g. an 8-core machine
+ # #' will speed up the first cycle by 0.8 * 8 = 6.4-fold. Subsequent cycles approach about 50\%, e.g. 4-fold increase on an 8-core
+ # #' machine. Overall speed up for a run will therefore depend on the number of cycles run and the number of cores.
+ #' @param algorithm The algorithm to use for the run. Default is "NPAG" for the **N**on-**P**arametric **A**daptive **G**rid. Alternatives: "NPOD".
+ #' @param report If missing, the default Pmetrics report template as specified in [getPMoptions]
+ #' is used. Otherwise can be "plotly", "ggplot", or "none".
+ #' @return A successful run will result in creation of a new folder in the working
+ #' directory with the results inside the folder.
+ #'
+ #' @author Michael Neely
+ #' @export
+ fit = function(data = NULL,
+ path = ".",
+ run = NULL,
+ include = NULL,
+ exclude = NULL,
+ cycles = 100,
+ prior = "sobol",
+ points = 100,
+ idelta = 0.1,
+ tad = 0,
+ seed = 23,
+ overwrite = FALSE,
+ algorithm = "NPAG", # POSTPROB for posteriors, select when cycles = 0, allow for "NPOD"
+ report = getPMoptions("report_template")) {
+ msg <- "" # status message at end of run
+
+ path <- stringr::str_replace(path, "/$", "") # remove trailing /
+
+ if (is.null(data)) {
+ cli::cli_abort(c("x" = " {.arg data} must be specified."))
+ }
+
+ if (is.null(self$model_list)) {
+ cli::cli_abort(c("x" = "Model is malformed."))
+ }
+
+ if (is.character(data)) {
+ # create PM_data object from file
+ data <- PM_data$new(normalizePath(file.path(path, data), mustWork = FALSE))
+ }
+
+ if (!inherits(data, "PM_data")) {
+ data <- tryCatch(
+ {
+ PM_data$new(data)
+ },
+ error = function(e) {
cli::cli_abort(
- c("x" = "Primary parameters are missing.", "i" = "Please provide a list of primary parameters.")
+ c("x" = "{.code data} must be a {.cls PM_data} object or an appropriate data frame.", "i" = "See help for {.fn Pmetrics::PM_data}.")
)
}
-
-
- # Either an ODE-based model or an analytical model must be provided in eqn
- if (is.null(self$arg_list$eqn)){
- cli::cli_abort(c("x" = "No equations or template provided.",
- "i" = "Please provide either a template (see {.help model_lib()}) or differential equations using {.code eqn}."))
+ )
+ }
+
+ #### checks
+
+ # covariates
+ modelCov <- self$model_list$cov
+ if (length(modelCov) > 0) {
+ dataCov <- tolower(getCov(data)$covnames)
+ missingCov <- modelCov[!modelCov %in% dataCov]
+ if (length(missingCov) > 0) { # if not identical, abort
+ msg <- glue::glue("{paste(modelCov, collapse = ', ')} {?is/are} missing from the data.")
+ cli::cli_abort(c("x" = msg))
+ }
+ }
+
+ # cycles
+ # if programmer is a crazy Norwegian....
+ if (cycles < 0) {
+ cli::cli_abort(c("x" = "Error: {.arg cycles} must be 0 or greater.", "i" = "See {.code $fit()} method for {.help PM_model}."))
+ }
+
+ # output equations
+
+ if (!is.null(data$standard_data$outeq)) {
+ dataOut <- max(data$standard_data$outeq, na.rm = TRUE)
+ } else {
+ dataOut <- 1
+ }
+
+ modelOut <- self$model_list$n_out
+
+
+ # check if model compiled and if not, do so
+ self$compile()
+
+ intern <- TRUE # always true until (if) rust can run separately from R
+
+
+ # make new output directory
+
+ if (is.null(run)) {
+ olddir <- list.dirs(path, recursive = FALSE)
+ olddir <- olddir[grep("^\\./[[:digit:]]+", olddir)]
+ olddir <- sub("^\\./", "", olddir)
+ if (length(olddir) > 0) {
+ run <- as.character(max(as.numeric(olddir)) + 1)
+ } else {
+ run <- "1"
+ }
+ } else {
+ if (!is.numeric(run)) {
+ cli::cli_abort(c("x" = " {.arg run} must be numeric."))
+ }
+ }
+
+ path_run <- normalizePath(file.path(path, run), mustWork = FALSE)
+
+ if (file.exists(path_run)) {
+ if (overwrite) {
+ unlink(path_run, recursive = TRUE)
+ msg <- c(msg, "The previous run in folder '{path_run}' was overwritten.")
+ } else {
+ cli::cli_inform(
+ c("i" = "The previous run from '{path_run}' was read.", " " = "Set {.arg overwrite} to {.val TRUE} to overwrite prior run in '{path_run}'.")
+ )
+ return(invisible(PM_load(file = normalizePath(file.path(path_run, "PMout.Rdata"), mustWork = FALSE))))
+ }
+ }
+
+ fs::dir_create(path_run)
+
+
+ #### Algorithm ####
+ algorithm <- toupper(algorithm)
+ if (cycles == 0) {
+ if (prior == "sobol") {
+ cli::cli_warn(c("!" = "Error: Cannot use {.code prior = 'sobol'} with {.code cycles = 0}.", "i" = "Use a prior from a previous run."))
+ }
+ algorithm <- "POSTPROB"
+ } else {
+ if (!(algorithm %in% c("NPAG", "NPOD"))) {
+ cli::cli_abort(c("x" = "Error: Unsupported algorithm.", "i" = "Supported algorithms are 'NPAG' and 'NPOD'."))
+ }
+ }
+ if (algorithm == "POSTPROB" && cycles > 0) {
+ cli::cli_warn(c("!" = "Warning: {.code algorithm = 'POSTPROB'} is used with {.code cycles = 0}.", "i" = "Continuing with {.code cycles = 0}."))
+ cycles <- 0
+ }
+
+
+
+ if (getPMoptions()$backend != "rust") {
+ cli::cli_abort(c("x" = "Error: unsupported backend.", "i" = "See help for {.fn setPMoptions}"))
+ }
+
+ #### Include or exclude subjects ####
+ if (is.null(include)) {
+ include <- unique(data$standard_data$id)
+ }
+ if (is.null(exclude)) {
+ exclude <- NA
+ }
+ data_filtered <- data$standard_data %>% includeExclude(include, exclude)
+
+ if (nrow(data_filtered) == 0) {
+ cli::cli_abort("x" = "No subjects remain after filtering.")
+ return(invisible(NULL))
+ }
+
+ #### Save input objects ####
+ fs::dir_create(normalizePath(file.path(path_run, "inputs"), mustWork = FALSE))
+ PM_data$new(data_filtered, quiet = TRUE)$save(normalizePath(file.path(path_run, "inputs", "gendata.csv"), mustWork = FALSE), header = FALSE)
+ saveRDS(list(data = data, model = self), file = normalizePath(file.path(path_run, "inputs", "fit.rds"), mustWork = FALSE))
+ file.copy(self$binary_path, normalizePath(file.path(path_run, "inputs"), mustWork = FALSE))
+
+ # Get ranges and calculate points
+ ranges <- lapply(self$model_list$pri, function(x) {
+ c(x$min, x$max)
+ })
+
+ names(ranges) <- tolower(names(ranges))
+ # Set initial grid points (only applies for sobol)
+ marginal_densities <- sapply(ranges, function(x) {
+ points / (x[2] - x[1])
+ })
+ if (any(marginal_densities < 2)) {
+ increase_to <- round(points * (max(2 / marginal_densities)), 0)
+ msg <- c(msg, "Recommend increasing {.arg points} to at least {increase_to} to ensure adequate coverage of parameter space.")
+ }
+
+
+
+ # set prior
+ if (prior != "sobol") {
+ if (is.numeric(prior)) {
+ # prior specified as a run number
+ if (!file.exists(glue::glue("{path}/{prior}/outputs/theta.csv"))) {
+ cli::cli_abort(c("x" = "Error: {.arg prior} file does not exist.", "i" = "Check the file path."))
}
-
-
-
- # Get model template name if present (NA if absent) and set type
- model_template <- get_found_model(self$arg_list$eqn) #function defined below, returns 0 if not found, -1 if error
-
- #change logic; need to accomodate library models that are ODEs
- if (length(model_template) > 1 && model_template$analytical) {
- type <- "Analytical"
- } else {
- if(model_template == -1){
- # length was 1, value 0
- cli::cli_abort(c(
- "x" = "You have included more than one model template.",
- "i" = "A maximum of one model template can be included in a model."
- ))
- }
-
- # length was 1, value 0
- type <- "ODE"
+ file.copy(glue::glue("{path}/{prior}/outputs/theta.csv"), "prior.csv", overwrite = TRUE)
+ prior <- "prior.csv"
+ } else if (is.character(prior)) {
+ # prior specified as a filename
+ if (!file.exists(prior)) {
+ cli::cli_abort(c("x" = "Error: {.arg prior} file does not exist.", "i" = "Check the file path."))
}
-
- # Number of equations
- n_eqn <- if (type == "Analytical"){ model_template$ncomp } else {get_assignments(self$arg_list$eqn, "dx")}
- n_out <- get_assignments(self$arg_list$out, "y")
-
- ## Get the names of the parameters
- parameters <- tolower(names(self$arg_list$pri))
- covariates <- tolower(names(self$arg_list$cov))
- ## check to make sure required parameters present if Analytical
- if (type == "Analytical"){
-
- # look in pri, sec, eqn, lag, fa, ini, out blocks for required parameters
- required_parameters <- tolower(model_template$parameters)
- pri_list <- map_lgl(required_parameters, \(x){
- if (x %in% parameters){
- return(TRUE)
- } else { return(FALSE)}
- })
-
- if(length(covariates)>0){
- cov_list <- map_lgl(required_parameters, \(x){
- if (x %in% covariates){
- return(TRUE)
- } else { return(FALSE)}
- })
- } else {
- cov_list <- rep(FALSE, length(required_parameters))
- }
-
- if(!is.null(self$arg_list$sec)){
- sec_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(tolower(func_to_char(self$arg_list$sec)), x))
- })
- } else {
- sec_list <- rep(FALSE, length(required_parameters))
- }
-
- eqn_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(
- stringr::str_remove_all(tolower(func_to_char(self$arg_list$eqn)), "\\s+"), # string
- paste0(x,"(?=(<-|=))")) # pattern
- )
- })
-
- if(!is.null(self$arg_lag)){
- lag_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(
- stringr::str_remove_all(tolower(func_to_char(self$arg_list$lag)), "\\s+"), # string
- paste0(x,"(?=(<-|=))")) # pattern
- )
- })
- } else {
- lag_list <- rep(FALSE, length(required_parameters))
- }
-
- if(!is.null(self$arg_fa)){
- lag_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(
- stringr::str_remove_all(tolower(func_to_char(self$arg_list$fa)), "\\s+"), # string
- paste0(x,"(?=(<-|=))")) # pattern
- )
- })
- } else {
- fa_list <- rep(FALSE, length(required_parameters))
- }
-
- if(!is.null(self$arg_ini)){
- ini_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(
- stringr::str_remove_all(tolower(func_to_char(self$arg_list$ini)), "\\s+"), # string
- paste0(x,"(?=(<-|=))")) # pattern
- )
- })
- } else {
- ini_list <- rep(FALSE, length(required_parameters))
- }
-
- out_list <- map_lgl(required_parameters, \(x){
- any(stringr::str_detect(
- stringr::str_remove_all(tolower(func_to_char(self$arg_list$out)), "\\s+"), # string
- paste0(x,"(?=(<-|=))")) # pattern
- )
- })
-
- all_lists <- bind_rows(
- tibble::tibble(
- parameter = required_parameters,
- pri = pri_list,
- cov = cov_list,
- sec = sec_list,
- eqn = eqn_list,
- lag = lag_list,
- fa = fa_list,
- ini = ini_list,
- out = out_list
- )
- ) %>% mutate(ok = purrr::pmap_lgl(across(pri:out), any))
-
-
- if (any(!all_lists$ok)) {
- missing <- all_lists$parameter[!all_lists$ok]
- cli::cli_abort(
- c("x" = "The following parameters are required for the {.code {model_template$name}} model template but are missing: {missing}",
- "i" = "They should be defined in one of the model blocks, likely {.code pri}, {.code sec}, {.code eqn}, or {.code out}.",
- " " = "Parameters defined in {.code pri} and {.code sec} are available to all blocks.",
- " " = "Parameters defined in other blocks are only available to that block."))
- }
- } # end parameter checks for Analytical model
-
-
- # if Analytical, need to combine sec and eqn
- if (type == "Analytical"){
- # shell function
- sec_eqn <- function() {}
- # define the body of the shell function
- body(sec_eqn) <- suppressWarnings(as.call(c(
- quote(`{`),
- as.list(body(self$arg_list$eqn))[-1], # remove outer `{` of f1
- as.list(body(self$arg_list$sec))[-1] # remove outer `{` of f2
- )))
-
- # this will include template and equations in both sec and eqn
- }
-
- # sec
- # still needed for analytic, because these equations will be used
- # in other blocks
- if (!is.null(self$arg_list$sec)){
- sec <- transpile_sec(self$arg_list$sec)
- } else {
- sec <- ""
- }
-
- # eqn
- if (type == "ODE") {
- eqn <- transpile_ode_eqn(self$arg_list$eqn, parameters, covariates, sec)
- } else if (type == "Analytical") {
-
- eqn <- transpile_analytic_eqn(sec_eqn, parameters, covariates)
- }
-
- # fa
- if (!is.null(self$arg_list$fa)) {
- fa <- transpile_fa(self$arg_list$fa, parameters, covariates, sec)
- } else {
- fa <- empty_fa()
- }
-
- # lag
- if (!is.null(self$arg_list$lag)) {
- lag <- transpile_lag(self$arg_list$lag, parameters, covariates, sec)
- } else {
- lag <- empty_lag()
- }
-
- # ini
- if (!is.null(self$arg_list$ini)) {
- ini <- transpile_ini(self$arg_list$ini, parameters, covariates, sec)
- } else {
- ini <- empty_ini()
- }
-
- # out
- if (!is.null(self$arg_list$out)) {
- out <- transpile_out(self$arg_list$out, parameters, covariates, sec)
- } else {
- out <- empty_out()
- }
-
- # err
- if(is.null(self$arg_list$err)) {
- cli::cli_abort(
- c("x" = "Error model is missing and required.",
- "i" = "Please see help for {.help PM_model()}.")
- )
- }
-
- #ensure length err matches length outeqs
- if (length(self$arg_list$err) != n_out) {
- cli::cli_abort(
- c("x" = "There must be one error model for each output equation.",
- "i" = "Please check the error model.")
- )
- }
- err <- self$arg_list$err
-
- # name
- name <- if(type == "Analytical") { model_template$name } else { "user" }
-
- # build the model list of rust components
- model_list <- list(
- pri = self$arg_list$pri,
- eqn = eqn,
- sec = sec,
- lag = lag,
- fa = fa,
- ini = ini,
- out = out,
- n_eqn = n_eqn,
- n_out = n_out,
- parameters = parameters,
- covariates = covariates,
- err = err,
- name = name
- )
- #make everything lower case if a character vector
- self$model_list <- purrr::map(model_list, \(x) {
- if (is.character(x)) {
- tolower(x)
- } else {
- x
- }
- })
-
- # this one needs to be capital
- self$model_list$type <- type
-
-
- extra_args <- list(...)
- if (!is.null(purrr::pluck(extra_args, "compile"))){
- if (extra_args$compile) {
- self$compile()
- }
- } else { # default is to compile
- self$compile()
+ file.copy(prior, "prior.csv", overwrite = TRUE) # ensure in current working directory
+ } else if (is.data.frame(prior)) {
+ # prior specified as a data frame
+ if (!all(c("prob", self$model_list$parameters) %in% names(prior))) {
+ cli::cli_abort(c("x" = "Error: {.arg prior} data frame must contain columns for parameters and probabilities.", "i" = "Check the data frame."))
+ }
+ prior <- prior %>% dplyr::select(all_of(self$model_list$parameters), prob)
+ write.csv(prior, "prior.csv", row.names = FALSE)
+ } else {
+ cli::cli_abort(
+ c("x" = "Error: {.arg prior} must be a numeric run number or character filename.", "i" = "Check the value.")
+ )
+ }
+ } else {
+ prior <- "sobol"
+ }
+
+
+
+ # get bolus info
+ if (self$model_list$name != "user") { # library model
+ bolus_models <- model_lib(show = FALSE) %>%
+ filter(stringr::str_detect(Compartments, "Bolus")) %>%
+ pull(Name)
+ bolus <- ifelse(self$model_list$name %in% bolus_models, 1, NA) # may need to generalize if models with multiple bolus compartments are added
+ } else { # user model
+ eqns <- func_to_char(self$arg_list$eqn)
+ bolus_comps <- stringr::str_which(eqns, stringr::regex("(b|bolus)\\[\\d+\\]", ignore_case = TRUE))
+ bolus_inputs <- as.integer(stringr::str_match(eqns[bolus_comps], stringr::regex("(b|bolus)\\[(\\d+)\\]", ignore_case = TRUE))[, 3])
+ }
+
+
+ if (intern) {
+ ### CALL RUST
+ out_path <- normalizePath(file.path(path_run, "outputs"), mustWork = FALSE)
+ msg <- c(msg, "Run results were saved in folder '{.path {out_path}}'")
+ rlang::try_fetch(
+ fit(
+ # defined in extendr-wrappers.R
+ model_path = normalizePath(self$binary_path),
+ data = normalizePath(file.path(path_run, "inputs", "gendata.csv")),
+ params = list(
+ ranges = ranges, # not important but needed for POSTPROB
+ algorithm = algorithm,
+ error_models = lapply(self$model_list$err, function(x) x$flatten()),
+ idelta = idelta,
+ tad = tad,
+ max_cycles = cycles, # will be hardcoded in Rust to 0 for POSTPROB
+ prior = prior, # needs warning if missing and algorithm = POSTPROB
+ points = points, # only relevant for sobol prior
+ seed = seed
+ ),
+ output_path = out_path,
+ kind = tolower(self$model_list$type)
+ ),
+ error = function(e) {
+ cli::cli_warn("Unable to create {.cls PM_result} object", parent = e)
+ return(NULL)
+ }
+ )
+
+ PM_parse(path = out_path)
+ res <- PM_load(path = normalizePath(out_path), file = "PMout.Rdata")
+ if (report != "none") {
+ valid_report <- tryCatch(
+ PM_report(res, path = normalizePath(out_path), template = report, quiet = TRUE),
+ error = function(e) {
+ -1
}
-
- },
-
- #' @description
- #' Print the model summary.
- #' @details
- #' This method prints a summary of the model.
- #' @param ... Not used.
- print = function(...) {
-
- cli::cli_div(theme = list(
- span.eqs = list(color = navy())
- ))
-
- cli::cli_h1("Model summary")
-
- cli::cli_h3(text = "Primary Parameters")
- # pars = self$model_list$parameters
- # cli::cli_text("{.eqs {pars}}")
-
- self$arg_list$pri %>%
- purrr::imap(\(x,y) cli::cli_text("{.strong {y}}: [{.strong {x$min}}, {.strong {x$max}}], {.emph ~N({round(x$mean,2)}}, {.emph {round(x$sd,2)})}")) %>%
- invisible() # to suppress NULL
-
-
- if (!is.null(self$model_list$covariates)) {
- cli::cli_h3(text = "Covariates")
-
- cov_list <- paste0(self$model_list$covariates,
- ifelse(self$arg_list$cov==1, "", " (no interpolation)"))
-
- cli::cli_text("{.eqs {cov_list}}")
- }
-
- if(!is.null(self$arg_list$sec)){
- cli::cli_h3(text = "Secondary (Global) Equations")
- eqs <- func_to_char(self$arg_list$sec) #function in PMutitlities
- for (i in eqs) {
- cli::cli_text("{.eqs {i}}")
- }
- }
-
- if(!is.null(self$arg_list$tem)){
- cli::cli_h3(text = "Analytical Model")
- cli::cli_text("{.eqs {self$arg_list$tem$name}})")
- }
-
- if(!is.null(self$arg_list$eqn)){
- cli::cli_h3(text = "Primary Equations")
- eqs <- func_to_char(self$arg_list$eqn) #function in PMutitlities
- for (i in eqs) {
- cli::cli_text("{.eqs {i}}")
- }
- }
-
- if(!is.null(self$arg_list$lag)){
- cli::cli_h3(text = "Lag Time")
- eqs <- func_to_char(self$arg_list$lag) #function in PMutitlities
- for (i in eqs) {
- cli::cli_text("{.eqs {i}}")
- }
- }
-
- if(!is.null(self$arg_list$fa)){
- cli::cli_h3(text = "Bioavailability (Fraction Absorbed)")
- eqs <- func_to_char(self$arg_list$fa) #function in PMutitlities
- for (i in eqs) {
- cli::cli_text("{.eqs {i}}")
- }
- }
-
- if(!is.null(self$arg_list$ini)){
- cli::cli_h3(text = "Initial Conditions")
- eqs <- func_to_char(self$arg_list$ini) #function in PMutitlities
- for (i in eqs) {
- cli::cli_text("{.eqs {i}}")
- }
- }
-
- cli::cli_h3(text = "Outputs")
- outs <- func_to_char(self$arg_list$out)
- for (i in outs) {
- cli::cli_text("{.eqs {i}}")
- }
-
- cli::cli_h3(text = "Error Model")
- for (i in self$model_list$err) {
- if (i$fixed) {
- cli::cli_text("{.strong {tools::toTitleCase(i$type)}}, with fixed value of {.val {i$initial}} and coefficients {.val {i$coeff}}.")
- } else {
- cli::cli_text("{.strong {tools::toTitleCase(i$type)}}, with initial value of {.val {i$initial}} and coefficients {.val {i$coeff}}.")
- }
- }
- cli::cli_end()
-
- invisible(self)
- },
- #' @description
- #' Plot the model.
- #' @details
- #' This method plots the model using the
- #' [plot.PM_model()] function.
- #' @param ... Additional arguments passed to the plot function.
- plot = function(...) {
- tryCatch(
- plot.PM_model(self, ...),
- error = function(e) {
- cat(crayon::red("Error:"), e$message, "\n")
- }
- )
- },
- #' @description
- #' This is the main method to run a population analysis.
- #' @details
- #' As of Pmetrics 3.0.0, models contain compiled code to fit
- #' the model equations to the data, optimizing the parameter
- #' value probability distributions in the population to
- #' maximize their likelihood, or more precisely, minimize
- #' the objective function, which is -2*log-likelihood.
- #'
- #' The `$fit()` method is the means of running that compiled
- #' code to conduct to fitting procedure. At a minimum, it requires
- #' a [PM_data] object, which can be created with
- #' `PM_data$new()`. There are a number of additional arguments
- #' to control the fitting procedure, such as the number of cycles
- #' to run, the initial number of support points,
- #' and the algorithm to use, among others.
- #'
- #' The `$fit()` method is the descendant of the legacy
- #' [NPrun] function, which is maintained as a wrapper to `$fit()`
- #' for backwards compatibility.
- #'
- #' @param data Either the name of a [PM_data]
- #' object in memory or the quoted filename (with or without a path) of a Pmetrics
- #' data file. If the path is not specified, the file is assumed to be in the current working directory,
- #' unless the `path` argument below is also specified as a global option for the fit.
- #' The file will be used to create a [PM_data]
- #' object on the fly. However, if created on the fly, this object
- #' will not be available to other
- #' methods or other instances of `$fit()`.
- #' @param path Optional full path or relative path from current working directory
- #' to the folder where `data` and `model` are located if specified as filenames without
- #' their own paths,
- #' and where the output will be saved. Default is the current working directory.
- #' @param run Specify the run number of the output folder. Default if missing is the next available number.
- #' @param include Vector of subject id values in the data file to include in the analysis.
- #' The default (missing) is all.
- #' @param exclude A vector of subject IDs to exclude in the analysis, e.g. `c(4,6:14,16:20)`
- # #' @param ode Ordinary Differential Equation solver log tolerance or stiffness.
- # Default is -4, i.e. 0.0001. Higher values will result in faster
- # #' runs, but parameter estimates may not be as accurate.
- # #' @param tol Tolerance for convergence of NPAG. Smaller numbers make it harder to converge.
- # #' Default value is 0.01.
- # #' @param salt Vector of salt fractions for each drug in the data file, default is 1 for each drug. This is not the same as bioavailability.
- #' @param cycles Number of cycles to run. Default is 100.
- #' @param prior The distribution for the initial support points, which can be
- #' one of several options.
- #' * The default is "sobol", which is a semi-random distribution. This is the distribution
- #' typically used when fitting a new model to the data. An example of this is
- #' on our [website](https://www.lapk.org/images/sobol_3d_plot.html).
- #'
- #' The following all specify non-random, informative prior distributions. They
- #' are useful for either continuing a previous
- #' run which did not converge or for fitting a model to new data, whether to simply
- #' calculate Bayesian posteriors with `cycles = 0` or to revise the model to a new
- #' covergence with the new data.
- #' * The name of a suitable [PM_result] object from a prior run loaded with [PM_load].
- #' This starts from the non-uniform, informative distribution obtained at the end of a prior NPAG run.
- #' Example: `run1 <- PM_load(1); fit1$run(prior = run1)`.
- #'
- #' * A character string with the filename of a csv file containing a prior distribution with
- #' format as for 'theta.csv' in the output folder of a prior run: column headers are parameter
- #' names, and rows are the support point values. A final column with probabilities
- #' for each support point is not necessary, but if present will be ignored, as these
- #' probabilities are calculated by the engine. Note that the parameter names must match the
- #' names of the primary variables in the model. Example: `fit1$run(prior = "mytheta.csv")`.
- #' * The number of a previous run with `theta.csv` in the output folder which will be read
- #' as for the filename option above. Example: `fit1$run(prior = 2)`.
- #' * A data frame obtained from reading an approriate file, such that the data frame
- #' is in the required format described in the filename option above. Example:
- #' `mytheta <- read_csv("mytheta.csv"); fit1$run(prior = mytheta)`.
- #'
- #' @param points The number of initial support points if one of
- #' the semi-random, uniform distributions are selected in the `prior` argument
- #' above. Default is 100. The initial points are
- #' spread through the hyperspace defined by the random parameter ranges
- #' and begin the search for the optimal
- #' parameter value distribution (support points) in the population.
- #' If there are fewer than 2 points per unit range for any parameter,
- #' Pmetrics will suggest the minimum number of points that should be tried.
- #' The greater the initial number of points, the less chance of
- #' missing the globally maximally likely parameter value distribution,
- #' but the slower the run.
- #'
- #' @param idelta How often to generate posterior predictions in units of time.
- #' Default is 0.1, which means a prediction is generated every 0.1 hours (6 minutes)
- #' if the unit of time is hours. Predictions are made at this interval until the time
- #' of the last event (dose or observation) or until `tad` if that value is greater
- #' than the time of the last dose or observation in the data.
- #'
- #' @param tad Length of time after the last dose event to add additional predictions
- #' at frequency `idelta`. Default is 0, which means no additional predictions
- #' beyond the last dose, assuming the dose is the last event. . If the
- #' last observation in the data is after `tad`, then a prediction will be generated at
- #' time = `tad` after the last dose
- #'
- #' @param seed Seed used if `prior = "sobol"`. Ignored otherwise.
- #' @param intern Run NPAG in the R console without a batch script. Default is TRUE.
- # #' @param quiet Boolean operator controlling whether a model summary report is given. Default is `TRUE`.
- #' @param overwrite Boolean operator to overwrite existing run result folders. Default is `FALSE`.
- # #' @param nocheck Suppress the automatic checking of the data file with [PM_data]. Default is `FALSE`.
- # #' @param parallel Run NPAG in parallel. Default is `NA`, which will be set to `TRUE` for models that use
- # #' differential equations, and `FALSE` for analytical/explicit models. The majority of the benefit for parallelization comes
- # #' in the first cycle, with a speed-up of approximately 80\% of the number of available cores on your machine, e.g. an 8-core machine
- # #' will speed up the first cycle by 0.8 * 8 = 6.4-fold. Subsequent cycles approach about 50\%, e.g. 4-fold increase on an 8-core
- # #' machine. Overall speed up for a run will therefore depend on the number of cycles run and the number of cores.
- #' @param algorithm The algorithm to use for the run. Default is "NPAG" for the **N**on-**P**arametric **A**daptive **G**rid. Alternatives: "NPOD".
- #' @param report If missing, the default Pmetrics report template as specified in [getPMoptions]
- #' is used. Otherwise can be "plotly", "ggplot", or "none".
- #' @return A successful run will result in creation of a new folder in the working
- #' directory with the results inside the folder.
- #'
- #' @author Michael Neely
- #' @export
- fit = function(data = NULL,
- path = ".",
- run = NULL,
- include = NULL,
- exclude = NULL,
- cycles = 100,
- prior = "sobol",
- points = 100,
- idelta = 0.1,
- tad = 0,
- seed = 23,
- overwrite = FALSE,
- algorithm = "NPAG", #POSTPROB for posteriors, select when cycles = 0, allow for "NPOD"
- report = getPMoptions("report_template")) {
-
- msg <- "" # status message at end of run
-
- path <- stringr::str_replace(path, "/$", "") # remove trailing /
-
- if (is.null(data)) {
- cli::cli_abort(c("x" = " {.arg data} must be specified."))
- }
-
- if (is.null(self$model_list)) {
- cli::cli_abort(c("x" = "Model is malformed."))
- }
-
- if (is.character(data)) {
- # create PM_data object from file
- data <- PM_data$new(normalizePath(file.path(path, data), mustWork = FALSE))
- }
-
- if (!inherits(data, "PM_data")) {
- data <- tryCatch({
- PM_data$new(data)
- }, error = function(e) {
- cli::cli_abort(
- c("x" = "{.code data} must be a {.cls PM_data} object or an appropriate data frame.", "i" = "See help for {.fn Pmetrics::PM_data}.")
- )
- })
- }
-
- #### checks
-
- # covariates
- modelCov <- self$model_list$cov
- if (length(modelCov) > 0) {
- dataCov <- tolower(getCov(data)$covnames)
- missingCov <- modelCov[!modelCov %in% dataCov]
- if (length(missingCov) > 0) { # if not identical, abort
- msg <- glue::glue("{paste(modelCov, collapse = ', ')} {?is/are} missing from the data.")
- cli::cli_abort(c("x" = msg))
- }
- }
-
- # cycles
- # if programmer is a crazy Norwegian....
- if (cycles < 0){
- cli::cli_abort(c("x" = "Error: {.arg cycles} must be 0 or greater.", "i" = "See {.code $fit()} method for {.help PM_model}."))
- }
-
- # output equations
-
- if (!is.null(data$standard_data$outeq)) {
- dataOut <- max(data$standard_data$outeq, na.rm = TRUE)
- } else {
- dataOut <- 1
- }
-
- modelOut <- self$model_list$n_out
-
-
- # check if model compiled and if not, do so
- self$compile()
-
- intern <- TRUE # always true until (if) rust can run separately from R
-
-
- # make new output directory
-
- if (is.null(run)) {
- olddir <- list.dirs(path, recursive = FALSE)
- olddir <- olddir[grep("^\\./[[:digit:]]+", olddir)]
- olddir <- sub("^\\./", "", olddir)
- if (length(olddir) > 0) {
- run <- as.character(max(as.numeric(olddir)) + 1)
- } else {
- run <- "1"
- }
- } else {
- if (!is.numeric(run)) {
- cli::cli_abort(c("x" = " {.arg run} must be numeric."))
- }
- }
-
- path_run <- normalizePath(file.path(path, run), mustWork = FALSE)
-
- if (file.exists(path_run)) {
- if (overwrite) {
- unlink(path_run, recursive = TRUE)
- msg <- c(msg, "The previous run in folder '{path_run}' was overwritten.")
- } else {
- cli::cli_inform(
- c("i" = "The previous run from '{path_run}' was read.", " " = "Set {.arg overwrite} to {.val TRUE} to overwrite prior run in '{path_run}'.")
- )
- return(invisible(PM_load(file = normalizePath(file.path(path_run, "PMout.Rdata"), mustWork = FALSE))))
- }
- }
-
- fs::dir_create(path_run)
-
-
- #### Algorithm ####
- algorithm <- toupper(algorithm)
- if (cycles == 0) {
- if (prior == "sobol") {
- cli::cli_warn(c("!" = "Error: Cannot use {.code prior = 'sobol'} with {.code cycles = 0}.", "i" = "Use a prior from a previous run."))
- }
- algorithm <- "POSTPROB"
- } else {
- if (!(algorithm %in% c("NPAG", "NPOD"))) {
- cli::cli_abort(c("x" = "Error: Unsupported algorithm.", "i" = "Supported algorithms are 'NPAG' and 'NPOD'."))
- }
- }
- if (algorithm == "POSTPROB" && cycles > 0) {
- cli::cli_warn(c("!" = "Warning: {.code algorithm = 'POSTPROB'} is used with {.code cycles = 0}.", "i" = "Continuing with {.code cycles = 0}."))
- cycles <- 0
- }
-
-
-
- if (getPMoptions()$backend != "rust") {
- cli::cli_abort(c("x" = "Error: unsupported backend.", "i" = "See help for {.fn setPMoptions}"))
- }
-
- #### Include or exclude subjects ####
- if (is.null(include)) {
- include <- unique(data$standard_data$id)
- }
- if (is.null(exclude)) {
- exclude <- NA
- }
- data_filtered <- data$standard_data %>% includeExclude(include, exclude)
-
- if (nrow(data_filtered) == 0) {
- cli::cli_abort("x" = "No subjects remain after filtering.")
- return(invisible(NULL))
- }
-
- #### Save input objects ####
- fs::dir_create(normalizePath(file.path(path_run, "inputs"), mustWork = FALSE))
- PM_data$new(data_filtered, quiet = TRUE)$save(normalizePath(file.path(path_run, "inputs", "gendata.csv"), mustWork = FALSE), header = FALSE)
- saveRDS(list(data = data, model = self), file = normalizePath(file.path(path_run, "inputs", "fit.rds"), mustWork = FALSE))
- file.copy(self$binary_path, normalizePath(file.path(path_run, "inputs"), mustWork = FALSE))
-
- # Get ranges and calculate points
- ranges <- lapply(self$model_list$pri, function(x) {
- c(x$min, x$max)
- })
-
- names(ranges) <- tolower(names(ranges))
- # Set initial grid points (only applies for sobol)
- marginal_densities <- sapply(ranges, function(x) {
- points / (x[2] - x[1])
- })
- if (any(marginal_densities < 2)) {
- increase_to <- round(points * (max(2/marginal_densities)), 0)
- msg <- c(msg, "Recommend increasing {.arg points} to at least {increase_to} to ensure adequate coverage of parameter space.")
- }
-
-
-
- # set prior
- if (prior != "sobol") {
- if (is.numeric(prior)) {
- # prior specified as a run number
- if (!file.exists(glue::glue("{path}/{prior}/outputs/theta.csv"))) {
- cli::cli_abort(c("x" = "Error: {.arg prior} file does not exist.", "i" = "Check the file path."))
- }
- file.copy(glue::glue("{path}/{prior}/outputs/theta.csv"), "prior.csv", overwrite = TRUE)
- prior <- "prior.csv"
- } else if (is.character(prior)) {
- # prior specified as a filename
- if (!file.exists(prior)) {
- cli::cli_abort(c("x" = "Error: {.arg prior} file does not exist.", "i" = "Check the file path."))
- }
- file.copy(prior, "prior.csv", overwrite = TRUE) # ensure in current working directory
- } else if (is.data.frame(prior)){
- # prior specified as a data frame
- if (!all(c("prob", self$model_list$parameters) %in% names(prior))) {
- cli::cli_abort(c("x" = "Error: {.arg prior} data frame must contain columns for parameters and probabilities.", "i" = "Check the data frame."))
- }
- prior <- prior %>% dplyr::select(all_of(self$model_list$parameters), prob)
- write.csv(prior, "prior.csv", row.names = FALSE)
-
- } else {
- cli::cli_abort(
- c("x" = "Error: {.arg prior} must be a numeric run number or character filename.", "i" = "Check the value.")
- )
- }
+ )
+ if (valid_report == 1) {
+ msg <- c(msg, "Report generated with {report} template.")
+ # if(tolower(algorithm) == "postprob") {this_alg <- "map"} else {this_alg <- "fit"}
+ msg <- c(msg, "If assigned to a variable, e.g. {.code run{run} <-}, results are available in {.code run{run}}.")
+ } else {
+ msg <- c(msg, "Report could not be generated.")
+ }
+ }
+
+
+ if (length(msg) > 1) {
+ cli::cli_h1("Notes:")
+ cli::cli_ul()
+ purrr::walk(msg[-1], ~ cli::cli_li(.x))
+ cli::cli_end()
+ }
+ return(invisible(res))
+ } else {
+ cli::cli_abort(
+ c("x" = "Error: Currently, the rust engine only supports internal runs.", "i" = "This is a temporary limitation.")
+ )
+ }
+ }, # end fit method
+
+ #' @description
+ #' Calculate posteriors from a fitted model.
+ #' #' @details
+ #' This method calculates posteriors from a compiled model. It is not necessary to have
+ #' run the model first, but it is necessary to have an informative prior distribution.
+ #' This prior will typically be the result of a previous run, but may also be a file
+ #' containing support points, with each column named as a parameter in the model plus a final column
+ #' for probability. Each row contains values for the parameters and the associated probability for
+ #' those parameter values. The file can be saved as a csv file.
+ #'
+ #' To calculate the posteriors, `map()` calls the `fit()` method with the `cycles` argument set to 0
+ #' and the `algorithm` argument set to "POSTPROB". If `data` are not provided as an argument to
+ #' `map()`, the model's `data` field is used instead. If `data` is provided, it must be a
+ #' [PM_data] object or the pathname of a file which can be loaded as a [PM_data] object.
+ #' @param ... Arguments passed to the `fit` method. Note that the `cycles` argument is set to 0,
+ #' and the `algorithm` argument is set to "POSTPROB" automatically.
+ map = function(...) {
+ # browser()
+ args <- list(...)
+
+ if (!is.null(purrr::pluck(args, "cycles")) && purrr::pluck(args, "cycles") != 0) {
+ cli::cli_inform(c("i" = "{.arg cycles} set to 0 for posteriors"))
+ }
+ args$cycles <- 0 # ensure cycles is set to 0
+
+
+ if (!is.null(purrr::pluck(args, "algorithm")) && purrr::pluck(args, "algorithm") != "POSTPROB") {
+ cli::cli_inform(c("i" = "{.arg algorithm} set to POSTPROB for posteriors"))
+ }
+ args$algorithm <- "POSTPROB" # ensure algorithm is set to POSTPROB
+
+
+ if (is.null(purrr::pluck(args, "data"))) {
+ cli::cli_abort(c("x" = "Data must be specified for posteriors."))
+ }
+
+ if (is.null(purrr::pluck(args, "prior")) || purrr::pluck(args, "prior") == "sobol") {
+ cli::cli_abort(c(
+ "x" = "Please specify a non-uniform prior for posteriors.",
+ " " = "This can be a prior run number or the name of a file with support points."
+ ))
+ }
+
+ do.call(self$fit, args)
+ },
+ #' @description
+ #' Simulate data from the model using a set of parameter values.
+ #' @details
+ #' This method simulates output from the model using a set of parameter values
+ #' provided in the `theta` matrix and the template for dosing/observations in
+ #' the `data` object.
+ #' @param data A [PM_data] object containing the dosing and observation information.
+ #' @param theta A matrix of parameter values to use for the simulation.
+ #' The `theta` matrix should have the same number of columns as the number of primary parameters in the model.
+ #' Each row of `theta` represents a different set of parameter values.
+ #'
+ sim = function(data, theta) {
+ if (!inherits(data, "PM_data")) {
+ cli::cli_abort(c("x" = "Data must be a PM_data object."))
+ }
+ if (!is.matrix(theta)) {
+ cli::cli_abort(c("x" = "theta must be a matrix."))
+ }
+ if (!is.numeric(theta)) {
+ cli::cli_abort(c("x" = "theta must be a matrix of numeric values."))
+ }
+ if (ncol(theta) != length(private$get_primary())) {
+ cli::cli_abort(c("x" = "theta must have the same number of columns as the number of parameters."))
+ }
+
+
+ temp_csv <- tempfile(fileext = ".csv")
+ data$save(temp_csv, header = FALSE)
+ if (getPMoptions()$backend == "rust") {
+ if (is.null(self$binary_path)) {
+ self$compile()
+ if (is.null(self$binary_path)) {
+ cli::cli_abort(c("x" = "Model must be compiled before simulating."))
+ }
+ }
+ sim <- simulate_all(temp_csv, self$binary_path, theta, kind = tolower(self$model_list$type))
+ } else {
+ cli::cli_abort(c("x" = "This function can only be used with the rust backend."))
+ }
+ return(sim)
+ },
+ #' @description
+ #' Compile the model to a binary file.
+ #' @details
+ #' This method write the model to a Rust file in a temporary path,
+ #' updates the `binary_path` field for the model, and compiles that
+ #' file to a binary file that can be used for fitting or simulation.
+ #' If the model is already compiled, the method does nothing.
+ #'
+ compile = function() {
+ if (!is.null(self$binary_path) && file.exists(self$binary_path)) {
+ # model is compiled
+ return(invisible(NULL))
+ }
+
+ model_path <- file.path(tempdir(), "model.rs")
+ private$write_model_to_rust(model_path)
+ output_path <- tempfile(pattern = "model_", fileext = ".pmx")
+ cli::cli_inform(c("i" = "Compiling model..."))
+ tryCatch(
+ {
+ compile_model(model_path, tools::R_user_dir(package = "Pmetrics"), output_path, private$get_primary(), kind = tolower(self$model_list$type))
+ self$binary_path <- output_path
+ },
+ error = function(e) {
+ cli::cli_abort(
+ c("x" = "Model compilation failed: {e$message}", "i" = "Please check the model file and try again.")
+ )
+ }
+ )
+
+ file.remove(model_path) # remove temporary model file
+ return(invisible(self))
+ },
+ #' @description
+ #' Update the model using recursive lists of changes and recompile the updated model.
+ #' @param ... Named elements corresponding to the blocks in the model,
+ #' such as "pri", "cov", "sec", "eqn", "ini", "lag", "fa", "out", and "err".
+ #' For each block, create a list of changes, which may be additions, edits, or deletions.
+ #' For deletions, set the value to `NULL`.
+ #'
+ update = function(...) {
+ changes <- list(...)
+ keys <- names(changes)
+ if (!all(tolower(keys) %in% c("pri", "cov", "sec", "eqn", "ini", "lag", "fa", "out", "err"))) {
+ cli::cli_abort(c(
+ "x" = "Invalid block name: {keys}",
+ "i" = "See help for {.fn PM_model}."
+ ))
+ }
+ self$arg_list <- modifyList2(self$arg_list, changes)
+ self <- PM_model$new(self$arg_list) # recreate and recompile the model
+ return(invisible(self))
+ }
+ ), # end public list
+ private = list(
+ R6fromFile = function(file) {
+ msg <- ""
+ blocks <- parseBlocks(file) # this function is in PMutilities
+ # check for reserved variable names
+ reserved <- c(
+ "t",
+ "x",
+ "dx",
+ "p",
+ "rateiv",
+ "cov",
+ "y"
+ )
+ conflict <- c(match(tolower(blocks$primVar), reserved, nomatch = -99), match(tolower(blocks$secVar), reserved, nomatch = -99), match(tolower(blocks$covar), reserved, nomatch = -99))
+ nconflict <- sum(conflict != -99)
+ if (nconflict > 0) {
+ msg <- paste("\n", paste(paste("'", reserved[conflict[conflict != -99]], "'", sep = ""), collapse = ", "), " ", c("is a", "are")[1 + as.numeric(nconflict > 1)], " reserved ", c("name", "names")[1 + as.numeric(nconflict > 1)], ", regardless of case.\nPlease choose non-reserved parameter/covariate names.\n", sep = "")
+ return(list(status = -1, msg = msg))
+ }
+
+ if (length(grep(";", blocks$primVar)) > 0) {
+ # using ';' as separator
+ sep <- ";"
+ } else {
+ if (length(grep(",", blocks$primVar)) > 0) {
+ # using ',' as separator
+ sep <- ","
+ } else {
+ return(list(status = -1, msg = "\nPrimary variables should be defined as 'var,lower_val,upper_val' or 'var,fixed_val'.\n"))
+ }
+ }
+
+ # build arg_list
+ arg_list <- list()
+ # this function makes pri for PM_model
+ arg_list$pri <- sapply(strsplit(blocks$primVar, sep), function(x) {
+ # find out if constrained to be positive
+ const_pos <- any(grepl("\\+", x))
+ if (const_pos) {
+ x <- gsub("\\+", "", x)
+ cli::cli_inform(c(
+ "i" = "Truncating variables to positive ranges is not required for NPAG/NPOD",
+ " " = "This may be updated as parametric algorithms come online, but will be ignored for now."
+ ))
+ }
+
+ # find out if constant
+ const_var <- any(grepl("!", x))
+ if (const_var) {
+ x <- gsub("!", "", x)
+ cli::cli_abort(c("x" = "Constants should be defined in the appropriate block, not #PRI."))
+ }
+
+ values <- as.numeric(x[-1])
+
+ if (length(x[-1]) == 1) { # fixed
+ cli::cli_abort(c(
+ "x" = "Fixed but unknown are no longer supported.",
+ "i" = "If necessary, fit them as random and then use a fixed value in subsequent runs."
+ ))
+ } else { # range
+ thisItem <- list(ab(values[1], values[2]))
+ }
+ names(thisItem) <- x[1]
+ thisItem
+ }) # end sapply
+
+ # covariates
+ covar <- blocks$covar
+ const_covar <- grepl("!", covar) # returns boolean vector, length = ncov
+ covar <- gsub("!", "", covar) # remove "!"
+ covar_list <- tolower(covar)
+
+ # add to arg_list
+ arg_list$cov <- purrr::map_vec(const_covar, \(x){
+ type <- ifelse(!x, "lm", "none")
+ interp(type)
+ }) %>%
+ purrr::set_names(covar_list)
+
+
+ # extra
+ # if (blocks$extra[1] != "") {
+ # arg_list$ext <- blocks$extra
+ # } else {
+ # arg_list$extra <- NULL
+ # }
+
+ # secondary variables
+ if (blocks$secVar[1] != "") {
+ arg_list$sec <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$secVar, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$sec <- NULL
+ }
+
+ # bioavailability
+ if (blocks$f[1] != "") {
+ arg_list$fa <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$f, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$fa <- NULL
+ }
+
+ # bolus
+ if (blocks$bol[1] != "") {
+ cli::cli_inform(c(
+ "i" = "The bolus block is no longer used as of Pmetrics 3.0.0.",
+ " " = "Indicate bolus inputs as {.code B[x]} in equations, where {.code x} is the input number."
+ ))
+ }
+
+ # initial conditions
+ if (blocks$ini[1] != "") {
+ arg_list$ini <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$ini, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$ini <- NULL
+ }
+
+ # lag time
+ if (blocks$lag[1] != "") {
+ arg_list$lag <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$lag, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$lag <- NULL
+ }
+
+ # differential equations - legacy
+ if (!is.null(blocks$diffeq) && blocks$diffeq[1] != "") {
+ cli::cli_inform(c(
+ "i" = "The #DIFFEQ block is no longer used as of Pmetrics 3.0.0.",
+ " " = "The block is now called #EQN for more general equations.",
+ " " = "Equations have been moved to the {.code eqn} element."
+ ))
+ arg_list$eqn <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$diffeq, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$eqn <- NULL
+ }
+
+ # model equations - will eventually replace diffeq above
+ if (blocks$eqn[1] != "") {
+ arg_list$eqn <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$eqn, collapse = '\n ')}\n}}")))
+ } else {
+ arg_list$eqn <- NULL
+ }
+
+ # out/err
+ n_outputLines <- length(blocks$output)
+ outputLines <- grep("y\\([[:digit:]]+\\)|y\\[[[:digit:]]+\\]", blocks$output)
+ if (length(outputLines) == 0) {
+ return(list(status = -1, msg = "\nYou must have at least one output equation of the form 'Y[1] = ...'\n"))
+ }
+ # otherLines <- (1:n_outputLines)[!(1:n_outputLines) %in% outputLines] # find other lines
+ # if (length(otherLines) > 0) {
+ # arg_list$sec <- c(arg_list$sec, blocks$output[otherLines]) # append to #sec block
+ # }
+ # output <- blocks$output[outputLines]
+ # remParen <- stringr::str_replace(blocks$output, regex("Y(?:\\[|\\()(\\d+)(?:\\]|\\))", ignore_case = TRUE), "Y\\1")
+ # diffeq <- stringr::str_split(remParen, "\\s*=\\s*")
+ # diffList <- sapply(diffeq, function(x) x[2])
+ # num_out <- length(diffList)
+
+ arg_list$out <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$out, collapse = '\n ')}\n}}")))
+
+ err <- tolower(gsub("[[:space:]]", "", blocks$error))
+ # process constant gamma/lambda
+ err_type <- c("additive", "proportional")[1 + grepl("^g", err[1])]
+ const_gamlam <- grepl("!", err[1])
+ gamlam_value <- as.numeric(stringr::str_match(err[1], "\\d+\\.?\\d*"))
+ # process constant coefficients
+ const_coeff <- grepl("!", err[-1]) # returns boolean vector, length = nout
+ err <- gsub("!", "", err) # remove "!"
+
+ coeff_fxns <- err[-1] %>%
+ purrr::imap(\(x, idx) {
+ glue::glue("{err_type}({gamlam_value}, c({x}), {const_coeff[{idx}]})")
+ }) %>%
+ unlist()
+
+ arg_list$err <- eval(parse(text = glue::glue("c(\n{paste({coeff_fxns}, collapse = ',\n')}\n)")))
+
+ cat(msg)
+ flush.console()
+ return(arg_list)
+ }, # end R6fromFile
+
+ write_model_to_rust = function(file_path = "main.rs") {
+ # Check if model_list is not NULL
+ if (is.null(self$model_list)) {
+ cli::cli_abort(c("x" = "Model list is empty.", "i" = "Please provide a valid model list."))
+ }
+
+ if (self$model_list$type %in% c("Analytical", "ODE")) {
+ placeholders <- c("eqn", "lag", "fa", "ini", "out", "n_eqn", "n_out")
+ base <- paste0(
+ "#[allow(unused_mut)]\nequation::",
+ self$model_list$type,
+ "::new(\n",
+ paste("<", placeholders[1:5], ">", sep = "", collapse = ",\n "),
+ ",\n (",
+ paste("<", placeholders[6:7], ">", sep = "", collapse = ", "),
+ "),\n)"
+ )
+ } else {
+ cli::cli_abort(c("x" = "Invalid model type.", "i" = "Please provide a valid model type."))
+ }
+
+
+ # Replace placeholders in the base string with actual values from model_list
+ base <- placeholders %>%
+ purrr::reduce(\(x, y) stringr::str_replace(x, stringr::str_c("<", y, ">"), as.character(self$model_list[[y]])), .init = base)
+ # Write the model to a file
+ writeLines(base, file_path)
+ },
+ from_file = function(file_path) {
+ self$model_list <- private$makeR6model(model_filename)
+ # self$content <- readChar(model_filename, file.info(model_filename)$size)
+ },
+ get_primary = function() {
+ return(tolower(self$model_list$parameters))
+ }
+ ) # end private
+) # end R6Class PM_model
+
+##### These functions create various model components
+
+#' @title Additive error model
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Create an additive (lambda) error model
+#' @param initial Initial value for lambda
+#' @param coeff Vector of coefficients defining assay error polynomial
+#' @param fixed Estimate if `FALSE` (default).
+#' @export
+additive <- function(initial, coeff, fixed = FALSE) {
+ PM_err$new(type = "additive", initial = initial, coeff = coeff, fixed = fixed)
+}
+
+
+
+#' @title Proportional error model
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Create an proportional (gamma) error model
+#' @param initial Initial value for gamma
+#' @param coeff Vector of coefficients defining assay error polynomial
+#' @param fixed Estimate if `FALSE` (default).
+#' @export
+proportional <- function(initial, coeff, fixed = FALSE) {
+ PM_err$new(type = "proportional", initial = initial, coeff = coeff, fixed = fixed)
+}
+
+PM_err <- R6::R6Class(
+ "PM_err",
+ public = list(
+ #' @field type Type of error model, either "additive" or "proportional".
+ type = NULL,
+ #' @field initial Initial value for the error model.
+ initial = NULL,
+ #' @field coeff Coefficients for the assay error polynomial.
+ coeff = NULL,
+ #' @field fixed If `TRUE`, the error model is fixed and not estimated.
+ fixed = NULL,
+ initialize = function(type, initial, coeff, fixed) {
+ self$type <- type
+ self$initial <- initial
+ self$coeff <- coeff
+ self$fixed <- fixed
+ },
+ print = function() {
+ if (self$fixed) {
+ cli::cli_text("{.strong {tools::toTitleCase(self$type)}}, with fixed value of {.emph {self$initial}} and coefficients {.emph {paste(self$coeff, collapse = ', ')}}.")
+ } else {
+ cli::cli_text("{.strong {tools::toTitleCase(self$type)}}, with initial value of {.emph {self$initial}} and coefficients {.emph {paste(self$coeff, collapse = ', ')}}.")
+ }
+ },
+ flatten = function() {
+ list(initial = self$initial, coeff = self$coeff, type = self$type, fixed = self$fixed)
+ }
+ )
+)
+
+#' @title Primary parameter values
+#' @description
+#' `r lifecycle::badge("experimental")`
+#' Define primary model parameter object.
+#' This is used internally by the `PM_model` class.
+#' @keywords internal
+PM_pri <- R6::R6Class(
+ "PM_pri",
+ public = list(
+ #' @field min Minimum value of the range.
+ min = NULL,
+ #' @field max Maximum value of the range.
+ max = NULL,
+ #' @field mean Mean value of the range, calculated as (min + max) / 2.
+ mean = NULL,
+ #' @field sd Standard deviation of the range, calculated as (max - min) / 6.
+ sd = NULL,
+ #' @description
+ #' Initialize a new range object.
+ #' @param min Minimum value of the range.
+ #' @param max Maximum value of the range.
+ initialize = function(min, max) {
+ self$min <- min
+ self$max <- max
+ self$mean <- (min + max) / 2
+ self$sd <- (max - min) / 6
+ },
+ #' @description
+ #' Print the range.
+ print = function() {
+ cli::cli_text("[{.strong {self$min}}, {.strong {self$max}}], {.emph ~N({round(self$mean,2)}}, {.emph {round(self$sd,2)})}")
+ }
+ )
+)
+
+
+#' @title Initial range for primary parameter values
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Define primary model parameter initial values as range. For nonparametric,
+#' this range will be absolutely respected. For parametric, the range serves
+#' to define the mean (midpoint) and standard deviation (1/6 of the range) of the
+#' initial parameter value distribution.
+#' @param min Minimum value.
+#' @param max Maximum value.
+#' @export
+ab <- function(min, max) {
+ PM_pri$new(min, max)
+}
+
+
+
+#' @title Initial mean/SD for primary parameter values
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Define primary model parameter initial values as mean and standard
+#' deviation, which translate to a range. The mean serves as the midpoint
+#' of the range, with 3 standard deviations above and below the mean to define
+#' the min and max of the range. For nonparametric,
+#' this range will be absolutely respected. For parametric,
+#' values can be estimated beyond the range.
+#' @param mean Initial mean.
+#' @param sd Initial standard deviation.
+#' @export
+msd <- function(mean, sd) {
+ min <- mean - 3 * sd
+ max <- mean + 3 * sd
+ if (min < 0) {
+ cli::cli_warn(c(
+ "i" = "Negative minimum value for primary parameter range.",
+ " " = "This may not be appropriate for your model."
+ ))
+ }
+ PM_pri$new(min, max)
+}
+
+
+
+#' @title Model covariate declaration
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Declare whether covariates in the data are to have
+#' interpolation between values or not.
+#' @param type If `type = "lm"` (the default) or `type = "linear"`,
+#' the covariate value will be
+#' linearly interpolated between values when fitting the model to the data.
+#' in a model list `cov` item. To fix covariate values to the value at the
+#' last time point, set `type = "none"`.
+#' @return A value of 1 for "lm" and 0 for "none", which will be passed to Rust.
+#' @examples
+#' \dontrun{
+#' cov <- c(
+#' wt = interp(), # same as interp("lm") or interp("linear")
+#' visit = interp("none")
+#' )
+#' }
+#' @export
+interp <- function(type = "lm") {
+ if (!type %in% c("lm", "linear", "none")) {
+ cli::cli_abort(c(
+ "x" = "{type} is not a valid covariate interpolation type.",
+ "i" = "See help for {.help PM_model()}."
+ ))
+ }
+ if (type %in% c("lm", "linear")) {
+ return(1)
+ } else {
+ return(0)
+ }
+}
+
+
+
+
+# PLOT --------------------------------------------------------------------
+
+#' @title Plot PM_model objects
+#' @description
+#' `r lifecycle::badge("stable")`
+#'
+#' Plots a [PM_model] based on differential equations using network plots from tidygraph and ggraph packages.
+#'
+#' @details
+#' This accepts a [PM_model] object and creates a network plot where nodes are compartments
+#' and edges are arrows connecting compartments.
+#' @method plot PM_model
+#' @param x The name of an [PM_model] object.
+#' @param marker Controls the characteristics of the compartments (nodes).
+#' It can be boolean or a list.
+#' `TRUE` will plot the compartments with default characteristics.
+#' `FALSE` will suppress compartment plotting.
+#' If a list, can control some marker characteristics, including overriding defaults.
+#' These include:
+#' \itemize{
+#' \item{`color`} Marker color (default: dodgerblue).
+#' \item{`opacity`} Ranging between 0 (fully transparent) to 1 (fully opaque). Default is 0.5.
+#' \item{`size`} Relative size of boxes, ranging from 0 to 1. Default is 0.25.
+#' \item{`line`} A list of additional attributes governing the outline for filled shapes, most commonly
+#' color (default: black) and width (default: 0.5).
+#' }
+#'
+#'
+#' Example: `marker = list(color = "red", opacity = 0.8, line = list(color = "black", width = 1))`
+#' @param line Controls characteristics of arrows (edges).
+#' `TRUE` will plot default lines. `FALSE` will suppress lines.
+#' If a list, can control some line characteristics, including overriding defaults.
+#' These include:
+#' \itemize{
+#' \item{`color`} Line color (default: black)
+#' \item{`width`} Thickness in points (default: 1).
+#' }
+#'
+#'
+#' Example: `line = list(color = "red", width = 2)`
+#' @param explicit A data frame or tibble containing two columns named `from` and `to`
+#' to add additional connecting arrows to the plot indicating transfer between
+#' compartments. For each row, the `from` column contains the compartment number of the arrow origin, and the
+#' `to` column contains the compartment number of the arrow destination. Use 0 to indicate
+#' a destination to the external sink. e.g., `explicit = data.frame(from = 3, to = 0)`
+#' @param implicit Similar to `explicit`, used to add dashed connecting arrows
+#' to the plot indicating implicit transfer between
+#' compartments. For each row, the `from` column contains the compartment number of the arrow origin, and the
+#' `to` column contains the compartment number of the arrow destination. Use 0 to indicate
+#' a destination to the external sink. e.g., `implicit = data.frame(from = 2, to = 4)`
+#' @param print If `TRUE`, will print the object and return it. If `FALSE`, will only return the object.
+#' @param ... Not used.
+#' @return A plot object of the model.
+#' @author Markus Hovd, Julian Otalvaro, Michael Neely
+#' @seealso [PM_model], [ggraph::ggraph()], [ggplot2::ggplot()]
+#' @export
+#' @examples
+#' \dontrun{
+#' NPex$model$plot()
+#' }
+#' @family PMplots
+
+plot.PM_model <- function(x,
+ marker = TRUE,
+ line = TRUE,
+ explicit,
+ implicit,
+ print = TRUE,
+ ...) {
+ model <- x
+ marker <- if (is.list(marker) || marker) {
+ amendMarker(marker,
+ default = list(
+ color = "dodgerblue",
+ size = 0.25,
+ line = list(width = 0.5)
+ )
+ )
+ } else {
+ FALSE
+ }
+ line <- if (is.list(line) || line) {
+ amendLine(line, default = list(color = "black"))
+ } else {
+ FALSE
+ }
+
+ if (inherits(model, "PM_lib")) {
+ eqns <- model$arg_list$eqn
+ outs <- model$arg_list$out
+ } else if (inherits(model, "PM_model")) {
+ if (model$model_list$name == "user") {
+ eqns <- model$arg_list$eqn
+ outs <- model$arg_list$out
+ } else {
+ eqns <- get(model$model_list$name)$arg_list$eqn
+ outs <- get(model$model_list$name)$arg_list$out
+ }
+ } else {
+ cli::cli_abort(c(
+ "x" = "Unknown model type to plot."
+ ))
+ }
+
+ eqns <- func_to_char(eqns)
+ outs <- func_to_char(outs)
+
+
+ # filter any equations that are not diffeq or outputs
+
+ eqns <- eqns %>%
+ map(
+ purrr::keep,
+ stringr::str_detect,
+ stringr::regex("dX\\[\\d+\\]|XP\\(\\d+\\)", ignore_case = TRUE)
+ ) %>%
+ unlist()
+
+ outs <- outs %>%
+ map(
+ purrr::keep,
+ stringr::str_detect,
+ stringr::regex("Y\\[\\d+\\]", ignore_case = TRUE)
+ ) %>%
+ unlist()
+
+
+
+
+
+ #### INTERNAL FUNCTIONS
+ # Parse the function body
+ parse_equations <- function(func) {
+ body_expr <- body(func)
+ equations <- list()
+
+ # Handle single expression or block
+ if (is.call(body_expr) && body_expr[[1]] == "{") {
+ # Multiple statements in braces
+ for (i in 2:length(body_expr)) {
+ eq <- body_expr[[i]]
+ if (is.call(eq) && length(eq) == 3 && as.character(eq[[1]]) %in% c("=", "<-")) {
+ equations <- append(equations, list(eq))
+ }
+ }
+ } else if (is.call(body_expr) && length(body_expr) == 3 &&
+ as.character(body_expr[[1]]) %in% c("=", "<-")) {
+ # Single assignment
+ equations <- list(body_expr)
+ }
+
+ return(equations)
+ }
+
+
+ ##### Handle distributions
+ # Recursively distribute products over sums in a single expression or equation.
+ # - Works symbolically (no evaluation).
+ # - Handles unary minus.
+ # - If given an assignment (= or <-), only the RHS is expanded.
+ # Fully distribute products over sums and flatten subtraction.
+ # If given an assignment (= or <-), only the RHS is expanded.
+ expand_distribute <- function(expr) {
+ op_of <- function(e) if (is.call(e)) as.character(e[[1]]) else ""
+
+ # Build a product call (no eval)
+ make_prod <- function(a, b) as.call(list(as.name("*"), a, b))
+
+ # Fold a list of factors into a product
+ fold_prod <- function(factors) Reduce(make_prod, factors)
+
+ # Rebuild a (flattened) sum from signed terms
+ build_sum <- function(terms) {
+ if (length(terms) == 0) {
+ return(0)
+ }
+ mk <- function(sign, e) if (sign == -1) as.call(list(as.name("-"), e)) else e
+ out <- mk(terms[[1]]$sign, terms[[1]]$expr)
+ if (length(terms) == 1) {
+ return(out)
+ }
+ for (k in 2:length(terms)) {
+ tk <- terms[[k]]
+ out <- if (tk$sign == 1) {
+ as.call(list(as.name("+"), out, tk$expr))
+ } else {
+ as.call(list(as.name("-"), out, tk$expr))
+ }
+ }
+ out
+ }
+
+ # Core: return a flat list of signed terms {sign=±1, expr=LANG}
+ expand_terms <- function(e, sign = 1) {
+ # atoms
+ if (!is.call(e)) {
+ return(list(list(sign = sign, expr = e)))
+ }
+
+ op <- op_of(e)
+
+ # parentheses
+ if (op == "(") {
+ return(expand_terms(e[[2]], sign))
+ }
+
+ # assignment: expand RHS only, rebuild later
+ if (op %in% c("=", "<-")) {
+ rhs_terms <- expand_terms(e[[3]], +1)
+ rhs_exp <- build_sum(rhs_terms)
+ return(list(list(sign = +1, expr = as.call(list(as.name(op), e[[2]], rhs_exp)))))
+ }
+
+ # addition
+ if (op == "+") {
+ return(c(
+ expand_terms(e[[2]], sign),
+ expand_terms(e[[3]], sign)
+ ))
+ }
+
+ # subtraction (binary or unary)
+ if (op == "-") {
+ if (length(e) == 3) {
+ return(c(
+ expand_terms(e[[2]], sign),
+ expand_terms(e[[3]], -sign)
+ ))
+ } else {
+ return(expand_terms(e[[2]], -sign)) # unary minus
+ }
+ }
+
+ # multiplication: distribute across additive factors
+ if (op == "*") {
+ # expand each factor into its additive term list
+ args <- as.list(e)[-1]
+ expanded_factors <- lapply(args, function(a) expand_terms(a, +1))
+
+ # start with neutral element (sign=+1, expr=1)
+ combos <- list(list(sign = +1, expr = 1))
+ for (f_terms in expanded_factors) {
+ newc <- list()
+ for (c1 in combos) {
+ for (t2 in f_terms) {
+ s <- c1$sign * t2$sign
+ # build product (avoid multiplying by 1 syntactically where possible)
+ e1 <- c1$expr
+ e2 <- t2$expr
+ prod_expr <-
+ if (is.numeric(e1) && length(e1) == 1 && e1 == 1) {
+ e2
+ } else if (is.numeric(e2) && length(e2) == 1 && e2 == 1) {
+ e1
+ } else if (is.numeric(e1) && length(e1) == 1 && e1 == -1) {
+ as.call(list(as.name("-"), e2))
+ } else if (is.numeric(e2) && length(e2) == 1 && e2 == -1) {
+ as.call(list(as.name("-"), e1))
} else {
- prior <- "sobol"
- }
-
-
-
- # get bolus info
- if (self$model_list$name != "user"){ # library model
- bolus_models <- model_lib(show = FALSE) %>% filter(stringr::str_detect(Compartments, "Bolus")) %>% pull(Name)
- bolus <- ifelse(self$model_list$name %in% bolus_models, 1, NA) # may need to generalize if models with multiple bolus compartments are added
- } else { # user model
- eqns <- func_to_char(self$arg_list$eqn)
- bolus_comps <- stringr::str_which(eqns, stringr::regex("(b|bolus)\\[\\d+\\]", ignore_case = TRUE))
- bolus_inputs <- as.integer(stringr::str_match(eqns[bolus_comps], stringr::regex("(b|bolus)\\[(\\d+)\\]", ignore_case = TRUE))[,3])
-
- }
-
-
- if (intern) {
- ### CALL RUST
- out_path <- normalizePath(file.path(path_run, "outputs"), mustWork = FALSE)
- msg <- c(msg, "Run results were saved in folder '{.path {out_path}}'")
- rlang::try_fetch(
- fit(
- # defined in extendr-wrappers.R
- model_path = normalizePath(self$binary_path),
- data = normalizePath(file.path(path_run, "inputs", "gendata.csv")),
- params = list(
- ranges = ranges, #not important but needed for POSTPROB
- algorithm = algorithm,
- error_models = lapply(self$model_list$err, function(x) x$flatten()),
- idelta = idelta,
- tad = tad,
- max_cycles = cycles, #will be hardcoded in Rust to 0 for POSTPROB
- prior = prior, # needs warning if missing and algorithm = POSTPROB
- points = points, # only relevant for sobol prior
- seed = seed
- ),
- output_path = out_path,
- kind = tolower(self$model_list$type)
- ),
- error = function(e) {
- cli::cli_warn("Unable to create {.cls PM_result} object", parent = e)
- return(NULL)
- }
- )
-
- PM_parse(path = out_path)
- res <- PM_load(path = normalizePath(out_path), file = "PMout.Rdata")
- if(report != "none"){
-
- valid_report <- tryCatch(
- PM_report(res, path = normalizePath(out_path), template = report, quiet = TRUE),
- error = function(e) {
- -1
- })
- if (valid_report == 1) {
- msg <- c(msg, "Report generated with {report} template.")
- # if(tolower(algorithm) == "postprob") {this_alg <- "map"} else {this_alg <- "fit"}
- msg <- c(msg, "If assigned to a variable, e.g. {.code run{run} <-}, results are available in {.code run{run}}.")
- } else {
- msg <- c(msg, "Report could not be generated.")
- }
- }
-
-
- if(length(msg) > 1) {
- cli::cli_h1("Notes:")
- cli::cli_ul()
- purrr::walk(msg[-1], ~ cli::cli_li(.x))
- cli::cli_end()
- }
- return(invisible(res))
- } else {
- cli::cli_abort(
- c("x" = "Error: Currently, the rust engine only supports internal runs.", "i" = "This is a temporary limitation.")
- )
- }
- }, # end fit method
-
- #' @description
- #' Calculate posteriors from a fitted model.
- #' #' @details
- #' This method calculates posteriors from a compiled model. It is not necessary to have
- #' run the model first, but it is necessary to have an informative prior distribution.
- #' This prior will typically be the result of a previous run, but may also be a file
- #' containing support points, with each column named as a parameter in the model plus a final column
- #' for probability. Each row contains values for the parameters and the associated probability for
- #' those parameter values. The file can be saved as a csv file.
- #'
- #' To calculate the posteriors, `map()` calls the `fit()` method with the `cycles` argument set to 0
- #' and the `algorithm` argument set to "POSTPROB". If `data` are not provided as an argument to
- #' `map()`, the model's `data` field is used instead. If `data` is provided, it must be a
- #' [PM_data] object or the pathname of a file which can be loaded as a [PM_data] object.
- #' @param ... Arguments passed to the `fit` method. Note that the `cycles` argument is set to 0,
- #' and the `algorithm` argument is set to "POSTPROB" automatically.
- map = function(...) {
- #browser()
- args <- list(...)
-
- if (!is.null(purrr::pluck(args, "cycles")) && purrr::pluck(args, "cycles") != 0) {
- cli::cli_inform(c("i" = "{.arg cycles} set to 0 for posteriors"))
- }
- args$cycles <- 0 # ensure cycles is set to 0
-
-
- if (!is.null(purrr::pluck(args, "algorithm")) && purrr::pluck(args, "algorithm") != "POSTPROB") {
- cli::cli_inform(c("i" = "{.arg algorithm} set to POSTPROB for posteriors"))
- }
- args$algorithm <- "POSTPROB" # ensure algorithm is set to POSTPROB
-
-
- if (is.null(purrr::pluck(args, "data"))) {
- cli::cli_abort(c("x" = "Data must be specified for posteriors."))
- }
-
- if (is.null(purrr::pluck(args, "prior")) || purrr::pluck(args, "prior") == "sobol") {
- cli::cli_abort(c("x" = "Please specify a non-uniform prior for posteriors.",
- " " = "This can be a prior run number or the name of a file with support points."))
- }
-
- do.call(self$fit, args)
- },
- #' @description
- #' Simulate data from the model using a set of parameter values.
- #' @details
- #' This method simulates output from the model using a set of parameter values
- #' provided in the `theta` matrix and the template for dosing/observations in
- #' the `data` object.
- #' @param data A [PM_data] object containing the dosing and observation information.
- #' @param theta A matrix of parameter values to use for the simulation.
- #' The `theta` matrix should have the same number of columns as the number of primary parameters in the model.
- #' Each row of `theta` represents a different set of parameter values.
- #'
- sim = function(data, theta) {
-
- if (!inherits(data, "PM_data")) {
- cli::cli_abort(c("x" = "Data must be a PM_data object."))
- }
- if (!is.matrix(theta)) {
- cli::cli_abort(c("x" = "theta must be a matrix."))
- }
- if (!is.numeric(theta)) {
- cli::cli_abort(c("x" = "theta must be a matrix of numeric values."))
- }
- if (ncol(theta) != length(private$get_primary())) {
- cli::cli_abort(c("x" = "theta must have the same number of columns as the number of parameters."))
- }
-
-
- temp_csv <- tempfile(fileext = ".csv")
- data$save(temp_csv, header = FALSE)
- if (getPMoptions()$backend == "rust") {
- if (is.null(self$binary_path)) {
- self$compile()
- if (is.null(self$binary_path)) {
- cli::cli_abort(c("x" = "Model must be compiled before simulating."))
- }
- }
- sim <- simulate_all(temp_csv, self$binary_path, theta, kind = tolower(self$model_list$type))
- } else {
- cli::cli_abort(c("x" = "This function can only be used with the rust backend."))
- }
- return(sim)
- },
- #' @description
- #' Compile the model to a binary file.
- #' @details
- #' This method write the model to a Rust file in a temporary path,
- #' updates the `binary_path` field for the model, and compiles that
- #' file to a binary file that can be used for fitting or simulation.
- #' If the model is already compiled, the method does nothing.
- #'
- compile = function() {
- if (!is.null(self$binary_path) && file.exists(self$binary_path)) {
- # model is compiled
- return(invisible(NULL))
- }
-
- model_path <- file.path(tempdir(), "model.rs")
- private$write_model_to_rust(model_path)
- output_path <- tempfile(pattern = "model_", fileext = ".pmx")
- cli::cli_inform(c("i" = "Compiling model..."))
- tryCatch({
- compile_model(model_path , output_path, private$get_primary(), kind = tolower(self$model_list$type))
- self$binary_path <- output_path
- }, error = function(e) {
- cli::cli_abort(
- c("x" = "Model compilation failed: {e$message}", "i" = "Please check the model file and try again.")
- )
- })
-
- file.remove(model_path) # remove temporary model file
- return(invisible(self))
- },
- #' @description
- #' Update the model using recursive lists of changes and recompile the updated model.
- #' @param ... Named elements corresponding to the blocks in the model,
- #' such as "pri", "cov", "sec", "eqn", "ini", "lag", "fa", "out", and "err".
- #' For each block, create a list of changes, which may be additions, edits, or deletions.
- #' For deletions, set the value to `NULL`.
- #'
- update = function(...) {
- changes <- list(...)
- keys <- names(changes)
- if (!all(tolower(keys) %in% c("pri", "cov", "sec", "eqn", "ini", "lag", "fa", "out", "err"))) {
- cli::cli_abort(c(
- "x" = "Invalid block name: {keys}",
- "i" = "See help for {.fn PM_model}."
- ))
- }
- self$arg_list <- modifyList2(self$arg_list, changes)
- self <- PM_model$new(self$arg_list) # recreate and recompile the model
- return(invisible(self))
- }
-
- ), # end public list
- private = list(
- R6fromFile = function(file) {
- msg <- ""
- blocks <- parseBlocks(file) # this function is in PMutilities
- # check for reserved variable names
- reserved <- c(
- "t",
- "x",
- "dx",
- "p",
- "rateiv",
- "cov",
- "y"
- )
- conflict <- c(match(tolower(blocks$primVar), reserved, nomatch = -99), match(tolower(blocks$secVar), reserved, nomatch = -99), match(tolower(blocks$covar), reserved, nomatch = -99))
- nconflict <- sum(conflict != -99)
- if (nconflict > 0) {
- msg <- paste("\n", paste(paste("'", reserved[conflict[conflict != -99]], "'", sep = ""), collapse = ", "), " ", c("is a", "are")[1 + as.numeric(nconflict > 1)], " reserved ", c("name", "names")[1 + as.numeric(nconflict > 1)], ", regardless of case.\nPlease choose non-reserved parameter/covariate names.\n", sep = "")
- return(list(status = -1, msg = msg))
- }
-
- if (length(grep(";", blocks$primVar)) > 0) {
- # using ';' as separator
- sep <- ";"
- } else {
- if (length(grep(",", blocks$primVar)) > 0) {
- # using ',' as separator
- sep <- ","
- } else {
- return(list(status = -1, msg = "\nPrimary variables should be defined as 'var,lower_val,upper_val' or 'var,fixed_val'.\n"))
- }
- }
-
- # build arg_list
- arg_list <- list()
- # this function makes pri for PM_model
- arg_list$pri <- sapply(strsplit(blocks$primVar, sep), function(x) {
- # find out if constrained to be positive
- const_pos <- any(grepl("\\+", x))
- if (const_pos) {
- x <- gsub("\\+", "", x)
- cli::cli_inform(c("i" = "Truncating variables to positive ranges is not required for NPAG/NPOD",
- " " = "This may be updated as parametric algorithms come online, but will be ignored for now."))
- }
-
- # find out if constant
- const_var <- any(grepl("!", x))
- if (const_var) {
- x <- gsub("!", "", x)
- cli::cli_abort(c("x" = "Constants should be defined in the appropriate block, not #PRI."))
- }
-
- values <- as.numeric(x[-1])
-
- if (length(x[-1]) == 1) { # fixed
- cli::cli_abort(c("x" = "Fixed but unknown are no longer supported.",
- "i" = "If necessary, fit them as random and then use a fixed value in subsequent runs."))
- } else { # range
- thisItem <- list(ab(values[1], values[2]))
- }
- names(thisItem) <- x[1]
- thisItem
- }) # end sapply
-
- # covariates
- covar <- blocks$covar
- const_covar <- grepl("!", covar) # returns boolean vector, length = ncov
- covar <- gsub("!", "", covar) # remove "!"
- covar_list <- tolower(covar)
-
- # add to arg_list
- arg_list$cov <- purrr::map_vec(const_covar, \(x){
- type <- ifelse(!x, "lm", "none")
- interp(type)
- }) %>%
- purrr::set_names(covar_list)
-
-
- # extra
- # if (blocks$extra[1] != "") {
- # arg_list$ext <- blocks$extra
- # } else {
- # arg_list$extra <- NULL
- # }
-
- # secondary variables
- if (blocks$secVar[1] != "") {
- arg_list$sec <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$secVar, collapse = '\n ')}\n}}")))
- } else {
- arg_list$sec <- NULL
- }
-
- # bioavailability
- if (blocks$f[1] != "") {
- arg_list$fa <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$f, collapse = '\n ')}\n}}")))
- } else {
- arg_list$fa <- NULL
- }
-
- # bolus
- if (blocks$bol[1] != "") {
- cli::cli_inform(c(
- "i" = "The bolus block is no longer used as of Pmetrics 3.0.0.",
- " " = "Indicate bolus inputs as {.code B[x]} in equations, where {.code x} is the input number."))
- }
-
- # initial conditions
- if (blocks$ini[1] != "") {
- arg_list$ini <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$ini, collapse = '\n ')}\n}}")))
- } else {
- arg_list$ini <- NULL
- }
-
- # lag time
- if (blocks$lag[1] != "") {
- arg_list$lag <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$lag, collapse = '\n ')}\n}}")))
- } else {
- arg_list$lag <- NULL
- }
-
- # differential equations - legacy
- if (!is.null(blocks$diffeq) && blocks$diffeq[1] != "") {
- cli::cli_inform(c(
- "i" = "The #DIFFEQ block is no longer used as of Pmetrics 3.0.0.",
- " " = "The block is now called #EQN for more general equations.",
- " " = "Equations have been moved to the {.code eqn} element."))
- arg_list$eqn <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$diffeq, collapse = '\n ')}\n}}")))
- } else {
- arg_list$eqn <- NULL
- }
-
- # model equations - will eventually replace diffeq above
- if (blocks$eqn[1] != "") {
- arg_list$eqn <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$eqn, collapse = '\n ')}\n}}")))
- } else {
- arg_list$eqn <- NULL
- }
-
- # out/err
- n_outputLines <- length(blocks$output)
- outputLines <- grep("y\\([[:digit:]]+\\)|y\\[[[:digit:]]+\\]", blocks$output)
- if (length(outputLines) == 0) {
- return(list(status = -1, msg = "\nYou must have at least one output equation of the form 'Y[1] = ...'\n"))
- }
- # otherLines <- (1:n_outputLines)[!(1:n_outputLines) %in% outputLines] # find other lines
- # if (length(otherLines) > 0) {
- # arg_list$sec <- c(arg_list$sec, blocks$output[otherLines]) # append to #sec block
- # }
- # output <- blocks$output[outputLines]
- # remParen <- stringr::str_replace(blocks$output, regex("Y(?:\\[|\\()(\\d+)(?:\\]|\\))", ignore_case = TRUE), "Y\\1")
- # diffeq <- stringr::str_split(remParen, "\\s*=\\s*")
- # diffList <- sapply(diffeq, function(x) x[2])
- # num_out <- length(diffList)
-
- arg_list$out <- eval(parse(text = glue::glue("function() {{\n {paste(blocks$out, collapse = '\n ')}\n}}")))
-
- err <- tolower(gsub("[[:space:]]", "", blocks$error))
- # process constant gamma/lambda
- err_type <- c("additive", "proportional")[1+grepl("^g", err[1])]
- const_gamlam <- grepl("!", err[1])
- gamlam_value <- as.numeric(stringr::str_match(err[1], "\\d+\\.?\\d*"))
- # process constant coefficients
- const_coeff <- grepl("!", err[-1]) # returns boolean vector, length = nout
- err <- gsub("!", "", err) # remove "!"
-
- coeff_fxns <- err[-1] %>%
- purrr::imap(\(x, idx) {
- glue::glue("{err_type}({gamlam_value}, c({x}), {const_coeff[{idx}]})")
- }) %>%
- unlist()
-
- arg_list$err <- eval(parse(text = glue::glue("c(\n{paste({coeff_fxns}, collapse = ',\n')}\n)")))
-
- cat(msg)
- flush.console()
- return(arg_list)
- }, # end R6fromFile
-
- write_model_to_rust = function(file_path = "main.rs") {
- # Check if model_list is not NULL
- if (is.null(self$model_list)) {
- cli::cli_abort(c("x" = "Model list is empty.", "i" = "Please provide a valid model list."))
- }
-
- if (self$model_list$type %in% c("Analytical", "ODE")){
- placeholders <- c("eqn", "lag", "fa", "ini", "out", "n_eqn", "n_out")
- base <- paste0("#[allow(unused_mut)]\nequation::",
- self$model_list$type,
- "::new(\n",
- paste("<", placeholders[1:5], ">", sep = "", collapse = ",\n "),
- ",\n (",
- paste("<", placeholders[6:7], ">", sep = "", collapse = ", "),
- "),\n)")
-
- } else {
- cli::cli_abort(c("x" = "Invalid model type.", "i" = "Please provide a valid model type."))
- }
-
-
- # Replace placeholders in the base string with actual values from model_list
- base <- placeholders %>%
- purrr::reduce(\(x, y) stringr::str_replace(x, stringr::str_c("<", y, ">"), as.character(self$model_list[[y]])), .init = base)
- # Write the model to a file
- writeLines(base, file_path)
- },
- from_file = function(file_path) {
- self$model_list <- private$makeR6model(model_filename)
- #self$content <- readChar(model_filename, file.info(model_filename)$size)
- },
- get_primary = function() {
- return(tolower(self$model_list$parameters))
- }
- ) # end private
- ) # end R6Class PM_model
-
- ##### These functions create various model components
-
- #' @title Additive error model
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Create an additive (lambda) error model
- #' @param initial Initial value for lambda
- #' @param coeff Vector of coefficients defining assay error polynomial
- #' @param fixed Estimate if `FALSE` (default).
- #' @export
- additive <- function(initial, coeff, fixed = FALSE) {
- PM_err$new(type = "additive", initial = initial, coeff = coeff, fixed = fixed)
- }
-
-
-
- #' @title Proportional error model
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Create an proportional (gamma) error model
- #' @param initial Initial value for gamma
- #' @param coeff Vector of coefficients defining assay error polynomial
- #' @param fixed Estimate if `FALSE` (default).
- #' @export
- proportional <- function(initial, coeff, fixed = FALSE) {
- PM_err$new(type = "proportional", initial = initial, coeff = coeff, fixed = fixed)
- }
-
- PM_err <- R6::R6Class(
- "PM_err",
- public = list(
- #' @field type Type of error model, either "additive" or "proportional".
- type = NULL,
- #' @field initial Initial value for the error model.
- initial = NULL,
- #' @field coeff Coefficients for the assay error polynomial.
- coeff = NULL,
- #' @field fixed If `TRUE`, the error model is fixed and not estimated.
- fixed = NULL,
- initialize = function(type, initial, coeff, fixed) {
- self$type <- type
- self$initial <- initial
- self$coeff <- coeff
- self$fixed <- fixed
- },
- print = function(){
- if (self$fixed) {
- cli::cli_text("{.strong {tools::toTitleCase(self$type)}}, with fixed value of {.emph {self$initial}} and coefficients {.emph {paste(self$coeff, collapse = ', ')}}.")
- } else {
- cli::cli_text("{.strong {tools::toTitleCase(self$type)}}, with initial value of {.emph {self$initial}} and coefficients {.emph {paste(self$coeff, collapse = ', ')}}.")
- }
- },
-
- flatten = function(){
- list(initial = self$initial, coeff = self$coeff, type = self$type, fixed = self$fixed)
- }
- )
- )
-
- #' @title Primary parameter values
- #' @description
- #' `r lifecycle::badge("experimental")`
- #' Define primary model parameter object.
- #' This is used internally by the `PM_model` class.
- #' @keywords internal
- PM_pri <- R6::R6Class(
- "PM_pri",
- public = list(
- #' @field min Minimum value of the range.
- min = NULL,
- #' @field max Maximum value of the range.
- max = NULL,
- #' @field mean Mean value of the range, calculated as (min + max) / 2.
- mean = NULL,
- #' @field sd Standard deviation of the range, calculated as (max - min) / 6.
- sd = NULL,
- #' @description
- #' Initialize a new range object.
- #' @param min Minimum value of the range.
- #' @param max Maximum value of the range.
- initialize = function(min, max) {
- self$min = min
- self$max = max
- self$mean = (min + max) / 2
- self$sd = (max - min) / 6
- },
- #' @description
- #' Print the range.
- print = function(){
- cli::cli_text("[{.strong {self$min}}, {.strong {self$max}}], {.emph ~N({round(self$mean,2)}}, {.emph {round(self$sd,2)})}")
- }
- )
- )
-
-
- #' @title Initial range for primary parameter values
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Define primary model parameter initial values as range. For nonparametric,
- #' this range will be absolutely respected. For parametric, the range serves
- #' to define the mean (midpoint) and standard deviation (1/6 of the range) of the
- #' initial parameter value distribution.
- #' @param min Minimum value.
- #' @param max Maximum value.
- #' @export
- ab <- function(min, max) {
- PM_pri$new(min, max)
+ make_prod(e1, e2)
}
-
-
-
- #' @title Initial mean/SD for primary parameter values
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Define primary model parameter initial values as mean and standard
- #' deviation, which translate to a range. The mean serves as the midpoint
- #' of the range, with 3 standard deviations above and below the mean to define
- #' the min and max of the range. For nonparametric,
- #' this range will be absolutely respected. For parametric,
- #' values can be estimated beyond the range.
- #' @param mean Initial mean.
- #' @param sd Initial standard deviation.
- #' @export
- msd <- function(mean, sd) {
- min <- mean - 3 * sd
- max <- mean + 3 * sd
- if(min< 0) {
- cli::cli_warn(c("i" = "Negative minimum value for primary parameter range.",
- " " = "This may not be appropriate for your model."))
- }
- PM_pri$new(min, max)
- }
-
-
-
- #' @title Model covariate declaration
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Declare whether covariates in the data are to have
- #' interpolation between values or not.
- #' @param type If `type = "lm"` (the default) or `type = "linear"`,
- #' the covariate value will be
- #' linearly interpolated between values when fitting the model to the data.
- #' in a model list `cov` item. To fix covariate values to the value at the
- #' last time point, set `type = "none"`.
- #' @return A value of 1 for "lm" and 0 for "none", which will be passed to Rust.
- #' @examples
- #' \dontrun{
- #' cov = c(
- #' wt = interp(), # same as interp("lm") or interp("linear")
- #' visit = interp("none")
- #' )
- #' }
- #' @export
- interp <- function(type = "lm") {
- if (!type %in% c("lm", "linear", "none")) {
- cli::cli_abort(c("x" = "{type} is not a valid covariate interpolation type.",
- "i" = "See help for {.help PM_model()}."))
- }
- if (type %in% c("lm", "linear")) {
- return(1)
- } else {
- return(0)
- }
- }
-
-
-
-
- # PLOT --------------------------------------------------------------------
-
- #' @title Plot PM_model objects
- #' @description
- #' `r lifecycle::badge("stable")`
- #'
- #' Plots a [PM_model] based on differential equations using network plots from tidygraph and ggraph packages.
- #'
- #' @details
- #' This accepts a [PM_model] object and creates a network plot where nodes are compartments
- #' and edges are arrows connecting compartments.
- #' @method plot PM_model
- #' @param x The name of an [PM_model] object.
- #' @param marker Controls the characteristics of the compartments (nodes).
- #' It can be boolean or a list.
- #' `TRUE` will plot the compartments with default characteristics.
- #' `FALSE` will suppress compartment plotting.
- #' If a list, can control some marker characteristics, including overriding defaults.
- #' These include:
- #' \itemize{
- #' \item{`color`} Marker color (default: dodgerblue).
- #' \item{`opacity`} Ranging between 0 (fully transparent) to 1 (fully opaque). Default is 0.5.
- #' \item{`size`} Relative size of boxes, ranging from 0 to 1. Default is 0.25.
- #' \item{`line`} A list of additional attributes governing the outline for filled shapes, most commonly
- #' color (default: black) and width (default: 0.5).
- #' }
- #'
- #'
- #' Example: `marker = list(color = "red", opacity = 0.8, line = list(color = "black", width = 1))`
- #' @param line Controls characteristics of arrows (edges).
- #' `TRUE` will plot default lines. `FALSE` will suppress lines.
- #' If a list, can control some line characteristics, including overriding defaults.
- #' These include:
- #' \itemize{
- #' \item{`color`} Line color (default: black)
- #' \item{`width`} Thickness in points (default: 1).
- #' }
- #'
- #'
- #' Example: `line = list(color = "red", width = 2)`
- #' @param explicit A data frame or tibble containing two columns named `from` and `to`
- #' to add additional connecting arrows to the plot indicating transfer between
- #' compartments. For each row, the `from` column contains the compartment number of the arrow origin, and the
- #' `to` column contains the compartment number of the arrow destination. Use 0 to indicate
- #' a destination to the external sink. e.g., `explicit = data.frame(from = 3, to = 0)`
- #' @param implicit Similar to `explicit`, used to add dashed connecting arrows
- #' to the plot indicating implicit transfer between
- #' compartments. For each row, the `from` column contains the compartment number of the arrow origin, and the
- #' `to` column contains the compartment number of the arrow destination. Use 0 to indicate
- #' a destination to the external sink. e.g., `implicit = data.frame(from = 2, to = 4)`
- #' @param print If `TRUE`, will print the object and return it. If `FALSE`, will only return the object.
- #' @param ... Not used.
- #' @return A plot object of the model.
- #' @author Markus Hovd, Julian Otalvaro, Michael Neely
- #' @seealso [PM_model], [ggraph::ggraph()], [ggplot2::ggplot()]
- #' @export
- #' @examples
- #' \dontrun{
- #' NPex$model$plot()
- #' }
- #' @family PMplots
-
- plot.PM_model <- function(x,
- marker = TRUE,
- line = TRUE,
- explicit,
- implicit,
- print = TRUE,
- ...) {
- model <- x
- marker <- if (is.list(marker) || marker) {
- amendMarker(marker,
- default = list(
- color = "dodgerblue",
- size = 0.25,
- line = list(width = 0.5)
- ))
- } else {
- FALSE
- }
- line <- if (is.list(line) || line) {
- amendLine(line, default = list(color = "black"))
- } else {
- FALSE
- }
-
- if(inherits(model, "PM_lib")){
- eqns <- model$arg_list$eqn
- outs <- model$arg_list$out
- } else if(inherits(model, "PM_model")){
- if(model$model_list$name == "user") {
- eqns <- model$arg_list$eqn
- outs <- model$arg_list$out
- } else {
- eqns <- get(model$model_list$name)$arg_list$eqn
- outs <- get(model$model_list$name)$arg_list$out
- }
-
- } else {
- cli::cli_abort(c(
- "x" = "Unknown model type to plot."
- ))
- }
-
- eqns <- func_to_char(eqns)
- outs <- func_to_char(outs)
-
-
- # filter any equations that are not diffeq or outputs
-
- eqns <- eqns %>%
- map(
- purrr::keep,
- stringr::str_detect,
- stringr::regex("dX\\[\\d+\\]|XP\\(\\d+\\)", ignore_case = TRUE)
- ) %>%
- unlist()
-
- outs <- outs %>%
- map(
- purrr::keep,
- stringr::str_detect,
- stringr::regex("Y\\[\\d+\\]", ignore_case = TRUE)
- ) %>%
- unlist()
-
-
-
-
-
- #### INTERNAL FUNCTIONS
- # Parse the function body
- parse_equations <- function(func) {
- body_expr <- body(func)
- equations <- list()
-
- # Handle single expression or block
- if (is.call(body_expr) && body_expr[[1]] == "{") {
- # Multiple statements in braces
- for (i in 2:length(body_expr)) {
- eq <- body_expr[[i]]
- if (is.call(eq) && length(eq) == 3 && as.character(eq[[1]]) %in% c("=", "<-")) {
-
- equations <- append(equations, list(eq))
- }
- }
- } else if (is.call(body_expr) && length(body_expr) == 3 &&
- as.character(body_expr[[1]]) %in% c("=", "<-")) {
- # Single assignment
- equations <- list(body_expr)
- }
-
- return(equations)
- }
-
-
- ##### Handle distributions
- # Recursively distribute products over sums in a single expression or equation.
- # - Works symbolically (no evaluation).
- # - Handles unary minus.
- # - If given an assignment (= or <-), only the RHS is expanded.
- # Fully distribute products over sums and flatten subtraction.
- # If given an assignment (= or <-), only the RHS is expanded.
- expand_distribute <- function(expr) {
- op_of <- function(e) if (is.call(e)) as.character(e[[1]]) else ""
-
- # Build a product call (no eval)
- make_prod <- function(a, b) as.call(list(as.name("*"), a, b))
-
- # Fold a list of factors into a product
- fold_prod <- function(factors) Reduce(make_prod, factors)
-
- # Rebuild a (flattened) sum from signed terms
- build_sum <- function(terms) {
- if (length(terms) == 0) return(0)
- mk <- function(sign, e) if (sign == -1) as.call(list(as.name("-"), e)) else e
- out <- mk(terms[[1]]$sign, terms[[1]]$expr)
- if (length(terms) == 1) return(out)
- for (k in 2:length(terms)) {
- tk <- terms[[k]]
- out <- if (tk$sign == 1)
- as.call(list(as.name("+"), out, tk$expr))
- else
- as.call(list(as.name("-"), out, tk$expr))
- }
- out
- }
-
- # Core: return a flat list of signed terms {sign=±1, expr=LANG}
- expand_terms <- function(e, sign = 1) {
- # atoms
- if (!is.call(e)) return(list(list(sign = sign, expr = e)))
-
- op <- op_of(e)
-
- # parentheses
- if (op == "(") return(expand_terms(e[[2]], sign))
-
- # assignment: expand RHS only, rebuild later
- if (op %in% c("=", "<-")) {
- rhs_terms <- expand_terms(e[[3]], +1)
- rhs_exp <- build_sum(rhs_terms)
- return(list(list(sign = +1, expr = as.call(list(as.name(op), e[[2]], rhs_exp)))))
- }
-
- # addition
- if (op == "+") {
- return(c(expand_terms(e[[2]], sign),
- expand_terms(e[[3]], sign)))
- }
-
- # subtraction (binary or unary)
- if (op == "-") {
- if (length(e) == 3) {
- return(c(expand_terms(e[[2]], sign),
- expand_terms(e[[3]], -sign)))
- } else {
- return(expand_terms(e[[2]], -sign)) # unary minus
- }
- }
-
- # multiplication: distribute across additive factors
- if (op == "*") {
- # expand each factor into its additive term list
- args <- as.list(e)[-1]
- expanded_factors <- lapply(args, function(a) expand_terms(a, +1))
-
- # start with neutral element (sign=+1, expr=1)
- combos <- list(list(sign = +1, expr = 1))
- for (f_terms in expanded_factors) {
- newc <- list()
- for (c1 in combos) for (t2 in f_terms) {
- s <- c1$sign * t2$sign
- # build product (avoid multiplying by 1 syntactically where possible)
- e1 <- c1$expr; e2 <- t2$expr
- prod_expr <-
- if (is.numeric(e1) && length(e1) == 1 && e1 == 1) e2 else
- if (is.numeric(e2) && length(e2) == 1 && e2 == 1) e1 else
- if (is.numeric(e1) && length(e1) == 1 && e1 == -1) as.call(list(as.name("-"), e2)) else
- if (is.numeric(e2) && length(e2) == 1 && e2 == -1) as.call(list(as.name("-"), e1)) else
- make_prod(e1, e2)
- newc[[length(newc) + 1]] <- list(sign = s, expr = prod_expr)
- }
- combos <- newc
- }
- # apply the incoming sign to all combos
- for (i in seq_along(combos)) combos[[i]]$sign <- sign * combos[[i]]$sign
- return(combos)
- }
-
- # other calls: expand children but treat as atomic w.r.t. addition
- args <- as.list(e)
- args[-1] <- lapply(args[-1], function(a) build_sum(expand_terms(a, +1)))
- list(list(sign = sign, expr = as.call(args)))
- }
-
- # If it's an assignment, expand_terms already rebuilt it as a single term.
- # Otherwise, build the flattened sum.
- terms <- expand_terms(expr, +1)
-
- # Special case: a single rebuilt assignment
- if (length(terms) == 1 && is.call(terms[[1]]$expr) &&
- op_of(terms[[1]]$expr) %in% c("=", "<-"))
- return(terms[[1]]$expr)
-
- build_sum(terms)
- }
-
- # Parse output equations
- parse_output_equations <- function(equations) {
- #if (is.null(func)) return(list())
-
- #equations <- parse_equations(func)
- outputs <- list()
-
- for (eq in equations) {
-
- lhs <- eq[[2]]
- rhs <- eq[[3]]
-
- # Extract output number from y[i]
- if (is.call(lhs) && as.character(lhs[[1]]) == "[" &&
- length(lhs) >= 3 && as.character(lhs[[2]]) == "y") {
- output_num <- as.numeric(as.character(lhs[[3]]))
-
- # Convert RHS to string representation
- rhs_str <- deparse(rhs, width.cutoff = 500)
-
- # Find which compartment this output refers to
- comp_ref <- extract_x_pattern(rhs)
- if (is.null(comp_ref)) {
- # Look deeper in the expression for x[i] patterns
- comp_ref <- find_x_in_expression(rhs)
- }
-
- outputs <- append(outputs, list(list(
- output_num = output_num,
- equation = rhs_str,
- compartment = comp_ref
- )))
- }
- }
-
- return(outputs)
- }
-
- # Find x[i] pattern in any expression
- find_x_in_expression <- function(expr) {
- if (is.call(expr)) {
- # Check current expression
- x_idx <- extract_x_pattern(expr)
- if (!is.null(x_idx)) return(x_idx)
-
- # Recursively check sub-expressions
- for (i in 1:length(expr)) {
- if (i > 1) { # Skip the function name
- x_idx <- find_x_in_expression(expr[[i]])
- if (!is.null(x_idx)) return(x_idx)
- }
- }
- }
- return(NULL)
- }
-
- # Parse terms from right-hand side recursively
- parse_rhs_terms <- function(rhs_expr) {
-
- terms <- list()
-
- # Recursively extract terms and track sign
- extract_terms <- function(expr, current_sign = "+") {
-
- if (is.call(expr)) {
- op <- as.character(expr[[1]])
-
- if (op == "+") {
- extract_terms(expr[[2]], current_sign)
- extract_terms(expr[[3]], current_sign)
- } else if (op == "-") {
- if (length(expr) == 3) {
- # Binary subtraction: a - b
- extract_terms(expr[[2]], current_sign)
- extract_terms(expr[[3]], ifelse(current_sign == "+", "-", "+"))
- } else {
- # Unary minus: -a
- extract_terms(expr[[2]], ifelse(current_sign == "+", "-", "+"))
- }
- } else if (op == "*") {
- # Look for x[i] and collect coefficient(s)
- vars <- lapply(expr[-1], extract_x_pattern)
- if (any(!sapply(vars, is.null))) {
-
-
- xi_index <- which(!sapply(vars, is.null))
- x_part <- expr[[xi_index + 1]]
- coeff_parts <- expr[-c(1, xi_index + 1)]
- coeff_str <- paste(sapply(coeff_parts, deparse), collapse = "*")
- terms <<- append(terms, list(list(expr = x_part, coeff = coeff_str, sign = current_sign)))
- } else {
- # No x[i], maybe just a numeric or unrelated variable
- terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
- }
- } else {
- # Some other operation; treat as atomic for now
- terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
- }
- } else {
- # Symbol or constant
- terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
- }
- }
-
- extract_terms(rhs_expr)
-
- return(terms)
- }
-
- # Extract x[i] pattern from expression
- extract_x_pattern <- function(expr) {
- if (is.call(expr) && as.character(expr[[1]]) == "[" &&
- length(expr) == 3 && as.character(expr[[2]]) == "x") {
- return(as.numeric(as.character(expr[[3]])))
- }
- return(NULL)
- }
-
- # Extract compartment connections
- extract_connections <- function(equations) {
- compartments <- c()
- all_terms <- list()
-
- # First pass: collect signed terms per compartment
- for (eq in equations) {
- lhs <- eq[[2]]
- rhs <- eq[[3]]
-
- if (is.call(lhs) && as.character(lhs[[1]]) == "[" &&
- length(lhs) >= 3 && as.character(lhs[[2]]) == "dx") {
-
- comp_num <- as.numeric(as.character(lhs[[3]]))
- compartments <- unique(c(compartments, comp_num))
-
- # dist_terms <- distribute_product(rhs)
- # terms <- parse_rhs_terms(dist_terms)
- terms <- parse_rhs_terms(rhs)
-
- for (term in terms) {
- expr <- term$expr
- sign <- term$sign
- coeff <- term$coeff
-
- x_index <- extract_x_pattern(expr)
- if (!is.null(x_index)) {
- all_terms <- append(all_terms, list(list(
- comp = comp_num,
- sign = sign,
- coeff = coeff,
- x_index = x_index
- )))
- }
- }
- }
- }
-
- # Second pass: match positive and negative terms
- used <- logical(length(all_terms))
- connections <- list()
-
- for (i in seq_along(all_terms)) {
- ti <- all_terms[[i]]
- if (used[i] || ti$sign != "-") next
-
- match_found <- FALSE
- for (j in seq_along(all_terms)) {
- tj <- all_terms[[j]]
- if (used[j] || tj$sign != "+") next
-
- # Match by coeff and x_index
- if (identical(ti$coeff, tj$coeff) && ti$x_index == tj$x_index) {
- connections <- append(connections, list(list(
- from = ti$comp,
- to = tj$comp,
- coeff = ti$coeff
- )))
- used[i] <- TRUE
- used[j] <- TRUE
- match_found <- TRUE
- break
- }
- }
-
- # If no match, it's elimination
- if (!match_found) {
- connections <- append(connections, list(list(
- from = ti$comp,
- to = 0,
- coeff = ti$coeff
- )))
- used[i] <- TRUE
- }
- }
-
- return(list(connections = connections, compartments = sort(compartments)))
- }
-
-
-
- # Modify layout logic to use circular positioning
- create_plot <- function(connections, compartments, outputs) {
- library(ggplot2)
- library(dplyr)
-
- box_width <- 1.2
- box_height <- 0.8
-
- n_comp <- length(compartments)
- if (n_comp == 0) {
- plot.new()
- title(main = "No compartments detected")
- return()
- }
-
- # Circular layout
- radius <- 4
- angles <- seq(0, 2 * pi, length.out = n_comp + 1)[- (n_comp + 1)]
- angles <- angles - angles[which(compartments == 1)] + pi / 2
- x_pos <- radius * cos(angles)
- y_pos <- radius * sin(angles)
- layout_df <- data.frame(compartment = compartments, x = x_pos, y = y_pos)
-
- # Elimination
- elim_comps <- unique(sapply(connections, function(c) if (c$to == 0) c$from else NULL))
- elim_comps <- elim_comps[!sapply(elim_comps, is.null)]
-
- arrow_segments <- list()
- arrow_heads <- list()
- labels <- list()
- label_tracker <- list()
-
- # Bidirectional detection
- pair_keys <- data.frame(
- original = sapply(connections, function(c) paste(c(c$from, c$to), collapse = "-")),
- sorted = sapply(connections, function(c) paste(sort(c(c$from, c$to)), collapse = "-"))
- )
- dup_table <- table(pair_keys$sorted)
- duplicates <- pair_keys$original[which(pair_keys$sorted %in% names(dup_table[dup_table > 1]))]
-
- for (conn in connections) {
- from <- as.numeric(conn$from)
- to <- as.numeric(conn$to)
- if (to == 0) next
-
- from_pos <- layout_df %>% filter(compartment == from)
- to_pos <- layout_df %>% filter(compartment == to)
-
- key <- paste(sort(c(from, to)), collapse = "-")
- offset <- if (key %in% duplicates) 0.25 else 0
-
- dx <- to_pos$x - from_pos$x
- dy <- to_pos$y - from_pos$y
- len <- sqrt(dx^2 + dy^2)
- norm_dx <- dx / len
- norm_dy <- dy / len
- perp_x <- -norm_dy
- perp_y <- norm_dx
-
- # Adjust start/end for box edges
- edge_dx <- box_width / 2 * norm_dx
- edge_dy <- box_height / 2 * norm_dy
-
- x1 <- from_pos$x + offset * perp_x + edge_dx
- y1 <- from_pos$y + offset * perp_y + edge_dy
- x2 <- to_pos$x + offset * perp_x - edge_dx
- y2 <- to_pos$y + offset * perp_y - edge_dy
-
- arrow_segments[[length(arrow_segments) + 1]] <- data.frame(
- x = x1, y = y1, xend = x2, yend = y2, color = "black"
- )
-
- # Arrowhead at 2/3
- frac <- 2 / 3
- xm <- x1 + frac * (x2 - x1)
- ym <- y1 + frac * (y2 - y1)
- perp_x_head <- -norm_dy * 0.10
- perp_y_head <- norm_dx * 0.10
-
- arrow_heads[[length(arrow_heads) + 1]] <- data.frame(
- x = c(xm - perp_x_head, xm + perp_x_head, xm + norm_dx * 0.3),
- y = c(ym - perp_y_head, ym + perp_y_head, ym + norm_dy * 0.3),
- group = paste0("arrow", length(arrow_heads) + 1),
- fill = "black"
- )
-
- if (!is.null(conn$coeff)) {
- key_xy <- paste(round((x1 + x2) / 2, 2), round((y1 + y2) / 2, 2))
- if (is.null(label_tracker[[key_xy]])) label_tracker[[key_xy]] <- 0
- vertical_offset <- 0.25 * label_tracker[[key_xy]]
- label_tracker[[key_xy]] <- label_tracker[[key_xy]] + 1
-
- mx <- (x1 + x2) / 2
- my <- (y1 + y2) / 2 - vertical_offset
-
- labels[[length(labels) + 1]] <- data.frame(
- x = mx, y = my, label = conn$coeff,
- color = "white", text_color = "black"
- )
- }
- }
-
- seg_df <- bind_rows(arrow_segments)
- head_df <- bind_rows(arrow_heads)
- label_df <- bind_rows(labels)
-
- elim_triangles <- layout_df %>%
- filter(compartment %in% elim_comps) %>%
- mutate(x = x - 0.4, y = y + 0.2)
-
- p <- ggplot()
-
- if (nrow(seg_df)>0){ # we have connections
- p <- p + geom_segment(data = seg_df,
- aes(x = x, y = y, xend = xend, yend = yend, color = color),
- linewidth = 0.7, show.legend = FALSE) +
- geom_polygon(data = head_df,
- aes(x = x, y = y, group = group, fill = fill),
- color = NA, show.legend = FALSE)
- }
-
- p <- p + geom_rect(data = layout_df,
- aes(xmin = x - box_width / 2, xmax = x + box_width / 2,
- ymin = y - box_height / 2, ymax = y + box_height / 2),
- fill = "grey80", color = "black") +
-
- geom_label(data = layout_df,
- aes(x = x, y = y + 0.15, label = compartment), fill = NA,
- color = "black", fontface = "bold", size = 7, label.size = NA) +
-
- geom_point(data = elim_triangles,
- aes(x = x, y = y),
- color = "black", shape = 2, size = 4)
-
- if (nrow(label_df) > 0) {
- p <- p + geom_label(
- data = label_df,
- aes(x = x, y = y, label = label),
- fill = label_df$color,
- color = label_df$text_color,
- fontface = "bold",
- size = 4,
- show.legend = FALSE,
- label.size = NA
- )
- }
-
- if (length(outputs) > 0) {
- out_df <- bind_rows(lapply(outputs, function(out) {
- comp <- out$compartment
- txt <- paste0("y[", out$output_num, "]")
- pos <- layout_df %>% filter(compartment == comp)
- data.frame(x = pos$x, y = pos$y-0.2, label = txt)
- }))
-
- p <- p + geom_label(
- data = out_df,
- aes(x = x, y = y, label = label),
- color = "black",
- fill = NA,
- fontface = "bold",
- size = 3,
- label.size = 0
- )
- }
-
- p <- p +
- coord_fixed() +
- xlim(range(layout_df$x) + c(-1.5, 1.5)) +
- ylim(range(layout_df$y) + c(-1.5, 1.5)) +
- theme_void() +
- ggtitle("Structural model") +
- scale_color_identity() +
- scale_fill_identity()
-
- return(p)
- }
-
- ##### FUNCTION CALLS
-
- #equations <- parse_equations(this_model)
- # Expand and distribute equations
-
- expanded_equations <- purrr::map(parse(text = tolower(eqns)), expand_distribute)
- outputs <- parse_output_equations(as.list(parse(text = tolower(outs))))
- out_comp <- map_chr(outputs, function(o) as.character(o$compartment))
- result <- extract_connections(expanded_equations)
- elim_count <- sum(sapply(result$connections, function(c) c$to == 0))
- elim_coeff <- map_chr(result$connections, function(c) if (c$to == 0) c$coeff else NA) %>% keep(~ !is.na(.))
-
- cli::cli_h1("Model elements")
- cli::cli_text("{length(result$compartments)} compartments")
- cli::cli_text("{length(result$connections)} connections, of which {elim_count} {?is an elimination/are eliminations}: {elim_coeff}")
- cli::cli_text("{length(outputs)} output{?s} in compartment{?s} {out_comp}")
-
-
- p <- create_plot(result$connections, result$compartments, outputs)
- if (print) print(p)
-
- return(
- invisible(list(
- p = p,
- connections = result$connections,
- compartments = result$compartments,
- outputs = outputs
- ))
- )
-
-
-
- }
-
-
-
\ No newline at end of file
+ newc[[length(newc) + 1]] <- list(sign = s, expr = prod_expr)
+ }
+ }
+ combos <- newc
+ }
+ # apply the incoming sign to all combos
+ for (i in seq_along(combos)) combos[[i]]$sign <- sign * combos[[i]]$sign
+ return(combos)
+ }
+
+ # other calls: expand children but treat as atomic w.r.t. addition
+ args <- as.list(e)
+ args[-1] <- lapply(args[-1], function(a) build_sum(expand_terms(a, +1)))
+ list(list(sign = sign, expr = as.call(args)))
+ }
+
+ # If it's an assignment, expand_terms already rebuilt it as a single term.
+ # Otherwise, build the flattened sum.
+ terms <- expand_terms(expr, +1)
+
+ # Special case: a single rebuilt assignment
+ if (length(terms) == 1 && is.call(terms[[1]]$expr) &&
+ op_of(terms[[1]]$expr) %in% c("=", "<-")) {
+ return(terms[[1]]$expr)
+ }
+
+ build_sum(terms)
+ }
+
+ # Parse output equations
+ parse_output_equations <- function(equations) {
+ # if (is.null(func)) return(list())
+
+ # equations <- parse_equations(func)
+ outputs <- list()
+
+ for (eq in equations) {
+ lhs <- eq[[2]]
+ rhs <- eq[[3]]
+
+ # Extract output number from y[i]
+ if (is.call(lhs) && as.character(lhs[[1]]) == "[" &&
+ length(lhs) >= 3 && as.character(lhs[[2]]) == "y") {
+ output_num <- as.numeric(as.character(lhs[[3]]))
+
+ # Convert RHS to string representation
+ rhs_str <- deparse(rhs, width.cutoff = 500)
+
+ # Find which compartment this output refers to
+ comp_ref <- extract_x_pattern(rhs)
+ if (is.null(comp_ref)) {
+ # Look deeper in the expression for x[i] patterns
+ comp_ref <- find_x_in_expression(rhs)
+ }
+
+ outputs <- append(outputs, list(list(
+ output_num = output_num,
+ equation = rhs_str,
+ compartment = comp_ref
+ )))
+ }
+ }
+
+ return(outputs)
+ }
+
+ # Find x[i] pattern in any expression
+ find_x_in_expression <- function(expr) {
+ if (is.call(expr)) {
+ # Check current expression
+ x_idx <- extract_x_pattern(expr)
+ if (!is.null(x_idx)) {
+ return(x_idx)
+ }
+
+ # Recursively check sub-expressions
+ for (i in 1:length(expr)) {
+ if (i > 1) { # Skip the function name
+ x_idx <- find_x_in_expression(expr[[i]])
+ if (!is.null(x_idx)) {
+ return(x_idx)
+ }
+ }
+ }
+ }
+ return(NULL)
+ }
+
+ # Parse terms from right-hand side recursively
+ parse_rhs_terms <- function(rhs_expr) {
+ terms <- list()
+
+ # Recursively extract terms and track sign
+ extract_terms <- function(expr, current_sign = "+") {
+ if (is.call(expr)) {
+ op <- as.character(expr[[1]])
+
+ if (op == "+") {
+ extract_terms(expr[[2]], current_sign)
+ extract_terms(expr[[3]], current_sign)
+ } else if (op == "-") {
+ if (length(expr) == 3) {
+ # Binary subtraction: a - b
+ extract_terms(expr[[2]], current_sign)
+ extract_terms(expr[[3]], ifelse(current_sign == "+", "-", "+"))
+ } else {
+ # Unary minus: -a
+ extract_terms(expr[[2]], ifelse(current_sign == "+", "-", "+"))
+ }
+ } else if (op == "*") {
+ # Look for x[i] and collect coefficient(s)
+ vars <- lapply(expr[-1], extract_x_pattern)
+ if (any(!sapply(vars, is.null))) {
+ xi_index <- which(!sapply(vars, is.null))
+ x_part <- expr[[xi_index + 1]]
+ coeff_parts <- expr[-c(1, xi_index + 1)]
+ coeff_str <- paste(sapply(coeff_parts, deparse), collapse = "*")
+ terms <<- append(terms, list(list(expr = x_part, coeff = coeff_str, sign = current_sign)))
+ } else {
+ # No x[i], maybe just a numeric or unrelated variable
+ terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
+ }
+ } else {
+ # Some other operation; treat as atomic for now
+ terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
+ }
+ } else {
+ # Symbol or constant
+ terms <<- append(terms, list(list(expr = expr, coeff = NULL, sign = current_sign)))
+ }
+ }
+
+ extract_terms(rhs_expr)
+
+ return(terms)
+ }
+
+ # Extract x[i] pattern from expression
+ extract_x_pattern <- function(expr) {
+ if (is.call(expr) && as.character(expr[[1]]) == "[" &&
+ length(expr) == 3 && as.character(expr[[2]]) == "x") {
+ return(as.numeric(as.character(expr[[3]])))
+ }
+ return(NULL)
+ }
+
+ # Extract compartment connections
+ extract_connections <- function(equations) {
+ compartments <- c()
+ all_terms <- list()
+
+ # First pass: collect signed terms per compartment
+ for (eq in equations) {
+ lhs <- eq[[2]]
+ rhs <- eq[[3]]
+
+ if (is.call(lhs) && as.character(lhs[[1]]) == "[" &&
+ length(lhs) >= 3 && as.character(lhs[[2]]) == "dx") {
+ comp_num <- as.numeric(as.character(lhs[[3]]))
+ compartments <- unique(c(compartments, comp_num))
+
+ # dist_terms <- distribute_product(rhs)
+ # terms <- parse_rhs_terms(dist_terms)
+ terms <- parse_rhs_terms(rhs)
+
+ for (term in terms) {
+ expr <- term$expr
+ sign <- term$sign
+ coeff <- term$coeff
+
+ x_index <- extract_x_pattern(expr)
+ if (!is.null(x_index)) {
+ all_terms <- append(all_terms, list(list(
+ comp = comp_num,
+ sign = sign,
+ coeff = coeff,
+ x_index = x_index
+ )))
+ }
+ }
+ }
+ }
+
+ # Second pass: match positive and negative terms
+ used <- logical(length(all_terms))
+ connections <- list()
+
+ for (i in seq_along(all_terms)) {
+ ti <- all_terms[[i]]
+ if (used[i] || ti$sign != "-") next
+
+ match_found <- FALSE
+ for (j in seq_along(all_terms)) {
+ tj <- all_terms[[j]]
+ if (used[j] || tj$sign != "+") next
+
+ # Match by coeff and x_index
+ if (identical(ti$coeff, tj$coeff) && ti$x_index == tj$x_index) {
+ connections <- append(connections, list(list(
+ from = ti$comp,
+ to = tj$comp,
+ coeff = ti$coeff
+ )))
+ used[i] <- TRUE
+ used[j] <- TRUE
+ match_found <- TRUE
+ break
+ }
+ }
+
+ # If no match, it's elimination
+ if (!match_found) {
+ connections <- append(connections, list(list(
+ from = ti$comp,
+ to = 0,
+ coeff = ti$coeff
+ )))
+ used[i] <- TRUE
+ }
+ }
+
+ return(list(connections = connections, compartments = sort(compartments)))
+ }
+
+
+
+ # Modify layout logic to use circular positioning
+ create_plot <- function(connections, compartments, outputs) {
+ library(ggplot2)
+ library(dplyr)
+
+ box_width <- 1.2
+ box_height <- 0.8
+
+ n_comp <- length(compartments)
+ if (n_comp == 0) {
+ plot.new()
+ title(main = "No compartments detected")
+ return()
+ }
+
+ # Circular layout
+ radius <- 4
+ angles <- seq(0, 2 * pi, length.out = n_comp + 1)[-(n_comp + 1)]
+ angles <- angles - angles[which(compartments == 1)] + pi / 2
+ x_pos <- radius * cos(angles)
+ y_pos <- radius * sin(angles)
+ layout_df <- data.frame(compartment = compartments, x = x_pos, y = y_pos)
+
+ # Elimination
+ elim_comps <- unique(sapply(connections, function(c) if (c$to == 0) c$from else NULL))
+ elim_comps <- elim_comps[!sapply(elim_comps, is.null)]
+
+ arrow_segments <- list()
+ arrow_heads <- list()
+ labels <- list()
+ label_tracker <- list()
+
+ # Bidirectional detection
+ pair_keys <- data.frame(
+ original = sapply(connections, function(c) paste(c(c$from, c$to), collapse = "-")),
+ sorted = sapply(connections, function(c) paste(sort(c(c$from, c$to)), collapse = "-"))
+ )
+ dup_table <- table(pair_keys$sorted)
+ duplicates <- pair_keys$original[which(pair_keys$sorted %in% names(dup_table[dup_table > 1]))]
+
+ for (conn in connections) {
+ from <- as.numeric(conn$from)
+ to <- as.numeric(conn$to)
+ if (to == 0) next
+
+ from_pos <- layout_df %>% filter(compartment == from)
+ to_pos <- layout_df %>% filter(compartment == to)
+
+ key <- paste(sort(c(from, to)), collapse = "-")
+ offset <- if (key %in% duplicates) 0.25 else 0
+
+ dx <- to_pos$x - from_pos$x
+ dy <- to_pos$y - from_pos$y
+ len <- sqrt(dx^2 + dy^2)
+ norm_dx <- dx / len
+ norm_dy <- dy / len
+ perp_x <- -norm_dy
+ perp_y <- norm_dx
+
+ # Adjust start/end for box edges
+ edge_dx <- box_width / 2 * norm_dx
+ edge_dy <- box_height / 2 * norm_dy
+
+ x1 <- from_pos$x + offset * perp_x + edge_dx
+ y1 <- from_pos$y + offset * perp_y + edge_dy
+ x2 <- to_pos$x + offset * perp_x - edge_dx
+ y2 <- to_pos$y + offset * perp_y - edge_dy
+
+ arrow_segments[[length(arrow_segments) + 1]] <- data.frame(
+ x = x1, y = y1, xend = x2, yend = y2, color = "black"
+ )
+
+ # Arrowhead at 2/3
+ frac <- 2 / 3
+ xm <- x1 + frac * (x2 - x1)
+ ym <- y1 + frac * (y2 - y1)
+ perp_x_head <- -norm_dy * 0.10
+ perp_y_head <- norm_dx * 0.10
+
+ arrow_heads[[length(arrow_heads) + 1]] <- data.frame(
+ x = c(xm - perp_x_head, xm + perp_x_head, xm + norm_dx * 0.3),
+ y = c(ym - perp_y_head, ym + perp_y_head, ym + norm_dy * 0.3),
+ group = paste0("arrow", length(arrow_heads) + 1),
+ fill = "black"
+ )
+
+ if (!is.null(conn$coeff)) {
+ key_xy <- paste(round((x1 + x2) / 2, 2), round((y1 + y2) / 2, 2))
+ if (is.null(label_tracker[[key_xy]])) label_tracker[[key_xy]] <- 0
+ vertical_offset <- 0.25 * label_tracker[[key_xy]]
+ label_tracker[[key_xy]] <- label_tracker[[key_xy]] + 1
+
+ mx <- (x1 + x2) / 2
+ my <- (y1 + y2) / 2 - vertical_offset
+
+ labels[[length(labels) + 1]] <- data.frame(
+ x = mx, y = my, label = conn$coeff,
+ color = "white", text_color = "black"
+ )
+ }
+ }
+
+ seg_df <- bind_rows(arrow_segments)
+ head_df <- bind_rows(arrow_heads)
+ label_df <- bind_rows(labels)
+
+ elim_triangles <- layout_df %>%
+ filter(compartment %in% elim_comps) %>%
+ mutate(x = x - 0.4, y = y + 0.2)
+
+ p <- ggplot()
+
+ if (nrow(seg_df) > 0) { # we have connections
+ p <- p + geom_segment(
+ data = seg_df,
+ aes(x = x, y = y, xend = xend, yend = yend, color = color),
+ linewidth = 0.7, show.legend = FALSE
+ ) +
+ geom_polygon(
+ data = head_df,
+ aes(x = x, y = y, group = group, fill = fill),
+ color = NA, show.legend = FALSE
+ )
+ }
+
+ p <- p + geom_rect(
+ data = layout_df,
+ aes(
+ xmin = x - box_width / 2, xmax = x + box_width / 2,
+ ymin = y - box_height / 2, ymax = y + box_height / 2
+ ),
+ fill = "grey80", color = "black"
+ ) +
+
+ geom_label(
+ data = layout_df,
+ aes(x = x, y = y + 0.15, label = compartment), fill = NA,
+ color = "black", fontface = "bold", size = 7, label.size = NA
+ ) +
+
+ geom_point(
+ data = elim_triangles,
+ aes(x = x, y = y),
+ color = "black", shape = 2, size = 4
+ )
+
+ if (nrow(label_df) > 0) {
+ p <- p + geom_label(
+ data = label_df,
+ aes(x = x, y = y, label = label),
+ fill = label_df$color,
+ color = label_df$text_color,
+ fontface = "bold",
+ size = 4,
+ show.legend = FALSE,
+ label.size = NA
+ )
+ }
+
+ if (length(outputs) > 0) {
+ out_df <- bind_rows(lapply(outputs, function(out) {
+ comp <- out$compartment
+ txt <- paste0("y[", out$output_num, "]")
+ pos <- layout_df %>% filter(compartment == comp)
+ data.frame(x = pos$x, y = pos$y - 0.2, label = txt)
+ }))
+
+ p <- p + geom_label(
+ data = out_df,
+ aes(x = x, y = y, label = label),
+ color = "black",
+ fill = NA,
+ fontface = "bold",
+ size = 3,
+ label.size = 0
+ )
+ }
+
+ p <- p +
+ coord_fixed() +
+ xlim(range(layout_df$x) + c(-1.5, 1.5)) +
+ ylim(range(layout_df$y) + c(-1.5, 1.5)) +
+ theme_void() +
+ ggtitle("Structural model") +
+ scale_color_identity() +
+ scale_fill_identity()
+
+ return(p)
+ }
+
+ ##### FUNCTION CALLS
+
+ # equations <- parse_equations(this_model)
+ # Expand and distribute equations
+
+ expanded_equations <- purrr::map(parse(text = tolower(eqns)), expand_distribute)
+ outputs <- parse_output_equations(as.list(parse(text = tolower(outs))))
+ out_comp <- map_chr(outputs, function(o) as.character(o$compartment))
+ result <- extract_connections(expanded_equations)
+ elim_count <- sum(sapply(result$connections, function(c) c$to == 0))
+ elim_coeff <- map_chr(result$connections, function(c) if (c$to == 0) c$coeff else NA) %>% keep(~ !is.na(.))
+
+ cli::cli_h1("Model elements")
+ cli::cli_text("{length(result$compartments)} compartments")
+ cli::cli_text("{length(result$connections)} connections, of which {elim_count} {?is an elimination/are eliminations}: {elim_coeff}")
+ cli::cli_text("{length(outputs)} output{?s} in compartment{?s} {out_comp}")
+
+
+ p <- create_plot(result$connections, result$compartments, outputs)
+ if (print) print(p)
+
+ return(
+ invisible(list(
+ p = p,
+ connections = result$connections,
+ compartments = result$compartments,
+ outputs = outputs
+ ))
+ )
+}
diff --git a/R/PM_result.R b/R/PM_result.R
index e0e324523..a2c4aaa64 100755
--- a/R/PM_result.R
+++ b/R/PM_result.R
@@ -54,7 +54,7 @@ PM_result <- R6::R6Class(
#' Use the `$save` method on the augmented `PM_result` object to save it with the
#' new optimal sampling results.
opt_samp = NULL,
-
+
#' @description
#' Create new object populated with data from previous run
#' @details
@@ -73,9 +73,9 @@ PM_result <- R6::R6Class(
if (!inherits(out[[x]], "R6")) { # older save
cli::cli_abort(c("x" = "The object was saved in an older format. Please re-run the analysis."))
} else {
- if(x == "model"){
- args <- list(x = out[[x]], compile = FALSE)
- } else {
+ if (x == "model") {
+ args <- list(x = out[[x]], compile = FALSE)
+ } else {
args <- list(out[[x]], path = path, quiet = TRUE)
}
self[[x]] <- do.call(get(paste0("PM_", x))$new, args = args) # was saved in R6 format, but remake to update if needed
@@ -83,24 +83,24 @@ PM_result <- R6::R6Class(
}
}
)
-
+
# these are diagnostics, not R6
self$errfile <- out$errfile
self$success <- out$success
-
+
# add the pop/post data to data
if (is.null(self$data$pop) | is.null(self$data$post)) {
self$data <- PM_data$new(self$data$data, quiet = TRUE)
self$data$pop <- self$pop$data
self$data$post <- self$post$data
}
-
+
return(self)
},
#' @description
#' Fit the model to the data
#' #' @details
- #' This method is used to fit the model in the [PM_result] object to data.
+ #' This method is used to fit the model in the [PM_result] object to data.
#' It calls the `$fit` method of the model stored in the `model` field.
#' @param data Optional data to fit. If not provided, the data stored in the
#' `data` field of the [PM_result] object will be used. This can be useful to
@@ -109,15 +109,15 @@ PM_result <- R6::R6Class(
#' @param ... Additional arguments passed to the model's `$fit` method.
#' @return Returns an invisible [PM_result].
#' @export
- #'
- fit = function(data, ...){
+ #'
+ fit = function(data, ...) {
if (missing(data)) {
data <- self$data
}
res <- self$model$fit(data = data, ...)
return(invisible(res))
},
-
+
#' @description
#' Plot generic function based on type
#' @param type Type of plot based on class of object
@@ -129,7 +129,7 @@ PM_result <- R6::R6Class(
self[[type]]$plot(...)
}
},
-
+
#' @description
#' Summary generic function based on type
#' @param type Type of summary based on class of object
@@ -141,7 +141,7 @@ PM_result <- R6::R6Class(
self[[type]]$summary(...)
}
},
-
+
#' @description
#' AUC generic function based on type
#' @param type Type of AUC based on class of object
@@ -152,7 +152,7 @@ PM_result <- R6::R6Class(
}
self[[type]]$auc(...)
},
-
+
#' @description
#' Perform non-compartmental analysis
#' @details
@@ -180,22 +180,88 @@ PM_result <- R6::R6Class(
if (!"poppar" %in% names(dots)) {
dots$poppar <- self$final
}
-
+
if (!"data" %in% names(dots)) {
dots$data <- self$data
}
-
+
if (!"model" %in% names(dots)) {
dots$model <- self$model
}
-
+
# store copy of the final object
bk_final <- self$final$clone()
sim <- do.call(PM_sim$new, dots)
self$final <- bk_final
return(sim)
},
-
+
+ #' @description
+ #' Run BestDose optimization using this result as the prior
+ #' @details
+ #' BestDose finds optimal dosing regimens to achieve target drug concentrations
+ #' or AUC values. The algorithm uses Bayesian posterior estimation combined with
+ #' dual optimization to balance patient-specific adaptation and population-level
+ #' robustness. By default, uses the `$final`, `$model`, and `$data` objects from
+ #' this result. Most commonly, you will supply a different `target` data object.
+ #' @param target PM_data object or path to CSV with target doses/observations.
+ #' Required. This defines the dosing template and target values. Set dose amounts
+ #' to 0 for doses to be optimized.
+ #' @param past_data Optional: PM_data object or path to CSV with patient history.
+ #' If NULL (default), uses the full dataset from this result.
+ #' @param dose_range Named list with min and max allowable doses.
+ #' Default: list(min = 0, max = 1000)
+ #' @param bias_weight Numeric [0,1] controlling personalization level.
+ #' 0 = fully personalized, 1 = population-based. Default: 0.5 (balanced)
+ #' @param target_type One of "concentration" (default), "auc_from_zero", or
+ #' "auc_from_last_dose"
+ #' @param time_offset Optional: time offset for past/future concatenation
+ #' @param ... Additional arguments passed to [PM_bestdose]
+ #' @return A [PM_bestdose] object containing optimal doses and predictions
+ #' @examples
+ #' \dontrun{
+ #' # Load NPAG result
+ #' result <- PM_load(1)
+ #'
+ #' # Create target data
+ #' target <- PM_data$new("target.csv")
+ #'
+ #' # Run BestDose optimization
+ #' bd <- result$bestdose(
+ #' target = target,
+ #' dose_range = list(min = 50, max = 500),
+ #' bias_weight = 0.5
+ #' )
+ #'
+ #' # View results
+ #' print(bd)
+ #' bd$get_doses()
+ #' }
+ bestdose = function(target,
+ past_data = NULL,
+ dose_range = list(min = 0, max = 1000),
+ bias_weight = 0.5,
+ target_type = "concentration",
+ time_offset = NULL,
+ ...) {
+ # Use this result's data as past_data if not specified
+ if (is.null(past_data)) {
+ past_data <- self$data
+ }
+
+ PM_bestdose$new(
+ prior = self,
+ model = self$model,
+ past_data = past_data,
+ target = target,
+ dose_range = dose_range,
+ bias_weight = bias_weight,
+ target_type = target_type,
+ time_offset = time_offset,
+ ...
+ )
+ },
+
#' @description
#' Save the current PM_result object to an .Rdata file.
#' @details
@@ -216,7 +282,7 @@ PM_result <- R6::R6Class(
#' your current working directory, specify `run = 1` to save the result to the "outputs"
#' subfolder of the "1" folder.
#' @param file Custom file name. Default is "PMout.Rdata". If `run` is not specified, `file`
- #' should be the full path and filename.
+ #' should be the full path and filename.
save = function(run, file = "PMout.Rdata") {
if (missing(run)) {
cli::cli_inform(c(
@@ -243,7 +309,7 @@ PM_result <- R6::R6Class(
)
save(PMout, file = paste0(outputfolder, "/", file))
},
-
+
#' @description
#' Validate the result by internal simulation methods.
#' @param ... Arguments passed to [PM_valid].
@@ -255,9 +321,8 @@ PM_result <- R6::R6Class(
" " = "For example, if your results are in {.code my_run}, use {.code my_run$save(1)} to save back to the outputs folder of run 1."
))
return(invisible(self))
-
},
-
+
#' @description
#' Conduct stepwise linear regression of Bayesian posterior parameter values
#' and covariates.
@@ -265,7 +330,7 @@ PM_result <- R6::R6Class(
step = function(...) {
PM_step(self$cov$data, ...)
},
-
+
#' @description
#' Calculate optimal sampling times.
#'
@@ -280,7 +345,7 @@ PM_result <- R6::R6Class(
})
return(invisible(self))
},
-
+
#' @description
#' `r lifecycle::badge("deprecated")`
#'
@@ -344,7 +409,7 @@ PM_result$load <- function(...) {
#' run4 <- PM_load(file = "Pmetrics/MyRuns/4/outputs/PMout.Rdata") # loads from Pmetrics/MyRuns/4/outputs/PMout.Rdata
#' run5 <- PM_load() # loads from ./PMout.Rdata
#' }
-#'
+#'
#' @author Michael Neely and Julian Otalvaro
#' @seealso [PM_final],
#' [PM_cycle], [PM_op], [PM_cov],
@@ -361,20 +426,22 @@ PM_load <- function(run, path = ".", file = "PMout.Rdata") {
names(aux_list) <- names(Out)[i]
result <- append(result, aux_list)
}
-
+
return(result)
}
-
+
found <- "" # initialize
-
+
if (!missing(run)) {
filepath <- file.path(path, run, "outputs", file)
} else {
filepath <- file.path(path, file)
- }
-
- if (file.exists(filepath)) { found <- filepath }
-
+ }
+
+ if (file.exists(filepath)) {
+ found <- filepath
+ }
+
if (found != "") {
result <- output2List(Out = get(load(found)))
rebuild <- PM_result$new(result, path = dirname(found), quiet = TRUE)
@@ -398,31 +465,32 @@ update <- function(res, found) {
# start conversion
n_cyc <- nrow(dat$mean)
n_out <- max(res$op$outeq)
- dat$gamlam <- dat$gamlam %>% select(starts_with("add")|starts_with("prop"))
+ dat$gamlam <- dat$gamlam %>% select(starts_with("add") | starts_with("prop"))
if (ncol(gamlam) == 1 & n_out > 1) {
gamlam <- cbind(gamlam, replicate((n_out - 1), gamlam[, 1]))
}
gamlam <- gamlam %>%
- pivot_longer(
- cols = everything(),
- values_to = "value", names_to = c("type", "outeq"),
- names_sep = "\\."
- ) %>%
- mutate(cycle = rep(1:n_cyc, each = n_out)) %>%
- select(cycle, value, outeq, type) %>% arrange(cycle, outeq)
+ pivot_longer(
+ cols = everything(),
+ values_to = "value", names_to = c("type", "outeq"),
+ names_sep = "\\."
+ ) %>%
+ mutate(cycle = rep(1:n_cyc, each = n_out)) %>%
+ select(cycle, value, outeq, type) %>%
+ arrange(cycle, outeq)
if (is.matrix(dat$mean)) { # old fortran format, but not rust format
dat$mean <- tibble::tibble(cycle = 1:n_cyc) %>%
- dplyr::bind_cols(tidyr::as_tibble(dat$mean))
+ dplyr::bind_cols(tidyr::as_tibble(dat$mean))
dat$median <- tibble::tibble(cycle = 1:n_cyc) %>%
- dplyr::bind_cols(tidyr::as_tibble(dat$median))
+ dplyr::bind_cols(tidyr::as_tibble(dat$median))
dat$sd <- tibble::tibble(cycle = 1:n_cyc) %>%
- dplyr::bind_cols(tidyr::as_tibble(dat$sd))
+ dplyr::bind_cols(tidyr::as_tibble(dat$sd))
}
msg <- c(msg, "cycle")
res$cycle <- dat
}
}
-
+
####### DONE PROCESSING, INFORM #########
if (!is.null(msg)) {
cat(
@@ -450,6 +518,6 @@ update <- function(res, found) {
cat("Results saved\n")
}
}
-
+
return(res)
}
diff --git a/R/PMbuild.R b/R/PMbuild.R
index fe6c5bcad..f4c774142 100755
--- a/R/PMbuild.R
+++ b/R/PMbuild.R
@@ -13,8 +13,7 @@ PM_build <- function() {
clear_build() # clean prior template/artifacts
if (is_rustup_installed()) {
cli::cli_text("Rust was detected in your system, Fetching dependencies and building base project.")
- template <- dummy_compile()
-
+ template <- dummy_compile(tools::R_user_dir(package = "Pmetrics"))
} else {
cli::cli_text("Rust was not detected in your system, this can be caused by multiple reasons:")
ul <- cli::cli_ul()
diff --git a/R/extendr-wrappers.R b/R/extendr-wrappers.R
index 0fa84eb57..e199aec1d 100755
--- a/R/extendr-wrappers.R
+++ b/R/extendr-wrappers.R
@@ -20,11 +20,11 @@ simulate_all <- function(data_path, model_path, theta, kind) .Call(wrap__simulat
#' Compiles the text representation of a model into a binary file.
#'@export
-compile_model <- function(model_path, output_path, params, kind) .Call(wrap__compile_model, model_path, output_path, params, kind)
+compile_model <- function(model_path, template_path, output_path, params, kind) .Call(wrap__compile_model, model_path, template_path, output_path, params, kind)
#' Dummy function to cache compilation artifacts.
#'@export
-dummy_compile <- function() .Call(wrap__dummy_compile)
+dummy_compile <- function(template_path) .Call(wrap__dummy_compile, template_path)
#'@export
is_cargo_installed <- function() .Call(wrap__is_cargo_installed)
@@ -35,14 +35,18 @@ fit <- function(model_path, data, params, output_path, kind) .Call(wrap__fit, mo
#'@export
model_parameters <- function(model_path, kind) .Call(wrap__model_parameters, model_path, kind)
-template_path <- function() .Call(wrap__template_path)
-
-clear_build <- function() invisible(.Call(wrap__clear_build))
-
#' Initialize the tracing subscriber with the custom R formatter
#' @keywords internal
#'@export
setup_logs <- function() .Call(wrap__setup_logs)
+#' Run BestDose optimization to find optimal doses
+#'@export
+bestdose <- function(model_path, prior_path, past_data_path, target_data_path, time_offset, dose_min, dose_max, bias_weight, target_type, params, kind) .Call(wrap__bestdose, model_path, prior_path, past_data_path, target_data_path, time_offset, dose_min, dose_max, bias_weight, target_type, params, kind)
+
+bestdose_prepare <- function(model_path, prior_path, past_data_path, target_data_path, time_offset, dose_min, dose_max, bias_weight, target_type, params, kind) .Call(wrap__bestdose_prepare, model_path, prior_path, past_data_path, target_data_path, time_offset, dose_min, dose_max, bias_weight, target_type, params, kind)
+
+bestdose_optimize <- function(handle, bias_weight) .Call(wrap__bestdose_optimize, handle, bias_weight)
+
# nolint end
diff --git a/inst/.gitignore b/inst/.gitignore
index 28f483682..4472f674a 100755
--- a/inst/.gitignore
+++ b/inst/.gitignore
@@ -1,3 +1,5 @@
.DS_Store
/.quarto/
+
+template/
diff --git a/inst/Examples/Rscript/bestdose_simple_test.R b/inst/Examples/Rscript/bestdose_simple_test.R
new file mode 100644
index 000000000..7cab124fa
--- /dev/null
+++ b/inst/Examples/Rscript/bestdose_simple_test.R
@@ -0,0 +1,50 @@
+library(Pmetrics)
+
+setwd("inst/Examples/Runs")
+
+mod_onecomp <- PM_model$new(
+ pri = list(
+ ke = ab(0.001, 3.0),
+ v = ab(25.0, 250.0)
+ ),
+ eqn = function() {
+ dx[1] <- -ke * X[1] + B[1]
+ },
+ out = function() {
+ Y[1] <- X[1] / v
+ },
+ err = list(
+ additive(1, c(0, 0.20, 0, 0))
+ )
+)
+
+
+past_file <- "../src/bestdose_past.csv"
+target_file <- "../src/bestdose_target.csv"
+prior_file <- "../src/bestdose_prior.csv"
+
+
+# Prepare the problem once (posterior + handle to optimized model)
+problem <- PM_bestdose_problem$new(
+ prior = prior_file,
+ model = mod_onecomp,
+ past_data = past_file,
+ target = target_file,
+ dose_range = list(min = 0, max = 300),
+ bias_weight = 0.0,
+ target_type = "concentration" # "concentration", "auc_from_zero", "auc_from_last_dose"
+)
+
+cat("\nPosterior support points:\n")
+print(head(problem$theta))
+
+# Reuse the same problem for different bias weights
+bias_weights <- seq(0, 1, by = 0.25)
+results <- lapply(bias_weights, function(lambda) {
+ problem$optimize(bias_weight = lambda)
+})
+
+for (i in seq_along(results)) {
+ cat("\n=== Bias weight:", bias_weights[i], "===\n")
+ results[[i]]$print()
+}
diff --git a/inst/Examples/Runs/bestdose_result.rds b/inst/Examples/Runs/bestdose_result.rds
new file mode 100644
index 000000000..ead529e5b
Binary files /dev/null and b/inst/Examples/Runs/bestdose_result.rds differ
diff --git a/inst/Examples/src/bestdose_past.csv b/inst/Examples/src/bestdose_past.csv
new file mode 100644
index 000000000..f31bbc5e9
--- /dev/null
+++ b/inst/Examples/src/bestdose_past.csv
@@ -0,0 +1,9 @@
+"id","evid","time","dur","dose","addl","ii","input","out","outeq","c0","c1","c2","c3"
+1,1,0,0,150,0,0,1,.,.,.,.,.,.
+1,0,2,.,.,.,.,.,0.759050697604428,1,.,.,.,.
+1,0,4,.,.,.,.,.,0.384085169721793,1,.,.,.,.
+1,0,6,.,.,.,.,.,0.194349887386702,1,.,.,.,.
+1,1,12,0,75,0,0,1,.,.,.,.,.,.
+1,0,14,.,.,.,.,.,0.392266577540038,1,.,.,.,.
+1,0,16,.,.,.,.,.,0.198489739204705,1,.,.,.,.
+1,0,18,.,.,.,.,.,0.100437250648841,1,.,.,.,.
diff --git a/inst/Examples/src/bestdose_prior.csv b/inst/Examples/src/bestdose_prior.csv
new file mode 100644
index 000000000..0559dea12
--- /dev/null
+++ b/inst/Examples/src/bestdose_prior.csv
@@ -0,0 +1,47 @@
+ke,v,prob
+0.08736658442020416,104.1576635837555,0.06411826509758521
+0.1305751524925232,97.43967413902283,0.03921568628597478
+0.3540655417442322,86.90321147441864,0.03930537754808413
+0.2908282660484314,113.02810430526733,0.05369391213529181
+0.10566180405616761,140.3564077615738,0.01960784315457728
+0.9795492498397828,221.82963848114014,0.019607843139348134
+0.3214192095041275,68.54407012462616,0.019607843027666164
+0.04363290762901306,85.5460512638092,0.01960721617788871
+0.3493796042442322,75.69715678691864,0.019607073213094923
+0.09591740503311158,70.4919171333313,0.019606975069368423
+0.3318302191734314,92.54958868026733,0.019607488410309255
+0.3091094713449478,92.16600060462952,0.01969419594801783
+0.3319534166574478,118.26951622962952,0.019631823530513178
+0.06250557525157929,107.17031538486481,0.019670607599452994
+0.06885235497951508,76.57066643238068,0.01970716827239974
+0.104416601395607,91.60142242908478,0.019644091781941257
+0.11730292952060699,78.54966461658478,0.019597452684180432
+0.06250557525157929,95.70058882236481,0.019558918237699178
+0.013994081234931946,178.82485330104828,0.019739518261060587
+0.3406021231412888,99.99475717544556,0.019495129816772868
+0.07190197534561157,67.0641827583313,0.019509009848450342
+0.02050767450332642,140.61931788921356,0.019494247900270656
+0.283145404958725,125.85132718086243,0.021689589949674865
+0.28171865940093993,140.98521947860718,0.020065175979272123
+0.2902425238609314,94.48318243026733,0.02049255770567427
+0.09111055810451509,93.09410393238068,0.019720100726323925
+0.2925854926109314,128.45290899276733,0.019127402830562693
+0.2925854926109314,105.60134649276733,0.02235654024528764
+0.3019573676109314,127.61794805526733,0.01954540376390492
+0.044804392004013066,99.6524965763092,0.021082697423336363
+0.28490263152122497,92.62867093086243,0.018708872021706954
+0.08112040762901307,98.0704653263092,0.021075404370655606
+0.036757444095611574,109.3835186958313,0.018114822039674315
+0.09826037378311157,111.2731671333313,0.021727337750962878
+0.09298869409561158,113.6022686958313,0.01713443002609149
+0.3132096666574478,115.89646935462952,0.00474084568678399
+0.07658791284561157,113.3385968208313,0.002945117861059519
+0.08678084223270416,87.5463354587555,0.02933991222454541
+0.08736658442020416,128.0199682712555,0.019952065962974042
+0.05615650746822357,130.38859486579895,0.000015108929159013186
+0.056742249655723574,131.13566517829895,0.012605671648896763
+0.08736658442020416,87.5463354587555,0.007152585492468555
+0.07658791284561157,113.3825421333313,0.03210569535127533
+0.3132096666574478,115.94041466712952,0.03478763091025865
+0.05615650746822357,130.43254017829895,0.04616814785864839
+0.08736658442020416,127.9760229587555,0.00001919610085464287
diff --git a/inst/Examples/src/bestdose_target.csv b/inst/Examples/src/bestdose_target.csv
new file mode 100644
index 000000000..29d8788a6
--- /dev/null
+++ b/inst/Examples/src/bestdose_target.csv
@@ -0,0 +1,9 @@
+"id","evid","time","dur","dose","addl","ii","input","out","outeq","c0","c1","c2","c3"
+1,1,0,0,0,0,0,1,.,.,.,.,.,.
+1,0,2,.,.,.,.,.,0.759050697604428,1,.,.,.,.
+1,0,4,.,.,.,.,.,0.384085169721793,1,.,.,.,.
+1,0,6,.,.,.,.,.,0.194349887386702,1,.,.,.,.
+1,1,12,0,0,0,0,1,.,.,.,.,.,.
+1,0,14,.,.,.,.,.,0.392266577540038,1,.,.,.,.
+1,0,16,.,.,.,.,.,0.198489739204705,1,.,.,.,.
+1,0,18,.,.,.,.,.,0.100437250648841,1,.,.,.,.
diff --git a/man/PM_model.Rd b/man/PM_model.Rd
index 4eb85ea03..685ee7ba1 100755
--- a/man/PM_model.Rd
+++ b/man/PM_model.Rd
@@ -32,41 +32,41 @@ a \code{donttest} block to avoid automatic compilation.
mod_list <- list(
pri = c(
- CL = ab(10, 200),
- V0 = ab(0, 100),
- ka = ab(0, 3),
- k23 = ab(0, 5),
- k32 = ab(0, 5),
- lag1 = ab(0, 2)
- ),
- cov = c(
- wt = interp()
- ),
- sec = function() {
- V = V0 * (wt/70)
- ke = CL/V # define here to make eqn simpler
- },
- eqn = function() {
- dx[1] = -ka * x[1]
- dx[2] = rateiv[1] + ka * x[1] - (ke + k23) * x[2] + k32 * x[3]
- dx[3] = k23 * x[2] - k32 * x[3]
- dx[4] = x[1] / V
- },
- lag = function() {
- tlag[1] = lag1
- },
- out = function() {
- y[1] = x[1]/V
- y[2] = x[4] # AUC, not fitted to any data, not required
- },
- err = c(
- proportional(2, c(0.1, 0.15, 0, 0)) # only applies to y[1]
- )
- )
+ CL = ab(10, 200),
+ V0 = ab(0, 100),
+ ka = ab(0, 3),
+ k23 = ab(0, 5),
+ k32 = ab(0, 5),
+ lag1 = ab(0, 2)
+ ),
+ cov = c(
+ wt = interp()
+ ),
+ sec = function() {
+ V <- V0 * (wt / 70)
+ ke <- CL / V # define here to make eqn simpler
+ },
+ eqn = function() {
+ dx[1] <- -ka * x[1]
+ dx[2] <- rateiv[1] + ka * x[1] - (ke + k23) * x[2] + k32 * x[3]
+ dx[3] <- k23 * x[2] - k32 * x[3]
+ dx[4] <- x[1] / V
+ },
+ lag = function() {
+ tlag[1] <- lag1
+ },
+ out = function() {
+ y[1] <- x[1] / V
+ y[2] <- x[4] # AUC, not fitted to any data, not required
+ },
+ err = c(
+ proportional(2, c(0.1, 0.15, 0, 0)) # only applies to y[1]
+ )
+)
\donttest{
- mod <- PM_model$new(mod_list)
- }
+mod <- PM_model$new(mod_list)
+}
}
\author{
@@ -115,10 +115,10 @@ and \emph{error models}. These
portions of the model have specific and defined creator functions and no additional
R code is permissible. They take this form:
-\if{html}{\out{