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{
}}\preformatted{block_name = c( - var1 = creator(), +\if{html}{\out{
}}\preformatted{block_name = c( + var1 = creator(), var2 = creator() -) +) }\if{html}{\out{
}} Note the comma separating the creator functions, "\verb{c(}" to open the vector and "\verb{)}" to close the vector. @@ -128,13 +128,13 @@ equations}, \emph{model equations} (e.g. ODEs), \emph{lag time}, \emph{bioavaila and \emph{outputs}. These parts of the model are defined as R functions without arguments, but whose body contains any permissible R code. -\if{html}{\out{
}}\preformatted{block_name = function() \{ +\if{html}{\out{
}}\preformatted{block_name = function() \{ - # any valid R code + # any valid R code # can use primary or secondary parameters and covariates # lines are not separated by commas -\} +\} }\if{html}{\out{
}} Note the absence of arguments between the "\verb{()}", the opening curly brace "\verb{\{}" to start @@ -225,9 +225,9 @@ Note that \code{wt = interp()} is equivalent to \code{wt = interp("lm")}, since are not estimated for these equations but they are available to every other block in the model. For example: -\if{html}{\out{
}}\preformatted{sec = function() \{ - V = V0 * (wt/70) -\} +\if{html}{\out{
}}\preformatted{sec = function() \{ + V = V0 * (wt/70) +\} }\if{html}{\out{
}} Note that the function @@ -361,7 +361,7 @@ but will be specific to the \code{out} block. For example, -\if{html}{\out{
}}\preformatted{out = function() \{ +\if{html}{\out{
}}\preformatted{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 diff --git a/man/PM_result.Rd b/man/PM_result.Rd index c6a5e7e56..2648cc753 100755 --- a/man/PM_result.Rd +++ b/man/PM_result.Rd @@ -14,6 +14,31 @@ After a run completes, results are stored on your hard drive. They are loaded back into R with \link{PM_load} to create the \link{PM_result} object, which contains both the results and functions to analyze or plot the result. } +\examples{ + +## ------------------------------------------------ +## Method `PM_result$bestdose` +## ------------------------------------------------ + +\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() +} +} \author{ Michael Neely, Julian Otalvaro } @@ -67,6 +92,7 @@ new optimal sampling results.} \item \href{#method-PM_result-nca}{\code{PM_result$nca()}} \item \href{#method-PM_result-report}{\code{PM_result$report()}} \item \href{#method-PM_result-sim}{\code{PM_result$sim()}} +\item \href{#method-PM_result-bestdose}{\code{PM_result$bestdose()}} \item \href{#method-PM_result-save}{\code{PM_result$save()}} \item \href{#method-PM_result-validate}{\code{PM_result$validate()}} \item \href{#method-PM_result-step}{\code{PM_result$step()}} @@ -247,6 +273,85 @@ arguments, e.g. \verb{$sim(include = 1:2, predInt = 1, limits = NA)}.} } \if{html}{\out{
}} } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-PM_result-bestdose}{}}} +\subsection{Method \code{bestdose()}}{ +Run BestDose optimization using this result as the prior +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{PM_result$bestdose( + target, + past_data = NULL, + dose_range = list(min = 0, max = 1000), + bias_weight = 0.5, + target_type = "concentration", + time_offset = NULL, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{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.} + +\item{\code{past_data}}{Optional: PM_data object or path to CSV with patient history. +If NULL (default), uses the full dataset from this result.} + +\item{\code{dose_range}}{Named list with min and max allowable doses. +Default: list(min = 0, max = 1000)} + +\item{\code{bias_weight}}{Numeric \link{0,1} controlling personalization level. +0 = fully personalized, 1 = population-based. Default: 0.5 (balanced)} + +\item{\code{target_type}}{One of "concentration" (default), "auc_from_zero", or +"auc_from_last_dose"} + +\item{\code{time_offset}}{Optional: time offset for past/future concatenation} + +\item{\code{...}}{Additional arguments passed to \link{PM_bestdose}} +} +\if{html}{\out{
}} +} +\subsection{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 \verb{$final}, \verb{$model}, and \verb{$data} objects from +this result. Most commonly, you will supply a different \code{target} data object. +} + +\subsection{Returns}{ +A \link{PM_bestdose} object containing optimal doses and predictions +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\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() +} +} +\if{html}{\out{
}} + +} + } \if{html}{\out{
}} \if{html}{\out{}} diff --git a/man/compile_model.Rd b/man/compile_model.Rd index bdd7b086f..943cc2811 100755 --- a/man/compile_model.Rd +++ b/man/compile_model.Rd @@ -4,7 +4,7 @@ \alias{compile_model} \title{Compiles the text representation of a model into a binary file.} \usage{ -compile_model(model_path, output_path, params, kind) +compile_model(model_path, template_path, output_path, params, kind) } \description{ Compiles the text representation of a model into a binary file. diff --git a/man/dummy_compile.Rd b/man/dummy_compile.Rd index 6e7b34390..8af0daf60 100755 --- a/man/dummy_compile.Rd +++ b/man/dummy_compile.Rd @@ -4,7 +4,7 @@ \alias{dummy_compile} \title{Dummy function to cache compilation artifacts.} \usage{ -dummy_compile() +dummy_compile(template_path) } \description{ Dummy function to cache compilation artifacts. diff --git a/man/interp.Rd b/man/interp.Rd index 8f9f5b295..6a75496a5 100755 --- a/man/interp.Rd +++ b/man/interp.Rd @@ -24,8 +24,8 @@ interpolation between values or not. } \examples{ \dontrun{ -cov = c( - wt = interp() # same as interp("lm") or interp("linear") +cov <- c( + wt = interp(), # same as interp("lm") or interp("linear") visit = interp("none") ) } diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 8c556ce9c..d8fe6025a 100755 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -9,8 +9,8 @@ name = 'pm_rs' [dependencies] extendr-api = '*' -pmcore = {version ="=0.20.0", features = ["exa"]} -# pmcore = { path = "/Users/jotalvaro/code/LAPKB/PMcore", features = ["exa"] } +pmcore = { version = "=0.22.1", features = ["exa"] } +libloading = "0.8" rayon = "1.10.0" anyhow = "1.0.97" diff --git a/src/rust/src/bestdose_executor.rs b/src/rust/src/bestdose_executor.rs new file mode 100644 index 000000000..2d83731f6 --- /dev/null +++ b/src/rust/src/bestdose_executor.rs @@ -0,0 +1,426 @@ +use crate::{logs::RFormatLayer, settings::settings}; +use extendr_api::prelude::*; +use pmcore::bestdose::{BestDoseProblem, BestDoseResult, DoseRange, Target}; +use pmcore::prelude::{data, ODE}; +use pmcore::routines::initialization::parse_prior; +use std::path::PathBuf; + +/// Helper to parse target type from string +pub(crate) fn parse_target_type(target_str: &str) -> std::result::Result { + match target_str.to_lowercase().as_str() { + "concentration" => Ok(Target::Concentration), + "auc_from_zero" | "auc" => Ok(Target::AUCFromZero), + "auc_from_last_dose" | "auc_interval" => Ok(Target::AUCFromLastDose), + _ => Err(format!( + "Invalid target type: {}. Must be 'concentration', 'auc_from_zero', or 'auc_from_last_dose'", + target_str + )), + } +} + +/// R-compatible prediction row for BestDose output +#[derive(Debug, IntoDataFrameRow)] +pub struct BestDosePredictionRow { + id: String, + time: f64, + observed: f64, + pop_mean: f64, + pop_median: f64, + post_mean: f64, + post_median: f64, + outeq: usize, +} + +impl BestDosePredictionRow { + pub fn from_np_prediction( + pred: &pmcore::routines::output::predictions::NPPredictionRow, + id: &str, + ) -> Self { + Self { + id: id.to_string(), + time: pred.time(), + observed: pred.obs().unwrap_or(0.0), + pop_mean: pred.pop_mean(), + pop_median: pred.pop_median(), + post_mean: pred.post_mean(), + post_median: pred.post_median(), + outeq: pred.outeq(), + } + } +} + +/// R-compatible AUC prediction row +#[derive(Debug, IntoDataFrameRow)] +pub struct BestDoseAucRow { + time: f64, + auc: f64, +} + +/// Convert BestDoseResult to R-compatible list structure +pub(crate) fn convert_bestdose_result_to_r( + result: BestDoseResult, +) -> std::result::Result { + // Extract doses + let doses: Vec = result.doses(); + + // Objective function + let objf = result.objf(); + + // Status + let status_str = format!("{:?}", result.status()); + + // Predictions as data frame + let pred_rows: Vec = result + .predictions() + .predictions() + .iter() + .map(|p| BestDosePredictionRow::from_np_prediction(p, "subject_1")) + .collect(); + let pred_df = pred_rows + .into_dataframe() + .map_err(|e| format!("Failed to create predictions dataframe: {:?}", e))?; + + // AUC predictions (if available) + let auc_val = if let Some(auc_preds) = result.auc_predictions() { + let auc_rows: Vec = auc_preds + .iter() + .map(|(time, auc)| BestDoseAucRow { + time: *time, + auc: *auc, + }) + .collect(); + let auc_df = auc_rows + .into_dataframe() + .map_err(|e| format!("Failed to create AUC dataframe: {:?}", e))?; + Robj::from(auc_df) + } else { + Robj::from(()) // NULL for no AUC + }; + + // Optimization method + let method_str = format!("{}", result.optimization_method()); + + // Build the list using list! macro + let output = list!( + doses = doses, + objf = objf, + status = status_str, + predictions = pred_df, + auc_predictions = auc_val, + method = method_str + ); + + Ok(output.into()) +} +/// Opaque handle that keeps the dynamic model library alive while reusing the +/// prepared `BestDoseProblem` for multiple optimization runs. +pub struct BestDoseProblemHandle { + problem: BestDoseProblem, + #[allow(dead_code)] + library: libloading::Library, +} + +impl BestDoseProblemHandle { + #[allow(clippy::too_many_arguments)] + pub fn new( + model_path: PathBuf, + prior_path: PathBuf, + past_data_path: Option, + target_data_path: PathBuf, + time_offset: Option, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, + ) -> std::result::Result { + let (library, (eq, meta)) = + unsafe { pmcore::prelude::pharmsol::exa::load::load::(model_path) }; + + let settings = settings(params, meta.get_params(), "/tmp/bestdose") + .map_err(|e| format!("Failed to parse settings: {}", e))?; + + let (population_theta, prior_weights) = + parse_prior(&prior_path.to_str().unwrap().to_string(), &settings) + .map_err(|e| format!("Failed to parse prior: {}", e))?; + + let population_weights = prior_weights + .ok_or_else(|| "Prior file must contain a 'prob' column with weights".to_string())?; + + let past_data = if let Some(path) = past_data_path { + let data = data::read_pmetrics(path.to_str().unwrap()) + .map_err(|e| format!("Failed to read past data: {}", e))?; + let subjects = data.subjects(); + if subjects.is_empty() { + return Err("Past data file contains no subjects".to_string()); + } + Some(subjects[0].clone()) + } else { + None + }; + + let target_data = { + let data = data::read_pmetrics(target_data_path.to_str().unwrap()) + .map_err(|e| format!("Failed to read target data: {}", e))?; + let subjects = data.subjects(); + if subjects.is_empty() { + return Err("Target data file contains no subjects".to_string()); + } + subjects[0].clone() + }; + + let target_enum = parse_target_type(target_type)?; + let doserange = DoseRange::new(dose_min, dose_max); + + let problem = BestDoseProblem::new( + &population_theta, + &population_weights, + past_data, + target_data, + time_offset, + eq, + doserange, + bias_weight, + settings, + target_enum, + ) + .map_err(|e| format!("Failed to create BestDose problem: {}", e))?; + + Ok(Self { problem, library }) + } + + pub fn optimize( + &self, + bias_weight: Option, + ) -> std::result::Result { + let configured_problem = match bias_weight { + Some(weight) => self.problem.clone().with_bias_weight(weight), + None => self.problem.clone(), + }; + + configured_problem + .optimize() + .map_err(|e| format!("Optimization failed: {}", e)) + } + + pub fn problem(&self) -> &BestDoseProblem { + &self.problem + } +} + +pub(crate) fn bestdose_ode( + model_path: PathBuf, + prior_path: PathBuf, + past_data_path: Option, + target_data_path: PathBuf, + time_offset: Option, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, +) -> std::result::Result { + let handle = BestDoseProblemHandle::new( + model_path, + prior_path, + past_data_path, + target_data_path, + time_offset, + dose_min, + dose_max, + bias_weight, + target_type, + params, + )?; + + handle.optimize(None) +} + +/// Execute bestdose optimization for analytical models (placeholder - not yet supported) +pub(crate) fn bestdose_analytical( + _model_path: PathBuf, + _prior_path: PathBuf, + _past_data_path: Option, + _target_data_path: PathBuf, + _time_offset: Option, + _dose_min: f64, + _dose_max: f64, + _bias_weight: f64, + _target_type: &str, + _params: List, +) -> std::result::Result { + Err("BestDose for analytical models is not yet supported".to_string()) +} + +pub(crate) struct PosteriorSummary { + theta_values: Vec, + theta_dim: (i32, i32), + param_names: Vec, + posterior_weights: Vec, + population_weights: Vec, + bias_weight: f64, + target_type: Target, +} + +fn summarize_problem(problem: &BestDoseProblem) -> PosteriorSummary { + let theta = problem.posterior_theta(); + let matrix = theta.matrix(); + let nrows = matrix.nrows() as i32; + let ncols = matrix.ncols() as i32; + let mut theta_values = vec![0.0; (nrows * ncols) as usize]; + + for col in 0..ncols as usize { + for row in 0..nrows as usize { + theta_values[row + col * nrows as usize] = *matrix.get(row, col); + } + } + + PosteriorSummary { + theta_values, + theta_dim: (nrows, ncols), + param_names: theta.param_names(), + posterior_weights: problem.posterior_weights().to_vec(), + population_weights: problem.population_weights().to_vec(), + bias_weight: problem.bias_weight(), + target_type: problem.target_type(), + } +} + +fn vec_to_doubles(values: Vec, label: &str) -> std::result::Result { + Doubles::try_from(values) + .map_err(|e| format!("Failed to convert {} to doubles: {:?}", label, e)) +} + +fn dims_to_integers(dim: (i32, i32)) -> std::result::Result { + Integers::try_from(vec![dim.0, dim.1]) + .map_err(|e| format!("Failed to convert dims to integers: {:?}", e)) +} + +fn names_to_strings(names: &[String]) -> Strings { + Strings::from_values(names.iter().map(|s| s.as_str())) +} + +pub(crate) fn prepare_bestdose_problem( + model_path: PathBuf, + prior_path: PathBuf, + past_data_path: Option, + target_data_path: PathBuf, + time_offset: Option, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, +) -> std::result::Result<(BestDoseProblemHandle, PosteriorSummary), String> { + let handle = BestDoseProblemHandle::new( + model_path, + prior_path, + past_data_path, + target_data_path, + time_offset, + dose_min, + dose_max, + bias_weight, + target_type, + params, + )?; + + let summary = summarize_problem(handle.problem()); + Ok((handle, summary)) +} + +pub(crate) fn bestdose_prepare_internal( + model_path: &str, + prior_path: &str, + past_data_path: Nullable, + target_data_path: &str, + time_offset: Nullable, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, + kind: &str, +) -> Robj { + RFormatLayer::reset_global_timer(); + let _ = crate::setup_logs(); + + let past_path = past_data_path.into_option().map(PathBuf::from); + let time_offset = time_offset.into_option(); + + let preparation = match kind { + "ode" => prepare_bestdose_problem( + PathBuf::from(model_path), + PathBuf::from(prior_path), + past_path, + PathBuf::from(target_data_path), + time_offset, + dose_min, + dose_max, + bias_weight, + target_type, + params.clone(), + ), + "analytical" => Err("BestDose for analytical models is not yet supported".to_string()), + other => Err(format!("{} is not a supported model type", other)), + }; + + match preparation { + Ok((handle, summary)) => { + let theta_values = match vec_to_doubles(summary.theta_values, "theta_values") { + Ok(values) => values, + Err(e) => return Robj::from(e), + }; + let theta_dim = match dims_to_integers(summary.theta_dim) { + Ok(dim) => dim, + Err(e) => return Robj::from(e), + }; + let posterior_weights = + match vec_to_doubles(summary.posterior_weights, "posterior_weights") { + Ok(values) => values, + Err(e) => return Robj::from(e), + }; + let population_weights = + match vec_to_doubles(summary.population_weights, "population_weights") { + Ok(values) => values, + Err(e) => return Robj::from(e), + }; + let param_names = names_to_strings(&summary.param_names); + let handle_ptr = ExternalPtr::new(handle); + + let output = list!( + handle = handle_ptr, + theta_values = theta_values, + theta_dim = theta_dim, + param_names = param_names, + posterior_weights = posterior_weights, + population_weights = population_weights, + bias_weight = summary.bias_weight, + target_type = format!("{:?}", summary.target_type), + nspp = summary.theta_dim.0, + n_parameters = summary.theta_dim.1 + ); + + output.into() + } + Err(e) => Robj::from(format!("BestDose prepare failed: {}", e)), + } +} + +pub(crate) fn bestdose_optimize_internal( + handle: ExternalPtr, + bias_weight: Nullable, +) -> Robj { + let weight = bias_weight.into_option(); + + match handle.try_addr() { + Ok(inner) => match inner.optimize(weight) { + Ok(result) => match convert_bestdose_result_to_r(result) { + Ok(robj) => robj, + Err(e) => Robj::from(format!("Failed to convert result: {}", e)), + }, + Err(e) => Robj::from(format!("BestDose optimization failed: {}", e)), + }, + Err(e) => Robj::from(format!("Invalid BestDose handle: {}", e)), + } +} diff --git a/src/rust/src/executor.rs b/src/rust/src/executor.rs index 69818461c..8cf5461f0 100755 --- a/src/rust/src/executor.rs +++ b/src/rust/src/executor.rs @@ -31,7 +31,7 @@ pub(crate) fn simulate( )) } -pub(crate) fn fit( +pub(crate) fn fit( model_path: PathBuf, data: PathBuf, params: List, @@ -42,7 +42,7 @@ pub(crate) fn fit( let data = data::read_pmetrics(data.to_str().unwrap()).expect("Failed to read data"); //dbg!(&data); let mut algorithm = dispatch_algorithm(settings, eq, data)?; - let result = algorithm.fit()?; + let mut result = algorithm.fit()?; result.write_outputs()?; Ok(()) } diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 69d6e947a..ffa43b6fc 100755 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -1,4 +1,5 @@ // mod build; +mod bestdose_executor; mod executor; mod logs; mod settings; @@ -8,9 +9,11 @@ use anyhow::Result; use extendr_api::prelude::*; use pmcore::prelude::{data::read_pmetrics, pharmsol::exa::build, Analytical, ODE}; use simulation::SimulationRow; +use std::path::PathBuf; use std::process::Command; use tracing_subscriber::layer::SubscriberExt; +use crate::bestdose_executor::BestDoseProblemHandle; use crate::logs::RFormatLayer; fn validate_paths(data_path: &str, model_path: &str) { @@ -164,7 +167,13 @@ fn parse_theta(matrix: RMatrix) -> Vec> { /// Compiles the text representation of a model into a binary file. ///@export #[extendr] -fn compile_model(model_path: &str, output_path: &str, params: Strings, kind: &str) -> Result<()> { +fn compile_model( + model_path: &str, + template_path: &str, + output_path: &str, + params: Strings, + kind: &str, +) -> Result<()> { let params: Vec = params.iter().map(|x| x.to_string()).collect(); let model_txt = std::fs::read_to_string(model_path).expect("Failed to read model file"); match kind { @@ -172,6 +181,7 @@ fn compile_model(model_path: &str, output_path: &str, params: Strings, kind: &st model_txt, Some(output_path.into()), params.to_vec(), + template_path.into(), |_key, val| { print!("{}", val); }, @@ -180,6 +190,7 @@ fn compile_model(model_path: &str, output_path: &str, params: Strings, kind: &st model_txt, Some(output_path.into()), params.to_vec(), + template_path.into(), |_key, val| { print!("{}", val); }, @@ -193,8 +204,8 @@ fn compile_model(model_path: &str, output_path: &str, params: Strings, kind: &st /// Dummy function to cache compilation artifacts. ///@export #[extendr] -fn dummy_compile() -> Result { - let build_path = build::dummy_compile(|_key, val| { +fn dummy_compile(template_path: &str) -> Result { + let build_path = build::dummy_compile(template_path.into(), |_key, val| { print!("{}", val); })?; Ok(build_path) @@ -218,18 +229,6 @@ fn model_parameters(model_path: &str, kind: &str) -> Result> { } } -//@export -#[extendr] -fn template_path() -> String { - build::template_path() -} - -//@export -#[extendr] -fn clear_build() { - build::clear_build(); -} - /// Initialize the tracing subscriber with the custom R formatter /// @keywords internal ///@export @@ -251,6 +250,110 @@ fn setup_logs() -> anyhow::Result<()> { Ok(()) } +/// Run BestDose optimization to find optimal doses +///@export +#[extendr] +fn bestdose( + model_path: &str, + prior_path: &str, + past_data_path: Nullable, + target_data_path: &str, + time_offset: Nullable, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, + kind: &str, +) -> Robj { + RFormatLayer::reset_global_timer(); + let _ = setup_logs(); + + println!("Starting BestDose optimization..."); + + let past_path = match past_data_path.into_option() { + Some(p) => Some(PathBuf::from(p)), + None => None, + }; + + let time_offset_opt = time_offset.into_option(); + + let result = match kind { + "ode" => bestdose_executor::bestdose_ode( + model_path.into(), + prior_path.into(), + past_path, + target_data_path.into(), + time_offset_opt, + dose_min, + dose_max, + bias_weight, + target_type, + params.clone(), + ), + "analytical" => bestdose_executor::bestdose_analytical( + model_path.into(), + prior_path.into(), + past_path, + target_data_path.into(), + time_offset_opt, + dose_min, + dose_max, + bias_weight, + target_type, + params.clone(), + ), + _ => { + return Robj::from(format!("{} is not a supported model type", kind)); + } + }; + + match result { + Ok(bd_result) => match bestdose_executor::convert_bestdose_result_to_r(bd_result) { + Ok(r) => r, + Err(e) => Robj::from(format!("Failed to convert result: {}", e)), + }, + Err(e) => Robj::from(format!("BestDose failed: {}", e)), + } +} + +#[extendr] +fn bestdose_prepare( + model_path: &str, + prior_path: &str, + past_data_path: Nullable, + target_data_path: &str, + time_offset: Nullable, + dose_min: f64, + dose_max: f64, + bias_weight: f64, + target_type: &str, + params: List, + kind: &str, +) -> Robj { + bestdose_executor::bestdose_prepare_internal( + model_path, + prior_path, + past_data_path, + target_data_path, + time_offset, + dose_min, + dose_max, + bias_weight, + target_type, + params, + kind, + ) +} + +#[extendr] +fn bestdose_optimize( + handle: ExternalPtr, + bias_weight: Nullable, +) -> Robj { + bestdose_executor::bestdose_optimize_internal(handle, bias_weight) +} + // Macro to generate exports. // This ensures exported functions are registered with R. // See corresponding C code in `entrypoint.c`. @@ -263,9 +366,10 @@ extendr_module! { fn is_cargo_installed; fn fit; fn model_parameters; - fn template_path; - fn clear_build; fn setup_logs; + fn bestdose; + fn bestdose_prepare; + fn bestdose_optimize; } // To generate the exported function in R, run the following command: