Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
docs
data-raw/
.claude
*.fitrx
!inst/extdata/**/*.fitrx
.luna.cache.*
8 changes: 5 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: luna
Type: Package
Title: Light-weight Universal NLME Admininstrator
Version: 0.1.0
Version: 0.2.0
Author: Ron Keizer
Maintainer: Ron Keizer <ron@insight-rx.com>
Description: Organizes model development workflows in NONMEM and nlmixr2. The
Expand All @@ -24,10 +24,11 @@ Imports:
stats,
job,
diffr
Suggests:
Suggests:
httr,
ellmer,
nlmixr2,
ferx,
ggplot2,
xpose,
vpc,
Expand All @@ -41,7 +42,8 @@ Suggests:
rstudioapi
Remotes:
InsightRX/irxutils,
InsightRX/pharmr.extra
InsightRX/pharmr.extra,
ferx=FeRx-NLME/ferx-r
License: MIT + file LICENSE
URL: https://github.com/InsightRX/uno, https://insightrx.github.io/uno/, https://insightrx.github.io/luna/
LazyData: TRUE
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export(luna_dataset)
export(luna_diff)
export(luna_edit)
export(luna_edit_project)
export(luna_ferx_info)
export(luna_gof)
export(luna_help)
export(luna_ind)
Expand Down
2 changes: 1 addition & 1 deletion R/get_luna_config.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ get_luna_config <- function(
proj_conf <- proj_yaml$project$config
## use any specified project conf to overwrite env conf
for(key in names(proj_conf)) {
if(is.null(conf[[key]]) || proj_conf[[key]] != conf[[key]]) {
if(is.null(conf[[key]]) || !identical(proj_conf[[key]], conf[[key]])) {
if(verbose)
cli::cli_alert_info("Overriding global luna setting for `{key}` with project-specific setting")
conf[[key]] <- proj_conf[[key]]
Expand Down
13 changes: 12 additions & 1 deletion R/get_status.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@
#'
get_status <- function(id, folder = ".") {
status <- "not run"
# NONMEM: check for .lst output file
tmp <- find_file_with_fallback(
folder,
file.path(id, paste0("run", ".lst")),
fallback = file.path(paste0(id, ".lst")),
verbose = FALSE,
abort = FALSE
)
if(!is.null(tmp)) {
if (!is.null(tmp)) {
status <- "finished"
}
# ferx: check for .fitrx result file and convergence
rds_path <- file.path(folder, paste0(id, ".fitrx"))
if (file.exists(rds_path)) {
fit <- tryCatch(ferx::ferx_load_fit(rds_path), error = function(e) NULL)
if (!is.null(fit) && isTRUE(fit$converged)) {
status <- "finished"
} else {
status <- "failed"
}
}
status
}
49 changes: 36 additions & 13 deletions R/luna_check.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#' Syntax-check a NONMEM model
#' Syntax-check a model
#'
#' Uses pharmpy to parse the model. A successful parse indicates the model is
#' valid. Any pharmpy parse error is reported as a syntax issue.
#' For NONMEM models (`method = "pharmpy"`, `"psn"`, `"nmfe"`): uses pharmpy
#' to parse the model file. For ferx models (`method = "ferx"`): verifies the
#' `.ferx` file exists.
#'
#' @inheritParams luna_run
#'
Expand All @@ -12,34 +13,48 @@ luna_check <- function(
verbose = FALSE,
...
) {

id <- unlist(lapply(id, validate_id))
if(length(id) > 1) {
cli::cli_abort("Sorry, checking multiple runs in batch is not yet supported.")
}

## Get cache and config
is_luna_cache_available(abort = TRUE)
config <- get_luna_config()
name <- .luna_cache$get("project")$metadata$name
if(is.null(folder)) {
folder <- .luna_cache$get("project")$metadata$folder
}

## make sure we're up to date
luna_load_project(
name = name,
folder = folder,
verbose = FALSE
)

folder <- normalizePath(folder, mustWork = TRUE)


method <- ifelse0(config$tools$modelfit$method, "pharmpy")

## ferx: check file existence only
if (method == "ferx") {
model_file <- file.path(folder, paste0(id, ".ferx"))
if (!file.exists(model_file)) {
cli::cli_alert_warning("ferx model file not found: {.file {model_file}}")
return(invisible(FALSE))
}
cli::cli_alert_success("ferx model file found: {.file {model_file}}")
return(invisible(TRUE))
}

## NONMEM path
model_file <- file.path(folder, paste0(id, ".mod"))
if (!file.exists(model_file)) {
cli::cli_abort("Model file for run {id} not found!")
}

model <- tryCatch(
pharmr::read_model(model_file),
error = function(e) {
Expand All @@ -48,14 +63,14 @@ luna_check <- function(
return(invisible(FALSE))
}
)

if (isFALSE(model)) return(invisible(FALSE))

if (!inherits(model, "pharmpy.model.model.Model")) {
cli::cli_alert_warning("Model could not be parsed as a pharmpy model.")
return(invisible(FALSE))
}

if (is.null(model$dataset)) {
cli::cli_alert_warning("Model parsed but dataset could not be loaded. Check the {.field $DATA} path.")
return(invisible(FALSE))
Expand All @@ -65,12 +80,20 @@ luna_check <- function(

model_ok <- pharmr.extra::run_nlme(
model,
id = id,
path = folder,
method = method,
check_only = TRUE,
verbose = verbose
)

if (isFALSE(model_ok)) {
cli::cli_alert_warning("Model failed NONMEM compilation check.")
cli::cli_alert_warning(
"Model failed NONMEM compilation check."
)
if (!is.null(attr(model_ok, "message"))) {
cat(attr(model_ok, "message"))
}
return(invisible(FALSE))
}

Expand Down
44 changes: 44 additions & 0 deletions R/luna_ferx_info.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#' Show fit results for a ferx model run
#'
#' Reads the `{id}.fitrx` file saved by [luna_run()] (when
#' `method = "ferx"`) and prints a formatted parameter summary.
#'
#' @inheritParams luna_run
#'
#' @return The `ferx_fit` result object, invisibly.
#'
#' @export
luna_ferx_info <- function(id, folder = NULL) {
id <- validate_id(id)
if (is.null(folder)) {
folder <- .luna_cache$get("project")$metadata$folder
}
folder <- normalizePath(folder, mustWork = TRUE)

result_file <- file.path(folder, paste0(id, ".fitrx"))
if (!file.exists(result_file)) {
cli::cli_abort(
c(
"No ferx results found for run {.val {id}}.",
"i" = "Run {.fn luna_run} first, or check that {.file {result_file}} exists."
)
)
}

result <- ferx::ferx_load_fit(result_file)

cli::cli_h1("ferx fit: {id}")
cli::cli_alert_info("Method: {result$method}")
cli::cli_alert_info("Converged: {result$converged}")
cli::cli_alert_info("OFV: {round(result$ofv, 4)}")
if (!is.null(result$aic))
cli::cli_alert_info("AIC: {round(result$aic, 4)}")
if (!is.null(result$bic))
cli::cli_alert_info("BIC: {round(result$bic, 4)}")
if (!is.null(result$n_iterations))
cli::cli_alert_info("Iterations: {result$n_iterations}")
if (!is.null(result$wall_time_secs))
cli::cli_alert_info("Wall time: {round(result$wall_time_secs, 1)} s")

invisible(result)
}
103 changes: 88 additions & 15 deletions R/luna_run.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#' Run a NONMEM model
#' Run a model
#'
#' @param id run id, e.g. `run1`. This will be the folder in which the NONMEM
#' model is run.
#' Dispatches to the configured execution backend. Supported methods:
#' `"pharmpy"` (default), `"psn"`, `"nmfe"` for NONMEM-based workflows, and
#' `"ferx"` for the ferx-nlme Rust engine.
#'
#' @param id run id, e.g. `run1`. This will be the folder in which the model
#' is run.
#' @param folder path to folder containing the model file. Default is current directory.
#' @param as_job run as an RStudio job (async), or in the console. If left `NULL`
#' will use setting in luna config.
#' will use setting in luna config. Not yet supported for `method = "ferx"`.
#'
#' @export
luna_run <- function(
Expand All @@ -14,12 +18,12 @@ luna_run <- function(
as_job = NULL,
...
) {

id <- unlist(lapply(id, validate_id))
if(length(id) > 1) {
cli::cli_abort("Sorry, running of multiple runs in batch is not yet supported.")
}

## Get cache and config
is_luna_cache_available(abort = TRUE)
config <- get_luna_config()
Expand All @@ -28,45 +32,51 @@ luna_run <- function(
folder <- .luna_cache$get("project")$metadata$folder
}
as_job <- is_run_as_job(config, as_job)

## make sure we're up to date
luna_load_project(
name = name,
folder = folder,
verbose = FALSE
)

# Transform folder path to absolute path
folder <- normalizePath(folder, mustWork = TRUE)


## Detect method early to dispatch non-NONMEM engines
method <- ifelse0(config$tools$modelfit$method, "pharmpy")

if (method == "ferx") {
return(luna_run_ferx(id = id, folder = folder, config = config, ...))
}

# read the model file with nm_read_model()
model_file <- file.path(folder, paste0(id, ".mod"))
if(! file.exists(model_file)) {
cli::cli_abort("Model file for run {id} not found!")
}
model <- pharmr::read_model(model_file)
# Some integrity checksa

# Some integrity checks
if(! inherits(model, "pharmpy.model.model.Model")) {
cli::cli_abort("Model is not a pharmpy model. Please check the model file.")
}
if(is.null(model$dataset)) {
cli::cli_abort("Model has no dataset. Please check the model and dataset files.")
}
cli::cli_alert_success("Model loaded successfully.")

## log event
log_add(
event = "action",
action = "modelfit",
id = id
)

# Determine nmfe location to use.
if(is.null(config$tools$modelfit$method)) {
cli::cli_alert_warning("Default method for modelfit not configured, using pharmpy dispatcher.")
}
method <- ifelse0(config$tools$modelfit$method, "pharmpy")
console <- ifelse0(config$tools$modelfit$console, TRUE)
if(is.null(nmfe)) {
nmfe <- ifelse0(config$tools$modelfit$nmfe, config$tools$nonmem$nmfe)
Expand All @@ -93,4 +103,67 @@ luna_run <- function(
console = console,
...
)
}
}

#' Run a ferx model (internal helper for luna_run)
#'
#' @inheritParams luna_run
#' @param config resolved luna config list
#'
#' @keywords internal
luna_run_ferx <- function(id, folder, config, ...) {
if (!requireNamespace("ferx", quietly = TRUE)) {
cli::cli_abort(c(
"The {.pkg ferx} package is required to run ferx models.",
"i" = "Install it with: {.code devtools::install_github('FeRx-NLME/ferx-r')}"
))
}

model_file <- file.path(folder, paste0(id, ".ferx"))
if (!file.exists(model_file)) {
cli::cli_abort("ferx model file {.file {model_file}} not found!")
}

data_path <- config$tools$modelfit$data
if (is.null(data_path)) {
cli::cli_abort(c(
"ferx method requires a data file path.",
"i" = "Set {.code tools.modelfit.data} in the project config YAML."
))
}
if (!file.exists(data_path)) {
data_path_abs <- file.path(folder, data_path)
if (!file.exists(data_path_abs)) {
cli::cli_abort(
"Data file {.file {data_path}} not found (also checked relative to project folder)."
)
}
data_path <- data_path_abs
}

## log event
log_add(
event = "action",
action = "modelfit",
id = id
)

cli::cli_alert_info("Running ferx model {.val {id}}...")

result_file <- file.path(folder, paste0(id, ".fitrx"))

result <- ferx::ferx_fit(
model = model_file, data = data_path, output = result_file, ...
)

if (isTRUE(result$converged)) {
cli::cli_alert_success(
"ferx run {.val {id}} converged. OFV = {round(result$ofv, 2)}"
)
} else {
cli::cli_alert_warning("ferx run {.val {id}} did not converge.")
}
cli::cli_alert_info("Results saved to {.file {result_file}}")

invisible(result)
}
Loading
Loading