diff --git a/DESCRIPTION b/DESCRIPTION index 109e076..f827968 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: pharmr.extra Title: Extension of pharmr (Pharmpy) functionality -Version: 0.0.0.9075 +Version: 0.0.0.9080 Authors@R: c( person("Ron", "Keizer", email = "ron@insight-rx.com", role = c("cre", "aut")), person("Michael", "McCarthy", email = "michael.mccarthy@insight-rx.com", role = "ctb"), diff --git a/NEWS.md b/NEWS.md index 79f6edc..bf30b1d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,27 @@ # pharmr.extra (development version) +* `create_model()` now writes a data.frame `data` argument to a CSV in the + session tempdir and points the model's `$DATA` record at that file, instead + of leaving pharmpy's `DUMMYPATH` placeholder. This gives the model an on-disk + dataset, which is required to use the `run_nlme(copy_dataset = FALSE)` + workflow (NONMEM models only; filename input already has an on-disk dataset). + +* `copy_dataset = FALSE` can only be honored when the dataset is a file on + disk (supplied via `data` or referenced by the model's `$DATA` record). When + only an in-memory dataset is available (a passed data frame, `model$dataset`, + or the original dataset), a warning is issued and the dataset is copied into + the run folder (with `$DATA` rewritten) as a fallback. +* `copy_dataset = FALSE` now leaves the model's `$DATA` record untouched + instead of rewriting it to the dataset's absolute path. Combined with not + copying the dataset into the run folder, the model's original data reference + is preserved verbatim. (`$DATA` is still rewritten when `copy_dataset = TRUE`, + i.e. when the dataset is placed into the run folder as `data.csv`.) +* `run_nlme(data = NULL, copy_dataset = FALSE)` now correctly leaves the + dataset in its existing location when the model's `$DATA` record points to a + real file. Previously the dataset was always copied into the run folder + because `run_nlme()` materialised `model$dataset` into a tempfile before + reaching `prepare_run_folder()`, and `prepare_run_folder()` preferred the + in-memory dataset over the on-disk $DATA path. * `update_parameters()` now also accepts a raw `nlmixr2FitCore` / `nlmixr2FitData` object — useful when fitting outside `run_nlme()`. Both diagonal and off-diagonal omega elements are extracted and named per pharmpy's diff --git a/R/create_model.R b/R/create_model.R index bdf4d8d..a870204 100644 --- a/R/create_model.R +++ b/R/create_model.R @@ -556,7 +556,23 @@ create_model <- function( ) } - ## Store the original data so prepare_run_folder() writes an exact copy + ## Persist an in-memory dataset to a temp CSV and point $DATA at it. + ## When `data` is supplied as a data.frame, pharmpy keeps the dataset in + ## memory and renders `$DATA DUMMYPATH` in the control stream. Writing the + ## (final) dataset to a CSV in the session tempdir and rewriting $DATA to that + ## path gives the model an on-disk dataset, which is what makes the + ## `run_nlme(copy_dataset = FALSE)` workflow (leave dataset in place) usable. + ## `original_data` is non-NULL exactly when `data` was supplied as a + ## data.frame; a filename input already has an on-disk dataset to point at. + if(tool == "nonmem" && !is.null(original_data) && !is.null(mod$dataset)) { + dataset_file <- tempfile(pattern = "data", fileext = ".csv") + write.csv(mod$dataset, dataset_file, quote = FALSE, row.names = FALSE) + mod <- pharmr::read_model_from_string( + change_nonmem_dataset(mod$code, dataset_file) + ) + } + + ## Store the original data so prepare_run_folder() can write an exact copy ## to the run folder (NONMEM reads by position, not by header name). ## Must be set last — pharmpy calls like set_name() create new objects ## that lose R attributes. diff --git a/R/prepare_run_folder.R b/R/prepare_run_folder.R index 8e0fe7a..880558a 100644 --- a/R/prepare_run_folder.R +++ b/R/prepare_run_folder.R @@ -7,6 +7,7 @@ prepare_run_folder <- function( force = FALSE, data = NULL, auto_stack_encounters = FALSE, + copy_dataset = TRUE, verbose = TRUE ) { @@ -20,6 +21,11 @@ prepare_run_folder <- function( ## Set up other files dataset_path <- file.path(fit_folder, "data.csv") + ## Whether to rewrite the model's $DATA record. Only do so when the dataset + ## is actually placed into the run folder (copied/written). When the dataset + ## is left in its existing location (`copy_dataset = FALSE`), $DATA is left + ## untouched so the model's original data reference is preserved verbatim. + update_data_record <- TRUE model_file <- "run.mod" output_file <- "run.lst" model_path <- file.path(fit_folder, model_file) @@ -31,25 +37,43 @@ prepare_run_folder <- function( if(!is.null(data)) { if(inherits(data, "character")) { - if(verbose) cli::cli_process_start("Copying dataset") if(!file.exists(data)) { cli::cli_abort("`data` file does not exist.") } if(isTRUE(auto_stack_encounters)) { cli::cli_warn("`auto_stack_encounters` can only be used when `data` is specified as data.frame, not when it is a CSV filename.") } - file.copy(from = data, to = dataset_path) - ## If the source CSV has quoted headers (e.g. `"ID","TIME",...`), NONMEM - ## will try to parse the header row as data. Detect this and rewrite the - ## dataset with unquoted headers. - first_line <- tryCatch(readLines(dataset_path, n = 1), error = function(e) character(0)) - if (length(first_line) && grepl('^["\']', first_line)) { - if (verbose) cli::cli_alert_info("Stripping quoted column names from dataset header") - df <- read.csv(dataset_path, check.names = FALSE) - df <- unquote_column_names(df) - write.csv(df, file = dataset_path, quote = FALSE, row.names = FALSE) + if(!copy_dataset) { + ## Leave the dataset in its existing location and leave the model's + ## $DATA record untouched. The file is not modified (so no quoted-header + ## rewrite); the user is responsible for the dataset being NONMEM-ready + ## and for $DATA already pointing at it correctly. + if(verbose) cli::cli_process_start("Using dataset in existing location (not copying into run folder, $DATA left unchanged)") + dataset_path <- normalizePath(data, mustWork = TRUE) + update_data_record <- FALSE + } else { + if(verbose) cli::cli_process_start("Copying dataset") + if(!isTRUE(file.copy(from = data, to = dataset_path))) { + cli::cli_abort("Failed to copy dataset from {.path {data}} to {.path {dataset_path}}.") + } + ## If the source CSV has quoted headers (e.g. `"ID","TIME",...`), NONMEM + ## will try to parse the header row as data. Detect this and rewrite the + ## dataset with unquoted headers. + first_line <- tryCatch(readLines(dataset_path, n = 1), error = function(e) character(0)) + if (length(first_line) && grepl('^["\']', first_line)) { + if (verbose) cli::cli_alert_info("Stripping quoted column names from dataset header") + df <- read.csv(dataset_path, check.names = FALSE) + df <- unquote_column_names(df) + write.csv(df, file = dataset_path, quote = FALSE, row.names = FALSE) + } } } else { + if(!copy_dataset) { + cli::cli_warn(c( + "!" = "{.code copy_dataset = FALSE} can only be honored when the dataset is a file on disk (supplied via {.arg data} or referenced by the model's $DATA record).", + "i" = "An in-memory data frame was supplied via {.arg data}; copying it into the run folder and updating $DATA instead." + )) + } if(verbose) cli::cli_process_start("Checking, cleaning, and copying dataset") data <- unquote_column_names(data) if(isTRUE(auto_stack_encounters)) { @@ -62,31 +86,58 @@ prepare_run_folder <- function( write.csv(data, file = dataset_path, quote = FALSE, row.names = FALSE) } } else if (!is.null(original_data)) { - if (verbose) cli::cli_process_start("Copying dataset (original column names)") - original_data <- unquote_column_names(original_data) - write.csv(original_data, file = dataset_path, quote = FALSE, row.names = FALSE) - } else { - # When `data` is NULL, prefer using an in-memory dataset if available - if (!is.null(model$dataset)) { - if (verbose) cli::cli_process_start("Copying dataset from model object") - write.csv(model$dataset, file = dataset_path, quote = FALSE, row.names = FALSE) + ## When `copy_dataset = FALSE` and the model's $DATA record already points + ## to an existing file (e.g. create_model() wrote the in-memory dataset to + ## a temp CSV and pointed $DATA at it, so it is no longer DUMMYPATH), honor + ## `copy_dataset = FALSE`: leave that file in place and leave $DATA + ## untouched, rather than re-writing the in-memory `original_data` into the + ## run folder. + dataset_file <- if(!copy_dataset) get_dataset_path_from_model(model) else NULL + if(!is.null(dataset_file)) { + if (verbose) cli::cli_process_start("Using dataset from model's $DATA record (not copying into run folder, $DATA left unchanged)") + dataset_path <- normalizePath(dataset_file, mustWork = TRUE) + update_data_record <- FALSE } else { - obj <- nm_read_model(code = model$code) - data_block <- stringr::str_replace_all(obj$DATA, "\\$DATA\\s*", "") - data_elem <- unlist(stringr::str_split(data_block, "\\s")) - data_elem <- data_elem[!grepl("(IGNORE=|ACCEPT=)", data_elem)] - dataset_file <- NULL - for (f in data_elem) { - if (file.exists(f)) { - dataset_file <- f - break() - } + if(!copy_dataset) { + cli::cli_warn(c( + "!" = "{.code copy_dataset = FALSE} can only be honored when the dataset is a file on disk (supplied via {.arg data} or referenced by the model's $DATA record).", + "i" = "Only the model's original (in-memory) dataset is available; copying it into the run folder and updating $DATA instead." + )) } - if (!is.null(dataset_file)) { - file.copy(from = dataset_file, to = dataset_path) + if (verbose) cli::cli_process_start("Copying dataset (original column names)") + original_data <- unquote_column_names(original_data) + write.csv(original_data, file = dataset_path, quote = FALSE, row.names = FALSE) + } + } else { + ## `data` is NULL: resolve dataset from the model. Try the $DATA record + ## path first — if it points to a real file we can honor `copy_dataset`. + ## Only fall back to writing `model$dataset` (in-memory) to the run folder + ## when no usable on-disk source exists. + dataset_file <- get_dataset_path_from_model(model) + if (!is.null(dataset_file)) { + if (!copy_dataset) { + ## Dataset already referenced by $DATA and present on disk: leave both + ## the file and the $DATA record untouched. + if (verbose) cli::cli_process_start("Using dataset from model's $DATA record (not copying into run folder, $DATA left unchanged)") + dataset_path <- normalizePath(dataset_file, mustWork = TRUE) + update_data_record <- FALSE } else { - cli::cli_abort("No dataset could be resolved: `model$dataset` is NULL and no existing file was found from the model's $DATA record.") + if (verbose) cli::cli_process_start("Copying dataset from model's $DATA record") + if (!isTRUE(file.copy(from = dataset_file, to = dataset_path))) { + cli::cli_abort("Failed to copy dataset from {.path {dataset_file}} to {.path {dataset_path}}.") + } + } + } else if (!is.null(model$dataset)) { + if(!copy_dataset) { + cli::cli_warn(c( + "!" = "{.code copy_dataset = FALSE} can only be honored when the dataset is a file on disk (supplied via {.arg data} or referenced by the model's $DATA record).", + "i" = "The model's $DATA record does not point to an existing file; falling back to the in-memory {.code model$dataset}, copying it into the run folder and updating $DATA." + )) } + if (verbose) cli::cli_process_start("Copying dataset from model object") + write.csv(model$dataset, file = dataset_path, quote = FALSE, row.names = FALSE) + } else { + cli::cli_abort("No dataset could be resolved: `model$dataset` is NULL and no existing file was found from the model's $DATA record.") } } @@ -94,10 +145,15 @@ prepare_run_folder <- function( model_code <- model$code ## Replace dictionary placeholder column names with DROP model_code <- gsub("_DDRP_[A-Za-z0-9_]+", "DROP", model_code, perl = TRUE) - model_code <- change_nonmem_dataset( - model_code, - dataset_path - ) + ## Only rewrite $DATA when the dataset was placed into the run folder. When + ## the dataset is left in place (`copy_dataset = FALSE`), preserve the + ## model's original $DATA record verbatim. + if (update_data_record) { + model_code <- change_nonmem_dataset( + model_code, + dataset_path + ) + } writeLines(model_code, model_path) if(verbose) cli::cli_process_done() @@ -109,3 +165,27 @@ prepare_run_folder <- function( dataset_path = dataset_path ) } + +#' Resolve an on-disk dataset path from a model's $DATA record +#' +#' Parses the $DATA record of a NONMEM model and returns the first element that +#' is an existing file on disk (ignoring `IGNORE=`/`ACCEPT=` options). Returns +#' `NULL` when no element points to an existing file (e.g. $DATA is the +#' `DUMMYPATH` placeholder used while the dataset lives only in memory). +#' +#' @param model pharmpy model object +#' +#' @returns path to an existing dataset file (character), or `NULL` +#' +get_dataset_path_from_model <- function(model) { + obj <- nm_read_model(code = model$code) + data_block <- stringr::str_replace_all(obj$DATA, "\\$DATA\\s*", "") + data_elem <- unlist(stringr::str_split(data_block, "\\s")) + data_elem <- data_elem[!grepl("(IGNORE=|ACCEPT=)", data_elem)] + for (f in data_elem) { + if (nzchar(f) && file.exists(f)) { + return(f) + } + } + NULL +} diff --git a/R/run_nlme.R b/R/run_nlme.R index 15ec482..52ae784 100644 --- a/R/run_nlme.R +++ b/R/run_nlme.R @@ -66,6 +66,17 @@ #' This feature is useful e.g. for crossover trials when data on the same #' individual ispresent but is included in the dataset as time-after-dose and #' not actual time since first overall dose. +#' @param copy_dataset copy the dataset into the run folder? If `TRUE`, the +#' dataset is copied into the run folder as `data.csv` and the model's `$DATA` +#' record is rewritten to point to that copy. If `FALSE` (default), the dataset +#' is left in its existing location and the model's `$DATA` record is left +#' untouched (the caller is responsible for `$DATA` already pointing at the +#' dataset correctly). `copy_dataset = FALSE` can only be honored when the +#' dataset is a file on disk — i.e. `data` is supplied as a file path, or the +#' model's `$DATA` record points to an existing file. If neither is the case +#' (only an in-memory data frame, `model$dataset`, or original dataset is +#' available), a warning is issued and the dataset is copied into the run +#' folder (with `$DATA` rewritten) anyway. #' @param clean clean up run folder after NONMEM execution? #' @param as_job run as RStudio job? #' @param save_final after running the model, should a file `final.mod` be created @@ -114,6 +125,7 @@ run_nlme <- function( estimation_options = NULL, sir_options = NULL, auto_stack_encounters = FALSE, + copy_dataset = FALSE, clean = TRUE, as_job = FALSE, save_final = TRUE, @@ -126,11 +138,17 @@ run_nlme <- function( ) { time_start <- Sys.time() + + ## An in-memory data.frame has no on-disk "existing location" to reference, + ## so it must always be written into the run folder regardless of + ## `copy_dataset` (otherwise $DATA would point at the ephemeral tempfile + ## created just below). + data_in_memory <- inherits(data, "data.frame") ## Make sure `data` is pointing to a file. This is to avoid issue with ## Pharmpy trying to parse the data.frame. `data` may also be NULL, in - ## which case `prepare_run_folder()` falls back to `model$dataset` or the - ## $DATA section of the model code. + ## which case `prepare_run_folder()` resolves the dataset from the model's + ## $DATA record or `model$dataset`. if(!is.null(data)) { if(inherits(data, "data.frame")) { datafile <- tempfile(pattern = "data_", fileext = ".csv") @@ -139,10 +157,6 @@ run_nlme <- function( } else if(!inherits(data, "character")) { cli::cli_abort("`data` is of unknown type.") } - } else if (!is.null(model$dataset)) { - data <- model$dataset - datafile <- tempfile(pattern = "data_", fileext = ".csv") - write.csv(data, datafile, quote = FALSE, row.names = FALSE) } ## Preserve R attributes across pharmpy calls (which create new Python objects) @@ -238,6 +252,7 @@ run_nlme <- function( data = data, force = force, auto_stack_encounters = auto_stack_encounters, + copy_dataset = copy_dataset || data_in_memory, verbose = verbose ) diff --git a/man/prepare_run_folder.Rd b/man/prepare_run_folder.Rd index 367c0b1..c8cd92c 100644 --- a/man/prepare_run_folder.Rd +++ b/man/prepare_run_folder.Rd @@ -11,6 +11,7 @@ prepare_run_folder( force = FALSE, data = NULL, auto_stack_encounters = FALSE, + copy_dataset = TRUE, verbose = TRUE ) } diff --git a/man/run_nlme.Rd b/man/run_nlme.Rd index d8916fc..bdc9515 100644 --- a/man/run_nlme.Rd +++ b/man/run_nlme.Rd @@ -20,7 +20,8 @@ run_nlme( estimation_method = NULL, estimation_options = NULL, sir_options = NULL, - auto_stack_encounters = TRUE, + auto_stack_encounters = FALSE, + copy_dataset = FALSE, clean = TRUE, as_job = FALSE, save_final = TRUE, @@ -106,6 +107,18 @@ This feature is useful e.g. for crossover trials when data on the same individual ispresent but is included in the dataset as time-after-dose and not actual time since first overall dose.} +\item{copy_dataset}{copy the dataset into the run folder? If \code{TRUE}, the +dataset is copied into the run folder as \code{data.csv} and the model's \verb{$DATA} +record is rewritten to point to that copy. If \code{FALSE} (default), the dataset +is left in its existing location and the model's \verb{$DATA} record is left +untouched (the caller is responsible for \verb{$DATA} already pointing at the +dataset correctly). \code{copy_dataset = FALSE} can only be honored when the +dataset is a file on disk — i.e. \code{data} is supplied as a file path, or the +model's \verb{$DATA} record points to an existing file. If neither is the case +(only an in-memory data frame, \code{model$dataset}, or original dataset is +available), a warning is issued and the dataset is copied into the run +folder (with \verb{$DATA} rewritten) anyway.} + \item{clean}{clean up run folder after NONMEM execution?} \item{as_job}{run as RStudio job?} diff --git a/tests/testthat/test-create_model.R b/tests/testthat/test-create_model.R index ef6270a..d721a10 100644 --- a/tests/testthat/test-create_model.R +++ b/tests/testthat/test-create_model.R @@ -3,6 +3,22 @@ test_that("create_model call without arguments works", { expect_s3_class(mod, "pharmpy.model.external.nonmem.model.Model") }) +test_that("create_model points $DATA at an on-disk temp CSV for data.frame input", { + test_data <- data.frame( + ID = 1, TIME = c(0, 1, 2), DV = c(0, 10, 5), + AMT = c(100, 0, 0), CMT = 1, EVID = c(1, 0, 0), MDV = c(1, 0, 0) + ) + mod <- create_model(route = "iv", data = test_data, verbose = FALSE, auto_init = FALSE) + + data_line <- grep("^\\$DATA", strsplit(mod$code, "\n")[[1]], value = TRUE) + ## $DATA must point to a real file, not pharmpy's DUMMYPATH placeholder + expect_no_match(data_line, "DUMMYPATH", fixed = TRUE) + data_path <- sub("^\\$DATA\\s+(\\S+).*", "\\1", data_line) + expect_true(file.exists(data_path)) + ## the on-disk dataset round-trips to the model's dataset + expect_equal(nrow(read.csv(data_path)), nrow(mod$dataset)) +}) + test_that("create_model basic functionality works", { # Create minimal test dataset test_data <- data.frame( diff --git a/tests/testthat/test-run_nlme.R b/tests/testthat/test-run_nlme.R index e7bf747..a7fe431 100644 --- a/tests/testthat/test-run_nlme.R +++ b/tests/testthat/test-run_nlme.R @@ -269,6 +269,104 @@ test_that("run_nlme / prepare_run_folder strip surrounding quotes from column na ) }) +test_that("prepare_run_folder respects copy_dataset", { + local_pharmr.extra_options() + skip_if_nonmem_not_available() + + mod <- create_model(route = "iv", verbose = FALSE) + + src_dir <- withr::local_tempdir() + src_csv <- file.path(src_dir, "mydata.csv") + writeLines(c("ID,TIME,DV", "1,0,0", "1,1,10"), src_csv) + + ## copy_dataset = FALSE: leave dataset in place AND leave $DATA untouched + orig_data_line <- grep("^\\$DATA", strsplit(mod$code, "\n")[[1]], value = TRUE) + obj_no_copy <- prepare_run_folder( + id = "run1", model = mod, path = withr::local_tempdir(), data = src_csv, + copy_dataset = FALSE, verbose = FALSE + ) + expect_false(file.exists(file.path(obj_no_copy$fit_folder, "data.csv"))) + expect_equal(obj_no_copy$dataset_path, normalizePath(src_csv)) + data_line <- grep("^\\$DATA", readLines( + file.path(obj_no_copy$fit_folder, obj_no_copy$model_file) + ), value = TRUE) + ## $DATA is preserved verbatim from the model, not rewritten to src_csv + expect_equal(data_line, orig_data_line) + expect_no_match(data_line, normalizePath(src_csv), fixed = TRUE) + + ## copy_dataset = TRUE: dataset copied into run folder, $DATA points to copy + obj_copy <- prepare_run_folder( + id = "run1", model = mod, path = withr::local_tempdir(), data = src_csv, + copy_dataset = TRUE, verbose = FALSE + ) + expect_true(file.exists(file.path(obj_copy$fit_folder, "data.csv"))) + expect_equal( + obj_copy$dataset_path, + file.path(obj_copy$fit_folder, "data.csv") + ) +}) + +test_that("prepare_run_folder respects copy_dataset when data=NULL and $DATA points to a real file", { + local_pharmr.extra_options() + skip_if_nonmem_not_available() + + ## Build a real on-disk CSV and a model file whose $DATA points to it. + src_dir <- withr::local_tempdir() + src_csv <- file.path(src_dir, "mydata.csv") + writeLines(c("ID,TIME,DV,AMT,EVID,MDV,CMT", "1,0,0,100,1,1,1", "1,1,10,0,0,0,1"), src_csv) + + mod0 <- create_model(route = "iv", verbose = FALSE) + mod_code <- change_nonmem_dataset(mod0$code, normalizePath(src_csv)) + mod_file <- file.path(src_dir, "run.mod") + writeLines(mod_code, mod_file) + mod <- create_model_from_file(mod_file) + ## Drop the in-memory attribute set by create_model_from_file so the only + ## resolvable source is the file referenced by $DATA. + attr(mod, "original_data") <- NULL + + ## copy_dataset = FALSE: $DATA should point to the existing CSV path, + ## and no copy should land in the run folder. + obj_no_copy <- prepare_run_folder( + id = "run1", model = mod, path = withr::local_tempdir(), data = NULL, + copy_dataset = FALSE, verbose = FALSE + ) + expect_false(file.exists(file.path(obj_no_copy$fit_folder, "data.csv"))) + expect_equal(obj_no_copy$dataset_path, normalizePath(src_csv)) + data_line <- grep("^\\$DATA", readLines( + file.path(obj_no_copy$fit_folder, obj_no_copy$model_file) + ), value = TRUE) + expect_match(data_line, normalizePath(src_csv), fixed = TRUE) +}) + +test_that("prepare_run_folder warns and falls back to copying when copy_dataset=FALSE but only an in-memory dataset is available", { + local_pharmr.extra_options() + skip_if_nonmem_not_available() + + mod <- create_model(route = "iv", verbose = FALSE) + dat <- data.frame( + ID = 1, TIME = c(0, 1), DV = c(0, 10), + AMT = c(100, 0), CMT = 1, EVID = c(1, 0), MDV = c(1, 0) + ) + + ## data is an in-memory data.frame with no on-disk location: copy_dataset + ## = FALSE cannot be honored, so it should warn and copy into the run folder. + fit_path <- withr::local_tempdir() + expect_warning( + obj <- prepare_run_folder( + id = "run1", model = mod, path = fit_path, data = dat, + copy_dataset = FALSE, verbose = FALSE + ), + "copy_dataset = FALSE" + ) + ## Fallback: dataset copied into run folder and $DATA points to that copy. + expect_true(file.exists(file.path(obj$fit_folder, "data.csv"))) + expect_equal(obj$dataset_path, file.path(obj$fit_folder, "data.csv")) + data_line <- grep("^\\$DATA", readLines( + file.path(obj$fit_folder, obj$model_file) + ), value = TRUE) + expect_match(data_line, "data.csv", fixed = TRUE) +}) + test_that("unquote_column_names strips a single pair of surrounding quotes", { df <- data.frame(a = 1, b = 2, c = 3) names(df) <- c('"a"', "'b'", "c") @@ -315,6 +413,33 @@ test_that("run_nlme converts data.frame input to a CSV file path", { expect_equal(written, dat, ignore_attr = TRUE) }) +test_that("run_nlme forces copy_dataset for in-memory data.frame input", { + local_pharmr.extra_options() + skip_if_nonmem_not_available() + mod <- create_model(route = "iv", verbose = FALSE) + dat <- data.frame( + ID = 1, TIME = c(0, 1, 2), DV = c(0, 10, 5), + AMT = c(100, 0, 0), CMT = 1, EVID = c(1, 0, 0), MDV = c(1, 0, 0) + ) + + ## A data.frame has no on-disk location to reference, so even with + ## copy_dataset = FALSE it must be written into the run folder (otherwise + ## $DATA would point at an ephemeral tempfile). + captured_copy <- "" + stub(run_nlme, "prepare_run_folder", function(id, model, path, data, ...) { + captured_copy <<- list(...)$copy_dataset + stop("abort before NONMEM") + }) + + tryCatch( + run_nlme(mod, data = dat, id = "run1", path = withr::local_tempdir(), + copy_dataset = FALSE, verbose = FALSE), + error = function(e) NULL + ) + + expect_true(captured_copy) +}) + test_that("run_nlme passes through a CSV file path unchanged", { local_pharmr.extra_options() mod <- create_model(route = "iv", verbose = FALSE)